pyloid 0.25.0__tar.gz → 0.26.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyloid
3
- Version: 0.25.0
3
+ Version: 0.26.1
4
4
  Summary:
5
5
  Author: aesthetics-of-record
6
6
  Author-email: 111675679+aesthetics-of-record@users.noreply.github.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyloid"
3
- version = "0.25.0"
3
+ version = "0.26.1"
4
4
  description = ""
5
5
  authors = ["aesthetics-of-record <111675679+aesthetics-of-record@users.noreply.github.com>"]
6
6
  readme = "README.md"
@@ -24,10 +24,10 @@ from PySide6.QtCore import (
24
24
  from PySide6.QtWebEngineCore import (
25
25
  QWebEnginePage,
26
26
  QWebEngineSettings,
27
+ # QWebEngineUrlRequestInterceptor,
27
28
  )
28
- from .api import PyloidAPI
29
29
  import uuid
30
- from typing import List, Optional, Dict, Callable
30
+ from typing import Optional, Dict, Callable
31
31
  import json
32
32
  from PySide6.QtWidgets import (
33
33
  QWidget,
@@ -42,9 +42,8 @@ from PySide6.QtWebEngineCore import (
42
42
  QWebEngineSettings,
43
43
  # QWebEngineDesktopMediaRequest, # 6.8.3 부터
44
44
  )
45
- import threading
46
45
 
47
- # from .url_interceptor import CustomUrlInterceptor
46
+ # from .url_interceptor import ServerUrlInterceptor
48
47
 
49
48
  if TYPE_CHECKING:
50
49
  from .pyloid import _Pyloid, Pyloid
@@ -112,7 +111,7 @@ class CustomWebPage(QWebEnginePage):
112
111
  # class CustomInterceptor(QWebEngineUrlRequestInterceptor):
113
112
  # def __init__(self, index_path=None):
114
113
  # super().__init__()
115
- # self.index_path = get_production_path()
114
+ # # self.index_path = get_production_path()
116
115
  # self.last_path = "/"
117
116
 
118
117
  # def interceptRequest(self, info):
@@ -322,10 +321,8 @@ class _BrowserWindow:
322
321
  self._window = QMainWindow()
323
322
  self.web_view = CustomWebEngineView(self)
324
323
 
325
-
326
-
327
324
  # interceptor ( all url request )
328
- # self.web_view.page().setUrlRequestInterceptor(self.interceptor)
325
+ # self.web_view.page().profile().setUrlRequestInterceptor(CustomInterceptor())
329
326
 
330
327
  self._window.closeEvent = self.closeEvent # Override closeEvent method
331
328
  ###########################################################################################
@@ -477,7 +474,7 @@ class _BrowserWindow:
477
474
  if sys.platform == "win32":
478
475
  import ctypes
479
476
 
480
- myappid = "mycompany.myproduct.subproduct.version"
477
+ myappid = f"pyloid.{self.app.app_name}.com"
481
478
  ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
482
479
 
483
480
  # Remove title bar and borders (if needed)
@@ -517,11 +514,10 @@ class _BrowserWindow:
517
514
  self.set_dev_tools(self.dev_tools)
518
515
 
519
516
  # get the profile and set the interceptor
520
- profile = self.web_view.page().profile()
517
+ # profile = self.web_view.page().profile()
518
+ # profile.setUrlRequestInterceptor(ServerUrlInterceptor(self.app.server.url, self.id))
519
+
521
520
 
522
- # # if the existing interceptor is present, remove it
523
- # if self.interceptor:
524
- # profile.setUrlRequestInterceptor(None)
525
521
 
526
522
  def _on_load_finished(self, ok):
527
523
  """Handles the event when the web page finishes loading."""
@@ -11,13 +11,13 @@ if TYPE_CHECKING:
11
11
 
12
12
  class BaseAPI(PyloidAPI):
13
13
  def __init__(
14
- self, window_id: str, data: dict, app: "_Pyloid", rpc_url: Optional[str] = None
14
+ self, window_id: str, data: dict, app: "_Pyloid", server_url: Optional[str] = None
15
15
  ):
16
16
  super().__init__()
17
17
  self.window_id: str = window_id
18
18
  self.data: dict = data
19
19
  self.app: "_Pyloid" = app
20
- self.rpc_url: Optional[str] = rpc_url
20
+ self.server_url: Optional[str] = server_url
21
21
 
22
22
  @Bridge(result=dict)
23
23
  def getData(self):
@@ -254,6 +254,6 @@ class BaseAPI(PyloidAPI):
254
254
  return get_production_path(path)
255
255
 
256
256
  @Bridge(result=str)
257
- def getRpcUrl(self):
257
+ def getServerUrl(self):
258
258
  """Returns the RPC URL of the application."""
259
- return self.rpc_url
259
+ return self.server_url
@@ -97,10 +97,10 @@ class _Pyloid(QApplication):
97
97
  def __init__(
98
98
  self,
99
99
  pyloid_wrapper: "Pyloid",
100
- app_name,
101
- single_instance=True,
102
- server=None,
103
- data=None,
100
+ app_name: str,
101
+ single_instance: bool = True,
102
+ server = None,
103
+ data = None,
104
104
  ):
105
105
  """
106
106
  Initializes the Pyloid application.
@@ -150,7 +150,7 @@ class _Pyloid(QApplication):
150
150
  self.tray_menu_items = []
151
151
  self.tray_actions = {}
152
152
 
153
- self.app_name = app_name
153
+ self.app_name: str = app_name
154
154
  self.app_path = sys.executable
155
155
 
156
156
  self.auto_start = AutoStart(self.app_name, self.app_path)
@@ -1632,6 +1632,10 @@ class Pyloid(QObject):
1632
1632
  super().__init__()
1633
1633
 
1634
1634
  self.data = None # 나중에 데이터 필요 시 수정
1635
+
1636
+ # server 에 Pyloid 주입
1637
+ if server:
1638
+ server.pyloid = self
1635
1639
 
1636
1640
  self.app = _Pyloid(self, app_name, single_instance, server, self.data)
1637
1641
 
@@ -1,232 +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
-
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
232
  return f"http://127.0.0.1:{port}"
@@ -0,0 +1,25 @@
1
+ from PySide6.QtWebEngineCore import QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo
2
+
3
+ # interceptor ( all url request )
4
+ class ServerUrlInterceptor(QWebEngineUrlRequestInterceptor):
5
+ def __init__(self, server_url: str, window_id: str):
6
+ super().__init__()
7
+ self.server_url = server_url
8
+ self.headers = {
9
+ "X-Pyloid-Window-Id": window_id,
10
+ }
11
+
12
+ print("interceptor init")
13
+
14
+ def interceptRequest(self, info):
15
+ # host = info.requestUrl().host()
16
+ url = info.requestUrl().toString()
17
+
18
+ print(url)
19
+
20
+ if url.startswith(self.server_url):
21
+ headers = info.httpHeaders()
22
+ print("before", headers)
23
+ headers.update(self.headers)
24
+ print("after", headers)
25
+ return
@@ -1,24 +0,0 @@
1
- from PySide6.QtWebEngineCore import QWebEngineUrlRequestInterceptor
2
- from PySide6.QtCore import QUrl
3
- from typing import Optional
4
-
5
- # interceptor ( all url request )
6
- class CustomUrlInterceptor(QWebEngineUrlRequestInterceptor):
7
- def __init__(self, rpc_url: Optional[str] = None):
8
- super().__init__()
9
- self.rpc_url = rpc_url
10
-
11
- def interceptRequest(self, info):
12
- host = info.requestUrl().host()
13
- url = info.requestUrl().toString()
14
-
15
- server_url = self.rpc_url
16
-
17
- if self.rpc_url is None:
18
- return
19
-
20
- if url.startswith(self.rpc_url):
21
- return
22
-
23
- if host == "pyloid.rpc":
24
- info.redirect(QUrl(server_url))
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