user repo/usecases/session/di
parent
f5ecba9c1e
commit
327ab86d1f
|
@ -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
|
|
@ -1,9 +1,13 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from api.app_builder.dependencies import init_dependencies
|
||||||
|
|
||||||
from .routers import init_routers
|
from .routers import init_routers
|
||||||
|
|
||||||
|
|
||||||
def app_factory() -> FastAPI:
|
def app_factory() -> FastAPI:
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
init_dependencies(app)
|
||||||
init_routers(app)
|
init_routers(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .uow import UnitOfWork
|
||||||
|
|
||||||
|
__all__ = ("UnitOfWork",)
|
|
@ -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
|
|
@ -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")
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class UserCreateRequest:
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
password: str
|
|
@ -1,7 +1,16 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class UserResponse:
|
class UserResponse:
|
||||||
name: str
|
name: str
|
||||||
email: str
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class UserDetaledResponse:
|
||||||
|
id: UUID
|
||||||
|
name: str
|
||||||
|
email: str
|
||||||
|
hashed_password: str
|
||||||
|
|
|
@ -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()
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .error import DomainError
|
||||||
|
|
||||||
|
__all__ = ("DomainError",)
|
|
@ -0,0 +1,4 @@
|
||||||
|
class DomainError(Exception):
|
||||||
|
def __init__(self, message: str, *args: object) -> None:
|
||||||
|
super().__init__(*args)
|
||||||
|
self.message = message
|
|
@ -0,0 +1,5 @@
|
||||||
|
from .error import UserNotFoundError, UserValidationError
|
||||||
|
from .model import User
|
||||||
|
from .repository import UserRepository
|
||||||
|
|
||||||
|
__all__ = ("UserValidationError", "UserNotFoundError", "User", "UserRepository")
|
|
@ -0,0 +1,9 @@
|
||||||
|
from api.domain import DomainError
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(DomainError):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class UserValidationError(DomainError):
|
||||||
|
...
|
|
@ -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,
|
||||||
|
)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 []
|
|
@ -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()
|
|
@ -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"])
|
user_router = APIRouter(prefix="/users", tags=["Users"])
|
||||||
|
|
||||||
|
@ -8,3 +12,11 @@ user_router = APIRouter(prefix="/users", tags=["Users"])
|
||||||
@user_router.get("/")
|
@user_router.get("/")
|
||||||
async def get_all_users() -> list[UserResponse]:
|
async def get_all_users() -> list[UserResponse]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.post("/")
|
||||||
|
async def create_task(
|
||||||
|
request: UserCreateRequest,
|
||||||
|
usecase: Annotated[CreateUser, Depends(Stub(CreateUser))],
|
||||||
|
) -> None:
|
||||||
|
return await usecase.execute(request)
|
||||||
|
|
Loading…
Reference in New Issue