style: apply ruff formatting to source and test files
All checks were successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/type Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful

This commit is contained in:
2026-05-02 12:05:14 +03:00
parent 1dbedf0f52
commit 14adcaa3e6
16 changed files with 50 additions and 95 deletions

View File

@@ -27,9 +27,7 @@ class CreatePostUseCase:
# Check if slug already exists
if await self._post_repo.slug_exists(slug.value):
raise AlreadyExistsException(
f"Post with slug '{slug.value}' already exists"
)
raise AlreadyExistsException(f"Post with slug '{slug.value}' already exists")
# Create domain entity
post = Post.create(

View File

@@ -38,9 +38,7 @@ class ListPostsUseCase:
offset: int | None = None,
) -> list[PostResponseDTO]:
"""Get posts by author."""
posts = await self._post_repo.get_by_author(
author_id, limit=limit, offset=offset
)
posts = await self._post_repo.get_by_author(author_id, limit=limit, offset=offset)
return [self._map_to_dto(post) for post in posts]
async def by_tag(

View File

@@ -2,7 +2,7 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from uuid import UUID, uuid4
@@ -12,8 +12,8 @@ class BaseEntity(ABC):
"""Base class for all domain entities."""
id: UUID = field(default_factory=uuid4)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
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:
if not isinstance(other, BaseEntity):
@@ -25,7 +25,7 @@ class BaseEntity(ABC):
def touch(self) -> None:
"""Update the updated_at timestamp."""
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
@abstractmethod
def to_dict(self) -> dict[str, Any]:

View File

@@ -1,8 +1,9 @@
"""Role-based access control definitions."""
from collections.abc import Callable
from enum import Enum
from functools import wraps
from typing import Any, Callable
from typing import Any
from app.domain.exceptions import ForbiddenException

View File

@@ -19,9 +19,7 @@ class Slug(ValueObject[str]):
if len(self.value) > self.MAX_LENGTH:
raise ValueError(f"Slug must be at most {self.MAX_LENGTH} characters")
if not re.match(self.SLUG_PATTERN, self.value):
raise ValueError(
"Slug must contain only lowercase letters, numbers, and hyphens"
)
raise ValueError("Slug must contain only lowercase letters, numbers, and hyphens")
@classmethod
def from_title(cls, title: str) -> "Slug":

View File

@@ -1,7 +1,7 @@
"""Database connection and session management."""
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import (
AsyncEngine,
@@ -38,7 +38,7 @@ AsyncSessionLocal = async_sessionmaker(
)
async def get_session() -> AsyncGenerator[AsyncSession, None]:
async def get_session() -> AsyncGenerator[AsyncSession]:
"""Get database session."""
async with AsyncSessionLocal() as session:
try:
@@ -48,7 +48,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
@asynccontextmanager
async def get_session_context() -> AsyncGenerator[AsyncSession, None]:
async def get_session_context() -> AsyncGenerator[AsyncSession]:
"""Get database session as context manager."""
async with AsyncSessionLocal() as session:
try:

View File

@@ -1,6 +1,6 @@
"""SQLAlchemy ORM models."""
from datetime import datetime, timezone
from datetime import UTC, datetime
from uuid import uuid4
from sqlalchemy import JSON, Boolean, DateTime, String, Text
@@ -14,27 +14,21 @@ class PostORM(Base): # type: ignore[valid-type,misc]
__tablename__ = "posts"
id: Mapped[str] = mapped_column(
String(36), primary_key=True, default=lambda: str(uuid4())
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
title: Mapped[str] = mapped_column(String(200), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
slug: Mapped[str] = mapped_column(
String(200), nullable=False, unique=True, index=True
)
slug: Mapped[str] = mapped_column(String(200), nullable=False, unique=True, index=True)
author_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
published: Mapped[bool] = mapped_column(
Boolean, default=False, nullable=False, index=True
)
published: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
tags: Mapped[list[str]] = mapped_column(JSON, default=list)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
default=lambda: datetime.now(UTC),
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
default=lambda: datetime.now(UTC),
onupdate=lambda: datetime.now(UTC),
nullable=False,
)

View File

@@ -1,6 +1,6 @@
"""Dishka providers for dependency injection."""
from typing import AsyncGenerator
from collections.abc import AsyncGenerator
from dishka import Provider, Scope, provide
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
@@ -30,7 +30,7 @@ class DatabaseProvider(Provider):
return engine
@provide(scope=Scope.REQUEST)
async def get_session(self) -> AsyncGenerator[AsyncSession, None]:
async def get_session(self) -> AsyncGenerator[AsyncSession]:
"""Provide database session per request."""
async with AsyncSessionLocal() as session:
try:

View File

@@ -1,6 +1,6 @@
"""Exception handling middleware."""
from datetime import datetime, timezone
from datetime import UTC, datetime
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
@@ -33,9 +33,7 @@ def get_status_code(exc: DomainException) -> int:
return 500
async def domain_exception_handler(
request: Request, exc: DomainException
) -> JSONResponse:
async def domain_exception_handler(request: Request, exc: DomainException) -> JSONResponse:
"""Handle domain exceptions."""
status_code = get_status_code(exc)
return JSONResponse(
@@ -43,22 +41,20 @@ async def domain_exception_handler(
content={
"error": exc.__class__.__name__,
"message": exc.message,
"timestamp": datetime.now(timezone.utc).isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
"path": str(request.url.path),
},
)
async def http_exception_handler(
request: Request, exc: StarletteHTTPException
) -> JSONResponse:
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
"""Handle HTTP exceptions."""
return JSONResponse(
status_code=exc.status_code,
content={
"error": "HTTPException",
"message": str(exc.detail),
"timestamp": datetime.now(timezone.utc).isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
"path": str(request.url.path),
},
)
@@ -71,7 +67,7 @@ async def generic_exception_handler(request: Request, exc: Exception) -> JSONRes
content={
"error": "InternalServerError",
"message": "An unexpected error occurred",
"timestamp": datetime.now(timezone.utc).isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
"path": str(request.url.path),
},
)

View File

@@ -47,9 +47,7 @@ class SQLAlchemyPostRepository(PostRepository):
async def get_by_id(self, entity_id: UUID) -> Post | None:
"""Get post by ID."""
result = await self._session.execute(
select(PostORM).where(PostORM.id == str(entity_id))
)
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
@@ -67,9 +65,7 @@ class SQLAlchemyPostRepository(PostRepository):
async def update(self, entity: Post) -> None:
"""Update existing post."""
result = await self._session.execute(
select(PostORM).where(PostORM.id == str(entity.id))
)
result = await self._session.execute(select(PostORM).where(PostORM.id == str(entity.id)))
orm = result.scalar_one()
orm.title = entity.title.value
@@ -83,25 +79,19 @@ class SQLAlchemyPostRepository(PostRepository):
async def delete(self, entity_id: UUID) -> None:
"""Delete post by ID."""
result = await self._session.execute(
select(PostORM).where(PostORM.id == str(entity_id))
)
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."""
result = await self._session.execute(
select(PostORM).where(PostORM.id == str(entity_id))
)
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."""
result = await self._session.execute(
select(PostORM).where(PostORM.slug == slug)
)
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
@@ -154,9 +144,7 @@ class SQLAlchemyPostRepository(PostRepository):
async def slug_exists(self, slug: str) -> bool:
"""Check if slug exists."""
result = await self._session.execute(
select(PostORM).where(PostORM.slug == slug)
)
result = await self._session.execute(select(PostORM).where(PostORM.slug == slug))
return result.scalar_one_or_none() is not None
async def search(

View File

@@ -1,7 +1,7 @@
"""Application entry point with DDD architecture."""
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import AsyncGenerator
import uvicorn
from dishka import make_async_container
@@ -21,7 +21,7 @@ from app.presentation import router
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
"""Application lifespan manager."""
# Startup
await init_db()

View File

@@ -1,6 +1,6 @@
"""API test fixtures."""
from typing import AsyncGenerator
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -25,7 +25,7 @@ def mock_keycloak_client() -> MagicMock:
@pytest.fixture
async def client(mock_keycloak_client: MagicMock) -> AsyncGenerator[AsyncClient, None]:
async def client(mock_keycloak_client: MagicMock) -> AsyncGenerator[AsyncClient]:
"""Create async HTTP client for API testing."""
with patch(
"app.presentation.api.deps.KeycloakAuthClient",

View File

@@ -1,14 +1,14 @@
# E2E test fixtures
# Provides: full application state, end-to-end workflows, cleanup
from typing import AsyncGenerator
from collections.abc import AsyncGenerator
import pytest
from fastapi import FastAPI
@pytest.fixture
async def e2e_app() -> AsyncGenerator[FastAPI, None]:
async def e2e_app() -> AsyncGenerator[FastAPI]:
"""Create full application instance for E2E testing."""
from app.main import app_factory

View File

@@ -1,6 +1,6 @@
"""Integration test fixtures."""
from typing import AsyncGenerator
from collections.abc import AsyncGenerator
import pytest
from sqlalchemy.ext.asyncio import (
@@ -39,7 +39,7 @@ def session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
@pytest.fixture(autouse=True)
async def setup_db(engine: AsyncEngine) -> AsyncGenerator[None, None]:
async def setup_db(engine: AsyncEngine) -> AsyncGenerator[None]:
"""Setup database tables for each test."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@@ -51,7 +51,7 @@ async def setup_db(engine: AsyncEngine) -> AsyncGenerator[None, None]:
@pytest.fixture
async def db_session(
session_factory: async_sessionmaker[AsyncSession],
) -> AsyncGenerator[AsyncSession, None]:
) -> AsyncGenerator[AsyncSession]:
"""Create database session for testing."""
async with session_factory() as session:
yield session

View File

@@ -123,9 +123,7 @@ class TestKeycloakAuthClient:
"""Create Keycloak client."""
return KeycloakAuthClient(settings)
def test_client_initialization(
self, client: KeycloakAuthClient, settings: Settings
) -> None:
def test_client_initialization(self, client: KeycloakAuthClient, settings: Settings) -> None:
"""Test client initialization."""
assert client._settings == settings
assert client._base_url == "http://localhost:8080/realms/test-realm"
@@ -144,10 +142,7 @@ class TestKeycloakAuthClient:
def test_get_userinfo_url(self, client: KeycloakAuthClient) -> None:
"""Test userinfo URL generation."""
url = client._get_userinfo_url()
assert (
url
== "http://localhost:8080/realms/test-realm/protocol/openid-connect/userinfo"
)
assert url == "http://localhost:8080/realms/test-realm/protocol/openid-connect/userinfo"
@pytest.mark.asyncio
async def test_introspect_token_success(self, client: KeycloakAuthClient) -> None:
@@ -196,18 +191,14 @@ class TestKeycloakAuthClient:
assert result.is_valid is False
@pytest.mark.asyncio
async def test_introspect_token_http_error(
self, client: KeycloakAuthClient
) -> None:
async def test_introspect_token_http_error(self, client: KeycloakAuthClient) -> None:
"""Test introspection with HTTP error."""
import httpx
mock_async_client = AsyncMock()
mock_async_client.__aenter__ = AsyncMock(return_value=mock_async_client)
mock_async_client.__aexit__ = AsyncMock(return_value=None)
mock_async_client.post = AsyncMock(
side_effect=httpx.HTTPError("Connection error")
)
mock_async_client.post = AsyncMock(side_effect=httpx.HTTPError("Connection error"))
with patch("httpx.AsyncClient", return_value=mock_async_client):
result = await client.introspect_token("test-token")
@@ -216,9 +207,7 @@ class TestKeycloakAuthClient:
assert result.is_valid is False
@pytest.mark.asyncio
async def test_introspect_token_uses_cache(
self, client: KeycloakAuthClient
) -> None:
async def test_introspect_token_uses_cache(self, client: KeycloakAuthClient) -> None:
"""Test that token introspection uses cache."""
mock_response = Mock()
mock_response.json.return_value = {
@@ -283,9 +272,7 @@ class TestKeycloakAuthClient:
mock_async_client = AsyncMock()
mock_async_client.__aenter__ = AsyncMock(return_value=mock_async_client)
mock_async_client.__aexit__ = AsyncMock(return_value=None)
mock_async_client.get = AsyncMock(
side_effect=httpx.HTTPError("Connection error")
)
mock_async_client.get = AsyncMock(side_effect=httpx.HTTPError("Connection error"))
with patch("httpx.AsyncClient", return_value=mock_async_client):
result = await client.get_userinfo("test-token")
@@ -293,9 +280,7 @@ class TestKeycloakAuthClient:
assert result is None
@pytest.mark.asyncio
async def test_introspect_token_no_realm_roles(
self, client: KeycloakAuthClient
) -> None:
async def test_introspect_token_no_realm_roles(self, client: KeycloakAuthClient) -> None:
"""Test introspection without realm_access roles."""
mock_response = Mock()
mock_response.json.return_value = {

View File

@@ -129,10 +129,7 @@ class TestSettings:
security=SecurityConfig(secret_key="test"),
kc=KCConfig(client_secret="test"),
)
assert (
s.database_url
== "postgresql+asyncpg://admin:secret@db.example.com:5433/mydb"
)
assert s.database_url == "postgresql+asyncpg://admin:secret@db.example.com:5433/mydb"
def test_database_url_override(self) -> None:
"""Test that explicit database URL overrides auto-building."""