- 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>
8.4 KiB
8.4 KiB
name, description
| name | description |
|---|---|
| python-development-database | 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/ordb/). 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__.pyexport 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.BaseTableand usesqlmodel.Fieldfor column declarations. They are declared withtable=True. - Keep table classes in the same module as their repository or in a dedicated
tables.pymodule 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_itemandto_itemclass/instance methods when the repository uses a DTO.
Example database/repositories/user.py:
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 frompydantic-filters). Filter fields are optional by default; simply declare the type without| None = None. - Keep filters in
database/filters.pywhen 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:
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.BaseRepositoryand is declared with keyword argumentstable=...andfilter_=.... Optionally adddto=...to enable automatic DTO mapping. - Repository classes must specify
table=andfilter_=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_itemsreturn anAsyncGenerator. Collect results with list comprehension:[item async for item in repo.get_items(...)].
Example database/repositories/user.py:
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:
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.pythat inherits frommetaorm.RepositorySettings. - Pass the settings instance to the repository constructor or to a shared
RepositoriesContainer.
Example database/settings.py:
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
RepositoriesContainerwhen a single atomic transaction must span multiple repositories. Retrieve repositories viacontainer.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()(orrepository.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:
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.OffsetPaginationormetaorm.PagePaginationfor result pagination. - Use
metaorm.BaseSortfor ordering. Pass it toget_itemsassort=BaseSort(sort_by="column", sort_by_order="asc|desc"). - Pass SQLAlchemy eager-loading options (
joinedload,selectinload) toget_itemsvia theoptionsparameter.
Example database/repositories/book.py:
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
DatabaseExceptionsubclasses. Catch them at the service layer, not inside repositories. - Available exception types:
NotFoundError,AlreadyExistsError,HaveNoSessionError. All inherit fromDatabaseException. - Let SQLAlchemy
IntegrityErrorpropagate asAlreadyExistsErrorwhen it signals a unique constraint violation; the library already does this internally forcreate_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
RepositoriesContainerdirectly.