Другая ORM
Реализуйте контракт quite_admin.adapters.base.Adapter:
class MyAdapter:
def discover(self): ... # Iterable[existing model], можно async
def describe(self, model): ... # ModelSchema с Field, не ORM/DTO-модель
async def list(self, model, query): ... # RecordPage
async def get(self, model, key): ... # dict | None
async def create(self, model, values): ... # dict
async def update(self, model, key, values): ... # dict
async def delete(self, model, key): ... # None
Результаты — скалярные mappings, без lazy ORM-объектов. key — словарь,
поэтому поддерживаются составные ключи. Адаптер отвечает за транзакции,
rollback, model-level проверки, tenant scopes и lifecycle своих сессий.
Ожидаемые ошибки переводите в Conflict, NotFound, ValidationError из
quite_admin.errors. Ядро отвечает за авторизацию, CSRF, allowlist полей, преобразование
типов и отображение. Пример стороннего адаптера находится в tests/test_integration.py исходного репозитория.
Фильтры и колонки
Перед запросом вызывайте query.validate(schema).
from decimal import Decimal
from quite_admin import Filter, Query
query = Query(
filters=(Filter("active", "eq", True), Filter("price", "gte", Decimal("10.00"))),
order_by="price", columns=("id", "name", "price"), limit=25,
)
result = await adapter.list(Product, query)
Значения имеют Python-тип поля, включая UUID, date и enum. Операторы: eq, lt,
lte, gt, gte, in, is_null. До 50 фильтров объединяются AND. in принимает
до 1000 значений; пустая последовательность не находит ничего. Для NULL используйте
Filter("note", "is_null", True). JSON/binary/неизвестные типы допускают только
is_null. Произвольный SQL и lookup-операторы не принимаются.
total считает совпадения до пагинации. Сортировка добавляет PK для стабильного порядка.
Проекция возвращает только указанные скалярные ключи; включайте PK, если запись должна
оставаться адресуемой. Встроенные адаптеры ограничивают и SELECT; свой адаптер обязан соблюдать возвращаемую проекцию. Запросы
связей используют тот же контракт. HTML-список не предоставляет язык фильтров и проекций.
Для своих адаптеров операторы API-1 выше остаются значениями по умолчанию. Для
расширенных условий задайте supported_filter_operators множеством реализованных
операторов. Встроенные адаптеры используют quite_admin.schema.FILTER_OPERATORS,
включающий также ne, icontains, not_icontains, istartswith, iendswith.
Поиск части текста не учитывает регистр и экранирует %, _, ! как обычные символы;
отрицательные сравнения исключают NULL. Объявляйте только реально реализованные в
list() операторы. UI-условия «Между», «За дату» и пустой строки преобразуются
в обычные типизированные предикаты.
Необязательно: задайте supports_key_search = True, реализовав Query.search_key.
При его наличии объединяйте (текстовый поиск OR точный полный первичный ключ)
внутри остальных AND-условий до подсчёта и пагинации. Проверяйте полноту ключа
и Python-типы значений. Тогда поиск связей найдёт числовой/UUID ID, даже если
search_fields содержит только имя. Без флага сохраняется только текстовый поиск.
DelegatingAdapter передаёт оба объявления возможностей своему адаптеру чтения.
Обязательный протокол API-1 не меняется.
Сервисы приложения
DelegatingAdapter повторно использует обнаружение, метаданные и чтение. Переопределите
каждую операцию записи, которой нужны бизнес-правила:
from quite_admin import DelegatingAdapter
class ProductAdminAdapter(DelegatingAdapter):
def __init__(self, reader, service):
super().__init__(reader)
self.service = service
async def create(self, model, values):
return await self.service.create_product(values)
async def update(self, model, key, values):
return await self.service.update_product(key, values)
async def delete(self, model, key):
await self.service.delete_product(key)
adapter = ProductAdminAdapter(native_adapter, product_service)
admin.add_adapter(adapter, discover=False)
admin.register(Product, adapter=adapter)
Сервис отвечает за проверки, транзакцию и rollback, возвращает скалярные mappings
и может выбрасывать публичные ошибки адаптера. Непереопределённые записи вызывает
delegate. Регистрируйте обёртку вместо повторного обнаружения тех же моделей нативным
адаптером. Методы RelationAdapter автоматически не делегируются: это могло бы обойти
переопределённый update(). Реализуйте все три явно, когда сервис умеет атомарно
сохранять поля и связи.
Совместимость контракта
ADAPTER_API_VERSION равен 1. Стороннему адаптеру не нужны атрибут версии или базовый
класс. Обязательный протокол отделён от необязательной поддержки связей. Изменение
обязательных методов требует новой версии протокола и описанного breaking release.
До 1.0 такие изменения поднимают MINOR пакета.
Общий набор — tests/test_adapters.py, tests/test_adapter_safety.py и
tests/test_relations.py: шесть режимов адаптеров и отдельная матрица серверов.
См. подтверждённые проверки и ограничения.
Конкурирующее редактирование
CheckedAdapter — необязательный структурный протокол; обязательный Adapter остаётся
версии 1. Все встроенные адаптеры его поддерживают. Для своего адаптера реализуйте
async save_checked(model, key, values, selections, *, guard). Публичный WriteGuard
проверяет прочитанные значения: guard.check_record(record) и
guard.check_relation(name, [schema.record_key(row) for row in related]).
Проверки должны выполняться внутри той же транзакции, что и запись, после блокировки
записи и состава изменяемых связей. При расхождении выбрасывается
quite_admin.concurrency.StaleRecord (наследник Conflict); вся операция откатывается.
Отдельное чтение перед обычным update не защищает от гонки. Нужна блокировка строк,
сериализованная транзакция либо атомарный compare-and-swap. Встроенные адаптеры
блокируют родительскую и текущие связанные записи; SQLite начинает транзакцию записи
до чтения. Чтение должно возвращать те же скалярные Python-типы, что и get().
DelegatingAdapter автоматически не делегирует save_checked, чтобы не обходить
бизнес-правила сервиса. Реализуйте метод явно через транзакцию сервиса. Адаптеры без
этого метода продолжают работать со своей политикой конкуренции. Прямые вызовы
update() / save_relations() остаются безусловными; проверку выполняет только
save_checked(). Удаление и свои действия используют отдельную политику приложения.
История версий не хранится: изменение с последующим возвратом к исходным значениям
неотличимо от неизменившейся записи. Пример границы сервисной транзакции приведён
в английской версии этой страницы.
Курсорное чтение list_window делегируется автоматически, если исходный адаптер
поддерживает WindowAdapter, а подкласс наследует обычный list(). Адаптер без
курсоров не объявляет этот необязательный протокол. Если подкласс переопределяет
list() (например, фильтрует по организации), реализуйте list_window() явно с той же
политикой чтения: автоматическое делегирование могло бы её обойти. Записи связей и
checked-записи по-прежнему требуют явной реализации и автоматически не делегируются.
Фильтры по связям
Query.related содержит RelatedFilter(relation, key), объединённые AND. Встроенные
адаптеры объявляют supports_related_filters = True и применяют условия в list()
до подсчёта total, сортировки, offset и проекции. В related() эти условия не применяются.
Свои адаптеры явно включают эту возможность и реализуют ту же семантику. Без флага
HTML-список предлагает только скалярные фильтры. DelegatingAdapter по умолчанию
его не объявляет.
Проверяйте query.validate(schema, related_schemas={name: target_schema, ...}).
Допускается до десяти уникальных известных связей с полными типизированными
ненулевыми ключами целевых записей. Обычный query.validate(schema) отклоняет
непустой related, чтобы условие нельзя было случайно проигнорировать. Используйте
EXISTS или аналог хранилища, исключающий дубли строк и завышенный total.