Programmation Python Flask
Programmation Python Flask
Licence 2 : GL
M. DIOP ElHadji Mar
Institut Sup rieur d'Informatique — Ann e 2025/2026
é
é
Objectifs du cours
● Comprendre le r le d'un framework web et celui de Flask en particulier
● Cr er une application web compl te avec Python et Flask
● Ma triser le routage, les templates Jinja2 et les fichiers statiques
● Organiser un projet Flask de mani re modulaire (Blueprints, POO)
● Manipuler une base de donn es PostgreSQL avec SQLAlchemy et Flask-Migrate
● G rer les formulaires, la session utilisateur et l'authentification
● Appliquer les bonnes pratiques de s curit (CSRF, hash, en-t tes…)
● D ployer une application Flask de mani re professionnelle
2
é
é
é
î
ô
é
è
è
é
è
é
ê
Plan du cours
● Partie 1 : Introduction et bases (Flask, Jinja2, fichiers statiques)
● Partie 2 : POO avanc e avec Flask (Blueprints, mod les, repositories)
● Partie 3 : Persistance avec PostgreSQL (SQLAlchemy, migrations, requ tes)
● Partie 4 : Formulaires et authentification (Flask-WTF, Flask-Login)
● Partie 5 : Bonnes pratiques et d ploiement
● Annexes : r f rentiel de commandes, glossaire, pour aller plus loin
3
é
é
é
é
è
ê
PARTIE 1
Introduction et bases
Chapitre 1
Introduction Flask
à
Objectifs du chapitre
● Comprendre le r le d'un framework web c t serveur
● D couvrir Flask et son positionnement dans l' cosyst me Python
● Comparer Flask avec Django et choisir le bon outil selon le projet
● Identifier les composants fondamentaux : Werkzeug, Jinja2, WSGI
7
é
ô
ô
é
é
è
1.1 Qu'est-ce qu'un framework web ?
● Un framework web est un ensemble coh rent de biblioth ques et de conventions qui automatisent
les t ches r p titives du d veloppement d'applications HTTP.
● Plut t que de r inventer la roue (routage, requ tes, rendu HTML, sessions...), le d veloppeur
s'appuie sur des briques prouv es.
● On distingue deux familles :
● Frameworks « full-stack » : tout-en-un, architecture impos e (Django).
● Micro-frameworks : noyau minimal, composition libre (Flask).
● Principe d'inversion de contr le : c'est le framework qui appelle votre code, et non l'inverse.
8
ô
â
é
é
é
é
é
ô
é
é
ê
è
é
é
1.2 Pr sentation de Flask
● Flask est un micro-framework web crit en Python, cr par Armin Ronacher en 2010.
● Il s'appuie sur deux briques fondamentales :
● Werkzeug : biblioth que WSGI fournissant routage, Request/Response, serveur de dev.
● Jinja2 : moteur de templates pour g n rer du HTML dynamique.
● L'adjectif « micro » d signe un noyau minimal — non un framework limit .
● Id al pour : APIs REST, prototypes, microservices, applications de taille moyenne.
9
é
é
é
è
é
é
é
é
é
é
Caract ristiques principales
● L ger et minimaliste : moins de 1 000 lignes de code dans le module principal
● Extensible via un cosyst me mature : Flask-SQLAlchemy, Flask-Login, Flask-WTF, Flask-Migrate…
● Compatible WSGI : d ployable sur Gunicorn, uWSGI, mod_wsgi
● Documentation officielle de tr s grande qualit
● Adopt en production par Pinterest, LinkedIn Learning, Netflix (microservices)
10
é
é
é
é
é
è
è
é
1.3 Flask vs Django — comparaison
Crit re Flask Django
Type Micro-framework Framework full-stack
Philosophie Libert , composition Convention sur configuration
ORM Optionnel (SQLAlchemy via ext.) ORM int gr
Admin Aucun ( coder ou via ext.) Admin riche int gr
Authentification Via Flask-Login Syst me int gr
Courbe d'apprentissage Douce Plus raide
Id al pour API, microservices, prototypes CMS, e-commerce, full-stack
Taille du noyau ~ 1 000 lignes ~ 100 000 lignes
11
é
è
è
é
à
é
é
é
é
é
é
Notre choix p dagogique
● Flask est particuli rement adapt l'apprentissage : on voit ce que l'on fait, on construit pierre par
pierre.
● Une fois Flask ma tris , l'apprentissage de Django ou de FastAPI devient bien plus naturel.
● Ce cours adopte une approche progressive : du « Hello World » l'application compl te d ploy e.
12
î
è
é
é
é
à
à
è
é
é
1.4 L' cosyst me Flask
● WSGI (PEP 3333) : standard Python d finissant la communication serveur ↔ application.
● Werkzeug : impl mente WSGI, fournit Request, Response, routage, erreurs HTTP.
● Jinja2 : moteur de templates puissant et s curis .
● Click : cr ation de commandes CLI personnalis es (flask run, flask db…).
● itsdangerous : signature s curis e des cookies de session.
13
é
é
é
è
é
é
é
é
é
é
Architecture conceptuelle
14
Chapitre 2
16
é
é
é
é
2.1 Pr requis
● Python 3.10 ou sup rieur
● pip, le gestionnaire de paquets Python (livr avec Python ≥ 3.4)
● Un diteur de code : Visual Studio Code ou PyCharm Community
● Un terminal : Bash sous Linux/macOS, PowerShell ou Git Bash sous Windows
17
é
é
é
é
V rifier l'installation Python
# V rifier pip
pip3 --version
pip 23.2.1
18
é
é
é
2.2 L'environnement virtuel : pourquoi ?
● Un environnement virtuel est un r pertoire isol contenant son propre interpr teur Python et ses
paquets.
● Chaque application a ses propres versions de Flask, SQLAlchemy, etc. — pas de conflit.
● On ne pollue jamais l'installation syst me de Python.
● R gle d'or : on ne fait JAMAIS pip install sans avoir activ un environnement virtuel.
19
è
é
è
é
é
é
Cr ation d'un environnement virtuel
Linux / macOS Windows (PowerShell)
20
é
À
é
é
é
é
é
é
2.3 Installation de Flask
Installation et v rification :
Astuce
(.venv) $ pip install Flask
Figez syst matiquement les d pendances apr s
Collecting Flask
chaque installation :
Downloading [Link] (101 kB)
pip freeze > [Link]
Collecting Werkzeug>=3.1
Collecting Jinja2>=3.1.2
Successfully installed Flask-3.1.0 ... Un coll gue (ou votre futur vous) pourra recr er
l'environnement avec :
(.venv) $ pip show Flask pip install -r [Link]
Name: Flask
Version: 3.1.0
21
è
é
é
é
é
è
2.4 Premi re application : Hello, ISI !
[Link]
Trois tapes :
# [Link]
① Importer la classe Flask et l'instancier
from flask import Flask
② D corer une fonction avec @[Link]("...")
# 1) Cr ation de l'objet application ③ Lancer le serveur avec [Link](debug=True)
app = Flask(__name__)
debug=True → rechargement automatique + page
# 2) D finition d'une route d'erreur interactive.
@[Link]("/")
def accueil():
return "Bonjour, ISI ! Bienvenue sur ma premi re application Flask."
22
é
é
é
é
è
è
Lancer l'application
M thode 1 — ex cution directe : R sultat dans le navigateur :
23
é
é
é
è
ô
é
é
é
2.5 Structure de projet recommand e
Arborescence professionnelle :
monblog/
├── .venv/ # environnement virtuel (jamais versionné)
├── app/ # paquet de l'application
│ ├── __init__.py # création de l'app (factory)
│ ├── models/ # modèles métier et SQLAlchemy
│ ├── views/ # blueprints (routes)
│ ├── forms/ # formulaires Flask-WTF
│ ├── templates/ # gabarits Jinja2 (HTML)
│ └── static/ # CSS, JS, images
├── migrations/ # généré par Flask-Migrate
├── tests/ # tests unitaires et d'intégration
├── con [Link] # classes de con guration
├── [Link] # dépendances gées
├── .env # variables sensibles (jamais versionné)
├── .gitignore
└── [Link] # point d'entrée
24
fi
fi
fi
é
.gitignore — mettre absolument
● .venv/
● __pycache__/
● *.pyc
● .env
● instance/
● *.sqlite
● Ne JAMAIS commiter de mots de passe, de cl s secr tes ou d'environnement virtuel.
25
à
é
è
Chapitre 3
27
é
é
é
3.1 Le concept de route
● Une route est une association entre une URL et une fonction Python.
● Quand le navigateur effectue une requ te, Flask appelle la fonction associ e et renvoie au
navigateur ce qu'elle retourne.
● C'est le d corateur @[Link] qui r alise cette association.
28
é
é
ê
é
Routes simples
@[Link]("/")
def accueil():
return "Page d'accueil"
@[Link]("/contact")
def contact():
return "Page de contact"
@[Link]("/a-propos")
def a_propos():
return " propos de l'application"
29
À
3.2 Routes dynamiques
Une route peut contenir des parties variables, d clar es avec des chevrons : <variable>. Flask transmet la valeur
captur e comme argument la vue.
@[Link]("/utilisateur/<nom>") ←C /utilisateur/aminata
def profil(nom): Profil de aminata
Le nom du param tre dans l'URL doit correspondre exactement au nom de l'argument de la fonction.
30
é
è
à
é
é
3.3 Convertisseurs
Convertisseur Type Python Exemple d'URL Description
string (d faut) str /article/python Toute cha ne sans /
int int /article/42 Entier positif
float float /produit/19.99 Nombre d cimal
path str /fichier/a/sous/[Link] Comme string, accepte le /
uuid [Link] /session/8c3a… Identifiant UUID
31
é
î
é
Exemples de convertisseurs
@[Link]("/article/<int:id_article>")
def voir_article(id_article):
# id_article est d j un int, pas besoin de int(...)
return f"Lecture de l'article num ro {id_article}"
@[Link]("/produit/<float:prix>")
def afficher_prix(prix):
return f"Prix HT : {prix} €, TTC : {prix * 1.18:.2f} €"
@[Link]("/fichier/<path:chemin>")
def voir_fichier(chemin):
return f"Vous demandez : {chemin}"
Erreur 404 : si la valeur ne correspond pas au type du convertisseur (ex. /article/abc avec int), Flask renvoie automatiquement 404 Not Found.
32
é
à
é
3.4 M thodes HTTP
Par d faut, une route ne r pond qu'aux requ tes GET. Pour autoriser d'autres m thodes, on pr cise methods=[...].
@[Link]("/")
def accueil():
lien = url_for("profil", nom="aminata")
return f"<a href='{lien}'>Profil d'Aminata</a>"
@[Link]("/utilisateur/<nom>")
def profil(nom):
return f"Profil de {nom}"
Encore plus pratique dans les templates Jinja2 — plus jamais d'URL en dur dans votre HTML !
34
é
è
à
â
Chapitre 4
Templates Jinja2
Objectifs du chapitre
● S parer la pr sentation (HTML) de la logique (Python)
● Utiliser variables, structures de contr le et filtres dans Jinja2
● Mettre en place l'h ritage de templates avec extends et block
● Ma triser url_for() c t template et le rendu de listes/objets
36
é
î
é
é
ô
é
ô
4.1 Pourquoi des templates ?
● Renvoyer du HTML brut depuis les vues devient vite impraticable.
● Un moteur de templates permet d' crire des fichiers HTML quasi-classiques dans lesquels on
ins re des balises sp ciales pour afficher des variables ou int grer de la logique.
● Jinja2 est le moteur int gr Flask — pas besoin de l'installer s par ment.
37
è
é
é
é
à
é
é
é
é
4.2 Le dossier templates et render_template
Structure : [Link]
{# templates/[Link] #}
<!doctype html>
<html lang="fr">
<head><title>{{ titre }}</title></head>
<body>
<h1>Bienvenue sur {{ titre }} !</h1>
<p>Bonjour, {{ prenom }}.</p>
</body>
</html>
38
fi
4.3 Syntaxe Jinja2 — trois balises
● {{ ... }} : affiche une expression (variable, calcul…).
● {% ... %} : structure de contr le (if, for, block, extends…).
● {# ... #} : commentaire (n'appara t pas dans le HTML produit).
39
ô
î
Conditions et boucles dans Jinja2
Condition : Boucle :
{% if utilisateur %} <ul>
{% for article in articles %}
<p>Bonjour, {{ [Link] }} !</p> <li>
{% else %} <a href="{{ url_for('voir_article', id=[Link]) }}">
<p>Vous n' tes pas connect .</p> {{ [Link] }}
</a>
{% endif %}
— par {{ [Link] }}
</li>
{% else %}
<li>Aucun article publi pour le moment.</li>
{% endfor %}
</ul>
Astuce m connue : le bloc {% else %} d'un {% for %} s'ex cute si la liste est vide. Tr s utile pour afficher « Aucun l ment ».
40
é
ê
é
é
é
è
é
é
4.4 Filtres Jinja2
Les filtres transforment une valeur. On les applique avec le caract re | (pipe).
41
é
é
é
é
è
chappement HTML automatique
● Jinja2 chappe par d faut les caract res dangereux : <, >, &,
● C'est une protection contre les attaques XSS (Cross-Site Scripting).
● N'utilisez |safe que sur du contenu dont vous tes absolument certain de l'origine.
42
É
é
é
è
ê
4.5 H ritage — le template parent
templates/[Link]
{# templates/[Link] #}
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>{% block titre %}MonBlog{% endblock %}</title>
<link rel="stylesheet"
href="{{ url_for('static', filename='css/[Link]') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{{ url_for('accueil') }}">Accueil</a>
<a href="{{ url_for('liste_articles') }}">Articles</a>
<a href="{{ url_for('contact') }}">Contact</a>
</nav>
</header>
<main>
{% block contenu %}{% endblock %}
</main>
<footer>© 2026 MonBlog — ISI</footer>
</body>
</html>
43
é
H ritage — le template enfant
templates/[Link]
retenir
{% extends "[Link]" %} extends doit tre la premi re instruction du
fichier.
Un block enfant peut appeler {{ super() }}
{% block titre %}Accueil — MonBlog{% endblock %} pour conserver et enrichir le contenu du
parent.
{% block contenu %}
<h1>Bienvenue sur MonBlog</h1>
<p>Le blog technique des tudiants L2 GL de l'ISI.</p>
{% endblock %}
44
À
é
ê
è
é
4.6 Inclusion de fragments
Pour factoriser du code r p t (carte d'article, formulaire de recherche…), on utilise {% include %} :
templates/_carte_article.html templates/[Link]
Convention : pr fixer d'un underscore les fragments destin s tre inclus (_carte_article.html, _formulaire.html…).
45
é
é
é
é
é
à
ê
Chapitre 5
Fichiers statiques
Objectifs du chapitre
● Comprendre le r le du dossier static
● R f rencer correctement les ressources avec url_for('static', filename=...)
● Organiser ses sous-dossiers (css/, js/, img/)
47
é
é
ô
5.1 Le dossier static
Comme pour les templates, Flask cherche par d faut un dossier static c t du module de l'application. Tout fichier qui
s'y trouve est servi l'URL /static/....
monblog/
├── [Link]
├── templates/
│ └── [Link]
└── static/
├── css/
│ └── [Link]
├── js/
│ └── [Link]
└── img/
└── [Link]
48
à
é
à
ô
é
5.2 Inclure les fichiers statiques
Plut t que d' crire en dur href="/static/css/[Link]", on utilise url_for — le code est plus robuste si la configuration de l'URL
des statiques change.
<link rel="stylesheet"
href="{{ url_for('static', filename='css/[Link]') }}">
49
ô
é
5.3 Exemple de feuille de style minimale
static/css/[Link]
:root {
--bleu: #0b5394;
--orange: #e69138;
--texte: #222;
--fond: #fafafa;
}
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
margin: 0; background: var(--fond); color: var(--texte);
}
header { background: var(--bleu); padding: 1rem; }
header a { color: #fff; margin-right: 1rem; text-decoration: none; }
main { max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
.carte {
background: #fff; border-left: 4px solid var(--orange);
padding: 1rem; margin-bottom: 1rem; border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,.08);
}
50
Performance et production
● En production : on minifie CSS/JS, on les regroupe, on les sert via un CDN ou un reverse proxy
(Nginx) avec un cache long.
● En d veloppement : on garde tout en clair pour faciliter le d bogage.
● Le serveur de dev de Flask n'est pas optimis pour les statiques — c'est Nginx qui doit s'en charger
en production.
51
é
é
é
Travaux pratiques
Partie 1
TP 1.1 — Mise en place et premier serveur
● Cr er un dossier monblog, y cr er un environnement virtuel, l'activer
● Installer Flask et figer les d pendances dans [Link]
● crire une application Flask renvoyant « Bienvenue l'ISI » sur la route /
● Lancer en mode debug et v rifier que la modification du message recharge automatiquement la
page
53
É
é
é
é
é
à
TP 1.2 — Routes et convertisseurs
● Ajouter une route /salut/<nom> qui affiche « Bonjour {nom} »
● Ajouter une route /carre/<int:n> qui affiche le carr de n
● Ajouter une route /tva/<float:ht> qui affiche le prix TTC 18 % de TVA, arrondi 2 d cimales
● Tester que /carre/abc renvoie bien une 404
54
é
à
à
é
TP 1.3 — Templates Jinja2
● Cr er un dossier templates et un gabarit [Link] avec en-t te, pied de page et zone de contenu
● Cr er [Link] h ritant de [Link]. La route / devra utiliser render_template
● Pr parer dans une vue une liste d'articles fictifs (titre, auteur, r sum ) et les afficher sous forme de
cartes via une boucle for et un template _carte_article.html inclus
● Ajouter un filtre |truncate(80) sur le r sum
55
é
é
é
é
é
é
ê
é
é
TP 1.4 — Fichiers statiques
● Cr er static/css/[Link] et l'inclure dans [Link] via url_for
● Ajouter un logo en static/img/ et l'afficher dans l'en-t te
● V rifier dans l'onglet R seau du navigateur que les ressources sont bien servies en 200 OK
56
é
é
é
ê
Crit res d' valuation de la Partie 1
● Le code respecte la PEP 8 (indentation, nommage snake_case)
● Aucune URL n'est cod e en dur dans les templates : tout passe par url_for
● Le fichier [Link] et un .gitignore correct sont pr sents
● Le projet est versionn avec Git (au moins 3 commits significatifs)
57
è
é
é
é
é
PARTIE 2
60
é
à
â
è
6.1 Pourquoi sortir d'[Link] ?
● Tant que l'application reste une page de code, [Link] suffit
● Mais d s que l'on d passe une dizaine de routes :
● Importations circulaires
● Code peu lisible
● Difficult tester
● Configuration m lang e la logique
● Deux outils Flask permettent de structurer proprement : la factory pattern et les Blueprints
61
è
é
à
é
é
é
à
6.2 La factory pattern
● Id e : au lieu de cr er l'objet Flask en variable globale, on l'instancie l'int rieur d'une fonction
create_app(config)
● Avantages :
● Plusieurs instances peuvent coexister (utile pour les tests)
● La configuration (dev/prod/test) est pass e en argument
● Les extensions sont initialis es de façon explicite et testable
62
é
é
é
é
à
é
app/__init__.py — create_app()
from flask import Flask
def create_app(nom_config="dev"):
app = Flask(__name__, instance_relative_config=True)
# 1) Charger la configuration
if nom_config == "prod":
[Link].from_object("[Link]")
elif nom_config == "test":
[Link].from_object("[Link]")
else:
[Link].from_object("[Link]")
# 4) Hooks d'erreurs
from .errors import enregistrer_handlers
enregistrer_handlers(app)
return app
63
[Link] — point d'entr e
import os
from app import create_app
if __name__ == "__main__":
[Link]()
$ export FLASK_APP=run:app
$ export FLASK_CONFIG=dev
$ flask run
64
é
6.3 Les Blueprints
● Un Blueprint est un « mini-Flask » : un objet qui regroupe routes, templates, statiques et erreurs.
● On enregistre ensuite le Blueprint sur l'application — avec ou sans pr fixe d'URL.
● C'est le m canisme officiel pour d couper l'application en modules m tier.
65
é
é
é
é
Blueprint « main »
app/views/[Link]
@bp_main.route("/")
def accueil():
return render_template("main/[Link]")
@bp_main.route("/contact")
def contact():
return render_template("main/[Link]")
R f rencer une URL d'un Blueprint : url_for("[Link]") — on pr fixe le nom de l'endpoint par le nom du Blueprint.
66
é
é
é
Blueprint « articles »
app/views/[Link]
@bp_articles.route("/")
def liste():
articles = [] # remplir plus tard depuis la BDD
return render_template("articles/[Link]", articles=articles)
@bp_articles.route("/<int:id>")
def detail(id):
if id <= 0:
abort(404)
return render_template("articles/[Link]", id=id)
67
à
6.4 Arborescence cible — MonBlog
monblog/
├── app/
│ ├── **init**.py # create_app()
│ ├── [Link] # db, login_manager, migrate, csrf
│ ├── [Link] # gestion d'erreurs globale
│ ├── models/
│ │ ├── [Link] # BaseModel abstraite
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ ├── views/ # blueprints
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ ├── forms/ # formulaires Flask-WTF
│ ├── services/ # logique métier
│ ├── [Link] # exceptions personnalisées
│ ├── templates/ # gabarits Jinja2
│ └── static/
├── con [Link]
├── [Link]
└── tests/
68
fi
Chapitre 7
70
è
é
è
é
é
é
é
7.1 Pourquoi des classes ?
● Une application web manipule en permanence des objets : utilisateurs, articles, commentaires,
commandes...
● Les repr senter par des classes Python apporte :
● Un type plut t qu'un dictionnaire flou
● Des m thodes pour le comportement m tier (publier(), valider_age()…)
● Une validation centralis e
● Un code lisible et auto-document
● Note : terme nos mod les h riteront de [Link] (SQLAlchemy). Mais on peut les concevoir
d'abord en pur Python pour bien s parer m tier et persistance.
71
é
é
à
ô
é
è
é
é
é
é
é
7.2 Premi re classe : User
from datetime import datetime
from [Link] import generate_password_hash, check_password_hash
class User:
"""Repr sente un utilisateur du blog."""
@property
def nom_complet(self) -> str:
return f"{[Link]} {[Link]}".strip()
73
è
è
é
é
7.3 Article et Commentaire
# app/models/[Link]
from datetime import datetime # app/models/[Link]
class Article: from datetime import datetime
def __init__(self, titre, contenu, auteur):
[Link] = None
[Link] = [Link]() class Commentaire:
[Link] = contenu def __init__(self, contenu, auteur):
[Link] = auteur [Link] = contenu
[Link] = False [Link] = auteur
self.cree_le = [Link]()
[Link] = None
[Link] = []
def publier(self): self.cree_le = [Link]()
if not [Link]:
raise ValueError("Un article doit avoir un titre.")
[Link] = True Le commentaire pointe vers son article — r f rence circulaire g r e par
def ajouter_commentaire(self, c): Article.ajouter_commentaire().
[Link] = self
[Link](c)
def resume(self, longueur=120):
return ([Link][:longueur] + "…"
if len([Link]) > longueur
else [Link])
74
é
é
é
é
7.4 Utiliser ces classes dans une vue
# app/views/[Link]
from flask import Blueprint, render_template
from ..[Link] import User
from ..[Link] import Article
@bp_main.route("/")
def accueil():
return render_template("main/[Link]", articles=ARTICLES)
75
é
é
é
Template associ
{% extends "[Link]" %}
{% block contenu %}
<h1>Derniers articles</h1>
{% for art in articles %}
<article class="carte">
<h3>{{ [Link] }}</h3>
<p>{{ [Link](150) }}</p>
<small>par {{ [Link].nom_complet }}
— {{ art.cree_le.strftime('%d/%m/%Y') }}</small>
</article>
{% endfor %}
{% endblock %}
76
é
Chapitre 8
H ritage, encapsulation,
polymorphisme
é
Objectifs du chapitre
● Mobiliser les piliers de la POO dans le contexte d'une application web
● Concevoir une classe abstraite BaseModel pour mutualiser le comportement
● Mettre en place un Repository r utilisable
78
é
8.1 Une BaseModel pour mutualiser
● De nombreux mod les partagent les m mes besoins :
● Avoir un id, des dates de cr ation/mise jour
● Une m thode to_dict() pour la s rialisation
● Un __repr__ standardis
● Plut t que de r p ter ce code, on l' crit une seule fois dans une classe parente abstraite.
79
ô
é
é
é
è
é
é
é
é
ê
à
BaseModel abstraite
from datetime import datetime ABC (Abstract Base Class)
from abc import ABC, abstractmethod
marque la classe comme abstraite : on ne peut
pas l'instancier directement.
class BaseModel(ABC):
"""Classe abstraite pour tous les mod les m tier."""
@abstractmethod oblige les classes filles
def __init__(self): fournir une impl mentation.
[Link] = None
self.cree_le = [Link]()
self.maj_le = [Link]() Excellent moyen de d finir un contrat partag .
def toucher(self):
self.maj_le = [Link]()
@abstractmethod
def valider(self):
"""L ve une exception si l'objet n'est pas valide."""
...
def to_dict(self):
return {k: v for k, v in self.__dict__.items()
if not [Link]("_")}
def __repr__(self):
return f"<{self.__class__.__name__} id={[Link]}>"
80
è
é
é
à
è
é
é
8.2 Faire h riter User de BaseModel
from .base import BaseModel
from [Link] import generate_password_hash, check_password_hash
class User(BaseModel):
def __init__(self, email, prenom, nom):
super().__init__()
[Link] = email
[Link] = prenom
[Link] = nom
self._mdp_hash = None
def valider(self):
if "@" not in [Link]:
raise ValueError("Email invalide.")
if not [Link] or not [Link]:
raise ValueError("Pr nom et nom sont obligatoires.")
@property
def nom_complet(self):
return f"{[Link]} {[Link]}"
81
é
é
8.3 Polymorphisme — un service pour tous les
mod les
Le polymorphisme se r v le utile lorsqu'on veut crire une fonction qui accepte n'importe quel mod le.
Tant que l'objet expose to_dict() et valider(), il est utilisable.
# app/services/[Link]
from typing import Iterable
from ..[Link] import BaseModel
82
è
é
è
é
è
Chapitre 9
Le pattern Repository
Objectifs du chapitre
● Comprendre l'int r t de s parer les vues de l'acc s aux donn es
● Mettre en place un Repository g n rique
● Pr parer la transition vers une vraie base de donn es
84
é
é
ê
é
é
é
è
é
é
9.1 Pourquoi un Repository ?
● Un Repository est une classe dont la responsabilit est de stocker, retrouver et lister des objets.
● Si demain on passe d'une liste en m moire PostgreSQL, on ne change que le Repository :
● Les vues continuent d'appeler les m mes m thodes : lister(), get(id), enregistrer(obj).
● C'est un excellent terrain d'entra nement la s paration des couches.
85
î
é
ê
à
à
é
é
é
9.2 Repository en m moire ( tape 1)
# app/repositories/base_repository.py
from typing import TypeVar, Generic
T = TypeVar("T")
class BaseRepository(Generic[T]):
def __init__(self):
self._items: dict[int, T] = {}
self._next_id = 1
86
é
é
Sp cialisation : ArticleRepository
# app/repositories/article_repository.py
from .base_repository import BaseRepository
from ..[Link] import Article
class ArticleRepository(BaseRepository[Article]):
def lister_publies(self) -> list[Article]:
return [a for a in [Link]() if [Link]]
87
é
9.3 Utilisation dans une vue
# app/views/[Link]
from flask import Blueprint, render_template, abort
from ..repositories.article_repository import ArticleRepository
@bp_articles.route("/")
def liste():
return render_template("articles/[Link]",
articles=repo_articles.lister_publies())
@bp_articles.route("/<int:id>")
def detail(id):
article = repo_articles.get(id)
if article is None:
abort(404)
return render_template("articles/[Link]", article=article)
88
é
é
Chapitre 10
Exceptions personnalis es
é
Objectifs du chapitre
● Concevoir une hi rarchie d'exceptions coh rente
● Distinguer erreurs m tier et erreurs techniques
● Convertir les exceptions en r ponses HTTP appropri es
90
é
é
é
é
é
10.1 Pourquoi des exceptions personnalis es ?
● Lever ValueError ou Exception partout finit par produire un code difficile diagnostiquer
● D finir ses propres exceptions, regroup es par domaine, permet :
● De distinguer rapidement la nature d'un probl me (UtilisateurInexistant ≠ DonneesInvalides)
● De convertir uniform ment ces exceptions en r ponses HTTP (404, 422, 401…)
● D'enrichir les exceptions avec un code, un message, des d tails de logging
91
é
é
é
è
é
é
à
é
10.2 D finition d'une hi rarchie
app/[Link]
class MonBlogError(Exception):
"""Classe de base pour toutes les exceptions m tier."""
code_http = 400
message = "Erreur applicative."
class RessourceIntrouvable(MonBlogError):
code_http = 404
message = "Ressource introuvable."
class DonneesInvalides(MonBlogError):
code_http = 422
message = "Donn es invalides."
class AuthentificationRequise(MonBlogError):
code_http = 401
message = "Authentification requise."
class AccesInterdit(MonBlogError):
code_http = 403
message = "Acc s refus ."
92
è
é
é
é
é
é
10.3 Lever et capter les exceptions
# Dans un service ou un repository
from .exceptions import RessourceIntrouvable, DonneesInvalides
def publier_article(article):
if not [Link]():
raise DonneesInvalides("Le titre est obligatoire.")
[Link]()
93
10.4 Conversion automatique en r ponse HTTP
app/[Link]
def enregistrer_handlers(app):
@[Link](MonBlogError)
def gerer_erreur_metier(err):
if [Link]("API_MODE"):
return jsonify(message=str(err), details=[Link]), err.code_http
return render_template("erreurs/[Link]",
message=str(err),
code=err.code_http), err.code_http
@[Link](404)
def not_found(_):
return render_template("erreurs/[Link]"), 404
@[Link](500)
def server_error(_):
return render_template("erreurs/[Link]"), 500
94
é
Strat gie globale
● Levez des exceptions m tier dans les services et repositories
● Convertissez-les en r ponses HTTP au niveau le plus haut (handlers globaux)
● Vos vues redeviennent simples et expressives
● B n fice : le code des routes ne contient plus que la logique de pr sentation.
95
é
é
é
é
é
é
Travaux pratiques
Partie 2
TP 2.1 — Refactoring vers la factory
● Convertir le projet du TP 1 en application factory : cr er create_app(), un fichier [Link] et un sous-
paquet app/
● D placer les routes existantes dans un Blueprint bp_main
● V rifier que flask --app run:app run fonctionne toujours
97
é
é
é
TP 2.2 — Mod les m tier
● Cr er la classe BaseModel abstraite avec valider() abstrait
● Impl menter User, Article, Commentaire en h ritant de BaseModel
● Ajouter une route /demo qui cr e 3 utilisateurs et 5 articles factices, puis les affiche
98
é
é
è
é
é
é
TP 2.3 — Repository et exceptions
● Impl menter BaseRepository et ArticleRepository (m moire)
● D finir la hi rarchie d'exceptions personnalis e et un handler global
● Faire en sorte qu'acc der /articles/9999 produise une page d'erreur 404 personnalis e via
RessourceIntrouvable
99
é
é
é
é
à
é
é
é
Pour aller plus loin
● Ajoutez un m canisme de filtres sur ArticleRepository : par mot-cl , par p riode
● Utilisez @property pour exposer une propri t calcul e (nombre de commentaires, est_recent…)
● Ajoutez des tests unitaires pour valider() et les Repository
100
é
é
é
é
é
é
PARTIE 3
PostgreSQL : installation et
premiers pas
Objectifs du chapitre
● Installer PostgreSQL sur Linux, macOS et Windows
● Cr er une base de donn es et un utilisateur d di l'application
● Se connecter avec psql et les outils graphiques
103
é
é
é
é
à
11.1 Pourquoi PostgreSQL ?
● SGBD relationnel libre, robuste et conforme aux standards SQL
● Fonctionnalit s avanc es :
● Transactions ACID
● Types JSON natifs (JSONB)
● Full-text search
● Fonctions et proc dures
● H ritage de tables
● Base de donn es par d faut de nombreuses soci t s et services cloud (Heroku, Render, Railway…)
104
é
é
é
é
é
é
é
é
11.2 Installation
Ubuntu / Debian
macOS (Homebrew)
Windows
T l chargez l'installeur officiel sur [Link]/download/windows. Il installe galement pgAdmin (interface graphique) et configure le service
Windows automatiquement.
105
é
é
é
11.3 Cr ation d'une base et d'un utilisateur
$ sudo -u postgres psql
106
é
11.4 URL de connexion SQLAlchemy
● Format g n ral :
● postgresql+psycopg2://USER:MOTDEPASSE@HOTE:PORT/NOM_BDD
● Pour notre projet :
● postgresql+psycopg2://monblog_user:monMotDePasse@localhost:5432/monblog
● Cette URL sera stock e dans .env, jamais en dur dans le code.
107
é
é
é
Chapitre 12
SQLAlchemy et Flask-SQLAlchemy
Objectifs du chapitre
● Comprendre ce qu'est un ORM et son int r t
● Installer Flask-SQLAlchemy et le pilote psycopg2
● Configurer la connexion PostgreSQL via [Link]
● D finir un premier mod le persistant
109
é
è
à
é
ê
12.1 Qu'est-ce qu'un ORM ?
● Un ORM (Object Relational Mapper) fait correspondre des classes Python des tables d'une base de
donn es.
● Avantages :
● S curit : chappe les param tres et vite les injections SQL
● Portabilit : fonctionne sur PostgreSQL, MySQL, SQLite…
● Expressivit : m thodes filter, join, group_by…
● SQLAlchemy est le standard Python ; Flask-SQLAlchemy en est l'int gration officielle pour Flask.
110
é
é
é
é
é
é
é
è
é
é
à
12.2 Installation
111
12.3 Initialisation c t application
app/[Link] app/__init__.py (extrait)
def create_app(nom_config="dev"):
app = Flask(__name__)
[Link].from_object(
f"config.{nom_config.capitalize()}Config")
db.init_app(app)
Pattern : on cr e l'instance db sans application, puis on l'attache avec init_app(app) dans la factory.
112
é
ô
é
12.4 Configuration via [Link]
# [Link]
import os
class BaseConfig:
SECRET_KEY = [Link]("SECRET_KEY", "changez-moi-en-prod")
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevConfig(BaseConfig):
DEBUG = True
SQLALCHEMY_DATABASE_URI = [Link](
"DEV_DATABASE_URL",
"postgresql+psycopg2://monblog_user:monMotDePasse@localhost:5432/monblog_dev"
)
class TestingConfig(BaseConfig):
TESTING = True
SQLALCHEMY_DATABASE_URI = [Link](
"TEST_DATABASE_URL",
"postgresql+psycopg2://monblog_user:monMotDePasse@localhost:5432/monblog_test"
)
class ProdConfig(BaseConfig):
DEBUG = False
SQLALCHEMY_DATABASE_URI = [Link]["DATABASE_URL"]
113
Variables d'environnement et .env
● Stockez les secrets (SECRET_KEY, mots de passe BDD) dans un fichier .env, JAMAIS dans le code
● Le paquet python-dotenv permet de charger .env automatiquement :
● pip install python-dotenv
● V rifiez que .env est bien dans .gitignore — jamais commit !
114
é
é
Chapitre 13
116
é
é
è
13.1 Premier mod le : User
# app/models/[Link]
from datetime import datetime
from [Link] import generate_password_hash, check_password_hash
from ..extensions import db
class User([Link]):
__tablename__ = "users"
id = [Link]([Link], primary_key=True)
email = [Link]([Link](120), unique=True, nullable=False, index=True)
prenom = [Link]([Link](50), nullable=False)
nom = [Link]([Link](50), nullable=False)
mdp_hash = [Link]([Link](255), nullable=False)
actif = [Link]([Link], default=True, nullable=False)
cree_le = [Link]([Link], default=[Link], nullable=False)
def __repr__(self):
return f"<User {[Link]}>"
117
è
13.2 Article et la relation OneToMany
# app/models/[Link]
from datetime import datetime
from ..extensions import db
class Article([Link]):
__tablename__ = "articles"
id = [Link]([Link], primary_key=True)
titre = [Link]([Link](200), nullable=False)
contenu = [Link]([Link], nullable=False)
publie = [Link]([Link], default=False, nullable=False)
cree_le = [Link]([Link], default=[Link], nullable=False)
maj_le = [Link]([Link], default=[Link],
onupdate=[Link], nullable=False)
commentaires = [Link](
"Commentaire", back_populates="article",
cascade="all, delete-orphan"
)
118
é
é
è
13.3 Commentaire
# app/models/[Link]
from datetime import datetime
from ..extensions import db
class Commentaire([Link]):
__tablename__ = "commentaires"
id = [Link]([Link], primary_key=True)
contenu = [Link]([Link], nullable=False)
cree_le = [Link]([Link], default=[Link], nullable=False)
119
13.4 Relations Many-to-Many : les tags
Un article peut avoir plusieurs tags, et un tag est partag entre plusieurs articles.
C'est une relation N-N qui n cessite une table d'association.
# app/models/[Link]
from ..extensions import db
# Table d'association
articles_tags = [Link](
"articles_tags",
[Link]("article_id", [Link], [Link]("[Link]"), primary_key=True),
[Link]("tag_id", [Link], [Link]("[Link]"), primary_key=True),
)
class Tag([Link]):
__tablename__ = "tags"
id = [Link]([Link], primary_key=True)
libelle = [Link]([Link](40), unique=True, nullable=False)
articles = [Link](
"Article", secondary=articles_tags, back_populates="tags"
)
120
é
é
13.5 Types de colonnes utiles
Type SQLAlchemy Type Python Type SQL
[Link] int INTEGER
[Link] int BIGINT
[Link](n) str VARCHAR(n)
[Link] str TEXT
[Link] bool BOOLEAN
[Link] float DOUBLE PRECISION
[Link](p, s) Decimal NUMERIC(p, s)
[Link] / [Link] date / datetime DATE / TIMESTAMP
[Link] dict / list JSONB (Postgres)
[Link](...) Enum ENUM
121
Sch ma relationnel de MonBlog
122
é
Chapitre 14
124
é
é
é
è
é
14.1 Pourquoi des migrations ?
● chaque modification de mod le (ajout de colonne, suppression de table…), la base doit suivre
● db.create_all() est utile pour d marrer mais ne sait pas modifier une base existante
● Les migrations enregistrent chaque changement dans un script num rot :
● On peut avancer, reculer, synchroniser plusieurs environnements
● Le sch ma est versionn comme le code
125
À
é
é
é
è
é
é
14.2 Installation et configuration
(.venv) $ pip install Flask-Migrate
(.venv) $ pip freeze > [Link]
app/[Link] app/__init__.py
126
14.3 Cycle de vie d'une migration
# 1) Initialiser le dossier migrations/ (une seule fois)
$ flask db init
# 6) Voir l'historique
$ flask db history
$ flask db current
127
é
é
è
é
é
é
é
à
è
Toujours relire la migration
● Alembic est puissant mais pas omniscient
● Exemple : il ne d tecte pas le renommage d'une colonne (il propose suppression + ajout)
● Relisez SYST MATIQUEMENT le script avant flask db upgrade
● Un script de migration peut tre dit la main pour ajuster les op rations
128
É
é
ê
é
é
à
é
Chapitre 15
130
î
é
ô
è
15.1 La session SQLAlchemy
● La session est l'unit de travail
● On y ajoute, modifie ou supprime des objets, puis on commit() pour valider la transaction
● Si une erreur survient, rollback() annule tout
● Flask-SQLAlchemy g re pour nous une session par requ te HTTP
131
é
è
ê
15.2 Create — cr er un objet
from [Link] import db
from [Link] import User
[Link](u)
[Link]()
print([Link]) # g n r automatiquement par PostgreSQL
add() place l'objet dans la session ; commit() crit en base. Apr s le commit, l'id est rempli.
132
é
é
é
é
é
è
15.3 Read — lire des objets
# Lire par cl primaire (SQLAlchemy 2.x)
u = [Link](User, 1)
133
é
è
é
15.4 / 15.5 — Update et Delete
Update Delete
u = [Link](User, 1) u = [Link](User, 1)
[Link] = "Amy" [Link](u)
[Link]()
[Link]() # cascade supprime les articles
# si configur ainsi
La cascade « all, delete-orphan » sur la relation Articles assure que les articles d'un utilisateur supprim sont aussi effac s.
134
é
é
é
15.6 Vue compl te : cr er un article
# app/views/[Link]
from flask import Blueprint, request, redirect, url_for, render_template, flash
from ..extensions import db
from ..[Link] import Article
return render_template("articles/[Link]")
135
é
é
è
è
é
Chapitre 16
137
é
16.1 Filtres
from sqlalchemy import or_, and_
# galit simple
[Link].filter_by(publie=True).all()
# Combiner conditions
[Link](
and_([Link] == True,
or_([Link]("%flask%"),
[Link]("%python%")))
).all()
# IN
[Link]([Link].in_([1, 2, 3])).all()
# BETWEEN
from datetime import datetime
debut = datetime(2026, 1, 1)
fin = datetime(2026, 6, 30)
[Link](Article.cree_le.between(debut, fin)).all()
138
É
é
à
16.2 Jointures
from [Link] import User
from [Link] import Article
139
é
é
16.3 Agr gations
from sqlalchemy import func
140
é
16.4 Pagination
@bp_articles.route("/")
def liste():
page = [Link]("page", 1, type=int)
pagination = ([Link]
.filter_by(publie=True)
.order_by(Article.cree_le.desc())
.paginate(page=page, per_page=10, error_out=False))
return render_template("articles/[Link]",
articles=[Link],
pagination=pagination)
{# templates/articles/[Link] (extrait) #}
{% if pagination.has_prev %}
<a href="{{ url_for('[Link]', page=pagination.prev_num) }}">« Pr c dent</a>
{% endif %}
Page {{ [Link] }} / {{ [Link] }}
{% if pagination.has_next %}
<a href="{{ url_for('[Link]', page=pagination.next_num) }}">Suivant »</a>
{% endif %}
141
é
é
Chapitre 17
Configuration multi-
environnements
Objectifs du chapitre
● S parer configuration de d veloppement, de test et de production
● S curiser les secrets via .env
● Initialiser la base au d marrage en mode dev
143
é
é
é
é
17.1 Le fichier .env
.env ( NE PAS commiter)
FLASK_APP=[Link]
FLASK_CONFIG=dev
FLASK_DEBUG=1
SECRET_KEY=ma-cle-tres-secrete-en-production
DEV_DATABASE_URL=postgresql+psycopg2://monblog_user:pass@localhost:5432/monblog_dev
TEST_DATABASE_URL=postgresql+psycopg2://monblog_user:pass@localhost:5432/monblog_test
# [Link]
import os
from dotenv import load_dotenv
load_dotenv()
from app import create_app
app = create_app([Link]("FLASK_CONFIG", "dev"))
144
à
17.2 Commande personnalis e pour initialiser la BDD
app/[Link]
import click
from [Link] import with_appcontext
from .extensions import db
from .[Link] import User
@[Link]("init-db")
@with_appcontext
def init_db():
"""Cr e les tables et un utilisateur admin de d monstration."""
db.create_all()
if not [Link].filter_by(email="admin@[Link]").first():
admin = User(email="admin@[Link]", prenom="Admin", nom="ISI")
admin.definir_mdp("Admin1234!")
[Link](admin)
[Link]()
[Link]("Base initialis e.")
145
é
é
é
é
é
Travaux pratiques
Partie 3
TP 3.1 — Cr ation de la base et des mod les
● Cr er la base de donn es monblog_dev sous PostgreSQL et l'utilisateur d di
● Configurer Flask-SQLAlchemy via [Link] et un fichier .env
● D finir les mod les User, Article, Commentaire, Tag
● Initialiser Flask-Migrate, g n rer la premi re migration, l'appliquer
147
é
é
è
é
é
é
é
è
é
é
è
TP 3.2 — Donn es et CRUD
● crire une commande Flask seed qui cr e 3 utilisateurs et 10 articles r partis entre eux
● Construire une page /articles pagin e par 5 articles, du plus r cent au plus ancien
● Ajouter le d tail /articles/<id> avec contenu, auteur et commentaires
● Ajouter un formulaire de cr ation (Chapitre 15) — sans authentification pour l'instant
148
É
é
é
é
é
é
é
é
TP 3.3 — Requ tes avanc es
● Impl menter une recherche /articles?q=mot qui filtre les titres et r sum s (ilike)
● Ajouter un filtre par tag : /articles?tag=python
● Afficher en page d'accueil le top 5 des auteurs (par nombre d'articles publi s)
149
é
ê
é
é
é
é
Conseils de d bogage
● Activez SQLALCHEMY_ECHO = True en d veloppement pour voir les requ tes SQL g n r es
● Utilisez psql et la commande \d articles pour visualiser le sch ma r ellement cr
● En cas de migration rat e, supprimez la BDD de dev et recr ez-la — c'est une BDD jetable
150
é
é
é
é
é
é
ê
é
é
é
é
é
PARTIE 4
Formulaires et authentification
Chapitre 18
153
é
é
ê
é
é
è
18.1 Pourquoi pas un simple <form> ?
● crire la main les formulaires avec [Link] fonctionne, mais :
● Code r p titif
● Pas de validation
● Pas de protection CSRF
● Pas de gestion fine des erreurs
● WTForms apporte une couche d'abstraction propre
● Flask-WTF y greffe la protection CSRF et l'int gration avec Flask
154
É
é
à
é
é
18.2 Installation
(.venv) $ pip install Flask-WTF email-validator
(.venv) $ pip freeze > [Link]
def create_app(nom_config="dev"):
app = Flask(__name__)
[Link].from_object(...)
db.init_app(app)
migrate.init_app(app, db)
csrf.init_app(app)
return app
155
CSRF (Cross-Site Request Forgery)
● Attaque consistant faire ex cuter une requ te un utilisateur authentifi son insu
● Flask-WTF injecte un jeton secret dans chaque formulaire
● la soumission, le jeton est v rifi — un attaquant ne peut pas le deviner
● Toujours laisser CSRFProtect activ en production.
156
À
à
é
é
é
é
ê
à
é
à
18.3 Premier formulaire : ArticleForm
app/forms/[Link]
class ArticleForm(FlaskForm):
titre = StringField("Titre", validators=[
DataRequired(message="Le titre est obligatoire."),
Length(min=3, max=200, message="Entre 3 et 200 caract res.")
])
contenu = TextAreaField("Contenu", validators=[
DataRequired(message="Le contenu est obligatoire."),
Length(min=10, message="Au moins 10 caract res.")
])
publier = BooleanField("Publier imm diatement")
submit = SubmitField("Enregistrer")
157
é
è
è
18.4 Utilisation dans une vue
from flask import Blueprint, render_template, redirect, url_for, flash
from ..[Link] import ArticleForm
from ..extensions import db
from ..[Link] import Article
<div>
{{ [Link] }}
{{ [Link](class_="champ", placeholder="Titre de l'article") }}
{% for err in [Link] %}
<p class="erreur">{{ err }}</p>
{% endfor %}
</div>
<div>
{{ [Link] }}
{{ [Link](rows=10, class_="champ") }}
{% for err in [Link] %}
<p class="erreur">{{ err }}</p>
{% endfor %}
</div>
159
é
18.6 Validateurs int gr s
Validateur R le
DataRequired() Champ non vide
Length(min, max) Longueur de la cha ne
Email() Adresse email valide (n cessite email-validator)
EqualTo('autre_champ') Doit tre gal un autre champ (confirmation de mot de passe)
NumberRange(min, max) Plage num rique
Regexp(motif) Doit matcher une expression r guli re
URL() URL valide
Optional() D sactive les autres validateurs si vide
160
ô
é
ê
é
é
à
î
é
é
è
é
é
Validateur personnalis
class InscriptionForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
# ...
def validate_email(self, field):
from ..[Link] import User
if [Link].filter_by(email=[Link]).first():
raise ValidationError("Cet email est d j utilis .")
Convention : une m thode validate_<nom_du_champ> est automatiquement appel e par WTForms la validation.
161
é
é
é
à
é
é
à
Chapitre 19
Authentification : concepts et
inscription
Objectifs du chapitre
● Distinguer authentification et autorisation
● Hasher proprement les mots de passe avec Werkzeug
● Cr er un formulaire et une vue d'inscription
163
é
19.1 Authentification vs Autorisation
● Authentification : prouver son identit (« qui tes-vous ? ») via un email et un mot de passe.
● Autorisation : accorder ou refuser une action (« avez-vous le droit ? ») — par exemple, seul l'auteur
d'un article peut le supprimer.
● Les deux m canismes sont distincts mais souvent trait s ensemble dans une application.
164
é
é
ê
é
19.2 Pourquoi hasher les mots de passe ?
● Si la base de donn es est compromise, l'attaquant ne doit pas pouvoir lire les mots de passe
● On stocke une empreinte cryptographique (hash) avec un sel al atoire
● Werkzeug fournit generate_password_hash et check_password_hash :
● Utilise PBKDF2-SHA256 avec un sel par d faut
● Co t ajustable pour rester s r face l' volution du mat riel
165
û
é
û
à
é
é
é
é
NE JAMAIS faire
● Stocker les mots de passe en clair en base
● Utiliser md5() ou sha1() seuls (trop rapides brute-forcer)
● R utiliser le m me sel pour tout le monde
● Loguer un mot de passe, m me en debug
166
À
é
ê
ê
à
19.3 Formulaires d'inscription et de connexion
app/forms/[Link]
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from [Link] import DataRequired, Email, Length, EqualTo, ValidationError
class InscriptionForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
prenom = StringField("Pr nom", validators=[DataRequired(), Length(2, 50)])
nom = StringField("Nom", validators=[DataRequired(), Length(2, 50)])
mdp = PasswordField("Mot de passe", validators=[
DataRequired(), Length(min=8, message="8 caract res minimum.")
])
mdp_confirm = PasswordField("Confirmation", validators=[
DataRequired(),
EqualTo("mdp", message="Les mots de passe ne correspondent pas.")
])
submit = SubmitField("Cr er mon compte")
class ConnexionForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
mdp = PasswordField("Mot de passe", validators=[DataRequired()])
se_souvenir = BooleanField("Se souvenir de moi")
submit = SubmitField("Se connecter")
167
é
é
é
à
é
è
19.4 Vue d'inscription
from flask import Blueprint, render_template, redirect, url_for, flash
from ..[Link] import InscriptionForm
from ..extensions import db
from ..[Link] import User
168
é
é
Chapitre 20
170
é
é
20.1 Installation
(.venv) $ pip install Flask-Login
(.venv) $ pip freeze > [Link]
app/[Link]
login_manager = LoginManager()
login_manager.login_view = "[Link]"
login_manager.login_message = "Veuillez vous connecter pour continuer."
login_manager.login_message_category = "warning"
# app/__init__.py (extrait)
from .extensions import db, migrate, csrf, login_manager
def create_app(...):
# ...
login_manager.init_app(app)
return app
171
20.2 Pr parer le mod le User
Pour tre compatible avec Flask-Login, User doit h riter de UserMixin (qui fournit is_authenticated, is_active, get_id…) et l'application
doit d clarer un user_loader.
# app/models/[Link] (ajouts)
from flask_login import UserMixin
from ..extensions import db, login_manager
@login_manager.user_loader
def charger_user(user_id):
return [Link](User, int(user_id))
172
ê
é
é
é
è
20.3 Vues login et logout
from flask_login import login_user, logout_user, login_required, current_user
from flask import request, flash, redirect, url_for, render_template
from ..[Link] import ConnexionForm
form = ConnexionForm()
if form.validate_on_submit():
user = [Link].filter_by(email=[Link]()).first()
if user and user.verifier_mdp([Link]):
login_user(user, remember=form.se_souvenir.data)
flash(f"Bonjour {[Link]} !", "success")
next_url = [Link]("next") or url_for("[Link]")
return redirect(next_url)
flash("Identifiants invalides.", "danger")
return render_template("auth/[Link]", form=form)
@bp_auth.route("/deconnexion")
@login_required
def deconnexion():
logout_user()
flash(" bient t !", "info")
return redirect(url_for("[Link]"))
173
À
ô
Le param tre ?next=...
● Quand un utilisateur non connect tente d'acc der une page prot g e, Flask-Login le redirige vers
la page de connexion avec ?next=URL
● R cup rez ce param tre pour le ramener sur la page d'origine apr s login
● S curit : validez que next pointe bien vers une URL interne (pas vers un site externe) pour viter
les attaques de redirection ouverte.
174
é
é
é
é
è
è
é
é
à
è
é
é
é
20.4 current_user dans les templates
[Link]
<nav>
<a href="{{ url_for('[Link]') }}">Accueil</a>
{% if current_user.is_authenticated %}
<span>Bonjour, {{ current_user.prenom }}</span>
<a href="{{ url_for('[Link]') }}">Nouvel article</a>
<a href="{{ url_for('[Link]') }}">D connexion</a>
{% else %}
<a href="{{ url_for('[Link]') }}">Connexion</a>
<a href="{{ url_for('[Link]') }}">Inscription</a>
{% endif %}
</nav>
current_user est disponible globalement dans tous les templates — pas besoin de le passer explicitement.
175
é
20.5 Prot ger des routes
from flask_login import login_required, current_user
176
é
Chapitre 21
178
É
é
é
é
é
ô
21.1 Rappel sur les d corateurs
● Un d corateur est une fonction qui prend une autre fonction en argument et renvoie une nouvelle
fonction enrichie
● @login_required en est un exemple — Flask-Login v rifie l'utilisateur, puis appelle votre vue
● Voyons comment en cr er un nous-m mes pour ajouter une v rification de r le
179
é
é
ê
é
é
é
ô
21.2 Ajouter un r le au mod le User
# app/models/[Link] (ajout)
class User(UserMixin, [Link]):
# ...
role = [Link]([Link](20), default="utilisateur", nullable=False)
# r les possibles : "utilisateur", "auteur", "admin"
@property
def est_admin(self) -> bool:
return [Link] == "admin"
@property
def est_auteur(self) -> bool:
return [Link] in ("auteur", "admin")
N'oubliez pas de cr er une migration : flask db migrate -m "Ajout du r le" && flask db upgrade
180
ô
é
ô
ô
è
21.3 D corateur @admin_required
app/[Link]
def admin_required(view):
@wraps(view)
def wrapper(*args, **kwargs):
if not current_user.is_authenticated:
abort(401)
if not current_user.est_admin:
abort(403)
return view(*args, **kwargs)
return wrapper
@wraps(view) pr serve le nom et la docstring de la vue d'origine — indispensable pour le bon fonctionnement de url_for().
181
é
é
21.4 Utilisation et v rification de propri t
Page admin :
@bp_admin.route("/utilisateurs")
@login_required
@admin_required
def lister_utilisateurs():
users = [Link].order_by(User.cree_le.desc()).all()
return render_template("admin/[Link]", users=users)
Partie 4
TP 4.1 — Formulaires Flask-WTF
● Convertir le formulaire de cr ation d'article du TP 3 en ArticleForm WTForms
● Ajouter un formulaire d'ajout de commentaire (CommentaireForm) sur la page de d tail
● V rifier que la protection CSRF fonctionne (essayer de soumettre sans token : doit chouer en 400)
184
é
é
é
é
TP 4.2 — Inscription et connexion
● Impl menter la vue d'inscription compl te avec validation
● Impl menter la connexion/d connexion avec Flask-Login
● Adapter [Link] pour afficher dynamiquement les liens selon current_user
185
é
é
é
è
TP 4.3 — Autorisations
● Ajouter le champ role User et cr er une migration
● crire le d corateur @admin_required et l'utiliser sur une page /admin/utilisateurs
● Emp cher la modification d'un article par quelqu'un d'autre que son auteur ou un admin
186
É
ê
é
à
é
Crit res d' valuation de la Partie 4
● Les mots de passe sont stock s hash s en base (v rifiable avec psql)
● Aucun secret n'appara t dans le code source : tout passe par .env
● Toutes les routes sensibles sont prot g es par @login_required
● Les messages flash s'affichent avec leur cat gorie (success, danger, info, warning)
187
è
é
î
é
é
é
é
é
é
PARTIE 5
é
Chapitre 22
Structure de projet
professionnelle
Objectifs du chapitre
● R capituler une structure de projet Flask de qualit production
● Comprendre la responsabilit de chaque dossier
● Mettre en place les outils compl mentaires : tests, qualit , logs
190
é
é
é
é
é
22.1 Arborescence cible finale
monblog/
├── app/
│ ├── **init**.py # create_app()
│ ├── [Link] # db, migrate, login_manager, csrf
│ ├── [Link] # MonBlogError, RessourceIntrouvable…
│ ├── [Link] # handlers globaux
│ ├── [Link] # admin_required, etc.
│ ├── [Link] # commandes Flask personnalisées
│ ├── models/
│ ├── views/ # blueprints
│ ├── forms/
│ ├── services/ # logique métier réutilisable
│ ├── repositories/ # accès aux données
│ ├── templates/
│ └── static/
├── migrations/
├── tests/
│ ├── [Link]
│ ├── test_models.py
│ └── test_routes.py
├── con [Link]
├── [Link]
├── [Link]
191
fi
22.2 Responsabilit s par dossier
Dossier / Fichier R le
app/__init__.py Application factory ; assemblage des extensions et blueprints
app/[Link] Instances partag es (db, migrate, login_manager…) sans application
app/models/ Repr sentation des entit s persist es ([Link])
app/views/ Blueprints — orchestration HTTP (formulaires, redirections, templates)
app/forms/ Formulaires WTForms (validation, rendu)
app/services/ Logique m tier complexe, ind pendante du web
app/repositories/ Acc s et requ tes BDD sp cifiques
tests/ Tests automatis s (pytest)
[Link] Classes de configuration (Dev, Test, Prod)
[Link] Point d'entr e pour le serveur de dev
192
ô
è
é
é
é
ê
é
é
é
é
é
é
é
R gle de d pendance entre couches
● views → services → repositories → models
● Les imports doivent toujours suivre ce sens
● Une vue n' crit JAMAIS de SQL
● Un service n'importe JAMAIS request ou render_template
● Le respect strict de ces r gles facilite norm ment les tests et la maintenance
193
è
é
é
è
é
é
22.3 Logging
# app/__init__.py (extrait)
import logging
from [Link] import RotatingFileHandler
import os
def configurer_logs(app):
if [Link]("DEBUG"):
return # le serveur de dev fait d j l'affaire
[Link]("logs", exist_ok=True)
handler = RotatingFileHandler("logs/[Link]",
maxBytes=1_000_000, backupCount=5)
[Link]([Link](
"%(asctime)s [%(levelname)s] %(name)s : %(message)s"
))
[Link]([Link])
[Link](handler)
[Link]([Link])
[Link]("MonBlog d marre.")
194
é
é
à
22.4 Tests automatis s — [Link]
tests/[Link]
import pytest
from app import create_app
from [Link] import db
@[Link]()
def app():
app = create_app("test")
with app.app_context():
db.create_all()
yield app
[Link]()
db.drop_all()
@[Link]()
def client(app):
return app.test_client()
tests/test_routes.py
def test_accueil_repond_200(client):
rep = [Link]("/")
assert rep.status_code == 200
assert b"MonBlog" in [Link]
195
é
Lancer les tests
(.venv) $ pip install pytest pytest-cov
(.venv) $ pytest -v
============================ test session starts =============================
tests/test_routes.py::test_accueil_repond_200 PASSED [ 20%]
tests/test_models.py::test_user_definir_mdp PASSED [ 40%]
tests/test_models.py::test_article_publier PASSED [ 60%]
...
============================== 5 passed in 0.42s =============================
196
Chapitre 23
198
é
ô
é
ô
é
é
é
23.1 Pages d'erreur personnalis es
app/[Link]
def enregistrer_handlers(app):
@[Link](404)
def page_404(_):
return render_template("erreurs/[Link]"), 404
@[Link](403)
def page_403(_):
return render_template("erreurs/[Link]"), 403
@[Link](500)
def page_500(err):
from .extensions import db
[Link]()
[Link]("Erreur 500 : %s", err)
return render_template("erreurs/[Link]"), 500
@[Link](MonBlogError)
def erreur_metier(err):
return render_template("erreurs/[Link]",
code=err.code_http,
message=str(err)), err.code_http
199
é
Template d'erreur 404
{# templates/erreurs/[Link] #}
{% extends "[Link]" %}
{% block contenu %}
<h1>Oups, page introuvable</h1>
<p>La page que vous cherchez n'existe pas ou a t d plac e.</p>
<a href="{{ url_for('[Link]') }}">Retour l'accueil</a>
{% endblock %}
200
à
é
é
é
é
23.2 Le pi ge du rollback
● Si une erreur se produit pendant une transaction, la session reste « cass e » jusqu' un rollback
explicite
● Toujours appeler [Link]() dans le handler 500
● Sans cela, toutes les requ tes suivantes choueront en cascade jusqu'au prochain red marrage du
worker
201
è
ê
é
é
à
é
23.3 D tection pr coce avec abort()
abort() permet de court-circuiter une vue avec un code HTTP donn , et de d clencher automatiquement le handler associ .
@bp_articles.route("/<int:id>/supprimer", methods=["POST"])
@login_required
def supprimer(id):
article = [Link].get_or_404(id)
if article.auteur_id != current_user.id and not current_user.est_admin:
abort(403) # d clenche page_403
[Link](article)
[Link]()
flash("Article supprim .", "success")
return redirect(url_for("[Link]"))
202
é
é
é
é
é
é
é
Chapitre 24
é
é
Objectifs du chapitre
● Identifier les principales failles web (OWASP Top 10)
● S curiser cookies, sessions et entr es utilisateur
● Configurer HTTPS et les en-t tes de s curit
204
é
ê
é
é
é
24.1 Le top 10 OWASP, version courte
Vuln rabilit Comment Flask vous aide
Injection SQL Toujours via SQLAlchemy ; jamais de f-string avec entr e utilisateur
XSS (script inject ) Jinja2 chappe le HTML par d faut. M fiez-vous de |safe
CSRF Flask-WTF / CSRFProtect ins re un jeton
Mauvaise authentification Flask-Login + Werkzeug pour les hash
Exposition de donn es Ne pas commiter .env ; s parer dev/prod
Mauvaise configuration D sactiver DEBUG en production. SECRET_KEY robuste
Composants vuln rables pip-audit ; mise jour r guli re
205
é
é
é
é
à
é
é
é
é
é
è
è
é
é
é
24.2 Cookies et sessions
[Link] — production
class ProdConfig(BaseConfig):
SESSION_COOKIE_SECURE = True # uniquement HTTPS
SESSION_COOKIE_HTTPONLY = True # pas accessible en JS
SESSION_COOKIE_SAMESITE = "Lax" # protection CSRF suppl mentaire
REMEMBER_COOKIE_SECURE = True
REMEMBER_COOKIE_HTTPONLY = True
En-t tes de s curit avec Flask-Talisman :
def create_app(...):
if not [Link]:
Talisman(app, content_security_policy={
"default-src": "'self'",
"img-src": "'self' data:",
"style-src": "'self' 'unsafe-inline'",
})
206
ê
é
é
é
24.3 Validation c t serveur — toujours
● Ne jamais faire confiance aux donn es reçues, m me valid es en JavaScript
● La validation c t client am liore l'exp rience utilisateur mais n'est PAS une s curit
● La validation c t serveur (WTForms, v rifications applicatives) est la seule barri re fiable
● « Never trust the client » — principe fondamental.
207
ô
ô
é
é
é
ô
é
é
é
é
ê
é
é
è
é
24.4 Limitation de d bit
(.venv) $ pip install Flask-Limiter
limiter = Limiter(key_func=get_remote_address)
208
é
û
è
Chapitre 25
D ploiement
é
Objectifs du chapitre
● Comprendre la cha ne Nginx + Gunicorn + Flask + PostgreSQL
● D ployer sur Render ou Railway en quelques minutes
● Configurer un VPS (Ubuntu) de A Z (notions)
210
é
î
à
25.1 Le serveur de dev n'est PAS pour la production
● Le serveur de flask run est mono-thread, lent et non s curis
● En production, on utilise :
● Un serveur WSGI comme Gunicorn ou uWSGI
● Un reverse proxy comme Nginx qui sert les fichiers statiques et termine SSL
● Cette architecture permet de scaler horizontalement (plusieurs workers) et de g rer le HTTPS
proprement
211
é
é
é
25.2 Sch ma de d ploiement classique
212
é
é
25.3 Pr parer l'application
(.venv) $ pip install gunicorn
(.venv) $ pip freeze > [Link]
(.venv) $ echo "web: gunicorn 'run:app'" > Procfile
[Link]
import os
from dotenv import load_dotenv
load_dotenv()
from app import create_app
app = create_app([Link]("FLASK_CONFIG", "prod"))
213
é
25.4 D ploiement sur Render
● Pousser le projet sur GitHub
● Sur [Link], cr er un Web Service partir du d p t
● Configurer Build Command : pip install -r [Link]
● Configurer Start Command : gunicorn run:app
● Ajouter les variables d'environnement (DATABASE_URL fourni par le service Postgres de Render)
● Lancer la migration via le shell distant : flask db upgrade
214
é
é
à
é
ô
25.5 D ploiement sur Railway
● La logique est tr s proche de Render
● Cr er un projet, ajouter un service Postgres, connecter le d p t GitHub
● Configurer la commande de d marrage : gunicorn run:app
● Railway d tecte automatiquement Python via [Link]
● Avantage : interface simple et tarification claire
215
é
é
é
è
é
é
ô
25.6 Notions sur un VPS Ubuntu
# Sur un VPS fra chement provisionn
$ sudo apt update && sudo apt install -y python3-venv nginx postgresql
216
î
é
é
é
Nginx + HTTPS via Let's Encrypt
# /etc/nginx/sites-available/monblog
server {
listen 80;
server_name [Link];
location /static/ {
alias /home/ubuntu/monblog/app/static/;
expires 30d;
}
location / {
proxy_pass [Link]
proxy_set_header X-Forwarded-For $remote_addr;
}
}
217
Avant la mise en production
● Mettre DEBUG = False et une SECRET_KEY de 32+ caract res al atoires
● Sauvegarder r guli rement la base avec pg_dump
● Surveiller les logs et l'utilisation CPU/m moire (htop, journalctl)
● Documenter le processus de mise jour
● Tester en staging (environnement identique la production) avant chaque release importante.
218
é
è
à
é
à
è
é
Travaux pratiques
Projet final
Sujet : MonBlog — application compl te
● l'issue de ce cours, vous livrez une application Flask compl te qui combine TOUS les concepts vus
● valuation sur : qualit du code, s curit , coh rence de l'architecture, exp rience utilisateur
220
À
É
é
é
é
é
è
è
é
Cahier des charges (1/2)
● Inscription et connexion s curis es (Flask-WTF, Flask-Login, Werkzeug)
● Trois r les : visiteur (non connect ), utilisateur, admin
● CRUD complet sur les articles : seul l'auteur ou un admin peut modifier/supprimer
● Syst me de commentaires sur chaque article (par les utilisateurs connect s uniquement)
● Tags pour les articles avec relation N-N et page /tags/<libelle>
● Pagination de la liste d'articles (10 par page)
221
è
ô
é
é
é
é
Cahier des charges (2/2)
● Recherche par mot-cl sur le titre et le contenu
● Pages d'erreurs personnalis es (404, 403, 500)
● Configuration multi-environnements (dev, test, prod) via .env
● Migrations Flask-Migrate maintenues jour
● Au moins 5 tests automatis s (pytest)
● D ploiement effectif sur Render ou Railway
● [Link] d crivant : installation locale, lancement des tests, d ploiement
222
é
é
é
é
é
à
é
Grille d' valuation indicative
Crit re Points
Architecture (factory + blueprints + repositories) 3
Mod les, relations et migrations 3
Authentification et autorisation 3
Formulaires et validation 2
Templates et exp rience utilisateur 2
Gestion des erreurs et logs 2
S curit (CSRF, hash, en-t tes, rate-limit) 2
Tests automatis s 1
D ploiement effectif 1
Qualit du code et README 1
Total 20
223
é
é
è
è
é
é
é
é
é
ê
tapes recommand es
● Reprendre le projet du TP 4.3 et compl ter les manques (tags, recherche, pagination)
● Mettre en place tests/[Link] et crire au moins 5 tests
● Personnaliser les pages 404/403/500
● Activer Flask-Talisman et Flask-Limiter
● Pr parer le projet pour la production : Procfile, gunicorn, variables d'env
● D ployer sur Render et noter l'URL publique dans le README
224
É
é
é
é
é
é
Annexes
Annexe A — R f rentiel de commandes (1/2)
Environnement Python
Flask CLI
Migrations
226
é
é
Annexe A — R f rentiel de commandes (2/2)
PostgreSQL — psql
Git essentiel
$ git init && git add . && git commit -m "Initial commit"
$ git remote add origin [Link]
$ git push -u origin main
$ git status / log / diff
227
é
é
é
Annexe B — Glossaire
Terme D finition courte
WSGI Interface standard Python entre serveur HTTP et application
Blueprint Module Flask regroupant routes et templates d'un domaine
Factory Fonction create_app() construisant une application param trable
ORM Outil traduisant classes Python ↔ tables relationnelles
Migration Script versionn modifiant le sch ma de la base
CSRF Attaque consistant ex cuter une action l'insu d'un utilisateur connect
XSS Injection de script malveillant dans une page web
Hash Empreinte unidirectionnelle d'une donn e (mot de passe)
Sel Donn e al atoire ajout e avant le hash
D corateur Fonction qui transforme une autre fonction (@login_required)
Reverse proxy Serveur (Nginx) intercal devant l'application (Gunicorn)
228
é
é
é
é
é
à
é
é
é
é
é
à
é
é
Annexe C — Pour aller plus loin
● API REST avec Flask-RESTful ou Flask-Smorest, s rialisation avec marshmallow
● API GraphQL avec strawberry-graphql ou Ariadne
● T ches asynchrones avec Celery + Redis (envoi d'emails, t ches longues)
● WebSockets avec Flask-SocketIO (notifications en temps r el)
● D couvrir FastAPI (alternative moderne pour les API) et Django (full-stack)
● Conteneurisation avec Docker ; orchestration avec docker-compose
● Int gration continue avec GitHub Actions : tests chaque push
● Observabilit : Sentry pour les erreurs, Prometheus + Grafana pour les m triques
229
â
é
é
é
à
é
â
é
é
Merci de votre attention !
Questions ?