crystalwindow 4.6__py3-none-any.whl → 4.8__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.
@@ -1,5 +1,6 @@
1
1
  """
2
- CrystalWindow WebSearch module Google (Serper.dev) Edition
2
+ WebBrowse Universal Web Search Module
3
+ A modern, clean search helper for any Python application.
3
4
  """
4
5
 
5
6
  from __future__ import annotations
@@ -8,94 +9,102 @@ import webbrowser
8
9
  from dataclasses import dataclass
9
10
  from typing import List, Tuple, Optional, Any
10
11
 
11
- # Optional Qt integration
12
- try:
13
- from PySide6.QtWidgets import (
14
- QWidget, QVBoxLayout, QLineEdit, QPushButton, QTextEdit,
15
- QListWidget, QListWidgetItem, QLabel, QHBoxLayout
16
- )
17
- from PySide6.QtCore import Qt, QUrl
18
- from PySide6.QtGui import QDesktopServices
19
- PYSIDE6_AVAILABLE = True
20
- except Exception:
21
- PYSIDE6_AVAILABLE = False
22
12
 
23
13
 
24
- # --------------------------
25
- # Data result structure
26
- # --------------------------
14
+ # ================================
15
+ # Dataclass: Unified Search Result
16
+ # ================================
27
17
 
28
18
  @dataclass
29
- class WebSearchResult:
30
- text: str
31
- links: List[Tuple[str, str]] # (title, url)
32
- raw: Any = None
33
-
34
-
35
- # --------------------------
36
- # Main WebSearch class
37
- # --------------------------
38
-
39
- class WebSearch:
40
- """ Real Web Search helper for CrystalWindow using Google (Serper.dev). """
41
-
42
- # --- DuckDuckGo Instant Answer (kept for fallback) ---
43
- DUCKDUCKGO_INSTANT = "https://api.duckduckgo.com/"
44
-
45
- # --- Google Search (Serper.dev) ---
19
+ class SearchResult:
20
+ summary: str
21
+ links: List[Tuple[str, str]] # (title, url)
22
+ raw: Any = None # raw API payload
23
+
24
+
25
+ # ================================
26
+ # Main WebBrowse Search Handler
27
+ # ================================
28
+
29
+ class WebBrowse:
30
+ """
31
+ Lightweight universal web search interface.
32
+ Supports:
33
+ - Google Search via Serper.dev (primary)
34
+ - DuckDuckGo Instant Answer (fallback)
35
+
36
+ Designed to be extendable and safe.
37
+ """
38
+
39
+ # --------------------------
40
+ # API Endpoints
41
+ # --------------------------
42
+ DUCK_URL = "https://api.duckduckgo.com/"
46
43
  SERPER_URL = "https://google.serper.dev/search"
47
- SERPER_API_KEY = "a8e02281a0c3b58bb29a3731b6a2aec1d8d8a487" # <<<<<<<< REPLACE THIS
48
44
 
49
- # --------------------------------------------------
50
- # DuckDuckGo Instant Answer (old API)
51
- # --------------------------------------------------
45
+ # --------------------------
46
+ # Set your Serper.dev key
47
+ # --------------------------
48
+ SERPER_API_KEY: str = "" # <-- user must set manually
49
+
50
+ # =====================================================
51
+ # DuckDuckGo Instant Answer (fallback, small summaries)
52
+ # =====================================================
52
53
  @classmethod
53
- def _duckduckgo_instant(cls, query: str) -> WebSearchResult:
54
+ def _search_duckduckgo(cls, query: str) -> SearchResult:
54
55
  params = {
55
56
  "q": query,
56
57
  "format": "json",
57
58
  "no_html": 1,
58
- "no_redirect": 1,
59
+ "no_redirect": 1
59
60
  }
61
+
60
62
  try:
61
- r = requests.get(cls.DUCKDUCKGO_INSTANT, params=params, timeout=8)
63
+ r = requests.get(cls.DUCK_URL, params=params, timeout=8)
62
64
  r.raise_for_status()
63
65
  except Exception as e:
64
- return WebSearchResult(text=f"DuckDuckGo error: {e}", links=[])
66
+ return SearchResult(f"DuckDuckGo error: {e}", [], None)
65
67
 
