Documentation d'une Flask RESTful API avec OpenAPI (Swagger) utilisant APISpec
Créez vos propres fonctions view class et utilitaires pour réduire le code et éviter les erreurs.
Lorsque vous créez une API, vous voulez la documenter et aujourd'hui il est évident d'utiliser la OpenAPI pour cela. J'utilise déjà le paquet Marshmallow . Les mêmes personnes ont également développé le paquet APIspec, avec un plugin Flask dans un paquet supplémentaire apispec-webframeworks. Pour présenter la documentation OpenAPI , j'utilise le paquet flask-swagger-ui et le paquet flask-jwt-extended est utilisé pour protéger les terminaux.
Je vous montre ci-dessous une façon de créer votre propre API view class avec les fonctions utilitaires API view class pour réduire le code et éviter les erreurs.
Démarrage rapide par l'exemple
Il y a beaucoup d'informations sur internet sur la construction d'un API avec Flask et Swagger. J'ai été heureux de trouver un petit exemple pour commencer, 'REST API Development with Flask', voir les liens ci-dessous. Ce qui se passe ici est :
- Configurer votre spécification OpenAPI de base.
- Documenter vos points de terminaison en utilisant docstrings
- Analysez toutes les fonctions de vue et ajoutez-les à votre spécification.
Génération de la documentation OpenAPI en analysant docstrings
Est-ce une bonne idée d'utiliser docstrings pour générer votre document OpenAPI ? Les docstrings sont liés à une fonction ou à une méthode. J'ai des sentiments mitigés à ce sujet. Lorsque vous avez de nombreuses fonctions ou méthodes dans différents fichiers, apporter des modifications à votre docstrings peut prendre beaucoup de temps.
L'alternative serait de créer un document OpenAPI autonome. Dans ce cas, toutes les informations se trouvent au même endroit, la plupart du temps, dans un seul document. Vous créez d'abord les spécifications et utilisez ce document pour créer vos fonctions et méthodes. Avec le logiciel approprié, vous pouvez probablement générer automatiquement de grandes parties du code. Cela permet également d'apporter très facilement des modifications.
Bien que cette dernière soit la méthode préférée, j'utilise ici la méthode de numérisation docstring .
Problème : Duplication de docstring
La plupart du temps, nous avons un certain nombre de classes de modèles qui sont traitées plus ou moins de la même manière. J'ai créé une application de démonstration composée des classes suivantes :
- Friend, peut avoir zéro ou une ville, et zéro ou plusieurs hobbies
- City
- Hobby
- User
Les OpenAPI docstrings de ces classes sont plus ou moins les mêmes. Par exemple, les méthodes de mise à jour pour les City et Hobby sont :
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>
et
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>
Notez que ces méthodes sont décorées avec le @jwt_required() decorator. Je suppose que vous voulez aussi faire cela (à un moment donné).
Pour éviter les doublons, j'ai créé une base APIModelViewClass et quelques fonctions utilitaires dans APIModelViewClassUtils.
Pourquoi vous ne pouvez pas utiliser Flask MethodView
J'ai examiné la classe Flask MethodView car la documentation indique qu'elle est utile pour construire APIs. Cette classe possède des méthodes comme get, post qui sont appelées en fonction de la méthode de requête. Cela signifie que la méthode de requête GET appelle la méthode get de la classe MethodView .
Pour une City, ma API doit avoir les fonctions suivantes :
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>
Nous utilisons la méthode GET à la fois pour une liste de villes et pour une seule ville. La documentation de la méthode Flask suggère que nous décidions dans la méthode get quelle fonction nous devons exécuter :
def get(self, city_id):
if city_id is None:
# return a list of citys
pass
else:
# expose a single city
pass
Mais maintenant, nous ne pouvons plus ajouter une docstring ... :-(
Une solution consiste à utiliser deux classes MethodView . Une pour les fonctions list et create et une pour les fonctions 'by_id'. Mais même avec cette solution, il y a des problèmes. Il semble que lorsque vous utilisez Blueprints, toutes les fonctions ne sont pas ajoutées par apispec, voir aussi 'Only single path added for Flask API MethodViews #14'.
Je ne suis pas sûr de l'état d'avancement de ce projet pour le moment, j'ai donc décidé de créer ma propre 'MethodView', appelée APIModelViewClass, et ma propre classe d'utilitaires APIModelViewClassUtils.
Utilisation d'une decorator pour ajouter la docstrings
Le choix évident est d'utiliser un decorator pour ajouter les méthodes OpenAPI docstring aux méthodes API . Une fois qu'ils y sont, ils peuvent être scannés et chargés par apispec.
Beaucoup de mes méthodes API sont déjà décorées avec la @jwt_required() decorator, ce qui signifie que nous devons combiner ces decorators.
J'ai créé une classe APIModelViewClass, qui n'est pour le moment qu'un stub, et une classe APIModelViewClassUtils
contenant des fonctions utilitaires, contenant pour le moment trois méthodes :
- decorate_with_docstring()
- endpoint_decorator()
- endpoints_register()
Plus tard, je pourrais fusionner ces classes. Voici une partie du code.
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
Dans decorate_with_docstring() nous décorons la méthode avec un format docstring. Dans endpoint_decorator() , nous décorons d'abord la méthode, puis nous vérifions si l'accès est accordé en utilisant verify_jwt_in_request(). Dans endpoints_register, nous enregistrons les points de terminaison utilisés par cette classe.
Maintenant (une partie) de la CityAPI ressemble :
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
Nous définissons les paramètres de la decorator en dehors de la fonction __init__(). Ce sont des constantes et ils ne prennent pas beaucoup de place. Ce qui précède peut être optimisé et mieux fait, mais au moins il est facile à lire et à maintenir.
Ouvrir la sécurité API
Ci-dessus j'ai montré comment j'ai utilisé flask_jwt_extended pour protéger les points de terminaison. Maintenant nous devons dire à OpenAPI ce que nous avons fait. J'ai choisi de protéger tous les points de terminaison par défaut dans OpenAPI et de supprimer plus tard cette protection dans docstrings des méthodes non protégées.
# 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)
Dans la docstrings des méthodes qui sont protégées, nous ne faisons rien. Dans la docstrings des méthodes qui ne sont pas protégées, j'ajoute :
security: []
Tester
Tester une API documentée avec OpenAPI est ... difficile. Nous avons l'interface réelle et l'interface documentée (OpenAPI). Nous devrions écrire une suite de tests au moins pour l'interface documentée. Je ne l'ai pas encore fait mais j'ai plutôt utilisé Pytest et le paquet de demandes pour faire des tests fonctionnels de API. J'ai commencé à faire quelques tests avec postman.
Résumé
Le codage de ma API en utilisant le paquet Marshmallow a été une bonne expérience. L'envoi de messages (d'erreur) cohérents a été un défi car chaque paquet a son propre format par défaut. Et documenter mon API avec OpenAPI (Swagger) avec Python était un vrai cauchemar. Q4_8291_TNEMECALPER_4 MethodView et Blueprints ne fonctionnant pas comme prévu avec apispec, documentation peu claire dans apispec-webframeworks.
J'ai dû apprendre de nouvelles choses, comme comment écrire les lignes de OpenAPI à la main. Je suis satisfait du résultat final et je sais exactement comment il fonctionne, ce qui signifie qu'il est facile de l'améliorer et d'y apporter des modifications.
Il doit y avoir de meilleures façons de procéder. J'ai lu des articles sur FastAPI, c'est un Python framework dédié à la construction de APIs rapides. Ses performances sont bien meilleures que celles de Flask et il semble prendre en charge de nombreux éléments dès la sortie de la boîte. Peut-être que j'essaierai la prochaine fois, ou que je convertirai cette API en FastAPI. Mais il a aussi 540 problèmes ouverts sur Github ... cela peut être bon ou mauvais.
Liens / crédits
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/
Laissez un commentaire
Commentez anonymement ou connectez-vous pour commenter.
Commentaires (1)
Laissez une réponse
Répondez de manière anonyme ou connectez-vous pour répondre.
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!
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow