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.
Files changed (181) hide show
  1. bertrend/BERTopicModel.py +320 -0
  2. bertrend/BERTrend.py +1211 -0
  3. bertrend/__init__.py +59 -0
  4. bertrend/config/bertopic_default_config.toml +48 -0
  5. bertrend/config/bertrend_default_config.toml +16 -0
  6. bertrend/config/parameters.py +92 -0
  7. bertrend/config/services_default_config.toml +32 -0
  8. bertrend/demos/__init__.py +4 -0
  9. bertrend/demos/demos_utils/__init__.py +13 -0
  10. bertrend/demos/demos_utils/data_loading_component.py +267 -0
  11. bertrend/demos/demos_utils/embed_documents_component.py +52 -0
  12. bertrend/demos/demos_utils/icons.py +41 -0
  13. bertrend/demos/demos_utils/messages.py +11 -0
  14. bertrend/demos/demos_utils/parameters_component.py +162 -0
  15. bertrend/demos/demos_utils/state_utils.py +99 -0
  16. bertrend/demos/start_summarizer_app.sh +9 -0
  17. bertrend/demos/start_topic_analysis_app.sh +9 -0
  18. bertrend/demos/start_weak_signals_app.sh +9 -0
  19. bertrend/demos/summarization/__init__.py +4 -0
  20. bertrend/demos/summarization/summarizer_app.py +98 -0
  21. bertrend/demos/topic_analysis/__init__.py +4 -0
  22. bertrend/demos/topic_analysis/app.py +87 -0
  23. bertrend/demos/topic_analysis/app_utils.py +69 -0
  24. bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
  25. bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
  26. bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
  27. bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
  28. bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
  29. bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
  30. bertrend/demos/topic_analysis/messages.py +13 -0
  31. bertrend/demos/weak_signals/__init__.py +4 -0
  32. bertrend/demos/weak_signals/app.py +510 -0
  33. bertrend/demos/weak_signals/messages.py +32 -0
  34. bertrend/demos/weak_signals/visualizations_utils.py +403 -0
  35. bertrend/llm_utils/__init__.py +4 -0
  36. bertrend/llm_utils/newsletter.css +131 -0
  37. bertrend/llm_utils/newsletter_features.py +272 -0
  38. bertrend/llm_utils/openai_client.py +235 -0
  39. bertrend/llm_utils/prompts.py +94 -0
  40. bertrend/metrics/__init__.py +4 -0
  41. bertrend/metrics/temporal_metrics_embedding.py +861 -0
  42. bertrend/metrics/topic_metrics.py +199 -0
  43. bertrend/resources/common_ngrams.json +23 -0
  44. bertrend/resources/stopwords-en.json +3 -0
  45. bertrend/resources/stopwords-fr.json +1 -0
  46. bertrend/resources/stopwords-rte.json +15 -0
  47. bertrend/services/__init__.py +4 -0
  48. bertrend/services/authentication.py +66 -0
  49. bertrend/services/embedding_client.py +154 -0
  50. bertrend/services/embedding_service.py +380 -0
  51. bertrend/services/summarizer.py +45 -0
  52. bertrend/services/summary/__init__.py +4 -0
  53. bertrend/services/summary/abstractive_summarizer.py +74 -0
  54. bertrend/services/summary/chatgpt_summarizer.py +67 -0
  55. bertrend/services/summary/extractive_summarizer.py +368 -0
  56. bertrend/services/summary/lexrank.py +126 -0
  57. bertrend/services/summary/prompts.py +43 -0
  58. bertrend/tests/__init__.py +4 -0
  59. bertrend/tests/test_abstractive_summarizer.py +140 -0
  60. bertrend/tests/test_bertrend.py +1091 -0
  61. bertrend/tests/test_cache_utils.py +111 -0
  62. bertrend/tests/test_chatgpt_summarizer.py +150 -0
  63. bertrend/tests/test_config_utils.py +54 -0
  64. bertrend/tests/test_data_loading.py +326 -0
  65. bertrend/tests/test_data_structure.py +53 -0
  66. bertrend/tests/test_embedding_client.py +546 -0
  67. bertrend/tests/test_extractive_summarizer.py +250 -0
  68. bertrend/tests/test_installation.py +183 -0
  69. bertrend/tests/test_newsletter_features.py +354 -0
  70. bertrend/tests/test_openai_client.py +210 -0
  71. bertrend/tests/test_representative_docs.py +125 -0
  72. bertrend/tests/test_summarizer.py +115 -0
  73. bertrend/tests/test_temporal_metrics_embedding.py +349 -0
  74. bertrend/tests/test_topic_description.py +159 -0
  75. bertrend/tests/test_topic_metrics.py +233 -0
  76. bertrend/tests/test_topic_model.py +285 -0
  77. bertrend/tests/test_trend_analysis_data_structure.py +174 -0
  78. bertrend/tests/test_trend_analysis_prompts.py +225 -0
  79. bertrend/tests/test_trend_analysis_visualizations.py +307 -0
  80. bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
  81. bertrend/tests/test_utils_init.py +70 -0
  82. bertrend/tests/test_visualizations.py +306 -0
  83. bertrend/topic_analysis/__init__.py +4 -0
  84. bertrend/topic_analysis/data_structure.py +13 -0
  85. bertrend/topic_analysis/prompts.py +42 -0
  86. bertrend/topic_analysis/representative_docs.py +57 -0
  87. bertrend/topic_analysis/topic_description.py +66 -0
  88. bertrend/topic_analysis/visualizations.py +315 -0
  89. bertrend/trend_analysis/__init__.py +4 -0
  90. bertrend/trend_analysis/data_structure.py +78 -0
  91. bertrend/trend_analysis/prompts.py +229 -0
  92. bertrend/trend_analysis/signal_llm_template_en.html +181 -0
  93. bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
  94. bertrend/trend_analysis/visualizations.py +605 -0
  95. bertrend/trend_analysis/weak_signals.py +425 -0
  96. bertrend/utils/__init__.py +31 -0
  97. bertrend/utils/cache_utils.py +31 -0
  98. bertrend/utils/config_utils.py +41 -0
  99. bertrend/utils/data_loading.py +329 -0
  100. bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
  101. bertrend-0.3.1.dist-info/LICENSE.md +373 -0
  102. bertrend-0.3.1.dist-info/METADATA +400 -0
  103. bertrend-0.3.1.dist-info/RECORD +181 -0
  104. bertrend-0.3.1.dist-info/WHEEL +4 -0
  105. bertrend_apps/__init__.py +4 -0
  106. bertrend_apps/common/__init__.py +4 -0
  107. bertrend_apps/common/crontab_utils.py +113 -0
  108. bertrend_apps/common/mail_utils.py +91 -0
  109. bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
  110. bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
  111. bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
  112. bertrend_apps/config/feeds/llm_feed.toml +16 -0
  113. bertrend_apps/config/feeds/nlp_feed.toml +15 -0
  114. bertrend_apps/config/feeds/rte_feed.toml +15 -0
  115. bertrend_apps/config/gmail_credentials.json +1 -0
  116. bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
  117. bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
  118. bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
  119. bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
  120. bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
  121. bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
  122. bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
  123. bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
  124. bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
  125. bertrend_apps/data_provider/__init__.py +9 -0
  126. bertrend_apps/data_provider/__main__.py +237 -0
  127. bertrend_apps/data_provider/arxiv_provider.py +161 -0
  128. bertrend_apps/data_provider/bing_news_provider.py +82 -0
  129. bertrend_apps/data_provider/curebot_provider.py +105 -0
  130. bertrend_apps/data_provider/data_provider.py +194 -0
  131. bertrend_apps/data_provider/google_news_provider.py +94 -0
  132. bertrend_apps/data_provider/newscatcher_provider.py +74 -0
  133. bertrend_apps/data_provider/utils.py +165 -0
  134. bertrend_apps/exploration/__init__.py +4 -0
  135. bertrend_apps/exploration/curebot/__init__.py +4 -0
  136. bertrend_apps/exploration/curebot/app.py +45 -0
  137. bertrend_apps/exploration/curebot/app_utils.py +317 -0
  138. bertrend_apps/exploration/curebot/config.toml +33 -0
  139. bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
  140. bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
  141. bertrend_apps/exploration/curebot/prompts.py +24 -0
  142. bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
  143. bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
  144. bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
  145. bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
  146. bertrend_apps/exploration/dataviz/Main_page.py +10 -0
  147. bertrend_apps/exploration/dataviz/__init__.py +4 -0
  148. bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
  149. bertrend_apps/exploration/geolocalization/__init__.py +4 -0
  150. bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
  151. bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
  152. bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
  153. bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
  154. bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
  155. bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
  156. bertrend_apps/install_feeds_and_newsletters.py +29 -0
  157. bertrend_apps/newsletters/__init__.py +4 -0
  158. bertrend_apps/newsletters/__main__.py +277 -0
  159. bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
  160. bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
  161. bertrend_apps/prospective_demo/__init__.py +70 -0
  162. bertrend_apps/prospective_demo/app.py +111 -0
  163. bertrend_apps/prospective_demo/authentication.py +44 -0
  164. bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
  165. bertrend_apps/prospective_demo/dashboard_common.py +79 -0
  166. bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
  167. bertrend_apps/prospective_demo/feeds_common.py +38 -0
  168. bertrend_apps/prospective_demo/feeds_config.py +261 -0
  169. bertrend_apps/prospective_demo/feeds_data.py +95 -0
  170. bertrend_apps/prospective_demo/i18n.py +136 -0
  171. bertrend_apps/prospective_demo/llm_utils.py +34 -0
  172. bertrend_apps/prospective_demo/models_info.py +361 -0
  173. bertrend_apps/prospective_demo/perf_utils.py +23 -0
  174. bertrend_apps/prospective_demo/process_new_data.py +348 -0
  175. bertrend_apps/prospective_demo/report_generation.py +214 -0
  176. bertrend_apps/prospective_demo/report_template.html +148 -0
  177. bertrend_apps/prospective_demo/report_utils.py +132 -0
  178. bertrend_apps/prospective_demo/start_demo.sh +9 -0
  179. bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
  180. bertrend_apps/start_apps.sh +15 -0
  181. bertrend_apps/stop_apps.sh +11 -0
