Друзья, приветствую вас в очередной статье, посвященной разработке API с использованием фреймворка FastAPI. В прошлой публикации мы познакомились с основами FastAPI и написали первые функции, освоив GET-запросы. Однако возможности HTTP общения клиента и сервера этим не ограничиваются. Сегодня мы изучим POST, PUT и DELETE запросы.

В прошлой статье мы рассмотрели GET запросы и научились писать свои первые функции. Сегодня же мы рассмотрим методы, позволяющие отправлять данные (POST), обновлять (PUT) и удалять данные (DELETE).

Для того чтобы эти операции были не только возможны, но и выполнялись правильно и эффективно, необходимо использовать модели.

Что такое модели в FastApi?

Модель в FastAPI — это нечто вроде схемы или шаблона, который описывает структуру данных, с которыми работает ваше приложение. Проще говоря, это способ сказать: "Вот как должны выглядеть данные, которые мы принимаем или отправляем."

Основные задачи моделей в FastAPI:

  1. Валидация данных: С помощью Pydantic мы можем проверять, что данные соответствуют ожидаемому формату. Например, если нам нужен объект пользователя с именем и возрастом, модель проверит, что имя — это строка, а возраст — число.

  2. Документирование данных: Модели помогают автоматически создавать документацию для вашего API. Клиенты могут легко понять, какие данные они должны отправить или могут ожидать в ответе.

  3. Работа с базами данных: Модели можно использовать для описания структуры данных в базе данных. Например, с помощью библиотек, таких как SQLAlchemy, мы можем создавать модели, которые напрямую связываются с таблицами в базе данных.

Корректно описывая собственные модели, вы, в первую очередь, облегчите пользователям вашего API понимание того, какие данные необходимо отправлять и ожидать в ответ. Это значительно упрощает интеграцию и использование вашего API, делая его более интуитивно понятным и удобным.

Пример модели в FastAPI:

Представим, что мы создаём API для управления пользователями. Мы можем создать модель, описывающую пользователя (подробно рассмотрим далее, сейчас просто посмотрите на общий синтаксис):

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

Эта модель поможет нам:

  • Проверить, что данные, отправляемые клиентом, содержат строку name и целое число age.

  • Сгенерировать документацию, показывающую, что ожидается в запросах и ответах.

  • Использовать эту модель для работы с базой данных, создавая таблицу для хранения пользователей.

Дополнительные возможности моделей:

  • Преобразование данных: Модели могут автоматически преобразовывать данные. Например, строку, содержащую дату, можно автоматически преобразовать в объект даты.

  • Описание вложенных структур: Модели могут включать в себя другие модели, что позволяет описывать сложные вложенные структуры данных.

  • Документация и примеры: Модели могут включать описание полей и примеры данных, что улучшает документацию вашего API.

Таким образом, модели в FastAPI являются мощным инструментом для обеспечения правильности, безопасности и удобства работы с данными в вашем приложении.

Pydantic

Pydantic — это библиотека, которая встроена в FastAPI и используется для работы с данными. Она помогает проверять и преобразовывать данные, чтобы они соответствовали нужным форматам. Когда вы создаете модели в FastAPI, вы фактически используете возможности Pydantic.

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

Представьте, что даже ваши бабушка и кот смогут разобраться, как пользоваться вашим API. И что самое приятное, вам не придётся краснеть за свою работу на встречах с коллегами или перед заказчиками. Всё будет выглядеть так, как будто вы — настоящий гуру API-разработки!

Что делает Pydantic:

  1. Валидация данных: Pydantic проверяет, что данные соответствуют ожидаемым типам. Например, если вы ожидаете строку, а вам прислали число, Pydantic выдаст ошибку.

  2. Преобразование данных: Он может автоматически преобразовывать данные. Например, если вы ожидаете дату, а получили строку, Pydantic попытается преобразовать эту строку в дату.

  3. Документирование данных: Pydantic позволяет добавлять описания к полям модели, что помогает в создании документации для API.

Модели в FastAPI, созданные с помощью Pydantic, играют ключевую роль. Они позволяют точно и безопасно описывать структуру данных, валидацию и приведение типов, что делает разработку API более предсказуемой и защищённой от ошибок. С помощью моделей мы можем задать строгие правила для входных и выходных данных, что значительно упрощает написание и поддержку кода.

Надеюсь, что я не  сильно утомил вас теорией, но то что я описал выше необходимо понять уже сейчас, не в будущем. Просто поймите, что на старте, создавая собственное API, его необходимо делать надежным и удобным для пользователя.

POST запросы на практике

POST запросы — это один из основных типов HTTP-запросов, используемых для отправки данных на сервер. В отличие от GET-запросов, которые обычно используются для получения данных, POST-запросы отправляют данные, которые сервер должен обработать. На практике это чаще всего используется для создания новых записей в базе данных или отправки данных для обработки.

Два самых частых и понятных примера POST запросов:

1. Отправка данных после заполнения формы на сайте (например, форма регистрации):

В этом сценарии фронтенд-часть веб-приложения собирает данные, которые пользователь ввел в форме регистрации.

