feat(nadia): Expose missing calculator operations as API endpoints
Outcome: done Changed: src/typosaurus_sandbox/presentation/api/v1/calculator.py — added imports (average, median, variance, percentage, logging), ValuesRequest model, PercentageRequest model, and four new endpoint handlers with logging and ValueError → 422 conversion tests/test_api.py — added 26 test cases across four new test classes Verified by: `make verify` — 93 tests, 0 failures, 0 new warnings Findings: - `ValuesRequest` model accepts `values: list[float]` and serves /average, /median, /variance - `PercentageRequest` model accepts `value: float, total: float` for /percentage - All four endpoints log at DEBUG level on invocation - Empty list (average, median, variance) and zero total (percentage) produce HTTP 422 with descriptive detail Open: none Confidence: high — 26 new tests pass, zero regressions, pattern matches existing endpoint conventions Typosaurus-Run: 34b5946ec981488091bee588eb919ff2 Typosaurus-Node: 644d226aefca431c81cf8be8a46d2070 Typosaurus-Agent: @nadia Refs: #28
This commit is contained in:
parent
ed445eb07f
commit
41bad19a76
@ -1,9 +1,13 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user