Разрешить передачу только определенных полей модели Pydantic в конечную точку FastAPI

Допустим, у меня есть модель Pydantic с проверкой:

Name = Annotated[str, AfterValidator(validate_name)]

class Foo(BaseModel):
    id: UUID = Field(default_factory=uuid4)
    name: Name

И конечная точка FastAPI:

@app.post('/foos')
def create_foo(foo: Foo) -> Foo:
    save_to_database(foo)
    return foo

Я хочу, чтобы вызывающая сторона могла передавать значение только для name, но не для id. Есть ли способ сделать что-то подобное?

def create_foo(foo: Annotated[Foo, Body(include=['id'])]) -> Foo:

Я знаю, что могу:

@app.post('/foos')
def create_foo(name: Annotated[str, Body(embed=True)]) -> Foo:
    foo = Foo(name=name)
    save_to_database(foo)
    return foo

Но тогда неявная обработка ошибок проверки больше не работает, и для этого мне нужно добавить больше кода.

Есть ли элегантный способ справиться с этим?

Я думаю, что общий способ сделать это — иметь BaseFooModel, у которого есть только имя, а затем наследовать его, чтобы создать модель, имеющую id. некоторые похожие примеры github.com/tiangolo/full-stack-fastapi-template/blob/master/‌​…

python_user 24.05.2024 11:07

Да, модель отдельного запроса тоже возможна, но имеет другие недостатки. Поэтому я хотел узнать, я просто что-то упускаю из виду или для этого действительно нет ничего встроенного.

deceze 24.05.2024 11:10
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
2
488
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Опция 1

Вы можете скрыть/исключить поле при создании/создании объекта, используя Атрибуты частной модели. Pydantic исключит атрибуты модели, имеющие начальное подчеркивание. Как описано в связанной документации:

Атрибуты, имя которых имеет начальное подчеркивание, не рассматриваются как атрибуты. поля от Pydantic и не включены в схему модели. Вместо, они преобразуются в «частный атрибут», который не проверяется. или даже устанавливаться во время звонков на __init__, model_validate и т. д.

Однако обратите внимание, что:

Начиная с Pydantic v2.1.0, вы получите NameError, если попытаетесь использовать функция Поле с частным атрибутом. Потому что частный атрибуты не рассматриваются как поля (как упоминалось ранее), функцию Field() применить нельзя.

Таким образом, в Pydantic V2 вы можете использовать функцию PrivateAttr вместо функции Field вместе с параметром default_factory, чтобы определить вызываемый объект, который будет вызываться для генерации динамического значения по умолчанию (т. е. разного для каждого экземпляр модели) — в данном случае UUID.

Рабочий пример

from fastapi import FastAPI
from pydantic import BaseModel, PrivateAttr
from uuid import UUID, uuid4


class Foo(BaseModel):
    _id: UUID = PrivateAttr(default_factory=uuid4)
    name: str


app = FastAPI()


@app.post("/foo")
def create_foo(foo: Foo):
    print(foo._id)
    return foo

Вариант 2

Просто вариант вышеизложенного (подробнее см. в документации), без использования PrivateAttr и default_factory. Вместо этого метод __init__ используется напрямую для автоматической генерации нового UUID.

Рабочий пример

from fastapi import FastAPI
from pydantic import BaseModel
from uuid import UUID, uuid4


class Foo(BaseModel):
    _id: UUID
    name: str
    
    def __init__(self, **data):
        super().__init__(**data)
        self._id = uuid4()


app = FastAPI()


@app.post("/foo")
def create_foo(foo: Foo):
    print(foo._id)
    return foo

Вариант 3

Другой способ — использовать две разные модели Pydantic: одну, предназначенную для использования пользователем, а вторую, которая должна наследовать от первой (базовой) модели, серверной частью. Подобные примеры приведены в документации FastAPI Дополнительные модели , а также в Полнофункциональный шаблон FastAPI.

Рабочий пример

from fastapi import FastAPI
from pydantic import BaseModel, Field
from uuid import UUID, uuid4


class BaseFoo(BaseModel):
    name: str


class Foo(BaseFoo):
    id: UUID = Field(default_factory=uuid4)
    

app = FastAPI()


@app.post("/foo")
def create_foo(base: BaseFoo):
    foo = Foo(**base.model_dump())  # Foo(name=base.name) should work as well
    print(foo.id)
    return base

Вариант 4

Это вариант вариантов 1 и 2, модифицированный таким образом, чтобы можно было определить их частный атрибут без использования подчеркивания (если это требуется в вашем проекте), но при этом получить тот же результат, что и при использовании любого из предыдущих вариантов. были использованы.

В этом случае используется функция Field. Поскольку атрибут должен быть скрыт от клиента, вам необходимо установить для атрибута exclude значение True, чтобы, если вы вернете экземпляр модели Pydantic обратно клиенту, этот атрибут не будет включен. Кроме того, в Pydantic V2 вы можете использовать аннотацию SkipJsonSchema , чтобы пропустить это поле из сгенерированной схемы JSON, как, например, в автодокументах Swagger UI (для решений Pydantic V1 ознакомьтесь с этим Пост на github и связанное с ним обсуждение).

Теперь нет ничего, кроме как помешать клиенту передать скрытый атрибут в теле запроса JSON, независимо от его сокрытия из схемы и определения его как необязательного/необязательного (т. е. Field(default=None)). Поскольку в этом решении используется обычный Field, а не PrivateAttr, используется атрибут default_factory, как в варианте 1 и как показано ниже:

class Foo(BaseModel):
    id: SkipJsonSchema[UUID] = Field(default_factory=uuid4, exclude=True)
    name: str

будет не самым подходящим подходом, поскольку если бы клиент передал значение для id, это значение было бы присвоено этому полю.

