23 - Flask

Razik Ikhlef

Ouverture. La bibliothèque Flask n’est pas au programme de NSI, mais ce qu’elle met en œuvre l’est : le dialogue entre un client et un serveur, les requêtes HTTP, les méthodes GET et POST et les formulaires, vus dans le cours IHM Web . Avec Flask, c’est nous qui écrivons le serveur, en Python. Cette page suppose connus les cours sur le HTML et le CSS . Les programmes s’exécutent sur l’ordinateur, dans Thonny ou un autre éditeur, pas dans le navigateur.

Le principe : un serveur écrit en Python

Quand on visite une page, le navigateur (le client) envoie une requête HTTP à un serveur, par exemple GET /bonjour. Le serveur renvoie une réponse qui contient du HTML, que le navigateur affiche.

Le principe : un serveur écrit en Python (suite)

Jusqu’ici, nos pages HTML étaient des fichiers écrits à l’avance : le serveur renvoyait toujours le même contenu. Avec Flask, à chaque adresse du site correspond une fonction Python. Quand une requête arrive, Flask appelle la fonction de cette adresse, et ce qu’elle renvoie devient la page. La page peut donc changer à chaque visite : résultat d’un calcul, réponse à un formulaire, score d’un joueur.

Le principe : un serveur écrit en Python (suite)

Le principe : un serveur écrit en Python (suite)

Une page fabriquée au moment de la requête s’appelle une page dynamique ; un fichier HTML renvoyé tel quel, une page statique.

Installer Flask et lancer un premier site

Flask s’installe une fois sur l’ordinateur :

  • dans Thonny : menu Outils, puis Gérer les paquets, chercher flask et cliquer sur Installer ;
  • dans un terminal : pip install flask.

On crée un dossier pour le projet, et dans ce dossier un fichier app.py.

Installer Flask et lancer un premier site (suite)

Fichier app.py :

from flask import Flask

app = Flask(__name__)          # l'application, c'est-à-dire le site


@app.route("/")                # adresse de la page d'accueil
def accueil():
    return "<h1>Bonjour !</h1><p>Mon premier site avec Flask.</p>"


@app.route("/apropos")         # adresse /apropos
def apropos():
    return "<p>Site réalisé en première NSI.</p>"


if __name__ == "__main__":
    app.run(debug=True)        # démarrer le serveur

Installer Flask et lancer un premier site (suite)

On exécute app.py comme n’importe quel programme. Le terminal affiche :

 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Le message WARNING rappelle que ce serveur sert à développer, pas à publier un site.

Installer Flask et lancer un premier site (suite)

Le programme ne s’arrête pas : c’est un serveur, qui attend les requêtes. On ouvre alors un navigateur à l’adresse http://127.0.0.1:5000.

Installer Flask et lancer un premier site (suite)

Une page de navigateur à l'adresse 127.0.0.1:5000 qui affiche le titre « Bonjour ! » et la phrase « Mon premier site avec Flask. ».

La page d’accueil, servie par notre propre serveur.

Installer Flask et lancer un premier site (suite)

  • 127.0.0.1 est l’adresse IP de l’ordinateur lui-même (le nom localhost désigne cette même adresse) : le client et le serveur sont ici sur la même machine. 5000 est le port sur lequel le serveur écoute.
  • @app.route("/") est un décorateur : il associe l’adresse / à la fonction écrite juste en dessous. Une fonction ainsi associée à une adresse s’appelle une vue.

Installer Flask et lancer un premier site (suite)

  • La vue renvoie une chaîne : c’est le HTML de la page.
  • Avec debug=True, le serveur redémarre tout seul quand on enregistre app.py, et une erreur affiche une page d’explication dans le navigateur. Ce mode ne sert qu’au développement, sur son propre ordinateur.
  • Pour arrêter le serveur : Ctrl + C dans le terminal, ou le bouton Stop de Thonny.

Question. Que renvoie le serveur si l’on visite http://127.0.0.1:5000/contact ?

Réponse

Une page d’erreur « Not Found » avec le code HTTP 404 : aucune vue n’est associée à l’adresse /contact. Le terminal du serveur affiche la ligne "GET /contact HTTP/1.1" 404.

Le bloc if __name__ == "__main__":