После клика на кнопку «ОТПРАВИТЬ», данные отправляются на бэкенд через POST-запрос. Бэкенд, настроенный для обработки запросов на определенном маршруте, ожидает получения этих данных.

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

Пример POST-запроса для регистрации пользователя (тоже пока смотрим, подробнее разбирать начнем совсем скоро):

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class UserRegistration(BaseModel):
    username: str
    password: str
    email: str


@app.post("/register/")
async def register_user(user: UserRegistration):
    # Логика регистрации пользователя
    return {"message": "User registered successfully", "user": user}

2. Покупка товара в любом интернет-магазине:

Корзина в интернет-магазине представляет собой массив данных с описанием товаров. В корзине обычно содержится информация об ID товара, его количестве, цене, а также данные о пользователе и прочая сопутствующая информация.

После клика на кнопку «КУПИТЬ» данные отправляются на бэкенд через POST-запрос, где происходит обработка заказа: проверка наличия товаров на складе, расчёт итоговой стоимости, создание заказа в системе и отправка подтверждения пользователю.

Пример POST-запроса для покупки товара:

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List

app = FastAPI()


class Item(BaseModel):
    item_id: int
    quantity: int


class Purchase(BaseModel):
    user_id: int
    items: List[Item]


@app.post("/purchase/")
async def create_purchase(purchase: Purchase):
    # Логика обработки покупки
    return {"message": "Покупка успешна!", "purchase": purchase}

Понимаю, что сейчас может быть не все понятно, но мы это совсем скоро исправим.

POST запросы на реальной практике

Если вы читали мою прошлую статью по FastAPI, то знаете что мы создаем API для некоего университета. Мы с вами уже работали со студентами, получали по ним информацию по разным запросам.

Давайте теперь выступим в роли администратора API, который должен добавить студента в базу данных.

Для начала давайте напишем модель нашего студента. Напоминаю, что массивы выглядели так:

{
  "student_id": 1,
  "first_name": "Иван",
  "last_name": "Иванов",
  "date_of_birth": "1998-05-15",
  "email": "ivan.ivanov@example.com",
  "phone_number": "+7 (123) 456-7890",
  "address": "г. Москва, ул. Пушкина, д. 10, кв. 5",
  "enrollment_year": 2017,
  "major": "Информатика",
  "course": 3,
  "special_notes": "Без особых примет"
}

Я предлагаю и далее придерживаться данной модели. Что мы тут видим?

Student_id – это целое число (int), как и enrollment_year и course. Но у них же есть ограничения, верно? Допустим наш админ решит по только ему ведомой причине написать, что студент в университет поступил в 1980, когда университет открылся в 2010, да ещё и на 7-й курс, когда курсов всего 5. Для того чтоб такое у нас не прошло мы и будем использовать Pydentic.

В Pydentic уже предусмотрены случаи, когда мы записываем диапазон допустимых значений. Например курс от 1 до 5 или год от 2010 до 2024. Далее мы посмотрим как оно работает.

Идем дальше. Что там у нас с остальными данными?

Есть у нас и специфические данные. К примеру это email. Через стандартный email: str мы не особо сильно сможем указать, что мы ждем emai, а не другую строку. Конечно, можно сильно заморочиться. Использовать регулярные выражения, писать запутанные валидаторы и прочее, но в этом смысла нет, ведь Pydentic уже многое придумал за нас, упростив нам жизнь.

В контексте email мы прямо с модуля Pydentic можем импортировать EmailStr, а после нам достаточно будет просто передать данный объект класса в описание поля Pydentic (совсем скоро рассмотрим на реальном примере, потерпите).

Некоторые поля, такие как «special_notes» нам, возможно, не захочется передавать явно и достаточно будет указать просто «Без особых примет» по умолчанию. Это нам тоже позволит сделать Pydentic.

Бывают случаи когда нам нужно выбрать из указанного значения. К примеру это факультеты. Мы знаем что в нашем университете, к примеру, 5 факультетов и, возможно, есть необходимость ограничить этот ввод данных. Тут, к сожалению, в самом Pydentic метода нет, но я вам покажу как можно легко обойти эту проблему без внутреннего валидатора.

А как быть, если Pydentic не имеет специального типа данных или у нас есть какая-то специфическая история с параметром? В этом случае нам на помощь прийдет внутренний валидатор Pydentic (рассмотрим как он работает на примере с телефоном).

Надеюсь, что вы поняли что у нас за проблема и зачем мы ее будем решать при помощи Pydentic. Начнем делать описание модели. Тут как обычно. Сначала полный код, а после пояснение кода.

from enum import Enum
from pydantic import BaseModel, EmailStr, Field, field_validator, ValidationError
from datetime import date, datetime
from typing import Optional
import re


