fast init

This commit is contained in:
ivankirpichnikov
2025-10-16 23:03:50 +03:00
parent b84e0370d6
commit 652da07d12
50 changed files with 1012 additions and 0 deletions
@@ -0,0 +1,17 @@
from abc import abstractmethod
from typing import Protocol
from template_project.application.access_token.entity import AccessTokenId
type RawAccessToken = str
class AccessTokenCryptographer(Protocol):
@abstractmethod
def crypto(self, access_token_id: AccessTokenId) -> RawAccessToken:
raise NotImplementedError
@abstractmethod
def decrypto(self, raw_access_token: RawAccessToken) -> AccessTokenId:
raise NotImplementedError
@@ -0,0 +1,10 @@
from abc import abstractmethod
from typing import Protocol
from template_project.application.access_token.entity import AccessToken, AccessTokenId
class AccessTokenDataGateway(Protocol):
@abstractmethod
async def load_with_id(self, access_token_id: AccessTokenId) -> AccessToken | None:
raise NotImplementedError
@@ -0,0 +1,49 @@
from datetime import UTC, datetime, timedelta
from typing import NewType, Self
from uuid import UUID
from uuid_utils.compat import uuid7
from template_project.application.access_token.errors import AccessTokenExpiredError
from template_project.application.common.entity import Entity, to_entity
from template_project.application.user.entity import UserId
AccessTokenId = NewType("AccessTokenId", UUID)
@to_entity
class AccessToken(Entity[AccessTokenId]):
user_id: UserId
revoked: bool
expires_in: datetime
@classmethod
def factory(
cls,
user_id: UserId,
expires_in: timedelta,
) -> Self:
current_date_time = datetime.now(tz=UTC)
return cls(
id=AccessTokenId(uuid7()),
created_at=current_date_time,
user_id=user_id,
expires_in=current_date_time + expires_in,
revoked=False,
)
def ensure_expired(self) -> None:
if self.expired_predicate():
raise AccessTokenExpiredError(id_=self.id)
def expired_predicate(self) -> bool:
return (
(self.expires_in < datetime.now(tz=UTC))
or self.revoked
or self.deleted_at is not None
)
def revoke(self) -> None:
self.revoked = True
self.deleted_at = datetime.now(tz=UTC)
@@ -0,0 +1,11 @@
from abc import abstractmethod
from typing import Protocol
from template_project.application.access_token.entity import AccessToken
from template_project.application.user.entity import UserId
class AccessTokenFactory(Protocol):
@abstractmethod
def execute(self, user_id: UserId) -> AccessToken:
raise NotImplementedError
@@ -0,0 +1,13 @@
from typing import override
from template_project.application.access_token.entity import AccessTokenId
from template_project.application.common.errors import ApplicationError, to_error
@to_error
class AccessTokenExpiredError(ApplicationError):
id_: AccessTokenId
@override
def __str__(self) -> str:
return f"Access token id={self.id_!r} expried"
@@ -0,0 +1,42 @@
from collections.abc import Container, Hashable, Sized
from typing import Any, Final, override
_SECRET_VALUE: Final = "********" # noqa: S105
class SecretString(Container[bool], Hashable, Sized):
__slots__ = ("_value",)
def __init__(self, value: str) -> None:
self._value = value
@override
def __hash__(self) -> int:
return hash(self._value)
@override
def __len__(self) -> int:
return len(self._value)
@override
def __eq__(self, value: object) -> bool:
if isinstance(value, str):
return self._value == value
return NotImplemented
@override
def __contains__(self, value: object) -> Any:
if isinstance(value, str):
return value in self._value
return NotImplemented
@override
def __str__(self) -> str:
return _SECRET_VALUE
@override
def __repr__(self) -> str:
return f"<{self.__class__.__name__}(value={_SECRET_VALUE!r})>"
def get_value(self) -> str:
return self._value
@@ -0,0 +1,17 @@
from dataclasses import dataclass
from typing import dataclass_transform
@dataclass_transform(
frozen_default=True,
eq_default=False,
kw_only_default=True,
)
def to_data_structure[_InteractorClsT](interactor_cls: type[_InteractorClsT]) -> type[_InteractorClsT]:
return dataclass(
kw_only=True,
eq=False,
match_args=False,
frozen=True,
slots=True,
)(interactor_cls)
@@ -0,0 +1,33 @@
from collections.abc import Hashable
from dataclasses import dataclass
from datetime import datetime
from typing import dataclass_transform, override
from uuid import UUID
from template_project.application.common.errors import EntityAlreadyDeletedError
@dataclass_transform(kw_only_default=True)
def to_entity[_EntityCLsT](entity_cls: type[_EntityCLsT]) -> type[_EntityCLsT]:
return dataclass(kw_only=True)(entity_cls)
@to_entity
class Entity[_EntityId: UUID](Hashable):
id: _EntityId
created_at: datetime
deleted_at: datetime | None = None
def ensure_not_deleted(self) -> None:
if self.deleted_at is not None:
raise EntityAlreadyDeletedError(entity_name=self.__class__.__name__)
@override
def __eq__(self, other: object) -> bool:
if isinstance(other, Entity):
return self.id == other.id
return NotImplemented
@override
def __hash__(self) -> int:
return hash(self.id)
@@ -0,0 +1,29 @@
from dataclasses import dataclass
from typing import dataclass_transform, override
@dataclass_transform(
kw_only_default=True,
eq_default=False,
)
def to_error[T](cls: type[T]) -> type[T]:
return dataclass(
kw_only=True,
eq=False,
repr=False,
match_args=False,
)(cls)
@to_error
class ApplicationError(Exception):
pass
@to_error
class EntityAlreadyDeletedError(ApplicationError):
entity_name: str
@override
def __str__(self) -> str:
return f"Entity {self.entity_name!r} already deleted"
@@ -0,0 +1,10 @@
from abc import abstractmethod
from typing import Protocol
from template_project.application.user.entity import User
class IdentityProvider(Protocol):
@abstractmethod
async def get_current_user(self) -> User:
raise NotImplementedError
@@ -0,0 +1,18 @@
from dataclasses import dataclass
from typing import dataclass_transform
@dataclass_transform(
frozen_default=True,
eq_default=False,
kw_only_default=True,
)
def to_interactor[_InteractorClsT](interactor_cls: type[_InteractorClsT]) -> type[_InteractorClsT]:
return dataclass(
kw_only=True,
eq=False,
repr=False,
frozen=True,
match_args=False,
slots=True,
)(interactor_cls)
@@ -0,0 +1,12 @@
from abc import abstractmethod
from typing import Any, Protocol
class UnitOfWork(Protocol):
@abstractmethod
def add(self, *entities: Any) -> None:
raise NotImplementedError
@abstractmethod
async def commit(self) -> None:
raise NotImplementedError
@@ -0,0 +1,17 @@
from abc import abstractmethod
from typing import Protocol
from template_project.application.user.entity import User, UserId
class UserDataGateway(Protocol):
@abstractmethod
async def load_with_id(self, id_: UserId) -> User | None:
raise NotImplementedError
@abstractmethod
async def exists_by_email(
self,
email: str
) -> bool:
raise NotImplementedError
@@ -0,0 +1,27 @@
from datetime import UTC, datetime
from typing import NewType, Self
from uuid import UUID
from uuid_utils.compat import uuid7
from template_project.application.common.entity import Entity, to_entity
UserId = NewType("UserId", UUID)
@to_entity
class User(Entity[UserId]):
email: str
hashed_password: str
@classmethod
def factory(
cls,
email: str,
hashed_password: str,
) -> Self:
return cls(
id=UserId(uuid7()),
email=email,
hashed_password=hashed_password,
created_at=datetime.now(tz=UTC),
)
@@ -0,0 +1,15 @@
from typing import override
from template_project.application.common.errors import ApplicationError, to_error
@to_error
class UserWithEmailAlreadyExistsError(ApplicationError):
email: str
@override
def __str__(self) -> str:
return f"User with the email={self.email!r} already exists"
@to_error
class UserUnauthorizedError(ApplicationError):
pass
@@ -0,0 +1,23 @@
from adaptix.conversion import get_converter
from template_project.application.common.data_structure import to_data_structure
from template_project.application.common.identity_provider import IdentityProvider
from template_project.application.common.interactor import to_interactor
from template_project.application.user.entity import User, UserId
@to_data_structure
class GetMeResponse:
id: UserId
email: str
response_converter = get_converter(User, GetMeResponse)
@to_interactor
class GetMeInteractor:
identity_provider: IdentityProvider
async def execute(self) -> GetMeResponse:
current_user = await self.identity_provider.get_current_user()
return response_converter(current_user)
@@ -0,0 +1,49 @@
from template_project.application.access_token.cryptographer import AccessTokenCryptographer
from template_project.application.access_token.entity_factory import AccessTokenFactory
from template_project.application.common.containers import SecretString
from template_project.application.common.data_structure import to_data_structure
from template_project.application.common.interactor import to_interactor
from template_project.application.common.unit_of_work import UnitOfWork
from template_project.application.user.data_gateway import UserDataGateway
from template_project.application.user.entity import User
from template_project.application.user.errors import UserWithEmailAlreadyExistsError
from template_project.application.user.password_utils import PasswordHasher
@to_data_structure
class UserSignUpResponse:
access_token: str
@to_interactor
class UserSignUpInteractor:
unit_of_work: UnitOfWork
password_hasher: PasswordHasher
user_data_gateway: UserDataGateway
access_token_factory: AccessTokenFactory
access_token_cryptographer: AccessTokenCryptographer
async def execute(
self,
email: str,
password: SecretString,
) -> UserSignUpResponse:
exists_by_email = await self.user_data_gateway.exists_by_email(email)
if exists_by_email:
raise UserWithEmailAlreadyExistsError(email=email)
hashed_password = self.password_hasher.hash(password)
user = User.factory(
email=email,
hashed_password=hashed_password,
)
access_token = self.access_token_factory.execute(user.id)
crypted_access_token = self.access_token_cryptographer.crypto(access_token)
response = UserSignUpResponse(access_token=crypted_access_token)
self.unit_of_work.add(user, access_token)
await self.unit_of_work.commit()
return response
@@ -0,0 +1,20 @@
from abc import abstractmethod
from typing import Protocol
from template_project.application.common.containers import SecretString
class PasswordHasher(Protocol):
@abstractmethod
def hash(self, password: SecretString) -> str:
raise NotImplementedError
class PasswordVerifying(Protocol):
@abstractmethod
def verify(
self,
verifiable_password: SecretString,
hashed_password: str,
) -> bool:
raise NotImplementedError