Docfile commited on
Commit
a415b40
·
verified ·
1 Parent(s): 1f8b86f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +303 -745
app.py CHANGED
@@ -1,758 +1,316 @@
1
- from flask import Flask, render_template, request, jsonify, redirect, url_for
2
- import asyncio
3
- import aiohttp
4
- import os
5
- import json
6
  import time
7
- import random
8
- from datetime import datetime
9
- import multiprocessing
10
- from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
11
- from threading import Lock
12
- import logging
13
- import sys
14
- from functools import partial
15
- from jinja2 import Undefined # Pour gérer les valeurs Undefined
16
-
17
- # Pour gérer la sérialisation des objets Undefined lors de l'utilisation de json.dump
18
- def default_json(o):
19
- if isinstance(o, Undefined):
20
- return ''
21
- raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
22
-
23
- # Configuration du logger
24
- logging.basicConfig(
25
- level=logging.INFO,
26
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
- handlers=[
28
- logging.StreamHandler(sys.stdout),
29
- logging.FileHandler('website_requester.log')
30
- ]
31
- )
32
- logger = logging.getLogger('website_requester')
33
 
34
  app = Flask(__name__)
35
 
36
- # Définition d'un fournisseur JSON personnalisé en étendant DefaultJSONProvider
37
- from flask.json.provider import DefaultJSONProvider
38
-
39
- class CustomJSONProvider(DefaultJSONProvider):
40
- def default(self, o):
41
- if isinstance(o, Undefined):
42
- return ""
43
- return super().default(o)
44
-
45
- # Assigner le fournisseur JSON personnalisé à l'application
46
- app.json = CustomJSONProvider(app)
47
-
48
- # Variables globales pour stocker l'état, utilisation d'un gestionnaire multiprocessing
49
- manager = multiprocessing.Manager()
50
- requests_in_progress = manager.Value('b', False)
51
- progress_counter = manager.Value('i', 0)
52
- total_requests = manager.Value('i', 0)
53
- requests_being_made = manager.list()
54
- requests_lock = manager.Lock() # Pour sécuriser l'accès aux données partagées
55
- process_pool = None
56
- background_tasks = []
57
-
58
- # Paramètres avancés
59
- MAX_CONNECTIONS = 1000 # Nombre maximal de connexions simultanées
60
- SAVE_FREQUENCY = 100 # Fréquence de sauvegarde des résultats (tous les X requêtes)
61
- CHUNK_SIZE = 5000 # Nombre de requêtes à traiter par processus
62
- MAX_RETRIES = 5 # Nombre maximum de tentatives pour chaque opération
63
- RETRY_DELAY = 0.5 # Délai entre les tentatives en secondes
64
- REQUESTS_FILE = "requetes_gabaohub.json"
65
- IP_ROTATION_COUNT = 50 # Rotation des IP après ce nombre de requêtes
66
- REQUEST_TIMEOUT = 15 # Timeout des requêtes en secondes
67
-
68
- def generate_gabonese_ip():
69
- """Génère une adresse IP du Gabon (plage 41.158.0.0/16)"""
70
- return f"41.158.{random.randint(0, 255)}.{random.randint(1, 254)}"
71
-
72
- def get_random_user_agent():
73
- """Retourne un User-Agent aléatoire parmi les plus courants"""
74
- user_agents = [
75
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
76
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
77
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15",
78
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/99.0.1150.55",
79
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0",
80
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36",
81
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
82
- "Mozilla/5.0 (iPhone; CPU iPhone OS 15_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1",
83
- "Mozilla/5.0 (iPad; CPU OS 15_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1",
84
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
85
- ]
86
- return random.choice(user_agents)
87
-
88
- def get_random_referrer():
89
- """Retourne un référent aléatoire plausible"""
90
- referrers = [
91
- "https://www.google.com/",
92
- "https://www.bing.com/",
93
- "https://www.yahoo.com/",
94
- "https://duckduckgo.com/",
95
- "https://www.facebook.com/",
96
- "https://twitter.com/",
97
- "https://www.linkedin.com/",
98
- "https://www.instagram.com/",
99
- "https://www.gabon.ga/",
100
- "https://www.gov.ga/",
101
- "https://www.youtube.com/",
102
- "" # Aucun référent (accès direct)
103
- ]
104
- return random.choice(referrers)
105
-
106
- async def with_retries(func, *args, max_retries=MAX_RETRIES, **kwargs):
107
- """Exécute une fonction avec plusieurs tentatives en cas d'échec"""
108
- for attempt in range(max_retries):
109
- try:
110
- return await func(*args, **kwargs)
111
- except Exception as e:
112
- if attempt == max_retries - 1:
113
- logger.error(f"Échec définitif après {max_retries} tentatives: {str(e)}")
114
- raise
115
- delay = RETRY_DELAY * (2 ** attempt)
116
- logger.warning(f"Tentative {attempt+1} échouée: {str(e)}. Nouvel essai dans {delay:.2f}s")
117
- await asyncio.sleep(delay)
118
-
119
- async def create_session(request_counter):
120
- """Crée une session HTTP optimisée avec rotation d'IP"""
121
- connector = aiohttp.TCPConnector(
122
- limit=MAX_CONNECTIONS,
123
- ssl=False,
124
- force_close=False,
125
- use_dns_cache=True,
126
- ttl_dns_cache=300
127
- )
128
-
129
- timeout = aiohttp.ClientTimeout(
130
- total=REQUEST_TIMEOUT,
131
- connect=10,
132
- sock_connect=10,
133
- sock_read=10
134
- )
135
-
136
- session = aiohttp.ClientSession(
137
- connector=connector,
138
- timeout=timeout,
139
- headers={
140
- "User-Agent": get_random_user_agent(),
141
- "X-Forwarded-For": generate_gabonese_ip(),
142
- "Referer": get_random_referrer(),
143
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
144
- "Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
145
- "Accept-Encoding": "gzip, deflate, br",
146
- "Connection": "keep-alive",
147
- "Upgrade-Insecure-Requests": "1"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
149
- )
150
-
151
- # Middleware pour la rotation des IP, User-Agent et référents
152
- original_request = session._request
153
-
154
- async def request_middleware(method, url, **kwargs):
155
- nonlocal request_counter
156
-
157
- if request_counter.value % IP_ROTATION_COUNT == 0:
158
- kwargs.setdefault('headers', {}).update({
159
- "X-Forwarded-For": generate_gabonese_ip(),
160
- "User-Agent": get_random_user_agent(),
161
- "Referer": get_random_referrer()
162
- })
163
-
164
- request_counter.value += 1
165
- return await original_request(method, url, **kwargs)
166
-
167
- session._request = request_middleware
168
- return session
169
-
170
- async def make_homepage_request(session, request_index, request_counter):
171
- """Effectue une requête vers la page d'accueil"""
172
- global requests_being_made
173
-
174
- try:
175
- url = "https://gabaohub.alwaysdata.net"
176
- params = {}
177
-
178
- if random.random() < 0.3:
179
- utm_sources = ["facebook", "twitter", "instagram", "direct", "google", "bing"]
180
- utm_mediums = ["social", "cpc", "email", "referral", "organic"]
181
- utm_campaigns = ["spring_promo", "launch", "awareness", "brand", "product"]
182
-
183
- params["utm_source"] = random.choice(utm_sources)
184
- params["utm_medium"] = random.choice(utm_mediums)
185
- params["utm_campaign"] = random.choice(utm_campaigns)
186
-
187
- with requests_lock:
188
- if request_index < len(requests_being_made):
189
- requests_being_made[request_index]["url"] = url
190
- requests_being_made[request_index]["params"] = params
191
- requests_being_made[request_index]["status"] = "in_progress"
192
- requests_being_made[request_index]["start_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
193
-
194
- try:
195
- start_time = time.time()
196
- async with session.get(url, params=params, allow_redirects=True) as response:
197
- content = await response.read()
198
- end_time = time.time()
199
- response_time = end_time - start_time
200
-
201
- with requests_lock:
202
- if request_index < len(requests_being_made):
203
- requests_being_made[request_index]["status"] = "success"
204
- requests_being_made[request_index]["status_code"] = response.status
205
- requests_being_made[request_index]["response_time"] = round(response_time, 3)
206
- requests_being_made[request_index]["content_length"] = len(content)
207
- requests_being_made[request_index]["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
208
- progress_counter.value += 1
209
- except Exception as e:
210
- with requests_lock:
211
- if request_index < len(requests_being_made):
212
- requests_being_made[request_index]["status"] = "failed"
213
- requests_being_made[request_index]["error"] = str(e)
214
- requests_being_made[request_index]["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
215
- progress_counter.value += 1
216
-
217
- except Exception as e:
218
- with requests_lock:
219
- if request_index < len(requests_being_made):
220
- requests_being_made[request_index]["status"] = "failed"
221
- requests_being_made[request_index]["error"] = str(e)
222
- requests_being_made[request_index]["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
223
- progress_counter.value += 1
224
-
225
- async def process_request_chunk(start_index, chunk_size, process_id=0):
226
- """Traite un groupe de requêtes dans un processus séparé"""
227
- logger.info(f"Processus {process_id}: Démarrage du traitement pour les indices {start_index} à {start_index+chunk_size-1}")
228
- request_counter = multiprocessing.Value('i', 0)
229
-
230
- async with await create_session(request_counter) as session:
231
- semaphore = asyncio.Semaphore(MAX_CONNECTIONS // multiprocessing.cpu_count())
232
-
233
- async def process_request(i):
234
- request_index = start_index + i
235
- if request_index >= total_requests.value:
236
- return
237
-
238
- async with semaphore:
239
- await make_homepage_request(session, request_index, request_counter)
240
- await asyncio.sleep(random.uniform(0.2, 2.0))
241
- if progress_counter.value % SAVE_FREQUENCY == 0:
242
- save_results_to_file()
243
-
244
- tasks = [process_request(i) for i in range(min(chunk_size, total_requests.value - start_index))]
245
- await asyncio.gather(*tasks)
246
-
247
- logger.info(f"Processus {process_id}: Traitement terminé pour le chunk commençant à l'indice {start_index}")
248
-
249
- def save_results_to_file():
250
- """Sauvegarde l'état actuel dans un fichier JSON de manière thread-safe"""
251
- with requests_lock:
252
- try:
253
- data_to_save = list(requests_being_made)
254
- temp_file = f"{REQUESTS_FILE}.tmp"
255
- with open(temp_file, "w", encoding="utf-8") as f:
256
- json.dump(data_to_save, f, indent=2, ensure_ascii=False, default=default_json)
257
- os.replace(temp_file, REQUESTS_FILE)
258
- logger.info(f"Sauvegarde effectuée: {progress_counter.value}/{total_requests.value} requêtes")
259
- except Exception as e:
260
- logger.error(f"Erreur lors de la sauvegarde: {str(e)}")
261
-
262
- def run_request_process(start_index, chunk_size, process_id):
263
- """Fonction exécutée dans chaque processus pour effectuer des requêtes"""
264
- try:
265
- loop = asyncio.new_event_loop()
266
- asyncio.set_event_loop(loop)
267
- loop.run_until_complete(process_request_chunk(start_index, chunk_size, process_id))
268
- loop.close()
269
- except Exception as e:
270
- logger.error(f"Erreur dans le processus {process_id}: {str(e)}")
271
-
272
- def start_request_process(num_requests, concurrency):
273
- """Démarre le processus d'envoi de requêtes avec multiprocessing"""
274
- global requests_in_progress, total_requests, progress_counter, requests_being_made, process_pool, background_tasks
275
-
276
- with requests_lock:
277
- progress_counter.value = 0
278
- total_requests.value = num_requests
279
- requests_being_made[:] = [{"status": "pending"} for _ in range(num_requests)]
280
-
281
- num_cpus = multiprocessing.cpu_count()
282
- num_processes = min(num_cpus, (num_requests + CHUNK_SIZE - 1) // CHUNK_SIZE)
283
-
284
- logger.info(f"Démarrage de l'envoi de {num_requests} requêtes avec {num_processes} processus et concurrence de {concurrency}")
285
- process_pool = ProcessPoolExecutor(max_workers=num_processes)
286
- background_tasks = []
287
-
288
- for i in range(num_processes):
289
- start_idx = i * CHUNK_SIZE
290
- if start_idx < num_requests:
291
- task = process_pool.submit(
292
- run_request_process,
293
- start_idx,
294
- min(CHUNK_SIZE, num_requests - start_idx),
295
- i
296
- )
297
- background_tasks.append(task)
298
-
299
- monitor_thread = ThreadPoolExecutor(max_workers=1)
300
- monitor_thread.submit(monitor_background_tasks)
301
-
302
- def monitor_background_tasks():
303
- """Surveille les tâches en arrière-plan et marque le processus comme terminé lorsque tout est fait"""
304
- global requests_in_progress, background_tasks
305
  try:
306
- for task in background_tasks:
307
- task.result()
308
- logger.info(f"Toutes les tâches d'envoi de requêtes sont terminées. {progress_counter.value}/{total_requests.value} requêtes traitées.")
309
- save_results_to_file()
310
- requests_in_progress.value = False
311
- except Exception as e:
312
- logger.error(f"Erreur lors de la surveillance des tâches: {str(e)}")
313
- requests_in_progress.value = False
314
-
315
- @app.route('/')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  def index():
317
- """Page d'accueil"""
318
- requests_data = []
319
- if os.path.exists(REQUESTS_FILE):
320
- try:
321
- with open(REQUESTS_FILE, "r", encoding="utf-8") as f:
322
- requests_data = json.load(f)
323
- except json.JSONDecodeError:
324
- requests_data = []
325
-
326
- return render_template('index.html',
327
- requests_in_progress=requests_in_progress.value,
328
- requests=requests_data[:1000],
329
- progress=progress_counter.value,
330
- total=total_requests.value)
331
 
332
  @app.route('/start', methods=['POST'])
333
  def start_requests():
334
- """Démarrer l'envoi de requêtes"""
335
- if not requests_in_progress.value:
336
- num_requests = int(request.form.get('num_requests', 1000))
337
- concurrency = int(request.form.get('concurrency', MAX_CONNECTIONS))
338
- requests_in_progress.value = True
339
- start_request_process(num_requests, concurrency)
340
- logger.info(f"Envoi de {num_requests} requêtes lancé avec concurrence {concurrency}")
341
- else:
342
- logger.warning("Un processus d'envoi de requêtes est déjà en cours")
343
-
344
- return redirect(url_for('index'))
345
-
346
- @app.route('/progress')
347
- def get_progress():
348
- """Endpoint API pour obtenir la progression complète"""
349
- requests_data = []
350
- if os.path.exists(REQUESTS_FILE):
351
- try:
352
- with open(REQUESTS_FILE, "r", encoding="utf-8") as f:
353
- requests_data = json.load(f)
354
- except json.JSONDecodeError:
355
- requests_data = []
356
-
357
- return jsonify({
358
- 'requests_in_progress': requests_in_progress.value,
359
- 'progress': progress_counter.value,
360
- 'total': total_requests.value,
361
- 'requests': requests_data[:200]
362
- })
363
-
364
- @app.route('/status')
365
- def get_status():
366
- """Endpoint API simplifié pour obtenir juste la progression"""
367
- return jsonify({
368
- 'requests_in_progress': requests_in_progress.value,
369
- 'progress': progress_counter.value,
370
- 'total': total_requests.value,
371
- 'success_count': sum(1 for req in requests_being_made if req.get('status') == 'success'),
372
- 'failed_count': sum(1 for req in requests_being_made if req.get('status') == 'failed'),
373
- 'pending_count': sum(1 for req in requests_being_made if req.get('status') == 'pending'),
374
- })
375
-
376
- @app.route('/reset', methods=['POST'])
377
- def reset():
378
- """Réinitialise le processus et supprime les données existantes"""
379
- global requests_in_progress, requests_being_made, progress_counter, total_requests, process_pool, background_tasks
380
-
381
- if not requests_in_progress.value:
382
- with requests_lock:
383
- requests_being_made[:] = []
384
- progress_counter.value = 0
385
- total_requests.value = 0
386
- if os.path.exists(REQUESTS_FILE):
387
- os.remove(REQUESTS_FILE)
388
- logger.info("Réinitialisation effectuée")
389
  else:
390
- logger.warning("Impossible de réinitialiser pendant un processus en cours")
391
-
392
- return redirect(url_for('index'))
393
-
394
- @app.route('/stop', methods=['POST'])
395
- def stop_requests():
396
- """Arrête le processus d'envoi de requêtes en cours"""
397
- global requests_in_progress, process_pool
398
- if requests_in_progress.value and process_pool:
399
- logger.info("Arrêt des processus d'envoi de requêtes...")
400
- process_pool.shutdown(wait=False)
401
- requests_in_progress.value = False
402
- save_results_to_file()
403
- logger.info("Processus d'envoi de requêtes arrêté")
404
-
405
- return redirect(url_for('index'))
406
-
407
- @app.route('/stats')
408
- def get_stats():
409
- """Obtient des statistiques sur les requêtes effectuées"""
410
- requests_data = []
411
- if os.path.exists(REQUESTS_FILE):
412
- try:
413
- with open(REQUESTS_FILE, "r", encoding="utf-8") as f:
414
- requests_data = json.load(f)
415
- except json.JSONDecodeError:
416
- requests_data = []
417
-
418
- successful_requests = [req for req in requests_data if req.get('status') == 'success']
419
- failed_requests = [req for req in requests_data if req.get('status') == 'failed']
420
-
421
- avg_response_time = 0
422
- if successful_requests:
423
- avg_response_time = sum(req.get('response_time', 0) for req in successful_requests) / len(successful_requests)
424
-
425
- status_codes = {}
426
- for req in successful_requests:
427
- code = req.get('status_code')
428
- if code:
429
- status_codes[code] = status_codes.get(code, 0) + 1
430
-
431
- error_types = {}
432
- for req in failed_requests:
433
- error = req.get('error', 'Unknown')
434
- error_type = error.split(':')[0] if ':' in error else error
435
- error_types[error_type] = error_types.get(error_type, 0) + 1
436
-
437
- return jsonify({
438
- 'total_requests': len(requests_data),
439
- 'successful_requests': len(successful_requests),
440
- 'failed_requests': len(failed_requests),
441
- 'avg_response_time': avg_response_time,
442
- 'status_codes': status_codes,
443
- 'error_types': error_types
444
- })
445
-
446
- # Template HTML pour l'interface utilisateur
447
- @app.route('/templates/index.html')
448
- def get_template():
449
- return """
450
- <!DOCTYPE html>
451
- <html lang="fr">
452
- <head>
453
- <meta charset="UTF-8">
454
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
455
- <title>Générateur de requêtes pour gabaohub.alwaysdata.net</title>
456
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
457
- <style>
458
- .status-badge {
459
- display: inline-block;
460
- padding: 0.25em 0.6em;
461
- font-size: 75%;
462
- font-weight: 700;
463
- line-height: 1;
464
- text-align: center;
465
- white-space: nowrap;
466
- vertical-align: baseline;
467
- border-radius: 0.25rem;
468
- }
469
- .status-pending { background-color: #6c757d; color: white; }
470
- .status-in_progress { background-color: #17a2b8; color: white; }
471
- .status-success { background-color: #28a745; color: white; }
472
- .status-failed { background-color: #dc3545; color: white; }
473
- .refresh-icon { cursor: pointer; }
474
- </style>
475
- </head>
476
- <body>
477
- <div class="container mt-4">
478
- <h1 class="mb-4">Générateur de requêtes pour gabaohub.alwaysdata.net</h1>
479
-
480
- <div class="card mb-4">
481
- <div class="card-header">
482
- Contrôles
483
- </div>
484
- <div class="card-body">
485
- <form action="/start" method="post" class="mb-3">
486
- <div class="row mb-3">
487
- <div class="col-md-6">
488
- <label for="num_requests" class="form-label">Nombre de requêtes à envoyer:</label>
489
- <input type="number" class="form-control" id="num_requests" name="num_requests" min="1" value="1000">
490
- </div>
491
- <div class="col-md-6">
492
- <label for="concurrency" class="form-label">Niveau de concurrence:</label>
493
- <input type="number" class="form-control" id="concurrency" name="concurrency" min="1" max="1000" value="100">
494
- </div>
495
- </div>
496
- <button type="submit" class="btn btn-primary" id="start-button">Démarrer</button>
497
- </form>
498
-
499
- <div class="d-flex">
500
- <form action="/stop" method="post" class="me-2">
501
- <button type="submit" class="btn btn-warning" id="stop-button">Arrêter</button>
502
- </form>
503
- <form action="/reset" method="post">
504
- <button type="submit" class="btn btn-danger" id="reset-button">Réinitialiser</button>
505
- </form>
506
- </div>
507
- </div>
508
- </div>
509
-
510
- <div class="card mb-4">
511
- <div class="card-header d-flex justify-content-between align-items-center">
512
- <span>Progression</span>
513
- <span class="refresh-icon" onclick="updateProgress()">🔄</span>
514
- </div>
515
- <div class="card-body">
516
- <div class="progress mb-3">
517
- <div id="progress-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
518
- </div>
519
- <p id="progress-text">0 / 0 requêtes traitées (0%)</p>
520
- <p id="status-text">État: Inactif</p>
521
-
522
- <div class="row mt-4">
523
- <div class="col-md-4">
524
- <div class="card bg-success text-white">
525
- <div class="card-body">
526
- <h5 class="card-title">Réussies</h5>
527
- <h2 id="success-count">0</h2>
528
- </div>
529
- </div>
530
- </div>
531
- <div class="col-md-4">
532
- <div class="card bg-danger text-white">
533
- <div class="card-body">
534
- <h5 class="card-title">Échouées</h5>
535
- <h2 id="failed-count">0</h2>
536
- </div>
537
- </div>
538
- </div>
539
- <div class="col-md-4">
540
- <div class="card bg-secondary text-white">
541
- <div class="card-body">
542
- <h5 class="card-title">En attente</h5>
543
- <h2 id="pending-count">0</h2>
544
- </div>
545
- </div>
546
- </div>
547
- </div>
548
- </div>
549
- </div>
550
-
551
- <div class="card mb-4">
552
- <div class="card-header d-flex justify-content-between align-items-center">
553
- <span>Statistiques</span>
554
- <span class="refresh-icon" onclick="updateStats()">🔄</span>
555
- </div>
556
- <div class="card-body" id="stats-container">
557
- <p>Aucune statistique disponible.</p>
558
- </div>
559
- </div>
560
-
561
- <div class="card">
562
- <div class="card-header d-flex justify-content-between align-items-center">
563
- <span>Dernières requêtes</span>
564
- <span class="refresh-icon" onclick="updateRequests()">🔄</span>
565
- </div>
566
- <div class="card-body">
567
- <div class="table-responsive">
568
- <table class="table table-striped">
569
- <thead>
570
- <tr>
571
- <th>#</th>
572
- <th>URL</th>
573
- <th>Statut</th>
574
- <th>Code</th>
575
- <th>Temps (s)</th>
576
- <th>Heure</th>
577
- </tr>
578
- </thead>
579
- <tbody id="requests-table">
580
- <tr>
581
- <td colspan="6" class="text-center">Chargement des données...</td>
582
- </tr>
583
- </tbody>
584
- </table>
585
- </div>
586
- </div>
587
- </div>
588
- </div>
589
-
590
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
591
- <script>
592
- let isPolling = false;
593
- let pollingInterval;
594
-
595
- window.onload = function() {
596
- updateProgress();
597
- updateRequests();
598
- updateStats();
599
-
600
- pollingInterval = setInterval(() => {
601
- if (isPolling) {
602
- updateProgress();
603
- updateStats();
604
- }
605
- }, 5000);
606
- };
607
-
608
- function updateProgress() {
609
- fetch('/status')
610
- .then(response => response.json())
611
- .then(data => {
612
- const percentage = data.total > 0 ? (data.progress / data.total * 100) : 0;
613
- document.getElementById('progress-bar').style.width = percentage + '%';
614
- document.getElementById('progress-bar').setAttribute('aria-valuenow', percentage);
615
-
616
- document.getElementById('progress-text').textContent =
617
- `${data.progress} / ${data.total} requêtes traitées (${percentage.toFixed(1)}%)`;
618
- document.getElementById('status-text').textContent =
619
- `État: ${data.requests_in_progress ? 'En cours' : 'Inactif'}`;
620
-
621
- document.getElementById('success-count').textContent = data.success_count;
622
- document.getElementById('failed-count').textContent = data.failed_count;
623
- document.getElementById('pending-count').textContent = data.pending_count;
624
-
625
- isPolling = data.requests_in_progress;
626
- document.getElementById('start-button').disabled = data.requests_in_progress;
627
- document.getElementById('stop-button').disabled = !data.requests_in_progress;
628
- document.getElementById('reset-button').disabled = data.requests_in_progress;
629
- })
630
- .catch(error => {
631
- console.error('Erreur lors de la récupération de la progression:', error);
632
- });
633
- }
634
-
635
- function updateRequests() {
636
- fetch('/progress')
637
- .then(response => response.json())
638
- .then(data => {
639
- const tableBody = document.getElementById('requests-table');
640
- tableBody.innerHTML = '';
641
-
642
- if (data.requests.length === 0) {
643
- tableBody.innerHTML = '<tr><td colspan="6" class="text-center">Aucune requête effectuée.</td></tr>';
644
- return;
645
- }
646
-
647
- data.requests.forEach((req, index) => {
648
- const row = document.createElement('tr');
649
- const statusClass = `status-${req.status || 'pending'}`;
650
-
651
- row.innerHTML = `
652
- <td>${index + 1}</td>
653
- <td>${req.url || 'N/A'}</td>
654
- <td><span class="status-badge ${statusClass}">${req.status || 'pending'}</span></td>
655
- <td>${req.status_code || '-'}</td>
656
- <td>${req.response_time || '-'}</td>
657
- <td>${req.end_time || req.start_time || '-'}</td>
658
- `;
659
-
660
- tableBody.appendChild(row);
661
- });
662
- })
663
- .catch(error => {
664
- console.error('Erreur lors de la récupération des requêtes:', error);
665
- });
666
- }
667
-
668
- function updateStats() {
669
- fetch('/stats')
670
- .then(response => response.json())
671
- .then(data => {
672
- const statsContainer = document.getElementById('stats-container');
673
-
674
- let statusCodeHtml = '<div class="mt-3"><h5>Codes de statut</h5>';
675
- if (Object.keys(data.status_codes).length > 0) {
676
- statusCodeHtml += '<div class="row">';
677
- for (const [code, count] of Object.entries(data.status_codes)) {
678
- const colorClass = code.startsWith('2') ? 'success' :
679
- code.startsWith('3') ? 'info' :
680
- code.startsWith('4') ? 'warning' : 'danger';
681
- statusCodeHtml += `
682
- <div class="col-md-3 mb-2">
683
- <div class="card bg-${colorClass} text-white">
684
- <div class="card-body p-2 text-center">
685
- <h5 class="card-title">${code}</h5>
686
- <p class="card-text">${count} requêtes</p>
687
- </div>
688
- </div>
689
- </div>
690
- `;
691
- }
692
- statusCodeHtml += '</div>';
693
- } else {
694
- statusCodeHtml += '<p>Aucun code de statut disponible.</p>';
695
- }
696
- statusCodeHtml += '</div>';
697
-
698
- let errorTypesHtml = '<div class="mt-3"><h5>Types d\'erreurs</h5>';
699
- if (Object.keys(data.error_types).length > 0) {
700
- errorTypesHtml += '<ul class="list-group">';
701
- for (const [type, count] of Object.entries(data.error_types)) {
702
- errorTypesHtml += `
703
- <li class="list-group-item d-flex justify-content-between align-items-center">
704
- ${type}
705
- <span class="badge bg-danger rounded-pill">${count}</span>
706
- </li>
707
- `;
708
- }
709
- errorTypesHtml += '</ul>';
710
- } else {
711
- errorTypesHtml += '<p>Aucune erreur disponible.</p>';
712
- }
713
- errorTypesHtml += '</div>';
714
-
715
- statsContainer.innerHTML = `
716
- <div class="row">
717
- <div class="col-md-6">
718
- <h5>Résumé</h5>
719
- <ul class="list-group">
720
- <li class="list-group-item d-flex justify-content-between align-items-center">
721
- Requêtes totales
722
- <span class="badge bg-primary rounded-pill">${data.total_requests}</span>
723
- </li>
724
- <li class="list-group-item d-flex justify-content-between align-items-center">
725
- Requêtes réussies
726
- <span class="badge bg-success rounded-pill">${data.successful_requests}</span>
727
- </li>
728
- <li class="list-group-item d-flex justify-content-between align-items-center">
729
- Requêtes échouées
730
- <span class="badge bg-danger rounded-pill">${data.failed_requests}</span>
731
- </li>
732
- </ul>
733
- </div>
734
- <div class="col-md-6">
735
- <h5>Performance</h5>
736
- <div class="card">
737
- <div class="card-body">
738
- <h3>${data.avg_response_time.toFixed(3)} s</h3>
739
- <p class="text-muted">Temps de réponse moyen</p>
740
- </div>
741
- </div>
742
- </div>
743
- </div>
744
- ${statusCodeHtml}
745
- ${errorTypesHtml}
746
- `;
747
- })
748
- .catch(error => {
749
- console.error('Erreur lors de la récupération des statistiques:', error);
750
- });
751
- }
752
- </script>
753
- </body>
754
- </html>
755
- """
756
 
 
757
  if __name__ == '__main__':
758
- app.run(debug=True)
 
 
 
1
+ import Flask
2
+ from flask import request, render_template_string, jsonify, redirect, url_for
3
+ import requests
4
+ import threading
5
+ import uuid # Pour générer des identifiants uniques pour chaque tâche
6
  import time
7
+ import copy # Pour copier le payload pour chaque requête
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  app = Flask(__name__)
10
 
11
+ # --- Configuration ---
12
+ TARGET_URL = "https://hook.us1.make.com/zal5qn0ggbewmvtsbo2uenfno8tz3n56"
13
+ BASE_PAYLOAD = {
14
+ "name": "Testeur Auto ",
15
+ "email": "[email protected]", # Ajout de '+auto' pour distinguer
16
+ "company": "aragon Inc.",
17
+ "message": "Ceci est un test automatisé via Flask.",
18
+ "date": "2023-10-27T10:30:00Z", # Tu pourrais rendre cette date dynamique si besoin
19
+ "source": "http://simulateur-bsbs-flask.com"
20
+ }
21
+ # Structure pour stocker l'état des tâches (jobs) en mémoire
22
+ # Format: { 'job_id': {'status': 'running'/'completed'/'failed', 'total': N, 'completed_count': M, 'error_count': E, 'errors': [...] } }
23
+ jobs = {}
24
+ jobs_lock = threading.Lock() # Pour éviter les problèmes d'accès concurrents au dict jobs
25
+
26
+ # --- Templates HTML ---
27
+
28
+ # Page d'accueil pour démarrer les requêtes
29
+ HTML_INDEX = """
30
+ <!DOCTYPE html>
31
+ <html lang="fr">
32
+ <head>
33
+ <meta charset="UTF-8">
34
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
+ <title>Lanceur de Requêtes</title>
36
+ <style>
37
+ body { font-family: sans-serif; margin: 20px; }
38
+ label { display: block; margin-bottom: 5px; }
39
+ input[type=number] { width: 100px; padding: 8px; margin-bottom: 15px; }
40
+ button { padding: 10px 15px; cursor: pointer; }
41
+ .error { color: red; margin-top: 10px; }
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <h1>Envoyer des Requêtes POST en Masse</h1>
46
+ <form method="POST" action="/start">
47
+ <label for="num_requests">Nombre de requêtes à envoyer :</label>
48
+ <input type="number" id="num_requests" name="num_requests" min="1" required>
49
+ <button type="submit">Lancer les requêtes</button>
50
+ </form>
51
+ {% if error %}
52
+ <p class="error">{{ error }}</p>
53
+ {% endif %}
54
+
55
+ <h2>Tâches en cours / terminées :</h2>
56
+ <ul>
57
+ {% for job_id, job_info in jobs_list.items() %}
58
+ <li>
59
+ <a href="{{ url_for('job_status', job_id=job_id) }}">Tâche {{ job_id }}</a>
60
+ ({{ job_info.status }}, {{ job_info.completed_count }}/{{ job_info.total }} complétées, {{ job_info.error_count }} erreurs)
61
+ </li>
62
+ {% else %}
63
+ <li>Aucune tâche récente.</li>
64
+ {% endfor %}
65
+ </ul>
66
+ </body>
67
+ </html>
68
+ """
69
+
70
+ # Page pour suivre la progression d'une tâche spécifique
71
+ HTML_STATUS = """
72
+ <!DOCTYPE html>
73
+ <html lang="fr">
74
+ <head>
75
+ <meta charset="UTF-8">
76
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
77
+ <title>Statut Tâche {{ job_id }}</title>
78
+ <style>
79
+ body { font-family: sans-serif; margin: 20px; }
80
+ #progress-bar-container { width: 100%; background-color: #f0f0f0; border-radius: 5px; margin-bottom: 10px; }
81
+ #progress-bar { width: 0%; height: 30px; background-color: #4CAF50; text-align: center; line-height: 30px; color: white; border-radius: 5px; transition: width 0.5s ease-in-out; }
82
+ .error-log { margin-top: 15px; max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9;}
83
+ .error-log p { margin: 5px 0; font-size: 0.9em; }
84
+ .status-message { font-weight: bold; margin-bottom: 15px; }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <h1>Statut de la Tâche : {{ job_id }}</h1>
89
+ <div id="status-message" class="status-message">Chargement...</div>
90
+ <div id="progress-bar-container">
91
+ <div id="progress-bar">0%</div>
92
+ </div>
93
+ <p>Requêtes complétées : <span id="completed">0</span> / <span id="total">?</span></p>
94
+ <p>Erreurs : <span id="errors">0</span></p>
95
+ <div id="error-details" class="error-log" style="display: none;">
96
+ <h3>Détails des erreurs :</h3>
97
+ <div id="error-list"></div>
98
+ </div>
99
+ <p><a href="/">Retour à l'accueil</a></p>
100
+
101
+ <script>
102
+ const jobId = "{{ job_id }}";
103
+ const statusMessageEl = document.getElementById('status-message');
104
+ const progressBarEl = document.getElementById('progress-bar');
105
+ const completedEl = document.getElementById('completed');
106
+ const totalEl = document.getElementById('total');
107
+ const errorsEl = document.getElementById('errors');
108
+ const errorDetailsEl = document.getElementById('error-details');
109
+ const errorListEl = document.getElementById('error-list');
110
+
111
+ let intervalId = null;
112
+
113
+ function updateStatus() {
114
+ fetch(`/api/status/${jobId}`)
115
+ .then(response => {
116
+ if (!response.ok) {
117
+ throw new Error(`Erreur HTTP: ${response.status}`);
118
+ }
119
+ return response.json();
120
+ })
121
+ .then(data => {
122
+ if (!data) { // Gère le cas où la tâche n'est pas encore prête
123
+ statusMessageEl.textContent = "En attente de démarrage...";
124
+ return;
125
+ }
126
+
127
+ completedEl.textContent = data.completed_count;
128
+ totalEl.textContent = data.total;
129
+ errorsEl.textContent = data.error_count;
130
+ statusMessageEl.textContent = `Statut : ${data.status}`;
131
+
132
+ let percentage = 0;
133
+ if (data.total > 0) {
134
+ percentage = Math.round((data.completed_count / data.total) * 100);
135
+ }
136
+ progressBarEl.style.width = percentage + '%';
137
+ progressBarEl.textContent = percentage + '%';
138
+
139
+ // Afficher les erreurs
140
+ if (data.error_count > 0 && data.errors && data.errors.length > 0) {
141
+ errorListEl.innerHTML = ''; // Clear previous errors
142
+ data.errors.forEach(err => {
143
+ const p = document.createElement('p');
144
+ p.textContent = `Req ${err.index}: ${err.error}`;
145
+ errorListEl.appendChild(p);
146
+ });
147
+ errorDetailsEl.style.display = 'block';
148
+ } else {
149
+ errorDetailsEl.style.display = 'none';
150
+ }
151
+
152
+
153
+ if (data.status === 'completed' || data.status === 'failed') {
154
+ if (intervalId) {
155
+ clearInterval(intervalId);
156
+ intervalId = null; // Arrête les mises à jour
157
+ console.log("Mises à jour arrêtées car la tâche est terminée.");
158
+ if (data.status === 'completed' && data.error_count == 0) {
159
+ progressBarEl.style.backgroundColor = '#4CAF50'; // Vert
160
+ } else if (data.status === 'completed' && data.error_count > 0) {
161
+ progressBarEl.style.backgroundColor = '#ff9800'; // Orange
162
+ } else { // failed
163
+ progressBarEl.style.backgroundColor = '#f44336'; // Rouge
164
+ }
165
+ }
166
+ }
167
+ })
168
+ .catch(error => {
169
+ console.error("Erreur lors de la récupération du statut:", error);
170
+ statusMessageEl.textContent = "Erreur lors de la récupération du statut.";
171
+ if (intervalId) {
172
+ clearInterval(intervalId); // Arrête en cas d'erreur persistante
173
+ intervalId = null;
174
+ }
175
+ });
176
  }
177
+
178
+ // Mettre à jour immédiatement puis toutes les 2 secondes
179
+ updateStatus();
180
+ intervalId = setInterval(updateStatus, 2000);
181
+ </script>
182
+ </body>
183
+ </html>
184
+ """
185
+
186
+ # --- Fonctions Logiques ---
187
+
188
+ def send_single_request(target_url, payload, job_id, request_index):
189
+ """Fonction pour envoyer UNE requête POST."""
190
+ # Crée une copie pour éviter de modifier l'original et pour ajouter un identifiant
191
+ current_payload = copy.deepcopy(payload)
192
+ current_payload['message'] += f" (Requête {request_index + 1})" # Ajoute un numéro à chaque message
193
+ current_payload['request_uuid'] = str(uuid.uuid4()) # Ajoute un id unique par requête
194
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  try:
196
+ response = requests.post(target_url, json=current_payload, timeout=30) # Timeout de 30s
197
+ response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx, 5xx)
198
+ return True, None # Succès
199
+ except requests.exceptions.RequestException as e:
200
+ print(f"Erreur requête {request_index + 1} pour job {job_id}: {e}")
201
+ return False, f"Req {request_index + 1}: {str(e)}" # Échec avec message d'erreur
202
+
203
+ def background_task(job_id, num_requests, target_url, base_payload):
204
+ """Fonction exécutée dans un thread séparé pour envoyer les requêtes."""
205
+ print(f"Tâche {job_id}: Démarrage de {num_requests} requêtes vers {target_url}")
206
+ completed_count = 0
207
+ error_count = 0
208
+ error_messages = []
209
+
210
+ # Initialiser le statut (on le fait déjà dans /start mais re-vérifier est ok)
211
+ with jobs_lock:
212
+ if job_id not in jobs:
213
+ jobs[job_id] = {
214
+ 'status': 'running',
215
+ 'total': num_requests,
216
+ 'completed_count': 0,
217
+ 'error_count': 0,
218
+ 'errors': []
219
+ }
220
+
221
+ for i in range(num_requests):
222
+ success, error_msg = send_single_request(target_url, base_payload, job_id, i)
223
+ completed_count += 1
224
+ if not success:
225
+ error_count += 1
226
+ error_messages.append({'index': i + 1, 'error': error_msg})
227
+
228
+ # Mettre à jour la progression dans le dictionnaire partagé (avec verrou)
229
+ with jobs_lock:
230
+ jobs[job_id]['completed_count'] = completed_count
231
+ jobs[job_id]['error_count'] = error_count
232
+ # Gardons seulement les X dernières erreurs pour éviter de saturer la mémoire
233
+ jobs[job_id]['errors'] = error_messages[-50:] # Garde les 50 dernières erreurs
234
+
235
+ # Petite pause optionnelle pour ne pas submerger la cible (ex: 0.1 seconde)
236
+ # time.sleep(0.1)
237
+
238
+ # Marquer la tâche comme terminée
239
+ final_status = 'failed' if error_count == num_requests else ('completed' if error_count == 0 else 'completed_with_errors')
240
+ with jobs_lock:
241
+ jobs[job_id]['status'] = final_status
242
+
243
+ print(f"Tâche {job_id}: Terminé. {completed_count - error_count} succès, {error_count} erreurs.")
244
+
245
+ # --- Routes Flask ---
246
+
247
+ @app.route('/', methods=['GET'])
248
  def index():
249
+ """Affiche la page d'accueil avec le formulaire."""
250
+ # On passe une copie triée des jobs récents au template
251
+ with jobs_lock:
252
+ # Trie par exemple par clé (qui approxime l'ordre de création ici)
253
+ # ou ajoute un timestamp à la création du job pour trier par date.
254
+ sorted_jobs = dict(sorted(jobs.items(), reverse=True))
255
+ return render_template_string(HTML_INDEX, jobs_list=sorted_jobs)
 
 
 
 
 
 
 
256
 
257
  @app.route('/start', methods=['POST'])
258
  def start_requests():
259
+ """Reçoit le nombre de requêtes et lance la tâche en arrière-plan."""
260
+ try:
261
+ num_requests = int(request.form.get('num_requests'))
262
+ if num_requests <= 0:
263
+ raise ValueError("Le nombre de requêtes doit être positif.")
264
+ except (TypeError, ValueError) as e:
265
+ with jobs_lock:
266
+ sorted_jobs = dict(sorted(jobs.items(), reverse=True))
267
+ return render_template_string(HTML_INDEX, error=f"Nombre invalide: {e}", jobs_list=sorted_jobs), 400
268
+
269
+ job_id = str(uuid.uuid4())[:8] # ID de tâche court et unique
270
+
271
+ # Initialiser l'état de la tâche avant de démarrer le thread
272
+ with jobs_lock:
273
+ jobs[job_id] = {
274
+ 'status': 'starting', # ou 'queued'
275
+ 'total': num_requests,
276
+ 'completed_count': 0,
277
+ 'error_count': 0,
278
+ 'errors': []
279
+ }
280
+
281
+ # Créer et démarrer le thread
282
+ thread = threading.Thread(target=background_task, args=(job_id, num_requests, TARGET_URL, BASE_PAYLOAD))
283
+ thread.daemon = True # Permet au programme principal de quitter même si des threads tournent
284
+ thread.start()
285
+
286
+ print(f"Nouvelle tâche démarrée avec ID: {job_id}")
287
+ # Rediriger vers la page de statut de cette tâche
288
+ return redirect(url_for('job_status', job_id=job_id))
289
+
290
+ @app.route('/status/<job_id>', methods=['GET'])
291
+ def job_status(job_id):
292
+ """Affiche la page HTML de suivi pour une tâche spécifique."""
293
+ with jobs_lock:
294
+ if job_id not in jobs:
295
+ return "Tâche non trouvée", 404
296
+ # La page HTML utilisera l'API /api/status pour les mises à jour dynamiques
297
+ return render_template_string(HTML_STATUS, job_id=job_id)
298
+
299
+ @app.route('/api/status/<job_id>', methods=['GET'])
300
+ def api_job_status(job_id):
301
+ """Fournit l'état actuel d'une tâche au format JSON (pour le JavaScript)."""
302
+ with jobs_lock:
303
+ job_info = jobs.get(job_id)
304
+
305
+ if job_info:
306
+ # Renvoyer une copie pour éviter les modifs concurrentes pendant la sérialisation JSON
307
+ return jsonify(copy.deepcopy(job_info))
 
 
 
 
 
 
308
  else:
309
+ # Renvoyer une réponse JSON même pour une erreur 404
310
+ return jsonify({"error": "Tâche non trouvée"}), 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ # --- Démarrage de l'application ---
313
  if __name__ == '__main__':
314
+ # Utilise host='0.0.0.0' pour rendre accessible depuis d'autres machines sur le réseau
315
+ # Attention: debug=True ne doit PAS être utilisé en production
316
+ app.run(host='0.0.0.0', port=5000, debug=True)