Files
blog.pyaqa.ru/AGENTS.md

20 KiB
Raw Permalink Blame History

Blog AGENTS.md

Generated: 2026-05-03 22:15 UTC
Commit: 41f2a3d
Branch: feature/tests

Stack

  • Python 3.13+, FastAPI, pydantic, uvicorn
  • SQLAlchemy 2.0 (async), aiosqlite
  • Package manager: uv
  • CI: Woodpecker (lint, test, type on push/PR to dev)

Commands

uv sync --group dev         # Install all dev dependencies
uv run pytest               # Run tests (coverage >= 70% required)
uv run pytest tests/unit/   # Run single test directory
uv run ruff check . --fix   # Lint
uv run ruff format          # Format
uv run isort .              # Sort imports
uv run mypy .               # Type check (strict mode)
uv run blog                 # Start dev server (port 8000)

Pre-commit order

ruff check --fixruff formatisortmypy

DDD Architecture

Layer Structure

app/
├── domain/                   # Domain Layer - business logic, no dependencies
│   ├── entities/            # Domain entities (Post, User, etc.)
│   │   ├── base.py          # Base entity class
│   │   └── post.py          # Post entity with business logic
│   ├── value_objects/       # Value objects (Title, Content, Slug)
│   │   ├── base.py
│   │   ├── title.py
│   │   ├── content.py
│   │   └── slug.py
│   ├── repositories/        # Repository interfaces (abstract)
│   │   ├── base.py
│   │   └── post.py
│   └── exceptions.py        # Domain exceptions
│
├── application/             # Application Layer - use cases
│   ├── dtos/               # Data Transfer Objects
│   │   └── post.py
│   ├── interfaces/         # Abstract interfaces (UoW)
│   │   └── unit_of_work.py
│   └── use_cases/          # Use cases (CQRS-like)
│       ├── create_post.py
│       ├── get_post.py
│       ├── update_post.py
│       ├── delete_post.py
│       ├── list_posts.py
│       └── publish_post.py
│
├── infrastructure/          # Infrastructure Layer - external concerns
│   ├── config/             # Configuration
│   │   └── settings.py
│   ├── database/           # Database connection & ORM models
│   │   ├── connection.py
│   │   └── models.py
│   ├── repositories/       # Repository implementations
│   │   ├── post.py         # SQLAlchemyPostRepository
│   │   └── unit_of_work.py # SQLAlchemyUnitOfWork
│   ├── di/                 # Dependency Injection
│   │   └── container.py
│   └── middleware/         # Exception handlers
│       └── error_handler.py
│
├── presentation/            # Presentation Layer - API
│   ├── api/                # FastAPI routes
│   │   ├── v1/            # API version 1
│   │   │   ├── __init__.py
│   │   │   └── posts.py   # Posts endpoints
│   │   ├── deps.py        # FastAPI dependencies
│   │   └── __init__.py
│   └── schemas/           # Pydantic schemas
│       └── post.py
│
└── main.py                # Application entry point

tests/
├── unit/                  # Unit tests (domain, use cases)
│   ├── domain/           # Domain layer tests
│   ├── application/      # Application layer tests
│   └── infrastructure/   # Infrastructure tests
├── integration/          # Integration tests (DB, repos)
├── api/                  # API endpoint tests
└── e2e/                  # End-to-end tests

Where to Look

Task Location Notes
Add a new use case app/application/use_cases/ Follow naming: {action}_post.py
Add a new API endpoint app/presentation/api/v1/posts.py Or create new module in v1/
Add a new web page app/presentation/web/routes.py Integrate real use cases, not mocks
Add a domain entity app/domain/entities/ Inherit from BaseEntity, add to domain/__init__.py
Add a repository method app/infrastructure/repositories/post.py Mirror in app/domain/repositories/post.py
Configure DI provider app/infrastructure/di/providers.py Add to existing provider class or create new one
Change database schema app/infrastructure/database/models.py Mirror changes in domain entity
Add/modify tests tests/unit/{layer}/ Mirror app/ structure exactly
Run linting uv run ruff check . --fix Pre-commit: ruff → ruff format → isort → mypy
Run tests uv run pytest Coverage auto-collected, HTML report at htmlcov/
Run type check uv run mypy . Strict mode; excludes tests/e2e

Code Map

