Chapitre 4 - Consommer une API en code

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 statutres.status_code
Succès (2xx) ?res.ok
Headerres.headers["Location"]
Body JSONres.json()
Envoyer du JSONjson=...
Lever une erreurres.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 :

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 :

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

  1. Le nombre total de commandes.
  2. Le nombre de commandes par statut : déjà résolue, c'est le modèle à réutiliser pour compter.
  3. Le cocktail le plus commandé.
  4. « L'ardoise » de la promo : le montant total des commandes, sans compter les commandes annulées.

2 - Bonus

  1. Le client qui a passé le plus de commandes.
  2. La note moyenne de chaque cocktail, arrondie au dixième (une requête par cocktail).
  3. La part de cocktails sans alcool parmi les commandes, en pourcentage.

3 - Défis (non notés)

Contraintes

Résultat attendu

Vos résultats seront comparés à ceux du formateur.