Однако, используя подход, аналогичный варианту 2, т. е. заменяя default_factory на __init__ (который используется для создания поля UUID для id), даже если клиент передал значение для id, оно будет «игнорировано», а сгенерированное по модели будут назначены полю.

Рабочий пример

from fastapi import FastAPI
from pydantic import BaseModel, Field
from uuid import UUID, uuid4
from pydantic.json_schema import SkipJsonSchema


class Foo(BaseModel):
    id: SkipJsonSchema[UUID] = Field(default=None, exclude=True)
    name: str
    
    def __init__(self, **data):
        super().__init__(**data)
        self.id = uuid4()
    

app = FastAPI()


@app.post("/foo")
def create_foo(foo: Foo):
    print(foo.id)
    return foo

Этот ответ и этот ответ также могут оказаться полезными для будущих читателей.

Как заполнить модель Pydantic без default_factory или __init__ перезаписывания предоставленного значения поля

Хотя это не является проблемой при использовании варианта 3, представленного выше (и при желании можно выбрать этот вариант), это может возникнуть при использовании одного из оставшихся вариантов, в зависимости от метода, используемого для заполнения модели.

Например, если вы заполняете модель, используя Foo(**data), где data — это уже существующий экземпляр модели из вашей базы данных, используя один из вариантов, описанных ранее (кроме варианта 3, который не страдает от этой проблемы), значение _id или id (включенное в в словаре data), переданный в модель, будет заменен/перезаписан вновь сгенерированным.

Для решения этой проблемы предлагаются следующие решения.

Решение 1

После вызова Foo(**data) вы можете просто заменить вновь сгенерированный идентификатор существующим, установив значение для этого конкретного поля на data["_id"] или data["id"] (в зависимости от используемой опции).

Пример:

data = {"name": "foo", "_id": "7c4308a2-0f32-1243-b2ad-bf214a24a5aa"}
f = Foo(**data)
f._id = data["_id"]

Решение 2

Вместо использования Foo(**data), который использует метод __init__ модели и, следовательно, при вызове генерируется новое значение id — для полноты картины следует также отметить, что существует дополнительный метод, т. е. model_validate(), который очень похож на метод __init__ модели, за исключением того, что он принимает словарь или объект, а не аргументы ключевого слова — можно было использовать метод model_construct() (в Pydantic V1 раньше это было construct()).

Согласно документации :

Создание моделей без проверки

Pydantic также предоставляет метод model_construct(), который позволяет создавать модели без проверки. Это может быть полезно по крайней мере в нескольких случаях:

  • при работе со сложными данными, о которых уже известно, что они действительны (из соображений производительности)
  • когда одна или несколько функций валидатора неидемпотентны, или
  • когда одна или несколько функций валидатора имеют побочные эффекты, которые вы не хотите запускать.
Предупреждение

model_construct() не выполняет никакой проверки, то есть может создавать недействительные модели. Вам следует использовать только model_construct() метод с уже проверенными данными, или которому вы определенно доверяете.

[...]

При создании экземпляра с использованием model_construct() нет __init__ будет метод из модели или любого из ее родительских классов вызывается, даже если определен собственный метод __init__.

Пример:

data = {"name": "foo", "_id": "7c4308a2-0f32-1243-b2ad-bf214a24a5aa"}
f = Foo.model_construct(**data)

Это не те решения, на которые я надеялся, но я признаю, что лучшего пути нет. Спасибо!

deceze 01.06.2024 09:44

Пожалуйста. Могу ли я спросить, например, в чем проблема с вариантом 1 или 2? Неужели вы не можете напечатать/передать в базу данных частный атрибут, т. е. _id, при использовании объекта foo и его нужно вызывать индивидуально, например, foo._id?

Chris 01.06.2024 13:11

Просто кажется, что всегда приходится использовать _id вместо обычного id, что оказывает ненужное влияние на остальную часть кода. В моем случае я также сериализую это в простое хранилище NoSQL, где _ может показаться странным, поэтому мне придется выполнить там дополнительное сопоставление.

deceze 01.06.2024 13:18

Я только что добавил четвертый вариант, который может удовлетворить ваши требования (однако я все равно, вероятно, выберу один из предыдущих вариантов).

Chris 03.06.2024 19:33

Ммм, это как бы противоположность желаемому. id должен быть включен в ответ JSON и не должен генерироваться с нуля каждый раз, когда используется модель. Я просто не хочу, чтобы клиент мог сам отправлять или помещать идентификатор. Я думаю, что это довольно распространенный вариант использования на самом деле‽

deceze 03.06.2024 20:02

«В ответ JSON следует включить id...» — в этом случае вы можете просто удалить exclude=True. «... не следует генерировать с нуля каждый раз, когда используется модель». - Вся цель default_factory (или альтернативного способа, упомянутого в документации и продемонстрированного в вариантах 2 и 4), заключается в том, что вам нужно значение поля ️ 🔁 «быть динамичным (т. е. разным для каждой модели)» , как в случае с UUID.

Chris 03.06.2024 22:10

Да, я хочу, чтобы для каждого нового экземпляра оно было разным. Я не хочу, чтобы идентификатор перезаписывался вновь сгенерированным идентификатором каждый раз, когда я восстанавливаю уже существующий экземпляр из своей базы данных с помощью Foo(**data).

deceze 04.06.2024 09:44

Ответ выше был обновлен соответствующими решениями. Надеюсь, что это работает для вас.

Chris 06.06.2024 08:21

Думаю, это вариант, спасибо. На данный момент я использовал свой последний образец, который я показал в своем вопросе + try..except для обработки ошибок проверки; но для более сложных случаев с большим количеством параметров я рассмотрю одно из ваших предложений.

deceze 06.06.2024 08:47

Другие вопросы по теме