diff --git a/src/typosaurus_sandbox/presentation/api/v1/calculator.py b/src/typosaurus_sandbox/presentation/api/v1/calculator.py index 9670759..86c1486 100644 --- a/src/typosaurus_sandbox/presentation/api/v1/calculator.py +++ b/src/typosaurus_sandbox/presentation/api/v1/calculator.py @@ -1,9 +1,13 @@ # retoor +import logging + from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field -from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, subtract +from typosaurus_sandbox.domain.calculator import add, average, clamp, clamp_to_byte, median, percentage, subtract, variance + +logger = logging.getLogger(__name__) calculator_router = APIRouter(prefix="/api/v1/calculator") @@ -28,6 +32,15 @@ class ClampToByteRequest(BaseModel): value: int = Field(ge=-2147483648, le=2147483647) +class ValuesRequest(BaseModel): + values: list[float] + + +class PercentageRequest(BaseModel): + value: float + total: float + + class IntResult(BaseModel): result: int @@ -38,11 +51,13 @@ class FloatResult(BaseModel): @calculator_router.post("/add", response_model=IntResult) def calculate_add(body: AddRequest) -> IntResult: + logger.debug("add %d + %d", body.left, body.right) return IntResult(result=add(body.left, body.right)) @calculator_router.post("/subtract", response_model=IntResult) def calculate_subtract(body: SubtractRequest) -> IntResult: + logger.debug("subtract %d - %d", body.left, body.right) return IntResult(result=subtract(body.left, body.right)) @@ -57,4 +72,46 @@ def calculate_clamp(body: ClampRequest) -> FloatResult: @calculator_router.post("/clamp-to-byte", response_model=IntResult) def calculate_clamp_to_byte(body: ClampToByteRequest) -> IntResult: + logger.debug("clamp-to-byte %d", body.value) return IntResult(result=clamp_to_byte(body.value)) + + +@calculator_router.post("/average", response_model=FloatResult) +def calculate_average(body: ValuesRequest) -> FloatResult: + logger.debug("average of %d values", len(body.values)) + try: + result = average(body.values) + except ValueError: + raise HTTPException(status_code=422, detail="values list must not be empty") + return FloatResult(result=result) + + +@calculator_router.post("/median", response_model=FloatResult) +def calculate_median(body: ValuesRequest) -> FloatResult: + logger.debug("median of %d values", len(body.values)) + try: + result = median(body.values) + except ValueError: + raise HTTPException(status_code=422, detail="values list must not be empty") + return FloatResult(result=result) + + +@calculator_router.post("/variance", response_model=FloatResult) +def calculate_variance(body: ValuesRequest) -> FloatResult: + logger.debug("variance of %d values", len(body.values)) + try: + result = variance(body.values) + except ValueError: + raise HTTPException(status_code=422, detail="values list must not be empty") + return FloatResult(result=result) + + +@calculator_router.post("/percentage", response_model=FloatResult) +def calculate_percentage(body: PercentageRequest) -> FloatResult: + logger.debug("percentage %f of %f", body.value, body.total) + try: + result = percentage(body.value, body.total) + except ValueError: + raise HTTPException(status_code=422, detail="total must not be zero") + return FloatResult(result=result) + diff --git a/tests/test_api.py b/tests/test_api.py index 26b4256..407072e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -118,3 +118,133 @@ class TestCalculatorClampToByteEndpoint(unittest.TestCase): response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": "abc"}) self.assertEqual(response.status_code, 422) + +class TestCalculatorAverageEndpoint(unittest.TestCase): + + def test_average_positive_values(self) -> None: + response = client.post("/api/v1/calculator/average", json={"values": [1, 2, 3, 4, 5]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 3.0}) + + def test_average_single_value(self) -> None: + response = client.post("/api/v1/calculator/average", json={"values": [5]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 5.0}) + + def test_average_negative_values(self) -> None: + response = client.post("/api/v1/calculator/average", json={"values": [-10, -20, -30]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": -20.0}) + + def test_average_empty_returns_422(self) -> None: + response = client.post("/api/v1/calculator/average", json={"values": []}) + self.assertEqual(response.status_code, 422) + + def test_average_invalid_input_returns_422(self) -> None: + response = client.post("/api/v1/calculator/average", json={"values": ["a", "b"]}) + self.assertEqual(response.status_code, 422) + + def test_average_missing_field_returns_422(self) -> None: + response = client.post("/api/v1/calculator/average", json={}) + self.assertEqual(response.status_code, 422) + + +class TestCalculatorMedianEndpoint(unittest.TestCase): + + def test_median_odd_length(self) -> None: + response = client.post("/api/v1/calculator/median", json={"values": [1, 3, 5]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 3.0}) + + def test_median_even_length(self) -> None: + response = client.post("/api/v1/calculator/median", json={"values": [1, 2, 3, 4]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 2.5}) + + def test_median_single_element(self) -> None: + response = client.post("/api/v1/calculator/median", json={"values": [7]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 7.0}) + + def test_median_unsorted_input(self) -> None: + response = client.post("/api/v1/calculator/median", json={"values": [3, 1, 2]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 2.0}) + + def test_median_empty_returns_422(self) -> None: + response = client.post("/api/v1/calculator/median", json={"values": []}) + self.assertEqual(response.status_code, 422) + + def test_median_invalid_input_returns_422(self) -> None: + response = client.post("/api/v1/calculator/median", json={"values": ["a"]}) + self.assertEqual(response.status_code, 422) + + def test_median_missing_field_returns_422(self) -> None: + response = client.post("/api/v1/calculator/median", json={}) + self.assertEqual(response.status_code, 422) + + +class TestCalculatorVarianceEndpoint(unittest.TestCase): + + def test_variance_known_set(self) -> None: + response = client.post("/api/v1/calculator/variance", json={"values": [1, 2, 3, 4, 5]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 2.0}) + + def test_variance_constant_values(self) -> None: + response = client.post("/api/v1/calculator/variance", json={"values": [1.0, 1.0, 1.0]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 0.0}) + + def test_variance_single_element(self) -> None: + response = client.post("/api/v1/calculator/variance", json={"values": [42.0]}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 0.0}) + + def test_variance_empty_returns_422(self) -> None: + response = client.post("/api/v1/calculator/variance", json={"values": []}) + self.assertEqual(response.status_code, 422) + + def test_variance_invalid_input_returns_422(self) -> None: + response = client.post("/api/v1/calculator/variance", json={"values": ["a", "b", "c"]}) + self.assertEqual(response.status_code, 422) + + def test_variance_missing_field_returns_422(self) -> None: + response = client.post("/api/v1/calculator/variance", json={}) + self.assertEqual(response.status_code, 422) + + +class TestCalculatorPercentageEndpoint(unittest.TestCase): + + def test_percentage_half(self) -> None: + response = client.post("/api/v1/calculator/percentage", json={"value": 50, "total": 100}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 50.0}) + + def test_percentage_quarter(self) -> None: + response = client.post("/api/v1/calculator/percentage", json={"value": 25, "total": 100}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 25.0}) + + def test_percentage_zero_value(self) -> None: + response = client.post("/api/v1/calculator/percentage", json={"value": 0, "total": 100}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"result": 0.0}) + + def test_percentage_total_zero_returns_422(self) -> None: + response = client.post("/api/v1/calculator/percentage", json={"value": 50, "total": 0}) + self.assertEqual(response.status_code, 422) + + def test_percentage_invalid_input_returns_422(self) -> None: + response = client.post("/api/v1/calculator/percentage", json={"value": "abc", "total": 100}) + self.assertEqual(response.status_code, 422) + + def test_percentage_missing_value_returns_422(self) -> None: + response = client.post("/api/v1/calculator/percentage", json={"total": 100}) + self.assertEqual(response.status_code, 422) + + def test_percentage_missing_total_returns_422(self) -> None: + response = client.post("/api/v1/calculator/percentage", json={"value": 50}) + self.assertEqual(response.status_code, 422) + +