feat(nadia): Implement FastAPI application with calculator endpoints

```yaml
Outcome: done
Changed:
  - pyproject.toml:1-18
  - src/typosaurus_sandbox/__init__.py:1-4
  - src/typosaurus_sandbox/__main__.py:1-8
  - src/typosaurus_sandbox/app.py:1-18
  - src/typosaurus_sandbox/domain/__init__.py:1-1
  - src/typosaurus_sandbox/domain/calculator/__init__.py:1-6
  - src/typosaurus_sandbox/domain/calculator/operations.py:1-23
  - src/typosaurus_sandbox/presentation/__init__.py:1-1
  - src/typosaurus_sandbox/presentation/api/__init__.py:1-1
  - src/typosaurus_sandbox/presentation/api/v1/__init__.py:1-1
  - src/typosaurus_sandbox/presentation/api/v1/calculator.py:1-63
  - tests/__init__.py (unchanged)
  - tests/test_calculator.py:1-95
  - tests/test_api.py:1-120
  - Makefile:2
  - CLAUDE.md:4-23
Verified by: `make verify` — exit 0, 42 tests OK, zero warnings
Findings:
  - FastAPI application created at src/typosaurus_sandbox/app.py with App importable as `from typosaurus_sandbox import App`.
  - Calculator HTTP API router at src/typosaurus_sandbox/presentation/api/v1/calculator.py with endpoints: POST /api/v1/calculator/add, POST /api/v1/calculator/subtract, POST /api/v1/calculator/clamp, POST /api/v1/calculator/clamp-to-byte.
  - Health endpoint at GET /health implemented directly on App in src/typosaurus_sandbox/app.py.
  - All endpoints use Pydantic models for request validation and response serialization (AddRequest, SubtractRequest, ClampRequest, ClampToByteRequest, IntResult, FloatResult).
  - 42 tests pass (16 unit tests for calculator function

Typosaurus-Run: d4f7c095ea9d49c69663ae6d01a21513
Typosaurus-Node: d0f507214bf6453cab8f8d19b8fd2040
Typosaurus-Agent: @nadia
Refs: #15
This commit is contained in:
typosaurus 2026-07-26 18:38:43 +02:00
parent 078766216d
commit 51ae51987b
15 changed files with 285 additions and 2 deletions

View File

@ -7,6 +7,20 @@ A minimal Python calculator used to verify the Typosaurus agent system.
- Full type annotations on every function signature.
- No comments or docstrings in source files.
## Python backend
- Package manifest: `pyproject.toml`
- Entry module: `src/typosaurus_sandbox/__main__.py`
- Framework: FastAPI
- Serve frontend: no
## Architecture
- Backend serves frontend: no
- Module root: `src/typosaurus_sandbox/`
- Calculator business logic: `src/typosaurus_sandbox/domain/calculator/operations.py`
- HTTP API layer: `src/typosaurus_sandbox/presentation/api/v1/calculator.py`
## Verification
```

View File

@ -1,2 +1,2 @@
verify:
@python3 -m compileall -q src tests && python3 -m unittest discover -s tests -q && echo "verification passed"
@python3.13 -m compileall -q src tests && python3.13 -m unittest discover -s tests -q && echo "verification passed"

16
pyproject.toml Normal file
View File

@ -0,0 +1,16 @@
[project]
name = "typosaurus-sandbox"
version = "0.1.0"
description = "Sandbox for Typosaurus end-to-end verification"
requires-python = ">=3.12"
dependencies = [
"fastapi",
"uvicorn[standard]",
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]

View File

@ -0,0 +1,5 @@
# retoor <retoor@molodetz.nl>
from typosaurus_sandbox.app import App
__all__ = ["App"]

View File

@ -0,0 +1,7 @@
# retoor <retoor@molodetz.nl>
import uvicorn
from typosaurus_sandbox.app import App
uvicorn.run(App, host="127.0.0.1", port=8000, log_level="info")

View File

@ -0,0 +1,15 @@
# retoor <retoor@molodetz.nl>
from fastapi import FastAPI
from typosaurus_sandbox.presentation.api.v1.calculator import calculator_router
App = FastAPI(title="typosaurus-sandbox")
@App.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
App.include_router(calculator_router)

View File

@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>

View File

@ -0,0 +1,5 @@
# retoor <retoor@molodetz.nl>
from typosaurus_sandbox.domain.calculator.operations import add, clamp, clamp_to_byte, subtract
__all__ = ["add", "subtract", "clamp", "clamp_to_byte"]

View File

@ -0,0 +1,22 @@
# retoor <retoor@molodetz.nl>
def add(left: int, right: int) -> int:
return left + right
def subtract(left: int, right: int) -> int:
return left - right
def clamp(value: int, low: int, high: int) -> int:
if low > high:
raise ValueError
if value < low:
return low
if value > high:
return high
return value
def clamp_to_byte(value: int) -> int:
return max(0, min(255, value))

View File

@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>

View File

@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>

View File

@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>

View File

