Traiter les pannes de l’API
Objectif : Distinguer une indisponibilité réseau d’une réponse invalide et appliquer un comportement de repli documenté.
Une requête peut échouer avant de recevoir une réponse, ou recevoir une réponse impossible à utiliser. Dans les deux cas, votre suivi de dépenses ne doit ni masquer l’erreur ni présenter un taux inventé comme une donnée actuelle. Vous allez distinguer ces situations et prévoir un taux de repli seulement s’il est explicitement fourni et valide.
Le module requests peut lever une exception lorsqu’une connexion échoue ou que le délai d’attente est dépassé. Une réponse reçue peut aussi être inutilisable : statut HTTP en erreur, corps qui ne contient pas de JSON valide, taux absent, ou valeur qui n’est pas un nombre positif fini. Traitez ces cas séparément afin que le message indique si le problème vient du transport ou des données renvoyées.
Vous avez déjà écrit la requête avec un délai maximal et vérifié le statut ainsi que l’extraction du taux dans la leçon précédente. Ici, vous conservez ces étapes, mais vous les entourez de contrôles adaptés : les erreurs de requête sont converties en erreur de domaine, et les réponses incohérentes sont rejetées avant que leur valeur soit utilisée.
Ajoutez dans depenses/erreurs.py une exception identifiable par le point d’entrée. Elle permet de présenter un message utile sans obliger le reste du programme à intercepter toutes les erreurs possibles. L’exception conserve une cause lorsque la bibliothèque réseau ou le décodage JSON a signalé un problème.
class ErreurTaux(Exception):
"""Signale un échec de récupération ou de validation du taux."""Dans depenses/change.py, séparez l’envoi de la requête de l’inspection de la réponse. Une erreur HTTP est reconnue à l’étape du statut ; une erreur de connexion ou de délai d’attente est reconnue pendant la requête. Les données JSON sont ensuite contrôlées : la clé attendue doit exister et sa valeur doit être un nombre strictement positif et fini.
import math
import sys
import requests
from depenses.erreurs import ErreurTaux
URL_TAUX = "https://api.frankfurter.app/latest"
def obtenir_reponse_taux(source="EUR", cible="USD"):
"""Retourne la réponse brute de l’API Frankfurter."""
try:
return requests.get(
URL_TAUX,
params={"from": source, "to": cible},
timeout=5,
)
except requests.exceptions.RequestException as erreur:
raise ErreurTaux(
f"Impossible de joindre l’API de taux : {erreur}"
) from erreur
def _valider_taux(valeur):
"""Retourne un taux valide ou signale une réponse inutilisable."""
if isinstance(valeur, bool) or not isinstance(valeur, (int, float)):
raise ErreurTaux("La réponse de l’API ne contient pas un taux numérique.")
if not math.isfinite(valeur) or valeur <= 0:
raise ErreurTaux("Le taux reçu doit être un nombre positif et fini.")
return float(valeur)
def obtenir_taux(source="EUR", cible="USD", taux_secours=None):
"""Retourne le taux distant, ou un taux de secours explicitement fourni."""
try:
reponse = obtenir_reponse_taux(source, cible)
try:
reponse.raise_for_status()
except requests.exceptions.HTTPError as erreur:
raise ErreurTaux(
f"L’API a répondu avec un statut HTTP invalide : {erreur}"
) from erreur
try:
donnees = reponse.json()
valeur = donnees["rates"][cible]
except (ValueError, KeyError, TypeError) as erreur:
raise ErreurTaux(
"La réponse de l’API ne contient pas le taux attendu."
) from erreur
return _valider_taux(valeur)
except ErreurTaux:
if taux_secours is None:
raise
taux_valide = _valider_taux(taux_secours)
print(
f"Attention : taux de secours utilisé pour {source}/{cible}.",
file=sys.stderr,
)
return taux_valide
if __name__ == "__main__":
try:
taux = obtenir_taux()
except ErreurTaux as erreur:
print(f"Erreur de taux : {erreur}", file=sys.stderr)
raise SystemExit(1) from erreur
else:
print(f"1 EUR = {taux:.4f} USD")En cas de succès, par exemple : 1 EUR = 1.0800 USD. En cas de panne sans taux de secours : un message « Erreur de taux » est affiché et le programme se termine avec un code d’échec.Le module expose une seule exception de domaine, ErreurTaux, pour ses échecs connus. Le point d’entrée de démonstration l’intercepte et affiche un message, puis retourne un code de sortie non nul. Si vous appelez obtenir_taux depuis depenses/main.py, appliquez le même principe à cet endroit : ne capturez pas toute exception avec un bloc trop large, car cela cacherait aussi des défauts de programmation.
- Une panne réseau et une réponse distante invalide sont des situations différentes, avec des causes distinctes.
- Validez la présence et le type du taux, puis vérifiez qu’il est positif et fini avant de l’utiliser.
- Regroupez les échecs attendus dans une exception de domaine et interceptez-la au niveau qui peut informer l’utilisateur.
- N’utilisez un taux de secours que s’il est fourni explicitement, valide et signalé comme tel.
Dans la prochaine leçon, vous relierez la lecture du relevé et la récupération du taux dans un parcours d’import complet.