MMOON commited on
Commit
5403c65
·
verified ·
1 Parent(s): 0e3b519

Rename apptest.py to app.py

Browse files
Files changed (2) hide show
  1. app.py +249 -0
  2. apptest.py +0 -0
app.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import urllib.parse
10
+
11
+ # Configuration Streamlit
12
+ st.set_page_config(page_title="Pesticide Data Explorer", page_icon="🌿", layout="wide")
13
+
14
+ # Configuration logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s - %(levelname)s - %(message)s",
18
+ handlers=[logging.FileHandler("pesticide_app.log", encoding="utf-8"), logging.StreamHandler()],
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class PesticideDataFetcher:
23
+ BASE_URL = "https://api.datalake.sante.service.ec.europa.eu/sante/pesticides"
24
+ HEADERS = {
25
+ "Content-Type": "application/json",
26
+ "Cache-Control": "no-cache",
27
+ "User-Agent": "Mozilla/5.0"
28
+ }
29
+
30
+ def __init__(self):
31
+ self.session = requests.Session()
32
+ self.session.headers.update(self.HEADERS)
33
+
34
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
35
+ def fetch_data(self, url: str, params: Optional[Dict] = None) -> Dict[str, Any]:
36
+ """Effectue une requête GET avec gestion des erreurs améliorée"""
37
+ try:
38
+ response = self.session.get(url, params=params, timeout=15)
39
+ response.raise_for_status()
40
+ return response.json()
41
+ except requests.RequestException as e:
42
+ logger.error(f"Erreur API : {e}")
43
+ if "timeout" in str(e).lower():
44
+ logger.warning("Timeout - tentative de reconnexion...")
45
+ return {}
46
+
47
+ def get_mrls(self, product_ids: Optional[List[int]] = None) -> List[Dict]:
48
+ """Récupère les LMR pour une liste de produits, gère la pagination."""
49
+ all_mrls = []
50
+
51
+ if not product_ids:
52
+ #If not product specified, get them all
53
+ url = f"{self.BASE_URL}/pesticide_residues_mrls?format=json&api-version=v2.0"
54
+ while url :
55
+ response = self.fetch_data(url)
56
+ if response and "value" in response:
57
+ all_mrls.extend(response["value"])
58
+ next_link = response.get("nextLink")
59
+ if next_link:
60
+ url = urllib.parse.urljoin(self.BASE_URL, next_link)
61
+ else:
62
+ url = None
63
+ return all_mrls
64
+
65
+ for product_id in product_ids:
66
+ url = f"{self.BASE_URL}/pesticide_residues_products/{product_id}/mrls?format=json&language=FR&api-version=v2.0"
67
+ while url:
68
+ response = self.fetch_data(url)
69
+ if response and "value" in response:
70
+ all_mrls.extend(response["value"])
71
+ next_link = response.get("nextLink")
72
+ if next_link:
73
+ url = urllib.parse.urljoin(self.BASE_URL, next_link)
74
+ else:
75
+ url = None # No more pages, exit the inner loop
76
+
77
+ return all_mrls
78
+
79
+ @st.cache_data(ttl=3600) # Cache augmented to 1 hour
80
+ def get_products() -> List[Dict]:
81
+ fetcher = PesticideDataFetcher()
82
+ url = f"{fetcher.BASE_URL}/pesticide_residues_products?format=json&language=FR&api-version=v2.0"
83
+
84
+ all_products = []
85
+ while url:
86
+ response = fetcher.fetch_data(url)
87
+ if not response or "value" not in response:
88
+ break
89
+ all_products.extend(response["value"])
90
+ next_link = response.get("nextLink")
91
+ if next_link:
92
+ url = urllib.parse.urljoin(fetcher.BASE_URL, next_link)
93
+ else:
94
+ url = None
95
+ return all_products
96
+
97
+ @st.cache_data(ttl=3600) # Cache augmented to 1 hour and corrected function
98
+ def get_all_substances() -> Dict[int, str]:
99
+ fetcher = PesticideDataFetcher()
100
+ url = f"{fetcher.BASE_URL}/active_substances?format=json&api-version=v2.0" # No language needed
101
+ all_substances = {} # Initialize as an empty dict
102
+ while url:
103
+ response = fetcher.fetch_data(url)
104
+ if not response or "value" not in response:
105
+ break
106
+ for item in response["value"]:
107
+ # Corrected Keys. Use get to avoid KeyError.
108
+ substance_id = item.get("substanceId")
109
+ substance_name = item.get("substanceName")
110
+ if substance_id and substance_name: # Check for None
111
+ all_substances[substance_id] = substance_name # int key, str value
112
+ next_link = response.get("nextLink")
113
+ if next_link:
114
+ url = urllib.parse.urljoin(fetcher.BASE_URL, next_link)
115
+ else:
116
+ url = None
117
+ return all_substances
118
+
119
+
120
+ class PesticideInterface:
121
+ def __init__(self):
122
+ self.fetcher = PesticideDataFetcher()
123
+ self.products = get_products()
124
+ self.product_choices = {p.get('productName', 'Unknown'): p.get('productId', None) for p in self.products}
125
+ # Ensure product_choices only contains valid entries
126
+ self.product_choices = {k: v for k, v in self.product_choices.items() if k is not None and v is not None}
127
+
128
+ self.substances = get_all_substances()
129
+
130
+
131
+ def get_product_details(self, product_names: List[str], future_only: bool = False) -> pd.DataFrame:
132
+ product_ids = [self.product_choices[name] for name in product_names if name in self.product_choices]
133
+ all_mrls = self.fetcher.get_mrls(product_ids)
134
+
135
+ df = pd.DataFrame(all_mrls)
136
+ if df.empty:
137
+ if future_only:
138
+ st.info("Aucun changement de LMR prévu dans les 6 prochains mois pour les produits sélectionnés.")
139
+ return df
140
+
141
+ logger.info(f"Nombre total d'entrées : {len(df)}")
142
+ logger.info(f"Colonnes disponibles : {df.columns.tolist()}")
143
+
144
+ df["Substance"] = df["pesticideResidueId"].map(self.substances)
145
+ missing_substances = df[df["Substance"].isna()]["pesticideResidueId"].unique()
146
+ if len(missing_substances) > 0:
147
+ logger.warning(f"Substances non trouvées: {missing_substances}")
148
+ df["Substance"] = df["Substance"].fillna("Inconnu")
149
+
150
+
151
+
152
+ df["Date d'application"] = pd.to_datetime(df["entryIntoForceDate"], errors="coerce")
153
+
154
+ if future_only:
155
+ now = datetime.now()
156
+ future_date = now + timedelta(days=180)
157
+ future_df = df[
158
+ (df["Date d'application"] > now) &
159
+ (df["Date d'application"] <= future_date)
160
+ ]
161
+
162
+ if future_df.empty:
163
+ 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.")
164
+ return pd.DataFrame() # Return empty df
165
+
166
+ df = future_df
167
+
168
+
169
+ df = df.rename(columns={"mrlValue": "Valeur LMR"})
170
+ df["Valeur LMR"] = pd.to_numeric(df["Valeur LMR"], errors='coerce')
171
+ columns = ["Substance", "Valeur LMR", "Date d'application"]
172
+
173
+ df = df[columns].sort_values("Date d'application", ascending=False)
174
+
175
+ return df
176
+
177
+ def create_interface(self):
178
+ st.title("🌿 Base de données des pesticides de l'UE")
179
+
180
+ col1, col2 = st.columns([3, 1])
181
+ with col1:
182
+ product_names = st.multiselect(
183
+ "Sélectionnez un ou plusieurs produits",
184
+ list(self.product_choices.keys())
185
+ )
186
+ with col2:
187
+ future_only = st.checkbox("Uniquement les 6 prochains mois", value=False)
188
+
189
+ if st.button("Afficher les données"):
190
+ if not product_names:
191
+ st.warning("Veuillez sélectionner au moins un produit.")
192
+ return
193
+
194
+ df = self.get_product_details(product_names, future_only)
195
+
196
+ if df.empty:
197
+ return # The info message has already been displayed in get_product_details
198
+ else:
199
+ if future_only:
200
+ st.markdown("### Changements de LMR prévus dans les 6 prochains mois")
201
+ else:
202
+ st.markdown("### Tableau des LMR")
203
+
204
+ df_display = df.copy()
205
+ df_display["Date d'application"] = df_display["Date d'application"].dt.strftime('%d/%m/%Y')
206
+
207
+ def format_value(val):
208
+ if pd.isna(val):
209
+ return '-'
210
+ elif isinstance(val, (int, float)):
211
+ return f"{val:.3f}" # Format numbers to 3 decimal places
212
+ return val
213
+
214
+
215
+ # Apply the custom formatting using .style.format
216
+ styled_df = df_display.style.format({
217
+ 'Valeur LMR': format_value,
218
+ })
219
+
220
+ # Add a summary of the changes
221
+ if not df.empty:
222
+ nb_changes = len(df)
223
+ st.info(f"📊 {nb_changes} entrée{'s' if nb_changes > 1 else ''} trouvée{'s' if nb_changes > 1 else ''}.")
224
+
225
+ st.dataframe(styled_df) # This is better than to_html
226
+
227
+ # Create visualizations
228
+ self.create_visualizations(df)
229
+
230
+ def create_visualizations(self, df: pd.DataFrame):
231
+ """Crée les visualisations des données"""
232
+ # Graphique d'évolution des LMR
233
+ if not df.empty:
234
+ fig1 = px.scatter(
235
+ df,
236
+ x="Date d'application",
237
+ y="Valeur LMR",
238
+ color="Substance",
239
+ title="Évolution des LMR dans le temps",
240
+ hover_data=["Substance"] # Added substance to hover
241
+ )
242
+ st.plotly_chart(fig1, use_container_width=True)
243
+
244
+ def main():
245
+ interface = PesticideInterface()
246
+ interface.create_interface()
247
+
248
+ if __name__ == "__main__":
249
+ main()
apptest.py DELETED
File without changes