La variable spéciale __name__ vaut "__main__" quand le fichier est exécuté, et le nom du module quand il est importé par un autre fichier. Le serveur ne démarre donc que si l’on lance app.py lui-même. C’est important dès qu’un projet a plusieurs fichiers : un fichier jeu.py importé par app.py ne doit contenir que des fonctions, et tout code de test doit être placé dans un tel bloc, sinon il s’exécute à chaque import.

Des adresses avec des paramètres

Une partie de l’adresse peut être variable : on l’écrit entre chevrons dans la route, et Flask la transmet à la vue comme paramètre.

Des adresses avec des paramètres (suite)

Fichier app.py :

from flask import Flask

app = Flask(__name__)


@app.route("/bonjour/<prenom>")
def bonjour(prenom):
    return f"<h1>Bonjour {prenom} !</h1>"


@app.route("/table/<int:n>")          # int: convertit en entier
def table(n):
    lignes = ""
    for i in range(1, 11):
        lignes = lignes + f"<li>{n} x {i} = {n * i}</li>"
    return f"<h1>Table de {n}</h1><ul>{lignes}</ul>"


if __name__ == "__main__":
    app.run(debug=True)

Des adresses avec des paramètres (suite)

Une page à l'adresse 127.0.0.1:5000/table/7 qui affiche le titre « Table de 7 » et la liste de 7 x 1 = 7 à 7 x 10 = 70.

L’adresse /table/7 : une seule vue pour toutes les tables.

Des adresses avec des paramètres (suite)

Adresse visitée Vue appelée Paramètre reçu
/bonjour/Lina bonjour prenom = "Lina" (une chaîne)
/table/7 table n = 7 (un entier)
/table/sept aucune : erreur 404 sept n’est pas un entier

Question. Sans int:, que renverrait la vue table pour l’adresse /table/7 ?

Réponse

Le paramètre serait la chaîne "7". Le calcul n * i répéterait alors la chaîne : la page afficherait par exemple 7 x 3 = 777. Le convertisseur int: fait la conversion, comme int().

Les gabarits : séparer le HTML du Python

Écrire du HTML dans des chaînes Python devient vite illisible. Flask range le HTML dans des fichiers à part, les gabarits (templates), placés dans un dossier nommé exactement templates, à côté de app.py. La fonction render_template remplit un gabarit avec des valeurs calculées en Python.

Les gabarits : séparer le HTML du Python (suite)

notes/
├── app.py
└── templates/
    └── notes.html

Les gabarits : séparer le HTML du Python (suite)

Fichier app.py :

from flask import Flask, render_template

app = Flask(__name__)


@app.route("/")
def notes():
    eleve = "Lina"
    resultats = {"Maths": 15, "NSI": 18, "Physique": 9, "Anglais": 13}
    moyenne = sum(resultats.values()) / len(resultats)
    return render_template("notes.html", eleve=eleve, resultats=resultats, moyenne=moyenne)


if __name__ == "__main__":
    app.run(debug=True)

Les gabarits : séparer le HTML du Python (suite)

Fichier templates/notes.html :

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>Notes de {{ eleve }}</title>
</head>
<body>
  <h1>Notes de {{ eleve }}</h1>
  <ul>
    {% for matiere, note in resultats.items() %}
      {% if note >= 10 %}
        <li>{{ matiere }} : {{ note }}</li>
      {% else %}
        <li style="color: crimson">{{ matiere }} : {{ note }} (à retravailler)</li>
      {% endif %}
    {% endfor %}
  </ul>
  <p>Moyenne : {{ moyenne | round(1) }}</p>
</body>
</html>

Les gabarits : séparer le HTML du Python (suite)

Une page « Notes de Lina » avec la liste Maths 15, NSI 18, Physique 9 en rouge suivi de « à retravailler », Anglais 13, puis « Moyenne : 13.8 ».

Le gabarit rempli : la note sous la moyenne est mise en évidence.

Les gabarits : séparer le HTML du Python (suite)

Le gabarit est du HTML, enrichi d’un petit langage appelé Jinja :

Écriture Rôle Exemple
{{ expression }} insérer une valeur {{ eleve }}, {{ note * 2 }}
{% for x in liste %} … {% endfor %} répéter un morceau de HTML une ligne <li> par matière
{% if condition %} … {% else %} … {% endif %} choisir un morceau de HTML couleur selon la note
{{ x | filtre }} transformer une valeur round(1), upper, length

Les gabarits : séparer le HTML du Python (suite)

