Files
ai-skills/skills/python-development-database/SKILL.md
Oleg Yurchik b8388fa4a2 Add database, telegram bot, and tests skills; update stack and fastapi skills
- Add python-development-database skill: metaorm patterns for tables, repositories, filters, settings, transactions, pagination, and exceptions
- Add python-development-telegram-bot skill: aiogram bot service with polling/webhook, router structure, middlewares, inline keyboards, and pagination
- Add python-development-tests skill: test organization guidelines with pytest, fixtures, and dependency injection rules
- Update python-development-stack: replace SQLModel/SQLAlchemy with metaorm; add pytest, pytest-asyncio, pytest-cov, faker
- Update python-development-fastapi: add sections for dependencies and list endpoints with ListResponse schema
- Update python-development-code-style: allow ctx as exception for typer.Context parameter
- Update python-development index with links to all three new skills

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-26 14:04:05 +03:00

194 lines
8.4 KiB
Markdown

---
name: python-development-database
description: Patterns for working with relational databases using metaorm. Use when defining tables, repositories, filters, database settings, or transaction logic.
---
## Rules
- Install the ORM layer with `pip install metaorm`. Requires Python `>=3.12`.
- Every database sub-system lives in its own package (conventionally named `database/` or `db/`). The package follows the repository pattern: one repository class per database table.
- The minimum package structure is:
```
database/
__init__.py
repositories/
__init__.py
user.py
...
filters.py # only if filters are not imported from an external schema package
settings.py # only if RepositorySettings from metaorm needs extension
cli.py # optional, for management commands (create-tables, migrations, etc.)
```
- In `database/__init__.py` export the public API: repository classes, filter classes, settings, and any shared exceptions. Keep `__all__` as a tuple grouped by source module.
### Tables
- Table models inherit from `metaorm.BaseTable` and use `sqlmodel.Field` for column declarations. They are declared with `table=True`.
- Keep table classes in the same module as their repository or in a dedicated `tables.py` module when shared across multiple repositories. Prefer co-location with the repository.
- Use type unions for nullable columns: `id: int | None = Field(default=None, primary_key=True)`.
- Define relationships with `metaorm.Relationship` (re-exported from SQLModel). Always declare the back-populates attribute.
- Provide `from_item` and `to_item` class/instance methods when the repository uses a DTO.
Example `database/repositories/user.py`:
```python
from metaorm import BaseTable, Field, Relationship
class UserTable(BaseTable, table=True):
__tablename__ = "users"
id: int | None = Field(default=None, primary_key=True)
name: str
email: str = Field(unique=True)
orders: list["OrderTable"] = Relationship(back_populates="user")
```
### Filters
- Filters inherit from `metaorm.BaseFilter` (re-exported from `pydantic-filters`). Filter fields are optional by default; simply declare the type without `| None = None`.
- Keep filters in `database/filters.py` when they are reused across the project, or inline in the repository module when they are private to that repository.
- Filter field names must match the table column names they filter.
Example `database/filters.py`:
```python
from metaorm import BaseFilter
class UserFilter(BaseFilter):
name: str
email: str
class OrderFilter(BaseFilter):
user_id: int
total: float
```
### Repositories
- Every repository inherits from `metaorm.BaseRepository` and is declared with keyword arguments `table=...` and `filter_=...`. Optionally add `dto=...` to enable automatic DTO mapping.
- Repository classes must specify `table=` and `filter_=` on the first class in the inheritance chain; subsequent subclasses can omit them if the parent already defined them.
- Keep each repository in its own module under `database/repositories/`. The module name matches the table name in singular snake_case.
- Provide a classmethod or factory function for repository instantiation when the project uses a shared `RepositoriesContainer`.
- Repository methods are async. The built-in CRUD methods are: `create_item`, `get_item`, `get_items`, `get_items_count`, `update_items`, `delete_items`.
- `get_items`, `update_items` return an `AsyncGenerator`. Collect results with list comprehension: `[item async for item in repo.get_items(...)]`.
Example `database/repositories/user.py`:
```python
from metaorm import BaseRepository
from project.database.filters import UserFilter
from .tables import UserTable
class UserRepository(BaseRepository, table=UserTable, filter_=UserFilter):
pass
```
Example with DTO `database/repositories/user.py`:
```python
from pydantic import BaseModel
from metaorm import BaseRepository
from project.database.filters import UserFilter
from .tables import UserTable
class User(BaseModel):
id: int | None = None
name: str
email: str
class UserRepository(BaseRepository, table=UserTable, filter_=UserFilter, dto=User):
pass
```
### Settings
- If the default `metaorm.RepositorySettings` (fields: `dsn`, `pool_size`, `pool_recycle`, `pool_timeout`) is sufficient, import and use it directly. Do not create a local wrapper.
- If the project needs additional database-level configuration (e.g. migration settings, schema names, connection retries), define a subclass in `database/settings.py` that inherits from `metaorm.RepositorySettings`.
- Pass the settings instance to the repository constructor or to a shared `RepositoriesContainer`.
Example `database/settings.py`:
```python
from metaorm import RepositorySettings
from pydantic import Field
class DatabaseSettings(RepositorySettings):
migration_path: str = "migrations"
auto_migrate: bool = False
```
### Transactions
- Every CRUD method automatically runs inside a transaction. Do not wrap single CRUD calls in an explicit transaction unless you need to combine multiple operations atomically.
- Use `repository.transaction()` as an async context manager when you need to group multiple operations on the same repository into one atomic unit.
- Use `RepositoriesContainer` when a single atomic transaction must span multiple repositories. Retrieve repositories via `container.get_repository(RepositoryClass)` so they share the container's session context.
- `RepositoriesContainer.create_tables(*repository_classes)` creates tables for all given repositories sequentially using the shared engine.
- Use `container.transaction()` (or `repository.nested_transaction()`) for savepoints inside an existing transaction. An exception inside a nested transaction rolls back only the savepoint, leaving the outer transaction intact.
Example multi-repository transaction `database/transactions.py`:
```python
from metaorm import RepositoriesContainer, RepositorySettings
from .repositories.order import OrderRepository, OrderTable
from .repositories.user import UserRepository, UserTable
async def create_user_with_order(settings: RepositorySettings) -> None:
container = RepositoriesContainer(settings=settings)
user_repo = container.get_repository(UserRepository)
order_repo = container.get_repository(OrderRepository)
async with container.transaction():
user = await user_repo.create_item(UserTable(name="Alice"))
await order_repo.create_item(OrderTable(user_id=user.id, total=100.00))
```
### Pagination, sorting, and eager loading
- Use `metaorm.OffsetPagination` or `metaorm.PagePagination` for result pagination.
- Use `metaorm.BaseSort` for ordering. Pass it to `get_items` as `sort=BaseSort(sort_by="column", sort_by_order="asc|desc")`.
- Pass SQLAlchemy eager-loading options (`joinedload`, `selectinload`) to `get_items` via the `options` parameter.
Example `database/repositories/book.py`:
```python
from sqlalchemy.orm import joinedload
from metaorm import BaseFilter, BaseRepository, BaseSort, BaseTable, Field, OffsetPagination
class BookTable(BaseTable, table=True):
__tablename__ = "books"
id: int | None = Field(default=None, primary_key=True)
title: str
author_id: int = Field(foreign_key="authors.id")
class BookFilter(BaseFilter):
title: str
author_id: int
class BookRepository(BaseRepository, table=BookTable, filter_=BookFilter):
pass
async def list_books(repository: BookRepository) -> list[BookTable]:
pagination = OffsetPagination(offset=0, limit=20)
sort = BaseSort(sort_by="title", sort_by_order="asc")
return [
item async for item in repository.get_items(
pagination=pagination,
sort=sort,
options=[joinedload(BookTable.author)],
)
]
```
### Exceptions
- Metaorm raises `DatabaseException` subclasses. Catch them at the service layer, not inside repositories.
- Available exception types: `NotFoundError`, `AlreadyExistsError`, `HaveNoSessionError`. All inherit from `DatabaseException`.
- Let SQLAlchemy `IntegrityError` propagate as `AlreadyExistsError` when it signals a unique constraint violation; the library already does this internally for `create_item`.
### CLI (optional)
- If the database package provides management commands (create tables, health check, seed data), place them in `database/cli.py`.
- CLI commands are async and accept the database settings as a parameter. They use the repository classes or `RepositoriesContainer` directly.