File size: 6,879 Bytes
22c0953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#!/usr/bin/env python3
"""
ClearDDI — lanceur de démo
Installe les dépendances si besoin, démarre le serveur, affiche un QR code
pour que n'importe qui (sur le même réseau, ou partout avec --public) puisse
ouvrir l'app sur son téléphone, et ouvre le navigateur local.

Usage :
  python3 run.py              → accès réseau local (Wi-Fi de la salle)
  python3 run.py --public     → accès public via un tunnel (n'importe où)
  python3 run.py --port 9000  → changer le port
  python3 run.py --no-browser → ne pas ouvrir le navigateur automatiquement
"""

import argparse
import socket
import subprocess
import sys
import time
import urllib.request
import webbrowser
from pathlib import Path

ROOT = Path(__file__).resolve().parent
BACKEND_DIR = ROOT / "backend"
QR_PATH = ROOT / "qr_acces.png"

REQUIRED_PACKAGES = [
    "fastapi", "uvicorn", "rdkit", "shap", "xgboost",
    "scikit-learn", "joblib", "pandas", "numpy", "pyarrow", "pydantic",
    "qrcode", "pillow",
]


def ensure_dependencies(need_ngrok: bool):
    print("Vérification des dépendances Python...")
    packages = list(REQUIRED_PACKAGES) + (["pyngrok"] if need_ngrok else [])
    result = subprocess.run(
        [sys.executable, "-m", "pip", "install", "--quiet", *packages],
        capture_output=True, text=True, check=False,
    )
    if result.returncode != 0:
        # Systèmes Debian/Ubuntu récents (PEP 668) refusent pip install hors venv.
        # On retente avec --break-system-packages plutôt que de planter au démarrage,
        # sans alarmer l'utilisateur avec le message pip brut si ça se résout seul.
        retry = subprocess.run(
            [sys.executable, "-m", "pip", "install", "--quiet", "--break-system-packages", *packages],
            capture_output=True, text=True, check=False,
        )
        if retry.returncode != 0:
            print(
                "⚠️  Installation automatique des dépendances impossible.\n"
                "   Essayez manuellement :\n"
                f"     {sys.executable} -m pip install --break-system-packages {' '.join(packages)}\n"
                "   ou créez un environnement virtuel (python3 -m venv .venv) avant de relancer."
            )


def get_lan_ip():
    """Adresse IP de cette machine sur le réseau local (celle que les autres
    appareils du même Wi-Fi doivent utiliser pour nous joindre). N'envoie
    aucun paquet : ouvre juste une socket UDP pour lire l'IP de sortie."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            s.connect(("8.8.8.8", 80))
            return s.getsockname()[0]
        finally:
            s.close()
    except OSError:
        return None


def wait_for_server(port, timeout=25.0):
    """Attend que /api/health réponde, au lieu d'un sleep() arbitraire."""
    deadline = time.time() + timeout
    url = f"http://127.0.0.1:{port}/api/health"
    while time.time() < deadline:
        try:
            with urllib.request.urlopen(url, timeout=1.5) as resp:
                if resp.status == 200:
                    return True
        except Exception:
            pass
        time.sleep(0.5)
    return False


def start_public_tunnel(port):
    """Ouvre un tunnel public (ngrok) vers le serveur local. Nécessite un
    compte ngrok gratuit + un authtoken configuré (voir README) — sans quoi
    ngrok refuse désormais les tunnels anonymes."""
    try:
        from pyngrok import ngrok
    except ImportError:
        print("⚠️  pyngrok n'est pas installé — accès public désactivé.")
        return None

    try:
        tunnel = ngrok.connect(port, "http")
        return tunnel.public_url
    except Exception as e:
        print(
            "⚠️  Impossible d'ouvrir un tunnel public via ngrok.\n"
            "   Cause probable : pas d'authtoken configuré.\n"
            "   → Créez un compte gratuit sur https://dashboard.ngrok.com/signup\n"
            "   → Récupérez votre token puis lancez une fois :\n"
            "       python3 -m pyngrok config add-authtoken VOTRE_TOKEN\n"
            f"   Détail technique : {e}\n"
            "   → On continue avec l'accès réseau local à la place."
        )
        return None


def show_qr(url):
    try:
        import qrcode
    except ImportError:
        print("⚠️  Le paquet 'qrcode' n'est pas installé : QR code non généré.")
        return

    qr = qrcode.QRCode(border=1)
    qr.add_data(url)
    qr.make()

    print("\nScannez ce QR code pour ouvrir ClearDDI :\n")
    qr.print_ascii(invert=True)

    try:
        img = qr.make_image(fill_color="black", back_color="white")
        img.save(QR_PATH)
        print(f"\n(QR code aussi enregistré dans : {QR_PATH})")
    except Exception:
        pass  # l'ASCII ci-dessus suffit si l'image ne peut pas être sauvegardée


def main():
    parser = argparse.ArgumentParser(description="Lanceur de démo ClearDDI")
    parser.add_argument("--port", type=int, default=8765, help="Port du serveur (défaut : 8765)")
    parser.add_argument("--public", action="store_true",
                         help="Rendre l'app accessible publiquement (tunnel ngrok), pas seulement sur le réseau local")
    parser.add_argument("--no-browser", action="store_true",
                         help="Ne pas ouvrir automatiquement le navigateur local")
    args = parser.parse_args()
    port = args.port

    ensure_dependencies(need_ngrok=args.public)

    print(f"Démarrage du serveur ClearDDI sur le port {port} ...")
    proc = subprocess.Popen(
        [sys.executable, "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", str(port)],
        cwd=str(BACKEND_DIR),
    )

    if not wait_for_server(port):
        print("⚠️  Le serveur ne répond pas encore après 25s — vérifiez les erreurs ci-dessus.")

    local_url = f"http://localhost:{port}"
    share_url = local_url
    access_note = "réseau local uniquement"

    if args.public:
        public_url = start_public_tunnel(port)
        if public_url:
            share_url = public_url
            access_note = "accessible depuis n'importe où sur Internet"

    if share_url == local_url:
        lan_ip = get_lan_ip()
        if lan_ip:
            share_url = f"http://{lan_ip}:{port}"
            access_note = "accessible à toute personne sur le même réseau Wi-Fi"
        else:
            access_note = "IP locale non détectée — partagez plutôt l'URL locale manuellement"

    print(f"\n✅ Serveur prêt : {local_url}  (sur cette machine)")
    print(f"📱 À partager ({access_note}) : {share_url}")
    show_qr(share_url)

    if not args.no_browser:
        webbrowser.open(local_url)

    print("\nServeur lancé. Appuyez sur Ctrl+C pour arrêter.")
    try:
        proc.wait()
    except KeyboardInterrupt:
        proc.terminate()


if __name__ == "__main__":
    main()