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
|
-
|
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
|
@@ -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=
|
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.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,,
|
File without changes
|
File without changes
|