20 KiB
20 KiB
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 --fix → ruff format → isort → mypy
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.Anycasts or# type: ignoreto bypass mypy strict mode - Dead code:
create_container()inapp/infrastructure/di/container.pyis defined but never used;main.pycallsmake_async_container()directly - Empty directories:
app/domain/exceptions/andapp/presentation/api/deps/are empty dirs that co-exist with.pyfiles of the same name — import ambiguity risk - Missing
__main__.py:python -m appfails; useuv run blogorpython app/main.py - Stale config:
pyproject.tomlexcludestests/e2ebut the directory does not exist - Unused dependency:
blackis in[dependency-groups] lintsbut never invoked; ruff format is used instead - Pre-commit excludes
__init__.py: All__init__.pyfiles 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 typesReturns- Return value description with typeRaises- Exceptions that may be raisedYields- For generator functionsExample- Usage examplesNote- Additional important informationWarning- Critical warningsAttributes- For class attributesSee 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.htmlwith 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}.csswith 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-themeattribute 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
StaticFilesmiddleware
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 KeycloakGET /auth/callback- OAuth callback handlerGET /auth/logout- Clear cookie and logoutGET /profile- User profile page (read-only)
Cookie Settings
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:
Postentity
Value Objects
- Immutable
- Defined by attributes
- Validated on creation
- Examples:
Title,Content,Slug
Aggregates & Repositories
Postis an aggregate rootPostRepositoryinterface in DomainSQLAlchemyPostRepositoryimplementation in Infrastructure
Domain Events
- Placeholder for future implementation
- Can be added via event bus in application layer
Configuration
.envfile 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()andclose_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.md→pyaqa/feature/{feature-name}.md - Bugfix:
pyaqa/bugfix/TEMPLATE.md→pyaqa/bugfix/{bug-name}.md - Refactor:
pyaqa/refactor/TEMPLATE.md→pyaqa/refactor/{name}.md
Commit Rules
При завершении коммитить во ВСЕ затронутые подпроекты:
blog/— если измененpytfm/— если измененpyaqa/(root) — всегда (обновление ссылок на подпроекты)
Notes
- Web routes (
app/presentation/web/routes.py) currently useMockPostandMOCK_POSTSinstead of real use cases — integrate with actual use cases when ready alembic/directory exists but is non-functional (noalembic.ini, no migration scripts)tests/integration/,tests/api/,tests/e2e/are documented in architecture but do not exist yetapp/domain/roles.pyexists but its symbols are not exported inapp/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.yamluses--group testswhilelint.yamlandtype.yamluse--only-group <X>