SPB Git forge

spb/home-ka

Public
10commits 1branches 0releases
793.0 KBsize
maindefault branch
20 days agolast push
Python 49.6% TypeScript 25.5% CSS 24.1%
5.8 KB · 142 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Home-Ka — US real-estate aggregator (Groupe KA)3# Author: Simon-Pierre Boucher — contact@spboucher.ai4# ingest.py : ingestion pipeline — runs the connectors and syncs the database5#             (adds / updates / removals) = content always current.6#7#   SOURCE → connector → raw ingestion → normalization → property matching8#          → deduplication → canonical listing → search index → Home-Ka9#10# Sources come from two registries (see connectors/__init__.py):11#   - rows of the `sources` table (config-driven family connectors)12#   - CUSTOM one-site classes (connectors/custom_broker/*.py)13# -----------------------------------------------------------------------------14from __future__ import annotations1516import sys17import time18import traceback1920from . import connectors as creg21from . import db222324def _targets(con, requested: list[str] | None) -> list[tuple[str, str, dict]]:25    """(source_id, connector_type, config) for every runnable source."""26    rows = {s["id"]: s for s in db.get_sources(con)}27    out: list[tuple[str, str, dict]] = []28    ids = requested or (29        [s["id"] for s in rows.values() if s["enabled"]]30        + [sid for sid in creg.CUSTOM if sid not in rows])31    for sid in ids:32        row = rows.get(sid)33        if row is not None:34            if not requested and not row["enabled"]:35                continue36            out.append((sid, row["connector_type"] or "", row["config"]))37        elif sid in creg.CUSTOM:38            out.append((sid, "custom", {}))39        else:40            print(f"[home-ka] unknown source: {sid}", file=sys.stderr)41    return out424344def run(sources: list[str] | None = None) -> list[dict]:45    """Run the ingestion for all sources (or the requested ones)."""46    con = db.connect()47    results = []48    for sid, ctype, config in _targets(con, sources):49        conn = creg.build(sid, ctype, config)50        if conn is None:51            print(f"[home-ka] no connector for {sid} (type {ctype!r})",52                  file=sys.stderr)53            continue54        # slow sources (public records...): config.sync_interval_hours skips55        # the source while its last successful sync is fresh enough56        interval_h = float((config or {}).get("sync_interval_hours") or 0)57        if interval_h and not sources:58            last = con.execute(59                "SELECT MAX(ts) ts FROM sync_log WHERE source=? AND ok=1",60                (sid,)).fetchone()["ts"]61            if last and time.time() - last < interval_h * 3600:62                continue63        t0 = time.time()64        print(f"[home-ka] sync {sid} ...")65        try:66            if getattr(conn, "is_public_records", False):67                from .connectors.public_data.arcgis import sync_records68                stats = sync_records(con, sid, conn.fetch_records())69            else:70                listings = conn.fetch()71                finalized, dropped = [], 072                for lst in listings:73                    try:74                        finalized.append(lst.finalize())75                    except Exception:   # one bad listing never blocks a source76                        dropped += 177                stats = db.sync_source(con, sid, finalized)78                if dropped:79                    stats["dropped"] = dropped80            stats["seconds"] = round(time.time() - t0, 1)81            if stats.get("alert"):82                print(f"[home-ka]   ⚠ ALERT {sid}: {stats['alert']}")83            print(f"[home-ka]   {stats}")84            results.append(stats)85        except Exception as exc:   # one source never blocks the others86            db.log_failure(con, sid, f"{exc}")87            traceback.print_exc()88            results.append({"source": sid, "error": str(exc)})89    # precomputed dedup (instant reads, same principle as immo-ka)90    try:91        hidden = db.refresh_dedup(con)92        print(f"[home-ka] dedup: {hidden} duplicate(s) hidden "93              "(MLS number + same-property cross-source)")94    except Exception:95        traceback.print_exc()96    # quality gate: completeness score, publication threshold (quarantine)97    try:98        from . import quality99        q = quality.refresh(con)100        print(f"[home-ka] quality: {q['published']} published, "101              f"{q['quarantined']} quarantined")102    except Exception:103        traceback.print_exc()104    try:105        con.execute("PRAGMA optimize")106    except Exception:107        pass108    con.close()109    return results110111112def watch(interval_seconds: int = 3600) -> None:113    """Periodic sync loop (webhook equivalent, via PM2/cron).114115    After each sync: batch geocoding of new addresses (US Census geocoder,116    free), then the budgeted image audit.117    """118    while True:119        run()120        try:121            from . import geocode122            geocode.run_batch()123        except Exception as exc:124            print(f"[home-ka] geocode: non-blocking error: {exc}", file=sys.stderr)125        try:126            from . import imgaudit127            ia = imgaudit.run_batch(limit=2000)128            print(f"[home-ka] images: {ia['urls_verifiees']} URL(s) checked, "129                  f"{ia['images_retirees']} removed, "130                  f"{ia['sans_image_valide']} listing(s) without a valid image")131        except Exception as exc:132            print(f"[home-ka] imgaudit: non-blocking error: {exc}", file=sys.stderr)133        try:   # budgeted brokerage discovery pass (inspect a few prospects)134            from . import discovery135            d = discovery.run_batch(limit=15)136            if d.get("inspected"):137                print(f"[home-ka] discovery: {d['inspected']} brokerage(s) inspected")138        except Exception as exc:139            print(f"[home-ka] discovery: non-blocking error: {exc}", file=sys.stderr)140        print(f"[home-ka] next sync in {interval_seconds // 60} min")141        time.sleep(interval_seconds)142