MMOON commited on
Commit
5f209db
·
verified ·
1 Parent(s): 5685aa9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +544 -276
app.py CHANGED
@@ -1,11 +1,13 @@
1
  import logging
2
  from datetime import datetime, timedelta
 
3
  import pandas as pd
4
  import requests
5
- import streamlit as st
6
  import plotly.express as px
 
 
 
7
  import time
8
- import json
9
 
10
  # Configuration Streamlit
11
  st.set_page_config(page_title="Pesticide Data Explorer", page_icon="🌿", layout="wide")
@@ -18,319 +20,585 @@ logging.basicConfig(
18
  )
19
  logger = logging.getLogger(__name__)
20
 
21
- # Fonction simple pour récupérer les données avec un délai pour éviter les erreurs de taux
22
- def get_api_data(url, headers=None, params=None):
23
- """Fonction simple pour récupérer les données API avec gestion d'erreur basique"""
24
- max_retries = 3
25
- for attempt in range(max_retries):
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  try:
27
- # Ajout d'un petit délai entre les tentatives
28
- if attempt > 0:
29
- time.sleep(2)
30
 
31
- # Log de l'URL
32
- logger.info(f"Appel API: {url}")
33
-
34
- # Headers par défaut
35
- if headers is None:
36
- headers = {
37
- "Content-Type": "application/json",
38
- "Cache-Control": "no-cache",
39
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
40
- }
41
-
42
- # Appel API
43
- response = requests.get(url, headers=headers, params=params, timeout=10)
44
-
45
- # Log du code de statut
46
- logger.info(f"Code de statut: {response.status_code}")
47
-
48
- # Si succès, retourner les données
49
- if response.status_code == 200:
50
- data = response.json()
51
- return data
52
- else:
53
- logger.error(f"Erreur API: {response.status_code} - {response.text[:200]}")
54
 
