pyloid 0.24.8__py3-none-any.whl → 0.25.0__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,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
- return
83
- print("Desktop media request received:", request)
80
+ # def _handleDesktopMediaRequest(self, request: QWebEngineDesktopMediaRequest):
81
+ # return
82
+ # print("Desktop media request received:", request)
84
83
 
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}")
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
- 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}")
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
- request.selectWindow(windows_model.index(3))
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
- if rpc:
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.rpc_url)]
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
- # Add properties for window movement
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)
pyloid/pyloid.py CHANGED
@@ -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
- self.rpc_servers: Set[PyloidRPC] = set()
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
- # # Collect and deduplicate RPC servers
319
- # rpc_servers: Set[PyloidRPC] = set()
320
- # for window in self.windows_dict.values():
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.server = QLocalServer()
346
- self.server.listen(self.app_name)
347
- self.server.newConnection.connect(self._handle_new_connection)
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)
pyloid/rpc.py CHANGED
@@ -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
+
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.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.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,23 +1,23 @@
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=u9DfdlPHUZFpwdVprPs_Vhkj1aV3K_16KIub0vLCrLc,103284
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
8
8
  pyloid/js_api/event_api.py,sha256=w0z1DcmwcmseqfcoZWgsQmFC2iBCgTMVJubTaHeXI1c,957
9
9
  pyloid/js_api/window_api.py,sha256=-isphU3m2wGB5U0yZrSuK_4XiBz2mG45HsjYTUq7Fxs,7348
10
10
  pyloid/monitor.py,sha256=1mXvHm5deohnNlTLcRx4sT4x-stnOIb0dUQnnxN50Uo,28295
11
- pyloid/pyloid.py,sha256=DSHpMRDW4WvZgAAo2nJMesMJuOkNTVEEsDhCFIvjB5w,84509
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
11
+ pyloid/pyloid.py,sha256=Ioq_jmyaKTvW3KsmqZX3uhSUc3xRW9FgOtWcH96cVfo,84152
12
+ pyloid/rpc.py,sha256=6UzQc-CDA72VYeF3Xy-7AbBculE-T0MIokR39jVdlcg,21726
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.25.0.dist-info/LICENSE,sha256=F96EzotgWhhpnQTW2TcdoqrMDir1jyEo6H915tGQ-QE,11524
21
+ pyloid-0.25.0.dist-info/METADATA,sha256=289p55TOz7kF41q9lyHqgB9h0fRlxCyR1zZSF9OTDYw,2298
22
+ pyloid-0.25.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
+ pyloid-0.25.0.dist-info/RECORD,,