"""ÉTAPE 0 — Découverte de l'API interne HelloFresh. Lance un navigateur visible, te laisse te connecter et naviguer (menu de la semaine, sélection de recettes), puis enregistre TOUTES les requêtes vers le gateway pour en déduire les 3 endpoints utiles : 1. abonnement + semaines éditables 2. menu d'une semaine (recettes / ingrédients / allergènes) 3. enregistrement de la sélection de recettes Usage : ANTICOCO_HEADLESS=0 python tools/discover_api.py Pendant que la fenêtre est ouverte : - connecte-toi, - ouvre le menu de la semaine, - (optionnel) change une recette pour capturer l'appel d'écriture, puis reviens dans le terminal et appuie sur Entrée pour écrire le rapport. Sortie : - .session/discovery_log.json : toutes les requêtes gateway observées (debug complet) - config/endpoints.json : squelette pré-rempli à compléter/valider à la main """ from __future__ import annotations import json import os import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) from playwright.sync_api import sync_playwright # noqa: E402 from hellofresh import auth # noqa: E402 LOG_PATH = auth.SESSION_DIR / "discovery_log.json" ENDPOINTS_PATH = ROOT / "config" / "endpoints.json" def main() -> None: auth.SESSION_DIR.mkdir(parents=True, exist_ok=True) requests_seen: list[dict] = [] with sync_playwright() as pw: kwargs = dict( user_data_dir=str(auth.PROFILE_DIR), headless=False, # toujours visible : c'est une étape interactive locale="fr-FR", viewport={"width": 1280, "height": 900}, ) channel = auth._channel() if channel: kwargs["channel"] = channel print(f"Navigateur : {channel}") ctx = pw.chromium.launch_persistent_context(**kwargs) page = ctx.pages[0] if ctx.pages else ctx.new_page() def on_request(req): if not auth._is_gateway_request(req.url): return entry = { "method": req.method, "url": req.url, "has_auth": bool(req.headers.get("authorization")), } if req.method in ("POST", "PUT", "PATCH"): try: entry["post_data"] = req.post_data except Exception: entry["post_data"] = None requests_seen.append(entry) print(f" [{req.method}] {req.url}") closed = {"v": False} ctx.on("close", lambda *_: closed.update(v=True)) page.on("close", lambda *_: closed.update(v=True)) page.on("request", on_request) print("Ouvre le menu de la semaine, change une recette si tu veux capturer l'écriture.") try: page.goto(auth.BASE_URL + "/my-account", wait_until="domcontentloaded", timeout=30000) except Exception as e: print("goto initial échoué (on continue quand même):", e) # On capture jusqu'à : Entrée (si TTY), fermeture de la fenêtre, ou fin du délai. # Le rapport est TOUJOURS écrit (finally) même si la fenêtre est fermée en cours. try: if sys.stdin and sys.stdin.isatty(): try: input("\n>>> Quand tu as fini, appuie sur Entrée (ou ferme la fenêtre)...\n") except (EOFError, KeyboardInterrupt): pass else: wait_s = int(os.environ.get("ANTICOCO_DISCOVER_WAIT", "240")) print(f"\n>>> Capture pendant {wait_s}s max. Connecte-toi (email + mot de passe, " "PAS Google), ouvre le menu, change une recette. Ferme la fenêtre quand fini.") waited = 0 while waited < wait_s and not closed["v"]: try: page.wait_for_timeout(2000) except Exception: break # page/contexte fermé waited += 2 finally: _save_report(requests_seen) try: if not closed["v"]: ctx.close() except Exception: pass def _save_report(requests_seen: list[dict]) -> None: """Écrit le log complet + un squelette d'endpoints découverts (sans écraser le vrai).""" LOG_PATH.write_text(json.dumps(requests_seen, indent=2, ensure_ascii=False), encoding="utf-8") print(f"\n{len(requests_seen)} requêtes gateway enregistrées dans {LOG_PATH}") def find(method: str, *needles: str) -> str: for r in requests_seen: if r["method"] == method and all(n in r["url"].lower() for n in needles): return r["url"] return "" noise = ("otlp/traces", "/login", "auth/email", "translations") writes = [r for r in requests_seen if r["method"] in ("POST", "PUT", "PATCH") and not any(n in r["url"] for n in noise)] discovered = ENDPOINTS_PATH.parent / "endpoints_discovered.json" skeleton = { "_comment": "Brouillon issu de la dernière discovery (ne remplace pas config/endpoints.json).", "menu": find("GET", "menus-service") or find("GET", "menu"), "weeks": find("GET", "deliveries") or find("GET", "subscriptions"), "set_selection_candidates": [f"[{r['method']}] {r['url']}" for r in writes], } discovered.parent.mkdir(parents=True, exist_ok=True) discovered.write_text(json.dumps(skeleton, indent=2, ensure_ascii=False), encoding="utf-8") print(f"Candidats d'écriture ({len(writes)}) -> {discovered}") if __name__ == "__main__": main()