Update app.py
Browse files
app.py
CHANGED
|
@@ -53,15 +53,15 @@ class PesticideDataFetcher:
|
|
| 53 |
self._product_cache: Dict[int, Dict[str, Any]] = {}
|
| 54 |
self._mrl_cache: Dict[int, List[Dict[str, Any]]] = {}
|
| 55 |
self.use_cache = use_cache
|
| 56 |
-
|
| 57 |
# Création du répertoire de cache si nécessaire
|
| 58 |
if not os.path.exists(self.CACHE_DIR):
|
| 59 |
os.makedirs(self.CACHE_DIR)
|
| 60 |
-
|
| 61 |
# Chargement des caches si disponibles
|
| 62 |
if use_cache:
|
| 63 |
self._load_caches()
|
| 64 |
-
|
| 65 |
# Préchargement des substances si le cache est vide
|
| 66 |
if not self._substance_cache:
|
| 67 |
self.preload_substance_names()
|
|
@@ -75,12 +75,12 @@ class PesticideDataFetcher:
|
|
| 75 |
for substance_id, substance_info in substance_data.items():
|
| 76 |
self._substance_cache[int(substance_id)] = SubstanceDetails(**substance_info)
|
| 77 |
logger.info(f"Cache de substances chargé: {len(self._substance_cache)} substances")
|
| 78 |
-
|
| 79 |
if os.path.exists(self.PRODUCT_CACHE_FILE):
|
| 80 |
with open(self.PRODUCT_CACHE_FILE, 'r', encoding='utf-8') as f:
|
| 81 |
self._product_cache = {int(k): v for k, v in json.load(f).items()}
|
| 82 |
logger.info(f"Cache de produits chargé: {len(self._product_cache)} produits")
|
| 83 |
-
|
| 84 |
if os.path.exists(self.MRL_CACHE_FILE):
|
| 85 |
with open(self.MRL_CACHE_FILE, 'r', encoding='utf-8') as f:
|
| 86 |
self._mrl_cache = {int(k): v for k, v in json.load(f).items()}
|
|
@@ -108,16 +108,16 @@ class PesticideDataFetcher:
|
|
| 108 |
}
|
| 109 |
for substance_id, details in self._substance_cache.items()
|
| 110 |
}
|
| 111 |
-
|
| 112 |
with open(self.SUBSTANCE_CACHE_FILE, 'w', encoding='utf-8') as f:
|
| 113 |
json.dump(substance_data, f, ensure_ascii=False, indent=2)
|
| 114 |
-
|
| 115 |
with open(self.PRODUCT_CACHE_FILE, 'w', encoding='utf-8') as f:
|
| 116 |
json.dump({str(k): v for k, v in self._product_cache.items()}, f, ensure_ascii=False, indent=2)
|
| 117 |
-
|
| 118 |
with open(self.MRL_CACHE_FILE, 'w', encoding='utf-8') as f:
|
| 119 |
json.dump({str(k): v for k, v in self._mrl_cache.items()}, f, ensure_ascii=False, indent=2)
|
| 120 |
-
|
| 121 |
logger.info("Tous les caches ont été sauvegardés avec succès")
|
| 122 |
except Exception as e:
|
| 123 |
logger.error(f"Erreur lors de la sauvegarde des caches: {e}")
|
|
@@ -158,7 +158,7 @@ class PesticideDataFetcher:
|
|
| 158 |
ec_number=substance.get("ecNumber")
|
| 159 |
)
|
| 160 |
substances_loaded += 1
|
| 161 |
-
|
| 162 |
# Chargement des détails supplémentaires pour chaque substance
|
| 163 |
self._load_substance_details(substance_id)
|
| 164 |
|
|
@@ -167,7 +167,7 @@ class PesticideDataFetcher:
|
|
| 167 |
|
| 168 |
if self.use_cache:
|
| 169 |
self._save_caches()
|
| 170 |
-
|
| 171 |
logger.info(f"Préchargement terminé. Total des substances: {len(self._substance_cache)}")
|
| 172 |
|
| 173 |
def _load_substance_details(self, substance_id: int) -> None:
|
|
@@ -175,7 +175,7 @@ class PesticideDataFetcher:
|
|
| 175 |
url = f"{self.BASE_URL}/pesticide_residues/{substance_id}?format=json&language=FR&api-version=v2.0"
|
| 176 |
try:
|
| 177 |
data = self.fetch_data(url)
|
| 178 |
-
|
| 179 |
substance = self._substance_cache.get(substance_id)
|
| 180 |
if substance:
|
| 181 |
substance.approval_date = data.get("approvalDate")
|
|
@@ -190,14 +190,14 @@ class PesticideDataFetcher:
|
|
| 190 |
"""Récupère le nom d'une substance à partir de son ID"""
|
| 191 |
if substance_id in self._substance_cache:
|
| 192 |
return self._substance_cache[substance_id].name
|
| 193 |
-
|
| 194 |
# Si la substance n'est pas dans le cache, essayer de la récupérer
|
| 195 |
try:
|
| 196 |
url = f"{self.BASE_URL}/pesticide_residues/{substance_id}?format=json&language=FR&api-version=v2.0"
|
| 197 |
data = self.fetch_data(url)
|
| 198 |
-
|
| 199 |
substance_name = data.get("pesticideResidueName", f"Substance {substance_id}")
|
| 200 |
-
|
| 201 |
# Mettre à jour le cache
|
| 202 |
self._substance_cache[substance_id] = SubstanceDetails(
|
| 203 |
name=substance_name,
|
|
@@ -208,42 +208,42 @@ class PesticideDataFetcher:
|
|
| 208 |
cas_number=data.get("casNumber"),
|
| 209 |
ec_number=data.get("ecNumber")
|
| 210 |
)
|
| 211 |
-
|
| 212 |
if self.use_cache:
|
| 213 |
self._save_caches()
|
| 214 |
-
|
| 215 |
return substance_name
|
| 216 |
except Exception as e:
|
| 217 |
logger.error(f"Erreur lors de la récupération de la substance {substance_id}: {e}")
|
| 218 |
return f"Substance inconnue ({substance_id})"
|
| 219 |
-
|
| 220 |
def get_product_list(self) -> List[Dict[str, Any]]:
|
| 221 |
"""Récupère la liste de tous les produits"""
|
| 222 |
if self._product_cache:
|
| 223 |
return list(self._product_cache.values())
|
| 224 |
-
|
| 225 |
logger.info("Récupération de la liste des produits...")
|
| 226 |
url = f"{self.BASE_URL}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
|
| 227 |
products_loaded = 0
|
| 228 |
-
|
| 229 |
while url:
|
| 230 |
data = self.fetch_data(url)
|
| 231 |
if "error" in data:
|
| 232 |
logger.error(f"Erreur produits: {data.get('error', 'Aucune info')}")
|
| 233 |
break
|
| 234 |
-
|
| 235 |
for product in data.get("value", []):
|
| 236 |
product_id = product.get("productId")
|
| 237 |
if product_id:
|
| 238 |
self._product_cache[product_id] = product
|
| 239 |
products_loaded += 1
|
| 240 |
-
|
| 241 |
url = data.get("nextLink")
|
| 242 |
logger.info(f"Produits récupérés jusqu'à présent: {products_loaded}")
|
| 243 |
-
|
| 244 |
if self.use_cache:
|
| 245 |
self._save_caches()
|
| 246 |
-
|
| 247 |
logger.info(f"Total des produits récupérés: {len(self._product_cache)}")
|
| 248 |
return list(self._product_cache.values())
|
| 249 |
|
|
@@ -252,26 +252,26 @@ class PesticideDataFetcher:
|
|
| 252 |
# Vérifier d'abord dans le cache
|
| 253 |
if product_id in self._mrl_cache:
|
| 254 |
return self._mrl_cache[product_id]
|
| 255 |
-
|
| 256 |
logger.info(f"Récupération des LMR pour le produit {product_id}...")
|
| 257 |
url = f"{self.BASE_URL}/pesticide_residues_products/{product_id}/mrls?format=json&language=FR&api-version=v2.0"
|
| 258 |
-
|
| 259 |
mrls = []
|
| 260 |
while url:
|
| 261 |
data = self.fetch_data(url)
|
| 262 |
if "error" in data:
|
| 263 |
logger.error(f"Erreur lors de la récupération des LMR: {data.get('error', 'Aucune info')}")
|
| 264 |
break
|
| 265 |
-
|
| 266 |
mrls.extend(data.get("value", []))
|
| 267 |
url = data.get("nextLink")
|
| 268 |
-
|
| 269 |
# Mise à jour du cache
|
| 270 |
self._mrl_cache[product_id] = mrls
|
| 271 |
-
|
| 272 |
if self.use_cache:
|
| 273 |
self._save_caches()
|
| 274 |
-
|
| 275 |
logger.info(f"LMR récupérées pour le produit {product_id}: {len(mrls)}")
|
| 276 |
return mrls
|
| 277 |
|
|
@@ -283,22 +283,22 @@ class PesticideDataFetcher:
|
|
| 283 |
if query in substance.name.lower()
|
| 284 |
]
|
| 285 |
return sorted(results, key=lambda x: x.name)
|
| 286 |
-
|
| 287 |
-
def get_substance_mrls(self, substance_id: int) -> Dict[str,
|
| 288 |
"""Récupère tous les produits avec LMR pour une substance donnée"""
|
| 289 |
logger.info(f"Récupération des LMR pour la substance {substance_id}...")
|
| 290 |
url = f"{self.BASE_URL}/pesticide_residues/{substance_id}/mrls?format=json&language=FR&api-version=v2.0"
|
| 291 |
-
|
| 292 |
all_mrls = []
|
| 293 |
while url:
|
| 294 |
data = self.fetch_data(url)
|
| 295 |
if "error" in data:
|
| 296 |
logger.error(f"Erreur lors de la récupération des LMR: {data.get('error', 'Aucune info')}")
|
| 297 |
break
|
| 298 |
-
|
| 299 |
all_mrls.extend(data.get("value", []))
|
| 300 |
url = data.get("nextLink")
|
| 301 |
-
|
| 302 |
logger.info(f"LMR récupérées pour la substance {substance_id}: {len(all_mrls)}")
|
| 303 |
return all_mrls
|
| 304 |
|
|
@@ -316,7 +316,7 @@ class PesticideApp:
|
|
| 316 |
def __init__(self, use_cache: bool = True):
|
| 317 |
logger.info("Initialisation de l'application...")
|
| 318 |
self.fetcher = PesticideDataFetcher(use_cache=use_cache)
|
| 319 |
-
|
| 320 |
# Récupération de la liste des produits
|
| 321 |
logger.info("Récupération de la liste des produits...")
|
| 322 |
products = self.fetcher.get_product_list()
|
|
@@ -324,12 +324,12 @@ class PesticideApp:
|
|
| 324 |
p.get('productName', 'Sans nom'): p.get('productId', 0)
|
| 325 |
for p in products
|
| 326 |
}
|
| 327 |
-
|
| 328 |
# Préparation de la liste des substances
|
| 329 |
self.substances = sorted([
|
| 330 |
sd.name for sd in self.fetcher._substance_cache.values()
|
| 331 |
])
|
| 332 |
-
|
| 333 |
logger.info(f"Application initialisée avec {len(self.product_list)} produits et {len(self.substances)} substances")
|
| 334 |
|
| 335 |
def format_date(self, date_str: Optional[str]) -> str:
|
|
@@ -342,17 +342,17 @@ class PesticideApp:
|
|
| 342 |
product_id = self.product_list.get(product_name)
|
| 343 |
if not product_id:
|
| 344 |
return pd.DataFrame([{"Erreur": "Produit non trouvé"}])
|
| 345 |
-
|
| 346 |
mrls = self.fetcher.get_mrls(product_id)
|
| 347 |
if not mrls:
|
| 348 |
return pd.DataFrame([{"Erreur": "Aucune donnée LMR trouvée"}])
|
| 349 |
-
|
| 350 |
data = []
|
| 351 |
for mrl in mrls:
|
| 352 |
substance_id = mrl.get("pesticideResidueId", 0)
|
| 353 |
substance_name = self.fetcher.get_substance_name(substance_id)
|
| 354 |
substance = self.fetcher._substance_cache.get(substance_id)
|
| 355 |
-
|
| 356 |
data.append({
|
| 357 |
"Substance": substance_name,
|
| 358 |
"Valeur LMR": mrl.get("mrlValue", "N/C"),
|
|
@@ -364,7 +364,7 @@ class PesticideApp:
|
|
| 364 |
"Date d'approbation": self.format_date(getattr(substance, "approval_date", None)),
|
| 365 |
"Date d'expiration": self.format_date(getattr(substance, "expiry_date", None))
|
| 366 |
})
|
| 367 |
-
|
| 368 |
df = pd.DataFrame(data)
|
| 369 |
logger.info(f"Détails récupérés pour {product_name}: {len(df)} entrées")
|
| 370 |
return df
|
|
@@ -374,11 +374,11 @@ class PesticideApp:
|
|
| 374 |
logger.info(f"Recherche de substances: {substance_query}")
|
| 375 |
if not substance_query or len(substance_query) < 3:
|
| 376 |
return pd.DataFrame([{"Message": "Veuillez entrer au moins 3 caractères pour la recherche"}])
|
| 377 |
-
|
| 378 |
results = self.fetcher.search_substances(substance_query)
|
| 379 |
if not results:
|
| 380 |
return pd.DataFrame([{"Message": "Aucune substance trouvée"}])
|
| 381 |
-
|
| 382 |
data = []
|
| 383 |
for substance in results:
|
| 384 |
data.append({
|
|
@@ -390,34 +390,35 @@ class PesticideApp:
|
|
| 390 |
"Date d'approbation": self.format_date(substance.approval_date),
|
| 391 |
"Date d'expiration": self.format_date(substance.expiry_date)
|
| 392 |
})
|
| 393 |
-
|
| 394 |
df = pd.DataFrame(data)
|
| 395 |
logger.info(f"Résultats de la recherche pour '{substance_query}': {len(df)} substances trouvées")
|
| 396 |
return df
|
| 397 |
|
| 398 |
def get_substance_mrls(self, substance_id: int) -> pd.DataFrame:
|
| 399 |
-
"""Récupère tous les produits avec LMR pour une substance donnée"""
|
| 400 |
logger.info(f"Récupération des LMR pour la substance ID: {substance_id}")
|
| 401 |
if not substance_id:
|
| 402 |
return pd.DataFrame([{"Erreur": "ID de substance non valide"}])
|
| 403 |
-
|
| 404 |
substance = self.fetcher._substance_cache.get(substance_id)
|
| 405 |
if not substance:
|
| 406 |
return pd.DataFrame([{"Erreur": "Substance non trouvée"}])
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
if not
|
| 410 |
return pd.DataFrame([{"Message": f"Aucune LMR trouvée pour {substance.name}"}])
|
| 411 |
-
|
|
|
|
| 412 |
data = []
|
| 413 |
-
for mrl in
|
| 414 |
product_id = mrl.get("productId")
|
| 415 |
product_name = "Inconnu"
|
| 416 |
-
|
| 417 |
# Récupérer le nom du produit si possible
|
| 418 |
if product_id in self.fetcher._product_cache:
|
| 419 |
product_name = self.fetcher._product_cache[product_id].get("productName", "Inconnu")
|
| 420 |
-
|
| 421 |
data.append({
|
| 422 |
"Produit": product_name,
|
| 423 |
"Valeur LMR": mrl.get("mrlValue", "N/C"),
|
|
@@ -425,10 +426,10 @@ class PesticideApp:
|
|
| 425 |
"Date d'effet": self.format_date(mrl.get("entryIntoForceDate")),
|
| 426 |
"Notes": mrl.get("footnotes", "")
|
| 427 |
})
|
| 428 |
-
|
| 429 |
-
df = pd.DataFrame(data)
|
| 430 |
logger.info(f"LMR récupérées pour {substance.name}: {len(df)} entrées")
|
| 431 |
-
return df
|
| 432 |
|
| 433 |
def create_histogram(self, df: pd.DataFrame) -> go.Figure:
|
| 434 |
"""Crée un histogramme des valeurs LMR"""
|
|
@@ -436,7 +437,7 @@ class PesticideApp:
|
|
| 436 |
fig = go.Figure()
|
| 437 |
fig.update_layout(title="Aucune donnée disponible pour l'histogramme")
|
| 438 |
return fig
|
| 439 |
-
|
| 440 |
# Convertir les valeurs LMR en nombres si possible
|
| 441 |
numeric_values = []
|
| 442 |
for val in df["Valeur LMR"]:
|
|
@@ -450,12 +451,12 @@ class PesticideApp:
|
|
| 450 |
numeric_values.append(float(val))
|
| 451 |
except (ValueError, TypeError):
|
| 452 |
continue
|
| 453 |
-
|
| 454 |
if not numeric_values:
|
| 455 |
fig = go.Figure()
|
| 456 |
fig.update_layout(title="Aucune valeur numérique disponible pour l'histogramme")
|
| 457 |
return fig
|
| 458 |
-
|
| 459 |
# Création de l'histogramme
|
| 460 |
fig = go.Figure(data=[go.Histogram(x=numeric_values, nbinsx=20)])
|
| 461 |
fig.update_layout(
|
|
@@ -472,16 +473,16 @@ class PesticideApp:
|
|
| 472 |
fig = go.Figure()
|
| 473 |
fig.update_layout(title=f"Aucune donnée disponible pour {column}")
|
| 474 |
return fig
|
| 475 |
-
|
| 476 |
# Compter les valeurs
|
| 477 |
value_counts = df[column].value_counts()
|
| 478 |
-
|
| 479 |
# Création du graphique
|
| 480 |
fig = go.Figure(data=[go.Pie(
|
| 481 |
labels=value_counts.index,
|
| 482 |
values=value_counts.values
|
| 483 |
)])
|
| 484 |
-
|
| 485 |
fig.update_layout(title=f"Répartition par {column}")
|
| 486 |
return fig
|
| 487 |
|
|
@@ -494,7 +495,7 @@ class PesticideApp:
|
|
| 494 |
<p>Consultez les limites maximales de résidus (LMR) de pesticides dans les produits alimentaires</p>
|
| 495 |
</div>
|
| 496 |
""")
|
| 497 |
-
|
| 498 |
with gr.Tab("Recherche par Produit"):
|
| 499 |
with gr.Row():
|
| 500 |
with gr.Column(scale=3):
|
|
@@ -505,20 +506,20 @@ class PesticideApp:
|
|
| 505 |
)
|
| 506 |
with gr.Column(scale=1):
|
| 507 |
search_btn = gr.Button("Rechercher", variant="primary")
|
| 508 |
-
|
| 509 |
with gr.Row():
|
| 510 |
output = gr.Dataframe(
|
| 511 |
headers=["Substance", "Valeur LMR", "Unité", "Date d'effet", "Statut"],
|
| 512 |
max_rows=20,
|
| 513 |
interactive=False
|
| 514 |
)
|
| 515 |
-
|
| 516 |
with gr.Row():
|
| 517 |
with gr.Column():
|
| 518 |
histogram = gr.Plot(label="Distribution des LMR")
|
| 519 |
with gr.Column():
|
| 520 |
status_pie = gr.Plot(label="Statut des substances")
|
| 521 |
-
|
| 522 |
search_btn.click(
|
| 523 |
fn=lambda p: (
|
| 524 |
self.get_product_details(p),
|
|
@@ -528,7 +529,7 @@ class PesticideApp:
|
|
| 528 |
inputs=[product],
|
| 529 |
outputs=[output, histogram, status_pie]
|
| 530 |
)
|
| 531 |
-
|
| 532 |
with gr.Tab("Recherche par Substance"):
|
| 533 |
with gr.Row():
|
| 534 |
with gr.Column(scale=3):
|
|
@@ -539,69 +540,69 @@ class PesticideApp:
|
|
| 539 |
)
|
| 540 |
with gr.Column(scale=1):
|
| 541 |
substance_search_btn = gr.Button("Rechercher", variant="primary")
|
| 542 |
-
|
| 543 |
substance_results = gr.Dataframe(
|
| 544 |
headers=["ID", "Nom", "Statut", "N° CAS", "N° EC", "Date d'approbation", "Date d'expiration"],
|
| 545 |
max_rows=15,
|
| 546 |
interactive=True
|
| 547 |
)
|
| 548 |
-
|
| 549 |
substance_select = gr.Number(label="ID de la substance sélectionnée", interactive=True, visible=False)
|
| 550 |
substance_mrls_btn = gr.Button("Voir les LMR pour cette substance", visible=True)
|
| 551 |
-
|
| 552 |
substance_mrls = gr.Dataframe(
|
| 553 |
headers=["Produit", "Valeur LMR", "Unité", "Date d'effet", "Notes"],
|
| 554 |
max_rows=20,
|
| 555 |
interactive=False
|
| 556 |
)
|
| 557 |
-
|
| 558 |
mrl_histogram = gr.Plot(label="Distribution des LMR par produit")
|
| 559 |
-
|
| 560 |
# Événements
|
| 561 |
substance_search_btn.click(
|
| 562 |
fn=self.search_substance,
|
| 563 |
inputs=[substance_query],
|
| 564 |
outputs=[substance_results]
|
| 565 |
)
|
| 566 |
-
|
| 567 |
# Mise à jour de l'ID de substance sélectionné
|
| 568 |
substance_results.select(
|
| 569 |
fn=lambda evt: evt["data"]["ID"] if evt and "data" in evt and "ID" in evt["data"] else None,
|
| 570 |
inputs=[],
|
| 571 |
outputs=[substance_select]
|
| 572 |
)
|
| 573 |
-
|
| 574 |
substance_mrls_btn.click(
|
| 575 |
fn=lambda sid: (
|
| 576 |
-
self.get_substance_mrls(sid),
|
| 577 |
-
self.create_histogram(self.get_substance_mrls(sid))
|
| 578 |
),
|
| 579 |
inputs=[substance_select],
|
| 580 |
outputs=[substance_mrls, mrl_histogram]
|
| 581 |
)
|
| 582 |
-
|
| 583 |
with gr.Tab("À propos"):
|
| 584 |
gr.HTML("""
|
| 585 |
<div style="padding: 20px;">
|
| 586 |
<h2>À propos de cette application</h2>
|
| 587 |
-
<p>Cette application permet de consulter les limites maximales de résidus (LMR) des pesticides
|
| 588 |
autorisés dans les produits alimentaires dans l'Union européenne.</p>
|
| 589 |
-
|
| 590 |
<h3>Sources de données</h3>
|
| 591 |
<p>Les données sont extraites de l'API officielle de la Commission européenne:</p>
|
| 592 |
<ul>
|
| 593 |
<li>API Pesticides: <a href="https://api.datalake.sante.service.ec.europa.eu/sante/pesticides">
|
| 594 |
https://api.datalake.sante.service.ec.europa.eu/sante/pesticides</a></li>
|
| 595 |
</ul>
|
| 596 |
-
|
| 597 |
<h3>Comment utiliser cette application</h3>
|
| 598 |
<ul>
|
| 599 |
-
<li><strong>Recherche par Produit</strong>: Sélectionnez un produit alimentaire pour voir
|
| 600 |
toutes les substances et leurs LMR associées.</li>
|
| 601 |
-
<li><strong>Recherche par Substance</strong>: Recherchez une substance active pour voir son
|
| 602 |
statut et les produits dans lesquels elle est réglementée.</li>
|
| 603 |
</ul>
|
| 604 |
-
|
| 605 |
<h3>Légende</h3>
|
| 606 |
<ul>
|
| 607 |
<li><strong>LMR</strong>: Limite Maximale de Résidus, exprimée généralement en mg/kg</li>
|
|
@@ -610,13 +611,13 @@ class PesticideApp:
|
|
| 610 |
</ul>
|
| 611 |
</div>
|
| 612 |
""")
|
| 613 |
-
|
| 614 |
return ui
|
| 615 |
|
| 616 |
def main():
|
| 617 |
# Paramètres de ligne de commande (vous pourriez les ajouter via argparse)
|
| 618 |
use_cache = True # Utiliser le cache par défaut
|
| 619 |
-
|
| 620 |
try:
|
| 621 |
app = PesticideApp(use_cache=use_cache)
|
| 622 |
logger.info("Lancement de l'interface utilisateur...")
|
|
|
|
| 53 |
self._product_cache: Dict[int, Dict[str, Any]] = {}
|
| 54 |
self._mrl_cache: Dict[int, List[Dict[str, Any]]] = {}
|
| 55 |
self.use_cache = use_cache
|
| 56 |
+
|
| 57 |
# Création du répertoire de cache si nécessaire
|
| 58 |
if not os.path.exists(self.CACHE_DIR):
|
| 59 |
os.makedirs(self.CACHE_DIR)
|
| 60 |
+
|
| 61 |
# Chargement des caches si disponibles
|
| 62 |
if use_cache:
|
| 63 |
self._load_caches()
|
| 64 |
+
|
| 65 |
# Préchargement des substances si le cache est vide
|
| 66 |
if not self._substance_cache:
|
| 67 |
self.preload_substance_names()
|
|
|
|
| 75 |
for substance_id, substance_info in substance_data.items():
|
| 76 |
self._substance_cache[int(substance_id)] = SubstanceDetails(**substance_info)
|
| 77 |
logger.info(f"Cache de substances chargé: {len(self._substance_cache)} substances")
|
| 78 |
+
|
| 79 |
if os.path.exists(self.PRODUCT_CACHE_FILE):
|
| 80 |
with open(self.PRODUCT_CACHE_FILE, 'r', encoding='utf-8') as f:
|
| 81 |
self._product_cache = {int(k): v for k, v in json.load(f).items()}
|
| 82 |
logger.info(f"Cache de produits chargé: {len(self._product_cache)} produits")
|
| 83 |
+
|
| 84 |
if os.path.exists(self.MRL_CACHE_FILE):
|
| 85 |
with open(self.MRL_CACHE_FILE, 'r', encoding='utf-8') as f:
|
| 86 |
self._mrl_cache = {int(k): v for k, v in json.load(f).items()}
|
|
|
|
| 108 |
}
|
| 109 |
for substance_id, details in self._substance_cache.items()
|
| 110 |
}
|
| 111 |
+
|
| 112 |
with open(self.SUBSTANCE_CACHE_FILE, 'w', encoding='utf-8') as f:
|
| 113 |
json.dump(substance_data, f, ensure_ascii=False, indent=2)
|
| 114 |
+
|
| 115 |
with open(self.PRODUCT_CACHE_FILE, 'w', encoding='utf-8') as f:
|
| 116 |
json.dump({str(k): v for k, v in self._product_cache.items()}, f, ensure_ascii=False, indent=2)
|
| 117 |
+
|
| 118 |
with open(self.MRL_CACHE_FILE, 'w', encoding='utf-8') as f:
|
| 119 |
json.dump({str(k): v for k, v in self._mrl_cache.items()}, f, ensure_ascii=False, indent=2)
|
| 120 |
+
|
| 121 |
logger.info("Tous les caches ont été sauvegardés avec succès")
|
| 122 |
except Exception as e:
|
| 123 |
logger.error(f"Erreur lors de la sauvegarde des caches: {e}")
|
|
|
|
| 158 |
ec_number=substance.get("ecNumber")
|
| 159 |
)
|
| 160 |
substances_loaded += 1
|
| 161 |
+
|
| 162 |
# Chargement des détails supplémentaires pour chaque substance
|
| 163 |
self._load_substance_details(substance_id)
|
| 164 |
|
|
|
|
| 167 |
|
| 168 |
if self.use_cache:
|
| 169 |
self._save_caches()
|
| 170 |
+
|
| 171 |
logger.info(f"Préchargement terminé. Total des substances: {len(self._substance_cache)}")
|
| 172 |
|
| 173 |
def _load_substance_details(self, substance_id: int) -> None:
|
|
|
|
| 175 |
url = f"{self.BASE_URL}/pesticide_residues/{substance_id}?format=json&language=FR&api-version=v2.0"
|
| 176 |
try:
|
| 177 |
data = self.fetch_data(url)
|
| 178 |
+
|
| 179 |
substance = self._substance_cache.get(substance_id)
|
| 180 |
if substance:
|
| 181 |
substance.approval_date = data.get("approvalDate")
|
|
|
|
| 190 |
"""Récupère le nom d'une substance à partir de son ID"""
|
| 191 |
if substance_id in self._substance_cache:
|
| 192 |
return self._substance_cache[substance_id].name
|
| 193 |
+
|
| 194 |
# Si la substance n'est pas dans le cache, essayer de la récupérer
|
| 195 |
try:
|
| 196 |
url = f"{self.BASE_URL}/pesticide_residues/{substance_id}?format=json&language=FR&api-version=v2.0"
|
| 197 |
data = self.fetch_data(url)
|
| 198 |
+
|
| 199 |
substance_name = data.get("pesticideResidueName", f"Substance {substance_id}")
|
| 200 |
+
|
| 201 |
# Mettre à jour le cache
|
| 202 |
self._substance_cache[substance_id] = SubstanceDetails(
|
| 203 |
name=substance_name,
|
|
|
|
| 208 |
cas_number=data.get("casNumber"),
|
| 209 |
ec_number=data.get("ecNumber")
|
| 210 |
)
|
| 211 |
+
|
| 212 |
if self.use_cache:
|
| 213 |
self._save_caches()
|
| 214 |
+
|
| 215 |
return substance_name
|
| 216 |
except Exception as e:
|
| 217 |
logger.error(f"Erreur lors de la récupération de la substance {substance_id}: {e}")
|
| 218 |
return f"Substance inconnue ({substance_id})"
|
| 219 |
+
|
| 220 |
def get_product_list(self) -> List[Dict[str, Any]]:
|
| 221 |
"""Récupère la liste de tous les produits"""
|
| 222 |
if self._product_cache:
|
| 223 |
return list(self._product_cache.values())
|
| 224 |
+
|
| 225 |
logger.info("Récupération de la liste des produits...")
|
| 226 |
url = f"{self.BASE_URL}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
|
| 227 |
products_loaded = 0
|
| 228 |
+
|
| 229 |
while url:
|
| 230 |
data = self.fetch_data(url)
|
| 231 |
if "error" in data:
|
| 232 |
logger.error(f"Erreur produits: {data.get('error', 'Aucune info')}")
|
| 233 |
break
|
| 234 |
+
|
| 235 |
for product in data.get("value", []):
|
| 236 |
product_id = product.get("productId")
|
| 237 |
if product_id:
|
| 238 |
self._product_cache[product_id] = product
|
| 239 |
products_loaded += 1
|
| 240 |
+
|
| 241 |
url = data.get("nextLink")
|
| 242 |
logger.info(f"Produits récupérés jusqu'à présent: {products_loaded}")
|
| 243 |
+
|
| 244 |
if self.use_cache:
|
| 245 |
self._save_caches()
|
| 246 |
+
|
| 247 |
logger.info(f"Total des produits récupérés: {len(self._product_cache)}")
|
| 248 |
return list(self._product_cache.values())
|
| 249 |
|
|
|
|
| 252 |
# Vérifier d'abord dans le cache
|
| 253 |
if product_id in self._mrl_cache:
|
| 254 |
return self._mrl_cache[product_id]
|
| 255 |
+
|
| 256 |
logger.info(f"Récupération des LMR pour le produit {product_id}...")
|
| 257 |
url = f"{self.BASE_URL}/pesticide_residues_products/{product_id}/mrls?format=json&language=FR&api-version=v2.0"
|
| 258 |
+
|
| 259 |
mrls = []
|
| 260 |
while url:
|
| 261 |
data = self.fetch_data(url)
|
| 262 |
if "error" in data:
|
| 263 |
logger.error(f"Erreur lors de la récupération des LMR: {data.get('error', 'Aucune info')}")
|
| 264 |
break
|
| 265 |
+
|
| 266 |
mrls.extend(data.get("value", []))
|
| 267 |
url = data.get("nextLink")
|
| 268 |
+
|
| 269 |
# Mise à jour du cache
|
| 270 |
self._mrl_cache[product_id] = mrls
|
| 271 |
+
|
| 272 |
if self.use_cache:
|
| 273 |
self._save_caches()
|
| 274 |
+
|
| 275 |
logger.info(f"LMR récupérées pour le produit {product_id}: {len(mrls)}")
|
| 276 |
return mrls
|
| 277 |
|
|
|
|
| 283 |
if query in substance.name.lower()
|
| 284 |
]
|
| 285 |
return sorted(results, key=lambda x: x.name)
|
| 286 |
+
|
| 287 |
+
def get_substance_mrls(self, substance_id: int) -> List[Dict[str,Any]]: #Changed List to Dict
|
| 288 |
"""Récupère tous les produits avec LMR pour une substance donnée"""
|
| 289 |
logger.info(f"Récupération des LMR pour la substance {substance_id}...")
|
| 290 |
url = f"{self.BASE_URL}/pesticide_residues/{substance_id}/mrls?format=json&language=FR&api-version=v2.0"
|
| 291 |
+
|
| 292 |
all_mrls = []
|
| 293 |
while url:
|
| 294 |
data = self.fetch_data(url)
|
| 295 |
if "error" in data:
|
| 296 |
logger.error(f"Erreur lors de la récupération des LMR: {data.get('error', 'Aucune info')}")
|
| 297 |
break
|
| 298 |
+
|
| 299 |
all_mrls.extend(data.get("value", []))
|
| 300 |
url = data.get("nextLink")
|
| 301 |
+
|
| 302 |
logger.info(f"LMR récupérées pour la substance {substance_id}: {len(all_mrls)}")
|
| 303 |
return all_mrls
|
| 304 |
|
|
|
|
| 316 |
def __init__(self, use_cache: bool = True):
|
| 317 |
logger.info("Initialisation de l'application...")
|
| 318 |
self.fetcher = PesticideDataFetcher(use_cache=use_cache)
|
| 319 |
+
|
| 320 |
# Récupération de la liste des produits
|
| 321 |
logger.info("Récupération de la liste des produits...")
|
| 322 |
products = self.fetcher.get_product_list()
|
|
|
|
| 324 |
p.get('productName', 'Sans nom'): p.get('productId', 0)
|
| 325 |
for p in products
|
| 326 |
}
|
| 327 |
+
|
| 328 |
# Préparation de la liste des substances
|
| 329 |
self.substances = sorted([
|
| 330 |
sd.name for sd in self.fetcher._substance_cache.values()
|
| 331 |
])
|
| 332 |
+
|
| 333 |
logger.info(f"Application initialisée avec {len(self.product_list)} produits et {len(self.substances)} substances")
|
| 334 |
|
| 335 |
def format_date(self, date_str: Optional[str]) -> str:
|
|
|
|
| 342 |
product_id = self.product_list.get(product_name)
|
| 343 |
if not product_id:
|
| 344 |
return pd.DataFrame([{"Erreur": "Produit non trouvé"}])
|
| 345 |
+
|
| 346 |
mrls = self.fetcher.get_mrls(product_id)
|
| 347 |
if not mrls:
|
| 348 |
return pd.DataFrame([{"Erreur": "Aucune donnée LMR trouvée"}])
|
| 349 |
+
|
| 350 |
data = []
|
| 351 |
for mrl in mrls:
|
| 352 |
substance_id = mrl.get("pesticideResidueId", 0)
|
| 353 |
substance_name = self.fetcher.get_substance_name(substance_id)
|
| 354 |
substance = self.fetcher._substance_cache.get(substance_id)
|
| 355 |
+
|
| 356 |
data.append({
|
| 357 |
"Substance": substance_name,
|
| 358 |
"Valeur LMR": mrl.get("mrlValue", "N/C"),
|
|
|
|
| 364 |
"Date d'approbation": self.format_date(getattr(substance, "approval_date", None)),
|
| 365 |
"Date d'expiration": self.format_date(getattr(substance, "expiry_date", None))
|
| 366 |
})
|
| 367 |
+
|
| 368 |
df = pd.DataFrame(data)
|
| 369 |
logger.info(f"Détails récupérés pour {product_name}: {len(df)} entrées")
|
| 370 |
return df
|
|
|
|
| 374 |
logger.info(f"Recherche de substances: {substance_query}")
|
| 375 |
if not substance_query or len(substance_query) < 3:
|
| 376 |
return pd.DataFrame([{"Message": "Veuillez entrer au moins 3 caractères pour la recherche"}])
|
| 377 |
+
|
| 378 |
results = self.fetcher.search_substances(substance_query)
|
| 379 |
if not results:
|
| 380 |
return pd.DataFrame([{"Message": "Aucune substance trouvée"}])
|
| 381 |
+
|
| 382 |
data = []
|
| 383 |
for substance in results:
|
| 384 |
data.append({
|
|
|
|
| 390 |
"Date d'approbation": self.format_date(substance.approval_date),
|
| 391 |
"Date d'expiration": self.format_date(substance.expiry_date)
|
| 392 |
})
|
| 393 |
+
|
| 394 |
df = pd.DataFrame(data)
|
| 395 |
logger.info(f"Résultats de la recherche pour '{substance_query}': {len(df)} substances trouvées")
|
| 396 |
return df
|
| 397 |
|
| 398 |
def get_substance_mrls(self, substance_id: int) -> pd.DataFrame:
|
| 399 |
+
"""Récupère tous les produits avec LMR pour une substance donnée et retourne un DataFrame."""
|
| 400 |
logger.info(f"Récupération des LMR pour la substance ID: {substance_id}")
|
| 401 |
if not substance_id:
|
| 402 |
return pd.DataFrame([{"Erreur": "ID de substance non valide"}])
|
| 403 |
+
|
| 404 |
substance = self.fetcher._substance_cache.get(substance_id)
|
| 405 |
if not substance:
|
| 406 |
return pd.DataFrame([{"Erreur": "Substance non trouvée"}])
|
| 407 |
+
|
| 408 |
+
all_mrls = self.fetcher.get_substance_mrls(substance_id) # Récupère la liste brute
|
| 409 |
+
if not all_mrls:
|
| 410 |
return pd.DataFrame([{"Message": f"Aucune LMR trouvée pour {substance.name}"}])
|
| 411 |
+
|
| 412 |
+
# Conversion en DataFrame ici:
|
| 413 |
data = []
|
| 414 |
+
for mrl in all_mrls:
|
| 415 |
product_id = mrl.get("productId")
|
| 416 |
product_name = "Inconnu"
|
| 417 |
+
|
| 418 |
# Récupérer le nom du produit si possible
|
| 419 |
if product_id in self.fetcher._product_cache:
|
| 420 |
product_name = self.fetcher._product_cache[product_id].get("productName", "Inconnu")
|
| 421 |
+
|
| 422 |
data.append({
|
| 423 |
"Produit": product_name,
|
| 424 |
"Valeur LMR": mrl.get("mrlValue", "N/C"),
|
|
|
|
| 426 |
"Date d'effet": self.format_date(mrl.get("entryIntoForceDate")),
|
| 427 |
"Notes": mrl.get("footnotes", "")
|
| 428 |
})
|
| 429 |
+
|
| 430 |
+
df = pd.DataFrame(data) # Crée le DataFrame
|
| 431 |
logger.info(f"LMR récupérées pour {substance.name}: {len(df)} entrées")
|
| 432 |
+
return df # MODIFICATION: retourner le dataframe
|
| 433 |
|
| 434 |
def create_histogram(self, df: pd.DataFrame) -> go.Figure:
|
| 435 |
"""Crée un histogramme des valeurs LMR"""
|
|
|
|
| 437 |
fig = go.Figure()
|
| 438 |
fig.update_layout(title="Aucune donnée disponible pour l'histogramme")
|
| 439 |
return fig
|
| 440 |
+
|
| 441 |
# Convertir les valeurs LMR en nombres si possible
|
| 442 |
numeric_values = []
|
| 443 |
for val in df["Valeur LMR"]:
|
|
|
|
| 451 |
numeric_values.append(float(val))
|
| 452 |
except (ValueError, TypeError):
|
| 453 |
continue
|
| 454 |
+
|
| 455 |
if not numeric_values:
|
| 456 |
fig = go.Figure()
|
| 457 |
fig.update_layout(title="Aucune valeur numérique disponible pour l'histogramme")
|
| 458 |
return fig
|
| 459 |
+
|
| 460 |
# Création de l'histogramme
|
| 461 |
fig = go.Figure(data=[go.Histogram(x=numeric_values, nbinsx=20)])
|
| 462 |
fig.update_layout(
|
|
|
|
| 473 |
fig = go.Figure()
|
| 474 |
fig.update_layout(title=f"Aucune donnée disponible pour {column}")
|
| 475 |
return fig
|
| 476 |
+
|
| 477 |
# Compter les valeurs
|
| 478 |
value_counts = df[column].value_counts()
|
| 479 |
+
|
| 480 |
# Création du graphique
|
| 481 |
fig = go.Figure(data=[go.Pie(
|
| 482 |
labels=value_counts.index,
|
| 483 |
values=value_counts.values
|
| 484 |
)])
|
| 485 |
+
|
| 486 |
fig.update_layout(title=f"Répartition par {column}")
|
| 487 |
return fig
|
| 488 |
|
|
|
|
| 495 |
<p>Consultez les limites maximales de résidus (LMR) de pesticides dans les produits alimentaires</p>
|
| 496 |
</div>
|
| 497 |
""")
|
| 498 |
+
|
| 499 |
with gr.Tab("Recherche par Produit"):
|
| 500 |
with gr.Row():
|
| 501 |
with gr.Column(scale=3):
|
|
|
|
| 506 |
)
|
| 507 |
with gr.Column(scale=1):
|
| 508 |
search_btn = gr.Button("Rechercher", variant="primary")
|
| 509 |
+
|
| 510 |
with gr.Row():
|
| 511 |
output = gr.Dataframe(
|
| 512 |
headers=["Substance", "Valeur LMR", "Unité", "Date d'effet", "Statut"],
|
| 513 |
max_rows=20,
|
| 514 |
interactive=False
|
| 515 |
)
|
| 516 |
+
|
| 517 |
with gr.Row():
|
| 518 |
with gr.Column():
|
| 519 |
histogram = gr.Plot(label="Distribution des LMR")
|
| 520 |
with gr.Column():
|
| 521 |
status_pie = gr.Plot(label="Statut des substances")
|
| 522 |
+
|
| 523 |
search_btn.click(
|
| 524 |
fn=lambda p: (
|
| 525 |
self.get_product_details(p),
|
|
|
|
| 529 |
inputs=[product],
|
| 530 |
outputs=[output, histogram, status_pie]
|
| 531 |
)
|
| 532 |
+
|
| 533 |
with gr.Tab("Recherche par Substance"):
|
| 534 |
with gr.Row():
|
| 535 |
with gr.Column(scale=3):
|
|
|
|
| 540 |
)
|
| 541 |
with gr.Column(scale=1):
|
| 542 |
substance_search_btn = gr.Button("Rechercher", variant="primary")
|
| 543 |
+
|
| 544 |
substance_results = gr.Dataframe(
|
| 545 |
headers=["ID", "Nom", "Statut", "N° CAS", "N° EC", "Date d'approbation", "Date d'expiration"],
|
| 546 |
max_rows=15,
|
| 547 |
interactive=True
|
| 548 |
)
|
| 549 |
+
|
| 550 |
substance_select = gr.Number(label="ID de la substance sélectionnée", interactive=True, visible=False)
|
| 551 |
substance_mrls_btn = gr.Button("Voir les LMR pour cette substance", visible=True)
|
| 552 |
+
|
| 553 |
substance_mrls = gr.Dataframe(
|
| 554 |
headers=["Produit", "Valeur LMR", "Unité", "Date d'effet", "Notes"],
|
| 555 |
max_rows=20,
|
| 556 |
interactive=False
|
| 557 |
)
|
| 558 |
+
|
| 559 |
mrl_histogram = gr.Plot(label="Distribution des LMR par produit")
|
| 560 |
+
|
| 561 |
# Événements
|
| 562 |
substance_search_btn.click(
|
| 563 |
fn=self.search_substance,
|
| 564 |
inputs=[substance_query],
|
| 565 |
outputs=[substance_results]
|
| 566 |
)
|
| 567 |
+
|
| 568 |
# Mise à jour de l'ID de substance sélectionné
|
| 569 |
substance_results.select(
|
| 570 |
fn=lambda evt: evt["data"]["ID"] if evt and "data" in evt and "ID" in evt["data"] else None,
|
| 571 |
inputs=[],
|
| 572 |
outputs=[substance_select]
|
| 573 |
)
|
| 574 |
+
|
| 575 |
substance_mrls_btn.click(
|
| 576 |
fn=lambda sid: (
|
| 577 |
+
self.get_substance_mrls(int(sid)), #MODIFICATION: int() conversion
|
| 578 |
+
self.create_histogram(self.get_substance_mrls(int(sid))) #MODIFICATION : int() conversion
|
| 579 |
),
|
| 580 |
inputs=[substance_select],
|
| 581 |
outputs=[substance_mrls, mrl_histogram]
|
| 582 |
)
|
| 583 |
+
|
| 584 |
with gr.Tab("À propos"):
|
| 585 |
gr.HTML("""
|
| 586 |
<div style="padding: 20px;">
|
| 587 |
<h2>À propos de cette application</h2>
|
| 588 |
+
<p>Cette application permet de consulter les limites maximales de résidus (LMR) des pesticides
|
| 589 |
autorisés dans les produits alimentaires dans l'Union européenne.</p>
|
| 590 |
+
|
| 591 |
<h3>Sources de données</h3>
|
| 592 |
<p>Les données sont extraites de l'API officielle de la Commission européenne:</p>
|
| 593 |
<ul>
|
| 594 |
<li>API Pesticides: <a href="https://api.datalake.sante.service.ec.europa.eu/sante/pesticides">
|
| 595 |
https://api.datalake.sante.service.ec.europa.eu/sante/pesticides</a></li>
|
| 596 |
</ul>
|
| 597 |
+
|
| 598 |
<h3>Comment utiliser cette application</h3>
|
| 599 |
<ul>
|
| 600 |
+
<li><strong>Recherche par Produit</strong>: Sélectionnez un produit alimentaire pour voir
|
| 601 |
toutes les substances et leurs LMR associées.</li>
|
| 602 |
+
<li><strong>Recherche par Substance</strong>: Recherchez une substance active pour voir son
|
| 603 |
statut et les produits dans lesquels elle est réglementée.</li>
|
| 604 |
</ul>
|
| 605 |
+
|
| 606 |
<h3>Légende</h3>
|
| 607 |
<ul>
|
| 608 |
<li><strong>LMR</strong>: Limite Maximale de Résidus, exprimée généralement en mg/kg</li>
|
|
|
|
| 611 |
</ul>
|
| 612 |
</div>
|
| 613 |
""")
|
| 614 |
+
|
| 615 |
return ui
|
| 616 |
|
| 617 |
def main():
|
| 618 |
# Paramètres de ligne de commande (vous pourriez les ajouter via argparse)
|
| 619 |
use_cache = True # Utiliser le cache par défaut
|
| 620 |
+
|
| 621 |
try:
|
| 622 |
app = PesticideApp(use_cache=use_cache)
|
| 623 |
logger.info("Lancement de l'interface utilisateur...")
|