scholarr/app/api/errors.py
Justin Visser bf04c77aa9 ci: add ruff linting and mypy type checking
Add ruff and mypy to dev dependencies with configuration in pyproject.toml.
Add a lint CI job that runs ruff check, ruff format --check, and mypy.
Auto-fix import sorting and formatting across the codebase. Exclude
alembic/versions from linting (auto-generated migrations). Ignore B008
(FastAPI Depends pattern) and RUF001 (unicode in user-facing strings).

21 ruff lint errors and 50 mypy errors remain for manual review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:11:41 +01:00

84 lines
2.6 KiB
Python

from __future__ import annotations
from typing import Any
from fastapi import FastAPI, HTTPException, Request
from fastapi.exception_handlers import (
http_exception_handler as fastapi_http_exception_handler,
)
from fastapi.exception_handlers import (
request_validation_exception_handler as fastapi_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from app.api.responses import error_response
def _is_api_path(path: str) -> bool:
return path.startswith("/api/")
def _status_code_to_error_code(status_code: int) -> str:
mapping = {
400: "bad_request",
401: "auth_required",
403: "forbidden",
404: "not_found",
409: "conflict",
422: "validation_error",
429: "rate_limited",
500: "internal_error",
}
return mapping.get(status_code, "error")
class ApiException(Exception):
def __init__(
self,
*,
status_code: int,
code: str,
message: str,
details: Any | None = None,
) -> None:
super().__init__(message)
self.status_code = status_code
self.code = code
self.message = message
self.details = details
def register_api_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(ApiException)
async def _handle_api_exception(request: Request, exc: ApiException):
return error_response(
request,
status_code=exc.status_code,
code=exc.code,
message=exc.message,
details=exc.details,
)
@app.exception_handler(HTTPException)
async def _handle_http_exception(request: Request, exc: HTTPException):
if not _is_api_path(request.url.path):
return await fastapi_http_exception_handler(request, exc)
return error_response(
request,
status_code=exc.status_code,
code=_status_code_to_error_code(exc.status_code),
message=str(exc.detail) if exc.detail is not None else "Request failed.",
details=exc.detail if isinstance(exc.detail, (dict, list)) else None,
)
@app.exception_handler(RequestValidationError)
async def _handle_validation_exception(request: Request, exc: RequestValidationError):
if not _is_api_path(request.url.path):
return await fastapi_validation_exception_handler(request, exc)
return error_response(
request,
status_code=422,
code="validation_error",
message="Request validation failed.",
details=exc.errors(),
)