diff --git a/CLAUDE.md b/CLAUDE.md index 5033363..46b62e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,7 @@ Python is 3.14, dependencies managed by `uv`. The API is exposed on `:8000`. ## Conventions - Routes live in `app/api/`, registered into a single `ROUTER` (prefix `/api`) referenced from `application.py`. Add a new resource by creating `app/api/.py`, defining handlers + a `Router`, and adding it to `application.build_app`. -- Pydantic schemas in `app/schemas.py` use `from_attributes=True` (via `Base`) so they validate directly from ORM instances (`schemas.X.model_validate(orm_instance)`). +- Pydantic schemas in `app/schemas.py` use `from_attributes=True` (via `Base`) so they validate directly from ORM instances (`schemas.X.model_validate(orm_instance)`). Collection responses go through `Collection[T].from_models(...)` (e.g. `schemas.Decks`, `schemas.Cards`). +- Deck responses are deliberately two-shaped: `list_decks`/`create_deck`/`update_deck` return the light `schemas.Deck` (no `cards`), while `get_deck` returns `schemas.DeckWithCards`. The split mirrors loading — lists use `noload` (no cards query), detail uses `selectinload` via `fetch_with_cards` — so the type states exactly what each endpoint loads. - Domain exceptions: register handlers in `application.build_app`'s `exception_handlers` dict (see `DuplicateKeyError` → `exceptions.duplicate_key_error_handler`). For per-handler 404s the code raises `litestar.exceptions.HTTPException` directly. - `ruff` is configured with `select = ["ALL"]` and a line length of 120 — expect strict lint. Type-check with `ty`; use `# ty: ignore[]` for suppressions (already used for `invalid-argument-type` around `LifespanManager` / `ASGITransport` / DTO list construction). diff --git a/app/api/decks.py b/app/api/decks.py index f2e53a6..0ac8ae0 100644 --- a/app/api/decks.py +++ b/app/api/decks.py @@ -14,9 +14,9 @@ async def list_decks(decks_repository: DecksRepository) -> schemas.Decks: @litestar.get("/decks/{deck_id:int}/") -async def get_deck(deck_id: FromPath[int], decks_repository: DecksRepository) -> schemas.Deck: +async def get_deck(deck_id: FromPath[int], decks_repository: DecksRepository) -> schemas.DeckWithCards: instance = await decks_repository.fetch_with_cards(deck_id) - return schemas.Deck.model_validate(instance) + return schemas.DeckWithCards.model_validate(instance) @litestar.put("/decks/{deck_id:int}/") diff --git a/app/schemas.py b/app/schemas.py index a4b2d5a..2dc630d 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -49,8 +49,15 @@ class DeckCreate(DeckBase): class Deck(DeckBase): + """Light deck view for lists and writes; cards are not loaded.""" + id: PositiveInt - cards: list[Card] | None + + +class DeckWithCards(Deck): + """Deck detail view; cards are eager-loaded (selectinload) and always present.""" + + cards: list[Card] class Decks(Collection[Deck]):