MMOON commited on
Commit
cb3f8ef
·
verified ·
1 Parent(s): 765f26e

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +163 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,165 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
 
 
 
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
1
+ # Fichier: app.py
2
+
3
+ # ===================================================================================
4
+ # WAHIS SCRAPER - VERSION TABLEAU DE BORD STREAMLIT (APPROCHE FINALE ET ROBUSTE)
5
+ # ===================================================================================
6
+
7
  import streamlit as st
8
+ import pandas as pd
9
+ import json
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ import warnings
13
+ import asyncio
14
+ import subprocess
15
+ from playwright.async_api import async_playwright
16
+ from playwright_stealth import stealth_async
17
+ from streamlit_folium import st_folium
18
+ import folium
19
+
20
+ # --- Configuration de la Page Streamlit ---
21
+ st.set_page_config(layout="wide", page_title="WAHIS Animal Disease Dashboard")
22
+
23
+ # --- Classes et Fonctions de Scraping (inchangées) ---
24
+ # ... (la logique de scraping est parfaite, on la garde telle quelle)
25
+ class WAHISScraper:
26
+ def __init__(self): self.logs = []
27
+ def log(self, message):
28
+ timestamp = datetime.now().strftime("%H:%M:%S")
29
+ self.logs.append(f"[{timestamp}] {message}")
30
+ print(message)
31
+ async def run_extraction_async(self):
32
+ self.log("🚀 Lancement de l'extraction en trois phases...")
33
+ async with async_playwright() as p:
34
+ browser = None
35
+ try:
36
+ self.log("🔧 Lancement d'un navigateur Chromium...")
37
+ browser = await p.chromium.launch(headless=True, args=["--no-sandbox"])
38
+ page = await browser.new_page()
39
+ self.log("🕵️ Application du camouflage 'stealth'...")
40
+ await stealth_async(page)
41
+ self.log("🌍 Visite de la page principale pour passer le challenge Cloudflare...")
42
+ await page.goto("https://wahis.woah.org/#/event-management", wait_until="networkidle", timeout=90000)
43
+ self.log("🍪 Challenge Cloudflare réussi.")
44
+ headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'clientid': 'OIEwebsite', 'env': 'PRD', 'security-token': 'token', 'type': 'REQUEST' }
45
+ self.log("--- PHASE 1 : Récupération des rapports ---")
46
+ list_api_url = "https://wahis.woah.org/api/v1/pi/event/filtered-list?language=fr"
47
+ payload_list = { "pageNumber": 1, "pageSize": 100, "sortColName": "REP_LAST_UPDATE", "sortColOrder": "DESC", "reportFilters": {}, "languageChanged": False }
48
+ list_response_json = await page.evaluate("async (args) => (await fetch(args.url, { method: 'POST', headers: args.headers, body: JSON.stringify(args.payload) })).json()", {'url': list_api_url, 'headers': headers, 'payload': payload_list})
49
+ report_list = list_response_json.get('list', [])
50
+ if not report_list: raise Exception("Échec de la phase 1.")
51
+ self.log(f"✅ Phase 1 réussie : {len(report_list)} rapports de base récupérés.")
52
+ unique_event_ids = sorted(list(set(item['eventId'] for item in report_list if 'eventId' in item)))
53
+ self.log(f"--- PHASE 2 : Récupération des données GPS pour {len(unique_event_ids)} événements...")
54
+ outbreaks_api_url = "https://wahis.woah.org/api/v1/pi/map-data/outbreaks-from-event-ids?language=fr"
55
+ all_outbreaks_data = await page.evaluate("async (args) => (await fetch(args.url, { method: 'POST', headers: args.headers, body: JSON.stringify(args.payload) })).json()", {'url': outbreaks_api_url, 'headers': headers, 'payload': unique_event_ids})
56
+ self.log(f"✅ Phase 2 réussie : {len(all_outbreaks_data)} foyers récupérés.")
57
+ unique_outbreak_ids = sorted(list(set(item['outbreakId'] for item in all_outbreaks_data if 'outbreakId' in item)))
58
+ self.log(f"--- PHASE 3 : Récupération des détails épidémiologiques pour {len(unique_outbreak_ids)} foyers...")
59
+ additional_info_data = []
60
+ if unique_outbreak_ids:
61
+ additional_info_api_url = "https://wahis.woah.org/api/v1/pi/outbreak/additional-information"
62
+ additional_info_data = await page.evaluate("async (args) => (await fetch(args.url, { method: 'POST', headers: args.headers, body: JSON.stringify(args.payload) })).json()", {'url': additional_info_api_url, 'headers': headers, 'payload': unique_outbreak_ids})
63
+ self.log(f"✅ Phase 3 réussie : {len(additional_info_data)} fiches de détails récupérées.")
64
+ return report_list, all_outbreaks_data, additional_info_data, "\n".join(self.logs)
65
+ finally:
66
+ if browser and browser.is_connected(): await browser.close()
67
+
68
+ # --- Fonctions de Traitement des Données ---
69
+ def process_data(reports, outbreaks, additional_infos):
70
+ valid_additional_infos = [info for info in additional_infos if isinstance(info, dict)]
71
+ additional_info_map = {info.get('outbreakId'): info for info in valid_additional_infos}
72
+
73
+ report_map = {report['eventId']: {'disease': report['disease']} for report in reports}
74
+
75
+ for outbreak in outbreaks:
76
+ event_info = report_map.get(outbreak.get('eventId'), {})
77
+ outbreak['diseaseName'] = event_info.get('disease')
78
+ outbreak_id = outbreak.get('outbreakId')
79
+ if outbreak_id in additional_info_map:
80
+ outbreak.update(additional_info_map[outbreak_id])
81
+
82
+ return pd.DataFrame(outbreaks)
83
+
84
+ # --- Construction de l'Interface Streamlit ---
85
+
86
+ st.title("🤖 Tableau de Bord WAHIS")
87
+ st.info("Ce tableau de bord extrait et affiche les derniers foyers de maladies animales signalés à l'Organisation Mondiale de la Santé Animale (WOAH).")
88
+
89
+ # Utilisation du "session_state" pour stocker les données après le premier chargement
90
+ if 'df_outbreaks' not in st.session_state:
91
+ st.session_state.df_outbreaks = pd.DataFrame()
92
+ st.session_state.logs = ""
93
+ st.session_state.last_click = None
94
+
95
+ if st.button("🚀 Lancer l'extraction des données"):
96
+ with st.spinner("Extraction en cours... (cela peut prendre 2-3 minutes)"):
97
+ scraper = WAHISScraper()
98
+ reports, outbreaks, additional, logs = asyncio.run(scraper.run_extraction_async())
99
+ if reports:
100
+ st.session_state.df_outbreaks = process_data(reports, outbreaks, additional)
101
+ st.session_state.logs = logs
102
+ st.success("Extraction terminée avec succès !")
103
+ else:
104
+ st.error("L'extraction a échoué. Veuillez consulter les logs.")
105
+ st.session_state.logs = logs
106
+
107
+ if not st.session_state.df_outbreaks.empty:
108
+ df = st.session_state.df_outbreaks
109
+
110
+ # --- Barre Latérale avec les Filtres ---
111
+ st.sidebar.header("🔍 Filtres")
112
+ all_diseases = ["Toutes"] + sorted(df['diseaseName'].dropna().unique())
113
+ all_species = ["Toutes"] + sorted(df['species'].dropna().unique())
114
+
115
+ selected_disease = st.sidebar.selectbox("Filtrer par Maladie", all_diseases)
116
+ selected_species = st.sidebar.selectbox("Filtrer par Espèce", all_species)
117
+
118
+ # Filtrage du DataFrame
119
+ filtered_df = df.copy()
120
+ if selected_disease != "Toutes":
121
+ filtered_df = filtered_df[filtered_df['diseaseName'] == selected_disease]
122
+ if selected_species != "Toutes":
123
+ filtered_df = filtered_df[filtered_df['species'] == selected_species]
124
+
125
+ # --- Affichage Principal : Carte et Détails ---
126
+ st.header(f"🗺️ Carte de {len(filtered_df)} Foyers")
127
+
128
+ if filtered_df.empty or not all(k in filtered_df for k in ['latitude', 'longitude']):
129
+ st.warning("Aucun foyer ne correspond à vos filtres, ou les données GPS sont manquantes.")
130
+ else:
131
+ # Création de la carte avec Folium
132
+ m = folium.Map(location=[filtered_df['latitude'].mean(), filtered_df['longitude'].mean()], zoom_start=2)
133
+
134
+ for _, row in filtered_df.iterrows():
135
+ popup_html = f"""
136
+ <b>Lieu:</b> {row.get('locationName', 'N/A')}<br>
137
+ <b>Maladie:</b> {row.get('diseaseName', 'N/A')}<br>
138
+ <b>Espèce:</b> {row.get('species', 'N/A')}<br>
139
+ <b>Cas:</b> {row.get('cases', 0)} | <b>Morts:</b> {row.get('deaths', 0)}
140
+ """
141
+ iframe = folium.IFrame(popup_html, width=250, height=100)
142
+ popup = folium.Popup(iframe, max_width=250)
143
+ folium.Marker(
144
+ location=[row['latitude'], row['longitude']],
145
+ popup=popup,
146
+ tooltip=row.get('diseaseName', 'N/A')
147
+ ).add_to(m)
148
+
149
+ # Affichage de la carte et récupération du dernier point cliqué
150
+ map_data = st_folium(m, width='100%')
151
+ if map_data and map_data['last_object_clicked_popup']:
152
+ st.session_state.last_click = map_data['last_object_clicked_popup']['html']
153
+
154
+ # Affichage des détails du dernier point cliqué
155
+ if st.session_state.last_click:
156
+ st.header("📋 Détails du Foyer Sélectionné")
157
+ st.markdown(st.session_state.last_click, unsafe_allow_html=True)
158
+
159
+ # --- Affichage du tableau de données ---
160
+ with st.expander("Voir le tableau de données des foyers filtrés"):
161
+ st.dataframe(filtered_df)
162
 
163
+ # --- Affichage des logs ---
164
+ with st.expander("Voir le journal d'exécution"):
165
+ st.text_area("Logs", st.session_state.logs, height=300)