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#!/usr/bin/env python32# -----------------------------------------------------------------------------3# Ora-Ka — Plateforme unifiée des agrégateurs Ka (Immo·Ka, Lou·Ka, Fabri·Ka,4# Auto·Ka, Food·Ka) — un seul serveur, cinq univers.5# Auteur : Simon-Pierre Boucher — contact@spboucher.ai6# run.py : point d'entrée — `serve`, `sync`, `watch`7# -----------------------------------------------------------------------------8"""Utilisation :9 python run.py serve [port] # démarre la plateforme unifiée (défaut 8200)10 python run.py sync [app ...] # synchronise les 5 apps (ou celles nommées :11 # immo, lou, fabri, auto, food)12 python run.py watch # boucle de synchronisation permanente13 # (cadence native de chaque app)14"""15from __future__ import annotations1617import os18import sys19import threading20import time21from pathlib import Path2223ROOT = Path(__file__).resolve().parent2425# Charger .env (FIRECRAWL_API_KEY, SCRAPFLY_KEY, SCRAPFLY_API_KEY) sans dépendance26_env = ROOT / ".env"27if _env.exists():28 for line in _env.read_text().splitlines():29 line = line.strip()30 if line and not line.startswith("#") and "=" in line:31 k, _, v = line.partition("=")32 os.environ.setdefault(k.strip(), v.strip())3334# Rendre les 5 packages importables (chaque app garde sa racine intacte :35# apps/<x>/<package>/ avec data/ et frontend/dist/ frères — leurs chemins36# internes calculés via __file__ restent donc valides).37APPS = ["immo", "lou", "fabri", "auto", "food"]38for _d in APPS:39 sys.path.insert(0, str(ROOT / "apps" / _d))4041# Cadence native de chaque app (minutes) — reprise des déploiements individuels42WATCH_MINUTES = {"immo": 180, "lou": 60, "fabri": 360, "auto": 120, "food": 360}434445def _ingest_module(app: str):46 if app == "immo":47 from immoka import ingest48 elif app == "lou":49 from louka import ingest50 elif app == "fabri":51 from fabrika import ingest52 elif app == "auto":53 from autoka import ingest54 elif app == "food":55 from foodka import ingest56 else:57 raise ValueError(f"app inconnue : {app}")58 return ingest596061def _watch_one(app: str) -> None:62 """Boucle de sync d'une app, avec sa cadence native (reproduit son watch)."""63 interval = WATCH_MINUTES[app] * 6064 while True:65 started = time.time()66 try:67 print(f"[ora-ka] sync {app} ...", flush=True)68 if app == "lou":69 # Lou-Ka enchaîne sync + geocode + poi + quartier dans son watch70 from louka import ingest, geocode, poi, quartier71 ingest.run(None)72 geocode.run(120)73 poi.run(80)74 quartier.enrich()75 else:76 _ingest_module(app).run(None)77 print(f"[ora-ka] sync {app} terminé en {time.time() - started:.0f}s", flush=True)78 except Exception as exc: # une app en échec ne tue jamais la boucle79 print(f"[ora-ka] sync {app} ÉCHEC : {exc}", file=sys.stderr, flush=True)80 time.sleep(max(60.0, interval - (time.time() - started)))818283def main() -> None:84 cmd = sys.argv[1] if len(sys.argv) > 1 else "serve"85 if cmd == "serve":86 import uvicorn87 port = int(sys.argv[2]) if len(sys.argv) > 2 else 820088 uvicorn.run("oraka.web:app", host="0.0.0.0", port=port)89 elif cmd == "sync":90 targets = [a for a in sys.argv[2:] if a in APPS] or APPS91 for app in targets:92 try:93 print(f"[ora-ka] sync {app} ...", flush=True)94 _ingest_module(app).run(None)95 except Exception as exc:96 print(f"[ora-ka] sync {app} ÉCHEC : {exc}", file=sys.stderr, flush=True)97 elif cmd == "index":98 from oraka import semantic99 targets = [a for a in sys.argv[2:] if a in APPS] or None100 semantic.build_index(targets)101 elif cmd == "watch":102 threads = [103 threading.Thread(target=_watch_one, args=(app,), daemon=True, name=f"watch-{app}")104 for app in APPS105 ]106 for t in threads:107 t.start()108 time.sleep(5) # décale les démarrages pour lisser la charge109 while True:110 time.sleep(3600)111 else:112 print(__doc__)113 sys.exit(1)114115116if __name__ == "__main__":117 main()118