# 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

{{ post.title }}

{{ post.content }}

``` ### 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 `` 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 ## 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 `