Leçon 09-5 — Automatiser de vraies tâches
1. Objectif
À la fin de cette leçon tu sauras : parcourir des dossiers avec pathlib (glob, iterdir), renommer et déplacer des fichiers en masse avec Path.rename et shutil — toujours en testant d'abord sur un dossier bac-à-sable et en mode simulation (« dry run »), générer un rapport automatique qui assemble API et CSV en un fichier de sortie, et structurer un script d'automatisation complet qui annonce ce qu'il fait et gère ses erreurs.
2. Pourquoi c'est important
C'est la leçon où tout le niveau converge : venv + requests + JSON + CSV
pathlib= des scripts qui font à ta place les corvées répétitives —
ranger un dossier Téléchargements, renommer 200 photos, produire chaque semaine le même rapport. L'automatisation est LA raison pour laquelle la moitié des gens apprennent Python. Mais elle a un prix : un script qui modifie de vrais fichiers peut faire de vrais dégâts — pas un traceback, des fichiers écrasés ou perdus. Cette leçon enseigne donc autant une discipline de sécurité (bac-à-sable, dry run, garde-fous) qu'une technique. Cette discipline est exactement celle des outils professionnels : git, terraform, rsync ont tous un mode simulation.
3. Explication simple
Automatiser une corvée de fichiers, c'est toujours la même boucle : lister → décider → agir.
from pathlib import Path
folder = Path("sandbox")
for file in folder.glob("*.jpg"): # LISTER : tous les .jpg du dossier
new_name = "photo-" + file.name # DÉCIDER : le nouveau nom
print(f"{file.name} -> {new_name}") # AGIR... ou plutôt : SIMULER d'abord
glob("*.jpg") signifie « tous les fichiers dont le nom se termine par .jpg » — le * remplace n'importe quoi. Et la vraie action (renommer) n'apparaît qu'à la toute fin, protégée par la règle des trois étapes :
- Bac-à-sable : on copie quelques fichiers dans un dossier de test — jamais d'essai sur les vrais fichiers.
- Dry run : le script AFFICHE ce qu'il ferait, sans rien toucher. C'est le mode par défaut.
- Exécution réelle : seulement quand la simulation affiche exactement ce qu'on veut, on passe un drapeau explicite (
--apply) pour agir.
Un script qui saute ces étapes n'est pas « rapide », il est dangereux.
4. Explication approfondie
a) La boîte à outils pathlib du parcours de dossiers (⏩ les bases de Path : niveau 05) :
folder.iterdir() # tout le contenu du dossier (fichiers ET sous-dossiers)
folder.glob("*.pdf") # les fichiers correspondant au motif
folder.glob("**/*.pdf") # pareil, mais RÉCURSIF (sous-dossiers inclus)
file.name # "rapport final.pdf"
file.stem # "rapport final" (sans extension)
file.suffix # ".pdf"
file.is_file() # True/False — filtre les sous-dossiers
file.stat().st_mtime # date de dernière modification (timestamp)
Deux subtilités : glob renvoie les résultats dans un ordre non garanti — sorted(folder.glob(...)) si l'ordre compte (et il compte, pour des dry runs comparables). Et sous Windows, glob("*.jpg") attrape aussi photo.JPG (système insensible à la casse), mais sur Linux/macOS non — ne compte pas dessus : filtre avec file.suffix.lower() == ".jpg" pour un script portable.
b) Renommer, déplacer : rename et shutil.
file.rename(new_path) # renomme OU déplace (même disque)
shutil.move(str(src), str(dst)) # déplace, même entre disques
shutil.copy2(src, dst) # copie avec métadonnées (dates…)
shutil.rmtree(folder) # ⚠️ supprime un dossier ENTIER, sans corbeille
Piège majeur : sous Windows, rename vers un chemin déjà existant lève FileExistsError... et shutil.move vers un fichier existant l'ÉCRASE sans un mot. D'où le garde-fou systématique avant toute action : if destination.exists(): ... (renoncer, ou suffixer -1, -2…). Et shutil.rmtree ne passe PAS par la corbeille : dans un script d'apprentissage, interdis-toi toute suppression — déplace vers un dossier a-trier/ ou corbeille/ à la place ; supprimer, c'est toi qui le feras à la main après vérification.
c) Le dry run n'est pas une option, c'est une architecture. Le mode simulation se code en UN booléen qui contrôle chaque action :
def move_file(src, dst, apply=False):
print(f"{'[RÉEL]' if apply else '[SIMULATION]'} {src.name} -> {dst}")
if apply:
shutil.move(str(src), str(dst))
Points cardinaux : le défaut est apply=False (l'oubli est sans danger — principe de sécurité par défaut) ; la simulation affiche exactement ce que le réel fera (même code de décision, seule l'action finale est conditionnée) ; et le passage au réel exige un geste explicite de l'utilisateur (--apply sur la ligne de commande, via sys.argv, ⏩ niveau 05).
d) Idempotence : relancer sans casser. Un bon script d'automatisation peut être lancé DEUX fois sans aggraver les choses. Relance ton rangeur sur un dossier déjà rangé : il doit dire « rien à faire », pas re-déplacer images/ dans images/images/ ni planter sur un doublon. Concrètement : ignorer les fichiers déjà à leur place, tester exists() avant de créer (mkdir(exist_ok=True)), et gérer les collisions de noms. Teste toujours le scénario « deuxième lancement » — c'est lui qui révèle les scripts fragiles.
e) Le rapport automatique : assembler les leçons. Deuxième famille d'automatisation : produire régulièrement un fichier de synthèse. Le motif est toujours : collecter (API via requests, fichiers CSV via csv, dossiers via pathlib) → calculer (agrégats du 09-4) → écrire (texte, CSV, JSON — horodaté). Une nouveauté d'état d'esprit : un script d'automatisation a vocation à tourner sans humain devant — chaque source peut échouer, et le rapport doit le DIRE (météo : indisponible) plutôt que planter à 3 h du matin ou, pire, écrire des données fausses en silence.
Difficulté honnête : techniquement, cette leçon n'introduit presque rien — elle assemble. La difficulté est ailleurs : la rigueur. C'est la première fois que ton code a des effets irréversibles hors de son propre dossier. Le réflexe bac-à-sable + dry run doit devenir physique, comme regarder avant de traverser. Prends le temps de le rendre automatique ici, sur des fichiers de test — pas le jour où tu lances un script sur tes photos de famille.
5. Exemples commentés
Exemple 1 — fabriquer le bac-à-sable (à réutiliser partout)
# make_sandbox.py — create a disposable test folder with fake files
from pathlib import Path
sandbox = Path("sandbox")
sandbox.mkdir(exist_ok=True) # exist_ok : pas d'erreur si déjà là
fake_files = ["photo1.jpg", "photo2.JPG", "rapport.pdf", "notes.txt",
"archive.zip", "IMG_2026.png", "vieux rapport.pdf"]
for name in fake_files:
(sandbox / name).write_text("fake content", encoding="utf-8")
print(f"{len(fake_files)} fichiers créés dans {sandbox.resolve()}")
Sortie :
7 fichiers créés dans C:\Users\jtron\Claude\learnpython\sandbox
Casser ce dossier n'a AUCUNE conséquence : on peut le supprimer et le régénérer en une commande. C'est ça, un bac-à-sable.
Exemple 2 — lister et inspecter avec glob
from pathlib import Path
sandbox = Path("sandbox")
for file in sorted(sandbox.glob("*")): # sorted : ordre stable
if file.is_file(): # ignorer d'éventuels dossiers
print(f"{file.name:<22} stem={file.stem:<15} suffix={file.suffix.lower()}")
Sortie :
IMG_2026.png stem=IMG_2026 suffix=.png
archive.zip stem=archive suffix=.zip
notes.txt stem=notes suffix=.txt
photo1.jpg stem=photo1 suffix=.jpg
photo2.JPG stem=photo2 suffix=.jpg
rapport.pdf stem=rapport suffix=.pdf
vieux rapport.pdf stem=vieux rapport suffix=.pdf
Note suffix.lower() : photo2.JPG est bien normalisé en .jpg — la comparaison d'extensions se fait TOUJOURS en minuscules.
Exemple 3 — renommage en masse, dry run d'abord
# rename_photos.py — add a date prefix to every image, dry run by default
import sys
from pathlib import Path
APPLY = "--apply" in sys.argv # ⏩ sys.argv : niveau 05
sandbox = Path("sandbox")
prefix = "2026-07-"
count = 0
for file in sorted(sandbox.iterdir()):
if not file.is_file() or file.suffix.lower() not in (".jpg", ".png"):
continue # pas une image : on saute
if file.name.startswith(prefix):
continue # déjà renommé : idempotence !
target = file.with_name(prefix + file.name)
if target.exists(): # garde-fou anti-écrasement
print(f"[CONFLIT] {target.name} existe déjà, non touché")
continue
tag = "[RÉEL]" if APPLY else "[SIMULATION]"
print(f"{tag} {file.name} -> {target.name}")
if APPLY:
file.rename(target)
count += 1
mode = "renommés" if APPLY else "à renommer (relance avec --apply)"
print(f"{count} fichiers {mode}")
Sortie du dry run (python rename_photos.py) :
[SIMULATION] IMG_2026.png -> 2026-07-IMG_2026.png
[SIMULATION] photo1.jpg -> 2026-07-photo1.jpg
[SIMULATION] photo2.JPG -> 2026-07-photo2.JPG
3 fichiers à renommer (relance avec --apply)
Tout y est : défaut inoffensif, même logique dans les deux modes, garde-fou exists(), idempotence (le startswith fait qu'une relance dit « 0 fichiers »).
Exemple 4 — trier un dossier par type, avec shutil.move
# sort_folder.py — move files into subfolders by category, dry run by default
import shutil
import sys
from pathlib import Path
CATEGORIES = {
".jpg": "images", ".png": "images", ".gif": "images",
".pdf": "documents", ".txt": "documents", ".docx": "documents",
".zip": "archives",
} # extension → sous-dossier ; le reste ira dans "autres"
def sort_folder(folder, apply=False):
moved = 0
for file in sorted(folder.iterdir()):
if not file.is_file():
continue
category = CATEGORIES.get(file.suffix.lower(), "autres") # .get + défaut !
dest_dir = folder / category
dest = dest_dir / file.name
if dest.exists():
print(f"[CONFLIT] {dest} existe déjà, non touché")
continue
tag = "[RÉEL]" if apply else "[SIMULATION]"
print(f"{tag} {file.name:<25} -> {category}/")
if apply:
dest_dir.mkdir(exist_ok=True) # créer le sous-dossier au besoin
shutil.move(str(file), str(dest))
moved += 1
print(f"{moved} fichiers traités" + ("" if apply else " (simulation)"))
sort_folder(Path("sandbox"), apply="--apply" in sys.argv)
Sortie du dry run :
[SIMULATION] 2026-07-IMG_2026.png -> images/
[SIMULATION] 2026-07-photo1.jpg -> images/
[SIMULATION] 2026-07-photo2.JPG -> images/
[SIMULATION] archive.zip -> archives/
[SIMULATION] notes.txt -> documents/
[SIMULATION] rapport.pdf -> documents/
[SIMULATION] vieux rapport.pdf -> documents/
7 fichiers traités (simulation)
Le dict CATEGORIES avec .get(..., "autres") : la même défense que sur le JSON (09-3), appliquée aux extensions inconnues.
Exemple 5 — le rapport automatique : API + CSV → fichier texte
# daily_report.py — assemble weather (API) and expenses (CSV) into a report
import csv
from datetime import date
from pathlib import Path
import requests
def fetch_temperature():
"""Return current temperature in Nice, or None on any network failure."""
try:
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": 43.7, "longitude": 7.27,
"current": "temperature_2m"},
timeout=10,
)
response.raise_for_status()
return response.json()["current"]["temperature_2m"]
except requests.exceptions.RequestException:
return None # le rapport le dira, sans planter
def expense_total(path):
"""Return (total, line count), or (None, 0) if the file is unusable."""
try:
with open(path, newline="", encoding="utf-8") as f:
amounts = [float(row["amount"]) for row in csv.DictReader(f)]
return sum(amounts), len(amounts)
except (FileNotFoundError, ValueError, KeyError):
return None, 0
temperature = fetch_temperature()
total, count = expense_total("expenses.csv")
report_path = Path(f"report-{date.today()}.txt")
with open(report_path, "w", encoding="utf-8") as f:
f.write(f"Rapport du {date.today()}\n")
f.write("=" * 30 + "\n")
f.write(f"Météo Nice : {temperature} °C\n" if temperature is not None
else "Météo Nice : indisponible\n")
f.write(f"Dépenses : {total:.2f} € ({count} lignes)\n" if total is not None
else "Dépenses : fichier illisible ou absent\n")
print(f"Rapport écrit : {report_path}")
Contenu de report-2026-07-05.txt (cas nominal) :
Rapport du 2026-07-05
==============================
Météo Nice : 26.7 °C
Dépenses : 254.90 € (7 lignes)
Chaque source a son propre filet ; une panne de l'une n'empêche ni l'autre ni l'écriture du rapport — et le rapport dit honnêtement ce qui a manqué. C'est le squelette de toute automatisation périodique.
6. Erreurs fréquentes
1) Tester directement sur les vrais fichiers
sort_folder(Path.home() / "Downloads", apply=True) # premier lancement !
Aucun traceback — le script « marche »… et une règle fausse a envoyé 40 PDF dans autres/, écrasé deux doublons, et tu ne sais plus ce qui a bougé. Correction : bac-à-sable (copie de 10 fichiers) → dry run → lecture ligne à ligne de la simulation → SEULEMENT ALORS le vrai dossier, d'abord en dry run aussi.
2) L'action réelle par défaut
def sort_folder(folder, apply=True): # défaut dangereux !
Celui qui appelle sort_folder(folder) « juste pour voir » modifie tout. Le défaut doit être l'option INOFFENSIVE : apply=False. Même logique que les médicaments à bouchon sécurisé : l'action dangereuse doit demander un geste volontaire.
3) Modifier ce qu'on est en train de parcourir
for file in folder.iterdir():
file.rename(folder / "done" / file.name) # on déplace PENDANT le parcours
Comportement indéfini : fichiers sautés, parfois erreurs — et pas pareil d'une machine à l'autre. Correction : figer la liste d'abord — for file in list(folder.iterdir()): (ou sorted(...), qui matérialise aussi). Même piège que modifier une liste pendant qu'on la parcourt (⏩ niveau 03).
4) Écraser sans le savoir avec shutil.move
shutil.move("a/rapport.pdf", "b/rapport.pdf") # b/rapport.pdf existait…
# aucun message — l'ancien b/rapport.pdf a DISPARU
Path.rename lève FileExistsError sous Windows dans ce cas, mais shutil.move écrase en silence. Correction : le garde-fou if dest.exists(): avant CHAQUE déplacement, avec une politique explicite (sauter + signaler, ou suffixer -1, -2).
5) Construire des chemins en collant des chaînes
dest = str(folder) + "\\" + category + "\\" + file.name # fragile et illisible
Ça casse à la première surprise (slash final déjà présent, portabilité). Correction : l'opérateur / de pathlib — dest = folder / category / file.name — conçu exactement pour ça (⏩ niveau 05).
6) Le glob récursif qui ramasse trop
for file in folder.glob("**/*"): # récursif : descend dans images/, documents/…
... # → re-trie ce qui est déjà trié, boucle sans fin
Un rangeur récursif retraite ses propres dossiers de destination. Correction : pour ranger UN niveau, iterdir() ou glob("*") suffisent ; si le récursif est voulu, exclure explicitement les dossiers de destination (if category_dir in file.parents: continue).
7. Lire les messages d'erreur
FileExistsError — la destination existe déjà
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\rename_photos.py", line 18, in <module>
file.rename(target)
FileExistsError: [WinError 183] Impossible de créer un fichier déjà existant:
'sandbox\\photo1.jpg' -> 'sandbox\\2026-07-photo1.jpg'
Lecture : la dernière ligne donne les DEUX chemins — source et cible. La cible existe déjà (souvent : le script a déjà tourné — ton idempotence a une faille, ou deux fichiers différents produisent le même nom cible). Ce traceback est en réalité une bonne nouvelle : Windows a refusé d'écraser. Traite le cas dans le code (garde-fou exists()), pas en supprimant le fichier gênant à la main.
PermissionError — le fichier est verrouillé ou protégé
PermissionError: [WinError 32] Le processus ne peut pas accéder au fichier car
ce fichier est utilisé par un autre processus: 'sandbox\\rapport.pdf'
Très fréquent sous Windows : le fichier est OUVERT ailleurs (le PDF affiché dans le navigateur, le CSV ouvert dans Excel — Excel verrouille !). Ferme le programme et relance. Dans un script robuste : try/except PermissionError autour de l'action, signaler le fichier et CONTINUER avec les suivants — un fichier verrouillé ne doit pas faire échouer les 199 autres.
FileNotFoundError sur un chemin que tu « viens de construire »
FileNotFoundError: [WinError 3] Le chemin d'accès spécifié est introuvable:
'sandbox\\photo1.jpg' -> 'sandbox\\images\\photo1.jpg'
Le déplacement vise un dossier (images/) qui n'existe pas encore : rename/move ne créent JAMAIS les dossiers intermédiaires. Correction : dest_dir.mkdir(exist_ok=True) (ou parents=True pour créer toute la chaîne) avant le déplacement — et seulement en mode réel, sinon ton dry run laisse des dossiers vides partout.
8. Exercices — faciles
Règles : fichiers dans
exercices/mes-reponses/, nommésex-09-5-a.py, etc. TOUT se passe dans un bac-à-sable regénérable (exemple 1) — aucun script de cette série ne touche un vrai dossier.
a) Écris ton make_sandbox.py : il crée sandbox/ avec au moins 10 fichiers de types variés (dont un .JPG majuscule et un nom avec espace), et affiche le compte. Relance-le : il doit fonctionner aussi la deuxième fois (quel argument de mkdir le permet ?).
b) L'inventaire : liste le bac-à-sable trié par extension — une ligne par fichier (nom, extension normalisée, taille en octets via file.stat().st_size), puis un résumé par extension avec le dict accumulateur du 09-4 ({'.jpg': 3, '.pdf': 2, ...}).
c) Dry run pur : un script qui affiche [SIMULATION] <nom> -> majuscules/<NOM> pour chaque .txt du bac-à-sable — SANS rien renommer, et sans même de variable apply (cette version ne sait QUE simuler). Vérifie après coup : sandbox/ doit être strictement intact.
9. Exercices — moyens
a) Complète l'exercice (c) : ajoute le mode réel derrière --apply (via sys.argv), le garde-fou exists(), et le compteur final. Scénario de test imposé, dans l'ordre : dry run → vérifier à l'œil → --apply → relancer --apply une DEUXIÈME fois. La deuxième exécution réelle doit afficher 0 fichiers — sinon ton script n'est pas idempotent, corrige.
b) Le rangeur par date : trie les fichiers du bac-à-sable dans des sous-dossiers 2026-07/, 2026-06/… selon leur date de modification (file.stat().st_mtime → datetime.fromtimestamp(...) → f"{dt.year}-{dt.month:02d}", ⏩ niveau 05). Dry run par défaut, shutil.move en réel, création des dossiers au bon moment (pas en simulation !).
c) Le rapport de dossier : un script qui génère folder_report.csv — une ligne par fichier du bac-à-sable (nom, extension, taille, date de modification ISO) + affiche le total. Assemble pathlib (lister) et DictWriter (09-4). Bonus : une deuxième passe qui lit ce CSV et affiche les 3 plus gros fichiers.
10. Exercices — difficiles
a) Le dédoublonneur prudent : dans un bac-à-sable où tu auras créé des doublons EXACTS (même contenu, noms différents — fais-le dans make_sandbox.py), écris un script qui les détecte par contenu, et propose en dry run de déplacer les copies (pas les originaux !) vers sandbox/doublons/. Jamais de suppression. Définis et affiche clairement ta règle : lequel est « l'original » ?
- Indice 1 : deux fichiers sont des doublons exacts si leurs CONTENUS sont égaux —
file.read_bytes()les compare simplement (nos fichiers de test sont petits). - Indice 2 : un dict
contenu → premier chemin rencontré: à chaque fichier, si son contenu est déjà dans le dict, c'est une copie ; sinon il devient l'original de ce contenu. - Indice 3 : parcours en
sorted(...)pour une règle d'originalité stable (« le premier par ordre alphabétique est l'original ») ; accumule les copies dans une liste de(copie, original), affiche, puis applique le motif dry run habituel.
b) L'automatisation hebdomadaire complète : weekly_report.py assemble TOUT le niveau — il (1) interroge Open-Meteo pour les prévisions à 7 jours, (2) lit ton expenses.csv du 09-4, (3) inventorie un dossier au choix, et écrit weekly-report-<date>.txt en trois sections. Contraintes : chaque source a son filet d'erreur indépendant (le rapport se génère TOUJOURS, sections « indisponible » comprises) ; les appels réseau ont leur timeout ; zéro modification de fichiers hors du rapport lui-même. Lance-le réseau coupé pour valider.
11. Mini-projet lié — « rangeur de Téléchargements »
Fiche complète : projects/mini-projects/10-script-rangement-fichiers.md.
C'est le grand œuvre de la leçon (2–3 h) : le script qui range un vrai dossier en désordre par catégories, dossier cible en argument, mode simulation par défaut, --apply explicite, garde-fous anti-écrasement, compte rendu final. La fiche détaille le cahier des charges et les étapes. Rappel solennel : c'est le premier projet du parcours qui modifie de vrais fichiers — la règle des trois étapes (bac-à-sable → dry run → réel) s'applique à CHAQUE essai, sans exception, même « juste pour tester un petit truc ».
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-09-5-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) compare d'abord les garde-fous (défaut inoffensif ? exists() avant chaque action ? idempotence testée ?) — c'est le cœur de la leçon, avant l'élégance du code ; (3) rejoue les scénarios de panne des solutions (fichier verrouillé, dossier manquant) sur ton propre code ; (4) note dans ton journal ta checklist personnelle de mise en production d'un script de fichiers — tu la relies avant CHAQUE --apply des mois à venir.
13. À retenir
- La boucle de toute automatisation : lister → décider → agir — et « agir » vient TOUJOURS après une simulation lue et validée.
- La règle des trois étapes : bac-à-sable → dry run →
--applyexplicite. Le défaut est TOUJOURS la simulation. glob("*.ext")/iterdir()pour lister ; figer avecsorted(...)avant de modifier ; extensions comparées en.lower().renamerefuse d'écraser (Windows),shutil.moveécrase EN SILENCE → garde-foudest.exists()avant chaque action.mkdir(exist_ok=True)avant de déplacer vers un dossier —movene crée pas les dossiers.- Un bon script est idempotent : le relancer ne casse rien et dit « rien à faire ».
- Jamais de suppression dans un script d'apprentissage : déplacer vers un dossier de quarantaine.
- Un rapport automatique signale ses sources en panne au lieu de planter — ou pire, de mentir.
14. Questions de révision
- Quelles sont les trois étapes obligatoires avant qu'un script de fichiers touche à de vraies données, et pourquoi cet ordre ?
- Pourquoi le mode simulation doit-il être le DÉFAUT, et pas une option ?
- Quelle différence de comportement entre
Path.renameetshutil.movequand la destination existe, et quel garde-fou neutralise les deux cas ? - Que signifie « idempotent » pour un script de rangement, et comment le teste-t-on concrètement ?
- Pourquoi faut-il figer (
sorted/list) le résultat d'iterdiravant de déplacer des fichiers ? - Un
PermissionErrorsous Windows en plein renommage : cause la plus probable, et que doit faire le script pour les 199 autres fichiers ? - Pourquoi
mkdirne doit-il s'exécuter qu'en mode réel dans un script à dry run ? - Dans un rapport automatique, pourquoi chaque source de données a-t-elle son propre
try/exceptplutôt qu'un seul global ?
15. Checklist de compréhension
- Je peux créer un bac-à-sable et le regénérer d'une commande.
- Je liste, filtre (extension normalisée) et inspecte un dossier avec pathlib sans regarder la leçon.
- J'écris le motif dry run (
apply=Falsepar défaut,--apply, affichage identique) de mémoire. - Je mets les garde-fous
exists()etmkdirau bon endroit, systématiquement. - Mon script relancé deux fois dit « rien à faire » la deuxième.
- Mon rapport automatique se génère même quand ses sources tombent, et le dit.
- J'ai fait le mini-projet rangeur sur bac-à-sable, ET je l'ai relu avant tout
--apply.
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT de passer au niveau suivant.
16. Commit conseillé
git add lessons/level-09-automatisation-apis-donnees/exercices/mes-reponses/
git commit -m "exercises: complete lesson 09-5 real-world automation"
C'est aussi la fin du niveau : coche les critères de sortie du README du niveau, puis fais le commit de fin de niveau qui y est indiqué. Entrée de journal + git push.