IO4IT 4.0.2__tar.gz → 4.0.4__tar.gz

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 (97) hide show
  1. {io4it-4.0.2 → io4it-4.0.4}/PKG-INFO +1 -1
  2. {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/PKG-INFO +1 -1
  3. io4it-4.0.4/orangecontrib/IO4IT/widgets/OWWebSearch.py +647 -0
  4. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owwebsearch.ui +71 -4
  5. {io4it-4.0.2 → io4it-4.0.4}/setup.py +1 -1
  6. io4it-4.0.2/orangecontrib/IO4IT/widgets/OWWebSearch.py +0 -307
  7. {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/SOURCES.txt +0 -0
  8. {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/dependency_links.txt +0 -0
  9. {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/entry_points.txt +0 -0
  10. {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/namespace_packages.txt +0 -0
  11. {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/requires.txt +0 -0
  12. {io4it-4.0.2 → io4it-4.0.4}/io4it.egg-info/top_level.txt +0 -0
  13. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/__init__.py +0 -0
  14. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/ocr_function/__init__.py +0 -0
  15. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/ocr_function/word_converter.py +0 -0
  16. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/designer/key_manager_ui.ui +0 -0
  17. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/designer/link_workflow_manager_ui.ui +0 -0
  18. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/key_manager_ui.py +0 -0
  19. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/toolViews/linkHTLMWorkflow_manager_ui.py +0 -0
  20. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/__init__.py +0 -0
  21. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/keys_manager.py +0 -0
  22. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/mail.py +0 -0
  23. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/pool_exec_utils.py +0 -0
  24. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/secret_manager.py +0 -0
  25. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/utils/utils_md.py +0 -0
  26. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWChatGpt.py +0 -0
  27. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWDeep_Search.py +0 -0
  28. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWDoclingToMarkdown.py +0 -0
  29. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWExportMarkdown.py +0 -0
  30. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWExtractTablesDocxToXlsx.py +0 -0
  31. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWInboxMailMonitoring.py +0 -0
  32. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWJsonToDataTable.py +0 -0
  33. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWMD2HTML.py +0 -0
  34. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWMarkdownLoader.py +0 -0
  35. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWMarkdownizer.py +0 -0
  36. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWOfficeNormalizer.py +0 -0
  37. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWParserHTML.py +0 -0
  38. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWPdfType.py +0 -0
  39. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWProcessPoolExecutor.py +0 -0
  40. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWS3Uploader.py +0 -0
  41. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWS3downloader.py +0 -0
  42. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWS3list.py +0 -0
  43. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWmailLoader.py +0 -0
  44. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWmailSender.py +0 -0
  45. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/OWwordpdf2docx.py +0 -0
  46. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/__init__.py +0 -0
  47. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/__init__.py +0 -0
  48. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/nogui.ui +0 -0
  49. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/ow_extract_tables_docx_to_xlsx.ui +0 -0
  50. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/ow_file_ext_selector.ui +0 -0
  51. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/ow_json_to_data_table.ui +0 -0
  52. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owchatgpt.ui +0 -0
  53. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owdeepsearch.ui +0 -0
  54. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owdoclingasr.ui +0 -0
  55. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owdoclingtomarkdown.ui +0 -0
  56. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owexportmarkdown.ui +0 -0
  57. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owinboxmailmonitoring.ui +0 -0
  58. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmailloader.ui +0 -0
  59. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmailsender.ui +0 -0
  60. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmarkdownizer.ui +0 -0
  61. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmarkdownloader.ui +0 -0
  62. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owmd2html.ui +0 -0
  63. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owofficenormalizer.ui +0 -0
  64. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owparserhtml.ui +0 -0
  65. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owpdftype.ui +0 -0
  66. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owprocesspoolexecutor.ui +0 -0
  67. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owspeechtotext.ui +0 -0
  68. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/owvisualizationer.ui +0 -0
  69. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/designer/wordpdf2docx.ui +0 -0
  70. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/__init__.py +0 -0
  71. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/chatgpt.png +0 -0
  72. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/check_pdf.png +0 -0
  73. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/deepsearch.svg +0 -0
  74. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/dep_md_old.png +0 -0
  75. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/download.png +0 -0
  76. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/export_md.png +0 -0
  77. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/extract_table.png +0 -0
  78. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/file_extensor.png +0 -0
  79. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/html.png +0 -0
  80. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/json-file.png +0 -0
  81. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/list_aws.png +0 -0
  82. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/load_md.png +0 -0
  83. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/mail_loader.png +0 -0
  84. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/mail_writer.png +0 -0
  85. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/md.png +0 -0
  86. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/monitor-email.svg +0 -0
  87. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/office_normalizer.png +0 -0
  88. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/owmd2html.svg +0 -0
  89. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/process_pool_executor.png +0 -0
  90. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/speech_to_text.png +0 -0
  91. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/upload.png +0 -0
  92. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/visualizationer.png +0 -0
  93. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/websearch.png +0 -0
  94. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons/wordpdf2docx.png +0 -0
  95. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/IO4IT/widgets/icons_dev/__init__.py +0 -0
  96. {io4it-4.0.2 → io4it-4.0.4}/orangecontrib/__init__.py +0 -0
  97. {io4it-4.0.2 → io4it-4.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: io4it
3
- Version: 4.0.2
3
+ Version: 4.0.4
4
4
  Home-page:
5
5
  Author:
6
6
  Author-email:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: io4it
3
- Version: 4.0.2
3
+ Version: 4.0.4
4
4
  Home-page:
5
5
  Author:
6
6
  Author-email:
@@ -0,0 +1,647 @@
1
+ import os
2
+ import sys
3
+ import threading
4
+ import time
5
+ import unicodedata
6
+ import re
7
+
8
+ import Orange
9
+ from Orange.widgets.widget import Input, Output
10
+ from AnyQt.QtWidgets import QApplication, QPushButton, QLabel, QWidget, QVBoxLayout, QGroupBox
11
+ from AnyQt.QtCore import QUrl, QTimer, Signal
12
+ from Orange.widgets.settings import Setting
13
+ from AnyQt.QtWebEngineWidgets import QWebEngineView
14
+
15
+ import requests
16
+ from bs4 import BeautifulSoup
17
+ from urllib.parse import quote, urlparse, urljoin, parse_qs
18
+
19
+ if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
20
+ from Orange.widgets.orangecontrib.AAIT.utils import thread_management, base_widget
21
+ from Orange.widgets.orangecontrib.HLIT_dev.remote_server_smb import convert
22
+ else:
23
+ from orangecontrib.AAIT.utils import thread_management, base_widget
24
+ from orangecontrib.HLIT_dev.remote_server_smb import convert
25
+
26
+
27
+
28
+
29
+
30
+ class WebSearch(base_widget.BaseListWidget):
31
+ name = "WebSearch"
32
+ description = "Search url website from a query with DDG."
33
+ icon = "icons/websearch.png"
34
+ if "site-packages/Orange/widgets" in os.path.dirname(os.path.abspath(__file__)).replace("\\", "/"):
35
+ icon = "icons_dev/websearch.png"
36
+ priority = 3000
37
+ gui = ""
38
+ want_control_area = False
39
+ category = "AAIT - TOOLBOX"
40
+ gui = os.path.join(os.path.dirname(os.path.abspath(__file__)), "designer/owwebsearch.ui")
41
+
42
+ selected_column_name = Setting("content")
43
+
44
+ browser_request_signal = Signal(str, int)
45
+
46
+ class Inputs:
47
+ data = Input("Data", Orange.data.Table)
48
+
49
+ class Outputs:
50
+ data = Output("Data", Orange.data.Table)
51
+
52
+ @Inputs.data
53
+ def set_data(self, in_data):
54
+ self.data = in_data
55
+ if in_data is None:
56
+ self.Outputs.data.send(None)
57
+ return
58
+ if self.data:
59
+ self.var_selector.add_variables(self.data.domain)
60
+ self.var_selector.select_variable_by_name(self.selected_column_name)
61
+ self.run()
62
+
63
+ def __init__(self):
64
+ super().__init__()
65
+
66
+ self.setFixedWidth(480)
67
+ self.setFixedHeight(760)
68
+
69
+ self.pushButton_run = self.findChild(QPushButton, 'pushButton_send')
70
+ self.pushButton_run.clicked.connect(self.run)
71
+
72
+ # UI viewer embarqué
73
+ self.groupBox_browser = self.findChild(QGroupBox, "groupBox_browser")
74
+ self.label_browser_status = self.findChild(QLabel, "label_browser_status")
75
+ self.web_placeholder = self.findChild(QWidget, "web_placeholder")
76
+ self.pushButton_browser_check = self.findChild(QPushButton, "pushButton_browser_check")
77
+ self.pushButton_browser_close = self.findChild(QPushButton, "pushButton_browser_close")
78
+
79
+ self.browser_view = None
80
+ self.browser_timer = QTimer(self)
81
+ self.browser_timer.timeout.connect(self._browser_periodic_check)
82
+
83
+ self._browser_request_event = None
84
+ self._browser_request_data = None
85
+ self._browser_wait_deadline = 0.0
86
+ self._browser_active = False
87
+ self._browser_last_html = ""
88
+
89
+ # Cookies du viewer Qt -> requests.Session
90
+ self.browser_cookies = []
91
+ self.browser_cookie_lock = threading.Lock()
92
+
93
+ self.browser_request_signal.connect(self._open_browser_for_query)
94
+ self._init_embedded_browser_ui()
95
+
96
+ def _init_embedded_browser_ui(self):
97
+ if self.groupBox_browser is not None:
98
+ self.groupBox_browser.hide()
99
+
100
+ if self.pushButton_browser_check is not None:
101
+ self.pushButton_browser_check.clicked.connect(self._browser_check_current_page)
102
+
103
+ if self.pushButton_browser_close is not None:
104
+ self.pushButton_browser_close.clicked.connect(self._browser_cancel_request)
105
+
106
+ if QWebEngineView is None:
107
+ if self.label_browser_status is not None:
108
+ self.label_browser_status.setText("QWebEngineView is not available.")
109
+ if self.pushButton_browser_check is not None:
110
+ self.pushButton_browser_check.setEnabled(False)
111
+ if self.pushButton_browser_close is not None:
112
+ self.pushButton_browser_close.setEnabled(False)
113
+ return
114
+
115
+ if self.web_placeholder is None:
116
+ return
117
+
118
+ layout = self.web_placeholder.layout()
119
+ if layout is None:
120
+ layout = QVBoxLayout(self.web_placeholder)
121
+ layout.setContentsMargins(0, 0, 0, 0)
122
+ layout.setSpacing(0)
123
+
124
+ self.browser_view = QWebEngineView(self.web_placeholder)
125
+ layout.addWidget(self.browser_view)
126
+ self.browser_view.hide()
127
+ self.browser_view.loadFinished.connect(self._browser_on_load_finished)
128
+
129
+ try:
130
+ cookie_store = self.browser_view.page().profile().cookieStore()
131
+ cookie_store.cookieAdded.connect(self._on_cookie_added)
132
+ except Exception as e:
133
+ print(f"[WARN] Impossible de brancher le cookie store Qt: {e}")
134
+
135
+ def _on_cookie_added(self, cookie):
136
+ try:
137
+ name = bytes(cookie.name()).decode("utf-8", errors="ignore")
138
+ value = bytes(cookie.value()).decode("utf-8", errors="ignore")
139
+ domain = cookie.domain() if hasattr(cookie, "domain") else ""
140
+ path = cookie.path() if hasattr(cookie, "path") else "/"
141
+
142
+ item = {
143
+ "name": name,
144
+ "value": value,
145
+ "domain": domain or "",
146
+ "path": path or "/",
147
+ }
148
+
149
+ with self.browser_cookie_lock:
150
+ replaced = False
151
+ for i, existing in enumerate(self.browser_cookies):
152
+ if (
153
+ existing["name"] == item["name"]
154
+ and existing["domain"] == item["domain"]
155
+ and existing["path"] == item["path"]
156
+ ):
157
+ self.browser_cookies[i] = item
158
+ replaced = True
159
+ break
160
+ if not replaced:
161
+ self.browser_cookies.append(item)
162
+ except Exception as e:
163
+ print(f"[WARN] _on_cookie_added error: {e}")
164
+
165
+ def _copy_qt_cookies_to_session(self, session):
166
+ with self.browser_cookie_lock:
167
+ cookies_snapshot = list(self.browser_cookies)
168
+
169
+ for c in cookies_snapshot:
170
+ try:
171
+ name = c.get("name", "")
172
+ value = c.get("value", "")
173
+ domain = c.get("domain", "")
174
+ path = c.get("path", "/") or "/"
175
+
176
+ if not name:
177
+ continue
178
+
179
+ if domain:
180
+ session.cookies.set(name, value, domain=domain, path=path)
181
+ # variante sans le point initial éventuel
182
+ if domain.startswith("."):
183
+ session.cookies.set(name, value, domain=domain.lstrip("."), path=path)
184
+ else:
185
+ session.cookies.set(name, value, path=path)
186
+ except Exception as e:
187
+ print(f"[WARN] copy cookie error: {e}")
188
+
189
+ def _get_browser_user_agent(self):
190
+ try:
191
+ if self.browser_view is not None:
192
+ return self.browser_view.page().profile().httpUserAgent()
193
+ except Exception:
194
+ pass
195
+ return "Mozilla/5.0"
196
+
197
+ def _is_duckduckgo_challenge(self, html):
198
+ if not html:
199
+ return False
200
+ h = html.lower()
201
+ markers = [
202
+ "anomaly-modal",
203
+ "bots use duckduckgo too",
204
+ "please complete the following challenge",
205
+ "select all squares containing",
206
+ "challenge-form",
207
+ "anomaly.js",
208
+ ]
209
+ return any(m in h for m in markers)
210
+
211
+ def _parse_ddg_results_from_html(self, html, max_results):
212
+ soup = BeautifulSoup(html, "html.parser")
213
+ resultats = []
214
+
215
+ for a in soup.select("a.result__a")[:max_results]:
216
+ titre = a.get_text(strip=True)
217
+ lien = a.get("href", "")
218
+
219
+ parsed = urlparse(lien)
220
+ if parsed.netloc and "duckduckgo.com" in parsed.netloc and parsed.path.startswith("/l/"):
221
+ qs = parse_qs(parsed.query)
222
+ if "uddg" in qs:
223
+ lien = qs["uddg"][0]
224
+
225
+ if lien.startswith("/"):
226
+ lien = urljoin("https://duckduckgo.com", lien)
227
+
228
+ resultats.append((titre, lien))
229
+
230
+ return resultats
231
+
232
+ def _open_browser_for_query(self, query, wait_timeout):
233
+ if self._browser_request_data is None:
234
+ return
235
+
236
+ if self.browser_view is None:
237
+ self._browser_finish_request(html=None, cancelled=True, message="QWebEngineView unavailable.")
238
+ return
239
+
240
+ q = quote(query)
241
+ url = f"https://duckduckgo.com/html/?q={q}"
242
+
243
+ self._browser_active = True
244
+ self._browser_last_html = ""
245
+ self._browser_wait_deadline = time.time() + wait_timeout
246
+
247
+ if self.groupBox_browser is not None:
248
+ self.groupBox_browser.show()
249
+
250
+ if self.label_browser_status is not None:
251
+ self.label_browser_status.setText(
252
+ "Captcha detected.\n"
253
+ "Solve it in the embedded browser."
254
+ )
255
+
256
+ self.browser_view.show()
257
+ self.browser_view.setUrl(QUrl(url))
258
+ self.browser_timer.start(2000)
259
+
260
+ def _browser_on_load_finished(self, ok):
261
+ if not self._browser_active:
262
+ return
263
+ if not ok:
264
+ if self.label_browser_status is not None:
265
+ self.label_browser_status.setText("DuckDuckGo load error.")
266
+ return
267
+ self._browser_check_current_page()
268
+
269
+ def _browser_periodic_check(self):
270
+ if not self._browser_active:
271
+ self.browser_timer.stop()
272
+ return
273
+
274
+ if time.time() >= self._browser_wait_deadline:
275
+ self._browser_finish_request(html=None, cancelled=True, message="Captcha timeout.")
276
+ return
277
+
278
+ self._browser_check_current_page()
279
+
280
+ def _browser_check_current_page(self):
281
+ if not self._browser_active or self.browser_view is None:
282
+ return
283
+
284
+ try:
285
+ page = self.browser_view.page()
286
+ if page is None:
287
+ return
288
+ page.toHtml(self._browser_process_html)
289
+ except Exception as e:
290
+ if self.label_browser_status is not None:
291
+ self.label_browser_status.setText(f"Check error: {e}")
292
+
293
+ def _browser_process_html(self, html):
294
+ if not self._browser_active:
295
+ return
296
+
297
+ html = html or ""
298
+ self._browser_last_html = html
299
+
300
+ if self._is_duckduckgo_challenge(html):
301
+ if self.label_browser_status is not None:
302
+ self.label_browser_status.setText(
303
+ "Captcha still present.\n"
304
+ "Solve it, then wait for automatic detection."
305
+ )
306
+ return
307
+
308
+ if self.label_browser_status is not None:
309
+ self.label_browser_status.setText("Results detected.")
310
+
311
+ self._browser_finish_request(html=html, cancelled=False, message=None)
312
+
313
+ def _browser_cancel_request(self):
314
+ self._browser_finish_request(html=None, cancelled=True, message="Validation cancelled.")
315
+
316
+ def _browser_finish_request(self, html=None, cancelled=False, message=None):
317
+ self.browser_timer.stop()
318
+ self._browser_active = False
319
+
320
+ if self.browser_view is not None:
321
+ try:
322
+ self.browser_view.stop()
323
+ except Exception:
324
+ pass
325
+ try:
326
+ self.browser_view.setUrl(QUrl("about:blank"))
327
+ except Exception:
328
+ pass
329
+ self.browser_view.hide()
330
+
331
+ if self.groupBox_browser is not None:
332
+ self.groupBox_browser.hide()
333
+
334
+ if self.label_browser_status is not None:
335
+ if message:
336
+ self.label_browser_status.setText(message)
337
+ else:
338
+ self.label_browser_status.setText(
339
+ "The embedded browser will appear here only if manual validation is needed."
340
+ )
341
+
342
+ if self._browser_request_data is not None:
343
+ self._browser_request_data["html"] = html
344
+ self._browser_request_data["cancelled"] = cancelled
345
+
346
+ if self._browser_request_event is not None:
347
+ self._browser_request_event.set()
348
+
349
+ def normaliser_texte(self, txt: str) -> str:
350
+ """
351
+ Met en minuscules, retire les accents et trim.
352
+ """
353
+ if not txt:
354
+ return ""
355
+ txt = txt.lower()
356
+ txt = unicodedata.normalize("NFD", txt)
357
+ txt = "".join(c for c in txt if not unicodedata.combining(c))
358
+ return txt.strip()
359
+
360
+ def extraire_mots_cles(self, requete: str):
361
+ """
362
+ Découpe la requête en mots-clés simples, en retirant
363
+ les mots très fréquents (du, de, le, la, etc.).
364
+ """
365
+ stopwords = {"du", "de", "des", "le", "la", "les", "un", "une", "au", "aux", "et", "en", "pour", "sur", "a"}
366
+ req_norm = self.normaliser_texte(requete)
367
+ mots = re.findall(r"\w+", req_norm)
368
+ mots_cles = [m for m in mots if m not in stopwords and len(m) > 2]
369
+ return mots_cles or mots
370
+
371
+ def recherche_duckduckgo(self, query, max_results=10):
372
+ q = quote(query)
373
+ url = f"https://duckduckgo.com/html/?q={q}"
374
+
375
+ session = requests.Session()
376
+ headers = {
377
+ "User-Agent": self._get_browser_user_agent(),
378
+ # "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8",
379
+ # "Referer": "https://duckduckgo.com/",
380
+ }
381
+
382
+ def do_request():
383
+ r = session.get(url, headers=headers, timeout=10)
384
+ r.raise_for_status()
385
+ return r.text
386
+
387
+ try:
388
+ r = session.get(url, headers=headers, timeout=10)
389
+ r.raise_for_status()
390
+ except requests.RequestException as e:
391
+ print(f"[ERREUR] Problème lors de la requête DuckDuckGo : {e}")
392
+ return []
393
+
394
+ html = r.text
395
+
396
+ # Cas normal = ton code d'origine
397
+ if not self._is_duckduckgo_challenge(html):
398
+ return self._parse_ddg_results_from_html(html, max_results)
399
+
400
+ # Cas captcha = viewer Qt
401
+ print("[INFO] Challenge DuckDuckGo détecté.")
402
+ print("[INFO] Ouverture du navigateur embarqué pour résolution manuelle...")
403
+
404
+ if QWebEngineView is None or self.browser_view is None:
405
+ print("[ERREUR] QWebEngineView non disponible.")
406
+ return []
407
+
408
+ wait_event = threading.Event()
409
+ request_data = {
410
+ "html": None,
411
+ "cancelled": False,
412
+ }
413
+
414
+ self._browser_request_event = wait_event
415
+ self._browser_request_data = request_data
416
+
417
+ self.browser_request_signal.emit(query, 120)
418
+ wait_event.wait(125)
419
+
420
+ data = self._browser_request_data
421
+ self._browser_request_event = None
422
+ self._browser_request_data = None
423
+
424
+ if not data or data.get("cancelled"):
425
+ print("[ERREUR] Validation DuckDuckGo annulée ou timeout.")
426
+ return []
427
+
428
+ viewer_html = data.get("html") or ""
429
+ if not viewer_html:
430
+ print("[ERREUR] HTML vide après validation.")
431
+ return []
432
+
433
+ if self._is_duckduckgo_challenge(viewer_html):
434
+ print("[ERREUR] Captcha toujours présent après validation.")
435
+ return []
436
+
437
+ # On copie les cookies Qt -> requests.Session
438
+ self._copy_qt_cookies_to_session(session)
439
+
440
+ # 1) on tente de relancer requests avec les mêmes cookies
441
+ try:
442
+ html_after = do_request()
443
+ if not self._is_duckduckgo_challenge(html_after):
444
+ resultats = self._parse_ddg_results_from_html(html_after, max_results)
445
+ if resultats:
446
+ return resultats
447
+ except requests.RequestException as e:
448
+ print(f"[WARN] Requête requests après synchro cookies en échec: {e}")
449
+
450
+ # 2) fallback: on parse directement le HTML du viewer
451
+ resultats = self._parse_ddg_results_from_html(viewer_html, max_results)
452
+ if resultats:
453
+ return resultats
454
+
455
+ print("[ERREUR] Aucun résultat parsable après validation captcha.")
456
+ return []
457
+
458
+ def extraire_domaines(self, resultats_search):
459
+ domaines = set()
460
+ for _titre, url in resultats_search:
461
+ try:
462
+ parsed = urlparse(url)
463
+ scheme = parsed.scheme or "https"
464
+ if not parsed.netloc:
465
+ continue
466
+ domaine = f"{scheme}://{parsed.netloc}"
467
+ domaines.add(domaine)
468
+ except Exception:
469
+ pass
470
+ return list(domaines)
471
+
472
+ def trouver_flux_rss(self, url_site):
473
+ try:
474
+ r = requests.get(url_site, headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
475
+ r.raise_for_status()
476
+ except requests.RequestException as e:
477
+ print(f"[ERREUR] Impossible d'accéder à {url_site} : {e}")
478
+ return []
479
+
480
+ soup = BeautifulSoup(r.text, "html.parser")
481
+ flux = []
482
+
483
+ for link in soup.find_all("link", type="application/rss+xml"):
484
+ href = link.get("href")
485
+ if href:
486
+ href = urljoin(url_site, href)
487
+ flux.append(href)
488
+
489
+ for link in soup.find_all("a"):
490
+ href = link.get("href", "")
491
+ if not href:
492
+ continue
493
+ href_norm = href.lower()
494
+ if "rss" in href_norm or "feed" in href_norm:
495
+ flux.append(urljoin(url_site, href))
496
+
497
+ return list(set(flux))
498
+
499
+ def rechercher_articles_dans_flux(self, requete, flux_list, max_results=20):
500
+ mots_cles = self.extraire_mots_cles(requete)
501
+ articles = []
502
+
503
+ headers = {"User-Agent": "Mozilla/5.0"}
504
+
505
+ for flux in flux_list:
506
+ try:
507
+ r = requests.get(flux, headers=headers, timeout=10)
508
+ r.raise_for_status()
509
+ # Parse du flux en XML
510
+ soup = BeautifulSoup(r.content, "xml")
511
+ except requests.RequestException as e:
512
+ print(f"[ERREUR] Problème lors de la lecture du flux {flux} : {e}")
513
+ continue
514
+
515
+ # Gestion RSS (<item>) et Atom (<entry>)
516
+ for entry in soup.find_all(["item", "entry"]):
517
+ # Titre
518
+ titre_tag = entry.find("title")
519
+ titre = titre_tag.get_text(strip=True) if titre_tag else ""
520
+
521
+ # Résumé / description
522
+ resume_tag = entry.find("description") or entry.find("summary")
523
+ resume = resume_tag.get_text(strip=True) if resume_tag else ""
524
+
525
+ # Lien
526
+ lien_tag = entry.find("link")
527
+ lien = ""
528
+ if lien_tag:
529
+ # Atom : <link href="...">
530
+ if lien_tag.has_attr("href"):
531
+ lien = lien_tag["href"]
532
+ else:
533
+ # RSS : <link>https://...</link>
534
+ lien = lien_tag.get_text(strip=True)
535
+
536
+ # Date
537
+ date_tag = (
538
+ entry.find("pubDate")
539
+ or entry.find("published")
540
+ or entry.find("updated")
541
+ )
542
+ date = date_tag.get_text(strip=True) if date_tag else "Date inconnue"
543
+
544
+ texte_complet = self.normaliser_texte(titre + " " + resume)
545
+
546
+ # Condition : au moins un mot-clé présent dans titre+résumé
547
+ if any(mot in texte_complet for mot in mots_cles):
548
+ articles.append(
549
+ {
550
+ "titre": titre,
551
+ "url": lien,
552
+ "date": date,
553
+ "source_flux": flux,
554
+ }
555
+ )
556
+ if len(articles) >= max_results:
557
+ return articles
558
+
559
+ return articles
560
+
561
+ def pipeline_veille_requete(self, requete):
562
+ resultats = self.recherche_duckduckgo(requete)
563
+ if not resultats:
564
+ print("Aucun résultat trouvé sur DuckDuckGo.")
565
+ return []
566
+ print(resultats)
567
+ domaines = self.extraire_domaines(resultats)
568
+
569
+ flux = []
570
+ for d in domaines:
571
+ found = self.trouver_flux_rss(d)
572
+ if found:
573
+ print(f"Flux trouvés sur {d}:")
574
+ for f in found:
575
+ print(" ->", f)
576
+ flux.extend(found)
577
+
578
+ flux = list(set(flux))
579
+
580
+ if not flux:
581
+ return [
582
+ {"titre": t, "url": u, "date": None, "source_flux": None, "source": "web"}
583
+ for t, u in resultats
584
+ ]
585
+
586
+ articles = self.rechercher_articles_dans_flux(requete, flux)
587
+
588
+ if not articles:
589
+ return [
590
+ {"titre": t, "url": u, "date": None, "source_flux": None, "source": "web"}
591
+ for t, u in resultats
592
+ ]
593
+ return articles
594
+
595
+ def run(self):
596
+ self.error("")
597
+ self.warning("")
598
+ if self.data is None:
599
+ self.Outputs.data.send(None)
600
+ return
601
+
602
+ if not self.selected_column_name in self.data.domain:
603
+ self.warning(f'Previously selected column "{self.selected_column_name}" does not exist in your data.')
604
+ return
605
+
606
+ self.query = self.data.get_column(self.selected_column_name)[0]
607
+
608
+ self.progressBarInit()
609
+ self.thread = thread_management.Thread(self.pipeline_veille_requete, self.query)
610
+ self.thread.progress.connect(self.handle_progress)
611
+ self.thread.result.connect(self.handle_result)
612
+ self.thread.finish.connect(self.handle_finish)
613
+ self.thread.start()
614
+
615
+ def handle_progress(self, progress) -> None:
616
+ value = progress[0]
617
+ text = progress[1]
618
+ if value is not None:
619
+ self.progressBarSet(value)
620
+ if text is None:
621
+ self.textBrowser.setText("")
622
+ else:
623
+ self.textBrowser.insertPlainText(text)
624
+
625
+ def handle_result(self, result):
626
+ if result is None or len(result) == 0:
627
+ self.Outputs.data.send(None)
628
+ return
629
+ data = convert.convert_json_implicite_to_data_table(result)
630
+ self.Outputs.data.send(data)
631
+
632
+ def handle_finish(self):
633
+ self.progressBarFinished()
634
+
635
+ def post_initialized(self):
636
+ pass
637
+
638
+
639
+ if __name__ == "__main__":
640
+ app = QApplication(sys.argv)
641
+ my_widget = WebSearch()
642
+ my_widget.show()
643
+
644
+ if hasattr(app, "exec"):
645
+ sys.exit(app.exec())
646
+ else:
647
+ sys.exit(app.exec_())