# ----------------------------------------------------------------------------- # Rent-Ka — Rental listings aggregator (Canada, outside Québec) # Author: Simon-Pierre Boucher — contact@spboucher.ai # scripts/probe_rentfaster_cities.py : discover RentFaster numeric city_ids. # /api/search.json only filters by numeric city_id (1=Calgary, 2=Edmonton, # 6=Vancouver…) but /api/map.json's `cities` array exposes names without ids. # This script probes city_id 1..N once, records {id, city, province, total} # and writes data/rentfaster_cities.json (consumed by connectors/rentfaster.py). # Province comes from the map.json cities array (name match) with a bbox # fallback on the first listing's coordinates. Resumable via the output file. # Usage: python scripts/probe_rentfaster_cities.py [max_id=700] # ----------------------------------------------------------------------------- from __future__ import annotations import json import sys import time from pathlib import Path import requests ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "data" / "rentfaster_cities.json" UA = {"User-Agent": "RentKaBot/1.0 (+https://www.rent-ka.com/bot; contact@spboucher.ai)", "Accept": "application/json"} sys.path.insert(0, str(ROOT)) from rentka.schema import PROVINCE_BBOX # noqa: E402 def bbox_province(lat: float, lng: float) -> str: for prov, (lo_lat, hi_lat, lo_lng, hi_lng) in PROVINCE_BBOX.items(): if lo_lat <= lat <= hi_lat and lo_lng <= lng <= hi_lng: return prov return "" def city_provinces() -> dict[str, str]: """name(lower) -> province from map.json's cities array (incl. markers).""" try: d = requests.post("https://www.rentfaster.ca/api/map.json", data={"x": "1"}, headers=UA, timeout=30).json() except Exception: return {} out: dict[str, str] = {} def walk(items): for c in items or []: name = (c.get("city") or "").strip().lower() if name and c.get("province"): out.setdefault(name, c["province"]) walk(c.get("markers")) walk(d.get("cities")) return out def main() -> None: max_id = int(sys.argv[1]) if len(sys.argv) > 1 else 700 known: dict[str, dict] = {} if OUT.exists(): try: known = {str(c["id"]): c for c in json.loads(OUT.read_text())["cities"]} except Exception: known = {} provs = city_provinces() print(f"[rf-probe] {len(provs)} city names with province from map.json") for cid in range(1, max_id + 1): if str(cid) in known: continue try: r = requests.get("https://www.rentfaster.ca/api/search.json", params={"city_id": str(cid), "cur_page": "0"}, headers=UA, timeout=25) d = r.json() except Exception: time.sleep(2) continue ls = d.get("listings") or [] total = int(d.get("total") or 0) if not ls: known[str(cid)] = {"id": cid, "city": "", "province": "", "total": 0} else: city = (ls[0].get("city") or "").strip() prov = provs.get(city.lower(), "") if not prov: try: prov = bbox_province(float(ls[0]["latitude"]), float(ls[0]["longitude"])) except (KeyError, TypeError, ValueError): prov = "" known[str(cid)] = {"id": cid, "city": city, "province": prov, "total": total} if cid % 50 == 0: print(f"[rf-probe] {cid}/{max_id}") OUT.write_text(json.dumps( {"_comment": "RentFaster city_id map — probed live; consumed " "by connectors/rentfaster.py (QC skipped there).", "updated": time.strftime("%Y-%m-%d"), "cities": sorted(known.values(), key=lambda c: c["id"])}, ensure_ascii=False, indent=1)) time.sleep(0.4) OUT.write_text(json.dumps( {"_comment": "RentFaster city_id map — probed live; consumed by " "connectors/rentfaster.py (QC skipped there).", "updated": time.strftime("%Y-%m-%d"), "cities": sorted(known.values(), key=lambda c: c["id"])}, ensure_ascii=False, indent=1)) active = [c for c in known.values() if c["total"] > 0] print(f"[rf-probe] done: {len(active)} active city_ids → {OUT}") if __name__ == "__main__": main()