crystalwindow 4.5__py3-none-any.whl → 4.7__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,101 @@ 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
-
24
- # --------------------------
25
- # Data result structure
26
- # --------------------------
13
+ # ================================
14
+ # Dataclass: Unified Search Result
15
+ # ================================
27
16
 
28
17
  @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) ---
18
+ class SearchResult:
19
+ summary: str
20
+ links: List[Tuple[str, str]] # (title, url)
21
+ raw: Any = None # raw API payload
22
+
23
+
24
+ # ================================
25
+ # Main WebBrowse Search Handler
26
+ # ================================
27
+
28
+ class WebBrowse:
29
+ """
30
+ Lightweight universal web search interface.
31
+ Supports:
32
+ - Google Search via Serper.dev (primary)
33
+ - DuckDuckGo Instant Answer (fallback)
34
+
35
+ Designed to be extendable and safe.
36
+ """
37
+
38
+ # --------------------------
39
+ # API Endpoints
40
+ # --------------------------
41
+ DUCK_URL = "https://api.duckduckgo.com/"
46
42
  SERPER_URL = "https://google.serper.dev/search"
47
- SERPER_API_KEY = "a8e02281a0c3b58bb29a3731b6a2aec1d8d8a487" # <<<<<<<< REPLACE THIS
48
43
 
49
- # --------------------------------------------------
50
- # DuckDuckGo Instant Answer (old API)
51
- # --------------------------------------------------
44
+ # --------------------------
45
+ # Set your Serper.dev key
46
+ # --------------------------
47
+ SERPER_API_KEY: str = "" # <-- user must set manually
48
+
49
+ # =====================================================
50
+ # DuckDuckGo Instant Answer (fallback, small summaries)
51
+ # =====================================================
52
52
  @classmethod
53
- def _duckduckgo_instant(cls, query: str) -> WebSearchResult:
53
+ def _search_duckduckgo(cls, query: str) -> SearchResult:
54
54
  params = {
55
55
  "q": query,
56
56
  "format": "json",
57
57
  "no_html": 1,
58
- "no_redirect": 1,
58
+ "no_redirect": 1
59
59
  }
60
+
60
61
  try:
61
- r = requests.get(cls.DUCKDUCKGO_INSTANT, params=params, timeout=8)
62
+ r = requests.get(cls.DUCK_URL, params=params, timeout=8)
62
63
  r.raise_for_status()
63
64
  except Exception as e:
64
- return WebSearchResult(text=f"DuckDuckGo error: {e}", links=[])
65
+ return SearchResult(f"DuckDuckGo error: {e}", [], None)
65
66
 
66
67
  data = r.json()
67
- abstract = data.get("AbstractText") or data.get("Abstract") or ""
68
+ summary = data.get("AbstractText") or data.get("Abstract") or ""
68
69
  links: List[Tuple[str, str]] = []
69
70
 
71
+ # Abstract main URL
70
72
  if data.get("AbstractURL"):
71
73
  links.append((data.get("Heading", "Result"), data["AbstractURL"]))
72
74
 
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
- # --------------------------------------------------
75
+ # Recursive topic extractor
76
+ def extract_topics(node):
77
+ if isinstance(node, dict):
78
+ if node.get("FirstURL") and node.get("Text"):
79
+ links.append((node["Text"], node["FirstURL"]))
80
+ for k in ("Topics", "RelatedTopics"):
81
+ if isinstance(node.get(k), list):
82
+ for sub in node[k]:
83
+ extract_topics(sub)
84
+ elif isinstance(node, list):
85
+ for sub in node:
86
+ extract_topics(sub)
87
+
88
+ extract_topics(data.get("RelatedTopics", []))
89
+
90
+ # If summary is empty, generate from first few topics
91
+ if not summary and links:
92
+ summary = "\n".join(t for t, _ in links[:3])
93
+
94
+ return SearchResult(summary.strip(), links, data)
95
+
96
+ # =====================================================
97
+ # Google Search via Serper.dev
98
+ # =====================================================
94
99
  @classmethod
95
- def _google_serper(cls, query: str) -> WebSearchResult:
100
+ def _search_google(cls, query: str) -> SearchResult:
101
+ if not cls.SERPER_API_KEY:
102
+ return SearchResult("Google engine requires SERPER_API_KEY.", [], None)
103
+
96
104
  headers = {
97
105
  "X-API-KEY": cls.SERPER_API_KEY,
98
- "Content-Type": "application/json"
106
+ "Content-Type": "application/json",
99
107
  }
100
108
  body = {"q": query}
101
109
 
@@ -103,12 +111,11 @@ class WebSearch:
103
111
  r = requests.post(cls.SERPER_URL, headers=headers, json=body, timeout=8)
104
112
  r.raise_for_status()
105
113
  except Exception as e:
106
- return WebSearchResult(text=f"Google search error: {e}", links=[])
114
+ return SearchResult(f"Google search error: {e}", [], None)
107
115
 
108
116
  data = r.json()
109
-
110
117
  links = []
111
- summary_parts = []
118
+ summaries = []
112
119
 
113
120
  for item in data.get("organic", []):
114
121
  title = item.get("title", "Untitled")
@@ -117,117 +124,36 @@ class WebSearch:
117
124
 
118
125
  links.append((title, url))
119
126
  if snippet:
120
- summary_parts.append(snippet)
127
+ summaries.append(snippet)
121
128
 
122
- summary = "\n".join(summary_parts[:3]).strip()
123
- if not summary:
124
- summary = "(no summary)"
129
+ summary = "\n".join(summaries[:3]).strip() or "(no summary)"
125
130
 
126
- return WebSearchResult(summary, links, data)
131
+ return SearchResult(summary, links, data)
127
132
 
128
- # --------------------------------------------------
129
- # Main public search()
130
- # --------------------------------------------------
133
+ # =====================================================
134
+ # Public search() method
135
+ # =====================================================
131
136
  @classmethod
132
- def search(cls, query: str, engine: str = "google") -> WebSearchResult:
133
- if not query or not query.strip():
134
- return WebSearchResult(text="", links=[])
137
+ def search(cls, query: str, engine: str = "google") -> SearchResult:
138
+ if not query.strip():
139
+ return SearchResult("", [], None)
135
140
 
136
- q = query.strip().lower()
137
- engine = engine.lower()
141
+ engine = engine.lower().strip()
138
142
 
139
143
  if engine == "google":
140
- return cls._google_serper(q)
144
+ return cls._search_google(query)
141
145
 
142
146
  if engine == "duckduckgo":
143
- return cls._duckduckgo_instant(q)
147
+ return cls._search_duckduckgo(query)
144
148
 
145
- return WebSearchResult(text=f"Unknown engine '{engine}'", links=[])
149
+ return SearchResult(f"Unknown search engine '{engine}'", [], None)
146
150
 
147
- # --------------------------------------------------
148
- # URL opener
149
- # --------------------------------------------------
151
+ # =====================================================
152
+ # URL Opener
153
+ # =====================================================
150
154
  @staticmethod
151
155
  def open_url(url: str) -> None:
152
156
  try:
153
157
  webbrowser.open(url)
154
158
  except Exception:
155
159
  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