Python 67%
TypeScript 18.2%
CSS 14.4%
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# hubprofile.py : profil membre lu depuis le HUB Groupe KA (groupe-ka.com)5# Le hub est LA source de vérité du profil (bio, ville, emploi, entreprise,6# site web, réseaux sociaux, photo, statut, profil public) — l'édition se7# fait sur groupe-ka.com/compte, Immo·Ka ne fait qu'afficher.8# GET {hub}/api/sso/profile?client_id=immo-ka&ka_id=…&ts=…&sig=…9# avec sig = HMAC-SHA256(KA_SSO_SECRET, "immo-ka.<ka_id>.<ts>") en hex10# (même secret que le SSO). Cache mémoire 60 s ; None sur toute erreur11# (réseau, 401, 404 : vieux compte non relié) -> l'appelant retombe sur12# les données locales.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import hashlib17import hmac18import os19import threading20import time21from datetime import datetime, timezone2223import requests2425KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")26CLIENT_ID = "house-ka"27CACHE_TTL = 60 # secondes28TIMEOUT = 5 # secondes2930_cache: dict[str, tuple[float, dict | None]] = {}31_lock = threading.Lock()323334def fetch_hub_profile(ka_id: str) -> dict | None:35 """Profil du membre au hub Groupe KA, ou None (inconnu ou injoignable)."""36 secret = os.environ.get("KA_SSO_SECRET")37 if not secret or not ka_id:38 return None39 now = time.time()40 with _lock:41 hit = _cache.get(ka_id)42 if hit and now - hit[0] < CACHE_TTL:43 return hit[1]44 data: dict | None = None45 try:46 ts = int(now)47 sig = hmac.new(secret.encode(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(),48 hashlib.sha256).hexdigest()49 r = requests.get(50 f"{KA_HUB_URL}/api/sso/profile",51 params={"client_id": CLIENT_ID, "ka_id": ka_id,52 "ts": ts, "sig": sig},53 timeout=TIMEOUT)54 if r.status_code == 200:55 data = r.json()56 if not isinstance(data, dict):57 return None58 elif r.status_code != 404:59 return None # erreur transitoire (401, 5xx…) : pas de cache60 except Exception:61 return None # réseau/JSON : pas de cache62 with _lock:63 _cache[ka_id] = (now, data) # 200 -> data ; 404 -> None (négatif)64 return data656667def to_epoch(v) -> float | None:68 """created_at du hub (epoch OU chaîne ISO) -> epoch secondes, sinon None."""69 if isinstance(v, (int, float)):70 return float(v)71 if isinstance(v, str) and v:72 try:73 dt = datetime.fromisoformat(v.replace("Z", "+00:00"))74 if dt.tzinfo is None: # chaîne naïve du hub = UTC75 dt = dt.replace(tzinfo=timezone.utc)76 return dt.timestamp()77 except ValueError:78 return None79 return None80