| """ |
| infra.api.errors — the API error model. |
| |
| One exception type, ``ApiError``, carrying a machine-readable ``code``, a |
| human ``message`` and the HTTP ``status`` it maps to. Handlers raise it; the |
| FastAPI shell (app.py) catches it and renders a consistent JSON body: |
| |
| {"error": {"code": "...", "message": "..."}} |
| |
| Pure module — no fastapi dependency, so handlers stay testable without a |
| web server. |
| """ |
| from __future__ import annotations |
|
|
|
|
| class ApiError(Exception): |
| """A handler-level error with a stable code and an HTTP status.""" |
|
|
| def __init__(self, code: str, message: str, status: int = 400): |
| super().__init__(message) |
| self.code = code |
| self.message = message |
| self.status = status |
|
|
| def to_dict(self) -> dict: |
| return {"error": {"code": self.code, "message": self.message}} |
|
|
|
|
| |
|
|
| def bad_request(message: str) -> ApiError: |
| return ApiError("bad_request", message, status=400) |
|
|
|
|
| def not_found(message: str) -> ApiError: |
| return ApiError("not_found", message, status=404) |
|
|
|
|
| def conflict(message: str) -> ApiError: |
| return ApiError("conflict", message, status=409) |
|
|
|
|
| def unprocessable(message: str) -> ApiError: |
| return ApiError("unprocessable", message, status=422) |
|
|