Chapitre 4 - Consommer une API en code
- Appeler une API avec requests
- Pagination, erreurs, 401 et 429
- Les machines consomment aussi vos API
- TP : Script de statistiques
Appeler une API avec requests
curl et Bruno, c'est bien pour explorer. Mais pour automatiser, il faut appeler l'API depuis du code. Les exemples sont donnés en Python, avec la bibliothèque requests.
Prérequis
Python 3 et la bibliothèque requests : pip install requests.
Votre clé d'API n'a rien à faire dans le code (ni dans git). Passez-la par une variable d'environnement : export API_KEY=votre-cle.
Lire une ressource
import requests
BASE = "http://api-ritif.portlatch.com/v1"
res = requests.get(BASE + "/cocktails/3")
cocktail = res.json()
print(res.status_code, cocktail["name"])
Paramètres de query
res = requests.get(BASE + "/cocktails", params={"alcohol_free": "true", "per_page": 5})
Envoyer du JSON avec un header
import os
res = requests.post(
BASE + "/orders",
json={"cocktail_id": 3},
headers={"X-API-Key": os.environ["API_KEY"]},
)
print(res.status_code, res.headers.get("Location"))
Avec json=, requests sérialise le body et ajoute le Content-Type tout seul.
Le piège : les erreurs HTTP
requests ne lève pas d'erreur sur un 404 ou un 500 : il ne le fait que si le réseau est coupé. Votre code continue donc tranquillement… avec une réponse d'erreur.
res = requests.get(BASE + "/cocktails/999")
res.raise_for_status() # lève une exception si le code est 4xx ou 5xx
En résumé
Python (requests) | |
|---|---|
| Code de statut | res.status_code |
| Succès (2xx) ? | res.ok |
| Header | res.headers["Location"] |
| Body JSON | res.json() |
| Envoyer du JSON | json=... |
| Lever une erreur | res.raise_for_status() |
Pagination, erreurs, 401 et 429
Un script qui marche sur la première page, avec un serveur de bonne humeur, c'est facile. Un script robuste doit aussi gérer la pagination, les erreurs et un serveur qui demande de ralentir.
La pagination
Une API ne renvoie jamais une liste de 10 000 éléments d'un coup : elle découpe en pages. Sur API-ritif, une liste ressemble à ceci :
{
"items": [ {"id": 1, "name": "Hello World"}, ... ],
"page": 1,
"per_page": 5,
"total": 13,
"_links": {
"self": {"href": "/v1/cocktails?page=1&per_page=5"},
"next": {"href": "/v1/cocktails?page=2&per_page=5"},
"last": {"href": "/v1/cocktails?page=3&per_page=5"}
}
}
La méthode la plus fiable : suivre le lien next jusqu'à ce qu'il n'y en ait plus. Pas besoin de calculer les numéros de page.
import pprint
from urllib.parse import urljoin
def get_all(path, headers=None):
items = []
url = urljoin(BASE, path)
while url:
res = requests.get(url, headers=headers)
res.raise_for_status()
data = res.json()
items.extend(data["items"])
next_link = data["_links"].get("next")
url = urljoin(BASE, next_link["href"]) if next_link else None
return items
cocktails = get_all("/v1/cocktails")
pprint.pp(cocktails)
Les liens sont relatifs (/v1/...) : urljoin() les combine avec l'adresse du serveur.
Lire les erreurs
API-ritif renvoie ses erreurs au format application/problem+json (détaillé au chapitre 5). Le champ detail est fait pour être affiché :
{
"type": "https://api-ritif.example/problems/out-of-stock",
"title": "Rupture de stock",
"status": 409,
"detail": "Le 404 Spritz est introuvable. Comme d'habitude."
}
if not res.ok:
problem = res.json()
print(problem["status"], problem["title"], ":", problem["detail"])
401 : l'authentification a échoué
Un 401 signifie que le serveur ne vous reconnaît pas : clé absente, clé fausse, ou token expiré. Certains tokens ne vivent que quelques minutes (c'est le cas des JWT d'API-ritif, voir chapitre 8).
Réaction attendue d'un client :
- clé absente ou fausse : inutile d'insister, il faut corriger la configuration ;
- token expiré : en redemander un nouveau, puis rejouer la requête une fois.
429 : le serveur demande de ralentir
Pour se protéger, une API limite le nombre de requêtes par client (rate limiting). Au-delà, elle répond 429 Too Many Requests, souvent avec un header Retry-After qui indique combien de secondes attendre.
HTTP/1.1 429 Too Many Requests
Retry-After: 10
Content-Type: application/problem+json
{"title": "Too Many Requests", "status": 429, "detail": "Le barman est débordé, reviens dans 10 s"}
Un bon client attend, puis réessaie :
import time
def polite_get(url, **kwargs):
while True:
res = requests.get(url, **kwargs)
if res.status_code != 429:
return res
wait = int(res.headers.get("Retry-After", 1))
print("429 reçu, pause de", wait, "s")
time.sleep(wait)
Sans Retry-After, la bonne pratique est le backoff exponentiel : attendre 1 s, puis 2 s, puis 4 s, puis 8 s… Le serveur respire, et vous finissez par passer.
Réessayer immédiatement en boucle après un 429, c'est comme klaxonner dans un embouteillage : ça n'avance pas plus vite, et tout le monde vous déteste.
Les machines consomment aussi vos API
Jusqu'ici, les clients de nos API étaient des programmes écrits par des humains. Depuis l'arrivée des LLM, un nouveau type de client apparaît : l'IA elle-même.
Un LLM qui utilise des outils
Un modèle de langage ne fait que produire du texte. Pour agir sur le monde (lire une météo, créer un ticket, allumer une lampe), on lui donne des outils : des fonctions décrites par un nom, une description et des paramètres. Le modèle décide quand les appeler, et le programme qui l'héberge exécute l'appel.
Utilisateur ──▶ LLM ──▶ « appelle l'outil get_weather(ville="Lyon") »
│
▼
Programme hôte ──▶ GET https://api.meteo.example/v1/previsions?ville=Lyon
│
▼
LLM ◀── {"temperature": 14, "ciel": "nuageux"}
│
▼
Utilisateur ◀── « Il fait 14 °C à Lyon, prenez un parapluie. »
Derrière la plupart des outils… il y a une API.
MCP
Le Model Context Protocol (MCP), publié par Anthropic fin 2024 et adopté depuis par la plupart des acteurs, standardise cette mécanique. Un serveur MCP expose des outils, et n'importe quel assistant compatible (Claude, ChatGPT, Copilot, des éditeurs de code…) peut les utiliser.
Un serveur MCP est souvent une simple surcouche d'une API existante : il traduit les outils en appels HTTP.
L'API, c'est le MCP du pauvre
Un agent IA capable d'exécuter des commandes (comme Claude Code) n'a même pas besoin de MCP : si on lui donne une URL, une clé et une documentation, il sait écrire les requêtes curl lui-même. À condition que l'API soit compréhensible.
Ce qui aide une IA à utiliser votre API, c'est exactement ce qui aide un développeur :
- des ressources et des routes bien nommées ;
- des codes de statut justes, plutôt qu'un
200pour tout ; - des messages d'erreur explicites, qui disent quoi corriger ;
- une documentation à jour, avec des descriptions et des exemples (OpenAPI, chapitre 6) ;
- des liens dans les réponses pour découvrir la suite (HATEOAS, chapitre 9).
Une API mal conçue fait perdre une heure à un développeur. Elle peut faire faire n'importe quoi à une IA, très vite et avec beaucoup d'assurance.
Donner une clé d'API à une IA, c'est lui donner les mêmes droits que vous. Préférez des clés limitées (lecture seule, périmètre réduit) et révocables.
TP : Script de statistiques
Objectif
Pendant la chasse au trésor, toute la promo a passé des commandes au bar. Le patron d'API-ritif veut maintenant des statistiques, et il compte sur vous.
Vous allez compléter un script Python qui récupère les données par l'API et calcule des statistiques.
Ce TP est individuel et noté.
Le fichier de départ
Copiez le code ci-dessous dans un fichier stats.py. La partie réseau est déjà écrite : la fonction get_all parcourt toutes les pages. Votre travail porte uniquement sur les données.
Le script fonctionne dès le départ : chaque question affiche None tant que vous ne l'avez pas faite. Commencez par le lancer, et regardez les deux exemples affichés en haut : ils montrent les champs d'une commande et d'un cocktail.
# TP2 - Script de statistiques sur API-ritif
#
# Lancement :
# export API_KEY=ma-cle (sous Windows PowerShell : $env:API_KEY="ma-cle")
# python stats.py > resultat.txt
#
# Le script fonctionne dès le départ : chaque question affiche None
# tant que vous ne l'avez pas faite. Complétez les blocs « À FAIRE ».
import os
from urllib.parse import urljoin
import requests
BASE = "http://api-ritif.portlatch.com/v1"
# La clé est lue dans une variable d'environnement, jamais écrite dans le code.
HEADERS = {"X-API-Key": os.environ["API_KEY"]}
# =============================================================================
# FOURNI : ne pas modifier
# =============================================================================
def get_all(path):
"""Renvoie TOUS les éléments d'une liste paginée, en suivant le lien next."""
items = []
url = urljoin(BASE, path)
while url:
res = requests.get(url, headers=HEADERS)
res.raise_for_status()
data = res.json()
items.extend(data["items"])
next_link = data["_links"].get("next")
url = urljoin(BASE, next_link["href"]) if next_link else None
return items
orders = get_all("/v1/orders?scope=all&per_page=50")
cocktails = get_all("/v1/cocktails?per_page=50")
# Regardez bien ces deux exemples : ils montrent les champs disponibles.
print("Exemple de commande :", orders[0])
print("Exemple de cocktail :", cocktails[0])
print()
# =============================================================================
# QUESTIONS OBLIGATOIRES
# =============================================================================
# ----- Question 1 : le nombre total de commandes -----------------------------
total = None # À FAIRE
print("Q1 - Nombre de commandes :", total)
# ----- Question 2 : le nombre de commandes par statut ------------------------
# EXEMPLE RÉSOLU : c'est le modèle pour compter, à réutiliser plus bas.
# On part d'un dictionnaire vide, et pour chaque commande on ajoute 1
# au compteur de son statut (get renvoie 0 si le statut n'est pas encore vu).
by_status = {}
for order in orders:
status = order["status"]
by_status[status] = by_status.get(status, 0) + 1
print("Q2 - Commandes par statut :", by_status)
# ----- Question 3 : le cocktail le plus commandé -----------------------------
# Indice 1 : comptez les commandes par cocktail, comme dans la question 2.
# Indice 2 : max(d, key=d.get) renvoie la clé du dictionnaire d
# qui a la plus grande valeur.
most_ordered = None # À FAIRE
print("Q3 - Cocktail le plus commandé :", most_ordered)
# ----- Question 4 : l'ardoise de la promo -------------------------------------
# Le montant total des commandes, SANS compter les commandes annulées
# (statut "cancelled").
# Indice : une variable qui commence à 0, une boucle, un if.
tab = None # À FAIRE
print("Q4 - Ardoise de la promo :", tab, "€")
# =============================================================================
# BONUS
# =============================================================================
# ----- Bonus 1 : le client qui a passé le plus de commandes ------------------
# Indice : même principe que la question 3.
best_customer = None # À FAIRE
print("B1 - Meilleur client :", best_customer)
# ----- Bonus 2 : la note moyenne de chaque cocktail ---------------------------
# Les avis d'un cocktail : get_all("/v1/cocktails/" + str(cocktail["id"]) + "/reviews")
# Moyenne = somme des notes / nombre de notes, arrondie au dixième avec round(x, 1).
# Attention : un cocktail peut n'avoir aucun avis (division par zéro !).
averages = None # À FAIRE : un dictionnaire nom du cocktail -> note moyenne
print("B2 - Notes moyennes :", averages)
# ----- Bonus 3 : la part de cocktails sans alcool parmi les commandes --------
# Le champ alcohol_free est dans la carte (cocktails), pas dans les commandes.
# Résultat en pourcentage, arrondi au dixième.
alcohol_free_percent = None # À FAIRE
print("B3 - Commandes sans alcool :", alcohol_free_percent, "%")
Consignes
1 - Questions obligatoires
- Le nombre total de commandes.
- Le nombre de commandes par statut : déjà résolue, c'est le modèle à réutiliser pour compter.
- Le cocktail le plus commandé.
- « L'ardoise » de la promo : le montant total des commandes, sans compter les commandes annulées.
2 - Bonus
- Le client qui a passé le plus de commandes.
- La note moyenne de chaque cocktail, arrondie au dixième (une requête par cocktail).
- La part de cocktails sans alcool parmi les commandes, en pourcentage.
3 - Défis (non notés)
- L'heure de pointe : la tranche de 10 minutes avec le plus de commandes.
- Un classement des cocktails par note moyenne, en n'incluant que ceux qui ont au moins 3 avis.
Contraintes
- La clé d'API est lue dans la variable d'environnement
API_KEY, jamais écrite dans le code. - Aucune donnée n'est saisie à la main : tout vient de l'API.
- Ne modifiez pas la partie « FOURNI ».
Résultat attendu
- Votre script complété (
stats.py). - Un fichier
resultat.txtcontenant sa sortie :python stats.py > resultat.txt.
Vos résultats seront comparés à ceux du formateur.