546 lines
20 KiB
Markdown
546 lines
20 KiB
Markdown
# 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
|
||
```bash
|
||
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.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:
|
||
```python
|
||
"""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:
|
||
```python
|
||
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:
|
||
```python
|
||
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:
|
||
```html
|
||
<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)
|
||
|
||
### Cookie Settings
|
||
```python
|
||
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.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
|
||
При завершении коммитить во ВСЕ затронутые подпроекты:
|
||
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>`
|