Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1# -----------------------------------------------------------------------------2# Rent-Ka — Rental listings aggregator (Canada, outside Québec)3# Author: Simon-Pierre Boucher — contact@spboucher.ai4# hubfav.py : favoris unifiés « Mon univers Ka » — chaque ♥ ajouté/retiré sur5# Rent-Ka est poussé au hub Groupe KA (magasin central des favoris du6# groupe), signé HMAC du secret SSO partagé. Poussée en arrière-plan7# (thread), jamais bloquante : un échec réseau n'affecte pas le ♥ local.8# Config .env : KA_SSO_SECRET, KA_HUB_URL (optionnel).9# -----------------------------------------------------------------------------10from __future__ import annotations1112import hashlib13import hmac14import os15import threading16import time1718import requests1920CLIENT_ID = "rent-ka"21KA_HUB_URL = os.environ.get("KA_HUB_URL", "https://www.groupe-ka.com").rstrip("/")222324def _sig(ka_id: str, ts: int) -> str | None:25 secret = os.environ.get("KA_SSO_SECRET")26 if not secret:27 return None28 return hmac.new(secret.encode(),29 f"{CLIENT_ID}.{ka_id}.{ts}".encode(),30 hashlib.sha256).hexdigest()313233def _push(ka_id: str, action: str, item: dict) -> None:34 ts = int(time.time())35 sig = _sig(ka_id, ts)36 if not sig:37 return38 try:39 requests.post(f"{KA_HUB_URL}/api/sso/favorites", timeout=6, json={40 "client_id": CLIENT_ID, "ka_id": ka_id,41 "ts": str(ts), "sig": sig,42 "action": action, "item": item,43 })44 except Exception:45 pass # best-effort : le favori local reste la référence d'affichage464748def push_favorite(ka_id: str | None, action: str, item: dict) -> None:49 """Pousse un ♥ (action « add » ou « remove ») au hub, sans bloquer."""50 if not ka_id or not str(ka_id).startswith("ka-"):51 return # compte legacy non relié au hub52 threading.Thread(target=_push, args=(ka_id, action, item),53 daemon=True).start()545556def listing_item(row) -> dict:57 """Transforme une ligne `listings` en item de favori pour le hub."""58 import json as _json59 d = dict(row)60 try:61 images = _json.loads(d.get("images") or "[]")62 except Exception:63 images = []64 title = d.get("title") or d.get("address") or "Logement"65 bits = [b for b in [d.get("unit_type"), d.get("sector") or d.get("city")]66 if b]67 return {68 "item_id": d["uid"],69 "title": str(title)[:200],70 "subtitle": " · ".join(str(b) for b in bits)[:200],71 "price_label": (d.get("price_label")72 or (f"{int(d['price'])} $/mois" if d.get("price") else ""))[:60],73 "image_url": (images[0] if images else "")[:500],74 "url": f"https://www.rent-ka.com/listing/{d['uid']}",75 }76