Symbol Type Location Refs Role
app_factory Function app/main.py:50 3 FastAPI app factory with DI lifespan
SQLAlchemyPostRepository Class app/infrastructure/repositories/post.py:18 1 Concrete repository implementation
Post Class app/domain/entities/post.py:17 1 Core domain entity
PostRepository Class app/domain/repositories/post.py:13 1 Repository interface
CreatePostUseCase Class app/application/use_cases/create_post.py:14 1 Use case for creating posts
home Function app/presentation/web/routes.py:189 1 Web home page route
create_post Function app/presentation/api/v1/posts.py:35 1 API create post endpoint

Key Conventions

Dependency Rule

  • Domain layer has NO dependencies on other layers
  • Application layer depends only on Domain
  • Infrastructure depends on Domain and Application
  • Presentation depends on all other layers

Testing

  • Unit tests: Test domain logic without DB/external services

Code Patterns

  • Use dataclasses for entities and value objects
  • Use frozen dataclasses for value objects (immutable)
  • Use Unit of Work pattern for transactions
  • Use Repository pattern for data access
  • Use Dependency Injection via FastAPI's Depends()

Anti-Patterns (This Project)

  • NO inline comments — Self-documenting code only; Google-style docstrings required
  • NO type suppression — Never use typing.Any casts or # type: ignore to bypass mypy strict mode
  • Dead code: create_container() in app/infrastructure/di/container.py is defined but never used; main.py calls make_async_container() directly
  • Empty directories: app/domain/exceptions/ and app/presentation/api/deps/ are empty dirs that co-exist with .py files of the same name — import ambiguity risk
  • Missing __main__.py: python -m app fails; use uv run blog or python app/main.py
  • Stale config: pyproject.toml excludes tests/e2e but the directory does not exist
  • Unused dependency: black is in [dependency-groups] lints but never invoked; ruff format is used instead
  • Pre-commit excludes __init__.py: All __init__.py files skip linting and import sorting

AI Code Generation Requirements

Documentation Standards

  • Write self-documenting code - use clear, descriptive variable names and function names
  • NO inline comments - code should be readable without explanatory comments
  • Google-style docstrings are REQUIRED for all modules, classes, and functions

Docstring Requirements

Modules

Every module must have a module-level docstring:

"""Module for managing blog posts.

This module provides use cases for creating, updating, and deleting
blog posts in the application layer.
"""

Classes

Every class must have a detailed docstring:

class CreatePostUseCase:
    """Use case for creating a new blog post.

    This class encapsulates the business logic for creating posts,
    including validation and slug generation.

    Attributes:
        uow: Unit of Work for database transactions.
        slug_service: Service for generating URL-friendly slugs.

    Example:
        >>> use_case = CreatePostUseCase(uow, slug_service)
        >>> result = await use_case.execute(dto)
    """

Functions/Methods

Every function must have a detailed docstring with Args, Returns, Raises:

async def execute(self, dto: CreatePostDTO) -> PostDTO:
    """Execute the use case to create a new post.

    Args:
        dto: Data transfer object containing post creation data
            including title, content, and author information.

    Returns:
        PostDTO: The created post data transfer object with
            generated ID and slug.

    Raises:
        TitleValidationError: If the title is empty or too long.
        ContentValidationError: If the content is empty.
        DuplicateSlugError: If a post with the same slug exists.

    Note:
        This method is idempotent - calling it multiple times with
        the same data will create separate posts with unique slugs.
    """

Google-Style Docstring Format

Use the following sections as appropriate:

  • Args - Parameter descriptions with types
  • Returns - Return value description with type
  • Raises - Exceptions that may be raised
  • Yields - For generator functions
  • Example - Usage examples
  • Note - Additional important information
  • Warning - Critical warnings
  • Attributes - For class attributes
  • See Also - References to related code

UI Development Requirements

HTML Templates (Jinja2)

  • All HTML templates use Jinja2 templating engine
  • Templates are located in app/presentation/templates/
  • Base template: base.html with theme support (light/dark)

data-testid Attributes (REQUIRED)

Every interactive and significant HTML element MUST have a data-testid attribute for automated testing.

