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%
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