Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 47.5%
HTML 27.9%
TypeScript 15.5%
CSS 7.2%
JavaScript 2%
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/agences_source_immo.py : agences INDÉPENDANTES sur la plateforme5# source.immo (plugin WordPress). Beaucoup de petites/moyennes agences QC6# l'utilisent (liste sur source.immo). Chacune expose sa config publique à7# /wp-content/uploads/_sourceimmo/_configs.json → on lit l'API publique8# api-v1.source.immo (voir SourceImmoConnector). Ce sont des agences9# autonomes : leurs inscriptions (leur propre n° Centris) sont ADDITIVES —10# pas de sous-agence d'une bannière déjà couverte → source_id normal (pas _ag_).11# EXCEPTION : les équipes/agences dont une partie des inscriptions est12# co-listée avec une bannière couverte (co-inscription Centris) portent un id13# « si_ag_<slug> » : la dédup Centris (db.refresh_dedup, sources %_ag_%)14# masque leurs doublons tout en gardant leurs fiches uniques visibles.15#16# Un connecteur par agence est généré depuis data/source_immo_agencies.json.17# -----------------------------------------------------------------------------18from __future__ import annotations1920import json21from pathlib import Path2223from .sourceimmo import SourceImmoConnector24from ..schema import PropertyListing2526REGISTRY = Path(__file__).resolve().parent.parent.parent / "data" / "source_immo_agencies.json"272829class _AgenceSourceImmo(SourceImmoConnector):30 """Agence indépendante source.immo. Recharge sa config depuis le site31 (résilient aux rotations d'UUID) ; agency_name pour l'affichage Sources."""3233 agency_name = ""34 # segment d'URL des fiches sur le site WordPress (raccourci source.immo) —35 # certains thèmes utilisent autre chose que « propriete » (ex. BLVD :36 # « details-de-la-propriete ») ; surchargé via listing_path du registre.37 listing_path = "propriete"3839 def fetch(self) -> list[PropertyListing]:40 base = self.site_url.rstrip("/")41 try:42 cfg = self.get(f"{base}/wp-content/uploads/_sourceimmo/_configs.json").json()43 self.account_id = cfg.get("account_id", self.account_id)44 self.api_key = cfg.get("api_key", self.api_key)45 self.app_id = cfg.get("app_id", self.app_id)46 self.view_id = cfg.get("default_view", self.view_id)47 except Exception:48 pass49 return super().fetch()5051 def _to_listing(self, it: dict) -> PropertyListing | None:52 lst = super()._to_listing(it)53 if lst is not None:54 lst.broker_name = self.agency_name55 lst.agency = self.agency_name56 ref = it.get("ref_number") or it.get("id")57 if ref and self.site_url:58 lst.url = f"{self.site_url.rstrip('/')}/{self.listing_path}/{ref}"59 return lst606162def _load() -> list[dict]:63 try:64 return json.loads(REGISTRY.read_text(encoding="utf-8"))65 except Exception:66 return []676869# Génère une classe par agence du registre.70for _ag in _load():71 if not all(_ag.get(k) for k in ("id", "domain", "account", "api", "app", "view")):72 continue73 _sid = _ag["id"]74 _dom = _ag["domain"].rstrip("/")75 globals()[f"AGENCE_{_sid.upper()}"] = type(76 "AgenceSI" + "".join(p.title() for p in _sid.split("_")),77 (_AgenceSourceImmo,),78 {79 "source_id": _sid,80 "account_id": _ag["account"], "api_key": _ag["api"],81 "app_id": _ag["app"], "view_id": _ag["view"],82 "site_url": f"https://{_dom}/",83 "agency_name": _ag.get("name", _sid.title()),84 "listing_path": _ag.get("listing_path", "propriete"),85 },86 )87