# ----------------------------------------------------------------------------- # Home-Ka — US real-estate aggregator (Groupe KA) # Author: Simon-Pierre Boucher — contact@spboucher.ai # connectors/json/feed.py : generic JSON API connector — partner APIs, # brokerage internal search APIs, WordPress REST (wp-json) listing CPTs. # Config-driven; pagination by page parameter or by next-link path. # # {"id": "acme_api", "connector_type": "json", "config": { # "url": "https://www.acmerealty.com/api/listings?page={page}", # "method": "GET", "headers": {}, "body": null, # "items_path": "data.results", # dot path to the record list # "next_path": "data.next", # OR page-based: start_page/max_pages # "start_page": 1, "max_pages": 50, "stop_when_empty": true, # "mapping": {"external_id": "id", "url": "permalink", # "list_price": "price", "images": "photos[].url", ...}, # "static": {"state": "CO"}}} # ----------------------------------------------------------------------------- from __future__ import annotations from ..base import BaseConnector from .._maputil import apply_mapping, dig from ...schema import Listing class JSONFeedConnector(BaseConnector): family = "json" request_delay = 0.5 def fetch(self) -> list[Listing]: cfg = self.config mapping = cfg.get("mapping") or {} static = cfg.get("static") or {} items_path = cfg.get("items_path", "") headers = cfg.get("headers") or {} out: list[Listing] = [] seen: set[str] = set() url_tpl = cfg.get("url", "") page = int(cfg.get("start_page", 1)) max_pages = int(cfg.get("max_pages", 100)) next_url: str | None = None for _ in range(max_pages): url = next_url or url_tpl.format(page=page) if cfg.get("method", "GET").upper() == "POST": body = cfg.get("body") if isinstance(body, dict): body = {k: (str(v).format(page=page) if isinstance(v, str) else v) for k, v in body.items()} resp = self.post(url, json=body, headers=headers) else: resp = self.get(url, headers=headers) data = resp.json() items = dig(data, items_path) if items_path else data if not isinstance(items, list): items = [items] if isinstance(items, dict) else [] new = 0 for rec in items: if not isinstance(rec, dict): continue lst = apply_mapping(self.source_id, rec, mapping, static) if lst is not None and lst.uid not in seen: seen.add(lst.uid) out.append(lst) new += 1 if cfg.get("next_path"): next_url = dig(data, cfg["next_path"]) if not next_url: break else: page += 1 if cfg.get("stop_when_empty", True) and new == 0: break if "{page}" not in url_tpl: break return out