@@ -0,0 +1,9 @@
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
+ # Define a pattern for a basic URL validation
6
+ URL_PATTERN = (
7
+ r"^(https?://)?([a-z0-9-]+\.)+[a-z]{2,6}(:\d+)?(/[\w.-]*)*$|"
8
+ r"^(https?://)?(localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?(/[\w.-]*)*$"
9
+ )
@@ -0,0 +1,237 @@
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 os
7
+ import tempfile
8
+ from datetime import timedelta, datetime
9
+
10
+ import typer
11
+ from loguru import logger
12
+ from pathlib import Path
13
+
14
+ from bertrend import FEED_BASE_PATH, load_toml_config
15
+ from bertrend_apps.common.crontab_utils import schedule_scrapping
16
+ from bertrend_apps.data_provider.arxiv_provider import ArxivProvider
17
+ from bertrend_apps.data_provider.bing_news_provider import BingNewsProvider
18
+ from bertrend_apps.data_provider.curebot_provider import CurebotProvider
19
+ from bertrend_apps.data_provider.google_news_provider import GoogleNewsProvider
20
+ from bertrend_apps.data_provider.newscatcher_provider import NewsCatcherProvider
21
+
22
+ # Ensures to write with +rw for both user and groups
23
+ os.umask(0o002)
24
+
25
+ PROVIDERS = {
26
+ "arxiv": ArxivProvider,
27
+ "curebot": CurebotProvider,
28
+ "google": GoogleNewsProvider,
29
+ "bing": BingNewsProvider,
30
+ "newscatcher": NewsCatcherProvider,
31
+ }
32
+
33
+ if __name__ == "__main__":
34
+ app = typer.Typer()
35
+
36
+ @app.command("scrape")
37
+ def scrape(
38
+ keywords: str = typer.Argument(help="keywords for data search engine."),
39
+ provider: str = typer.Option(
40
+ "google", help="source for data [arxiv, google, bing, newscatcher]"
41
+ ),
42
+ after: str = typer.Option(
43
+ None, help="date after which to consider news [format YYYY-MM-DD]"
44
+ ),
45
+ before: str = typer.Option(
46
+ None, help="date before which to consider news [format YYYY-MM-DD]"
47
+ ),
48
+ max_results: int = typer.Option(
49
+ 50, help="maximum number of results per request"
50
+ ),
51
+ save_path: Path = typer.Option(
52
+ None, help="Path for writing results. File is in jsonl format."
53
+ ),
54
+ language: str = typer.Option(None, help="Language filter"),
55
+ ):
56
+ """Scrape data from Arxiv, Google, Bing or NewsCatcher news (single request).
57
+
58
+ Parameters
59
+ ----------
60
+ keywords: str
61
+ query described as keywords
62
+ provider: str
63
+ News data provider. Current authorized values [google, bing, newscatcher]
64
+ after: str
65
+ "from" date, formatted as YYYY-MM-DD
66
+ before: str
67
+ "to" date, formatted as YYYY-MM-DD
68
+ max_results: int
69
+ Maximum number of results per request
70
+ save_path: Path
71
+ Path to the output file (jsonl format)
72
+ language: str
73
+ Language filter
74
+
75
+ Returns
76
+ -------
77
+
78
+ """
79
+ provider_class = PROVIDERS.get(provider)
80
+ provider = provider_class()
81
+ results = provider.get_articles(keywords, after, before, max_results, language)
82
+ provider.store_articles(results, save_path)
83
+
84
+ @app.command("auto-scrape")
85
+ def auto_scrape(
86
+ requests_file: str = typer.Argument(
87
+ help="path of jsonlines input file containing the expected queries."
88
+ ),
89
+ max_results: int = typer.Option(
90
+ 50, help="maximum number of results per request"
91
+ ),
92
+ provider: str = typer.Option(
93
+ "google", help="source for news [google, bing, newscatcher]"
94
+ ),
95
+ save_path: Path = typer.Option(None, help="Path for writing results."),
96
+ language: str = typer.Option(None, help="Language filter"),
97
+ ):
98
+ """Scrape data from Arxiv, Google, Bing news or NewsCatcher (multiple requests from a configuration file: each line of the file shall be compliant with the following format:
99
+ <keyword list>;<after_date, format YYYY-MM-DD>;<before_date, format YYYY-MM-DD>)
100
+
101
+ Parameters
102
+ ----------
103
+ requests_file: str
104
+ Text file containing the list of requests to be processed
105
+ max_results: int
106
+ Maximum number of results per request
107
+ provider: str
108
+ News data provider. Current authorized values [google, bing, newscatcher]
109
+ save_path: Path
110
+ Path to the output file (jsonl format)
111
+ language: str
112
+ Language filter
113
+
114
+ Returns
115
+ -------
116
+
117
+ """
118
+ provider_class = PROVIDERS.get(provider)
119
+ provider = provider_class()
120
+ logger.info(f"Opening query file: {requests_file}")
121
+ with open(requests_file) as file:
122
+ try:
123
+ requests = [line.rstrip().split(";") for line in file]
124
+ except:
125
+ logger.error("Bad file format")
126
+ return -1
127
+ results = provider.get_articles_batch(requests, max_results, language)
128
+ logger.info(f"Storing {len(results)} articles")
129
+ provider.store_articles(results, save_path)
130
+
131
+ @app.command("generate-query-file")
132
+ def generate_query_file(
133
+ keywords: str = typer.Argument(help="keywords for news search engine."),
134
+ after: str = typer.Option(
135
+ None, help="date after which to consider news [format YYYY-MM-DD]"
136
+ ),
137
+ before: str = typer.Option(
138
+ None, help="date before which to consider news [format YYYY-MM-DD]"
139
+ ),
140
+ save_path: str = typer.Option(
141
+ None, help="Path for writing results. File is in jsonl format."
142
+ ),
143
+ interval: int = typer.Option(30, help="Range of days of atomic requests"),
144
+ ):
145
+ """Generates a query file to be used with the auto-scrape command. This is useful for queries generating many results.
146
+ This will split the broad query into many ones, each one covering an 'interval' (range) in days covered by each atomic
147
+ request.
148
+ If you want to cover several keywords, run the command several times with the same output file.
149
+
150
+ Parameters
151
+ ----------
152
+ keywords: str
153
+ query described as keywords
154
+ after: str
155
+ "from" date, formatted as YYYY-MM-DD
156
+ before: str
157
+ "to" date, formatted as YYYY-MM-DD
158
+ save_path: str
159
+ Path to the output file (jsonl format)
160
+
161
+ Returns
162
+ -------
163
+
164
+ """
165
+ date_format = "%Y-%m-%d"
166
+ start_date = datetime.strptime(after, date_format)
167
+ end_date = datetime.strptime(before, date_format)
168
+ dates_l = list(_daterange(start_date, end_date, interval))
169
+
170
+ with open(save_path, "a") as query_file:
171
+ for elem in dates_l:
172
+ query_file.write(
173
+ f"{keywords};{elem[0].strftime(date_format)};{elem[1].strftime(date_format)}\n"
174
+ )
175
+
176
+ def _daterange(start_date, end_date, ndays):
177
+ for n in range(int((end_date - start_date).days / ndays)):
178
+ yield (
179
+ start_date + timedelta(ndays * n),
180
+ start_date + timedelta(ndays * (n + 1)),
181
+ )
182
+
183
+ @app.command("scrape-feed")
184
+ def scrape_from_feed(
185
+ feed_cfg: Path = typer.Argument(help="Path of the data feed config file"),
186
+ ):
187
+ """Scrape data from Arxiv, Google, Bing news or NewsCatcher on the basis of a feed configuration file"""
188
+ data_feed_cfg = load_toml_config(feed_cfg)
189
+ current_date = datetime.today()
190
+ current_date_str = current_date.strftime("%Y-%m-%d")
191
+ days_to_subtract = data_feed_cfg["data-feed"].get("number_of_days")
192
+ provider = data_feed_cfg["data-feed"].get("provider")
193
+ keywords = data_feed_cfg["data-feed"].get("query")
194
+ max_results = data_feed_cfg["data-feed"].get("max_results")
195
+ before = current_date_str
196
+ after = (current_date - timedelta(days=days_to_subtract)).strftime("%Y-%m-%d")
197
+ language = data_feed_cfg["data-feed"].get("language")
198
+ save_path = (
199
+ FEED_BASE_PATH
200
+ / data_feed_cfg["data-feed"].get("feed_dir_path")
201
+ / f"{current_date_str}_{data_feed_cfg['data-feed'].get('id')}.jsonl"
202
+ )
203
+ save_path.parent.mkdir(parents=True, exist_ok=True)
204
+
205
+ # Generate a query file
206
+ with tempfile.NamedTemporaryFile() as query_file:
207
+ if provider == "arxiv" or provider == "curebot": # already returns batches
208
+ scrape(
209
+ keywords=keywords,
210
+ provider=provider,
211
+ after=after,
212
+ before=before,
213
+ max_results=max_results,
214
+ save_path=save_path,
215
+ language=language,
216
+ )
217
+ else:
218
+ generate_query_file(
219
+ keywords, after, before, interval=1, save_path=query_file.name
220
+ )
221
+ auto_scrape(
222
+ requests_file=query_file.name,
223
+ max_results=max_results,
224
+ provider=provider,
225
+ save_path=save_path,
226
+ language=language,
227
+ )
228
+
229
+ @app.command("schedule-scrapping")
230
+ def automate_scrapping(
231
+ feed_cfg: Path = typer.Argument(help="Path of the data feed config file"),
232
+ ):
233
+ """Schedule data scrapping on the basis of a feed configuration file"""
234
+ schedule_scrapping(feed_cfg)
235
+
236
+ ##################
237
+ app()
@@ -0,0 +1,161 @@
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 itertools
7
+ import os
8
+ from datetime import datetime
9
+ from collections import defaultdict
10
+
11
+ import arxiv
12
+
13
+ from loguru import logger
14
+ import requests
15
+
16
+ from bertrend.utils.data_loading import TEXT_COLUMN
17
+ from bertrend_apps.data_provider.data_provider import DataProvider
18
+ from bertrend_apps.data_provider.utils import wait
19
+
20
+ PAGE_SIZE = 2000
21
+ DELAY_SECONDS = 3
22
+ NUM_RETRIES = 10
23
+
24
+ ### Request parameters ###
25
+ query = "cat:cs.CL"
26
+ max_results = float("inf")
27
+
28
+ ### Parameters Semantic Scholar ###
29
+ SEMANTIC_SCHOLAR_API_KEY = os.getenv("SEMANTIC_SCHOLAR_API_KEY")
30
+ if not SEMANTIC_SCHOLAR_API_KEY:
31
+ raise ValueError("SEMANTIC_SCHOLAR_API_KEY environment variable is not set")
32
+
33
+ PAPERS_PER_REQUEST = 500 # should not be more than 500
34
+ SEMANTIC_SCHOLAR_FIELDS = "title,abstract,citationCount,publicationDate,url"
35
+
36
+ DATE_FORMAT_YYYYMMDD = "%Y-%m-%d"
37
+ DATE_FORMAT_YYYYMMDD_TIME = "%Y-%m-%d %H:%M:%S"
38
+
39
+
40
+ class ArxivProvider(DataProvider):
41
+ """Scientific articles providers for Arxiv"""
42
+
43
+ def __init__(self):
44
+ self.client = arxiv.Client(
45
+ page_size=PAGE_SIZE,
46
+ delay_seconds=DELAY_SECONDS,
47
+ num_retries=NUM_RETRIES,
48
+ )
49
+
50
+ def get_articles(
51
+ self,
52
+ query: str,
53
+ after: str,
54
+ before: str,
55
+ max_results: int,
56
+ language: str = None,
57
+ ) -> list[dict]:
58
+ """Requests the news data provider, collects a set of URLs to be parsed, return results as json lines.
59
+
60
+ Parameters
61
+ ----------
62
+ query: str
63
+ keywords describing the request
64
+ after: str
65
+ date after which to consider articles, formatted as YYYY-MM-DD, currently ignored
66
+ before: str
67
+ date before which to consider articles, formatted as YYYY-MM-DD, currently ignored
68
+ max_results: int
69
+ Maximum number of results per request
70
+ language: str
71
+ Language filter
72
+
73
+ Returns
74
+ -------
75
+ A list of dict entries, each one describing an article
76
+ """
77
+ begin = datetime.strptime(after, DATE_FORMAT_YYYYMMDD)
78
+ end = datetime.strptime(before, DATE_FORMAT_YYYYMMDD)
79
+
80
+ logger.info(f"Querying Arxiv: {query}")
81
+ entries = list(
82
+ self.client.results(
83
+ arxiv.Search(
84
+ query=query,
85
+ max_results=max_results,
86
+ sort_by=arxiv.SortCriterion.SubmittedDate,
87
+ sort_order=arxiv.SortOrder.Descending,
88
+ )
89
+ )
90
+ )
91
+ results = self.process_entries(entries, language)
92
+ # post-filtering by date
93
+ results = [
94
+ res
95
+ for res in results
96
+ if begin
97
+ <= datetime.strptime(res["timestamp"], DATE_FORMAT_YYYYMMDD_TIME)
98
+ <= end
99
+ ]
100
+ # add citations count
101
+ return self.add_citations_count(results)
102
+
103
+ def _parse_entry(self, entry: arxiv.Result) -> dict | None:
104
+ """Parses a Arxiv entry"""
105
+ try:
106
+ id = entry.entry_id
107
+ title = entry.title
108
+ published = entry.published.strftime(
109
+ "%Y-%m-%d %H:%M:%S"
110
+ ) # dateparser.parse(entry.published).strftime("%Y-%m-%d %H:%M:%S")
111
+ logger.debug(f"----- Title: {title},\tDate: {published}")
112
+ return {
113
+ "id": id,
114
+ "title": title,
115
+ "summary": entry.summary,
116
+ "text": entry.summary,
117
+ "timestamp": published,
118
+ }
119
+ except Exception as e:
120
+ logger.error(str(e) + f"\nError occurred with parsing of: {entry}")
121
+ return None
122
+
123
+ @wait(1)
124
+ def _request_semantic_scholar_chunk(self, chunk: list[dict]):
125
+ """Get information from semantic scholar API per batch of articles IDs"""
126
+ ids_list = ["URL:" + entry["id"] for entry in chunk]
127
+ response = requests.post(
128
+ "https://api.semanticscholar.org/graph/v1/paper/batch",
129
+ params={"fields": SEMANTIC_SCHOLAR_FIELDS},
130
+ json={"ids": ids_list},
131
+ headers={"x-api-key": SEMANTIC_SCHOLAR_API_KEY},
132
+ )
133
+ return [item for item in response.json() if item is not None]
134
+
135
+ def add_citations_count(self, entries: list[dict]):
136
+ """Uses the semantic_scholar API to get the number of counts per paper"""
137
+ # split list into chunks and request semantic scholar
138
+ chunks = [
139
+ entries[i : i + PAPERS_PER_REQUEST]
140
+ for i in range(0, len(entries), PAPERS_PER_REQUEST)
141
+ ]
142
+ # request API
143
+ semantic_scholar_items_list = list(
144
+ itertools.chain(
145
+ [self._request_semantic_scholar_chunk(chunk) for chunk in chunks]
146
+ )
147
+ )[0]
148
+
149
+ # merge semantic scholar entries with arxiv entries
150
+
151
+ # update values of entries
152
+ d = defaultdict(dict)
153
+
154
+ for item in semantic_scholar_items_list + entries:
155
+ # careful, order is important, semantic_scholar_items may contain fewer items than entries
156
+ d[item["title"]].update(item)
157
+
158
+ # filter out possible missing values
159
+ new_entries = [item for item in list(d.values()) if TEXT_COLUMN in item.keys()]
160
+
161
+ return new_entries
@@ -0,0 +1,82 @@
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 urllib.parse
7
+
8
+ import dateparser
9
+ import feedparser
10
+ from loguru import logger
11
+
12
+ from bertrend_apps.data_provider.data_provider import DataProvider
13
+ from bertrend_apps.data_provider.utils import wait
14
+
15
+ PATTERN = "{QUERY}"
16
+
17
+
18
+ class BingNewsProvider(DataProvider):
19
+ """News provider for Bing News.
20
+ Limitations:
21
+ - since of results limited to 12
22
+ - hard to request specific dates
23
+ """
24
+
25
+ URL_ENDPOINT = f"https://www.bing.com/news/search?q={PATTERN}&format=rss&setLang=fr&sortBy=Date"
26
+
27
+ def __init__(self):
28
+ super().__init__()
29
+
30
+ @wait(0.2)
31
+ def get_articles(
32
+ self,
33
+ query: str,
34
+ after: str,
35
+ before: str,
36
+ max_results: int,
37
+ language: str = None,
38
+ ) -> list[dict]:
39
+ """Requests the news data provider, collects a set of URLs to be parsed, return results as json lines"""
40
+ q = self._build_query(query, after, before)
41
+ logger.info(f"Querying Bing: {q}")
42
+ result = feedparser.parse(q)
43
+ entries = result["entries"][:max_results]
44
+ return self.process_entries(entries, language)
45
+
46
+ def _build_query(self, keywords: str, after: str = None, before: str = None) -> str:
47
+ # FIXME: don't know how to use after/before parameters with Bing news queries
48
+ query = self.URL_ENDPOINT.replace(PATTERN, f"{urllib.parse.quote(keywords)}")
49
+ return query
50
+
51
+ def _clean_url(self, bing_url) -> str:
52
+ """Clean encoded URLs returned by Bing news such as "http://www.bing.com/news/apiclick.aspx?ref=FexRss&amp;aid=&amp;tid=649475a6257945d6900378c8310bcfde&amp;url=https%3a%2f%2fwww.lemondeinformatique.fr%2factualites%2flire-avec-schema-gpt-translator-datastax-automatise-la-creation-de-pipelines-de-donnees-90737.html&amp;c=15009376565431680830&amp;mkt=fr-fr" """
53
+ try:
54
+ clean_url = bing_url.split("url=")[1].split("& ")[0]
55
+ return urllib.parse.unquote(clean_url)
56
+ except IndexError:
57
+ # fallback (the URL does not match the expected pattern)
58
+ return bing_url
59
+
60
+ def _parse_entry(self, entry: dict) -> dict | None:
61
+ """Parses a Bing news entry, uses wait decorator to force delay between 2 successive calls"""
62
+ try:
63
+ link = entry["link"]
64
+ url = self._clean_url(link)
65
+ summary = entry["summary"]
66
+ published = dateparser.parse(entry["published"]).strftime(
67
+ "%Y-%m-%d %H:%M:%S"
68
+ )
69
+ text, title = self._get_text(url=link)
70
+ return {
71
+ "title": title,
72
+ "summary": summary,
73
+ "link": link,
74
+ "url": url,
75
+ "text": text,
76
+ "timestamp": published,
77
+ }
78
+ except Exception as e:
79
+ logger.error(
80
+ str(e) + f"\nError occurred with text parsing of url in : {entry}"
81
+ )
82
+ return None
@@ -0,0 +1,105 @@
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
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+ from loguru import logger
10
+
11
+ from bertrend_apps.data_provider import URL_PATTERN
12
+ from bertrend_apps.data_provider.data_provider import DataProvider
13
+ import feedparser
14
+
15
+
16
+ class CurebotProvider(DataProvider):
17
+ """Class used to process exports from Curebot tool"""
18
+
19
+ def __init__(self, curebot_export_file: Path = None, feed_url: str = None):
20
+ super().__init__()
21
+ self.data_file = curebot_export_file
22
+ if self.data_file:
23
+ self.df_dict = pd.read_excel(self.data_file, sheet_name=None, dtype=str)
24
+ else:
25
+ self.df_dict = None
26
+ self.feed_url = feed_url
27
+
28
+ def get_articles(
29
+ self,
30
+ query: str = None,
31
+ after: str = None,
32
+ before: str = None,
33
+ max_results: int = None,
34
+ language: str = "fr",
35
+ ) -> list[dict]:
36
+ """Requests the news data provider, collects a set of URLs to be parsed, return results as json lines"""
37
+ if query and re.match(URL_PATTERN, query):
38
+ # if using a config file, the "query" field may contain the feed url
39
+ self.feed_url = query
40
+ if self.feed_url:
41
+ return self.parse_ATOM_feed()
42
+
43
+ if self.df_dict:
44
+ entries = []
45
+ for k in self.df_dict.keys():
46
+ entries += self.df_dict[k].to_dict(orient="records")
47
+ results = [self._parse_entry(res) for res in entries]
48
+ return [
49
+ res for res in results if res is not None
50
+ ] # sanity check to remove errors
51
+
52
+ return []
53
+
54
+ def parse_ATOM_feed(self) -> list[dict]:
55
+ feed = feedparser.parse(self.feed_url)
56
+ # Initialize an empty list to store the entries
57
+ entries = []
58
+ # Extract information for each entry
59
+ for entry in feed.entries:
60
+ link = entry.get("link", "")
61
+ summary = entry.get("summary", "")
62
+ published = entry.get("published", "")
63
+ formatted_date = self.parse_date(published)
64
+ text, title = self._get_text(url=link)
65
+ if text is None or text == "":
66
+ continue
67
+ # Store the extracted information in a dictionary
68
+ entry_info = {
69
+ "title": title,
70
+ "link": link,
71
+ "url": link,
72
+ "summary": summary,
73
+ "text": text,
74
+ "timestamp": formatted_date,
75
+ }
76
+ # Add the dictionary to the list
77
+ entries.append(entry_info)
78
+
79
+ return entries
80
+
81
+ def _parse_entry(self, entry: dict) -> dict | None:
82
+ """Parses a Curebot news entry"""
83
+ try:
84
+ # NB. we do not use the title from Gnews as it is sometimes truncated
85
+ link = entry["URL de la ressource"]
86
+ url = link
87
+ summary = entry["Contenu de la ressource"]
88
+ published = self.parse_date(entry["Date de trouvaille"])
89
+ text, title = self._get_text(url=url)
90
+ if text is None or text == "":
91
+ return None
92
+ logger.debug(f"----- Title: {title},\tDate: {published}")
93
+ return {
94
+ "title": title,
95
+ "summary": summary,
96
+ "link": link,
97
+ "url": url,
98
+ "text": text,
99
+ "timestamp": published,
100
+ }
101
+ except Exception as e:
102
+ logger.error(
103
+ str(e) + f"\nError occurred with text parsing of url in : {entry}"
104
+ )
105
+ return None