Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 28 additions & 24 deletions app/api/decks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import typing

import fastapi
import sqlalchemy
from advanced_alchemy.exceptions import NotFoundError
from sqlalchemy import orm
from starlette import status
from that_depends.providers import container_context

from app import ioc, models, schemas
from app.repositories.decks import CardsRepository, DecksRepository
from app.repositories import CardsService, DecksService


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

@ROUTER.get("/decks/")
async def list_decks(
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
) -> schemas.Decks:
objects = await decks_repo.all()
objects = await decks_service.list()
return typing.cast(schemas.Decks, {"items": objects})


@ROUTER.get("/decks/{deck_id}/")
async def get_deck(
deck_id: int,
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
) -> schemas.Deck:
instance = await decks_repo.get_by_id(deck_id, prefetch=("cards",))
instance = await decks_service.get_one_or_none(
models.Deck.id == deck_id,
statement=sqlalchemy.select(models.Deck).options(orm.selectinload(models.Deck.cards)),
)
if not instance:
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found")

Expand All @@ -40,42 +46,40 @@ async def get_deck(
async def update_deck(
deck_id: int,
data: schemas.DeckCreate,
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
) -> schemas.Deck:
instance = await decks_repo.get_by_id(deck_id)
if not instance:
raise fastapi.HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Deck is not found")
try:
instance = await decks_service.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

await decks_repo.update_attrs(instance, **data.model_dump())
await decks_repo.save(instance)
return typing.cast(schemas.Deck, instance)


@ROUTER.post("/decks/")
async def create_deck(
data: schemas.DeckCreate,
decks_repo: DecksRepository = fastapi.Depends(ioc.IOCContainer.decks_repo),
decks_service: DecksService = fastapi.Depends(ioc.IOCContainer.decks_service),
) -> schemas.Deck:
instance = models.Deck(**data.model_dump())
await decks_repo.save(instance)
instance = await decks_service.create(data)
return typing.cast(schemas.Deck, instance)


@ROUTER.get("/decks/{deck_id}/cards/")
async def list_cards(
deck_id: int,
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
) -> schemas.Cards:
objects = await cards_repo.filter({"deck_id": deck_id})
objects = await cards_service.list(models.Card.deck_id == deck_id)
return typing.cast(schemas.Cards, {"items": objects})


@ROUTER.get("/cards/{card_id}/")
async def get_card(
card_id: int,
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
) -> schemas.Card:
instance = await cards_repo.get_by_id(card_id)
instance = await cards_service.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")
return typing.cast(schemas.Card, instance)
Expand All @@ -85,10 +89,10 @@ async def get_card(
async def create_cards(
deck_id: int,
data: list[schemas.CardCreate],
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
) -> schemas.Cards:
objects = await cards_repo.bulk_create(
[models.Card(**card.model_dump(), deck_id=deck_id) for card in data],
objects = await cards_service.create_many(
data=[models.Card(**card.model_dump(), deck_id=deck_id) for card in data],
)
return typing.cast(schemas.Cards, {"items": objects})

Expand All @@ -97,9 +101,9 @@ async def create_cards(
async def update_cards(
deck_id: int,
data: list[schemas.Card],
cards_repo: CardsRepository = fastapi.Depends(ioc.IOCContainer.cards_repo),
cards_service: CardsService = fastapi.Depends(ioc.IOCContainer.cards_service),
) -> schemas.Cards:
objects = await cards_repo.bulk_update(
[models.Card(**card.model_dump(exclude={"deck_id"}), deck_id=deck_id) for card in data],
objects = await cards_service.upsert_many(
data=[models.Card(**card.model_dump(exclude={"deck_id"}), deck_id=deck_id) for card in data],
)
return typing.cast(schemas.Cards, {"items": objects})
4 changes: 2 additions & 2 deletions app/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import typing

import fastapi
from advanced_alchemy.exceptions import ForeignKeyError

from app import exceptions, ioc
from app.api.decks import ROUTER
from app.exceptions import DatabaseValidationError


def include_routers(app: fastapi.FastAPI) -> None:
Expand All @@ -22,7 +22,7 @@ def __init__(self) -> None:
)
include_routers(self.app)
self.app.add_exception_handler(
DatabaseValidationError,
ForeignKeyError,
exceptions.database_validation_exception_handler, # type: ignore[arg-type]
)

Expand Down
25 changes: 0 additions & 25 deletions app/db/helpers.py

This file was deleted.

21 changes: 6 additions & 15 deletions app/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
from advanced_alchemy.exceptions import ForeignKeyError
from fastapi.exception_handlers import request_validation_exception_handler
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.requests import Request


class DatabaseError(Exception):
pass


class DatabaseValidationError(DatabaseError):
def __init__(self, message: str, field: str | None = None) -> None:
self.message = message
self.field = field


async def database_validation_exception_handler(request: Request, exc: DatabaseValidationError) -> JSONResponse:
async def database_validation_exception_handler(request: Request, exc: ForeignKeyError) -> JSONResponse:
validation_error = RequestValidationError(
[
{
"loc": [exc.field or "__root__"],
"msg": exc.message,
"loc": ["__root__"],
"msg": exc.detail,
"input": {},
"ctx": {"error": exc.message},
"ctx": {"error": exc.detail},
},
],
body=exc.message,
body=exc.detail,
)
return await request_validation_exception_handler(request, validation_error)
Empty file removed app/helpers/__init__.py
Empty file.
6 changes: 0 additions & 6 deletions app/helpers/datetime.py

This file was deleted.

6 changes: 3 additions & 3 deletions app/ioc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from that_depends import BaseContainer, providers

from app import repositories
from app.db.resource import create_sa_engine, create_session
from app.repositories.decks import CardsRepository, DecksRepository
from app.settings import Settings


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

decks_repo = providers.Factory(DecksRepository, session=session)
cards_repo = providers.Factory(CardsRepository, session=session)
decks_service = providers.Factory(repositories.DecksService, session=session)
cards_service = providers.Factory(repositories.CardsService, session=session)
38 changes: 4 additions & 34 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,23 @@
import datetime
import logging
import typing

import sqlalchemy as sa
from advanced_alchemy.base import BigIntAuditBase
from sqlalchemy import orm

from app.helpers.datetime import generate_utc_dt


logger = logging.getLogger(__name__)


METADATA: typing.Final = sa.MetaData()
orm.DeclarativeBase.metadata = METADATA


class Base(orm.DeclarativeBase):
metadata = METADATA


class BaseModel(Base):
__abstract__ = True

id: orm.Mapped[typing.Annotated[int, orm.mapped_column(primary_key=True)]]
created_at: orm.Mapped[
typing.Annotated[
datetime.datetime,
orm.mapped_column(sa.DateTime(timezone=True), default=generate_utc_dt, nullable=False),
]
]
updated_at: orm.Mapped[
typing.Annotated[
datetime.datetime,
orm.mapped_column(sa.DateTime(timezone=True), default=generate_utc_dt, nullable=False),
]
]

def __str__(self) -> str:
return f"<{type(self).__name__}({self.id=})>"


class Deck(BaseModel):
class Deck(BigIntAuditBase):
__tablename__ = "decks"

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


class Card(BaseModel):
class Card(BigIntAuditBase):
__tablename__ = "cards"
__table_args__ = (sa.UniqueConstraint("deck_id", "front", name="card_deck_id_front_uc"),)

Expand Down
20 changes: 20 additions & 0 deletions app/repositories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from advanced_alchemy.repository import SQLAlchemyAsyncRepository
from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService

from app import models


class DecksRepository(SQLAlchemyAsyncRepository[models.Deck]):
model_type = models.Deck


class DecksService(SQLAlchemyAsyncRepositoryService[models.Deck]):
repository_type = DecksRepository


class CardsRepository(SQLAlchemyAsyncRepository[models.Card]):
model_type = models.Card


class CardsService(SQLAlchemyAsyncRepositoryService[models.Card]):
repository_type = CardsRepository
Empty file removed app/repositories/__init__.py
Empty file.
Loading
Loading