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

Dokumentieren einer Flask RESTful API mit OpenAPI (Swagger) mit APISpec

Erstellen Sie Ihre eigenen view class - und Utility-Funktionen, um Code zu reduzieren und Fehler zu vermeiden.

22 April 2021
In API, Flask
post main image
https://unsplash.com/@sincerelymedia

Wenn Sie eine API erstellen, möchten Sie diese dokumentieren und heute ist es naheliegend, dafür OpenAPI zu verwenden. Ich verwende bereits das Paket Marshmallow . Die gleichen Leute haben auch das Paket APIspec entwickelt, mit einem Flask Plugin in einem weiteren Paket apispec-webframeworks. Zur Darstellung der OpenAPI -Dokumentation verwende ich das Paket flask-swagger-ui und zum Schutz der Endpunkte wird das Paket flask-jwt-extended verwendet.

Im Folgenden zeige ich Ihnen eine Möglichkeit, wie Sie zusammen mit API view class eigene Hilfsfunktionen erstellen können, um den Code zu reduzieren und Fehler zu vermeiden.

Schnellstart am Beispiel

Es gibt sehr viele Informationen im Internet über den Aufbau einer API mit Flask und Swagger. Ich war froh, ein nettes kleines Beispiel für den Anfang zu finden, 'REST API Development with Flask', siehe Links unten. Was hier passiert, ist:

  • Erstellen Sie Ihre grundlegende OpenAPI -Spezifikation
  • Dokumentieren Sie Ihre Endpunkte mit docstrings
  • Scannen Sie alle View-Funktionen und fügen Sie sie zu Ihrer Spezifikation hinzu

Erzeugen von OpenAPI -Dokumentation durch Scannen von docstrings

Ist es eine gute Idee, docstrings zu verwenden, um Ihr OpenAPI -Dokument zu generieren? Docstrings sind an eine Funktion oder Methode gebunden. Ich habe diesbezüglich gemischte Gefühle. Wenn Sie viele Funktionen oder Methoden in verschiedenen Dateien haben, kann es sehr zeitaufwendig sein, Änderungen an Ihrer docstrings vorzunehmen.

Die Alternative wäre, ein eigenständiges OpenAPI -Dokument zu erstellen. Hier befinden sich alle Informationen an einem Ort, meist in einem einzigen Dokument. Sie erstellen zunächst die Spezifikationen und verwenden dieses Dokument, um Ihre Funktionen und Methoden zu erstellen. Mit der richtigen Software können Sie wahrscheinlich große Teile des Codes automatisch generieren. Das macht es auch sehr einfach, Änderungen vorzunehmen.

Obwohl letzteres der bevorzugte Weg ist, verwende ich hier die Scan-Methode docstring .

Problem: docstring -Duplizierung

Meistens haben wir eine Reihe von Modellklassen, die mehr oder weniger gleich behandelt werden. Ich habe eine Demo-Anwendung erstellt, die aus den folgenden Klassen besteht:

  • Friend, kann null oder eine Stadt und null oder mehrere Hobbys haben
  • City
  • Hobby
  • User

Die OpenAPI docstrings für diese Klassen sind mehr oder weniger die gleichen. Zum Beispiel sind die Update-Methoden für die City und 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>

und

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>

Beachten Sie, dass diese Methoden mit dem @jwt_required() decorator dekoriert sind. Ich nehme an, dass Sie dies auch (irgendwann) tun wollen.

Um Doppelarbeit zu vermeiden, habe ich eine Basis APIModelViewClass und einige Hilfsfunktionen in APIModelViewClassUtils erstellt.

Warum Sie Flask nicht verwenden können MethodView

Ich habe mir Flask MethodView angesehen, da die Dokumentation besagt, dass sie beim Aufbau von APIs nützlich ist. Diese Klasse hat Methoden wie get, post, die abhängig von der Request-Methode aufgerufen werden. Das bedeutet, dass die Anforderungsmethode GET die get-Methode der Klasse MethodView aufruft.

Für eine City muss meine API die folgenden Funktionen haben:

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>

Wir verwenden die Methode GET sowohl für eine Liste von Städten als auch für eine einzelne Stadt. Die Flask -Dokumentation schlägt vor, dass wir innerhalb der get-Methode entscheiden, welche Funktion wir ausführen müssen:

    def get(self, city_id):
        if city_id is  None:
            # return a list of citys
            pass
        else:
            # expose a single city
            pass

