#!/usr/bin/env python3 """Essayer un parcours de bout en bout, avec une clé d'API de bac à sable. Ouvre un dossier, crée la session de parcours, affiche le lien à ouvrir sur un téléphone, puis suit le dossier jusqu'à sa décision. Aucune dépendance : la bibliothèque standard de Python 3. export TANO_API_KEY='tano_sandbox_…' # la clé de bac à sable, créée dans la console python3 essai_parcours.py mon_parcours CI # le nom du parcours, le pays du sujet C'est aussi un exemple d'intégration : chaque requête de création est **signée** (en-têtes X-Tano-Timestamp et X-Tano-Signature) et porte une **clé d'idempotence**, comme l'exige le contrat (docs/cadre/03-conventions-api.md). Le secret de signature est la dernière partie de la clé d'API ; il ne quitte jamais votre serveur. """ from __future__ import annotations import argparse import hashlib import hmac import json import os import sys import time import urllib.error import urllib.request import uuid API = os.environ.get("TANO_API", "https://api.tano.africa") if not API.startswith(("https://", "http://localhost", "http://127.0.0.1")): sys.exit("TANO_API doit être une adresse https (ou locale).") TERMINAUX = {"decided", "abandoned", "expired"} def signature(secret: str, horodatage: str, methode: str, cible: str, corps: bytes) -> str: """`v2=hex(HMAC-SHA256(secret, horodatage.MÉTHODE.cible.corps))` — cible = chemin et requête.""" message = f"{horodatage}.{methode.upper()}.{cible}.".encode() + corps return "v2=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() def appeler(cle: str, methode: str, cible: str, corps: dict | None = None) -> dict: brut = b"" if corps is None else json.dumps(corps, separators=(",", ":")).encode() entetes = {"Authorization": f"Bearer {cle}", "Accept": "application/json"} if corps is not None: horodatage = str(int(time.time())) entetes |= { "Content-Type": "application/json", "Idempotency-Key": str(uuid.uuid4()), "X-Tano-Timestamp": horodatage, "X-Tano-Signature": signature(cle.rsplit("_", 1)[-1], horodatage, methode, cible, brut), } requete = urllib.request.Request( # noqa: S310 — adresse vérifiée plus haut API + cible, data=brut or None, method=methode, headers=entetes ) try: with urllib.request.urlopen(requete, timeout=30) as reponse: # noqa: S310 return json.loads(reponse.read() or b"{}") except urllib.error.HTTPError as erreur: detail = json.loads(erreur.read() or b"{}").get("error", {}) sys.exit( f"✗ {methode} {cible} → {erreur.code} {detail.get('code')} : {detail.get('message')}" ) def main() -> None: options = argparse.ArgumentParser(description=__doc__.splitlines()[0]) options.add_argument( "parcours", help="le nom du parcours (flow_name), tel qu'affiché dans la console" ) options.add_argument("pays", help="le pays du sujet, deux lettres (CI, SN…)") options.add_argument("--reference", help="votre référence du dossier (facultative)") options.add_argument("--sans-suivi", action="store_true", help="ne pas attendre la décision") args = options.parse_args() cle = os.environ.get("TANO_API_KEY") or sys.exit( "Définissez TANO_API_KEY (clé de bac à sable)." ) dossier = appeler( cle, "POST", "/v1/cases", { "flow_name": args.parcours, "country": args.pays.upper(), **({"external_ref": args.reference} if args.reference else {}), }, ) print(f"✓ dossier ouvert : {dossier['id']} ({dossier['status']})") session = appeler(cle, "POST", "/v1/sessions", {"case_id": dossier["id"], "locale": "fr"}) print( f"✓ lien du parcours, valable jusqu'à {session['expires_at']} :\n\n {session['url']}\n" ) print(" Ouvrez-le sur un téléphone (caméra) ; il n'est affiché qu'une fois.") if args.sans_suivi: return precedent = None while True: etat = appeler(cle, "GET", f"/v1/cases/{dossier['id']}")["status"] if etat != precedent: print(f" … {time.strftime('%H:%M:%S')} dossier : {etat}") precedent = etat if etat in TERMINAUX: print("✓ terminé — décision dans la console, ou par webhook (case.decided).") return time.sleep(5) if __name__ == "__main__": main()