Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +459 -231
src/streamlit_app.py
CHANGED
|
@@ -1,259 +1,487 @@
|
|
| 1 |
-
# app.py
|
| 2 |
import streamlit as st
|
|
|
|
| 3 |
import pandas as pd
|
|
|
|
|
|
|
| 4 |
import json
|
| 5 |
-
import
|
| 6 |
-
import
|
|
|
|
|
|
|
| 7 |
|
| 8 |
# Configuration de la page
|
| 9 |
st.set_page_config(
|
| 10 |
page_title="Moniteur Codex Alimentarius",
|
| 11 |
page_icon="📋",
|
| 12 |
layout="wide",
|
| 13 |
-
initial_sidebar_state="expanded"
|
| 14 |
)
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
except Exception as e:
|
| 72 |
-
st.error(f"❌ Erreur lors du chargement des données extraites: {e}")
|
| 73 |
-
else:
|
| 74 |
-
st.error("❌ L'extraction a échoué ou le fichier n'a pas été créé.")
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
st.session_state.data_loaded = True
|
| 138 |
-
st.session_state.last_extraction_file = file_path
|
| 139 |
-
st.success(f"✅ Données chargées depuis {selected_file}!")
|
| 140 |
-
st.experimental_rerun()
|
| 141 |
-
else:
|
| 142 |
-
st.error("❌ Aucun document trouvé dans le fichier sélectionné.")
|
| 143 |
-
except Exception as e:
|
| 144 |
-
st.error(f"❌ Erreur lors du chargement du fichier {selected_file}: {e}")
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
#
|
| 150 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
col1, col2, col3, col4 = st.columns(4)
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
# Filtres
|
| 158 |
-
st.
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
filtered_df = df.copy()
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
filtered_df['title'].str.contains(search_term, case=False, na=False) |
|
| 180 |
-
filtered_df['code'].str.contains(search_term, case=False, na=False) |
|
| 181 |
-
filtered_df['committee'].str.contains(search_term, case=False, na=False)
|
| 182 |
-
)
|
| 183 |
-
filtered_df = filtered_df[mask]
|
| 184 |
-
if selected_category != "Toutes les catégories":
|
| 185 |
filtered_df = filtered_df[filtered_df['category_name'] == selected_category]
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
filtered_df = filtered_df[filtered_df['is_new']]
|
| 190 |
-
|
|
|
|
| 191 |
filtered_df = filtered_df[filtered_df['is_2024']]
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
sort_col, ascending = sort_options[selected_sort]
|
| 205 |
-
# Pour le tri sur 'title' et 'committee', pandas triera par ordre lexicographique
|
| 206 |
-
filtered_df = filtered_df.sort_values(by=[sort_col], ascending=ascending).reset_index(drop=True)
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
# Affichage des documents
|
| 210 |
-
if filtered_df.empty:
|
| 211 |
-
st.info("🔍 Aucun document trouvé pour les critères sélectionnés.")
|
| 212 |
-
else:
|
| 213 |
-
# Afficher le nombre de résultats
|
| 214 |
-
st.write(f"Affichage de {len(filtered_df)} document(s) sur {len(df)}.")
|
| 215 |
-
|
| 216 |
-
# Utiliser st.dataframe pour un affichage interactif
|
| 217 |
-
# Ou créer une liste personnalisée comme dans l'HTML
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
-
#
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
}
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
import requests
|
| 3 |
import pandas as pd
|
| 4 |
+
import re
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
import json
|
| 7 |
+
import plotly.express as px
|
| 8 |
+
import plotly.graph_objects as go
|
| 9 |
+
from bs4 import BeautifulSoup
|
| 10 |
+
import time
|
| 11 |
|
| 12 |
# Configuration de la page
|
| 13 |
st.set_page_config(
|
| 14 |
page_title="Moniteur Codex Alimentarius",
|
| 15 |
page_icon="📋",
|
| 16 |
layout="wide",
|
| 17 |
+
initial_sidebar_state="expanded"
|
| 18 |
)
|
| 19 |
|
| 20 |
+
# URLs du Codex Alimentarius
|
| 21 |
+
CODEX_URLS = {
|
| 22 |
+
'guidelines': {
|
| 23 |
+
'name': 'Directives (CXG)',
|
| 24 |
+
'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/guidelines/fr/',
|
| 25 |
+
'prefix': 'CXG'
|
| 26 |
+
},
|
| 27 |
+
'standards': {
|
| 28 |
+
'name': 'Normes (CXS)',
|
| 29 |
+
'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/list-standards/fr/',
|
| 30 |
+
'prefix': 'CXS'
|
| 31 |
+
},
|
| 32 |
+
'codes': {
|
| 33 |
+
'name': 'Codes de Pratique (CXC)',
|
| 34 |
+
'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/codes-of-practice/fr/',
|
| 35 |
+
'prefix': 'CXC'
|
| 36 |
+
},
|
| 37 |
+
'misc': {
|
| 38 |
+
'name': 'Documents Divers',
|
| 39 |
+
'url': 'https://www.fao.org/fao-who-codexalimentarius/codex-texts/miscellaneous/fr/',
|
| 40 |
+
'prefix': 'CXM'
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
|
| 44 |
+
@st.cache_data(ttl=3600) # Cache pour 1 heure
|
| 45 |
+
def extract_documents_from_url(url, category):
|
| 46 |
+
"""Extrait les documents d'une page du Codex Alimentarius"""
|
| 47 |
+
try:
|
| 48 |
+
headers = {
|
| 49 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
response = requests.get(url, headers=headers, timeout=30)
|
| 53 |
+
response.raise_for_status()
|
| 54 |
+
|
| 55 |
+
# Parser le HTML
|
| 56 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 57 |
+
|
| 58 |
+
# Extraire le texte et chercher les patterns de documents
|
| 59 |
+
text = soup.get_text()
|
| 60 |
+
|
| 61 |
+
# Pattern pour les documents: CODE | TITRE | COMITE | ANNEE | |
|
| 62 |
+
pattern = r'(CX[GSC][\w\-R]*\d+(?:-\d+)?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*(\d{4})\s*\|'
|
| 63 |
+
|
| 64 |
+
documents = []
|
| 65 |
+
matches = re.findall(pattern, text)
|
| 66 |
+
|
| 67 |
+
for match in matches:
|
| 68 |
+
code, title, committee, year = match
|
| 69 |
+
documents.append({
|
| 70 |
+
'code': code.strip(),
|
| 71 |
+
'title': title.strip(),
|
| 72 |
+
'committee': committee.strip(),
|
| 73 |
+
'year': int(year),
|
| 74 |
+
'category': category,
|
| 75 |
+
'category_name': CODEX_URLS[category]['name'],
|
| 76 |
+
'is_new': int(year) >= 2023,
|
| 77 |
+
'is_2024': int(year) == 2024,
|
| 78 |
+
'source_url': url,
|
| 79 |
+
'extracted_at': datetime.now().isoformat()
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
return documents
|
| 83 |
+
|
| 84 |
+
except Exception as e:
|
| 85 |
+
st.error(f"Erreur lors de l'extraction de {CODEX_URLS[category]['name']}: {str(e)}")
|
| 86 |
+
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
def parse_sample_data():
|
| 89 |
+
"""Parse les données d'exemple intégrées"""
|
| 90 |
+
sample_guidelines = """CXG 105-2024 | Guidelines on the use of technology to provide food information in food labelling | CCFL | 2024 | |
|
| 91 |
+
CXG 104-2024 | Guidelines on the provision of food information for pre-packaged foods to be offered via e-commerce | CCFL | 2024 | |
|
| 92 |
+
CXG 103-2024 | Guidelines for food hygiene control measures in traditional markets for food | CCFH | 2024 | |
|
| 93 |
+
CXG 100-2023 | Guidelines for the Safe Use and Reuse of Water in Food Production and Processing | CCFH | 2024 | |
|
| 94 |
+
CXG 99-2023 | Directives pour la maîtrise des Escherichia coli producteurs de shiga-toxines (stec) dans le bœuf cru, les légumes-feuilles frais, le lait cru et les fromages au lait cru, ainsi que les graines germées | CCFH | 2024 | |
|
| 95 |
+
CXG 36-1989 | Noms de catégorie et système international de numérotation des additifs alimentaires | CCFA | 2024 | |
|
| 96 |
+
CXG 2-1985 | Directives concernant l'étiquetage nutritionnel | CCFL | 2024 | |
|
| 97 |
+
CXG 101-2023 | Guidelines on Recognition and Maintenance of Equivalence of National Food Control Systems (NFCS) | CCFICS | 2023 | |
|
| 98 |
+
CXG 102-2023 | Principles and Guidelines on the Use of Remote Audit and Inspection in Regulatory Frameworks | CCFICS | 2023 | |
|
| 99 |
+
CXG 95-2022 | Lignes directrices pour les aliments thérapeutiques prêts à l'emploi | CCNFSDU | 2023 | |
|
| 100 |
+
CXG 10-1979 | Listes consultatives d'éléments nutritifs utilisables dans les aliments diététiques ou de régime pour nourrissons et enfants en bas âge | CCNFSDU | 2023 | |
|
| 101 |
+
CXG 50-2004 | Directives générales sur l'échantillonnage | CCMAS | 2023 | |
|
| 102 |
+
CXG 38-2001 | Directives pour la conception, l'établissement, la délivrance et l'utilisation des certificats officiels génériques | CCFICS | 2021 | |
|
| 103 |
+
CXG 77-2011 | Lignes directrices pour l'analyse des risques liés à la résistance aux antimicrobiens d'origine alimentaire | TFAMR | 2021 | |
|
| 104 |
+
CXG 93-2021 | Principes et directives pour l'evaluation et l'utilisation de programmes volontaires d'assurance par des tiers | CCFICS | 2021 | |
|
| 105 |
+
CXG 94-2021 | Directives sur le suivi et la surveillance intégrés de la résistance aux antimicrobiens d'origine alimentaire | TFAMR | 2021 | |
|
| 106 |
+
CXG 96-2022 | Directives pour la gestion des épidémies biologiques d'origine alimentaire | CCFH | 2022 | |
|
| 107 |
+
CXG 97-2022 | Guidelines for the Recognition of Active Substances or Authorized Uses of Active Substances of Low Public Health Concern that are Considered Exempted from the Establishment of Maximum Residue Limits or do not give rise to Residues | CCPR | 2022 | |
|
| 108 |
+
CXG 98-2022 | Directives relatives à l'élaboration d'une législation harmonisée sur la sécurité sanitaire des aliments dans la région couverte par le Comité FAO/OMS de Coordination pour l'Afrique | CCAFRICA | 2022 | |
|
| 109 |
+
CXG 87-2016 | Directives sur la maîtrise des salmonella spp. non typhiques dans la viande de boeuf et la viande de porc | CCFH | 2016 | |
|
| 110 |
+
CXG 88-2016 | Directives pour l'application des principes généraux d'hygiène alimentaire à la maîtrise des parasites d'origine alimentaire | CCFH | 2016 | |
|
| 111 |
+
CXG 89-2016 | Principes et directives sur l'échange d'informations entre des pays importateurs et exportateurs pour soutenir le commerce alimentaire | CCFICS | 2016 | |
|
| 112 |
+
CXG 90-2017 | Directive sur les critères de performance pour les méthodes d'analyse en vue de la détermination des résidus de pesticides dans les produits destinés à l'alimentation humaine et animale | CCPR | 2017 | |
|
| 113 |
+
CXG 91-2017 | Principes et directives pour le suivi des performances de systemes nationaux de controle des aliments | CCFICS | 2017 | |
|
| 114 |
+
CXG 8-1991 | Lignes directrices pour la mise au point des préparations alimentaires complémentaires destinées aux nourrissons du deuxième âge et aux enfants en bas âge | CCNFSDU | 2017 | |
|
| 115 |
+
CXG 84-2012 | Principes et directives pour la sélection de produits représentatifs en vue d'extrapolation de limites maximales de résidus de pesticides aux groupes de produits | CCPR | 2017 | |
|
| 116 |
+
CXG 86-2015 | Directives sur la maîtrise des Trichinella Spp. dans la viande de suidés | CCFH | 2015 | |
|
| 117 |
+
CXG 83-2013 | Principes régissant l'application des procédures d'échantillonnage et d'essai dans le commerce international des denrées alimentaires | CCMAS | 2015 | |
|
| 118 |
+
CXG 82-2013 | Principes et directives concernant les systèmes nationaux de contrôle des aliments | CCFICS | 2013 | |
|
| 119 |
+
CXG 21-1997 | Principes et directives pour l'établissement et l'application de critères microbiologiques relatifs aux aliments | CCFH | 2013 | |
|
| 120 |
+
CXG 32-1999 | Directives concernant la production, la transformation, l'étiquetage et la commercialisation des aliments issus de l'agriculture biologique | CCFL | 2013 | |
|
| 121 |
+
CXG 23-1997 | Directives pour l'emploi des allégations relatives à la nutrition et à la santé | CCFL | 2013 | |
|
| 122 |
+
CXG 69-2008 | Directives relatives à la validation des mesures de maîtrise de la sécurite alimentaire | CCFH | 2013 | |"""
|
| 123 |
|
| 124 |
+
sample_standards = """CXS 359-2024 | Standard for dried or dehydrated roots, rhizomes and bulbs – Turmeric | CCSCH | 2024 | |
|
| 125 |
+
CXS 358-2024 | Standard for spices derived from dried or dehydrated fruits and berries - Allspice, juniper berry and star anise | CCSCH | 2024 | |
|
| 126 |
+
CXS 357-2024 | Standard for spices derived from dried or dehydrated fruits and berries – Small cardamom | CCSCH | 2024 | |
|
| 127 |
+
CXS 193-1995 | Norme générale pour les contaminants et les toxines présents dans les produits de consommation humaine et animale | CCCF | 2024 | |
|
| 128 |
+
CXS 1-1985 | Norme générale pour l'étiquetage des denrées alimentaires préemballées | CCFL | 2024 | |
|
| 129 |
+
CXS 283-1978 | Norme générale pour le fromage | CCMMP | 2024 | |
|
| 130 |
+
CXS 192-1995 | Norme générale pour les additifs alimentaires | CCFA | 2024 | |
|
| 131 |
+
CXS 72-1981 | Norme pour les préparations destinées aux nourrissons et les préparations données à des fins médicales spéciales aux nourrissons | CCNFSDU | 2024 | |
|
| 132 |
+
CXS 66-1981 | Norme pour les olives de table | CCPFV | 2024 | |
|
| 133 |
+
CXS 33-1981 | Norme pour les huiles d'olive et les huiles de grignons d'olive | CCFO | 2024 | |
|
| 134 |
+
CXS 19-1981 | Norme pour les graisses et les huiles comestibles non visées par des normes individuelles | CCFO | 2024 | |
|
| 135 |
+
CXS 240-2003 | Norme pour les produits aqueux a base de noix de coco – Lait de coco et crème de coco | CCPFV | 2024 | |
|
| 136 |
+
CXS 288-1976 | Norme pour la crème et les crèmes préparées | CCMMP | 2024 | |
|
| 137 |
+
CXS 115-1981 | Norme pour les cornichons (concombres) en conserve | CCPFV | 2024 | |
|
| 138 |
+
CXS 256-1999 | Norme pour les matières grasses tartinables et les mélanges tartinables | CCFO | 2024 | |
|
| 139 |
+
CXS 243-2003 | Norme pour les laits fermentés | CCMMP | 2024 | |
|
| 140 |
+
CXS 247-2005 | Norme générale pour les jus et les nectars de fruits | CCPFV | 2024 | |
|
| 141 |
+
CXS 296-2009 | Norme pour les confitures, gelées et marmelades | CCPFV | 2024 | |
|
| 142 |
+
CXS 210-1999 | Norme pour les huiles végétales portant un nom spécifique | CCFO | 2024 | |
|
| 143 |
+
CXS 211-1999 | Norme pour les graisses animales portant un nom spécifique | CCFO | 2024 | |
|
| 144 |
+
CXS 329-2017 | Norme pour les huiles de poisson | CCFO | 2024 | |
|
| 145 |
+
CXS 234-1999 | Méthodes d'analyse et d'échantillonnage recommandées | CCMAS | 2024 | |
|
| 146 |
+
CXS 356R-2023 | Norme régionale sur le jus de noni fermenté | CCNASWP | 2023 | |
|
| 147 |
+
CXS 354R-2023 | Norme régionale sur les produits à base de soja fermenté sous l'action de Bacillus spp. (Asia) | CCASIA | 2023 | |
|
| 148 |
+
CXS 355R-2023 | Norme régionale sur le riz cuit enveloppé dans des feuilles | CCASIA | 2023 | |
|
| 149 |
+
CXS 306-2023 | Norme pour la sauce au piment (sauce «chili») («piments forts») | CCPFV | 2023 | |
|
| 150 |
+
CXS 294-2023 | Norme pour la pâte de soja fermentée au piment fort | CCPFV | 2023 | |
|
| 151 |
+
CXS 151-1985 | Norme pour le gari | CCCPL | 2023 | |
|
| 152 |
+
CXS 152-1985 | Norme pour la farine de blé | CCCPL | 2023 | |
|
| 153 |
+
CXS 155-1985 | Norme pour la farine de maïs dégermé et le gruau de maïs dégermé | CCCPL | 2023 | |
|
| 154 |
+
CXS 169-1989 | Norme pour le mil chandelle en grains entiers et décortiqués | CCCPL | 2023 | |
|
| 155 |
+
CXS 172-1989 | Norme pour le sorgho en grains | CCCPL | 2023 | |
|
| 156 |
+
CXS 173-1989 | Norme pour la farine de sorgho | CCCPL | 2023 | |
|
| 157 |
+
CXS 176-1989 | Norme pour la farine comestible de manioc | CCCPL | 2023 | |
|
| 158 |
+
CXS 178-1991 | Norme pour la semoule et farine de blé dur | CCCPL | 2023 | |
|
| 159 |
+
CXS 38-1981 | Norme pour les champignons comestibles et produits dérivés | CCPFV | 2023 | |
|
| 160 |
+
CXS 39-1981 | Norme pour les champignons comestibles séchés | CCPFV | 2023 | |
|
| 161 |
+
CXS 60-1981 | Norme pour les framboises en conserve | CCPFV | 2023 | |
|
| 162 |
+
CXS 131-1981 | Norme pour les pistaches non décortiquées | CCPFV | 2023 | |
|
| 163 |
+
CXS 160-1987 | Norme pour le chutney de mangue | CCPFV | 2023 | |
|
| 164 |
+
CXS 281-1971 | Norme pour les laits concentrés | CCMMP | 2023 | |
|
| 165 |
+
CXS 282-1971 | Norme pour les laits concentrés sucrés | CCMMP | 2023 | |
|
| 166 |
+
CXS 290-1995 | Norme pour la caséine alimentaire et produits dérivés | CCMMP | 2023 | |
|
| 167 |
+
CXS 13-1981 | Norme pour les tomates en conserve | CCPFV | 2023 | |
|
| 168 |
+
CXS 73-1981 | Norme pour les aliments diversifiés de l'enfance ("baby foods") | CCNFSDU | 2023 | |
|
| 169 |
+
CXS 74-1981 | Norme pour les aliments transformés à base de céréales destinés aux nourrissons et enfants en bas âge | CCNFSDU | 2023 | |
|
| 170 |
+
CXS 181-1991 | Norme pour les préparations alimentaires utilisées dans les régimes amaigrissants | CCNFSDU | 2023 | |
|
| 171 |
+
CXS 203-1995 | Norme pour les préparations alimentaires utilisées dans les régimes amaigrissants à valeur énergétique très faible | CCNFSDU | 2023 | |
|
| 172 |
+
CXS 348-2022 | Norme pour les oignons et les echalotes | CCFFV | 2022 | |
|
| 173 |
+
CXS 349-2022 | Norme pour les baies | CCFFV | 2022 | |
|
| 174 |
+
CXS 352-2022 | Norme pour les graines séchées – Noix de muscade | CCSCH | 2022 | |
|
| 175 |
+
CXS 350R-2022 | Norme régionale sur la viande séchée | CCAFRICA | 2022 | |
|
| 176 |
+
CXS 353-2022 | Norme pour le piment et le paprika séchés ou déshydratés | CCSCH | 2022 | |
|
| 177 |
+
CXS 351-2022 | Standard for dried floral parts –saffron | CCSCH | 2022 | |
|
| 178 |
+
CXS 342-2021 | Norme pour l'origan séché | CCSCH | 2022 | |
|
| 179 |
+
CXS 343-2021 | Norme pour les racines, les rhizomes et les bulbes séchés : gingembre séché ou déshydraté | CCSCH | 2022 | |
|
| 180 |
+
CXS 344-2021 | Norme pour les parties florales séchées: clous de girofle | CCSCH | 2022 | |
|
| 181 |
+
CXS 345-2021 | Norme pour le basilic séché | CCSCH | 2022 | |
|
| 182 |
+
CXS 347-2019 | Norme pour l'ail séché ou déshydraté | CCSCH | 2022 | |"""
|
| 183 |
|
| 184 |
+
documents = []
|
| 185 |
+
|
| 186 |
+
# Parser les directives
|
| 187 |
+
for line in sample_guidelines.strip().split('\n'):
|
| 188 |
+
if '|' in line:
|
| 189 |
+
parts = line.split('|')
|
| 190 |
+
if len(parts) >= 4:
|
| 191 |
+
documents.append({
|
| 192 |
+
'code': parts[0].strip(),
|
| 193 |
+
'title': parts[1].strip(),
|
| 194 |
+
'committee': parts[2].strip(),
|
| 195 |
+
'year': int(parts[3].strip()),
|
| 196 |
+
'category': 'guidelines',
|
| 197 |
+
'category_name': 'Directives (CXG)',
|
| 198 |
+
'is_new': int(parts[3].strip()) >= 2023,
|
| 199 |
+
'is_2024': int(parts[3].strip()) == 2024,
|
| 200 |
+
'source_url': CODEX_URLS['guidelines']['url'],
|
| 201 |
+
'extracted_at': datetime.now().isoformat()
|
| 202 |
+
})
|
| 203 |
+
|
| 204 |
+
# Parser les normes
|
| 205 |
+
for line in sample_standards.strip().split('\n'):
|
| 206 |
+
if '|' in line:
|
| 207 |
+
parts = line.split('|')
|
| 208 |
+
if len(parts) >= 4:
|
| 209 |
+
documents.append({
|
| 210 |
+
'code': parts[0].strip(),
|
| 211 |
+
'title': parts[1].strip(),
|
| 212 |
+
'committee': parts[2].strip(),
|
| 213 |
+
'year': int(parts[3].strip()),
|
| 214 |
+
'category': 'standards',
|
| 215 |
+
'category_name': 'Normes (CXS)',
|
| 216 |
+
'is_new': int(parts[3].strip()) >= 2023,
|
| 217 |
+
'is_2024': int(parts[3].strip()) == 2024,
|
| 218 |
+
'source_url': CODEX_URLS['standards']['url'],
|
| 219 |
+
'extracted_at': datetime.now().isoformat()
|
| 220 |
+
})
|
| 221 |
+
|
| 222 |
+
return documents
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
+
def main():
|
| 225 |
+
# Header
|
| 226 |
+
st.title("📋 Moniteur Codex Alimentarius")
|
| 227 |
+
st.markdown("""
|
| 228 |
+
**Surveillance et analyse en temps réel des documents de sécurité alimentaire**
|
| 229 |
+
|
| 230 |
+
Cette application extrait et analyse automatiquement les documents du Codex Alimentarius pour votre veille réglementaire en food safety.
|
| 231 |
+
""")
|
| 232 |
|
| 233 |
+
# Sidebar
|
| 234 |
+
st.sidebar.header("🎛️ Configuration")
|
| 235 |
+
|
| 236 |
+
# Option de source de données
|
| 237 |
+
data_source = st.sidebar.radio(
|
| 238 |
+
"Source des données:",
|
| 239 |
+
["Données d'exemple", "Extraction en temps réel"]
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Bouton de chargement
|
| 243 |
+
if st.sidebar.button("🔄 Charger les données", type="primary"):
|
| 244 |
+
with st.spinner("Chargement des données..."):
|
| 245 |
+
if data_source == "Données d'exemple":
|
| 246 |
+
st.session_state.documents = parse_sample_data()
|
| 247 |
+
st.success(f"✅ {len(st.session_state.documents)} documents d'exemple chargés!")
|
| 248 |
+
else:
|
| 249 |
+
# Extraction en temps réel
|
| 250 |
+
all_documents = []
|
| 251 |
+
progress_bar = st.progress(0)
|
| 252 |
+
|
| 253 |
+
for i, (category, info) in enumerate(CODEX_URLS.items()):
|
| 254 |
+
st.info(f"Extraction des {info['name']}...")
|
| 255 |
+
documents = extract_documents_from_url(info['url'], category)
|
| 256 |
+
all_documents.extend(documents)
|
| 257 |
+
progress_bar.progress((i + 1) / len(CODEX_URLS))
|
| 258 |
+
time.sleep(1) # Pause pour éviter de surcharger le serveur
|
| 259 |
+
|
| 260 |
+
st.session_state.documents = all_documents
|
| 261 |
+
st.success(f"✅ {len(all_documents)} documents extraits en temps réel!")
|
| 262 |
+
|
| 263 |
+
# Vérifier si on a des données
|
| 264 |
+
if 'documents' not in st.session_state:
|
| 265 |
+
st.info("👆 Utilisez le panneau latéral pour charger les données")
|
| 266 |
+
return
|
| 267 |
+
|
| 268 |
+
df = pd.DataFrame(st.session_state.documents)
|
| 269 |
+
|
| 270 |
+
if df.empty:
|
| 271 |
+
st.warning("Aucun document trouvé")
|
| 272 |
+
return
|
| 273 |
+
|
| 274 |
+
# Statistiques principales
|
| 275 |
col1, col2, col3, col4 = st.columns(4)
|
| 276 |
+
|
| 277 |
+
with col1:
|
| 278 |
+
st.metric("📊 Total Documents", len(df))
|
| 279 |
+
|
| 280 |
+
with col2:
|
| 281 |
+
new_docs = len(df[df['is_new']])
|
| 282 |
+
st.metric("🆕 Nouveaux (2023+)", new_docs)
|
| 283 |
+
|
| 284 |
+
with col3:
|
| 285 |
+
docs_2024 = len(df[df['is_2024']])
|
| 286 |
+
st.metric("📅 Mis à jour 2024", docs_2024)
|
| 287 |
+
|
| 288 |
+
with col4:
|
| 289 |
+
committees = df['committee'].nunique()
|
| 290 |
+
st.metric("🏢 Comités Actifs", committees)
|
| 291 |
+
|
| 292 |
+
st.divider()
|
| 293 |
+
|
| 294 |
# Filtres
|
| 295 |
+
st.sidebar.header("🔍 Filtres")
|
| 296 |
+
|
| 297 |
+
# Filtre par catégorie
|
| 298 |
+
categories = ['Toutes'] + list(df['category_name'].unique())
|
| 299 |
+
selected_category = st.sidebar.selectbox("Catégorie:", categories)
|
| 300 |
+
|
| 301 |
+
# Filtre par année
|
| 302 |
+
years = ['Toutes'] + sorted(df['year'].unique(), reverse=True)
|
| 303 |
+
selected_year = st.sidebar.selectbox("Année:", years)
|
| 304 |
+
|
| 305 |
+
# Filtre par comité
|
| 306 |
+
committees = ['Tous'] + sorted(df['committee'].unique())
|
| 307 |
+
selected_committee = st.sidebar.selectbox("Comité:", committees)
|
| 308 |
+
|
| 309 |
+
# Filtre par nouveauté
|
| 310 |
+
filter_new = st.sidebar.checkbox("Seulement les nouveaux documents (2023+)")
|
| 311 |
+
filter_2024 = st.sidebar.checkbox("Seulement les mises à jour 2024")
|
| 312 |
+
|
| 313 |
+
# Recherche textuelle
|
| 314 |
+
search_term = st.sidebar.text_input("🔍 Recherche dans les titres:")
|
| 315 |
+
|
| 316 |
+
# Application des filtres
|
| 317 |
filtered_df = df.copy()
|
| 318 |
+
|
| 319 |
+
if selected_category != 'Toutes':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
filtered_df = filtered_df[filtered_df['category_name'] == selected_category]
|
| 321 |
+
|
| 322 |
+
if selected_year != 'Toutes':
|
| 323 |
+
filtered_df = filtered_df[filtered_df['year'] == selected_year]
|
| 324 |
+
|
| 325 |
+
if selected_committee != 'Tous':
|
| 326 |
+
filtered_df = filtered_df[filtered_df['committee'] == selected_committee]
|
| 327 |
+
|
| 328 |
+
if filter_new:
|
| 329 |
filtered_df = filtered_df[filtered_df['is_new']]
|
| 330 |
+
|
| 331 |
+
if filter_2024:
|
| 332 |
filtered_df = filtered_df[filtered_df['is_2024']]
|
| 333 |
+
|
| 334 |
+
if search_term:
|
| 335 |
+
filtered_df = filtered_df[
|
| 336 |
+
filtered_df['title'].str.contains(search_term, case=False, na=False) |
|
| 337 |
+
filtered_df['code'].str.contains(search_term, case=False, na=False)
|
| 338 |
+
]
|
| 339 |
+
|
| 340 |
+
# Graphiques
|
| 341 |
+
tab1, tab2, tab3 = st.tabs(["📋 Documents", "📊 Analyses", "💾 Export"])
|
| 342 |
+
|
| 343 |
+
with tab1:
|
| 344 |
+
st.header(f"📋 Documents ({len(filtered_df)} résultats)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
+
if not filtered_df.empty:
|
| 347 |
+
# Trier par année décroissante puis par code
|
| 348 |
+
filtered_df = filtered_df.sort_values(['year', 'code'], ascending=[False, True])
|
| 349 |
+
|
| 350 |
+
for _, doc in filtered_df.iterrows():
|
| 351 |
+
with st.container():
|
| 352 |
+
col1, col2 = st.columns([4, 1])
|
| 353 |
+
|
| 354 |
+
with col1:
|
| 355 |
+
# Badges
|
| 356 |
+
badges = f"**{doc['code']}** "
|
| 357 |
+
if doc['is_new']:
|
| 358 |
+
badges += "🆕 `NOUVEAU` "
|
| 359 |
+
if doc['is_2024']:
|
| 360 |
+
badges += "📅 `2024` "
|
| 361 |
+
badges += f"`{doc['category_name']}`"
|
| 362 |
+
|
| 363 |
+
st.markdown(badges)
|
| 364 |
+
st.markdown(f"**{doc['title']}**")
|
| 365 |
+
st.caption(f"🏢 {doc['committee']} • 📅 {doc['year']}")
|
| 366 |
+
|
| 367 |
+
with col2:
|
| 368 |
+
st.link_button("🔗 Voir Section", doc['source_url'])
|
| 369 |
+
|
| 370 |
+
st.divider()
|
| 371 |
+
else:
|
| 372 |
+
st.info("Aucun document ne correspond aux critères sélectionnés")
|
| 373 |
+
|
| 374 |
+
with tab2:
|
| 375 |
+
st.header("📊 Analyses des Documents")
|
| 376 |
|
| 377 |
+
if not df.empty:
|
| 378 |
+
# Répartition par catégorie
|
| 379 |
+
col1, col2 = st.columns(2)
|
| 380 |
+
|
| 381 |
+
with col1:
|
| 382 |
+
category_counts = df['category_name'].value_counts()
|
| 383 |
+
fig1 = px.pie(
|
| 384 |
+
values=category_counts.values,
|
| 385 |
+
names=category_counts.index,
|
| 386 |
+
title="Répartition par Catégorie"
|
| 387 |
+
)
|
| 388 |
+
st.plotly_chart(fig1, use_container_width=True)
|
| 389 |
+
|
| 390 |
+
with col2:
|
| 391 |
+
# Top 10 des comités les plus actifs
|
| 392 |
+
committee_counts = df['committee'].value_counts().head(10)
|
| 393 |
+
fig2 = px.bar(
|
| 394 |
+
x=committee_counts.values,
|
| 395 |
+
y=committee_counts.index,
|
| 396 |
+
orientation='h',
|
| 397 |
+
title="Top 10 Comités les Plus Actifs"
|
| 398 |
+
)
|
| 399 |
+
fig2.update_layout(yaxis={'categoryorder': 'total ascending'})
|
| 400 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 401 |
+
|
| 402 |
+
# Évolution temporelle
|
| 403 |
+
year_counts = df.groupby(['year', 'category_name']).size().reset_index(name='count')
|
| 404 |
+
fig3 = px.line(
|
| 405 |
+
year_counts,
|
| 406 |
+
x='year',
|
| 407 |
+
y='count',
|
| 408 |
+
color='category_name',
|
| 409 |
+
title="Évolution des Documents par Année"
|
| 410 |
+
)
|
| 411 |
+
st.plotly_chart(fig3, use_container_width=True)
|
| 412 |
|
| 413 |
+
# Documents récents
|
| 414 |
+
st.subheader("🆕 Documents Récents (2023-2024)")
|
| 415 |
+
recent_docs = df[df['is_new']].groupby(['year', 'category_name']).size().reset_index(name='count')
|
| 416 |
+
if not recent_docs.empty:
|
| 417 |
+
fig4 = px.bar(
|
| 418 |
+
recent_docs,
|
| 419 |
+
x='year',
|
| 420 |
+
y='count',
|
| 421 |
+
color='category_name',
|
| 422 |
+
title="Nouveaux Documents par Année"
|
| 423 |
+
)
|
| 424 |
+
st.plotly_chart(fig4, use_container_width=True)
|
| 425 |
|
| 426 |
+
# Analyse par comité
|
| 427 |
+
st.subheader("📊 Analyse Détaillée par Comité")
|
| 428 |
+
committee_analysis = df.groupby('committee').agg({
|
| 429 |
+
'code': 'count',
|
| 430 |
+
'is_new': 'sum',
|
| 431 |
+
'is_2024': 'sum'
|
| 432 |
+
}).rename(columns={
|
| 433 |
+
'code': 'Total',
|
| 434 |
+
'is_new': 'Nouveaux',
|
| 435 |
+
'is_2024': 'Mis à jour 2024'
|
| 436 |
+
}).sort_values('Total', ascending=False)
|
| 437 |
|
| 438 |
+
st.dataframe(committee_analysis, use_container_width=True)
|
| 439 |
+
|
| 440 |
+
with tab3:
|
| 441 |
+
st.header("💾 Export des Données")
|
| 442 |
+
|
| 443 |
+
col1, col2 = st.columns(2)
|
| 444 |
+
|
| 445 |
+
with col1:
|
| 446 |
+
# Export CSV
|
| 447 |
+
csv = filtered_df.to_csv(index=False)
|
| 448 |
+
st.download_button(
|
| 449 |
+
label="📄 Télécharger CSV",
|
| 450 |
+
data=csv,
|
| 451 |
+
file_name=f"codex_documents_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 452 |
+
mime="text/csv"
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
with col2:
|
| 456 |
+
# Export JSON
|
| 457 |
+
json_data = filtered_df.to_json(orient='records', indent=2)
|
| 458 |
+
st.download_button(
|
| 459 |
+
label="📋 Télécharger JSON",
|
| 460 |
+
data=json_data,
|
| 461 |
+
file_name=f"codex_documents_{datetime.now().strftime('%Y%m%d')}.json",
|
| 462 |
+
mime="application/json"
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
# Statistiques d'export
|
| 466 |
+
st.subheader("📊 Statistiques d'Export")
|
| 467 |
+
export_stats = {
|
| 468 |
+
"Total documents": len(filtered_df),
|
| 469 |
+
"Nouveaux documents (2023+)": len(filtered_df[filtered_df['is_new']]),
|
| 470 |
+
"Documents 2024": len(filtered_df[filtered_df['is_2024']]),
|
| 471 |
+
"Comités uniques": filtered_df['committee'].nunique(),
|
| 472 |
+
"Catégories": list(filtered_df['category_name'].unique()),
|
| 473 |
+
"Période couverte": f"{filtered_df['year'].min()} - {filtered_df['year'].max()}",
|
| 474 |
+
"Date d'extraction": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
st.json(export_stats)
|
| 478 |
+
|
| 479 |
+
# Aperçu des données filtrées
|
| 480 |
+
st.subheader("👀 Aperçu des Données Filtrées")
|
| 481 |
+
st.dataframe(
|
| 482 |
+
filtered_df[['code', 'title', 'committee', 'year', 'category_name']].head(20),
|
| 483 |
+
use_container_width=True
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
if __name__ == "__main__":
|
| 487 |
+
main()
|