Skip to content

Commit 1356e3c

Browse files
lesnik512claude
andauthored
refactor: collapse not-found handling into one seam (#52)
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) <noreply@anthropic.com>
1 parent 9c49356 commit 1356e3c

4 files changed

Lines changed: 17 additions & 17 deletions

File tree

app/api/decks.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import typing
22

33
import fastapi
4-
from advanced_alchemy.exceptions import NotFoundError
54
from modern_di_fastapi import FromDI
6-
from starlette import status
75

86
from app import models, schemas
97
from app.repositories import CardsRepository, DecksRepository
@@ -26,9 +24,6 @@ async def get_deck(
2624
decks_repository: DecksRepository = FromDI(DecksRepository),
2725
) -> schemas.Deck:
2826
instance = await decks_repository.fetch_with_cards(deck_id)
29-
if not instance:
30-
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found")
31-
3227
return typing.cast("schemas.Deck", instance)
3328

3429

@@ -38,11 +33,7 @@ async def update_deck(
3833
data: schemas.DeckCreate,
3934
decks_repository: DecksRepository = FromDI(DecksRepository),
4035
) -> schemas.Deck:
41-
try:
42-
instance = await decks_repository.update(data=data.model_dump(), item_id=deck_id)
43-
except NotFoundError:
44-
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found") from None
45-
36+
instance = await decks_repository.update(data=data.model_dump(), item_id=deck_id)
4637
return typing.cast("schemas.Deck", instance)
4738

4839

@@ -69,9 +60,7 @@ async def get_card(
6960
card_id: int,
7061
cards_repository: CardsRepository = FromDI(CardsRepository),
7162
) -> schemas.Card:
72-
instance = await cards_repository.get_one_or_none(models.Card.id == card_id)
73-
if not instance:
74-
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card is not found")
63+
instance = await cards_repository.get_one(models.Card.id == card_id)
7564
return typing.cast("schemas.Card", instance)
7665

7766

app/application.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import modern_di
55
import modern_di_fastapi
6-
from advanced_alchemy.exceptions import DuplicateKeyError
6+
from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError
77
from lite_bootstrap import FastAPIBootstrapper
88
from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
99
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
@@ -38,4 +38,8 @@ def build_app() -> fastapi.FastAPI:
3838
DuplicateKeyError,
3939
exceptions.duplicate_key_error_handler, # ty: ignore[invalid-argument-type]
4040
)
41+
app.add_exception_handler(
42+
NotFoundError,
43+
exceptions.not_found_error_handler, # ty: ignore[invalid-argument-type]
44+
)
4145
return app

app/exceptions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
if TYPE_CHECKING:
8-
from advanced_alchemy.exceptions import DuplicateKeyError
8+
from advanced_alchemy.exceptions import DuplicateKeyError, NotFoundError
99
from starlette.requests import Request
1010

1111

@@ -14,3 +14,10 @@ async def duplicate_key_error_handler(_: Request, exc: DuplicateKeyError) -> JSO
1414
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
1515
content={"detail": exc.detail},
1616
)
17+
18+
19+
async def not_found_error_handler(_: Request, __: NotFoundError) -> JSONResponse:
20+
return JSONResponse(
21+
status_code=status.HTTP_404_NOT_FOUND,
22+
content={"detail": "Not found"},
23+
)

app/repositories.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class BaseRepository(SQLAlchemyAsyncRepository[models.Deck]):
1717

1818
repository_type = BaseRepository
1919

20-
async def fetch_with_cards(self, deck_id: int) -> models.Deck | None:
21-
return await self.get_one_or_none(
20+
async def fetch_with_cards(self, deck_id: int) -> models.Deck:
21+
return await self.get_one(
2222
models.Deck.id == deck_id,
2323
load=[orm.selectinload(models.Deck.cards)],
2424
)

0 commit comments

Comments
 (0)