Автоматическая документация для Flask с использованием OpenAPI
Техническая документация, как известно, крайне важная часть любого проекта. До недавнего времени мы прекрасно жили с таким генератором документаций как Sphinx. Но наступил момент переходить на технологии с бОльшим набором возможностей, поэтому мы приняли решение переписать нашу документацию на более современный стандарт: OpenAPI Specification. Эта статья является скромным гайдом по такому переезду. Она будет интересна Python-разработчикам, особенно тем, которые используют Flask. После ее прочтения вы узнаете, как создать статическую OpenAPI документацию для Flask приложения и развернуть ее в GitLab Pages.
apispec + marshmallow
В качестве веб-фреймворка у нас используется Flask. Документацию для API, созданного с помощью него, мы и хотим создать. Спецификация по стандарту OpenAPI описывается форматом YAML (или JSON). Чтобы преобразовать докстринги нашего API в необходимый формат, будем использовать такой инструмент, как apispec, и его плагины. Например, MarshmallowPlugin, с помощью которого (и самой библиотеки marshmallow) можно удобно — за счет возможности наследования и переиспользования — описать входные и выходные данные эндпоинтов в виде python классов, а также провалидировать их.
Используя библиотеку marshmallow, создадим класс, описывающий параметры API:
from marshmallow import Schema, fields
class InputSchema(Schema):
number = fields.Int(description="Число", required=True, example=5)
power = fields.Int(description="Степень", required=True, example=2)
Аналогично сделаем для выходных параметров:
class OutputSchema(Schema):
result = fields.Int(description="Результат", required=True, example=25)
Для группировки запросов в OpenAPI используются теги. Создадим тег и добавим его в объект APISpec:
def create_tags(spec):
""" Создаем теги.
:param spec: объект APISpec для сохранения тегов
"""
tags = [{'name': 'math', 'description': 'Математические функции'}]
for tag in tags:
print(f"Добавляем тег: {tag['name']}")
spec.tag(tag)
Далее нам нужно интегрировать параметры в докстринг так, чтобы это соответствовало OpenAPI спецификации.
Пример:
from flask import Blueprint, current_app, json, request
blueprint_power = Blueprint(name="power", import_name=__name__)
@blueprint_power.route('/power')
def power():
"""
---
get:
summary: Возводит число в степень
parameters:
- in: query
schema: InputSchema
responses:
'200':
description: Результат возведения в степень
content:
application/json:
schema: OutputSchema
'400':
description: Не передан обязательный параметр
content:
application/json:
schema: ErrorSchema
tags:
- math
"""
args = request.args
number = args.get('number')
if number is None:
return current_app.response_class(
response=json.dumps(
{'error': 'Не передан параметр number'}
),
status=400,
mimetype='application/json'
)
power = args.get('power')
if power is None:
return current_app.response_class(
response=json.dumps(
{'error': 'Не передан параметр power'}
),
status=400,
mimetype='application/json'
)
return current_app.response_class(
response=json.dumps(
{'response': int(number)**int(power)}
),
status=200,
mimetype='application/json'
)
Эта функция — пример реализации метода GET в нашем API.
Блок summary. Краткое описание функции. Для более подробного описания можно добавить блок description.
Блок parameters. Описание параметров запроса. У параметра указывается, откуда он берется:
- path, для /power/{number}
- query, для /power? number=5
- header, для X-MyHeader: Value
- cookie, для параметров переданных в cookie файле
и schema, в которую передается python класс, описывающий данный параметр.
Блок responses. Описание вариантов ответа команды и их структура.
Блок tags. Описание тегов, которые используются для логической группировки эндпоинтов.
Для POST запроса, например, можно указать еще requestBody, в котором описываются параметры, передаваемые в теле. Подробнее можно почитать в официальной документации.
После того, как мы описали методы API, можем загрузить их описание в объект APISpec:
def load_docstrings(spec, app):
""" Загружаем описание API.
:param spec: объект APISpec, куда загружаем описание функций
:param app: экземпляр Flask приложения, откуда берем описание функций
"""
for fn_name in app.view_functions:
if fn_name == 'static':
continue
print(f'Загружаем описание для функции: {fn_name}')
view_fn = app.view_functions[fn_name]
spec.path(view=view_fn)
Создаем метод get_apispec, который будет возвращать объект APISpec, в нем добавляем общую информацию о проекте и вызываем описанные ранее методы load_docstrings и create_tags:
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
def get_apispec(app):
""" Формируем объект APISpec.
:param app: объект Flask приложения
"""
spec = APISpec(
title="My App",
version="1.0.0",
openapi_version="3.0.3",
plugins=[FlaskPlugin(), MarshmallowPlugin()],
)
spec.components.schema("Input", schema=InputSchema)
spec.components.schema("Output", schema=OutputSchema)
spec.components.schema("Error", schema=ErrorSchema)
create_tags(spec)
load_docstrings(spec, app)
return spec
Swagger UI
Swagger UI позволяет создать интерактивную страницу с документацией.
Создадим эндпоинт, который будет возвращать спецификацию в json формате, и вызываем в нем get_apispec:
@app.route('/swagger')
def create_swagger_spec():
return json.dumps(get_apispec(app).to_dict())
Теперь, когда мы получили json спецификацию, нам нужно сформировать из неё html документ. Для этого воспользуемся пакетом flask_swagger_ui, с помощью которого можно встроить интерактивную страницу с документацией на базе Swagger UI в наше Flask приложение:
from flask_swagger_ui import get_swaggerui_blueprint
SWAGGER_URL = '/docs'
API_URL = '/swagger'
swagger_ui_blueprint = get_swaggerui_blueprint(
SWAGGER_URL,
API_URL,
config={
'app_name': 'My App'
}
)
Таким образом, мы сделали эндпоинт /docs, при обращении по которому получаем документацию следующего вида:
GitLab Pages + ReDoc
Если мы не хотим формировать документацию во время обращения по эндпоинту, то можно собрать статичный документ.
Использование GitLab позволяет сгенерировать такую статическую страницу с документацией в CI/CD процессах.
Таким образом, мы один раз соберем документацию, и при обращении по заданному адресу она будет просто отображаться без какой-либо дополнительной обработки.
Для этого сохраним APISpec в YAML файл:
DOCS_FILENAME = 'docs.yaml'
def write_yaml_file(spec: APISpec):
""" Экспортируем объект APISpec в YAML файл.
:param spec: объект APISpec
"""
with open(DOCS_FILENAME, 'w') as file:
file.write(spec.to_yaml())
print(f'Сохранили документацию в {DOCS_FILENAME}’)
Теперь, когда мы получили YAML файл по спецификации OpenAPI, нужно сформировать HTML документ. Для этого будем использовать ReDoc, так как он позволяет сгенерировать документ в gitlab-ci с красивой и удобной структурой. Публиковать его будем с помощью GitLab Pages.
Добавим следующие строки в файл gitlab-ci.yml:
pages:
stage: docs
image: alpine:latest
script:
- apk add --update nodejs npm
- npm install -g redoc-cli
- redoc-cli bundle -o public/index.html docs.yaml
artifacts:
paths:
- public
Стоит отметить, что index.html нужно сохранять в папку public, так как она зарезервирована GitLab«ом.
Теперь, если мы запушим изменения в репозиторий, по адресу namespace.gitlab.com/project появится документация:
Также путь до документации можно посмотреть в Settings/Pages
Пример документации с использованием ReDoc: ivi-ru.github.io/hydra
Заключение
Таким образом, мы научились собирать OpenAPI документацию с использованием ReDoc и хостить ее на GitLab Pages. В эту статью не попало еще несколько возможностей этих инструментов, например, валидация параметров с помощью marshmallow. Но основной ее целью было показать непосредственно процесс создания документации.