# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de propriétés à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # vraiprix.py : estimation de valeur marchande via Vrai-Prix (www.vrai-prix.com) # Pour chaque annonce : /api/search?q= → id de l'unité d'évaluation, # puis /api/estimate?id → valeur estimée, fourchette P10-P90, indice de # confiance. Repli : POST /api/estimate (municipalité + type + superficie). # Résultat stocké dans listings.vraiprix (JSON) + lien vers /estimation/{id}. # ----------------------------------------------------------------------------- from __future__ import annotations import json import time import unicodedata import requests from . import db BASE = "https://www.vrai-prix.com" UA = "ImmoKaBot/1.0 (+https://www.immo-ka.com; contact@spboucher.ai)" DELAY = 0.25 # politesse # type Immo-Ka -> typeProp Vrai-Prix (repli manuel) _TYPE_MAP = { "Maison": "unifamilial", "Condo": "condo", "Duplex": "plex", "Triplex": "plex", "Multiplex": "plex", "Jumelé": "unifamilial", "Maison de ville": "unifamilial", "Chalet": "unifamilial", "Fermette/Agricole": "unifamilial", "Maison mobile": "unifamilial", } def _norm(s: str) -> str: return "".join(c for c in unicodedata.normalize("NFD", (s or "").lower()) if unicodedata.category(c) != "Mn").strip() def _session() -> requests.Session: s = requests.Session() s.headers["User-Agent"] = UA return s def _search_id(s: requests.Session, address: str, city: str) -> str | None: """Retourne l'id de l'unité Vrai-Prix la mieux appariée à l'adresse.""" q = f"{address}, {city}".strip(", ") if len(q) < 4: return None try: r = s.get(f"{BASE}/api/search", params={"q": q}, timeout=20) results = r.json().get("results", []) except Exception: return None if not results: return None ncity = _norm(city) # préférer une correspondance de municipalité, sinon le 1er for res in results: if ncity and _norm(res.get("municipalite", "")) == ncity: return res.get("id") return results[0].get("id") def _estimate_by_id(s: requests.Session, uid: str) -> dict | None: try: r = s.get(f"{BASE}/api/estimate", params={"id": uid}, timeout=25) return r.json() except Exception: return None def _pack(unit_id: str, result: dict) -> dict: """Champs conservés pour l'affichage sur la fiche Immo-Ka.""" return { "id": unit_id, "value": result.get("estimate"), "low": result.get("low"), "high": result.get("high"), "confidence_pct": result.get("confidencePct"), "confidence": result.get("confidenceLevel"), "url": f"{BASE}/estimation/{unit_id}", } def estimate_for(s: requests.Session, address: str, city: str) -> dict | None: uid = _search_id(s, address, city) if not uid: return None data = _estimate_by_id(s, uid) if not data or not isinstance(data.get("result"), dict): return None return _pack(uid, data["result"]) def run(limit: int | None = None) -> dict: """Enrichit les annonces sans estimation Vrai-Prix (adresse requise).""" con = db.connect() s = _session() rows = con.execute( "SELECT uid, address, city FROM listings" " WHERE active=1 AND vraiprix IS NULL AND address<>''" " ORDER BY first_seen DESC").fetchall() done = miss = 0 for r in rows: if limit is not None and done + miss >= limit: break est = estimate_for(s, r["address"], r["city"]) # on marque toujours (même un échec) pour ne pas re-sonder sans cesse ; # « {} » = tenté sans correspondance. con.execute("UPDATE listings SET vraiprix=? WHERE uid=?", (json.dumps(est or {}, ensure_ascii=False), r["uid"])) con.commit() if est: done += 1 else: miss += 1 time.sleep(DELAY) con.close() out = {"estimated": done, "no_match": miss} print(f"[immo-ka] vraiprix: {out}") return out