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 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, # 6.8.3 부터
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
- # 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,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
- # 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
+ # 사용 가능한 화면 목록 확인
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
+ # 사용 가능한 창 목록 확인
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):
@@ -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
- self._window.setAttribute(Qt.WA_TranslucentBackground)
495
- self.web_view.setAttribute(Qt.WA_TranslucentBackground)
496
- self.web_view.page().setBackgroundColor(Qt.transparent)
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
- self._window.setAttribute(Qt.WA_TranslucentBackground)
821
- self.web_view.setAttribute(Qt.WA_TranslucentBackground)
822
- self.web_view.page().setBackgroundColor(Qt.transparent)
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, self, title, width, height, x, y, frame, context_menu, dev_tools, rpc
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
- def __init__(self):
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(self._app, defaults={
137
- "*": aiohttp_cors.ResourceOptions(
138
- allow_credentials=True,
139
- expose_headers="*",
140
- allow_headers="*",
141
- allow_methods=["POST"]
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(f"RPC function name '{rpc_name}' is already registered.")
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 = 'ctx' in sig.parameters
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 'ctx' not in kwargs:
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['ctx'] = ctx
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 {"code": -32600, "message": "Invalid Request: Request must be a JSON object."}
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 {"code": -32600, "message": "Invalid Request: 'jsonrpc' version must be '2.0'."}
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 {"code": -32600, "message": "Invalid Request: 'method' must be a string."}
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 {"code": -32602, "message": "Invalid params: 'params' must be an array or object."}
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 # Request structure is valid
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 # Define data outside try block for broader scope if needed
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 != 'application/json':
280
- # Cannot determine ID if content type is wrong, respond with null ID
281
- error_resp = {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error: Content-Type must be application/json."}, "id": None}
282
- return web.json_response(error_resp, status=415) # Unsupported Media Type
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") # Can be str, int, null, or absent
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 = {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error: Invalid JSON format."}, "id": None}
294
- return web.json_response(error_resp, status=400) # Bad Request
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
- # Use extracted ID if available, otherwise it remains None
300
- error_resp = {"jsonrpc": "2.0", "error": validation_error, "id": request_id}
301
- return web.json_response(error_resp, status=400) # Bad Request
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 = {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": request_id}
312
- return web.json_response(error_resp, status=404) # Not Found
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 = 'ctx' in sig.parameters
320
-
375
+ has_ctx_param = "ctx" in sig.parameters
376
+
321
377
  # ctx 매개변수가 있으면 컨텍스트 객체 생성
322
- if has_ctx_param and isinstance(params, dict) and 'ctx' not in params:
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['ctx'] = ctx
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(pyloid=self.pyloid, window=self.pyloid.get_window_by_id(request_id))
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['_pyloid_window_id'] = internal_window_id
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 = {k: v for k, v in params.items() if k in allowed_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 request_id is not None: # Notifications (id=null or absent) don't get responses
352
- response_data = {"jsonrpc": "2.0", "result": result, "id": request_id}
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
- # Application-specific error during method execution
363
- log.warning(f"RPC execution error in method '{method_name}': {e}", exc_info=False)
364
- if request_id is not None:
365
- error_resp = {"jsonrpc": "2.0", "error": e.to_dict(), "id": request_id}
366
- # Use 500 or a more specific 4xx/5xx if applicable based on error code?
367
- # Sticking to 500 for server-side execution errors.
368
- return web.json_response(error_resp, status=500)
369
- else:
370
- return web.Response(status=204) # No response for notification errors
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(f"Unexpected error during execution of RPC method '{method_name}':") # Log full traceback
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 = {"jsonrpc": "2.0", "error": {"code": -32000, "message": f"Server error: {type(e).__name__}"}, "id": request_id}
377
- return web.json_response(error_resp, status=500) # Internal Server Error
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(status=204) # No response for notification errors
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 = {"jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error"}, "id": request_id}
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 = {'print': None, 'access_log': None}
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.4.dev0
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.13
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.0)
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=-tXG3zUFBliYo-vOb3pSiZ5uABnjC5wtOPzT7PoXnY4,101317
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=y3kHGahvNkJ_4vMESBVHh1j3OU9foM4GLQF2MID3Vhg,84099
12
- pyloid/rpc.py,sha256=U3G6d5VgCibT0XwSX6eNhLePNyUG8ejfJSf8F43zBpk,18601
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.4.dev0.dist-info/LICENSE,sha256=F96EzotgWhhpnQTW2TcdoqrMDir1jyEo6H915tGQ-QE,11524
21
- pyloid-0.24.4.dev0.dist-info/METADATA,sha256=Kh9etxF0hhZKZoR3rm3_ekwVftvLmr78E0gR2h4sfWM,2158
22
- pyloid-0.24.4.dev0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
- pyloid-0.24.4.dev0.dist-info/RECORD,,
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,,