MMOON commited on
Commit
4a7770b
·
verified ·
1 Parent(s): f6a5ec2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +181 -87
app.py CHANGED
@@ -5,11 +5,26 @@ import gradio as gr
5
  import pandas as pd
6
  from typing import Dict, List, Optional
7
  import logging
 
 
8
 
9
  # Configuration du logging
10
- logging.basicConfig(level=logging.INFO)
 
 
 
11
  logger = logging.getLogger(__name__)
12
 
 
 
 
 
 
 
 
 
 
 
13
  class PesticideDataFetcher:
14
  """Classe pour gérer la récupération des données sur les pesticides."""
15
 
@@ -20,38 +35,51 @@ class PesticideDataFetcher:
20
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
21
  }
22
 
23
- @staticmethod
24
- def fetch_data(url: str, output_file: str) -> Dict:
25
- """Récupère les données depuis l'API avec gestion d'erreurs."""
26
- try:
27
- command = ['curl'] + [
28
- item for header, value in PesticideDataFetcher.HEADERS.items()
29
- for item in ['-H', f'{header}: {value}']
30
- ] + [url, '-o', output_file]
31
-
32
- subprocess.run(command, check=True, capture_output=True)
33
- with open(output_file, 'r') as file:
34
- data = json.load(file)
 
 
 
 
 
 
 
35
  logger.info(f"Fetched data from {url}: {str(data)[:200]}...")
36
  return data
37
- except Exception as e:
38
- logger.error(f"Erreur lors de la récupération des données : {str(e)}")
39
- return {"error": str(e)}
 
 
 
40
 
41
- @classmethod
42
- def get_products(cls) -> List[Dict]:
43
  """Récupère la liste des produits."""
44
- url = f"{cls.BASE_URL}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
45
- response = cls.fetch_data(url, "products.json")
46
  return response.get("value", [])
47
 
48
- @classmethod
49
- def get_mrls(cls, product_id: int) -> List[Dict]:
50
  """Récupère les LMR pour un produit spécifique."""
51
- url = f"{cls.BASE_URL}/pesticide_residues_mrls?format=json&product_id={product_id}&api-version=v2.0"
52
- response = cls.fetch_data(url, "mrls.json")
53
  return response.get("value", [])
54
 
 
 
 
 
 
55
  class PesticideInterface:
56
  """Classe pour gérer l'interface utilisateur Gradio."""
57
 
@@ -61,11 +89,15 @@ class PesticideInterface:
61
  self.product_choices = {
62
  p['product_name']: p['product_id'] for p in self.products
63
  }
 
64
  logger.info(f"Initialized interface with {len(self.product_choices)} products.")
65
 
66
  def parse_date(self, date_str: str) -> Optional[str]:
67
- """Convertit une date au format 'YYYY-MM-DD', détecte automatiquement le format."""
68
- for fmt in ("%Y-%m-%d", "%d/%m/%Y"):
 
 
 
69
  try:
70
  return datetime.strptime(date_str, fmt).strftime("%Y-%m-%d")
71
  except ValueError:
@@ -79,112 +111,174 @@ class PesticideInterface:
79
  return data
80
 
81
  today = datetime.now()
82
- start_date = today - timedelta(days=7) if period == "Dernière semaine" else today - timedelta(days=30)
 
 
 
83
 
 
 
 
84
  filtered_data = []
85
  for item in data:
86
- raw_date = item.get("entry_into_force_date")
87
- if raw_date:
88
- corrected_date = self.parse_date(raw_date)
89
- if corrected_date and datetime.strptime(corrected_date, "%Y-%m-%d") >= start_date:
90
- item["entry_into_force_date"] = corrected_date
91
  filtered_data.append(item)
 
92
  logger.info(f"Filtered {len(data)} items to {len(filtered_data)} items for period {period}.")
93
  return filtered_data
94
 
95
- def get_product_details(self, product_name: str, period: str) -> pd.DataFrame:
 
 
 
 
 
 
96
  """Récupère et formate les détails des MRLs pour un produit donné."""
97
  try:
98
- logger.info(f"Getting details for product: {product_name}, period: {period}")
99
-
100
  if not product_name:
101
- logger.warning("No product selected.")
102
- return pd.DataFrame(columns=["Message"])
103
 
104
  product_id = self.product_choices.get(product_name)
105
  if not product_id:
106
- logger.error(f"Product ID not found for {product_name}.")
107
- return pd.DataFrame(columns=["Message"], data=[["Produit introuvable."]])
 
 
 
 
108
 
109
  # Récupérer les MRLs
110
  mrls = self.fetcher.get_mrls(product_id)
