Files
blog.pyaqa.ru/app/infrastructure/repositories/post.py
Sergey Vanyushkin ca4e8877a5 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
2026-05-02 13:15:21 +03:00

281 lines
8.3 KiB
Python

"""SQLAlchemy implementation of PostRepository.
This module provides the concrete implementation of PostRepository
using SQLAlchemy ORM for data persistence.
"""
from uuid import UUID
from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.entities import Post
from app.domain.repositories import PostRepository
from app.domain.value_objects import Content, Slug, Title
from app.infrastructure.database.models import PostORM
class SQLAlchemyPostRepository(PostRepository):
"""SQLAlchemy implementation of Post repository.
Provides data access methods for Post entities using SQLAlchemy ORM.
Handles conversion between domain entities and ORM models.
Attributes:
_session: SQLAlchemy async session for database operations.
Example:
>>> repo = SQLAlchemyPostRepository(session)
>>> post = await repo.get_by_id(post_id)
"""
def __init__(self, session: AsyncSession) -> None:
"""Initialize repository with session.
Args:
session: SQLAlchemy async session instance.
"""
self._session = session
def _to_domain(self, orm: PostORM) -> Post:
"""Convert ORM model to domain entity.
Args:
orm: SQLAlchemy ORM model instance.
Returns:
Domain Post entity with validated value objects.
"""
return Post(
id=UUID(orm.id),
title=Title(orm.title),
content=Content(orm.content),
slug=Slug(orm.slug),
author_id=orm.author_id,
published=orm.published,
tags=orm.tags or [],
created_at=orm.created_at,
updated_at=orm.updated_at,
)
def _to_orm(self, post: Post) -> PostORM:
"""Convert domain entity to ORM model.
Args:
post: Domain Post entity.
Returns:
SQLAlchemy ORM model instance.
"""
return PostORM(
id=str(post.id),
title=post.title.value,
content=post.content.value,
slug=post.slug.value,
author_id=post.author_id,
published=post.published,
tags=post.tags,
created_at=post.created_at,
updated_at=post.updated_at,
)
async def get_by_id(self, entity_id: UUID) -> Post | None:
"""Get post by ID.
Args:
entity_id: Unique identifier of the post.
Returns:
Post entity if found, None otherwise.
"""
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id)))
orm = result.scalar_one_or_none()
return self._to_domain(orm) if orm else None
async def get_all(self) -> list[Post]:
"""Get all posts.
Returns:
List of all Post entities.
"""
result = await self._session.execute(select(PostORM))
orms = result.scalars().all()
return [self._to_domain(orm) for orm in orms]
async def add(self, entity: Post) -> None:
"""Add new post.
Args:
entity: Post entity to add.
"""
orm = self._to_orm(entity)
self._session.add(orm)
async def update(self, entity: Post) -> None:
"""Update existing post.
Args:
entity: Post entity with updated data.
"""
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity.id)))
orm = result.scalar_one()
orm.title = entity.title.value
orm.content = entity.content.value
orm.slug = entity.slug.value
orm.published = entity.published
orm.tags = entity.tags
orm.updated_at = entity.updated_at
async def delete(self, entity_id: UUID) -> None:
"""Delete post by ID.
Args:
entity_id: Unique identifier of the post to delete.
"""
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id)))
orm = result.scalar_one_or_none()
if orm:
await self._session.delete(orm)
async def exists(self, entity_id: UUID) -> bool:
"""Check if post exists.
Args:
entity_id: Unique identifier of the post.
Returns:
True if post exists, False otherwise.
"""
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity_id)))
return result.scalar_one_or_none() is not None
async def get_by_slug(self, slug: str) -> Post | None:
"""Get post by slug.
Args:
slug: URL-friendly slug identifier.
Returns:
Post entity if found, None otherwise.
"""
result = await self._session.execute(select(PostORM).where(PostORM.slug == slug))
orm = result.scalar_one_or_none()
return self._to_domain(orm) if orm else None
async def get_by_author(
self,
author_id: str,
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""Get 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 Post entities by the author.
"""
query = select(PostORM).where(PostORM.author_id == author_id)
if limit is not None:
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
result = await self._session.execute(query)
orms = result.scalars().all()
return [self._to_domain(orm) for orm in orms]
async def get_published(
self,
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""Get published posts.
Args:
limit: Maximum number of posts to return.
offset: Number of posts to skip.
Returns:
List of published Post entities.
"""
query = select(PostORM).where(PostORM.published.is_(True))
if limit is not None:
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
result = await self._session.execute(query)
orms = result.scalars().all()
return [self._to_domain(orm) for orm in orms]
async def get_by_tag(
self,
tag: str,
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""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 Post entities with the tag.
"""
query = select(PostORM).where(PostORM.tags.contains([tag]))
if limit is not None:
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
result = await self._session.execute(query)
orms = result.scalars().all()
return [self._to_domain(orm) for orm in orms]
async def slug_exists(self, slug: str) -> bool:
"""Check if slug exists.
Args:
slug: Slug to check for existence.
Returns:
True if slug exists, False otherwise.
"""
result = await self._session.execute(select(PostORM).where(PostORM.slug == slug))
return result.scalar_one_or_none() is not None
async def search(
self,
query: str,
limit: int | None = None,
offset: int | None = None,
) -> list[Post]:
"""Search posts.
Args:
query: Search query string.
limit: Maximum number of posts to return.
offset: Number of posts to skip.
Returns:
List of Post entities matching the query.
"""
search_pattern = f"%{query}%"
stmt = select(PostORM).where(
or_(
PostORM.title.ilike(search_pattern),
PostORM.content.ilike(search_pattern),
)
)
if limit is not None:
stmt = stmt.limit(limit)
if offset is not None:
stmt = stmt.offset(offset)
result = await self._session.execute(stmt)
orms = result.scalars().all()
return [self._to_domain(orm) for orm in orms]