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>
This commit is contained in:
193
skills/python-development-database/SKILL.md
Normal file
193
skills/python-development-database/SKILL.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user