Skip to content

Commit 05e47cd

Browse files
authored
integrate advanced-alchemy (#25)
1 parent 1ddf5b4 commit 05e47cd

18 files changed

+156
-331
lines changed

app/api/decks.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import typing
22

33
import fastapi
4+
import sqlalchemy
5+
from advanced_alchemy.exceptions import NotFoundError
6+
from sqlalchemy import orm
47
from starlette import status
58
from that_depends.providers import container_context
69

710
from app import ioc, models, schemas
8-
from app.repositories.decks import CardsRepository, DecksRepository
11+
from app.repositories import CardsService, DecksService
912

1013

1114
async def init_di_context() -> typing.AsyncIterator[None]:
@@ -18,18 +21,21 @@ async def init_di_context() -> typing.AsyncIterator[None]:
1821

1922
@ROUTER.get("/decks/")
2023
async def list_decks(
21-
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
24+
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
2225
) -> schemas.Decks:
23-
objects = await decks_repo.all()
26+
objects = await decks_service.list()
2427
return typing.cast(schemas.Decks, {"items": objects})
2528

2629

2730
@ROUTER.get("/decks/{deck_id}/")
2831
async def get_deck(
2932
deck_id: int,
30-
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
33+
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
3134
) -> schemas.Deck:
32-
instance = await decks_repo.get_by_id(deck_id, prefetch=("cards",))
35+
instance = await decks_service.get_one_or_none(
36+
models.Deck.id == deck_id,
37+
statement=sqlalchemy.select(models.Deck).options(orm.selectinload(models.Deck.cards)),
38+
)
3339
if not instance:
3440
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found")
3541

@@ -40,42 +46,40 @@ async def get_deck(
4046
async def update_deck(
4147
deck_id: int,
4248
data: schemas.DeckCreate,
43-
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
49+
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
4450
) -> schemas.Deck:
45-
instance = await decks_repo.get_by_id(deck_id)
46-
if not instance:
47-
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found")
51+
try:
52+
instance = await decks_service.update(data=data.model_dump(), item_id=deck_id)
53+
except NotFoundError:
54+
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found") from None
4855

49-
await decks_repo.update_attrs(instance, **data.model_dump())
50-
await decks_repo.save(instance)
5156
return typing.cast(schemas.Deck, instance)
5257

5358

5459
@ROUTER.post("/decks/")
5560
async def create_deck(
5661
data: schemas.DeckCreate,
57-
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
62+
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
5863
) -> schemas.Deck:
59-
instance = models.Deck(**data.model_dump())
60-
await decks_repo.save(instance)
64+
instance = await decks_service.create(data)
6165
return typing.cast(schemas.Deck, instance)
6266

6367

6468
@ROUTER.get("/decks/{deck_id}/cards/")
6569
async def list_cards(
6670
deck_id: int,
67-
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
71+
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
6872
) -> schemas.Cards:
69-
objects = await cards_repo.filter({"deck_id": deck_id})
73+
objects = await cards_service.list(models.Card.deck_id == deck_id)
7074
return typing.cast(schemas.Cards, {"items": objects})
7175

7276

7377
@ROUTER.get("/cards/{card_id}/")
7478
async def get_card(
7579
card_id: int,
76-
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
80+
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
7781
) -> schemas.Card:
78-
instance = await cards_repo.get_by_id(card_id)
82+
instance = await cards_service.get_one_or_none(models.Card.id == card_id)
7983
if not instance:
8084
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Card is not found")
8185
return typing.cast(schemas.Card, instance)
@@ -85,10 +89,10 @@ async def get_card(
8589
async def create_cards(
8690
deck_id: int,
8791
data: list[schemas.CardCreate],
88-
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
92+
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
8993
) -> schemas.Cards:
90-
objects = await cards_repo.bulk_create(
91-
[models.Card(**card.model_dump(), deck_id=deck_id) for card in data],
94+
objects = await cards_service.create_many(
95+
data=[models.Card(**card.model_dump(), deck_id=deck_id) for card in data],
9296
)
9397
return typing.cast(schemas.Cards, {"items": objects})
9498

@@ -97,9 +101,9 @@ async def create_cards(
97101
async def update_cards(
98102
deck_id: int,
99103
data: list[schemas.Card],
100-
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
104+
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
101105
) -> schemas.Cards:
102-
objects = await cards_repo.bulk_update(
103-
[models.Card(**card.model_dump(exclude={"deck_id"}), deck_id=deck_id) for card in data],
106+
objects = await cards_service.upsert_many(
107+
data=[models.Card(**card.model_dump(exclude={"deck_id"}), deck_id=deck_id) for card in data],
104108
)
105109
return typing.cast(schemas.Cards, {"items": objects})

app/application.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import typing
33

44
import fastapi
5+
from advanced_alchemy.exceptions import ForeignKeyError
56

67
from app import exceptions, ioc
78
from app.api.decks import ROUTER
8-
from app.exceptions import DatabaseValidationError
99

1010

1111
def include_routers(app: fastapi.FastAPI) -> None:
@@ -22,7 +22,7 @@ def __init__(self) -> None:
2222
)
2323
include_routers(self.app)
2424
self.app.add_exception_handler(
25-
DatabaseValidationError,
25+
ForeignKeyError,
2626
exceptions.database_validation_exception_handler, # type: ignore[arg-type]
2727
)
2828

app/db/helpers.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

app/exceptions.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,20 @@
1+
from advanced_alchemy.exceptions import ForeignKeyError
12
from fastapi.exception_handlers import request_validation_exception_handler
23
from fastapi.exceptions import RequestValidationError
34
from fastapi.responses import JSONResponse
45
from starlette.requests import Request
56

67

7-
class DatabaseError(Exception):
8-
pass
9-
10-
11-
class DatabaseValidationError(DatabaseError):
12-
def __init__(self, message: str, field: str | None = None) -> None:
13-
self.message = message
14-
self.field = field
15-
16-
17-
async def database_validation_exception_handler(request: Request, exc: DatabaseValidationError) -> JSONResponse:
8+
async def database_validation_exception_handler(request: Request, exc: ForeignKeyError) -> JSONResponse:
189
validation_error = RequestValidationError(
1910
[
2011
{
21-
"loc": [exc.field or "__root__"],
22-
"msg": exc.message,
12+
"loc": ["__root__"],
13+
"msg": exc.detail,
2314
"input": {},
24-
"ctx": {"error": exc.message},
15+
"ctx": {"error": exc.detail},
2516
},
2617
],
27-
body=exc.message,
18+
body=exc.detail,
2819
)
2920
return await request_validation_exception_handler(request, validation_error)

app/helpers/__init__.py

Whitespace-only changes.

app/helpers/datetime.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

app/ioc.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from that_depends import BaseContainer, providers
22

3+
from app import repositories
34
from app.db.resource import create_sa_engine, create_session
4-
from app.repositories.decks import CardsRepository, DecksRepository
55
from app.settings import Settings
66

77

@@ -11,5 +11,5 @@ class IOCContainer(BaseContainer):
1111
database_engine = providers.Resource(create_sa_engine, settings=settings.cast)
1212
session = providers.ContextResource(create_session, engine=database_engine.cast)
1313

14-
decks_repo = providers.Factory(DecksRepository, session=session)
15-
cards_repo = providers.Factory(CardsRepository, session=session)
14+
decks_service = providers.Factory(repositories.DecksService, session=session)
15+
cards_service = providers.Factory(repositories.CardsService, session=session)

app/models.py

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,23 @@
1-
import datetime
2-
import logging
31
import typing
42

53
import sqlalchemy as sa
4+
from advanced_alchemy.base import BigIntAuditBase
65
from sqlalchemy import orm
76

8-
from app.helpers.datetime import generate_utc_dt
9-
10-
11-
logger = logging.getLogger(__name__)
12-
137

148
METADATA: typing.Final = sa.MetaData()
9+
orm.DeclarativeBase.metadata = METADATA
1510

1611

17-
class Base(orm.DeclarativeBase):
18-
metadata = METADATA
19-
20-
21-
class BaseModel(Base):
22-
__abstract__ = True
23-
24-
id: orm.Mapped[typing.Annotated[int, orm.mapped_column(primary_key=True)]]
25-
created_at: orm.Mapped[
26-
typing.Annotated[
27-
datetime.datetime,
28-
orm.mapped_column(sa.DateTime(timezone=True), default=generate_utc_dt, nullable=False),
29-
]
30-
]
31-
updated_at: orm.Mapped[
32-
typing.Annotated[
33-
datetime.datetime,
34-
orm.mapped_column(sa.DateTime(timezone=True), default=generate_utc_dt, nullable=False),
35-
]
36-
]
37-
38-
def __str__(self) -> str:
39-
return f"<{type(self).__name__}({self.id=})>"
40-
41-
42-
class Deck(BaseModel):
12+
class Deck(BigIntAuditBase):
4313
__tablename__ = "decks"
4414

4515
name: orm.Mapped[str] = orm.mapped_column(sa.String, nullable=False)
4616
description: orm.Mapped[str | None] = orm.mapped_column(sa.String, nullable=True)
4717
cards: orm.Mapped[list["Card"]] = orm.relationship("Card", lazy="noload", uselist=True)
4818

4919

50-
class Card(BaseModel):
20+
class Card(BigIntAuditBase):
5121
__tablename__ = "cards"
5222
__table_args__ = (sa.UniqueConstraint("deck_id", "front", name="card_deck_id_front_uc"),)
5323

app/repositories.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from advanced_alchemy.repository import SQLAlchemyAsyncRepository
2+
from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService
3+
4+
from app import models
5+
6+
7+
class DecksRepository(SQLAlchemyAsyncRepository[models.Deck]):
8+
model_type = models.Deck
9+
10+
11+
class DecksService(SQLAlchemyAsyncRepositoryService[models.Deck]):
12+
repository_type = DecksRepository
13+
14+
15+
class CardsRepository(SQLAlchemyAsyncRepository[models.Card]):
16+
model_type = models.Card
17+
18+
19+
class CardsService(SQLAlchemyAsyncRepositoryService[models.Card]):
20+
repository_type = CardsRepository

app/repositories/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)