111
- filtered_mrls = self.filter_by_period(mrls, period)
112
-
113
- if not filtered_mrls:
114
- logger.warning("No MRLs found after filtering.")
115
- return pd.DataFrame(columns=["Message"], data=[["Aucune donnée trouvée."]])
116
-
117
- # Créer un DataFrame
118
- mrls_df = pd.DataFrame(filtered_mrls)
119
 
120
- # Colonnes disponibles
121
- available_columns = set(mrls_df.columns)
122
- logger.info(f"Available columns: {available_columns}")
123
 
124
- # Colonnes attendues
125
- expected_columns = {
 
 
 
126
  "substance_name": "Substance",
127
  "mrl_value": "Valeur LMR",
128
- "entry_into_force_date": "Date d'entrée en vigueur",
129
  "regulation_number": "Règlement",
130
- "regulation_url": "Lien"
131
  }
132
- display_columns = {key: value for key, value in expected_columns.items() if key in available_columns}
 
 
 
 
 
 
133
 
134
- # Ajouter une colonne par défaut si `substance_name` est manquant
135
- if "substance_name" not in available_columns:
136
- mrls_df["substance_name"] = "Nom de la substance indisponible"
137
- display_columns["substance_name"] = "Substance"
 
138
 
139
- if not display_columns:
140
- logger.warning("No relevant columns available to display.")
141
- return pd.DataFrame(columns=["Message"], data=[["Colonnes insuffisantes."]])
 
 
 
 
 
 
142
 
143
- mrls_df = mrls_df[list(display_columns.keys())].rename(columns=display_columns)
 
 
144
 
145
- # Convertir les URLs en liens cliquables
146
- if "Lien" in mrls_df.columns:
147
- mrls_df["Lien"] = mrls_df["Lien"].apply(lambda x: f"[Lien texte]({x})" if pd.notna(x) else "")
 
 
 
 
148
 
149
- return mrls_df
150
-
151
  except Exception as e:
152
- logger.error(f"Error in get_product_details: {str(e)}")
153
- return pd.DataFrame(columns=["Message"], data=[[f"Erreur : {str(e)}"]])
154
 
155
  def create_interface(self) -> gr.Blocks:
156
  """Crée l'interface Gradio avec un design amélioré."""
157
- with gr.Blocks() as interface:
158
- gr.Markdown("# 🌿 Base de données des pesticides de l'UE")
 
 
 
 
 
 
 
 
159
 
160
  with gr.Row():
161
- product_dropdown = gr.Dropdown(
162
- choices=list(self.product_choices.keys()),
163
- label="Produit",
164
- info="Choisissez un produit dans la liste"
165
- )
166
- period_radio = gr.Radio(
167
- choices=["Dernière semaine", "Dernier mois", "Toutes les dates"],
168
- value="Toutes les dates",
169
- label="Période",
170
- info="Sélectionnez la période d'analyse"
171
- )
 
 
 
 
 
 
 
 
 
172
 
173
- fetch_btn = gr.Button("📊 Analyser les données")
174
- mrls_table = gr.DataFrame()
 
 
 
 
175
 
176
  fetch_btn.click(
177
  fn=self.get_product_details,
178
- inputs=[product_dropdown, period_radio],
179
  outputs=[mrls_table]
180
  )
181
 
 
 
 
 
 
 
 
 
182
  return interface
183
 
184
  def main():
185
  interface = PesticideInterface()
186
  app = interface.create_interface()
187
- app.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
188
 
189
  if __name__ == "__main__":
190
  main()
 
5
  import pandas as pd
6
  from typing import Dict, List, Optional
7
  import logging
8
+ from dataclasses import dataclass
9
+ from concurrent.futures import ThreadPoolExecutor
10
 
11
  # Configuration du logging
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
15
+ )
16
  logger = logging.getLogger(__name__)
17
 
18
+ @dataclass
19
+ class PesticideRecord:
20
+ """Structure de données pour les enregistrements de pesticides."""
21
+ substance_name: str
22
+ mrl_value: float
23
+ entry_into_force_date: str
24
+ regulation_number: str
25
+ regulation_url: str
26
+ modification_date: Optional[str] = None
27
+
28
  class PesticideDataFetcher:
29
  """Classe pour gérer la récupération des données sur les pesticides."""
30
 
 
35
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
36
  }
37
 
