pyloid 0.24.7__py3-none-any.whl → 0.24.9__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.
- pyloid/browser_window.py +19 -16
- pyloid/serve.py +190 -14
- {pyloid-0.24.7.dist-info → pyloid-0.24.9.dist-info}/METADATA +4 -3
- {pyloid-0.24.7.dist-info → pyloid-0.24.9.dist-info}/RECORD +6 -6
- {pyloid-0.24.7.dist-info → pyloid-0.24.9.dist-info}/LICENSE +0 -0
- {pyloid-0.24.7.dist-info → pyloid-0.24.9.dist-info}/WHEEL +0 -0
pyloid/browser_window.py
CHANGED
|
@@ -82,7 +82,7 @@ class CustomWebPage(QWebEnginePage):
|
|
|
82
82
|
# return
|
|
83
83
|
# print("Desktop media request received:", request)
|
|
84
84
|
|
|
85
|
-
# #
|
|
85
|
+
# # check the available screen list
|
|
86
86
|
# screens_model = request.screensModel()
|
|
87
87
|
# print("\n=== Available Screens ===")
|
|
88
88
|
# for i in range(screens_model.rowCount()):
|
|
@@ -90,7 +90,7 @@ class CustomWebPage(QWebEnginePage):
|
|
|
90
90
|
# screen_name = screens_model.data(screen_index)
|
|
91
91
|
# print(f"Screen {i}: {screen_name}")
|
|
92
92
|
|
|
93
|
-
# #
|
|
93
|
+
# # check the available window list
|
|
94
94
|
# windows_model = request.windowsModel()
|
|
95
95
|
# print("\n=== Available Windows ===")
|
|
96
96
|
# for i in range(windows_model.rowCount()):
|
|
@@ -102,7 +102,7 @@ class CustomWebPage(QWebEnginePage):
|
|
|
102
102
|
|
|
103
103
|
# # interceptor ( navigation request )
|
|
104
104
|
# def acceptNavigationRequest(self, url, navigation_type, is_main_frame):
|
|
105
|
-
# """
|
|
105
|
+
# """method to handle navigation requests"""
|
|
106
106
|
# print(f"Navigation Request - URL: {url.toString()}")
|
|
107
107
|
# print(f"Navigation Type: {navigation_type}")
|
|
108
108
|
# print(f"Is Main Frame: {is_main_frame}")
|
|
@@ -228,10 +228,10 @@ class CustomWebEngineView(QWebEngineView):
|
|
|
228
228
|
|
|
229
229
|
def eventFilter(self, source, event):
|
|
230
230
|
if self.focusProxy() is source:
|
|
231
|
-
#
|
|
231
|
+
# when in the resize area, all click events are intercepted
|
|
232
232
|
if self.is_in_resize_area and event.type() == QEvent.MouseButtonPress:
|
|
233
233
|
self.mouse_press_event(event)
|
|
234
|
-
return True #
|
|
234
|
+
return True # consume the event so it is not passed to the web view
|
|
235
235
|
|
|
236
236
|
if event.type() == QEvent.MouseButtonPress:
|
|
237
237
|
self.mouse_press_event(event)
|
|
@@ -358,18 +358,18 @@ class _BrowserWindow:
|
|
|
358
358
|
self.close_on_load = True
|
|
359
359
|
self.splash_screen = None
|
|
360
360
|
###########################################################################################
|
|
361
|
-
# RPC
|
|
361
|
+
# if the RPC server is not present, do not add it
|
|
362
362
|
if not self.rpc:
|
|
363
363
|
return
|
|
364
364
|
|
|
365
365
|
self.rpc.pyloid = self.app.pyloid_wrapper
|
|
366
366
|
# self.rpc.window = self.window_wrapper
|
|
367
367
|
|
|
368
|
-
#
|
|
368
|
+
# prevent duplicate RPC servers
|
|
369
369
|
if self.rpc in self.app.rpc_servers:
|
|
370
370
|
return
|
|
371
371
|
|
|
372
|
-
# RPC
|
|
372
|
+
# add the RPC server
|
|
373
373
|
self.app.rpc_servers.add(self.rpc)
|
|
374
374
|
|
|
375
375
|
# Start unique RPC servers
|
|
@@ -405,7 +405,7 @@ class _BrowserWindow:
|
|
|
405
405
|
central_widget.setLayout(layout)
|
|
406
406
|
self._window.setCentralWidget(central_widget)
|
|
407
407
|
|
|
408
|
-
#
|
|
408
|
+
# add properties for window movement
|
|
409
409
|
self._window.moving = False
|
|
410
410
|
self._window.offset = QPoint()
|
|
411
411
|
else:
|
|
@@ -490,6 +490,9 @@ class _BrowserWindow:
|
|
|
490
490
|
self.web_view.settings().setAttribute(
|
|
491
491
|
QWebEngineSettings.WebAttribute.WebRTCPublicInterfacesOnly, False
|
|
492
492
|
)
|
|
493
|
+
self.web_view.settings().setAttribute(
|
|
494
|
+
QWebEngineSettings.WebAttribute.WebGLEnabled, True
|
|
495
|
+
)
|
|
493
496
|
|
|
494
497
|
# Set icon
|
|
495
498
|
if self.app.icon:
|
|
@@ -540,10 +543,10 @@ class _BrowserWindow:
|
|
|
540
543
|
# Set F12 shortcut
|
|
541
544
|
self.set_dev_tools(self.dev_tools)
|
|
542
545
|
|
|
543
|
-
#
|
|
546
|
+
# get the profile and set the interceptor
|
|
544
547
|
profile = self.web_view.page().profile()
|
|
545
548
|
|
|
546
|
-
# #
|
|
549
|
+
# # if the existing interceptor is present, remove it
|
|
547
550
|
# if self.interceptor:
|
|
548
551
|
# profile.setUrlRequestInterceptor(None)
|
|
549
552
|
|
|
@@ -562,15 +565,15 @@ class _BrowserWindow:
|
|
|
562
565
|
new QWebChannel(qt.webChannelTransport, function (channel) {
|
|
563
566
|
window.pyloid = {
|
|
564
567
|
EventAPI: {
|
|
565
|
-
_listeners: {}, //
|
|
568
|
+
_listeners: {}, // object to store the callback functions
|
|
566
569
|
|
|
567
570
|
listen: function(eventName, callback) {
|
|
568
|
-
//
|
|
571
|
+
// if the callback array for the event is not present, create it
|
|
569
572
|
if (!this._listeners[eventName]) {
|
|
570
573
|
this._listeners[eventName] = [];
|
|
571
574
|
}
|
|
572
575
|
|
|
573
|
-
//
|
|
576
|
+
// save the callback function
|
|
574
577
|
this._listeners[eventName].push(callback);
|
|
575
578
|
|
|
576
579
|
document.addEventListener(eventName, function(event) {
|
|
@@ -585,12 +588,12 @@ class _BrowserWindow:
|
|
|
585
588
|
},
|
|
586
589
|
|
|
587
590
|
unlisten: function(eventName) {
|
|
588
|
-
//
|
|
591
|
+
// remove all listeners for the event
|
|
589
592
|
if (this._listeners[eventName]) {
|
|
590
593
|
this._listeners[eventName].forEach(callback => {
|
|
591
594
|
document.removeEventListener(eventName, callback);
|
|
592
595
|
});
|
|
593
|
-
//
|
|
596
|
+
// remove the saved callback
|
|
594
597
|
delete this._listeners[eventName];
|
|
595
598
|
}
|
|
596
599
|
}
|
pyloid/serve.py
CHANGED
|
@@ -1,21 +1,180 @@
|
|
|
1
1
|
import threading
|
|
2
|
-
|
|
2
|
+
import asyncio
|
|
3
|
+
import mimetypes
|
|
4
|
+
import aiofiles
|
|
5
|
+
from aiohttp import web
|
|
6
|
+
from aiohttp.web_fileresponse import FileResponse
|
|
3
7
|
from typing import Optional
|
|
4
|
-
from
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from .utils import get_free_port, is_production
|
|
10
|
+
import logging
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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)
|
|
9
144
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
|
|
12
171
|
|
|
13
172
|
def pyloid_serve(
|
|
14
173
|
directory: str,
|
|
15
174
|
port: Optional[int] = None,
|
|
16
175
|
) -> str:
|
|
17
176
|
"""
|
|
18
|
-
|
|
177
|
+
zero-copy optimized static file server starts.
|
|
19
178
|
|
|
20
179
|
Args
|
|
21
180
|
----
|
|
@@ -44,13 +203,30 @@ def pyloid_serve(
|
|
|
44
203
|
if port is None:
|
|
45
204
|
port = get_free_port()
|
|
46
205
|
|
|
47
|
-
|
|
48
|
-
|
|
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()
|
|
49
227
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
daemon=True
|
|
53
|
-
)
|
|
228
|
+
# start server in a daemon thread
|
|
229
|
+
thread = threading.Thread(target=run_zero_copy_server, daemon=True)
|
|
54
230
|
thread.start()
|
|
55
231
|
|
|
56
232
|
return f"http://127.0.0.1:{port}"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyloid
|
|
3
|
-
Version: 0.24.
|
|
3
|
+
Version: 0.24.9
|
|
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.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
pyloid/__init__.py,sha256=YKwMCSOds1QVi9N7EGfY0Z7BEjJn8j6HGqRblZlZClA,235
|
|
2
2
|
pyloid/api.py,sha256=A61Kmddh8BlpT3LfA6NbPQNzFmD95vQ4WKX53oKsGYU,2419
|
|
3
3
|
pyloid/autostart.py,sha256=K7DQYl4LHItvPp0bt1V9WwaaZmVSTeGvadkcwG-KKrI,3899
|
|
4
|
-
pyloid/browser_window.py,sha256=
|
|
4
|
+
pyloid/browser_window.py,sha256=brCNmrQB-sTUvIC8VR5glpg-__DYmkrpVlIN0jf29Sw,104377
|
|
5
5
|
pyloid/custom/titlebar.py,sha256=itzK9pJbZMQ7BKca9kdbuHMffurrw15UijR6OU03Xsk,3894
|
|
6
6
|
pyloid/filewatcher.py,sha256=3M5zWVUf1OhlkWJcDFC8ZA9agO4Q-U8WdgGpy6kaVz0,4601
|
|
7
7
|
pyloid/js_api/base.py,sha256=Z3ID-4AJ0eHusmljRltlSaK4m2RKvRNfmqX76NLF77o,8585
|
|
@@ -10,14 +10,14 @@ pyloid/js_api/window_api.py,sha256=-isphU3m2wGB5U0yZrSuK_4XiBz2mG45HsjYTUq7Fxs,7
|
|
|
10
10
|
pyloid/monitor.py,sha256=1mXvHm5deohnNlTLcRx4sT4x-stnOIb0dUQnnxN50Uo,28295
|
|
11
11
|
pyloid/pyloid.py,sha256=DSHpMRDW4WvZgAAo2nJMesMJuOkNTVEEsDhCFIvjB5w,84509
|
|
12
12
|
pyloid/rpc.py,sha256=OnF1sRGok9OJ-Q5519eQARD4oZTohyPhsPAT2Mg4_Gg,20377
|
|
13
|
-
pyloid/serve.py,sha256=
|
|
13
|
+
pyloid/serve.py,sha256=KOs9984J2qboEXsquorkXgcQvvao_C2c9F6N2gSLfiA,8122
|
|
14
14
|
pyloid/store.py,sha256=8PnBxtkUgbF8Lxh-iYlEuhbLE76bGBF8t5KV5u5NzoQ,4831
|
|
15
15
|
pyloid/thread_pool.py,sha256=fKOBb8jMfZn_7crA_fJCno8dObBRZE31EIWaNQ759aw,14616
|
|
16
16
|
pyloid/timer.py,sha256=RqMsChFUd93cxMVgkHWiIKrci0QDTBgJSTULnAtYT8M,8712
|
|
17
17
|
pyloid/tray.py,sha256=D12opVEc2wc2T4tK9epaN1oOdeziScsIVNM2uCN7C-A,1710
|
|
18
18
|
pyloid/url_interceptor.py,sha256=AFjPANDELc9-E-1TnVvkNVc-JZBJYf0677dWQ8LDaqw,726
|
|
19
19
|
pyloid/utils.py,sha256=J6owgVE1YDOEfcOPmoP9m9Q6nbYDyNEo9uqPsJs5p5g,6644
|
|
20
|
-
pyloid-0.24.
|
|
21
|
-
pyloid-0.24.
|
|
22
|
-
pyloid-0.24.
|
|
23
|
-
pyloid-0.24.
|
|
20
|
+
pyloid-0.24.9.dist-info/LICENSE,sha256=F96EzotgWhhpnQTW2TcdoqrMDir1jyEo6H915tGQ-QE,11524
|
|
21
|
+
pyloid-0.24.9.dist-info/METADATA,sha256=aUI7SwfcwgFgAoAfRfm1nfzgrtYX2vtH2BupDHqc8eo,2298
|
|
22
|
+
pyloid-0.24.9.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
23
|
+
pyloid-0.24.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|