Tutoriel sur Flask

imt

1. Objectif

Nous allons ici implémenter un micro-service Movie avec Flask.

Important
Merci de vous référer à la page sur la préparation et les installations nécessaires à cette UE avant de continuer.

Téléchargez le contenu du repository git suivant : https://github.com/IMTA-FIL/UE-AD-A1-REST

Le contenu de ce repository sera votre espace de travail pour ce tutoriel et votre TP sur REST. Il contient un répertoire par service à implémenter dans le TP, dont un répertoire pour le service Movie qui nous intéresse ici. Chaque répertoire de service contient les données json qui lui sont associées ainsi que la spécification OpenAPI.

Le repository, comme indiqué dans le guide d’installation, contient également un fichier requirements.txt racine utile pour installer les dépendances nécessaires (à savoir ici les bibliothèques Flask et requests). Il contient enfin les fichiers nécessaire à la construction de l’environnement Docker à savoir un fichier docker-compose.yaml et dans chaque répertoire un fichier requirements.txt et un Dockerfile.

2. Les bases de notre service Movie

2.1. Création et lecture des données JSON

Le code ci-dessous permet la lecture du fichier JSON databases/movie.json. L’objet movies récupéré est obtenu en lisant la clé "movies" et est donc un array.

from flask import Flask
import json

with open('{}/databases/movies.json'.format("."), "r") as jsf:
   movies = json.load(jsf)["movies"]

2.2. Création d’un point d’entrée

Nous allons créer un point d’entrée pour notre service. Ce point d’entrée se situe à la racine /, reçoit des requêtes HTTP de type GET et construit une réponse (au moyen de la méthode make_response) contenant une balise HTML.

from flask import Flask, make_response
import json

app = Flask(__name__)

PORT = 3200
HOST = '0.0.0.0'

with open('{}/databases/movies.json'.format("."), "r") as jsf:
   movies = json.load(jsf)["movies"]

@app.route('/', methods=['GET'])
def index():
    return make_response("<h1 style='color:blue'>Welcome to the Movie service!</h1>",200)

if __name__ == "__main__":
    app.run(host=HOST, port=PORT)

