"""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