diff --git a/AGENTS.md b/AGENTS.md index 912936c..753d251 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,38 +1,30 @@ # AGENTS.md -This project is a FastAPI backend for generating structured math problems. The intended design should scale to grades 1 through 6 and multiple problem types per grade. +This project is a Python library for generating structured math problems. The intended design should scale to grades 1 through 6 and multiple problem types per grade. ## Run And Test -- Start the API with `uv run uvicorn main:app --reload`. -- Open API docs at `http://127.0.0.1:8000/docs`. - Run tests with `uv run python -m unittest`. - Compile changed Python files with `uv run python -m py_compile `. +- Run the usage example with `uv run python test.py`. -## Endpoint example +## Usage example -- `POST /math/grade_1/join_pictures_with_quantity` -- The caller sends image metadata and optional generation settings. -- The endpoint returns a JSON-ready math problem where students count images and match groups to numbers. +- `from math_problems_structure.grade_1 import join_pictures_with_quantity` +- The caller passes image metadata and optional generation settings. +- The function returns a JSON-ready math problem where students count images and match groups to numbers. ## Responsibilities -- `main.py` is only the uvicorn entry point. Keep it thin. -- `app/main.py` defines `create_app()` and includes the top-level API router. -- `app/routers/math.py` owns the `/math` router and includes grade routers. -- `app/routers/grade_1.py` registers all grade 1 HTTP endpoints. +- `app/__init__.py` exposes the public library functions. - `app/problems/grade_1/` contains reusable generation logic for grade 1 problems. - `app/schemas/grade_1/` contains Pydantic request/response models for grade 1 problems. -- `tests/` contains endpoint tests using `fastapi.testclient.TestClient`. +- `tests/` contains direct function tests. ## Design Rules -- Keep HTTP code in `app/routers/`. - Keep generation logic in `app/problems/`. - Keep all Pydantic models in `app/schemas/`. -- Do not put generation logic in routers. -- Do not put FastAPI router code in problem generator modules. -- Prefer one router file per grade, not one router file per problem. - Prefer one schema file per problem type. - Prefer one generator file per problem type. @@ -42,22 +34,18 @@ For a new grade 1 problem named `example_problem`: 1. Add schemas in `app/schemas/grade_1/example_problem.py`. 2. Add generator logic in `app/problems/grade_1/example_problem.py`. -3. Register the endpoint in `app/routers/grade_1.py`. -4. Add endpoint tests in `tests/`. +3. Export the function from `app/problems/grade_1/__init__.py` and `app/__init__.py` if it is public. +4. Add direct function tests in `tests/`. 5. Run `uv run python -m unittest`. For a new grade, follow the same pattern: 1. Add `app/schemas/grade_2/` and `app/problems/grade_2/`. -2. Add `app/routers/grade_2.py`. -3. Include the new grade router from `app/routers/math.py`. -4. Add tests. - -Every time you add a new problem, also add an endpoint curl example on the ENDPOINTS-EXAMPLE.md file. +2. Export public functions from the relevant package `__init__.py` files. +3. Add tests. ## Naming Conventions -- Endpoint paths use the existing style: `/math/grade_1/`. - Python package names should use valid identifiers like `grade_1`. - Problem files should use snake case, for example `join_pictures_with_quantity.py`. - Request schema names should end in `Request`. diff --git a/ENDPOINTS-EXAMPLE.md b/ENDPOINTS-EXAMPLE.md deleted file mode 100644 index ac6d5fd..0000000 --- a/ENDPOINTS-EXAMPLE.md +++ /dev/null @@ -1,129 +0,0 @@ -# Endpoints examples - -Test quick examples for each endpoint: - -## Grade 1 - -### Subtract with image reference - -```bash -curl -X POST "http://127.0.0.1:8000/math/grade_1/subtract_with_image_reference" \ - -H "Content-Type: application/json" \ - -d '{ - "object_name": "peces", - "initial_quantity": 5, - "removed_quantity": 2, - "actor_name": "Diego", - "figure": { - "id": "fish", - "name": "Fish", - "image_path": "/images/fish.png" - } - }' -``` - -### Join corresponding sums - -```bash -curl -X POST "http://127.0.0.1:8000/math/grade_1/join_corresponding_sums" \ - -H "Content-Type: application/json" \ - -d '{ - "pair_count": 3, - "min_sum": 2, - "max_sum": 10, - "min_addend": 1, - "max_addend": 9, - "seed": 1 - }' -``` - -### Join pictures with quantity - -```bash -curl -X POST "http://127.0.0.1:8000/math/grade_1/join_pictures_with_quantity" \ - -H "Content-Type: application/json" \ - -d '{ - "available_pictures": [ - {"id": "picture-1", "name": "Apple", "image_path": "/images/apple.png"}, - {"id": "picture-2", "name": "Banana", "image_path": "/images/banana.png"}, - {"id": "picture-3", "name": "Car", "image_path": "/images/car.png"}, - {"id": "picture-4", "name": "Dog", "image_path": "/images/dog.png"}, - {"id": "picture-5", "name": "Elephant", "image_path": "/images/elephant.png"}, - {"id": "picture-6", "name": "Fish", "image_path": "/images/fish.png"}, - {"id": "picture-7", "name": "Grape", "image_path": "/images/grape.png"}, - {"id": "picture-8", "name": "Hat", "image_path": "/images/hat.png"}, - {"id": "picture-9", "name": "Ice Cream", "image_path": "/images/ice-cream.png"}, - {"id": "picture-10", "name": "Juice", "image_path": "/images/juice.png"} - ], - "container_count_per_side": 5, - "min_quantity": 1, - "max_quantity": 10, - "seed": 1 - }' -``` - -### Where are more items - -```bash -curl -X POST "http://127.0.0.1:8000/math/grade_1/where_are_more_items" \ - -H "Content-Type: application/json" \ - -d '{ - "available_pictures": [ - {"id": "picture-1", "name": "Panda", "image_path": "/images/panda.png"}, - {"id": "picture-2", "name": "Koala", "image_path": "/images/koala.png"}, - {"id": "picture-3", "name": "Orange", "image_path": "/images/orange.png"}, - {"id": "picture-4", "name": "Apple", "image_path": "/images/apple.png"}, - {"id": "picture-5", "name": "Donut", "image_path": "/images/donut.png"}, - {"id": "picture-6", "name": "Candy", "image_path": "/images/candy.png"} - ], - "comparison_count": 3, - "min_quantity": 1, - "max_quantity": 10, - "seed": 1 - }' -``` - -### Compose and decompose numbers - -```bash -curl -X POST "http://127.0.0.1:8000/math/grade_1/compose_and_decompose_numbers" \ - -H "Content-Type: application/json" \ - -d '{ - "picture": { - "id": "cube", - "name": "Cube", - "image_path": "/images/cube.png" - }, - "whole": 10, - "randomize_rows": false, - "seed": 1 - }' -``` - -### Sum with image reference - -```bash -curl -X POST "http://127.0.0.1:8000/math/grade_1/sum_with_image_reference" \ - -H "Content-Type: application/json" \ - -d '{ - "first_quantity": 5, - "second_quantity": 4, - "first_description": "flores rojas", - "second_description": "flores blancas", - "common_object": "flores", - "first_picture": { - "id": "red-flower", - "name": "Red flower", - "image_path": "/images/red-flower.png" - }, - "second_picture": { - "id": "white-flower", - "name": "White flower", - "image_path": "/images/white-flower.png" - }, - "option_count": 3, - "min_option_quantity": 1, - "max_option_quantity": 20, - "seed": 1 - }' -``` diff --git a/README.md b/README.md index 579df53..be6c15b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # math-problems-structure -This repository provides functions for generating all types of math problems, ranging from graphical exercises to text-based ones. +This repository provides a Python library for generating structured math problems, ranging from graphical exercises to text-based ones. ## Installation @@ -12,45 +12,28 @@ uv sync ## Usage -Run the backend server using the following command: +Import and call the problem generator functions directly: -```bash -uv run uvicorn main:app --reload +```python +from math_problems_structure.grade_1 import join_pictures_with_quantity + +problem = join_pictures_with_quantity( + available_pictures=[ + {"id": f"picture-{index}", "name": f"Picture {index}", "image_path": f"/images/{index}.png"} + for index in range(10) + ], + seed=1, +) ``` -This will start the server, and you can access the API documentation at `http://localhost:8000/docs` to explore the available endpoints for generating math problems. +You can also run the included example script: + +```bash +uv run python test.py +``` Each grade has its `images_for_reference` folder, which contains the images that were used to create the structure of the problems. -### API Endpoints - -The API is organized by grade level and problem type. For example, to generate a grade 1 problem that involves joining pictures with quantities, you can send a POST request to the following endpoint: - -```bash -curl -X POST "http://127.0.0.1:8000/math/grade_1/join_pictures_with_quantity" \ - -H "Content-Type: application/json" \ - -d '{ - "available_pictures": [ - {"id": "picture-1", "name": "Apple", "image_path": "/images/apple.png"}, - {"id": "picture-2", "name": "Banana", "image_path": "/images/banana.png"}, - {"id": "picture-3", "name": "Car", "image_path": "/images/car.png"}, - {"id": "picture-4", "name": "Dog", "image_path": "/images/dog.png"}, - {"id": "picture-5", "name": "Elephant", "image_path": "/images/elephant.png"}, - {"id": "picture-6", "name": "Fish", "image_path": "/images/fish.png"}, - {"id": "picture-7", "name": "Grape", "image_path": "/images/grape.png"}, - {"id": "picture-8", "name": "Hat", "image_path": "/images/hat.png"}, - {"id": "picture-9", "name": "Ice Cream", "image_path": "/images/ice-cream.png"}, - {"id": "picture-10", "name": "Juice", "image_path": "/images/juice.png"} - ], - "container_count_per_side": 5, - "min_quantity": 1, - "max_quantity": 10, - "seed": 1 - }' -``` - -To see more examples, look at [this](./ENDPOINTS-EXAMPLE.md) file. - ## Test To run the tests, use the following command: diff --git a/app/__init__.py b/app/__init__.py index 4dc803c..65ba2f5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,18 @@ -from app.main import create_app +from app.problems.grade_1 import ( + compose_and_decompose_numbers, + join_corresponding_sums, + join_pictures_with_quantity, + subtract_with_image_reference, + sum_with_image_reference, + where_are_more_items, +) -__all__ = ["create_app"] +__all__ = [ + "compose_and_decompose_numbers", + "join_corresponding_sums", + "join_pictures_with_quantity", + "subtract_with_image_reference", + "sum_with_image_reference", + "where_are_more_items", +] diff --git a/app/main.py b/app/main.py deleted file mode 100644 index 0c79ec3..0000000 --- a/app/main.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import FastAPI - -from app.routers import api_router - - -def create_app() -> FastAPI: - app = FastAPI(title="Math Problems Structure") - app.include_router(api_router) - return app diff --git a/app/routers/__init__.py b/app/routers/__init__.py deleted file mode 100644 index f3b7b88..0000000 --- a/app/routers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from app.routers.math import router as api_router - - -__all__ = ["api_router"] diff --git a/app/routers/grade_1.py b/app/routers/grade_1.py deleted file mode 100644 index c805b1c..0000000 --- a/app/routers/grade_1.py +++ /dev/null @@ -1,150 +0,0 @@ -from fastapi import APIRouter, HTTPException - -from app.problems.grade_1 import ( - compose_and_decompose_numbers, - join_corresponding_sums, - join_pictures_with_quantity, - subtract_with_image_reference, - sum_with_image_reference, - where_are_more_items, -) -from app.schemas.grade_1 import ( - ComposeAndDecomposeNumbersProblem, - ComposeAndDecomposeNumbersRequest, - JoinCorrespondingSumsProblem, - JoinCorrespondingSumsRequest, - JoinPicturesWithQuantityProblem, - JoinPicturesWithQuantityRequest, - SubtractWithImageReferenceProblem, - SubtractWithImageReferenceRequest, - SumWithImageReferenceProblem, - SumWithImageReferenceRequest, - WhereAreMoreItemsProblem, - WhereAreMoreItemsRequest, -) - -router = APIRouter(prefix="/grade_1", tags=["Grade 1"]) - - -@router.post( - "/subtract_with_image_reference", - response_model=SubtractWithImageReferenceProblem, -) -def create_subtract_with_image_reference_problem( - request: SubtractWithImageReferenceRequest, -) -> dict: - try: - return subtract_with_image_reference( - object_name=request.object_name, - initial_quantity=request.initial_quantity, - removed_quantity=request.removed_quantity, - figure=request.figure.model_dump(), - actor_name=request.actor_name, - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - -@router.post( - "/join_corresponding_sums", - response_model=JoinCorrespondingSumsProblem, -) -def create_join_corresponding_sums_problem( - request: JoinCorrespondingSumsRequest, -) -> dict: - try: - return join_corresponding_sums( - pair_count=request.pair_count, - min_sum=request.min_sum, - max_sum=request.max_sum, - min_addend=request.min_addend, - max_addend=request.max_addend, - seed=request.seed, - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - -@router.post( - "/compose_and_decompose_numbers", - response_model=ComposeAndDecomposeNumbersProblem, -) -def create_compose_and_decompose_numbers_problem( - request: ComposeAndDecomposeNumbersRequest, -) -> dict: - try: - return compose_and_decompose_numbers( - picture=request.picture.model_dump(), - whole=request.whole, - randomize_rows=request.randomize_rows, - seed=request.seed, - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - -@router.post( - "/join_pictures_with_quantity", - response_model=JoinPicturesWithQuantityProblem, -) -def create_join_pictures_with_quantity_problem( - request: JoinPicturesWithQuantityRequest, -) -> dict: - try: - return join_pictures_with_quantity( - available_pictures=[ - picture.model_dump() for picture in request.available_pictures - ], - container_count_per_side=request.container_count_per_side, - min_quantity=request.min_quantity, - max_quantity=request.max_quantity, - seed=request.seed, - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - -@router.post( - "/sum_with_image_reference", - response_model=SumWithImageReferenceProblem, -) -def create_sum_with_image_reference_problem( - request: SumWithImageReferenceRequest, -) -> dict: - try: - return sum_with_image_reference( - first_quantity=request.first_quantity, - second_quantity=request.second_quantity, - first_description=request.first_description, - second_description=request.second_description, - common_object=request.common_object, - first_picture=request.first_picture.model_dump(), - second_picture=request.second_picture.model_dump(), - option_count=request.option_count, - min_option_quantity=request.min_option_quantity, - max_option_quantity=request.max_option_quantity, - seed=request.seed, - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - -@router.post( - "/where_are_more_items", - response_model=WhereAreMoreItemsProblem, -) -def create_where_are_more_items_problem( - request: WhereAreMoreItemsRequest, -) -> dict: - try: - return where_are_more_items( - available_pictures=[ - picture.model_dump() for picture in request.available_pictures - ], - comparison_count=request.comparison_count, - min_quantity=request.min_quantity, - max_quantity=request.max_quantity, - seed=request.seed, - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc diff --git a/app/routers/math.py b/app/routers/math.py deleted file mode 100644 index 28516cb..0000000 --- a/app/routers/math.py +++ /dev/null @@ -1,7 +0,0 @@ -from fastapi import APIRouter - -from app.routers import grade_1 - - -router = APIRouter(prefix="/math") -router.include_router(grade_1.router) diff --git a/main.py b/main.py deleted file mode 100644 index c798479..0000000 --- a/main.py +++ /dev/null @@ -1,4 +0,0 @@ -from app.main import create_app - - -app = create_app() diff --git a/math_problems_structure/__init__.py b/math_problems_structure/__init__.py new file mode 100644 index 0000000..48d90ab --- /dev/null +++ b/math_problems_structure/__init__.py @@ -0,0 +1,18 @@ +from math_problems_structure.grade_1 import ( + compose_and_decompose_numbers, + join_corresponding_sums, + join_pictures_with_quantity, + subtract_with_image_reference, + sum_with_image_reference, + where_are_more_items, +) + + +__all__ = [ + "compose_and_decompose_numbers", + "join_corresponding_sums", + "join_pictures_with_quantity", + "subtract_with_image_reference", + "sum_with_image_reference", + "where_are_more_items", +] diff --git a/math_problems_structure/grade_1/__init__.py b/math_problems_structure/grade_1/__init__.py new file mode 100644 index 0000000..65ba2f5 --- /dev/null +++ b/math_problems_structure/grade_1/__init__.py @@ -0,0 +1,18 @@ +from app.problems.grade_1 import ( + compose_and_decompose_numbers, + join_corresponding_sums, + join_pictures_with_quantity, + subtract_with_image_reference, + sum_with_image_reference, + where_are_more_items, +) + + +__all__ = [ + "compose_and_decompose_numbers", + "join_corresponding_sums", + "join_pictures_with_quantity", + "subtract_with_image_reference", + "sum_with_image_reference", + "where_are_more_items", +] diff --git a/pyproject.toml b/pyproject.toml index 08d3b78..2e50817 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,9 @@ [project] name = "math-problems-structure" version = "0.1.0" -description = "Add your description here" +description = "Definition to generate all sort of math problems." readme = "README.md" requires-python = ">=3.12" dependencies = [ - "fastapi>=0.136.3", - "httpx>=0.28.1", "pydantic>=2.13.4", - "uvicorn>=0.48.0", ] diff --git a/test-lib.py b/test-lib.py new file mode 100644 index 0000000..c1a95a7 --- /dev/null +++ b/test-lib.py @@ -0,0 +1,22 @@ +from math_problems_structure.grade_1 import join_pictures_with_quantity + + +def main() -> None: + problem = join_pictures_with_quantity( + container_count_per_side=2, + available_pictures=[ + { + "id": f"picture-{index}", + "name": f"Picture {index}", + "image_path": f"/images/{index}.png", + } + for index in range(4) + ], + seed=1, + ) + + print(problem) + + +if __name__ == "__main__": + main() diff --git a/tests/test_compose_and_decompose_numbers_endpoint.py b/tests/test_compose_and_decompose_numbers.py similarity index 55% rename from tests/test_compose_and_decompose_numbers_endpoint.py rename to tests/test_compose_and_decompose_numbers.py index e207a32..e867070 100644 --- a/tests/test_compose_and_decompose_numbers_endpoint.py +++ b/tests/test_compose_and_decompose_numbers.py @@ -1,13 +1,10 @@ import unittest -from fastapi.testclient import TestClient - -from app.main import create_app +from math_problems_structure.grade_1 import compose_and_decompose_numbers -class ComposeAndDecomposeNumbersEndpointTest(unittest.TestCase): +class ComposeAndDecomposeNumbersTest(unittest.TestCase): def setUp(self) -> None: - self.client = TestClient(create_app()) self.picture = { "id": "cube", "name": "Cube", @@ -15,13 +12,8 @@ class ComposeAndDecomposeNumbersEndpointTest(unittest.TestCase): } def test_creates_ordered_problem(self) -> None: - response = self.client.post( - "/math/grade_1/compose_and_decompose_numbers", - json={"picture": self.picture}, - ) + problem = compose_and_decompose_numbers(picture=self.picture) - self.assertEqual(response.status_code, 200) - problem = response.json() rows = problem["rows"] self.assertEqual(problem["instructions"], "Compón y descompón el número.") @@ -39,27 +31,22 @@ class ComposeAndDecomposeNumbersEndpointTest(unittest.TestCase): self.assertEqual(row["picture"], self.picture) def test_can_randomize_rows(self) -> None: - response = self.client.post( - "/math/grade_1/compose_and_decompose_numbers", - json={"picture": self.picture, "randomize_rows": True, "seed": 1}, + problem = compose_and_decompose_numbers( + picture=self.picture, + randomize_rows=True, + seed=1, ) - self.assertEqual(response.status_code, 200) - rows = response.json()["rows"] + rows = problem["rows"] ordered_pairs = [(9, 1), (8, 2), (7, 3), (6, 4), (5, 5), (4, 6), (3, 7), (2, 8), (1, 9)] randomized_pairs = [(row["first_part"], row["second_part"]) for row in rows] self.assertNotEqual(randomized_pairs, ordered_pairs) self.assertCountEqual(randomized_pairs, ordered_pairs) - def test_returns_bad_request_for_whole_less_than_two(self) -> None: - response = self.client.post( - "/math/grade_1/compose_and_decompose_numbers", - json={"picture": self.picture, "whole": 1}, - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), {"detail": "whole must be at least 2"}) + def test_raises_for_whole_less_than_two(self) -> None: + with self.assertRaisesRegex(ValueError, "whole must be at least 2"): + compose_and_decompose_numbers(picture=self.picture, whole=1) if __name__ == "__main__": diff --git a/tests/test_join_corresponding_sums_endpoint.py b/tests/test_join_corresponding_sums.py similarity index 62% rename from tests/test_join_corresponding_sums_endpoint.py rename to tests/test_join_corresponding_sums.py index f194504..ad40abb 100644 --- a/tests/test_join_corresponding_sums_endpoint.py +++ b/tests/test_join_corresponding_sums.py @@ -1,22 +1,11 @@ import unittest -from fastapi.testclient import TestClient - -from app.main import create_app +from math_problems_structure.grade_1 import join_corresponding_sums -class JoinCorrespondingSumsEndpointTest(unittest.TestCase): - def setUp(self) -> None: - self.client = TestClient(create_app()) - +class JoinCorrespondingSumsTest(unittest.TestCase): def test_creates_problem_with_matching_sums(self) -> None: - response = self.client.post( - "/math/grade_1/join_corresponding_sums", - json={"pair_count": 3, "seed": 1}, - ) - - self.assertEqual(response.status_code, 200) - problem = response.json() + problem = join_corresponding_sums(pair_count=3, seed=1) self.assertEqual(problem["instructions"], "Conecta.") self.assertEqual(len(problem["left_expressions"]), 3) @@ -47,17 +36,12 @@ class JoinCorrespondingSumsEndpointTest(unittest.TestCase): self.assertEqual(left_expression["match_id"], right_expression["match_id"]) self.assertEqual(left_expression["match_id"], connection["match_id"]) - def test_returns_bad_request_for_impossible_ranges(self) -> None: - response = self.client.post( - "/math/grade_1/join_corresponding_sums", - json={"pair_count": 3, "min_sum": 2, "max_sum": 2}, - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), - {"detail": "sum and addend ranges must contain enough matchable sums"}, - ) + def test_raises_for_impossible_ranges(self) -> None: + with self.assertRaisesRegex( + ValueError, + "sum and addend ranges must contain enough matchable sums", + ): + join_corresponding_sums(pair_count=3, min_sum=2, max_sum=2) if __name__ == "__main__": diff --git a/tests/test_join_pictures_with_quantity.py b/tests/test_join_pictures_with_quantity.py new file mode 100644 index 0000000..74edfba --- /dev/null +++ b/tests/test_join_pictures_with_quantity.py @@ -0,0 +1,45 @@ +import unittest + +from math_problems_structure.grade_1 import join_pictures_with_quantity + + +class JoinPicturesWithQuantityTest(unittest.TestCase): + def test_creates_problem(self) -> None: + problem = join_pictures_with_quantity( + available_pictures=[ + { + "id": f"picture-{index}", + "name": f"Picture {index}", + "image_path": f"/images/{index}.png", + } + for index in range(10) + ], + seed=1, + ) + + self.assertEqual( + problem["instructions"], + "Cuenta las imágenes y une cada grupo con el número correcto.", + ) + self.assertEqual(len(problem["left_containers"]), 5) + self.assertEqual(len(problem["number_cards"]), 5) + self.assertEqual(len(problem["right_containers"]), 5) + + def test_raises_for_too_few_pictures(self) -> None: + with self.assertRaisesRegex( + ValueError, + "available_pictures must contain at least 10 pictures", + ): + join_pictures_with_quantity( + available_pictures=[ + { + "id": "picture-1", + "name": "Picture 1", + "image_path": "/images/1.png", + } + ] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_join_pictures_with_quantity_endpoint.py b/tests/test_join_pictures_with_quantity_endpoint.py deleted file mode 100644 index 7029426..0000000 --- a/tests/test_join_pictures_with_quantity_endpoint.py +++ /dev/null @@ -1,60 +0,0 @@ -import unittest - -from fastapi.testclient import TestClient - -from app.main import create_app - - -class JoinPicturesWithQuantityEndpointTest(unittest.TestCase): - def setUp(self) -> None: - self.client = TestClient(create_app()) - - def test_creates_problem(self) -> None: - response = self.client.post( - "/math/grade_1/join_pictures_with_quantity", - json={ - "available_pictures": [ - { - "id": f"picture-{index}", - "name": f"Picture {index}", - "image_path": f"/images/{index}.png", - } - for index in range(10) - ], - "seed": 1, - }, - ) - - self.assertEqual(response.status_code, 200) - problem = response.json() - self.assertEqual( - problem["instructions"], - "Cuenta las imágenes y une cada grupo con el número correcto.", - ) - self.assertEqual(len(problem["left_containers"]), 5) - self.assertEqual(len(problem["number_cards"]), 5) - self.assertEqual(len(problem["right_containers"]), 5) - - def test_returns_bad_request_for_too_few_pictures(self) -> None: - response = self.client.post( - "/math/grade_1/join_pictures_with_quantity", - json={ - "available_pictures": [ - { - "id": "picture-1", - "name": "Picture 1", - "image_path": "/images/1.png", - } - ] - }, - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), - {"detail": "available_pictures must contain at least 10 pictures"}, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_subtract_with_image_reference.py b/tests/test_subtract_with_image_reference.py new file mode 100644 index 0000000..000c0d9 --- /dev/null +++ b/tests/test_subtract_with_image_reference.py @@ -0,0 +1,76 @@ +import unittest + +from math_problems_structure.grade_1 import subtract_with_image_reference + + +class SubtractWithImageReferenceTest(unittest.TestCase): + def setUp(self) -> None: + self.figure = { + "id": "fish", + "name": "Fish", + "image_path": "/images/fish.png", + } + + def test_creates_subtraction_problem(self) -> None: + problem = subtract_with_image_reference( + object_name="peces", + initial_quantity=5, + removed_quantity=2, + actor_name="Diego", + figure=self.figure, + ) + + self.assertEqual(problem["title"], "¿Cuántos quedan?") + self.assertEqual( + problem["question"], + "Hay 5 peces. Diego sacó 2 peces. ¿Cuántos peces quedan?", + ) + self.assertEqual(problem["object_name"], "peces") + self.assertEqual(problem["actor_name"], "Diego") + self.assertEqual(problem["figure"], self.figure) + self.assertEqual( + problem["remaining_group"], + {"label": "quedan", "quantity": 3, "picture": self.figure}, + ) + self.assertEqual( + problem["subtracted_group"], + {"label": "sacó", "quantity": 2, "picture": self.figure}, + ) + self.assertEqual( + problem["equation"], + { + "initial_quantity": 5, + "removed_quantity": 2, + "remaining_quantity": 3, + "symbol": "-", + "equals_symbol": "=", + }, + ) + + def test_allows_zero_remaining_figures(self) -> None: + problem = subtract_with_image_reference( + object_name="peces", + initial_quantity=2, + removed_quantity=2, + figure=self.figure, + ) + + self.assertEqual(problem["equation"]["remaining_quantity"], 0) + self.assertEqual(problem["remaining_group"]["quantity"], 0) + self.assertEqual(problem["subtracted_group"]["quantity"], 2) + + def test_raises_when_removed_quantity_is_too_large(self) -> None: + with self.assertRaisesRegex( + ValueError, + "removed_quantity must be less than or equal to initial_quantity", + ): + subtract_with_image_reference( + object_name="peces", + initial_quantity=2, + removed_quantity=5, + figure=self.figure, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_subtract_with_image_reference_endpoint.py b/tests/test_subtract_with_image_reference_endpoint.py deleted file mode 100644 index 8861592..0000000 --- a/tests/test_subtract_with_image_reference_endpoint.py +++ /dev/null @@ -1,95 +0,0 @@ -import unittest - -from fastapi.testclient import TestClient - -from app.main import create_app - - -class SubtractWithImageReferenceEndpointTest(unittest.TestCase): - def setUp(self) -> None: - self.client = TestClient(create_app()) - self.figure = { - "id": "fish", - "name": "Fish", - "image_path": "/images/fish.png", - } - - def test_creates_subtraction_problem(self) -> None: - response = self.client.post( - "/math/grade_1/subtract_with_image_reference", - json={ - "object_name": "peces", - "initial_quantity": 5, - "removed_quantity": 2, - "actor_name": "Diego", - "figure": self.figure, - }, - ) - - self.assertEqual(response.status_code, 200) - problem = response.json() - - self.assertEqual(problem["title"], "¿Cuántos quedan?") - self.assertEqual( - problem["question"], - "Hay 5 peces. Diego sacó 2 peces. ¿Cuántos peces quedan?", - ) - self.assertEqual(problem["object_name"], "peces") - self.assertEqual(problem["actor_name"], "Diego") - self.assertEqual(problem["figure"], self.figure) - self.assertEqual( - problem["remaining_group"], - {"label": "quedan", "quantity": 3, "picture": self.figure}, - ) - self.assertEqual( - problem["subtracted_group"], - {"label": "sacó", "quantity": 2, "picture": self.figure}, - ) - self.assertEqual( - problem["equation"], - { - "initial_quantity": 5, - "removed_quantity": 2, - "remaining_quantity": 3, - "symbol": "-", - "equals_symbol": "=", - }, - ) - - def test_allows_zero_remaining_figures(self) -> None: - response = self.client.post( - "/math/grade_1/subtract_with_image_reference", - json={ - "object_name": "peces", - "initial_quantity": 2, - "removed_quantity": 2, - "figure": self.figure, - }, - ) - - self.assertEqual(response.status_code, 200) - problem = response.json() - self.assertEqual(problem["equation"]["remaining_quantity"], 0) - self.assertEqual(problem["remaining_group"]["quantity"], 0) - self.assertEqual(problem["subtracted_group"]["quantity"], 2) - - def test_returns_bad_request_when_removed_quantity_is_too_large(self) -> None: - response = self.client.post( - "/math/grade_1/subtract_with_image_reference", - json={ - "object_name": "peces", - "initial_quantity": 2, - "removed_quantity": 5, - "figure": self.figure, - }, - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), - {"detail": "removed_quantity must be less than or equal to initial_quantity"}, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_sum_with_image_reference_endpoint.py b/tests/test_sum_with_image_reference.py similarity index 51% rename from tests/test_sum_with_image_reference_endpoint.py rename to tests/test_sum_with_image_reference.py index 7f6f46c..519f5aa 100644 --- a/tests/test_sum_with_image_reference_endpoint.py +++ b/tests/test_sum_with_image_reference.py @@ -1,13 +1,10 @@ import unittest -from fastapi.testclient import TestClient - -from app.main import create_app +from math_problems_structure.grade_1 import sum_with_image_reference -class SumWithImageReferenceEndpointTest(unittest.TestCase): +class SumWithImageReferenceTest(unittest.TestCase): def setUp(self) -> None: - self.client = TestClient(create_app()) self.first_picture = { "id": "red-flower", "name": "Red flower", @@ -20,23 +17,18 @@ class SumWithImageReferenceEndpointTest(unittest.TestCase): } def test_creates_problem_with_one_correct_option(self) -> None: - response = self.client.post( - "/math/grade_1/sum_with_image_reference", - json={ - "first_quantity": 5, - "second_quantity": 4, - "first_description": "flores rojas", - "second_description": "flores blancas", - "common_object": "flores", - "first_picture": self.first_picture, - "second_picture": self.second_picture, - "option_count": 3, - "seed": 1, - }, + problem = sum_with_image_reference( + first_quantity=5, + second_quantity=4, + first_description="flores rojas", + second_description="flores blancas", + common_object="flores", + first_picture=self.first_picture, + second_picture=self.second_picture, + option_count=3, + seed=1, ) - self.assertEqual(response.status_code, 200) - problem = response.json() correct_options = [option for option in problem["options"] if option["is_correct"]] self.assertEqual( @@ -61,27 +53,22 @@ class SumWithImageReferenceEndpointTest(unittest.TestCase): self.assertEqual(first_group["picture"], self.first_picture) self.assertEqual(second_group["picture"], self.second_picture) - def test_returns_bad_request_when_total_is_outside_option_range(self) -> None: - response = self.client.post( - "/math/grade_1/sum_with_image_reference", - json={ - "first_quantity": 5, - "second_quantity": 4, - "first_description": "flores rojas", - "second_description": "flores blancas", - "common_object": "flores", - "first_picture": self.first_picture, - "second_picture": self.second_picture, - "min_option_quantity": 1, - "max_option_quantity": 8, - }, - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), - {"detail": "option quantity range must include the correct total"}, - ) + def test_raises_when_total_is_outside_option_range(self) -> None: + with self.assertRaisesRegex( + ValueError, + "option quantity range must include the correct total", + ): + sum_with_image_reference( + first_quantity=5, + second_quantity=4, + first_description="flores rojas", + second_description="flores blancas", + common_object="flores", + first_picture=self.first_picture, + second_picture=self.second_picture, + min_option_quantity=1, + max_option_quantity=8, + ) if __name__ == "__main__": diff --git a/tests/test_where_are_more_items.py b/tests/test_where_are_more_items.py new file mode 100644 index 0000000..107d6bb --- /dev/null +++ b/tests/test_where_are_more_items.py @@ -0,0 +1,57 @@ +import unittest + +from math_problems_structure.grade_1 import where_are_more_items + + +class WhereAreMoreItemsTest(unittest.TestCase): + def test_creates_problem(self) -> None: + problem = where_are_more_items( + available_pictures=[ + { + "id": f"picture-{index}", + "name": f"Picture {index}", + "image_path": f"/images/{index}.png", + } + for index in range(6) + ], + seed=1, + ) + + self.assertEqual(problem["instructions"], "Marca dónde hay más.") + self.assertEqual(len(problem["comparisons"]), 3) + + for comparison in problem["comparisons"]: + left_group = comparison["left_group"] + right_group = comparison["right_group"] + + self.assertEqual(left_group["side"], "left") + self.assertEqual(right_group["side"], "right") + self.assertNotEqual(left_group["quantity"], right_group["quantity"]) + self.assertIn("picture", left_group) + self.assertIn("picture", right_group) + + expected_answer_side = ( + "left" + if left_group["quantity"] > right_group["quantity"] + else "right" + ) + self.assertEqual(comparison["answer_side"], expected_answer_side) + + def test_raises_for_too_few_figures(self) -> None: + with self.assertRaisesRegex( + ValueError, + "available_pictures must contain at least 2 pictures", + ): + where_are_more_items( + available_pictures=[ + { + "id": "picture-1", + "name": "Picture 1", + "image_path": "/images/1.png", + } + ] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_where_are_more_items_endpoint.py b/tests/test_where_are_more_items_endpoint.py deleted file mode 100644 index 8a172a3..0000000 --- a/tests/test_where_are_more_items_endpoint.py +++ /dev/null @@ -1,72 +0,0 @@ -import unittest - -from fastapi.testclient import TestClient - -from app.main import create_app - - -class WhereAreMoreItemsEndpointTest(unittest.TestCase): - def setUp(self) -> None: - self.client = TestClient(create_app()) - - def test_creates_problem(self) -> None: - response = self.client.post( - "/math/grade_1/where_are_more_items", - json={ - "available_pictures": [ - { - "id": f"picture-{index}", - "name": f"Picture {index}", - "image_path": f"/images/{index}.png", - } - for index in range(6) - ], - "seed": 1, - }, - ) - - self.assertEqual(response.status_code, 200) - problem = response.json() - self.assertEqual(problem["instructions"], "Marca dónde hay más.") - self.assertEqual(len(problem["comparisons"]), 3) - - for comparison in problem["comparisons"]: - left_group = comparison["left_group"] - right_group = comparison["right_group"] - - self.assertEqual(left_group["side"], "left") - self.assertEqual(right_group["side"], "right") - self.assertNotEqual(left_group["quantity"], right_group["quantity"]) - self.assertIn("picture", left_group) - self.assertIn("picture", right_group) - - expected_answer_side = ( - "left" - if left_group["quantity"] > right_group["quantity"] - else "right" - ) - self.assertEqual(comparison["answer_side"], expected_answer_side) - - def test_returns_bad_request_for_too_few_figures(self) -> None: - response = self.client.post( - "/math/grade_1/where_are_more_items", - json={ - "available_pictures": [ - { - "id": "picture-1", - "name": "Picture 1", - "image_path": "/images/1.png", - } - ] - }, - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), - {"detail": "available_pictures must contain at least 2 pictures"}, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/uv.lock b/uv.lock index 3c5bdc1..c42d487 100644 --- a/uv.lock +++ b/uv.lock @@ -2,15 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.12" -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -20,129 +11,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, -] - -[[package]] -name = "certifi" -version = "2026.5.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, -] - -[[package]] -name = "click" -version = "8.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "fastapi" -version = "0.136.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "idna" -version = "3.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, -] - [[package]] name = "math-problems-structure" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "fastapi" }, - { name = "httpx" }, { name = "pydantic" }, - { name = "uvicorn" }, ] [package.metadata] -requires-dist = [ - { name = "fastapi", specifier = ">=0.136.3" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "pydantic", specifier = ">=2.13.4" }, - { name = "uvicorn", specifier = ">=0.48.0" }, -] +requires-dist = [{ name = "pydantic", specifier = ">=2.13.4" }] [[package]] name = "pydantic" @@ -234,19 +112,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] -[[package]] -name = "starlette" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -267,16 +132,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] - -[[package]] -name = "uvicorn" -version = "0.48.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" }, -]