class Student(BaseModel):
    student_id: int
    phone_number: str = Field(default=..., description="Номер телефона в международном формате, начинающийся с '+'")
    first_name: str = Field(default=..., min_length=1, max_length=50, description="Имя студента, от 1 до 50 символов")
    last_name: str = Field(default=..., min_length=1, max_length=50, description="Фамилия студента, от 1 до 50 символов")
    date_of_birth: date = Field(default=..., description="Дата рождения студента в формате ГГГГ-ММ-ДД")
    email: EmailStr = Field(default=..., description="Электронная почта студента")
    address: str = Field(default=..., min_length=10, max_length=200, description="Адрес студента, не более 200 символов")
    enrollment_year: int = Field(default=..., ge=2002, description="Год поступления должен быть не меньше 2002")
    major: Major = Field(default=..., description="Специальность студента")
    course: int = Field(default=..., ge=1, le=5, description="Курс должен быть в диапазоне от 1 до 5")
    special_notes: Optional[str] = Field(default=None, max_length=500,
                                         description="Дополнительные заметки, не более 500 символов")

    @field_validator("phone_number")
    @classmethod
    def validate_phone_number(cls, values: str) -> str:
        if not re.match(r'^\+\d{1,15}$', values):
            raise ValueError('Номер телефона должен начинаться с "+" и содержать от 1 до 15 цифр')
        return values

    @field_validator("date_of_birth")
    @classmethod
    def validate_date_of_birth(cls, values: date):
        if values and values >= datetime.now().date():
            raise ValueError('Дата рождения должна быть в прошлом')
        return values

Надеюсь, что не сильно страшно, но это и не важно. Сейчас разберемся.

Импорты:

from enum import Enum
from pydantic import BaseModel, EmailStr, Field, field_validator, ValidationError
from datetime import date, datetime
from typing import Optional
import re
  • from enum import Enum: для создания перечислений (enums).

  • from pydantic import BaseModel, EmailStr, Field, field_validator, ValidationError: Pydantic используется для создания моделей данных и валидации.

  • from datetime import date, datetime: для работы с датами.

  • from typing import Optional: для указания необязательных полей.

  • import re: для использования регулярных выражений.

Перечисления, как вы поняли, нам будут нужны для того чтоб задать те факультеты, которые есть в нашем университете и это мы использовали в данном классе:

class Major(str, Enum):
    informatics = "Информатика"
    economics = "Экономика"
    law = "Право"
    medicine = "Медицина"
    engineering = "Инженерия"
    languages = "Языки"

Класс Major наследуется от str и Enum для обеспечения определенных функциональных возможностей.

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

Синтаксис, как вы видите, достаточно простой, но, при этом, очень функциональный и я уверен, что использовать подобные записи вы будете часто.

Конечно, сам по себе класс Major нам не особо интересен и далее он будет использован в описании модели нашего студента.

Класс Student

Класс Student позволяет описывать студента и проверять корректность его данных. Он может быть использован в POST-запросах для проверки, что данные при добавления студента передаются корректно, а также для документирования API, чтобы правильно получать данные о студенте. Все это мы рассмотрим далее на конкретных примерах.

Сам класс можно разделить на две условные группы:

  1. Описание полей (field)

  2. Внутренние валидаторы

Класс всегда будет наследоваться от BaseModel.

Зачем это нужно?

Когда мы наследуем класс от BaseModel, это означает, что наш класс Student получает все функции и возможности, которые предоставляет Pydantic для работы с моделями данных.

Описание полей может быть, как максимально простым:

student_id: int

Так и с указанием параметров и условий:

phone_number: str = Field(default=..., description="Номер телефона в международном формате, начинающийся с '+'")

В данном случае мы использовали класс Field для описания поля. Данный класс в библиотеке Pydantic используется для добавления дополнительных метаданных к полям моделей, которые наследуются от BaseModel.

Обратите внимание. Тут мы использовали 2 параметра: default и description.

Тут все достаточно логично. Default = это то значение, которое в поле будет использоваться по умолчанию. Если мы передадим значение «…» аргументом этого параметра, то это будет значить, что данное значение обязательно.

В связи с этим, часто указание «default» игнорируется и запись начинается с …

phone_number: str = Field(..., description="Номер телефона в международном формате, начинающийся с '+'")

description в Field используется для предоставления описания поля. Это описание может быть использовано для генерации автоматической документации, подсказок в IDE (средах разработки) или для облегчения понимания назначения поля.

Теперь разберем более детальную валидацию поля.

first_name: str = Field(default=..., min_length=1, max_length=50, description="Имя студента, от 1 до 50 символов")

Тут вы видите 2 новых параметра: min_lenght и max_lenght. Не трудно догадаться, что тем самым мы описываем минимальную длину строки и максимальную длину строки.

То есть, если значение фамилии будет больше 50, то мы получим ошибку валидации.

course: int = Field(default=..., ge=1, le=5, description="Курс должен быть в диапазоне от 1 до 5")

Аббревиатуры ge и le в контексте библиотеки Pydantic используются для установки ограничений на числовые значения полей моделей данных. Давайте разберем, что они означают и почему используется именно такая аббревиатура.

