Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -30,10 +30,45 @@ DEFAULT_FORMAT = "json"
|
|
| 30 |
RETRY_ATTEMPTS = 3
|
| 31 |
RETRY_DELAY = 2 # secondes
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# Cache pour les données
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
@st.cache_data(ttl=3600) # Cache d'une heure
|
| 35 |
def get_products():
|
| 36 |
"""Récupère la liste complète des produits agricoles."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
url = f"{BASE_URL}/pesticide_residues_products"
|
| 38 |
params = {
|
| 39 |
"format": DEFAULT_FORMAT,
|
|
@@ -129,9 +164,82 @@ def get_residue_name(residue_id):
|
|
| 129 |
logger.error(f"Erreur lors de la récupération du nom de la substance {residue_id}: {str(e)}")
|
| 130 |
return f"Substance {residue_id}"
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
@st.cache_data(ttl=1800) # Cache de 30 minutes
|
| 133 |
def get_mrls_for_product(product_id, time_filter=None):
|
| 134 |
"""Récupère les LMR pour un produit spécifique avec gestion de la pagination."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
url = f"{BASE_URL}/pesticide_residues_mrls"
|
| 136 |
params = {
|
| 137 |
"format": DEFAULT_FORMAT,
|
|
@@ -236,9 +344,27 @@ def get_mrls_for_product(product_id, time_filter=None):
|
|
| 236 |
|
| 237 |
def make_api_request(url, params=None):
|
| 238 |
"""Effectue une requête API avec gestion des erreurs et tentatives."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
for attempt in range(RETRY_ATTEMPTS):
|
| 240 |
try:
|
| 241 |
-
response = requests.get(url, params=params)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
response.raise_for_status()
|
| 243 |
return response
|
| 244 |
|
|
@@ -250,6 +376,10 @@ def make_api_request(url, params=None):
|
|
| 250 |
else:
|
| 251 |
logger.error(f"Échec de la requête après {RETRY_ATTEMPTS} tentatives: {url}")
|
| 252 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
def generate_download_link(df, filename="pesticide_data.csv"):
|
| 255 |
"""Génère un lien de téléchargement pour un DataFrame."""
|
|
@@ -306,6 +436,28 @@ def main():
|
|
| 306 |
st.title("Pesticide Data Explorer")
|
| 307 |
st.markdown("Application pour explorer les données de pesticides de l'Union Européenne.")
|
| 308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
# Bouton de réinitialisation du cache
|
| 310 |
if st.button("Réinitialiser le cache"):
|
| 311 |
st.cache_data.clear()
|
|
@@ -315,11 +467,77 @@ def main():
|
|
| 315 |
st.title("Base de données des pesticides de l'UE")
|
| 316 |
st.markdown("Explorez les Limites Maximales de Résidus (LMR) par produit agricole")
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
# Récupération de la liste des produits
|
| 319 |
df_products = get_products()
|
| 320 |
|
| 321 |
-
if df_products.empty:
|
| 322 |
-
st.warning("Aucun produit disponible. Veuillez vérifier la connexion à l'API.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
return
|
| 324 |
|
| 325 |
# Interface de sélection
|
|
@@ -403,5 +621,4 @@ def main():
|
|
| 403 |
st.info("Pas assez de données pour générer un graphique.")
|
| 404 |
|
| 405 |
if __name__ == "__main__":
|
| 406 |
-
main()
|
| 407 |
-
|
|
|
|
| 30 |
RETRY_ATTEMPTS = 3
|
| 31 |
RETRY_DELAY = 2 # secondes
|
| 32 |
|
| 33 |
+
# Paramètres d'authentification
|
| 34 |
+
def get_api_key():
|
| 35 |
+
"""Récupère la clé API depuis les secrets Streamlit ou l'interface utilisateur."""
|
| 36 |
+
# Essayer de récupérer depuis les secrets Streamlit
|
| 37 |
+
try:
|
| 38 |
+
return st.secrets["eu_pesticides_api_key"]
|
| 39 |
+
except:
|
| 40 |
+
# Si pas disponible dans les secrets, demander à l'utilisateur
|
| 41 |
+
if "api_key" not in st.session_state:
|
| 42 |
+
st.session_state.api_key = ""
|
| 43 |
+
|
| 44 |
+
return st.session_state.api_key
|
| 45 |
+
|
| 46 |
# Cache pour les données
|
| 47 |
+
# Fonction pour charger des données de démonstration
|
| 48 |
+
def load_demo_data():
|
| 49 |
+
"""Charge des données de démonstration pour montrer les fonctionnalités de l'application."""
|
| 50 |
+
# Produits de démonstration
|
| 51 |
+
products_data = [
|
| 52 |
+
{"product_id": 1, "product_name": "Pommes", "product_code": "0130010"},
|
| 53 |
+
{"product_id": 2, "product_name": "Poires", "product_code": "0130020"},
|
| 54 |
+
{"product_id": 3, "product_name": "Tomates", "product_code": "0231010"},
|
| 55 |
+
{"product_id": 4, "product_name": "Raisins", "product_code": "0151010"},
|
| 56 |
+
{"product_id": 5, "product_name": "Oranges", "product_code": "0110010"},
|
| 57 |
+
{"product_id": 6, "product_name": "Blé", "product_code": "0500010"},
|
| 58 |
+
{"product_id": 7, "product_name": "Riz", "product_code": "0500060"},
|
| 59 |
+
{"product_id": 8, "product_name": "Carottes", "product_code": "0213020"},
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
# Convertir en DataFrame
|
| 63 |
+
return pd.DataFrame(products_data)
|
| 64 |
+
|
| 65 |
@st.cache_data(ttl=3600) # Cache d'une heure
|
| 66 |
def get_products():
|
| 67 |
"""Récupère la liste complète des produits agricoles."""
|
| 68 |
+
# Vérifier si le mode démo est activé
|
| 69 |
+
if hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode:
|
| 70 |
+
return load_demo_data()
|
| 71 |
+
|
| 72 |
url = f"{BASE_URL}/pesticide_residues_products"
|
| 73 |
params = {
|
| 74 |
"format": DEFAULT_FORMAT,
|
|
|
|
| 164 |
logger.error(f"Erreur lors de la récupération du nom de la substance {residue_id}: {str(e)}")
|
| 165 |
return f"Substance {residue_id}"
|
| 166 |
|
| 167 |
+
# Fonction pour charger des LMR de démonstration
|
| 168 |
+
def load_demo_mrls(product_id, time_filter=None):
|
| 169 |
+
"""Charge des LMR de démonstration pour le produit sélectionné."""
|
| 170 |
+
# Substances de démonstration
|
| 171 |
+
substances = {
|
| 172 |
+
1: "Glyphosate",
|
| 173 |
+
2: "Clothianidine",
|
| 174 |
+
3: "Pendiméthaline",
|
| 175 |
+
4: "Imidaclopride",
|
| 176 |
+
5: "Thiamethoxam",
|
| 177 |
+
6: "Lambda-cyhalothrine",
|
| 178 |
+
7: "Acetamipride"
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
# Générer des données aléatoires
|
| 182 |
+
import random
|
| 183 |
+
|
| 184 |
+
# Date de base
|
| 185 |
+
base_date = datetime.datetime.now().date()
|
| 186 |
+
|
| 187 |
+
# Générer entre 20 et 40 entrées aléatoires
|
| 188 |
+
num_entries = random.randint(20, 40)
|
| 189 |
+
|
| 190 |
+
mrls_data = []
|
| 191 |
+
|
| 192 |
+
for _ in range(num_entries):
|
| 193 |
+
# Choisir une substance aléatoire
|
| 194 |
+
substance_id = random.choice(list(substances.keys()))
|
| 195 |
+
|
| 196 |
+
# Générer une date aléatoire dans les 3 dernières années
|
| 197 |
+
days_offset = random.randint(-1095, 180) # -3 ans à +6 mois
|
| 198 |
+
random_date = base_date + datetime.timedelta(days=days_offset)
|
| 199 |
+
|
| 200 |
+
# Générer une valeur LMR aléatoire (entre 0.01 et 5.0)
|
| 201 |
+
mrl_value = round(random.uniform(0.01, 5.0), 2)
|
| 202 |
+
|
| 203 |
+
# Règlement fictif
|
| 204 |
+
regulation = f"{random.randint(2020, 2024)}/{random.randint(100, 999)}"
|
| 205 |
+
|
| 206 |
+
mrls_data.append({
|
| 207 |
+
"pesticide_residue_id": substance_id,
|
| 208 |
+
"substance_name": substances[substance_id],
|
| 209 |
+
"mrl_value": mrl_value,
|
| 210 |
+
"application_date": pd.to_datetime(random_date),
|
| 211 |
+
"regulation_number": regulation
|
| 212 |
+
})
|
| 213 |
+
|
| 214 |
+
# Convertir en DataFrame
|
| 215 |
+
df_mrls = pd.DataFrame(mrls_data)
|
| 216 |
+
|
| 217 |
+
# Appliquer le filtre temporel si spécifié
|
| 218 |
+
if time_filter:
|
| 219 |
+
today = datetime.datetime.now().date()
|
| 220 |
+
|
| 221 |
+
if time_filter == 'last_week':
|
| 222 |
+
one_week_ago = today - datetime.timedelta(days=7)
|
| 223 |
+
df_mrls = df_mrls[df_mrls['application_date'].dt.date >= one_week_ago]
|
| 224 |
+
|
| 225 |
+
elif time_filter == 'last_month':
|
| 226 |
+
one_month_ago = today - datetime.timedelta(days=30)
|
| 227 |
+
df_mrls = df_mrls[df_mrls['application_date'].dt.date >= one_month_ago]
|
| 228 |
+
|
| 229 |
+
elif time_filter == 'next_six_months':
|
| 230 |
+
six_months_later = today + datetime.timedelta(days=180)
|
| 231 |
+
df_mrls = df_mrls[(df_mrls['application_date'].dt.date >= today) &
|
| 232 |
+
(df_mrls['application_date'].dt.date <= six_months_later)]
|
| 233 |
+
|
| 234 |
+
return df_mrls
|
| 235 |
+
|
| 236 |
@st.cache_data(ttl=1800) # Cache de 30 minutes
|
| 237 |
def get_mrls_for_product(product_id, time_filter=None):
|
| 238 |
"""Récupère les LMR pour un produit spécifique avec gestion de la pagination."""
|
| 239 |
+
# Vérifier si le mode démo est activé
|
| 240 |
+
if hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode:
|
| 241 |
+
return load_demo_mrls(product_id, time_filter)
|
| 242 |
+
|
| 243 |
url = f"{BASE_URL}/pesticide_residues_mrls"
|
| 244 |
params = {
|
| 245 |
"format": DEFAULT_FORMAT,
|
|
|
|
| 344 |
|
| 345 |
def make_api_request(url, params=None):
|
| 346 |
"""Effectue une requête API avec gestion des erreurs et tentatives."""
|
| 347 |
+
# Récupérer la clé API
|
| 348 |
+
api_key = get_api_key()
|
| 349 |
+
|
| 350 |
+
# Ajouter les en-têtes d'authentification si une clé API est disponible
|
| 351 |
+
headers = {}
|
| 352 |
+
if api_key:
|
| 353 |
+
headers["Authorization"] = f"Bearer {api_key}"
|
| 354 |
+
# Selon l'API, il peut y avoir d'autres formats d'authentification comme :
|
| 355 |
+
# headers["X-API-Key"] = api_key
|
| 356 |
+
# ou
|
| 357 |
+
# headers["Ocp-Apim-Subscription-Key"] = api_key
|
| 358 |
+
|
| 359 |
for attempt in range(RETRY_ATTEMPTS):
|
| 360 |
try:
|
| 361 |
+
response = requests.get(url, params=params, headers=headers)
|
| 362 |
+
|
| 363 |
+
# Si nous obtenons une erreur 403 et que nous n'avons pas de clé API,
|
| 364 |
+
# lever une exception spécifique
|
| 365 |
+
if response.status_code == 403 and not api_key:
|
| 366 |
+
raise Exception("Authentification requise pour accéder à l'API")
|
| 367 |
+
|
| 368 |
response.raise_for_status()
|
| 369 |
return response
|
| 370 |
|
|
|
|
| 376 |
else:
|
| 377 |
logger.error(f"Échec de la requête après {RETRY_ATTEMPTS} tentatives: {url}")
|
| 378 |
return None
|
| 379 |
+
|
| 380 |
+
except Exception as e:
|
| 381 |
+
logger.error(f"Erreur: {str(e)}")
|
| 382 |
+
return None
|
| 383 |
|
| 384 |
def generate_download_link(df, filename="pesticide_data.csv"):
|
| 385 |
"""Génère un lien de téléchargement pour un DataFrame."""
|
|
|
|
| 436 |
st.title("Pesticide Data Explorer")
|
| 437 |
st.markdown("Application pour explorer les données de pesticides de l'Union Européenne.")
|
| 438 |
|
| 439 |
+
# Section d'authentification
|
| 440 |
+
st.subheader("Authentification API")
|
| 441 |
+
api_key = st.text_input("Clé API (obligatoire)",
|
| 442 |
+
type="password",
|
| 443 |
+
help="Entrez votre clé API pour accéder aux données")
|
| 444 |
+
st.session_state.api_key = api_key
|
| 445 |
+
|
| 446 |
+
st.info("""
|
| 447 |
+
Pour obtenir une clé API, inscrivez-vous sur le portail européen des données
|
| 448 |
+
et demandez l'accès à l'API des pesticides.
|
| 449 |
+
""")
|
| 450 |
+
|
| 451 |
+
# Mode démonstration
|
| 452 |
+
st.subheader("Mode démonstration")
|
| 453 |
+
use_demo = st.checkbox("Utiliser des données de démonstration",
|
| 454 |
+
help="Activez cette option pour utiliser des données fictives et tester l'application sans clé API")
|
| 455 |
+
if use_demo:
|
| 456 |
+
st.session_state.demo_mode = True
|
| 457 |
+
st.success("Mode démonstration activé!")
|
| 458 |
+
else:
|
| 459 |
+
st.session_state.demo_mode = False
|
| 460 |
+
|
| 461 |
# Bouton de réinitialisation du cache
|
| 462 |
if st.button("Réinitialiser le cache"):
|
| 463 |
st.cache_data.clear()
|
|
|
|
| 467 |
st.title("Base de données des pesticides de l'UE")
|
| 468 |
st.markdown("Explorez les Limites Maximales de Résidus (LMR) par produit agricole")
|
| 469 |
|
| 470 |
+
# Vérifier si le mode démo est activé ou si une clé API est fournie
|
| 471 |
+
if not ((hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode) or get_api_key()):
|
| 472 |
+
st.error("""
|
| 473 |
+
⚠️ Authentification requise!
|
| 474 |
+
|
| 475 |
+
Cette API nécessite une clé d'accès. Veuillez soit:
|
| 476 |
+
1. Entrer votre clé API dans la barre latérale pour accéder aux données réelles, ou
|
| 477 |
+
2. Activer le "Mode démonstration" pour utiliser des données fictives.
|
| 478 |
+
|
| 479 |
+
Erreur actuelle: 403 Forbidden - L'accès à l'API est refusé sans authentification valide.
|
| 480 |
+
""")
|
| 481 |
+
|
| 482 |
+
# Afficher une section d'aide pour obtenir une clé API
|
| 483 |
+
col1, col2 = st.columns(2)
|
| 484 |
+
|
| 485 |
+
with col1:
|
| 486 |
+
st.subheader("Option 1: Obtenir une clé API")
|
| 487 |
+
st.markdown("""
|
| 488 |
+
1. Rendez-vous sur le [Portail des données de l'UE](https://data.europa.eu/)
|
| 489 |
+
2. Créez un compte ou connectez-vous
|
| 490 |
+
3. Naviguez vers la section API des pesticides
|
| 491 |
+
4. Demandez une clé d'accès pour l'API
|
| 492 |
+
5. Copiez la clé dans le champ "Clé API" dans la barre latérale
|
| 493 |
+
""")
|
| 494 |
+
|
| 495 |
+
with col2:
|
| 496 |
+
st.subheader("Option 2: Mode démonstration")
|
| 497 |
+
st.markdown("""
|
| 498 |
+
1. Activez l'option "Utiliser des données de démonstration" dans la barre latérale
|
| 499 |
+
2. Utilisez l'application avec des données fictives pour tester les fonctionnalités
|
| 500 |
+
3. Notez que les données affichées ne seront pas réelles
|
| 501 |
+
""")
|
| 502 |
+
|
| 503 |
+
if st.button("Activer le mode démonstration"):
|
| 504 |
+
st.session_state.demo_mode = True
|
| 505 |
+
st.experimental_rerun()
|
| 506 |
+
|
| 507 |
+
return
|
| 508 |
+
|
| 509 |
+
# Afficher un message si en mode démonstration
|
| 510 |
+
if hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode:
|
| 511 |
+
st.warning("""
|
| 512 |
+
🔍 **Mode démonstration activé**
|
| 513 |
+
|
| 514 |
+
Vous utilisez des données fictives pour tester l'application.
|
| 515 |
+
Les valeurs affichées ne correspondent pas aux données réelles de l'API de l'UE.
|
| 516 |
+
|
| 517 |
+
Pour accéder aux données réelles, désactivez le mode démonstration et fournissez une clé API valide.
|
| 518 |
+
""")
|
| 519 |
+
|
| 520 |
# Récupération de la liste des produits
|
| 521 |
df_products = get_products()
|
| 522 |
|
| 523 |
+
if df_products.empty and not (hasattr(st.session_state, 'demo_mode') and st.session_state.demo_mode):
|
| 524 |
+
st.warning("Aucun produit disponible. Veuillez vérifier votre clé API ou la connexion à l'API.")
|
| 525 |
+
|
| 526 |
+
# Ajouter des options alternatives
|
| 527 |
+
st.subheader("Options alternatives")
|
| 528 |
+
st.markdown("""
|
| 529 |
+
Si vous rencontrez des difficultés pour accéder à l'API en direct, vous pouvez:
|
| 530 |
+
|
| 531 |
+
1. **Activer le mode démonstration** dans la barre latérale pour tester les fonctionnalités
|
| 532 |
+
2. Accéder directement aux données via le site web de l'UE:
|
| 533 |
+
- [Base de données des pesticides de l'UE](https://ec.europa.eu/food/plant/pesticides/eu-pesticides-database/products/)
|
| 534 |
+
- [Téléchargement direct des fichiers CSV](https://ec.europa.eu/food/plant/pesticides/eu-pesticides-database/products/download/)
|
| 535 |
+
""")
|
| 536 |
+
|
| 537 |
+
if st.button("Activer le mode démonstration", key="demo_btn_alt"):
|
| 538 |
+
st.session_state.demo_mode = True
|
| 539 |
+
st.experimental_rerun()
|
| 540 |
+
|
| 541 |
return
|
| 542 |
|
| 543 |
# Interface de sélection
|
|
|
|
| 621 |
st.info("Pas assez de données pour générer un graphique.")
|
| 622 |
|
| 623 |
if __name__ == "__main__":
|
| 624 |
+
main()
|
|
|