From f73c722e01368cc9c179ebef86e3eb3a8e611719 Mon Sep 17 00:00:00 2001 From: Sergey Vanyushkin Date: Tue, 20 Aug 2024 22:35:48 +0000 Subject: [PATCH] ADD session deps, add usecase --- src/fastfood_two/app/dependencies.py | 12 ++++++ .../{dto => app/depends}/__init__.py | 0 src/fastfood_two/app/depends/session.py | 37 +++++++++++++++++ src/fastfood_two/common/interactor.py | 9 ++++ src/fastfood_two/common/stub.py | 41 +++++++++++++++++++ src/fastfood_two/contracts/__init__.py | 0 src/fastfood_two/contracts/menu_contracts.py | 14 +++++++ .../storage/pg_storage/migrations/env.py | 4 +- .../storage/pg_storage/models/__init__.py | 14 +++++++ .../storage/pg_storage/models/base.py | 5 +++ .../storage/pg_storage/models/common_attrs.py | 16 ++++++++ .../storage/pg_storage/models/dish_model.py | 17 ++++++++ .../storage/pg_storage/models/menu_model.py | 18 ++++++++ .../pg_storage/models/submenu_model.py | 22 ++++++++++ src/fastfood_two/usecases/__init__.py | 0 .../usecases/menu/get_all_menus.py | 8 ++++ 16 files changed, 216 insertions(+), 1 deletion(-) rename src/fastfood_two/{dto => app/depends}/__init__.py (100%) create mode 100644 src/fastfood_two/app/depends/session.py create mode 100644 src/fastfood_two/common/interactor.py create mode 100644 src/fastfood_two/common/stub.py create mode 100644 src/fastfood_two/contracts/__init__.py create mode 100644 src/fastfood_two/contracts/menu_contracts.py create mode 100644 src/fastfood_two/storage/pg_storage/models/common_attrs.py create mode 100644 src/fastfood_two/storage/pg_storage/models/dish_model.py create mode 100644 src/fastfood_two/storage/pg_storage/models/menu_model.py create mode 100644 src/fastfood_two/storage/pg_storage/models/submenu_model.py create mode 100644 src/fastfood_two/usecases/__init__.py create mode 100644 src/fastfood_two/usecases/menu/get_all_menus.py diff --git a/src/fastfood_two/app/dependencies.py b/src/fastfood_two/app/dependencies.py index 6d951c4..6dcb3c5 100644 --- a/src/fastfood_two/app/dependencies.py +++ b/src/fastfood_two/app/dependencies.py @@ -1,7 +1,13 @@ import logging from fastapi import FastAPI +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker +from fastfood_two.app.depends.session import ( + create_engine, + create_session_maker, + get_session, +) from fastfood_two.common.logger import configure_logger logger = logging.getLogger(__name__) @@ -11,4 +17,10 @@ configure_logger(level=logging.INFO) def init_dependencies(app: FastAPI) -> None: """Initialize FastAPI dependencies.""" + # app.dependency_overrides[Settings] = get_settings + + app.dependency_overrides[AsyncEngine] = create_engine + app.dependency_overrides[async_sessionmaker[AsyncSession]] = create_session_maker + app.dependency_overrides[AsyncSession] = get_session + logger.info("Dependencies initialized") diff --git a/src/fastfood_two/dto/__init__.py b/src/fastfood_two/app/depends/__init__.py similarity index 100% rename from src/fastfood_two/dto/__init__.py rename to src/fastfood_two/app/depends/__init__.py diff --git a/src/fastfood_two/app/depends/session.py b/src/fastfood_two/app/depends/session.py new file mode 100644 index 0000000..eca21e0 --- /dev/null +++ b/src/fastfood_two/app/depends/session.py @@ -0,0 +1,37 @@ +from collections.abc import AsyncIterable +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) + +from fastfood_two.common.stub import Stub +from fastfood_two.config import Config + + +@lru_cache() +def create_engine( + config: Annotated[Config, Depends(Stub(Config))], +) -> AsyncEngine: + return create_async_engine(config.db.url) + + +def create_session_maker( + engine: Annotated[AsyncEngine, Depends(Stub(AsyncEngine))], +) -> async_sessionmaker[AsyncSession]: + return async_sessionmaker(engine, expire_on_commit=False) + + +async def get_session( + session_maker: Annotated[ + async_sessionmaker[AsyncSession], + Depends(Stub(async_sessionmaker[AsyncSession])), + ], +) -> AsyncIterable[AsyncSession]: + async with session_maker() as session: + yield session diff --git a/src/fastfood_two/common/interactor.py b/src/fastfood_two/common/interactor.py new file mode 100644 index 0000000..b96a1bf --- /dev/null +++ b/src/fastfood_two/common/interactor.py @@ -0,0 +1,9 @@ +from typing import Generic, Protocol, TypeVar + +Request = TypeVar("Request") +Response = TypeVar("Response") + + +class Interactor(Generic[Request, Response], Protocol): # type: ignore + async def __call__(self, request: Request) -> Response: + raise NotImplementedError diff --git a/src/fastfood_two/common/stub.py b/src/fastfood_two/common/stub.py new file mode 100644 index 0000000..8af7a5d --- /dev/null +++ b/src/fastfood_two/common/stub.py @@ -0,0 +1,41 @@ +from collections.abc import Callable + + +class Stub: + """ + This class is used to prevent fastapi from digging into + real dependencies attributes detecting them as request data + + So instead of + `interactor: Annotated[Interactor, Depends()]` + Write + `interactor: Annotated[Interactor, Depends(Stub(Interactor))]` + + And then you can declare how to create it: + `app.dependency_overrids[Interactor] = some_real_factory` + + """ + + def __init__(self, dependency: Callable, **kwargs): + self._dependency = dependency + self._kwargs = kwargs + + def __call__(self): + raise NotImplementedError + + def __eq__(self, other) -> bool: + if isinstance(other, Stub): + return self._dependency == other._dependency and self._kwargs == other._kwargs + else: + if not self._kwargs: + return self._dependency == other + return False + + def __hash__(self): + if not self._kwargs: + return hash(self._dependency) + serial = ( + self._dependency, + *self._kwargs.items(), + ) + return hash(serial) diff --git a/src/fastfood_two/contracts/__init__.py b/src/fastfood_two/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastfood_two/contracts/menu_contracts.py b/src/fastfood_two/contracts/menu_contracts.py new file mode 100644 index 0000000..ae3a6fc --- /dev/null +++ b/src/fastfood_two/contracts/menu_contracts.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from uuid import UUID + + +@dataclass(frozen=True) +class MenuResponse: + id: UUID + name: str + description: str + + +@dataclass(frozen=True) +class MenusResponse: + menus: list[MenuResponse] diff --git a/src/fastfood_two/storage/pg_storage/migrations/env.py b/src/fastfood_two/storage/pg_storage/migrations/env.py index 9dd6c6c..c0dd2a5 100644 --- a/src/fastfood_two/storage/pg_storage/migrations/env.py +++ b/src/fastfood_two/storage/pg_storage/migrations/env.py @@ -3,6 +3,8 @@ from logging.config import fileConfig from alembic import context from sqlalchemy import engine_from_config, pool +from fastfood_two.storage.pg_storage.models import Base + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config @@ -16,7 +18,7 @@ if config.config_file_name is not None: # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -target_metadata = None +target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: diff --git a/src/fastfood_two/storage/pg_storage/models/__init__.py b/src/fastfood_two/storage/pg_storage/models/__init__.py index e69de29..50b7e0a 100644 --- a/src/fastfood_two/storage/pg_storage/models/__init__.py +++ b/src/fastfood_two/storage/pg_storage/models/__init__.py @@ -0,0 +1,14 @@ +from .base import Base +from .menu_model import Menu +from .submenu_model import SubMenu +from .dish_model import Dish +from .common_attrs import uuidpk, str_25 + +__all__ = [ + "Base", + "Menu", + "SubMenu", + "Dish", + "uuidpk", + "str_25", +] diff --git a/src/fastfood_two/storage/pg_storage/models/base.py b/src/fastfood_two/storage/pg_storage/models/base.py index e69de29..fa2b68a 100644 --- a/src/fastfood_two/storage/pg_storage/models/base.py +++ b/src/fastfood_two/storage/pg_storage/models/base.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass diff --git a/src/fastfood_two/storage/pg_storage/models/common_attrs.py b/src/fastfood_two/storage/pg_storage/models/common_attrs.py new file mode 100644 index 0000000..45bfe6d --- /dev/null +++ b/src/fastfood_two/storage/pg_storage/models/common_attrs.py @@ -0,0 +1,16 @@ +import uuid +from typing import Annotated + +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import mapped_column + +uuidpk = Annotated[ + uuid.UUID, + mapped_column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + ), +] + +str_25 = Annotated[str, 25] diff --git a/src/fastfood_two/storage/pg_storage/models/dish_model.py b/src/fastfood_two/storage/pg_storage/models/dish_model.py new file mode 100644 index 0000000..844a69c --- /dev/null +++ b/src/fastfood_two/storage/pg_storage/models/dish_model.py @@ -0,0 +1,17 @@ +import uuid + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column + +from . import Base, str_25, uuidpk + + +class Dish(Base): + __tablename__ = "dish" + + id: Mapped[uuidpk] + title: Mapped[str_25] + description: Mapped[str | None] + price: Mapped[float] + + parent_submenu: Mapped[uuid.UUID] = mapped_column(ForeignKey("submenu.id", ondelete="CASCADE")) diff --git a/src/fastfood_two/storage/pg_storage/models/menu_model.py b/src/fastfood_two/storage/pg_storage/models/menu_model.py new file mode 100644 index 0000000..cdbd4f4 --- /dev/null +++ b/src/fastfood_two/storage/pg_storage/models/menu_model.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import Mapped, relationship + +from . import Base, str_25, uuidpk + + +class Menu(Base): + __tablename__ = "menu" + + id: Mapped[uuidpk] + title: Mapped[str_25] + description: Mapped[str | None] + + submenus: Mapped[list["SubMenu"]] = relationship( + "SubMenu", + backref="menu", + lazy="selectin", + cascade="all, delete", + ) diff --git a/src/fastfood_two/storage/pg_storage/models/submenu_model.py b/src/fastfood_two/storage/pg_storage/models/submenu_model.py new file mode 100644 index 0000000..6313259 --- /dev/null +++ b/src/fastfood_two/storage/pg_storage/models/submenu_model.py @@ -0,0 +1,22 @@ +import uuid + +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm.properties import ForeignKey + +from . import Base, str_25, uuidpk + + +class SubMenu(Base): + __tablename__ = "submenu" + + id: Mapped[uuidpk] + title: Mapped[str_25] + description: Mapped[str | None] + + parent_menu: Mapped[uuid.UUID] = mapped_column(ForeignKey("menu.id", ondelete="CASCADE")) + dishes: Mapped[list["Dish"]] = relationship( + "Dish", + backref="submenu", + lazy="selectin", + cascade="all, delete", + ) diff --git a/src/fastfood_two/usecases/__init__.py b/src/fastfood_two/usecases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastfood_two/usecases/menu/get_all_menus.py b/src/fastfood_two/usecases/menu/get_all_menus.py new file mode 100644 index 0000000..1c8d540 --- /dev/null +++ b/src/fastfood_two/usecases/menu/get_all_menus.py @@ -0,0 +1,8 @@ +from fastfood_two.common.interactor import Interactor +from fastfood_two.contracts.menu_contracts import MenusResponse + + +class GetAllMenus(Interactor[None, MenusResponse]): + def __init__(self) -> None: ... + + async def __call__(self, request=None) -> MenusResponse: ...