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:
parent
078766216d
commit
51ae51987b
14
CLAUDE.md
14
CLAUDE.md
@ -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
|
||||
|
||||
```
|
||||
|
||||
2
Makefile
2
Makefile
@ -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
16
pyproject.toml
Normal 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"]
|
||||
5
src/typosaurus_sandbox/__init__.py
Normal file
5
src/typosaurus_sandbox/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from typosaurus_sandbox.app import App
|
||||
|
||||
__all__ = ["App"]
|
||||
7
src/typosaurus_sandbox/__main__.py
Normal file
7
src/typosaurus_sandbox/__main__.py
Normal 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")
|
||||
15
src/typosaurus_sandbox/app.py
Normal file
15
src/typosaurus_sandbox/app.py
Normal 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)
|
||||
1
src/typosaurus_sandbox/domain/__init__.py
Normal file
1
src/typosaurus_sandbox/domain/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
5
src/typosaurus_sandbox/domain/calculator/__init__.py
Normal file
5
src/typosaurus_sandbox/domain/calculator/__init__.py
Normal 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"]
|
||||
22
src/typosaurus_sandbox/domain/calculator/operations.py
Normal file
22
src/typosaurus_sandbox/domain/calculator/operations.py
Normal 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))
|
||||
1
src/typosaurus_sandbox/presentation/__init__.py
Normal file
1
src/typosaurus_sandbox/presentation/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
1
src/typosaurus_sandbox/presentation/api/__init__.py
Normal file
1
src/typosaurus_sandbox/presentation/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
1
src/typosaurus_sandbox/presentation/api/v1/__init__.py
Normal file
1
src/typosaurus_sandbox/presentation/api/v1/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
56
src/typosaurus_sandbox/presentation/api/v1/calculator.py
Normal file
56
src/typosaurus_sandbox/presentation/api/v1/calculator.py
Normal 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
99
tests/test_api.py
Normal 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)
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user