angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Документирование Flask RESTful API с OpenAPI (Swagger) с использованием APISpec

Создайте свой собственный view class и функции утилиты для сокращения кода и избежания ошибок.

22 апреля 2021
В API, Flask
post main image
https://unsplash.com/@sincerelymedia

Когда вы создаете 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/

Подробнее

API Flask

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии (1)

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.

avatar

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!