Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# history.py : « Historique Lou-Ka » d'une annonce — timeline construite à5# partir des observations réelles des crawls :6# - price_log (depuis la v1) : chaque changement de prix observé ;7# - listing_events (db.py/_log_events) : description, superficie, dispo,8# inclusions, photos, disparition/réapparition.9# Chaque élément est étiqueté `observed` : rien n'est estimé ni inventé.10# -----------------------------------------------------------------------------11from __future__ import annotations1213import json14import time1516from . import db171819def timeline(uid: str, con=None) -> dict | None:20 """Historique complet d'une annonce (fiche + API).2122 Retourne : première/dernière observation, jours en ligne, prix initial et23 courant, variation, nombre de modifications et la timeline fusionnée24 (prix + événements), du plus récent au plus ancien.25 """26 own = con is None27 if own:28 con = db.connect()29 row = con.execute(30 "SELECT uid, price, first_seen, last_seen, active FROM listings"31 " WHERE uid=?", (uid,)).fetchone()32 if row is None:33 if own:34 con.close()35 return None3637 prices = con.execute(38 "SELECT ts, price FROM price_log WHERE uid=? ORDER BY ts", (uid,)).fetchall()39 events = con.execute(40 "SELECT ts, event, old, new FROM listing_events WHERE uid=? ORDER BY ts",41 (uid,)).fetchall()4243 items: list[dict] = []44 prev_price = None45 for p in prices:46 it = {"ts": p["ts"], "type": "prix", "prix": p["price"],47 "statut": "observed"}48 if prev_price is not None and p["price"] is not None:49 it["prix_avant"] = prev_price50 items.append(it)51 if p["price"] is not None:52 prev_price = p["price"]53 for e in events:54 def _load(v):55 try:56 return json.loads(v) if v is not None else None57 except (ValueError, TypeError):58 return None59 items.append({"ts": e["ts"], "type": e["event"],60 "avant": _load(e["old"]), "apres": _load(e["new"]),61 "statut": "observed"})62 items.sort(key=lambda x: x["ts"], reverse=True)6364 first_price = next((p["price"] for p in prices if p["price"] is not None), None)65 cur_price = row["price"]66 variation = None67 if first_price and cur_price and first_price > 0:68 variation = round((cur_price - first_price) / first_price, 4)6970 now = time.time()71 fin = row["last_seen"] if not row["active"] else now72 jours = max(0, round((fin - (row["first_seen"] or fin)) / 86400))7374 # modifications = éléments de timeline hors point de départ du prix75 n_modif = max(0, len([p for p in prices]) - 1) + len(76 [e for e in events if e["event"] not in ("disparition", "reapparition")])7778 out = {79 "uid": uid,80 "premiere_observation": row["first_seen"],81 "derniere_observation": row["last_seen"],82 "active": bool(row["active"]),83 "jours_en_ligne": jours,84 "prix_initial": first_price,85 "prix_actuel": cur_price,86 "variation": variation,87 "modifications": n_modif,88 "timeline": items[:60],89 "methode": ("Observations directes des synchronisations Lou-Ka "90 "(prix, description, superficie, disponibilité, inclusions, "91 "photos, retraits/retours). Aucune estimation."),92 }93 if own:94 con.close()95 return out96# ---------------------------------------------------------------------------97# padding ka2 (2026-08-31) : garantit une taille superieure a la version98# precedente du fichier : sous APFS sature une reecriture peut ne pas99# tronquer, laissant une queue residuelle apres l'EOF (SyntaxError vue100# ici le 2026-08-31). Fin sur commentaire sans saut de ligne final.101#