66
68
  data = r.json()
67
- abstract = data.get("AbstractText") or data.get("Abstract") or ""
69
+ summary = data.get("AbstractText") or data.get("Abstract") or ""
68
70
  links: List[Tuple[str, str]] = []
69
71
 
72
+ # Abstract main URL
70
73
  if data.get("AbstractURL"):
71
74
  links.append((data.get("Heading", "Result"), data["AbstractURL"]))
72
75
 
73
- def extract_related(rt):
74
- if isinstance(rt, dict):
75
- if rt.get("FirstURL") and rt.get("Text"):
76
- links.append((rt["Text"], rt["FirstURL"]))
77
- for key in ("Topics", "RelatedTopics"):
78
- if isinstance(rt.get(key), list):
79
- for sub in rt[key]:
80
- extract_related(sub)
81
- elif isinstance(rt, list):
82
- for item in rt:
83
- extract_related(item)
84
-
85
- extract_related(data.get("RelatedTopics", []))
86
- if not abstract and links:
87
- abstract = "\n".join([t for t, _ in links[:3]])
88
-
89
- return WebSearchResult(text=abstract.strip(), links=links, raw=data)
90
-
91
- # --------------------------------------------------
92
- # Google Search via Serper.dev (NEW)
93
- # --------------------------------------------------
76
+ # Recursive topic extractor
77
+ def extract_topics(node):
78
+ if isinstance(node, dict):
79
+ if node.get("FirstURL") and node.get("Text"):
80
+ links.append((node["Text"], node["FirstURL"]))
81
+ for k in ("Topics", "RelatedTopics"):
82
+ if isinstance(node.get(k), list):
83
+ for sub in node[k]:
84
+ extract_topics(sub)
85
+ elif isinstance(node, list):
86
+ for sub in node:
87
+ extract_topics(sub)
88
+
89
+ extract_topics(data.get("RelatedTopics", []))
90
+
91
+ # If summary is empty, generate from first few topics
92
+ if not summary and links:
93
+ summary = "\n".join(t for t, _ in links[:3])
94
+
95
+ return SearchResult(summary.strip(), links, data)
96
+
97
+ # =====================================================
98
+ # Google Search via Serper.dev
99
+ # =====================================================
94
100
  @classmethod
95
- def _google_serper(cls, query: str) -> WebSearchResult:
101
+ def _search_google(cls, query: str) -> SearchResult:
102
+ if not cls.SERPER_API_KEY:
103
+ return SearchResult("Google engine requires SERPER_API_KEY.", [], None)
104
+
96
105
  headers = {
97
106
  "X-API-KEY": cls.SERPER_API_KEY,
98
- "Content-Type": "application/json"
107
+ "Content-Type": "application/json",
99
108
  }
100
109
  body = {"q": query}
101
110
 
@@ -103,12 +112,11 @@ class WebSearch:
103
112
  r = requests.post(cls.SERPER_URL, headers=headers, json=body, timeout=8)
104
113
  r.raise_for_status()
105
114
  except Exception as e:
106
- return WebSearchResult(text=f"Google search error: {e}", links=[])
115
+ return SearchResult(f"Google search error: {e}", [], None)
107
116
 
108
117
  data = r.json()
109
-
110
118
  links = []
111
- summary_parts = []
119
+ summaries = []
112
120
 
113
121
  for item in data.get("organic", []):
114
122
  title = item.get("title", "Untitled")
@@ -117,117 +125,36 @@ class WebSearch:
117
125
 
118
126
  links.append((title, url))
119
127
  if snippet:
120
- summary_parts.append(snippet)
128
+ summaries.append(snippet)
121
129
 
122
- summary = "\n".join(summary_parts[:3]).strip()
123
- if not summary:
124
- summary = "(no summary)"
130
+ summary = "\n".join(summaries[:3]).strip() or "(no summary)"
125
131
 
126
- return WebSearchResult(summary, links, data)
132
+ return SearchResult(summary, links, data)
127
133
 
128
- # --------------------------------------------------
129
- # Main public search()
130
- # --------------------------------------------------
134
+ # =====================================================
135
+ # Public search() method
136
+ # =====================================================
131
137
  @classmethod
