logo le blog invivoo blanc

Designer des APIs Rest avec Flask-RESTPlus

17 juin 2019 | Python | 0 comments

Flask est un microservice web qui permet entre autres d’implémenter des API REST. Mais lorsqu’il s’agit de les documenter, visualiser, contrôler et valider les schémas de données d’entrées et de sorties, Flask atteint vite ses limites.

C’est ici qu’intervient son extension Flask-RESTplus qui – comme son nom l’indique – est dédiée au design des api REST.


Flask-RESTPlus prend en charge la création rapide d’API REST. Il encourage les meilleures pratiques avec une configuration minimale. Et fournit également une collection de décorateurs et d’outils pour décrire votre API et afficher correctement sa documentation (à l’aide de Swagger).

Le but de cet article que je déroulerai comme un tuto est d’implémenter une API REST avec Flask-RESTPlus. Cette api sera connectée à une base de données mongodb, et nous permettra de récupérer, ajouter, modifier et supprimer des données.

Tout au long de ce tuto, nous verrons les points forts de Flask-RESTPlus :

  • Structuration
  • Documentation
  • Validation des données d’entrées et des réponses

Cette api permettra d’ajouter, récupérer et supprimer des données dans notre base mongodb. Ces données décriront des livres. Un livre est défini par :

  • Titre : titre du livre
  • Auteur : auteur du livre
  • Langue : français ou anglais

1. Configuration et installation

Pour le tuto de cet article, j’ai utilisé Python 3. Nous avons besoin d’installer Flask et Flask-RestPlus et désigner notre api.

pip install Flask
pip install Flask-RestPlus

Nous installons également pymongo pour gérer notre base mongodb.

pip install pymongo

2. Création base mongodb

import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")

mydb = myclient["tuto"]

Notre api effectuera des opérations sur cette base mydb.

3. Implémentation

Avant de passer à la pratique, listons les opérations qu’un utilisateur pourra effectuer sur notre base via notre api.

Il pourra :

  • Récupérer la liste de tous les livres présents en base,
  • Ajouter un nouveau livre,
  • Récupérer un livre à partir de son titre,
  • Supprimer un livre.

Il y a évidemment plusieurs autres opérations possibles, mais pour le reste de l’article je me concentrerai sur ces fonctionnalités.

Nous commençons donc par créer une application Flask qui portera notre API REST.

from flask import Flask, request
from flask_restplus import Api, Resource

app = Flask(__name__)

api = Api(app=app, version='0.1', title='Books Api', description='', validate=True)

Nous passons maintenant à l’implémentation du contenu de notre api. N’ayez pas peur si vous ne comprenez pas tout de suite le code ci-dessous, les explications vont suivre.

@api.route("/books/")
class BooksList(Resource):
    def get(self):
        """
        returns a list of books
        """

        cursor = mydb.books.find({}, {"_id": 0})
        data = []
        for book in cursor:
            data.append(book)

        return {"response": data}

    def post(self):
        """
        Add a new book to the list
        """

        data = request.get_json()
        if not data:
            data = {"response": "ERROR"}
            return data, 404
        else:
            title = data.get('title')
            if title:
                if mydb.books.find_one({"title": title}):
                    return {"response": "book already exists."}, 403
                else:
                    mydb.insert(data)


@api.route("/books/<string:title>")
class Book(Resource):

    def put(self, title):
        """
        Edits a selected book
        """

        data = request.get_json()

        mydb.books.update({'title': title}, {'set': data})

    def delete(self, title):
        """
    delete a selected book
    """

        mydb.books.delete({'title': title})

1. Resource

Première particularité de RESTPlus, ce sont les classes « Resource ». Elles permettent de regrouper les méthodes http par type de données.

Quand vous créez une api avec Flask-RESTPlus, toutes les méthodes http doivent être déclarées dans des classes Resource.

Et si vous avez besoin d’implémenter plusieurs méthode GET pour votre API. Il vous suffit de créer plusieurs classes Resource et de placer chaque méthode dans la classe ressource correspondante.

Le décorateur @api.route() est utilisé pour spécifier les URL qui seront associées à une ressource donnée.

Vous pouvez spécifier les paramètres de chemin à l’aide de crochets, comme ici :

@api.route (‘/ <int: id>’)

Vous pouvez éventuellement spécifier le type de paramètre à l’aide du nom d’un convertisseur et de deux points. Les convertisseurs disponibles sont :

  • string: (valeur par défaut)
  • path:
  • int :
  • float:
  • uuid :

2. Documentation swagger

Voyons à quoi ressemblera notre API. Nous lançons notre application Flask :

app.run(port= 8887, host= ‘localhost’)

