Настройка и Hare.init()¶
Как приложение запускает hare и останавливает его: Hare.init() и словарь config, который он
принимает, HareContext, в котором работает процесс или тест, запросы, созданные до init(), привязка
моделей без базы, закрытие подключений и Hare.run_async() для скрипта.
Hare.init()¶
Приложение на hare начинается с вызова Hare.init(). Он регистрирует модели, настраивает подключения к
базам и возвращает HareContext, который нужно хранить, пока работает приложение. Само соединение (или
пул соединений) открывается при первом запросе, а не в init().
class Hare:
@classmethod
async def init(
cls,
config: Mapping[str, Any] | HareConfig | str,
*,
connect: bool = True,
_create_db: bool = False,
use_timezone: bool = True,
timezone: str = "UTC",
routers: list[str | type] | None = None,
table_name_generator: Callable[[type[Model]], str] | None = None,
slow_query_threshold_ms: float = 500.0,
_enable_global_fallback: bool = False,
) -> HareContext: ...
Где лежат настройки, говорит один аргумент — config, — и он принимает любую форму (читает его
HareConfig.load(source) — и для Hare.init(), и для CLI):
await Hare.init({"connections": {...}, "apps": {...}}) # словарь — формат описан ниже
await Hare.init(HareConfig(connections=..., apps=...)) # то же типизированным объектом
await Hare.init("config/hare.yml") # файл .json/.yml/.yaml в том же формате
await Hare.init("settings.HARE_ORM") # module.VARIABLE со словарём или HareConfig
await Hare.init(HareConfig.from_db_url("sqlite+aiosqlite://db.sqlite3", {"models": ["library.models"]})) # один адрес
HareConfig.from_db_url(db_url, modules) строит настройки одного подключения ("default") из адреса
базы и {app_label: [module paths]}. Источник, который нельзя прочитать, — отсутствующий файл,
несуществующий модуль или переменная — даёт ConfigurationError.
Общие для всего процесса настройки (slow_query_threshold_ms и table_name_generator) меняются только
после успешного init(); если init() завершился ошибкой, они остаются прежними.
| Аргумент | Что задаёт |
|---|---|
config |
Словарь, HareConfig, путь к файлу или "module.VARIABLE" — см. выше; формат описан ниже. |
connect |
False — настроить модели и подключения, не подключаясь ни к одной базе; имена подключений всё равно сверяются с настройками. Для инструментов, которым нужны модели и их SQL, но не база: создание миграций, вывод DDL. |
_create_db |
Сначала создать саму базу (удобно в тестах). |
use_timezone / timezone |
Как DatetimeField работает с часовыми поясами. |
routers |
Список классов маршрутизаторов или путей к ним через точку — см. Маршрутизаторы. |
table_name_generator |
Функция Callable[[type[Model]], str], которая заменяет правило именования таблиц — см. table_name_generator |
slow_query_threshold_ms |
Запросы, которые выполнялись столько миллисекунд или дольше, записываются в журнал на уровне DEBUG как «Slow query». По умолчанию 500 мс — см. Журнал медленных запросов. |
_enable_global_fallback |
Позволяет коду, который выполняется не в той задаче asyncio, что вызвала init() (например, в фоновой задаче, запущенной при старте ASGI-приложения), найти текущий контекст. Интеграции с Litestar, FastAPI и Robyn включают это сами. |
table_name_generator¶
Функция Callable[[type[Model]], str], которая заменяет правило именования таблиц. Сгенерированное имя
вычисляется заново при каждом init(): следующий init() с другой функцией или без неё переименует
таблицу модели. Явно указанная Meta.table всегда важнее. Имена, которые выводятся из имени таблицы при
первой настройке связей, — автоматически созданная промежуточная таблица связи «многие-ко-многим», её
колонки, related_name по умолчанию — сохраняют первое значение.
Словарь config¶
Настройки, которые Hare.init() принимает словарём, — подключения, приложения и настройки всего
контекста:
config = {
"connections": {
"default": "postgresql://postgres:qwerty123@localhost:5432/library", # просто строка-адрес...
"reporting": { # ...или подробный словарь - engine это схема, которая была бы в адресе
"engine": "postgresql",
"credentials": {
"host": "localhost", "port": "5432",
"user": "postgres", "password": "qwerty123", "database": "reporting",
},
},
},
"apps": {
"library": {
"models": ["library.models"],
"default_connection": "default", # необязательно, по умолчанию "default"
"migrations": "library.migrations", # необязательно, нужно командам CLI
},
},
"routers": ["library.routers.ReportingRouter"],
"use_timezone": True,
"timezone": "UTC",
"swappable": {"USER_MODEL": "library.Reader"}, # необязательно, см. ниже
"migrations": {"lock_timeout": 5}, # необязательно, см. ниже
}
context = await Hare.init(config=config)
HareConfig и другие классы настроек¶
class HareConfig:
def __init__(
self,
connections: dict[str, ConnectionConfig | DBUrlConfig],
apps: dict[str, AppConfig],
routers: list[str | type] | None = None,
read_your_writes_seconds: float | None = None,
use_timezone: bool | None = None,
timezone: str | None = None,
cli: CliConfig | None = None,
swappable: dict[str, str | dict[str, str]] | None = None,
migrations: MigrationsConfig | None = None,
) -> None: ...
@classmethod
def from_db_url(cls, db_url: str, modules: Mapping[str, Iterable[str | ModuleType]]) -> Self: ...
@classmethod
def load(cls, source: Mapping[str, Any] | Self | str) -> Self: ...
class ConnectionConfig:
def __init__(
self, engine: str | None = None, credentials: dict[str, Any] = {}, db_url: str | None = None
) -> None: ...
class DBUrlConfig:
def __init__(self, url: str) -> None: ...
class AppConfig:
def __init__(
self,
models: Iterable[str | ModuleType],
default_connection: str | None = None,
migrations: str | None = None,
) -> None: ...
class CliConfig:
def __init__(self, commands: list[str] = []) -> None: ...
class MigrationsConfig:
def __init__(self, lock_timeout: float | None = None, safety: MigrationSafetyConfig | None = None) -> None: ...
class MigrationSafetyConfig:
def __init__(self, large_table_rows: int = 100000) -> None: ...
Настройки можно собрать в коде из этих классов hare.core.config, а не словарём.
read_your_writes_seconds — см. Чтение своих записей; cli
задаёт дополнительные подкоманды hare. Подключение можно задать и словарём {"db_url": "..."} — в
такой вид превращается ConnectionConfig(db_url=...). models — список или кортеж путей к модулям или
самих модулей.
Раздел migrations¶
migrations принимает lock_timeout: сколько секунд команда миграции может ждать блокировку, взятую
другим сеансом, прежде чем migrate завершится ошибкой, — см.
Ожидание блокировок; и safety — настройки проверки
миграций: {"large_table_rows": 100000} — с какого числа строк checkmigrations считает таблицу
большой, целое число от 0 до 1012, — см. Проверка
миграций.
Раздел swappable¶
swappable сопоставляет имени настройки модель в виде "app_label.ModelName" — ту, на которую указывают
связи, объявленные через swappable("USER_MODEL"). Так проект заменяет модель, поставляемую пакетом,
своей (см. Заменяемые модели). Имя настройки пишется
заглавными буквами (USER_MODEL), значение называет одно из настроенных приложений. Кроме того, init()
проверяет, что модель зарегистрирована, не абстрактная и сама не заменена другой моделью через свою
настройку. Каждая из этих ошибок даёт свой ConfigurationError. Hare.swappable_label("USER_MODEL")
возвращает метку модели, на которую указывает настройка, а Hare.get_swappable_model("USER_MODEL") —
саму модель. Если настройки нет в конфигурации, она указывает на модель, объявившую
Meta.swappable = "USER_MODEL". Значением настройки может быть и словарь «имя ветки → метка» —
{"COMMENT_TARGETS": {"post": "blog.Post", "photo": "media.Photo"}}, цели
GenericForeignKeyField(swappable("COMMENT_TARGETS"));
каждое имя ветки — идентификатор Python, каждая метка проверяется так же, как выше. Такая настройка
должна быть задана в конфигурации, и принимает её только обобщённая связь.
Проверки при init()¶
Ключи словаря настроек — это поля тех же классов: на верхнем уровне поля HareConfig, в подключении,
заданном словарём, — поля ConnectionConfig, в приложении — AppConfig, в разделе cli — CliConfig.
Любой другой ключ даёт ConfigurationError с ближайшим известным именем: иначе опечатка вроде
default_conection молча оставила бы модели приложения на подключении по умолчанию. Сами настройки,
их разделы connections и apps и каждое приложение — словари, каждое подключение — словарь или адрес:
что-то другое (пустой YAML-файл, список) тоже даёт ConfigurationError.
Неизвестный engine даёт ConfigurationError, как и параметр подключения, которого не знает драйвер:
опечатку в credentials (comand_timeout) отклоняют оба драйвера PostgreSQL. rust_pg принимает только
свои параметры, asyncpg — только то, что принимают asyncpg.connect()/create_pool().
Связь с db_constraint=True (так по умолчанию) между моделями, приложения которых работают через разные
подключения, даёт ConfigurationError при init(): внешний ключ базы не может ссылаться на таблицу в
другой базе. Объявите такую связь с db_constraint=False — тогда её on_delete выполняет сам hare.
Секреты в repr() и журнале¶
repr() этих объектов (и настройки подключений, которые hare пишет в журнал на уровне DEBUG при
init()) заменяет каждое секретное значение на ***: пароль в адресе базы, секретный параметр адреса
(sslpassword=...) и каждый параметр credentials, в имени которого есть password, passwd, pwd,
secret или token. Никакая часть секрета не показывается.
HareContext¶
context = await Hare.init(HareConfig.from_db_url("sqlite+aiosqlite://:memory:", {"models": ["library.models"]}))
# позже, при остановке приложения:
await context.close_connections()
HareContext можно использовать и в блоке async with — так обычно делают в тестах и скриптах:
async with HareContext() as context:
await context.init(HareConfig.from_db_url("sqlite+aiosqlite://:memory:", {"models": ["library.models"]}))
await context.generate_schemas()
...
Hare — это контекст текущей задачи под постоянным именем: Hare.init() настраивает текущий
HareContext задачи (новый, если у задачи его нет), а все остальные методы Hare работают с текущим.
Всё определено один раз, в HareContext: .init(config, ...), .generate_schemas(safe=True),
.close_connections(), .connections (ConnectionHandler), .apps, .router, .routers, .inited,
.default_connection, .use_timezone, .timezone, .get_model("app.Model") (метка, класс модели или
настройка swappable()),
.get_connection(connection_alias=None), .bind_models(config, ...), .swappable_label(setting),
.get_swappable_model(setting), .register_live_models(...), .unregister_live_models(...) и
.observe(event_type, callback)/.unobserve(...) (см. Наблюдение за
ORM). HareContext.get_current() возвращает
текущий контекст или None, HareContext.require_current() — текущий контекст или ConfigurationError.
Классы моделей общие для всех контекстов, загрузивших их модуль: контекст, настроенный внутри другого,
привязывает их (соединение, имена таблиц, связи) к своей конфигурации, а выход из него — with или
async with — привязывает их обратно к контексту, который снова стал текущим.
Запросы, построенные до init()¶
QuerySet можно построить до Hare.init() — при импорте, как константу модуля или атрибут класса:
PUBLISHED = Book.objects.filter(published=True).order_by("-rating") # init() ещё не было
async def list_published() -> list[Book]:
return await PUBLISHED.limit(20) # после init(): обычный QuerySet
Пока модели не привязаны, цепочку нельзя проверить по ним (связи ещё не настроены), поэтому каждый такой
QuerySet запоминается, а init() повторяет его вызовы сразу после привязки моделей. С этого момента это
обычный, полностью построенный QuerySet — ничего ленивого в нём нет. Поэтому ошибка в такой цепочке
(неизвестное поле, неверный оператор фильтра) даёт ConfigurationError из Hare.init() (или
Hare.bind_models()) с текстом ошибки, которую дал бы этот вызов после init(). Выполнение запроса до
init() даёт ConfigurationError.
Привязка моделей без базы¶
Привязывает модели из настроек — связи, заменяемые модели, фильтры и сортировки, — не подключаясь и не
оставляя текущего контекста. Это для кода, которому модели нужны до запуска приложения: веб-фреймворку,
который при импорте строит сигнатуры обработчиков по
lookup_info. Подключения потом, как обычно, настраивает
Hare.init(). await Hare.init(config, connect=False) — асинхронная форма, которая оставляет текущий
контекст: с моделями, маршрутизаторами и настройками подключений, но без единого соединения.
Закрытие подключений¶
Hare.close_connections() закрывает все подключения текущего контекста и выбрасывает их; без контекста
ничего не делает. Сколько он ждёт ещё идущие запросы, зависит от базы:
| База данных | Поведение |
|---|---|
| PostgreSQL | Каждый пул (и asyncpg, и rust_pg) до 10 секунд ждёт соединения, которые держат или открывают его запросы и транзакции, затем разрывает их: сервер откатывает ещё открытые транзакции и закрывает их сессии |
| SQLite | Подключение до 10 секунд ждёт транзакцию или запрос, которые его используют, затем закрывается всё равно |
| ClickHouse | clickhouse_driver до 10 секунд ждёт уже переданные команды; команда, которая ещё идёт, заканчивается сама. clickhouse_connect закрывает свой HTTP-клиент, когда на уже отправленные запросы пришли ответы, — они завершаются со своими результатами |
Задача, которая ещё держит выброшенный объект подключения, на следующем запросе получает DBConnectionError,
а не открывает новый пул; запрос через модель снова ищет своё подключение — и внутри транзакции тоже — и
получает ConfigurationError, как до Hare.init(). Контекст забывает свои подключения, даже если закрытие не
удалось, а первая ошибка поднимается после; наблюдателям, которые ещё отправляют метрику или трассировку,
даётся до 10 секунд. close_all() у ConnectionHandler — то же закрытие для одного контекста — см.
Подключения.
Другие методы класса Hare¶
Hare.generate_schemas(safe=True)— сразу создать таблицы всех зарегистрированных моделей, без миграций. Удобно для тестов и прототипов, но не для рабочей базы. Таблицы создаются в порядке, которого требуют их внешние ключи; если внешние ключи моделей ссылаются друг на друга по кругу, одна таблица круга создаётся без внешнего ключа на таблицу, создаваемую позже, а ключ добавляется черезALTER TABLE ... ADD CONSTRAINT, когда та таблица уже есть (сsafe=True— только если его нет). SQLite проверяет ссылку только при записи строки, поэтому там каждая таблица сохраняет свойREFERENCES.connection.get_schema_sql(safe)— вернуть те же команды создания таблиц строкой, не выполняя их, — чтобы посмотреть, записать в журнал или сравнить.Hare.is_inited(),Hare.apps(модели текущего контекста;None, если его нет),Hare.get_context()(текущийHareContext; без него —ConfigurationError).Hare.swappable_label(setting)/Hare.get_swappable_model(setting)— см. Заменяемые модели.Hare.register_live_models(models, app_label, connection_alias="default", *, managed=None)/Hare.unregister_live_models(models)— см. Регистрация модели во время работы.
Чтобы описать модель — её поля, связи, фильтры и сортировки каждого поля, — читайте Model._meta и
lookup_info; аргументы конструктора поля даёт
field.deconstruct().
Примечание
Функция для тестов hare_test_context(modules, db_url="sqlite+aiosqlite://:memory:", ...) выполняет
весь цикл — init(), создание таблиц, закрытие и удаление базы — как один асинхронный контекстный
менеджер. Используйте её в своих фикстурах, а не повторяйте эти шаги вручную. Подробнее — в разделе
Тестирование, вместе с остальным hare.contrib.test.
Скрипты: Hare.run_async()¶
Для разового скрипта (не долго работающего приложения и не теста) Hare.run_async() вызывает
asyncio.run и сам закрывает соединения в конце: