Tom Daluzeau
← Parcours Python · système
Python · système · leçon 8 / 8Ch. 5 · API REST et requests12 minTerminée

API GitLab et webhook : token, pagination, alerte

Appeler une API authentifiée avec un token placé dans un en-tête et lu dans os.environ, récupérer toutes les pages d'une liste, nettoyer un nom avec removeprefix, puis envoyer une alerte utile sur un webhook avec requests.post.

Mis à jour le

À recouper avec le cours. Cette leçon a été écrite à partir du plan du chapitre, avant relecture avec les supports. En cas de doute, le cours fait foi.

Pourquoi on voit ça

L’outil sait maintenant dire si un serveur est ok, degraded ou down. Il lui manque deux choses pour être utile en équipe :

  • lire des informations ailleurs : par exemple la liste des projets dans GitLab, grâce à son API ;
  • prévenir quelqu’un : poster une alerte dans le salon de l’équipe, grâce à un webhook.

Dans les deux cas, on manipule un secret. Cette leçon montre où le mettre, et surtout où ne pas le mettre.

Appeler une API authentifiée

Une API comme celle de GitLab ne répond pas à n’importe qui : elle veut savoir qui demande. On lui présente un token, une longue chaîne qui joue le rôle de mot de passe.

Le token : dans un en-tête, lu dans os.environ

Deux règles.

1. La valeur du token vient de l’environnement. On la lit avec os.environ, jamais on ne l’écrit dans le script.

import os

token = os.environ["GITLAB_TOKEN"]

Si la variable manque, les crochets lèvent KeyError : sans token, l’outil ne peut rien faire, autant s’arrêter tout de suite.

2. Le token voyage dans un en-tête. Pour GitLab, l’en-tête s’appelle PRIVATE-TOKEN.

import os
import requests

s = requests.Session()
s.headers.update({"PRIVATE-TOKEN": os.environ["GITLAB_TOKEN"]})

r = s.get("https://gitlab.example.com/api/v4/projects", timeout=5)
r.raise_for_status()
print(len(r.json()))
20

Pourquoi ces deux règles ?

Endroit Problème
dans le code le script part dans Git : toute personne qui lit le dépôt lit le token
dans l’URL les adresses sont enregistrées dans les journaux des serveurs et s’affichent dans les messages d’erreur
dans un en-tête, lu dans os.environ le token n’est ni dans le dépôt, ni dans l’adresse

Le traceback de raise_for_status() affiche l’URL complète. Si le token y était, il s’étalerait dans les logs.

environnement GITLAB_TOKEN=… fleetcheck os.environ[…] la requête GET /api/v4/projects PRIVATE-TOKEN: glpat-… jamais ailleurs url + "?private_token=" + token l'URL finit dans les journaux TOKEN = "glpat-abc123" le script finit dans Git
La valeur vient de l'environnement et voyage dans un en-tête : elle n'est ni dans l'adresse, ni dans le dépôt.

La pagination

Dans l’exemple, la réponse contient 20 projets, alors qu’il y en a peut-être 250. Une API ne renvoie jamais une longue liste d’un seul coup : elle la découpe en pages.

Deux paramètres règlent ce découpage :

Paramètre Sens
page le numéro de la page voulue, à partir de 1
per_page le nombre de résultats par page

On les passe avec params= : requests les ajoute à la fin de l’adresse (?page=1&per_page=100).

Pour tout récupérer, on boucle tant qu’une page renvoie des résultats :

url = "https://gitlab.example.com/api/v4/projects"
projets = []
page = 1
while True:
    r = s.get(url, params={"page": page, "per_page": 100}, timeout=5)
    r.raise_for_status()
    lot = r.json()
    if not lot:
        break
    projets.extend(lot)
    page = page + 1
print(len(projets), "projets")
250 projets

À lire dans ce code :

  • lot est une liste. Une liste vide compte comme faux : if not lot signifie « si la page est vide ».
  • projets.extend(lot) ajoute tous les éléments de lot à la fin de projets.
  • page = page + 1 : sans cette ligne, on redemanderait la page 1 sans fin.

