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# scripts/merge_candidates.py : merge data/<platform>_candidates.json (output5# of the discovery campaigns) into the live registries + sources.json.6# Platforms: appfolio (apf_), buildium (bld_). Dedup by id/subdomain and by7# normalized name across every platform registry (a manager already covered8# via LiftSystem/RentCafe/etc. must not be ingested twice). Idempotent.9# Usage: python scripts/merge_candidates.py10# -----------------------------------------------------------------------------11from __future__ import annotations1213import json14import re15from pathlib import Path1617ROOT = Path(__file__).resolve().parents[1]18SOURCES = ROOT / "data" / "sources.json"19PLATFORMS = { # candidates file -> (registry file, source_id prefix)20 "appfolio": ("appfolio_clients.json", "apf_"),21 "buildium": ("buildium_clients.json", "bld_"),22}232425def norm(s: str) -> str:26 return re.sub(r"[^a-z0-9]", "", (s or "").lower())272829def main() -> None:30 sources = json.loads(SOURCES.read_text("utf-8"))31 have_names = {norm(s.get("name")) for s in sources["sources"]}32 all_regs = ["appfolio_clients.json", "buildium_clients.json",33 "liftsystem_clients.json", "rentcafe_clients.json",34 "rentsyncgw_clients.json"]35 for reg in all_regs:36 try:37 d = json.loads((ROOT / "data" / reg).read_text("utf-8"))38 have_names |= {norm(c.get("name")) for c in d.get("clients") or []}39 except (OSError, ValueError):40 pass4142 total_added = 043 for plat, (reg_name, prefix) in PLATFORMS.items():44 cand_path = ROOT / "data" / f"{plat}_candidates.json"45 if not cand_path.exists():46 print(f"[merge] {plat}: no candidates file, skipped")47 continue48 cands = json.loads(cand_path.read_text("utf-8")).get("clients") or []49 reg_path = ROOT / "data" / reg_name50 reg = json.loads(reg_path.read_text("utf-8"))51 have_ids = {c.get("id") for c in reg["clients"]}52 have_subs = {c.get("subdomain") for c in reg["clients"]}53 added = 054 for c in cands:55 if c.get("status") != "valide" or not c.get("subdomain"):56 continue57 if c["id"] in have_ids or c["subdomain"] in have_subs or \58 norm(c.get("name")) in have_names:59 continue60 reg["clients"].append(c)61 have_ids.add(c["id"])62 have_subs.add(c["subdomain"])63 have_names.add(norm(c.get("name")))64 if plat == "appfolio":65 listing_url = f"https://{c['subdomain']}.appfolio.com/listings"66 else:67 listing_url = (f"https://{c['subdomain']}.managebuilding.com"68 f"/Resident/public/rentals")69 regions = c.get("regions") or []70 sources["sources"].append({71 "id": f"{prefix}{c['id']}",72 "name": c.get("name") or c["id"],73 "url": c.get("site") or listing_url,74 "listing_url": listing_url,75 "sectors": ", ".join(regions) if isinstance(regions, list)76 else str(regions),77 "connector": f"{prefix}{c['id']}",78 "status": "actif",79 "region": "Canada (outside Québec)",80 "notes": f"canada-expansion 2026-08-28 — {plat} — "81 f"{c.get('notes', '')}"[:300],82 })83 added += 184 reg["updated"] = "2026-08-28"85 reg_path.write_text(json.dumps(reg, ensure_ascii=False, indent=1))86 print(f"[merge] {plat}: +{added} clients "87 f"(registry now {len(reg['clients'])})")88 total_added += added8990 SOURCES.write_text(json.dumps(sources, ensure_ascii=False, indent=2))91 print(f"[merge] sources.json now {len(sources['sources'])} entries "92 f"(+{total_added})")939495if __name__ == "__main__":96 main()97