pyloid 0.24.4.dev0__py3-none-any.whl → 0.24.6__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 +124 -30
- pyloid/pyloid.py +9 -0
- pyloid/rpc.py +169 -77
- {pyloid-0.24.4.dev0.dist-info → pyloid-0.24.6.dist-info}/LICENSE +1 -1
- {pyloid-0.24.4.dev0.dist-info → pyloid-0.24.6.dist-info}/METADATA +4 -3
- {pyloid-0.24.4.dev0.dist-info → pyloid-0.24.6.dist-info}/RECORD +7 -7
- {pyloid-0.24.4.dev0.dist-info → pyloid-0.24.6.dist-info}/WHEEL +0 -0
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
|
-
|
43
|
+
QWebEngineDesktopMediaRequest,
|
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
|
-
|
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,27 +78,27 @@ class CustomWebPage(QWebEnginePage):
|
|
78
78
|
"""Register a handler for a specific permission"""
|
79
79
|
self._permission_handlers[feature] = handler
|
80
80
|
|
81
|
-
|
82
|
-
|
83
|
-
|
81
|
+
def _handleDesktopMediaRequest(self, request: QWebEngineDesktopMediaRequest):
|
82
|
+
return
|
83
|
+
print("Desktop media request received:", request)
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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}")
|
92
92
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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}")
|
100
100
|
|
101
|
-
|
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):
|
@@ -317,6 +317,7 @@ class _BrowserWindow:
|
|
317
317
|
dev_tools: bool = False,
|
318
318
|
# js_apis: List[PyloidAPI] = [],
|
319
319
|
rpc: Optional[PyloidRPC] = None,
|
320
|
+
transparent: bool = False,
|
320
321
|
):
|
321
322
|
###########################################################################################
|
322
323
|
self.id = str(uuid.uuid4()) # Generate unique ID
|
@@ -344,6 +345,7 @@ class _BrowserWindow:
|
|
344
345
|
self.x = x
|
345
346
|
self.y = y
|
346
347
|
self.frame = frame
|
348
|
+
self.transparent = transparent
|
347
349
|
self.context_menu = context_menu
|
348
350
|
self.dev_tools = dev_tools
|
349
351
|
|
@@ -413,6 +415,20 @@ class _BrowserWindow:
|
|
413
415
|
|
414
416
|
self._window.show()
|
415
417
|
|
418
|
+
def _apply_transparency(self):
|
419
|
+
"""Applies transparency settings based on self.transparent and self.frame."""
|
420
|
+
if self.transparent:
|
421
|
+
# It's generally better if FramelessWindowHint is set for full transparency,
|
422
|
+
# but WA_TranslucentBackground can still have effects otherwise.
|
423
|
+
self._window.setAttribute(Qt.WA_TranslucentBackground, True)
|
424
|
+
self.web_view.setAttribute(Qt.WA_TranslucentBackground, True)
|
425
|
+
self.web_view.page().setBackgroundColor(Qt.transparent)
|
426
|
+
else:
|
427
|
+
self._window.setAttribute(Qt.WA_TranslucentBackground, False)
|
428
|
+
self.web_view.setAttribute(Qt.WA_TranslucentBackground, False)
|
429
|
+
# Reset background color for web_view page, QColor() or a specific color like Qt.white
|
430
|
+
self.web_view.page().setBackgroundColor(Qt.white)
|
431
|
+
|
416
432
|
def _load(self):
|
417
433
|
self.set_title(self.title)
|
418
434
|
|
@@ -491,9 +507,11 @@ class _BrowserWindow:
|
|
491
507
|
# Remove title bar and borders (if needed)
|
492
508
|
if not self.frame:
|
493
509
|
self._window.setWindowFlags(Qt.FramelessWindowHint)
|
494
|
-
|
495
|
-
|
496
|
-
self.
|
510
|
+
else:
|
511
|
+
# Ensure standard window flags if frame is True, otherwise flags might be missing
|
512
|
+
self._window.setWindowFlags(Qt.Window)
|
513
|
+
|
514
|
+
self._apply_transparency()
|
497
515
|
|
498
516
|
# Disable default context menu
|
499
517
|
if not self.context_menu:
|
@@ -817,12 +835,43 @@ class _BrowserWindow:
|
|
817
835
|
self._window.setWindowFlags(Qt.Window)
|
818
836
|
else:
|
819
837
|
self._window.setWindowFlags(Qt.FramelessWindowHint)
|
820
|
-
|
821
|
-
|
822
|
-
|
838
|
+
|
839
|
+
self._apply_transparency()
|
840
|
+
|
823
841
|
if was_visible:
|
824
842
|
self._window.show()
|
825
843
|
|
844
|
+
def set_transparent(self, transparent: bool):
|
845
|
+
"""
|
846
|
+
Sets the transparency of the window.
|
847
|
+
|
848
|
+
Parameters
|
849
|
+
----------
|
850
|
+
transparent : bool
|
851
|
+
If True, the window background will be transparent.
|
852
|
+
If False, it will be opaque.
|
853
|
+
|
854
|
+
Examples
|
855
|
+
--------
|
856
|
+
>>> window.set_transparent(True)
|
857
|
+
"""
|
858
|
+
self.transparent = transparent
|
859
|
+
self._apply_transparency()
|
860
|
+
|
861
|
+
if self._window.isVisible():
|
862
|
+
self._window.show()
|
863
|
+
|
864
|
+
def get_transparent(self) -> bool:
|
865
|
+
"""
|
866
|
+
Returns the transparency state of the window.
|
867
|
+
|
868
|
+
Returns
|
869
|
+
-------
|
870
|
+
bool
|
871
|
+
True if the window is set to be transparent, False otherwise.
|
872
|
+
"""
|
873
|
+
return self.transparent
|
874
|
+
|
826
875
|
def set_context_menu(self, context_menu: bool):
|
827
876
|
"""
|
828
877
|
Sets the context menu of the window.
|
@@ -1296,6 +1345,7 @@ class _BrowserWindow:
|
|
1296
1345
|
"x": self.x,
|
1297
1346
|
"y": self.y,
|
1298
1347
|
"frame": self.frame,
|
1348
|
+
"transparent": self.transparent, # Add transparent to properties
|
1299
1349
|
"context_menu": self.context_menu,
|
1300
1350
|
"dev_tools": self.dev_tools,
|
1301
1351
|
}
|
@@ -2069,14 +2119,26 @@ class BrowserWindow(QObject):
|
|
2069
2119
|
height: int,
|
2070
2120
|
x: int,
|
2071
2121
|
y: int,
|
2072
|
-
frame: bool,
|
2073
|
-
context_menu: bool,
|
2074
|
-
dev_tools: bool,
|
2122
|
+
frame: bool = True,
|
2123
|
+
context_menu: bool = False,
|
2124
|
+
dev_tools: bool = False,
|
2075
2125
|
rpc: Optional[PyloidRPC] = None,
|
2126
|
+
transparent: bool = False,
|
2076
2127
|
):
|
2077
2128
|
super().__init__()
|
2078
2129
|
self._window = _BrowserWindow(
|
2079
|
-
app,
|
2130
|
+
app,
|
2131
|
+
self,
|
2132
|
+
title,
|
2133
|
+
width,
|
2134
|
+
height,
|
2135
|
+
x,
|
2136
|
+
y,
|
2137
|
+
frame,
|
2138
|
+
context_menu,
|
2139
|
+
dev_tools,
|
2140
|
+
rpc,
|
2141
|
+
transparent,
|
2080
2142
|
)
|
2081
2143
|
self.command_signal.connect(self._handle_command)
|
2082
2144
|
|
@@ -2112,6 +2174,10 @@ class BrowserWindow(QObject):
|
|
2112
2174
|
result = self._window.set_position_by_anchor(params["anchor"])
|
2113
2175
|
elif command_type == "set_frame":
|
2114
2176
|
result = self._window.set_frame(params["frame"])
|
2177
|
+
elif command_type == "set_transparent":
|
2178
|
+
result = self._window.set_transparent(params["transparent"])
|
2179
|
+
elif command_type == "get_transparent":
|
2180
|
+
result = self._window.get_transparent()
|
2115
2181
|
elif command_type == "set_context_menu":
|
2116
2182
|
result = self._window.set_context_menu(params["context_menu"])
|
2117
2183
|
elif command_type == "set_dev_tools":
|
@@ -2395,6 +2461,34 @@ class BrowserWindow(QObject):
|
|
2395
2461
|
"""
|
2396
2462
|
return self.execute_command("set_frame", {"frame": frame})
|
2397
2463
|
|
2464
|
+
# TODO: Can't use this function in runtime
|
2465
|
+
# def set_transparent(self, transparent: bool) -> None:
|
2466
|
+
# """
|
2467
|
+
# Sets the transparency of the window.
|
2468
|
+
|
2469
|
+
# Parameters
|
2470
|
+
# ----------
|
2471
|
+
# transparent : bool
|
2472
|
+
# If True, the window background will be transparent.
|
2473
|
+
# If False, it will be opaque.
|
2474
|
+
|
2475
|
+
# Examples
|
2476
|
+
# --------
|
2477
|
+
# >>> window.set_transparent(True)
|
2478
|
+
# """
|
2479
|
+
# return self.execute_command("set_transparent", {"transparent": transparent})
|
2480
|
+
|
2481
|
+
def get_transparent(self) -> bool:
|
2482
|
+
"""
|
2483
|
+
Returns the transparency state of the window.
|
2484
|
+
|
2485
|
+
Returns
|
2486
|
+
-------
|
2487
|
+
bool
|
2488
|
+
True if the window is set to be transparent, False otherwise.
|
2489
|
+
"""
|
2490
|
+
return self.execute_command("get_transparent", {})
|
2491
|
+
|
2398
2492
|
def set_context_menu(self, context_menu: bool) -> None:
|
2399
2493
|
"""
|
2400
2494
|
Sets the context menu of the window.
|
pyloid/pyloid.py
CHANGED
@@ -243,6 +243,7 @@ class _Pyloid(QApplication):
|
|
243
243
|
context_menu: bool = False,
|
244
244
|
dev_tools: bool = False,
|
245
245
|
rpc: Optional[PyloidRPC] = None,
|
246
|
+
transparent: bool = False,
|
246
247
|
) -> BrowserWindow:
|
247
248
|
"""
|
248
249
|
Creates a new browser window.
|
@@ -267,6 +268,8 @@ class _Pyloid(QApplication):
|
|
267
268
|
Whether to use developer tools (default is False)
|
268
269
|
rpc : PyloidRPC, optional
|
269
270
|
The RPC server instance to be used in the window
|
271
|
+
transparent : bool, optional
|
272
|
+
Whether the window is transparent (default is False)
|
270
273
|
|
271
274
|
Returns
|
272
275
|
-------
|
@@ -290,6 +293,7 @@ class _Pyloid(QApplication):
|
|
290
293
|
context_menu,
|
291
294
|
dev_tools,
|
292
295
|
rpc,
|
296
|
+
transparent,
|
293
297
|
)
|
294
298
|
self.windows_dict[window._window.id] = window
|
295
299
|
# latest_window_id = list(self.windows_dict.keys())[-1]
|
@@ -1654,6 +1658,7 @@ class Pyloid(QObject):
|
|
1654
1658
|
context_menu=params.get("context_menu", False),
|
1655
1659
|
dev_tools=params.get("dev_tools", False),
|
1656
1660
|
rpc=params.get("rpc", None),
|
1661
|
+
transparent=params.get("transparent", False),
|
1657
1662
|
)
|
1658
1663
|
result = window
|
1659
1664
|
|
@@ -1829,6 +1834,7 @@ class Pyloid(QObject):
|
|
1829
1834
|
context_menu: bool = False,
|
1830
1835
|
dev_tools: bool = False,
|
1831
1836
|
rpc: Optional[PyloidRPC] = None,
|
1837
|
+
transparent: bool = False,
|
1832
1838
|
) -> BrowserWindow:
|
1833
1839
|
"""
|
1834
1840
|
Creates a new browser window.
|
@@ -1853,6 +1859,8 @@ class Pyloid(QObject):
|
|
1853
1859
|
Whether to use developer tools (default is False)
|
1854
1860
|
rpc : PyloidRPC, optional
|
1855
1861
|
The RPC server instance to be used in the window
|
1862
|
+
transparent : bool, optional
|
1863
|
+
Whether the window is transparent (default is False)
|
1856
1864
|
|
1857
1865
|
Returns
|
1858
1866
|
-------
|
@@ -1874,6 +1882,7 @@ class Pyloid(QObject):
|
|
1874
1882
|
"context_menu": context_menu,
|
1875
1883
|
"dev_tools": dev_tools,
|
1876
1884
|
"rpc": rpc,
|
1885
|
+
"transparent": transparent,
|
1877
1886
|
}
|
1878
1887
|
return self.execute_command("create_window", params)
|
1879
1888
|
|
pyloid/rpc.py
CHANGED
@@ -10,6 +10,7 @@ import threading
|
|
10
10
|
import time
|
11
11
|
import aiohttp_cors
|
12
12
|
from typing import TYPE_CHECKING
|
13
|
+
|
13
14
|
if TYPE_CHECKING:
|
14
15
|
from .pyloid import Pyloid
|
15
16
|
from .browser_window import BrowserWindow
|
@@ -18,10 +19,11 @@ if TYPE_CHECKING:
|
|
18
19
|
logging.basicConfig(level=logging.INFO)
|
19
20
|
log = logging.getLogger("pyloid.rpc")
|
20
21
|
|
22
|
+
|
21
23
|
class RPCContext:
|
22
24
|
"""
|
23
25
|
Class that provides context information when calling RPC methods.
|
24
|
-
|
26
|
+
|
25
27
|
Attributes
|
26
28
|
----------
|
27
29
|
pyloid : Pyloid
|
@@ -29,10 +31,12 @@ class RPCContext:
|
|
29
31
|
window : BrowserWindow
|
30
32
|
Current browser window instance.
|
31
33
|
"""
|
34
|
+
|
32
35
|
def __init__(self, pyloid: "Pyloid", window: "BrowserWindow"):
|
33
36
|
self.pyloid: "Pyloid" = pyloid
|
34
37
|
self.window: "BrowserWindow" = window
|
35
38
|
|
39
|
+
|
36
40
|
class RPCError(Exception):
|
37
41
|
"""
|
38
42
|
Custom exception for RPC-related errors.
|
@@ -50,6 +54,7 @@ class RPCError(Exception):
|
|
50
54
|
data : Any, optional
|
51
55
|
Additional information about the error, by default None.
|
52
56
|
"""
|
57
|
+
|
53
58
|
def __init__(self, message: str, code: int = -32000, data: Any = None):
|
54
59
|
"""
|
55
60
|
Initialize the RPCError.
|
@@ -82,6 +87,7 @@ class RPCError(Exception):
|
|
82
87
|
error_obj["data"] = self.data
|
83
88
|
return error_obj
|
84
89
|
|
90
|
+
|
85
91
|
class PyloidRPC:
|
86
92
|
"""
|
87
93
|
A simple JSON-RPC server wrapper based on aiohttp.
|
@@ -104,17 +110,23 @@ class PyloidRPC:
|
|
104
110
|
_app : web.Application
|
105
111
|
The underlying aiohttp web application instance.
|
106
112
|
"""
|
107
|
-
|
113
|
+
|
114
|
+
def __init__(self, client_max_size: int = 1024 * 1024 * 10):
|
108
115
|
"""
|
109
116
|
Initialize the PyloidRPC server instance.
|
110
|
-
|
117
|
+
|
118
|
+
Parameters
|
119
|
+
----------
|
120
|
+
client_max_size : int, optional
|
121
|
+
The maximum size of client requests (bytes). Default is 10MB.
|
122
|
+
|
111
123
|
Examples
|
112
124
|
--------
|
113
125
|
```python
|
114
126
|
from pyloid.rpc import PyloidRPC
|
115
|
-
|
127
|
+
|
116
128
|
rpc = PyloidRPC()
|
117
|
-
|
129
|
+
|
118
130
|
@rpc.method()
|
119
131
|
async def add(a: int, b: int) -> int:
|
120
132
|
return a + b
|
@@ -123,29 +135,32 @@ class PyloidRPC:
|
|
123
135
|
self._host = "127.0.0.1"
|
124
136
|
self._port = get_free_port()
|
125
137
|
self._rpc_path = "/rpc"
|
126
|
-
|
138
|
+
|
127
139
|
self.url = f"http://{self._host}:{self._port}{self._rpc_path}"
|
128
|
-
|
140
|
+
|
129
141
|
self._functions: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {}
|
130
|
-
self._app = web.Application()
|
131
|
-
|
142
|
+
self._app = web.Application(client_max_size=client_max_size)
|
143
|
+
|
132
144
|
self.pyloid: Optional["Pyloid"] = None
|
133
145
|
# self.window: Optional["BrowserWindow"] = None
|
134
|
-
|
146
|
+
|
135
147
|
# CORS 설정 추가
|
136
|
-
cors = aiohttp_cors.setup(
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
148
|
+
cors = aiohttp_cors.setup(
|
149
|
+
self._app,
|
150
|
+
defaults={
|
151
|
+
"*": aiohttp_cors.ResourceOptions(
|
152
|
+
allow_credentials=True,
|
153
|
+
expose_headers="*",
|
154
|
+
allow_headers="*",
|
155
|
+
allow_methods=["POST"],
|
156
|
+
)
|
157
|
+
},
|
158
|
+
)
|
159
|
+
|
145
160
|
# CORS 적용된 라우트 추가
|
146
161
|
resource = cors.add(self._app.router.add_resource(self._rpc_path))
|
147
162
|
cors.add(resource.add_route("POST", self._handle_rpc))
|
148
|
-
|
163
|
+
|
149
164
|
log.info(f"RPC server initialized.")
|
150
165
|
self._runner: Optional[web.AppRunner] = None
|
151
166
|
self._site: Optional[web.TCPSite] = None
|
@@ -153,7 +168,7 @@ class PyloidRPC:
|
|
153
168
|
def method(self, name: Optional[str] = None) -> Callable:
|
154
169
|
"""
|
155
170
|
Use a decorator to register an async function as an RPC method.
|
156
|
-
|
171
|
+
|
157
172
|
If there is a 'ctx' parameter, an RPCContext object is automatically injected.
|
158
173
|
This object allows access to the pyloid application and current window.
|
159
174
|
|
@@ -173,14 +188,14 @@ class PyloidRPC:
|
|
173
188
|
If the decorated function is not an async function (`coroutinefunction`).
|
174
189
|
ValueError
|
175
190
|
If an RPC function with the specified name is already registered.
|
176
|
-
|
191
|
+
|
177
192
|
Examples
|
178
193
|
--------
|
179
194
|
```python
|
180
195
|
from pyloid.rpc import PyloidRPC, RPCContext
|
181
|
-
|
196
|
+
|
182
197
|
rpc = PyloidRPC()
|
183
|
-
|
198
|
+
|
184
199
|
@rpc.method()
|
185
200
|
async def add(ctx: RPCContext, a: int, b: int) -> int:
|
186
201
|
# Access the application and window through ctx.pyloid and ctx.window
|
@@ -189,31 +204,36 @@ class PyloidRPC:
|
|
189
204
|
return a + b
|
190
205
|
```
|
191
206
|
"""
|
207
|
+
|
192
208
|
def decorator(func: Callable[..., Coroutine[Any, Any, Any]]):
|
193
209
|
rpc_name = name or func.__name__
|
194
210
|
if not asyncio.iscoroutinefunction(func):
|
195
211
|
raise TypeError(f"RPC function '{rpc_name}' must be an async function.")
|
196
212
|
if rpc_name in self._functions:
|
197
|
-
raise ValueError(
|
213
|
+
raise ValueError(
|
214
|
+
f"RPC function name '{rpc_name}' is already registered."
|
215
|
+
)
|
198
216
|
|
199
217
|
# Analyze function signature
|
200
218
|
sig = inspect.signature(func)
|
201
|
-
has_ctx_param =
|
202
|
-
|
219
|
+
has_ctx_param = "ctx" in sig.parameters
|
220
|
+
|
203
221
|
# Store the original function
|
204
222
|
self._functions[rpc_name] = func
|
205
223
|
log.info(f"RPC function registered: {rpc_name}")
|
206
224
|
|
207
225
|
@wraps(func)
|
208
226
|
async def wrapper(*args, _pyloid_window_id=None, **kwargs):
|
209
|
-
if has_ctx_param and
|
227
|
+
if has_ctx_param and "ctx" not in kwargs:
|
210
228
|
ctx = RPCContext(
|
211
229
|
pyloid=self.pyloid,
|
212
|
-
window=self.pyloid.get_window_by_id(_pyloid_window_id)
|
230
|
+
window=self.pyloid.get_window_by_id(_pyloid_window_id),
|
213
231
|
)
|
214
|
-
kwargs[
|
232
|
+
kwargs["ctx"] = ctx
|
215
233
|
return await func(*args, **kwargs)
|
234
|
+
|
216
235
|
return wrapper
|
236
|
+
|
217
237
|
return decorator
|
218
238
|
|
219
239
|
def _validate_jsonrpc_request(self, data: Any) -> Optional[Dict[str, Any]]:
|
@@ -239,19 +259,31 @@ class PyloidRPC:
|
|
239
259
|
request_id = data.get("id") if isinstance(data, dict) else None
|
240
260
|
|
241
261
|
if not isinstance(data, dict):
|
242
|
-
return {
|
262
|
+
return {
|
263
|
+
"code": -32600,
|
264
|
+
"message": "Invalid Request: Request must be a JSON object.",
|
265
|
+
}
|
243
266
|
if data.get("jsonrpc") != "2.0":
|
244
|
-
return {
|
267
|
+
return {
|
268
|
+
"code": -32600,
|
269
|
+
"message": "Invalid Request: 'jsonrpc' version must be '2.0'.",
|
270
|
+
}
|
245
271
|
if "method" not in data or not isinstance(data["method"], str):
|
246
|
-
return {
|
272
|
+
return {
|
273
|
+
"code": -32600,
|
274
|
+
"message": "Invalid Request: 'method' must be a string.",
|
275
|
+
}
|
247
276
|
if "params" in data and not isinstance(data["params"], (list, dict)):
|
248
277
|
# JSON-RPC 2.0: "params" must be array or object if present
|
249
|
-
return {
|
278
|
+
return {
|
279
|
+
"code": -32602,
|
280
|
+
"message": "Invalid params: 'params' must be an array or object.",
|
281
|
+
}
|
250
282
|
# JSON-RPC 2.0: "id" is optional, but if present, must be string, number, or null.
|
251
283
|
# This validation is simplified here. A more robust check could be added.
|
252
284
|
# if "id" in data and not isinstance(data.get("id"), (str, int, float, type(None))):
|
253
285
|
# return {"code": -32600, "message": "Invalid Request: 'id', if present, must be a string, number, or null."}
|
254
|
-
return None
|
286
|
+
return None # Request structure is valid
|
255
287
|
|
256
288
|
async def _handle_rpc(self, request: web.Request) -> web.Response:
|
257
289
|
"""
|
@@ -272,14 +304,23 @@ class PyloidRPC:
|
|
272
304
|
An aiohttp JSON response object containing the JSON-RPC response or error.
|
273
305
|
"""
|
274
306
|
request_id: Optional[Union[str, int, None]] = None
|
275
|
-
data: Any = None
|
307
|
+
data: Any = None # Define data outside try block for broader scope if needed
|
276
308
|
|
277
309
|
try:
|
278
310
|
# 1. Check Content-Type
|
279
|
-
if request.content_type !=
|
280
|
-
|
281
|
-
|
282
|
-
|
311
|
+
if request.content_type != "application/json":
|
312
|
+
# Cannot determine ID if content type is wrong, respond with null ID
|
313
|
+
error_resp = {
|
314
|
+
"jsonrpc": "2.0",
|
315
|
+
"error": {
|
316
|
+
"code": -32700,
|
317
|
+
"message": "Parse error: Content-Type must be application/json.",
|
318
|
+
},
|
319
|
+
"id": None,
|
320
|
+
}
|
321
|
+
return web.json_response(
|
322
|
+
error_resp, status=415
|
323
|
+
) # Unsupported Media Type
|
283
324
|
|
284
325
|
# 2. Parse JSON Body
|
285
326
|
try:
|
@@ -287,18 +328,29 @@ class PyloidRPC:
|
|
287
328
|
data = json.loads(raw_data)
|
288
329
|
# Extract ID early for inclusion in potential error responses
|
289
330
|
if isinstance(data, dict):
|
290
|
-
request_id = data.get("id")
|
331
|
+
request_id = data.get("id") # Can be str, int, null, or absent
|
291
332
|
except json.JSONDecodeError:
|
292
333
|
# Invalid JSON, ID might be unknown, respond with null ID
|
293
|
-
error_resp = {
|
294
|
-
|
334
|
+
error_resp = {
|
335
|
+
"jsonrpc": "2.0",
|
336
|
+
"error": {
|
337
|
+
"code": -32700,
|
338
|
+
"message": "Parse error: Invalid JSON format.",
|
339
|
+
},
|
340
|
+
"id": None,
|
341
|
+
}
|
342
|
+
return web.json_response(error_resp, status=400) # Bad Request
|
295
343
|
|
296
344
|
# 3. Validate JSON-RPC Structure
|
297
345
|
validation_error = self._validate_jsonrpc_request(data)
|
298
346
|
if validation_error:
|
299
|
-
|
300
|
-
|
301
|
-
|
347
|
+
# Use extracted ID if available, otherwise it remains None
|
348
|
+
error_resp = {
|
349
|
+
"jsonrpc": "2.0",
|
350
|
+
"error": validation_error,
|
351
|
+
"id": request_id,
|
352
|
+
}
|
353
|
+
return web.json_response(error_resp, status=400) # Bad Request
|
302
354
|
|
303
355
|
# Assuming validation passed, data is a dict with 'method'
|
304
356
|
method_name: str = data["method"]
|
@@ -308,48 +360,63 @@ class PyloidRPC:
|
|
308
360
|
# 4. Find and Call Method
|
309
361
|
func = self._functions.get(method_name)
|
310
362
|
if func is None:
|
311
|
-
error_resp = {
|
312
|
-
|
363
|
+
error_resp = {
|
364
|
+
"jsonrpc": "2.0",
|
365
|
+
"error": {"code": -32601, "message": "Method not found."},
|
366
|
+
"id": request_id,
|
367
|
+
}
|
368
|
+
return web.json_response(error_resp, status=404) # Not Found
|
313
369
|
|
314
370
|
try:
|
315
371
|
log.debug(f"Executing RPC method: {method_name}(params={params})")
|
316
|
-
|
372
|
+
|
317
373
|
# 함수의 서명 분석하여 ctx 매개변수 유무 확인
|
318
374
|
sig = inspect.signature(func)
|
319
|
-
has_ctx_param =
|
320
|
-
|
375
|
+
has_ctx_param = "ctx" in sig.parameters
|
376
|
+
|
321
377
|
# ctx 매개변수가 있으면 컨텍스트 객체 생성
|
322
|
-
if has_ctx_param and isinstance(params, dict) and
|
378
|
+
if has_ctx_param and isinstance(params, dict) and "ctx" not in params:
|
323
379
|
ctx = RPCContext(
|
324
380
|
pyloid=self.pyloid,
|
325
|
-
window=self.pyloid.get_window_by_id(request_id)
|
381
|
+
window=self.pyloid.get_window_by_id(request_id),
|
326
382
|
)
|
327
383
|
# 딕셔너리 형태로 params 사용할 때
|
328
384
|
params = params.copy() # 원본 params 복사
|
329
|
-
params[
|
330
|
-
|
385
|
+
params["ctx"] = ctx
|
386
|
+
|
331
387
|
# Call the function with positional or keyword arguments
|
332
388
|
if isinstance(params, list):
|
333
389
|
# 리스트 형태로 params 사용할 때 처리 필요
|
334
390
|
if has_ctx_param:
|
335
|
-
ctx = RPCContext(
|
391
|
+
ctx = RPCContext(
|
392
|
+
pyloid=self.pyloid,
|
393
|
+
window=self.pyloid.get_window_by_id(request_id),
|
394
|
+
)
|
336
395
|
result = await func(ctx, *params, request_id=request_id)
|
337
396
|
else:
|
338
397
|
result = await func(*params, request_id=request_id)
|
339
398
|
else: # isinstance(params, dict)
|
340
399
|
internal_window_id = request_id
|
341
400
|
params = params.copy()
|
342
|
-
params[
|
401
|
+
params["_pyloid_window_id"] = internal_window_id
|
343
402
|
|
344
403
|
# 함수 시그니처에 맞는 인자만 추려서 전달
|
345
404
|
sig = inspect.signature(func)
|
346
405
|
allowed_params = set(sig.parameters.keys())
|
347
|
-
filtered_params = {
|
406
|
+
filtered_params = {
|
407
|
+
k: v for k, v in params.items() if k in allowed_params
|
408
|
+
}
|
348
409
|
result = await func(**filtered_params)
|
349
410
|
|
350
411
|
# 5. Format Success Response (only for non-notification requests)
|
351
|
-
if
|
352
|
-
|
412
|
+
if (
|
413
|
+
request_id is not None
|
414
|
+
): # Notifications (id=null or absent) don't get responses
|
415
|
+
response_data = {
|
416
|
+
"jsonrpc": "2.0",
|
417
|
+
"result": result,
|
418
|
+
"id": request_id,
|
419
|
+
}
|
353
420
|
return web.json_response(response_data)
|
354
421
|
else:
|
355
422
|
# No response for notifications, return 204 No Content might be appropriate
|
@@ -357,32 +424,57 @@ class PyloidRPC:
|
|
357
424
|
# For clarity/standard compliance, maybe return 204?
|
358
425
|
return web.Response(status=204)
|
359
426
|
|
360
|
-
|
361
427
|
except RPCError as e:
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
428
|
+
# Application-specific error during method execution
|
429
|
+
log.warning(
|
430
|
+
f"RPC execution error in method '{method_name}': {e}",
|
431
|
+
exc_info=False,
|
432
|
+
)
|
433
|
+
if request_id is not None:
|
434
|
+
error_resp = {
|
435
|
+
"jsonrpc": "2.0",
|
436
|
+
"error": e.to_dict(),
|
437
|
+
"id": request_id,
|
438
|
+
}
|
439
|
+
# Use 500 or a more specific 4xx/5xx if applicable based on error code?
|
440
|
+
# Sticking to 500 for server-side execution errors.
|
441
|
+
return web.json_response(error_resp, status=500)
|
442
|
+
else:
|
443
|
+
return web.Response(
|
444
|
+
status=204
|
445
|
+
) # No response for notification errors
|
371
446
|
except Exception as e:
|
372
447
|
# Unexpected error during method execution
|
373
|
-
log.exception(
|
448
|
+
log.exception(
|
449
|
+
f"Unexpected error during execution of RPC method '{method_name}':"
|
450
|
+
) # Log full traceback
|
374
451
|
if request_id is not None:
|
375
452
|
# Minimize internal details exposed to the client
|
376
|
-
error_resp = {
|
377
|
-
|
453
|
+
error_resp = {
|
454
|
+
"jsonrpc": "2.0",
|
455
|
+
"error": {
|
456
|
+
"code": -32000,
|
457
|
+
"message": f"Server error: {type(e).__name__}",
|
458
|
+
},
|
459
|
+
"id": request_id,
|
460
|
+
}
|
461
|
+
return web.json_response(
|
462
|
+
error_resp, status=500
|
463
|
+
) # Internal Server Error
|
378
464
|
else:
|
379
|
-
return web.Response(
|
465
|
+
return web.Response(
|
466
|
+
status=204
|
467
|
+
) # No response for notification errors
|
380
468
|
|
381
469
|
except Exception as e:
|
382
470
|
# Catch-all for fatal errors during request handling itself (before/after method call)
|
383
471
|
log.exception("Fatal error in RPC handler:")
|
384
472
|
# ID might be uncertain at this stage, include if available
|
385
|
-
error_resp = {
|
473
|
+
error_resp = {
|
474
|
+
"jsonrpc": "2.0",
|
475
|
+
"error": {"code": -32603, "message": "Internal error"},
|
476
|
+
"id": request_id,
|
477
|
+
}
|
386
478
|
return web.json_response(error_resp, status=500)
|
387
479
|
|
388
480
|
async def start_async(self, **kwargs):
|
@@ -419,10 +511,10 @@ class PyloidRPC:
|
|
419
511
|
"""
|
420
512
|
log.info(f"Starting RPC server")
|
421
513
|
# Default to print=None to avoid duplicate startup messages, can be overridden via kwargs
|
422
|
-
run_app_kwargs = {
|
514
|
+
run_app_kwargs = {"print": None, "access_log": None}
|
423
515
|
run_app_kwargs.update(kwargs)
|
424
516
|
try:
|
425
517
|
web.run_app(self._app, host=self._host, port=self._port, **run_app_kwargs)
|
426
518
|
except Exception as e:
|
427
519
|
log.exception(f"Failed to start or run the server: {e}")
|
428
|
-
raise
|
520
|
+
raise
|
@@ -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,19 +1,20 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: pyloid
|
3
|
-
Version: 0.24.
|
3
|
+
Version: 0.24.6
|
4
4
|
Summary:
|
5
5
|
Author: aesthetics-of-record
|
6
6
|
Author-email: 111675679+aesthetics-of-record@users.noreply.github.com
|
7
|
-
Requires-Python: >=3.9,<3.
|
7
|
+
Requires-Python: >=3.9,<3.14
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.9
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
13
14
|
Requires-Dist: aiohttp-cors (>=0.8.1,<0.9.0)
|
14
15
|
Requires-Dist: pickledb (>=1.3.2,<2.0.0)
|
15
16
|
Requires-Dist: platformdirs (>=4.3.7,<5.0.0)
|
16
|
-
Requires-Dist: pyside6 (==6.8.
|
17
|
+
Requires-Dist: pyside6 (==6.8.3)
|
17
18
|
Description-Content-Type: text/markdown
|
18
19
|
|
19
20
|
<h1 style="text-align: center; font-size: 200px; font-weight: 500;">
|
@@ -1,15 +1,15 @@
|
|
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=EkYcqwdX_WC2woIUcElwI7E4BgpXk_a_hgJJcDS1vc4,104226
|
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=
|
12
|
-
pyloid/rpc.py,sha256=
|
11
|
+
pyloid/pyloid.py,sha256=DSHpMRDW4WvZgAAo2nJMesMJuOkNTVEEsDhCFIvjB5w,84509
|
12
|
+
pyloid/rpc.py,sha256=OnF1sRGok9OJ-Q5519eQARD4oZTohyPhsPAT2Mg4_Gg,20377
|
13
13
|
pyloid/serve.py,sha256=wJIBqiLr1-8FvBdV3yybeBtVXsu94FfWYKjHL0eQ68s,1444
|
14
14
|
pyloid/store.py,sha256=teoa-HYzwm93Rivcw3AhKw6rAmQqQ_kmF6XYSkC3G_I,4541
|
15
15
|
pyloid/thread_pool.py,sha256=fKOBb8jMfZn_7crA_fJCno8dObBRZE31EIWaNQ759aw,14616
|
@@ -17,7 +17,7 @@ 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.6.dist-info/LICENSE,sha256=MTYF-6xpRekyTUglRweWtbfbwBL1I_3Bgfbm_SNOuI8,11525
|
21
|
+
pyloid-0.24.6.dist-info/METADATA,sha256=MUdtV5-iru99UsWv46KP_VVAtmFu3f-nP6NAreUTy1g,2204
|
22
|
+
pyloid-0.24.6.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
23
|
+
pyloid-0.24.6.dist-info/RECORD,,
|
File without changes
|