ge и le в Pydantic

  • ge: Это сокращение от "greater than or equal" (больше или равно). Используется для установки минимального допустимого значения для числового поля. Если значение поля меньше указанного, будет вызвано исключение валидации.

  • le: Это сокращение от "less than or equal" (меньше или равно). Используется для установки максимального допустимого значения для числового поля. Если значение поля больше указанного, также будет вызвано исключение валидации.

В описании полей есть и другие значения. Далее я представил короткое описание некоторых параметров, которые достаточно часто используются в описании:

  • title: Заголовок поля. Используется для документации или автоматической генерации API.

  • examples: Примеры значений для поля. Используются для документации и обучения.

  • gt, ge, lt, le: Ограничения для числовых значений (больше, больше или равно, меньше, меньше или равно).

  • multiple_of: Число, на которое значение должно быть кратно.

  • max_digits, decimal_places: Ограничения для чисел с плавающей точкой (максимальное количество цифр, количество десятичных знаков).

Больше информации вы можете получить в официальной документации Pydentic.

Вот полное описание каждого поля, которое относится к студенту:

student_id: int
phone_number: str = Field(default=..., description="Номер телефона в международном формате, начинающийся с '+'")
first_name: str = Field(default=..., min_length=1, max_length=50, description="Имя студента, от 1 до 50 символов")
last_name: str = Field(default=..., min_length=1, max_length=50, description="Фамилия студента, от 1 до 50 символов")
date_of_birth: date = Field(default=..., description="Дата рождения студента в формате ГГГГ-ММ-ДД")
email: EmailStr = Field(default=..., description="Электронная почта студента")
address: str = Field(default=..., min_length=10, max_length=200, description="Адрес студента, не более 200 символов")
enrollment_year: int = Field(default=..., ge=2002, description="Год поступления должен быть не меньше 2002")
major: Major = Field(default=..., description="Специальность студента")
course: int = Field(default=..., ge=1, le=5, description="Курс должен быть в диапазоне от 1 до 5")
special_notes: Optional[str] = Field(default=None, max_length=500,
                                     description="Дополнительные заметки, не более 500 символов")

Теперь вы понимаете что тут и к чему. Отлично. Теперь рассмотрим внутренние валидаторы.

@field_validator("phone_number")
@classmethod
def validate_phone_number(cls, values: str) -> str:
    if not re.match(r'^\+\d{1,15}$', values):
        raise ValueError('Номер телефона должен начинаться с "+" и содержать от 1 до 15 цифр')
    return values

@field_validator("date_of_birth")
@classmethod
def validate_date_of_birth(cls, values: date):
    if values and values >= datetime.now().date():
        raise ValueError('Дата рождения должна быть в прошлом')
    return values

Как вы видите, в моем примере есть 2 валидатора: тот, который проверяет корректность номера телефона и тот, который проверяет корректность даты рождения. Давай рассмотрим каждый.

Валидатор для номера телефона:

@field_validator("phone_number")
@classmethod
def validate_phone_number(cls, values: str) -> str:
    if not re.match(r'^+\d{1,15}$', values):
        raise ValueError('Номер телефона должен начинаться с "+" и содержать от 1 до 15 цифр')
    return values

Проверяет, что номер телефона начинается с "+" и содержит от 1 до 15 цифр. Для этого мы используем простое регулярное выражение.

Смысл работы тут достаточно простой. Сначала мы в декоратор field_validator передаем название поля, в котором мы будем проверять валидность данных.

Далее мы начинаем писать функцию. Так как это метод класса – первым аргументом мы передаем класс, а второй аргумент - это то значение, валидность, которого мы будем проверять.

Далее все просто или вернем значение, тем самым подтвердим, что данные валидны или вернем исключение.

Валидатор для даты рождения:

@field_validator("date_of_birth")
@classmethod
def validate_date_of_birth(cls, values: date):
    if values and values >= datetime.now().date():
        raise ValueError('Дата рождения должна быть в прошлом')
    return values

Проверяет, что дата рождения находится в прошлом.

Основные моменты:

  • Pydantic: Обеспечивает валидацию и автоматическую сериализацию данных.

  • Enum: Используется для ограничения значений специальности.

  • Field: Определяет ограничения на поля, такие как минимальная/максимальная длина, описание и т.д.

  • Validators: Пользовательские проверки данных (например, формат телефона и дата рождения)

Важный момент. Pydantic не обязательно использовать только в контексте FastApi, ведь это достаточно универсальная технология. К примеру, в следующих статьях, когда мы начнем рассматривать взаимодействие с базой данных через ORM мы так же будем использовать Pydentic.

Теперь, чтоб закрепить полученные знания, давайте протестируем наш класс на простых примерах (пока без привязки к FastApi). Напоминаю, что исходники кода по циклу статей про разработку собственного API вы найдете только в моем телеграмм канале.

Тестируем модель

Для начала напишем простую функцию. Принимать она будет словарь с данными о студенте. Далее, в результате, она будет или печатать ошибку или будет выводить информацию о студенте:

def test_valid_student(data: dict) -> None:
    try:
        student = Student(**data)
        print(student)
    except ValidationError as e:
        print(f"Ошибка валидации: {e}")

