docs: add AI code generation requirements and comprehensive Google-style docstrings

- Add AI code generation requirements to AGENTS.md
- Add module-level docstrings to all 46 Python modules
- Add detailed Google-style docstrings to all classes and functions
- Remove all inline comments following self-documenting code principle
- Include Args, Returns, Raises sections in function docstrings
- Add Attributes and Examples sections to class docstrings
This commit is contained in:
2026-05-02 13:15:21 +03:00
parent 6a528bcbb9
commit ca4e8877a5
52 changed files with 2043 additions and 304 deletions

View File

@@ -1,4 +1,8 @@
"""Domain layer exports."""
"""Domain layer exports.
This module re-exports all domain layer components including
entities, value objects, repositories, and exceptions.
"""
from app.domain.entities import BaseEntity, Post
from app.domain.exceptions import (
@@ -13,18 +17,14 @@ from app.domain.repositories import PostRepository, Repository
from app.domain.value_objects import Content, Slug, Title, ValueObject
__all__ = [
# Entities
"BaseEntity",
"Post",
# Value Objects
"ValueObject",
"Title",
"Content",
"Slug",
# Repositories
"Repository",
"PostRepository",
# Exceptions
"DomainException",
"ValidationException",
"NotFoundException",

View File

@@ -1,4 +1,8 @@
"""Domain entities."""
"""Domain entities.
This module re-exports all domain entities that represent
core business objects with identity.
"""
from app.domain.entities.base import BaseEntity
from app.domain.entities.post import Post

View File

@@ -1,4 +1,9 @@
"""Base entity for DDD domain layer."""
"""Base entity for DDD domain layer.
This module provides the foundational BaseEntity class that all domain
entities must inherit from. It implements common entity patterns including
identity management, equality comparison, and timestamp tracking.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
@@ -9,25 +14,62 @@ from uuid import UUID, uuid4
@dataclass(kw_only=True)
class BaseEntity(ABC):
"""Base class for all domain entities."""
"""Base class for all domain entities.
Provides common functionality for domain entities including unique
identification, creation/update timestamps, and equality comparison
based on identity.
Attributes:
id: Unique identifier for the entity, automatically generated.
created_at: Timestamp when the entity was created.
updated_at: Timestamp when the entity was last updated.
Example:
>>> class User(BaseEntity):
... name: str
... def to_dict(self) -> dict[str, Any]:
... return {"id": str(self.id), "name": self.name}
"""
id: UUID = field(default_factory=uuid4)
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
def __eq__(self, other: object) -> bool:
"""Compare entities by identity.
Args:
other: Another object to compare with.
Returns:
True if both objects are BaseEntity instances with same ID.
"""
if not isinstance(other, BaseEntity):
return NotImplemented
return self.id == other.id
def __hash__(self) -> int:
"""Get hash based on entity identity.
Returns:
Hash of the entity ID.
"""
return hash(self.id)
def touch(self) -> None:
"""Update the updated_at timestamp."""
"""Update the updated_at timestamp.
Should be called whenever the entity is modified to track
the last modification time.
"""
self.updated_at = datetime.now(UTC)
@abstractmethod
def to_dict(self) -> dict[str, Any]:
"""Convert entity to dictionary."""
"""Convert entity to dictionary representation.
Returns:
Dictionary containing all entity attributes.
"""
...

View File

@@ -1,4 +1,9 @@
"""Domain entity for Blog Post."""
"""Domain entity for Blog Post.
This module defines the Post aggregate root entity that encapsulates
all business logic related to blog posts including publishing, content
management, and tag operations.
"""
from dataclasses import dataclass, field
from typing import Any
@@ -11,7 +16,28 @@ from app.domain.value_objects.title import Title
@dataclass(kw_only=True)
class Post(BaseEntity):
"""Blog post domain entity."""
"""Blog post domain entity.
Represents a blog post with title, content, slug, and metadata.
Encapsulates business logic for post lifecycle management.
Attributes:
title: Post title value object with validation.
content: Post content value object with validation.
slug: URL-friendly identifier generated from title.
author_id: Identifier of the post author.
published: Publication status flag.
tags: List of tags associated with the post.
Example:
>>> post = Post.create(
... title_str="My First Post",
... content_str="This is the content...",
... author_id="user-123",
... tags=["python", "fastapi"]
... )
>>> post.publish()
"""
title: Title
content: Content
@@ -21,40 +47,66 @@ class Post(BaseEntity):
tags: list[str] = field(default_factory=list)
def publish(self) -> None:
"""Publish the post."""
"""Publish the post.
Sets the published flag to True and updates the timestamp.
"""
self.published = True
self.touch()
def unpublish(self) -> None:
"""Unpublish the post."""
"""Unpublish the post.
Sets the published flag to False and updates the timestamp.
"""
self.published = False
self.touch()
def update_content(self, content: Content) -> None:
"""Update post content."""
"""Update post content.
Args:
content: New content value object.
"""
self.content = content
self.touch()
def update_title(self, title: Title) -> None:
"""Update post title and regenerate slug."""
"""Update post title and regenerate slug.
Args:
title: New title value object.
"""
self.title = title
self.slug = Slug.from_title(title.value)
self.touch()
def add_tag(self, tag: str) -> None:
"""Add a tag to the post."""
"""Add a tag to the post.
Args:
tag: Tag string to add. Only adds if not already present.
"""
if tag not in self.tags:
self.tags.append(tag)
self.touch()
def remove_tag(self, tag: str) -> None:
"""Remove a tag from the post."""
"""Remove a tag from the post.
Args:
tag: Tag string to remove. Only removes if present.
"""
if tag in self.tags:
self.tags.remove(tag)
self.touch()
def to_dict(self) -> dict[str, Any]:
"""Convert entity to dictionary."""
"""Convert entity to dictionary.
Returns:
Dictionary representation with all post attributes.
"""
return {
"id": str(self.id),
"title": self.title.value,
@@ -75,7 +127,17 @@ class Post(BaseEntity):
author_id: str,
tags: list[str] | None = None,
) -> "Post":
"""Factory method to create a new post."""
"""Factory method to create a new post.
Args:
title_str: Title string for the post.
content_str: Content string for the post.
author_id: Identifier of the post author.
tags: Optional list of tags.
Returns:
New Post instance with validated value objects.
"""
title = Title(title_str)
content = Content(content_str)
slug = Slug.from_title(title_str)

View File

@@ -1,39 +1,78 @@
"""Domain exceptions."""
"""Domain exceptions for business logic errors.
This module defines the exception hierarchy for domain layer errors.
All domain exceptions inherit from DomainException base class.
"""
class DomainException(Exception):
"""Base exception for domain layer."""
"""Base exception for domain layer.
All domain-specific exceptions should inherit from this class.
Provides a consistent interface for error messages.
Attributes:
message: Human-readable error description.
Example:
>>> raise DomainException("Business rule violated")
"""
def __init__(self, message: str) -> None:
"""Initialize domain exception.
Args:
message: Error message describing the exception.
"""
self.message = message
super().__init__(self.message)
class ValidationException(DomainException):
"""Raised when validation fails."""
"""Raised when validation fails.
pass
Used when entity or value object validation does not pass.
Example:
>>> raise ValidationException("Title is too long")
"""
class NotFoundException(DomainException):
"""Raised when an entity is not found."""
"""Raised when an entity is not found.
pass
Used when requesting an entity that does not exist in the repository.
Example:
>>> raise NotFoundException("Post with id 123 not found")
"""
class AlreadyExistsException(DomainException):
"""Raised when trying to create an entity that already exists."""
"""Raised when trying to create an entity that already exists.
pass
Used when attempting to create a duplicate entity.
Example:
>>> raise AlreadyExistsException("Post with this slug already exists")
"""
class UnauthorizedException(DomainException):
"""Raised when user is not authorized."""
"""Raised when user is not authorized.
pass
Used when authentication is required but not provided or invalid.
Example:
>>> raise UnauthorizedException("Authentication required")
"""
class ForbiddenException(DomainException):
"""Raised when access is forbidden."""
"""Raised when access is forbidden.
pass
Used when authenticated user lacks required permissions.
Example:
>>> raise ForbiddenException("Only admins can delete posts")
"""

View File

@@ -1,4 +1,8 @@
"""Repository interfaces."""
"""Repository interfaces.
This module re-exports all repository interfaces that define
the contract for data access operations.
"""
from app.domain.repositories.base import Repository
from app.domain.repositories.post import PostRepository

View File

@@ -1,4 +1,8 @@
"""Base repository interface for DDD."""
"""Base repository interface for DDD.
This module defines the generic repository pattern interface that all
repository implementations must follow. Provides standard CRUD operations.
"""
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
@@ -10,34 +14,76 @@ T = TypeVar("T", bound=BaseEntity)
class Repository(ABC, Generic[T]):
"""Generic repository interface."""
"""Generic repository interface.
Defines the contract for repository implementations. All repositories
must provide standard CRUD operations for their entity type.
Type Parameters:
T: Entity type that must inherit from BaseEntity.
Example:
>>> class PostRepository(Repository[Post]):
... async def get_by_id(self, entity_id: UUID) -> Post | None:
... ...
"""
@abstractmethod
async def get_by_id(self, entity_id: UUID) -> T | None:
"""Get entity by ID."""
"""Get entity by ID.
Args:
entity_id: Unique identifier of the entity.
Returns:
Entity instance if found, None otherwise.
"""
...
@abstractmethod
async def get_all(self) -> list[T]:
"""Get all entities."""
"""Get all entities.
Returns:
List of all entity instances.
"""
...
@abstractmethod
async def add(self, entity: T) -> None:
"""Add new entity."""
"""Add new entity.
Args:
entity: Entity instance to add.
"""
...
@abstractmethod
async def update(self, entity: T) -> None:
"""Update existing entity."""
"""Update existing entity.
Args:
entity: Entity instance with updated data.
"""
...
@abstractmethod
async def delete(self, entity_id: UUID) -> None:
"""Delete entity by ID."""
"""Delete entity by ID.
Args:
entity_id: Unique identifier of the entity to delete.
"""
...
@abstractmethod
async def exists(self, entity_id: UUID) -> bool:
"""Check if entity exists."""
"""Check if entity exists.
Args:
entity_id: Unique identifier of the entity.
Returns:
True if entity exists, False otherwise.
"""
...

View File

@@ -1,4 +1,8 @@
"""Post repository interface."""
"""Post repository interface.
This module extends the base repository interface with post-specific
query methods including slug lookup, author filtering, and search.
"""
from abc import abstractmethod
@@ -7,11 +11,27 @@ from app.domain.repositories.base import Repository
class PostRepository(Repository[Post]):
"""Repository interface for Blog Posts."""
"""Repository interface for Blog Posts.
Extends the generic repository with post-specific operations
including slug-based lookup, author filtering, tag filtering,
and full-text search capabilities.
Example:
>>> posts = await repo.get_by_author("user-123", limit=10)
>>> exists = await repo.slug_exists("my-first-post")
"""
@abstractmethod
async def get_by_slug(self, slug: str) -> Post | None:
"""Get post by slug."""
"""Get post by slug.
Args:
slug: URL-friendly slug identifier.
Returns:
Post instance if found, None otherwise.
"""
...
@abstractmethod
@@ -21,7 +41,16 @@ class PostRepository(Repository[Post]):
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""Get all posts by author."""
"""Get all posts by author.
Args:
author_id: Identifier of the author.
limit: Maximum number of posts to return.
offset: Number of posts to skip.
Returns:
List of posts by the specified author.
"""
...
@abstractmethod
@@ -30,7 +59,15 @@ class PostRepository(Repository[Post]):
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""Get all published posts."""
"""Get all published posts.
Args:
limit: Maximum number of posts to return.
offset: Number of posts to skip.
Returns:
List of published posts.
"""
...
@abstractmethod
@@ -40,12 +77,28 @@ class PostRepository(Repository[Post]):
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""Get posts by tag."""
"""Get posts by tag.
Args:
tag: Tag to filter by.
limit: Maximum number of posts to return.
offset: Number of posts to skip.
Returns:
List of posts with the specified tag.
"""
...
@abstractmethod
async def slug_exists(self, slug: str) -> bool:
"""Check if slug already exists."""
"""Check if slug already exists.
Args:
slug: Slug to check for existence.
Returns:
True if slug exists, False otherwise.
"""
...
@abstractmethod
@@ -55,5 +108,14 @@ class PostRepository(Repository[Post]):
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""Search posts by query string."""
"""Search posts by query string.
Args:
query: Search query string.
limit: Maximum number of posts to return.
offset: Number of posts to skip.
Returns:
List of posts matching the search query.
"""
...

View File

@@ -1,4 +1,8 @@
"""Role-based access control definitions."""
"""Role-based access control definitions.
This module provides role and permission definitions for the application
along with utility functions and decorators for permission checking.
"""
from collections.abc import Callable
from enum import Enum
@@ -9,7 +13,20 @@ from app.domain.exceptions import ForbiddenException
class Role(str, Enum):
"""User roles in the system."""
"""User roles in the system.
Defines the available user roles with hierarchical permissions.
ADMIN has full access, USER has standard access, GUEST has read-only.
Attributes:
ADMIN: Administrator with full system access.
USER: Regular authenticated user.
GUEST: Unauthenticated or limited access user.
Example:
>>> if role == Role.ADMIN:
... grant_full_access()
"""
ADMIN = "admin"
USER = "user"
@@ -17,9 +34,16 @@ class Role(str, Enum):
class Permission:
"""Permission definitions."""
"""Permission definitions.
Contains string constants for all available permissions in the system.
Used for role-based access control checks.
Example:
>>> if has_permission(role, Permission.POST_CREATE):
... allow_post_creation()
"""
# Post permissions
POST_CREATE = "post:create"
POST_READ = "post:read"
POST_READ_UNPUBLISHED = "post:read_unpublished"
@@ -28,7 +52,6 @@ class Permission:
POST_PUBLISH = "post:publish"
# Role-based permission mapping
ROLE_PERMISSIONS: dict[Role, list[str]] = {
Role.ADMIN: [
Permission.POST_CREATE,
@@ -52,24 +75,52 @@ ROLE_PERMISSIONS: dict[Role, list[str]] = {
def has_permission(role: Role, permission: str) -> bool:
"""Check if role has specific permission."""
"""Check if role has specific permission.
Args:
role: User role to check.
permission: Permission string to verify.
Returns:
True if role has the permission, False otherwise.
Example:
>>> has_permission(Role.ADMIN, Permission.POST_DELETE)
True
"""
return permission in ROLE_PERMISSIONS.get(role, [])
def require_permission(
permission: str,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Decorator to require specific permission."""
"""Decorator to require specific permission.
Creates a decorator that checks if the user has the required permission
before executing the decorated function.
Args:
permission: Permission string required for execution.
Returns:
Decorator function for permission checking.
Raises:
ForbiddenException: If user lacks the required permission.
Example:
>>> @require_permission(Permission.POST_CREATE)
... async def create_post():
... ...
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
# Get token_info from kwargs
token_info = kwargs.get("token_info")
if not token_info:
raise ForbiddenException("Authentication required")
# Determine role from token or default to guest
roles = getattr(token_info, "roles", [])
if Role.ADMIN.value in roles:
role = Role.ADMIN
@@ -93,7 +144,18 @@ def require_permission(
def get_effective_role(roles: list[str]) -> Role:
"""Determine effective role from list of roles.
Priority: admin > user > guest
Evaluates multiple roles and returns the highest privilege role.
Priority order: admin > user > guest.
Args:
roles: List of role strings from token.
Returns:
Highest privilege Role enum value.
Example:
>>> get_effective_role(["user", "admin"])
<Role.ADMIN: 'admin'>
"""
if Role.ADMIN.value in roles:
return Role.ADMIN

View File

@@ -1,4 +1,8 @@
"""Value objects."""
"""Value objects.
This module re-exports all domain value objects that represent
immutable validated domain concepts.
"""
from app.domain.value_objects.base import ValueObject
from app.domain.value_objects.content import Content

View File

@@ -1,4 +1,9 @@
"""Base value object for DDD domain layer."""
"""Base value object for DDD domain layer.
This module provides the foundational ValueObject class that all domain
value objects must inherit from. Implements equality, hashing, and
validation patterns for immutable value objects.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
@@ -9,29 +14,78 @@ T = TypeVar("T")
@dataclass(frozen=True, slots=True)
class ValueObject(ABC, Generic[T]):
"""Base class for all value objects."""
"""Base class for all value objects.
Value objects are immutable objects defined by their attributes rather
than identity. They are validated on creation and provide type safety.
Attributes:
value: The underlying value wrapped by the value object.
Type Parameters:
T: Type of the wrapped value.
Example:
>>> class Email(ValueObject[str]):
... def _validate(self) -> None:
... if "@" not in self.value:
... raise ValueError("Invalid email")
"""
value: T
def __post_init__(self) -> None:
"""Validate value object after initialization.
Automatically called by dataclass after __init__. Triggers
the validation method to ensure value integrity.
"""
self._validate()
@abstractmethod
def _validate(self) -> None:
"""Validate the value object. Raise ValueError if invalid."""
"""Validate the value object.
Must be implemented by subclasses to enforce value constraints.
Raises:
ValueError: If the value does not meet validation criteria.
"""
...
def __eq__(self, other: object) -> bool:
"""Compare value objects by value.
Args:
other: Another object to compare with.
Returns:
True if both are ValueObjects with equal values.
"""
if not isinstance(other, ValueObject):
return False
return bool(self.value == other.value)
def __hash__(self) -> int:
"""Get hash based on wrapped value.
Returns:
Hash of the underlying value.
"""
return hash(self.value)
def __str__(self) -> str:
"""Convert value object to string.
Returns:
String representation of the wrapped value.
"""
return str(self.value)
def to_primitive(self) -> Any:
"""Convert value object to primitive type."""
"""Convert value object to primitive type.
Returns:
The underlying primitive value.
"""
return self.value

View File

@@ -1,4 +1,8 @@
"""Content value object."""
"""Content value object.
This module defines the Content value object for blog post content
with validation for minimum and maximum length constraints.
"""
from dataclasses import dataclass
@@ -7,12 +11,35 @@ from app.domain.value_objects.base import ValueObject
@dataclass(frozen=True, slots=True)
class Content(ValueObject[str]):
"""Blog post content value object."""
"""Blog post content value object.
Wraps and validates blog post content ensuring it meets length
requirements and is not empty.
Attributes:
value: The content string value.
MIN_LENGTH: Minimum allowed content length (10 characters).
MAX_LENGTH: Maximum allowed content length (50000 characters).
Raises:
ValueError: If content is empty, too short, or too long.
Example:
>>> content = Content("This is valid content...")
>>> print(content.value)
"""
MIN_LENGTH: int = 10
MAX_LENGTH: int = 50000
def _validate(self) -> None:
"""Validate content string.
Checks that content is a non-empty string within length bounds.
Raises:
ValueError: If content fails validation criteria.
"""
if not isinstance(self.value, str):
raise ValueError("Content must be a string")
if not self.value.strip():

View File

@@ -1,4 +1,9 @@
"""Slug value object for URL-friendly identifiers."""
"""Slug value object for URL-friendly identifiers.
This module defines the Slug value object for generating and validating
URL-friendly slugs from titles. Enforces lowercase, alphanumeric, and
hyphen-only format.
"""
import re
from dataclasses import dataclass
@@ -8,12 +13,36 @@ from app.domain.value_objects.base import ValueObject
@dataclass(frozen=True, slots=True)
class Slug(ValueObject[str]):
"""URL slug value object."""
"""URL slug value object.
Represents a URL-friendly identifier generated from titles.
Validates format and provides factory method for slug generation.
Attributes:
value: The slug string value.
MAX_LENGTH: Maximum allowed slug length (200 characters).
SLUG_PATTERN: Regex pattern for valid slug format.
Raises:
ValueError: If slug format is invalid.
Example:
>>> slug = Slug.from_title("My First Post!")
>>> print(slug.value)
'my-first-post'
"""
MAX_LENGTH: int = 200
SLUG_PATTERN: str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
def _validate(self) -> None:
"""Validate slug format.
Ensures slug contains only lowercase letters, numbers, and hyphens.
Raises:
ValueError: If slug format is invalid.
"""
if not isinstance(self.value, str):
raise ValueError("Slug must be a string")
if len(self.value) > self.MAX_LENGTH:
@@ -23,17 +52,22 @@ class Slug(ValueObject[str]):
@classmethod
def from_title(cls, title: str) -> "Slug":
"""Generate slug from title."""
# Convert to lowercase, replace spaces with hyphens
"""Generate slug from title.
Converts title to URL-friendly format by lowercasing, removing
special characters, and replacing spaces with hyphens.
Args:
title: Source title string.
Returns:
New Slug instance with generated value.
"""
slug = title.lower().strip()
# Keep only alphanumeric, spaces, and hyphens
slug = re.sub(r"[^a-z0-9\s-]", "", slug)
# Replace spaces and multiple hyphens with single hyphen
slug = re.sub(r"[-\s]+", "-", slug)
# Limit length and strip hyphens
max_len = 200 # Same as MAX_LENGTH
max_len = 200
slug = slug[:max_len].strip("-")
# Ensure we have at least one character
if not slug:
slug = "post"
return cls(value=slug)

View File

@@ -1,4 +1,8 @@
"""Title value object."""
"""Title value object.
This module defines the Title value object for blog post titles
with validation for minimum and maximum length constraints.
"""
from dataclasses import dataclass
@@ -7,12 +11,35 @@ from app.domain.value_objects.base import ValueObject
@dataclass(frozen=True, slots=True)
class Title(ValueObject[str]):
"""Blog post title value object."""
"""Blog post title value object.
Wraps and validates blog post titles ensuring they meet length
requirements and are not empty.
Attributes:
value: The title string value.
MIN_LENGTH: Minimum allowed title length (3 characters).
MAX_LENGTH: Maximum allowed title length (200 characters).
Raises:
ValueError: If title is empty, too short, or too long.
Example:
>>> title = Title("My Blog Post")
>>> print(title.value)
"""
MIN_LENGTH: int = 3
MAX_LENGTH: int = 200
def _validate(self) -> None:
"""Validate title string.
Checks that title is a non-empty string within length bounds.
Raises:
ValueError: If title fails validation criteria.
"""
if not isinstance(self.value, str):
raise ValueError("Title must be a string")
if len(self.value) < self.MIN_LENGTH: