В этой статье я расскажу о 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 активно развивается, и эти ограничения могут быть устранены в будущем.
Автор: Gerbylev