SPB Git

spb/ora-ka Public

Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)

Python 80% TypeScript 12.9% CSS 6.8%
4.1 KB · 122 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# vraiprix.py : estimation de valeur marchande via Vrai-Prix (www.vrai-prix.com)5#   Pour chaque annonce : /api/search?q=<adresse> → id de l'unité d'évaluation,6#   puis /api/estimate?id → valeur estimée, fourchette P10-P90, indice de7#   confiance. Repli : POST /api/estimate (municipalité + type + superficie).8#   Résultat stocké dans listings.vraiprix (JSON) + lien vers /estimation/{id}.9# -----------------------------------------------------------------------------10from __future__ import annotations1112import json13import time14import unicodedata1516import requests1718from . import db1920BASE = "https://www.vrai-prix.com"21UA = "ImmoKaBot/1.0 (+https://www.immo-ka.com; contact@spboucher.ai)"22DELAY = 0.25   # politesse2324# type Immo-Ka -> typeProp Vrai-Prix (repli manuel)25_TYPE_MAP = {26    "Maison": "unifamilial", "Condo": "condo", "Duplex": "plex",27    "Triplex": "plex", "Multiplex": "plex", "Jumelé": "unifamilial",28    "Maison de ville": "unifamilial", "Chalet": "unifamilial",29    "Fermette/Agricole": "unifamilial", "Maison mobile": "unifamilial",30}313233def _norm(s: str) -> str:34    return "".join(c for c in unicodedata.normalize("NFD", (s or "").lower())35                   if unicodedata.category(c) != "Mn").strip()363738def _session() -> requests.Session:39    s = requests.Session()40    s.headers["User-Agent"] = UA41    return s424344def _search_id(s: requests.Session, address: str, city: str) -> str | None:45    """Retourne l'id de l'unité Vrai-Prix la mieux appariée à l'adresse."""46    q = f"{address}, {city}".strip(", ")47    if len(q) < 4:48        return None49    try:50        r = s.get(f"{BASE}/api/search", params={"q": q}, timeout=20)51        results = r.json().get("results", [])52    except Exception:53        return None54    if not results:55        return None56    ncity = _norm(city)57    # préférer une correspondance de municipalité, sinon le 1er58    for res in results:59        if ncity and _norm(res.get("municipalite", "")) == ncity:60            return res.get("id")61    return results[0].get("id")626364def _estimate_by_id(s: requests.Session, uid: str) -> dict | None:65    try:66        r = s.get(f"{BASE}/api/estimate", params={"id": uid}, timeout=25)67        return r.json()68    except Exception:69        return None707172def _pack(unit_id: str, result: dict) -> dict:73    """Champs conservés pour l'affichage sur la fiche Immo-Ka."""74    return {75        "id": unit_id,76        "value": result.get("estimate"),77        "low": result.get("low"),78        "high": result.get("high"),79        "confidence_pct": result.get("confidencePct"),80        "confidence": result.get("confidenceLevel"),81        "url": f"{BASE}/estimation/{unit_id}",82    }838485def estimate_for(s: requests.Session, address: str, city: str) -> dict | None:86    uid = _search_id(s, address, city)87    if not uid:88        return None89    data = _estimate_by_id(s, uid)90    if not data or not isinstance(data.get("result"), dict):91        return None92    return _pack(uid, data["result"])939495def run(limit: int | None = None) -> dict:96    """Enrichit les annonces sans estimation Vrai-Prix (adresse requise)."""97    con = db.connect()98    s = _session()99    rows = con.execute(100        "SELECT uid, address, city FROM listings"101        " WHERE active=1 AND vraiprix IS NULL AND address<>''"102        " ORDER BY first_seen DESC").fetchall()103    done = miss = 0104    for r in rows:105        if limit is not None and done + miss >= limit:106            break107        est = estimate_for(s, r["address"], r["city"])108        # on marque toujours (même un échec) pour ne pas re-sonder sans cesse ;109        # « {} » = tenté sans correspondance.110        con.execute("UPDATE listings SET vraiprix=? WHERE uid=?",111                    (json.dumps(est or {}, ensure_ascii=False), r["uid"]))112        con.commit()113        if est:114            done += 1115        else:116            miss += 1117        time.sleep(DELAY)118    con.close()119    out = {"estimated": done, "no_match": miss}120    print(f"[immo-ka] vraiprix: {out}")121    return out122