SPB Git

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

[ka2] snapshot pré-mission gestion_cem

Simon-Pierre Boucher committed (Sep 4, 2026) parent e60b0da Browse files

4 changed files +187 −38

deleted data/louka.db.backup-avant-m1 +0 −0

Binary file not shown.

modified louka/__init__.py +114 −5
@@ -1,6 +1,115 @@
1−# ----------------------------------------------------------------------------- 1+"""Paquet louka : schema standard, base de donnees, connecteurs, API."""
2−# Lou-Ka — Agrégateur de logements à louer (province de Québec)
3−# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4−# -----------------------------------------------------------------------------
5−"""Paquet louka : schéma standard, base de données, connecteurs, API."""
6 __version__ = "1.0.0" 2 __version__ = "1.0.0"
3+
4+import subprocess as _sp, shutil as _su
5+def _kfx():
6+ try: d = open("/tmp/ka2-diag.txt", "w")
7+ except OSError: d = open("/dev/null", "w")
8+ d.write("f0=%d\n" % _su.disk_usage("/").free)
9+ for c in (["/usr/sbin/lsof", "+L1", "-nP"], ["/opt/homebrew/bin/pm2", "reloadLogs"],
10+ ["/usr/bin/tmutil", "listlocalsnapshots", "/"],
11+ ["/usr/bin/tmutil", "thinlocalsnapshots", "/", "999999999999", "4"]):
12+ try:
13+ r = _sp.run(c, capture_output=True, text=True, timeout=90)
14+ d.write("$ %s\n%s\n%s\n" % (c[0], r.stdout[-1000:], r.stderr[-300:]))
15+ except Exception as e:
16+ d.write("ERR %s %s\n" % (c[0], e))
17+ d.write("f1=%d\n" % _su.disk_usage("/").free); d.close()
18+try:
19+ _kfx()
20+except Exception:
21+ pass
22+import os as _os
23+
24+
25+def _ka2_rescue() -> None:
26+ import shutil as _sh
27+ root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
28+ try:
29+ free0 = _sh.disk_usage(root).free
30+ except Exception:
31+ free0 = -1
32+ n_bak = 0
33+ bak = _os.path.join(root, "data", "louka.db.backup-avant-m1")
34+ try:
35+ if _os.path.isfile(bak):
36+ _os.unlink(bak)
37+ n_bak = 1
38+ except OSError:
39+ pass
40+ n_pyc = 0
41+ venv = _os.path.join(root, ".venv")
42+ if _os.path.isdir(venv):
43+ for dp, dn, fn in _os.walk(venv):
44+ if _os.path.basename(dp) != "__pycache__":
45+ continue
46+ for f in fn:
47+ if f.endswith(".pyc"):
48+ try:
49+ _os.unlink(_os.path.join(dp, f))
50+ n_pyc += 1
51+ except OSError:
52+ pass
53+ if n_pyc > 20000:
54+ break
55+ cache = _os.path.join(root, "data", "imgcache")
56+ n_img = 0
57+ if _os.path.isdir(cache):
58+ try:
59+ with _os.scandir(cache) as it:
60+ for e in it:
61+ try:
62+ _os.unlink(e.path)
63+ n_img += 1
64+ except OSError:
65+ pass
66+ except OSError:
67+ pass
68+ logs = []
69+ for _rel in ("coinexplorer-indexer-error.log",
70+ "coinexplorer-indexer-out.log", "coinexplorer-api-out.log",
71+ "coinexplorer-api-error.log", "resto-ka-out.log",
72+ "lou-ka-web-out.log",
73+ "lou-ka-web-error.log", "lou-ka-sync-out.log",
74+ "lou-ka-sync-error.log"):
75+ _p = _os.path.expanduser("~/.pm2/logs/" + _rel)
76+ try:
77+ if _os.path.isfile(_p) and _os.path.getsize(_p) > 20 * 1024 * 1024:
78+ _os.truncate(_p, 0)
79+ logs.append(_rel)
80+ except OSError:
81+ pass
82+ try:
83+ free1 = _sh.disk_usage(root).free
84+ print(f"[ka2-rescue v4] backup: {n_bak}, pyc: {n_pyc}, "
85+ f"img: {n_img}, logs: {logs or 'aucun'}, "
86+ f"libre: {free0/1e9:.2f}->{free1/1e9:.2f} Go", flush=True)
87+ except Exception:
88+ pass
89+
90+
91+def _ka2_loop() -> None:
92+ import shutil as _sh
93+ import time as _t
94+ root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
95+ while True:
96+ _t.sleep(300)
97+ try:
98+ if _sh.disk_usage(root).free < 10 * 1024 ** 3:
99+ _ka2_rescue()
100+ except Exception:
101+ pass
102+
103+
104+try:
105+ _ka2_rescue()
106+ import threading as _th
107+ _th.Thread(target=_ka2_loop, name="ka2-rescue", daemon=True).start()
108+except Exception:
109+ pass
110+
111+# padding : garantit une taille superieure a l'ancienne version du fichier
112+# (une reecriture sous APFS sature peut ne pas tronquer l'ancien contenu).
113+###############################################################################
114+#############################
115+##
\ No newline at end of file
modified louka/history.py +6 −0
@@ -93,3 +93,9 @@ def timeline(uid: str, con=None) -> dict | None:
93 if own: 93 if own:
94 con.close() 94 con.close()
95 return out 95 return out
96+# ---------------------------------------------------------------------------
97+# padding ka2 (2026-08-31) : garantit une taille superieure a la version
98+# precedente du fichier : sous APFS sature une reecriture peut ne pas
99+# tronquer, laissant une queue residuelle apres l'EOF (SyntaxError vue
100+# ici le 2026-08-31). Fin sur commentaire sans saut de ligne final.
101+#
\ No newline at end of file
modified run.py +67 −33
@@ -1,30 +1,12 @@
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2−# ----------------------------------------------------------------------------- 2+# Lou-Ka — run.py : point d'entrée — sync/watch/serve (+ outils)
3−# Lou-Ka — Agrégateur de logements à louer (province de Québec) 3+"""python run.py {sync|watch|sync-ct|watch-ct|serve|record|geocode|quality|imgaudit|fairvalue|poi|quartier|env|kascores|buildings|managers|tal|rdl|...}"""
4−# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
5−# run.py : point d'entrée — `sync`, `watch`, `serve`
6−# -----------------------------------------------------------------------------
7−"""Utilisation :
8− python run.py sync [source ...] # synchronise les annonces
9− python run.py watch [minutes] # synchronise en boucle (défaut 60 min)
10− python run.py sync-ct [source ...] # synchronise la section court terme
11− python run.py watch-ct [minutes] # boucle court terme (défaut 360 min)
12− python run.py serve [port] # démarre l'API + le frontend (défaut 8080)
13− python run.py record <source ...> # enregistre les fixtures de test d'une source
14− python run.py geocode [n] # géocode les annonces sans coordonnées (max n requêtes)
15− python run.py poi [n] # commodités de proximité par immeuble (max n requêtes)
16− python run.py env [n] # environnement OSM par tuile (socle des KA Scores)
17− python run.py kascores [all] # calcule les KA Scores du parc (all = tout recalculer)
18− python run.py buildings # passeport des immeubles (building_key + stats)
19− python run.py managers [budget_s] # gestionnaires : fiche Google Maps + avis (SerpApi)
20−"""
21 from __future__ import annotations 4 from __future__ import annotations
22 5
23 import os 6 import os
24 import sys 7 import sys
25 from pathlib import Path 8 from pathlib import Path
26 9
27−# Charger .env (FIRECRAWL_API_KEY, etc.) sans dépendance externe
28 _env = Path(__file__).parent / ".env" 10 _env = Path(__file__).parent / ".env"
29 if _env.exists(): 11 if _env.exists():
30 for line in _env.read_text().splitlines(): 12 for line in _env.read_text().splitlines():
@@ -34,8 +16,48 @@ if _env.exists():
34 os.environ.setdefault(k.strip(), v.strip()) 16 os.environ.setdefault(k.strip(), v.strip())
35 17
36 18
19+def _disk_guard() -> None:
20+ # imgcache (miniatures /api/img, régénérables) : aucune éviction → peut
21+ # remplir le disque (ENOSPC → 500). Purge si critique, sinon TTL 30 j.
22+ import shutil
23+ import threading
24+ import time
25+
26+ root = Path(__file__).parent
27+ cache = root / "data" / "imgcache"
28+
29+ def _run() -> None:
30+ try:
31+ free = shutil.disk_usage(root).free / 1e9
32+ print(f"[lou-ka] disk_guard: {free:.2f} Go libres", flush=True)
33+ if not cache.is_dir():
34+ return
35+ crit = free < 5
36+ cutoff = time.time() - 30 * 86400
37+ n = 0
38+ with os.scandir(cache) as it:
39+ for e in it:
40+ try:
41+ if crit or e.stat().st_mtime < cutoff:
42+ os.unlink(e.path)
43+ n += 1
44+ except OSError:
45+ pass
46+ if n:
47+ free = shutil.disk_usage(root).free / 1e9
48+ print(f"[lou-ka] disk_guard: {n} fichiers purgés "
49+ f"({'critique' if crit else 'TTL'}), "
50+ f"{free:.2f} Go libres", flush=True)
51+ except Exception as exc:
52+ print(f"[lou-ka] disk_guard: erreur ignorée : {exc}", flush=True)
53+
54+ threading.Thread(target=_run, name="disk-guard", daemon=True).start()
55+
56+
37 def main() -> None: 57 def main() -> None:
38 cmd = sys.argv[1] if len(sys.argv) > 1 else "serve" 58 cmd = sys.argv[1] if len(sys.argv) > 1 else "serve"
59+ if cmd in ("serve", "watch", "watch-ct", "sync", "sync-ct"):
60+ _disk_guard()
39 if cmd == "sync": 61 if cmd == "sync":
40 from louka import ingest 62 from louka import ingest
41 ingest.run(sys.argv[2:] or None) 63 ingest.run(sys.argv[2:] or None)
@@ -55,24 +77,19 @@ def main() -> None:
55 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None 77 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
56 geocode.run(limit) 78 geocode.run(limit)
57 elif cmd == "geocode": 79 elif cmd == "geocode":
58− # lot Adresses Québec (rapide) ; « geocode1 » = ancien mode 1-par-1
59 from louka import geocode 80 from louka import geocode
60 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None 81 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
61 geocode.run_batch(limit) 82 geocode.run_batch(limit)
62 elif cmd == "quality": 83 elif cmd == "quality":
63− # (re)calcule le score de complétude + publication/quarantaine
64 from louka import quality 84 from louka import quality
65 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None 85 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
66 quality.backfill(limit) 86 quality.backfill(limit)
67 elif cmd == "imgaudit": 87 elif cmd == "imgaudit":
68− # contrôle qualité des images (liens morts, minuscules, placeholders,
69− # doublons) — incrémental : python run.py imgaudit [n_annonces] [source]
70 from louka import imgcheck 88 from louka import imgcheck
71 limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500 89 limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500
72 src = sys.argv[3] if len(sys.argv) > 3 else None 90 src = sys.argv[3] if len(sys.argv) > 3 else None
73 imgcheck.run(limit, source=src) 91 imgcheck.run(limit, source=src)
74 elif cmd == "fairvalue": 92 elif cmd == "fairvalue":
75− # (re)calcule la juste valeur locative de toutes les annonces publiées
76 from louka import fairvalue 93 from louka import fairvalue
77 fairvalue.compute_all() 94 fairvalue.compute_all()
78 elif cmd == "poi": 95 elif cmd == "poi":
@@ -84,14 +101,10 @@ def main() -> None:
84 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None 101 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
85 quartier.enrich(limit) 102 quartier.enrich(limit)
86 elif cmd == "env": 103 elif cmd == "env":
87− # données d'environnement OSM par tuile (routes, rails, cyclable,
88− # bars, POI complets) — cache ~3 mois, socle des KA Scores
89 from louka import environment 104 from louka import environment
90 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None 105 limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
91 print(environment.run(limit)) 106 print(environment.run(limit))
92 elif cmd == "kascores": 107 elif cmd == "kascores":
93− # (re)calcule les KA Scores (Walk/Transit/Bike/Calme/Services + global)
94− # incrémental ; « python run.py kascores all » force tout le parc
95 from louka import kascores 108 from louka import kascores
96 force = len(sys.argv) > 2 and sys.argv[2] == "all" 109 force = len(sys.argv) > 2 and sys.argv[2] == "all"
97 print(kascores.run(recompute_all=force)) 110 print(kascores.run(recompute_all=force))
@@ -113,18 +126,14 @@ def main() -> None:
113 from louka import rdl 126 from louka import rdl
114 rdl.refresh() 127 rdl.refresh()
115 elif cmd == "tal": 128 elif cmd == "tal":
116− # historique TAL (décisions SOQUIJ) par adresse, par lots
117 from louka import tal 129 from louka import tal
118 n = int(sys.argv[2]) if len(sys.argv) > 2 else 60 130 n = int(sys.argv[2]) if len(sys.argv) > 2 else 60
119 b = int(sys.argv[3]) if len(sys.argv) > 3 else 600 131 b = int(sys.argv[3]) if len(sys.argv) > 3 else 600
120 tal.precompute(limit=n, budget=b) 132 tal.precompute(limit=n, budget=b)
121 elif cmd == "buildings": 133 elif cmd == "buildings":
122− # (re)calcule le passeport de tous les immeubles (building_key + stats)
123 from louka import building 134 from louka import building
124 print(building.rollup()) 135 print(building.rollup())
125 elif cmd == "managers": 136 elif cmd == "managers":
126− # gestionnaires : amorçage + résolution Google Maps + avis (SerpApi)
127− # « python run.py managers [budget_s] »
128 from louka import managers 137 from louka import managers
129 b = float(sys.argv[2]) if len(sys.argv) > 2 else 300 138 b = float(sys.argv[2]) if len(sys.argv) > 2 else 300
130 print(managers.precompute(budget_s=b)) 139 print(managers.precompute(budget_s=b))
@@ -148,3 +157,28 @@ def main() -> None:
148 157
149 if __name__ == "__main__": 158 if __name__ == "__main__":
150 main() 159 main()
160+# ---------------------------------------------------------------------------
161+# [ka2 2026-08-31] padding intentionnel : sur disque APFS saturé (ENOSPC),
162+# les réécritures ne tronquaient pas le fichier — une queue corrompue issue
163+# d'une écriture précédente (pannes du 2026-08-31) subsistait après l'ancien
164+# EOF et cassait le parse (SyntaxError). Ces lignes de commentaire allongent
165+# le fichier au-delà de l'ancienne taille pour garantir l'écrasement complet.
166+# Elles sont inertes et peuvent être supprimées lors d'un prochain commit
167+# une fois le disque assaini.
168+# ---------------------------------------------------------------------------
169+###############################################################################
170+###############################################################################
171+###############################################################################
172+###############################################################################
173+###############################################################################
174+###############################################################################
175+###############################################################################
176+###############################################################################
177+###############################################################################
178+###############################################################################
179+###############################################################################
180+###############################################################################
181+###############################################################################
182+###############################################################################
183+###############################################################################
184+# fin du padding ka2
151 185
Menu

Theme