Друзья, приветствую вас в очередной статье, посвященной разработке 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. Для того чтоб такое у нас не прошло мы и будем использовать Pydantic.

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

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

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

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

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

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

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

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

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: Ограничения для чисел с плавающей точкой (максимальное количество цифр, количество десятичных знаков).

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

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

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 мы так же будем использовать Pydantic.

Теперь, чтоб закрепить полученные знания, давайте протестируем наш класс на простых примерах (пока без привязки к 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]

Мы получаем ошибку, но нас Pydantic не оставляет в неведении. Он сообщает что мы передали год поступления 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.

Pydantic модель и 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», но при этом мы не получили ошибку. Это одна из фишек Pydantic.

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

{
  "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 (тела запроса) через отдельный класс и он поделился одной хитростью, которой я поделюсь с вами.

Сейчас мы создадим самый обыкновенный класс, без Pydantic, так как он, к сожалению, не предназначен для формирования тела запроса (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 опишет корректный ответ).

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

Надеюсь, что к данному моменту вы полностью закрепили тему с Pydantic и описанием модели запроса, а это значит, что можно переходить к 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».

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

 

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Продолжим?
91.86%Да, кончено!158
8.14%Нет.14
Проголосовали 172 пользователя. Воздержались 6 пользователей.