user repo/usecases/session/di

main
Сергей Ванюшкин 2024-03-31 04:18:41 +03:00
parent f5ecba9c1e
commit 327ab86d1f
26 changed files with 301 additions and 4 deletions

View File

@ -0,0 +1,23 @@
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from api.application.abstractions.uow import UnitOfWork
from api.application.usecase.create_user import CreateUser
from api.domain.user.repository import UserRepository
from api.infrastructure.dependencies.adapters import (
create_engine,
create_session_maker,
new_session,
new_unit_of_work,
)
from api.infrastructure.dependencies.repositories import get_user_repository
from api.infrastructure.dependencies.usecases import provide_create_user
def init_dependencies(app: FastAPI) -> None:
app.dependency_overrides[AsyncEngine] = create_engine
app.dependency_overrides[async_sessionmaker[AsyncSession]] = create_session_maker
app.dependency_overrides[AsyncSession] = new_session
app.dependency_overrides[UserRepository] = get_user_repository
app.dependency_overrides[UnitOfWork] = new_unit_of_work
app.dependency_overrides[CreateUser] = provide_create_user

View File

@ -1,9 +1,13 @@
from fastapi import FastAPI
from api.app_builder.dependencies import init_dependencies
from .routers import init_routers
def app_factory() -> FastAPI:
app = FastAPI()
init_dependencies(app)
init_routers(app)
return app

View File

@ -0,0 +1,3 @@
from .uow import UnitOfWork
__all__ = ("UnitOfWork",)

View File

@ -0,0 +1,9 @@
from typing import Protocol
class UnitOfWork(Protocol):
async def commit(self) -> None:
raise NotImplementedError
async def rollback(self) -> None:
raise NotImplementedError

View File

@ -1,3 +1,4 @@
from .user_response import UserResponse
from .user_request import UserCreateRequest
from .user_response import UserDetaledResponse, UserResponse
__all__ = ("UserResponse",)
__all__ = ("UserResponse", "UserDetaledResponse", "UserCreateRequest")

View File

@ -0,0 +1,8 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class UserCreateRequest:
name: str
email: str
password: str

View File

@ -1,7 +1,16 @@
from dataclasses import dataclass
from uuid import UUID
@dataclass(frozen=True)
class UserResponse:
name: str
email: str
@dataclass(frozen=True)
class UserDetaledResponse:
id: UUID
name: str
email: str
hashed_password: str

View File

View File

@ -0,0 +1,15 @@
from api.application.abstractions import UnitOfWork
from api.application.contracts.user.user_request import UserCreateRequest
from api.domain.user.model import User
from api.domain.user.repository import UserRepository
class CreateUser:
def __init__(self, uow: UnitOfWork, user_repository: UserRepository) -> None:
self.uow = uow
self.user_repository = user_repository
async def execute(self, request: UserCreateRequest) -> None:
user = User.create(name=request.name, email=request.email, password=request.password)
await self.user_repository.create_user(user=user)
await self.uow.commit()

3
api/domain/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .error import DomainError
__all__ = ("DomainError",)

4
api/domain/error.py Normal file
View File

@ -0,0 +1,4 @@
class DomainError(Exception):
def __init__(self, message: str, *args: object) -> None:
super().__init__(*args)
self.message = message

View File

@ -0,0 +1,5 @@
from .error import UserNotFoundError, UserValidationError
from .model import User
from .repository import UserRepository
__all__ = ("UserValidationError", "UserNotFoundError", "User", "UserRepository")

9
api/domain/user/error.py Normal file
View File

@ -0,0 +1,9 @@
from api.domain import DomainError
class UserNotFoundError(DomainError):
...
class UserValidationError(DomainError):
...

33
api/domain/user/model.py Normal file
View File

@ -0,0 +1,33 @@
from dataclasses import dataclass
from uuid import UUID, uuid4
from api.domain.user import UserValidationError
@dataclass
class User:
id: UUID
name: str
email: str
password: str
@staticmethod
def create(name: str, email: str, password: str) -> "User":
if not name:
raise UserValidationError("User name cannot be empty")
if not email:
raise UserValidationError("User email cannot be empty")
if len(name) > 50:
raise UserValidationError("User name cannot be longer than 50 characters")
if len(email) > 30:
raise UserValidationError("User email cannot be longer than 30 characters")
return User(
id=uuid4(),
name=name,
email=email,
password=password,
)

