SPB Git forge

spb/rent-ka

Public
8commits 1branches 0releases
7.4 MBsize
maindefault branch
19 days agolast push
Python 68.8% TypeScript 18.6% CSS 8.7% JavaScript 3.3% HTML 0.6%
7.3 KB · 176 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Rent-Ka — Rental listings aggregator (Canada, outside Québec)3# Author: Simon-Pierre Boucher — contact@spboucher.ai4# scripts/build_lift_registry.py : turn data/liftsystem_probe.jsonl (output of5#   probe_liftsystem.py) into liftsystem_clients.json entries + sources.json6#   entries. Keeps clients with >= MIN_LISTINGS residential properties outside7#   Québec; excludes clients already covered (existing lift_* registry entries,8#   dedicated connectors, other platform registries) by client_id and by9#   normalized name. Idempotent: re-running refreshes the generated entries10#   (marker "canada-expansion" in notes) without touching hand-written ones.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import json15import re16from pathlib import Path1718ROOT = Path(__file__).resolve().parents[1]19PROBE = ROOT / "data" / "liftsystem_probe.jsonl"20LIFT = ROOT / "data" / "liftsystem_clients.json"21SOURCES = ROOT / "data" / "sources.json"2223MIN_LISTINGS = 324MARKER = "canada-expansion 2026-08-27"2526# client_ids covered by dedicated Rent-Ka connectors (or the platform vendor27# itself) — verified against docs/canada-sources.md.28EXCLUDE_IDS = {29    1,      # Landlord Web Solutions (the vendor)30    203,    # Boardwalk (dedicated)31    228,    # CAPREIT (dedicated)32    99,     # Killam (dedicated)33    153,    # Minto (dedicated)34    6,      # Homestead (dedicated)35    96,     # Skyline (dedicated)36    436,    # Realstar (dedicated)37    497,    # Hazelview (dedicated)38    21,     # Centurion (dedicated)39    518,    # InterRent / CLV (dedicated)40    5,      # Osgoode (rc_osgoode)41    17,     # Effort Trust (rc_effort)42    53,     # Sterling Karamar (rsgw_sterlingkaramar)43    4,      # Drewlo (rsgw_drewlo)44    1746,   # « Sterling Karamar » (2e flux LiftSystem — déjà rsgw_sterlingkaramar)45    1198,   # « (NEW WEBSITE) Drewlo Holdings2 » (déjà rsgw_drewlo)46    467,    # « Minto Corp Services (Investors) » (déjà minto dédié)47}48PROV_NAMES = {49    "ON": "Ontario", "BC": "British Columbia", "AB": "Alberta",50    "SK": "Saskatchewan", "MB": "Manitoba", "NB": "New Brunswick",51    "NS": "Nova Scotia", "PE": "Prince Edward Island",52    "NL": "Newfoundland and Labrador", "YT": "Yukon",53    "NT": "Northwest Territories", "NU": "Nunavut",54}555657def norm(s: str) -> str:58    return re.sub(r"[^a-z0-9]", "", (s or "").lower())596061def slug(s: str) -> str:62    s = re.sub(r"[^a-z0-9]+", "", (s or "").lower()63               .replace("properties", "").replace("property", "")64               .replace("management", "").replace("apartments", "")65               .replace("rentals", "").replace("group", "")66               .replace("realestate", "").replace("inc", ""))67    return s[:24] or "client"686970def main() -> None:71    lift = json.loads(LIFT.read_text("utf-8"))72    sources = json.loads(SOURCES.read_text("utf-8"))7374    # drop previously generated entries FIRST (idempotence), keep hand-written75    # ones — coverage sets must be computed on the FILTERED lists, otherwise a76    # re-run sees its own past output in have_names and re-adds nothing (bug77    # found 2026-08-28: sources.json fell from 675 to 100 entries).78    lift["clients"] = [c for c in lift["clients"]79                       if MARKER not in (c.get("notes") or "")]80    sources["sources"] = [s for s in sources["sources"]81                          if MARKER not in (s.get("notes") or "")]8283    # existing coverage: ids + normalized names from lift registry and sources84    have_cids = {c.get("client_id") for c in lift["clients"]}85    have_slugs = {c["id"] for c in lift["clients"]}86    have_names = {norm(c.get("name")) for c in lift["clients"]}87    have_names |= {norm(s.get("name")) for s in sources["sources"]}88    for reg in ("rentcafe_clients.json", "buildium_clients.json",89                "appfolio_clients.json", "rentsyncgw_clients.json"):90        try:91            d = json.loads((ROOT / "data" / reg).read_text("utf-8"))92            have_names |= {norm(c.get("name")) for c in d.get("clients") or []}93        except (OSError, ValueError):94            pass9596    added, skipped_cov, skipped_small = [], 0, 097    for line in PROBE.read_text("utf-8").splitlines():98        try:99            rec = json.loads(line)100        except ValueError:101            continue102        cid = rec.get("client_id")103        if not cid or rec.get("error"):104            continue105        provs = {p: n for p, n in (rec.get("provinces") or {}).items()106                 if p != "QC" and p in PROV_NAMES}107        n_roc = sum(provs.values())108        if cid in EXCLUDE_IDS or cid in have_cids or \109                norm(rec.get("name")) in have_names:110            skipped_cov += 1111            continue112        if n_roc < MIN_LISTINGS:113            skipped_small += 1114            continue115        base = slug(rec["name"])116        sid = base117        i = 2118        while sid in have_slugs:119            sid = f"{base}{i}"120            i += 1121        have_slugs.add(sid)122        have_names.add(norm(rec["name"]))123        dominant = max(provs, key=provs.get)124        site = rec.get("website") or ""125        if site and not site.startswith("http"):126            site = "https://" + site127        prov_txt = ", ".join(f"{p} {n}" for p, n in128                             sorted(provs.items(), key=lambda x: -x[1]))129        lift["clients"].append({130            "id": sid,131            "name": rec["name"],132            "site": site,133            "client_id": cid,134            "listing_url": site,135            "regions": [PROV_NAMES[p] for p in136                        sorted(provs, key=provs.get, reverse=True)],137            "province": dominant,138            "status": "valide",139            "notes": f"{MARKER} — probe live: {n_roc} propriétés "140                     f"résidentielles hors QC ({prov_txt})",141        })142        sources["sources"].append({143            "id": f"lift_{sid}",144            "name": rec["name"],145            "url": site,146            "listing_url": site,147            "sectors": [PROV_NAMES[p] for p in provs],148            "connector": f"lift_{sid}",149            "status": "actif",150            "region": " / ".join(PROV_NAMES[p] for p in151                                 sorted(provs, key=provs.get, reverse=True))152                      + " — LiftSystem",153            "notes": f"{MARKER} — API LiftSystem client_id {cid}",154        })155        added.append((sid, cid, dominant, n_roc))156157    lift["updated"] = "2026-08-27"158    n = len(sources["sources"])159    sources["_comment"] = re.sub(r"^Rent-Ka source registry[^.]*\.",160                                 f"Rent-Ka source registry — {n} rental "161                                 f"sources for Canada outside Québec "162                                 f"(national expansion 2026-08-27).",163                                 sources["_comment"])164    LIFT.write_text(json.dumps(lift, ensure_ascii=False, indent=2))165    SOURCES.write_text(json.dumps(sources, ensure_ascii=False, indent=2))166    from collections import Counter167    per_prov = Counter(d for _, _, d, _ in added)168    print(f"[lift-registry] +{len(added)} clients "169          f"(covered elsewhere: {skipped_cov}, <{MIN_LISTINGS} listings: "170          f"{skipped_small}) — dominant province: {dict(per_prov)}")171    print(f"[lift-registry] sources.json now {n} entries")172173174if __name__ == "__main__":175    main()176