# ----------------------------------------------------------------------------- # Home-Ka — US real-estate aggregator (Groupe KA) # Author: Simon-Pierre Boucher — contact@spboucher.ai # connectors/_maputil.py : shared helpers for config-driven field mapping. # # Family connectors (json, xml, csv, sftp, reso) turn a raw record into a # Listing through a declarative mapping stored in the source config: # "mapping": {"external_id": "ListingId", "list_price": "price.amount", # "images": "photos[].url", "city": "address.city", ...} # Paths are dot-separated; "[]" fans out over a list. Unmapped raw fields are # preserved in Listing.details for the RESO passthrough in schema.finalize(). # ----------------------------------------------------------------------------- from __future__ import annotations from ..schema import Listing __all__ = ["dig", "apply_mapping"] _LIST_FIELDS = {"images", "features"} def dig(record, path: str): """Resolve a dot path in a nested dict/list structure. "photos[].url" → [p["url"] for p in record["photos"]]; "address.city" → record["address"]["city"]. Returns None when absent. """ cur = record for part in str(path).split("."): if cur is None: return None fan = part.endswith("[]") key = part[:-2] if fan else part if key: if isinstance(cur, dict): cur = cur.get(key) elif isinstance(cur, list): cur = [c.get(key) if isinstance(c, dict) else None for c in cur] else: return None if fan and not isinstance(cur, list): cur = [cur] if cur is not None else [] return cur def apply_mapping(source_id: str, record: dict, mapping: dict, static: dict | None = None) -> Listing | None: """Build a Listing from a raw record and a declarative mapping.""" kw: dict = {"source": source_id, "external_id": "", "url": ""} for field, path in (mapping or {}).items(): v = dig(record, path) if v is None: continue if field in _LIST_FIELDS: v = [x for x in (v if isinstance(v, list) else [v]) if x] kw[field] = v for field, v in (static or {}).items(): kw.setdefault(field, v) if not kw.get("external_id"): return None kw["external_id"] = str(kw["external_id"]) kw.setdefault("details", {}) # keep flat raw scalars for the RESO passthrough / debugging if isinstance(record, dict): for k, v in record.items(): if isinstance(v, (str, int, float, bool)) and k not in kw["details"]: kw["details"][k] = v allowed = set(Listing.__dataclass_fields__) return Listing(**{k: v for k, v in kw.items() if k in allowed})