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#!/usr/bin/env python32# -----------------------------------------------------------------------------3# Immo-Ka — Annuaire des bannières & courtiers immobiliers du Québec4# bannieres.py : sert data/bannieres.json — les ~690 agences (bannières) du5# registre OACIQ et leurs 17 357 courtiers (recensement complet, 17 régions).6# Régénération du dataset : rescraper registre.oaciq.com (voir7# data/oaciq_census.json pour la méthode) puis reconstruire le JSON.8# -----------------------------------------------------------------------------9from __future__ import annotations1011import json12import threading13from pathlib import Path1415from fastapi import APIRouter, HTTPException1617ROOT = Path(__file__).resolve().parent.parent18DATA = ROOT / "data" / "bannieres.json"1920router = APIRouter()21_cache: dict = {"mtime": None, "doc": None, "index": None, "light": None}22_lock = threading.Lock()232425def load() -> dict:26 """Charge (avec cache par mtime) l'annuaire canonique des bannières."""27 try:28 mtime = DATA.stat().st_mtime29 except FileNotFoundError:30 return {"generated": None, "count": 0, "brokers_total": 0,31 "bannieres": [], "regions": []}32 with _lock:33 if _cache["doc"] is None or mtime != _cache["mtime"]:34 doc = json.loads(DATA.read_text(encoding="utf-8"))35 regs: dict[str, int] = {}36 for b in doc.get("bannieres", []):37 for r in b.get("regions", []):38 regs[r] = regs.get(r, 0) + 139 doc["regions"] = [{"region": r, "n": n}40 for r, n in sorted(regs.items(),41 key=lambda x: (-x[1], x[0]))]42 _cache["doc"], _cache["mtime"] = doc, mtime43 _cache["index"] = {b["id"]: b for b in doc.get("bannieres", [])}44 _cache["light"] = [{k: v for k, v in b.items() if k != "brokers"}45 for b in doc.get("bannieres", [])]46 return _cache["doc"]474849@router.get("/api/bannieres")50def api_bannieres(region: str | None = None, q: str | None = None):51 """Liste des bannières (sans le détail des courtiers) — filtres région/texte."""52 doc = load()53 items = _cache["light"] or []54 if region:55 items = [b for b in items if region in b.get("regions", [])]56 if q:57 needle = q.strip().lower()58 items = [b for b in items if needle in b["name"].lower()]59 return {"generated": doc.get("generated"),60 "total": doc.get("count", 0),61 "brokers_total": doc.get("brokers_total", 0),62 "count": len(items),63 "regions": doc.get("regions", []),64 "bannieres": items}656667@router.get("/api/bannieres/{banniere_id}")68def api_banniere(banniere_id: str):69 """Une bannière avec la liste complète de ses courtiers."""70 load()71 b = (_cache["index"] or {}).get(banniere_id)72 if b is None:73 raise HTTPException(status_code=404, detail="bannière inconnue")74 return b75