[feat] add basic domain
This commit is contained in:
parent
a5577c1501
commit
39f28d8f30
5 changed files with 155 additions and 0 deletions
18
domain/error.py
Normal file
18
domain/error.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
class DomainError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserError(DomainError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserNotFoundError(UserError):
|
||||||
|
def __init__(self, user_id: str) -> None:
|
||||||
|
super().__init__('user_not_found')
|
||||||
|
self.user_id = user_id
|
||||||
|
|
||||||
|
|
||||||
|
class UserConflictError(UserError):
|
||||||
|
def __init__(self, email: str) -> None:
|
||||||
|
super().__init__('user_conflict')
|
||||||
|
self.email = email
|
||||||
8
domain/user.py
Normal file
8
domain/user.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||||
|
class User:
|
||||||
|
id: str
|
||||||
|
email: str
|
||||||
|
name: str
|
||||||
21
repository/user.py
Normal file
21
repository/user.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from domain.user import User
|
||||||
|
from usecase.interface import UserRepository
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryUserRepository(UserRepository):
|
||||||
|
def __init__(self, users: Iterable[User] | None = None) -> None:
|
||||||
|
self._users = {user.id: user for user in users or ()}
|
||||||
|
|
||||||
|
def get(self, user_id: str) -> User | None:
|
||||||
|
return self._users.get(user_id)
|
||||||
|
|
||||||
|
def get_by_email(self, email: str) -> User | None:
|
||||||
|
for user in self._users.values():
|
||||||
|
if user.email == email:
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save(self, user: User) -> None:
|
||||||
|
self._users[user.id] = user
|
||||||
67
usecase/interface.py
Normal file
67
usecase/interface.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import Protocol, TypeAlias
|
||||||
|
|
||||||
|
from domain.user import User
|
||||||
|
|
||||||
|
AttrValue: TypeAlias = str | int | float | bool
|
||||||
|
Attrs: TypeAlias = Mapping[str, AttrValue]
|
||||||
|
|
||||||
|
|
||||||
|
class UserRepository(Protocol):
|
||||||
|
def get(self, user_id: str) -> User | None: ...
|
||||||
|
|
||||||
|
def get_by_email(self, email: str) -> User | None: ...
|
||||||
|
|
||||||
|
def save(self, user: User) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Logger(Protocol):
|
||||||
|
def debug(self, message: str, attrs: Attrs | None = None) -> None: ...
|
||||||
|
|
||||||
|
def info(self, message: str, attrs: Attrs | None = None) -> None: ...
|
||||||
|
|
||||||
|
def warning(self, message: str, attrs: Attrs | None = None) -> None: ...
|
||||||
|
|
||||||
|
def error(self, message: str, attrs: Attrs | None = None) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Metrics(Protocol):
|
||||||
|
def increment(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
value: int = 1,
|
||||||
|
attrs: Attrs | None = None,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
def record(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
value: float,
|
||||||
|
attrs: Attrs | None = None,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Span(Protocol):
|
||||||
|
def set_attribute(self, name: str, value: AttrValue) -> None: ...
|
||||||
|
|
||||||
|
def record_error(self, error: Exception) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class SpanContext(Protocol):
|
||||||
|
def __enter__(self) -> Span: ...
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: type[BaseException] | None,
|
||||||
|
exc: BaseException | None,
|
||||||
|
traceback: TracebackType | None,
|
||||||
|
) -> bool | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Tracer(Protocol):
|
||||||
|
def start_span(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
attrs: Attrs | None = None,
|
||||||
|
) -> SpanContext: ...
|
||||||
41
usecase/user.py
Normal file
41
usecase/user.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from domain.error import UserNotFoundError
|
||||||
|
from domain.user import User
|
||||||
|
from usecase.interface import Logger, Tracer, UserRepository
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GetUserQuery:
|
||||||
|
user_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class GetUser:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
repository: UserRepository,
|
||||||
|
logger: Logger,
|
||||||
|
tracer: Tracer,
|
||||||
|
) -> None:
|
||||||
|
self._repository = repository
|
||||||
|
self._logger = logger
|
||||||
|
self._tracer = tracer
|
||||||
|
|
||||||
|
def execute(self, query: GetUserQuery) -> User:
|
||||||
|
with self._tracer.start_span(
|
||||||
|
'usecase.get_user',
|
||||||
|
attrs={'user.id': query.user_id},
|
||||||
|
) as span:
|
||||||
|
user = self._repository.get(query.user_id)
|
||||||
|
if user is None:
|
||||||
|
error = UserNotFoundError(query.user_id)
|
||||||
|
span.record_error(error)
|
||||||
|
self._logger.warning(
|
||||||
|
'user_not_found',
|
||||||
|
attrs={'user_id': query.user_id},
|
||||||
|
)
|
||||||
|
raise error
|
||||||
|
|
||||||
|
span.set_attribute('user.email', user.email)
|
||||||
|
self._logger.info('user_loaded', attrs={'user_id': user.id})
|
||||||
|
return user
|
||||||
Loading…
Add table
Add a link
Reference in a new issue