В этой статье я расскажу о Swagger и о том, как сгенерировать API и Pydantic модели из Swagger-документации для FastAPI, используя инструмент OpenAPI Generator. В конце статьи вы найдете ссылки на исходный код.
Итак, давайте разбираться!
Для чего это нужно?
Когда вы работаете над API, написание ручного кода для каждого маршрута и модели данных может занять много времени, особенно если система сложная. Описав API в формате OpenAPI (ранее известный как Swagger), вы сможете автоматически генерировать готовые маршруты и Pydantic-модели, которые легко интегрируются в FastAPI. Это освобождает вас от рутины написания однотипных классов и функций, позволяя сосредоточиться на логике приложения.
Что такое Swagger
Swagger — это набор инструментов, облегчающих разработку, тестирование и документирование API. Его основной компонент — это спецификаций в формате OpenAPI, который используется для описания структуры API, доступных маршрутов, типов данных и взаимодействий. Благодаря этому формату Swagger позволяет генерировать документацию, тестировать API, а также создавать клиентские и серверные SDK для различных языков программирования. В этой статье мы сосредоточимся на генерации исходного кода для FastAPI с помощью OpenAPI Generator.
Генерация исходного кода
Для начала необходимо написать документацию нашего API в формате OpenAPI. Создадим файл spec.yml
, который будет содержать описание наших маршрутов:
openapi: 3.0.0
info:
description: "API"
version: "1.0.0"
title: "API"
paths:
/mock:
get:
tags:
- Mock
operationId: mock
parameters:
- name: id
in: query
required: true
schema:
type: integer
responses:
'200':
description: "Successful response"
content:
application/json:
schema:
$ref: '#/components/schemas/MockDataResponse'
post:
tags:
- Mock
operationId: add_mock
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateMockData'
responses:
'200':
description: "Successful response"
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseCreateMock'
components:
schemas:
MockDataResponse:
type: object
properties:
id:
type: integer
info:
type: string
CreateMockData:
type: object
properties:
info:
type: string
ResponseCreateMock:
type: object
properties:
id:
type: integer
Как видно из примера, наша документация содержит два основных запроса: GET
и POST
. Мы также описали объекты, которые эти запросы принимают и возвращают.
Теперь, когда документация готова, мы можем перейти к процессу генерации исходного кода с помощью OpenAPI Generator.
OpenAPI Generator — это инструмент, предназначенный для автоматической генерации кода на основе спецификаций в формате OpenAPI. Он поддерживает множество языков программирования и фреймворков, включая Python, Java, JavaScript, Ruby и многие другие. С полным списком поддерживаемых языков и фреймворков можно ознакомиться здесь.
Установить OpenAPI Generator можно разными способами, но для удобства мы будем использовать Docker. Для этого создадим скрипт generate-api.sh
, который автоматизирует процесс генерации:
#!/bin/sh
set -e
ROOT=$(dirname $0)
cd "$ROOT"
sudo rm -Rf ./endpoints/apis ./endpoints/models ./endpoints/router_init.py
mkdir -p "$ROOT/endpoints"
sudo rm -Rf ./openapi-generator-output
docker run --rm -v "${PWD}":/app openapitools/openapi-generator-cli:latest-release generate \
-i /app/spec.yml -g python-fastapi -o /app/openapi-generator-output \
--additional-properties=packageName=endpoints --additional-properties=fastapiImplementationPackage=endpoints
Теперь разберем ключевые аргументы, которые мы используем в команде generate
:
i указывает на файл с документацией (в нашем случае —
spec.yml
).g определяет генератор для конкретного фреймворка или языка. Например, чтобы сгенерировать код для приложения на Flask, указываем
python-flask
, но в нашем случае используетсяpython-fastapi
.o задаёт директорию, в которую будет помещен сгенерированный код.
-additional-properties=packageName задаёт имя пакета для сгенерированного кода.
-additional-properties=fastapiImplementationPackage указывает, где будет находиться реализация FastAPI.
Этот скрипт создаст новую папку openapi-generator-output
с множеством файлов. Давайте посмотрим что он создал.
Обзор сгенерированных файлов
После генерации в папке появляется несколько конфигурационных файлов, но основное внимание стоит уделить папке src
, так как именно в ней содержится код нашего приложения. Вот краткий обзор её структуры:
main.py — файл, содержащий код для запуска приложения FastAPI.
security_api.py — здесь описана логика проверки доступа к маршрутам.
-
/api/ — папка с маршрутами API и логикой каждого из них:
_api.py
— файлы, которые инициализируют маршруты и проверяют, что методы были реализованы._api_base.py
— базовые классы, от которых можно наследоваться для реализации маршрутов.
Также в папке /models/ находятся файлы с классами Pydantic, которые отвечают за валидацию и сериализацию данных.
На этом этапе у нас уже есть сгенерированный код, и его можно использовать в проекте. Чтобы приложение FastAPI нашло все необходимые классы и методы, достаточно создать файл с именем, которое было указано в параметре:
bash
--additional-properties=fastapiImplementationPackage
Этот файл будет содержать бизнес логику всех маршрутов, наследующихся от базовых классов *_api_base.py
.
Однако, при генерации OpenAPI Generator создает много лишних файлов, которые не всегда нужны. Поэтому давайте немного изменим наш скрипт generate-api.sh
, чтобы убрать ненужные файлы и оставить только то, что нужно:
#!/bin/sh
set -e
ROOT=$(dirname $0)
cd "$ROOT"
sudo rm -Rf ./endpoints/apis ./endpoints/models ./endpoints/router_init.py
mkdir -p "$ROOT/endpoints"
sudo rm -Rf ./openapi-generator-output
docker run --rm -v "${PWD}":/app openapitools/openapi-generator-cli:latest-release generate \
-i /app/spec.yml -g python-fastapi -o /app/openapi-generator-output \
--additional-properties=packageName=endpoints --additional-properties=fastapiImplementationPackage=endpoints
sudo chown "$USER":"$USER" -R openapi-generator-output
rm -Rf endpoints/apis endpoints/models
mv openapi-generator-output/src/endpoints/apis endpoints/
mv openapi-generator-output/src/endpoints/models endpoints/
mv openapi-generator-output/src/endpoints/main.py endpoints/router_init.py
rm -Rf openapi-generator-output
Теперь все нужные файлы находятся в папке endpoints
, что делает структуру проекта более чистой и организованной
Как добавить бизнес-логику?
Теперь, когда у нас есть сгенерированные маршруты, давайте разберемся, как добавить бизнес-логику в наше приложение FastAPI. Для этого заглянем в папку /endpoints/apis
, где находятся основные файлы с маршрутизаторами.
Пример содержимого /enpoints/apis
В файле mock_api.py
мы видим следующее:
router = APIRouter()
ns_pkg = endpoints
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
importlib.import_module(name)
@router.post(
"/mock",
responses={
200: {"model": ResponseCreateMock, "description": "Successful response"},
},
tags=["Mock"],
response_model_by_alias=True,
)
async def add_mock(
create_mock_data: CreateMockData = Body(None, description=""),
) -> ResponseCreateMock:
if not BaseMockApi.subclasses:
raise HTTPException(status_code=500, detail="Not implemented")
return await BaseMockApi.subclasses[0]().add_mock(create_mock_data)
@router.get(
"/mock",
responses={
200: {"model": MockDataResponse, "description": "Successful response"},
},
tags=["Mock"],
response_model_by_alias=True,
)
async def mock(
id: int = Query(None, description="", alias="id"),
) -> MockDataResponse:
if not BaseMockApi.subclasses:
raise HTTPException(status_code=500, detail="Not implemented")
return await BaseMockApi.subclasses[0]().mock(id)
В файле mock_api.py
создаётся роут для fastAPI и проверяется, что наш метод кто то реализует, а так же динамически импортирует все подмодули из переменой ns_pkg
(содержит имя папки которое передали в --additional-properties=fastapiImplementationPackage
).
В файле mock_api_base.py
реализована логика, которая автоматически сохраняет всех наследников класса BaseMockApi
в массив и содержит методы, которые нам надо реализовать:
class BaseMockApi:
subclasses: ClassVar[Tuple] = ()
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
BaseMockApi.subclasses = BaseMockApi.subclasses + (cls,)
async def add_mock(
self,
create_mock_data: CreateMockData,
) -> ResponseCreateMock:
...
async def mock(
self,
id: int,
) -> MockDataResponse:
...
Теперь когда мы разобрались в том , что мы сгенерировали, нам не составит труда сделать реализацию наших роутов. Для этого в папке /endpoints
создадим файл mock_impl.py
и напишем простейшую реализацию.
class MockImpl(BaseMockApi):
data = [MockDataResponse(id=1, info="hello world!"), MockDataResponse(id=2, info="bye world!")]
async def add_mock(self, create_mock_data: CreateMockData) -> ResponseCreateMock:
new_id = max(mock.id for mock in self.data) + 1 #
new_mock = MockDataResponse(id=new_id, info=create_mock_data.info)
self.data.append(new_mock)
return ResponseCreateMock(id=new_id)
async def mock(self, id: int) -> MockDataResponse:
for mock in self.data:
if mock.id == id:
return mock
raise ValueError("Data with given ID not found.")
Создадим файл main.py
для старта приложения. Если у вас есть необходимость добавить какую-то логику перед запуском сервера, вы можете сделать это в этом файле:
from endpoints.router_init import app
Теперь запустим сервис командой:
uvicorn main:app --host 0.0.0.0 --port 8080
Проверим работу сервиса, отправив в него запрос:
curl --location 'http://localhost:8080/mock?id=1'
{"id":1,"info":"hello world!"}
В результате мы получили работающий сервер на основе нашей документации и потратили минимум времени на его реализацию.
Заключение
Использование Swagger и OpenAPI Generator значительно упрощает разработку API, сокращает количество рутинной работы и улучшает качество документации. Этот подход позволяет быстро перейти от описания API к готовому коду, что экономит время и силы. Однако на данный момент не все возможности OpenAPI поддерживаются для генерации кода под Flask и FastAPI. Тем не менее, OpenAPI Generator активно развивается, и эти ограничения могут быть устранены в будущем.
pvzh
Что-то у меня голова закружилась. С ног на голову перевёрнуто. Как раз-таки ключевая особенность FastAPI это чудесный встроенный генератор OpenAPI из Pydantic-моделей. А писать Pydantic-модели проще некуда и уж намного приятнее всяких Swagger Editor-ов.
Gerbylev Автор
Вы правы, но многие выбирают Swagger для документации API. Если вы всё равно пишете Swagger, то почему бы не сократить часть работы? В случае с FastAPI это может быть не так очевидно, но тот же подход применим к Flask, где он действительно полезен.
Лично мне приятнее писать Swagger, чем Python. А так же я использую одну спецификацию для фронта и бэка, что сильно упрощает разработку.
pvzh
Но ведь в FastAPI уже из коробки есть Swagger UI. Я не пишу его, он там работает автомагически. Зачем выбирать, когда он уже там есть?
Я вижу противоестественное использование инструмента и это меня напрягает. Если смотреть в масштабе команды, когда можно выбирать инструменты, то FastAPI полностью закрывает и валидацию и автодокументацию. Это его лучшие стороны. Мой опыт с JSON Schema и другими валидаторами (и другими стеками) позволяет сравнить и сделать вывод, что FastAPI здесь крайне хорош. Рекомендую почитать, как автор объясняет причину создания FastAPI: https://fastapi.tiangolo.com/alternatives/#swagger-openapi
Про Flask и сокращение работы - скажу кратко и поэтому довольно категорично. В новом проекте я бы забыл про Flask и попросил бы аналитиков работать с вики-текстом и схемами и не тратить время на собирание yaml руками. Само собой, у фронтэнда и тестировщиков будет всегда актуальный Swagger UI, бесплатно. Если в проекте Flask и автогенерация Pydantic моделей - это не мой проект, извините.
gersp
Не с ног на голову, а API first. Вот, например, статья объясняющая различия принципов code first и api first: https://habr.com/ru/companies/axenix/articles/694340/
И тот и другой принцип имеет свои плюсы и минусы и оба используются в разных проектах. Как мне кажется, code first даже более распространён в виду простоты реализации. Но ничего противоестественного в API first нет.
Мне очень понравился комментарий пользователя @lebedecиз статьи, что я привёл выше в этом комменте. Потому перепечатаю его здесь и посоветую прочитать статью, если тема вам кажется интересной.
pvzh
API first это хорошо и понятно, и с этим нигде не спорил. Противоестественность вижу только в использовании конкретного инструмента, FastAPI. Ведь автор фреймворка подробно изложил свой подход, ссылка в предыдущем комменте. Взять инструмент и не пользоваться половиной его возможностей или пользоваться поперёк - не мой путь.
Да, тема настолько интересна, что каждый день по ней работаю. Во многих статьях красиво изложено на паре примитивных роутов и на типах integer и string. На практике бывают весьма развесистые джсоны на входе/выходе, с кучей разнообразных проверок, включая непустые массивы, диапазоны чисел, url и прочее. Можете подсказать видео, очень хочется посмотреть процесс, как с нуля собирается yaml для нетривиального API хотя бы на пять полноценных сущностей?
Если проект большой, много команд и есть отдельный опытный аналитик, то конечно же удобно и правильно сперва зафиксировать спеку. Я не лезу в такое, пишу только с позиции одной команды и проекта типа стартапа. Так вот, на мой взгляд FastAPI особенно хорош для стартапов, и удивительно и приятно видеть его заход в более крупные проекты.
gersp
Что ж тут противоестественного. Возможность генерации спеки из кода никак не запрещает генерить из спеки код.
Ознакомьтесь со списоком генераторов. Тут есть минимум три генератора, которые создают pydantic модели из swagger-спеки:
https://openapi-generator.tech/docs/generators
Зачем-то ведь люди это делают.
И вот ещё по теме:
https://github.com/fastapi/fastapi/issues/519
Обратите внимание на реакцию сообщества на первый комментарий.
pvzh
Реакция сообщества - это вы про 8 дизлайков в гитхабе? Это ни о чём. Я сугубо практик. Про connexion там верно подметили, вот инструмент гораздо более подходящий.
Даже пробовал похожее, для генерации нагрузочных тестов. Не впечатлило.
Вы не подумайте, мне нравится сама идея API first. Ещё бы поглядеть на видео как полагается спеку собирать, опенсорсными local-first инструментами, желательно не выходя из VS Code. Swagger Editor не предлагать;)
gersp
В этом процессе нет ничего особенно, по сравнению даже с тем, что описано в этой статье.
На практике (проектов среднего размера) скажу, что в API first подходе определённо есть ряд проблем:
несовершенство генераторов порой не позволяет описать спеку так, как хочется, потому что генератор чего-то не поддерживает. Иногда приходится править спеку, а иногда коммитить в генератор.
могут быть сложности с описанием правил валидации. В спеке не описать всех проверок, которые можно описать в коде. Но такие проверки надо и оставить в коде. Так что это не существенная проблема.
могут быть сложности с добавлением кастомных аннотаций (декораторов) к сгенерированным классам. Но мне такой необходимости удавалось избегать.
Но при Code first будет ряд других проблем связанных с построением процессов интеграции изменений в генерируемой спеке между несколькими компонентами. И на мой субъективный взгляд, проблем настройки CI в случае code-first - больше.