This commit is contained in:
e4www
2026-01-30 16:34:56 +03:00
commit 12f2ceaf9b
27 changed files with 1331 additions and 0 deletions

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

View 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

View 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

View 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

View File

@@ -0,0 +1,14 @@
class DomainError(Exception):
pass
class InvalidEmail(Exception):
pass
class UserAlreadyExists(Exception):
pass
class UserNotFound(Exception):
pass

View 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.")

View 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