diff --git a/ENDPOINTS-EXAMPLE.md b/ENDPOINTS-EXAMPLE.md index 2f84c6a..ac6d5fd 100644 --- a/ENDPOINTS-EXAMPLE.md +++ b/ENDPOINTS-EXAMPLE.md @@ -4,6 +4,24 @@ 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 diff --git a/app/problems/grade_1/__init__.py b/app/problems/grade_1/__init__.py index a4d8adb..f19403c 100644 --- a/app/problems/grade_1/__init__.py +++ b/app/problems/grade_1/__init__.py @@ -5,6 +5,9 @@ from app.problems.grade_1.join_corresponding_sums import join_corresponding_sums from app.problems.grade_1.join_pictures_with_quantity import ( join_pictures_with_quantity, ) +from app.problems.grade_1.subtract_with_image_reference import ( + subtract_with_image_reference, +) from app.problems.grade_1.sum_with_image_reference import sum_with_image_reference from app.problems.grade_1.where_are_more_items import where_are_more_items @@ -12,6 +15,7 @@ __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/problems/grade_1/images_for_reference/substract-with-image-reference.png b/app/problems/grade_1/images_for_reference/substract-with-image-reference.png new file mode 100644 index 0000000..7c1d690 Binary files /dev/null and b/app/problems/grade_1/images_for_reference/substract-with-image-reference.png differ diff --git a/app/problems/grade_1/subtract_with_image_reference.py b/app/problems/grade_1/subtract_with_image_reference.py new file mode 100644 index 0000000..80176eb --- /dev/null +++ b/app/problems/grade_1/subtract_with_image_reference.py @@ -0,0 +1,49 @@ +from app.schemas.grade_1.subtract_with_image_reference import ( + FigureGroup, + PictureAsset, + SubtractWithImageReferenceProblem, + SubtractionEquation, +) + + +def subtract_with_image_reference( + object_name: str, + initial_quantity: int, + removed_quantity: int, + figure: dict, + actor_name: str = "Diego", +) -> dict: + """Generate a subtraction story problem with remaining and removed groups.""" + if removed_quantity > initial_quantity: + raise ValueError("removed_quantity must be less than or equal to initial_quantity") + + selected_figure = PictureAsset.model_validate(figure) + remaining_quantity = initial_quantity - removed_quantity + question = ( + f"Hay {initial_quantity} {object_name}. " + f"{actor_name} sacó {removed_quantity} {object_name}. " + f"¿Cuántos {object_name} quedan?" + ) + problem = SubtractWithImageReferenceProblem( + question=question, + object_name=object_name, + actor_name=actor_name, + figure=selected_figure, + remaining_group=FigureGroup( + label="quedan", + quantity=remaining_quantity, + picture=selected_figure, + ), + subtracted_group=FigureGroup( + label="sacó", + quantity=removed_quantity, + picture=selected_figure, + ), + equation=SubtractionEquation( + initial_quantity=initial_quantity, + removed_quantity=removed_quantity, + remaining_quantity=remaining_quantity, + ), + ) + + return problem.model_dump() diff --git a/app/routers/grade_1.py b/app/routers/grade_1.py index 6ef1ab2..c805b1c 100644 --- a/app/routers/grade_1.py +++ b/app/routers/grade_1.py @@ -4,6 +4,7 @@ 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, ) @@ -14,6 +15,8 @@ from app.schemas.grade_1 import ( JoinCorrespondingSumsRequest, JoinPicturesWithQuantityProblem, JoinPicturesWithQuantityRequest, + SubtractWithImageReferenceProblem, + SubtractWithImageReferenceRequest, SumWithImageReferenceProblem, SumWithImageReferenceRequest, WhereAreMoreItemsProblem, @@ -23,6 +26,25 @@ from app.schemas.grade_1 import ( 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, diff --git a/app/schemas/grade_1/__init__.py b/app/schemas/grade_1/__init__.py index 1cfba8a..82a20e9 100644 --- a/app/schemas/grade_1/__init__.py +++ b/app/schemas/grade_1/__init__.py @@ -10,6 +10,10 @@ from app.schemas.grade_1.join_pictures_with_quantity import ( JoinPicturesWithQuantityProblem, JoinPicturesWithQuantityRequest, ) +from app.schemas.grade_1.subtract_with_image_reference import ( + SubtractWithImageReferenceProblem, + SubtractWithImageReferenceRequest, +) from app.schemas.grade_1.sum_with_image_reference import ( SumWithImageReferenceProblem, SumWithImageReferenceRequest, @@ -26,6 +30,8 @@ __all__ = [ "JoinCorrespondingSumsRequest", "JoinPicturesWithQuantityProblem", "JoinPicturesWithQuantityRequest", + "SubtractWithImageReferenceProblem", + "SubtractWithImageReferenceRequest", "SumWithImageReferenceProblem", "SumWithImageReferenceRequest", "WhereAreMoreItemsProblem", diff --git a/app/schemas/grade_1/subtract_with_image_reference.py b/app/schemas/grade_1/subtract_with_image_reference.py new file mode 100644 index 0000000..8dc94ee --- /dev/null +++ b/app/schemas/grade_1/subtract_with_image_reference.py @@ -0,0 +1,40 @@ +from pydantic import BaseModel, Field, NonNegativeInt, PositiveInt + + +class PictureAsset(BaseModel): + id: str = Field(min_length=1) + name: str = Field(min_length=1) + image_path: str = Field(min_length=1) + + +class SubtractionEquation(BaseModel): + initial_quantity: PositiveInt + removed_quantity: PositiveInt + remaining_quantity: NonNegativeInt + symbol: str = "-" + equals_symbol: str = "=" + + +class FigureGroup(BaseModel): + label: str = Field(min_length=1) + quantity: NonNegativeInt + picture: PictureAsset + + +class SubtractWithImageReferenceProblem(BaseModel): + title: str = "¿Cuántos quedan?" + question: str + object_name: str = Field(min_length=1) + actor_name: str = Field(min_length=1) + figure: PictureAsset + remaining_group: FigureGroup + subtracted_group: FigureGroup + equation: SubtractionEquation + + +class SubtractWithImageReferenceRequest(BaseModel): + object_name: str = Field(min_length=1) + initial_quantity: PositiveInt + removed_quantity: PositiveInt + figure: PictureAsset + actor_name: str = Field(default="Diego", min_length=1) diff --git a/tests/test_subtract_with_image_reference_endpoint.py b/tests/test_subtract_with_image_reference_endpoint.py new file mode 100644 index 0000000..8861592 --- /dev/null +++ b/tests/test_subtract_with_image_reference_endpoint.py @@ -0,0 +1,95 @@ +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()