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:
@@ -1,4 +1,8 @@
|
||||
"""Infrastructure layer exports."""
|
||||
"""Infrastructure layer exports.
|
||||
|
||||
This module re-exports all infrastructure components including
|
||||
config, database, repositories, DI, and middleware.
|
||||
"""
|
||||
|
||||
from app.infrastructure.config import Settings, settings
|
||||
from app.infrastructure.database import (
|
||||
@@ -15,10 +19,8 @@ from app.infrastructure.middleware import register_exception_handlers
|
||||
from app.infrastructure.repositories import SQLAlchemyPostRepository
|
||||
|
||||
__all__ = [
|
||||
# Config
|
||||
"Settings",
|
||||
"settings",
|
||||
# Database
|
||||
"Base",
|
||||
"PostORM",
|
||||
"engine",
|
||||
@@ -26,10 +28,7 @@ __all__ = [
|
||||
"get_session",
|
||||
"init_db",
|
||||
"close_db",
|
||||
# Repositories
|
||||
"SQLAlchemyPostRepository",
|
||||
# DI
|
||||
"create_container",
|
||||
# Middleware
|
||||
"register_exception_handlers",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Authentication infrastructure package."""
|
||||
"""Authentication infrastructure package.
|
||||
|
||||
This module provides Keycloak authentication client and models
|
||||
for token validation and user info retrieval.
|
||||
"""
|
||||
|
||||
from app.infrastructure.auth.client import KeycloakAuthClient
|
||||
from app.infrastructure.auth.models import KeycloakUser, TokenInfo
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Keycloak authentication client."""
|
||||
"""Keycloak authentication client.
|
||||
|
||||
This module provides a client for Keycloak authentication operations
|
||||
including token introspection and user info retrieval.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
@@ -9,10 +13,30 @@ from app.infrastructure.config.settings import Settings
|
||||
|
||||
|
||||
class KeycloakAuthClient:
|
||||
"""Client for Keycloak authentication operations."""
|
||||
"""Client for Keycloak authentication operations.
|
||||
|
||||
Handles token validation via introspection and user info retrieval.
|
||||
Implements token caching to reduce Keycloak server load.
|
||||
|
||||
Attributes:
|
||||
_settings: Application settings with Keycloak config.
|
||||
_base_url: Keycloak realm base URL.
|
||||
_client_id: OAuth client identifier.
|
||||
_client_secret: OAuth client secret.
|
||||
_cache: Token info cache for performance.
|
||||
_cache_ttl: Cache time-to-live in seconds.
|
||||
|
||||
Example:
|
||||
>>> client = KeycloakAuthClient(settings)
|
||||
>>> token_info = await client.introspect_token(token)
|
||||
"""
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
"""Initialize Keycloak client with settings."""
|
||||
"""Initialize Keycloak client with settings.
|
||||
|
||||
Args:
|
||||
settings: Application settings with Keycloak configuration.
|
||||
"""
|
||||
self._settings = settings
|
||||
self._base_url = f"{settings.kc.server_url}/realms/{settings.kc.realm}"
|
||||
self._client_id = settings.kc.client_id
|
||||
@@ -21,15 +45,30 @@ class KeycloakAuthClient:
|
||||
self._cache_ttl = settings.kc.token_cache_ttl
|
||||
|
||||
def _get_introspection_url(self) -> str:
|
||||
"""Get token introspection endpoint URL."""
|
||||
"""Get token introspection endpoint URL.
|
||||
|
||||
Returns:
|
||||
Full URL for token introspection endpoint.
|
||||
"""
|
||||
return f"{self._base_url}/protocol/openid-connect/token/introspection"
|
||||
|
||||
def _get_userinfo_url(self) -> str:
|
||||
"""Get userinfo endpoint URL."""
|
||||
"""Get userinfo endpoint URL.
|
||||
|
||||
Returns:
|
||||
Full URL for userinfo endpoint.
|
||||
"""
|
||||
return f"{self._base_url}/protocol/openid-connect/userinfo"
|
||||
|
||||
def _get_cached_token(self, token: str) -> TokenInfo | None:
|
||||
"""Get cached token info if valid."""
|
||||
"""Get cached token info if valid.
|
||||
|
||||
Args:
|
||||
token: Access token string.
|
||||
|
||||
Returns:
|
||||
Cached TokenInfo if valid and not expired, None otherwise.
|
||||
"""
|
||||
if token not in self._cache:
|
||||
return None
|
||||
|
||||
@@ -41,9 +80,13 @@ class KeycloakAuthClient:
|
||||
return token_info
|
||||
|
||||
def _cache_token(self, token: str, token_info: TokenInfo) -> None:
|
||||
"""Cache token info."""
|
||||
"""Cache token info.
|
||||
|
||||
Args:
|
||||
token: Access token string as cache key.
|
||||
token_info: TokenInfo to cache.
|
||||
"""
|
||||
self._cache[token] = (token_info, time.time())
|
||||
# Simple cleanup of old entries
|
||||
current_time = time.time()
|
||||
expired_keys = [
|
||||
k for k, (_, t) in self._cache.items() if current_time - t > self._cache_ttl
|
||||
@@ -52,13 +95,21 @@ class KeycloakAuthClient:
|
||||
del self._cache[k]
|
||||
|
||||
async def introspect_token(self, token: str) -> TokenInfo:
|
||||
"""Introspect access token using Keycloak."""
|
||||
# Check cache first
|
||||
"""Introspect access token using Keycloak.
|
||||
|
||||
Validates token with Keycloak server and extracts user information.
|
||||
Uses cache to reduce server requests for recently validated tokens.
|
||||
|
||||
Args:
|
||||
token: Access token to validate.
|
||||
|
||||
Returns:
|
||||
TokenInfo with validation result and user claims.
|
||||
"""
|
||||
cached = self._get_cached_token(token)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
# Prepare introspection request
|
||||
data = {
|
||||
"token": token,
|
||||
"client_id": self._client_id,
|
||||
@@ -81,7 +132,6 @@ class KeycloakAuthClient:
|
||||
if not result.get("active", False):
|
||||
return TokenInfo(active=False, raw_claims=result)
|
||||
|
||||
# Extract roles from realm_access or resource_access
|
||||
roles: list[str] = []
|
||||
realm_access = result.get("realm_access", {})
|
||||
if isinstance(realm_access, dict):
|
||||
@@ -96,13 +146,21 @@ class KeycloakAuthClient:
|
||||
raw_claims=result,
|
||||
)
|
||||
|
||||
# Cache valid token
|
||||
self._cache_token(token, token_info)
|
||||
|
||||
return token_info
|
||||
|
||||
async def get_userinfo(self, token: str) -> KeycloakUser | None:
|
||||
"""Get user information from Keycloak using access token."""
|
||||
"""Get user information from Keycloak using access token.
|
||||
|
||||
Fetches detailed user profile from Keycloak userinfo endpoint.
|
||||
|
||||
Args:
|
||||
token: Valid access token.
|
||||
|
||||
Returns:
|
||||
KeycloakUser with profile data, or None on error.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Keycloak authentication models."""
|
||||
"""Keycloak authentication models.
|
||||
|
||||
This module defines data models for Keycloak authentication data
|
||||
including token info and user profiles.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
@@ -6,7 +10,24 @@ from typing import Any
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TokenInfo:
|
||||
"""Information about validated token from Keycloak."""
|
||||
"""Information about validated token from Keycloak.
|
||||
|
||||
Contains the result of token introspection including user claims
|
||||
and role information.
|
||||
|
||||
Attributes:
|
||||
active: Whether the token is active and valid.
|
||||
user_id: Subject identifier from token.
|
||||
username: Username from token claims.
|
||||
email: Email from token claims.
|
||||
roles: List of roles from token.
|
||||
raw_claims: Complete raw claims from token.
|
||||
|
||||
Example:
|
||||
>>> token_info = TokenInfo(active=True, user_id="123", roles=["user"])
|
||||
>>> if token_info.is_valid:
|
||||
... grant_access()
|
||||
"""
|
||||
|
||||
active: bool
|
||||
user_id: str = ""
|
||||
@@ -17,13 +38,32 @@ class TokenInfo:
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if token is valid and active."""
|
||||
"""Check if token is valid and active.
|
||||
|
||||
Returns:
|
||||
True if token is active and has user_id.
|
||||
"""
|
||||
return self.active and bool(self.user_id)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KeycloakUser:
|
||||
"""User information from Keycloak."""
|
||||
"""User information from Keycloak.
|
||||
|
||||
Contains user profile data from Keycloak userinfo endpoint.
|
||||
|
||||
Attributes:
|
||||
id: User subject identifier.
|
||||
username: Username.
|
||||
email: Email address.
|
||||
first_name: First name.
|
||||
last_name: Last name.
|
||||
roles: List of user roles.
|
||||
is_active: Whether user account is active.
|
||||
|
||||
Example:
|
||||
>>> user = KeycloakUser(id="123", username="john", email="john@example.com")
|
||||
"""
|
||||
|
||||
id: str
|
||||
username: str
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Infrastructure configuration."""
|
||||
"""Infrastructure configuration.
|
||||
|
||||
This module re-exports all configuration classes and the global
|
||||
settings instance for application configuration.
|
||||
"""
|
||||
|
||||
from app.infrastructure.config.settings import (
|
||||
AppConfig,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Application settings with composition pattern."""
|
||||
"""Application settings with composition pattern.
|
||||
|
||||
This module defines the application configuration using pydantic-settings.
|
||||
Provides typed configuration for database, Keycloak, security, and app settings.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from functools import cached_property
|
||||
@@ -8,14 +12,38 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Environment(str, Enum):
|
||||
"""Application environment modes."""
|
||||
"""Application environment modes.
|
||||
|
||||
Defines the available deployment environments.
|
||||
Each environment may have different configuration defaults.
|
||||
|
||||
Attributes:
|
||||
DEV: Development environment with debug features.
|
||||
PROD: Production environment with strict security.
|
||||
|
||||
Example:
|
||||
>>> if settings.environment == Environment.PROD:
|
||||
... enable_strict_security()
|
||||
"""
|
||||
|
||||
DEV = "dev"
|
||||
PROD = "prod"
|
||||
|
||||
|
||||
class AppConfig(BaseSettings):
|
||||
"""Application configuration."""
|
||||
"""Application configuration.
|
||||
|
||||
Contains general application settings like name, host, and port.
|
||||
|
||||
Attributes:
|
||||
name: Application display name.
|
||||
debug: Debug mode flag.
|
||||
host: Server bind host.
|
||||
port: Server bind port.
|
||||
|
||||
Example:
|
||||
>>> config = AppConfig(name="My API", port=8000)
|
||||
"""
|
||||
|
||||
name: str = "Blog API"
|
||||
debug: bool = False
|
||||
@@ -30,14 +58,27 @@ class AppConfig(BaseSettings):
|
||||
|
||||
|
||||
class DBConfig(BaseSettings):
|
||||
"""Database configuration."""
|
||||
"""Database configuration.
|
||||
|
||||
Contains database connection settings. Supports both SQLite for
|
||||
development and PostgreSQL for production.
|
||||
|
||||
Attributes:
|
||||
url: Full database URL (optional, can build from components).
|
||||
echo: Enable SQL query logging.
|
||||
host: Database server host.
|
||||
port: Database server port.
|
||||
user: Database username.
|
||||
password: Database password.
|
||||
name: Database name.
|
||||
|
||||
Example:
|
||||
>>> db_config = DBConfig(host="localhost", name="blog")
|
||||
"""
|
||||
|
||||
# For dev: sqlite+aiosqlite:///./blog.db
|
||||
# For prod: postgresql+asyncpg://user:pass@host:port/db
|
||||
url: str | None = None
|
||||
echo: bool = False
|
||||
|
||||
# PostgreSQL-specific settings (used in prod)
|
||||
host: str = "localhost"
|
||||
port: int = 5432
|
||||
user: str = "postgres"
|
||||
@@ -53,7 +94,17 @@ class DBConfig(BaseSettings):
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_url(cls, v: str | None) -> str | None:
|
||||
"""Validate database URL if provided."""
|
||||
"""Validate database URL if provided.
|
||||
|
||||
Args:
|
||||
v: Database URL string to validate.
|
||||
|
||||
Returns:
|
||||
Validated URL string.
|
||||
|
||||
Raises:
|
||||
ValueError: If URL does not start with supported prefix.
|
||||
"""
|
||||
if v is None:
|
||||
return v
|
||||
if not any(v.startswith(prefix) for prefix in ("sqlite+", "postgresql+")):
|
||||
@@ -62,7 +113,20 @@ class DBConfig(BaseSettings):
|
||||
|
||||
|
||||
class KCConfig(BaseSettings):
|
||||
"""Keycloak configuration."""
|
||||
"""Keycloak configuration.
|
||||
|
||||
Contains Keycloak authentication server settings.
|
||||
|
||||
Attributes:
|
||||
server_url: Keycloak server base URL.
|
||||
realm: Keycloak realm name.
|
||||
client_id: OAuth client identifier.
|
||||
client_secret: OAuth client secret.
|
||||
token_cache_ttl: Token cache time-to-live in seconds.
|
||||
|
||||
Example:
|
||||
>>> kc = KCConfig(server_url="http://localhost:8080", realm="blog")
|
||||
"""
|
||||
|
||||
server_url: str = "http://localhost:8080"
|
||||
realm: str = "blog"
|
||||
@@ -71,7 +135,7 @@ class KCConfig(BaseSettings):
|
||||
default="",
|
||||
description="Keycloak client secret - must be set via env in production",
|
||||
)
|
||||
token_cache_ttl: int = 60 # seconds
|
||||
token_cache_ttl: int = 60
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="KC_",
|
||||
@@ -81,12 +145,26 @@ class KCConfig(BaseSettings):
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Check if Keycloak is properly configured."""
|
||||
"""Check if Keycloak is properly configured.
|
||||
|
||||
Returns:
|
||||
True if client_secret is set.
|
||||
"""
|
||||
return bool(self.client_secret)
|
||||
|
||||
|
||||
class SecurityConfig(BaseSettings):
|
||||
"""Security configuration."""
|
||||
"""Security configuration.
|
||||
|
||||
Contains security-related settings for JWT and authentication.
|
||||
|
||||
Attributes:
|
||||
secret_key: Secret key for JWT signing.
|
||||
access_token_expire_minutes: Token expiration time in minutes.
|
||||
|
||||
Example:
|
||||
>>> security = SecurityConfig(secret_key="super-secret-key")
|
||||
"""
|
||||
|
||||
secret_key: str = Field(
|
||||
default="", description="Secret key for JWT - must be set via env in production"
|
||||
@@ -101,17 +179,37 @@ class SecurityConfig(BaseSettings):
|
||||
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Check if security is properly configured."""
|
||||
"""Check if security is properly configured.
|
||||
|
||||
Returns:
|
||||
True if secret_key is set.
|
||||
"""
|
||||
return bool(self.secret_key)
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application configuration settings with composition."""
|
||||
"""Application configuration settings with composition.
|
||||
|
||||
Main settings class that composes all sub-configurations.
|
||||
Validates production settings and provides computed properties.
|
||||
|
||||
Attributes:
|
||||
environment: Current deployment environment.
|
||||
app: Application configuration.
|
||||
db: Database configuration.
|
||||
kc: Keycloak configuration.
|
||||
security: Security configuration.
|
||||
|
||||
Raises:
|
||||
ValueError: If required production settings are missing.
|
||||
|
||||
Example:
|
||||
>>> settings = Settings()
|
||||
>>> print(settings.database_url)
|
||||
"""
|
||||
|
||||
# Environment mode
|
||||
environment: Environment = Environment.DEV
|
||||
|
||||
# Sub-configurations
|
||||
app: AppConfig = Field(default_factory=AppConfig)
|
||||
db: DBConfig = Field(default_factory=DBConfig)
|
||||
kc: KCConfig = Field(default_factory=KCConfig)
|
||||
@@ -125,7 +223,13 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
def model_post_init(self, __context: object) -> None:
|
||||
"""Validate settings after initialization."""
|
||||
"""Validate settings after initialization.
|
||||
|
||||
Checks that required settings are configured for production mode.
|
||||
|
||||
Raises:
|
||||
ValueError: If required production settings are missing.
|
||||
"""
|
||||
if self.is_prod:
|
||||
if not self.security.is_configured:
|
||||
raise ValueError("SECURITY_SECRET_KEY must be set in production mode")
|
||||
@@ -136,14 +240,16 @@ class Settings(BaseSettings):
|
||||
def database_url(self) -> str:
|
||||
"""Get database URL based on environment.
|
||||
|
||||
- In dev: uses SQLite if no URL provided
|
||||
- In prod: uses PostgreSQL if no URL provided
|
||||
Returns configured URL or builds one from components.
|
||||
Uses SQLite for development, PostgreSQL for production.
|
||||
|
||||
Returns:
|
||||
Complete database URL string.
|
||||
"""
|
||||
if self.db.url:
|
||||
return self.db.url
|
||||
|
||||
if self.environment == Environment.PROD:
|
||||
# Build PostgreSQL URL from components
|
||||
return str(
|
||||
PostgresDsn.build(
|
||||
scheme="postgresql+asyncpg",
|
||||
@@ -155,19 +261,25 @@ class Settings(BaseSettings):
|
||||
)
|
||||
)
|
||||
|
||||
# Default dev SQLite URL
|
||||
return "sqlite+aiosqlite:///./blog.db"
|
||||
|
||||
@property
|
||||
def is_dev(self) -> bool:
|
||||
"""Check if running in development mode."""
|
||||
"""Check if running in development mode.
|
||||
|
||||
Returns:
|
||||
True if environment is DEV.
|
||||
"""
|
||||
return self.environment == Environment.DEV
|
||||
|
||||
@property
|
||||
def is_prod(self) -> bool:
|
||||
"""Check if running in production mode."""
|
||||
"""Check if running in production mode.
|
||||
|
||||
Returns:
|
||||
True if environment is PROD.
|
||||
"""
|
||||
return self.environment == Environment.PROD
|
||||
|
||||
|
||||
# Global settings instance
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Database infrastructure."""
|
||||
"""Database infrastructure.
|
||||
|
||||
This module re-exports database connection utilities and ORM models
|
||||
for data persistence.
|
||||
"""
|
||||
|
||||
from app.infrastructure.database.connection import (
|
||||
AsyncSessionLocal,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Database connection and session management."""
|
||||
"""Database connection and session management.
|
||||
|
||||
This module handles database engine creation, session management,
|
||||
and connection lifecycle for the application.
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
@@ -13,22 +17,26 @@ from sqlalchemy.ext.asyncio import (
|
||||
from app.infrastructure.config import settings
|
||||
|
||||
|
||||
# Convert SQLite URL to async format if needed
|
||||
def _get_database_url() -> str:
|
||||
"""Get database URL with SQLite async compatibility.
|
||||
|
||||
Converts SQLite URL to async format if needed.
|
||||
|
||||
Returns:
|
||||
Database URL string ready for async engine.
|
||||
"""
|
||||
url = settings.database_url
|
||||
if url.startswith("sqlite:///") and not url.startswith("sqlite+aiosqlite:///"):
|
||||
return url.replace("sqlite:///", "sqlite+aiosqlite:///")
|
||||
return url
|
||||
|
||||
|
||||
# Create async engine
|
||||
engine: AsyncEngine = create_async_engine(
|
||||
_get_database_url(),
|
||||
echo=settings.db.echo,
|
||||
future=True,
|
||||
)
|
||||
|
||||
# Create session factory
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
@@ -39,7 +47,11 @@ AsyncSessionLocal = async_sessionmaker(
|
||||
|
||||
|
||||
async def get_session() -> AsyncGenerator[AsyncSession]:
|
||||
"""Get database session."""
|
||||
"""Get database session.
|
||||
|
||||
Yields:
|
||||
AsyncSession instance for database operations.
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
@@ -49,7 +61,11 @@ async def get_session() -> AsyncGenerator[AsyncSession]:
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_session_context() -> AsyncGenerator[AsyncSession]:
|
||||
"""Get database session as context manager."""
|
||||
"""Get database session as context manager.
|
||||
|
||||
Yields:
|
||||
AsyncSession instance for database operations.
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
@@ -58,7 +74,11 @@ async def get_session_context() -> AsyncGenerator[AsyncSession]:
|
||||
|
||||
|
||||
async def init_db() -> None:
|
||||
"""Initialize database tables."""
|
||||
"""Initialize database tables.
|
||||
|
||||
Creates all tables defined in the metadata.
|
||||
Should be called on application startup.
|
||||
"""
|
||||
from app.infrastructure.database.models import Base
|
||||
|
||||
async with engine.begin() as conn:
|
||||
@@ -66,5 +86,9 @@ async def init_db() -> None:
|
||||
|
||||
|
||||
async def close_db() -> None:
|
||||
"""Close database connections."""
|
||||
"""Close database connections.
|
||||
|
||||
Disposes of the engine and all connections.
|
||||
Should be called on application shutdown.
|
||||
"""
|
||||
await engine.dispose()
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""SQLAlchemy ORM models."""
|
||||
"""SQLAlchemy ORM models.
|
||||
|
||||
This module defines the database ORM models that map to database tables.
|
||||
Models are used by repositories for data persistence.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from uuid import uuid4
|
||||
@@ -10,7 +14,25 @@ Base = declarative_base()
|
||||
|
||||
|
||||
class PostORM(Base): # type: ignore[valid-type,misc]
|
||||
"""SQLAlchemy model for Blog Post."""
|
||||
"""SQLAlchemy model for Blog Post.
|
||||
|
||||
Database table representation of blog posts.
|
||||
Maps to the 'posts' table with all post attributes.
|
||||
|
||||
Attributes:
|
||||
id: Primary key as UUID string.
|
||||
title: Post title (max 200 chars).
|
||||
content: Post content (text).
|
||||
slug: URL-friendly unique identifier.
|
||||
author_id: Author reference.
|
||||
published: Publication status flag.
|
||||
tags: JSON array of tags.
|
||||
created_at: Creation timestamp.
|
||||
updated_at: Last update timestamp.
|
||||
|
||||
Example:
|
||||
>>> post = PostORM(title="Post", content="...", slug="post", author_id="user-1")
|
||||
"""
|
||||
|
||||
__tablename__ = "posts"
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Dependency Injection using Dishka."""
|
||||
"""Dependency Injection using Dishka.
|
||||
|
||||
This module provides DI container setup and configuration
|
||||
for the application using Dishka library.
|
||||
"""
|
||||
|
||||
from app.infrastructure.di.container import create_container
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Dishka providers for dependency injection."""
|
||||
"""Dishka providers for dependency injection.
|
||||
|
||||
This module defines Dishka providers for all application dependencies.
|
||||
Providers configure how dependencies are created and scoped.
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
@@ -22,16 +26,31 @@ from app.infrastructure.repositories.post import SQLAlchemyPostRepository
|
||||
|
||||
|
||||
class DatabaseProvider(Provider):
|
||||
"""Provider for database-related dependencies."""
|
||||
"""Provider for database-related dependencies.
|
||||
|
||||
Provides database engine and session scoped appropriately.
|
||||
Engine is application-scoped, sessions are request-scoped.
|
||||
|
||||
Example:
|
||||
>>> provider = DatabaseProvider()
|
||||
"""
|
||||
|
||||
@provide(scope=Scope.APP)
|
||||
def get_engine(self) -> AsyncEngine:
|
||||
"""Provide SQLAlchemy engine."""
|
||||
"""Provide SQLAlchemy engine.
|
||||
|
||||
Returns:
|
||||
AsyncEngine instance for database operations.
|
||||
"""
|
||||
return engine
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
async def get_session(self) -> AsyncGenerator[AsyncSession]:
|
||||
"""Provide database session per request."""
|
||||
"""Provide database session per request.
|
||||
|
||||
Yields:
|
||||
AsyncSession instance for the request lifetime.
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
@@ -40,27 +59,62 @@ class DatabaseProvider(Provider):
|
||||
|
||||
|
||||
class RepositoryProvider(Provider):
|
||||
"""Provider for repository implementations."""
|
||||
"""Provider for repository implementations.
|
||||
|
||||
Provides concrete repository implementations for interfaces.
|
||||
All repositories are request-scoped.
|
||||
|
||||
Example:
|
||||
>>> provider = RepositoryProvider()
|
||||
"""
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_post_repository(self, session: AsyncSession) -> PostRepository:
|
||||
"""Provide PostRepository implementation."""
|
||||
"""Provide PostRepository implementation.
|
||||
|
||||
Args:
|
||||
session: Database session from DI container.
|
||||
|
||||
Returns:
|
||||
SQLAlchemyPostRepository instance.
|
||||
"""
|
||||
return SQLAlchemyPostRepository(session)
|
||||
|
||||
|
||||
class TransactionManagerProvider(Provider):
|
||||
"""Provider for transaction manager."""
|
||||
"""Provider for transaction manager.
|
||||
|
||||
Provides transaction manager implementation for use cases.
|
||||
Scoped per request for transaction isolation.
|
||||
|
||||
Example:
|
||||
>>> provider = TransactionManagerProvider()
|
||||
"""
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_transaction_manager(self, session: AsyncSession) -> TransactionManager:
|
||||
"""Provide TransactionManager implementation."""
|
||||
"""Provide TransactionManager implementation.
|
||||
|
||||
Args:
|
||||
session: Database session from DI container.
|
||||
|
||||
Returns:
|
||||
SessionTransactionManager instance.
|
||||
"""
|
||||
from app.infrastructure.di.transaction_manager import SessionTransactionManager
|
||||
|
||||
return SessionTransactionManager(session)
|
||||
|
||||
|
||||
class UseCaseProvider(Provider):
|
||||
"""Provider for use cases."""
|
||||
"""Provider for use cases.
|
||||
|
||||
Provides all application use cases with their dependencies.
|
||||
All use cases are request-scoped for transaction isolation.
|
||||
|
||||
Example:
|
||||
>>> provider = UseCaseProvider()
|
||||
"""
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_create_post_use_case(
|
||||
@@ -68,7 +122,15 @@ class UseCaseProvider(Provider):
|
||||
post_repo: PostRepository,
|
||||
tx_manager: TransactionManager,
|
||||
) -> CreatePostUseCase:
|
||||
"""Provide CreatePostUseCase."""
|
||||
"""Provide CreatePostUseCase.
|
||||
|
||||
Args:
|
||||
post_repo: Post repository dependency.
|
||||
tx_manager: Transaction manager dependency.
|
||||
|
||||
Returns:
|
||||
Configured CreatePostUseCase instance.
|
||||
"""
|
||||
return CreatePostUseCase(
|
||||
post_repo=post_repo,
|
||||
tx_manager=tx_manager,
|
||||
@@ -80,7 +142,15 @@ class UseCaseProvider(Provider):
|
||||
post_repo: PostRepository,
|
||||
tx_manager: TransactionManager,
|
||||
) -> GetPostUseCase:
|
||||
"""Provide GetPostUseCase."""
|
||||
"""Provide GetPostUseCase.
|
||||
|
||||
Args:
|
||||
post_repo: Post repository dependency.
|
||||
tx_manager: Transaction manager dependency.
|
||||
|
||||
Returns:
|
||||
Configured GetPostUseCase instance.
|
||||
"""
|
||||
return GetPostUseCase(
|
||||
post_repo=post_repo,
|
||||
tx_manager=tx_manager,
|
||||
@@ -92,7 +162,15 @@ class UseCaseProvider(Provider):
|
||||
post_repo: PostRepository,
|
||||
tx_manager: TransactionManager,
|
||||
) -> UpdatePostUseCase:
|
||||
"""Provide UpdatePostUseCase."""
|
||||
"""Provide UpdatePostUseCase.
|
||||
|
||||
Args:
|
||||
post_repo: Post repository dependency.
|
||||
tx_manager: Transaction manager dependency.
|
||||
|
||||
Returns:
|
||||
Configured UpdatePostUseCase instance.
|
||||
"""
|
||||
return UpdatePostUseCase(
|
||||
post_repo=post_repo,
|
||||
tx_manager=tx_manager,
|
||||
@@ -104,7 +182,15 @@ class UseCaseProvider(Provider):
|
||||
post_repo: PostRepository,
|
||||
tx_manager: TransactionManager,
|
||||
) -> DeletePostUseCase:
|
||||
"""Provide DeletePostUseCase."""
|
||||
"""Provide DeletePostUseCase.
|
||||
|
||||
Args:
|
||||
post_repo: Post repository dependency.
|
||||
tx_manager: Transaction manager dependency.
|
||||
|
||||
Returns:
|
||||
Configured DeletePostUseCase instance.
|
||||
"""
|
||||
return DeletePostUseCase(
|
||||
post_repo=post_repo,
|
||||
tx_manager=tx_manager,
|
||||
@@ -116,7 +202,15 @@ class UseCaseProvider(Provider):
|
||||
post_repo: PostRepository,
|
||||
tx_manager: TransactionManager,
|
||||
) -> ListPostsUseCase:
|
||||
"""Provide ListPostsUseCase."""
|
||||
"""Provide ListPostsUseCase.
|
||||
|
||||
Args:
|
||||
post_repo: Post repository dependency.
|
||||
tx_manager: Transaction manager dependency.
|
||||
|
||||
Returns:
|
||||
Configured ListPostsUseCase instance.
|
||||
"""
|
||||
return ListPostsUseCase(
|
||||
post_repo=post_repo,
|
||||
tx_manager=tx_manager,
|
||||
@@ -128,7 +222,15 @@ class UseCaseProvider(Provider):
|
||||
post_repo: PostRepository,
|
||||
tx_manager: TransactionManager,
|
||||
) -> PublishPostUseCase:
|
||||
"""Provide PublishPostUseCase."""
|
||||
"""Provide PublishPostUseCase.
|
||||
|
||||
Args:
|
||||
post_repo: Post repository dependency.
|
||||
tx_manager: Transaction manager dependency.
|
||||
|
||||
Returns:
|
||||
Configured PublishPostUseCase instance.
|
||||
"""
|
||||
return PublishPostUseCase(
|
||||
post_repo=post_repo,
|
||||
tx_manager=tx_manager,
|
||||
@@ -136,9 +238,20 @@ class UseCaseProvider(Provider):
|
||||
|
||||
|
||||
class KeycloakProvider(Provider):
|
||||
"""Provider for Keycloak authentication client."""
|
||||
"""Provider for Keycloak authentication client.
|
||||
|
||||
Provides Keycloak client as application-scoped singleton.
|
||||
Client is stateless and can be shared across requests.
|
||||
|
||||
Example:
|
||||
>>> provider = KeycloakProvider()
|
||||
"""
|
||||
|
||||
@provide(scope=Scope.APP)
|
||||
def get_keycloak_client(self) -> KeycloakAuthClient:
|
||||
"""Provide KeycloakAuthClient singleton."""
|
||||
"""Provide KeycloakAuthClient singleton.
|
||||
|
||||
Returns:
|
||||
KeycloakAuthClient instance.
|
||||
"""
|
||||
return KeycloakAuthClient(settings)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""SQLAlchemy implementation of Transaction Manager."""
|
||||
"""SQLAlchemy implementation of Transaction Manager.
|
||||
|
||||
This module provides the concrete implementation of TransactionManager
|
||||
using SQLAlchemy async sessions for transaction control.
|
||||
"""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -6,19 +10,44 @@ from app.application.interfaces import TransactionManager
|
||||
|
||||
|
||||
class SessionTransactionManager(TransactionManager):
|
||||
"""SQLAlchemy Session-based Transaction Manager."""
|
||||
"""SQLAlchemy Session-based Transaction Manager.
|
||||
|
||||
Implements transaction control using SQLAlchemy async session.
|
||||
Tracks commit state to prevent duplicate commits.
|
||||
|
||||
Attributes:
|
||||
_session: SQLAlchemy async session for transactions.
|
||||
_committed: Flag indicating if transaction was committed.
|
||||
|
||||
Example:
|
||||
>>> tx_manager = SessionTransactionManager(session)
|
||||
>>> await tx_manager.commit()
|
||||
"""
|
||||
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
"""Initialize transaction manager.
|
||||
|
||||
Args:
|
||||
session: SQLAlchemy async session instance.
|
||||
"""
|
||||
self._session = session
|
||||
self._committed: bool = False
|
||||
|
||||
async def commit(self) -> None:
|
||||
"""Commit the current transaction."""
|
||||
"""Commit the current transaction.
|
||||
|
||||
Persists all pending changes to the database.
|
||||
Only commits once - subsequent calls are no-ops.
|
||||
"""
|
||||
if not self._committed:
|
||||
await self._session.commit()
|
||||
self._committed = True
|
||||
|
||||
async def rollback(self) -> None:
|
||||
"""Rollback the current transaction."""
|
||||
"""Rollback the current transaction.
|
||||
|
||||
Discards all pending changes.
|
||||
Only rolls back if not already committed.
|
||||
"""
|
||||
if not self._committed:
|
||||
await self._session.rollback()
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Infrastructure middleware."""
|
||||
"""Infrastructure middleware.
|
||||
|
||||
This module re-exports exception handling middleware for
|
||||
centralized error management in the application.
|
||||
"""
|
||||
|
||||
from app.infrastructure.middleware.error_handler import (
|
||||
domain_exception_handler,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Exception handling middleware."""
|
||||
"""Exception handling middleware.
|
||||
|
||||
This module provides exception handlers for FastAPI application.
|
||||
Maps domain exceptions to appropriate HTTP status codes.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
@@ -17,7 +21,14 @@ from app.domain.exceptions import (
|
||||
|
||||
|
||||
def get_status_code(exc: DomainException) -> int:
|
||||
"""Map domain exceptions to HTTP status codes."""
|
||||
"""Map domain exceptions to HTTP status codes.
|
||||
|
||||
Args:
|
||||
exc: Domain exception instance.
|
||||
|
||||
Returns:
|
||||
HTTP status code for the exception type.
|
||||
"""
|
||||
match exc:
|
||||
case ValidationException():
|
||||
return 400
|
||||
@@ -34,7 +45,17 @@ def get_status_code(exc: DomainException) -> int:
|
||||
|
||||
|
||||
async def domain_exception_handler(request: Request, exc: DomainException) -> JSONResponse:
|
||||
"""Handle domain exceptions."""
|
||||
"""Handle domain exceptions.
|
||||
|
||||
Converts domain exceptions to JSON error responses.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
exc: Domain exception instance.
|
||||
|
||||
Returns:
|
||||
JSONResponse with error details.
|
||||
"""
|
||||
status_code = get_status_code(exc)
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
@@ -48,7 +69,17 @@ async def domain_exception_handler(request: Request, exc: DomainException) -> JS
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
|
||||
"""Handle HTTP exceptions."""
|
||||
"""Handle HTTP exceptions.
|
||||
|
||||
Converts Starlette HTTP exceptions to JSON error responses.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
exc: Starlette HTTP exception instance.
|
||||
|
||||
Returns:
|
||||
JSONResponse with error details.
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
@@ -61,7 +92,18 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException)
|
||||
|
||||
|
||||
async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""Handle generic exceptions."""
|
||||
"""Handle generic exceptions.
|
||||
|
||||
Converts unhandled exceptions to generic error responses.
|
||||
Hides internal details for security.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
exc: Generic exception instance.
|
||||
|
||||
Returns:
|
||||
JSONResponse with generic error message.
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
@@ -74,16 +116,16 @@ async def generic_exception_handler(request: Request, exc: Exception) -> JSONRes
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI) -> None:
|
||||
"""Register all exception handlers with FastAPI app."""
|
||||
"""Register all exception handlers with FastAPI app.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance.
|
||||
|
||||
Raises:
|
||||
TypeError: If app is not a FastAPI instance.
|
||||
"""
|
||||
if not isinstance(app, FastAPI):
|
||||
raise TypeError("app must be a FastAPI instance")
|
||||
|
||||
# Domain exceptions
|
||||
app.add_exception_handler(DomainException, domain_exception_handler) # type: ignore[arg-type]
|
||||
|
||||
# HTTP exceptions
|
||||
app.add_exception_handler(StarletteHTTPException, http_exception_handler) # type: ignore[arg-type]
|
||||
|
||||
# Generic exceptions (only in production)
|
||||
# In development, let FastAPI show detailed traceback
|
||||
# app.add_exception_handler(Exception, generic_exception_handler)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Repository implementations."""
|
||||
"""Repository implementations.
|
||||
|
||||
This module re-exports concrete repository implementations
|
||||
for data access using SQLAlchemy ORM.
|
||||
"""
|
||||
|
||||
from app.infrastructure.repositories.post import SQLAlchemyPostRepository
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""SQLAlchemy implementation of PostRepository."""
|
||||
"""SQLAlchemy implementation of PostRepository.
|
||||
|
||||
This module provides the concrete implementation of PostRepository
|
||||
using SQLAlchemy ORM for data persistence.
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
@@ -12,13 +16,36 @@ from app.infrastructure.database.models import PostORM
|
||||
|
||||
|
||||
class SQLAlchemyPostRepository(PostRepository):
|
||||
"""SQLAlchemy implementation of Post repository."""
|
||||
"""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."""
|
||||
"""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),
|
||||
@@ -32,7 +59,14 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
)
|
||||
|
||||
def _to_orm(self, post: Post) -> PostORM:
|
||||
"""Convert domain entity to ORM model."""
|
||||
"""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,
|
||||
@@ -46,25 +80,43 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
)
|
||||
|
||||
async def get_by_id(self, entity_id: UUID) -> Post | None:
|
||||
"""Get post by ID."""
|
||||
"""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."""
|
||||
"""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."""
|
||||
"""Add new post.
|
||||
|
||||
Args:
|
||||
entity: Post entity to add.
|
||||
"""
|
||||
orm = self._to_orm(entity)
|
||||
self._session.add(orm)
|
||||
# Commit делает TransactionManager
|
||||
|
||||
async def update(self, entity: Post) -> None:
|
||||
"""Update existing post."""
|
||||
"""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()
|
||||
|
||||
@@ -75,22 +127,38 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
orm.tags = entity.tags
|
||||
orm.updated_at = entity.updated_at
|
||||
|
||||
# Commit делает TransactionManager
|
||||
|
||||
async def delete(self, entity_id: UUID) -> None:
|
||||
"""Delete post by ID."""
|
||||
"""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."""
|
||||
"""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."""
|
||||
"""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
|
||||
@@ -101,7 +169,16 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Post]:
|
||||
"""Get posts by author."""
|
||||
"""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)
|
||||
@@ -116,7 +193,15 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Post]:
|
||||
"""Get published posts."""
|
||||
"""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)
|
||||
@@ -132,7 +217,16 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
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 Post entities with the tag.
|
||||
"""
|
||||
query = select(PostORM).where(PostORM.tags.contains([tag]))
|
||||
if limit is not None:
|
||||
query = query.limit(limit)
|
||||
@@ -143,7 +237,14 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
return [self._to_domain(orm) for orm in orms]
|
||||
|
||||
async def slug_exists(self, slug: str) -> bool:
|
||||
"""Check if slug exists."""
|
||||
"""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
|
||||
|
||||
@@ -153,7 +254,16 @@ class SQLAlchemyPostRepository(PostRepository):
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> list[Post]:
|
||||
"""Search posts."""
|
||||
"""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_(
|
||||
|
||||
Reference in New Issue
Block a user