SPB Git

spb/toit-ka Public

Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com

Python 40.2% TypeScript 39% CSS 20.2% HTML 0.7%
2.7 KB · 77 lines python
Raw Blame History
1# Author: Simon-Pierre Boucher2# Contact: contact@spboucher.ai3# Project: Toit-Ka4# -----------------------------------------------------------------------------5# hubprofile.py : profil membre lu depuis le HUB Groupe KA (groupe-ka.com).6#   Le hub est LA source de vérité (bio, ville, emploi, réseaux, photo, statut)7#   — l'édition se fait sur groupe-ka.com/compte, Toit·Ka ne fait qu'afficher.8#   GET {hub}/api/sso/profile?client_id=toit-ka&ka_id=…&ts=…&sig=…9#   avec sig = HMAC-SHA256(KA_SSO_SECRET, "toit-ka.<ka_id>.<ts>") en hex.10#   Cache mémoire 60 s ; None sur toute erreur -> repli sur les données locales.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import hashlib15import hmac16import os17import threading18import time19from datetime import datetime, timezone2021import requests2223KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")24CLIENT_ID = "toit-ka"25CACHE_TTL = 6026TIMEOUT = 52728_cache: dict[str, tuple[float, dict | None]] = {}29_lock = threading.Lock()303132def fetch_hub_profile(ka_id: str) -> dict | None:33    """Profil du membre au hub Groupe KA, ou None (inconnu ou injoignable)."""34    secret = os.environ.get("KA_SSO_SECRET")35    if not secret or not ka_id:36        return None37    now = time.time()38    with _lock:39        hit = _cache.get(ka_id)40        if hit and now - hit[0] < CACHE_TTL:41            return hit[1]42    data: dict | None = None43    try:44        ts = int(now)45        sig = hmac.new(secret.encode(), f"{CLIENT_ID}.{ka_id}.{ts}".encode(),46                       hashlib.sha256).hexdigest()47        r = requests.get(48            f"{KA_HUB_URL}/api/sso/profile",49            params={"client_id": CLIENT_ID, "ka_id": ka_id, "ts": ts, "sig": sig},50            timeout=TIMEOUT)51        if r.status_code == 200:52            data = r.json()53            if not isinstance(data, dict):54                return None55        elif r.status_code != 404:56            return None     # erreur transitoire (401, 5xx…) : pas de cache57    except Exception:58        return None         # réseau/JSON : pas de cache59    with _lock:60        _cache[ka_id] = (now, data)     # 200 -> data ; 404 -> None (négatif)61    return data626364def to_epoch(v) -> float | None:65    """created_at du hub (epoch OU chaîne ISO, UTC si naïve) -> epoch secondes."""66    if isinstance(v, (int, float)):67        return float(v)68    if isinstance(v, str) and v:69        try:70            dt = datetime.fromisoformat(v.replace("Z", "+00:00"))71            if dt.tzinfo is None:72                dt = dt.replace(tzinfo=timezone.utc)73            return dt.timestamp()74        except ValueError:75            return None76    return None77