pyloid 0.24.8__tar.gz → 0.25.0__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.
- {pyloid-0.24.8 → pyloid-0.25.0}/LICENSE +1 -1
- {pyloid-0.24.8 → pyloid-0.25.0}/PKG-INFO +4 -3
- {pyloid-0.24.8 → pyloid-0.25.0}/README.md +1 -1
- {pyloid-0.24.8 → pyloid-0.25.0}/pyproject.toml +3 -2
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/browser_window.py +33 -62
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/pyloid.py +20 -28
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/rpc.py +29 -1
- pyloid-0.25.0/src/pyloid/serve.py +232 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/store.py +7 -2
- pyloid-0.24.8/src/pyloid/serve.py +0 -56
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/__init__.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/api.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/autostart.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/custom/titlebar.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/filewatcher.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/js_api/base.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/js_api/event_api.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/js_api/window_api.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/monitor.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/thread_pool.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/timer.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/tray.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/url_interceptor.py +0 -0
- {pyloid-0.24.8 → pyloid-0.25.0}/src/pyloid/utils.py +0 -0
|
@@ -198,4 +198,4 @@ Apache License
|
|
|
198
198
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
199
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
200
|
See the License for the specific language governing permissions and
|
|
201
|
-
limitations under the License.
|
|
201
|
+
limitations under the License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyloid
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: aesthetics-of-record
|
|
6
6
|
Author-email: 111675679+aesthetics-of-record@users.noreply.github.com
|
|
@@ -11,10 +11,11 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
|
14
15
|
Requires-Dist: aiohttp-cors (>=0.8.1,<0.9.0)
|
|
15
16
|
Requires-Dist: pickledb (>=1.3.2,<2.0.0)
|
|
16
17
|
Requires-Dist: platformdirs (>=4.3.7,<5.0.0)
|
|
17
|
-
Requires-Dist: pyside6 (==6.9.
|
|
18
|
+
Requires-Dist: pyside6 (==6.9.2)
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
20
21
|
<h1 style="text-align: center; font-size: 200px; font-weight: 500;">
|
|
@@ -23,7 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
23
24
|
|
|
24
25
|

|
|
25
26
|
|
|
26
|
-
<h2 align="center" style="font-size: 28px;"><b>Pyloid:
|
|
27
|
+
<h2 align="center" style="font-size: 28px;"><b>Pyloid: Electron for Python Developer • Web-based desktop app framework</b></h2>
|
|
27
28
|
|
|
28
29
|
## 💡 Key Features
|
|
29
30
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
<h2 align="center" style="font-size: 28px;"><b>Pyloid:
|
|
7
|
+
<h2 align="center" style="font-size: 28px;"><b>Pyloid: Electron for Python Developer • Web-based desktop app framework</b></h2>
|
|
8
8
|
|
|
9
9
|
## 💡 Key Features
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pyloid"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.0"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["aesthetics-of-record <111675679+aesthetics-of-record@users.noreply.github.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -10,10 +10,11 @@ packages = [
|
|
|
10
10
|
|
|
11
11
|
[tool.poetry.dependencies]
|
|
12
12
|
python = ">=3.9,<3.14"
|
|
13
|
-
pyside6 = "6.9.
|
|
13
|
+
pyside6 = "6.9.2"
|
|
14
14
|
platformdirs = "^4.3.7"
|
|
15
15
|
pickledb = "^1.3.2"
|
|
16
16
|
aiohttp-cors = "^0.8.1"
|
|
17
|
+
aiofiles = "^24.1.0"
|
|
17
18
|
|
|
18
19
|
[build-system]
|
|
19
20
|
requires = ["poetry-core"]
|
|
@@ -40,12 +40,11 @@ from PySide6.QtWidgets import QSplashScreen, QLabel
|
|
|
40
40
|
from typing import TYPE_CHECKING, Any
|
|
41
41
|
from PySide6.QtWebEngineCore import (
|
|
42
42
|
QWebEngineSettings,
|
|
43
|
-
QWebEngineDesktopMediaRequest,
|
|
43
|
+
# QWebEngineDesktopMediaRequest, # 6.8.3 부터
|
|
44
44
|
)
|
|
45
45
|
import threading
|
|
46
46
|
|
|
47
47
|
# from .url_interceptor import CustomUrlInterceptor
|
|
48
|
-
from .rpc import PyloidRPC
|
|
49
48
|
|
|
50
49
|
if TYPE_CHECKING:
|
|
51
50
|
from .pyloid import _Pyloid, Pyloid
|
|
@@ -55,7 +54,7 @@ class CustomWebPage(QWebEnginePage):
|
|
|
55
54
|
def __init__(self, profile=None):
|
|
56
55
|
super().__init__(profile)
|
|
57
56
|
self.featurePermissionRequested.connect(self._handlePermissionRequest)
|
|
58
|
-
self.desktopMediaRequested.connect(self._handleDesktopMediaRequest)
|
|
57
|
+
# self.desktopMediaRequested.connect(self._handleDesktopMediaRequest)
|
|
59
58
|
self._permission_handlers = {}
|
|
60
59
|
self._desktop_media_handler = None
|
|
61
60
|
self._url_handlers = {} # URL 핸들러 저장을 위한 딕셔너리 추가
|
|
@@ -78,31 +77,31 @@ class CustomWebPage(QWebEnginePage):
|
|
|
78
77
|
"""Register a handler for a specific permission"""
|
|
79
78
|
self._permission_handlers[feature] = handler
|
|
80
79
|
|
|
81
|
-
def _handleDesktopMediaRequest(self, request: QWebEngineDesktopMediaRequest):
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
# def _handleDesktopMediaRequest(self, request: QWebEngineDesktopMediaRequest):
|
|
81
|
+
# return
|
|
82
|
+
# print("Desktop media request received:", request)
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
# # check the available screen list
|
|
85
|
+
# screens_model = request.screensModel()
|
|
86
|
+
# print("\n=== Available Screens ===")
|
|
87
|
+
# for i in range(screens_model.rowCount()):
|
|
88
|
+
# screen_index = screens_model.index(i)
|
|
89
|
+
# screen_name = screens_model.data(screen_index)
|
|
90
|
+
# print(f"Screen {i}: {screen_name}")
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
# # check the available window list
|
|
93
|
+
# windows_model = request.windowsModel()
|
|
94
|
+
# print("\n=== Available Windows ===")
|
|
95
|
+
# for i in range(windows_model.rowCount()):
|
|
96
|
+
# window_index = windows_model.index(i)
|
|
97
|
+
# window_name = windows_model.data(window_index)
|
|
98
|
+
# print(f"Window {i}: {window_name}")
|
|
100
99
|
|
|
101
|
-
|
|
100
|
+
# request.selectWindow(windows_model.index(3))
|
|
102
101
|
|
|
103
102
|
# # interceptor ( navigation request )
|
|
104
103
|
# def acceptNavigationRequest(self, url, navigation_type, is_main_frame):
|
|
105
|
-
# """
|
|
104
|
+
# """method to handle navigation requests"""
|
|
106
105
|
# print(f"Navigation Request - URL: {url.toString()}")
|
|
107
106
|
# print(f"Navigation Type: {navigation_type}")
|
|
108
107
|
# print(f"Is Main Frame: {is_main_frame}")
|
|
@@ -228,10 +227,10 @@ class CustomWebEngineView(QWebEngineView):
|
|
|
228
227
|
|
|
229
228
|
def eventFilter(self, source, event):
|
|
230
229
|
if self.focusProxy() is source:
|
|
231
|
-
#
|
|
230
|
+
# when in the resize area, all click events are intercepted
|
|
232
231
|
if self.is_in_resize_area and event.type() == QEvent.MouseButtonPress:
|
|
233
232
|
self.mouse_press_event(event)
|
|
234
|
-
return True #
|
|
233
|
+
return True # consume the event so it is not passed to the web view
|
|
235
234
|
|
|
236
235
|
if event.type() == QEvent.MouseButtonPress:
|
|
237
236
|
self.mouse_press_event(event)
|
|
@@ -316,7 +315,6 @@ class _BrowserWindow:
|
|
|
316
315
|
context_menu: bool = False,
|
|
317
316
|
dev_tools: bool = False,
|
|
318
317
|
# js_apis: List[PyloidAPI] = [],
|
|
319
|
-
rpc: Optional[PyloidRPC] = None,
|
|
320
318
|
transparent: bool = False,
|
|
321
319
|
):
|
|
322
320
|
###########################################################################################
|
|
@@ -324,15 +322,9 @@ class _BrowserWindow:
|
|
|
324
322
|
self._window = QMainWindow()
|
|
325
323
|
self.web_view = CustomWebEngineView(self)
|
|
326
324
|
|
|
327
|
-
|
|
328
|
-
self.rpc = rpc
|
|
329
|
-
self.rpc_url = rpc.url
|
|
330
|
-
else:
|
|
331
|
-
self.rpc = None
|
|
332
|
-
self.rpc_url = None
|
|
325
|
+
|
|
333
326
|
|
|
334
327
|
# interceptor ( all url request )
|
|
335
|
-
# self.interceptor = CustomUrlInterceptor(rpc_url=self.rpc_url)
|
|
336
328
|
# self.web_view.page().setUrlRequestInterceptor(self.interceptor)
|
|
337
329
|
|
|
338
330
|
self._window.closeEvent = self.closeEvent # Override closeEvent method
|
|
@@ -349,7 +341,7 @@ class _BrowserWindow:
|
|
|
349
341
|
self.context_menu = context_menu
|
|
350
342
|
self.dev_tools = dev_tools
|
|
351
343
|
|
|
352
|
-
self.js_apis = [BaseAPI(self.id, self.app.data, self.app, self.
|
|
344
|
+
self.js_apis = [BaseAPI(self.id, self.app.data, self.app, self.app.server.url)]
|
|
353
345
|
|
|
354
346
|
# for js_api in js_apis:
|
|
355
347
|
# self.js_apis.append(js_api)
|
|
@@ -357,25 +349,6 @@ class _BrowserWindow:
|
|
|
357
349
|
self.shortcuts = {}
|
|
358
350
|
self.close_on_load = True
|
|
359
351
|
self.splash_screen = None
|
|
360
|
-
###########################################################################################
|
|
361
|
-
# RPC 서버가 없으면 추가하지 않음
|
|
362
|
-
if not self.rpc:
|
|
363
|
-
return
|
|
364
|
-
|
|
365
|
-
self.rpc.pyloid = self.app.pyloid_wrapper
|
|
366
|
-
# self.rpc.window = self.window_wrapper
|
|
367
|
-
|
|
368
|
-
# RPC 서버 중복 방지
|
|
369
|
-
if self.rpc in self.app.rpc_servers:
|
|
370
|
-
return
|
|
371
|
-
|
|
372
|
-
# RPC 서버 추가
|
|
373
|
-
self.app.rpc_servers.add(self.rpc)
|
|
374
|
-
|
|
375
|
-
# Start unique RPC servers
|
|
376
|
-
server_thread = threading.Thread(target=self.rpc.start, daemon=True)
|
|
377
|
-
server_thread.start()
|
|
378
|
-
###########################################################################################
|
|
379
352
|
|
|
380
353
|
def _set_custom_frame(
|
|
381
354
|
self,
|
|
@@ -405,7 +378,7 @@ class _BrowserWindow:
|
|
|
405
378
|
central_widget.setLayout(layout)
|
|
406
379
|
self._window.setCentralWidget(central_widget)
|
|
407
380
|
|
|
408
|
-
#
|
|
381
|
+
# add properties for window movement
|
|
409
382
|
self._window.moving = False
|
|
410
383
|
self._window.offset = QPoint()
|
|
411
384
|
else:
|
|
@@ -543,10 +516,10 @@ class _BrowserWindow:
|
|
|
543
516
|
# Set F12 shortcut
|
|
544
517
|
self.set_dev_tools(self.dev_tools)
|
|
545
518
|
|
|
546
|
-
#
|
|
519
|
+
# get the profile and set the interceptor
|
|
547
520
|
profile = self.web_view.page().profile()
|
|
548
521
|
|
|
549
|
-
# #
|
|
522
|
+
# # if the existing interceptor is present, remove it
|
|
550
523
|
# if self.interceptor:
|
|
551
524
|
# profile.setUrlRequestInterceptor(None)
|
|
552
525
|
|
|
@@ -565,15 +538,15 @@ class _BrowserWindow:
|
|
|
565
538
|
new QWebChannel(qt.webChannelTransport, function (channel) {
|
|
566
539
|
window.pyloid = {
|
|
567
540
|
EventAPI: {
|
|
568
|
-
_listeners: {}, //
|
|
541
|
+
_listeners: {}, // object to store the callback functions
|
|
569
542
|
|
|
570
543
|
listen: function(eventName, callback) {
|
|
571
|
-
//
|
|
544
|
+
// if the callback array for the event is not present, create it
|
|
572
545
|
if (!this._listeners[eventName]) {
|
|
573
546
|
this._listeners[eventName] = [];
|
|
574
547
|
}
|
|
575
548
|
|
|
576
|
-
//
|
|
549
|
+
// save the callback function
|
|
577
550
|
this._listeners[eventName].push(callback);
|
|
578
551
|
|
|
579
552
|
document.addEventListener(eventName, function(event) {
|
|
@@ -588,12 +561,12 @@ class _BrowserWindow:
|
|
|
588
561
|
},
|
|
589
562
|
|
|
590
563
|
unlisten: function(eventName) {
|
|
591
|
-
//
|
|
564
|
+
// remove all listeners for the event
|
|
592
565
|
if (this._listeners[eventName]) {
|
|
593
566
|
this._listeners[eventName].forEach(callback => {
|
|
594
567
|
document.removeEventListener(eventName, callback);
|
|
595
568
|
});
|
|
596
|
-
//
|
|
569
|
+
// remove the saved callback
|
|
597
570
|
delete this._listeners[eventName];
|
|
598
571
|
}
|
|
599
572
|
}
|
|
@@ -2125,7 +2098,6 @@ class BrowserWindow(QObject):
|
|
|
2125
2098
|
frame: bool = True,
|
|
2126
2099
|
context_menu: bool = False,
|
|
2127
2100
|
dev_tools: bool = False,
|
|
2128
|
-
rpc: Optional[PyloidRPC] = None,
|
|
2129
2101
|
transparent: bool = False,
|
|
2130
2102
|
):
|
|
2131
2103
|
super().__init__()
|
|
@@ -2140,7 +2112,6 @@ class BrowserWindow(QObject):
|
|
|
2140
2112
|
frame,
|
|
2141
2113
|
context_menu,
|
|
2142
2114
|
dev_tools,
|
|
2143
|
-
rpc,
|
|
2144
2115
|
transparent,
|
|
2145
2116
|
)
|
|
2146
2117
|
self.command_signal.connect(self._handle_command)
|
|
@@ -32,7 +32,6 @@ from PySide6.QtCore import QEventLoop
|
|
|
32
32
|
from typing import Any, Set
|
|
33
33
|
from platformdirs import PlatformDirs
|
|
34
34
|
from .store import Store
|
|
35
|
-
from .rpc import PyloidRPC
|
|
36
35
|
import threading
|
|
37
36
|
import signal
|
|
38
37
|
|
|
@@ -100,6 +99,7 @@ class _Pyloid(QApplication):
|
|
|
100
99
|
pyloid_wrapper: "Pyloid",
|
|
101
100
|
app_name,
|
|
102
101
|
single_instance=True,
|
|
102
|
+
server=None,
|
|
103
103
|
data=None,
|
|
104
104
|
):
|
|
105
105
|
"""
|
|
@@ -132,7 +132,6 @@ class _Pyloid(QApplication):
|
|
|
132
132
|
self.data = data
|
|
133
133
|
|
|
134
134
|
self.windows_dict: Dict[str, BrowserWindow] = {} # 윈도우 ID를 키로 사용하는 딕셔너리
|
|
135
|
-
self.server = None
|
|
136
135
|
|
|
137
136
|
self.app_name = app_name
|
|
138
137
|
self.icon = None
|
|
@@ -171,7 +170,14 @@ class _Pyloid(QApplication):
|
|
|
171
170
|
|
|
172
171
|
self.dirs = PlatformDirs(self.app_name, appauthor=False)
|
|
173
172
|
|
|
174
|
-
|
|
173
|
+
|
|
174
|
+
###################################################
|
|
175
|
+
# Pyloid Server Integration
|
|
176
|
+
if server:
|
|
177
|
+
self.server = server
|
|
178
|
+
else:
|
|
179
|
+
self.server = None
|
|
180
|
+
###################################################
|
|
175
181
|
|
|
176
182
|
# def set_theme(self, theme: Literal["system", "dark", "light"]):
|
|
177
183
|
# """
|
|
@@ -242,7 +248,6 @@ class _Pyloid(QApplication):
|
|
|
242
248
|
frame: bool = True,
|
|
243
249
|
context_menu: bool = False,
|
|
244
250
|
dev_tools: bool = False,
|
|
245
|
-
rpc: Optional[PyloidRPC] = None,
|
|
246
251
|
transparent: bool = False,
|
|
247
252
|
) -> BrowserWindow:
|
|
248
253
|
"""
|
|
@@ -266,8 +271,6 @@ class _Pyloid(QApplication):
|
|
|
266
271
|
Whether to use the context menu (default is False)
|
|
267
272
|
dev_tools : bool, optional
|
|
268
273
|
Whether to use developer tools (default is False)
|
|
269
|
-
rpc : PyloidRPC, optional
|
|
270
|
-
The RPC server instance to be used in the window
|
|
271
274
|
transparent : bool, optional
|
|
272
275
|
Whether the window is transparent (default is False)
|
|
273
276
|
|
|
@@ -292,7 +295,6 @@ class _Pyloid(QApplication):
|
|
|
292
295
|
frame,
|
|
293
296
|
context_menu,
|
|
294
297
|
dev_tools,
|
|
295
|
-
rpc,
|
|
296
298
|
transparent,
|
|
297
299
|
)
|
|
298
300
|
self.windows_dict[window._window.id] = window
|
|
@@ -315,17 +317,9 @@ class _Pyloid(QApplication):
|
|
|
315
317
|
```
|
|
316
318
|
"""
|
|
317
319
|
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
# if window._window.rpc is not None:
|
|
322
|
-
# rpc_servers.add(window._window.rpc)
|
|
323
|
-
|
|
324
|
-
# # Start unique RPC servers
|
|
325
|
-
# for rpc in rpc_servers:
|
|
326
|
-
# server_thread = threading.Thread(target=rpc.start, daemon=True)
|
|
327
|
-
# server_thread.start()
|
|
328
|
-
|
|
320
|
+
# Start Pyloid Integrated Server
|
|
321
|
+
if self.server:
|
|
322
|
+
self.server.run()
|
|
329
323
|
|
|
330
324
|
if is_production():
|
|
331
325
|
sys.exit(self.exec())
|
|
@@ -341,10 +335,10 @@ class _Pyloid(QApplication):
|
|
|
341
335
|
# Another instance is already running
|
|
342
336
|
sys.exit(1)
|
|
343
337
|
|
|
344
|
-
# Create a new server
|
|
345
|
-
self.
|
|
346
|
-
self.
|
|
347
|
-
self.
|
|
338
|
+
# Create a new Single Instance server
|
|
339
|
+
self.single_instance_server = QLocalServer()
|
|
340
|
+
self.single_instance_server.listen(self.app_name)
|
|
341
|
+
self.single_instance_server.newConnection.connect(self._handle_new_connection)
|
|
348
342
|
|
|
349
343
|
def _handle_new_connection(self):
|
|
350
344
|
"""Handles new connections for the single instance server."""
|
|
@@ -1616,6 +1610,7 @@ class Pyloid(QObject):
|
|
|
1616
1610
|
self,
|
|
1617
1611
|
app_name: str,
|
|
1618
1612
|
single_instance: bool = True,
|
|
1613
|
+
server = None,
|
|
1619
1614
|
# data: Optional[Dict[str, Any]] = None,
|
|
1620
1615
|
):
|
|
1621
1616
|
"""
|
|
@@ -1631,12 +1626,14 @@ class Pyloid(QObject):
|
|
|
1631
1626
|
The name of the application.
|
|
1632
1627
|
single_instance : bool, optional
|
|
1633
1628
|
Determines whether to run as a single instance. (Default is True)
|
|
1629
|
+
server : optional
|
|
1630
|
+
The pyloid server instance to be used in the application
|
|
1634
1631
|
"""
|
|
1635
1632
|
super().__init__()
|
|
1636
1633
|
|
|
1637
1634
|
self.data = None # 나중에 데이터 필요 시 수정
|
|
1638
1635
|
|
|
1639
|
-
self.app = _Pyloid(self, app_name, single_instance, self.data)
|
|
1636
|
+
self.app = _Pyloid(self, app_name, single_instance, server, self.data)
|
|
1640
1637
|
|
|
1641
1638
|
self.command_signal.connect(self._handle_command)
|
|
1642
1639
|
|
|
@@ -1657,7 +1654,6 @@ class Pyloid(QObject):
|
|
|
1657
1654
|
frame=params.get("frame", True),
|
|
1658
1655
|
context_menu=params.get("context_menu", False),
|
|
1659
1656
|
dev_tools=params.get("dev_tools", False),
|
|
1660
|
-
rpc=params.get("rpc", None),
|
|
1661
1657
|
transparent=params.get("transparent", False),
|
|
1662
1658
|
)
|
|
1663
1659
|
result = window
|
|
@@ -1833,7 +1829,6 @@ class Pyloid(QObject):
|
|
|
1833
1829
|
frame: bool = True,
|
|
1834
1830
|
context_menu: bool = False,
|
|
1835
1831
|
dev_tools: bool = False,
|
|
1836
|
-
rpc: Optional[PyloidRPC] = None,
|
|
1837
1832
|
transparent: bool = False,
|
|
1838
1833
|
) -> BrowserWindow:
|
|
1839
1834
|
"""
|
|
@@ -1857,8 +1852,6 @@ class Pyloid(QObject):
|
|
|
1857
1852
|
Whether to use the context menu (default is False)
|
|
1858
1853
|
dev_tools : bool, optional
|
|
1859
1854
|
Whether to use developer tools (default is False)
|
|
1860
|
-
rpc : PyloidRPC, optional
|
|
1861
|
-
The RPC server instance to be used in the window
|
|
1862
1855
|
transparent : bool, optional
|
|
1863
1856
|
Whether the window is transparent (default is False)
|
|
1864
1857
|
|
|
@@ -1881,7 +1874,6 @@ class Pyloid(QObject):
|
|
|
1881
1874
|
"frame": frame,
|
|
1882
1875
|
"context_menu": context_menu,
|
|
1883
1876
|
"dev_tools": dev_tools,
|
|
1884
|
-
"rpc": rpc,
|
|
1885
1877
|
"transparent": transparent,
|
|
1886
1878
|
}
|
|
1887
1879
|
return self.execute_command("create_window", params)
|
|
@@ -220,7 +220,7 @@ class PyloidRPC:
|
|
|
220
220
|
|
|
221
221
|
# Store the original function
|
|
222
222
|
self._functions[rpc_name] = func
|
|
223
|
-
log.info(f"RPC function registered: {rpc_name}")
|
|
223
|
+
# log.info(f"RPC function registered: {rpc_name}")
|
|
224
224
|
|
|
225
225
|
@wraps(func)
|
|
226
226
|
async def wrapper(*args, _pyloid_window_id=None, **kwargs):
|
|
@@ -518,3 +518,31 @@ class PyloidRPC:
|
|
|
518
518
|
except Exception as e:
|
|
519
519
|
log.exception(f"Failed to start or run the server: {e}")
|
|
520
520
|
raise
|
|
521
|
+
|
|
522
|
+
def run(self):
|
|
523
|
+
"""
|
|
524
|
+
Runs start_async in a separate thread.
|
|
525
|
+
|
|
526
|
+
This method is useful when you want to start the aiohttp server in the background
|
|
527
|
+
without blocking the main thread. It creates a new thread, sets up a new asyncio event loop
|
|
528
|
+
in that thread, and starts the asynchronous server. The thread is marked as daemon so that
|
|
529
|
+
it will not prevent the program from exiting if only daemon threads remain.
|
|
530
|
+
"""
|
|
531
|
+
import asyncio
|
|
532
|
+
|
|
533
|
+
def _run_asyncio():
|
|
534
|
+
# Create a new event loop for this thread.
|
|
535
|
+
loop = asyncio.new_event_loop()
|
|
536
|
+
# Set the newly created event loop as the current event loop for this thread.
|
|
537
|
+
asyncio.set_event_loop(loop)
|
|
538
|
+
# Start the asynchronous server; this coroutine will set up the server.
|
|
539
|
+
loop.run_until_complete(self.start_async())
|
|
540
|
+
# Keep the event loop running forever to handle incoming requests.
|
|
541
|
+
loop.run_forever()
|
|
542
|
+
|
|
543
|
+
# Create a new thread to run the event loop and server in the background.
|
|
544
|
+
# The thread is set as a daemon so it will not block program exit.
|
|
545
|
+
server_thread = threading.Thread(target=_run_asyncio, daemon=True)
|
|
546
|
+
# Start the background server thread.
|
|
547
|
+
server_thread.start()
|
|
548
|
+
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import asyncio
|
|
3
|
+
import mimetypes
|
|
4
|
+
import aiofiles
|
|
5
|
+
from aiohttp import web
|
|
6
|
+
from aiohttp.web_fileresponse import FileResponse
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from .utils import get_free_port, is_production
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
# 로깅 설정
|
|
13
|
+
logging.getLogger('aiohttp').setLevel(logging.WARNING)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ZeroCopyFileResponse(FileResponse):
|
|
17
|
+
"""zero-copy optimized file response class"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, path, chunk_size=65536, headers=None):
|
|
20
|
+
"""
|
|
21
|
+
Args:
|
|
22
|
+
path: file path
|
|
23
|
+
chunk_size: streaming chunk size (64KB default)
|
|
24
|
+
headers: additional HTTP headers
|
|
25
|
+
"""
|
|
26
|
+
super().__init__(path, chunk_size=chunk_size, headers=headers)
|
|
27
|
+
self._enable_sendfile = True # sendfile() system call activation
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ZeroCopyStaticHandler:
|
|
31
|
+
"""zero-copy optimized static file handler"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, directory: str):
|
|
34
|
+
self.directory = Path(directory).resolve()
|
|
35
|
+
self.chunk_size = 65536 # 64KB chunk
|
|
36
|
+
|
|
37
|
+
async def handle_request(self, request):
|
|
38
|
+
"""HTTP request processing"""
|
|
39
|
+
try:
|
|
40
|
+
# URL path parsing
|
|
41
|
+
path = request.path_qs.split('?')[0] # 쿼리 파라미터 제거
|
|
42
|
+
if path.endswith('/'):
|
|
43
|
+
path += 'index.html'
|
|
44
|
+
|
|
45
|
+
# Security: prevent directory traversal attacks
|
|
46
|
+
file_path = (self.directory / path.lstrip('/')).resolve()
|
|
47
|
+
if not str(file_path).startswith(str(self.directory)):
|
|
48
|
+
return web.Response(status=403, text="Forbidden")
|
|
49
|
+
|
|
50
|
+
# Check if the file exists
|
|
51
|
+
if not file_path.exists() or not file_path.is_file():
|
|
52
|
+
return web.Response(status=404, text="File not found")
|
|
53
|
+
|
|
54
|
+
# Check the file size and type
|
|
55
|
+
stat = file_path.stat()
|
|
56
|
+
file_size = stat.st_size
|
|
57
|
+
content_type, _ = mimetypes.guess_type(str(file_path))
|
|
58
|
+
|
|
59
|
+
# Range request processing (partial download supported)
|
|
60
|
+
range_header = request.headers.get('Range')
|
|
61
|
+
if range_header:
|
|
62
|
+
return await self._handle_range_request(file_path, range_header, file_size, content_type, request)
|
|
63
|
+
|
|
64
|
+
if is_production():
|
|
65
|
+
cache_control = 'public, max-age=3600' # 1 hour caching
|
|
66
|
+
else:
|
|
67
|
+
cache_control = 'no-cache'
|
|
68
|
+
|
|
69
|
+
# zero-copy file response creation
|
|
70
|
+
headers = {
|
|
71
|
+
'Content-Type': content_type or 'application/octet-stream',
|
|
72
|
+
'Content-Length': str(file_size),
|
|
73
|
+
'Accept-Ranges': 'bytes',
|
|
74
|
+
'Cache-Control': cache_control,
|
|
75
|
+
'ETag': f'"{stat.st_mtime}-{file_size}"'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# ETag based cache check
|
|
79
|
+
if_none_match = request.headers.get('If-None-Match')
|
|
80
|
+
if if_none_match == headers['ETag']:
|
|
81
|
+
return web.Response(status=304, headers=headers)
|
|
82
|
+
|
|
83
|
+
# zero-copy response return (sendfile used)
|
|
84
|
+
return ZeroCopyFileResponse(
|
|
85
|
+
path=file_path,
|
|
86
|
+
chunk_size=self.chunk_size,
|
|
87
|
+
headers=headers
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return web.Response(status=500, text=f"Internal Server Error: {str(e)}")
|
|
92
|
+
|
|
93
|
+
async def _handle_range_request(self, file_path, range_header, file_size, content_type, request):
|
|
94
|
+
"""Range request processing (partial download)"""
|
|
95
|
+
try:
|
|
96
|
+
# Range header parsing: bytes=start-end
|
|
97
|
+
range_match = range_header.replace('bytes=', '').split('-')
|
|
98
|
+
start = int(range_match[0]) if range_match[0] else 0
|
|
99
|
+
end = int(range_match[1]) if range_match[1] else file_size - 1
|
|
100
|
+
|
|
101
|
+
# Range validation
|
|
102
|
+
if start >= file_size or end >= file_size or start > end:
|
|
103
|
+
return web.Response(
|
|
104
|
+
status=416, # Range Not Satisfiable
|
|
105
|
+
headers={'Content-Range': f'bytes */{file_size}'}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
content_length = end - start + 1
|
|
109
|
+
|
|
110
|
+
headers = {
|
|
111
|
+
'Content-Type': content_type or 'application/octet-stream',
|
|
112
|
+
'Content-Length': str(content_length),
|
|
113
|
+
'Content-Range': f'bytes {start}-{end}/{file_size}',
|
|
114
|
+
'Accept-Ranges': 'bytes'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# zero-copy partial response
|
|
118
|
+
response = web.StreamResponse(status=206, headers=headers)
|
|
119
|
+
await response.prepare(request)
|
|
120
|
+
|
|
121
|
+
# aiofiles to async zero-copy transfer
|
|
122
|
+
async with aiofiles.open(file_path, 'rb') as f:
|
|
123
|
+
await f.seek(start)
|
|
124
|
+
remaining = content_length
|
|
125
|
+
|
|
126
|
+
while remaining > 0:
|
|
127
|
+
chunk_size = min(self.chunk_size, remaining)
|
|
128
|
+
chunk = await f.read(chunk_size)
|
|
129
|
+
if not chunk:
|
|
130
|
+
break
|
|
131
|
+
await response.write(chunk)
|
|
132
|
+
remaining -= len(chunk)
|
|
133
|
+
|
|
134
|
+
await response.write_eof()
|
|
135
|
+
return response
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
return web.Response(status=500, text=f"Range Request Error: {str(e)}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def start_zero_copy_server(directory: str, port: int):
|
|
142
|
+
"""zero-copy optimized server starts"""
|
|
143
|
+
handler = ZeroCopyStaticHandler(directory)
|
|
144
|
+
|
|
145
|
+
# aiohttp app creation
|
|
146
|
+
app = web.Application()
|
|
147
|
+
app.router.add_route('GET', '/{path:.*}', handler.handle_request)
|
|
148
|
+
|
|
149
|
+
# server configuration optimization
|
|
150
|
+
runner = web.AppRunner(
|
|
151
|
+
app,
|
|
152
|
+
access_log=None, # access log disabled (performance improvement)
|
|
153
|
+
keepalive_timeout=75, # Keep-Alive timeout
|
|
154
|
+
client_timeout=600, # client timeout
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
await runner.setup()
|
|
158
|
+
|
|
159
|
+
site = web.TCPSite(
|
|
160
|
+
runner,
|
|
161
|
+
'127.0.0.1',
|
|
162
|
+
port,
|
|
163
|
+
backlog=1024, # connection backlog size
|
|
164
|
+
reuse_address=True # port reuse only
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
await site.start()
|
|
168
|
+
|
|
169
|
+
return runner, site
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def pyloid_serve(
|
|
173
|
+
directory: str,
|
|
174
|
+
port: Optional[int] = None,
|
|
175
|
+
) -> str:
|
|
176
|
+
"""
|
|
177
|
+
zero-copy optimized static file server starts.
|
|
178
|
+
|
|
179
|
+
Args
|
|
180
|
+
----
|
|
181
|
+
directory (str): Path to the static file directory to serve
|
|
182
|
+
port (int, optional): Server port (default: None - will use a random free port)
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
str
|
|
187
|
+
URL of the started server
|
|
188
|
+
|
|
189
|
+
Examples
|
|
190
|
+
--------
|
|
191
|
+
```python
|
|
192
|
+
from pyloid import Pyloid
|
|
193
|
+
from pyloid.serve import pyloid_serve
|
|
194
|
+
|
|
195
|
+
app = Pyloid("Pyloid-App")
|
|
196
|
+
url = pyloid_serve("dist")
|
|
197
|
+
window = app.create_window("Pyloid-App")
|
|
198
|
+
window.load_url(url)
|
|
199
|
+
window.show_and_focus()
|
|
200
|
+
```
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
if port is None:
|
|
204
|
+
port = get_free_port()
|
|
205
|
+
|
|
206
|
+
def run_zero_copy_server():
|
|
207
|
+
"""run async server in a separate thread"""
|
|
208
|
+
loop = asyncio.new_event_loop()
|
|
209
|
+
asyncio.set_event_loop(loop)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
runner, site = loop.run_until_complete(
|
|
213
|
+
start_zero_copy_server(directory, port)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
print(f"🚀 Zero-copy server started on http://127.0.0.1:{port}")
|
|
217
|
+
print(f"📁 Serving directory: {directory}")
|
|
218
|
+
print(f"⚡ Features: sendfile, Range requests, ETag caching")
|
|
219
|
+
|
|
220
|
+
# wait until the server starts
|
|
221
|
+
loop.run_forever()
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"Zero-copy server error: {e}")
|
|
225
|
+
finally:
|
|
226
|
+
loop.close()
|
|
227
|
+
|
|
228
|
+
# start server in a daemon thread
|
|
229
|
+
thread = threading.Thread(target=run_zero_copy_server, daemon=True)
|
|
230
|
+
thread.start()
|
|
231
|
+
|
|
232
|
+
return f"http://127.0.0.1:{port}"
|
|
@@ -20,7 +20,7 @@ class Store:
|
|
|
20
20
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
21
21
|
self.db = PickleDB(path)
|
|
22
22
|
|
|
23
|
-
def get(self, key: str) -> Any:
|
|
23
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
24
24
|
"""
|
|
25
25
|
Retrieve the value associated with the specified key.
|
|
26
26
|
|
|
@@ -28,6 +28,8 @@ class Store:
|
|
|
28
28
|
----------
|
|
29
29
|
key: str
|
|
30
30
|
The key to look up in the database
|
|
31
|
+
default: Any
|
|
32
|
+
The value to return if the value does not exist in the database
|
|
31
33
|
|
|
32
34
|
Returns
|
|
33
35
|
-------
|
|
@@ -44,8 +46,11 @@ class Store:
|
|
|
44
46
|
{'name': 'John Doe', 'age': 30}
|
|
45
47
|
>>> print(store.get("non_existent_key"))
|
|
46
48
|
None
|
|
49
|
+
>>> print(store.get("non_existent_key", "default_value"))
|
|
50
|
+
'default_value'
|
|
47
51
|
"""
|
|
48
|
-
|
|
52
|
+
stored_value = self.db.get(key)
|
|
53
|
+
return stored_value if stored_value is not None else default
|
|
49
54
|
|
|
50
55
|
def set(self, key: str, value: Any) -> bool:
|
|
51
56
|
"""
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
3
|
-
from typing import Optional
|
|
4
|
-
from .utils import get_free_port
|
|
5
|
-
|
|
6
|
-
class CustomStaticHandler(SimpleHTTPRequestHandler):
|
|
7
|
-
def __init__(self, *args, directory=None, **kwargs):
|
|
8
|
-
super().__init__(*args, directory=directory, **kwargs)
|
|
9
|
-
|
|
10
|
-
def log_message(self, format, *args):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
def pyloid_serve(
|
|
14
|
-
directory: str,
|
|
15
|
-
port: Optional[int] = None,
|
|
16
|
-
) -> str:
|
|
17
|
-
"""
|
|
18
|
-
Static file server starts.
|
|
19
|
-
|
|
20
|
-
Args
|
|
21
|
-
----
|
|
22
|
-
directory (str): Path to the static file directory to serve
|
|
23
|
-
port (int, optional): Server port (default: None - will use a random free port)
|
|
24
|
-
|
|
25
|
-
Returns
|
|
26
|
-
-------
|
|
27
|
-
str
|
|
28
|
-
URL of the started server
|
|
29
|
-
|
|
30
|
-
Examples
|
|
31
|
-
--------
|
|
32
|
-
```python
|
|
33
|
-
from pyloid import Pyloid
|
|
34
|
-
from pyloid.serve import pyloid_serve
|
|
35
|
-
|
|
36
|
-
app = Pyloid("Pyloid-App")
|
|
37
|
-
url = pyloid_serve("dist")
|
|
38
|
-
window = app.create_window("Pyloid-App")
|
|
39
|
-
window.load_url(url)
|
|
40
|
-
window.show_and_focus()
|
|
41
|
-
```
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
if port is None:
|
|
45
|
-
port = get_free_port()
|
|
46
|
-
|
|
47
|
-
handler = lambda *args: CustomStaticHandler(*args, directory=directory)
|
|
48
|
-
server = HTTPServer(("127.0.0.1", port), handler)
|
|
49
|
-
|
|
50
|
-
thread = threading.Thread(
|
|
51
|
-
target=server.serve_forever,
|
|
52
|
-
daemon=True
|
|
53
|
-
)
|
|
54
|
-
thread.start()
|
|
55
|
-
|
|
56
|
-
return f"http://127.0.0.1:{port}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|