Chaque variable utilisée dans le gabarit doit être transmise par render_template, sous la forme nom_dans_le_gabarit=valeur.

Question. Que contient la page si l’on écrit render_template("notes.html", eleve=eleve) ?

Réponse

Une erreur jinja2.exceptions.UndefinedError: 'resultats' is undefined : la variable resultats n’a pas été transmise, et le gabarit appelle sa méthode items(). Une variable absente simplement affichée, comme {{ eleve }} si on ne l’avait pas transmise, ne provoque pas d’erreur : elle donne un texte vide, ce qui rend l’oubli plus difficile à repérer. Passée à un filtre, comme {{ moyenne | round(1) }}, elle provoque en revanche une erreur.

Les gabarits : séparer le HTML du Python

Jinja protège automatiquement le texte inséré : si une variable contient <script>, il est affiché tel quel et non exécuté. Un utilisateur ne peut donc pas injecter de code dans la page en tapant du HTML dans un formulaire.

Une mise en page commune et des fichiers statiques

Toutes les pages d’un site partagent le même en-tête, le même menu, la même feuille de style. Plutôt que de les recopier dans chaque gabarit, on écrit un gabarit de base qui contient des blocs ; chaque page hérite de la base et ne remplit que ses blocs.

Les fichiers qui ne changent jamais (feuilles de style CSS, images, scripts JavaScript) sont les fichiers statiques : on les range dans un dossier nommé exactement static.

Une mise en page commune et des fichiers statiques (suite)

mon_site/
├── app.py
├── static/
│   └── style.css
└── templates/
    ├── base.html
    ├── accueil.html
    └── jeux.html

Une mise en page commune et des fichiers statiques (suite)

Fichier templates/base.html :

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>{% block titre %}Mon site{% endblock %}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <nav>
    <a href="{{ url_for('accueil') }}">Accueil</a>
    <a href="{{ url_for('jeux') }}">Jeux</a>
  </nav>
  <main>
    {% block contenu %}{% endblock %}
  </main>
</body>
</html>

Une mise en page commune et des fichiers statiques (suite)

Fichier templates/accueil.html :

{% extends "base.html" %}

{% block titre %}Accueil{% endblock %}

{% block contenu %}
  <h1>Bienvenue</h1>
  <p>Ce site rassemble nos projets de première NSI.</p>
{% endblock %}

Une mise en page commune et des fichiers statiques (suite)

Fichier templates/jeux.html :

{% extends "base.html" %}

{% block titre %}Jeux{% endblock %}

{% block contenu %}
  <h1>Nos jeux</h1>
  <ul>
    {% for jeu in jeux %}
      <li>{{ jeu }}</li>
    {% endfor %}
  </ul>
{% endblock %}

Une mise en page commune et des fichiers statiques (suite)

Fichier static/style.css :

