Техническая документация, как известно, крайне важная часть любого проекта. До недавнего времени мы прекрасно жили с таким генератором документаций как 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, fieldsclass 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, requestblueprint_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 APISpecfrom apispec.ext.marshmallow import MarshmallowPluginfrom apispec_webframeworks.flask import FlaskPlugindef 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_blueprintSWAGGER_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. Но основной ее целью было показать непосредственно процесс создания документации.