Avec 250 projets et per_page=100 :

Requête page Résultats reçus Suite
1 1 100 on continue
2 2 100 on continue
3 3 50 on continue
4 4 0 page vide : break

Il faut donc quatre requêtes : c’est la page vide qui annonce la fin.

page=1 page=2 page=3 page=4 100 100 50 [ ] total : 100 total : 200 total : 250 break la page vide arrête tout 4 requêtes pour 250 projets : la page vide coûte elle aussi une requête
On demande page après page, jusqu'à recevoir une liste vide.

str.removeprefix

Les noms rendus par une API portent souvent un début dont on ne veut pas : le groupe devant le nom d’un projet, refs/heads/ devant le nom d’une branche. removeprefix retire ce début s’il est présent.

nom = "infra/fleetcheck"
print(nom.removeprefix("infra/"))
print(nom.removeprefix("ops/"))
print(nom)
fleetcheck
infra/fleetcheck
infra/fleetcheck

Trois choses à voir :

  • le préfixe est là : il est retiré ;
  • le préfixe n’est pas là : la chaîne est rendue telle quelle, sans erreur ;
  • comme toutes les méthodes de str, removeprefix renvoie une chaîne neuve. nom n’a pas changé. Pour garder le résultat : nom = nom.removeprefix("infra/").

Le webhook : envoyer l’alerte

Un webhook est une adresse fournie par un outil de discussion. Tout ce qu’on y envoie s’affiche comme un message dans un salon.

Jusqu’ici, on lisait avec GET. Pour envoyer des données, on utilise la méthode POST :

import os
import requests

alerte = {"text": "web-2 (prod) est down : HTTP 503 sur /health depuis 14:32"}
r = requests.post(os.environ["WEBHOOK_URL"], json=alerte, timeout=5)
r.raise_for_status()

Le paramètre json= reçoit un dictionnaire Python. requests le convertit en JSON, le place dans le corps de la requête et prévient le serveur que c’est du JSON. Le nom de la clé (text ici) dépend de l’outil de discussion.

Le reste ne change pas : toujours un timeout=, et raise_for_status() pour savoir si l’alerte est bien partie.

Une alerte utile : quoi, où, depuis quand

Une alerte est lue par quelqu’un qui doit décider vite. Elle répond à trois questions.

Question Dans l’exemple
Quoi ? est down, HTTP 503
Où ? web-2, en prod, sur /health
Depuis quand ? 14:32
fleetcheck web-2 est down requests.post(…, json=…) le dict part en JSON webhook outil de discussion l'URL du webhook se lit dans os.environ : c'est un secret le message dans le salon de l'équipe web-2 (prod), /health est down : HTTP 503 depuis 14:32 où quoi depuis quand
Un POST vers l'URL secrète, et le message arrive dans le salon. Il doit dire où, quoi et depuis quand.

L’URL du webhook est un secret

Un webhook ne demande ni token ni mot de passe : connaître l’URL suffit pour poster. L’adresse est à elle seule la clé du salon.

Si elle fuit, n’importe qui peut envoyer de fausses alertes à l’équipe ou noyer les vraies. On la traite donc comme un token : lue dans os.environ, jamais écrite dans le code, jamais dans Git, jamais affichée dans un log.

À retenir

  • Le token se lit dans os.environ et voyage dans un en-tête (PRIVATE-TOKEN pour GitLab). Jamais dans l’URL, jamais dans le code.
  • Pagination : paramètres page et per_page. On boucle tant qu’une page renvoie des résultats ; la page vide arrête la boucle.
  • s.removeprefix("x") retire le début s’il est présent, sinon rend la chaîne telle quelle. L’originale ne change pas.
  • requests.post(url, json=dico, timeout=5) envoie le dictionnaire en JSON dans le corps de la requête.
  • Une alerte utile dit quoi, où, depuis quand.
  • L’URL d’un webhook est un secret : qui la connaît peut poster.

Entraînement

12 questions, dont 4 à compléter. Tu réponds, on passe à la suivante. La note arrive à la fin.

Cartes · 7

Réviser les cartes ici

Espace : retourner