Nun können wir aber keine docstring mehr hinzufügen ... :-(

Ein Ausweg ist, zwei MethodView -Klassen zu verwenden. Eine für die List-Funktion und die Create-Funktionen und eine für die 'by_id'-Funktionen. Aber auch bei dieser Lösung gibt es Probleme. Es scheint, dass bei Verwendung von Blueprints nicht alle Funktionen von apispec hinzugefügt werden, siehe auch 'Only single path added for Flask API MethodViews #14'.

Ich bin mir nicht sicher, wie der Stand der Dinge ist, deshalb habe ich mich entschlossen, meine eigene 'MethodView', genannt APIModelViewClass, und meine eigene Utility-Klasse APIModelViewClassUtils zu erstellen.

Verwendung einer decorator zum Hinzufügen der docstrings

Die offensichtliche Wahl ist die Verwendung eines decorator , um die OpenAPI docstring zu den API Methoden hinzuzufügen. Sobald sie dort sind, können sie von apispec gescannt und geladen werden.

Viele meiner API -Methoden sind bereits mit der @jwt_required() decorator dekoriert, so dass wir diese decorators kombinieren müssen.

Ich habe eine Klasse APIModelViewClass erstellt, die im Moment nur ein Stub ist, und eine Klasse APIModelViewClassUtils
mit Utility-Funktionen, die im Moment drei Methoden enthält:

  • decorate_with_docstring()
  • endpoint_decorator()
  • endpoints_register()

Später kann ich diese Klassen zusammenführen. Unten ist ein Teil des Codes.

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

In decorate_with_docstring() schmücken wir die Methode mit einem formatierten docstring. In endpoint_decorator() dekorieren wir zunächst die Methode und prüfen dann mit verify_jwt_in_request(), ob der Zugriff gewährt wird. In endpoints_register registrieren wir die von dieser Klasse verwendeten Endpunkte.

Nun sieht (ein Teil) der CityAPI so aus:

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

Wir definieren die Parameter für den decorator außerhalb der Funktion __init__(). Sie sind Konstanten und nehmen nicht viel Platz ein. Das obige kann optimiert und besser gemacht werden, aber zumindest ist es einfach zu lesen und zu pflegen.

Öffnen Sie API security

Oben habe ich gezeigt, wie ich flask_jwt_extended verwendet habe, um Endpunkte zu schützen. Jetzt müssen wir OpenAPI mitteilen, was wir getan haben. Ich habe mich dafür entschieden, alle Endpunkte standardmäßig in OpenAPI zu schützen und diesen Schutz später in der docstrings der ungeschützten Methoden zu entfernen.

# 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)

In der docstrings von Methoden, die geschützt sind, machen wir nichts. In der docstrings von Methoden, die nicht geschützt sind, füge ich hinzu:

    security: []

Testen

Das Testen einer API , die mit OpenAPI dokumentiert ist, ist ... schwierig. Wir haben die eigentliche Schnittstelle und die dokumentierte (OpenAPI) Schnittstelle. Zumindest für die dokumentierte Schnittstelle sollten wir eine Testsuite schreiben. Ich habe das noch nicht getan, sondern stattdessen Pytest und das Requests-Paket verwendet, um funktionale Tests für die API durchzuführen. Ich habe begonnen, einige Tests mit postman durchzuführen.

Zusammenfassung

Die Codierung meines API unter Verwendung des Marshmallow -Pakets war eine schöne Erfahrung. Das Versenden von konsistenten (Fehler-)Meldungen war eine Herausforderung, da jedes Paket sein eigenes Standardformat hat. Und das Dokumentieren meines API mit OpenAPI (Swagger) mit Python war wie ein Alptraum. Flask ( MethodView ) und Blueprints ( apispec) funktionierten nicht wie erwartet, unklare Dokumentation in apispec-webframeworks.

Ich musste neue Dinge lernen, z. B. wie man OpenAPI -Zeilen von Hand schreibt. Ich bin mit dem Endergebnis zufrieden und weiß genau, wie es funktioniert, was bedeutet, dass es einfach ist, es zu verbessern und Änderungen vorzunehmen.

Es muss doch bessere Möglichkeiten geben, dies zu tun. Ich habe über FastAPI gelesen, das ist ein Python , der für den Bau von schnellen APIs gedacht ist. Es hat eine viel bessere Performance im Vergleich zu Flask und scheint viele Dinge out-of-the-box zu unterstützen. Vielleicht werde ich das beim nächsten Mal ausprobieren, oder diesen API in FastAPI umwandeln. Aber es hat auch 540 Open Issues auf Github ... kann gut sein, kann schlecht sein.

Links / Impressum

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/

Mehr erfahren

API Flask

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare (1)

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.

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!