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.
- crystalwindow/FileHelper.py +159 -38
- crystalwindow/Icons/file_icons.png +0 -0
- crystalwindow/__init__.py +16 -27
- crystalwindow/assets.py +250 -59
- crystalwindow/clock.py +21 -63
- 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 +93 -3
- crystalwindow/websearch.py +91 -164
- crystalwindow/window.py +430 -12
- {crystalwindow-4.6.dist-info → crystalwindow-4.8.dist-info}/METADATA +6 -2
- crystalwindow-4.8.dist-info/RECORD +43 -0
- crystalwindow/cw_client.py +0 -95
- crystalwindow/player.py +0 -106
- crystalwindow-4.6.dist-info/RECORD +0 -42
- {crystalwindow-4.6.dist-info → crystalwindow-4.8.dist-info}/WHEEL +0 -0
- {crystalwindow-4.6.dist-info → crystalwindow-4.8.dist-info}/licenses/LICENSE +0 -0
- {crystalwindow-4.6.dist-info → crystalwindow-4.8.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,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
|
-
#
|
|
26
|
-
#
|
|
14
|
+
# ================================
|
|
15
|
+
# Dataclass: Unified Search Result
|
|
16
|
+
# ================================
|
|
27
17
|
|
|
28
18
|
@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
|
-
|
|
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
|
-
#
|
|
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
|
|
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.
|
|
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
|
|
66
|
+
return SearchResult(f"DuckDuckGo error: {e}", [], None)
|
|
65
67
|
|
|
66
68
|
data = r.json()
|
|
67
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
115
|
+
return SearchResult(f"Google search error: {e}", [], None)
|
|
107
116
|
|
|
108
117
|
data = r.json()
|
|
109
|
-
|
|
110
118
|
links = []
|
|
111
|
-
|
|
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
|
-
|
|
128
|
+
summaries.append(snippet)
|
|
121
129
|
|
|
122
|
-
summary = "\n".join(
|
|
123
|
-
if not summary:
|
|
124
|
-
summary = "(no summary)"
|
|
130
|
+
summary = "\n".join(summaries[:3]).strip() or "(no summary)"
|
|
125
131
|
|
|
126
|
-
return
|
|
132
|
+
return SearchResult(summary, links, data)
|
|
127
133
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
134
|
+
# =====================================================
|
|
135
|
+
# Public search() method
|
|
136
|
+
# =====================================================
|
|
131
137
|
@classmethod
|
|
132
|
-
def search(cls, query: str, engine: str = "google") ->
|
|
133
|
-
if not query
|
|
134
|
-
return
|
|
138
|
+
def search(cls, query: str, engine: str = "google") -> SearchResult:
|
|
139
|
+
if not query.strip():
|
|
140
|
+
return SearchResult("", [], None)
|
|
135
141
|
|
|
136
|
-
|
|
137
|
-
engine = engine.lower()
|
|
142
|
+
engine = engine.lower().strip()
|
|
138
143
|
|
|
139
144
|
if engine == "google":
|
|
140
|
-
return cls.
|
|
145
|
+
return cls._search_google(query)
|
|
141
146
|
|
|
142
147
|
if engine == "duckduckgo":
|
|
143
|
-
return cls.
|
|
148
|
+
return cls._search_duckduckgo(query)
|
|
144
149
|
|
|
145
|
-
return
|
|
150
|
+
return SearchResult(f"Unknown search engine '{engine}'", [], None)
|
|
146
151
|
|
|
147
|
-
#
|
|
148
|
-
# URL
|
|
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
|