Несмотря на кажущуюся простоту функции, прямо сейчас, на данном примере, вы увидите как работает валидация.

Для начала возьмем такие данные:

student_data = {
    "student_id": 1,
    "phone_number": "+1234567890",
    "first_name": "Иван",
    "last_name": "Иванов",
    "date_of_birth": date(2000, 1, 1),
    "email": "ivan.ivanov@example.com",
    "address": "Москва, ул. Пушкина, д. Колотушкина",
    "enrollment_year": 1022,
    "major": "Информатика",
    "course": 3,
    "special_notes": "Увлекается программированием"
}

Вызовем функцию и посмотрим на результат:

Ошибка валидации:

1 validation error for Student

enrollment_year

Input should be greater than or equal to 2002 [type=greater_than_equal, input_value=1022, input_type=int]

Мы получаем ошибку, но нас Pydentic не оставляет в неведении. Он сообщает что мы передали год поступления 1022, а нужно было указать минимум 2002.

Исправим это значение:

student_data = {
    "student_id": 1,
    "phone_number": "+1234567890",
    "first_name": "Иван",
    "last_name": "Иванов",
    "date_of_birth": date(2000, 1, 1),
    "email": "ivan.ivanov@example.com",
    "address": "Москва, ул. Пушкина, д. Колотушкина",
    "enrollment_year": 2022,
    "major": "Информатика",
    "course": 6,
    "special_notes": "Увлекается программированием"
}

Снова ошибка. Что не так? Смотрим:

Ошибка валидации: 1 validation error for Student

course

  Input should be less than or equal to 5 [type=less_than_equal, input_value=6, input_type=int]

К году поступления больше нет вопросов, а вот курс у нас указан 6-й, когда максимальный 5. Все понятно. Исправим.

student_data = {
    "student_id": 1,
    "phone_number": "+1234567890",
    "first_name": "Иван",
    "last_name": "Иванов",
    "date_of_birth": date(2000, 1, 1),
    "email": "ivan.ivanov@example.com",
    "address": "Москва, ул. Пушкина, д. Колотушкина",
    "enrollment_year": 2022,
    "major": "Информатика",
    "course": 3,
    "special_notes": "Увлекается программированием"
}
student_id=1 phone_number='+1234567890' first_name='Иван' last_name='Иванов' date_of_birth=datetime.date(2000, 1, 1) email='ivan.ivanov@example.com' address='Москва, ул. Пушкина, д. Колотушкина' enrollment_year=2022 major=<Major.informatics: 'Информатика'> course=3 special_notes='Увлекается программированием'

Результат мы получили такой, а значит все данные переданы корректно.

Теперь, чтоб убедиться что у нас корректно отрабатывает перечисление передадим факультет, которого не существует у нас.

student_data = {
    "student_id": 1,
    "phone_number": "+1234567890",
    "first_name": "Иван",
    "last_name": "Иванов",
    "date_of_birth": date(2000, 1, 1),
    "email": "ivan.ivanov@example.com",
    "address": "Москва, ул. Пушкина, д. Колотушкина",
    "enrollment_year": 2022,
    "major": "Программирование",
    "course": 3,
    "special_notes": "Увлекается программированием"
}

Результат:

Input should be 'Информатика', 'Экономика', 'Право', 'Медицина', 'Инженерия' or 'Языки' [type=enum, input_value='Программирование', input_type=str]

Мы видим ошибку и говорит она о том, что мы не попали в перечисленные факультеты:

Доступные варианты: 'Информатика', 'Экономика', 'Право', 'Медицина', 'Инженерия' or 'Языки'

student_data = {
    "student_id": 1,
    "phone_number": "+1234567890",
    "first_name": "Иван",
    "last_name": "Иванов",
    "date_of_birth": date(2000, 1, 1),
    "email": "ivan.ivanov@example.com",
    "address": "Москва, ул. Пушкина, д. Колотушкина",
    "enrollment_year": 2022,
    "major": "Информатика",
    "course": 3
}

Результат:

student_id=1 phone_number='+1234567890' first_name='Иван' last_name='Иванов' date_of_birth=datetime.date(2000, 1, 1) email='ivan.ivanov@example.com' address='Москва, ул. Пушкина, д. Колотушкина' enrollment_year=2022 major=<Major.informatics: 'Информатика'> course=3 special_notes=None

Обратите внимание. Мы не передали ключ special_notes, но при этом никаких ошибок не получили. Все дело в том, что в описании этого поля мы передавали default = None.

Старался объяснить работу Pydantic максимально доступно для каждого и надеюсь что к данному моменту вы разобрались что тут и к чему, а значит что мы можем начать внедрять данную модель в FastApi.

Pydentic модель и GET эндпоинт

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

Для того чтоб все это работало нам необходимо передать response_model (модель ответа). Передать ее можно двумя разными способами.

@app.get("/student", response_model=SStudent)
def get_student_from_param_id(student_id: int):
    students = json_to_dict_list(path_to_json)
    for student in students:
        if student["student_id"] == student_id:
            return student

