Le module de programmation avancée en Python : construire des applications web avec Flask, les relier à une base relationnelle puis à une base de séries temporelles, et y brancher des modèles d’intelligence artificielle.
Ce que j’y ai appris
Structurer une app Flask
Application factory, Blueprints, gabarits Jinja et API JSON.
Un ORM bien utilisé
Modèles SQLAlchemy 2.0, relations, et le chargement des données sans requêtes en trop.
Séries temporelles
InfluxDB et le langage Flux pour un tableau de bord météo, devises et cryptomonnaies.
Mettre un modèle en ligne
Prétraiter une image, interroger un CNN Keras et renvoyer la prédiction, le tout dans Docker.
La suite
Le rendu final est une application Flask complète qui réunit base de données, envoi d’images et modèles d’IA.
Detailed write-ups and revision notes are in French: they are my own study notes.
Trois TP, une même app Flask qui grandit
Chaque TP tourne dans son conteneur Docker sur le serveur Flask de l’IUT, où je travaille en SSH avec mon groupe (groupe 5). Je les ai relancés ici, tels quels, pour récupérer leurs vraies sorties.
TP2 · Flask-SQLAlchemy sur la base Chinook
Une application Flask organisée en application factory (create_app) avec un Blueprint, deux modèles SQLAlchemy 2.0 (Artist 1 → n Album) et un seul gabarit Jinja qui change d’affichage selon mode.
Clique sur une route : la page est rendue avec mon gabarit et les vraies données de chinook.db (275 artistes, 347 albums), et tu vois le SQL que SQLAlchemy a réellement envoyé.
Sans option, afficher les albums des 204 artistes coûte 205 requêtes : une pour les artistes, puis une par artiste dès que le gabarit lit artist.albums. Avec selectinload (mon code) : 2 requêtes, la seconde avec un IN (…). Avec joinedload : 1 requête avec jointure.
TP3 · Dashboard InfluxDB
Un tableau de bord façon Grafana : Flask interroge un serveur InfluxDB 2 de l’IUT (séries temporelles météo, devises, cryptomonnaies et CPU) en langage Flux, puis dessine les courbes avec Chart.js. Le serveur InfluxDB n’est joignable que depuis le réseau de l’IUT : je ne peux pas rejouer les données ici, mais je peux rejouer ce que fait mon code.
Le formulaire du dashboard, et les requêtes Flux exactes que build_dashboard() en tire :
URL envoyée par le formulaire :
La valeur est collée telle quelle dans la requête : le guillemet ferme la chaîne et la suite devient du code Flux. InfluxDB OSS 2 ne gère pas les requêtes paramétrées : il faut valider la valeur, par exemple vérifier qu’elle fait partie de get_tag_values("weather", "location"), que mon code calcule déjà.
Le jeton InfluxDB est masqué (***). Le gabarit index.html (650 lignes de HTML/CSS, Chart.js) n’est pas reproduit.
TP4 · Servir des modèles d’IA avec Flask
Une route par page (/model/<nom>) et une API POST /predict/<nom> qui reçoit une image en base64, la prétraite avec Pillow, la passe au modèle Keras (mis en cache après le premier chargement) et renvoie {"class", "confidence"} en JSON. Quatre modèles pré-entraînés au format .h5 :
| Modèle | Entrée | Architecture | Paramètres | Précision mesurée |
|---|---|---|---|---|
| MNIST | 28×28 gris | 4 conv + BatchNorm, 2 pool, dense 256 | 330 730 | 99,6 % sur 5 000 chiffres* |
| Fashion-MNIST | 28×28 gris | 3 conv, 2 pool, dense 64, sans softmax | 93 322 | 95,4 % sur les 10 000 images de test |
| CIFAR-10 | 32×32 couleur | 4 conv, 2 pool, dense 512 | 1 250 858 | 78,3 % sur les 10 000 images de test |
| CIFAR-100 | 32×32 couleur | 6 conv (ELU), 3 pool, dense 1024 | 6 777 060 | non mesurée (pas de jeu de test ici) |
* Échantillon MNIST de la bibliothèque mlxtend, tiré du jeu d’entraînement : le chiffre est sans doute optimiste.
Ce que montrait mon app sur le serveur
Mon script de test, relancé sur le code tel quel :
La page d’accueil liste les modèles :
- MNIST - available
- Fashion-MNIST - missing (place model file)
- CIFAR-10 - missing (place model file)
- CIFAR-100 - missing (place model file)
Les trois fichiers sont pourtant dans app/models/, mais MODELS_DIR pointe vers models/. En corrigeant ce chemin, un nouveau problème apparaît (un 7 tracé, envoyé aux quatre modèles) :
Les quatre modèles, dans ton navigateur
Les poids viennent des fichiers .h5 de mon app, convertis pour JavaScript (BatchNorm fusionné, poids sur 8 bits : même classe prédite que le modèle d’origine dans 97,6 à 100 % des cas). Le prétraitement reprend celui de exo.py, et la réponse affichée est celle que renverrait mon API.
MNIST
Images de test :
MNIST a été construit avec des chiffres recadrés dans 20×20 pixels puis centrés par leur centre de masse dans 28×28. Mon app réduit tout le canevas de 280 à 28 pixels en « plus proche voisin » : un chiffre dessiné petit ou dans un coin arrive très différent de ce que le réseau a appris. Compare les deux prétraitements sur un chiffre tracé en petit, dans un coin.
Ce que l’exécution a révélé
Mauvais dossier (models/ au lieu de app/models/), puis, une fois corrigé, CIFAR-10 et CIFAR-100 ne se chargent pas : ils ont été enregistrés avec Keras 2.0 et 2.2, et tensorflow>=2.15 installe Keras 3, qui ne lit plus ces anciens .h5 (« pop expected at most 1 argument »). Solution : reconstruire l’architecture en Keras 3 et charger seulement les poids, comme je l’ai fait pour cette page, puis réenregistrer au format .keras.
Le modèle Fashion-MNIST se termine par une couche Dense(10) sans activation : predict() renvoie des logits, et np.max(preds) vaut 10,08 sur mon 7 tracé. Il faut appliquer tf.nn.softmax avant de parler de probabilité.
Un réseau de classification choisit toujours une classe, même pour une image vide. Il faut refuser les entrées vides en amont, ou fixer un seuil de confiance.
Le formulaire envoie ?quotes=EUR"es=GBP"es=CHF, mais index() lit request.args.get("quotes"), qui ne renvoie que la première valeur. Il faut request.args.getlist("quotes"), comme le fait déjà la route /query (même problème pour symbols).
- Sécurité : les trois apps tournent en
debug=Truesur0.0.0.0. Le débogueur Werkzeug permet d’exécuter du Python depuis le navigateur en cas d’erreur : à ne jamais exposer sur un serveur partagé. - Secret : le jeton InfluxDB est écrit en clair dans
docker-compose.yml. Il devrait être dans un fichier.envnon versionné. - Injection : le TP3 construit ses requêtes Flux avec des f-strings à partir de l’URL (
bucket,location…). - Code mort :
heatindex.htmln’a pas de route (404) et son script cherche des éléments qui n’existent pas ;draw.htmlappelle/predict(inexistant) et litjson.digitau lieu dejson.class. La page MNIST n’a donc pas de zone de dessin, contrairement à ce qu’annonce l’accueil. - Docker : les trois
docker-compose.ymlpublient le même port 5005, donc un seul TP peut tourner à la fois. Bon point du TP4 :requirements.txtest copié avant le code pour garder le calquepip installen cache. - TP2 : le cas « No album available » du gabarit ne peut jamais s’afficher, car la jointure exclut déjà les 71 artistes sans album.
La suite
Le rendu final du module est une application Flask complète qui réunit tout : base de données, envoi d’images et modèles d’IA.
Detailed write-ups and revision notes are in French: they are my own study notes.
Fiche de révision
1. Flask : routes, requêtes, réponses
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
from app.routes import main_bp
app.register_blueprint(main_bp)
return app
main_bp = Blueprint("main", __name__)
@main_bp.route("/artists")
def artists():
rows = Artist.query.order_by(Artist.Name).all()
return render_template("artists.html", items=rows)
- Application factory : créer l’app dans une fonction, pour pouvoir en créer plusieurs (tests, configurations différentes) et éviter les imports circulaires.
- Blueprint : un groupe de routes enregistré sur l’app ;
url_for("main.artists"). - Paramètres d’URL :
@app.route("/model/<model_name>"); convertisseurs<int:id>,<path:p>. - Méthodes :
methods=["POST"]. Corps JSON :request.get_json(); formulaire :request.form; fichier :request.files["image"]. - Query string :
request.args.get("k", défaut)donne une valeur,request.args.getlist("k")donne toutes les valeurs d’un paramètre répété. - Réponse JSON :
return jsonify(obj), 400(le tuple fixe le code HTTP). Erreur :abort(404). app.run(host="0.0.0.0")pour écouter hors du conteneur ; le serveur intégré n’est fait que pour le développement (en production : Gunicorn).
2. Gabarits Jinja2
{{ variable }}affiche (échappé automatiquement contre le XSS) ;{% if %}…{% elif %}…{% endif %};{% for x in liste %}…{% else %}(liste vide){% endfor %}.- Héritage :
{% extends "base.html" %}et{% block content %}{% endblock %}pour ne pas répéter l’en-tête. - Liens et fichiers statiques :
{{ url_for('static', filename='style.css') }}. |safedésactive l’échappement : uniquement pour du HTML de confiance.
3. Flask-SQLAlchemy (SQLAlchemy 2.0)
class Artist(db.Model):
__tablename__ = "artists"
ArtistId: Mapped[int] = mapped_column(primary_key=True)
Name: Mapped[str]
albums: Mapped[list["Album"]] = relationship(back_populates="artist")
class Album(db.Model):
__tablename__ = "albums"
AlbumId: Mapped[int] = mapped_column(primary_key=True)
ArtistId: Mapped[int] = mapped_column(ForeignKey("artists.ArtistId"))
artist: Mapped[Artist] = relationship(back_populates="albums")
- Configuration :
SQLALCHEMY_DATABASE_URI = "sqlite:///chemin/chinook.db";SQLALCHEMY_ECHO = Truepour voir le SQL. back_populatesrelie les deux côtés de la relation ; la clé étrangère est du côté « n ».- Requêtes :
Model.query.filter_by(Name="AC/DC").first()(style 1.x) oudb.session.execute(select(Artist).where(...)).scalars()(style 2.0). - Écrire :
db.session.add(obj)puisdb.session.commit(); en cas d’erreurrollback().
| Chargement | Requêtes pour 204 artistes | Quand l’utiliser |
|---|---|---|
| lazy (défaut) | 1 + 204 | on n’accède à la relation que rarement |
selectinload | 2 | relation « vers plusieurs », listes |
joinedload | 1 | relation « vers un », ou peu de lignes |
4. InfluxDB et Flux
- Série temporelle : des mesures horodatées. InfluxDB est optimisé pour en écrire et en agréger beaucoup.
- Bucket : la base (avec une durée de rétention). Measurement : l’équivalent d’une table (
weather_current). - Tag : métadonnée indexée, texte, pour filtrer et grouper (
location,symbol). Field : la valeur mesurée, non indexée (temperature_2m,price). - Point = measurement + tags + fields + horodatage. Une série = une combinaison measurement + tags + field.
from(bucket: "weather") |> range(start: -7d) // fenêtre de temps (obligatoire) |> filter(fn: (r) => r._measurement == "weather_current") |> filter(fn: (r) => r.location == "annecy") // filtre sur un tag |> filter(fn: (r) => r._field == "temperature_2m") |> aggregateWindow(every: 1h, fn: mean, createEmpty: false) // une moyenne par heure
- Colonnes du résultat :
_time,_value,_field,_measurementet les tags. - Découvrir le schéma :
schema.measurements(),schema.fieldKeys(),schema.tagValues()(import"influxdata/influxdb/schema"). - Client Python :
InfluxDBClient(url, token, org).query_api().query(flux)renvoie des tables de records ; penser àclient.close().
5. Servir un modèle d’IA avec Flask
- Charger le modèle une seule fois (au démarrage ou dans un cache), jamais à chaque requête.
- Le prétraitement doit reproduire celui de l’entraînement : taille (28×28, 32×32), canaux, normalisation (
/255), couleurs (MNIST : trait blanc sur fond noir, d’où l’invert), cadrage. - Forme d’entrée Keras :
(batch, hauteur, largeur, canaux), d’oùreshape(1, 28, 28, 1). np.argmax(preds)donne la classe ; la confiance n’est une probabilité que si la dernière couche est un softmax.- Image reçue en base64 (data URL) : retirer le préfixe
data:image/png;base64,, puisbase64.b64decodeetImage.open(io.BytesIO(...)). - Formats :
.h5(ancien, dépend de la version de Keras) et.keras(Keras 3). Fixer les versions dansrequirements.txt. - Rappel CNN : convolution (filtres appris) → activation (ReLU) → pooling (réduit la taille) → … → aplatissement → couches denses → softmax.
6. Flask dans Docker
FROM python:3.12-slim WORKDIR /TP4 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # gardé en cache tant que requirements.txt ne change pas COPY . . EXPOSE 5005 CMD ["python", "exo.py"]
ports: "5005:5005"= port de l’hôte : port du conteneur. Deux services ne peuvent pas publier le même port de l’hôte.volumes: - .:/TP4monte le code : pratique en développement (rechargement), à éviter en production.- Variables d’environnement :
environment:ouenv_file: .env, lues avecos.getenv().
7. Sécurité d’une app Flask
- Injection : ne jamais coller une entrée utilisateur dans du SQL ou du Flux. SQLAlchemy paramètre ses requêtes (
?) ; pour Flux, valider la valeur contre une liste autorisée. - Secrets (jetons, mots de passe,
SECRET_KEY) : variables d’environnement, fichier.envdans.gitignore, jamais dans le dépôt. - Mode debug : désactivé en production (débogueur interactif = exécution de code à distance).
- Envoi de fichiers : limiter la taille (
MAX_CONTENT_LENGTH), vérifier le type,secure_filename()pour le nom. - XSS : Jinja échappe par défaut ; ne pas utiliser
|safesur des données utilisateur.