Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Appeler une API REST avec Python et requests

Par mis à jour le

Améliorer cette page

Appeler une API REST depuis un outil, c'est envoyer une requête HTTP et lire une réponse, avec trois précautions que curl à la main fait oublier : le jeton dans un en-tête, un délai maximal, et une erreur transformée en message clair. Cette leçon s'adresse à un Ops qui débute en développement. Elle utilise requests, la bibliothèque Python la plus répandue pour ce travail, sur un cas réel : certcheck, un outil Python qui contrôle la date d'expiration de certificats et, avec --publish, ouvre un ticket sur la forge Forgejo (une forge Git auto-hébergée) quand l'un d'eux est à renouveler.

  • Lire une requête et une réponse HTTP : méthode, URL, en-têtes, corps, code.
  • Appeler une API avec requests et lire sa réponse JSON.
  • Envoyer le jeton dans l'en-tête Authorization, jamais dans l'URL.
  • Fixer un délai maximal et traduire une erreur HTTP en code de sortie.
  • Chercher avant de créer pour ne pas dupliquer un ticket.

Une API REST est un service web qu'un programme appelle en HTTP. Elle expose des ressources (un ticket, un serveur), chacune à une URL, et le client agit dessus avec une méthode : GET pour lire, POST pour créer. Les données voyagent le plus souvent en JSON. Une requête comme une réponse se découpent en quatre éléments : la ligne de départ, les en-têtes (des métadonnées), une ligne vide, puis un corps facultatif. Voici, abrégée, la liste des tickets ouverts d'un dépôt Forgejo :

GET /api/v1/repos/exploitation/certificats/issues?state=open HTTP/1.1
Host: forge.entreprise.local
Authorization: token <jeton>
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
[{"number": 12, "title": "certcheck : certificats à renouveler", ...}]

La première ligne de la réponse porte le code de statut, trois chiffres dont le premier décide de la suite : 2xx la requête a réussi, 4xx elle est fautive (jeton refusé, URL inconnue), 5xx le serveur a failli. Le détail du protocole est dans HTTP et HTTPS. Écrire soi-même une API relève du côté serveur : voir FastAPI.

Un appel requests tient en quatre lignes : la méthode, l'URL, l'en-tête, le délai. requests n'est pas dans la bibliothèque standard de Python : on la déclare comme n'importe quelle dépendance (voir Dépendances Python). La fonction suivante lit les tickets ouverts d'une forge. Elle utilise params= pour construire la partie ?state=open&type=issues de l'URL, et reponse.json() pour décoder le JSON reçu en liste de dictionnaires Python. L'adresse 127.0.0.1:8099 est celle de la forge factice du lab, une forge de démonstration lancée en local.

import requests
BASE = "http://127.0.0.1:8099/api/v1/repos/exploitation/certificats/issues"
def lister_tickets(jeton):
reponse = requests.get(
BASE,
params={"state": "open", "type": "issues"},
headers={"Authorization": f"token {jeton}"},
timeout=5,
)
reponse.raise_for_status()
return reponse.json()

Contre une forge qui répond correctement, lister_tickets("jeton-de-demo") rend [] quand aucun ticket n'est ouvert. La création d'un ticket par un POST fonctionne de la même façon :

requests.post(
BASE,
json={"title": "certcheck : certificats à renouveler", "body": "web-02 : expiré"},
headers={"Authorization": f"token {jeton}"},
timeout=5,
)

Le paramètre json= encode le dictionnaire en JSON et ajoute l'en-tête Content-Type. La forge répond 201 (créé) avec le ticket en JSON, que reponse.json() décode comme ci-dessus.

requests.get et requests.post sont deux raccourcis d'une même forme générique, requests.request(methode, url, ...), où la méthode HTTP est le premier argument ("GET", "POST", "PATCH"...). Elle accepte les mêmes paramètres (params=, json=, headers=, timeout=) et sert pour les méthodes qui n'ont pas de raccourci écrit dans cette leçon.

