Друзья, приветствую вас в очередной статье, посвященной разработке API с использованием фреймворка FastAPI. В прошлой публикации мы познакомились с основами FastAPI и написали первые функции, освоив GET-запросы. Однако возможности HTTP общения клиента и сервера этим не ограничиваются. Сегодня мы изучим POST, PUT и DELETE запросы.
В прошлой статье мы рассмотрели GET запросы и научились писать свои первые функции. Сегодня же мы рассмотрим методы, позволяющие отправлять данные (POST), обновлять (PUT) и удалять данные (DELETE).
Для того чтобы эти операции были не только возможны, но и выполнялись правильно и эффективно, необходимо использовать модели.
Что такое модели в FastApi?
Модель в FastAPI — это нечто вроде схемы или шаблона, который описывает структуру данных, с которыми работает ваше приложение. Проще говоря, это способ сказать: "Вот как должны выглядеть данные, которые мы принимаем или отправляем."
Основные задачи моделей в FastAPI:
Валидация данных: С помощью Pydantic мы можем проверять, что данные соответствуют ожидаемому формату. Например, если нам нужен объект пользователя с именем и возрастом, модель проверит, что имя — это строка, а возраст — число.
Документирование данных: Модели помогают автоматически создавать документацию для вашего API. Клиенты могут легко понять, какие данные они должны отправить или могут ожидать в ответе.
Работа с базами данных: Модели можно использовать для описания структуры данных в базе данных. Например, с помощью библиотек, таких как 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:
Валидация данных: Pydantic проверяет, что данные соответствуют ожидаемым типам. Например, если вы ожидаете строку, а вам прислали число, Pydantic выдаст ошибку.
Преобразование данных: Он может автоматически преобразовывать данные. Например, если вы ожидаете дату, а получили строку, Pydantic попытается преобразовать эту строку в дату.
Документирование данных: 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 refrom 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, чтобы правильно получать данные о студенте. Все это мы рассмотрим далее на конкретных примерах.
Сам класс можно разделить на две условные группы:
Описание полей (field)
Внутренние валидаторы
Класс всегда будет наследоваться от 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 ошибку
Думаю с этим все понятно, а как быть с ситуацией когда нам нужно получить информацию по нескольким студентам? К примеру это 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_studentsFastApi больше не ругается и мы можем воспользоваться данной функцией в документации (как раз посмотрим как FastApi опишет корректный ответ).


Видим, что все корректно отработало, но, при этом, мы сильно очистили свой код.
Надеюсь, что к данному моменту вы полностью закрепили тему с Pydantic и описанием модели запроса, а это значит, что можно переходить к POST, PUT и DELETE методам.
Небольшая подготовка.
Далее я буду выполнять демонстрацию работы POST, PUT и DELETE методов на примере библиотеки json_db_lite. Вам ее использовать не обязательно. Основная суть в том, что мы превращаем стандартный JSON в некое подобие мини-базы данных. В следующих же статьях мы будем говорить про интеграцию и работу SQLAlchemi.
pip install --upgrade json_db_litePOST методы в 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».
Всего доброго!
