Pydantic¶
Строит модель Pydantic прямо из модели hare — для схем ответов API, без ручного написания параллельного класса Pydantic для каждой модели.
from hare.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
AuthorSchema = pydantic_model_creator(Author)
AuthorListSchema = pydantic_queryset_creator(Author)
pydantic_model_creator()¶
def pydantic_model_creator(
model: type[Model],
*,
name: str | None = None,
exclude: tuple[str, ...] | None = None,
include: tuple[str, ...] | None = None,
computed: tuple[str, ...] | None = None,
extra_fields: dict[str, Any] | None = None,
related_values: dict[str, str] | None = None,
optional: tuple[str, ...] | None = None,
allow_cycles: bool | None = None,
sort_alphabetically: bool | None = None,
exclude_readonly: bool = False,
meta_override: type | None = None,
model_config: ConfigDict | None = None,
validators: dict[str, Any] | None = None,
module: str = "hare.contrib.pydantic",
exclude_sensitive: bool = False,
relations_as_ids: bool = False,
) -> type[PydanticModel]: ...
| Параметр | Что задаёт |
|---|---|
name |
Явное имя модели вместо автоматического. |
exclude / include |
Какие поля дополнительно убрать или оставить, поверх того, что уже задаёт PydanticMeta. Путь через связь ("author.name") указывает на поле схемы связанной модели; в include он ещё и оставляет саму связь — только с перечисленными полями. |
computed |
Имена @property или методов модели, которые нужно показать как вычисляемые поля; путь через связь ("books.title_length") добавляет такое поле в схему связанной модели. |
extra_fields |
Значения, которые читаются со строки по имени, — аннотация запроса, атрибут, положенный на строку, — с типом; см. Дополнительные поля и значения связей. |
related_values |
Поля связанных строк в плоском виде — имя к пути поля связанной строки; см. Дополнительные поля и значения связей. |
optional |
Поля, которые в схеме становятся необязательными, хотя в модели они не допускают NULL. |
allow_cycles |
Разрешить связям модели с самой собой и циклическим связям уходить в рекурсию (по умолчанию False — циклы обрываются). |
sort_alphabetically |
Упорядочить поля по алфавиту, а не в порядке объявления. |
exclude_readonly |
Схема для записи без полей, которые заполняет база или hare — см. exclude_readonly |
meta_override |
Класс в виде PydanticMeta, заменяющий собственный класс модели, без изменения самой модели: каждая заданная в нём опция, в том числе model_config, заменяет опцию модели. |
model_config |
ConfigDict Pydantic, сливаемый с настройками созданной модели. |
validators |
{validator_name: field_validator(...)(func)} — передаются в __validators__ у pydantic.create_model. |
module |
__module__ созданного класса — по умолчанию "hare.contrib.pydantic" (PYDANTIC_MODELS_MODULE). |
exclude_sensitive |
Убрать каждое поле с sensitive=True (Model._meta.sensitive_fields — см. Чувствительные поля), включая вложенные схемы связей — см. exclude_sensitive |
relations_as_ids |
Показывать каждую прямую связь (внешний ключ, «один-к-одному») плоским полем <name>_id вместо вложенной подмодели — см. Связи как идентификаторы. |
Имя в exclude, include, optional или computed, которое не называет ни поле, ни связь, ни
вычисляемое поле модели (для пути — связанной модели), вызывает ConfigurationError: опечатка в
exclude оставила бы в схеме поле, которое хотели убрать.
exclude_readonly¶
Построить схему для записи, из которой убраны все поля, которые заполняет база: первичный ключ, который
присваивает база или ORM (генерируемый или с default/db_default), и всё с generated=True — в том
числе auto_now/auto_now_add и TSVectorField(stored=True), — а также все обратные связи и
вычисляемые поля (ни то, ни другое нельзя передать при создании). Прямые связи (внешний ключ,
«один-к-одному», «многие-ко-многим») остаются в схеме: их заполняет не база, вызывающий должен их
передать (обязательно или нет — по тем же правилам NULL и значения по умолчанию, что и у любого поля).
Естественный первичный ключ (CharField(primary_key=True) или части CompositePrimaryKey без значения
по умолчанию) остаётся в схеме обязательным полем — вызывающий должен передать его, чтобы создать строку.
Первичный ключ «один-к-одному» представлен своей связью (или полем <name>_id при
relations_as_ids=True).
exclude_sensitive¶
Убрать каждое поле с sensitive=True (Model._meta.sensitive_fields — см. Чувствительные
поля), включая вложенные схемы связей и
значение связи (related_values) такого поля. Полезно для открытой схемы или схемы выгрузки. Схемы,
построенные с ним и без него, сохраняются отдельно и никогда не смешиваются. Поле <name>_blind_index
зашифрованного поля с blind_index=True не попадает ни в какую схему, с ним или без него: hare
записывает его вместе с полем, а его HMAC — не данные.
Созданные модели сохраняются и используются повторно для одинаковых сочетаний (model, options): два
вызова pydantic_model_creator(Author) возвращают один и тот же класс.
Собственные проверки поля в ORM — Field(validators=[...]) (см.
Проверки значений) — тоже выполняются в созданной схеме автоматически:
model_validate() или создание объекта дают ValidationError Pydantic, если любая из них отклонит
значение, — в дополнение к (а не вместо) тому, что вы передали для этого поля через параметр
validators= выше. Поле из optional= принимает явный null (означает «не передано») — проверки ORM
для него не выполняются. Значения внутри контейнера (ArrayField, MapField, TupleField) на любой
глубине проверяются своими полями, как при записи: элемент длиннее своего CharField(max_length=5) или
вне диапазона своего SmallIntField даёт то же сообщение (tags[0]: Length of 'abcdefg' 7 > 5).
Типы значений: BinaryField передаётся в JSON как base64 (model_dump_json() его кодирует,
model_validate_json() раскодирует, в схеме format: base64url), поэтому любые байты доходят
неизменными; чтобы изменить это, передайте свои ser_json_bytes/val_json_bytes в model_config.
Число с плавающей точкой, для которого в JSON нет числа, — NaN или бесконечность у FloatField, в том числе
внутри ArrayField, — пишется текстом ("NaN", "Infinity", как в событии outbox) и читается схемой
обратно (ser_json_inf_nan); так же — бесконечная граница диапазона DecimalRangeField или
DecimalMultiRangeField.
JSONField с объявленным field_type получает в схеме этот тип (без него — Any).
IntEnumField/CharEnumField — это $ref только на перечисление, без числового диапазона или
ограничения длины колонки.
Поле в созданной схеме необязательно, если выполняется хотя бы одно: оно перечислено в optional=; в
модели оно допускает NULL; у него есть значение по умолчанию на стороне Python (default=); у него
есть db_default=; это не первичный ключ и оно явно помечено generated=True (GeneratedField или
любое другое поле, которое заполняет база, — вызывающий не должен его передавать, и в JSON Schema оно
помечено readOnly); это DatetimeField/TimeField с auto_now=True/auto_now_add=True (ORM
заполняет его при сохранении, по той же причине, тоже readOnly). Иначе поле обязательно. Первичный ключ
— отдельный случай: целочисленный автоинкрементный первичный ключ тоже generated=True, но остаётся в
схеме обязательным, как любая часть первичного ключа, — если только он не перечислен в optional=, что
делает его необязательным и допускающим null, как любое другое поле.
Поле, необязательное только потому, что его заполняет база или ORM (db_default, вычисляемое значение,
отметка auto_now/auto_now_add, арендатор), и не допускающее NULL в модели, не попадает в
model_dump(), пока в нём None: Model.objects.create(**schema.model_dump()) оставляет его им, а не
пишет NULL, который колонка отвергнет.
Это работает и с алиасами сериализации, включая model_dump(by_alias=True) и
model_config["serialize_by_alias"]; поля модели, допускающие NULL, сохраняют явный None.
Модель с CompositePrimaryKey обрабатывается так же,
как модель с ключом из одной колонки: каждая часть составного ключа становится отдельным полем созданной
схемы.
Meta.tenant_field тоже необязательно: save()/create() заполняют его текущим арендатором — когда
область состоит из одного значения; при нескольких
значениях входные данные обязаны его
передать, и оно проверяется по области. Это относится и к внешнему ключу на арендатора
(tenant_field = "company_id" или "company" для company = fields.ForeignKeyField(...)) — и во
вложенной подмодели, и в поле company_id.
Вложенная подмодель прямой связи — это <Submodel> | None даже для внешнего ключа без NULL, если у
связанной модели есть Meta.soft_delete_field, Meta.tenant_field или фильтр своего Meta.manager:
чтение связи даёт None для цели, которую скрывает это ограничение, поэтому схема это принимает.
Связи как идентификаторы¶
По умолчанию прямая связь становится вложенной подмоделью связанной модели. API на JSON обычно нужен
плоский идентификатор — это делает relations_as_ids=True:
class Book(Model):
id = fields.IntField(primary_key=True)
title = fields.CharField(max_length=200)
author: fields.ForeignKeyRelation[Author] = fields.ForeignKeyField("models.Author", related_name="books")
editor: fields.ForeignKeyNullableRelation[Author] = fields.ForeignKeyField("models.Author", null=True)
tags: fields.ManyToManyRelation[Tag] = fields.ManyToManyField("models.Tag")
BookSchema = pydantic_model_creator(Book, relations_as_ids=True)
# поля: id, title, author_id (UUID, обязательное), editor_id (UUID | None, необязательное), tags (list[Tag])
BookCreate = pydantic_model_creator(Book, relations_as_ids=True, exclude_readonly=True, exclude=("tags",))
- Имя поля — собственный атрибут модели для колонки ключа,
<name>_id(book.author_id), в том числе когда у внешнего ключа своя колонка в базе черезsource_field=: меняется имя колонки, а не атрибута. Внешний ключ на модель сCompositePrimaryKeyполучает по полю на каждую часть (<name>_<pk_component>). - Тип — тип первичного ключа связанной модели (
int,UUID,strсо своимmax_length, …); допустимостьnullи обязательность определяются самим внешним ключом, как у обычного поля: внешний ключ сnull=True— это<type> | Noneи необязательное поле, внешний ключ безNULLи без значения по умолчанию — обязательное. optional=,exclude_readonly=,exclude_sensitive=действуют на поле идентификатора так же, как на любое другое.exclude_readonly=Trueего оставляет (вызывающий передаёт его при создании). Внешний ключ сsensitive=Trueтеряет свой идентификатор приexclude_sensitive=True.exclude=/include=/optional=принимают и имя связи ("author"), и имя поля идентификатора ("author_id") — оба выбирают одно и то же поле.- «Один-к-одному» с
primary_key=Trueстановится просто своим полем идентификатора, которое и есть первичный ключ;exclude=("owner",)этот первичный ключ никогда не убирает — убирает толькоexclude=("owner_id",). - Обратные связи (обратный внешний ключ, «один-к-одному») не имеют колонки в этой строке, поэтому
плоская схема их вообще не включает —
PydanticMeta.backward_relationsи аннотации не учитываются. - Связи «многие-ко-многим» остаются вложенными подмоделями (
list[TagSchema]) и по-прежнему загружаются заранее. Вложенные подмодели (схема цели «многие-ко-многим») тоже строятся сrelations_as_ids=True. from_hare_orm()/from_queryset()читают идентификатор прямо из объекта — без запроса к связанному объекту. Заранее загружаются только связи «многие-ко-многим», оставшиеся в схеме.- Схема, построенная с этим флагом, сохраняется отдельно от схемы без него; имя схемы без флага не меняется.
GenericForeignKeyField занимает в схеме место своих
веток: на выходе это объединение схем целей, различаемых полем type (имя ветки), на входе
(exclude_readonly=True) и с relations_as_ids=True — {"type": "post", "id": 1}, составной ключ —
полями ключа. from_hare_orm()/from_queryset() заранее загружают его ветки. Имя поля в
exclude=/include= обозначает его ветки вместе с их полями ключа: exclude=("target",) убирает их
все, include=("id", "target") оставляет поле.
Настройки модели по умолчанию: PydanticMeta¶
Объявите в самой модели hare вложенный класс, чтобы задать настройки, которые подхватывает каждый вызов
pydantic_model_creator() для этой модели (явный аргумент функции всё равно важнее):
class Author(Model):
id = fields.UUIDField(primary_key=True)
name = fields.CharField(max_length=120)
books: fields.ReverseRelation["Book"]
class PydanticMeta:
exclude = ("some_internal_field",)
max_recursion = 2
backward_relations = False # не включать `books`, если она явно не указана в аннотации
| Опция | По умолчанию | Что задаёт |
|---|---|---|
include / exclude |
() / ("Meta",) |
Какие поля оставить / убрать. |
computed |
() |
Дополнительные вычисляемые поля. |
extra_fields |
{} |
Значения, которые читаются со строки по имени, с типом. |
related_values |
{} |
Поля связанных строк в плоском виде. |
backward_relations |
True |
Включать ли обратные связи (внешний ключ, «один-к-одному») даже без явной аннотации типа, которая их запрашивает. Не рекомендуется: так можно бесконтрольно загрузить очень много данных. |
max_recursion |
3 |
На сколько уровней вглубь идёт обход связей. |
allow_cycles |
False |
Разрешить рекурсию через связь модели с самой собой. |
exclude_raw_fields |
True |
Убирать колонку ключа внешнего ключа (например, author_id), если сам связанный объект включён. |
sort_alphabetically |
False |
Порядок полей. |
model_config |
None |
Дополнительный ConfigDict Pydantic. |
Вычисляемые поля¶
class Author(Model):
first_name = fields.CharField(max_length=60)
last_name = fields.CharField(max_length=60)
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
AuthorSchema = pydantic_model_creator(Author, computed=("full_name",))
Подходит обычный метод, @property или functools.cached_property. Аннотация возвращаемого типа функции
становится типом поля в схеме. Вычисляемое поле, которое обращается к незагруженной связи, даёт
NoValuesFetched с подсказкой: либо укажите связь в аннотации, чтобы она загружалась заранее, либо сами
сначала загрузите её через prefetch_related_objects().
Дополнительные поля и значения связей¶
BookSchema = pydantic_model_creator(
Book,
extra_fields={"reviews_count": int, "author.books_count": int},
related_values={"author_name": "author.name"},
)
book = await Book.objects.select_related("author").annotate(reviews_count=Count("reviews")).get(id=1)
BookSchema.model_validate(book)
extra_fields — значения, которые читаются со строки по имени, — аннотация запроса, атрибут, положенный на
строку после чтения (их кладёт и api_getter запроса из параметров), — с типом: их строит запрос, и модель
о них не знает. related_values ставят поле связанной строки рядом с полями самой строки, через связи с
одной строкой; при проверке словаря значение берётся и по своему имени. Оба равны None, где у строки
значения нет: запрос его не посчитал, связь не загружена. Имя, записанное путём через связи
("author.books_count"), добавляет поле в схему связанной модели. Имя, которое в схеме уже есть, имя,
объявленное дважды, путь через то, что не связь, и значение связи через связь со многими строками или с
путём, оканчивающимся связью, дают ConfigurationError. Оба объявляются и в PydanticMeta. Схема для
записи (exclude_readonly=True) их не получает — строку с ними не пишут; sort_alphabetically сортирует
их вместе с остальными полями. from_hare_orm()/from_queryset() загружают связи, через которые читается
значение связи, даже если схема саму связь исключает или показывает идентификатором
(relations_as_ids=True).
include и exclude выбирают эти поля по объявленным именам в схеме, как поля модели и вычисляемые
поля. Пути выбирают дополнительные поля вложенных схем ("author.books_count"). Исключённое значение
связи не требует её загрузки. Эти правила действуют и при объявлении в PydanticMeta.
Создание объектов схемы: PydanticModel / PydanticListModel¶
Каждая созданная схема наследует PydanticModel (или PydanticListModel для варианта со списком),
которые добавляют к обычному model_validate() конструкторы, знающие про ORM:
class PydanticModel(BaseModel):
# один объект — сам загружает связи
@classmethod
async def from_hare_orm(cls, obj: Model) -> Self: ...
@classmethod
async def from_queryset_single(cls, queryset: QuerySetSingle[Model]) -> Self: ...
@classmethod
async def from_queryset(cls, queryset: QuerySet[Model]) -> list[Self]: ...
author = await Author.objects.get(pk=author_id)
schema = await AuthorSchema.from_hare_orm(author)
schemas = await AuthorSchema.from_queryset(Author.objects.filter(name__icontains="le guin"))
list_schema = await AuthorListSchema.from_queryset(Author.objects.all())
Все три заранее загружают связи, на которые ссылается сама схема, поэтому вызывать
select_related()/prefetch_related() ради её аннотаций не нужно. Простой
AuthorSchema.model_validate(author) тоже работает (это даёт from_attributes=True), но даёт
NoValuesFetched с именем поля, если схема ссылается на внешний ключ, «один-к-одному»,
«многие-ко-многим» или обратную связь, которые не загружены в author, — даже внешний ключ с NULL,
значение которого действительно None, требует явной загрузки: иначе нельзя отличить «точно пусто» от
«ещё не проверяли». Используйте один из трёх методов выше, а не загружайте связи вручную.
Схема, называющая поле, которого у экземпляра нет, тоже даёт NoValuesFetched — и в from_hare_orm(), и
другими способами: поле, исключённое .only()/.defer(), или значение, которое вычисляет база
(GeneratedField) и которое запись не прочитала обратно, — у нового экземпляра на базе, которая не
возвращает строк записи (ClickHouse), или у ещё не сохранённой копии clone(). Загрузите поле
(refresh_from_db()) или уберите его из схемы.
from_queryset_single() принимает запрос одной строки — .get(...) или .first(): запрос, который не
читает ни одной строки (first() без строк), даёт DoesNotExist, как get(), а запрос многих строк —
QueryError (для него есть from_queryset()).
Поля модели по типам: ModelDescription¶
from hare.contrib.pydantic.descriptions import ModelDescription
description = ModelDescription.from_model(Book)
То, из чего строится схема: поля модели, сгруппированные по типам, каждая группа — список объектов
Field в порядке объявления: pk_fields (по одному на каждую часть составного ключа), data_fields
(все остальные поля с обычным значением, включая колонки ключей прямых связей вроде author_id),
foreign_key_fields, backward_foreign_key_fields, one_to_one_fields, backward_one_to_one_fields и
many_to_many_fields. Связи модели должны быть настроены (Hare.init() или Hare.bind_models()).
pydantic_queryset_creator()¶
def pydantic_queryset_creator(
model: type[Model],
*,
name: str | None = None,
exclude: tuple[str, ...] | None = None,
include: tuple[str, ...] | None = None,
computed: tuple[str, ...] | None = None,
extra_fields: dict[str, Any] | None = None,
related_values: dict[str, str] | None = None,
optional: tuple[str, ...] | None = None,
allow_cycles: bool | None = None,
sort_alphabetically: bool | None = None,
exclude_readonly: bool = False,
meta_override: type | None = None,
model_config: ConfigDict | None = None,
validators: dict[str, Any] | None = None,
module: str = "hare.contrib.pydantic",
exclude_sensitive: bool = False,
relations_as_ids: bool = False,
) -> type[PydanticListModel]: ...
Оборачивает схему одного объекта в схему списка на основе RootModel (по умолчанию с именем
<Model>_list) — удобно как аннотация возвращаемого типа для точки API «список всех X». name= задаёт
имя только схемы списка; схема элемента сохраняет своё имя по умолчанию. Все остальные аргументы строят
схему элемента, как в pydantic_model_creator().