Документирование Flask RESTful API с OpenAPI (Swagger) с использованием APISpec
Создайте свой собственный view class и функции утилиты для сокращения кода и избежания ошибок.
Когда вы создаете API, вы хотите задокументировать его, и сегодня для этого очевидно использовать OpenAPI . Я уже использую пакет Marshmallow . Те же люди разработали пакет APIspec, в дополнительном пакете apispec-webframeworks использовался плагин Flask . Для представления документации OpenAPI я использую пакет flask-swagger-ui , а пакет flask-jwt-extended используется для защиты конечных точек.
Ниже я показываю способ создания собственных функций утилиты API view class вместе с API view class , чтобы уменьшить объем кода и избежать ошибок.
Быстрый запуск на примере
В интернете очень много информации о сборке API с Flask и Swagger. Я был рад найти хороший небольшой пример для начала, 'REST API Development with Flask', смотрите ссылки ниже. Вот что здесь происходит:
- Настройте вашу базовую спецификацию OpenAPI
- Документируйте конечные точки с помощью docstrings.
- Сканируйте все функции просмотра и добавьте их в вашу спецификацию.
Генерация документации OpenAPI путем сканирования docstrings
Хорошая ли идея использовать docstrings для генерации документа OpenAPI ? Строки документа привязаны к функции или методу. У меня смешанные чувства по этому поводу. Когда у вас много функций или методов в разных файлах, внесение изменений в ваш docstrings может занять очень много времени.
Альтернативой может быть создание отдельного документа OpenAPI . Здесь вся информация находится в одном месте, большую часть времени, в одном документе. Сначала вы создаете спецификации и используете этот документ для создания своих функций и методов. С помощью соответствующего программного обеспечения вы, вероятно, сможете автоматически генерировать большие части кода. Это также облегчает внесение изменений.
Хотя последний способ является предпочтительным, здесь я использую метод сканирования docstring .
Проблема: Дублирование docstring
В большинстве случаев мы имеем ряд классов модели, которые обрабатываются более или менее одинаково. Я создал демонстрационное приложение, состоящее из следующих классов:
- Friend, может иметь ноль или один город, и ноль или больше хобби
- City
- Hobby
- User
OpenAPI docstrings для этих классов более или менее одинаковы. Например, методы обновления для City и Hobby являются одинаковыми:
class CitiesAPI(...):
...
@jwt_required()
def update_by_id(self, city_id):
'''
---
put:
tags:
- City
summary: Update city by id
description: Update city by id
parameters:
- in: path
schema: CitiesRequestPathParamsSchema
requestBody:
required: true
content:
application/json:
schema: CitiesUpdateRequestBodyParamsSchema
responses:
400:
description: One or more request parameters did not validate
404:
description: City not found
200:
description: City updated
content:
application/json:
schema:
type: object
properties:
data: CitiesResponseSchema
'''
<code of the method>
и
class HobbiesAPI(...):
...
@jwt_required()
def update_by_id(self, hobby_id):
"""
---
put:
tags:
- Hobby
summary: Update hobby by id
description: Update hobby by id
parameters:
- in: path
schema: HobbiesRequestPathParamsSchema
requestBody:
required: true
content:
application/json:
schema: HobbiesUpdateRequestBodyParamsSchema
responses:
400:
description: One or more request parameters did not validate
404:
description: Hobby not found
200:
description: Hobby updated
content:
application/json:
schema:
type: object
properties:
data: HobbiesResponseSchema
"""
<code of the method>
Обратите внимание, что эти методы декорированы @jwt_required() decorator. Я предполагаю, что вы также хотите сделать это (в какой-то момент).
Чтобы избежать дублирования, я создал базу APIModelViewClass и некоторые функции утилиты в APIModelViewClassUtils.
Почему вы не можете использовать Flask MethodView.
Я посмотрел Flask MethodView , так как состояния документации полезны при сборке APIs. В данном классе есть такие методы как get, post, которые вызываются в зависимости от метода запроса. Это означает, что метод запроса GET вызывает метод get класса MethodView .
Для City, мой API должен иметь следующие функции:
list GET /cities
create POST /cities
get_by_id GET /cities/<int:city_id>
update_by_id GET /cities/<int:city_id>
delete_by_id GET /cities/<int:city_id>
Мы используем метод GET как для списка городов, так и для одного города. Документация Flask предлагает нам внутри метода получения решить, какую функцию мы должны выполнять:
def get(self, city_id):
if city_id is None:
# return a list of citys
pass
else:
# expose a single city
pass
Но теперь мы больше не можем добавлять метод docstring ... :-(
Выходом из этого является использование двух классов MethodView . Один для функции списка и создания функций и один для функции 'by_id'. Но даже с этим решением есть проблемы. Похоже, что при использовании Blueprints не все функции добавляются apispec, смотрите также 'Добавлен только один путь для Flask API MethodViews #14'.
На данный момент я не уверен в статусе, поэтому решил создать свой собственный 'MethodView', названный APIModelViewClass, и свой собственный класс утилит APIModelViewClassUtils.
Использование decorator для добавления docstrings
Очевидным выбором является использование decorator для добавления OpenAPI docstring к методам API . После их появления они могут быть отсканированы и загружены методами apispec.
Многие из моих методов API уже декорированы @jwt_required() decorator, что означает, что мы должны объединить эти decorators.
Я создал класс APIModelViewClass, то есть на данный момент просто корешок, и класс APIModelViewClassUtils
, содержащий функции утилиты, на данный момент содержащий три метода:
- decorate_with_docstring()
- endpoint_decorator()
- endpoints_register()
Позже я могу объединить эти занятия. Ниже приведена часть кода.
class APIModelViewClassUtils:
def decorate_with_docstring(fn, op, params):
tags = params.get('tags')
item_name_cap, item_name, item_name_plural_cap, item_name_plural = params.get('item_names')
in_path_schema = params.get('in_path_schema')
in_query_schema = params.get('in_query_schema')
in_body_schema_create = params.get('in_body_schema_create')
in_body_schema_update = params.get('in_body_schema_update')
out_data_schema = params.get('out_data_schema')
'''
if op == 'list':
fn.__doc__ = """
...
elif op == 'update_by_id':
fn.__doc__ = """
---
put:
tags:
- {tags}
summary: Update {item_name} by id
description: Update {item_name} by id
parameters:
- in: path
schema: {in_path_schema}
requestBody:
required: true
content:
application/json:
schema: {in_body_schema_update}
responses:
400:
description: One or more request parameters did not validate
404:
description: {item_name_cap} not found
200:
description: {item_name_cap} updated
content:
application/json:
schema:
type: object
properties:
data: {out_data_schema}
""".format(
tags=tags,
item_name=item_name,
item_name_cap=item_name_cap,
in_path_schema=in_path_schema,
in_body_schema_update=in_body_schema_update,
out_data_schema=out_data_schema)
'''
@classmethod
def endpoint_decorator(cls, params):
def wrapper(fn):
op = fn.__name__
# decorate endpoint
cls.decorate_with_docstring(fn, op, params)
@wraps(fn)
def decorator(*args, **kwargs):
# verify_jwt_in_request works like @jwt_required
verify_jwt_in_request()
return fn(*args, **kwargs)
return wrapper
@staticmethod
def endpoints_register(bp, api_model_view_class, url, id_attr_name, id_type='int'):
model_obj = api_model_view_class()
'''
# update_by_id
if hasattr(model_obj, 'update_by_id'):
bp.add_url_rule(url_by_id, view_func=model_obj.update_by_id, methods=['PUT'])
'''
class APIModelViewClass():
pass
В decorate_with_docstring() мы декорируем метод форматированным docstring. В endpoint_decorator() сначала декорируем метод, а затем проверяем, предоставлен ли доступ с помощью функции verify_jwt_in_request(). В endpoints_register мы регистрируем конечные точки, используемые этим классом.
Теперь (часть) CityAPI выглядит так:
class CitiesAPI(APIModelViewClass):
params = {
'tags': 'City',
'item_names': ['City', 'city', 'Cities', 'cities'],
'in_query_schema': 'CitiesRequestQueryParamsSchema',
'in_path_schema': 'CitiesRequestPathParamsSchema',
'in_body_schema_create': 'CitiesCreateRequestBodyParamsSchema',
'in_body_schema_update': 'CitiesUpdateRequestBodyParamsSchema',
'out_data_schema': 'CitiesResponseSchema',
}
'''
@APIModelViewClassUtils.endpoint_decorator(params)
def update_by_id(self, city_id):
result = APIUtils.request_schemas_load([
CitiesRequestPathParamsSchema(),
CitiesUpdateRequestBodyParamsSchema()])
city = APIUtils.get_by_id_or_404(City, city_id, 'City', 'city_id')
for k, v in result['body'].items():
setattr(city, k, v)
app_db.session.commit()
return jsonify({
'data': CitiesResponseSchema().dump(city)
}), 200
Мы определяем параметры decorator вне функции __init_(). Они являются константами и не занимают много места. Вышеизложенное можно оптимизировать и сделать лучше, но, по крайней мере, это легко читается и поддерживается.
Открыть API безопасность
Выше я показал, как я использовал flask_jwt_extended для защиты конечных точек. Теперь мы должны сообщить OpenAPI , что мы сделали. Я выбрал защиту всех конечных точек по умолчанию в OpenAPI и позже удалил эту защиту в docstrings незащищенных методов.
# create apispec
spec = APISpec(
title=title,
version=spec_version,
openapi_version=openapi_version,
plugins=[FlaskPlugin(), MarshmallowPlugin()],
**settings
)
security_scheme_bearer = {
"type": "http",
"description": "Enter JWT Bearer token",
"scheme": "bearer",
"bearerFormat": "JWT"
}
security_scheme_basic_auth = {
"type": "http",
"scheme": "basic"
}
spec.components.security_scheme("BearerAuth", security_scheme_bearer)
spec.components.security_scheme("BasicAuth", security_scheme_basic_auth)
В docstrings методов, которые защищены, мы ничего не делаем. В docstrings методов, которые не защищены, я добавляю:
security: []
Тестирование
Тестирование API , документированное OpenAPI , является ... сложным. У нас есть фактический интерфейс и документированный (OpenAPI) интерфейс. Мы должны написать тестовый набор хотя бы для документированного интерфейса. Я этого ещё не делал, но вместо этого использовал Pytest и пакет запросов для проведения функционального тестирования API. Некоторые тесты я начал выполнять с postman.
Резюме .
Кодирование моего API с помощью пакета Marshmallow было приятным опытом. Отправка последовательных (ошибочных) сообщений была непростой задачей, потому что каждый пакет имеет свой собственный формат по умолчанию. А документирование моего API OpenAPI (Swagger) Python было похоже на кошмар. Flask MethodView и Blueprints не работали как ожидалось с apispec, неясная документация в apispec-webframeworks.
Пришлось учиться новым вещам, например, как вручную писать строки OpenAPI . Я доволен конечным результатом и точно знаю, как он работает, а это значит, что его легко улучшать и вносить изменения.
Должны быть способы и получше. Я читал о FastAPI, это Python framework , посвященная сборке быстрых APIs. Она имеет гораздо лучшую производительность по сравнению с Flask и, похоже, поддерживает многие вещи "из коробки". Может быть, в следующий раз я попробую это сделать или преобразую API в FastAPI. Но у него также есть 540 открытых выпусков на Github ... может быть хорошим, может быть плохим.
Ссылки / кредиты
apispec
https://apispec.readthedocs.io/en/latest/
Authentication and Authorization (OAS3)
https://swagger.io/docs/specification/authentication/
FastAPI - The Good, the bad and the ugly
https://dev.to/fuadrafid/fastapi-the-good-the-bad-and-the-ugly-20ob
Flask-JWT-Extended’s Documentation
https://flask-jwt-extended.readthedocs.io/en/stable/
Only single path added for Flask API MethodViews #14
https://github.com/marshmallow-code/apispec-webframeworks/issues/14
Pluggable Views
https://flask.palletsprojects.com/en/1.1.x/views/
Postman | The Collaboration Platform for API Development
https://www.postman.com
REST API Development with Flask
https://www.datascienceblog.net/post/programming/flask-api-development/
Оставить комментарий
Комментируйте анонимно или войдите в систему, чтобы прокомментировать.
Комментарии (1)
Оставьте ответ
Ответьте анонимно или войдите в систему, чтобы ответить.
Why can't a MethodView be used? I beleive there is an error in your assessment:
list GET /cities
create POST /cities
get_by_id GET /cities/<int:city_id>
update_by_id PUT /cities/<int:city_id>
delete_by_id DELETE /cities/<int:city_id>
You hadnt made use of DELETE and PUT - so docstrings can be preserved!
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу