Создаем инстансы нужных моделей Pydantic по входным данным
Задача
Рассмотрим практическую задачу в несколько упрощенном виде. Допустим, бэкенду нужно по полученным с фронта данным – названию ресурса API (скажем, их несколько десятков), и специфичным для каждого ресурса параметрам (произвольный набор и количество для каждого) сделать запрос к соответствующему ресурсу API с переданным набором параметров.
Можно, конечно, использовать условные операторы, таблицы принятия решений и т.п., но гораздо проще использовать Pydantic (тем более, если он уже есть в проекте) для решения данной задачи.
Ниже на примере пошагово рассмотрим, как это реализовать.
Предлагаемая реализация
Предположим, что в списке - варианты данных, которые мы можем получить с фронта: ресурс API, к которому надо обратиться, и специфичные параметры для запроса.
data = [
{"resource_name": "user", "username": "hazadus"},
{"resource_name": "client", "phone": "(812) 355-66-55"},
{"resource_name": "note", "text": "Buy the milk"},
]
Опишем базовую модель ресурса следующим образом:
class BaseResource(BaseModel):
# В этой модели могут быть поля, общие для всех ресурсов, например host, auth_token, etc.
# Опустим их для краткости
http_method: ClassVar[str] = "GET"
def _get_request_url(self) -> str: ...
def _get_headers(self) -> dict: ...
def _get_json(self) -> dict: ...
def request(self) -> str:
"""Условный запрос к API используя указанный метод, URL и данные"""
return f"{self.http_method} {self._get_request_url()} data={self._get_json()}"
Далее, наследуем BaseResource
и описываем данные и поведение индивидуальных классов, реализующих доступ к определенным ресурсам и методам:
class User(BaseResource):
# Для каждого ресурса укажем его тип в `resource_name`. По этому полю Pydantic будет отличать ресурсы
resource_name: Literal["user"]
# Это поле мы планиуем получить из входных данных, оно будет валидироваться
username: str
# Константа, которой не должно быть в данных
# Ref: https://docs.pydantic.dev/2.6/errors/usage_errors/#model-field-missing-annotation
endpoint: ClassVar[str] = "/users/"
http_method: ClassVar[str] = "GET"
# специфичная логика запроса к ресурсу прописывается здесь, общую
# нужно вынести в базовый класс
def _get_request_url(self):
return f"http://localhost{self.endpoint}{self.username}/?order=desc"
class Client(BaseResource):
resource_name: Literal["client"]
phone: str
endpoint: ClassVar[str] = "/clients/"
http_method: ClassVar[str] = "POST"
def _get_request_url(self):
return f"http://localhost{self.endpoint}"
def _get_json(self) -> dict:
return {"phone": self.phone}
class Note(BaseResource):
resource_name: Literal["note"]
text: str
endpoint: ClassVar[str] = "/notes/"
http_method: ClassVar[str] = "POST"
def _get_request_url(self):
return f"http://localhost{self.endpoint}"
def _get_json(self) -> dict:
return {"text": self.text}
Самая «изюминка» происходит в следующей части примера:
class API(BaseModel):
# При помощи union перечисляем, какого типа может быть свойство resource
# и указываем дискриминатор - поле, по которому будет идентифицирован нужный тип
resource: User | Client | Note = Field(..., discriminator="resource_name")
for json in data:
try:
# ✨ Магия происходит здесь:
api = API(resource=json)
# `api.resource` будет инстансом нужного класса, в зависимости от `json.resource_name`!
# Поэтому `api.resource.request()` запустит специфичную для нужного ресурса логику запроса:
print(api.resource.request())
except ValidationError as ex:
print("\n\nWrong resource type or incorrect params:", ex)
Результат работы примера будет следующим:
GET http://localhost/users/hazadus/?order=desc data=None
POST http://localhost/clients/ data={'phone': '(812) 355-66-55'}
POST http://localhost/notes/ data={'text': 'Buy the milk'}
Wrong resource type or incorrect params: 1 validation error for API
resource.note.text
Field required [type=missing, input_value={'resource_name': 'note'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.6/v/missing
Wrong resource type or incorrect params: 1 validation error for API
resource
Input tag 'unknown' found using 'resource_name' does not match any of the expected tags: 'user', 'client', 'note' [type=union_tag_invalid, input_value={'resource_name': 'unknown'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.6/v/union_tag_invalid
Мы видим, что по корректным входным данным были созданы инстансы требуемых моделей. В случае неверных данных (они есть в полном примере), ошибка, выдаваемая Pydantic, подробнейшим образом описывает, что пошло не так.
Рассмотренное решение работает благодаря фиче Pydantic под названием Discriminated Unions. В документации есть более компактный пример и описание этой возможности на котиках и собачках.
В реальном коде кроме resource_name
стоит добавить еще поле method
для обращения к различным методам
ресурса, и создать соответствующие подклассы, например NotePost, NoteGetById, NotePatch и т.п.,
прописав в них только специфичные параметры и логику – всё остальное сделает базовый класс.
Надеюсь, эта крутая фишка Pydantic пригодится и вам. Мне она помогла радикально сократить и упростить код при написании микросервиса для интеграции с большим количеством методов внешнего API!
Дополнительные материалы
Отправить сообщение
С помощью формы ниже, вы можете связаться с автором сайта. Пожалуйста, укажите ваш ник в Телеграме или e-mail, чтобы я смог вам ответить!