pyloid 0.24.4__py3-none-any.whl → 0.24.5__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/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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyloid
3
- Version: 0.24.4
3
+ Version: 0.24.5
4
4
  Summary:
5
5
  Author: aesthetics-of-record
6
6
  Author-email: 111675679+aesthetics-of-record@users.noreply.github.com
@@ -9,7 +9,7 @@ pyloid/js_api/event_api.py,sha256=w0z1DcmwcmseqfcoZWgsQmFC2iBCgTMVJubTaHeXI1c,95
9
9
  pyloid/js_api/window_api.py,sha256=-isphU3m2wGB5U0yZrSuK_4XiBz2mG45HsjYTUq7Fxs,7348
10
10
  pyloid/monitor.py,sha256=1mXvHm5deohnNlTLcRx4sT4x-stnOIb0dUQnnxN50Uo,28295
11
11
  pyloid/pyloid.py,sha256=y3kHGahvNkJ_4vMESBVHh1j3OU9foM4GLQF2MID3Vhg,84099
12
- pyloid/rpc.py,sha256=U3G6d5VgCibT0XwSX6eNhLePNyUG8ejfJSf8F43zBpk,18601
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.dist-info/LICENSE,sha256=MTYF-6xpRekyTUglRweWtbfbwBL1I_3Bgfbm_SNOuI8,11525
21
- pyloid-0.24.4.dist-info/METADATA,sha256=VVyDB3g6nS6loWBspj7QlPkrkyPWl5Qwox_6N5ajaEs,2204
22
- pyloid-0.24.4.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
- pyloid-0.24.4.dist-info/RECORD,,
20
+ pyloid-0.24.5.dist-info/LICENSE,sha256=MTYF-6xpRekyTUglRweWtbfbwBL1I_3Bgfbm_SNOuI8,11525
21
+ pyloid-0.24.5.dist-info/METADATA,sha256=zn8TgAWx2pKIHGsJOJvpRr0IUL42VQiHXmV8DNizAvY,2204
22
+ pyloid-0.24.5.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
+ pyloid-0.24.5.dist-info/RECORD,,