image alt


Техническая документация, как известно, крайне важная часть любого проекта. До недавнего времени мы прекрасно жили с таким генератором документаций как 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


image alt


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, при обращении по которому получаем документацию следующего вида:

image alt


image alt

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 появится документация:

image alt


Также путь до документации можно посмотреть в Settings/Pages

Пример документации с использованием ReDoc: ivi-ru.github.io/hydra

Заключение


Таким образом, мы научились собирать OpenAPI документацию с использованием ReDoc и хостить ее на GitLab Pages. В эту статью не попало еще несколько возможностей этих инструментов, например, валидация параметров с помощью marshmallow. Но основной ее целью было показать непосредственно процесс создания документации.

Полезные ссылки