38
+ def __init__(self):
39
+ self.session = self._create_session()
40
+
41
+ def _create_session(self):
42
+ """Crée une session pour les requêtes HTTP."""
43
+ import requests
44
+ session = requests.Session()
45
+ for header, value in self.HEADERS.items():
46
+ session.headers[header] = value
47
+ return session
48
+
49
+ def fetch_data(self, url: str) -> Dict:
50
+ """Récupère les données depuis l'API avec gestion d'erreurs et retry."""
51
+ max_retries = 3
52
+ for attempt in range(max_retries):
53
+ try:
54
+ response = self.session.get(url, timeout=10)
55
+ response.raise_for_status()
56
+ data = response.json()
57
  logger.info(f"Fetched data from {url}: {str(data)[:200]}...")
58
  return data
59
+ except Exception as e:
60
+ logger.warning(f"Tentative {attempt + 1}/{max_retries} échouée: {str(e)}")
61
+ if attempt == max_retries - 1:
62
+ logger.error(f"Échec de la récupération des données: {str(e)}")
63
+ return {"error": str(e)}
64
+ return {"error": "Max retries exceeded"}
65
 
66
+ def get_products(self) -> List[Dict]:
 
67
  """Récupère la liste des produits."""
68
+ url = f"{self.BASE_URL}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
69
+ response = self.fetch_data(url)
70
  return response.get("value", [])
71
 
72
+ def get_mrls(self, product_id: int) -> List[Dict]:
 
73
  """Récupère les LMR pour un produit spécifique."""
74
+ url = f"{self.BASE_URL}/pesticide_residues_mrls?format=json&product_id={product_id}&api-version=v2.0"
75
+ response = self.fetch_data(url)
76
  return response.get("value", [])
77
 
78
+ def get_substance_details(self, substance_id: int) -> Dict:
79
+ """Récupère les détails d'une substance."""
80
+ url = f"{self.BASE_URL}/substances/{substance_id}?format=json&api-version=v2.0"
81
+ return self.fetch_data(url)
82
+
83
  class PesticideInterface:
84
  """Classe pour gérer l'interface utilisateur Gradio."""
85
 
 
89
  self.product_choices = {
90
  p['product_name']: p['product_id'] for p in self.products
91
  }
92
+ self._cache = {}
93
  logger.info(f"Initialized interface with {len(self.product_choices)} products.")
94
 
95
  def parse_date(self, date_str: str) -> Optional[str]:
96
+ """Convertit une date au format 'YYYY-MM-DD'."""
97
+ if not date_str:
98
+ return None
99
+
100
+ for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%Y-%m-%dT%H:%M:%S"):
101
  try:
102
  return datetime.strptime(date_str, fmt).strftime("%Y-%m-%d")
103
  except ValueError:
 
111
  return data
112
 
113
  today = datetime.now()
114
+ start_date = {
115
+ "Dernière semaine": today - timedelta(days=7),
116
+ "Dernier mois": today - timedelta(days=30)
117
+ }.get(period)
118
 
119
+ if not start_date:
120
+ return data
121
+
122
  filtered_data = []
123
  for item in data:
124
+ date_str = item.get("entry_into_force_date") or item.get("modification_date")
125
+ if date_str:
126
+ parsed_date = self.parse_date(date_str)
127
+ if parsed_date and datetime.strptime(parsed_date, "%Y-%m-%d") >= start_date:
128
+ item["parsed_date"] = parsed_date
129
  filtered_data.append(item)
130
+
131
  logger.info(f"Filtered {len(data)} items to {len(filtered_data)} items for period {period}.")
132
  return filtered_data
133
 
134
+ def format_regulation_link(self, regulation_url: str, regulation_number: str) -> str:
135
+ """Formate un lien de règlement en HTML cliquable."""
136
+ if not regulation_url:
137
+ return regulation_number
138
+ return f'<a href="{regulation_url}" target="_blank">{regulation_number}</a>'
139
+
140
+ def get_product_details(self, product_name: str, period: str, show_only_changes: bool) -> pd.DataFrame:
141
  """Récupère et formate les détails des MRLs pour un produit donné."""
142
  try:
 
 
143
  if not product_name:
144
+ return pd.DataFrame({"Message": ["Veuillez sélectionner un produit"]})
 
145
 
146
  product_id = self.product_choices.get(product_name)
147
  if not product_id:
148
+ return pd.DataFrame({"Message": ["Produit non trouvé"]})
149
+
150
+ # Utiliser le cache si disponible
151
+ cache_key = f"{product_id}_{period}_{show_only_changes}"
152
+ if cache_key in self._cache:
153
+ return self._cache[cache_key]
154
 
155
  # Récupérer les MRLs
156
  mrls = self.fetcher.get_mrls(product_id)
157
+
158
+ # Filtrer par période si nécessaire
159
+ if period != "Toutes les dates":
160
+ mrls = self.filter_by_period(mrls, period)
 
 
 
 
161
 
