From f6fd6a92da5844ae572611220df1fefc4824ee6d Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 26 Jun 2026 13:40:22 +0300 Subject: [PATCH] refactor: move the Card HTTP surface into its own module The four card handlers lived in app/api/decks.py, scattering the Card concept across model / schema / repository / a misnamed handler file. To read the Card HTTP surface you had to open decks.py. Move all four card handlers into app/api/cards.py with its own ROUTER. The deck-nested /decks/{deck_id}/cards/ routes move too -- they operate on Card rows (CardsRepository, return schemas.Cards); the /decks prefix is just scoping, not ownership. Handlers keep their full paths, so the URL tree is unchanged. include_routers now registers both decks.ROUTER and cards.ROUTER; each mounts at /api and resolves to distinct paths -- no collision. Ports modern-python/litestar-sqlalchemy-template#31 to FastAPI. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- app/api/cards.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++ app/api/decks.py | 42 ++-------------------------------------- app/application.py | 5 +++-- 4 files changed, 54 insertions(+), 43 deletions(-) create mode 100644 app/api/cards.py diff --git a/CLAUDE.md b/CLAUDE.md index d3f6880..6767e09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ CI (`.github/workflows/main.yml`) runs `ruff format --check`, `ruff check --no-f 1. Creates a `modern_di.Container` with the `Dependencies` group from `app/ioc.py`. 2. Builds a `FastAPIBootstrapper` from `settings.api_bootstrapper_config`, injecting SQLAlchemy + asyncpg OpenTelemetry instrumentors. 3. `modern_di_fastapi.setup_di(app, container)` wires DI scopes onto the FastAPI app. -4. Includes `app.api.decks.ROUTER` under `/api`. +4. Includes one `ROUTER` per resource (`app.api.decks.ROUTER`, `app.api.cards.ROUTER`) under `/api` via `include_routers`. Add a new resource by creating `app/api/.py` with its own `APIRouter`-based `ROUTER` and registering it there. 5. Registers `DuplicateKeyError` → 422 handler from `app/exceptions.py`. ### DI scopes (modern-di) diff --git a/app/api/cards.py b/app/api/cards.py new file mode 100644 index 0000000..0291b3c --- /dev/null +++ b/app/api/cards.py @@ -0,0 +1,48 @@ +import typing + +import fastapi +from modern_di_fastapi import FromDI + +from app import models, schemas +from app.repositories import CardsRepository + + +ROUTER: typing.Final = fastapi.APIRouter() + + +@ROUTER.get("/decks/{deck_id}/cards/") +async def list_cards( + deck_id: int, + cards_repository: CardsRepository = FromDI(CardsRepository), +) -> schemas.Cards: + objects = await cards_repository.list_for_deck(deck_id) + return schemas.Cards.from_models(objects) + + +@ROUTER.get("/cards/{card_id}/") +async def get_card( + card_id: int, + cards_repository: CardsRepository = FromDI(CardsRepository), +) -> schemas.Card: + instance = await cards_repository.get_one(models.Card.id == card_id) + return schemas.Card.model_validate(instance) + + +@ROUTER.post("/decks/{deck_id}/cards/") +async def create_cards( + deck_id: int, + data: list[schemas.CardCreate], + cards_repository: CardsRepository = FromDI(CardsRepository), +) -> schemas.Cards: + objects = await cards_repository.add_cards(deck_id, data) + return schemas.Cards.from_models(objects) + + +@ROUTER.put("/decks/{deck_id}/cards/") +async def update_cards( + deck_id: int, + data: list[schemas.Card], + cards_repository: CardsRepository = FromDI(CardsRepository), +) -> schemas.Cards: + objects = await cards_repository.upsert_cards(deck_id, data) + return schemas.Cards.from_models(objects) diff --git a/app/api/decks.py b/app/api/decks.py index d17c154..de0e465 100644 --- a/app/api/decks.py +++ b/app/api/decks.py @@ -3,8 +3,8 @@ import fastapi from modern_di_fastapi import FromDI -from app import models, schemas -from app.repositories import CardsRepository, DecksRepository +from app import schemas +from app.repositories import DecksRepository ROUTER: typing.Final = fastapi.APIRouter() @@ -44,41 +44,3 @@ async def create_deck( ) -> schemas.Deck: instance = await decks_repository.create(data.model_dump()) return schemas.Deck.model_validate(instance) - - -@ROUTER.get("/decks/{deck_id}/cards/") -async def list_cards( - deck_id: int, - cards_repository: CardsRepository = FromDI(CardsRepository), -) -> schemas.Cards: - objects = await cards_repository.list_for_deck(deck_id) - return schemas.Cards.from_models(objects) - - -@ROUTER.get("/cards/{card_id}/") -async def get_card( - card_id: int, - cards_repository: CardsRepository = FromDI(CardsRepository), -) -> schemas.Card: - instance = await cards_repository.get_one(models.Card.id == card_id) - return schemas.Card.model_validate(instance) - - -@ROUTER.post("/decks/{deck_id}/cards/") -async def create_cards( - deck_id: int, - data: list[schemas.CardCreate], - cards_repository: CardsRepository = FromDI(CardsRepository), -) -> schemas.Cards: - objects = await cards_repository.add_cards(deck_id, data) - return schemas.Cards.from_models(objects) - - -@ROUTER.put("/decks/{deck_id}/cards/") -async def update_cards( - deck_id: int, - data: list[schemas.Card], - cards_repository: CardsRepository = FromDI(CardsRepository), -) -> schemas.Cards: - objects = await cards_repository.upsert_cards(deck_id, data) - return schemas.Cards.from_models(objects) diff --git a/app/application.py b/app/application.py index 56e7f0e..ad34aeb 100644 --- a/app/application.py +++ b/app/application.py @@ -9,7 +9,7 @@ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor from app import exceptions, ioc -from app.api.decks import ROUTER +from app.api import cards, decks from app.settings import settings @@ -18,7 +18,8 @@ def include_routers(app: fastapi.FastAPI) -> None: - app.include_router(ROUTER, prefix="/api") + app.include_router(decks.ROUTER, prefix="/api") + app.include_router(cards.ROUTER, prefix="/api") def build_app() -> fastapi.FastAPI: