Compare commits
No commits in common. "main" and "typosaurus/13-add-a-median-function-to-the-calculator" have entirely different histories.
main
...
typosaurus
@ -1,21 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, master]
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -e .
|
|
||||||
- name: Run tests
|
|
||||||
run: make verify
|
|
||||||
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,2 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.env.json
|
|
||||||
logs/
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
# typosaurus-sandbox
|
# typosaurus-sandbox
|
||||||
|
|
||||||
A minimal Python calculator used to verify the Typosaurus agent system.
|
A minimal Python calculator used to verify the Typosaurus agent system.
|
||||||
@ -27,11 +26,3 @@ A minimal Python calculator used to verify the Typosaurus agent system.
|
|||||||
```
|
```
|
||||||
make verify
|
make verify
|
||||||
```
|
```
|
||||||
|
|
||||||
## CI
|
|
||||||
|
|
||||||
- Workflow file: `.gitea/workflows/ci.yml`
|
|
||||||
- Trigger: push to `main` or `master` branches
|
|
||||||
- Steps: checkout, Python 3.12 setup, dependency install, `make verify`
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
85
README.md
85
README.md
@ -113,90 +113,6 @@ Response:
|
|||||||
{"result": 255}
|
{"result": 255}
|
||||||
```
|
```
|
||||||
|
|
||||||
### POST /api/v1/calculator/average
|
|
||||||
|
|
||||||
Compute the arithmetic mean of a list of values.
|
|
||||||
|
|
||||||
Request:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"values": [1, 2, 3, 4, 5]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"result": 3.0}
|
|
||||||
```
|
|
||||||
|
|
||||||
An empty list produces a 422 validation response.
|
|
||||||
|
|
||||||
### POST /api/v1/calculator/median
|
|
||||||
|
|
||||||
Compute the median of a list of values. Values are sorted internally; an even-length list returns the average of the two middle values as a float.
|
|
||||||
|
|
||||||
Request:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"values": [1, 3, 5]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"result": 3.0}
|
|
||||||
```
|
|
||||||
|
|
||||||
Request (even length):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"values": [1, 2, 3, 4]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"result": 2.5}
|
|
||||||
```
|
|
||||||
|
|
||||||
An empty list produces a 422 validation response.
|
|
||||||
|
|
||||||
### POST /api/v1/calculator/variance
|
|
||||||
|
|
||||||
Compute the population variance of a list of values.
|
|
||||||
|
|
||||||
Request:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"values": [1, 2, 3, 4, 5]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"result": 2.0}
|
|
||||||
```
|
|
||||||
|
|
||||||
An empty list produces a 422 validation response.
|
|
||||||
|
|
||||||
### POST /api/v1/calculator/percentage
|
|
||||||
|
|
||||||
Compute what percentage `value` is of `total`.
|
|
||||||
|
|
||||||
Request:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"value": 50, "total": 100}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"result": 50.0}
|
|
||||||
```
|
|
||||||
|
|
||||||
A zero `total` produces a 422 validation response.
|
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -205,4 +121,3 @@ make verify
|
|||||||
|
|
||||||
Runs compile-all checks against all source and test files, then executes the full test suite.
|
Runs compile-all checks against all source and test files, then executes the full test suite.
|
||||||
Zero warnings are tolerated.
|
Zero warnings are tolerated.
|
||||||
|
|
||||||
|
|||||||
@ -8,10 +8,10 @@ from flask import make_response
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask.wrappers import Response
|
from flask.wrappers import Response
|
||||||
|
|
||||||
from typosaurus_sandbox.domain.calculator import add
|
from src.calculator import add
|
||||||
from typosaurus_sandbox.domain.calculator import clamp
|
from src.calculator import clamp
|
||||||
from typosaurus_sandbox.domain.calculator import clamp_to_byte
|
from src.calculator import clamp_to_byte
|
||||||
from typosaurus_sandbox.domain.calculator import subtract
|
from src.calculator import subtract
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@ -65,4 +65,3 @@ def clamp_to_byte_route() -> Response:
|
|||||||
except (KeyError, TypeError, ValueError):
|
except (KeyError, TypeError, ValueError):
|
||||||
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
|
return make_response(jsonify({'error': 'Invalid or missing parameters'}), 400)
|
||||||
return make_response(jsonify({'result': clamp_to_byte(value)}), 200)
|
return make_response(jsonify({'result': clamp_to_byte(value)}), 200)
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
fastapi
|
Flask>=3.0,<4.0
|
||||||
uvicorn[standard]
|
|
||||||
|
|
||||||
|
|||||||
56
src/calculator.py
Normal file
56
src/calculator.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
|
||||||
|
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 average(values: list[int | float]) -> float:
|
||||||
|
if not values:
|
||||||
|
raise ValueError
|
||||||
|
return sum(values) / len(values)
|
||||||
|
|
||||||
|
|
||||||
|
def median(values: list[float]) -> float:
|
||||||
|
if not values:
|
||||||
|
raise ValueError
|
||||||
|
sorted_values = sorted(values)
|
||||||
|
n = len(sorted_values)
|
||||||
|
mid = n // 2
|
||||||
|
if n % 2 == 1:
|
||||||
|
return sorted_values[mid]
|
||||||
|
return (sorted_values[mid - 1] + sorted_values[mid]) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def percentage(value: Union[int, float], total: Union[int, float]) -> float:
|
||||||
|
if total == 0:
|
||||||
|
raise ValueError
|
||||||
|
return (value / total) * 100
|
||||||
|
|
||||||
|
|
||||||
|
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,7 +1,5 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
from typosaurus_sandbox.app import App
|
from typosaurus_sandbox.app import App
|
||||||
from typosaurus_sandbox.core import Config, setup_logging
|
|
||||||
|
|
||||||
__all__ = ["App", "Config", "setup_logging"]
|
|
||||||
|
|
||||||
|
__all__ = ["App"]
|
||||||
|
|||||||
@ -1,22 +1,7 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from typosaurus_sandbox.app import App
|
from typosaurus_sandbox.app import App
|
||||||
from typosaurus_sandbox.core import Config, setup_logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
setup_logging()
|
|
||||||
config = Config.load()
|
|
||||||
logger.info("starting server on %s:%d", config.host, config.port)
|
|
||||||
uvicorn.run(App, host=config.host, port=config.port, log_level="info")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
uvicorn.run(App, host="127.0.0.1", port=8000, log_level="info")
|
||||||
|
|||||||
@ -1,26 +1,15 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from typosaurus_sandbox.presentation.api.v1.calculator import calculator_router
|
from typosaurus_sandbox.presentation.api.v1.calculator import calculator_router
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
App = FastAPI(title="typosaurus-sandbox")
|
App = FastAPI(title="typosaurus-sandbox")
|
||||||
|
|
||||||
|
|
||||||
@App.on_event("startup")
|
|
||||||
def on_startup() -> None:
|
|
||||||
logger.info("application startup complete")
|
|
||||||
|
|
||||||
|
|
||||||
@App.get("/health")
|
@App.get("/health")
|
||||||
def health() -> dict[str, str]:
|
def health() -> dict[str, str]:
|
||||||
logger.debug("health check requested")
|
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
App.include_router(calculator_router)
|
App.include_router(calculator_router)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
|
|
||||||
from typosaurus_sandbox.core.config import Config
|
|
||||||
from typosaurus_sandbox.core.logging import setup_logging
|
|
||||||
|
|
||||||
__all__ = ["Config", "setup_logging"]
|
|
||||||
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Config:
|
|
||||||
host: str = "127.0.0.1"
|
|
||||||
port: int = 8000
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load(cls) -> "Config":
|
|
||||||
config_path = Path(".env.json")
|
|
||||||
if not config_path.exists():
|
|
||||||
logger.info("no .env.json found, using defaults")
|
|
||||||
return cls()
|
|
||||||
with config_path.open() as f:
|
|
||||||
data = json.load(f)
|
|
||||||
host = data.get("host", cls.host)
|
|
||||||
port = data.get("port", cls.port)
|
|
||||||
logger.info("loaded config from .env.json: host=%s port=%s", host, port)
|
|
||||||
return cls(host=host, port=port)
|
|
||||||
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import logging.handlers
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging() -> None:
|
|
||||||
log_dir = Path("logs")
|
|
||||||
log_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
handler = logging.handlers.RotatingFileHandler(
|
|
||||||
log_dir / "typosaurus-sandbox.log",
|
|
||||||
maxBytes=10 * 1024 * 1024,
|
|
||||||
backupCount=5,
|
|
||||||
)
|
|
||||||
handler.setFormatter(
|
|
||||||
logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG, handlers=[handler])
|
|
||||||
logging.getLogger(__name__).info("logging configured")
|
|
||||||
|
|
||||||
@ -1,13 +1,9 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from typosaurus_sandbox.domain.calculator import add, average, clamp, clamp_to_byte, median, percentage, subtract, variance
|
from typosaurus_sandbox.domain.calculator import add, clamp, clamp_to_byte, subtract
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
calculator_router = APIRouter(prefix="/api/v1/calculator")
|
calculator_router = APIRouter(prefix="/api/v1/calculator")
|
||||||
|
|
||||||
@ -32,15 +28,6 @@ class ClampToByteRequest(BaseModel):
|
|||||||
value: int = Field(ge=-2147483648, le=2147483647)
|
value: int = Field(ge=-2147483648, le=2147483647)
|
||||||
|
|
||||||
|
|
||||||
class ValuesRequest(BaseModel):
|
|
||||||
values: list[float]
|
|
||||||
|
|
||||||
|
|
||||||
class PercentageRequest(BaseModel):
|
|
||||||
value: float
|
|
||||||
total: float
|
|
||||||
|
|
||||||
|
|
||||||
class IntResult(BaseModel):
|
class IntResult(BaseModel):
|
||||||
result: int
|
result: int
|
||||||
|
|
||||||
@ -51,13 +38,11 @@ class FloatResult(BaseModel):
|
|||||||
|
|
||||||
@calculator_router.post("/add", response_model=IntResult)
|
@calculator_router.post("/add", response_model=IntResult)
|
||||||
def calculate_add(body: AddRequest) -> IntResult:
|
def calculate_add(body: AddRequest) -> IntResult:
|
||||||
logger.debug("add %d + %d", body.left, body.right)
|
|
||||||
return IntResult(result=add(body.left, body.right))
|
return IntResult(result=add(body.left, body.right))
|
||||||
|
|
||||||
|
|
||||||
@calculator_router.post("/subtract", response_model=IntResult)
|
@calculator_router.post("/subtract", response_model=IntResult)
|
||||||
def calculate_subtract(body: SubtractRequest) -> IntResult:
|
def calculate_subtract(body: SubtractRequest) -> IntResult:
|
||||||
logger.debug("subtract %d - %d", body.left, body.right)
|
|
||||||
return IntResult(result=subtract(body.left, body.right))
|
return IntResult(result=subtract(body.left, body.right))
|
||||||
|
|
||||||
|
|
||||||
@ -72,46 +57,4 @@ def calculate_clamp(body: ClampRequest) -> FloatResult:
|
|||||||
|
|
||||||
@calculator_router.post("/clamp-to-byte", response_model=IntResult)
|
@calculator_router.post("/clamp-to-byte", response_model=IntResult)
|
||||||
def calculate_clamp_to_byte(body: ClampToByteRequest) -> 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))
|
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)
|
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
# retoor <retoor@molodetz.nl>
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", category=DeprecationWarning, module="starlette")
|
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
@ -121,134 +118,3 @@ class TestCalculatorClampToByteEndpoint(unittest.TestCase):
|
|||||||
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": "abc"})
|
response = client.post("/api/v1/calculator/clamp-to-byte", json={"value": "abc"})
|
||||||
self.assertEqual(response.status_code, 422)
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import math
|
import math
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from typosaurus_sandbox.domain.calculator import add, average, clamp, clamp_to_byte, median, percentage, subtract, variance
|
from typosaurus_sandbox.domain.calculator import add, average, clamp, clamp_to_byte, median, subtract, variance
|
||||||
|
|
||||||
|
|
||||||
class TestAddFunction(unittest.TestCase):
|
class TestAddFunction(unittest.TestCase):
|
||||||
@ -181,46 +181,3 @@ class TestVarianceFunction(unittest.TestCase):
|
|||||||
def test_tuple_input_returns_variance(self) -> None:
|
def test_tuple_input_returns_variance(self) -> None:
|
||||||
self.assertEqual(variance((1, 2, 3, 4, 5)), 2.0)
|
self.assertEqual(variance((1, 2, 3, 4, 5)), 2.0)
|
||||||
|
|
||||||
|
|
||||||
class TestPercentageFunction(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_half_returns_50(self) -> None:
|
|
||||||
self.assertEqual(percentage(50, 100), 50.0)
|
|
||||||
|
|
||||||
def test_quarter_returns_25(self) -> None:
|
|
||||||
self.assertEqual(percentage(25, 100), 25.0)
|
|
||||||
|
|
||||||
def test_zero_value_returns_zero(self) -> None:
|
|
||||||
self.assertEqual(percentage(0, 100), 0.0)
|
|
||||||
|
|
||||||
def test_value_exceeds_total(self) -> None:
|
|
||||||
self.assertEqual(percentage(150, 100), 150.0)
|
|
||||||
|
|
||||||
def test_negative_value(self) -> None:
|
|
||||||
self.assertEqual(percentage(-50, 100), -50.0)
|
|
||||||
|
|
||||||
def test_negative_total(self) -> None:
|
|
||||||
self.assertEqual(percentage(50, -100), -50.0)
|
|
||||||
|
|
||||||
def test_both_negative(self) -> None:
|
|
||||||
self.assertEqual(percentage(-50, -100), 50.0)
|
|
||||||
|
|
||||||
def test_float_inputs(self) -> None:
|
|
||||||
result = percentage(33.0, 100.0)
|
|
||||||
self.assertAlmostEqual(result, 33.0)
|
|
||||||
|
|
||||||
def test_total_zero_raises_value_error(self) -> None:
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
percentage(50, 0)
|
|
||||||
|
|
||||||
def test_total_zero_float_raises_value_error(self) -> None:
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
percentage(50.0, 0.0)
|
|
||||||
|
|
||||||
def test_integer_inputs_return_float(self) -> None:
|
|
||||||
result = percentage(1, 4)
|
|
||||||
self.assertIsInstance(result, float)
|
|
||||||
self.assertEqual(result, 25.0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user