D’autres options sont à paramétrer dans la fonction run, mais pour ce tuto je ne vais pas m’attarder dessus. Nous allons sur le lien http:/localhost :8888/ et nous obtenons l’interface Swagger suivante :

Cette interface vous permet de visualiser votre API :

  • Titre, version, description
  • Différentes méthodes et leurs routes
  • Paramètres des méthodes et leurs types

Elle vous permet également de tester vos méthodes. Autre avantage, si vous avez ajouté une docstring dans une de vos méthodes, elle sera automatiquement affichée dans l’interface Swagger.

3. Namespace

Notre api contient uniquement quatre méthodes. Nous pouvons donc facilement retrouver la méthode que nous souhaitons tester dans l’interface Swagger.

Imaginons que dans la même api, nous voulons effectuer des opérations sur une base de livres mais également une base de films. Il est alors plus clair et plus lisible de les séparer en deux sections différentes : une section pour les opérations sur les livres et une autre pour les opérations sur les films.

Ceci est facilement réalisable grâce aux namespaces.

Les namespaces, autre option de Flask-RESTPlus, permettent d’organiser notre api en regroupant des ressources connexes sous une racine commune.

Chaque section est alors portée par son namespace.

On peut aussi ajouter une description à chaque namespace qui sera affichée dans l’interface Swagger.

app = Flask(__name__)
api = Api(app=app)

ns_books = api.namespace('books', description = "Books operations")
ns_movies = api.namespace('movies', description = "Movies operations")

@ns_books.route("/")
class BooksList(Resource):
    def get(self):
        """
        returns a list of books
        """

    def post(self):
        """
        Add a new book to the list
        """

@ns_books.route("/<string:title>")
class Book(Resource):

    def put(self, title):
        """
        Edits a selected book
        """


    def delete(self, title):
        """
    delete a selected book
    """

@ns_movies.route("/")
class MoviesList(Resource):
    def get(self):
        """
        returns a list of movies
        """

    def post(self):
        """
        Add a new movie to the list
        """

@ns_movies.route("/<string:title>")
class Movie(Resource):

    def put(self, title):
        """
        Edits a selected movie
        """

    def delete(self, title):
        """
    delete a selected movie
    """

Pour amener certaines ressources dans un espace de nom donné, il vous suffit de remplacer @api par @ns_books.

La fonction api.namespace() crée un nouvel espace de nom avec un préfixe d’URL. Le champ de description sera affiché dans l’interface utilisateur Swagger pour décrire cet ensemble de méthodes.

Les namespaces permettent également de mieux organiser notre code : nous pouvons créer un fichier par namespace avec les classes Resource requises, et ensuite ajouter ces namespaces à notre api via la méthode add_namespace.

4. @api.response()

Vous pouvez utiliser le décorateur @api.response() pour répertorier les codes de statut HTTP que chaque méthode est censée renvoyer.

@ns_books.route("/")
class BooksList(Resource):
    @api.response(200, 'Flask RESTPlus tuto : Success')
    @api.response(400, 'Flask RESTPlus tuto: Validation error')
    def get(self):
        """
        returns a list of books
        """

        cursor = mydb.books.find({}, {"_id": 0})
        data = []
        for book in cursor:
            data.append(book)

        return {"response": data}, 200

5. Validation

Certaines des méthodes http implémentées ont des paramètres d’entrées et/ou des valeurs de sorties qui seront peut-être utilisées à leur tour comme paramètres d’entrée.

Comment pouvons-nous valider le format de ces inputs / outputs ?

On peut se lancer dans des contrôles à base de if dans le corps de nos méthodes, mais il existe beaucoup plus simple.

Flask-RESTPlus fournit tous les outils nécessaires pour facilement valider les paramètres d’entrée et les données de sortie.

a. Validation des paramètres d’entrées pour les méthodes “GET”

Afin de définir ces paramètres, nous utilisons un objet appelé RequestParser. Cet objet a une fonction nommée add_argument(), qui nous permet de spécifier le nom du paramètre et ses valeurs autorisées.

Une fois défini, nous pouvons utiliser le décorateur @api.expect pour attacher cet objet à une méthode. Une fois qu’elle est décorée, l’interface utilisateur Swagger de la méthode affiche un formulaire pour spécifier les valeurs des arguments.

Le RequestParser permet de valider les valeurs d’arguments. Si une validation de valeur échoue, l’api renvoie une erreur HTTP 400 avec un message approprié.

À savoir que vous pouvez activer ou désactiver la validation d’argument pour chaque méthode à l’aide de l’argument validate de la méthode @api.expect.

Vous pouvez également activer la validation à l’aide de la variable de configuration RESTPLUS_VALIDATE lors du démarrage de votre application Flask.

app.config(‘RESTPLUS_VALIDATE’)=True

