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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Lire une requête et une réponse HTTP : méthode, URL, en-têtes, corps, code.
- Appeler une API avec
requestset 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.
Prérequis
Section intitulée « Prérequis »- La leçon Structurer un projet Python en outil,
qui pose le contrat de
certcheck: les codes de sortie 0, 1 et 2. Cette leçon-ci y ajoute le code 3, « la publication a échoué ». - Lire du Python court ; la syntaxe relève du parcours Python fondamentaux.
- Savoir ce qu'est le JSON : des objets entre accolades, des listes entre crochets (voir JSON en Python).
Qu'est-ce qu'une API REST pour un outil ?
Section intitulée « Qu'est-ce qu'une API REST pour un outil ? »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.1Host: forge.entreprise.localAuthorization: token <jeton>Accept: application/json
HTTP/1.1 200 OKContent-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.
Comment appeler une API avec requests ?
Section intitulée « Comment appeler une API avec requests ? »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.
Où mettre le jeton d'accès ?
Section intitulée « Où mettre le jeton d'accès ? »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 journauxrequests.get(f"{BASE}?access_token={jeton}", timeout=5)
# correct : le jeton reste dans l'en-têterequests.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.
Pourquoi fixer un délai maximal ?
Section intitulée « Pourquoi fixer un délai maximal ? »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.
Que faire quand l'API répond une erreur ?
Section intitulée « Que faire quand l'API répond une erreur ? »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.
Le tableau donne le sens des codes que vous croiserez le plus souvent avec une forge, et ce que l'erreur demande de corriger.
| Code | Signification | Ce qu'il faut corriger |
|---|---|---|
401 | jeton absent, périmé ou refusé | le jeton |
403 | jeton valide mais droit insuffisant | les droits du jeton |
404 | dépôt ou ressource introuvable | l'URL |
503 | serveur indisponible | rien 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.
Comment ouvrir un ticket sans créer de doublon ?
Section intitulée « Comment ouvrir un ticket sans créer de doublon ? »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.
Mettre en pratique
Section intitulée « Mettre en pratique »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.
À retenir
Section intitulée « À retenir »- 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) :
2xxréussi,4xxrequête fautive,5xxserveur défaillant. requestsappelle 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 un4xxou un5xxen exception ; un seulexcept requests.RequestExceptionattrape 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).
POSTn'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
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Livrer un outil Python : version, étiquette, installation
Continuer le cours : Consommer une API REST avec curl et jq
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »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.
Toujours dans l'en-tête
Un jeton collé dans l'URL (?access_token=...) est écrit partout : journaux du serveur et des proxys traversés, historique du shell, message d'erreur de votre propre outil. Un jeton fuité doit être révoqué.requests.get(url, headers={"Authorization": f"token {jeton}"}, timeout=5)
- Forgejo accepte
Authorization: token <jeton>etAuthorization: Bearer <jeton>. - Lisez le jeton dans une variable d'environnement, pas dans une option de ligne de commande que
psrend visible à tous les utilisateurs. - Donnez-lui la portée minimale : un jeton limité aux tickets limite les dégâts s'il fuit.
Sans délai, l'outil attend pour toujours
requests n'impose aucun délai par défaut. Un outil lancé par cron reste suspendu, et la nuit suivante un deuxième exemplaire démarre.reponse = requests.get(url, headers=entetes, timeout=5)
- Contre un serveur muet,
timeout=1lève une exceptionReadTimeoutau bout d'une seconde. - Elle hérite de
requests.RequestException, comme toutes les erreurs derequests: un seulexceptsuffit à l'attraper. - Posez le délai sur chaque appel, il ne se règle pas une fois pour toutes.
Le code de statut décide
| Code | Signification | Ce qu'il faut corriger |
|---|---|---|
2xx |
réussi | rien |
401 |
jeton absent, périmé ou refusé | le jeton |
403 |
jeton valide, droit insuffisant | les droits du jeton |
404 |
ressource ou dépôt introuvable | l'URL |
5xx |
le serveur a failli | rien chez vous, réessayer après un délai |
reponse.raise_for_status()
donnees = reponse.json()
Un 401 ne se corrige pas en relançant : il faut changer le jeton.Chercher, comparer, puis créer
GET ne change rien sur le serveur et se répète sans danger ; POST crée à chaque appel. Pour qu'une relance laisse le même état :- lister les tickets ouverts ;
- comparer le titre exact, qui sert de clé et doit rester constant (pas de date dedans) ;
- ne créer que si aucun ticket ne le porte.
Une panne n'est pas un constat
Dans le contrat decertcheck, 1 veut dire « au moins un certificat à renouveler » et 3 « la publication a échoué ». Une exception non attrapée fait sortir Python en code 1 : une panne réseau passerait pour une alerte de sécurité.try:
tickets = lister_tickets(jeton)
except requests.RequestException as exc:
print(f"certcheck: forge injoignable : {exc}", file=sys.stderr)
sys.exit(3)
- Le message va sur la sortie d'erreur, le résultat des certificats reste lisible sur la sortie standard.
- N'imprimez jamais le jeton ni l'en-tête, même en mode
--verbose.
Deux sujets à approfondir
- Reprise : certaines erreurs (
429,503) disparaissent d'elles-mêmes. On réessaie quelques fois, en attendant de plus en plus longtemps entre deux essais. - Pagination : une liste d'API est découpée en pages. Une instance Forgejo rend 30 éléments par défaut et 50 au plus, valeurs réglables que l'instance publie sur
/api/v1/settings/api; la liste se lit avec les paramètrespageetlimit.