"""Lanceur application de bureau (fenêtre native via pywebview). Démarre le serveur FastAPI en arrière-plan sur un port libre de la boucle locale, attend qu'il réponde, puis ouvre une fenêtre native pointant dessus. C'est le point d'entrée de l'exécutable empaqueté (PyInstaller). ``pywebview`` est importé paresseusement : le reste du paquet reste utilisable sans cette dépendance (serveur headless, Docker, CI). """ import os import socket import threading import httpx import uvicorn from .api import app WINDOW_TITLE = "Plesna Gérance" HOST = "127.0.0.1" def _find_free_port() -> int: """Réserve un port TCP libre sur la boucle locale.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind((HOST, 0)) return s.getsockname()[1] class _ThreadedServer(uvicorn.Server): """Serveur uvicorn lançable dans un thread (sans handlers de signaux).""" def install_signal_handlers(self) -> None: # noqa: D102 - voir docstring classe pass def _wait_until_ready(base_url: str, timeout: float = 30.0) -> bool: """Attend que ``/api/health`` réponde, jusqu'à ``timeout`` secondes.""" import time deadline = time.monotonic() + timeout while time.monotonic() < deadline: try: resp = httpx.get(f"{base_url}/api/health", timeout=1.0) if resp.status_code == 200: return True except httpx.HTTPError: pass time.sleep(0.2) return False def run() -> None: """Lance le serveur puis la fenêtre native. Bloque jusqu'à fermeture.""" port = _find_free_port() base_url = f"http://{HOST}:{port}" config = uvicorn.Config(app, host=HOST, port=port, log_level="warning") server = _ThreadedServer(config) thread = threading.Thread(target=server.run, daemon=True) thread.start() if not _wait_until_ready(base_url): server.should_exit = True raise RuntimeError( "Le serveur n'a pas démarré à temps. " "Consultez les journaux pour plus de détails." ) # Import paresseux : pywebview n'est requis que pour le mode bureau. import webview # PLESNA_DEBUG=1 active l'inspecteur web (DevTools) dans la fenetre native : # clic droit -> « Inspecter » (ou « Inspect element ») pour ouvrir la console. debug = os.environ.get("PLESNA_DEBUG", "").lower() in ("1", "true", "yes") webview.create_window(WINDOW_TITLE, base_url, width=1280, height=860) try: webview.start(debug=debug) finally: # Fermeture de la fenêtre -> arrêt propre du serveur. server.should_exit = True if __name__ == "__main__": run()