spb/immo-ka Public
Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 66.4%
TypeScript 19.9%
CSS 13.2%
HTML 0.5%
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/century21_agences.py : Century 21 par SOUS-AGENCES (robuste)5#6# Le portail central c21.ca délègue à une iframe moxiworks verrouillée7# (503/WAF, même via Firecrawl). Mais les bureaux Century 21 du Québec tournent8# sur la plateforme source.immo (ID-3 Technologies) : quebec.c21.ca,9# excel.c21.ca et century21immoplus.com partagent un même compte10# « Century 21 - All Quebec » (inventaire provincial complet), et11# vision.c21.ca a son propre compte. L'API publique (api-v1.source.immo)12# est paginée par st/nt (voir sourceimmo.py) — inventaire complet accessible.13#14# Une classe par sous-agence est générée depuis data/century21_agencies.json15# (source_id « c21_ag_<slug> »). Le n° Centris sert de clé de déduplication16# (voir web.py, sources « %_ag_% ») — aucun double-comptage entre les skins17# partageant le même compte ni avec le reste du site.18# -----------------------------------------------------------------------------19from __future__ import annotations2021import json22import re23from pathlib import Path2425from .sourceimmo import SourceImmoConnector26from ..schema import PropertyListing2728REGISTRY = Path(__file__).resolve().parent.parent.parent / "data" / "century21_agencies.json"293031class Century21AgenceConnector(SourceImmoConnector):32 """Sous-agence Century 21 sur source.immo. Les sous-classes fournissent33 account_id / api_key / app_id / view_id / site_url / agency_name."""3435 agency_name = "Century 21"3637 def fetch(self) -> list[PropertyListing]:38 # auto-guérison : recharger les identifiants depuis la config publique39 # du site (résilient si source.immo fait tourner ses UUID).40 try:41 cfg = self.get(42 f"{self.site_url.rstrip('/')}/wp-content/uploads/_sourceimmo/_configs.json"43 ).json()44 self.account_id = cfg.get("account_id", self.account_id)45 self.api_key = cfg.get("api_key", self.api_key)46 self.app_id = cfg.get("app_id", self.app_id)47 self.view_id = cfg.get("default_view", self.view_id)48 except Exception:49 pass # on garde les valeurs du registre50 return super().fetch()5152 def _to_listing(self, it: dict) -> PropertyListing | None:53 lst = super()._to_listing(it)54 if lst is not None:55 lst.broker_name = 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('/')}/propriete/{ref}"59 return lst606162def _load() -> list[dict]:63 try:64 return json.loads(REGISTRY.read_text(encoding="utf-8"))65 except Exception:66 return []676869def _slug(banner: str) -> str:70 return "c21_ag_" + re.sub(r"[^a-z0-9]+", "_", banner.lower()).strip("_")717273for _ag in _load():74 if not all(_ag.get(k) for k in ("domain", "account", "api", "app", "view")):75 continue76 _sid = _slug(_ag["banner"])77 _name = "Century 21 " + _ag["banner"].replace("-", " ").title()78 globals()[_sid.upper()] = type(79 "".join(p.title() for p in _sid.split("_")),80 (Century21AgenceConnector,),81 {82 "source_id": _sid,83 "account_id": _ag["account"],84 "api_key": _ag["api"],85 "app_id": _ag["app"],86 "view_id": _ag["view"],87 "site_url": f"https://{_ag['domain']}",88 "agency_name": _name,89 },90 )91