Required Elements:

  • Navigation: data-testid="nav-link-{name}", data-testid="nav-logo"
  • Buttons: data-testid="btn-{action}" (e.g., btn-create, btn-save, btn-delete)
  • Forms: data-testid="form-{name}", data-testid="input-{field}", data-testid="submit-{action}"
  • Cards/Posts: data-testid="post-card-{id}", data-testid="post-title", data-testid="post-content"
  • Lists: data-testid="list-{name}", data-testid="list-item-{index}"
  • Theme Switcher: data-testid="theme-toggle", data-testid="theme-{light|dark}"
  • Messages/Alerts: data-testid="alert-{type}", `data-testid="alert-message"

Example:

<button data-testid="btn-create-post" class="btn btn-primary">
    Create Post
</button>

<article data-testid="post-card-{{ post.id }}" class="card">
    <h2 data-testid="post-title">{{ post.title }}</h2>
    <p data-testid="post-content">{{ post.content }}</p>
</article>

CSS Architecture (Gitea-inspired)

  • Theme files: static/css/themes/theme-{light|dark}.css with CSS variables
  • Base styles: static/css/base.css - reset, typography, CSS variables usage
  • Components: static/css/components.css - buttons, cards, forms, inputs
  • Layout: static/css/layout.css - grid, navigation, containers

Theme Support

  • Light and dark themes based on Gitea color scheme
  • Theme switching via data-theme attribute on <html> element
  • LocalStorage persistence for user preference
  • All colors use CSS custom properties (variables)

Static Assets

  • All assets are local - no external CDN dependencies
  • Location: static/ directory at project root
  • Served via FastAPI StaticFiles middleware

Authentication & Authorization

Web UI Authentication

  • Token storage: HTTP-only secure cookies
  • Login flow: Redirect to Keycloak login page → Callback → Set cookie → Redirect back
  • Registration: Only through Keycloak admin interface
  • Profile: Read-only display of user info

Authorization Rules

Post Visibility

Role Published Posts Own Drafts Other Drafts
GUEST (unauthenticated)
USER
ADMIN

UI Elements by Role

Element GUEST USER ADMIN
"New Post" button
"Edit" button on own posts
"Edit" button on other posts
"Delete" button on own posts
"Delete" button on other posts
Draft badges Own only All
User menu in header
Profile page access

Auth Routes

  • GET /auth/login - Redirect to Keycloak
  • GET /auth/callback - OAuth callback handler
  • GET /auth/logout - Clear cookie and logout
  • GET /profile - User profile page (read-only)
response.set_cookie(
    key="access_token",
    value=token,
    httponly=True,
    secure=True,  # In production
    samesite="lax",
    max_age=3600,  # 1 hour
)

DDD Concepts Used

Entities

  • Have identity (UUID)
  • Mutable state
  • Business logic methods (publish, update_title, etc.)
  • Example: Post entity

Value Objects

  • Immutable
  • Defined by attributes
  • Validated on creation
  • Examples: Title, Content, Slug

Aggregates & Repositories

  • Post is an aggregate root
  • PostRepository interface in Domain
  • SQLAlchemyPostRepository implementation in Infrastructure

Domain Events

  • Placeholder for future implementation
  • Can be added via event bus in application layer

Configuration

  • .env file loaded by pydantic-settings
  • Settings available via app.infrastructure.config.settings

Database

  • SQLAlchemy 2.0 with async support
  • SQLite by default (aiosqlite)
  • Tables auto-created on startup
  • Use init_db() and close_db() in lifespan

TDD Development Workflow

This project uses Test-Driven Development (TDD) with a formal test agreement process.

Feature Lifecycle

User: "начнем новую фичу"
  |
  v
Discovery Phase (автоматически)
  |-- Анализ существующего кода
  |-- Определение затронутых слоев DDD
  |-- Рекомендации по тесткейсам
  |
  v
User Agreement (согласование)
  |-- Пользователь подтверждает/корректирует тесткейсы
  |
  v
Test Design
  |-- Актуализация FEATURE_*.md
  |-- Создание artifact: pyaqa/feature/{feature-name}.md
  |-- Назначение TC-UNIT-NNN, TC-API-NNN, TC-WEB-NNN, TC-E2E-NNN
  |
  v
Write Tests (RED)
  |-- Написать тесты, убедиться что они падают
  |
  v
Implementation (GREEN)
  |-- Domain -> Application -> Infrastructure -> Presentation
  |-- Минимальная реализация для прохождения тестов
  |
  v
Refactor
  |-- Улучшение кода с сохранением зеленых тестов
  |-- Линтеры, type checker
  |
  v
Verification
  |-- ruff check, ruff format, isort, mypy
  |-- pytest (coverage ≥70%)
  |-- E2E tests
  |
  v
User Acceptance
  |-- Пользователь подтверждает приемку
  |
  v
Commit (во все затронутые проекты)
  |-- blog, pytfm, pyaqa (root)
  |
  v
Merge & Cleanup
  |-- Дождаться влития PR в целевую ветку (dev/main)
  |-- Переключиться на целевую ветку
  |-- `git pull` — подтянуть изменения
  |-- Удалить локальную фича-ветку: `git branch -d feature/{name}`

Bugfix Lifecycle

User: "исправить баг"
  |
  v
Reproduction Phase
  |-- Анализ бага, воспроизведение
  |-- Определение root cause
  |-- Создание artifact: pyaqa/bugfix/{name}.md
  |
  v
Write Regression Test
  |-- Написать тест, воспроизводящий баг
  |-- Убедиться что тест падает (RED)
  |
  v
Fix (GREEN)
  |-- Минимальный фикс
  |-- Убедиться что тест проходит
  |
  v
Verification
  |-- Все существующие тесты проходят
  |-- Coverage не упал
  |-- Линтеры, type checker
  |
  v
User Acceptance
  |-- Пользователь проверяет исправление
  |
  v
Commit (во все затронутые проекты)
  |
  v
Merge & Cleanup
  |-- Дождаться влития PR в целевую ветку (dev/main)
  |-- Переключиться на целевую ветку
  |-- `git pull` — подтянуть изменения
  |-- Удалить локальную фича-ветку: `git branch -d feature/{name}`

Refactoring Lifecycle

User: "отрефакторить"
  |
  v
Analysis Phase
  |-- Анализ кода
  |-- Определение scope и рисков
  |-- Создание artifact: pyaqa/refactor/{name}.md (опционально)
  |
  v
Pre-check
  |-- Все тесты проходят ДО рефакторинга
  |-- Фиксация coverage baseline
  |
  v
Refactoring
  |-- Пошаговые изменения
  |-- Проверка тестов после каждого шага
  |
  v
Post-check
  |-- Все тесты проходят ПОСЛЕ рефакторинга
  |-- Coverage не ниже baseline
  |-- Поведение не изменилось
  |
  v
Verification
  |-- Линтеры, type checker
  |-- Нет новых warnings
  |
  v
User Acceptance (опционально)
  |-- Пользователь проверяет, что ничего не сломалось
  |
  v
Commit (во все затронутые проекты)
  |
  v
Merge & Cleanup
  |-- Дождаться влития PR в целевую ветку (dev/main)
  |-- Переключиться на целевую ветку
  |-- `git pull` — подтянуть изменения
  |-- Удалить локальную фича-ветку: `git branch -d feature/{name}`

Branch Naming

  • Feature: feature/{feature-name} от dev
  • Bugfix: bugfix/{bug-name} от dev
  • Refactor: refactor/{name} от dev

Test Case IDs

  • TC-UNIT-NNN — unit тесты (domain, use cases)
  • TC-API-NNN — API endpoint тесты
  • TC-WEB-NNN — Web route тесты (HTML responses)
  • TC-E2E-NNN — End-to-end тесты (Playwright)

Test Level Selection

Все 4 уровня по умолчанию. Можно сокращать в зависимости от задачи:

  • Domain-only фича: только TC-UNIT
  • API-only фича: TC-UNIT + TC-API
  • Web UI фича: TC-UNIT + TC-WEB + TC-E2E
  • Full-stack фича: все 4 уровня
  • Bugfix: уровни в зависимости от слоя бага (минимум unit + regression)
  • Refactor: все существующие тесты (unit + api + web + e2e)

Artifact Location

  • Feature: pyaqa/feature/TEMPLATE.mdpyaqa/feature/{feature-name}.md
  • Bugfix: pyaqa/bugfix/TEMPLATE.mdpyaqa/bugfix/{bug-name}.md
  • Refactor: pyaqa/refactor/TEMPLATE.mdpyaqa/refactor/{name}.md

Commit Rules

При завершении коммитить во ВСЕ затронутые подпроекты:

  1. blog/ — если изменен
  2. pytfm/ — если изменен
  3. pyaqa/ (root) — всегда (обновление ссылок на подпроекты)

Notes

  • Web routes (app/presentation/web/routes.py) currently use MockPost and MOCK_POSTS instead of real use cases — integrate with actual use cases when ready
  • alembic/ directory exists but is non-functional (no alembic.ini, no migration scripts)
  • tests/integration/, tests/api/, tests/e2e/ are documented in architecture but do not exist yet
  • app/domain/roles.py exists but its symbols are not exported in app/domain/__init__.py
  • Woodpecker CI uses .woodpecker/ directory (3 separate YAML files) instead of single .woodpecker.yml — valid but non-standard
  • CI pipelines have copy-paste boilerplate; test.yaml uses --group tests while lint.yaml and type.yaml use --only-group <X>