patristic-be / src /api /dto /common.py
Mario33333's picture
deploy: reader-access security hardening (feature/audiobook@06a5ed8)
7c2d250 verified
Raw
History Blame Contribute Delete
2.02 kB
"""Shared DTO base + the two universal response shapes.
CONTRACT.md §1 mandates camelCase on the wire everywhere. This is enforced
exactly once, here, via the :class:`ApiModel` base. Every other DTO in the
``src/api/dto/`` tree inherits from it — that's how we avoid the v1 problem
where only ``BookDTO`` had ``alias_generator`` and everything else accidentally
shipped snake_case.
Why ``populate_by_name=True``: routers construct DTOs from Python kwargs that
use snake_case (e.g. ``ErrorResponse(code=..., message=...)``); the JSON they
produce on the wire is camelCase via the alias generator. Both sides win.
Why ``from_attributes=True``: lets a DTO be built from an ORM-like object or a
``dataclass`` instance (``BookDTO.model_validate(book)``), which the read-only
routers in Stage 3 will rely on.
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class ApiModel(BaseModel):
"""Base for every API DTO. Aliases snake_case → camelCase on the wire."""
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
from_attributes=True,
use_enum_values=True,
)
class ErrorResponse(ApiModel):
"""Canonical error envelope. See ``CONTRACT.md §8`` for the full code list.
``details`` keys must be camelCase on the wire. Modeling it as ``dict`` is
fine for Stage 1; routers that need typed details should subclass
:class:`ApiModel` per error type so the alias generator handles them.
"""
code: str
message: str
details: dict[str, Any] | None = None
class JobStartResponse(ApiModel):
"""Returned by every endpoint that kicks off a background operation.
See ``CONTRACT.md §6``. The shape is uniform across ingest, query, eval,
config-migrate — anything that may run longer than a sync request.
"""
job_id: str
sse_url: str