body { font-family: Arial, sans-serif; margin: 0; background: #f4f6fb; color: #222; }
nav { background: #2a4d8f; padding: 12px 24px; }
nav a { color: white; margin-right: 18px; text-decoration: none; font-weight: bold; }
main { max-width: 640px; margin: 24px auto; padding: 0 16px; }
h1 { color: #2a4d8f; }

Une mise en page commune et des fichiers statiques (suite)

Fichier app.py :

from flask import Flask, render_template

app = Flask(__name__)


@app.route("/")
def accueil():
    return render_template("accueil.html")


@app.route("/jeux")
def jeux():
    return render_template("jeux.html", jeux=["Mots cachés", "Snake", "2048"])


if __name__ == "__main__":
    app.run(debug=True)

Une mise en page commune et des fichiers statiques (suite)

La page « Nos jeux » : une barre de menu bleue avec les liens Accueil et Jeux, puis le titre bleu « Nos jeux » et la liste Mots cachés, Snake, 2048.

La page /jeux : le menu et le style viennent de base.html.

Une mise en page commune et des fichiers statiques (suite)

  • {% extends "base.html" %} doit être la première instruction du gabarit. Ce qui est écrit après, en dehors des blocs {% block %}, est ignoré ; ce qui est écrit avant est recopié en tête de la page, avant même <!DOCTYPE html>, ce qui la rend incorrecte. Un <link> vers une feuille de style se place donc dans base.html.
  • url_for('static', filename='style.css') calcule l’adresse du fichier statique : /static/style.css.
  • url_for('jeux') calcule l’adresse de la vue jeux (on donne le nom de la fonction, pas l’adresse) : si l’on change un jour la route, tous les liens suivent.

Les formulaires

Un formulaire HTML envoie des données au serveur ; le cours IHM Web présente les deux méthodes, GET et POST. Dans Flask, l’objet request contient tout ce que la requête a apporté.

GET POST
Où sont les données ? dans l’adresse : /recherche?mot=chat dans le corps de la requête, invisibles dans l’adresse
Lecture dans Flask request.args.get("mot") request.form.get("mot")
Usage recherche, filtre : la page peut être mise en favori envoi qui modifie quelque chose : inscription, réponse, message

Un formulaire GET : une recherche

Fichier app.py :

from flask import Flask, render_template, request

app = Flask(__name__)

PAYS = {"France": "Paris", "Italie": "Rome", "Espagne": "Madrid",
        "Allemagne": "Berlin", "Portugal": "Lisbonne", "Japon": "Tokyo"}


@app.route("/capitales")
def capitales():
    mot = request.args.get("mot", "")          # "" si le paramètre est absent
    trouves = {}
    for pays in PAYS:
        if mot.lower() in pays.lower():
            trouves[pays] = PAYS[pays]
    return render_template("capitales.html", mot=mot, trouves=trouves)


if __name__ == "__main__":
    app.run(debug=True)

Un formulaire GET : une recherche (suite)

Fichier templates/capitales.html :

<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>Capitales</title></head>
<body>
  <h1>Chercher un pays</h1>
  <form action="/capitales" method="get">
    <input type="text" name="mot" value="{{ mot }}">
    <button type="submit">Chercher</button>
  </form>
  <ul>
    {% for pays, capitale in trouves.items() %}
      <li>{{ pays }} : {{ capitale }}</li>
    {% else %}
      <li>Aucun pays trouvé.</li>
    {% endfor %}
  </ul>
</body>
</html>

Un formulaire GET : une recherche (suite)

Une page à l'adresse 127.0.0.1:5000/capitales?mot=al, avec un champ contenant « al » et les résultats Italie : Rome, Allemagne : Berlin, Portugal : Lisbonne.

L’adresse contient la recherche : ?mot=al.

Un formulaire GET : une recherche (suite)

L’attribut name="mot" du champ donne le nom du paramètre ; c’est ce nom qu’on lit avec request.args.get("mot"). Le second argument de get est la valeur renvoyée quand le paramètre est absent, à la première visite. Dans le gabarit, {% else %} placé dans une boucle for s’affiche quand la boucle n’a rien à parcourir.

Question. Quelle adresse le navigateur demande-t-il si l’on tape pagne dans le champ et qu’on clique sur « Chercher » ? Quel résultat s’affiche ?

Réponse

/capitales?mot=pagne : avec la méthode GET, les données du formulaire sont ajoutées à l’adresse après un point d’interrogation. Le résultat est « Espagne : Madrid », seul pays dont le nom contient pagne.

Un formulaire POST : la même adresse pour afficher et pour recevoir

Une vue peut accepter les deux méthodes : GET pour afficher le formulaire vide, POST pour traiter ce qu’on a envoyé. On le précise avec methods=["GET", "POST"], et request.method indique la méthode de la requête en cours.

Un formulaire POST : la même adresse pour afficher et pour recevoir (suite)

Fichier app.py :

from flask import Flask, render_template, request

app = Flask(__name__)


@app.route("/imc", methods=["GET", "POST"])
def imc():
    message = ""
    if request.method == "POST":
        try:
            masse = float(request.form["masse"].replace(",", "."))
            taille = float(request.form["taille"].replace(",", "."))
            message = f"IMC : {masse / taille ** 2:.1f}"
        except (ValueError, ZeroDivisionError):
            message = "Saisir deux nombres, la taille n'étant pas nulle."
    return render_template("imc.html", message=message)


if __name__ == "__main__":
    app.run(debug=True)

Un formulaire POST : la même adresse pour afficher et pour recevoir (suite)

Fichier templates/imc.html :

<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>IMC</title></head>
<body>
  <h1>Indice de masse corporelle</h1>
  <form method="post">
    <p><label>Masse (kg) : <input type="text" name="masse"></label></p>
    <p><label>Taille (m) : <input type="text" name="taille"></label></p>
    <button type="submit">Calculer</button>
  </form>
  <p><strong>{{ message }}</strong></p>
</body>
</html>

Un formulaire POST : la même adresse pour afficher et pour recevoir (suite)

Une page « Indice de masse corporelle » avec deux champs, un bouton « Calculer » et le résultat en gras « IMC : 21.5 ».

Après l’envoi du formulaire avec 62 et 1,70.

Un formulaire POST : la même adresse pour afficher et pour recevoir (suite)

C’est le même calcul que dans la page Tkinter : seule l’interface change. Les données d’un formulaire arrivent toujours sous forme de chaînes, comme input() ou Entry.get() : on les convertit, et on prévoit le cas où la conversion échoue.

Sans methods=["GET", "POST"], la route n’accepte que GET, et l’envoi du formulaire produit l’erreur 405 Method Not Allowed.

Garder une information d’une page à l’autre : la session

HTTP est un protocole sans mémoire : chaque requête est indépendante, et le serveur ne sait pas que deux requêtes viennent du même joueur. Une variable Python ordinaire ne convient pas non plus : elle serait partagée par tous les visiteurs du site.

Flask fournit pour cela la session : un dictionnaire propre à chaque visiteur, session, que Flask range dans un cookie du navigateur. Ce cookie est signé avec une clé secrète, app.secret_key : le visiteur peut le lire, mais pas le modifier sans que le serveur s’en aperçoive.

Garder une information d’une page à l’autre : la session (suite)

Le jeu du nombre mystère illustre tout ce qui précède : le nombre à deviner et le nombre d’essais sont gardés dans la session.

Garder une information d’une page à l’autre : la session (suite)

Fichier app.py :

import random
from flask import Flask, redirect, render_template, request, session, url_for

app = Flask(__name__)
app.secret_key = "une phrase longue et difficile à deviner"


@app.route("/")
def accueil():
    """Commence une nouvelle partie."""
    session["secret"] = random.randint(1, 100)
    session["essais"] = 0
    session["message"] = "J'ai choisi un nombre entre 1 et 100."
    return redirect(url_for("jouer"))


@app.route("/jouer", methods=["GET", "POST"])
def jouer():
    if "secret" not in session:                 # pas de partie en cours
        return redirect(url_for("accueil"))
    if request.method == "POST":
        proposition = request.form.get("proposition", "")
        if not proposition.isdigit():
            session["message"] = "Il faut taper un nombre entier."
        else:
            n = int(proposition)
            session["essais"] = session["essais"] + 1
            if n < session["secret"]:
                session["message"] = f"{n} : c'est plus."
            elif n > session["secret"]:
                session["message"] = f"{n} : c'est moins."
            else:
                session["message"] = f"Bravo, trouvé en {session['essais']} essai(s) !"
        return redirect(url_for("jouer"))        # on revient en GET
    return render_template("jouer.html", message=session["message"], essais=session["essais"])


if __name__ == "__main__":
    app.run(debug=True)

Garder une information d’une page à l’autre : la session (suite)

Fichier templates/jouer.html :

<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>Nombre mystère</title></head>
<body>
  <h1>Nombre mystère</h1>
  <p>{{ message }}</p>
  <p>Essais : {{ essais }}</p>
  <form method="post">
    <input type="text" name="proposition" autofocus>
    <button type="submit">Proposer</button>
  </form>
  <p><a href="{{ url_for('accueil') }}">Nouvelle partie</a></p>
</body>
</html>

Garder une information d’une page à l’autre : la session (suite)

Une page « Nombre mystère » qui affiche « 37 : c'est plus. », « Essais : 3 », un champ, un bouton « Proposer » et un lien « Nouvelle partie ».

Après trois propositions : 50, 25, puis 37.

Garder une information d’une page à l’autre : la session (suite)

Le déroulement d’une partie montre le rôle de chaque morceau :

Requête Vue Ce qui se passe Réponse
GET / accueil tirage du nombre, essais à 0, rangés dans la session redirection vers /jouer
GET /jouer jouer lecture du message et des essais dans la session page du jeu
POST /jouer avec proposition=50 jouer comparaison, essais à 1, nouveau message dans la session redirection vers /jouer
GET /jouer jouer lecture de la session page avec « 50 : c’est moins. »

Garder une information d’une page à l’autre : la session (suite)

  • redirect(url_for("jouer")) répond au navigateur « va plutôt à l’adresse /jouer » (code HTTP 302) ; le navigateur fait aussitôt une nouvelle requête GET. Rediriger après un POST évite qu’une actualisation de la page renvoie la même proposition une seconde fois.
  • Sans app.secret_key, la première utilisation de session provoque une erreur : Flask refuse de créer un cookie qu’il ne peut pas signer.

Question. Deux élèves jouent en même temps sur le même serveur. Ont-ils le même nombre mystère ? Et si l’on avait rangé le nombre dans une variable globale secret ?

Réponse

Avec la session, chacun a son propre nombre : il est rangé dans le cookie de son navigateur. Avec une variable globale, il n’y aurait qu’un nombre pour tout le serveur : chaque nouvelle partie lancée par l’un changerait le nombre de l’autre, et les compteurs d’essais se mélangeraient.

Lire des données dans un fichier

Un site lit souvent ses données dans un fichier : liste de mots, questions d’un quiz, scores. Comme pour les images de Pygame, le chemin doit être construit à partir de l’emplacement du programme, sinon le site ne fonctionne que si on le lance depuis son dossier, ou que sur l’ordinateur de son auteur :

Lire des données dans un fichier (suite)

import json
import os

DOSSIER = os.path.dirname(os.path.abspath(__file__))

with open(os.path.join(DOSSIER, "donnees", "mots.json"), encoding="utf-8") as f:
    MOTS = json.load(f)        # chargé une fois, au démarrage du serveur

Un chemin comme /workspaces/mon-projet/donnees/mots.json ou C:/Users/Lina/Bureau/donnees/mots.json ne fonctionne que sur la machine où il a été écrit.

Renvoyer des données plutôt qu’une page

Une vue peut renvoyer des données au format JSON au lieu d’une page HTML, grâce à jsonify. Un programme JavaScript de la page les demande alors avec fetch et met la page à jour sans la recharger (voir le cours JavaScript ) :

Renvoyer des données plutôt qu’une page (suite)

from flask import jsonify


@app.route("/api/score")
def api_score():
    return jsonify({"joueur": "Lina", "score": 42})
fetch("/api/score")
  .then(reponse => reponse.json())
  .then(donnees => {
    document.getElementById("score").textContent = donnees.score;
  });

Qui fait quoi : le serveur et le navigateur

Un site Flask fait travailler deux programmes : le serveur, lancé par app.py, et le navigateur, qui affiche les pages. Pendant le développement, ils tournent sur le même ordinateur ; une fois le site publié, le serveur tourne chez un hébergeur et le navigateur chez chaque visiteur. La partie qui s’exécute sur le serveur s’appelle la partie serveur (en anglais back end), celle qui s’exécute dans le navigateur la partie client (front end).

Qui fait quoi : le serveur et le navigateur (suite)

Fichier Langage Où il s’exécute Partie
app.py Python serveur serveur
templates/page.html HTML et Jinja serveur, puis navigateur les deux
static/style.css CSS navigateur client
static/script.js JavaScript navigateur client

Les gabarits sont un cas mixte : Jinja est rempli sur le serveur, puis le HTML obtenu est affiché par le navigateur.

Qui fait quoi : le serveur et le navigateur (suite)

Flask ne comprend pas le JavaScript : pour lui, script.js n’est que du texte, comme une image. Il l’envoie sans le lire, comme un facteur apporte une lettre écrite dans une langue qu’il ne parle pas ; c’est le navigateur, qui contient son propre moteur JavaScript, qui l’exécute. À l’inverse, le navigateur ne reçoit jamais le code Python : il ne voit que ce que les vues renvoient.

Qui fait quoi : le serveur et le navigateur (suite)

Avec l’exemple du score de la section précédente, une visite se déroule ainsi :

  1. Le navigateur demande / ; Flask, en Python, remplit le gabarit et renvoie le HTML.
  2. Le HTML demande static/script.js ; Flask l’envoie tel quel.
  3. Le navigateur exécute script.js, qui appelle fetch("/api/score").
  4. Flask exécute la vue api_score et renvoie le JSON.
  5. Le JavaScript, dans le navigateur, écrit le score dans la page.

Les deux programmes n’exécutent jamais le code l’un de l’autre : ils échangent seulement des requêtes et des réponses.

Qui fait quoi : le serveur et le navigateur (suite)

Question. Dans la vue notes de la section gabarits , on ajoute la ligne print(moyenne). Où s’affiche ce nombre ?

Réponse

Dans le terminal où tourne le serveur (celui de Thonny, par exemple), et non dans la page : print s’exécute sur le serveur. Seul ce que la vue renvoie part vers le navigateur.

Qui fait quoi : le serveur et le navigateur

Question. Un élève écrit {{ moyenne }} dans static/script.js pour afficher la moyenne. Que se passe-t-il ?

Réponse

Jinja ne remplit que les gabarits du dossier templates : les fichiers de static partent tels quels. Le navigateur reçoit donc le texte {{ moyenne }}, qu’il lit comme du JavaScript ; aucune variable moyenne n’y existe, et la console du navigateur affiche l’erreur moyenne is not defined. Pour transmettre une valeur au JavaScript, on la demande avec fetch à une vue qui renvoie du JSON.

Publier son site

Cette section dépasse le programme : elle sert à mettre un projet en ligne pour le montrer.

Avec app.run(), le site n’est visible que sur l’ordinateur qui le lance : 127.0.0.1 désigne cet ordinateur lui-même. Pour le publier, il faut une machine allumée en permanence et joignable depuis Internet ; on la loue à un hébergeur. PythonAnywhere et Render proposent une formule gratuite pour un petit site, avec des limites : chez PythonAnywhere, le site doit être réactivé chaque mois ; chez Render, il s’endort après un quart d’heure sans visite, et la première page suivante met quelques secondes à venir.

Publier son site (suite)

Le serveur lancé par app.run() ne convient pas en ligne : il est fait pour une seule personne qui développe. Sur l’hébergeur, on confie l’accueil des visiteurs à un serveur prévu pour cela, comme gunicorn : il lance plusieurs copies de l’application et répartit les requêtes entre elles, si bien que plusieurs visiteurs sont servis en même temps. Le code Flask ne change pas : Flask reste le cuisinier qui prépare les pages, gunicorn devient le maître d’hôtel qui place les visiteurs.

Publier son site (suite)

On fournit à l’hébergeur trois choses :

  1. le dossier du projet, avec app.py, templates et static, le plus souvent par un dépôt GitHub ;
  2. un fichier requirements.txt, la liste des bibliothèques qu’il doit installer ;
  3. la commande de démarrage, gunicorn app:app, qui se lit « dans le fichier app.py, l’objet app ».

Fichier requirements.txt :

flask
gunicorn

Publier son site (suite)

En ligne, debug=True ne doit jamais être actif : sa page d’erreur montre le code du serveur à n’importe quel visiteur. La clé app.secret_key ne doit pas non plus être publiée avec le code : quelqu’un qui la connaît peut fabriquer de fausses sessions.

Comptes et mots de passe

Un site avec des comptes ne doit jamais enregistrer les mots de passe en clair : si le fichier ou la base de données est volé, tous les mots de passe le sont aussi. On enregistre une empreinte (hash) du mot de passe, calculée par une fonction à sens unique : on ne peut pas retrouver le mot de passe à partir de l’empreinte, mais on peut vérifier qu’un mot de passe tapé lui correspond. La bibliothèque Werkzeug, installée avec Flask, fournit ces deux fonctions :

Comptes et mots de passe (suite)

from werkzeug.security import check_password_hash, generate_password_hash

empreinte = generate_password_hash("motdepasse")      # à l'inscription : on enregistre empreinte
check_password_hash(empreinte, "motdepasse")          # à la connexion : True
check_password_hash(empreinte, "autre")               # False

Une fois l’utilisateur connecté, on range son nom dans la session (session["utilisateur"] = nom), et chaque vue réservée vérifie sa présence.

Erreurs fréquentes

Symptôme Cause Correction
jinja2.exceptions.TemplateNotFound: notes.html le gabarit n’est pas dans un dossier nommé exactement templates, à côté de app.py vérifier le nom et l’emplacement du dossier
la feuille de style n’est pas appliquée fichier hors du dossier static, ou <link> placé hors des blocs d’une page fille static/style.css, url_for('static', filename='style.css') dans base.html
404 Not Found aucune route pour cette adresse, ou faute de frappe comparer l’adresse et le @app.route
405 Method Not Allowed formulaire POST envoyé à une route qui n’accepte que GET methods=["GET", "POST"]

Erreurs fréquentes (suite)

Symptôme Cause Correction
BadRequestKeyError (400 Bad Request), KeyError: 'nom' request.form["nom"] alors que le formulaire n’a pas de champ name="nom" accorder les noms, ou request.form.get("nom", "")
RuntimeError: The session is unavailable because no secret key was set. app.secret_key absent définir app.secret_key
une variable n’apparaît pas dans la page elle n’est pas transmise à render_template render_template("page.html", nom=valeur)
Address already in use, ou page « Access denied » sur le port 5000 un autre programme (un autre serveur, ou un service du système) occupe le port 5000 arrêter l’autre serveur, ou app.run(debug=True, port=5001)

Erreurs fréquentes (suite)

Symptôme Cause Correction
FileNotFoundError au démarrage chemin relatif au dossier courant, ou chemin absolu d’une autre machine os.path.join(DOSSIER, ...)
le serveur exécute du code de test au démarrage, ou des erreurs d’import en boucle app.py importe un fichier qui l’importe à son tour, ou du code hors de if __name__ == "__main__": les fonctions dans un module, le code de test dans le bloc if __name__ == "__main__":
les modifications ne s’affichent pas serveur lancé sans debug=True, ou ancienne version en cache relancer le serveur, actualiser la page

Pour aller plus loin

  • Ajouter au nombre mystère un meilleur score (le plus petit nombre d’essais), gardé dans la session.
  • Faire un quiz : les questions dans un fichier JSON, la question en cours et le score dans la session, une page de résultat à la fin.
  • Reprendre le site de la section mise en page commune avec une page par projet de la classe.
  • La documentation de Flask (en anglais), et en particulier son tutoriel.

À retenir

  1. Flask est une bibliothèque Python pour écrire un serveur web : à chaque adresse correspond une vue, une fonction dont la valeur renvoyée devient la page.
  2. @app.route("/adresse") associe une adresse à une vue ; <nom> ou <int:nom> dans la route transmet une partie de l’adresse en paramètre.
  3. Le serveur se lance avec app.run(debug=True) dans un bloc if __name__ == "__main__": ; le site se visite à l’adresse http://127.0.0.1:5000.

À retenir (suite)

  1. Les gabarits, dans templates/, sont du HTML enrichi de Jinja : {{ valeur }}, {% for %}, {% if %} ; render_template leur transmet les variables.
  2. {% extends "base.html" %} et {% block %} donnent une mise en page commune ; CSS, images et scripts vont dans static/.
  3. request.args contient les données d’un formulaire GET, request.form celles d’un formulaire POST ; elles arrivent toujours sous forme de chaînes.
  4. La session, signée avec app.secret_key, garde des informations propres à chaque visiteur d’une requête à l’autre.

À retenir (suite)

  1. redirect(url_for("vue")) renvoie le navigateur vers une autre vue ; on redirige après le traitement d’un POST.
  2. Python et Jinja s’exécutent sur le serveur (partie serveur, back end) ; HTML, CSS et JavaScript dans le navigateur (partie client, front end). Flask envoie les fichiers de static sans les exécuter.
  3. app.run() est un serveur de développement ; en ligne, un hébergeur fait tourner l’application avec un serveur comme gunicorn, d’après requirements.txt et la commande gunicorn app:app.

Résumé

Concept Syntaxe Exemple
Application app = Flask(__name__) une par site
Vue @app.route("/adresse") puis def vue(): @app.route("/jeux")
Paramètre d’adresse <nom>, <int:nom> @app.route("/table/<int:n>")
Lancer app.run(debug=True) http://127.0.0.1:5000
Gabarit render_template("page.html", x=valeur) dossier templates/
Insérer, répéter, choisir {{ x }}, {% for %}, {% if %} {{ eleve }}
Hériter {% extends "base.html" %}, {% block nom %} menu commun

Résumé (suite)

Concept Syntaxe Exemple
Adresse d’une vue ou d’un fichier url_for("vue"), url_for("static", filename=...) href="{{ url_for('accueil') }}"
Formulaire GET request.args.get("nom", "") recherche
Formulaire POST methods=["GET", "POST"], request.form["nom"] envoi d’une réponse
Session app.secret_key, session["cle"] score d’un joueur
Redirection redirect(url_for("vue")) après un POST
Données JSON jsonify(dictionnaire) fetch en JavaScript
Publier requirements.txt, gunicorn app:app chez un hébergeur