init
This commit is contained in:
19
app/domain/shared/base_entity.py
Normal file
19
app/domain/shared/base_entity.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
TId = TypeVar("TId")
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class Entity(Generic[TId]):
|
||||
id: TId
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Entity):
|
||||
return False
|
||||
return self.id == other.id and self.__class__ is other.__class__
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.__class__, self.id))
|
||||
10
app/domain/shared/base_value_object.py
Normal file
10
app/domain/shared/base_value_object.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValueObject:
|
||||
"""Marker base class for value objects."""
|
||||
|
||||
pass
|
||||
38
app/domain/users/entities.py
Normal file
38
app/domain/users/entities.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from uuid import UUID
|
||||
|
||||
from app.domain.shared.base_entity import Entity
|
||||
from app.domain.users.events import UserRegistered
|
||||
from app.domain.users.value_objects import Email
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class User(Entity[UUID]):
|
||||
email: Email
|
||||
full_name: str
|
||||
is_active: bool = True
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
_events: list[object] = field(default_factory=list, init=False, repr=False)
|
||||
|
||||
@property
|
||||
def events(self) -> list[object]:
|
||||
return list(self._events)
|
||||
|
||||
def pull_events(self) -> list[object]:
|
||||
ev, self._events = self._events, []
|
||||
return ev
|
||||
|
||||
@classmethod
|
||||
def register(cls, *, user_id: UUID, email: Email, full_name: str) -> "User":
|
||||
user = cls(id=user_id, email=email, full_name=full_name)
|
||||
user._events.append(
|
||||
UserRegistered(
|
||||
user_id=user_id,
|
||||
email=str(email),
|
||||
occurred_at=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
return user
|
||||
12
app/domain/users/events.py
Normal file
12
app/domain/users/events.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UserRegistered:
|
||||
user_id: UUID
|
||||
email: str
|
||||
occurred_at: datetime
|
||||
14
app/domain/users/exceptions.py
Normal file
14
app/domain/users/exceptions.py
Normal file
@@ -0,0 +1,14 @@
|
||||
class DomainError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidEmail(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserAlreadyExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFound(Exception):
|
||||
pass
|
||||
9
app/domain/users/services.py
Normal file
9
app/domain/users/services.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.domain.users.entities import User
|
||||
from app.domain.users.exceptions import UserAlreadyExists
|
||||
|
||||
|
||||
def ensure_unique_email(*, existing_user: User | None) -> None:
|
||||
if existing_user is not None:
|
||||
raise UserAlreadyExists("User with this email already exists.")
|
||||
21
app/domain/users/value_objects.py
Normal file
21
app/domain/users/value_objects.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.domain.shared.base_value_object import ValueObject
|
||||
from app.domain.users.exceptions import InvalidEmail
|
||||
|
||||
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Email(ValueObject):
|
||||
value: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not _EMAIL_RE.match(self.value):
|
||||
raise InvalidEmail(f"Invalid email: {self.value}")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
Reference in New Issue
Block a user