В данном случае мы передаем дополнительный аргумент response_model прямо в декоратор. Обратите внимание, я добавил в имя класса дополнительную S. Тем самым я, обычно, описываю что в данном случае речь конкретно про схему (модель).

Второй способ передачи response_model выглядит так:

@app.get("/student")
def get_student_from_param_id(student_id: int) -> SStudent:
    students = json_to_dict_list(path_to_json)
    for student in students:
        if student["student_id"] == student_id:
            return student

В данном синтаксисе мы тоже указываем, что response_model – это SStudent. В работе я чаще использую такой вариант, но вы выбирайте тот что вам более удобен.

Теперь, когда мы передали модель ответа, давайте посмотрим в документацию. Изменилось ли там что либо. Напоминаю, что для запуска необходимо выполнить эту команду из корня проекта:

uvicorn app.main:app --reload  

Заходим в документацию (http://127.0.0.1:8000/docs) и видим:

Мы видим, что благодаря данной модели FastApi построил свой пример ответа. Это очень удобно, не так ли?

Выполним запрос для получения студента с ID = 1 и получим данные:

{
  "student_id": 1,
  "phone_number": "+71234567890",
  "first_name": "Иван",
  "last_name": "Иванов",
  "date_of_birth": "1998-05-15",
  "email": "ivan.ivanov@example.com",
  "address": "г. Москва, ул. Пушкина, д. 10, кв. 5",
  "enrollment_year": 2017,
  "major": "Информатика",
  "course": 3,
  "special_notes": "Без особых примет"
}

В JSON (оттуда мы тянем информацию) данные записаны у меня так:

{
  "student_id": 1,
  "first_name": "Иван",
  "last_name": "Иванов",
  "date_of_birth": "1998-05-15",
  "email": "ivan.ivanov@example.com",
  "phone_number": "+71234567890",
  "address": "г. Москва, ул. Пушкина, д. 10, кв. 5",
  "enrollment_year": 2017,
  "major": "Информатика",
  "course": 3,
  "special_notes": "Без особых примет"
},

Тут интересно поле с датой рождения. Вы видите, что у меня она записана строкой «1998-05-15», но при этом мы не получили ошибку. Это одна из фишек Pydentic.

Давайте теперь изменим одно из значений у этого пользователя (сделаем не валидным) и посмотрим что у нас получится.

{
  "student_id": 1,
  "first_name": "Иван",
  "last_name": "Иванов",
  "date_of_birth": "1998-05-15",
  "email": "ivan.ivanov@example.com",
  "phone_number": "+71234567890",
  "address": "г. Москва, ул. Пушкина, д. 10, кв. 5",
  "enrollment_year": 2040,
  "major": "Информатика",
  "course": 6,
  "special_notes": "Без особых примет"
}

Тут я намеренно допустил 2 ошибки: некорректный год поступления и курс. Проверяем:

Мы видим, что есть ошибка 500, но, при этом, расклада по данной ошибке мы не видим. Это сделано намеренно для безопасности. Так где же ознакомиться с ошибкой сервера?

Смотрим в терминал, где мы выполняли запуск FastApi приложения:

fastapi.exceptions.ResponseValidationError: 1 validation errors:
{'type': 'less_than_equal', 'loc': ('response', 'course'), 'msg': 'Input should be less than or equal to 5', 'input': 6, 'ctx': {'le': 5}}

Мы видим ровно то же описание ошибки, что было при обычном тестировании. Такой ответ позволяет нам сделать несколько выводов:

  1. С ошибкой можно будет ознакомиться в логах (пока просто в консоли)

  2. Несмотря на количество ошибок в валидации, мы получаем сообщение про 1 ошибку

Думаю с этим все понятно, а как быть с ситуацией когда нам нужно получить информацию по нескольким студентам? К примеру это 10 студентов, а наша модель описывает только одного студента. Все просто!

from typing import Optional, List


@app.get("/students/{course}")
def get_all_students_course(course: int, major: Optional[str] = None, enrollment_year: Optional[int] = 2018) -> List[
    SStudent]:
    students = json_to_dict_list(path_to_json)
    filtered_students = []
    for student in students:
        if student["course"] == course:
            filtered_students.append(student)

    if major:
        filtered_students = [student for student in filtered_students if student['major'].lower() == major.lower()]

    if enrollment_year:
        filtered_students = [student for student in filtered_students if student['enrollment_year'] == enrollment_year]

    return filtered_students

Нам достаточно из модуля typing передать List, а после просто передаем нашу модель. Тем самым мы указываем, что данный метод должен будет вернуть список студентов.

Особо внимательные читатели уже на данном примере видят, что как-то многовато мы передаем аргументов (фильтров), а ведь их может быть десятки (если не верите зайдите в любой интернет-магазин и посмотрите на длину ссылки, когда вы передадите штук 10 фильтров).

(course: int, major: Optional[str] = None, enrollment_year: Optional[int] = 2018)

Конечно, мы можем их все 10, 20 или сколько там будет передавать и описывать, но это как то не по питоновски, правда?

На удивление, для оптимизации официального метода нет, но есть полу-официальный метод. Создателю FastApi задали вопрос о том как обойти проблему с описанием request_body (тела запроса) через отдельный класс и он поделился одной хитростью, которой я поделюсь с вами.

Сейчас мы создадим самый обыкновенный класс, без Pydentic, так как он, к сожалению, не предназначен для формирования тела запроса (request body).

class RBStudent:
    def __init__(self, course: int, major: Optional[str] = None, enrollment_year: Optional[int] = 2018):
        self.course: int = course
        self.major: Optional[str] = major
        self.enrollment_year: Optional[int] = enrollment_year

Теперь нам необходимо этот класс передать в наш эндпоинт. Тут тоже будет хитрость.

@app.get("/students/{course}")
def get_all_students_course(request_body: RBStudent) -> List[SStudent]:
    students = json_to_dict_list(path_to_json)
    filtered_students = []
    for student in students:
        if student["course"] == request_body.course:
            filtered_students.append(student)

    if request_body.major:
        filtered_students = [student for student in filtered_students if
                             student['major'].lower() == request_body.major.lower()]

    if request_body.enrollment_year:
        filtered_students = [student for student in filtered_students if
                             student['enrollment_year'] == request_body.enrollment_year]

    return filtered_students

В таком виде, к сожалению, код работать не будет, а мы получим такую ошибку:

fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that <class 'app.main.RBStudent'> is a valid Pydantic field type. If you are using a return type an

notation that is not a valid Pydantic field (e.g. Union[Response, dict, None]) you can disable generating the response model from the type annotation with the path operation decorator parameter response_model=None. 

Ничего. Сейчас мы это исправим.

Для того чтоб решить данную проблему нам необходимо воспользоваться функцией Depends. Импортируем ее из FastApi:

from fastapi import FastAPI, Depends

Данная функцию мы будем подробно рассматривать в других статьях, пока просто импортируем.

Изменяем функцию:

@app.get("/students/{course}")
def get_all_students_course(request_body: RBStudent = Depends()) -> List[SStudent]:
    students = json_to_dict_list(path_to_json)
    filtered_students = []
    for student in students:
        if student["course"] == request_body.course:
            filtered_students.append(student)

    if request_body.major:
        filtered_students = [student for student in filtered_students if
                             student['major'].lower() == request_body.major.lower()]

    if request_body.enrollment_year:
        filtered_students = [student for student in filtered_students if
                             student['enrollment_year'] == request_body.enrollment_year]

    return filtered_students

FastApi больше не ругается и мы можем воспользоваться данной функцией в документации (как раз посмотрим как FastApi опишет корректный ответ).

Видим, что все корректно отработало, но, при этом, мы сильно очистили свой код.

Надеюсь, что к данному моменту вы полностью закрепили тему с Pydentic и описанием модели запроса, а это значит, что можно переходить к POST, PUT и DELETE методам.

Небольшая подготовка.

Далее я буду выполнять демонстрацию работы POST, PUT и DELETE методов на примере библиотеки json_db_lite. Вам ее использовать не обязательно. Основная суть в том, что мы превращаем стандартный JSON в некое подобие мини-базы данных. В следующих же статьях мы будем говорить про интеграцию и работу SQLAlchemi.

pip install --upgrade json_db_lite

POST методы в FastApi

Напоминаю, что смысл POST методов в том, чтоб отправить данные от клиента в сервер (базу данных) и, как по мне, лучшим тут примером будет добавление нового студента в базу данных.

Для начала напишем функции, которые позволят нам имитировать работу с базой данных:

from json_db_lite import JSONDatabase

# инициализация объекта
small_db = JSONDatabase(file_path='students.json')


# получаем все записи
def json_to_dict_list():
    return small_db.get_all_records()


# добавляем студента
def add_student(student: dict):
    student['date_of_birth'] = student['date_of_birth'].strftime('%Y-%m-%d')
    small_db.add_records(student)
    return True


# обновляем данные по студенту
def upd_student(upd_filter: dict, new_data: dict):
    small_db.update_record_by_key(upd_filter, new_data)
    return True


# удаляем студента
def dell_student(key: str, value: str):
    small_db.delete_record_by_key(key, value)
    return True

Функции будут выглядеть так, а подробное описание каждого метода этой библиотеки вы найдете в статье «Новая библиотека для работы с JSON: json_db_lite».

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

@app.post("/add_student")
def add_student_handler(student: SStudent):
    student_dict = student.dict()
    check = add_student(student_dict)
    if check:
        return {"message": "Студент успешно добавлен!"}
    else:
        return {"message": "Ошибка при добавлении студента"}

Зайдем в документацию и посмотрим что у нас получилось:

Вы видите, что полей для ввода данных у нас больше нет, но, при этом, FastApi нам подсказывает как должны выглядеть данные для добавления. Давайте их и попробуем добавить.

{

  "detail": [

    {

      "type": "value_error",

      "loc": [

        "body",

        "phone_number"

      ],

      "msg": "Value error, Номер телефона должен начинаться с \"+\" и содержать от 1 до 15 цифр",

      "input": "string",

      "ctx": {

        "error": {}

      }

    },

    {

      "type": "value_error",

      "loc": [

        "body",

        "date_of_birth"

      ],

      "msg": "Value error, Дата рождения должна быть в прошлом",

      "input": "2024-07-07",

      "ctx": {

        "error": {}

      }

    },

    {

      "type": "greater_than_equal",

      "loc": [

        "body",

        "enrollment_year"

      ],

      "msg": "Input should be greater than or equal to 2002",

      "input": 0,

      "ctx": {

        "ge": 2002

      }

    },

    {

      "type": "greater_than_equal",

      "loc": [

        "body",

        "course"

      ],

      "msg": "Input should be greater than or equal to 1",

      "input": 0,

      "ctx": {

        "ge": 1

      }

    }

  ]

}

И вот мы получили столько ошибок. Обратите внимание, что в данном случае мы видим ошибки явно, а не через бэкенд, как в случае с ошибками при ошибках валидации на GET запросах.

Ошибки все те-же. Давайте исправим и повторим запрос:

Корректное тело запроса
Корректное тело запроса
Результат
Результат

Мы видим, что студент успешно добавлен, а мы не получили никаких ошибок.

На практике, конечно, данные не добавляются в JSON после POST запроса, но текущего примера, как по мне, будет более чем достаточно чтоб объяснить вам общий принцип обработки POST запросов в FastApi.

Теперь рассмотрим PUT и DELETE методы.

Обработка PUT методов в FastAPI

Сейчас мы напишем метод, который обновит у студента курс и специальность. Задачу мы немного усложним и передадим не одну, а сразу две модели, ведь функция для обновления данных у нас выглядит так:

def upd_student(upd_filter: dict, new_data: dict):
    small_db.update_record_by_key(upd_filter, new_data)
    return True

Следовательно, тут у нас будет одна модель для фильтрации, а вторая модель с новыми данными для студента. Опишем обе модели.

class SUpdateFilter(BaseModel):
    student_id: int


# Определение модели для новых данных студента
class SStudentUpdate(BaseModel):
    course: int = Field(..., ge=1, le=5, description="Курс должен быть в диапазоне от 1 до 5")
    major: Optional[Major] = Field(..., description="Специальность студента")

Метод будет выглядеть так:

@app.put("/update_student")
def update_student_handler(filter_student: SUpdateFilter, new_data: SStudentUpdate):
    check = upd_student(filter_student.dict(), new_data.dict())
    if check:
        return {"message": "Информация о студенте успешно обновлена!"}
    else:
        raise HTTPException(status_code=400, detail="Ошибка при обновлении информации о студенте")

Как вы можете заметить, проблем по передаче нескольких моделей для валидации нет.

Данный метод будет обновлять данные по конкретному студенту, принимая его ID. В новых данных мы должны будем передать курс и специальность студента. К данному моменту вопросов к синтаксису у вас уже не должно быть.

Смотрим.

Обратите внимание на то как выглядит тело запроса в документации. Именно то что нам нужно, не так ли?

Передадим данные и попробуем выполнить обновление.

{

  "filter_student": {

    "student_id": 12

  },

  "new_data": {

    "course": 5,

    "major": "Экономика"

  }

}

Отлично и на последок посмотрим на DELETE запрос.

Для начала напишем модель под функцию

def dell_student(key: str, value: str):
    small_db.delete_record_by_key(key, value)
    return True

Вот пример модели:

class SDeleteFilter(BaseModel):
    key: str
    value: Any

Так как значение ключа может быть любым, я в качестве описания указал Any (не забудьте импортировать с typing).

Пример функции для удаления студента:

@app.delete("/delete_student")
def delete_student_handler(filter_student: SDeleteFilter):
    check = dell_student(filter_student.key, filter_student.value)
    if check:
        return {"message": "Студент успешно удален!"}
    else:
        raise HTTPException(status_code=400, detail="Ошибка при удалении студента")

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

Давайте посмотрим на то, как выглядит метод для удаления студента:

Выполняю запрос:

{"key": "student_id", "value": 12}

Результат:

Заключение

Друзья, теперь вы знаете:

  • Как обрабатывать GET, POST, DELETE и PUT запросы в FastAPI.

  • Что такое модели в FastAPI и, в частности, модели Pydantic.

  • Если не просто читали, а писали код вместе со мной, то вы на практике закрепили и методы и работу с моделями

Как и в прошлый раз, ваша поддержка в виде лайков, подписок и комментариев даст мне понять, что стоит продолжать создавать такой объемный контент, на который всегда не хватает времени.

Эти навыки дадут вам достаточную основу для более глубоких тем, таких как: асинхронная работа, интеграция с базой данных SQLite, фоновые задачи, самописная авторизация и многое другое.

Исходники кода из этой публикации, а также эксклюзивный контент, вы найдете в моем телеграмм-канале «Легкий путь в Python».

Всего доброго!

 

Комментарии (0)