# ============================================ # Projet : API-KA # Fichier : src/config.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-16 # ============================================ """Configuration centrale d'API-KA : chargement du .env et vérification du node m3u96b.""" from __future__ import annotations import os import socket from dataclasses import dataclass from functools import lru_cache from pathlib import Path from dotenv import load_dotenv from src.ka_registry import resolve_source_url # Nœud imposé (optionnel) : APIKA_REQUIRED_NODE=m3u96b ; vide = tourne sur n'importe quel nœud (placement par maclustr-dispatch). REQUIRED_NODE = os.getenv("APIKA_REQUIRED_NODE", "").strip().lower() SERVICES: tuple[str, ...] = ( "louka", "immoka", "foodka", "autoka", "fabrika", "restoka", "sortika", "creaka", "jobka", "houseka", "rentka", ) SERVICE_TABLES: dict[str, str] = {service: f"{service}_data" for service in SERVICES} SERVICE_SOURCE_ENV: dict[str, str] = { "louka": "LOUKA_SOURCE_URL", "immoka": "IMMOKA_SOURCE_URL", "foodka": "FOODKA_SOURCE_URL", "autoka": "AUTOKA_SOURCE_URL", "fabrika": "FABRIKA_SOURCE_URL", "restoka": "RESTOKA_SOURCE_URL", "sortika": "SORTIKA_SOURCE_URL", "creaka": "CREAKA_SOURCE_URL", "jobka": "JOBKA_SOURCE_URL", "houseka": "HOUSEKA_SOURCE_URL", "rentka": "RENTKA_SOURCE_URL", } BASE_DIR = Path(__file__).resolve().parents[1] load_dotenv(BASE_DIR / ".env") def verify_node() -> None: """Refuse de démarrer en production si le hostname n'est pas m3u96b. Le suffixe éventuel (ex. ``.local``) est ignoré et la comparaison est insensible à la casse, mais la règle reste stricte : en production, API-KA ne tourne que sur le node m3u96b. """ hostname = socket.gethostname() if os.getenv("APP_ENV") == "production" and REQUIRED_NODE: short = hostname.split(".")[0].lower() if short != REQUIRED_NODE: raise RuntimeError( f"API-KA doit tourner sur {REQUIRED_NODE}, node actuel : {hostname}" ) @dataclass(frozen=True) class Settings: """Configuration immuable de la plateforme, chargée depuis le .env.""" app_env: str node_name: str database_url: str ngrok_authtoken: str ngrok_domain: str api_port: int daily_run_hour: int backup_retention_days: int rate_limit_per_minute: int trouveka_search_url: str trouveka_suggest_url: str source_urls: dict[str, str] base_dir: Path logs_dir: Path backups_dir: Path @lru_cache(maxsize=1) def get_settings() -> Settings: """Charge et met en cache la configuration depuis les variables d'environnement.""" logs_dir = BASE_DIR / "logs" backups_dir = BASE_DIR / "data" / "backups" logs_dir.mkdir(parents=True, exist_ok=True) backups_dir.mkdir(parents=True, exist_ok=True) default_db = f"sqlite:///{BASE_DIR / 'data' / 'apika_dev.db'}" return Settings( app_env=os.getenv("APP_ENV", "development"), node_name=os.getenv("NODE_NAME", REQUIRED_NODE), database_url=os.getenv("DATABASE_URL", default_db), ngrok_authtoken=os.getenv("NGROK_AUTHTOKEN", ""), ngrok_domain=os.getenv("NGROK_DOMAIN", "www.api-ka.com"), api_port=int(os.getenv("API_PORT", "8000")), daily_run_hour=int(os.getenv("DAILY_RUN_HOUR", "02")), backup_retention_days=max(90, int(os.getenv("BACKUP_RETENTION_DAYS", "90"))), rate_limit_per_minute=int(os.getenv("RATE_LIMIT_PER_MINUTE", "120")), trouveka_search_url=os.getenv( "TROUVEKA_SEARCH_URL", "https://www.trouve-ka.com/api/search" ), trouveka_suggest_url=os.getenv( "TROUVEKA_SUGGEST_URL", "https://www.trouve-ka.com/api/suggest" ), source_urls={ service: resolve_source_url(service, os.getenv(env_var, "")) for service, env_var in SERVICE_SOURCE_ENV.items() }, base_dir=BASE_DIR, logs_dir=logs_dir, backups_dir=backups_dir, ) def source_url(service: str) -> str: """URL de collecte LIVE d'un service : registre maclustr-dispatch (suit les ``mld move``), repli sur ``_SOURCE_URL`` du .env.""" return resolve_source_url(service, os.getenv(SERVICE_SOURCE_ENV.get(service, ""), ""))