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.
- crystalwindow/FileHelper.py +159 -38
- crystalwindow/Icons/file_icons.png +0 -0
- crystalwindow/__init__.py +19 -24
- crystalwindow/assets.py +250 -59
- crystalwindow/clock.py +137 -13
- crystalwindow/color_handler.py +3 -21
- crystalwindow/crystal3d.py +135 -56
- crystalwindow/fun_helpers.py +4 -0
- crystalwindow/gametests/3dsquare.py +2 -2
- crystalwindow/gametests/gravitytest.py +96 -23
- crystalwindow/gametests/guitesting.py +4 -2
- crystalwindow/gametests/squaremove.py +4 -1
- crystalwindow/gametests/testtttagain.py +17 -0
- crystalwindow/gui.py +87 -17
- crystalwindow/gui_ext.py +51 -39
- crystalwindow/objects.py +171 -0
- crystalwindow/sprites.py +174 -25
- crystalwindow/tilemap.py +1 -1
- crystalwindow/websearch.py +91 -165
- crystalwindow/window.py +430 -12
- {crystalwindow-4.5.dist-info → crystalwindow-4.7.dist-info}/METADATA +6 -2
- crystalwindow-4.7.dist-info/RECORD +43 -0
- crystalwindow/cw_client.py +0 -95
- crystalwindow/player.py +0 -106
- crystalwindow-4.5.dist-info/RECORD +0 -42
- {crystalwindow-4.5.dist-info → crystalwindow-4.7.dist-info}/WHEEL +0 -0
- {crystalwindow-4.5.dist-info → crystalwindow-4.7.dist-info}/licenses/LICENSE +0 -0
- {crystalwindow-4.5.dist-info → crystalwindow-4.7.dist-info}/top_level.txt +0 -0
crystalwindow/websearch.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
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
|
-
#
|
|
26
|
-
# --------------------------
|
|
13
|
+
# ================================
|
|
14
|
+
# Dataclass: Unified Search Result
|
|
15
|
+
# ================================
|
|
27
16
|
|
|
28
17
|
@dataclass
|
|
29
|
-
class
|
|
30
|
-
|
|
31
|
-
links: List[Tuple[str, str]]
|
|
32
|
-
raw: Any = None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
# Main
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
class
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
#
|
|
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
|
|
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.
|
|
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
|
|
65
|
+
return SearchResult(f"DuckDuckGo error: {e}", [], None)
|
|
65
66
|
|
|
66
67
|
data = r.json()
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
114
|
+
return SearchResult(f"Google search error: {e}", [], None)
|
|
107
115
|
|
|
108
116
|
data = r.json()
|
|
109
|
-
|
|
110
117
|
links = []
|
|
111
|
-
|
|
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
|
-
|
|
127
|
+
summaries.append(snippet)
|
|
121
128
|
|
|
122
|
-
summary = "\n".join(
|
|
123
|
-
if not summary:
|
|
124
|
-
summary = "(no summary)"
|
|
129
|
+
summary = "\n".join(summaries[:3]).strip() or "(no summary)"
|
|
125
130
|
|
|
126
|
-
return
|
|
131
|
+
return SearchResult(summary, links, data)
|
|
127
132
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
133
|
+
# =====================================================
|
|
134
|
+
# Public search() method
|
|
135
|
+
# =====================================================
|
|
131
136
|
@classmethod
|
|
132
|
-
def search(cls, query: str, engine: str = "google") ->
|
|
133
|
-
if not query
|
|
134
|
-
return
|
|
137
|
+
def search(cls, query: str, engine: str = "google") -> SearchResult:
|
|
138
|
+
if not query.strip():
|
|
139
|
+
return SearchResult("", [], None)
|
|
135
140
|
|
|
136
|
-
|
|
137
|
-
engine = engine.lower()
|
|
141
|
+
engine = engine.lower().strip()
|
|
138
142
|
|
|
139
143
|
if engine == "google":
|
|
140
|
-
return cls.
|
|
144
|
+
return cls._search_google(query)
|
|
141
145
|
|
|
142
146
|
if engine == "duckduckgo":
|
|
143
|
-
return cls.
|
|
147
|
+
return cls._search_duckduckgo(query)
|
|
144
148
|
|
|
145
|
-
return
|
|
149
|
+
return SearchResult(f"Unknown search engine '{engine}'", [], None)
|
|
146
150
|
|
|
147
|
-
#
|
|
148
|
-
# URL
|
|
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
|