- 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>
194 lines
8.4 KiB
Markdown
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.
|