Files
lifetrack/apps/api/app/core/errors.py
T
MeeJayandClaude Opus 5 93f0689c1e Initial import: LifeTrack v1 (santé, vape, finances)
Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de
pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et
finances personnelles avec import de relevés bancaires.

Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS +
Vite + Tailwind + ECharts, déploiement Docker Compose. Modules
auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework
de connecteurs à deux voies (importeurs de fichiers + ingestion JSON)
pour brancher de nouvelles sources sans toucher au noyau.

Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié
contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16
(28 tables, SPA servie par nginx, wizard de premier démarrage).

Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les
documents de conception et de recherche dans docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:48:57 +02:00

95 lines
2.7 KiB
Python

from typing import Any
from fastapi import FastAPI, Request
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
class AppError(Exception):
status_code: int = 400
code: str = "bad_request"
def __init__(self, message: str, *, details: dict[str, Any] | None = None):
self.message = message
self.details = details or {}
super().__init__(message)
class NotFoundError(AppError):
status_code, code = 404, "not_found"
class ConflictError(AppError):
status_code, code = 409, "conflict"
class UnauthorizedError(AppError):
status_code, code = 401, "unauthorized"
class ForbiddenError(AppError):
status_code, code = 403, "forbidden"
class DomainValidationError(AppError):
status_code, code = 422, "validation_error"
class PayloadTooLargeError(AppError):
status_code, code = 413, "payload_too_large"
_HTTP_CODES = {
400: "bad_request",
401: "unauthorized",
403: "forbidden",
404: "not_found",
405: "method_not_allowed",
409: "conflict",
413: "payload_too_large",
422: "validation_error",
500: "internal_error",
}
def _payload(code: str, message: str, details: Any = None) -> dict[str, Any]:
return {"error": {"code": code, "message": message, "details": details or {}}}
def register_error_handlers(app: FastAPI) -> None:
@app.exception_handler(AppError)
async def app_error_handler(_: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content=_payload(exc.code, exc.message, exc.details),
)
@app.exception_handler(RequestValidationError)
async def validation_handler(
_: Request, exc: RequestValidationError
) -> JSONResponse:
return JSONResponse(
status_code=422,
content=_payload(
"validation_error",
"Les données envoyées sont invalides.",
{"errors": jsonable_encoder(exc.errors())},
),
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(
_: Request, exc: StarletteHTTPException
) -> JSONResponse:
# Normalize framework-raised HTTP errors (404 route, 405…) to the
# single error shape of §8.3.
code = _HTTP_CODES.get(exc.status_code, "error")
message = (
exc.detail if isinstance(exc.detail, str) else "Une erreur est survenue."
)
return JSONResponse(
status_code=exc.status_code, content=_payload(code, message)
)