Pour créer un argument qui accepte plusieurs valeurs, utilisez le mot clé action et indiquez “append” comme valeur :

parser.add_argument('multiple', type=int, action='append', required=True)

Pour spécifier une liste de valeurs d’arguments valides, utilisez le mot-clé choices et spécifiez un itérateur :

parser.add_argument(“language”, choices = [‘en, ‘fr’])

Reprenons notre api :

books_arguments = reqparse.RequestParser()
books_arguments.add_argument('author', type=str, required=True)
books_arguments.add_argument('language', type=str, choices=['en', 'fr'])


@ns_books.route("/")
class BooksList(Resource):
    @api.expect(books_arguments)
    def get(self):
        """
        returns a list of books
        """
        data = books_arguments.parse_args(request)

        if data.get('language'):
            cursor = mydb.books.find({'author': data.get('author'), 'language': data.get('language')}, {"_id": 0})
        else:
            cursor = mydb.books.find({'author': data.get('author')}, {"_id": 0})
        data = []
        for book in cursor:
            data.append(book)

        return {"response": data}

b. Validation des paramètres d’entrées pour les méthodes “POST” et “PUT”

Si vous souhaitez mettre à jour ou créer un nouveau livre, vous devez envoyer les données de l’élément sérialisées au format JSON dans le corps d’une requête.

Flask-RESTPlus vous permet de documenter et de valider automatiquement le format des objets JSON entrants à l’aide de modèles d’API.

Un modèle d’API RESTPlus définit le format d’un objet en répertoriant tous les champs attendus. Chaque champ a un type associé (par exemple, String, Integer, DateTime).

Définissons le modèle correspondant aux informations d’un livre :

from flask_restplus import fields

book_definition = api.model('Book Informations', {
    'author': fields.String(required=True),
    'title': fields.String(required=True),
    'language': fields.String(required=True)
})

Une fois le modèle défini, vous pouvez l’associer à une méthode à l’aide du décorateur @api.expect().

@ns_books.route("/")
class BooksList(Resource):
    @api.expect(books_arguments)
    def get(self):
        """
        returns a list of books
        """

    @api.expect(book_definition)
    def post(self):
        """
        Add a new book to the list
        """
        data = request.get_json()
        title = data.get('title')
        if title:
            if mydb.books.find_one({"title": title}):
                return {"response": "book already exists."}
            else:
                mydb.books.insert_one(data)

Nous les remarquons tout de suite, utiliser un modèle simplifie amplement le corps de la méthode POST en nous épargnant tous les tests sur les données reçues.

Options supplémentaires pour les fields :

  • required : True si le champs est obligatoire
  • default : valeur par défaut pour le champ
  • description : description du champ (apparaîtra dans l’interface utilisateur Swagger)
  • example : valeur d’exemple optionnelle (apparaîtra dans l’interface utilisateur Swagger)

Des options de validation supplémentaires peuvent être ajoutées aux fields :

  • String:
    • min_length et max_length : longueur minimale et maximale d’une chaîne
    • pattern :  une expression régulière à laquelle le string doit correspondre
  • Nombre (entier, flottant, fixe, arbitraire) :
    • min et max : valeurs minimales et maximales
    • multiple : le nombre doit être un multiple de cette valeur

Un field d’un modèle d’api peut utiliser un autre modèle comme valeur attendue. Vous devez ensuite fournir un objet JSON en tant que valeur valide pour ce field.

items: fields.Nested (item)

Un champ peut également nécessiter une liste de valeurs voire une liste d’objets imbriqués.

‘item_ids’: fields.List (fields.Integer)

‘items’: fields.List (fields.Nested (item)

6. Validation des output JSON

Les modèles d’API peuvent également être utilisés pour les outputs JSON des méthodes.

Si vous décorez une méthode avec @api.marshal_with (modèleà, Flask-RESTPlus générera un objet JSON avec les mêmes champs que ceux spécifiés dans le modèle.

La méthode doit simplement renvoyer à un objet ayant des attributs portant les m^mes noms que les champs. Sinon, la méthode peut renvoyer à un dictionnaire avec des valeurs attribués aux mêmes clés que les noms des champs de modèle.

list_books = api.model('Books', {
    'books': fields.List(fields.Nested(book_definition))
})


@ns_books.route("/")
class BooksList(Resource):
    @api.marshal_with(list_books)
    def get(self):
        """
        returns a list of books
        """
        data = books_arguments.parse_args(request)

        if data.get('language'):
            cursor=mydb.books.find({'author':data.get('author'),'language':data.get('language')}, {"_id":0})
        else:
            cursor = mydb.books.find({'author': data.get('author')}, {"_id":0})
        data = []
        for book in cursor:
            data.append(book)

        return {"books": data}