# ----------------------------------------------------------------------------- # Fabri-Ka — Agrégateur de produits québécois # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/scrapfly.py : transport de secours via l'API Scrapfly (anti-bot, # rendu JS optionnel). Utilisé quand l'accès direct échoue (403/429/HTML # au lieu de JSON). Chaque appel consomme des crédits : on ne l'emploie # qu'en dernier recours et on mémorise les échecs définitifs. # ----------------------------------------------------------------------------- from __future__ import annotations import json import os import threading import time import requests API = "https://api.scrapfly.io/scrape" _LOCK = threading.Lock() _MIN_INTERVAL = 0.5 _last = [0.0] def available() -> bool: return bool(os.environ.get("SCRAPFLY_API_KEY")) def scrapfly_get(url: str, render_js: bool = False, timeout: int = 150) -> tuple[int, str]: """GET via Scrapfly. Retourne (status_code_amont, contenu).""" key = os.environ.get("SCRAPFLY_API_KEY") if not key: raise RuntimeError("SCRAPFLY_API_KEY manquant (voir .env)") with _LOCK: wait = _MIN_INTERVAL - (time.time() - _last[0]) if wait > 0: time.sleep(wait) _last[0] = time.time() params = {"key": key, "url": url, "asp": "true", "country": "ca"} if render_js: params["render_js"] = "true" r = requests.get(API, params=params, timeout=timeout) r.raise_for_status() res = r.json().get("result", {}) return int(res.get("status_code") or 0), res.get("content") or "" def scrapfly_json(url: str, render_js: bool = False) -> dict | list: status, content = scrapfly_get(url, render_js=render_js) if status != 200: raise RuntimeError(f"scrapfly upstream {status} pour {url}") return json.loads(content)