55
- except Exception as e:
56
- logger.error(f"Erreur lors de l'appel API: {str(e)}")
57
-
58
- # Si toutes les tentatives échouent, retourner un dictionnaire vide
59
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- # Données statiques pour les cas où l'API ne fonctionne pas
62
- # Ces données sont fictives mais permettent à l'application de fonctionner
63
- STATIC_PRODUCTS = {
64
- "Pommes": 163,
65
- "Oranges": 191,
66
- "Blé": 254,
67
- "Tomates": 401,
68
- "Fraises": 113,
69
- "Riz": 510,
70
- "Raisins": 305,
71
- "Maïs": 240,
72
- "Laitue": 355,
73
- "Carottes": 228
74
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- STATIC_SUBSTANCES = {
77
- 1: "Glyphosate",
78
- 2: "Chlorpyrifos",
79
- 3: "Imidaclopride",
80
- 4: "Atrazine",
81
- 5: "Trifluraline",
82
- 6: "Malathion",
83
- 7: "Diazinon",
84
- 8: "Carbaryl",
85
- 9: "Paraquat",
86
- 10: "Métolachlore"
87
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- # Fonction pour récupérer les produits
90
- @st.cache_data(ttl=3600)
91
- def get_products():
92
- """Récupération des produits avec fallback sur données statiques"""
93
- base_url = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
94
- url = f"{base_url}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
95
-
96
- # Récupérer les données
97
- data = get_api_data(url)
98
-
99
- # Si la réponse contient des données
100
- if data and "value" in data:
101
- products = data["value"]
102
- # Créer un dictionnaire pour le sélecteur
103
- product_dict = {}
104
- for p in products:
105
- # Vérifier les noms de champs
106
- product_id = p.get("productId", None)
107
- product_name = p.get("productName", "")
108
-
109
- # Si les champs n'existent pas, essayer d'autres noms possibles
110
- if product_id is None:
111
- product_id = p.get("product_id", None)
112
- if not product_name:
113
- product_name = p.get("product_name", "Produit sans nom")
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
- # Ajouter au dictionnaire seulement si ID valide
116
- if product_id is not None:
117
- product_dict[product_name] = product_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- logger.info(f"Nombre de produits récupérés de l'API: {len(product_dict)}")
 
 
 
 
 
120
 
121
- if product_dict: # Si des produits ont été trouvés
122
- return product_dict
123
-
124
- # Si on arrive ici, utiliser les données statiques
125
- logger.warning("Utilisation des données statiques pour les produits")
126
- return STATIC_PRODUCTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
- # Fonction pour récupérer les substances
129
- @st.cache_data(ttl=3600)
130
- def get_substances():
131
- """Récupération des substances avec fallback sur données statiques"""
132
- base_url = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
133
- url = f"{base_url}/active_substances?format=json&api-version=v2.0"
134
-
135
- # Récupérer les données
136
- data = get_api_data(url)
137
-
138
- # Si la réponse contient des données
139
- if data and "value" in data:
140
- substances = data["value"]
141
- # Créer un dictionnaire pour le mapping
142
- substance_dict = {}
143
- for s in substances:
144
- # Vérifier les noms de champs
145
- substance_id = s.get("substanceId", None)
146
- substance_name = s.get("substanceName", "")
147
-
148
- # Si les champs n'existent pas, essayer d'autres noms possibles
149
- if substance_id is None:
150
- substance_id = s.get("substance_id", None)
151
- if not substance_name:
152
- substance_name = s.get("substance_name", "Substance sans nom")
153
 
154
- # Ajouter au dictionnaire seulement si ID valide
155
- if substance_id is not None:
156
- substance_dict[substance_id] = substance_name
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- logger.info(f"Nombre de substances récupérées de l'API: {len(substance_dict)}")
 
 
 
159
 
160
- if substance_dict: # Si des substances ont été trouvées
161
- return substance_dict
162
-
163
- # Si on arrive ici, utiliser les données statiques
164
- logger.warning("Utilisation des données statiques pour les substances")
165
- return STATIC_SUBSTANCES
166
-
167
- # Fonction pour récupérer les LMR
168
- def get_mrls(product_ids):
169
- """Récupération des LMR pour les produits donnés"""
170
- # Vérifier si product_ids est une liste
171
- if not isinstance(product_ids, list):
172
- product_ids = [product_ids]
173
 
174
- # Si la liste est vide, retourner une liste vide
175
- if not product_ids:
176
- return []
177
-
178
- all_mrls = []
179
- substances = get_substances()
180
-
181
- # En cas d'erreur d'API, générer des données statiques pour chaque produit
182
- for product_id in product_ids:
183
- # Générer des LMR aléatoires pour chaque substance
184
- for substance_id, substance_name in substances.items():
185
- # Générer des valeurs aléatoires mais réalistes
186
- import random
187
-
188
- # Valeur LMR entre 0.01 et 5.0
189
- mrl_value = round(random.uniform(0.01, 5.0), 3)
190
-
191
- # Date d'application entre 1 an dans le passé et 1 an dans le futur
192
- days_offset = random.randint(-365, 365)
193
- application_date = datetime.now() + timedelta(days=days_offset)
194
-
195
- all_mrls.append({
196
- "product_id": product_id,
197
- "substance_id": substance_id,
198
- "substance_name": substance_name,
199
- "mrl_value": mrl_value,
200
- "application_date": application_date,
201
- "regulation_number": f"EU {random.randint(2020, 2025)}/{random.randint(100, 999)}"
202
- })
203
-
204
- return all_mrls
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
- # Fonction principale
207
  def main():
208
  try:
209
- st.title("🌿 Base de données des pesticides de l'UE")
210
-
211
  # Ajout d'un bouton pour réinitialiser le cache
212
  if st.sidebar.button("🔄 Réinitialiser le cache"):
213
  st.cache_data.clear()
214
- st.rerun() # Utilisation de st.rerun() au lieu de st.experimental_rerun()
215
 
216
  st.sidebar.image("https://ec.europa.eu/info/sites/default/files/european-commission.logo_.png", width=200)
217
  st.sidebar.title("🌿 Pesticide Data Explorer")
 
 
 
 
 
 
218
 
219
- # Informations sur l'application
220
  with st.sidebar.expander("ℹ️ À propos"):
221
  st.markdown("""
222
- Cette application permet d'explorer les données de pesticides
223
- de l'Union Européenne, notamment les limites maximales de résidus (LMR).
224
 
225
- **Source des données:** Données simulées basées sur l'API de la Commission Européenne
 
 
 
226
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
  # Afficher l'état de l'API
229
  with st.sidebar.expander("📊 État de l'API"):
 
 
 
 
230
  try:
231
  url = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
232
- response = requests.get(url, timeout=5)
233
- st.markdown(f"**Statut API:** {response.status_code}")
234
  if response.status_code == 200:
235
- st.success("✅ Connexion API réussie")
236
  else:
237
- st.error(f"❌ Erreur API ({response.status_code})")
238
- st.info("⚠️ L'application utilise des données simulées")
239
  except Exception as e:
240
- st.error(f"❌ Impossible de se connecter à l'API: {str(e)[:100]}")
241
- st.info("⚠️ L'application utilise des données simulées")
242
-
243
- # Récupérer les produits
244
- with st.spinner("Chargement des produits..."):
245
- products = get_products()
246
-
247
- # Interface utilisateur
248
- col1, col2 = st.columns([3, 1])
249
- with col1:
250
- selected_products = st.multiselect(
251
- "Sélectionnez un ou plusieurs produits",
252
- list(products.keys())
253
- )
254
- with col2:
255
- future_only = st.checkbox("Uniquement les 6 prochains mois", value=False)
256
-
257
- if st.button("Afficher les données"):
258
- if not selected_products:
259
- st.warning("Veuillez sélectionner au moins un produit.")
260
- else:
261
- with st.spinner("Récupération des données..."):
262
- # Récupérer les IDs des produits sélectionnés
263
- selected_ids = [products[p] for p in selected_products]
264
-
265
- # Récupérer les LMR pour ces produits
266
- all_mrls = get_mrls(selected_ids)
267
-
268
- # Créer un DataFrame
269
- df = pd.DataFrame(all_mrls)
270
-
271
- # Filtrer pour les 6 prochains mois si demandé
272
- if future_only:
273
- now = datetime.now()
274
- future_date = now + timedelta(days=180)
275
- df = df[(df['application_date'] > now) & (df['application_date'] <= future_date)]
276
-
277
- if df.empty:
278
- st.info(f"🔍 Aucun changement de LMR prévu dans les 6 prochains mois pour les produits sélectionnés.")
279
- return
280
-
281
- # Renommer les colonnes pour l'affichage
282
- df = df.rename(columns={
283
- "substance_name": "Substance",
284
- "mrl_value": "Valeur LMR",
285
- "application_date": "Date d'application",
286
- "regulation_number": "Règlement"
287
- })
288
-
289
- # Trier par date d'application
290
- df = df.sort_values("Date d'application", ascending=False)
291
-
292
- # Afficher les informations
293
- nb_entries = len(df)
294
- st.success(f"Données récupérées pour {len(selected_products)} produit(s)")
295
- st.info(f"📊 {nb_entries} entrée{'s' if nb_entries > 1 else ''} trouvée{'s' if nb_entries > 1 else ''}.")
296
-
297
- # Formater le DataFrame pour l'affichage
298
- df_display = df.copy()
299
- df_display["Date d'application"] = df_display["Date d'application"].dt.strftime('%d/%m/%Y')
300
-
301
- # Afficher le tableau
302
- st.dataframe(df_display[["Substance", "Valeur LMR", "Date d'application", "Règlement"]], use_container_width=True)
303
-
304
- # Bouton de téléchargement
305
- st.download_button(
306
- "Télécharger les données (CSV)",
307
- df_display.to_csv(index=False, sep=';').encode('utf-8-sig'),
308
- "pesticide_data.csv",
309
- "text/csv",
310
- key='download-csv'
311
- )
312
-
313
- # Créer des visualisations
314
- st.subheader("Visualisation des LMR")
315
- fig = px.scatter(
316
- df,
317
- x="Date d'application",
318
- y="Valeur LMR",
319
- color="Substance",
320
- title="Évolution des LMR dans le temps",
321
- labels={
322
- "Date d'application": "Date d'entrée en vigueur",
323
- "Valeur LMR": "Limite Maximale de Résidus (mg/kg)"
324
- }
325
- )
326
- st.plotly_chart(fig, use_container_width=True)
327
-
328
  except Exception as e:
329
- logger.error(f"Erreur dans l'application: {str(e)}", exc_info=True)
330
  st.error(f"Une erreur est survenue: {str(e)}")
 
331
 
332
- # Afficher des détails techniques pour le débogage
333
- with st.expander("Détails techniques"):
334
  import traceback
335
  st.code(traceback.format_exc())
336
 
 
1
  import logging
2
  from datetime import datetime, timedelta
3
+ from typing import Dict, List, Optional, Any
4
  import pandas as pd
5
  import requests
 
6
  import plotly.express as px
7
+ import streamlit as st
8
+ from tenacity import retry, stop_after_attempt, wait_exponential
9
+ from concurrent.futures import ThreadPoolExecutor
10
  import time
 
11
 
12
  # Configuration Streamlit
13
  st.set_page_config(page_title="Pesticide Data Explorer", page_icon="🌿", layout="wide")
 
20
  )
21
  logger = logging.getLogger(__name__)
22
 
23
+ class PesticideDataFetcher:
24
+ """Classe pour gérer la récupération des données sur les pesticides."""
25
+ BASE_URL = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
26
+ HEADERS = {
27
+ 'Content-Type': 'application/json',
28
+ 'Cache-Control': 'no-cache',
29
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
30
+ }
31
+
32
+ def __init__(self):
33
+ self.session = requests.Session()
34
+ self.session.headers.update(self.HEADERS)
35
+ self._substance_cache = {}
36
+ self._product_cache = {}
37
+
38
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
39
+ def fetch_data(self, url: str, params: Optional[Dict] = None) -> Dict[str, Any]:
40
+ """Effectue une requête GET avec gestion des erreurs améliorée"""
41
  try:
42
+ logger.info(f"Fetching URL: {url}")
43
+ if params:
44
+ logger.info(f"Parameters: {params}")
45
 
46
+ response = self.session.get(url, params=params, timeout=15)
47
+
48
+ # Log du statut HTTP
49
+ logger.info(f"Status code: {response.status_code}")
50
+
51
+ # En cas d'erreur, retourner un dictionnaire avec l'erreur
52
+ if response.status_code >= 400:
53
+ logger.error(f"Erreur HTTP {response.status_code}: {response.text[:200]}...")
54
+ return {"error": f"Erreur HTTP {response.status_code}", "status_code": response.status_code}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ response.raise_for_status()
57
+ data = response.json()
58
+
59
+ # Log d'un aperçu des données pour débogage
60
+ if "value" in data:
61
+ logger.info(f"Données récupérées: {len(data['value'])} éléments")
62
+ if data["value"]:
63
+ logger.info(f"Premier élément: {str(data['value'][0])[:200]}...")
64
+
65
+ return data
66
+ except requests.RequestException as e:
67
+ logger.error(f"Erreur API: {e}")
68
+ if hasattr(e, 'response') and hasattr(e.response, 'text'):
69
+ logger.error(f"Détails de l'erreur: {e.response.text[:500]}")
70
+ return {"error": str(e)}
71
+ except ValueError as e:
72
+ # Erreur de parsing JSON
73
+ logger.error(f"Erreur de parsing JSON: {e}")
74
+ return {"error": f"Erreur de parsing JSON: {e}"}
75
 
76
+ def get_products(self) -> List[Dict]:
77
+ """Récupère la liste complète des produits avec pagination."""
78
+ if self._product_cache:
79
+ return self._product_cache
80
+
81
+ all_products = []
82
+ url = f"{self.BASE_URL}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
83
+
84
+ # Tester chaque combinaison possible pour le paramètre format
85
+ formats_to_try = ["json", "application/json"]
86
+
87
+ for fmt in formats_to_try:
88
+ test_url = f"{self.BASE_URL}/pesticide_residues_products?format={fmt}&language=FR&api-version=v2.0"
89
+ response = self.fetch_data(test_url)
90
+
91
+ # Si l'API répond avec succès
92
+ if "error" not in response and "value" in response:
93
+ url = test_url
94
+ break
95
+
96
+ # Si on ne peut pas accéder à l'API du tout
97
+ test_response = self.fetch_data(url)
98
+ if "error" in test_response:
99
+ logger.error(f"Impossible d'accéder à l'API des produits: {test_response['error']}")
100
+ return []
101
+
102
+ # Si on arrive ici, on a un URL fonctionnel
103
+ while url:
104
+ response = self.fetch_data(url)
105
+
106
+ # Vérifier s'il y a une erreur
107
+ if "error" in response:
108
+ logger.error(f"Erreur lors de la récupération des produits: {response['error']}")
109
+ break
110
+
111
+ if "value" not in response:
112
+ logger.error(f"Format de réponse inattendu: {response}")
113
+ break
114
+
115
+ all_products.extend(response["value"])
116
+
117
+ # Gérer la pagination (vérifier toutes les variantes possibles)
118
+ next_link = None
119
+ for key in ["@odata.nextLink", "nextLink", "@data.nextLink", "next"]:
120
+ if key in response:
121
+ next_link = response[key]
122
+ break
123
+
124
+ if next_link:
125
+ url = next_link
126
+ else:
127
+ break
128
+
129
+ # Mise en cache des résultats
130
+ self._product_cache = all_products
131
+ logger.info(f"Récupéré {len(all_products)} produits au total")
132
+ return all_products
133
 
134
+ def get_all_substances(self) -> Dict[int, str]:
135
+ """Récupère toutes les substances actives avec pagination."""
136
+ if self._substance_cache:
137
+ return self._substance_cache
138
+
139
+ substances_dict = {}
140
+ url = f"{self.BASE_URL}/active_substances?format=json&api-version=v2.0"
141
+
142
+ # Tester chaque combinaison possible pour le paramètre format
143
+ formats_to_try = ["json", "application/json"]
144
+
145
+ for fmt in formats_to_try:
146
+ test_url = f"{self.BASE_URL}/active_substances?format={fmt}&api-version=v2.0"
147
+ response = self.fetch_data(test_url)
148
+
149
+ # Si l'API répond avec succès
150
+ if "error" not in response and "value" in response:
151
+ url = test_url
152
+ break
153
+
154
+ # Si on ne peut pas accéder à l'API du tout
155
+ test_response = self.fetch_data(url)
156
+ if "error" in test_response:
157
+ logger.error(f"Impossible d'accéder à l'API des substances: {test_response['error']}")
158
+ return {}
159
+
160
+ # Si on arrive ici, on a un URL fonctionnel
161
+ while url:
162
+ response = self.fetch_data(url)
163
+
164
+ # Vérifier s'il y a une erreur
165
+ if "error" in response:
166
+ logger.error(f"Erreur lors de la récupération des substances: {response['error']}")
167
+ break
168
+
169
+ if "value" not in response:
170
+ logger.error(f"Format de réponse inattendu: {response}")
171
+ break
172
+
173
+ for item in response["value"]:
174
+ # Essayer tous les noms de champs possibles
175
+ substance_id = None
176
+ substance_name = None
177
+
178
+ for id_field in ["substanceId", "substance_id", "id"]:
179
+ if id_field in item:
180
+ substance_id = item[id_field]
181
+ break
182
+
183
+ for name_field in ["substanceName", "substance_name", "name"]:
184
+ if name_field in item:
185
+ substance_name = item[name_field]
186
+ break
187
+
188
+ if substance_id is not None and substance_name:
189
+ substances_dict[substance_id] = substance_name
190
+
191
+ # Gérer la pagination (vérifier toutes les variantes possibles)
192
+ next_link = None
193
+ for key in ["@odata.nextLink", "nextLink", "@data.nextLink", "next"]:
194
+ if key in response:
195
+ next_link = response[key]
196
+ break
197
+
198
+ if next_link:
199
+ url = next_link
200
+ else:
201
+ break
202
+
203
+ # Mise en cache des résultats
204
+ self._substance_cache = substances_dict
205
+ logger.info(f"Récupéré {len(substances_dict)} substances au total")
206
+ return substances_dict
207
 
208
+ def get_mrls(self, product_ids: List[int]) -> List[Dict]:
209
+ """Récupère les LMR pour une liste de produits."""
210
+ if not product_ids:
211
+ logger.warning("Aucun ID de produit fourni pour la récupération des LMR")
212
+ return []
213
+
214
+ all_mrls = []
215
+
216
+ # Tester différentes variantes d'URL
217
+ urls_to_try = [
218
+ f"{self.BASE_URL}/pesticide_residues_mrls?format=json&product_id={{product_id}}&api-version=v2.0",
219
+ f"{self.BASE_URL}/pesticide_residues_products/{{product_id}}/mrls?format=json&api-version=v2.0",
220
+ f"{self.BASE_URL}/pesticide_residues_products/{{product_id}}/mrls?format=json&language=FR&api-version=v2.0"
221
+ ]
222
+
223
+ # Tester quelle URL fonctionne avec le premier produit
224
+ working_url_template = None
225
+
226
+ for url_template in urls_to_try:
227
+ test_url = url_template.format(product_id=product_ids[0])
228
+ response = self.fetch_data(test_url)
229
+
230
+ if "error" not in response and "value" in response:
231
+ working_url_template = url_template
232
+ logger.info(f"URL de LMR fonctionnelle trouvée: {working_url_template}")
233
+ break
234
+
235
+ if not working_url_template:
236
+ logger.error("Aucune URL fonctionnelle trouvée pour récupérer les LMR")
237
+ return []
238
+
239
+ # Utiliser ThreadPoolExecutor pour les requêtes parallèles
240
+ with ThreadPoolExecutor(max_workers=4) as executor:
241
+ futures = []
242
+ for product_id in product_ids:
243
+ url = working_url_template.format(product_id=product_id)
244
+ futures.append(executor.submit(self.fetch_data, url))
245
 
246
+ for future in futures:
247
+ response = future.result()
248
+ if "error" in response:
249
+ logger.error(f"Erreur lors de la récupération des LMR: {response['error']}")
250
+ continue
251
+
252
+ if "value" in response:
253
+ all_mrls.extend(response["value"])
254
+
255
+ logger.info(f"Récupéré {len(all_mrls)} LMR au total")
256
+ return all_mrls
257
+
258
+ class PesticideInterface:
259
+ def __init__(self):
260
+ logger.info("Initialisation de l'application...")
261
+ self.fetcher = PesticideDataFetcher()
262
+
263
+ with st.spinner("Chargement des données de produits et substances..."):
264
+ # Récupération des produits
265
+ logger.info("Récupération de la liste des produits...")
266
+ self.products = self.fetcher.get_products()
267
+ self.product_choices = {}
268
+
269
+ if self.products:
270
+ for p in self.products:
271
+ # Essayer tous les noms de champs possibles pour le nom et l'ID
272
+ product_name = None
273
+ product_id = None
274
+
275
+ for name_field in ["productName", "product_name", "name"]:
276
+ if name_field in p:
277
+ product_name = p[name_field]
278
+ break
279
+
280
+ for id_field in ["productId", "product_id", "id"]:
281
+ if id_field in p:
282
+ product_id = p[id_field]
283
+ break
284
+
285
+ if product_name and product_id is not None:
286
+ self.product_choices[product_name] = product_id
287
+
288
+ logger.info(f"Produits récupérés: {len(self.product_choices)}")
289
+ else:
290
+ logger.error("Aucun produit récupéré. Vérifiez la connexion à l'API.")
291
+
292
+ # Récupération des substances
293
+ logger.info("Récupération des substances actives...")
294
+ self.substances = self.fetcher.get_all_substances()
295
+ logger.info(f"Substances récupérées: {len(self.substances)}")
296
+
297
+ logger.info(f"Application initialisée avec {len(self.product_choices)} produits et {len(self.substances)} substances")
298
+
299
+ def get_product_details(self, product_names: List[str], future_only: bool = False) -> pd.DataFrame:
300
+ """Récupère les détails des produits sélectionnés."""
301
+ if not product_names:
302
+ return pd.DataFrame()
303
+
304
+ product_ids = [self.product_choices[name] for name in product_names if name in self.product_choices]
305
+ if not product_ids:
306
+ st.warning("Aucun produit valide sélectionné.")
307
+ return pd.DataFrame()
308
+
309
+ all_mrls = self.fetcher.get_mrls(product_ids)
310
+
311
+ if not all_mrls:
312
+ st.info("Aucune donnée trouvée pour les produits sélectionnés.")
313
+ return pd.DataFrame()
314
+
315
+ # Création du DataFrame
316
+ df = pd.DataFrame(all_mrls)
317
+ logger.info(f"Nombre total d'entrées: {len(df)}")
318
+
319
+ # Log des colonnes disponibles pour débogage
320
+ logger.info(f"Colonnes disponibles: {df.columns.tolist()}")
321
+
322
+ # Enrichir avec les noms de substances
323
+ substance_id_field = None
324
+ for field in ["pesticideResidueId", "pesticide_residue_id", "substance_id"]:
325
+ if field in df.columns:
326
+ substance_id_field = field
327
+ break
328
 
329
+ if substance_id_field:
330
+ df["Substance"] = df[substance_id_field].map(self.substances)
331
+ df["Substance"] = df["Substance"].fillna("Substance inconnue")
332
+ else:
333
+ df["Substance"] = "Substance inconnue"
334
+ logger.warning("Champ d'ID de substance non trouvé dans les données")
335
 
336
+ # Conversion des dates
337
+ date_field = None
338
+ for field in ["entryIntoForceDate", "entry_into_force_date", "applicationDate"]:
339
+ if field in df.columns:
340
+ date_field = field
341
+ break
342
+
343
+ if date_field:
344
+ df["Date d'application"] = pd.to_datetime(df[date_field], errors="coerce")
345
+ else:
346
+ df["Date d'application"] = pd.NaT
347
+ logger.warning("Champ de date non trouvé dans les données")
348
+
349
+ # Filtrer pour les changements futurs si demandé
350
+ if future_only and "Date d'application" in df.columns:
351
+ now = datetime.now()
352
+ future_date = now + timedelta(days=180)
353
+ future_df = df[
354
+ (df["Date d'application"] > now) &
355
+ (df["Date d'application"] <= future_date)
356
+ ]
357
 
358
+ if future_df.empty:
359
+ st.info(f"🔍 Aucun changement de LMR prévu entre le {now.strftime('%d/%m/%Y')} et le {future_date.strftime('%d/%m/%Y')} pour les produits sélectionnés.")
360
+ return pd.DataFrame()
361
+
362
+ df = future_df
363
+
364
+ # Renommer et formater les colonnes
365
+ mrl_field = None
366
+ for field in ["mrlValue", "mrl_value", "mrl"]:
367
+ if field in df.columns:
368
+ mrl_field = field
369
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
+ if mrl_field:
372
+ df = df.rename(columns={mrl_field: "Valeur LMR"})
373
+ df["Valeur LMR"] = pd.to_numeric(df["Valeur LMR"], errors='coerce')
374
+ else:
375
+ logger.warning("Champ de valeur LMR non trouvé dans les données")
376
+
377
+ # Ajouter la colonne Règlement si possible
378
+ regulation_number_field = None
379
+ regulation_url_field = None
380
+
381
+ for number_field in ["regulationNumber", "regulation_number", "regulation"]:
382
+ if number_field in df.columns:
383
+ regulation_number_field = number_field
384
+ break
385
 
386
+ for url_field in ["regulationUrl", "regulation_url", "url"]:
387
+ if url_field in df.columns:
388
+ regulation_url_field = url_field
389
+ break
390
 
391
+ if regulation_number_field:
392
+ if regulation_url_field:
393
+ df["Règlement"] = df.apply(
394
+ lambda x: f'<a href="{x[regulation_url_field]}" target="_blank">{x[regulation_number_field]}</a>'
395
+ if pd.notna(x.get(regulation_url_field)) else x.get(regulation_number_field, ""),
396
+ axis=1
397
+ )
398
+ else:
399
+ df["Règlement"] = df[regulation_number_field]
400
+
401
+ # Sélection des colonnes finales
402
+ columns = []
 
403
 
404
+ # Ajouter les colonnes si elles existent
405
+ if "Substance" in df.columns:
406
+ columns.append("Substance")
407
+ if "Valeur LMR" in df.columns:
408
+ columns.append("Valeur LMR")
409
+ if "Date d'application" in df.columns:
410
+ columns.append("Date d'application")
411
+ if "Règlement" in df.columns:
412
+ columns.append("Règlement")
413
+
414
+ # Si aucune colonne n'est disponible, utiliser toutes les colonnes
415
+ if not columns:
416
+ columns = df.columns.tolist()
417
+
418
+ df = df[columns]
419
+
420
+ # Trier par date si disponible
421
+ if "Date d'application" in df.columns:
422
+ df = df.sort_values("Date d'application", ascending=False)
423
+
424
+ return df
425
+
426
+ def create_interface(self):
427
+ st.title("🌿 Base de données des pesticides de l'UE")
428
+
429
+ if not self.product_choices:
430
+ st.error("⚠️ Aucun produit n'a pu être récupéré depuis l'API. Vérifiez votre connexion ou les logs pour plus d'informations.")
431
+ st.info("Essayez de cliquer sur le bouton 'Réinitialiser le cache' dans la barre latérale puis rechargez la page.")
432
+ return
433
+
434
+ col1, col2 = st.columns([3, 1])
435
+ with col1:
436
+ product_names = st.multiselect(
437
+ "Sélectionnez un ou plusieurs produits",
438
+ list(self.product_choices.keys())
439
+ )
440
+ with col2:
441
+ future_only = st.checkbox("Uniquement les 6 prochains mois", value=False)
442
+
443
+ if st.button("Afficher les données"):
444
+ if not product_names:
445
+ st.warning("Veuillez sélectionner au moins un produit.")
446
+ return
447
+
448
+ with st.spinner("Récupération des données en cours..."):
449
+ df = self.get_product_details(product_names, future_only)
450
+
451
+ if df.empty:
452
+ return # Le message d'info a déjà été affiché dans get_product_details
453
+ else:
454
+ if future_only:
455
+ st.markdown("### Changements de LMR prévus dans les 6 prochains mois")
456
+ else:
457
+ st.markdown("### Tableau des LMR")
458
+
459
+ # Formatage du DataFrame pour l'affichage
460
+ df_display = df.copy()
461
+ if "Date d'application" in df_display.columns:
462
+ df_display["Date d'application"] = df_display["Date d'application"].dt.strftime('%d/%m/%Y')
463
+
464
+ # Ajouter un résumé des changements
465
+ nb_changes = len(df)
466
+ st.info(f"📊 {nb_changes} entrée{'s' if nb_changes > 1 else ''} trouvée{'s' if nb_changes > 1 else ''}.")
467
+
468
+ # Afficher le tableau
469
+ st.dataframe(df_display, use_container_width=True)
470
+
471
+ # Option pour télécharger les données
472
+ st.download_button(
473
+ "Télécharger les données (CSV)",
474
+ df_display.to_csv(index=False, sep=';').encode('utf-8-sig'),
475
+ "pesticide_data.csv",
476
+ "text/csv",
477
+ key='download-csv'
478
+ )
479
+
480
+ # Créer des visualisations si les données requises sont présentes
481
+ if "Valeur LMR" in df.columns and "Date d'application" in df.columns:
482
+ self.create_visualizations(df)
483
+ else:
484
+ st.warning("Données insuffisantes pour créer des visualisations")
485
+
486
+ def create_visualizations(self, df: pd.DataFrame):
487
+ """Crée les visualisations des données de LMR"""
488
+ # Vérifier que les données ne sont pas vides
489
+ if df.empty:
490
+ return
491
+
492
+ # Graphique d'évolution des LMR dans le temps
493
+ fig1 = px.scatter(
494
+ df,
495
+ x="Date d'application",
496
+ y="Valeur LMR",
497
+ color="Substance",
498
+ title="Évolution des LMR dans le temps",
499
+ hover_data=["Substance"],
500
+ size_max=15,
501
+ labels={
502
+ "Date d'application": "Date d'entrée en vigueur",
503
+ "Valeur LMR": "Limite Maximale de Résidus (mg/kg)"
504
+ }
505
+ )
506
+ st.plotly_chart(fig1, use_container_width=True)
507
 
 
508
  def main():
509
  try:
 
 
510
  # Ajout d'un bouton pour réinitialiser le cache
511
  if st.sidebar.button("🔄 Réinitialiser le cache"):
512
  st.cache_data.clear()
513
+ st.rerun()
514
 
515
  st.sidebar.image("https://ec.europa.eu/info/sites/default/files/european-commission.logo_.png", width=200)
516
  st.sidebar.title("🌿 Pesticide Data Explorer")
517
+ st.sidebar.markdown("""
518
+ Cette application permet d'explorer les données de pesticides
519
+ de l'Union Européenne, notamment les limites maximales de résidus (LMR).
520
+
521
+ **Source des données:** API officielle de la Commission Européenne
522
+ """)
523
 
524
+ # Ajout d'informations et d'aide dans la barre latérale
525
  with st.sidebar.expander("ℹ️ À propos"):
526
  st.markdown("""
527
+ Cette application récupère et visualise les données de pesticides depuis l'API officielle
528
+ de la Commission Européenne. Elle permet de:
529
 
530
+ * Rechercher des informations sur les LMR par produit
531
+ * Visualiser l'évolution des LMR dans le temps
532
+
533
+ Les données sont mises en cache pendant 1 heure pour améliorer les performances.
534
  """)
535
+
536
+ with st.sidebar.expander("❓ Aide"):
537
+ st.markdown("""
538
+ **Comment utiliser cette application:**
539
+
540
+ 1. Sélectionnez un ou plusieurs produits dans la liste déroulante
541
+ 2. Cochez "Uniquement les 6 prochains mois" si vous souhaitez voir seulement les changements à venir
542
+ 3. Cliquez sur "Afficher les données" pour visualiser les résultats
543
+
544
+ Vous pouvez télécharger les résultats au format CSV pour une analyse plus approfondie.
545
+
546
+ En cas de problème, utilisez le bouton "Réinitialiser le cache" en haut de cette barre latérale.
547
+ """)
548
+
549
+ # Ajouter contact et crédits
550
+ st.sidebar.markdown("---")
551
+ st.sidebar.markdown("Développé avec ❤️ pour l'UE")
552
 
553
  # Afficher l'état de l'API
554
  with st.sidebar.expander("📊 État de l'API"):
555
+ st.markdown("### Tests de connexion API")
556
+ col1, col2 = st.columns(2)
557
+
558
+ # Test de connexion pour l'API des produits
559
  try:
560
  url = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
561
+ fetcher = PesticideDataFetcher()
562
+ response = fetcher.session.get(url, timeout=5)
563
  if response.status_code == 200:
564
+ col1.success("✅ API Produits")
565
  else:
566
+ col1.error(f"❌ API Produits ({response.status_code})")
567
+ col1.info("Essayez de consulter les logs pour plus d'informations")
568
  except Exception as e:
569
+ col1.error(f"❌ API Produits: {str(e)[:50]}...")
570
+
571
+ # Test de connexion pour l'API des substances
572
+ try:
573
+ url = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides/active_substances?format=json&api-version=v2.0"
574
+ fetcher = PesticideDataFetcher()
575
+ response = fetcher.session.get(url, timeout=5)
576
+ if response.status_code == 200:
577
+ col2.success("✅ API Substances")
578
+ else:
579
+ col2.error(f"❌ API Substances ({response.status_code})")
580
+ col2.info("Essayez de consulter les logs pour plus d'informations")
581
+ except Exception as e:
582
+ col2.error(f"❌ API Substances: {str(e)[:50]}...")
583
+
584
+ # Afficher les logs dans la barre latérale pour faciliter le débogage
585
+ with st.sidebar.expander("📜 Logs récents"):
586
+ try:
587
+ with open("pesticide_app.log", "r") as f:
588
+ logs = f.readlines()
589
+ st.code("".join(logs[-20:])) # Afficher les 20 dernières lignes
590
+ except Exception as e:
591
+ st.warning(f"Impossible de lire les logs: {e}")
592
+
593
+ interface = PesticideInterface()
594
+ interface.create_interface()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  except Exception as e:
596
+ logger.error(f"Erreur lors de l'exécution de l'application: {e}", exc_info=True)
597
  st.error(f"Une erreur est survenue: {str(e)}")
598
+ st.info("Veuillez rafraîchir la page et réessayer. Si le problème persiste, contactez l'administrateur.")
599
 
600
+ # Fournir des informations de débogage supplémentaires
601
+ with st.expander("Détails de l'erreur"):
602
  import traceback
603
  st.code(traceback.format_exc())
604