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 19:57:06 +00:00
parent 697e926dfe
commit 1278d5c332
15 changed files with 280 additions and 95 deletions
+5
View File
@@ -0,0 +1,5 @@
# retoor <retoor@molodetz.nl>
from typosaurus_sandbox.app import App
__all__ = ["App"]
+7
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")
+15
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)
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
@@ -0,0 +1,6 @@
# retoor <retoor@molodetz.nl>
from typosaurus_sandbox.domain.calculator.operations import add, clamp, clamp_to_byte, subtract, variance
__all__ = ["add", "subtract", "clamp", "clamp_to_byte", "variance"]
@@ -0,0 +1,33 @@
# retoor <retoor@molodetz.nl>
from typing import Sequence
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))
def variance(values: Sequence[float]) -> float:
if not values:
raise ValueError
mean = sum(values) / len(values)
return sum((x - mean) ** 2 for x in values) / len(values)
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
@@ -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))