Update app.py
Browse files
app.py
CHANGED
|
@@ -32,12 +32,27 @@ st.info("Les données sont chargées depuis la Plateforme SCA. Utilisez les filt
|
|
| 32 |
# 📦 Fonction pour charger les données avec cache
|
| 33 |
@st.cache_data(ttl=3600) # Mettre en cache les données pendant 1 heure
|
| 34 |
def load_data():
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
try:
|
|
|
|
| 37 |
response = requests.get(file_url, headers={'User-Agent': 'Mozilla/5.0'})
|
| 38 |
response.raise_for_status()
|
|
|
|
| 39 |
df = pd.read_excel(BytesIO(response.content), engine='openpyxl')
|
|
|
|
|
|
|
| 40 |
df.columns = df.columns.str.strip().str.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
return df
|
| 42 |
except Exception as e:
|
| 43 |
st.error(f"Erreur critique lors du chargement des données : {e}")
|
|
@@ -50,11 +65,11 @@ if df_full is None:
|
|
| 50 |
st.error("Impossible de continuer car les données n'ont pas pu être chargées.")
|
| 51 |
st.stop()
|
| 52 |
|
| 53 |
-
# --- Définition des noms de colonnes ---
|
| 54 |
COL_BUSCA = 'busca'
|
| 55 |
COL_TITRE = 'titre'
|
| 56 |
-
COL_MATRICE = 'matrices'
|
| 57 |
-
COL_DANGER = 'dangers'
|
| 58 |
COL_SECTION = 'section'
|
| 59 |
COL_TEXTE = 'texte'
|
| 60 |
COL_LIEN1 = 'lien'
|
|
@@ -64,9 +79,15 @@ COL_LIEN2 = 'lien2'
|
|
| 64 |
essential_cols = [COL_BUSCA, COL_TITRE, COL_TEXTE, COL_MATRICE, COL_DANGER]
|
| 65 |
missing_cols = [col for col in essential_cols if col not in df_full.columns]
|
| 66 |
if missing_cols:
|
| 67 |
-
st.error(f"ERREUR : Les colonnes essentielles suivantes sont manquantes : {', '.join(missing_cols)}")
|
|
|
|
| 68 |
st.stop()
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
# Tri des données
|
| 71 |
df_full = df_full.sort_values(by=COL_BUSCA, ascending=False)
|
| 72 |
|
|
@@ -76,7 +97,7 @@ with st.sidebar:
|
|
| 76 |
|
| 77 |
if st.button("🔄 Rafraîchir les données"):
|
| 78 |
st.cache_data.clear()
|
| 79 |
-
st.
|
| 80 |
|
| 81 |
with st.expander("📌 Plage de numéros de BuSCA", expanded=True):
|
| 82 |
min_val = int(df_full[COL_BUSCA].min())
|
|
@@ -84,14 +105,15 @@ with st.sidebar:
|
|
| 84 |
busca_range = st.slider("Numéros de BuSCA", min_val, max_val, (max_val - 20, max_val))
|
| 85 |
|
| 86 |
with st.expander("🌍 Matrices"):
|
| 87 |
-
|
|
|
|
| 88 |
matrices = st.multiselect("Sélectionner les matrices", options=unique_matrices)
|
| 89 |
|
| 90 |
with st.expander("⚠️ Dangers"):
|
| 91 |
-
unique_dangers = sorted(df_full[COL_DANGER].
|
| 92 |
dangers = st.multiselect("Sélectionner les dangers", options=unique_dangers)
|
| 93 |
|
| 94 |
-
# ---
|
| 95 |
with st.expander("🔎 Recherche par mots-clés"):
|
| 96 |
keywords = st.text_area("Mots-clés (séparés par des virgules)", placeholder="ex: listeria, lait, rappel...")
|
| 97 |
|
|
@@ -99,37 +121,40 @@ with st.sidebar:
|
|
| 99 |
|
| 100 |
# Logique de filtrage
|
| 101 |
df_display = df_full.copy()
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
# Affichage des résultats
|
| 135 |
st.markdown(f"### 📑 Affichage de {len(df_display)} résultats")
|
|
@@ -146,9 +171,15 @@ else:
|
|
| 146 |
|
| 147 |
st.markdown(f"**Danger :** `{danger_val}` | **Matrice :** `{matrice_val}`")
|
| 148 |
st.markdown("---")
|
| 149 |
-
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
# 📦 Fonction pour charger les données avec cache
|
| 33 |
@st.cache_data(ttl=3600) # Mettre en cache les données pendant 1 heure
|
| 34 |
def load_data():
|
| 35 |
+
# --- MODIFICATION DE L'URL ICI ---
|
| 36 |
+
file_url = "https://www.plateforme-sca.fr/media/398/download"
|
| 37 |
+
|
| 38 |
try:
|
| 39 |
+
# En Python (côté serveur), pas besoin de proxy CORS, mais on garde le User-Agent
|
| 40 |
response = requests.get(file_url, headers={'User-Agent': 'Mozilla/5.0'})
|
| 41 |
response.raise_for_status()
|
| 42 |
+
|
| 43 |
df = pd.read_excel(BytesIO(response.content), engine='openpyxl')
|
| 44 |
+
|
| 45 |
+
# Nettoyage des noms de colonnes (minuscules, sans espaces)
|
| 46 |
df.columns = df.columns.str.strip().str.lower()
|
| 47 |
+
|
| 48 |
+
# --- ROBUSTESSE : Renommage pour gérer Singulier/Pluriel ---
|
| 49 |
+
# Si le fichier contient "matrice" au lieu de "matrices", on normalise
|
| 50 |
+
rename_map = {
|
| 51 |
+
'matrice': 'matrices',
|
| 52 |
+
'danger': 'dangers'
|
| 53 |
+
}
|
| 54 |
+
df.rename(columns=rename_map, inplace=True)
|
| 55 |
+
|
| 56 |
return df
|
| 57 |
except Exception as e:
|
| 58 |
st.error(f"Erreur critique lors du chargement des données : {e}")
|
|
|
|
| 65 |
st.error("Impossible de continuer car les données n'ont pas pu être chargées.")
|
| 66 |
st.stop()
|
| 67 |
|
| 68 |
+
# --- Définition des noms de colonnes (Normalisés) ---
|
| 69 |
COL_BUSCA = 'busca'
|
| 70 |
COL_TITRE = 'titre'
|
| 71 |
+
COL_MATRICE = 'matrices' # On utilise le pluriel car on a normalisé ci-dessus
|
| 72 |
+
COL_DANGER = 'dangers' # On utilise le pluriel car on a normalisé ci-dessus
|
| 73 |
COL_SECTION = 'section'
|
| 74 |
COL_TEXTE = 'texte'
|
| 75 |
COL_LIEN1 = 'lien'
|
|
|
|
| 79 |
essential_cols = [COL_BUSCA, COL_TITRE, COL_TEXTE, COL_MATRICE, COL_DANGER]
|
| 80 |
missing_cols = [col for col in essential_cols if col not in df_full.columns]
|
| 81 |
if missing_cols:
|
| 82 |
+
st.error(f"ERREUR : Les colonnes essentielles suivantes sont manquantes dans le fichier Excel : {', '.join(missing_cols)}")
|
| 83 |
+
st.write("Colonnes trouvées :", df_full.columns.tolist())
|
| 84 |
st.stop()
|
| 85 |
|
| 86 |
+
# Nettoyage des données (suppression des lignes sans N° BuSCA)
|
| 87 |
+
df_full = df_full.dropna(subset=[COL_BUSCA])
|
| 88 |
+
# Conversion du N° BuSCA en entier
|
| 89 |
+
df_full[COL_BUSCA] = df_full[COL_BUSCA].astype(int)
|
| 90 |
+
|
| 91 |
# Tri des données
|
| 92 |
df_full = df_full.sort_values(by=COL_BUSCA, ascending=False)
|
| 93 |
|
|
|
|
| 97 |
|
| 98 |
if st.button("🔄 Rafraîchir les données"):
|
| 99 |
st.cache_data.clear()
|
| 100 |
+
st.rerun() # Utilisation de st.rerun() au lieu de experimental_rerun
|
| 101 |
|
| 102 |
with st.expander("📌 Plage de numéros de BuSCA", expanded=True):
|
| 103 |
min_val = int(df_full[COL_BUSCA].min())
|
|
|
|
| 105 |
busca_range = st.slider("Numéros de BuSCA", min_val, max_val, (max_val - 20, max_val))
|
| 106 |
|
| 107 |
with st.expander("🌍 Matrices"):
|
| 108 |
+
# Conversion en string pour éviter les erreurs de tri si données mixtes
|
| 109 |
+
unique_matrices = sorted(df_full[COL_MATRICE].fillna('Non spécifié').astype(str).unique())
|
| 110 |
matrices = st.multiselect("Sélectionner les matrices", options=unique_matrices)
|
| 111 |
|
| 112 |
with st.expander("⚠️ Dangers"):
|
| 113 |
+
unique_dangers = sorted(df_full[COL_DANGER].fillna('Non spécifié').astype(str).unique())
|
| 114 |
dangers = st.multiselect("Sélectionner les dangers", options=unique_dangers)
|
| 115 |
|
| 116 |
+
# --- FILTRE TEXTE LIBRE ---
|
| 117 |
with st.expander("🔎 Recherche par mots-clés"):
|
| 118 |
keywords = st.text_area("Mots-clés (séparés par des virgules)", placeholder="ex: listeria, lait, rappel...")
|
| 119 |
|
|
|
|
| 121 |
|
| 122 |
# Logique de filtrage
|
| 123 |
df_display = df_full.copy()
|
| 124 |
+
|
| 125 |
+
# Note: Streamlit relance le script à chaque interaction, donc si on n'appuie pas sur le bouton
|
| 126 |
+
# on affiche quand même les données filtrées par défaut (tout ou dernière action).
|
| 127 |
+
# Si vous voulez que rien ne change tant qu'on ne clique pas, il faut gérer le state,
|
| 128 |
+
# mais ici on applique la logique standard :
|
| 129 |
+
if apply_filter or True: # 'or True' permet un affichage dynamique réactif immédiat (optionnel selon préférence UX)
|
| 130 |
+
# Filtre par plage de BuSCA
|
| 131 |
+
df_display = df_display[
|
| 132 |
+
(df_display[COL_BUSCA] >= busca_range[0]) &
|
| 133 |
+
(df_display[COL_BUSCA] <= busca_range[1])
|
| 134 |
+
]
|
| 135 |
+
# Filtre par listes
|
| 136 |
+
if matrices:
|
| 137 |
+
df_display = df_display[df_display[COL_MATRICE].astype(str).isin(matrices)]
|
| 138 |
+
if dangers:
|
| 139 |
+
df_display = df_display[df_display[COL_DANGER].astype(str).isin(dangers)]
|
| 140 |
+
|
| 141 |
+
# --- LOGIQUE DE FILTRAGE PAR MOTS-CLÉS ---
|
| 142 |
+
if keywords:
|
| 143 |
+
# Prépare la liste de mots-clés : minuscule, sans espaces superflus
|
| 144 |
+
keyword_list = [kw.strip().lower() for kw in keywords.split(',') if kw.strip()]
|
| 145 |
+
|
| 146 |
+
# Applique le filtre si des mots-clés ont été saisis
|
| 147 |
+
if keyword_list:
|
| 148 |
+
df_display = df_display[df_display.apply(
|
| 149 |
+
lambda row: any(
|
| 150 |
+
kw in str(row[COL_TITRE]).lower() or
|
| 151 |
+
kw in str(row[COL_TEXTE]).lower() or
|
| 152 |
+
kw in str(row[COL_DANGER]).lower() or
|
| 153 |
+
kw in str(row[COL_MATRICE]).lower()
|
| 154 |
+
for kw in keyword_list
|
| 155 |
+
),
|
| 156 |
+
axis=1
|
| 157 |
+
)]
|
| 158 |
|
| 159 |
# Affichage des résultats
|
| 160 |
st.markdown(f"### 📑 Affichage de {len(df_display)} résultats")
|
|
|
|
| 171 |
|
| 172 |
st.markdown(f"**Danger :** `{danger_val}` | **Matrice :** `{matrice_val}`")
|
| 173 |
st.markdown("---")
|
| 174 |
+
# Remplacement des sauts de ligne pour un affichage propre
|
| 175 |
+
texte_content = str(row.get(COL_TEXTE, 'Texte manquant')).replace('\n', ' \n')
|
| 176 |
+
st.markdown(texte_content)
|
| 177 |
|
| 178 |
+
st.markdown("---")
|
| 179 |
+
col1, col2 = st.columns(2)
|
| 180 |
+
with col1:
|
| 181 |
+
if pd.notna(row.get(COL_LIEN1)):
|
| 182 |
+
st.link_button("🔗 Lien Principal", row[COL_LIEN1])
|
| 183 |
+
with col2:
|
| 184 |
+
if pd.notna(row.get(COL_LIEN2)):
|
| 185 |
+
st.link_button("🔗 Lien Secondaire", row[COL_LIEN2])
|