Le jeton voyage dans l'en-tête Authorization, jamais dans l'URL. Un jeton (token) est un mot de passe d'application : il prouve votre identité à l'API. Une URL est écrite partout, dans les journaux du serveur et des intermédiaires réseau, dans l'historique du shell, dans le message d'erreur de votre propre outil. Un jeton passé dans l'URL doit ensuite être révoqué. Le premier bloc est à éviter, le second est correct :

# ❌ le jeton finit dans l'URL, donc dans les journaux
requests.get(f"{BASE}?access_token={jeton}", timeout=5)
# correct : le jeton reste dans l'en-tête
requests.get(BASE, headers={"Authorization": f"token {jeton}"}, timeout=5)

Forgejo accepte l'en-tête Authorization: token <jeton>. Le jeton lui-même vient d'une variable d'environnement (FORGE_TOKEN dans certcheck), pas d'une option de ligne de commande, que la commande ps rend visible à tous les utilisateurs de la machine. Donnez-lui la portée minimale : un jeton qui ne sait gérer que les tickets limite les dégâts s'il fuit.

Sans délai, requests attend indéfiniment une réponse qui ne vient pas. Une forge qui accepte la connexion puis se tait, un pare-feu qui jette les paquets : l'outil lancé chaque nuit par cron (le planificateur de tâches de Linux) reste suspendu, et la nuit suivante un deuxième exemplaire démarre. Le paramètre timeout= pose une limite en secondes. Contre un serveur muet, timeout=1 lève au bout d'une seconde une exception ReadTimeout, dont le message ressemble à :

HTTPConnectionPool(host='127.0.0.1', port=57809): Read timed out. (read timeout=1)

Fixez toujours timeout= sur chaque appel : requests n'en impose aucun par défaut. Un délai de 5 secondes est un point de départ raisonnable pour une API interne. La forge qui ne répond jamais donne alors une erreur franche en quelques secondes, que l'outil peut transformer en message et en code de sortie.

Une erreur HTTP ne lève rien toute seule : c'est raise_for_status() qui la transforme en exception. Sans cet appel, une réponse 401 serait traitée comme une réponse normale et reponse.json() décoderait le message d'erreur comme s'il s'agissait de données. Avec lui, tout code 4xx ou 5xx lève une HTTPError. Le délai dépassé et le réseau coupé lèvent d'autres exceptions, mais toutes héritent de requests.RequestException : un seul except suffit à les attraper.

Le schéma suit un appel de haut en bas. Regardez les deux sorties d'erreur : le délai dépassé et le code 4xx ou 5xx rejoignent la même capture, qui produit un message sur la sortie d'erreur et un code de sortie précis. Seul le code 2xx mène à la lecture des données.

Un appel requests.get avec en-tête et timeout de 5 secondes. Si aucune réponse n'arrive avant 5 secondes, une exception de délai ou de réseau est levée. Sinon raise_for_status examine le code HTTP : un code 2xx mène à reponse.json qui lit les données, un code 4xx ou 5xx lève une exception HTTPError. Les deux exceptions rejoignent un seul except requests.RequestException, qui écrit un message sur la sortie d'erreur puis sort avec le code 3.nonoui2xx4xx ou 5xxrequests.get(url, headers=..., timeout=5)Réponse reçueavant 5 secondes ?Exception :délai dépassé ou réseau coupéreponse.raise_for_status()Code HTTPreponse.json()lire les donnéesException HTTPError(4xx ou 5xx)except requests.RequestExceptionMessage sur la sortie d'erreurpuis code de sortie 3
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Le tableau donne le sens des codes que vous croiserez le plus souvent avec une forge, et ce que l'erreur demande de corriger.

CodeSignificationCe qu'il faut corriger
401jeton absent, périmé ou refuséle jeton
403jeton valide mais droit insuffisantles droits du jeton
404dépôt ou ressource introuvablel'URL
503serveur indisponiblerien chez vous : réessayer après un délai

