MMOON commited on
Commit
70e46bf
·
verified ·
1 Parent(s): 39834d1

Create app1.py

Browse files
Files changed (1) hide show
  1. app1.py +426 -0
app1.py ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from datetime import datetime, timedelta
4
+ from typing import Dict, List, Optional
5
+ import gradio as gr
6
+ import pandas as pd
7
+ import requests
8
+ from dataclasses import dataclass
9
+ from tenacity import retry, stop_after_attempt, wait_fixed
10
+ import plotly.express as px
11
+
12
+ # Configuration du logging
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+ logger = logging.getLogger(__name__)
18
+
19
+ @dataclass
20
+ class PesticideRecord:
21
+ """Structure de données pour les enregistrements de pesticides."""
22
+ substance_name: str
23
+ mrl_value: float
24
+ entry_into_force_date: str
25
+ regulation_number: str
26
+ regulation_url: str
27
+ modification_date: Optional[str] = None
28
+ substance_status: Optional[str] = None
29
+ approval_date: Optional[str] = None
30
+ expiry_date: Optional[str] = None
31
+
32
+ class PesticideDataFetcher:
33
+ """Classe pour gérer la récupération des données sur les pesticides."""
34
+ BASE_URL = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
35
+ HEADERS = {
36
+ 'Content-Type': 'application/json',
37
+ 'Cache-Control': 'no-cache',
38
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
39
+ }
40
+
41
+ def __init__(self):
42
+ self.session = self._create_session()
43
+ self._substance_cache = {}
44
+ self._product_cache = {}
45
+
46
+ def _create_session(self):
47
+ """Crée une session pour les requêtes HTTP."""
48
+ session = requests.Session()
49
+ for header, value in self.HEADERS.items():
50
+ session.headers[header] = value
51
+ return session
52
+
53
+ @retry(stop=stop_after_attempt(3), wait=wait_fixed(2))
54
+ def fetch_data(self, url: str) -> Dict:
55
+ """Récupère les données depuis l'API avec gestion d'erreurs et retry."""
56
+ try:
57
+ response = self.session.get(url, timeout=10)
58
+ response.raise_for_status()
59
+ data = response.json()
60
+ logger.info(f"Fetched data from {url}: {str(data)[:200]}...")
61
+ return data
62
+ except requests.RequestException as e:
63
+ logger.error(f"Failed to fetch data from {url}: {str(e)}", exc_info=True)
64
+ return {"error": str(e)}
65
+ except Exception as e:
66
+ logger.error(f"Unexpected error fetching data from {url}: {str(e)}", exc_info=True)
67
+ return {"error": str(e)}
68
+
69
+ def get_active_substance_details(self, substance_name: str) -> Dict:
70
+ """Récupère les détails d'une substance active."""
71
+ if substance_name in self._substance_cache:
72
+ return self._substance_cache[substance_name]
73
+ url = f"{self.BASE_URL}/active_substances?format=json&substance_name={substance_name}&api-version=v2.0"
74
+ response = self.fetch_data(url)
75
+ if response and "value" in response and response["value"]:
76
+ substance_data = response["value"][0]
77
+ self._substance_cache[substance_name] = {
78
+ "status": substance_data.get("substance_status"),
79
+ "approval_date": substance_data.get("approval_date"),
80
+ "expiry_date": substance_data.get("expiry_date")
81
+ }
82
+ return self._substance_cache[substance_name]
83
+ return {}
84
+
85
+ def get_products(self) -> List[Dict]:
86
+ """Récupère la liste complète des produits avec pagination."""
87
+ if self._product_cache:
88
+ return self._product_cache
89
+ all_products = []
90
+ base_url = f"{self.BASE_URL}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
91
+ url = base_url
92
+ while url:
93
+ response = self.fetch_data(url)
94
+ if not response or "value" not in response:
95
+ break
96
+ all_products.extend(response["value"])
97
+ next_link = response.get("nextLink")
98
+ if next_link:
99
+ url = next_link
100
+ else:
101
+ break
102
+ self._product_cache = all_products
103
+ logger.info(f"Récupéré {len(all_products)} produits au total")
104
+ return all_products
105
+
106
+ def get_mrls(self, product_id: int) -> List[Dict]:
107
+ """Récupère les LMR pour un produit spécifique."""
108
+ url = f"{self.BASE_URL}/pesticide_residues_mrls?format=json&product_id={product_id}&api-version=v2.0"
109
+ response = self.fetch_data(url)
110
+ return response.get("value", [])
111
+
112
+ def get_substance_details(self, pesticide_residue_id: int) -> Dict:
113
+ """Récupère les détails d'une substance à partir de son ID."""
114
+ url = f"{self.BASE_URL}/pesticide_residues/{pesticide_residue_id}?format=json&api-version=v2.0"
115
+ response = self.fetch_data(url)
116
+ if not response or "value" not in response or not response["value"]:
117
+ logger.warning(f"Pas de détails trouvés pour la substance {pesticide_residue_id}")
118
+ return {"substance_name": f"Substance {pesticide_residue_id}"}
119
+ substance_data = response["value"][0]
120
+ substance_name = substance_data.get("substance_name")
121
+ if not substance_name:
122
+ logger.warning(f"Nom de substance non trouvé pour l'ID {pesticide_residue_id}")
123
+ return {"substance_name": f"Substance {pesticide_residue_id}"}
124
+ active_url = f"{self.BASE_URL}/active_substances?format=json&substance_name={substance_name}&api-version=v2.0"
125
+ active_response = self.fetch_data(active_url)
126
+ details = {
127
+ "substance_name": substance_name,
128
+ "status": None,
129
+ "approval_date": None,
130
+ "expiry_date": None
131
+ }
132
+ if active_response and "value" in active_response and active_response["value"]:
133
+ active_data = active_response["value"][0]
134
+ details.update({
135
+ "status": active_data.get("substance_status"),
136
+ "approval_date": active_data.get("approval_date"),
137
+ "expiry_date": active_data.get("expiry_date")
138
+ })
139
+ return details
140
+
141
+ def get_substance_name_by_id(self, substance_id: int) -> str:
142
+ """Récupère le nom de la substance à partir de son ID."""
143
+ url = f"{self.BASE_URL}/active_substances/{substance_id}?format=json&api-version=v2.0"
144
+ response = self.fetch_data(url)
145
+ if response and "value" in response and response["value"]:
146
+ substance_data = response["value"][0]
147
+ substance_name = substance_data.get("substance_name", f"Substance {substance_id}")
148
+ logger.info(f"Nom de la substance récupéré: {substance_name}")
149
+ return substance_name
150
+ logger.warning(f"Nom de la substance non trouvé pour l'ID {substance_id}")
151
+ return f"Substance {substance_id}"
152
+
153
+ def get_all_substances(self) -> List[str]:
154
+ """Récupère la liste complète des substances actives avec pagination."""
155
+ if self._substance_cache:
156
+ return list(self._substance_cache.keys())
157
+ all_substances = set()
158
+ base_url = f"{self.BASE_URL}/active_substances?format=json&api-version=v2.0"
159
+ url = base_url
160
+ while url:
161
+ response = self.fetch_data(url)
162
+ if not response or "value" not in response:
163
+ break
164
+ for item in response.get("value", []):
165
+ substance_name = item.get("substance_name")
166
+ if substance_name:
167
+ all_substances.add(substance_name)
168
+ self._substance_cache[substance_name] = {
169
+ "status": item.get("substance_status"),
170
+ "approval_date": item.get("approval_date"),
171
+ "expiry_date": item.get("expiry_date")
172
+ }
173
+ next_link = response.get("nextLink")
174
+ if next_link:
175
+ url = next_link
176
+ else:
177
+ break
178
+ logger.info(f"Récupéré {len(all_substances)} substances au total")
179
+ return sorted(all_substances)
180
+
181
+ class PesticideInterface:
182
+ """Classe pour gérer l'interface utilisateur Gradio."""
183
+ def __init__(self):
184
+ self.fetcher = PesticideDataFetcher()
185
+ self.products = self.fetcher.get_products()
186
+ self.product_choices = {
187
+ p['product_name']: p['product_id'] for p in self.products
188
+ }
189
+ self.substances = self.fetcher.get_all_substances()
190
+ self._cache = {}
191
+ logger.info(f"Initialized interface with {len(self.product_choices)} products and {len(self.substances)} substances.")
192
+
193
+ def parse_date(self, date_str: str) -> Optional[str]:
194
+ """Convertit une date au format 'YYYY-MM-DD'."""
195
+ if not date_str:
196
+ return None
197
+ for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%SZ"):
198
+ try:
199
+ return datetime.strptime(date_str, fmt).strftime("%Y-%m-%d")
200
+ except ValueError:
201
+ continue
202
+ logger.warning(f"Impossible de parser la date : {date_str}")
203
+ return None
204
+
205
+ def filter_by_period(self, data: List[Dict], period: str) -> List[Dict]:
206
+ """Filtre les données selon la période sélectionnée."""
207
+ if period == "Toutes les dates":
208
+ return data
209
+ today = datetime.now()
210
+ start_date = {
211
+ "Dernière semaine": today - timedelta(days=7),
212
+ "Dernier mois": today - timedelta(days=30),
213
+ "Prochains 6 mois": today + timedelta(days=180)
214
+ }.get(period)
215
+ if not start_date:
216
+ return data
217
+ filtered_data = []
218
+ for item in data:
219
+ date_str = item.get("entry_into_force_date") or item.get("modification_date")
220
+ if date_str:
221
+ parsed_date = self.parse_date(date_str)
222
+ if parsed_date:
223
+ item_date = datetime.strptime(parsed_date, "%Y-%m-%d")
224
+ if (period == "Prochains 6 mois" and item_date <= start_date) or (period != "Prochains 6 mois" and item_date >= start_date):
225
+ item["parsed_date"] = parsed_date
226
+ filtered_data.append(item)
227
+ logger.info(f"Filtered {len(data)} items to {len(filtered_data)} items for period {period}.")
228
+ return filtered_data
229
+
230
+ def format_regulation_link(self, regulation_url: str, regulation_number: str) -> str:
231
+ """Formate un lien de règlement en HTML cliquable."""
232
+ if not regulation_url:
233
+ return regulation_number
234
+ return f'<a href="{regulation_url}" target="_blank">{regulation_number}</a>'
235
+
236
+ def get_product_details(self, product_name: str, period: str, show_only_changes: bool) -> pd.DataFrame:
237
+ """Récupère et formate les détails des MRLs pour un produit donné."""
238
+ logger.info(f"Récupération des détails pour le produit: {product_name}")
239
+ try:
240
+ if not product_name:
241
+ return pd.DataFrame({"Message": ["Veuillez sélectionner un produit"]})
242
+ product_id = self.product_choices.get(product_name)
243
+ if not product_id:
244
+ return pd.DataFrame({"Message": ["Produit non trouvé"]})
245
+ cache_key = f"{product_id}_{period}_{show_only_changes}"
246
+ if cache_key in self._cache:
247
+ return self._cache[cache_key]
248
+ mrls = self.fetcher.get_mrls(product_id)
249
+ if period != "Toutes les dates":
250
+ mrls = self.filter_by_period(mrls, period)
251
+ if not mrls:
252
+ return pd.DataFrame({"Message": ["Aucune donnée trouvée pour la période sélectionnée"]})
253
+ processed_mrls = []
254
+ with ThreadPoolExecutor(max_workers=10) as executor:
255
+ futures = {
256
+ executor.submit(self.fetcher.get_substance_details, mrl["pesticide_residue_id"]): mrl
257
+ for mrl in mrls
258
+ }
259
+ for future in futures:
260
+ mrl = futures[future]
261
+ try:
262
+ substance_details = future.result()
263
+ logger.info(f"Détails de la substance récupérés: {substance_details}")
264
+ substance_name = substance_details.get("substance_name", "Nom non trouvé")
265
+ logger.info(f"Nom de la substance: {substance_name}")
266
+ # Formatage de la valeur LMR
267
+ mrl_value = mrl.get("mrl_value", "")
268
+ if isinstance(mrl_value, (int, float)):
269
+ formatted_mrl = f"{mrl_value}{'*' if str(mrl_value).endswith('*') else ''}"
270
+ else:
271
+ formatted_mrl = str(mrl_value)
272
+ mrl_data = {
273
+ "Substance": substance_name,
274
+ "Valeur LMR": formatted_mrl,
275
+ "Date d'application": self.parse_date(mrl.get("entry_into_force_date")),
276
+ "Date de modification": self.parse_date(mrl.get("modification_date")),
277
+ "Règlement": self.format_regulation_link(
278
+ mrl.get("regulation_url", ""),
279
+ mrl.get("regulation_number", "") or mrl.get("regulation_reference", "")
280
+ ),
281
+ "Statut": substance_details.get("status", ""),
282
+ "Date d'approbation": self.parse_date(substance_details.get("approval_date")),
283
+ "Date d'expiration": self.parse_date(substance_details.get("expiry_date"))
284
+ }
285
+ logger.info(f"Données MRL formatées: {mrl_data}")
286
+ processed_mrls.append(mrl_data)
287
+ except Exception as e:
288
+ logger.error(f"Erreur lors du traitement de la substance: {e}")
289
+ df = pd.DataFrame(processed_mrls)
290
+ if show_only_changes and "Date de modification" in df.columns:
291
+ df = df[df["Date de modification"].notna()]
292
+ df = df.sort_values("Date d'application", ascending=False)
293
+ columns_order = [
294
+ "Substance", "Valeur LMR", "Date d'application", "Date de modification",
295
+ "Règlement", "Statut", "Date d'approbation", "Date d'expiration"
296
+ ]
297
+ df = df[columns_order]
298
+ self._cache[cache_key] = df
299
+ return df
300
+ except Exception as e:
301
+ logger.error(f"Erreur dans get_product_details: {str(e)}")
302
+ return pd.DataFrame({"Message": [f"Erreur: {str(e)}"]})
303
+
304
+ def create_graph(self, df: pd.DataFrame) -> gr.Plot:
305
+ """Crée un graphique interactif pour les dates d'application."""
306
+ fig = px.scatter(df, x='Date d\'application', y='Valeur LMR', color='Substance', title='Dates d\'application des LMR')
307
+ return fig
308
+
309
+ def export_data(self, df: pd.DataFrame) -> str:
310
+ """Exporte les données sous forme de fichier CSV."""
311
+ csv_file_path = "mrls_data.csv"
312
+ df.to_csv(csv_file_path, index=False)
313
+ return csv_file_path
314
+
315
+ def get_substance_details_table(self, substance_name: str) -> pd.DataFrame:
316
+ """Récupère et formate les détails d'une substance active."""
317
+ logger.info(f"Récupération des détails pour la substance: {substance_name}")
318
+ try:
319
+ if not substance_name:
320
+ return pd.DataFrame({"Message": ["Veuillez sélectionner une substance"]})
321
+ substance_details = self.fetcher.get_active_substance_details(substance_name)
322
+ if not substance_details:
323
+ return pd.DataFrame({"Message": ["Substance non trouvée"]})
324
+ df = pd.DataFrame([substance_details])
325
+ df = df.rename(columns={
326
+ "status": "Statut",
327
+ "approval_date": "Date d'approbation",
328
+ "expiry_date": "Date d'expiration"
329
+ })
330
+ df.insert(0, "Substance", substance_name)
331
+ return df
332
+ except Exception as e:
333
+ logger.error(f"Erreur dans get_substance_details_table: {str(e)}")
334
+ return pd.DataFrame({"Message": [f"Erreur: {str(e)}"]})
335
+
336
+ def create_interface(self) -> gr.Blocks:
337
+ """Crée l'interface Gradio avec un design amélioré."""
338
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="green", secondary_hue="blue")) as interface:
339
+ gr.Markdown("""
340
+ # 🌿 Base de données des pesticides de l'UE
341
+ Consultez les Limites Maximales de Résidus (LMR) et les informations sur les substances actives.
342
+ """)
343
+ with gr.Row():
344
+ with gr.Column(scale=2):
345
+ product_dropdown = gr.Dropdown(
346
+ choices=sorted(list(self.product_choices.keys())),
347
+ label="Produit",
348
+ info="Sélectionnez un produit agricole"
349
+ )
350
+ with gr.Column(scale=1):
351
+ period_radio = gr.Radio(
352
+ choices=["Dernière semaine", "Dernier mois", "Prochains 6 mois", "Toutes les dates"],
353
+ value="Toutes les dates",
354
+ label="Période",
355
+ info="Filtrer par période"
356
+ )
357
+ show_changes = gr.Checkbox(
358
+ label="Afficher uniquement les modifications récentes",
359
+ info="Cochez pour voir uniquement les LMR qui ont été modifiées"
360
+ )
361
+ with gr.Row():
362
+ fetch_btn = gr.Button("📊 Analyser les données", variant="primary")
363
+ with gr.Row():
364
+ mrls_table = gr.Dataframe(
365
+ headers=["Substance", "Valeur LMR", "Date d'application",
366
+ "Date de modification", "Règlement", "Statut",
367
+ "Date d'approbation", "Date d'expiration"],
368
+ interactive=False
369
+ )
370
+ graph_output = gr.Plot(label="Graphique des Dates d'Application")
371
+ with gr.Row():
372
+ export_btn = gr.Button("Exporter les données", variant="secondary")
373
+ export_output = gr.File(label="Fichier CSV Exporté")
374
+ with gr.Row():
375
+ substance_dropdown = gr.Dropdown(
376
+ choices=sorted(self.substances),
377
+ label="Substance",
378
+ info="Sélectionnez une substance active"
379
+ )
380
+ substance_table = gr.Dataframe(
381
+ headers=["Substance", "Statut", "Date d'approbation", "Date d'expiration"],
382
+ interactive=False
383
+ )
384
+ fetch_btn.click(
385
+ fn=self.get_product_details,
386
+ inputs=[product_dropdown, period_radio, show_changes],
387
+ outputs=[mrls_table]
388
+ )
389
+ mrls_table.change(
390
+ fn=self.create_graph,
391
+ inputs=mrls_table,
392
+ outputs=graph_output
393
+ )
394
+ export_btn.click(
395
+ fn=self.export_data,
396
+ inputs=mrls_table,
397
+ outputs=export_output
398
+ )
399
+ substance_dropdown.change(
400
+ fn=self.get_substance_details_table,
401
+ inputs=substance_dropdown,
402
+ outputs=substance_table
403
+ )
404
+ gr.Markdown("""
405
+ ### Légende
406
+ Valeur LMR** : Limite Maximale de Résidus autorisée
407
+ Date d'application** : Date d'entrée en vigueur de la LMR
408
+ Date de modification** : Date de la dernière modification
409
+ Règlement** : Référence du règlement européen (cliquez pour accéder au texte)
410
+ Statut** : État d'approbation de la substance active
411
+ Date d'approbation** : Date d'approbation de la substance active
412
+ Date d'expiration** : Date d'expiration de l'approbation
413
+ """)
414
+ return interface
415
+
416
+ def main():
417
+ interface = PesticideInterface()
418
+ app = interface.create_interface()
419
+ app.launch(
420
+ server_name="0.0.0.0",
421
+ server_port=7860,
422
+ share=True
423
+ )
424
+
425
+ if __name__ == "__main__":
426
+ main()