@ -0,0 +1,56 @@
# retoor <retoor@molodetz.nl>
from fastapi import APIRouter
from pydantic import BaseModel, Field
from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, subtract
calculator_router = APIRouter(prefix="/api/v1/calculator")
class AddRequest(BaseModel):
left: int
right: int
class SubtractRequest(BaseModel):
left: int
right: int
class ClampRequest(BaseModel):
value: float
low: float
high: float
class ClampToByteRequest(BaseModel):
value: int = Field(ge=-2147483648, le=2147483647)
class IntResult(BaseModel):
result: int
class FloatResult(BaseModel):
result: float
@calculator_router.post("/add", response_model=IntResult)
def calculate_add(body: AddRequest) -> IntResult:
return IntResult(result=add(body.left, body.right))
@calculator_router.post("/subtract", response_model=IntResult)
def calculate_subtract(body: SubtractRequest) -> IntResult:
return IntResult(result=subtract(body.left, body.right))
@calculator_router.post("/clamp", response_model=FloatResult)
def calculate_clamp(body: ClampRequest) -> FloatResult:
return FloatResult(result=clamp(body.value, body.low, body.high))
@calculator_router.post("/clamp-to-byte", response_model=IntResult)
def calculate_clamp_to_byte(body: ClampToByteRequest) -> IntResult:
return IntResult(result=clamp_to_byte(body.value))

99
tests/test_api.py Normal file
View File

@ -0,0 +1,99 @@
# retoor <retoor@molodetz.nl>
import unittest
from fastapi.testclient import TestClient
from typosaurus_sandbox.app import App
client = TestClient(App)
class TestHealthEndpoint(unittest.TestCase):
def test_health_returns_ok(self) -> None:
response = client.get("/health")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok"})
class TestCalculatorAddEndpoint(unittest.TestCase):
def test_add_positive_integers(self) -> None:
response = client.post("/api/v1/calculator/add", json={"left": 3, "right": 5})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 8})
def test_add_negative_integers(self) -> None:
response = client.post("/api/v1/calculator/add", json={"left": -3, "right": -5})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": -8})
def test_add_invalid_input_returns_422(self) -> None:
response = client.post("/api/v1/calculator/add", json={"left": "abc", "right": 5})
self.assertEqual(response.status_code, 422)
def test_add_missing_field_returns_422(self) -> None:
response = client.post("/api/v1/calculator/add", json={"left": 3})
self.assertEqual(response.status_code, 422)
class TestCalculatorSubtractEndpoint(unittest.TestCase):
def test_subtract_positive(self) -> None:
response = client.post("/api/v1/calculator/subtract", json={"left": 10, "right": 3})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 7})
def test_subtract_negative_result(self) -> None:
response = client.post("/api/v1/calculator/subtract", json={"left": 3, "right": 10})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": -7})
def test_subtract_invalid_input_returns_422(self) -> None:
response = client.post("/api/v1/calculator/subtract", json={"left": 10, "right": None})
self.assertEqual(response.status_code, 422)
class TestCalculatorClampEndpoint(unittest.TestCase):
def test_clamp_value_below_low(self) -> None:
response = client.post("/api/v1/calculator/clamp", json={"value": -5, "low": 0, "high": 10})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 0})
def test_clamp_value_above_high(self) -> None:
response = client.post("/api/v1/calculator/clamp", json={"value": 15, "low": 0, "high": 10})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 10})
def test_clamp_value_in_range(self) -> None:
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "low": 0, "high": 10})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 5.0})
def test_clamp_invalid_input_returns_422(self) -> None:
response = client.post("/api/v1/calculator/clamp", json={"value": "x", "low": 0, "high": 10})
self.assertEqual(response.status_code, 422)
class TestCalculatorClampToByteEndpoint(unittest.TestCase):
def test_clamp_to_byte_within_range(self) -> None:
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": 128})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 128})
def test_clamp_to_byte_below_zero(self) -> None:
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": -10})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 0})
def test_clamp_to_byte_above_255(self) -> None:
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": 300})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"result": 255})
def test_clamp_to_byte_invalid_input_returns_422(self) -> None:
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": "abc"})
self.assertEqual(response.status_code, 422)

View File

@ -3,7 +3,47 @@
import math
import unittest
from src.calculator import clamp
from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, subtract
class TestAddFunction(unittest.TestCase):
def test_add_positive_integers(self) -> None:
self.assertEqual(add(3, 5), 8)
def test_add_negative_integers(self) -> None:
self.assertEqual(add(-3, -5), -8)
def test_add_mixed_sign(self) -> None:
self.assertEqual(add(-3, 5), 2)
class TestSubtractFunction(unittest.TestCase):
def test_subtract_positive(self) -> None:
self.assertEqual(subtract(10, 3), 7)
def test_subtract_negative_result(self) -> None:
self.assertEqual(subtract(3, 10), -7)
def test_subtract_negative_numbers(self) -> None:
self.assertEqual(subtract(-5, -3), -2)
class TestClampToByteFunction(unittest.TestCase):
def test_clamp_to_byte_within_range(self) -> None:
self.assertEqual(clamp_to_byte(128), 128)
def test_clamp_to_byte_below_zero(self) -> None:
self.assertEqual(clamp_to_byte(-10), 0)
def test_clamp_to_byte_above_255(self) -> None:
self.assertEqual(clamp_to_byte(300), 255)
def test_clamp_to_byte_boundaries(self) -> None:
self.assertEqual(clamp_to_byte(0), 0)
self.assertEqual(clamp_to_byte(255), 255)
class TestClampFunction(unittest.TestCase):