View File

@ -0,0 +1,14 @@
from typing import Protocol
from api.domain.user.model import User
class UserRepository(Protocol):
async def get_user(self, filter: dict) -> User | None:
raise NotImplementedError
async def create_user(self, user: User) -> None:
raise NotImplementedError
async def get_users(self) -> list[User] | None:
raise NotImplementedError

View File

View File

@ -0,0 +1,42 @@
from collections.abc import AsyncIterable
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from api.application.abstractions import UnitOfWork
from api.infrastructure.dependencies.stub import Stub
from api.infrastructure.persistence.uow import SqlAlchemyUnitOfWork
def new_unit_of_work(
session: Annotated[AsyncSession, Depends(Stub(AsyncSession))],
) -> UnitOfWork:
return SqlAlchemyUnitOfWork(session)
def create_engine() -> AsyncEngine:
return create_async_engine("postgresql+asyncpg://postgresql+asyncpg//demo_user:user_pass@db:5432/serviceman_db")
def create_session_maker(
engine: Annotated[AsyncEngine, Depends(Stub(AsyncEngine))],
) -> async_sessionmaker[AsyncSession]:
maker = async_sessionmaker(engine, expire_on_commit=False)
print("session_maker id:", id(maker))
return maker
async def new_session(
session_maker: Annotated[
async_sessionmaker[AsyncSession],
Depends(Stub(async_sessionmaker[AsyncSession])),
],
) -> AsyncIterable[AsyncSession]:
async with session_maker() as session:
yield session

View File

@ -0,0 +1,17 @@
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from api.domain.user import UserRepository
from api.infrastructure.persistence.repositories.user_repository import (
SqlAlchemyUserRepository,
)
from .stub import Stub
def get_user_repository(
session: Annotated[AsyncSession, Depends(Stub(AsyncSession))],
) -> UserRepository:
return SqlAlchemyUserRepository(session)

View File

@ -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)

View File

@ -0,0 +1,14 @@
from typing import Annotated
from fastapi import Depends
from api.application.abstractions.uow import UnitOfWork
from api.application.usecase.create_user import CreateUser
from api.domain.user.repository import UserRepository
def provide_create_user(
user_repository: Annotated[UserRepository, Depends()],
uow: Annotated[UnitOfWork, Depends()],
) -> CreateUser:
return CreateUser(uow=uow, user_repository=user_repository)

View File

@ -0,0 +1,17 @@
from sqlalchemy.ext.asyncio import AsyncSession
from api.domain.user import User, UserRepository
class SqlAlchemyUserRepository(UserRepository):
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def create_user(self, user: User) -> None:
pass
async def get_user(self, filter: dict) -> User | None:
pass
async def get_users(self) -> list[User]:
return []

View File

@ -0,0 +1,14 @@
from sqlalchemy.ext.asyncio import AsyncSession
from api.application.abstractions import UnitOfWork
class SqlAlchemyUnitOfWork(UnitOfWork):
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def commit(self) -> None:
await self.session.commit()
async def rollback(self) -> None:
await self.session.rollback()

View File

@ -1,6 +1,10 @@
from fastapi import APIRouter
from typing import Annotated
from api.application.contracts.user import UserResponse
from fastapi import APIRouter, Depends
from api.application.contracts.user import UserCreateRequest, UserResponse
from api.application.usecase.create_user import CreateUser
from api.infrastructure.dependencies.stub import Stub
user_router = APIRouter(prefix="/users", tags=["Users"])
@ -8,3 +12,11 @@ user_router = APIRouter(prefix="/users", tags=["Users"])
@user_router.get("/")
async def get_all_users() -> list[UserResponse]:
return []
@user_router.post("/")
async def create_task(
request: UserCreateRequest,
usecase: Annotated[CreateUser, Depends(Stub(CreateUser))],
) -> None:
return await usecase.execute(request)