From a5047540c174b463d1d4bd62aa5546ec03c24c1f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 26 Jun 2026 11:54:32 +0300 Subject: [PATCH] refactor: collapse not-found handling into one seam Three handlers re-derived the not-found policy via two mechanisms: a None-check after get_one_or_none, and an except NotFoundError around update. The 404 knowledge was spread across the HTTP handlers. Register a single NotFoundError -> 404 handler in build_app (mirroring the existing DuplicateKeyError handler). Reads switch to advanced-alchemy's raising variant (get_one), fetch_with_cards tightens to a non-optional return, and update_deck drops its try/except. No handler mentions 404. The 404 body becomes a generic {"detail": "Not found"}; status behaviour is unchanged and the existing 404 tests stay green. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/decks.py | 15 ++------------- app/application.py | 3 ++- app/exceptions.py | 10 +++++++++- app/repositories.py | 4 ++-- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/app/api/decks.py b/app/api/decks.py index 8034399..8110f6f 100644 --- a/app/api/decks.py +++ b/app/api/decks.py @@ -1,9 +1,6 @@ import typing import litestar -from advanced_alchemy.exceptions import NotFoundError -from litestar import status_codes -from litestar.exceptions import HTTPException from litestar.params import FromPath # noqa: TC002 from litestar.plugins.pydantic import PydanticDTO @@ -20,9 +17,6 @@ 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: instance = await decks_repository.fetch_with_cards(deck_id) - if not instance: - raise HTTPException(status_code=status_codes.HTTP_404_NOT_FOUND, detail="Deck is not found") - return schemas.Deck.model_validate(instance) @@ -32,10 +26,7 @@ async def update_deck( data: schemas.DeckCreate, decks_repository: DecksRepository, ) -> schemas.Deck: - try: - instance = await decks_repository.update(data=data.model_dump(), item_id=deck_id) - except NotFoundError: - raise HTTPException(status_code=status_codes.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 schemas.Deck.model_validate(instance) @@ -53,9 +44,7 @@ async def list_cards(deck_id: FromPath[int], cards_repository: CardsRepository) @litestar.get("/cards/{card_id:int}/", return_dto=PydanticDTO[schemas.Card]) async def get_card(card_id: FromPath[int], cards_repository: CardsRepository) -> schemas.Card: - instance = await cards_repository.get_one_or_none(models.Card.id == card_id) - if not instance: - raise HTTPException(status_code=status_codes.HTTP_404_NOT_FOUND, detail="Card is not found") + instance = await cards_repository.get_one(models.Card.id == card_id) return schemas.Card.model_validate(instance) diff --git a/app/application.py b/app/application.py index 6f195f3..28f2dd2 100644 --- a/app/application.py +++ b/app/application.py @@ -3,7 +3,7 @@ import modern_di import modern_di_litestar -from advanced_alchemy.exceptions import DuplicateKeyError +from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError from lite_bootstrap import LitestarBootstrapper from litestar.config.app import AppConfig from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor @@ -25,6 +25,7 @@ def build_app() -> litestar.Litestar: application_config=AppConfig( exception_handlers={ # ty: ignore[invalid-argument-type] DuplicateKeyError: exceptions.duplicate_key_error_handler, + NotFoundError: exceptions.not_found_error_handler, }, route_handlers=[ROUTER], plugins=[modern_di_litestar.ModernDIPlugin(di_container)], diff --git a/app/exceptions.py b/app/exceptions.py index b045de9..8378a37 100644 --- a/app/exceptions.py +++ b/app/exceptions.py @@ -5,7 +5,7 @@ if typing.TYPE_CHECKING: - from advanced_alchemy.exceptions import DuplicateKeyError + from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError def duplicate_key_error_handler(_: object, exc: DuplicateKeyError) -> litestar.Response[dict[str, typing.Any]]: @@ -14,3 +14,11 @@ def duplicate_key_error_handler(_: object, exc: DuplicateKeyError) -> litestar.R content={"detail": exc.detail}, status_code=status_codes.HTTP_400_BAD_REQUEST, ) + + +def not_found_error_handler(_: object, __: NotFoundError) -> litestar.Response[dict[str, typing.Any]]: + return litestar.Response( + media_type=litestar.MediaType.JSON, + content={"detail": "Not found"}, + status_code=status_codes.HTTP_404_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)], )