Exécuter ce code (placez vous dans le bon répertoire movie) avec au choix python movie.py (attention de bien utiliser python3 si vous avez plusieurs versions) ou pymon movie.py (pour éviter de devoir arrêter et relancer le service à chaque modification). Accéder à la page indiquée dans la sortie de l’exécution sur votre navigateur (http://127.0.0.1:3200).

2.3. Utilisation des templates dans Flask

Dans Flask il est possible d’utiliser des templates Jinja

L’idée de base est de créer un répertoire templates dans lequel seront placés des fichiers template. Ici nous allons créer un fichier templates/index.html avec ce contenu :

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Flask tutorial</title>
    </head>
    <body>
        <h1>{{ body_text }}</h1>
    </body>
</html>

Toute partie de texte présente entre {{ }} est une expression qui sera remplacée dans le document final. Notez qu’il existe aussi la balise {% %} qui permet d’ajouter des conditions et des boucles au template.

Créons donc un nouveau point d’entrée pour exploiter ce template en appelant la fonction render_template.

from flask import Flask, render_template, make_response
import json

...

@app.route("/template", methods=['GET'])
def template():
    return make_response(render_template('index.html', body_text='This is my HTML template for Movie service'),200)

Sauvegardez et (optionnellement si vous n’utilisez pas pymon) relancer le service. Accéder à la page URL/template dans votre navigateur pour observer le résultat.

3. Points d’entrée pour obtenir des données

Pour chacun de ces points d’entrée testez le résultat à chaque fois !

3.1. GET JSON en entier

Créons un point d’entrée retournant le fichier JSON entièrement. Pour cela nous utilisons la méthode jsonify qui permet de créer une réponse HTTP à partir d’un format JSON (see this link).

from flask import Flask, render_template, jsonify, make_response
import json

...

@app.route("/json", methods=['GET'])
def get_json():
    res = make_response(jsonify(movies), 200)
    return res

3.2. GET information d’un film à partir de son ID

Rappelons que l’objet movies est un array, on peut donc itérer sur ses éléments que nous appelons ici movie. Chaque objet movie est un dictionnaire. Nous vérifions pour chaque élément si la valeur associée à la clé "id" est l’ID recherché. Si tel est le cas le dictionnaire movie courant est retourné.

@app.route("/movies/<movieid>", methods=['GET'])
def get_movie_byid(movieid):
    for movie in movies:
        if str(movie["id"]) == str(movieid):
            res = make_response(jsonify(movie),200)
            return res
    return make_response(jsonify({"error":"Movie ID not found"}),400)

Vous voyez ici que l’ID est indiqué dans l’adresse directement. C’est la méthode la plus classique en REST pour donner une information à la requête. A noter que l’on peut complexifier l’adresse comme on le souhaite, par exemple si on souhaite donner plus d’éléments pour la requête /entry_point/<val1>/<val2>/<val3>.

3.3. Postman ou équivalent

Il est temps de tester votre service avec Postman ou un équivalent comme (ou votre solution préférée)

Vous pouvez dans ce type d’outils créer des collections de requêtes pour tester vos API REST (mais aussi on le verra les API GraphQL et gRPC). Vous pouvez aussi créer des documentations par exemple et d’autres fonctionnalités.

Installez l’un de ces outils et créez une requête pour tester le point d’entrée précédent. Sauvegardez là pour pouvoir facilement la réutiliser.

3.4. GET information à partir du titre avec un argument dans la requête

Dans ce point d’entrée nous allons voir comment utiliser des arguments dans une requête. Ici l’argument est une clé-valeur avec pour clé title et pour valeur le titre du film à chercher.

À noter que request.args retourne un werkzeug.MultiDict de Flask formé de cette façon [(key,value),(key,value),…​]. Toutes les informations sont ici

@app.route("/moviesbytitle", methods=['GET'])
def get_movie_bytitle():
    json = ""
    if request.args:
        req = request.args
        for movie in movies:
            if str(movie["title"]) == str(req["title"]):
                json = movie

    if not json:
        res = make_response(jsonify({"error":"movie title not found"}),400)
    else:
        res = make_response(jsonify(json),200)
    return res

Vous voyez ici que le titre ne fait plus partie de l’adresse d’accès du point d’entrée. Le titre sera donné comme un argument de la requête HTML. C’est une autre façon de procéder qui utilise plus les mécanismes HTTP mais moins la logique REST.

Pour faire une requête et tester ce point d’entrée il faut donc passer un paramètre. Cela est facile à faire avec Postman (ou ses équivalents). Si vous souhaitez tester le point d’entrée à la main dans votre navigateur il faudra adopter la notation suivante : URL/moviesbytitle&title=TITRE.

4. Points d’entrée pour modifier, ajouter et supprimer des données

4.1. POST ajouter un nouveau film

Nous créons ici un point d’entrée de l’API permettant l’ajout d’un nouveau film en base. Ce point d’entrée reçoit des requêtes de type POST. Nous avons ici aussi besoin d’utiliser request pour récupérer le JSON donné dans le corps de la requête. Ce JSON est de la forme :

{
  "title": "Test",
  "rating": 1.2,
  "director": "Someone",
  "id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"
}
@app.route("/addmovie/<movieid>", methods=['POST'])
def add_movie(movieid):
    req = request.get_json()

    for movie in movies:
        if str(movie["id"]) == str(movieid):
            return make_response(jsonify({"error":"movie ID already exists"}),409)

    movies.append(req)
    write(movies)
    res = make_response(jsonify({"message":"movie added"}),200)
    return res
def write(movies):
    with open('{}/databases/movies.json'.format("."), 'w') as f:
        json.dump(movies, f)

Pour tester une requête de type POST avec un body de requête, nous ne pouvons plus utiliser le navigateur web. Il est de toute façon préférable maintenant de passer complètement sur un outil comme Postman.

Warning
Attention de bien utiliser le bon type de requêtes dans Postman vous aurez sinon des erreurs !
Tip
Sous Postman pour utiliser du json en body il faut aller dans body→raw et sélectionner json dans la liste des types.
Tip
Le fichier json écrit est mal formaté. Les IDE permettent un formattage automatique. Par exemple sur VSCode c’est CTRL+k CTRL+f.

postman

4.2. PUT modifier la note d’un film

Ici nous ajoutons un point d’entrée pour une requête de type PUT permettant de modifier la note d’un film.

@app.route("/movies/<movieid>/<rate>", methods=['PUT'])
def update_movie_rating(movieid, rate):
    for movie in movies:
        if str(movie["id"]) == str(movieid):
            movie["rating"] = rate
            res = make_response(jsonify(movie),200)
            return res

    res = make_response(jsonify({"error":"movie ID not found"}),201)
    return res

On voit ici que le même point d’entrée peut être utilisé pour différents types de requêtes ! Ici on réutilise le point d’entrée movies.

4.3. DELETE un film

Ici nous supprimons un film.

@app.route("/movies/<movieid>", methods=['DELETE'])
def del_movie(movieid):
    for movie in movies:
        if str(movie["id"]) == str(movieid):
            movies.remove(movie)
            return make_response(jsonify(movie),200)

    res = make_response(jsonify({"error":"movie ID not found"}),400)
    return res