Почему я выбираю FastAPI: основные возможности и преимущества фреймворка
Привет! Меня зовут Ярослав Мартыненко, я Python Developer в NIX. Раньше я занимался Embedded-разработкой, позже пошел в сторону веба. Уже больше года разрабатываю бэкенд на Python. Стараюсь постоянно изучать что-то новое и создавать то, что упростит жизнь окружающим.
Високорівневий курс від laba: Фінансовий аналіз.
Оцінюйте фінансову стабільність та перспективи.
Год назад я узнал о FastAPI. Он «наследник» философии Flask, но уже «из коробки» предоставляет интересные фичи, о которых я расскажу в этой статье.
FastAPI не предлагает больше необходимого минимума, поэтому разработчик может свободно использовать вместе с этим фреймворком любые инструменты.
Что же это за FastAPI
FastAPI — это относительно новый асинхронный веб-фреймворк для Python. По сути это гибрид Starlett и Pydantic.
Starlett — асинхронный веб-фреймворк, Pydantic – библиотека для валидации данных, сериализации и т.д. В документации FastAPI написано, что он может приблизиться по скорости к Node.js и Golang. Я этого не проверял, потому и верить в это не буду. Для меня он быстр по другой причине. FastAPI позволяет просто и оперативно написать небольшой REST API, не затратив на это много усилий.
Цифровий курс від robotdreams: DevOps Engineer.
підходи для створення сучасних і масштабованих застосунків.
Давайте посмотрим, как легко (это только мое субъективное мнение) можно начать работу с FastAPI.
Начало работы
В первую очередь стоит установить нужные нам зависимости, а это сам фреймворк и ASGI-сервер, поскольку у FastAPI нет встроенного сервера, как у Flask или Django. В документации предлагается использовать uvicorn в качестве ASGI-сервера:
pip install fastapi pip install uvicorn
В FastAPI используется подобная Flask система объявления эндпоинтов — с помощью декораторов. Поэтому работающим с Flask будет достаточно легко приспособиться к FastAPI. Теперь создадим объект нашей программы и добавим роут HelloWorld:
from fastapi import FastAPI app = FastAPI() @app.get(«/») async def root(): return
Мы объявили, что при GET-запросе на / мы вернем json — особенных отличий от Flask здесь нет.
Важная ремарка: эндпоинт также можно объявить в синхронном стиле, используя просто def , если вы хотите использовать await . FastAPI разрулит все за вас. За что я люблю FastAPI — так это за его лаконичность.
Давайте объявим роут, который будет ожидать какой-либо параметр как часть пути:
@app.get("/item/") async def get_item(id): return id
Теперь, если мы перейдем по адресу /item/2 , то получим 2 в ответ. А что делать, если кто-то захочет нам прислать вместо цифры, например, dva ? Хотелось бы защитить себя от таких конфузов. И здесь нам приходит на помощь Python 3.6+ і type_hints .
Тайп-хинтинг (объявление типов) в целом помогает сделать код более понятным и позволяет использовать инструменты для статического анализа (такие, как mypy ). FastAPI заставляет вас использовать тайп-хинтинг, тем самым улучшая качество кода и уменьшая вероятность того, что вы где-то по невнимательности допустили ошибку.
Теперь определим, что наш id должен быть типа int :
Освітній курс від robotdreams: Аналітик даних.
Перетворюйте дані на рішення.
@app.get("/item/") async def get_item(id: int): return id
Мы достаточно просто добавили валидацию и теперь можно попытаться передать dva и посмотреть, что же получится. В ответ получим сообщение, что сделали что-нибудь не так.
Сервер вернет нам 422 статус-код и следующий json:
< "detail": [ < "loc": [ "path",< "detail": [ < "loc": [ "path", "item_id" ], "msg": "value is not a valid integer", "type": "type_error.integer" >] > "item_id" ], "msg": "value is not a valid integer", "type": "type_error.integer" > ] >
На этом этапе пришло время Pydantic . Он сгенерирует данные о том, где обнаружена ошибка, и подскажет, что мы сделали не так. Опять же, не всем придется по душе статус-код 422 и данные об ошибке, которые нам генерирует Pydantic. Но все это можно кастомизировать, если очень хочется.
А как объявить, что мы хотим какой-то квери-параметр, да еще чтобы он был необязательным? Все просто: если аргумент функции не объявлен как часть пути, FastAPI будет считать, что он должен быть получен как квери-параметр. Для того чтобы сделать его необязательным, придадим ему дефолтное значение.
Еще одна прекрасная фича FastAPI — то, что мы можем объявить, например, enum , чтобы задать определенные значения, которые ожидаем на вход:
class Framework(str, Enum): flask = «flask» django = «django» fastapi = «fastapi @app.get(«/framework») def framework(framework: Framework = Framework.flask): return
Следующая интересная фича — преобразование типов. Если мы хотим получить булевое значение как квери-параметр, нам все равно придется его передать как число или строку. Рydantic предлагает превратить логически правильное значение в булевый тип вот так:
@app.get("/items") async def read_item(short: bool = False): if short: return "Short items description" else: return "Full items description"
Для эндпоинта, указанного выше, следующие значения будут валидны и превращены в булевое значение True :
Цифровий курс від mate.academy: QA (Тестування).
Станьте професійним тестувальником, навчайтеся після роботи.
Иногда нам нужно более гибко настраивать момент, где искать и откуда доставать параметры. Например, мы хотим извлечь значение из хедера. Для этого FastAPI предоставляет нам следующие инструменты: Query , Body , Path , Header , Cookie, импортируемые из FastAPI. Они помогают не только явно определить, где искать параметр, но и объявить дополнительную валидацию.
Давайте рассмотрим это на примере:
from typing import Optional from fastapi import FastAPI, Query, Header app = FastAPI() @app.get(«/») async def test(number: Optional[int] = Query(None, alias=»num», gt=0, le=10), owner: str = Header(. )): return
Мы определили эндпоинт, ожидающий, что мы передадим ему число от 0 до 10 включительно как квери-параметр. Причем квери-параметр мы должны передавать как /?num=3 , поскольку определили alias для этого параметра и теперь ожидаем, что он придет под именем num , и что у нас будет хедер Owner .
Pydantic-модели
Чаще всего, когда мы строим REST API, то хотим передавать какие-либо более сложные структуры в виде json в теле запроса. Эти структуры можно описать с помощью Рydantic-моделей.
Например, мы хотим принимать объект item , у которого есть имя, цена и опциональное описание:
class Item(BaseModel): name: str description: Optional[str] = None price: float
Также мы хотим добавить эндпоинт, который будет принимать POST-запросы, десериализировать и валидировать json и где-то хранить его. Наша модель Item — это класс, поэтому мы можем наследоваться от нее и создать модель, которая также будет содержать id . Ведь нам хочется сохранить где-то наш item . Ему присваивается id и уже вместе с этим мы можем вернуть клиенту ответ с кодом 201 .
Для начала создадим модель с новым полем id :
class ItemOut(Item): id: int
Далее — эндпоинт с аргументом item типа Item . Поскольку Item — это Pydantic-модель, FastAPI предполагает, что нам нужно достать item из тела запроса і content-type = application/json . Pydantic десериализирует эти данные и провалидирует их. Затем создадим объект типа ItemOut , у которого будет поле id , и вернем все это пользователю:
@app.post("/item/", response_model=ItemOut, status_code=201) async def create_item(item: Item): item_with_id = ItemOut(**item.dict(), return item_with_id
Как вы можете увидеть в декораторе, мы определили, что возвращаемые данные будут типа ItemOut , а статус код — 201 . Указание response_model необходимо для того, чтобы правильно сгенерировать документацию (об этом расскажу далее), а также сериализировать и провалидировать данные. Мы могли бы передать словарь вместо объекта ItemOut . Тогда FastAPI попытался превратить этот словарь в ItemOut -объект и провалидировать данные.
Если хотим создать более сложные структуры с вложенностью, то здесь тоже не возникает особого труда. Мы просто определяем нашу модель Pydantic , содержащую объекты с типом другой Pydantic – модели:
class OrderOut(BaseModel): id: int items: list[Item]
Еще одно преимущество FastAPI — автогенерация OpenApi-документации. Ничего не нужно подключать, не нужно танцевать с бубном — просто бери и пользуйся. По умолчанию документация находится по пути /docs.
Отложенные задачи
Иногда бывает, что мы хотим быстро вернуть респонс клиенту, а затратные задачи выполнить потом на фоне. Обычно для этого используется что-то вроде Celery или RQ . Чтобы не возиться с очередями и воркерами, у FastAPI есть такая фича, как background tasks . Мы можем объявить, что наша функция приобретает аргумент типа BackgroundTasks , и этот объект будет интерфейсом для создания бэкграунд-тасков:
def write_notification(email: str, message=»»): with open(«log.txt», mode=»w») as email_file: content = f»notification for : » email_file.write(content) @app.post(«/send-notification/») async def send_notification(email: str, background_tasks: BackgroundTasks): background_tasks.add_task(write_notification, email, message=»some notification») return
На примере выше показана функция, которая что-то записывает в файл. Нам нужно обработать ее после того, как вернем пользователю респонс. Для этого объявим аргумент background_tasks с типом BackgroundTasks и с помощью него сможем прибавлять функции, которые нам нужно выполнить после того, как отработает наша view .
Здесь следует понимать, что это не замена Celery и подобных инструментов для выполнения асинхронных задач. В данном случае у нас есть процесс с Python, в котором обрабатываются наши запросы, и в нем будет запущена отложенная функция, в отличие от той же Celery , где есть очередь и отдельные процессы-воркеры, которые обрабатывают нашу задачу.
Инъекция зависимостей
FastAPI предоставляет систему для инъекции зависимостей в наши view . Для этого есть Depends . Зависимостью может быть callable – объект, в котором будет реализована определенная логика. Инжектируемый объект будет иметь доступ к контексту реквеста. Это означает, что мы сможем извлечь определенную общую логику из наших view и переиспользовать ее.
Предлагаю рассмотреть этот процесс на примере:
from typing import Optional from fastapi import Depends, FastAPI app = FastAPI() async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): return @app.get("/items/") def read_items(commons: dict = Depends(common_parameters)): return commons @app.get("/users/") async def read_users(commons: dict = Depends(common_parameters)): return commons
Мы создали функцию, которая получает нам параметр для фильтрации и возвращает его как словарь. Затем подключили эту функцию как зависимость в наши view – функции read_items и read_users . Объявили аргумент типа common и предоставили ему Depends ( common_parameters ).
Depends принимает как аргумент callable -объект, который вызовется перед обработкой нашего view . В этом случае он вернет словарь с параметрами фильтрации. Интересно здесь то, что нам безразлично, синхронная ли функция. Мы можем объявить common_parameters как синхронную и асинхронную. FastAPI все разрулит за нас.
Поскольку зависимостями могут быть callable -объекты, мы можем заменить нашу функцию, которая возвращает словарь с параметрами на что-то более элегантное:
class CommonQueryParams: def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): self.q = q self.skip = skip self.limit = limit @app.get("/items/") def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)): return commons
Как видите, мы заменили функцию на класс и теперь передаем его в Depends . В результате нам возвращается объект класса CommonQueryParams . Теперь мы можем получить доступ к его атрибутам через точку, например, commons.q . Вот так выглядят наши зависимости:
По сути, это граф. Мы можем сделать его более сложным, где в текущие зависимости добавим другие и сделаем более специфическими. Предположим, у нас будет зависимость, которая проверяет, авторизован ли пользователь, и достает его из базы. Другая — связанная с первой зависимостью — проверяет, активен ли пользователь, а третья — которая зависит от второй — определяет, является ли он админом:
Поскольку это граф, возникает вопрос ромбовидной зависимости: сколько раз будет выполняться первая родительская зависимость? Ответ прост — всего раз. Обычно нам нужно только один раз выполнить действие над реквестом, а затем закэшировать данные, которые вернут зависимость. Это можно переопределить, передав в Dependsuse_cache=False :
def dep_a(): logger.warning("A") def dep_b(a = Depends(dep_a)): logger.warning("B") def dep_c(a = Depends(dep_a, use_cache=False)): logger.warning("C") @app.get("/test") def test(dep_a = Depends(dep_a), dep_b = Depends(dep_b), dep_c = Depends(dep_c)): return "Hello world"
Зависимость dep_a идет первой в аргументах и не имеет других зависимостей, поэтому она выполнится и кэширует ее. Зависимость dep_b идет следующей и имеет зависимость от dep_a , но вызов dep_a был сделан и ответ закэшен, поэтому dep_a не будет вызываться.
Далее следует dep_c , которая зависит от dep_a и определяет use_cache=False для зависимости dep_a . Несмотря на то, что dep_a была закэшена, она все равно будет вызываться, и ответ также закэшивается. Затем вызовется dep_c . И только в конце выполнится наша функция test .
И это еще не все. Мы можем использовать наши зависимости вместе с yield . Это будет нечто вроде контекстного менеджера. Мы сможем выполнить какую-нибудь инициализацию до yield , затем выполнится наша view , далее — бекграунд-таски, а также отработает код после yield . Это можно использовать для инициализации ресурсов, например для настройки подключения к базе данных:
async def get_db(): logger.warning("Open connection") yield "database" logger.warning("Close connection") async def task(database): logger.warning("Some task") logger.warning(f"DB: ") @app.get("/test") async def test(background_tasks: BackgroundTasks, database = Depends(get_db)): background_tasks.add_task(task, database) return database
Dependency Injector необходим для того, чтобы легко подменить нашу зависимость на mock. Предположим, эта зависимость — и есть клиент, который обращается к стороннему API по http. Делается это просто: подменяем возвращающую клиента зависимость на зависимость, которая возвращает mock с таким же публичным API.
Если у нас есть сервис для отправки сообщений, то при попытке запустить тесты с этим сервисом они упадут с ошибкой. Но мы можем определить pytest -фикстуру, в которой наша зависимость будет подменяться. Как это сделать? Добавим функцию, которая вернет mock в dependency_overrides , и после того, как тест сработает, очистим наши переопределения зависимостей app.dependency_overrides = <> :
import pytest from fastapi import Depends from fastapi.testclient import TestClient from main import app client = TestClient(app) def send_msg(): raise ValueError("Error") @app.get("/api") def some_api(msg = Depends(send_msg)): return msg @pytest.fixture def mock_dependencies(): def get_msg_mocked(): return "Test pass" app.dependency_overrides[send_msg] = get_msg_mocked yield app.dependency_overrides = <> @pytest.mark.usefixtures("mock_dependencies") def test_my_api(): res = client.get("/api") assert res.status_code == 200
Вывод
Я попытался кратко описать основные возможности FastAPI и показать, чем мне нравится этот фреймворк. Попробуйте его хотя бы для небольшого pet-проекта . Вокруг FastAPI достаточно быстро разрастается сообщество его поклонников, чуть ли не каждый день появляются новые библиотеки. Поэтому некоторые проекты постепенно переходят с Flask на FastAPI. Удачи!
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
Основы FastAPI
FastAPI представляет быстрый высокопроизводительный фреймворк для создания веб-приложений на языке Python.
Официальный сайт проекта: https://fastapi.tiangolo.com/. Исходный код фреймворка доступен на github по адресу: https://github.com/tiangolo/fastapi
На данный момент поддерживается Python версии 3.6 и выше.
Необходимые инструменты и установка
Для работы с FastAPI естественно потребуется интерпретатор Python. Как его установить, можно прочитать тут: Установка и первая программа на Python
Для установки пакетов FastAPI потребует пакетный менеджер pip . Менеджер pip позволяет загружать пакеты и управлять ими. Обычно при установке python также устанавливается и менеджер pip.
Для установки пакетов FastAPI откроем терминал и введем команду
pip install fastapi
Также для работы с FastAPI нам потребуется ASGI веб-сервер (веб сервер с поддержкой протокола Asynchronous Server Gateway Interface). В качестве такового в Python можно использовать Univer или Hypercorn. В данном случае будем использовать Univer . Также установим его пакеты с помощью менеджера pip с помощью следующей команды:
pip install "uvicorn[standard]"
Создание первого приложения
Определим на диске папку, где будут располагаться файлы с исходным кодом приложения. Например, в моем случае это папка C:\fastapi . Создадим в этой папке новый файл, который назовем main.py и который будет иметь следующий код:
from fastapi import FastAPI from fastapi.responses import HTMLResponse app = FastAPI() @app.get("/") def read_root(): html_content = "Hello METANIT.COM!
" return HTMLResponse(content=html_content)
Для обработки запросов к приложения вначале необходимо создать объект приложения с помощью конструктора FastAPI из пакета fastapi
app = FastAPI()
Затем определяем функцию, которая будет обрабатывать запросы. К этой функции применяется специальный декоратор в виде метода app.get() :
@app.get("/")
В этот метод передается шаблон маршрута,по которому функция будет обрабатывать запросы. В данном случае это строка «/», которая означает, что функция будет обрабатывать запросы по пути «/», то есть запросы к корню веб-приложения.
После декоратора app.get идет собственно определение функции, которая обрабатывает запрос:
def read_root(): html_content = "Hello METANIT.COM!
" return HTMLResponse(content=html_content)
Это обычная функция python. Она называется read_root (имя произвольное). Для отправки ответа она использует класс HTMLResponse из пакета fastapi.responses . Класс HTMLResponse позволяет отправить в ответ некоторое содержимое в виде кода html.
Для установки отправляемого содержимого в конструкторе HTMLResponse применяется параметр content , которому в данном случае передается строка «
Hello METANIT.COM!
» со значением «Hello METANIT.COM!». То есть когда клиент обратится к веб-приложению по пути «/», ему будет отправлен html-код «
Hello METANIT.COM!
«.
Запуск приложения
Теперь запустим приложение. Для этого перейдем в терминале к папке, где располагает файл main.py и затем выполним команду
uvicorn main:app --reload
В данном случае мы запускаем сервер uvicorn и передаем ему ряд параметров:
- main указывает на название модуля, которое по умолчанию совпадает с названием файла — main
- app указывает на объект приложения, созданный в строке app = FastAPI()
- —reload позволяет отслеживать изменения в файлах исходного кода и автоматически перезапускать проект
Почему вам стоит выбрать фреймворк FastAPI для своего следующего проекта
Senior Python Developer рассказывает о возможностях и особенностях фреймворка FastAPI, а также о проектах, в которых его стоит использовать.
Максим Кузнецов
Senior Python Developer в Akvelon
FastAPI — это фреймворк для создания лаконичных и довольно быстрых HTTP API-серверов со встроенными валидацией, сериализацией и асинхронностью. Стоит он на плечах двух других фреймворков. Работой с web в FastAPI занимается Starlette, за валидацию отвечает Pydantic.
Комбайн получился лёгким, не перегруженным и более чем достаточным по функционалу.
Starlette — новый, шустрый и классные фреймворк, реализующий подход ASGI. В нем всё заточено на асинхронность и новые фишки 3-й ветки Python. Кроме этого в Starlette есть ещё целая пачка серьёзных плюшек:
- GraphQL из коробки
- Веб-сокеты уже встроены и готовы к работе
- Готовый набор миддлверов для работы с авторизацией/аутентификацией, CORS
- Встроенные асинхронные таски
Асинхронное программирование — это потоковая обработка программного обеспечения /пользовательского пространства, где приложение, а не процессор, управляет потоками и переключением контекста. В асинхронном программировании контекст переключается только в заданных точках переключения, а не с периодичностью, определённой CPU.
Когда вы запускаете что-то асинхронно, это означает, что оно не блокируется, вы выполняете его, не дожидаясь завершения, и продолжаете выполнять другие вещи. Параллелизм означает выполнение нескольких задач одновременно, параллельно. Параллелизм хорошо работает, когда вы можете разделить задачи на независимые части работы.
Основные возможности FastAPI
FastAPI — это, по сути, нашлёпка на родные классы Starlette, добавляющая пачку новых фич к уже и так неплохому фреймворку.
- Плюшки для создания REST API сервисов + Swagger-документация для методов. Starlette ориентируется на модный GraphQL, FastAPI заботится о тех, кто пилит REST.
- Удобные примочки, построенные на подсказках типов переменных. Например, встроенные валидаторы данных.
- Приятные полезности для процессов авторизации и аутентификации — поддержка JWT, OAuth2.
Важные причины выбрать FastAPI:
- Асинхронность
- Типизированность
- Встроенная документация (Swagger)
- Применение websockets
Когда стоит использовать FastAPI
- Когда в вашем проекте нужна производительность и она должна быть выше, чем у других фреймворков — например, Django.
- Когда в вашем проекте нужна асинхронность.
- Если вы в проекте используете запросы к другим сервисам (использование асинхронных http-запросов).
- Если хотите использовать websockets из коробки.
Если вы ответили на большую часть «нет», то вам стоит присмотреться к другим фреймворкам, либо использовать FastAPI. Выбор остаётся за вами.
Полезные ссылки
Следите за новыми постами по любимым темам
Подпишитесь на интересующие вас теги, чтобы следить за новыми постами и быть в курсе событий.
Пишем FastAPI с нуля на python
Если вы здесь, то вероятно уже знаете, что такое FastAPI. Это простой в понимании, легковесный веб-фреймворк изначально созданный для создания API. Работает он с помощью ASGI-сервера, о котором можно почитать во многих статьях, здесь же мы только кратко затронем его.
И без лишних слов, сегодня, как ясно из названия, мы будем писать FastAPI. В двух словах, он построен на Starlette, который работает с ASGI, и на Pydantic, который позволяет производить автоматическую валидацию получаемых данных. FastAPI же очень удобная оболочка над ними.
Что в статье
- Рассмотрим приложение FastAPI, которое мы по итогу захотим запустить.
- Напишем FastAPI, с рабочим названием myfastapi.
- Запустим и проверим изначальное приложение написаное на fastapi с помощь myfastapi.
Что хотим написать
Все начинается с экземпляра главного класса FastAPI, который в свою очередь, во-первых наследуются от класса Starlette, который и выполняет всю основную работу по разрешению запросов, во-вторых создает декораторы, которые отправляют роуты в класс Starlette. Эти роуты хранятся там до запроса.
Когда происходит запрос к хосту на котором запущено приложение FastAPI, работая с ASGI фреймворком, приложение получает три параметра Scope, Receive и Send. Для работы ASGI приложению, точнее классу, нужен всего один метод __call__. Данный встроенный метод в классе FastAPI реализован лишь в виде вызова super().__call__, то есть класса Starlette. Но пока Starlette нас не интересует, до него мы еще дойдем.
В этой статье мы создадим функциональность, которая с помощью запуска ASGI сервера используя uvicorn main:app —reload, позволит работать следующему приложению
from typing import Union from fastapi import FastAPI from starlette.responses import JSONResponse from pydantic import BaseModel class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None app = FastAPI() @app.post("/items/") async def create_item(item: Item, item_id: int): return JSONResponse() @app.get("/") async def homepage(): return JSONResponse() @app.get("/get_items/") async def read_item(item_id: int): print("item_id", item_id) return
Для этого нам сделать, во-первых, декораторы, которые будут оборачивать роуты с их данными и отправлять в класс, который будет их обрабатывать, во-вторых, получения параметров пути, параметров запроса, тела запроса и валидацию на соответствие типам, если они используются, будь то встроенные типы, типы из typing или даже pydantic, что и является основной чертой FastApi, автоматической проверкой типов получаемых данных.
Далее нам надо будет реализовать распределение запросов, то есть какой endpoint использовать при, например, GET запросе с путем “/items”. И в конце статьи, мы захотим сделать класс Response, который будет отлично работать в качестве ответа для ASGI сервера.
Конечно же, мы не будем использовать всю функциональность в рассматриваемых классах и методах, так как они могут охватывать большое количество путей использования. Поэтому будем использовать только необходимое, ведь когда мы это сделаем, то постепенно уже можно будет изучать и добавлять новую функциональность, доходя до реализации близкой к FastAPI. Если мы начнем охватывать все сразу, многое будет вызывать вопросы, но идя постепенно, код будет становититься все более понятным.
Пишем модуль myfastapi
Начнем с малого и самого простого — класс FastApi. Декораторы.
Как вы помните, инициализация простого приложения FastAPI, а возможно и большинства происходит следующим вызовом:
app = FastAPI()
Так как мы не будем рассматривать дебаггинг и OpenAPI здесь, то в метод __init__ мы просто добавим версию нашего приложения.
myfastapi::applications::FastApi::__init__
from typing import Callable from starlette import Starlette from myfastapi.routing import ApiRouter class FastApi(Starlette): def __init__( self, version: str = "0.1.0" ) -> None: self.version = version self.router: APIRouter = APIRouter()
Да, это все, что нам нужно здесь.
Сейчас посмотрим на то, что нас действительно интересует в этом классе — декораторы.
Мы видим класс ApiRouter, который мы будем использовать для создания декораторов, но об этом чуть позже.
На самом деле, как вы видите, наш класс FastApi не такой уж и простой в функциональности, так как он наследуется от класса Starlette, и это важно. На самом деле всю основную работу выполняет именно Starlette, а FastAPI просто удобная для использования оболочка над ним, но с довольно полезным функционалом и возможностью создания некоторого вида архитектуры для нашего приложения.
Итак, посмотрим на два метода get и post, которые используется в виде декораторов.
myfastapi::applications::FastApi::get
def get( self, path: str, ) -> Callable[. Any]: return self.router.get(path)
myfastapi::applications::FastApi::post
def post( self, path: str, ) -> Callable[. Any]: return self.router.post(path)
В двух словах, основной класс FastApi, который мы используем для создания приложения имеет под собой такой простой вызов методов класса ApiRouter.
Все, что мы передаем это путь, который будет запускать данный endpoint.
Становится очевидно, по тому, какой тип возврата указан у методов, что именно следующий вызов self.router.post(path) возвращает декоратор. Мы перейдем к классу ApiRouter уже скоро.
Так, помните, что для запуска ASGI приложения, необходимо, что класс был вызываемым, то есть имел метод __call__, и принимал три параметра Scope, Receive, Send. Где Scope отвечает за все метаданные и параметры пути, Receive хранит в себе тело запроса, а Send используется уже в самом конце обработки запроса для отправки заголовков и json ответа.
myfastapi::applications::FastApi::__call__
async def __call__( self, scope: Scope, receive: Receive, send: Send ) -> None: await super().__call__(scope, receive, send)
Ага. Он просто вызывает класс Starlette, передавая ему полученные параметры.
Собственно, если использовать только два метода запроса get и post, так как для других методов используется идентичный подход, то класс FastApi для нашей библиотеки myfastapi, готов. Посмотрим на весь код вместе.
myfastapi::applications::FastApi
from typing import Callable from starlette import Starlette from myfastapi.routing import APIRouter class FastApi(Starlette): def __init__( self, version: str = "0.1.0" ) -> None: self.version = version def get( self, path: str, ) -> Callable[. Any]: return self.router.get(path) def post( self, path: str, ) -> Callable[. Any]: return self.router.post(path) async def __call__( self, scope: Scope, receive: Receive, send: Send ) -> None: await super().__call__(scope, receive, send)
Точно, именно этот класс и будет использоваться для успешной инициализации нашего FastApi приложения.
Теперь пойдем дальше и перейдем в следующий файл routing.py, в котором у нас будет всего два класса ApiRouter и ApiRoute. Очевидно у них происходит определенное взаимодействие. ApiRouter действительно создает декораторы для методов запроса, возвращает из этих декораторы роуты, который представлены в виде класса ApiRoute, который в свою очередь содержит метод для проверки пути запроса и вызова endpoint если путь к экземпляре соответствующий, и по итогу, созданные из декораторов роуты или ApiRoute’s передаются в основной список роутов, в класс Router библиотеки starlette. И на этом работа FastApi с роутами заканчивается. А то, как проходить через них, какие методы вызывать у ApiRoute для соответствующих роутов остается на starlette.
Опять же, мы не будем лишний раз усложнять, и все добавляем постепенно. ApiRouter является классом для создания декораторов для функций, обрабатывающих запросы, и для передачи роутов к список, который хранится в starlette классе Routing, которые представлены классом ApiRoute состоящим из метода, пути, параметров и самой функции, то есть ApiRoute является оболочкой для определенного запроса.
Поэтому при инициализации ApiRouter мы без аргументов инициализируем класс Router от которого он наследуется, и просто добавим неинициализированный ApiRoute, который мы рассмотрим чуть позже, в переменную класса, для дальнейшей инициализации и использования при создании декоратора для определенного запроса.
myfastapi::routing::ApiRouter
from typing import Callable from starlette.routing import Router, Any class APIRouter(Router): def __init__(self) -> None: super().__init__() self.route_class = ApiRoute
Итак, следующий метод self.add_api_route будет принимать во-первых путь, который мы передадим в декоратор, например, @app.post(«/items/»), во-вторых endpoint, то есть функцию, которая обрабатывает данные запрос, например:
async def create_item(item: Item): return JSONResponse(item.dict())
После чего эти данные будут использоваться для создания экземпляра класса ApiRoute, используя self.route_class, который будет сразу передаваться в список self.routes, находящийся в классе Routing (starlette).
myfastapi::routing::ApiRouter::add_api_route
def add_api_route( self, path: str, endpoint: Callable[. Any], method: str ) -> None: route = self.route_class( path, endpoint=endpoint, method=method ) self.routes.append(route)
Собственно это и есть главная функциональность данного класса. Теперь осталось сделать только декораторы для методов post и get (помните, что мы используем только их в этой статье?).
То есть, возвращаясь к классу FastApi, нам нужно сделать возможным следующее:
myfastapi::applications::FastApi::get
def get( self, path: str, ) -> Callable[. Any]: return self.router.get(path)
И как вы догадываетесь, это очень просто:
myfastapi::routing::ApiRouter::get
def get(self, path: str) -> Callable[[Callable[. Any]], [Callable[. Any]]: def decorator(func: [Callable[. Any]) -> [Callable[. Any]: self.add_api_route(path, func, method=”get”) return func return decorator
Ничего необычного, простой декоратор, где func это наш endpoint для get запроса, и мы просто передаем путь и функцию для него в метод self.add_api_route, который мы рассмотрели выше. То есть таким простым способом мы регистрируем наши роуты в ASGI приложении.
И тоже самое для метода post
myfastapi::routing::ApiRouter::post
def post(self, path: str) -> Callable[[Callable[. Any]], [Callable[. Any]]: def decorator(func: [Callable[. Any]) -> [Callable[. Any]: self.add_api_route(path, func, method=”post”) return func return decorator
Мы также передаем и метод запроса, который используется для вызова функции для определенного пути, но в виде строки, хотя мы также могли бы привязать одну функцию для определенного пути для нескольких методов запроса. Просто будем иметь это ввиду.
Ну а сейчас, того, что есть в этом классе нам достаточно, посмотрим на весь получившийся класс:
myfastapi::routing::ApiRouter
from typing import Callable from starlette.routing import Router, Any class APIRouter(Router): def __init__(self) -> None: super().__init__() self.route_class = ApiRoute def add_api_route( self, path: str, endpoint: Callable[. Any], method: str ) -> None: route = self.route_class( path, endpoint=endpoint, method=method ) self.routes.append(route) def get(self, path: str) -> Callable[[Callable[. Any]], [Callable[. Any]]: def decorator(func: [Callable[. Any]) -> [Callable[. Any]: self.add_api_route(path, func, method=”get”) return func return decorator def post(self, path: str) -> Callable[[Callable[. Any]], [Callable[. Any]]: def decorator(func: [Callable[. Any]) -> [Callable[. Any]: self.add_api_route(path, func, method=”post”) return func return decorator
Теперь мы идем дальше, и уже начинается что-то интересное. Сейчас мы рассмотрим класс ApiRoute. Данный класс наследуется от starlette класса Route. И данный класс интересен нам тем, что он не просто является оболочкой, а переписывает поведение наследуемого класса. Вы наверное помните, что библиотека FastApi делает автоматическую валидацию типов для получаемых данных. Библиотека Starlette не поддерживает такую функциональность в своей основе, поэтому если бы вы использовали Starlette напрямую вместо FastApi, то вы могли бы присвоить своей функцию любую сигнатуру, то есть входящие переменные, присвоить им типы, но если в запросе могли бы прийти совершенно другие данные, и ошибка у вас возникла бы только при использовании заданных вами переменных функции, которая обрабатывает входящий запрос. FastApi же выдаст вам ошибку еще на этапе обработки полученных данных самой библиотекой, поэтому ваша функция даже не начнет работать, если входящие данные не соответствующие.
Собственно, данную функциональность в упрощенном виде, увеличивая ее функциональность в дальнейшем, мы будем реализовывать сейчас.
myfastapi::routing::ApiRoute
from starlette.routing import request_response class ApiRoute(routing.Router): def __init__( self, path: str, endpoint: Callable[. Any], method: str ) -> None: self.path = path self.endpoint = endpoint self.method = method assert callable(endpoint), "An endpoint must be a callable" self.dependant = get_dependant(path=self.path, call=self.endpoint) self.app = request_response(get_request_handler())
При инициализации ApiRoute мы видим несколько дополнительных функций, где get_dependant ключевая для нас сейчас, так как ее вызов зависимости для роута в виде параметров пути, запроса и тела, к которым присваиваются названия параметров, типы, является ли параметр обязательным, и дефолтное значение если нет. В двух словах, FastApi работает таким образом, что если вы не укажите дефолтное значение, при этом поставите тип Optional из typing, и не передадите этот параметр при вызове, то возникнет ошибка. Чтобы сделать параметр необязательным, вам нужно передать дефолтное значение.
Сейчас мы рассмотрим get_dependant, а к переменной self.app вернемся после.
myfastapi::dependencies::utils::get_dependant
def get_dependant( *, path: str, call: Callable[. Any], ) -> None: path_param_names = get_path_param_names(path) endpoint_signature = inspect.signature(call) signature_params = endpoint_signature.parameters dependant = Dependant(call=call, path=path) for param_name, param in signature_params.items(): param_field = get_param_field( param=param, param_name=param_name ) if param_name in path_param_names: dependant.path_params.append(param_field) elif (lenient_issubclass(param_field.type_, (list, set, tuple, dict)) or lenient_issubclass(param_field.type_, BaseModel) ): dependant.body_params.append(param_field) else: dependant.query_params.append(param_field)
Пойдем по порядку. get_dependant принимает путь и endpoint запроса. get_path_param_names просто возвращает нам имена параметров, которые являются частью пути:
myfastapi::utils::get_path_param_names
def get_path_param_names(path: str) -> Set[str]: return set(re.findall("<(.*?)>", path))
inspect.signature позволяет получить сигнатуру функции, то есть ее параметры с названием, типом и дефолтным значением. Класс Dependant просто хранит все параметры, которые в дальнейшем при вызове определенного роута будут использоваться для валидации полученных параметров.
myfastapi::dependencies::models::Dependant
from typing import Any, Callable, List, Optional from pydantic.fields import ModelField class Dependant: def __init__( self, *, path_params: Optional[List[ModelField]] = None, query_params: Optional[List[ModelField]] = None, body_params: Optional[List[ModelField]] = None, call: Optional[Callable[. Any]] = None, path: Optional[str] = None ) -> None: self.path_params = path_params or [] self.query_params = query_params or [] self.body_params = body_params or [] self.call = call self.path = path
Вы можете видеть, что параметры хранятся в виде класса ModelField библиотеки pydantic. В документации FastApi написано, он основан на pydantic, что имеет смысл, так как вся валидация параметров, которые делает FastApi происходит с помощью pydantic. Именно мы поэтому мы будем оборачивать наши параметры в ModelField.
Итак, возвращаясь к get_dependant, для каждого параметра endpoint функции, который был получен с помощью выявления сигнатуры, происходит цикл, в котором параметр сначала становится инстансом класса ModelField
param_field = get_param_field( param=param, param_name=param_name )
после чего, выявляя, что это за параметр, добавляется в класс Dependant:
if param_name in path_param_names: dependant.path_params.append(param_field) elif (lenient_issubclass(param_field.type_, (list, set, tuple, dict)) or lenient_issubclass(param_field.type_, BaseModel) ): dependant.body_params.append(param_field) else: dependant.query_params.append(param_field)
Функция get_param_field возвращает инстанс ModelField для параметра, и выглядит она следующим образом:
myfastapi::dependencies::utils::get_param_field
from pydantic import BaseConfig from pydantic.fields import ModelField, Undefined def get_param_field( param: inspect.Parameter, param_name: str ) -> ModelField: default_value: Any = Undefined if not param.default == param.empty: default_value = param.default required = True if default_value is not Undefined: required = False annotation: Any = Any if not param.annotation == param.empty: annotation = param.annotation field = ModelField( name=param_name, type_=annotation, default=default_value, class_validators=None, required=required, model_config=BaseConfig, ) return field
В get_param_field мы сначала делаем проверку на дефолтное значение, если оно присутствует, значит обозначаем параметр как необязательный, добавляем аннотацию, то есть тип параметра, и инициализируем ModelField с нашими и необходимыми параметрами.
При проверке, в какой список класса Dependant добавить данный инстанс параметра:
if param_name in path_param_names: dependant.path_params.append(param_field) elif (lenient_issubclass(param_field.type_, (list, set, tuple, dict)) or lenient_issubclass(param_field.type_, BaseModel) ): dependant.body_params.append(param_field) else: dependant.query_params.append(param_field)
Если название параметра в path_param_names, то есть он является частью пути, то он добавляется в path_params, если параметр является подклассом одной из четырех коллекций или BaseModel от pydantic, например:
class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None
то данный параметр добавляется в список body_params, и соответственно, если параметр не является ни частью пути, ни тела запроса, то он добавляется в query_params и расматривается как параметр пути, который указывается после “?” в запросе.
Отлично! Наши зависимости разрешены, и мы можем запускать приложение в режиме —reload. Они будут сидеть и ждать своей очереди на использование до запроса, который бы вызывал endpoint к которому вместе с путем определенный Dependant и относится.
Все, что нам осталось сделать, это добавить методы для обработки запросов, и для этого мы возвращаемся к классу ApiRoute:
myfastapi::routing::ApiRoute
from starlette.routing import request_response class ApiRoute(routing.Router): def __init__( self, path: str, endpoint: Callable[. Any], method: str ) -> None: self.path = path self.endpoint = endpoint self.method = method assert callable(endpoint), "An endpoint must be a callable" self.dependant = get_dependant(path=self.path, call=self.endpoint) self.app = request_response(get_request_handler(dependant=self.dependant))
Сейчас нам надо вспомнить то, о чем мы говорили в контексте ASGI приложения.
Чтобы обработать запрос ASGI приложения, запущенного, например с помощью uvicorn нужно следующее:
async def app(scope, receive, send): """ Echo the request body back in an HTTP response. """ body = await read_body(receive) await send(< 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ] >) await send(< 'type': 'http.response.body', 'body': body, >)
То есть приложение должно принять три параметра — scope, receive, send. Где scope это данные о запроса, включая путь, receive это тело запроса, а send метод для отправки ответа.
Но нам не нужно в статье, рассматривающей FastApi писать это с нуля, так как приемом запроса и отправкой ответа занимается starlette, нам, что и делает FastApi, нужно лишь использовать соответствующие классы.
Так вот, в нашем коде, в методе инициализации класса ApiRoute функция request_response, которая, из названия ясно, используется одновременно и для обработки request, и для отправки response, принимает пользовательскую функцию, которая принимает обработанный request с помощью класса Request от starlette, и возвращает функцию или класс response, который при вызове __call__ принимает три ASGI параметра, описанных выше, и использует await send для отправки ответа, что у нас благодаря starlette уже есть, а именно класс Response.
Соответственно, наша функция get_request_handler:
self.app = request_response(get_request_handler(dependant=self.dependant))
должна быть такой, как описано выше.
Давайте взглянем на нее.
myfastapi::routing::get_request_handler
def get_request_handler( dependant: Dependant, ) -> Callable[[Request], Coroutine[Any, Any, Response]]: is_coroutine = asyncio.iscoroutinefunction(dependant.call) async def app(request: Request) -> Response: body = None if dependant.body_params: body = await request.json() буду solved_result = await solve_dependencies( request=request, dependant=dependant, body=body ) values, errors = solved_result if errors: raise ValidationError(errors, RequestErrorModel) raw_response = await run_endpoint_function( dependant=dependant, values=values, is_coroutine=is_coroutine ) if isinstance(raw_response, Response): return raw_response if isinstance(raw_response, (dict, str, int, float, type(None))): return JSONResponse(raw_response) else: raise Exception("Type of response is not supported yet.") return app
Итак, get_request_handler принимает созданный нами ранее dependant для роута, после чего проверяет на асинхронность endpoint для роута, который хранится в переменной call у dependant.
Функция app будет использоваться во время запроса к роуту, в котором она находится. Функциональность данной функции во время запроса выглядит следующим образом. Сначала она получает тело запроса с помощью json функции класса Request от starlette, после чего, вызывает функцию solve_dependencies, которая производит валидацию полученных параметров, и возвращает необходимые значения для сигнатуры endpoint и ошибки, если они были получены при валидации. Если ошибок нет, то вызывается наша оригинальная функция run_endpoint, которая используя полученные значения параметров из запроса вызывает endpoint, который и возвращает response. Данный response как уже было описано выше, должен принимать ASGI параметры и отправлять ответ. И данный ответ, легко можно сделать, просто обернув наш ответ, например JsonResponse от starlette.
Теперь давайте рассмотрим solve_dependencies, а именно то, как она производит валидацию параметров.
myfastapi::dependencies::utils::solve_dependencies
async def solve_dependencies( *, request: Request, dependant: Dependant, body: Dict[str, Any], ) -> Tuple[Dict[str, Any], List[ErrorWrapper], Response]: values: Dict[str, any] = <> errors: List[ErrorWrapper] = [] path_values, path_errors = request_params_to_args( dependant.path_params, request.path_params ) query_values, query_errors = request_params_to_args( dependant.query_params, request.query_params ) values.update(path_values) values.update(query_values) errors += path_errors + query_errors if dependant.body_params: ( body_values, body_errors, ) = await request_body_to_args( # body_params checked above required_params=dependant.body_params, received_body=body ) values.update(body_values) errors.extend(body_errors) return values, errors
Вот наконец-то мы видим где используется dependant. Используя две довольно похожие функции request_params_to_args и request_body_to_args, которые принимают ожидаемые параметры, которые хранятся в списках класса Dependant в виде экземпляров класса ModelField от pydantic, магию которого мы увидим в этих методах, и полученные параметры, которые любезно достаются из scope классом Request от starlette, и body, который мы уже достали в get_request_handler.
Посмотрим сначала на request_params_to_args
myfastapi::dependencies::utils::request_params_to_args
from pydantic.error_wrappers import ErrorWrapper from pydantic.errors import MissingError def request_params_to_args( required_params: Sequence[ModelField], recieved_params: Union[Mapping[str, Any], QueryParams] ) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: values: Dict[str, Any] = <> errors: List[ErrorWrapper] = [] for field in required_params: value = recieved_params.get(field.alias) if value is None: if field.required: errors.append(ErrorWrapper( MissingError(), loc=field.alias) ) else: values[field.name] = deepcopy(field.default) continue v_, errors_ = field.validate(value, values, loc=field.alias) if isinstance(errors_, ErrorWrapper): errors.append(errors_) elif isinstance(errors_, list): errors.extend(errors_) else: values[field.name] = v_ return values, errors
Здесь происходит следующее: мы проходим циклом по каждому параметру, который нам требуется для endpoint, после чего используя переменную alias, значение которой, это имя параметра, мы пытаемся получить значение для данного параметра из объекта полученных параметров для запроса. Если значения для параметра нету и он является обязательным, то список ошибок добавляется MissingError, если он необязательный и значения нет, то просто копируется дефолтное значение и цикл переходит к следующему параметру.
Если же значение есть, то, храня параметр со всеми его данными в виде экземпляра класса ModelField от pydantic, нам всего лишь нужно вызвать метод validate с полученным значениями, которые вернет значение для параметра и ошибки если они возникли при валидации.
Соответственно, используя request_params_to_args, мы проверяем параметры пути и запроса:
path_values, path_errors = request_params_to_args( dependant.path_params, request.path_params ) query_values, query_errors = request_params_to_args( dependant.query_params, request.query_params ) values.update(path_values) values.update(query_values) errors += path_errors + query_errors
Теперь посмотрим на похожий метод request_body_to_args.
myfastapi::dependencies::utils::request_body_to_args
async def request_body_to_args( required_params: List[ModelField], received_body: Union[Dict[str, any], None], ) -> Tuple[Dict[str, any], List[ErrorWrapper]]: values: Dict[str, Any] = <> errors: List[ErrorWrapper] = [] if required_params: field_alias_omitted = len(required_params) if field_alias_omitted == 1: field = required_params[0] received_body = for field in required_params: if field_alias_omitted: loc = ("body",) else: loc = ("body", field.alias) value: Optional[Any] = None if received_body is not None: try: value = received_body.get(field.alias) except AttributeError: errors.append(ErrorWrapper(MissingError(), loc=loc)) continue if value is None: if field.required: errors.append(ErrorWrapper(MissingError(), loc=loc)) else: values[field.name] = deepcopy(field.default) continue v_, errors_ = field.validate(value, values, loc=loc) if isinstance(errors_, ErrorWrapper): errors.append(errors_) elif isinstance(errors_, list): errors.extend(errors_) else: values[field.name] = v_ return values, errors
Как мы видим, здесь практически все тоже самое как и в request_params_to_args, только теперь мы проверяем количество необходимых параметров в теле запроса, и идем циклам по ним.
Соответственно, мы используем функцию request_body_to_args, если список body_params класса Dependant, который хранит необходимые параметры для тела запроса, выявленные с помощью инспектирования сигнатуры endpoint, является не пустым.
if dependant.body_params: ( body_values, body_errors, ) = await request_body_to_args( # body_params checked above required_params=dependant.body_params, received_body=body ) values.update(body_values) errors.extend(body_errors) return values, errors
Полученные значения и ошибки возвращаются в функцию get_request_handler для дальнейшего использования:
solved_result = await solve_dependencies( request=request, dependant=dependant, body=body ) values, errors = solved_result if errors: raise ValidationError(errors, RequestErrorModel) raw_response = await run_endpoint_function( dependant=dependant, values=values, is_coroutine=is_coroutine )
Теперь, имея полученные данные, которые прошли валидацию, мы можем запустить endpoint роута используя функцию run_endpoint_function:
async def run_endpoint_function( *, dependant: Dependant, values: List[str, Any], is_coroutine: bool ) -> Any: assert dependant.call is not None, "dependant.call must be a function" if is_coroutine: return await dependant.call(**values) else: return await run_in_threadpool(dependant.call, **values)
В ней мы проверяем, если endpoint является асинхронным, то запускаем его в обычном await, если же нет, мы используем функцию run_in_threadpool от starlette, чтобы запустить endpoint в асинхронном режиме, не блокируя event loop.
И сейчас, после того, как наш endpoint отработал и вернул ответ, нам нужно проверить, что это за ответ:
if isinstance(raw_response, Response): return raw_response if isinstance(raw_response, (dict, str, int, float, type(None))): return JSONResponse(raw_response) else: raise Exception("Type of response is not supported yet.")
Что здесь происходит? Сперва мы проверяем, является ли ответ экземпляром класса Response от starlette, который имеет всю необходимую функциональность для отправки ответа ASGI приложению.
Для этого нужно импортировать класс JSONResponse:
from starlette.responses import JSONResponse
и использовать его для создания ответа в endpoint
@app.post("/items/") async def create_item(item_id: int): return JSONResponse()
У нас, в случае если ответ, это dict или простой тип, то мы просто оборачиваем их в JSONResponse, но у FastApi идет более сложная обработка, так, что нужно понять, это, используя класс JSONResponse в вашем пользовательском endpoint вы увеличиваете скорость ответа вашего приложения, что может быть очень важно. Также, если вы снова обратите внимание на run_endpoint_function, то поймете, что сразу делая endpoint асинхронным, вы также сокращаете время обработки.
Вот так вот, понимая внутреннее устройство, можно увеличить производительность вашего FastApi приложения — используйте асинхронность и Response от starlette для создания ваших endpoint’s.
Собственно вот, мы рассмотрели, как сделать простое FastApi приложение с нуля. Но как вы поняли, это совсем не с нуля. FastApi построен не на pydantic, как это указано в документации, а полностью на starlette.
Запускаем наше приложение
Все, что нам нужно сделать, это в первом куске кода в начале статьи заменить импорт главного класса, из которого создается приложение:
from typing import Union from myfastapi.applications import FastAPI from starlette.responses import JSONResponse from pydantic import BaseModel class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None app = FastAPI() @app.post("/items/") async def create_item(item: Item, item_id: int): return JSONResponse() @app.get("/") async def homepage(): return JSONResponse() @app.get("/get_items/") async def read_item(item_id: int): return
Просто используйте следующую команду:
uvicorn main:app --reload
Теперь вы можете протестировать эти API’s с помощью, например, Postman.