SPB Git

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%
5.9 KB · 157 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Ora-Ka — Plateforme unifiée des agrégateurs Ka3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# web.py : app maîtresse — landing + montage des 5 apps sous leurs préfixes5# -----------------------------------------------------------------------------6from __future__ import annotations78import sqlite39import sys10import time11from pathlib import Path1213from fastapi import FastAPI, Request14from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse15from fastapi.middleware.gzip import GZipMiddleware1617ROOT = Path(__file__).resolve().parent.parent18APPS_DIR = ROOT / "apps"1920# Chaque app garde son arborescence d'origine (<pkg>/, data/, frontend/dist/)21# sous apps/<alias>/ — on rend les 5 packages importables tels quels.22for _d in ("immo", "lou", "fabri", "auto", "food"):23    p = str(APPS_DIR / _d)24    if p not in sys.path:25        sys.path.insert(0, p)2627from immoka.web import app as immo_app  # noqa: E40228from louka.web import app as lou_app  # noqa: E40229from fabrika.web import app as fabri_app  # noqa: E40230from autoka.web import app as auto_app  # noqa: E40231from foodka.web import app as food_app  # noqa: E4023233from oraka.auth import current_user, router as auth_router  # noqa: E40234from oraka.hubfav import hub_list_all  # noqa: E40235from oraka.landing import LANDING  # noqa: E40236from oraka.legal import PRIVACY, TERMS  # noqa: E40237from oraka.search import search_hits  # noqa: E4023839app = FastAPI(title="Ora-Ka", version="1.0", docs_url=None, redoc_url=None)40app.add_middleware(GZipMiddleware, minimum_size=1024)4142# Connexion « KA ID » (SSO du Groupe KA) — inclus AVANT les montages /immo etc.43# pour que /api/auth/* soit toujours servi par l'app racine.44app.include_router(auth_router)4546# ---------------------------------------------------------------------------47# Statistiques agrégées (lecture directe des 5 bases, cache mémoire 5 min)48# ---------------------------------------------------------------------------49_DBS = {50    "immo": APPS_DIR / "immo" / "data" / "immoka.db",51    "lou": APPS_DIR / "lou" / "data" / "louka.db",52    "fabri": APPS_DIR / "fabri" / "data" / "fabrika.db",53    "auto": APPS_DIR / "auto" / "data" / "autoka.db",54    "food": APPS_DIR / "food" / "data" / "foodka.db",55}5657_COUNT_SQL = {58    "immo": "SELECT COUNT(*) FROM listings WHERE active=1 AND dup_hidden=0 AND price IS NOT NULL",59    "lou": "SELECT COUNT(*) FROM listings WHERE active=1",60    "fabri": "SELECT COUNT(*) FROM products WHERE active=1",61    "auto": "SELECT COUNT(*) FROM vehicles WHERE active=1",62    "food": "SELECT COUNT(*) FROM products WHERE active=1",63}6465_overview_cache: dict = {"ts": 0.0, "data": None}666768def _count(db_path: Path, sql: str) -> int:69    if not db_path.exists():70        return 071    try:72        con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=5)73        try:74            return int(con.execute(sql).fetchone()[0])75        finally:76            con.close()77    except Exception:78        return 0798081@app.get("/api/overview")82def overview() -> JSONResponse:83    now = time.time()84    if _overview_cache["data"] is not None and now - _overview_cache["ts"] < 300:85        return JSONResponse(_overview_cache["data"])86    counts = {k: _count(_DBS[k], _COUNT_SQL[k]) for k in _DBS}87    data = {88        "apps": {89            "immo": {"label": "propriétés à vendre", "n": counts["immo"]},90            "lou": {"label": "logements à louer", "n": counts["lou"]},91            "fabri": {"label": "produits québécois", "n": counts["fabri"]},92            "auto": {"label": "véhicules d'occasion", "n": counts["auto"]},93            "food": {"label": "produits d'épicerie", "n": counts["food"]},94        },95        "total": sum(counts.values()),96        "generated_at": now,97    }98    _overview_cache.update(ts=now, data=data)99    return JSONResponse(data)100101102@app.get("/api/search")103def api_search(q: str = "", scope: str | None = None, limit: int = 60) -> JSONResponse:104    """Moteur hybride : exact + sémantique dans les 5 univers en une requête."""105    limit = max(1, min(int(limit), 120))106    return JSONResponse(search_hits(q, scope, limit))107108109@app.get("/api/favorites")110def api_favorites(request: Request) -> JSONResponse:111    """« Mon univers Ka » : les favoris du membre connecté, TOUTES plateformes112    du groupe confondues, lus depuis le magasin central du hub Groupe KA."""113    user = current_user(request)114    if not user:115        return JSONResponse({"error": "non connecté"}, status_code=401)116    return JSONResponse({"favorites": hub_list_all(user.get("ka_id"))})117118119@app.get("/healthz")120def healthz() -> dict:121    return {"status": "ok", "apps": {k: _DBS[k].exists() for k in _DBS}}122123124# Redirections sans barre oblique finale vers la racine de chaque app125for _alias in ("immo", "lou", "fabri", "auto", "food"):126    def _mk(alias: str):127        def _redir() -> RedirectResponse:128            return RedirectResponse(url=f"/{alias}/", status_code=308)129        return _redir130    app.get(f"/{_alias}", include_in_schema=False)(_mk(_alias))131132133@app.get("/", response_class=HTMLResponse, include_in_schema=False)134def landing() -> HTMLResponse:135    return HTMLResponse(LANDING)136137138@app.get("/conditions", response_class=HTMLResponse, include_in_schema=False)139def terms() -> HTMLResponse:140    return HTMLResponse(TERMS)141142143@app.get("/confidentialite", response_class=HTMLResponse, include_in_schema=False)144def privacy() -> HTMLResponse:145    return HTMLResponse(PRIVACY)146147148# ---------------------------------------------------------------------------149# Montage des 5 apps — chacune reproduite fidèlement sous son préfixe.150# Leurs catch-all SPA restent confinés à leur préfixe grâce au mount.151# ---------------------------------------------------------------------------152app.mount("/immo", immo_app)153app.mount("/lou", lou_app)154app.mount("/fabri", fabri_app)155app.mount("/auto", auto_app)156app.mount("/food", food_app)157