From c9012b8a68640542ef2b660fb67f394984578408 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 26 Jun 2026 12:07:56 +0300 Subject: [PATCH] refactor: collapse not-found handling into one seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three handlers each re-derived the not-found → 404 policy via two different mechanisms: - get_deck / get_card: get_one_or_none(...) then if not instance: raise HTTPException(404, ...) - update_deck: try: ... except NotFoundError: raise HTTPException(404) Consolidate that policy into one seam: a NotFoundError → 404 handler registered in build_app, mirroring the existing DuplicateKeyError handler. The not-found decision now lives where the data access happens (the repository's raising get_one), mapped to a response in one place. - app/exceptions.py — new not_found_error_handler returning 404 {"detail": "Not found"}. - app/application.py — register NotFoundError handler. - app/repositories.py — fetch_with_cards tightens to -> models.Deck, using get_one (raises on miss) instead of get_one_or_none. - app/api/decks.py — get_deck / update_deck / get_card drop all 404 code; get_card uses get_one. Removed now-unused imports (NotFoundError, starlette status, HTTPException usage). 404 body is generic {"detail": "Not found"} — advanced-alchemy's NotFoundError.detail is empty and no test asserts the message. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/decks.py | 15 ++------------- app/application.py | 6 +++++- app/exceptions.py | 9 ++++++++- app/repositories.py | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/api/decks.py b/app/api/decks.py index 881947b..eda9b88 100644 --- a/app/api/decks.py +++ b/app/api/decks.py @@ -1,9 +1,7 @@ import typing import fastapi -from advanced_alchemy.exceptions import NotFoundError from modern_di_fastapi import FromDI -from starlette import status from app import models, schemas from app.repositories import CardsRepository, DecksRepository @@ -26,9 +24,6 @@ async def get_deck( decks_repository: DecksRepository = FromDI(DecksRepository), ) -> schemas.Deck: instance = await decks_repository.fetch_with_cards(deck_id) - if not instance: - raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found") - return typing.cast("schemas.Deck", instance) @@ -38,11 +33,7 @@ async def update_deck( data: schemas.DeckCreate, decks_repository: DecksRepository = FromDI(DecksRepository), ) -> schemas.Deck: - try: - instance = await decks_repository.update(data=data.model_dump(), item_id=deck_id) - except NotFoundError: - raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found") from None - + instance = await decks_repository.update(data=data.model_dump(), item_id=deck_id) return typing.cast("schemas.Deck", instance) @@ -69,9 +60,7 @@ async def get_card( card_id: int, cards_repository: CardsRepository = FromDI(CardsRepository), ) -> schemas.Card: - instance = await cards_repository.get_one_or_none(models.Card.id == card_id) - if not instance: - raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card is not found") + instance = await cards_repository.get_one(models.Card.id == card_id) return typing.cast("schemas.Card", instance) diff --git a/app/application.py b/app/application.py index d26aa25..56e7f0e 100644 --- a/app/application.py +++ b/app/application.py @@ -3,7 +3,7 @@ import modern_di import modern_di_fastapi -from advanced_alchemy.exceptions import DuplicateKeyError +from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError from lite_bootstrap import FastAPIBootstrapper from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor @@ -38,4 +38,8 @@ def build_app() -> fastapi.FastAPI: DuplicateKeyError, exceptions.duplicate_key_error_handler, # ty: ignore[invalid-argument-type] ) + app.add_exception_handler( + NotFoundError, + exceptions.not_found_error_handler, # ty: ignore[invalid-argument-type] + ) return app diff --git a/app/exceptions.py b/app/exceptions.py index 1ff5e02..11a7bed 100644 --- a/app/exceptions.py +++ b/app/exceptions.py @@ -5,7 +5,7 @@ if TYPE_CHECKING: - from advanced_alchemy.exceptions import DuplicateKeyError + from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError from starlette.requests import Request @@ -14,3 +14,10 @@ async def duplicate_key_error_handler(_: Request, exc: DuplicateKeyError) -> JSO status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, content={"detail": exc.detail}, ) + + +async def not_found_error_handler(_: Request, __: NotFoundError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={"detail": "Not found"}, + ) diff --git a/app/repositories.py b/app/repositories.py index 3c0d608..af6b577 100644 --- a/app/repositories.py +++ b/app/repositories.py @@ -17,8 +17,8 @@ class BaseRepository(SQLAlchemyAsyncRepository[models.Deck]): repository_type = BaseRepository - async def fetch_with_cards(self, deck_id: int) -> models.Deck | None: - return await self.get_one_or_none( + async def fetch_with_cards(self, deck_id: int) -> models.Deck: + return await self.get_one( models.Deck.id == deck_id, load=[orm.selectinload(models.Deck.cards)], )