Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
935051a72b |
@@ -1,2 +0,0 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -7,20 +7,6 @@ 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
|
||||
|
||||
```
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
verify:
|
||||
@PYTHONPATH=src python3 -m compileall -q src tests && PYTHONPATH=src python3 -m unittest discover -s tests -q && echo "verification passed"
|
||||
|
||||
|
||||
@python3 -m compileall -q src && echo "verification passed"
|
||||
|
||||
@@ -1,123 +1,3 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
# typosaurus-sandbox
|
||||
|
||||
Sandbox for Typosaurus end-to-end verification.
|
||||
|
||||
A FastAPI application serving arithmetic operations over HTTP with JSON request/response bodies.
|
||||
|
||||
## Configuration
|
||||
|
||||
The application uses a single `.env.json` file at the project root as its central point of truth
|
||||
for configuration. Defaults are plug-and-play and require no setup:
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"port": 8000
|
||||
}
|
||||
```
|
||||
|
||||
When no `.env.json` is present, the application starts with these defaults. To customise, create
|
||||
`.env.json` in the project root and populate only the keys that differ.
|
||||
|
||||
## Usage
|
||||
|
||||
### Start the server
|
||||
|
||||
```sh
|
||||
python -m typosaurus_sandbox
|
||||
```
|
||||
|
||||
The server listens on `http://127.0.0.1:8000` by default.
|
||||
|
||||
### Health check
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
## API endpoints
|
||||
|
||||
All calculator endpoints accept `POST` requests with a JSON body and return a JSON response.
|
||||
|
||||
### POST /api/v1/calculator/add
|
||||
|
||||
Add two integers.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"left": 3, "right": 5}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"result": 8}
|
||||
```
|
||||
|
||||
### POST /api/v1/calculator/subtract
|
||||
|
||||
Subtract the right integer from the left.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"left": 10, "right": 3}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"result": 7}
|
||||
```
|
||||
|
||||
### POST /api/v1/calculator/clamp
|
||||
|
||||
Clamp a value between a low and high bound.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"value": 15, "low": 0, "high": 10}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"result": 10}
|
||||
```
|
||||
|
||||
Boundaries are inclusive. A `low > high` combination produces a 422 validation response.
|
||||
|
||||
### POST /api/v1/calculator/clamp-to-byte
|
||||
|
||||
Clamp an integer to the byte range [0, 255].
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"value": 300}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"result": 255}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
make verify
|
||||
```
|
||||
|
||||
Runs compile-all checks against all source and test files, then executes the full test suite.
|
||||
Zero warnings are tolerated.
|
||||
Sandbox for Typosaurus end-to-end verification
|
||||
@@ -1,16 +0,0 @@
|
||||
[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"]
|
||||
Binary file not shown.
+2
-24
@@ -1,8 +1,5 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
def add(left: int, right: int) -> int:
|
||||
return left + right
|
||||
|
||||
@@ -11,24 +8,5 @@ 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)
|
||||
|
||||
|
||||
def power(base: int, exponent: int) -> int:
|
||||
return base ** exponent
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from typosaurus_sandbox.app import App
|
||||
|
||||
__all__ = ["App"]
|
||||
@@ -1,7 +0,0 @@
|
||||
# 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")
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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"]
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -1 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -1 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -1,60 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
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:
|
||||
try:
|
||||
result = clamp(body.value, body.low, body.high)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=422, detail="low must not exceed high")
|
||||
return FloatResult(result=result)
|
||||
|
||||
|
||||
@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))
|
||||
@@ -1 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
@@ -1,119 +0,0 @@
|
||||
# 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)
|
||||
|
||||
def test_subtract_missing_left_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/subtract", json={"right": 3})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_subtract_missing_right_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/subtract", json={"left": 10})
|
||||
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)
|
||||
|
||||
def test_clamp_low_greater_than_high_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "low": 10, "high": 0})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_clamp_missing_low_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "high": 10})
|
||||
self.assertEqual(response.status_code, 422)
|
||||
|
||||
def test_clamp_missing_high_returns_422(self) -> None:
|
||||
response = client.post("/api/v1/calculator/clamp", json={"value": 5, "low": 0})
|
||||
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)
|
||||
@@ -1,135 +0,0 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import math
|
||||
import unittest
|
||||
|
||||
from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, subtract, variance
|
||||
|
||||
|
||||
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):
|
||||
|
||||
def test_value_below_low_returns_low(self) -> None:
|
||||
self.assertEqual(clamp(-5, 0, 10), 0)
|
||||
|
||||
def test_value_above_high_returns_high(self) -> None:
|
||||
self.assertEqual(clamp(15, 0, 10), 10)
|
||||
|
||||
def test_value_in_range_returns_value(self) -> None:
|
||||
self.assertEqual(clamp(5, 0, 10), 5)
|
||||
|
||||
def test_value_equals_low_returns_low(self) -> None:
|
||||
self.assertEqual(clamp(0, 0, 10), 0)
|
||||
|
||||
def test_value_equals_high_returns_high(self) -> None:
|
||||
self.assertEqual(clamp(10, 0, 10), 10)
|
||||
|
||||
def test_low_greater_than_high_raises_value_error(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
clamp(5, 10, 0)
|
||||
|
||||
def test_all_negative_values(self) -> None:
|
||||
self.assertEqual(clamp(-10, -5, -1), -5)
|
||||
|
||||
def test_float_value_below_low_returns_low_as_int(self) -> None:
|
||||
self.assertEqual(clamp(-1.0, 0, 10), 0)
|
||||
|
||||
def test_float_value_above_high_returns_high_as_int(self) -> None:
|
||||
self.assertEqual(clamp(15.0, 0, 10), 10)
|
||||
|
||||
def test_float_value_in_range_returns_float(self) -> None:
|
||||
result = clamp(5.0, 0, 10)
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertEqual(result, 5.0)
|
||||
|
||||
def test_float_value_equals_boundary_returns_boundary(self) -> None:
|
||||
self.assertEqual(clamp(0.0, 0, 10), 0)
|
||||
self.assertEqual(clamp(10.0, 0, 10), 10)
|
||||
|
||||
def test_float_low_greater_than_float_high_raises_value_error(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
clamp(5.0, 10.0, 0.0)
|
||||
|
||||
def test_large_values(self) -> None:
|
||||
self.assertEqual(clamp(10**9, 0, 10**6), 10**6)
|
||||
|
||||
def test_negative_infinity_not_clamped_by_default(self) -> None:
|
||||
self.assertEqual(clamp(-math.inf, 0, 10), 0)
|
||||
|
||||
def test_positive_infinity_not_clamped_by_default(self) -> None:
|
||||
self.assertEqual(clamp(math.inf, 0, 10), 10)
|
||||
|
||||
def test_nan_returns_nan(self) -> None:
|
||||
result = clamp(math.nan, 0, 10)
|
||||
self.assertTrue(math.isnan(result))
|
||||
|
||||
|
||||
class TestVarianceFunction(unittest.TestCase):
|
||||
|
||||
def test_empty_list_raises_value_error(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
variance([])
|
||||
|
||||
def test_empty_tuple_raises_value_error(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
variance(())
|
||||
|
||||
def test_single_element_returns_zero(self) -> None:
|
||||
self.assertEqual(variance([42.0]), 0.0)
|
||||
|
||||
def test_constant_values_return_zero_variance(self) -> None:
|
||||
self.assertEqual(variance([1.0, 1.0, 1.0]), 0.0)
|
||||
|
||||
def test_population_variance_of_known_set(self) -> None:
|
||||
self.assertEqual(variance([1, 2, 3, 4, 5]), 2.0)
|
||||
|
||||
def test_two_element_variance(self) -> None:
|
||||
self.assertEqual(variance([0, 2]), 1.0)
|
||||
|
||||
def test_integer_inputs_return_float(self) -> None:
|
||||
result = variance([10, 20, 30])
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertEqual(result, 200.0 / 3.0)
|
||||
|
||||
def test_tuple_input_returns_variance(self) -> None:
|
||||
self.assertEqual(variance((1, 2, 3, 4, 5)), 2.0)
|
||||
|
||||
Reference in New Issue
Block a user