Source code for alchemiq.fastapi.errors

"""Exception-to-HTTP mapping for alchemiq persistence errors."""

from __future__ import annotations

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

from alchemiq.exceptions import (
    ConcurrentModificationError,
    MultipleResultsFound,
    NotFoundError,
    PersistenceError,
    RelationNotLoaded,
)

_STATUS: dict[type[PersistenceError], int] = {
    NotFoundError: 404,
    MultipleResultsFound: 409,
    ConcurrentModificationError: 409,
    RelationNotLoaded: 500,
}


[docs] def status_for(exc: PersistenceError) -> int: """Return the HTTP status code for a ``PersistenceError``. The mapping is: - ``NotFoundError`` -> 404 - ``MultipleResultsFound`` -> 409 - ``ConcurrentModificationError`` -> 409 - ``RelationNotLoaded`` -> 500 - any other ``PersistenceError`` subtype -> 500 (fallback) ``InvalidCursorError`` is **not** in this map - :func:`.crud_router` raises it directly as an ``HTTPException`` with status 400. :param exc: a ``PersistenceError`` (or subclass) instance. :return: the HTTP status code integer. """ for exc_type, status in _STATUS.items(): if isinstance(exc, exc_type): return status return 500
[docs] def http_exception_for(exc: PersistenceError) -> HTTPException: """Convert a ``PersistenceError`` to an ``HTTPException`` with the mapped status code. :param exc: a ``PersistenceError`` (or subclass) instance. :return: an ``HTTPException`` whose ``status_code`` is determined by :func:`.status_for` and whose ``detail`` is the string representation of *exc*. """ return HTTPException(status_code=status_for(exc), detail=str(exc))
[docs] def install_exception_handlers(app: FastAPI) -> None: """Register a global handler that converts ``PersistenceError`` to JSON responses. Attaches a single ``app.add_exception_handler`` for ``PersistenceError``. Each subtype is mapped to an HTTP status by :func:`.status_for`; the response body is ``{"detail": "<exception message>"}``. E.g.:: from fastapi import FastAPI from alchemiq.fastapi import install_exception_handlers, crud_router app = FastAPI() install_exception_handlers(app) app.include_router(crud_router(User, prefix="/rows")) :param app: the ``FastAPI`` application instance. .. note:: ``InvalidCursorError`` is **not** caught by this handler. The list endpoint in :func:`.crud_router` raises it as an ``HTTPException(400)`` directly. .. seealso:: :func:`.status_for` - the exception->status mapping. """ async def _handle(request: Request, exc: Exception) -> JSONResponse: assert isinstance(exc, PersistenceError) return JSONResponse(status_code=status_for(exc), content={"detail": str(exc)}) app.add_exception_handler(PersistenceError, _handle)