bertrend 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bertrend/BERTopicModel.py +320 -0
- bertrend/BERTrend.py +1211 -0
- bertrend/__init__.py +59 -0
- bertrend/config/bertopic_default_config.toml +48 -0
- bertrend/config/bertrend_default_config.toml +16 -0
- bertrend/config/parameters.py +92 -0
- bertrend/config/services_default_config.toml +32 -0
- bertrend/demos/__init__.py +4 -0
- bertrend/demos/demos_utils/__init__.py +13 -0
- bertrend/demos/demos_utils/data_loading_component.py +267 -0
- bertrend/demos/demos_utils/embed_documents_component.py +52 -0
- bertrend/demos/demos_utils/icons.py +41 -0
- bertrend/demos/demos_utils/messages.py +11 -0
- bertrend/demos/demos_utils/parameters_component.py +162 -0
- bertrend/demos/demos_utils/state_utils.py +99 -0
- bertrend/demos/start_summarizer_app.sh +9 -0
- bertrend/demos/start_topic_analysis_app.sh +9 -0
- bertrend/demos/start_weak_signals_app.sh +9 -0
- bertrend/demos/summarization/__init__.py +4 -0
- bertrend/demos/summarization/summarizer_app.py +98 -0
- bertrend/demos/topic_analysis/__init__.py +4 -0
- bertrend/demos/topic_analysis/app.py +87 -0
- bertrend/demos/topic_analysis/app_utils.py +69 -0
- bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
- bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
- bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
- bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
- bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
- bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
- bertrend/demos/topic_analysis/messages.py +13 -0
- bertrend/demos/weak_signals/__init__.py +4 -0
- bertrend/demos/weak_signals/app.py +510 -0
- bertrend/demos/weak_signals/messages.py +32 -0
- bertrend/demos/weak_signals/visualizations_utils.py +403 -0
- bertrend/llm_utils/__init__.py +4 -0
- bertrend/llm_utils/newsletter.css +131 -0
- bertrend/llm_utils/newsletter_features.py +272 -0
- bertrend/llm_utils/openai_client.py +235 -0
- bertrend/llm_utils/prompts.py +94 -0
- bertrend/metrics/__init__.py +4 -0
- bertrend/metrics/temporal_metrics_embedding.py +861 -0
- bertrend/metrics/topic_metrics.py +199 -0
- bertrend/resources/common_ngrams.json +23 -0
- bertrend/resources/stopwords-en.json +3 -0
- bertrend/resources/stopwords-fr.json +1 -0
- bertrend/resources/stopwords-rte.json +15 -0
- bertrend/services/__init__.py +4 -0
- bertrend/services/authentication.py +66 -0
- bertrend/services/embedding_client.py +154 -0
- bertrend/services/embedding_service.py +380 -0
- bertrend/services/summarizer.py +45 -0
- bertrend/services/summary/__init__.py +4 -0
- bertrend/services/summary/abstractive_summarizer.py +74 -0
- bertrend/services/summary/chatgpt_summarizer.py +67 -0
- bertrend/services/summary/extractive_summarizer.py +368 -0
- bertrend/services/summary/lexrank.py +126 -0
- bertrend/services/summary/prompts.py +43 -0
- bertrend/tests/__init__.py +4 -0
- bertrend/tests/test_abstractive_summarizer.py +140 -0
- bertrend/tests/test_bertrend.py +1091 -0
- bertrend/tests/test_cache_utils.py +111 -0
- bertrend/tests/test_chatgpt_summarizer.py +150 -0
- bertrend/tests/test_config_utils.py +54 -0
- bertrend/tests/test_data_loading.py +326 -0
- bertrend/tests/test_data_structure.py +53 -0
- bertrend/tests/test_embedding_client.py +546 -0
- bertrend/tests/test_extractive_summarizer.py +250 -0
- bertrend/tests/test_installation.py +183 -0
- bertrend/tests/test_newsletter_features.py +354 -0
- bertrend/tests/test_openai_client.py +210 -0
- bertrend/tests/test_representative_docs.py +125 -0
- bertrend/tests/test_summarizer.py +115 -0
- bertrend/tests/test_temporal_metrics_embedding.py +349 -0
- bertrend/tests/test_topic_description.py +159 -0
- bertrend/tests/test_topic_metrics.py +233 -0
- bertrend/tests/test_topic_model.py +285 -0
- bertrend/tests/test_trend_analysis_data_structure.py +174 -0
- bertrend/tests/test_trend_analysis_prompts.py +225 -0
- bertrend/tests/test_trend_analysis_visualizations.py +307 -0
- bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
- bertrend/tests/test_utils_init.py +70 -0
- bertrend/tests/test_visualizations.py +306 -0
- bertrend/topic_analysis/__init__.py +4 -0
- bertrend/topic_analysis/data_structure.py +13 -0
- bertrend/topic_analysis/prompts.py +42 -0
- bertrend/topic_analysis/representative_docs.py +57 -0
- bertrend/topic_analysis/topic_description.py +66 -0
- bertrend/topic_analysis/visualizations.py +315 -0
- bertrend/trend_analysis/__init__.py +4 -0
- bertrend/trend_analysis/data_structure.py +78 -0
- bertrend/trend_analysis/prompts.py +229 -0
- bertrend/trend_analysis/signal_llm_template_en.html +181 -0
- bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
- bertrend/trend_analysis/visualizations.py +605 -0
- bertrend/trend_analysis/weak_signals.py +425 -0
- bertrend/utils/__init__.py +31 -0
- bertrend/utils/cache_utils.py +31 -0
- bertrend/utils/config_utils.py +41 -0
- bertrend/utils/data_loading.py +329 -0
- bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
- bertrend-0.3.1.dist-info/LICENSE.md +373 -0
- bertrend-0.3.1.dist-info/METADATA +400 -0
- bertrend-0.3.1.dist-info/RECORD +181 -0
- bertrend-0.3.1.dist-info/WHEEL +4 -0
- bertrend_apps/__init__.py +4 -0
- bertrend_apps/common/__init__.py +4 -0
- bertrend_apps/common/crontab_utils.py +113 -0
- bertrend_apps/common/mail_utils.py +91 -0
- bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
- bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_feed.toml +16 -0
- bertrend_apps/config/feeds/nlp_feed.toml +15 -0
- bertrend_apps/config/feeds/rte_feed.toml +15 -0
- bertrend_apps/config/gmail_credentials.json +1 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
- bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
- bertrend_apps/data_provider/__init__.py +9 -0
- bertrend_apps/data_provider/__main__.py +237 -0
- bertrend_apps/data_provider/arxiv_provider.py +161 -0
- bertrend_apps/data_provider/bing_news_provider.py +82 -0
- bertrend_apps/data_provider/curebot_provider.py +105 -0
- bertrend_apps/data_provider/data_provider.py +194 -0
- bertrend_apps/data_provider/google_news_provider.py +94 -0
- bertrend_apps/data_provider/newscatcher_provider.py +74 -0
- bertrend_apps/data_provider/utils.py +165 -0
- bertrend_apps/exploration/__init__.py +4 -0
- bertrend_apps/exploration/curebot/__init__.py +4 -0
- bertrend_apps/exploration/curebot/app.py +45 -0
- bertrend_apps/exploration/curebot/app_utils.py +317 -0
- bertrend_apps/exploration/curebot/config.toml +33 -0
- bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
- bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
- bertrend_apps/exploration/curebot/prompts.py +24 -0
- bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
- bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
- bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
- bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
- bertrend_apps/exploration/dataviz/Main_page.py +10 -0
- bertrend_apps/exploration/dataviz/__init__.py +4 -0
- bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
- bertrend_apps/exploration/geolocalization/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
- bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
- bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
- bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
- bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
- bertrend_apps/install_feeds_and_newsletters.py +29 -0
- bertrend_apps/newsletters/__init__.py +4 -0
- bertrend_apps/newsletters/__main__.py +277 -0
- bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
- bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
- bertrend_apps/prospective_demo/__init__.py +70 -0
- bertrend_apps/prospective_demo/app.py +111 -0
- bertrend_apps/prospective_demo/authentication.py +44 -0
- bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
- bertrend_apps/prospective_demo/dashboard_common.py +79 -0
- bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
- bertrend_apps/prospective_demo/feeds_common.py +38 -0
- bertrend_apps/prospective_demo/feeds_config.py +261 -0
- bertrend_apps/prospective_demo/feeds_data.py +95 -0
- bertrend_apps/prospective_demo/i18n.py +136 -0
- bertrend_apps/prospective_demo/llm_utils.py +34 -0
- bertrend_apps/prospective_demo/models_info.py +361 -0
- bertrend_apps/prospective_demo/perf_utils.py +23 -0
- bertrend_apps/prospective_demo/process_new_data.py +348 -0
- bertrend_apps/prospective_demo/report_generation.py +214 -0
- bertrend_apps/prospective_demo/report_template.html +148 -0
- bertrend_apps/prospective_demo/report_utils.py +132 -0
- bertrend_apps/prospective_demo/start_demo.sh +9 -0
- bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
- bertrend_apps/start_apps.sh +15 -0
- bertrend_apps/stop_apps.sh +11 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import streamlit as st
|
|
11
|
+
|
|
12
|
+
from bertrend.demos.demos_utils.icons import WARNING_ICON
|
|
13
|
+
from bertrend_apps.prospective_demo import NOISE, WEAK_SIGNALS, STRONG_SIGNALS
|
|
14
|
+
from bertrend_apps.prospective_demo.models_info import get_models_info
|
|
15
|
+
|
|
16
|
+
COLS_RATIO_ID_TS = [2 / 7, 5 / 7]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@st.cache_data
|
|
20
|
+
def get_df_topics(model_interpretation_path=None) -> dict[str, pd.DataFrame]:
|
|
21
|
+
dfs_topics = {}
|
|
22
|
+
for df_id in [NOISE, WEAK_SIGNALS, STRONG_SIGNALS]:
|
|
23
|
+
df_path = model_interpretation_path / f"{df_id}.parquet"
|
|
24
|
+
dfs_topics[df_id] = (
|
|
25
|
+
pd.read_parquet(df_path) if df_path.exists() else pd.DataFrame()
|
|
26
|
+
)
|
|
27
|
+
return dfs_topics
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def update_key(key: str, new_value: Any):
|
|
31
|
+
st.session_state[key] = new_value
|
|
32
|
+
# reset ts value in order to avoid errors if a previous ts value is not available for the new key value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def update_key_and_ts(key: str, new_value: Any):
|
|
36
|
+
update_key(key, new_value)
|
|
37
|
+
# reset ts value in order to avoid errors if a previous ts value is not available for the new key value
|
|
38
|
+
if "reference_ts" in st.session_state:
|
|
39
|
+
del st.session_state["reference_ts"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def choose_id_and_ts():
|
|
43
|
+
col1, col2 = st.columns(COLS_RATIO_ID_TS)
|
|
44
|
+
with col1:
|
|
45
|
+
options = sorted(st.session_state.user_feeds.keys())
|
|
46
|
+
if "model_id" not in st.session_state:
|
|
47
|
+
st.session_state.model_id = options[0]
|
|
48
|
+
model_id_key = uuid.uuid4()
|
|
49
|
+
model_id = st.selectbox(
|
|
50
|
+
"Sélection de la veille",
|
|
51
|
+
options=options,
|
|
52
|
+
index=options.index(st.session_state.model_id),
|
|
53
|
+
key=model_id_key, # to avoid pb of unicity if displayed on several places
|
|
54
|
+
on_change=lambda: update_key_and_ts(
|
|
55
|
+
"model_id", st.session_state[model_id_key]
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
with col2:
|
|
59
|
+
list_models = get_models_info(model_id)
|
|
60
|
+
if not list_models:
|
|
61
|
+
st.warning(f"{WARNING_ICON} Pas de modèle disponible")
|
|
62
|
+
st.stop()
|
|
63
|
+
elif len(list_models) < 2:
|
|
64
|
+
st.warning(
|
|
65
|
+
f"{WARNING_ICON} 2 modèles minimum pour analyser les tendances !"
|
|
66
|
+
)
|
|
67
|
+
st.stop()
|
|
68
|
+
if "reference_ts" not in st.session_state:
|
|
69
|
+
st.session_state.reference_ts = list_models[-1]
|
|
70
|
+
ts_key = uuid.uuid4()
|
|
71
|
+
st.select_slider(
|
|
72
|
+
"Date d'analyse",
|
|
73
|
+
options=list_models,
|
|
74
|
+
value=st.session_state.reference_ts,
|
|
75
|
+
format_func=lambda ts: ts.strftime("%d/%m/%Y"),
|
|
76
|
+
help="Sélection de la date d'analyse parmi celles disponibles",
|
|
77
|
+
key=ts_key, # to avoid pb of unicity if displayed on several places
|
|
78
|
+
on_change=lambda: update_key("reference_ts", st.session_state[ts_key]),
|
|
79
|
+
)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
from bertrend.demos.demos_utils.icons import (
|
|
9
|
+
WARNING_ICON,
|
|
10
|
+
WEAK_SIGNAL_ICON,
|
|
11
|
+
STRONG_SIGNAL_ICON,
|
|
12
|
+
NOISE_ICON,
|
|
13
|
+
)
|
|
14
|
+
from bertrend_apps.prospective_demo import (
|
|
15
|
+
LLM_TOPIC_DESCRIPTION_COLUMN,
|
|
16
|
+
NOISE,
|
|
17
|
+
WEAK_SIGNALS,
|
|
18
|
+
STRONG_SIGNALS,
|
|
19
|
+
get_model_interpretation_path,
|
|
20
|
+
LLM_TOPIC_TITLE_COLUMN,
|
|
21
|
+
URLS_COLUMN,
|
|
22
|
+
)
|
|
23
|
+
from bertrend_apps.prospective_demo.dashboard_common import (
|
|
24
|
+
choose_id_and_ts,
|
|
25
|
+
get_df_topics,
|
|
26
|
+
)
|
|
27
|
+
from bertrend_apps.prospective_demo.i18n import translate
|
|
28
|
+
|
|
29
|
+
COLS_RATIO = [4 / 7, 3 / 7]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def signal_analysis():
|
|
33
|
+
# ID and timestamp selection
|
|
34
|
+
choose_id_and_ts()
|
|
35
|
+
model_id = st.session_state.model_id
|
|
36
|
+
reference_ts = st.session_state.reference_ts
|
|
37
|
+
|
|
38
|
+
model_interpretation_path = get_model_interpretation_path(
|
|
39
|
+
user_name=st.session_state.username,
|
|
40
|
+
model_id=model_id,
|
|
41
|
+
reference_ts=reference_ts,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# TODO: decide which column to display
|
|
45
|
+
columns = [
|
|
46
|
+
"Topic",
|
|
47
|
+
LLM_TOPIC_TITLE_COLUMN,
|
|
48
|
+
LLM_TOPIC_DESCRIPTION_COLUMN,
|
|
49
|
+
# "Representation",
|
|
50
|
+
URLS_COLUMN,
|
|
51
|
+
"Latest_Popularity",
|
|
52
|
+
"Docs_Count",
|
|
53
|
+
# "Paragraphs_Count",
|
|
54
|
+
"Documents",
|
|
55
|
+
"Sources",
|
|
56
|
+
"Source_Diversity",
|
|
57
|
+
"Latest_Timestamp",
|
|
58
|
+
]
|
|
59
|
+
column_config = {
|
|
60
|
+
"Topic": st.column_config.NumberColumn(
|
|
61
|
+
"Topic",
|
|
62
|
+
pinned=True,
|
|
63
|
+
),
|
|
64
|
+
LLM_TOPIC_TITLE_COLUMN: st.column_config.TextColumn(
|
|
65
|
+
translate("title"), pinned=True, width="large"
|
|
66
|
+
),
|
|
67
|
+
"Latest_Popularity": st.column_config.ProgressColumn(
|
|
68
|
+
format="%i",
|
|
69
|
+
max_value=50,
|
|
70
|
+
),
|
|
71
|
+
"Source_Diversity": st.column_config.ProgressColumn(
|
|
72
|
+
format="%i",
|
|
73
|
+
max_value=50,
|
|
74
|
+
),
|
|
75
|
+
"Latest_Timestamp": st.column_config.DateColumn(
|
|
76
|
+
format="DD/MM/YYYY",
|
|
77
|
+
),
|
|
78
|
+
URLS_COLUMN: st.column_config.LinkColumn(),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
dfs_topics = get_df_topics(model_interpretation_path)
|
|
82
|
+
|
|
83
|
+
col1, col2 = st.columns(COLS_RATIO)
|
|
84
|
+
with col1:
|
|
85
|
+
# Display dataframes for weak_signals, strong, etc
|
|
86
|
+
display_translated_signal_categories(
|
|
87
|
+
dfs_topics[NOISE],
|
|
88
|
+
dfs_topics[WEAK_SIGNALS],
|
|
89
|
+
dfs_topics[STRONG_SIGNALS],
|
|
90
|
+
reference_ts,
|
|
91
|
+
columns=columns,
|
|
92
|
+
column_config=column_config,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
with col2:
|
|
96
|
+
st.info(translate("todo_message"))
|
|
97
|
+
explore_topic_sources(dfs_topics)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@st.fragment
|
|
101
|
+
def explore_topic_sources(dfs_topics):
|
|
102
|
+
st.write(f"**{translate('explore_sources_by_topic')}**")
|
|
103
|
+
selected_signal_type = st.pills(
|
|
104
|
+
translate("signal_type"),
|
|
105
|
+
label_visibility="hidden",
|
|
106
|
+
options=[translate("emerging_topics"), translate("strong_topics")],
|
|
107
|
+
selection_mode="single",
|
|
108
|
+
default=translate("emerging_topics"),
|
|
109
|
+
)
|
|
110
|
+
if selected_signal_type == translate("strong_topics"):
|
|
111
|
+
selected_df = dfs_topics.get(STRONG_SIGNALS)
|
|
112
|
+
else:
|
|
113
|
+
selected_df = dfs_topics.get(WEAK_SIGNALS)
|
|
114
|
+
if selected_df is None or selected_df.empty:
|
|
115
|
+
st.warning(f"{WARNING_ICON} {translate('no_data')}")
|
|
116
|
+
else:
|
|
117
|
+
selected_df = selected_df.sort_values(by=["Latest_Popularity"], ascending=False)
|
|
118
|
+
options = selected_df["Topic"].tolist()
|
|
119
|
+
topic_id = st.selectbox(
|
|
120
|
+
index=None,
|
|
121
|
+
label=translate("topic_selection"),
|
|
122
|
+
label_visibility="hidden",
|
|
123
|
+
options=options,
|
|
124
|
+
format_func=lambda x: f"{translate('topic')} {x}: "
|
|
125
|
+
+ selected_df[selected_df["Topic"] == x][LLM_TOPIC_TITLE_COLUMN].values[0],
|
|
126
|
+
)
|
|
127
|
+
if topic_id is None:
|
|
128
|
+
return
|
|
129
|
+
if selected_signal_type == translate("strong_topics"):
|
|
130
|
+
color = "green"
|
|
131
|
+
else:
|
|
132
|
+
color = "orange"
|
|
133
|
+
row = selected_df[selected_df["Topic"] == topic_id]
|
|
134
|
+
display_topic_links(
|
|
135
|
+
title=f":{color}[**{row[LLM_TOPIC_TITLE_COLUMN].values[0]}**]",
|
|
136
|
+
desc=row[LLM_TOPIC_DESCRIPTION_COLUMN].values[0],
|
|
137
|
+
df=list(selected_df.query(f"Topic == {topic_id}")[URLS_COLUMN])[0],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def display_translated_signal_categories(
|
|
142
|
+
noise_topics_df: pd.DataFrame,
|
|
143
|
+
weak_signal_topics_df: pd.DataFrame,
|
|
144
|
+
strong_signal_topics_df: pd.DataFrame,
|
|
145
|
+
window_end: pd.Timestamp,
|
|
146
|
+
columns=None,
|
|
147
|
+
column_order=None,
|
|
148
|
+
column_config=None,
|
|
149
|
+
):
|
|
150
|
+
"""Wrapper around display_signal_categories_df that uses translated text."""
|
|
151
|
+
# Weak Signals
|
|
152
|
+
with st.expander(
|
|
153
|
+
f":orange[{WEAK_SIGNAL_ICON} {translate('weak_signals')}]", expanded=True
|
|
154
|
+
):
|
|
155
|
+
st.subheader(f":orange[{translate('weak_signals')}]")
|
|
156
|
+
if not weak_signal_topics_df.empty:
|
|
157
|
+
displayed_df = weak_signal_topics_df[columns].sort_values(
|
|
158
|
+
by=["Latest_Popularity"], ascending=False
|
|
159
|
+
)
|
|
160
|
+
displayed_df["Documents"] = displayed_df["Documents"].astype(str)
|
|
161
|
+
st.dataframe(
|
|
162
|
+
displayed_df,
|
|
163
|
+
column_order=column_order if column_order else columns,
|
|
164
|
+
column_config=column_config,
|
|
165
|
+
hide_index=True,
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
st.info(
|
|
169
|
+
translate("no_weak_signals").format(timestamp=window_end),
|
|
170
|
+
icon=WARNING_ICON,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Strong Signals
|
|
174
|
+
with st.expander(
|
|
175
|
+
f":green[{STRONG_SIGNAL_ICON} {translate('strong_signals')}]", expanded=True
|
|
176
|
+
):
|
|
177
|
+
st.subheader(f":green[{translate('strong_signals')}]")
|
|
178
|
+
if not strong_signal_topics_df.empty:
|
|
179
|
+
displayed_df = strong_signal_topics_df[columns].sort_values(
|
|
180
|
+
by=["Latest_Popularity"], ascending=False
|
|
181
|
+
)
|
|
182
|
+
displayed_df["Documents"] = displayed_df["Documents"].astype(str)
|
|
183
|
+
st.dataframe(
|
|
184
|
+
displayed_df,
|
|
185
|
+
column_order=column_order if column_order else columns,
|
|
186
|
+
column_config=column_config,
|
|
187
|
+
hide_index=True,
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
st.info(
|
|
191
|
+
translate("no_strong_signals").format(timestamp=window_end),
|
|
192
|
+
icon=WARNING_ICON,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Noise
|
|
196
|
+
with st.expander(f":grey[{NOISE_ICON} {translate('noise')}]", expanded=True):
|
|
197
|
+
st.subheader(f":grey[{translate('noise')}]")
|
|
198
|
+
if not noise_topics_df.empty:
|
|
199
|
+
displayed_df = noise_topics_df[columns].sort_values(
|
|
200
|
+
by=["Latest_Popularity"], ascending=False
|
|
201
|
+
)
|
|
202
|
+
displayed_df["Documents"] = displayed_df["Documents"].astype(str)
|
|
203
|
+
st.dataframe(
|
|
204
|
+
displayed_df,
|
|
205
|
+
column_order=column_order if column_order else columns,
|
|
206
|
+
column_config=column_config,
|
|
207
|
+
hide_index=True,
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
st.info(
|
|
211
|
+
translate("no_noise_signals").format(timestamp=window_end),
|
|
212
|
+
icon=WARNING_ICON,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@st.dialog(translate("explore_sources"), width="large")
|
|
217
|
+
def display_topic_links(title: str, desc: str, df: pd.DataFrame):
|
|
218
|
+
st.subheader(title)
|
|
219
|
+
st.write(desc)
|
|
220
|
+
st.dataframe(
|
|
221
|
+
df,
|
|
222
|
+
use_container_width=True,
|
|
223
|
+
column_config={
|
|
224
|
+
"value": st.column_config.LinkColumn(translate("reference_articles")),
|
|
225
|
+
},
|
|
226
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from bertrend import FEED_BASE_PATH, load_toml_config
|
|
10
|
+
from bertrend_apps.prospective_demo import CONFIG_FEEDS_BASE_PATH
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_user_feeds(username: str) -> tuple[dict[str, dict], dict[str, Path]]:
|
|
14
|
+
"""Read user feed config files"""
|
|
15
|
+
user_feed_dir = CONFIG_FEEDS_BASE_PATH / username
|
|
16
|
+
user_feed_dir.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
logger.debug(f"Reading user feeds from: {user_feed_dir}")
|
|
18
|
+
matching_files = user_feed_dir.rglob("*_feed.toml")
|
|
19
|
+
|
|
20
|
+
user_feeds = {}
|
|
21
|
+
feed_files = {}
|
|
22
|
+
for f in matching_files:
|
|
23
|
+
feed_id = f.name.split("_feed.toml")[0]
|
|
24
|
+
user_feeds[feed_id] = load_toml_config(f)
|
|
25
|
+
feed_files[feed_id] = f
|
|
26
|
+
|
|
27
|
+
return user_feeds, feed_files
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_all_files_for_feed(user_feeds: dict[str, dict], feed_id: str) -> list[Path]:
|
|
31
|
+
"""Returns the paths of all files associated to a feed for the current user."""
|
|
32
|
+
feed_base_dir = user_feeds[feed_id]["data-feed"]["feed_dir_path"]
|
|
33
|
+
list_all_files = list(
|
|
34
|
+
Path(FEED_BASE_PATH, feed_base_dir).glob(
|
|
35
|
+
f"*{user_feeds[feed_id]['data-feed'].get('id')}*.jsonl*"
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
return list_all_files
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import streamlit as st
|
|
11
|
+
import toml
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from bertrend.config.parameters import LANGUAGES
|
|
15
|
+
from bertrend.demos.demos_utils.icons import (
|
|
16
|
+
INFO_ICON,
|
|
17
|
+
ERROR_ICON,
|
|
18
|
+
ADD_ICON,
|
|
19
|
+
EDIT_ICON,
|
|
20
|
+
DELETE_ICON,
|
|
21
|
+
WARNING_ICON,
|
|
22
|
+
TOGGLE_ON_ICON,
|
|
23
|
+
TOGGLE_OFF_ICON,
|
|
24
|
+
)
|
|
25
|
+
from bertrend_apps.common.crontab_utils import (
|
|
26
|
+
get_understandable_cron_description,
|
|
27
|
+
check_if_scrapping_active_for_user,
|
|
28
|
+
remove_scrapping_for_user,
|
|
29
|
+
schedule_scrapping,
|
|
30
|
+
)
|
|
31
|
+
from bertrend_apps.data_provider import URL_PATTERN
|
|
32
|
+
from bertrend_apps.prospective_demo.feeds_common import (
|
|
33
|
+
read_user_feeds,
|
|
34
|
+
)
|
|
35
|
+
from bertrend_apps.prospective_demo import CONFIG_FEEDS_BASE_PATH
|
|
36
|
+
from bertrend_apps.prospective_demo.models_info import (
|
|
37
|
+
remove_scheduled_training_for_user,
|
|
38
|
+
)
|
|
39
|
+
from bertrend_apps.prospective_demo.streamlit_utils import clickable_df
|
|
40
|
+
|
|
41
|
+
# Default feed configs
|
|
42
|
+
DEFAULT_GNEWS_CRONTAB_EXPRESSION = "1 0 * * 1"
|
|
43
|
+
DEFAULT_CUREBOT_CRONTAB_EXPRESSION = "42 0,6,12,18 * * *" # 4 times a day
|
|
44
|
+
DEFAULT_MAX_RESULTS = 20
|
|
45
|
+
DEFAULT_NUMBER_OF_DAYS = 14
|
|
46
|
+
FEED_SOURCES = ["google", "curebot"]
|
|
47
|
+
TRANSLATION = {"English": "Anglais", "French": "Français"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@st.dialog("Configuration d'un nouveau flux de données")
|
|
51
|
+
def edit_feed_monitoring(config: dict | None = None):
|
|
52
|
+
"""Create or update a feed monitoring configuration."""
|
|
53
|
+
chosen_id = st.text_input(
|
|
54
|
+
"ID :red[*]",
|
|
55
|
+
help="Identifiant du flux de données",
|
|
56
|
+
value=None if not config else config["id"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
provider = st.segmented_control(
|
|
60
|
+
"Source",
|
|
61
|
+
selection_mode="single",
|
|
62
|
+
options=FEED_SOURCES,
|
|
63
|
+
default=FEED_SOURCES[0] if not config else config["provider"],
|
|
64
|
+
help="Sélection de la source de données",
|
|
65
|
+
)
|
|
66
|
+
if provider == "google":
|
|
67
|
+
query = st.text_input(
|
|
68
|
+
"Requête :red[*]",
|
|
69
|
+
value="" if not config else config["query"],
|
|
70
|
+
help="Saisir ici la requête qui sera faite sur Google News",
|
|
71
|
+
)
|
|
72
|
+
language = st.segmented_control(
|
|
73
|
+
"Langue",
|
|
74
|
+
selection_mode="single",
|
|
75
|
+
options=LANGUAGES,
|
|
76
|
+
default=LANGUAGES[0],
|
|
77
|
+
format_func=lambda lang: TRANSLATION[lang],
|
|
78
|
+
help="Choix de la langue",
|
|
79
|
+
)
|
|
80
|
+
if "update_frequency" not in st.session_state:
|
|
81
|
+
st.session_state.update_frequency = (
|
|
82
|
+
DEFAULT_GNEWS_CRONTAB_EXPRESSION
|
|
83
|
+
if not config
|
|
84
|
+
else config["update_frequency"]
|
|
85
|
+
)
|
|
86
|
+
new_freq = st.text_input(
|
|
87
|
+
f"Fréquence d'exécution",
|
|
88
|
+
value=st.session_state.update_frequency,
|
|
89
|
+
help=f"Fréquence de collecte des données",
|
|
90
|
+
)
|
|
91
|
+
st.session_state.update_frequency = new_freq
|
|
92
|
+
st.write(display_crontab_description(st.session_state.update_frequency))
|
|
93
|
+
|
|
94
|
+
elif provider == "curebot":
|
|
95
|
+
query = st.text_input(
|
|
96
|
+
"ATOM feed :red[*]",
|
|
97
|
+
value="" if not config else config["query"],
|
|
98
|
+
help="URL du flux de données Curebot",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
get_understandable_cron_description(st.session_state.update_frequency)
|
|
103
|
+
valid_cron = True
|
|
104
|
+
except:
|
|
105
|
+
valid_cron = False
|
|
106
|
+
|
|
107
|
+
if st.button(
|
|
108
|
+
"OK",
|
|
109
|
+
disabled=not chosen_id
|
|
110
|
+
or not query
|
|
111
|
+
or (query and provider == "curebot" and not re.match(URL_PATTERN, query)),
|
|
112
|
+
):
|
|
113
|
+
if not config:
|
|
114
|
+
config = {}
|
|
115
|
+
config["id"] = "feed_" + chosen_id
|
|
116
|
+
config["feed_dir_path"] = (
|
|
117
|
+
"users/" + st.session_state.username + "/feed_" + chosen_id
|
|
118
|
+
)
|
|
119
|
+
config["query"] = query
|
|
120
|
+
config["provider"] = provider
|
|
121
|
+
if not config.get("max_results"):
|
|
122
|
+
config["max_results"] = DEFAULT_MAX_RESULTS
|
|
123
|
+
if not config.get("number_of_days"):
|
|
124
|
+
config["number_of_days"] = DEFAULT_NUMBER_OF_DAYS
|
|
125
|
+
if provider == "google":
|
|
126
|
+
config["language"] = "fr" if language == "French" else "en"
|
|
127
|
+
config["update_frequency"] = (
|
|
128
|
+
st.session_state.update_frequency
|
|
129
|
+
if valid_cron
|
|
130
|
+
else DEFAULT_GNEWS_CRONTAB_EXPRESSION
|
|
131
|
+
)
|
|
132
|
+
elif provider == "curebot":
|
|
133
|
+
config["language"] = "fr"
|
|
134
|
+
config["update_frequency"] = DEFAULT_CUREBOT_CRONTAB_EXPRESSION
|
|
135
|
+
|
|
136
|
+
if "update_frequency" in st.session_state:
|
|
137
|
+
del st.session_state["update_frequency"] # to avoid memory effect
|
|
138
|
+
|
|
139
|
+
# Remove prevous crontab if any
|
|
140
|
+
remove_scrapping_for_user(feed_id=chosen_id, user=st.session_state.username)
|
|
141
|
+
|
|
142
|
+
# Save feed config and update crontab
|
|
143
|
+
save_feed_config(chosen_id, config)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def save_feed_config(chosen_id, feed_config: dict):
|
|
147
|
+
"""Save the feed configuration to disk as a TOML file."""
|
|
148
|
+
feed_path = (
|
|
149
|
+
CONFIG_FEEDS_BASE_PATH / st.session_state.username / f"{chosen_id}_feed.toml"
|
|
150
|
+
)
|
|
151
|
+
# Save the dictionary to a TOML file
|
|
152
|
+
with open(feed_path, "w") as toml_file:
|
|
153
|
+
toml.dump({"data-feed": feed_config}, toml_file)
|
|
154
|
+
logger.debug(f"Saved feed config {feed_config} to {feed_path}")
|
|
155
|
+
schedule_scrapping(feed_path, user=st.session_state.username)
|
|
156
|
+
st.rerun()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def display_crontab_description(crontab_expr: str) -> str:
|
|
160
|
+
try:
|
|
161
|
+
return f":blue[{INFO_ICON} {get_understandable_cron_description(crontab_expr)}]"
|
|
162
|
+
except Exception:
|
|
163
|
+
return f":red[{ERROR_ICON} Expression mal écrite !]"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def configure_information_sources():
|
|
167
|
+
"""Configure Information Sources."""
|
|
168
|
+
# if "user_feeds" not in st.session_state:
|
|
169
|
+
st.session_state.user_feeds, st.session_state.feed_files = read_user_feeds(
|
|
170
|
+
st.session_state.username
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
displayed_list = []
|
|
174
|
+
for k, v in st.session_state.user_feeds.items():
|
|
175
|
+
displayed_list.append(
|
|
176
|
+
{
|
|
177
|
+
"id": k,
|
|
178
|
+
"provider": v["data-feed"]["provider"],
|
|
179
|
+
"query": v["data-feed"]["query"],
|
|
180
|
+
"language": v["data-feed"]["language"],
|
|
181
|
+
"update_frequency": v["data-feed"]["update_frequency"],
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
df = pd.DataFrame(displayed_list)
|
|
185
|
+
if not df.empty:
|
|
186
|
+
df = df.sort_values(by="id", inplace=False).reset_index(drop=True)
|
|
187
|
+
|
|
188
|
+
if st.button(f":green[{ADD_ICON}]", type="tertiary", help="Nouveau flux de veille"):
|
|
189
|
+
edit_feed_monitoring()
|
|
190
|
+
|
|
191
|
+
clickable_df_buttons = [
|
|
192
|
+
(EDIT_ICON, edit_feed_monitoring, "secondary"),
|
|
193
|
+
(lambda x: toggle_icon(df, x), toggle_feed, "secondary"),
|
|
194
|
+
(DELETE_ICON, handle_delete, "primary"),
|
|
195
|
+
]
|
|
196
|
+
clickable_df(df, clickable_df_buttons)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def toggle_icon(df: pd.DataFrame, index: int) -> str:
|
|
200
|
+
"""Switch the toggle icon depending on the statis of the scrapping feed in the crontab"""
|
|
201
|
+
feed_id = df["id"][index]
|
|
202
|
+
return (
|
|
203
|
+
f":green[{TOGGLE_ON_ICON}]"
|
|
204
|
+
if check_if_scrapping_active_for_user(
|
|
205
|
+
feed_id=feed_id, user=st.session_state.username
|
|
206
|
+
)
|
|
207
|
+
else f":red[{TOGGLE_OFF_ICON}]"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def toggle_feed(cfg: dict):
|
|
212
|
+
"""Activate / deactivate the feed from the crontab"""
|
|
213
|
+
feed_id = cfg["id"]
|
|
214
|
+
if check_if_scrapping_active_for_user(
|
|
215
|
+
feed_id=feed_id, user=st.session_state.username
|
|
216
|
+
):
|
|
217
|
+
if remove_scrapping_for_user(feed_id=feed_id, user=st.session_state.username):
|
|
218
|
+
st.toast(f"Le flux **{feed_id}** est déactivé !", icon=INFO_ICON)
|
|
219
|
+
logger.info(f"Flux {feed_id} désactivé !")
|
|
220
|
+
else:
|
|
221
|
+
schedule_scrapping(
|
|
222
|
+
st.session_state.feed_files[feed_id], user=st.session_state.username
|
|
223
|
+
)
|
|
224
|
+
st.toast(f"Le flux **{feed_id}** est activé !", icon=WARNING_ICON)
|
|
225
|
+
logger.info(f"Flux {feed_id} activé !")
|
|
226
|
+
time.sleep(0.2)
|
|
227
|
+
st.rerun()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def delete_feed_config(feed_id: str):
|
|
231
|
+
# remove config file
|
|
232
|
+
file_path: Path = st.session_state.feed_files[feed_id]
|
|
233
|
+
try:
|
|
234
|
+
file_path.unlink()
|
|
235
|
+
logger.debug(f"Feed file {file_path} has been removed.")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error(f"An error occurred: {e}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@st.dialog("Confirmation")
|
|
241
|
+
def handle_delete(row_dict: dict):
|
|
242
|
+
"""Function to handle remove click events"""
|
|
243
|
+
feed_id = row_dict["id"]
|
|
244
|
+
st.write(
|
|
245
|
+
f":orange[{WARNING_ICON}] Voulez-vous vraiment supprimer le flux de veille **{feed_id}** ?"
|
|
246
|
+
)
|
|
247
|
+
col1, col2, _ = st.columns([2, 2, 8])
|
|
248
|
+
with col1:
|
|
249
|
+
if st.button("Oui", type="primary"):
|
|
250
|
+
remove_scrapping_for_user(feed_id=feed_id, user=st.session_state.username)
|
|
251
|
+
delete_feed_config(feed_id)
|
|
252
|
+
logger.info(f"Flux {feed_id} supprimé !")
|
|
253
|
+
# Remove from crontab associated training
|
|
254
|
+
remove_scheduled_training_for_user(
|
|
255
|
+
model_id=feed_id, user=st.session_state.username
|
|
256
|
+
)
|
|
257
|
+
time.sleep(0.2)
|
|
258
|
+
st.rerun()
|
|
259
|
+
with col2:
|
|
260
|
+
if st.button("Non"):
|
|
261
|
+
st.rerun()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import streamlit as st
|
|
10
|
+
|
|
11
|
+
from bertrend import FEED_BASE_PATH
|
|
12
|
+
from bertrend.utils.data_loading import (
|
|
13
|
+
load_data,
|
|
14
|
+
TIMESTAMP_COLUMN,
|
|
15
|
+
TITLE_COLUMN,
|
|
16
|
+
URL_COLUMN,
|
|
17
|
+
TEXT_COLUMN,
|
|
18
|
+
)
|
|
19
|
+
from bertrend_apps.prospective_demo.feeds_common import get_all_files_for_feed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def display_data_status():
|
|
23
|
+
if not st.session_state.user_feeds:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
col1, col2 = st.columns(2)
|
|
27
|
+
with col1:
|
|
28
|
+
st.selectbox(
|
|
29
|
+
"Sélection de la veille",
|
|
30
|
+
options=sorted(st.session_state.user_feeds.keys()),
|
|
31
|
+
key="id_data",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
with col2:
|
|
35
|
+
if "data_time_window" not in st.session_state:
|
|
36
|
+
st.session_state.data_time_window = 7
|
|
37
|
+
st.slider(
|
|
38
|
+
"Fenêtre temporelle (jours)",
|
|
39
|
+
min_value=1,
|
|
40
|
+
max_value=60,
|
|
41
|
+
step=1,
|
|
42
|
+
key="data_time_window",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
display_data_info_for_feed(st.session_state.id_data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def display_data_info_for_feed(feed_id: str):
|
|
49
|
+
all_files = get_all_files_for_feed(st.session_state.user_feeds, feed_id)
|
|
50
|
+
df = get_all_data(files=all_files)
|
|
51
|
+
|
|
52
|
+
if df.empty:
|
|
53
|
+
df_filtered = pd.DataFrame()
|
|
54
|
+
else:
|
|
55
|
+
df = df[
|
|
56
|
+
[TITLE_COLUMN, URL_COLUMN, TEXT_COLUMN, TIMESTAMP_COLUMN]
|
|
57
|
+
] # filter useful columns
|
|
58
|
+
|
|
59
|
+
cutoff_date = datetime.datetime.now() - datetime.timedelta(
|
|
60
|
+
days=st.session_state.data_time_window
|
|
61
|
+
)
|
|
62
|
+
df_filtered = df[df[TIMESTAMP_COLUMN] >= cutoff_date]
|
|
63
|
+
|
|
64
|
+
stats = {
|
|
65
|
+
"ID": feed_id,
|
|
66
|
+
"# Fichiers": len(all_files),
|
|
67
|
+
"Date début": df[TIMESTAMP_COLUMN].min() if not df.empty else None,
|
|
68
|
+
"Date fin": df[TIMESTAMP_COLUMN].max() if not df.empty else None,
|
|
69
|
+
"# Articles": len(df),
|
|
70
|
+
f"# Articles ({st.session_state.data_time_window} derniers jours)": len(
|
|
71
|
+
df_filtered
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
st.dataframe(pd.DataFrame([stats]))
|
|
76
|
+
|
|
77
|
+
st.write(f"#### Données des derniers {st.session_state.data_time_window} jours")
|
|
78
|
+
st.dataframe(
|
|
79
|
+
df_filtered,
|
|
80
|
+
use_container_width=True,
|
|
81
|
+
hide_index=True,
|
|
82
|
+
column_config={"url": st.column_config.LinkColumn("url")},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@st.cache_data
|
|
87
|
+
def get_all_data(files: list[Path]) -> pd.DataFrame:
|
|
88
|
+
"""Returns the data contained in the provided files as a single DataFrame."""
|
|
89
|
+
if not files:
|
|
90
|
+
return pd.DataFrame()
|
|
91
|
+
dfs = [load_data(Path(f)) for f in files]
|
|
92
|
+
new_df = pd.concat(dfs).drop_duplicates(
|
|
93
|
+
subset=["title"], keep="first", inplace=False
|
|
94
|
+
)
|
|
95
|
+
return new_df
|