Pourquoi un code de sortie à part ? Dans le contrat de certcheck, 1 veut dire « un certificat est à renouveler » et 2 « appel ou entrée inexploitable ». Une panne de la forge qui sortirait aussi en 1 ferait chercher une alerte qui n'existe pas : la publication étend le contrat avec un quatrième code, 3. Voici l'enchaînement, avec la fonction lister_tickets vue plus haut :

try:
tickets = lister_tickets(jeton)
except requests.RequestException as exc:
print(f"certcheck: forge injoignable : {exc}", file=sys.stderr)
sys.exit(3)

Avec un mauvais jeton, la forge de démonstration du lab répond 401 et le message qui s'affiche contient 401 Client Error: Unauthorized for url: suivi de l'adresse appelée. Cette adresse ne contient aucun jeton, conséquence directe de la règle sur l'en-tête.

Un outil lancé par cron doit chercher avant de créer, sinon il ouvre un ticket par nuit. GET ne change rien sur le serveur, on peut le répéter à volonté. POST crée à chaque appel : relancé, il crée un doublon. Un geste est idempotent quand le répéter laisse le même état final ; c'est donc à l'outil de rendre sa publication idempotente.

La méthode tient en trois temps : lister les tickets ouverts, comparer leur titre à celui qu'on veut créer, et n'envoyer le POST que si aucun ne le porte. Le titre sert de clé, il doit donc rester constant : certcheck : certificats à renouveler, sans date dedans. Cette leçon s'en tient à la première page de la liste, comme le lab ; le reste est renvoyé plus bas à des pages dédiées.

Et la reprise, la pagination, la création d'une API ?

Section intitulée « Et la reprise, la pagination, la création d'une API ? »

Plusieurs sujets dépassent cette initiation et ont leur page. Une API qui répond parfois 429 ou 503 se reprend automatiquement, et une liste de plus de 30 éléments arrive découpée en pages : le guide Appeler une API REST en Python avec requests traite les deux. La relance sans effet de bord est développée dans Idempotence cloud. Pour exposer une API plutôt que l'appeler, voir FastAPI. Le même sujet en ligne de commande est dans Consommer une API REST avec curl et jq.

Dans le lab, certcheck sait déjà décider quels certificats sont à renouveler ; il lui manque les deux fonctions qui parlent à la forge. Vous écrivez requete() et ticket_existe() dans src/certcheck/forge.py avec requests, puis vous essayez l'outil contre une forge factice lancée en local, qui répond comme l'API de Forgejo (option --panne pour simuler un 503). La validation vérifie que le jeton part dans un en-tête et jamais dans l'URL, qu'une forge muette donne le code 3 en moins de 30 secondes, que les erreurs 401 et 503 donnent le code 3 avec un message qui cite le code HTTP, qu'un seul ticket est ouvert au bon titre, et qu'une relance n'en ouvre pas un second.

  • Une API REST se lit comme une requête (méthode, URL, en-têtes, corps) et une réponse (code de statut, en-têtes, corps) : 2xx réussi, 4xx requête fautive, 5xx serveur défaillant.
  • requests appelle l'API en une instruction ; reponse.json() décode le JSON reçu.
  • Le jeton voyage dans l'en-tête Authorization, jamais dans l'URL, et vient d'une variable d'environnement.
  • Chaque appel porte un timeout= : sans lui, une forge muette bloque l'outil.
  • raise_for_status() transforme un 4xx ou un 5xx en exception ; un seul except requests.RequestException attrape aussi le délai et le réseau coupé.
  • Une panne d'API a son propre code de sortie (3), distinct de l'alerte métier (1).
  • POST n'est pas idempotent : un outil relancé cherche avant de créer, avec un titre constant comme clé.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Les questions ci-dessous reviennent quand un ops passe d'un appel curl à un outil Python qui appelle une API toutes les nuits : où mettre le jeton, quel délai fixer, comment savoir si l'appel a réussi et comment ne pas créer deux fois la même chose.

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn