Cours Python · Module 5: Flask et applications web
Routage et fonctions de vue - techniques avancées
Dans cette leçon12
Bon retour ! Darwin est là, prêt à te montrer des techniques de routage avancées dans Flask !
Dans la leçon précédente tu as créé une application Flask simple avec des routes de base. Maintenant tu vas apprendre des techniques professionnelles utilisées dans les applications en production !
Analogie Safari : Le routage est comme un réseau de sentiers de safari - tu dois savoir rediriger les touristes (les utilisateurs), gérer différents itinéraires, réagir aux erreurs (se perdre) et construire des cartes (construction d'URL) !
Rappel - les bases du routage
1from flask import Flask
2
3app = Flask(__name__)
4
5@app.route('/')
6def home():
7 return "Page d'accueil"
8
9@app.route('/species/<int:species_id>')
10def show_species(species_id):
11 return f"ID de l'espèce : {species_id}"Route = le décorateur @app.route() qui associe une URL à une fonction de vue.
Maintenant tu vas apprendre des techniques avancées !
Plusieurs routes pour une seule fonction
Tu peux associer plusieurs URL à la même fonction :
1@app.route('/')
2@app.route('/home')
3@app.route('/index')
4def home():
5 return "<h1>Page d'accueil de Safari Database</h1>"L'utilisateur peut visiter :
/→home()s'exécute/home→home()s'exécute/index→home()s'exécute
Pourquoi ? Plusieurs entrées possibles vers le même contenu - plus pratique pour les utilisateurs !
Analogie : C'est comme plusieurs entrées vers le même habitat - les touristes peuvent entrer par le nord, le sud ou l'est, mais ils voient tous les mêmes lions !
Trailing slash - différence entre / et sans /
Flask distingue les URL avec / à la fin et sans :
1# Avec trailing slash (par défaut)
2@app.route('/species/')
3def list_species():
4 return "Liste des espèces"
5
6# Sans trailing slash
7@app.route('/about')
8def about():
9 return "À propos du système"Comment ça marche ?
Avec trailing slash (/species/) :
/species/→ fonctionne/species→ redirection 301 vers/species/
Sans trailing slash (/about) :
/about→ fonctionne/about/→ 404 Not Found
Bonne pratique : Utilise avec trailing slash pour les « dossiers » (collections), sans slash pour les « fichiers » (ressources individuelles) :
1@app.route('/species/') # Liste (dossier)
2@app.route('/species/<int:id>') # Élément unique (fichier)Méthodes HTTP - en détail
Flask permet de gérer différentes méthodes HTTP dans la même route :
1from flask import request
2
3@app.route('/species', methods=['GET', 'POST', 'PUT', 'DELETE'])
4def species_handler():
5 if request.method == 'GET':
6 return "Récupérer la liste des espèces"
7
8 elif request.method == 'POST':
9 return "Créer une nouvelle espèce"
10
11 elif request.method == 'PUT':
12 return "Mettre à jour une espèce"
13
14 elif request.method == 'DELETE':
15 return "Supprimer une espèce"Meilleure approche - des fonctions séparées :
1@app.route('/species', methods=['GET'])
2def list_species():
3 return "Liste des espèces"
4
5@app.route('/species', methods=['POST'])
6def create_species():
7 return "Espèce créée"Ou encore mieux - utilise les décorateurs raccourcis :
1@app.get('/species')
2def list_species():
3 return "Liste des espèces (GET)"
4
5@app.post('/species')
6def create_species():
7 return "Espèce créée (POST)"
8
9@app.put('/species/<int:id>')
10def update_species(id):
11 return f"Espèce {id} mise à jour (PUT)"
12
13@app.delete('/species/<int:id>')
14def delete_species(id):
15 return f"Espèce {id} supprimée (DELETE)"Analogie : Les différentes méthodes HTTP sont comme différentes activités en safari - GET c'est l'observation (sûr), POST c'est ajouter une nouvelle espèce à la base de données (changement d'état), DELETE c'est supprimer une entrée !
L'objet request - accéder aux données
L'objet request contient toutes les informations sur la requête HTTP :
1from flask import request
2
3@app.route('/search')
4def search():
5 # Paramètres de requête (?name=lion&habitat=savanna)
6 species_name = request.args.get('name')
7 habitat = request.args.get('habitat')
8 page = request.args.get('page', 1, type=int) # 1 par défaut
9
10 return f"""
11 <h1>Recherche</h1>
12 <p>Espèce : {species_name}</p>
13 <p>Habitat : {habitat}</p>
14 <p>Page : {page}</p>
15 """
16
17@app.route('/add-species', methods=['POST'])
18def add_species():
19 # Données du formulaire (depuis un formulaire HTML)
20 name = request.form.get('name')
21 population = request.form.get('population', type=int)
22
23 return f"Ajouté : {name} (population : {population})"
24
25@app.route('/api/species', methods=['POST'])
26def api_create_species():
27 # Données JSON (depuis fetch/API)
28 data = request.get_json()
29 name = data.get('name')
30 population = data.get('population')
31
32 return {"message": f"Créé {name}", "id": 123}
33
34@app.route('/info')
35def request_info():
36 return f"""
37 <h1>Informations sur la requête</h1>
38 <p><strong>URL :</strong> {request.url}</p>
39 <p><strong>Méthode :</strong> {request.method}</p>
40 <p><strong>Chemin :</strong> {request.path}</p>
41 <p><strong>User-Agent :</strong> {request.user_agent}</p>
42 <p><strong>IP :</strong> {request.remote_addr}</p>
43 <p><strong>Cookies :</strong> {request.cookies}</p>
44 <p><strong>En-têtes :</strong> {dict(request.headers)}</p>
45 """Attributs utiles de request :
request.args- paramètres de requête (?key=value)request.form- données de formulaire (POST)request.get_json()- données JSONrequest.files- fichiers téléversésrequest.cookies- cookiesrequest.headers- en-têtes HTTPrequest.method- méthode HTTP (GET, POST, etc.)request.url- URL complèterequest.path- chemin de l'URLrequest.remote_addr- IP du client
Exemple - moteur de recherche Safari :
1@app.route('/search')
2def search():
3 query = request.args.get('q', '')
4 filter_type = request.args.get('type', 'all')
5 sort = request.args.get('sort', 'name')
6
7 # Simulation de recherche
8 results = [
9 {"name": "Lion", "type": "mammal"},
10 {"name": "Guépard", "type": "mammal"},
11 ]
12
13 # Filtrer par type
14 if filter_type != 'all':
15 results = [r for r in results if r['type'] == filter_type]
16
17 html = f"<h1>Résultats pour : {query}</h1>"
18 html += f"<p>Type : {filter_type}, Tri : {sort}</p>"
19
20 for result in results:
21 html += f"<p>{result['name']} ({result['type']})</p>"
22
23 return htmlTeste :
/search?q=lion/search?q=lion&type=mammal&sort=population
L'objet response - renvoyer des réponses
Flask convertit automatiquement les valeurs renvoyées en objets Response :
1from flask import make_response, jsonify
2
3# Chaîne simple (statut 200)
4@app.route('/hello')
5def hello():
6 return "Hello Safari!"
7
8# Chaîne + code de statut
9@app.route('/created')
10def created():
11 return "Espèce créée !", 201
12
13# Chaîne + statut + en-têtes
14@app.route('/custom')
15def custom():
16 return "Réponse personnalisée", 200, {'X-Custom-Header': 'Safari'}
17
18# Réponse JSON
19@app.route('/api/species/<int:id>')
20def api_species(id):
21 species = {"id": id, "name": "Lion", "population": 120}
22 return jsonify(species)
23
24# make_response() - plus de contrôle
25@app.route('/cookie')
26def set_cookie():
27 response = make_response("Cookie défini !")
28 response.set_cookie('user_id', '123', max_age=3600)
29 return responseFormat de retour :
return "texte"→ 200 OKreturn "texte", 201→ 201 Createdreturn "texte", 404→ 404 Not Foundreturn jsonify(data)→ réponse JSONreturn make_response(...)→ Response personnalisée
Redirections
Utilise redirect() pour rediriger l'utilisateur vers une autre URL :
1from flask import redirect, url_for
2
3@app.route('/old-species')
4def old_species():
5 # Rediriger vers la nouvelle URL
6 return redirect('/species')
7
8@app.route('/species/<int:id>')
9def show_species(id):
10 # Si l'espèce n'existe pas, rediriger vers la liste
11 species = get_species_by_id(id)
12
13 if species is None:
14 return redirect('/species')
15
16 return f"<h1>{species['name']}</h1>"
17
18@app.route('/login', methods=['POST'])
19def login():
20 username = request.form.get('username')
21 password = request.form.get('password')
22
23 if authenticate(username, password):
24 # Succès - rediriger vers le tableau de bord
25 return redirect('/dashboard')
26 else:
27 # Erreur - rediriger de nouveau vers la connexion
28 return redirect('/login?error=invalid')Analogie : Une redirection est comme un changement d'itinéraire de safari - si un sentier est fermé (l'espèce n'existe pas), le guide redirige le groupe vers un autre itinéraire (la liste des espèces) !
url_for() - construire des URL
url_for() construit des URL à partir du nom de la fonction plutôt que d'URL codées en dur :
1from flask import url_for
2
3@app.route('/')
4def home():
5 # Au lieu de '/species' codé en dur
6 species_url = url_for('list_species')
7
8 # Au lieu de '/species/1' codé en dur
9 lion_url = url_for('show_species', species_id=1)
10
11 return f"""
12 <h1>Safari Database</h1>
13 <a href="{species_url}">Liste des espèces</a> |
14 <a href="{lion_url}">Voir le lion</a>
15 """
16
17@app.route('/species')
18def list_species():
19 return "<h1>Liste des espèces</h1>"
20
21@app.route('/species/<int:species_id>')
22def show_species(species_id):
23 return f"<h1>ID de l'espèce : {species_id}</h1>"Pourquoi url_for() ?
Routage centralisé - change l'URL en un seul endroit
Plus sûr - Flask échappe automatiquement les paramètres
Plus lisible - tu utilises des noms de fonctions au lieu d'URL
Exemple avec paramètres de requête :
1@app.route('/search')
2def search():
3 return "Résultats de recherche"
4
5# Construire une URL avec des query params
6url = url_for('search', q='lion', type='mammal')
7# Résultat : /search?q=lion&type=mammalRedirection avec url_for() :
1@app.route('/old-url')
2def old_url():
3 return redirect(url_for('list_species'))Gestionnaires d'erreurs
Crée des pages d'erreur personnalisées pour 404, 500, etc. :
1# 404 - Not Found
2@app.errorhandler(404)
3def page_not_found(error):
4 return """
5 <!DOCTYPE html>
6 <html>
7 <head>
8 <title>404 - Safari Database</title>
9 <style>
10 body {
11 font-family: Arial, sans-serif;
12 text-align: center;
13 padding: 100px;
14 background-color: #f4f1de;
15 }
16 h1 { color: #d62828; font-size: 72px; }
17 </style>
18 </head>
19 <body>
20 <h1>404</h1>
21 <h2>Espèce introuvable !</h2>
22 <p>La ressource demandée n'existe pas dans Safari Database.</p>
23 <a href="/">← Retour à la page d'accueil</a>
24 </body>
25 </html>
26 """, 404
27
28# 500 - Internal Server Error
29@app.errorhandler(500)
30def internal_error(error):
31 return """
32 <h1>500 - Erreur du serveur</h1>
33 <p>Quelque chose s'est mal passé. Nos lions travaillent à la réparation !</p>
34 <a href="/">← Retour à la page d'accueil</a>
35 """, 500
36
37# Exception personnalisée
38class SpeciesNotFoundError(Exception):
39 pass
40
41@app.errorhandler(SpeciesNotFoundError)
42def handle_species_not_found(error):
43 return f"<h1>Espèce introuvable</h1><p>{error}</p>", 404
44
45@app.route('/species/<int:id>')
46def show_species(id):
47 species = get_species_by_id(id)
48
49 if species is None:
50 raise SpeciesNotFoundError(f"L'espèce avec l'ID {id} n'existe pas")
51
52 return f"<h1>{species['name']}</h1>"Analogie : Les gestionnaires d'erreurs sont comme des procédures d'urgence en safari - quand un touriste se perd (404) ou qu'il y a une panne de véhicule (500), le guide sait comment réagir !
Hooks Before/After Request
Flask permet d'exécuter du code avant et après chaque requête :
1import time
2
3# Avant chaque requête
4@app.before_request
5def before_request():
6 # Enregistrer l'heure de début
7 request.start_time = time.time()
8
9 # Journaliser la requête
10 print(f"{request.method} {request.path}")
11
12# Après chaque requête
13@app.after_request
14def after_request(response):
15 # Calculer le temps d'exécution
16 if hasattr(request, 'start_time'):
17 duration = time.time() - request.start_time
18 print(f"Temps d'exécution : {duration:.3f}s")
19
20 # Ajouter un en-tête personnalisé
21 response.headers['X-Safari-Database'] = 'v1.0'
22
23 return response
24
25# Après chaque requête (même en cas d'erreur)
26@app.teardown_request
27def teardown_request(exception=None):
28 if exception:
29 print(f"Une erreur est survenue : {exception}")
30
31 # Fermer la connexion à la base de données
32 # db.close()Cas d'usage :
- Authentification (vérifier si l'utilisateur est connecté)
- Journalisation des requêtes
- ⏱Mesure du temps d'exécution
- Ouverture/fermeture de la connexion à la base de données
- En-têtes de sécurité
Analogie : Les hooks sont comme des procédures pré/post-safari - avant : vérifier l'équipement, compter les touristes ; après : consigner les observations, s'assurer que tout le monde est revenu !
Exemple pratique - Safari Database v2
Créons une version améliorée de Safari Database en utilisant toutes les techniques apprises !
1# app.py
2from flask import Flask, request, redirect, url_for, make_response, jsonify
3import time
4
5app = Flask(__name__)
6
7# Données de test
8species_db = {
9 1: {"id": 1, "name": "Lion", "scientific_name": "Panthera leo", "population": 120},
10 2: {"id": 2, "name": "Éléphant", "scientific_name": "Loxodonta africana", "population": 450},
11 3: {"id": 3, "name": "Guépard", "scientific_name": "Acinonyx jubatus", "population": 7100},
12}
13next_id = 4
14
15# --- Hooks ---
16@app.before_request
17def log_request():
18 request.start_time = time.time()
19 print(f"[{request.method}] {request.path}")
20
21@app.after_request
22def log_response(response):
23 if hasattr(request, 'start_time'):
24 duration = time.time() - request.start_time
25 print(f"Statut : {response.status_code} | Temps : {duration:.3f}s")
26 return response
27
28# --- Routes ---
29@app.route('/')
30@app.route('/home')
31def home():
32 return f"""
33 <!DOCTYPE html>
34 <html>
35 <head>
36 <title>Safari Database</title>
37 <style>
38 body {{ font-family: Arial; max-width: 800px; margin: 50px auto; background: #f4f1de; }}
39 h1 {{ color: #2c5f2d; }}
40 nav {{ margin: 20px 0; }}
41 nav a {{ margin-right: 15px; color: #2c5f2d; text-decoration: none; font-weight: bold; }}
42 nav a:hover {{ text-decoration: underline; }}
43 </style>
44 </head>
45 <body>
46 <h1>Safari Database</h1>
47 <nav>
48 <a href="{url_for('home')}">Accueil</a>
49 <a href="{url_for('list_species')}">Liste des espèces</a>
50 <a href="{url_for('add_species_form')}">Ajouter une espèce</a>
51 <a href="{url_for('search')}">Rechercher</a>
52 </nav>
53 <h2>Bienvenue sur Safari Database !</h2>
54 <p>La base de données contient <strong>{len(species_db)} espèces</strong>.</p>
55 <p><a href="{url_for('list_species')}">Tout voir →</a></p>
56 </body>
57 </html>
58 """
59
60@app.get('/species')
61def list_species():
62 species_html = ""
63 for spec in species_db.values():
64 species_html += f"""
65 <div style="background: white; padding: 15px; margin: 10px 0; border-radius: 8px;">
66 <h3>{spec['name']}</h3>
67 <p><strong>Population :</strong> {spec['population']}</p>
68 <a href="{url_for('show_species', species_id=spec['id'])}">Détails</a> |
69 <a href="{url_for('delete_species', species_id=spec['id'])}" style="color: red;">Supprimer</a>
70 </div>
71 """
72
73 return f"""
74 <!DOCTYPE html>
75 <html>
76 <head><title>Liste des espèces</title></head>
77 <body style="font-family: Arial; max-width: 800px; margin: 50px auto; background: #f4f1de;">
78 <h1>Liste des espèces ({len(species_db)})</h1>
79 {species_html}
80 <p><a href="{url_for('home')}">← Retour</a></p>
81 </body>
82 </html>
83 """
84
85@app.route('/species/<int:species_id>')
86def show_species(species_id):
87 spec = species_db.get(species_id)
88
89 if spec is None:
90 # Utiliser le gestionnaire d'erreur personnalisé
91 return f"<h1>404</h1><p>L'espèce {species_id} n'existe pas</p>", 404
92
93 return f"""
94 <!DOCTYPE html>
95 <html>
96 <head><title>{spec['name']}</title></head>
97 <body style="font-family: Arial; max-width: 800px; margin: 50px auto; background: #f4f1de;">
98 <h1>{spec['name']}</h1>
99 <div style="background: white; padding: 30px; border-radius: 12px;">
100 <p><strong>Nom scientifique :</strong> {spec['scientific_name']}</p>
101 <p><strong>Population :</strong> {spec['population']} individus</p>
102 <p><strong>ID :</strong> {spec['id']}</p>
103 </div>
104 <p style="margin-top: 20px;">
105 <a href="{url_for('list_species')}">← Liste</a> |
106 <a href="{url_for('home')}">← Accueil</a>
107 </p>
108 </body>
109 </html>
110 """
111
112@app.route('/species/add', methods=['GET'])
113def add_species_form():
114 return f"""
115 <!DOCTYPE html>
116 <html>
117 <head><title>Ajouter une espèce</title></head>
118 <body style="font-family: Arial; max-width: 800px; margin: 50px auto; background: #f4f1de;">
119 <h1>Ajouter une nouvelle espèce</h1>
120 <form method="POST" action="{url_for('add_species')}" style="background: white; padding: 30px; border-radius: 12px;">
121 <p><label>Nom : <input type="text" name="name" required></label></p>
122 <p><label>Nom scientifique : <input type="text" name="scientific_name" required></label></p>
123 <p><label>Population : <input type="number" name="population" required></label></p>
124 <button type="submit" style="padding: 10px 20px; background: #2c5f2d; color: white; border: none; border-radius: 4px;">Ajouter</button>
125 </form>
126 <p><a href="{url_for('home')}">← Retour</a></p>
127 </body>
128 </html>
129 """
130
131@app.post('/species/add')
132def add_species():
133 global next_id
134
135 name = request.form.get('name')
136 scientific_name = request.form.get('scientific_name')
137 population = request.form.get('population', type=int)
138
139 # Ajouter à la base de données
140 new_species = {
141 "id": next_id,
142 "name": name,
143 "scientific_name": scientific_name,
144 "population": population
145 }
146 species_db[next_id] = new_species
147 next_id += 1
148
149 # Rediriger vers les détails de la nouvelle espèce
150 return redirect(url_for('show_species', species_id=new_species['id']))
151
152@app.route('/species/<int:species_id>/delete')
153def delete_species(species_id):
154 if species_id in species_db:
155 del species_db[species_id]
156
157 # Rediriger vers la liste
158 return redirect(url_for('list_species'))
159
160@app.route('/search')
161def search():
162 query = request.args.get('q', '').lower()
163
164 if not query:
165 return f"""
166 <h1>Recherche</h1>
167 <form method="GET">
168 <input type="text" name="q" placeholder="Saisis le nom d'une espèce">
169 <button type="submit">Rechercher</button>
170 </form>
171 <a href="{url_for('home')}">← Retour</a>
172 """
173
174 # Recherche
175 results = [s for s in species_db.values() if query in s['name'].lower()]
176
177 results_html = f"<h2>Trouvés : {len(results)}</h2>"
178 for spec in results:
179 results_html += f"<p><a href='{url_for('show_species', species_id=spec['id'])}'>{spec['name']}</a></p>"
180
181 return f"""
182 <h1>Résultats pour : "{query}"</h1>
183 {results_html}
184 <p><a href="{url_for('search')}">← Nouvelle recherche</a></p>
185 """
186
187# --- Points d'accès API ---
188@app.get('/api/species')
189def api_list_species():
190 return jsonify(list(species_db.values()))
191
192@app.get('/api/species/<int:species_id>')
193def api_get_species(species_id):
194 spec = species_db.get(species_id)
195 if spec is None:
196 return jsonify({"error": "Not found"}), 404
197 return jsonify(spec)
198
199# --- Gestionnaires d'erreurs ---
200@app.errorhandler(404)
201def page_not_found(error):
202 return f"""
203 <div style="text-align: center; padding: 100px; background: #f4f1de;">
204 <h1 style="font-size: 72px; color: #d62828;">404</h1>
205 <h2>Introuvable !</h2>
206 <p><a href="{url_for('home')}">← Retour à la page d'accueil</a></p>
207 </div>
208 """, 404
209
210if __name__ == '__main__':
211 print("Démarrage de Safari Database v2...")
212 app.run(debug=True)Teste l'application :
- http://localhost:5000/ - page d'accueil
- http://localhost:5000/species - liste des espèces
- http://localhost:5000/species/1 - détails du lion
- http://localhost:5000/species/add - ajouter une espèce
- http://localhost:5000/search?q=lion - recherche
- http://localhost:5000/api/species - API JSON
Résumé
Dans cette leçon tu as appris :
- Plusieurs routes pour une seule fonction
- Le trailing slash (/ vs sans /)
- Les méthodes HTTP (GET, POST, PUT, DELETE)
- L'objet request (args, form, json, headers)
- L'objet response (make_response, jsonify)
- Les redirections (redirect, url_for)
- La construction d'URL avec
url_for() - Les gestionnaires d'erreurs (404, 500, personnalisés)
- Les hooks Before/After request
- L'application pratique Safari Database v2
Analogie Safari finale : Le routage avancé est comme un système professionnel de sentiers de safari - tu peux rediriger les touristes (redirect), gérer les égarements (404), mesurer le temps d'itinéraire (hooks) et construire des cartes à partir de noms plutôt que de coordonnées (url_for) !
Prochaine leçon : Darwin te montrera les templates Jinja2 - fini les chaînes HTML codées en dur, place aux templates dynamiques !
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment définit-on un paramètre dynamique « species_id » dans une URL Flask ?
2. Comment forcer le paramètre « id » à être de type int dans Flask ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Range le décorateur de route avec un paramètre dynamique de type int :
- Clique dans l'ordre
Range une fonction de vue qui accepte un paramètre species_id :
- Éditeur de code
Écris une fonction de vue avec un paramètre dynamique id de type int.
- Tri vertical
Range le code de gestion d'une erreur 404 dans Flask :