MMOON commited on
Commit
84cd06f
·
verified ·
1 Parent(s): 0117a5a

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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 os
6
- import extract_codex # Importer le module d'extraction
 
 
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
- # Titre principal
17
- st.title("📋 Moniteur Codex Alimentarius")
18
- st.subheader("Analyse en temps réel des documents de sécurité alimentaire du Codex")
19
-
20
- # Initialisation de l'état de session
21
- if 'data_loaded' not in st.session_state:
22
- st.session_state.data_loaded = False
23
- if 'all_documents_df' not in st.session_state:
24
- st.session_state.all_documents_df = pd.DataFrame()
25
- if 'last_extraction_file' not in st.session_state:
26
- st.session_state.last_extraction_file = None
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # Barre latérale
29
- with st.sidebar:
30
- st.header("Contrôles")
31
-
32
- # Bouton pour charger/exécuter l'extraction
33
- if st.button("🔍 Charger/Actualiser les Données"):
34
- with st.spinner("Extraction des données du site Codex Alimentarius..."):
35
- extraction_file_path = extract_codex.run_extraction(output_dir="data")
36
- if extraction_file_path and os.path.exists(extraction_file_path):
37
- try:
38
- with open(extraction_file_path, 'r', encoding='utf-8') as f:
39
- data = json.load(f)
40
-
41
- # Transformer les données en DataFrame
42
- documents_list = []
43
- for category_key, docs in data.get("categories", {}).items():
44
- for doc in docs:
45
- doc['category_key'] = category_key
46
- # Ajouter un nom de catégorie lisible
47
- category_names = {
48
- "guidelines": "Directives (CXG)",
49
- "standards": "Normes (CXS)",
50
- "codes_of_practice": "Codes de Pratique (CXC)",
51
- "miscellaneous": "Divers (CXM)"
52
- }
53
- doc['category_name'] = category_names.get(category_key, category_key)
54
- doc['is_new'] = int(doc.get('year', 0)) >= 2023
55
- doc['is_2024'] = str(doc.get('year', '')) == '2024'
56
- documents_list.append(doc)
57
-
58
- if documents_list:
59
- df = pd.DataFrame(documents_list)
60
- # Trier par année décroissante, puis par code
61
- df['year'] = pd.to_numeric(df['year'], errors='coerce').fillna(0).astype(int)
62
- df = df.sort_values(by=['year', 'code'], ascending=[False, True]).reset_index(drop=True)
63
-
64
- st.session_state.all_documents_df = df
65
- st.session_state.data_loaded = True
66
- st.session_state.last_extraction_file = extraction_file_path
67
- st.success(f"✅ Données chargées avec succès depuis {extraction_file_path}!")
68
- st.experimental_rerun() # Recharger pour afficher les filtres
69
- else:
70
- st.error("❌ Aucun document n'a pu être extrait.")
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
- # Afficher le statut de chargement
77
- if st.session_state.data_loaded:
78
- st.success("Données chargées.")
79
- if st.session_state.last_extraction_file:
80
- st.info(f"Dernière extraction: {os.path.basename(st.session_state.last_extraction_file)}")
81
- else:
82
- st.info("Les données n'ont pas encore été chargées.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- # Section pour exporter les données actuelles
85
- st.markdown("---")
86
- st.header("Exporter")
87
- if st.session_state.data_loaded and not st.session_state.all_documents_df.empty:
88
- json_data = st.session_state.all_documents_df.to_json(orient='records', date_format='iso')
89
- st.download_button(
90
- label="💾 Télécharger les données (JSON)",
91
- data=json_data,
92
- file_name=f"codex_data_{pd.Timestamp.now().strftime('%Y%m%d_%H%M%S')}.json",
93
- mime='application/json'
94
- )
95
- else:
96
- st.button("💾 Télécharger les données (JSON)", disabled=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- # Corps principal de l'application
99
- if not st.session_state.data_loaded or st.session_state.all_documents_df.empty:
100
- st.info("🔍 Cliquez sur 'Charger/Actualiser les Données' dans la barre latérale pour commencer.")
101
- st.write("L'application va extraire et analyser tous les documents disponibles des Directives, Normes, Codes de Pratique et Divers du Codex Alimentarius.")
102
-
103
- # Afficher les fichiers d'extraction précédents s'ils existent
104
- data_dir = "data"
105
- if os.path.exists(data_dir):
106
- existing_files = [f for f in os.listdir(data_dir) if f.startswith("codex_data_") and f.endswith(".json")]
107
- if existing_files:
108
- st.markdown("---")
109
- st.subheader("Chargements précédents")
110
- st.write("Vous pouvez charger un fichier d'extraction précédent :")
111
- selected_file = st.selectbox("Sélectionner un fichier :", [""] + sorted(existing_files, reverse=True))
112
- if selected_file:
113
- file_path = os.path.join(data_dir, selected_file)
114
- if st.button(f"Charger {selected_file}"):
115
- try:
116
- with open(file_path, 'r', encoding='utf-8') as f:
117
- data = json.load(f)
118
- documents_list = []
119
- for category_key, docs in data.get("categories", {}).items():
120
- for doc in docs:
121
- doc['category_key'] = category_key
122
- category_names = {
123
- "guidelines": "Directives (CXG)",
124
- "standards": "Normes (CXS)",
125
- "codes_of_practice": "Codes de Pratique (CXC)",
126
- "miscellaneous": "Divers (CXM)"
127
- }
128
- doc['category_name'] = category_names.get(category_key, category_key)
129
- doc['is_new'] = int(doc.get('year', 0)) >= 2023
130
- doc['is_2024'] = str(doc.get('year', '')) == '2024'
131
- documents_list.append(doc)
132
- if documents_list:
133
- df = pd.DataFrame(documents_list)
134
- df['year'] = pd.to_numeric(df['year'], errors='coerce').fillna(0).astype(int)
135
- df = df.sort_values(by=['year', 'code'], ascending=[False, True]).reset_index(drop=True)
136
- st.session_state.all_documents_df = df
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
- else:
147
- df = st.session_state.all_documents_df.copy()
 
 
 
 
 
 
148
 
149
- # Statistiques
150
- st.subheader("Statistiques")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  col1, col2, col3, col4 = st.columns(4)
152
- col1.metric("Documents Total", len(df))
153
- col2.metric("Nouveaux (2023+)", len(df[df['is_new']]))
154
- col3.metric("Mis à jour en 2024", len(df[df['is_2024']]))
155
- col4.metric("Comités Actifs", df['committee'].nunique())
156
-
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  # Filtres
158
- st.markdown("---")
159
- st.subheader("Filtres")
160
-
161
- col_filter1, col_filter2, col_filter3, col_filter4 = st.columns(4)
162
-
163
- with col_filter1:
164
- search_term = st.text_input("🔍 Recherche (Titre, Code, Comité)", "")
165
- with col_filter2:
166
- category_options = ["Toutes les catégories"] + sorted(df['category_name'].unique())
167
- selected_category = st.selectbox("Catégorie", category_options)
168
- with col_filter3:
169
- year_options = ["Toutes les années"] + sorted(df['year'].unique(), reverse=True)
170
- selected_year = st.selectbox("Année", year_options)
171
- with col_filter4:
172
- new_options = ["Tous", "Nouveaux seulement (2023+)", "Mis à jour en 2024"]
173
- selected_new = st.selectbox("Nouveaux documents", new_options)
174
-
175
- # Appliquer les filtres
 
 
 
 
176
  filtered_df = df.copy()
177
- if search_term:
178
- mask = (
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
- if selected_year != "Toutes les années":
187
- filtered_df = filtered_df[filtered_df['year'] == int(selected_year)]
188
- if selected_new == "Nouveaux seulement (2023+)":
 
 
 
 
 
189
  filtered_df = filtered_df[filtered_df['is_new']]
190
- elif selected_new == "Mis à jour en 2024":
 
191
  filtered_df = filtered_df[filtered_df['is_2024']]
192
-
193
- # Tri
194
- st.markdown("---")
195
- st.subheader("Documents")
196
- sort_options = {
197
- "Année (Décroissante)": ['year', False],
198
- "Année (Croissante)": ['year', True],
199
- "Code": ['code', True],
200
- "Titre": ['title', True],
201
- "Comité": ['committee', True]
202
- }
203
- selected_sort = st.selectbox("Trier par :", list(sort_options.keys()))
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
- # Option 1: st.dataframe (simple mais moins personnalisé)
220
- # st.dataframe(filtered_df[['code', 'title', 'committee', 'year', 'category_name']])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
- # Option 2: Affichage personnalisé (similaire à l'HTML)
223
- for index, row in filtered_df.iterrows():
224
- # Déterminer les badges
225
- badges = []
226
- if row['is_new']:
227
- badges.append("🆕 Nouveau")
228
- if row['is_2024']:
229
- badges.append("📅 2024")
230
- badges.append(f"📁 {row['category_name']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- badge_html = " ".join([f"<span style='background-color: #e0e0e0; padding: 2px 6px; border-radius: 4px; margin-right: 5px; font-size: 0.8em;'>{badge}</span>" for badge in badges])
 
 
 
 
 
 
 
 
 
 
 
233
 
234
- # URL de base pour le lien
235
- base_url_map = {
236
- "guidelines": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/guidelines/fr/",
237
- "standards": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/list-standards/fr/",
238
- "codes_of_practice": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/codes-of-practice/fr/",
239
- "miscellaneous": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/miscellaneous/fr/"
240
- }
241
- doc_url = base_url_map.get(row['category_key'], "https://www.fao.org/fao-who-codexalimentarius/codex-texts/fr/")
 
 
 
242
 
243
- with st.container():
244
- st.markdown(f"""
245
- <div style="border: 1px solid #e0e0e0; border-radius: 5px; padding: 15px; margin-bottom: 10px;">
246
- <div style="display: flex; justify-content: space-between; align-items: flex-start;">
247
- <div>
248
- <h4 style="margin: 0; color: #1f77b4;">{row['code']}</h4>
249
- {badge_html}
250
- </div>
251
- <a href="{doc_url}" target="_blank" style="background-color: #1f77b4; color: white; padding: 5px 10px; text-decoration: none; border-radius: 3px; font-size: 0.8em;">🔗 Voir Section</a>
252
- </div>
253
- <p style="margin: 10px 0;"><strong>{row['title']}</strong></p>
254
- <div style="display: flex; gap: 20px; font-size: 0.9em; color: #666;">
255
- <span>🏢 Comité: {row['committee']}</span>
256
- <span>📅 Année: {row['year']}</span>
257
- </div>
258
- </div>
259
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()