Permet de distribuer l'application en executable Windows double-clic, sans Python/Node/terminal pour l'utilisateur final. - `desktop.py` : demarre le serveur sur un port libre dans un thread et ouvre une fenetre native (pywebview). `PLESNA_DEBUG=1` active les DevTools. Commande `plesna-gerance desktop` (import paresseux). - `paths.py` : resolution centralisee des chemins selon le contexte (dev, executable PyInstaller, surcharge par variables d'env). Les donnees vont dans un dossier utilisateur inscriptible (%APPDATA%), les ressources dans le bundle. connection.py/storage.py utilisent `get_data_dir()`, app.py `resource_path()`. - app.py : type MIME `.mjs` force (modules ES / worker PDF.js sous Windows). - Groupes de deps optionnels desktop / desktop-linux / build. - packaging/ : spec PyInstaller, build_windows.ps1, installeur Inno Setup. Workflow GitHub build-windows.yml (runner Windows). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
2.6 KiB
Python
88 lines
2.6 KiB
Python
"""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()
|