Skip to content

Commit 0eb97b5

Browse files
authored
integrate advanced alchemy (#7)
1 parent 2f8d015 commit 0eb97b5

File tree

18 files changed

+188
-369
lines changed

18 files changed

+188
-369
lines changed

app/api/decks.py

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

33
import litestar
4+
from advanced_alchemy.exceptions import NotFoundError
45
from litestar import status_codes
56
from litestar.contrib.pydantic import PydanticDTO
67
from litestar.exceptions import HTTPException
8+
from sqlalchemy import orm
79
from that_depends.providers import container_context
810

911
from app import ioc, models, schemas
1012

1113

14+
if typing.TYPE_CHECKING:
15+
from app.repositories import CardsService, DecksService
16+
17+
1218
@litestar.get("/decks/")
1319
@container_context()
1420
async def list_decks() -> schemas.Decks:
15-
decks_repo = await ioc.IOCContainer.decks_repo()
16-
objects = await decks_repo.all()
21+
decks_service: DecksService = await ioc.IOCContainer.decks_service()
22+
objects = await decks_service.list()
1723
return schemas.Decks(items=objects) # type: ignore[arg-type]
1824

1925

2026
@litestar.get("/decks/{deck_id:int}/")
2127
@container_context()
2228
async def get_deck(deck_id: int) -> schemas.Deck:
23-
decks_repo = await ioc.IOCContainer.decks_repo()
24-
instance = await decks_repo.get_by_id(deck_id, prefetch=("cards",))
29+
decks_service: DecksService = await ioc.IOCContainer.decks_service()
30+
instance = await decks_service.get_one_or_none(
31+
models.Deck.id == deck_id,
32+
load=[orm.selectinload(models.Deck.cards)],
33+
)
2534
if not instance:
2635
raise HTTPException(status_code=status_codes.HTTP_404_NOT_FOUND, detail="Deck is not found")
2736

@@ -34,38 +43,35 @@ async def update_deck(
3443
deck_id: int,
3544
data: schemas.DeckCreate,
3645
) -> schemas.Deck:
37-
decks_repo = await ioc.IOCContainer.decks_repo()
38-
instance = await decks_repo.get_by_id(deck_id)
39-
if not instance:
40-
raise HTTPException(status_code=status_codes.HTTP_404_NOT_FOUND, detail="Deck is not found")
41-
42-
await decks_repo.update_attrs(instance, **data.model_dump())
43-
await decks_repo.save(instance)
46+
decks_service: DecksService = await ioc.IOCContainer.decks_service()
47+
try:
48+
instance = await decks_service.update(data=data.model_dump(), item_id=deck_id)
49+
except NotFoundError:
50+
raise HTTPException(status_code=status_codes.HTTP_404_NOT_FOUND, detail="Deck is not found") from None
4451
return schemas.Deck.model_validate(instance)
4552

4653

4754
@litestar.post("/decks/")
4855
@container_context()
4956
async def create_deck(data: schemas.DeckCreate) -> schemas.Deck:
50-
decks_repo = await ioc.IOCContainer.decks_repo()
51-
instance = models.Deck(**data.model_dump())
52-
await decks_repo.save(instance)
57+
decks_service: DecksService = await ioc.IOCContainer.decks_service()
58+
instance = await decks_service.create(data)
5359
return schemas.Deck.model_validate(instance)
5460

5561

5662
@litestar.get("/decks/{deck_id:int}/cards/")
5763
@container_context()
5864
async def list_cards(deck_id: int) -> schemas.Cards:
59-
cards_repo = await ioc.IOCContainer.cards_repo()
60-
objects = await cards_repo.filter({"deck_id": deck_id})
65+
cards_service: CardsService = await ioc.IOCContainer.cards_service()
66+
objects = await cards_service.list(models.Card.deck_id == deck_id)
6167
return schemas.Cards(items=objects) # type: ignore[arg-type]
6268

6369

6470
@litestar.get("/cards/{card_id:int}/", return_dto=PydanticDTO[schemas.Card])
6571
@container_context()
6672
async def get_card(card_id: int) -> schemas.Card:
67-
cards_repo = await ioc.IOCContainer.cards_repo()
68-
instance = await cards_repo.get_by_id(card_id)
73+
cards_service: CardsService = await ioc.IOCContainer.cards_service()
74+
instance = await cards_service.get_one_or_none(models.Card.id == card_id)
6975
if not instance:
7076
raise HTTPException(status_code=status_codes.HTTP_404_NOT_FOUND, detail="Card is not found")
7177
return schemas.Card.model_validate(instance)
@@ -77,9 +83,9 @@ async def create_cards(
7783
deck_id: int,
7884
data: list[schemas.CardCreate],
7985
) -> schemas.Cards:
80-
cards_repo = await ioc.IOCContainer.cards_repo()
81-
objects = await cards_repo.bulk_create(
82-
[models.Card(**card.model_dump(), deck_id=deck_id) for card in data],
86+
cards_service: CardsService = await ioc.IOCContainer.cards_service()
87+
objects = await cards_service.create_many(
88+
data=[models.Card(**card.model_dump(), deck_id=deck_id) for card in data],
8389
)
8490
return schemas.Cards(items=objects) # type: ignore[arg-type]
8591

@@ -90,9 +96,9 @@ async def update_cards(
9096
deck_id: int,
9197
data: list[schemas.Card],
9298
) -> schemas.Cards:
93-
cards_repo = await ioc.IOCContainer.cards_repo()
94-
objects = await cards_repo.bulk_update(
95-
[models.Card(**card.model_dump(exclude={"deck_id"}), deck_id=deck_id) for card in data],
99+
cards_service: CardsService = await ioc.IOCContainer.cards_service()
100+
objects = await cards_service.upsert_many(
101+
data=[models.Card(**card.model_dump(exclude={"deck_id"}), deck_id=deck_id) for card in data],
96102
)
97103
return schemas.Cards(items=objects) # type: ignore[arg-type]
98104

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 litestar
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
class AppBuilder:
@@ -14,7 +14,7 @@ def __init__(self) -> None:
1414
self.app: litestar.Litestar = litestar.Litestar(
1515
debug=self.settings.debug,
1616
lifespan=[self.lifespan_manager],
17-
exception_handlers={DatabaseValidationError: exceptions.database_validation_exception_handler},
17+
exception_handlers={ForeignKeyError: exceptions.database_validation_exception_handler},
1818
route_handlers=[ROUTER],
1919
)
2020

app/db/helpers.py

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

app/exceptions.py

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

33
import litestar
4+
from advanced_alchemy.exceptions import ForeignKeyError
45
from litestar import status_codes
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-
def database_validation_exception_handler(
18-
_: object, exc: DatabaseValidationError
19-
) -> litestar.Response[dict[str, typing.Any]]:
8+
def database_validation_exception_handler(_: object, exc: ForeignKeyError) -> litestar.Response[dict[str, typing.Any]]:
209
return litestar.Response(
2110
media_type=litestar.MediaType.JSON,
2211
content={
2312
"detail": "Database validation failed",
24-
"extra": [{"message": exc.message, "key": exc.field or "__root__"}],
13+
"extra": [{"message": exc.detail, "key": "__root__"}],
2514
},
2615
status_code=status_codes.HTTP_400_BAD_REQUEST,
2716
)

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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from that_depends import BaseContainer, providers
22

3-
from app.db.resource import create_sa_engine, create_session
4-
from app.repositories.decks import CardsRepository, DecksRepository
3+
from app import repositories
4+
from app.resources.db import create_sa_engine, create_session
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)