#!/usr/bin/env python3 # Lou-Ka — run.py : point d'entrée — sync/watch/serve (+ outils) """python run.py {sync|watch|sync-ct|watch-ct|serve|record|geocode|quality|imgaudit|fairvalue|poi|quartier|env|kascores|buildings|managers|tal|rdl|...}""" from __future__ import annotations import os import sys from pathlib import Path _env = Path(__file__).parent / ".env" if _env.exists(): for line in _env.read_text().splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: k, _, v = line.partition("=") os.environ.setdefault(k.strip(), v.strip()) def _disk_guard() -> None: # imgcache (miniatures /api/img, régénérables) : aucune éviction → peut # remplir le disque (ENOSPC → 500). Purge si critique, sinon TTL 30 j. import shutil import threading import time root = Path(__file__).parent cache = root / "data" / "imgcache" def _run() -> None: try: free = shutil.disk_usage(root).free / 1e9 print(f"[lou-ka] disk_guard: {free:.2f} Go libres", flush=True) if not cache.is_dir(): return crit = free < 5 cutoff = time.time() - 30 * 86400 n = 0 with os.scandir(cache) as it: for e in it: try: if crit or e.stat().st_mtime < cutoff: os.unlink(e.path) n += 1 except OSError: pass if n: free = shutil.disk_usage(root).free / 1e9 print(f"[lou-ka] disk_guard: {n} fichiers purgés " f"({'critique' if crit else 'TTL'}), " f"{free:.2f} Go libres", flush=True) except Exception as exc: print(f"[lou-ka] disk_guard: erreur ignorée : {exc}", flush=True) threading.Thread(target=_run, name="disk-guard", daemon=True).start() def main() -> None: cmd = sys.argv[1] if len(sys.argv) > 1 else "serve" if cmd in ("serve", "watch", "watch-ct", "sync", "sync-ct"): _disk_guard() if cmd == "sync": from louka import ingest ingest.run(sys.argv[2:] or None) elif cmd == "watch": from louka import ingest minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 60 ingest.watch(minutes * 60) elif cmd == "sync-ct": from louka.shortterm import ingest as ct_ingest ct_ingest.run(sys.argv[2:] or None) elif cmd == "watch-ct": from louka.shortterm import ingest as ct_ingest minutes = int(sys.argv[2]) if len(sys.argv) > 2 else 360 ct_ingest.watch(minutes * 60) elif cmd == "geocode1": from louka import geocode limit = int(sys.argv[2]) if len(sys.argv) > 2 else None geocode.run(limit) elif cmd == "geocode": from louka import geocode limit = int(sys.argv[2]) if len(sys.argv) > 2 else None geocode.run_batch(limit) elif cmd == "quality": from louka import quality limit = int(sys.argv[2]) if len(sys.argv) > 2 else None quality.backfill(limit) elif cmd == "imgaudit": from louka import imgcheck limit = int(sys.argv[2]) if len(sys.argv) > 2 else 1500 src = sys.argv[3] if len(sys.argv) > 3 else None imgcheck.run(limit, source=src) elif cmd == "fairvalue": from louka import fairvalue fairvalue.compute_all() elif cmd == "poi": from louka import poi limit = int(sys.argv[2]) if len(sys.argv) > 2 else None poi.run(limit) elif cmd == "quartier": from louka import quartier limit = int(sys.argv[2]) if len(sys.argv) > 2 else None quartier.enrich(limit) elif cmd == "env": from louka import environment limit = int(sys.argv[2]) if len(sys.argv) > 2 else None print(environment.run(limit)) elif cmd == "kascores": from louka import kascores force = len(sys.argv) > 2 and sys.argv[2] == "all" print(kascores.run(recompute_all=force)) elif cmd == "inondation-build": from louka import inondation inondation.build() elif cmd == "air-refresh": from louka import air air.refresh() elif cmd == "gaz-refresh": from louka import gaz print(f"[gaz] {gaz.refresh()} stations") elif cmd == "hydro-precompute": from louka import hydro n = int(sys.argv[2]) if len(sys.argv) > 2 else 100 b = int(sys.argv[3]) if len(sys.argv) > 3 else 40 hydro.precompute(limit=n, budget=b) elif cmd == "rdl": from louka import rdl rdl.refresh() elif cmd == "tal": from louka import tal n = int(sys.argv[2]) if len(sys.argv) > 2 else 60 b = int(sys.argv[3]) if len(sys.argv) > 3 else 600 tal.precompute(limit=n, budget=b) elif cmd == "buildings": from louka import building print(building.rollup()) elif cmd == "managers": from louka import managers b = float(sys.argv[2]) if len(sys.argv) > 2 else 300 print(managers.precompute(budget_s=b)) elif cmd == "record": from louka import fixtures from louka.connectors import CONNECTORS targets = sys.argv[2:] or sorted(CONNECTORS) for sid in targets: try: print(f"[lou-ka] record {sid} ... {fixtures.record(sid)}") except Exception as exc: print(f"[lou-ka] record {sid} ÉCHEC : {exc}", file=sys.stderr) elif cmd == "serve": import uvicorn port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080 uvicorn.run("louka.web:app", host="0.0.0.0", port=port) else: print(__doc__) sys.exit(1) if __name__ == "__main__": main() # --------------------------------------------------------------------------- # [ka2 2026-08-31] padding intentionnel : sur disque APFS saturé (ENOSPC), # les réécritures ne tronquaient pas le fichier — une queue corrompue issue # d'une écriture précédente (pannes du 2026-08-31) subsistait après l'ancien # EOF et cassait le parse (SyntaxError). Ces lignes de commentaire allongent # le fichier au-delà de l'ancienne taille pour garantir l'écrasement complet. # Elles sont inertes et peuvent être supprimées lors d'un prochain commit # une fois le disque assaini. # --------------------------------------------------------------------------- ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### ############################################################################### # fin du padding ka2