pyloid 0.24.8__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 CHANGED
@@ -40,7 +40,7 @@ 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
 
@@ -55,7 +55,7 @@ class CustomWebPage(QWebEnginePage):
55
55
  def __init__(self, profile=None):
56
56
  super().__init__(profile)
57
57
  self.featurePermissionRequested.connect(self._handlePermissionRequest)
58
- self.desktopMediaRequested.connect(self._handleDesktopMediaRequest)
58
+ # self.desktopMediaRequested.connect(self._handleDesktopMediaRequest)
59
59
  self._permission_handlers = {}
60
60
  self._desktop_media_handler = None
61
61
  self._url_handlers = {} # URL 핸들러 저장을 위한 딕셔너리 추가
@@ -78,31 +78,31 @@ class CustomWebPage(QWebEnginePage):
78
78
  """Register a handler for a specific permission"""
79
79
  self._permission_handlers[feature] = handler
80
80
 
81
- def _handleDesktopMediaRequest(self, request: QWebEngineDesktopMediaRequest):
82
- return
83
- print("Desktop media request received:", request)
81
+ # def _handleDesktopMediaRequest(self, request: QWebEngineDesktopMediaRequest):
82
+ # return
83
+ # print("Desktop media request received:", request)
84
84
 
85
- # 사용 가능한 화면 목록 확인
86
- screens_model = request.screensModel()
87
- print("\n=== Available Screens ===")
88
- for i in range(screens_model.rowCount()):
89
- screen_index = screens_model.index(i)
90
- screen_name = screens_model.data(screen_index)
91
- print(f"Screen {i}: {screen_name}")
85
+ # # check the available screen list
86
+ # screens_model = request.screensModel()
87
+ # print("\n=== Available Screens ===")
88
+ # for i in range(screens_model.rowCount()):
89
+ # screen_index = screens_model.index(i)
90
+ # screen_name = screens_model.data(screen_index)
91
+ # print(f"Screen {i}: {screen_name}")
92
92
 
93
- # 사용 가능한 목록 확인
94
- windows_model = request.windowsModel()
95
- print("\n=== Available Windows ===")
96
- for i in range(windows_model.rowCount()):
97
- window_index = windows_model.index(i)
98
- window_name = windows_model.data(window_index)
99
- print(f"Window {i}: {window_name}")
93
+ # # check the available window list
94
+ # windows_model = request.windowsModel()
95
+ # print("\n=== Available Windows ===")
96
+ # for i in range(windows_model.rowCount()):
97
+ # window_index = windows_model.index(i)
98
+ # window_name = windows_model.data(window_index)
99
+ # print(f"Window {i}: {window_name}")
100
100
 
101
- request.selectWindow(windows_model.index(3))
101
+ # request.selectWindow(windows_model.index(3))
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
- # RPC 서버 중복 방지
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
- # Add properties for window movement
408
+ # add properties for window movement
409
409
  self._window.moving = False
410
410
  self._window.offset = QPoint()
411
411
  else:
@@ -543,10 +543,10 @@ class _BrowserWindow:
543
543
  # Set F12 shortcut
544
544
  self.set_dev_tools(self.dev_tools)
545
545
 
546
- # 프로필 가져오기 인터셉터 설정
546
+ # get the profile and set the interceptor
547
547
  profile = self.web_view.page().profile()
548
548
 
549
- # # 기존 인터셉터가 있다면 제거
549
+ # # if the existing interceptor is present, remove it
550
550
  # if self.interceptor:
551
551
  # profile.setUrlRequestInterceptor(None)
552
552
 
@@ -565,15 +565,15 @@ class _BrowserWindow:
565
565
  new QWebChannel(qt.webChannelTransport, function (channel) {
566
566
  window.pyloid = {
567
567
  EventAPI: {
568
- _listeners: {}, // 콜백 함수들을 저장할 객체
568
+ _listeners: {}, // object to store the callback functions
569
569
 
570
570
  listen: function(eventName, callback) {
571
- // 이벤트에 대한 콜백 배열이 없다면 생성
571
+ // if the callback array for the event is not present, create it
572
572
  if (!this._listeners[eventName]) {
573
573
  this._listeners[eventName] = [];
574
574
  }
575
575
 
576
- // 콜백 함수 저장
576
+ // save the callback function
577
577
  this._listeners[eventName].push(callback);
578
578
 
579
579
  document.addEventListener(eventName, function(event) {
@@ -588,12 +588,12 @@ class _BrowserWindow:
588
588
  },
589
589
 
590
590
  unlisten: function(eventName) {
591
- // 해당 이벤트의 모든 리스너 제거
591
+ // remove all listeners for the event
592
592
  if (this._listeners[eventName]) {
593
593
  this._listeners[eventName].forEach(callback => {
594
594
  document.removeEventListener(eventName, callback);
595
595
  });
596
- // 저장된 콜백 제거
596
+ // remove the saved callback
597
597
  delete this._listeners[eventName];
598
598
  }
599
599
  }
pyloid/serve.py CHANGED
@@ -1,21 +1,180 @@
1
1
  import threading
2
- from http.server import HTTPServer, SimpleHTTPRequestHandler
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 .utils import get_free_port
8
+ from pathlib import Path
9
+ from .utils import get_free_port, is_production
10
+ import logging
5
11
 
6
- class CustomStaticHandler(SimpleHTTPRequestHandler):
7
- def __init__(self, *args, directory=None, **kwargs):
8
- super().__init__(*args, directory=directory, **kwargs)
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
- def log_message(self, format, *args):
11
- pass
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
- Static file server starts.
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
- handler = lambda *args: CustomStaticHandler(*args, directory=directory)
48
- server = HTTPServer(("127.0.0.1", port), handler)
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
- thread = threading.Thread(
51
- target=server.serve_forever,
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}"
pyloid/store.py CHANGED
@@ -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
- return self.db.get(key)
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
  """
@@ -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.24.8
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.9.1)
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
  ![example image](example.png)
25
26
 
26
- <h2 align="center" style="font-size: 28px;"><b>Pyloid: Thread-Safe Desktop Apps—Unified with Any Frontend and Python Technology</b></h2>
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=xy_AOZiqY5kffMXgkeDa5a1u1VSPv860_W9xg3wyWEI,104349
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=wJIBqiLr1-8FvBdV3yybeBtVXsu94FfWYKjHL0eQ68s,1444
14
- pyloid/store.py,sha256=teoa-HYzwm93Rivcw3AhKw6rAmQqQ_kmF6XYSkC3G_I,4541
13
+ pyloid/serve.py,sha256=KOs9984J2qboEXsquorkXgcQvvao_C2c9F6N2gSLfiA,8122
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.8.dist-info/LICENSE,sha256=MTYF-6xpRekyTUglRweWtbfbwBL1I_3Bgfbm_SNOuI8,11525
21
- pyloid-0.24.8.dist-info/METADATA,sha256=IGiJsO7-sG69DSi9Rw_Hn4Ttuf-T2nquIacuZBWSI5Q,2264
22
- pyloid-0.24.8.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
- pyloid-0.24.8.dist-info/RECORD,,
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,,