Skip to content

Commit 4e59e1c

Browse files
lesnik512claude
andcommitted
refactor: split the Deck response contract from its detail view
A single Deck schema served two endpoints with different card-loading strategies, leaking that difference into a cards: list[Card] | None field. Worse, list_decks reported cards: [] for every deck regardless of its real cards, because the relationship is lazy="noload" — the list contract was lying, and the | None was dead (noload yields []). Split into Deck (no cards) and DeckWithCards(Deck) (cards: list[Card], always populated). get_deck returns DeckWithCards; list/create/update return the light Deck. Each endpoint's type now states exactly what it loads, and the dead | None is gone. This is an intentional wire-contract change: the cards key disappears from list/create/update responses. The two-shape convention is recorded in CLAUDE.md and in docstrings on the Deck/DeckWithCards schemas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 250109c commit 4e59e1c

3 files changed

Lines changed: 12 additions & 4 deletions

File tree

‎CLAUDE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Python is 3.14, dependencies managed by `uv`. The API is exposed on `:8000`.
3636
## Conventions
3737

3838
- Routes live in `app/api/`, registered into a single `ROUTER` (prefix `/api`) referenced from `application.py`. Add a new resource by creating `app/api/<name>.py`, defining handlers + a `Router`, and adding it to `application.build_app`.
39-
- 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)`).
39+
- 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`).
40+
- 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.
4041
- 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.
4142
- `ruff` is configured with `select = ["ALL"]` and a line length of 120 — expect strict lint. Type-check with `ty`; use `# ty: ignore[<rule>]` for suppressions (already used for `invalid-argument-type` around `LifespanManager` / `ASGITransport` / DTO list construction).

‎app/api/decks.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ async def list_decks(decks_repository: DecksRepository) -> schemas.Decks:
1414

1515

1616
@litestar.get("/decks/{deck_id:int}/")
17-
async def get_deck(deck_id: FromPath[int], decks_repository: DecksRepository) -> schemas.Deck:
17+
async def get_deck(deck_id: FromPath[int], decks_repository: DecksRepository) -> schemas.DeckWithCards:
1818
instance = await decks_repository.fetch_with_cards(deck_id)
19-
return schemas.Deck.model_validate(instance)
19+
return schemas.DeckWithCards.model_validate(instance)
2020

2121

2222
@litestar.put("/decks/{deck_id:int}/")

‎app/schemas.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,15 @@ class DeckCreate(DeckBase):
4949

5050

5151
class Deck(DeckBase):
52+
"""Light deck view for lists and writes; cards are not loaded."""
53+
5254
id: PositiveInt
53-
cards: list[Card] | None
55+
56+
57+
class DeckWithCards(Deck):
58+
"""Deck detail view; cards are eager-loaded (selectinload) and always present."""
59+
60+
cards: list[Card]
5461

5562

5663
class Decks(Collection[Deck]):

0 commit comments

Comments
 (0)