Facebook Pixel
Fit'DistanceDocumentation
Ouvrir l'app
Sur cette page

Écrire un script

Un script Python ou JavaScript qui lit toutes les pages, ne relit que les changements, écrit sans doublon et reprend après un 429.

Python utilise requests, JavaScript fetch (Node.js récent). Chaque étape s'ajoute au même fichier.

1. Garde la clé hors du code

Donne ta clé au script par une variable d'environnement, jamais dans le fichier. Sur Mac et Linux, installe requests, puis passe la clé au lancement :

pip install requests
FITDISTANCE_KEY="fd_live_xxxxxxxx" python3 script.py
FITDISTANCE_KEY="fd_live_xxxxxxxx" node script.mjs

2. Fais le premier appel

call ajoute ta clé et réessaie après un 429.

Python

import os
import time
import uuid

import requests

BASE = "https://api.fitdistance.io/api/partner/v1"
KEY = os.environ["FITDISTANCE_KEY"]


def call(method, path, headers=None, **kwargs):
    for _ in range(5):
        res = requests.request(
            method,
            BASE + path,
            headers={"Authorization": f"Bearer {KEY}", **(headers or {})},
            timeout=30,
            **kwargs,
        )
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", "1")))
            continue
        res.raise_for_status()
        return res.json()
    raise RuntimeError("Trop de 429")


print(call("GET", "/me")["credits"])

JavaScript (fichier .mjs)

import { randomUUID } from "node:crypto";

const BASE = "https://api.fitdistance.io/api/partner/v1";
const KEY = process.env.FITDISTANCE_KEY;

async function call(method, path, { headers, body } = {}) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(BASE + path, {
      method,
      headers: {
        Authorization: `Bearer ${KEY}`,
        "Content-Type": "application/json",
        ...headers,
      },
      body: body && JSON.stringify(body),
    });
    if (res.status === 429) {
      const seconds = Number(res.headers.get("Retry-After") ?? 1);
      await new Promise((done) => setTimeout(done, seconds * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json();
  }
  throw new Error("Trop de 429");
}

console.log((await call("GET", "/me")).credits);

3. Lis toutes les pages

Chaque liste renvoie data, has_more et next_cursor. Tant que has_more vaut true, rappelle la route avec cursor égal à next_cursor. Une page coûte un crédit, quel que soit son nombre d'élèves : demande limit=200, le maximum autorisé.

Python

students, cursor, has_more = [], None, True
while has_more:
    page = call("GET", "/students", params={"limit": 200, "cursor": cursor})
    students += page["data"]
    has_more, cursor = page["has_more"], page["next_cursor"]

JavaScript

const students = [];
let cursor;
let hasMore = true;
while (hasMore) {
  const query = new URLSearchParams({ limit: "200" });
  if (cursor) query.set("cursor", cursor);
  const page = await call("GET", `/students?${query}`);
  students.push(...page.data);
  hasMore = page.has_more;
  cursor = page.next_cursor;
}

4. Ne relis que les changements

Garde le plus grand updated_at lu et passe-le ensuite dans since : tu reçois les objets dont updated_at est supérieur ou égal. Le test est inclusif. Le dernier objet lu revient donc, repère-le par son id. Le updated_at d'un élève bouge aussi à chaque connexion, donc une synchro quotidienne revoit les élèves connectés. Suis les pages comme à l'étape 3.

Python

since = max(s["updated_at"] for s in students)
changed = call("GET", "/students", params={"since": since, "limit": 200})["data"]

JavaScript

const since = students.map((s) => s.updated_at).sort().at(-1);
const query = new URLSearchParams({ since, limit: "200" });
const changed = (await call("GET", `/students?${query}`)).data;

5. Écris sans doublon

Une écriture passe par POST /operations/<nom>, avec un corps JSON et un en-tête Idempotency-Key. Cet exemple marque une conversation comme lue ; il demande Messages › Écrire (messaging:write), sinon tu reçois un 403 SCOPE_MISSING. Le threadUid est le champ uid d'une conversation de messaging.threads_list, qui demande Messages › Lire (messaging:read).

Python

call(
    "POST",
    "/operations/messaging.mark_read",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"threadUid": "uid-de-la-conversation"},
)

JavaScript

await call("POST", "/operations/messaging.mark_read", {
  headers: { "Idempotency-Key": randomUUID() },
  body: { threadUid: "uid-de-la-conversation" },
});

Idempotency-Key est une chaîne unique par écriture, comme un UUID. Génère-la une fois par écriture et réutilise-la à chaque nouvel essai. La même clé avec le même corps ne tourne qu'une fois, et le rejeu renvoie la première réponse avec Idempotency-Replayed: true, sans coût. Si le script redémarre, il envoie une nouvelle clé et l'écriture peut avoir lieu deux fois. Garde la clé de côté quand c'est important. Un autre corps avec la même clé donne un 409 IDEMPOTENCY_MISMATCH. La première écriture réussie d'une clé t'envoie un e-mail qui nomme l'opération et sa cible.

6. Reprends après un 429

Un 429 RATE_LIMITED veut dire que tu vas trop vite : 10 appels par seconde et 300 par minute pour une clé, dont 60 écritures et 20 suppressions par minute. Il ne coûte rien. Retry-After donne les secondes à attendre, et RateLimit-* dit où tu en es. call attend puis réessaie, cinq fois au plus.

Page suivanteClés et créditsCe qui coûte un crédit, ce qui est gratuit, d'où viennent tes crédits et où suivre ta consommation.