Создаем инстансы нужных моделей 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, чтобы я смог вам ответить!