162
+ if not mrls:
163
+ return pd.DataFrame({"Message": ["Aucune donnée trouvée pour la période sélectionnée"]})
 
164
 
165
+ # Créer le DataFrame
166
+ df = pd.DataFrame(mrls)
167
+
168
+ # Formater les colonnes
169
+ columns_mapping = {
170
  "substance_name": "Substance",
171
  "mrl_value": "Valeur LMR",
172
+ "entry_into_force_date": "Date d'application",
173
  "regulation_number": "Règlement",
174
+ "modification_date": "Date de modification"
175
  }
176
+
177
+ df = df.rename(columns=columns_mapping)
178
+
179
+ # Formater les dates
180
+ for date_col in ["Date d'application", "Date de modification"]:
181
+ if date_col in df.columns:
182
+ df[date_col] = df[date_col].apply(self.parse_date)
183
 
184
+ # Formater les valeurs LMR
185
+ if "Valeur LMR" in df.columns:
186
+ df["Valeur LMR"] = df["Valeur LMR"].apply(
187
+ lambda x: f"{x}*" if str(x).endswith('*') else x
188
+ )
189
 
190
+ # Ajouter les liens cliquables pour les règlements
191
+ if "regulation_url" in df.columns:
192
+ df["Règlement"] = df.apply(
193
+ lambda row: self.format_regulation_link(
194
+ row.get("regulation_url", ""),
195
+ row["Règlement"]
196
+ ),
197
+ axis=1
198
+ )
199
 
200
+ # Filtrer pour ne montrer que les changements si demandé
201
+ if show_only_changes:
202
+ df = df[df["Date de modification"].notna()]
203
 
204
+ # Trier par date d'application décroissante
205
+ df = df.sort_values("Date d'application", ascending=False)
206
+
207
+ # Mettre en cache
208
+ self._cache[cache_key] = df
209
+
210
+ return df
211
 
 
 
212
  except Exception as e:
213
+ logger.error(f"Erreur dans get_product_details: {str(e)}")
214
+ return pd.DataFrame({"Message": [f"Erreur: {str(e)}"]})
215
 
216
  def create_interface(self) -> gr.Blocks:
217
  """Crée l'interface Gradio avec un design amélioré."""
218
+ with gr.Blocks(
219
+ theme=gr.themes.Soft(
220
+ primary_hue="green",
221
+ secondary_hue="blue"
222
+ )
223
+ ) as interface:
224
+ gr.Markdown("""
225
+ # 🌿 Base de données des pesticides de l'UE
226
+ Consultez les Limites Maximales de Résidus (LMR) pour les produits agricoles.
227
+ """)
228
 
229
  with gr.Row():
230
+ with gr.Column(scale=2):
231
+ product_dropdown = gr.Dropdown(
232
+ choices=sorted(list(self.product_choices.keys())),
233
+ label="Produit",
234
+ info="Sélectionnez un produit agricole"
235
+ )
236
+ with gr.Column(scale=1):
237
+ period_radio = gr.Radio(
238
+ choices=["Dernière semaine", "Dernier mois", "Toutes les dates"],
239
+ value="Toutes les dates",
240
+ label="Période",
241
+ info="Filtrer par période"
242
+ )
243
+ show_changes = gr.Checkbox(
244
+ label="Afficher uniquement les modifications récentes",
245
+ info="Cochez pour voir uniquement les LMR qui ont été modifiées"
246
+ )
247
+
248
+ with gr.Row():
249
+ fetch_btn = gr.Button("📊 Analyser les données", variant="primary")
250
 
251
+ with gr.Row():
252
+ mrls_table = gr.DataFrame(
253
+ headers=["Substance", "Valeur LMR", "Date d'application",
254
+ "Date de modification", "Règlement"],
255
+ interactive=False
256
+ )
257
 
258
  fetch_btn.click(
259
  fn=self.get_product_details,
260
+ inputs=[product_dropdown, period_radio, show_changes],
261
  outputs=[mrls_table]
262
  )
263
 
264
+ gr.Markdown("""
265
+ ### Légende
266
+ - **Valeur LMR** : Limite Maximale de Résidus autorisée
267
+ - **Date d'application** : Date d'entrée en vigueur de la LMR
268
+ - **Date de modification** : Date de la dernière modification
269
+ - **Règlement** : Référence du règlement européen (cliquez pour accéder au texte)
270
+ """)
271
+
272
  return interface
273
 
274
  def main():
275
  interface = PesticideInterface()
276
  app = interface.create_interface()
277
+ app.launch(
278
+ server_name="0.0.0.0",
279
+ server_port=7860,
280
+ share=True
281
+ )
282
 
283
  if __name__ == "__main__":
284
  main()