132
- def search(cls, query: str, engine: str = "google") -> WebSearchResult:
133
- if not query or not query.strip():
134
- return WebSearchResult(text="", links=[])
138
+ def search(cls, query: str, engine: str = "google") -> SearchResult:
139
+ if not query.strip():
140
+ return SearchResult("", [], None)
135
141
 
136
- q = query.strip().lower()
137
- engine = engine.lower()
142
+ engine = engine.lower().strip()
138
143
 
139
144
  if engine == "google":
140
- return cls._google_serper(q)
145
+ return cls._search_google(query)
141
146
 
142
147
  if engine == "duckduckgo":
143
- return cls._duckduckgo_instant(q)
148
+ return cls._search_duckduckgo(query)
144
149
 
145
- return WebSearchResult(text=f"Unknown engine '{engine}'", links=[])
150
+ return SearchResult(f"Unknown search engine '{engine}'", [], None)
146
151
 
147
- # --------------------------------------------------
148
- # URL opener
149
- # --------------------------------------------------
152
+ # =====================================================
153
+ # URL Opener
154
+ # =====================================================
150
155
  @staticmethod
151
156
  def open_url(url: str) -> None:
152
157
  try:
153
158
  webbrowser.open(url)
154
159
  except Exception:
155
160
  pass
156
-
157
- # --------------------------------------------------
158
- # Optional PySide6 GUI
159
- # --------------------------------------------------
160
- if PYSIDE6_AVAILABLE:
161
- class _CWWebTab(QWidget):
162
- def __init__(self, parent=None):
163
- super().__init__(parent)
164
- self.setWindowTitle("WebSearch")
165
-
166
- layout = QVBoxLayout(self)
167
-
168
- # Search bar
169
- bar = QHBoxLayout()
170
- self.qbox = QLineEdit()
171
- self.qbox.setPlaceholderText("Search the web...")
172
- btn = QPushButton("Search")
173
- btn.clicked.connect(self._do_search)
174
- bar.addWidget(self.qbox)
175
- bar.addWidget(btn)
176
- layout.addLayout(bar)
177
-
178
- # Summary text
179
- self.summary = QTextEdit()
180
- self.summary.setReadOnly(True)
181
- self.summary.setFixedHeight(150)
182
- layout.addWidget(self.summary)
183
-
184
- # Links list
185
- self.links = QListWidget()
186
- self.links.itemActivated.connect(self._open_item)
187
- layout.addWidget(self.links)
188
-
189
- self.note = QLabel("Powered by Google (Serper.dev) API")
190
- self.note.setAlignment(Qt.AlignCenter)
191
- layout.addWidget(self.note)
192
-
193
- def _populate(self, result: WebSearchResult):
194
- self.summary.setPlainText(result.text)
195
- self.links.clear()
196
- for t, u in result.links:
197
- item = QListWidgetItem(f"{t}\n{u}")
198
- item.setData(Qt.UserRole, u)
199
- self.links.addItem(item)
200
-
201
- def _open_item(self, item: QListWidgetItem):
202
- url = item.data(Qt.UserRole)
203
- if url:
204
- QDesktopServices.openUrl(QUrl(url))
205
-
206
- def _do_search(self):
207
- q = self.qbox.text().strip()
208
- if not q:
209
- self.summary.setPlainText("Type something to search.")
210
- return
211
- self.summary.setPlainText("Searching...")
212
- res = WebSearch.search(q, engine="google")
213
- self._populate(res)
214
-
215
- @classmethod
216
- def register_cw_tab(cls, window):
217
- try:
218
- tab = cls._CWWebTab()
219
- if hasattr(window, "add_tab"):
220
- window.add_tab("WebSearch", tab)
221
- elif hasattr(window, "addTab"):
222
- window.addTab(tab, "WebSearch")
223
- else:
224
- if hasattr(window, "setCentralWidget"):
225
- window.setCentralWidget(tab)
226
- return tab
227
- except Exception:
228
- return None
229
-
230
- else:
231
- @classmethod
232
- def register_cw_tab(cls, window):
233
- return None