pyloid 0.22.0.dev6__py3-none-any.whl → 0.23.0__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 +156 -84
- pyloid/js_api/base.py +234 -8
- pyloid/js_api/window_api.py +9 -52
- pyloid/pyloid.py +596 -100
- pyloid/rpc.py +339 -0
- pyloid/store.py +172 -0
- pyloid/url_interceptor.py +24 -0
- {pyloid-0.22.0.dev6.dist-info → pyloid-0.23.0.dist-info}/METADATA +4 -1
- {pyloid-0.22.0.dev6.dist-info → pyloid-0.23.0.dist-info}/RECORD +11 -8
- {pyloid-0.22.0.dev6.dist-info → pyloid-0.23.0.dist-info}/LICENSE +0 -0
- {pyloid-0.22.0.dev6.dist-info → pyloid-0.23.0.dist-info}/WHEEL +0 -0
pyloid/rpc.py
ADDED
@@ -0,0 +1,339 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
from functools import wraps
|
5
|
+
from typing import Any, Callable, Coroutine, Dict, List, Optional, Union
|
6
|
+
from .utils import get_free_port
|
7
|
+
from aiohttp import web
|
8
|
+
import threading
|
9
|
+
import time
|
10
|
+
import aiohttp_cors # CORS 지원을 위한 패키지 추가
|
11
|
+
|
12
|
+
# Configure logging
|
13
|
+
logging.basicConfig(level=logging.INFO)
|
14
|
+
log = logging.getLogger("pyloid.rpc")
|
15
|
+
|
16
|
+
class RPCError(Exception):
|
17
|
+
"""
|
18
|
+
Custom exception for RPC-related errors.
|
19
|
+
|
20
|
+
Follows the JSON-RPC 2.0 error object structure.
|
21
|
+
|
22
|
+
Attributes
|
23
|
+
----------
|
24
|
+
message : str
|
25
|
+
A human-readable description of the error.
|
26
|
+
code : int, optional
|
27
|
+
A number indicating the error type that occurred. Standard JSON-RPC
|
28
|
+
codes are used where applicable, with application-specific codes
|
29
|
+
also possible. Defaults to -32000 (Server error).
|
30
|
+
data : Any, optional
|
31
|
+
Additional information about the error, by default None.
|
32
|
+
"""
|
33
|
+
def __init__(self, message: str, code: int = -32000, data: Any = None):
|
34
|
+
"""
|
35
|
+
Initialize the RPCError.
|
36
|
+
|
37
|
+
Parameters
|
38
|
+
----------
|
39
|
+
message : str
|
40
|
+
The error message.
|
41
|
+
code : int, optional
|
42
|
+
The error code. Defaults to -32000.
|
43
|
+
data : Any, optional
|
44
|
+
Additional data associated with the error. Defaults to None.
|
45
|
+
"""
|
46
|
+
self.message = message
|
47
|
+
self.code = code
|
48
|
+
self.data = data
|
49
|
+
super().__init__(self.message)
|
50
|
+
|
51
|
+
def to_dict(self) -> Dict[str, Any]:
|
52
|
+
"""
|
53
|
+
Convert the error details into a dictionary suitable for JSON-RPC responses.
|
54
|
+
|
55
|
+
Returns
|
56
|
+
-------
|
57
|
+
Dict[str, Any]
|
58
|
+
A dictionary representing the JSON-RPC error object.
|
59
|
+
"""
|
60
|
+
error_obj = {"code": self.code, "message": self.message}
|
61
|
+
if self.data is not None:
|
62
|
+
error_obj["data"] = self.data
|
63
|
+
return error_obj
|
64
|
+
|
65
|
+
class PyloidRPC:
|
66
|
+
"""
|
67
|
+
A simple JSON-RPC server wrapper based on aiohttp.
|
68
|
+
|
69
|
+
Allows registering asynchronous functions as RPC methods using the `@rpc`
|
70
|
+
decorator and handles JSON-RPC 2.0 request parsing, validation,
|
71
|
+
method dispatching, and response formatting.
|
72
|
+
|
73
|
+
Attributes
|
74
|
+
----------
|
75
|
+
_host : str
|
76
|
+
The hostname or IP address to bind the server to.
|
77
|
+
_port : int
|
78
|
+
The port number to listen on.
|
79
|
+
_rpc_path : str
|
80
|
+
The URL path for handling RPC requests.
|
81
|
+
_functions : Dict[str, Callable[..., Coroutine[Any, Any, Any]]]
|
82
|
+
A dictionary mapping registered RPC method names to their
|
83
|
+
corresponding asynchronous functions.
|
84
|
+
_app : web.Application
|
85
|
+
The underlying aiohttp web application instance.
|
86
|
+
"""
|
87
|
+
def __init__(self):
|
88
|
+
"""
|
89
|
+
Initialize the PyloidRPC server instance.
|
90
|
+
"""
|
91
|
+
self._host = "127.0.0.1"
|
92
|
+
self._port = get_free_port()
|
93
|
+
self._rpc_path = "/rpc"
|
94
|
+
|
95
|
+
self.url = f"http://{self._host}:{self._port}{self._rpc_path}"
|
96
|
+
|
97
|
+
self._functions: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {}
|
98
|
+
self._app = web.Application()
|
99
|
+
|
100
|
+
# CORS 설정 추가
|
101
|
+
cors = aiohttp_cors.setup(self._app, defaults={
|
102
|
+
"*": aiohttp_cors.ResourceOptions(
|
103
|
+
allow_credentials=True,
|
104
|
+
expose_headers="*",
|
105
|
+
allow_headers="*",
|
106
|
+
allow_methods=["POST"]
|
107
|
+
)
|
108
|
+
})
|
109
|
+
|
110
|
+
# CORS 적용된 라우트 추가
|
111
|
+
resource = cors.add(self._app.router.add_resource(self._rpc_path))
|
112
|
+
cors.add(resource.add_route("POST", self._handle_rpc))
|
113
|
+
|
114
|
+
log.info(f"RPC server initialized.")
|
115
|
+
self._runner: Optional[web.AppRunner] = None
|
116
|
+
self._site: Optional[web.TCPSite] = None
|
117
|
+
|
118
|
+
def rpc(self, name: Optional[str] = None) -> Callable:
|
119
|
+
"""
|
120
|
+
Decorator to register an async function as an RPC method.
|
121
|
+
|
122
|
+
Parameters
|
123
|
+
----------
|
124
|
+
name : Optional[str], optional
|
125
|
+
The name to register the RPC method under. If None, the
|
126
|
+
function's name is used. Defaults to None.
|
127
|
+
|
128
|
+
Returns
|
129
|
+
-------
|
130
|
+
Callable
|
131
|
+
The decorator function.
|
132
|
+
|
133
|
+
Raises
|
134
|
+
------
|
135
|
+
TypeError
|
136
|
+
If the decorated function is not an async function (`coroutinefunction`).
|
137
|
+
ValueError
|
138
|
+
If an RPC function with the specified name is already registered.
|
139
|
+
"""
|
140
|
+
def decorator(func: Callable[..., Coroutine[Any, Any, Any]]):
|
141
|
+
rpc_name = name or func.__name__
|
142
|
+
if not asyncio.iscoroutinefunction(func):
|
143
|
+
raise TypeError(f"RPC function '{rpc_name}' must be an async function.")
|
144
|
+
if rpc_name in self._functions:
|
145
|
+
raise ValueError(f"RPC function name '{rpc_name}' is already registered.")
|
146
|
+
|
147
|
+
self._functions[rpc_name] = func
|
148
|
+
log.info(f"RPC function registered: {rpc_name}")
|
149
|
+
|
150
|
+
@wraps(func)
|
151
|
+
async def wrapper(*args, **kwargs):
|
152
|
+
# This wrapper exists to follow the decorator pattern.
|
153
|
+
# The actual call uses the original function stored in _functions.
|
154
|
+
return await func(*args, **kwargs)
|
155
|
+
return wrapper
|
156
|
+
return decorator
|
157
|
+
|
158
|
+
def _validate_jsonrpc_request(self, data: Any) -> Optional[Dict[str, Any]]:
|
159
|
+
"""
|
160
|
+
Validate the structure of a potential JSON-RPC request object.
|
161
|
+
|
162
|
+
Checks for required fields ('jsonrpc', 'method') and validates the
|
163
|
+
types of fields like 'params' and 'id' according to the JSON-RPC 2.0 spec.
|
164
|
+
|
165
|
+
Parameters
|
166
|
+
----------
|
167
|
+
data : Any
|
168
|
+
The parsed JSON data from the request body.
|
169
|
+
|
170
|
+
Returns
|
171
|
+
-------
|
172
|
+
Optional[Dict[str, Any]]
|
173
|
+
None if the request is valid according to the basic structure,
|
174
|
+
otherwise a dictionary representing the JSON-RPC error object
|
175
|
+
to be returned to the client.
|
176
|
+
"""
|
177
|
+
# Attempt to extract the ID if possible, even for invalid requests
|
178
|
+
request_id = data.get("id") if isinstance(data, dict) else None
|
179
|
+
|
180
|
+
if not isinstance(data, dict):
|
181
|
+
return {"code": -32600, "message": "Invalid Request: Request must be a JSON object."}
|
182
|
+
if data.get("jsonrpc") != "2.0":
|
183
|
+
return {"code": -32600, "message": "Invalid Request: 'jsonrpc' version must be '2.0'."}
|
184
|
+
if "method" not in data or not isinstance(data["method"], str):
|
185
|
+
return {"code": -32600, "message": "Invalid Request: 'method' must be a string."}
|
186
|
+
if "params" in data and not isinstance(data["params"], (list, dict)):
|
187
|
+
# JSON-RPC 2.0: "params" must be array or object if present
|
188
|
+
return {"code": -32602, "message": "Invalid params: 'params' must be an array or object."}
|
189
|
+
# JSON-RPC 2.0: "id" is optional, but if present, must be string, number, or null.
|
190
|
+
# This validation is simplified here. A more robust check could be added.
|
191
|
+
# if "id" in data and not isinstance(data.get("id"), (str, int, float, type(None))):
|
192
|
+
# return {"code": -32600, "message": "Invalid Request: 'id', if present, must be a string, number, or null."}
|
193
|
+
return None # Request structure is valid
|
194
|
+
|
195
|
+
async def _handle_rpc(self, request: web.Request) -> web.Response:
|
196
|
+
"""
|
197
|
+
Handles incoming JSON-RPC requests.
|
198
|
+
|
199
|
+
Parses the request, validates it, dispatches to the appropriate
|
200
|
+
registered RPC method, executes the method, and returns the
|
201
|
+
JSON-RPC response or error object.
|
202
|
+
|
203
|
+
Parameters
|
204
|
+
----------
|
205
|
+
request : web.Request
|
206
|
+
The incoming aiohttp request object.
|
207
|
+
|
208
|
+
Returns
|
209
|
+
-------
|
210
|
+
web.Response
|
211
|
+
An aiohttp JSON response object containing the JSON-RPC response or error.
|
212
|
+
"""
|
213
|
+
request_id: Optional[Union[str, int, None]] = None
|
214
|
+
data: Any = None # Define data outside try block for broader scope if needed
|
215
|
+
|
216
|
+
try:
|
217
|
+
# 1. Check Content-Type
|
218
|
+
if request.content_type != 'application/json':
|
219
|
+
# Cannot determine ID if content type is wrong, respond with null ID
|
220
|
+
error_resp = {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error: Content-Type must be application/json."}, "id": None}
|
221
|
+
return web.json_response(error_resp, status=415) # Unsupported Media Type
|
222
|
+
|
223
|
+
# 2. Parse JSON Body
|
224
|
+
try:
|
225
|
+
raw_data = await request.read()
|
226
|
+
data = json.loads(raw_data)
|
227
|
+
# Extract ID early for inclusion in potential error responses
|
228
|
+
if isinstance(data, dict):
|
229
|
+
request_id = data.get("id") # Can be str, int, null, or absent
|
230
|
+
except json.JSONDecodeError:
|
231
|
+
# Invalid JSON, ID might be unknown, respond with null ID
|
232
|
+
error_resp = {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error: Invalid JSON format."}, "id": None}
|
233
|
+
return web.json_response(error_resp, status=400) # Bad Request
|
234
|
+
|
235
|
+
# 3. Validate JSON-RPC Structure
|
236
|
+
validation_error = self._validate_jsonrpc_request(data)
|
237
|
+
if validation_error:
|
238
|
+
# Use extracted ID if available, otherwise it remains None
|
239
|
+
error_resp = {"jsonrpc": "2.0", "error": validation_error, "id": request_id}
|
240
|
+
return web.json_response(error_resp, status=400) # Bad Request
|
241
|
+
|
242
|
+
# Assuming validation passed, data is a dict with 'method'
|
243
|
+
method_name: str = data["method"]
|
244
|
+
# Use empty list/dict if 'params' is omitted, as per spec flexibility
|
245
|
+
params: Union[List, Dict] = data.get("params", [])
|
246
|
+
|
247
|
+
# 4. Find and Call Method
|
248
|
+
func = self._functions.get(method_name)
|
249
|
+
if func is None:
|
250
|
+
error_resp = {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": request_id}
|
251
|
+
return web.json_response(error_resp, status=404) # Not Found
|
252
|
+
|
253
|
+
try:
|
254
|
+
log.debug(f"Executing RPC method: {method_name}(params={params})")
|
255
|
+
# Call the function with positional or keyword arguments
|
256
|
+
if isinstance(params, list):
|
257
|
+
result = await func(*params)
|
258
|
+
else: # isinstance(params, dict)
|
259
|
+
result = await func(**params)
|
260
|
+
|
261
|
+
# 5. Format Success Response (only for non-notification requests)
|
262
|
+
if request_id is not None: # Notifications (id=null or absent) don't get responses
|
263
|
+
response_data = {"jsonrpc": "2.0", "result": result, "id": request_id}
|
264
|
+
return web.json_response(response_data)
|
265
|
+
else:
|
266
|
+
# No response for notifications, return 204 No Content might be appropriate
|
267
|
+
# or just an empty response. aiohttp handles this implicitly if nothing is returned.
|
268
|
+
# For clarity/standard compliance, maybe return 204?
|
269
|
+
return web.Response(status=204)
|
270
|
+
|
271
|
+
|
272
|
+
except RPCError as e:
|
273
|
+
# Application-specific error during method execution
|
274
|
+
log.warning(f"RPC execution error in method '{method_name}': {e}", exc_info=False)
|
275
|
+
if request_id is not None:
|
276
|
+
error_resp = {"jsonrpc": "2.0", "error": e.to_dict(), "id": request_id}
|
277
|
+
# Use 500 or a more specific 4xx/5xx if applicable based on error code?
|
278
|
+
# Sticking to 500 for server-side execution errors.
|
279
|
+
return web.json_response(error_resp, status=500)
|
280
|
+
else:
|
281
|
+
return web.Response(status=204) # No response for notification errors
|
282
|
+
except Exception as e:
|
283
|
+
# Unexpected error during method execution
|
284
|
+
log.exception(f"Unexpected error during execution of RPC method '{method_name}':") # Log full traceback
|
285
|
+
if request_id is not None:
|
286
|
+
# Minimize internal details exposed to the client
|
287
|
+
error_resp = {"jsonrpc": "2.0", "error": {"code": -32000, "message": f"Server error: {type(e).__name__}"}, "id": request_id}
|
288
|
+
return web.json_response(error_resp, status=500) # Internal Server Error
|
289
|
+
else:
|
290
|
+
return web.Response(status=204) # No response for notification errors
|
291
|
+
|
292
|
+
except Exception as e:
|
293
|
+
# Catch-all for fatal errors during request handling itself (before/after method call)
|
294
|
+
log.exception("Fatal error in RPC handler:")
|
295
|
+
# ID might be uncertain at this stage, include if available
|
296
|
+
error_resp = {"jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error"}, "id": request_id}
|
297
|
+
return web.json_response(error_resp, status=500)
|
298
|
+
|
299
|
+
async def start_async(self, **kwargs):
|
300
|
+
"""Starts the server asynchronously without blocking."""
|
301
|
+
self._runner = web.AppRunner(self._app, access_log=None, **kwargs)
|
302
|
+
await self._runner.setup()
|
303
|
+
self._site = web.TCPSite(self._runner, self._host, self._port)
|
304
|
+
await self._site.start()
|
305
|
+
log.info(f"RPC server started asynchronously on {self.url}")
|
306
|
+
# 서버가 백그라운드에서 실행되도록 여기서 블로킹하지 않습니다.
|
307
|
+
# 이 코루틴은 서버 시작 후 즉시 반환됩니다.
|
308
|
+
|
309
|
+
async def stop_async(self):
|
310
|
+
"""Stops the server asynchronously."""
|
311
|
+
if self._runner:
|
312
|
+
await self._runner.cleanup()
|
313
|
+
log.info("RPC server stopped.")
|
314
|
+
self._site = None
|
315
|
+
self._runner = None
|
316
|
+
|
317
|
+
def start(self, **kwargs):
|
318
|
+
"""
|
319
|
+
Start the aiohttp web server to listen for RPC requests (blocking).
|
320
|
+
|
321
|
+
This method wraps `aiohttp.web.run_app` and blocks until the server stops.
|
322
|
+
Prefer `start_async` for non-blocking operation within an asyncio event loop.
|
323
|
+
|
324
|
+
Parameters
|
325
|
+
----------
|
326
|
+
**kwargs
|
327
|
+
Additional keyword arguments to pass directly to `aiohttp.web.run_app`.
|
328
|
+
For example, `ssl_context` for HTTPS. By default, suppresses the
|
329
|
+
default `aiohttp` startup message using `print=None`.
|
330
|
+
"""
|
331
|
+
log.info(f"Starting RPC server")
|
332
|
+
# Default to print=None to avoid duplicate startup messages, can be overridden via kwargs
|
333
|
+
run_app_kwargs = {'print': None, 'access_log': None}
|
334
|
+
run_app_kwargs.update(kwargs)
|
335
|
+
try:
|
336
|
+
web.run_app(self._app, host=self._host, port=self._port, **run_app_kwargs)
|
337
|
+
except Exception as e:
|
338
|
+
log.exception(f"Failed to start or run the server: {e}")
|
339
|
+
raise
|
pyloid/store.py
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
from pickledb import PickleDB
|
2
|
+
from typing import Any, List, Optional
|
3
|
+
|
4
|
+
|
5
|
+
class Store:
|
6
|
+
def __init__(self, path: str):
|
7
|
+
"""
|
8
|
+
Initialize a Store instance.
|
9
|
+
|
10
|
+
Parameters
|
11
|
+
----------
|
12
|
+
path: str
|
13
|
+
Path to the database file where data will be stored
|
14
|
+
|
15
|
+
Examples
|
16
|
+
--------
|
17
|
+
>>> store = Store("data.json")
|
18
|
+
"""
|
19
|
+
self.db = PickleDB(path)
|
20
|
+
|
21
|
+
def get(self, key: str) -> Any:
|
22
|
+
"""
|
23
|
+
Retrieve the value associated with the specified key.
|
24
|
+
|
25
|
+
Parameters
|
26
|
+
----------
|
27
|
+
key: str
|
28
|
+
The key to look up in the database
|
29
|
+
|
30
|
+
Returns
|
31
|
+
-------
|
32
|
+
Any
|
33
|
+
The value associated with the key, or None if the key doesn't exist
|
34
|
+
|
35
|
+
Examples
|
36
|
+
--------
|
37
|
+
>>> store = Store("data.json")
|
38
|
+
>>> store.set("user", {"name": "John Doe", "age": 30})
|
39
|
+
True
|
40
|
+
>>> user = store.get("user")
|
41
|
+
>>> print(user)
|
42
|
+
{'name': 'John Doe', 'age': 30}
|
43
|
+
>>> print(store.get("non_existent_key"))
|
44
|
+
None
|
45
|
+
"""
|
46
|
+
return self.db.get(key)
|
47
|
+
|
48
|
+
def set(self, key: str, value: Any) -> bool:
|
49
|
+
"""
|
50
|
+
Add or update a key-value pair in the database.
|
51
|
+
|
52
|
+
Parameters
|
53
|
+
----------
|
54
|
+
key: str
|
55
|
+
The key to set in the database
|
56
|
+
value: Any
|
57
|
+
The value to associate with the key (must be a JSON-serializable Python data type)
|
58
|
+
|
59
|
+
Returns
|
60
|
+
-------
|
61
|
+
bool
|
62
|
+
Always returns True to indicate the operation was performed
|
63
|
+
|
64
|
+
Examples
|
65
|
+
--------
|
66
|
+
>>> store = Store("data.json")
|
67
|
+
>>> store.set("settings", {"theme": "dark", "notifications": True})
|
68
|
+
True
|
69
|
+
>>> store.set("counter", 42)
|
70
|
+
True
|
71
|
+
>>> store.set("items", ["apple", "banana", "orange"])
|
72
|
+
True
|
73
|
+
"""
|
74
|
+
return self.db.set(key, value)
|
75
|
+
|
76
|
+
def remove(self, key: str) -> bool:
|
77
|
+
"""
|
78
|
+
Delete the value associated with the key from the database.
|
79
|
+
|
80
|
+
Parameters
|
81
|
+
----------
|
82
|
+
key: str
|
83
|
+
The key to remove from the database
|
84
|
+
|
85
|
+
Returns
|
86
|
+
-------
|
87
|
+
bool
|
88
|
+
True if the key was deleted, False if the key didn't exist
|
89
|
+
|
90
|
+
Examples
|
91
|
+
--------
|
92
|
+
>>> store = Store("data.json")
|
93
|
+
>>> store.set("temp", "temporary data")
|
94
|
+
True
|
95
|
+
>>> store.remove("temp")
|
96
|
+
True
|
97
|
+
>>> store.remove("non_existent_key")
|
98
|
+
False
|
99
|
+
"""
|
100
|
+
return self.db.remove(key)
|
101
|
+
|
102
|
+
def all(self) -> List[str]:
|
103
|
+
"""
|
104
|
+
Retrieve a list of all keys in the database.
|
105
|
+
|
106
|
+
Returns
|
107
|
+
-------
|
108
|
+
List[str]
|
109
|
+
A list containing all keys currently stored in the database
|
110
|
+
|
111
|
+
Examples
|
112
|
+
--------
|
113
|
+
>>> store = Store("data.json")
|
114
|
+
>>> store.set("key1", "value1")
|
115
|
+
True
|
116
|
+
>>> store.set("key2", "value2")
|
117
|
+
True
|
118
|
+
>>> keys = store.all()
|
119
|
+
>>> print(keys)
|
120
|
+
['key1', 'key2']
|
121
|
+
"""
|
122
|
+
return self.db.all()
|
123
|
+
|
124
|
+
def purge(self) -> bool:
|
125
|
+
"""
|
126
|
+
Clear all keys and values from the database.
|
127
|
+
|
128
|
+
Returns
|
129
|
+
-------
|
130
|
+
bool
|
131
|
+
Always returns True to indicate the operation was performed
|
132
|
+
|
133
|
+
Examples
|
134
|
+
--------
|
135
|
+
>>> store = Store("data.json")
|
136
|
+
>>> store.set("key1", "value1")
|
137
|
+
True
|
138
|
+
>>> store.set("key2", "value2")
|
139
|
+
True
|
140
|
+
>>> store.purge()
|
141
|
+
True
|
142
|
+
>>> print(store.all())
|
143
|
+
[]
|
144
|
+
"""
|
145
|
+
return self.db.purge()
|
146
|
+
|
147
|
+
def save(self, option: Optional[int] = None) -> bool:
|
148
|
+
"""
|
149
|
+
Save the current state of the database to file.
|
150
|
+
|
151
|
+
Parameters
|
152
|
+
----------
|
153
|
+
option: Optional[int]
|
154
|
+
Optional orjson.OPT_* flags that configure serialization behavior.
|
155
|
+
These flags can control formatting, special type handling, etc.
|
156
|
+
|
157
|
+
Returns
|
158
|
+
-------
|
159
|
+
bool
|
160
|
+
True if the operation was successful, False otherwise
|
161
|
+
|
162
|
+
Examples
|
163
|
+
--------
|
164
|
+
>>> store = Store("data.json")
|
165
|
+
>>> store.set("key", "value")
|
166
|
+
True
|
167
|
+
>>> store.save()
|
168
|
+
True
|
169
|
+
"""
|
170
|
+
if option is not None:
|
171
|
+
return self.db.save(option)
|
172
|
+
return self.db.save()
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from PySide6.QtWebEngineCore import QWebEngineUrlRequestInterceptor
|
2
|
+
from PySide6.QtCore import QUrl
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
# interceptor ( all url request )
|
6
|
+
class CustomUrlInterceptor(QWebEngineUrlRequestInterceptor):
|
7
|
+
def __init__(self, rpc_url: Optional[str] = None):
|
8
|
+
super().__init__()
|
9
|
+
self.rpc_url = rpc_url
|
10
|
+
|
11
|
+
def interceptRequest(self, info):
|
12
|
+
host = info.requestUrl().host()
|
13
|
+
url = info.requestUrl().toString()
|
14
|
+
|
15
|
+
server_url = self.rpc_url
|
16
|
+
|
17
|
+
if self.rpc_url is None:
|
18
|
+
return
|
19
|
+
|
20
|
+
if url.startswith(self.rpc_url):
|
21
|
+
return
|
22
|
+
|
23
|
+
if host == "pyloid.rpc":
|
24
|
+
info.redirect(QUrl(server_url))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: pyloid
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.23.0
|
4
4
|
Summary:
|
5
5
|
Author: aesthetics-of-record
|
6
6
|
Author-email: 111675679+aesthetics-of-record@users.noreply.github.com
|
@@ -11,6 +11,9 @@ Classifier: Programming Language :: Python :: 3.10
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
14
|
+
Requires-Dist: aiohttp-cors (>=0.8.1,<0.9.0)
|
15
|
+
Requires-Dist: pickledb (>=1.3.2,<2.0.0)
|
16
|
+
Requires-Dist: platformdirs (>=4.3.7,<5.0.0)
|
14
17
|
Requires-Dist: pyside6 (>=6.8.2.1,<7.0.0.0)
|
15
18
|
Description-Content-Type: text/markdown
|
16
19
|
|
@@ -1,20 +1,23 @@
|
|
1
1
|
pyloid/__init__.py,sha256=4xh7DHBMw2ciDFwI376xcIArhH8GaM4K_NdIa3N0BFo,335
|
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=mQhdMOK7iHSer_rgewDuxEXXF4CrNs4TVt-OQ7YJDTs,99591
|
5
5
|
pyloid/custom/titlebar.py,sha256=itzK9pJbZMQ7BKca9kdbuHMffurrw15UijR6OU03Xsk,3894
|
6
6
|
pyloid/filewatcher.py,sha256=3M5zWVUf1OhlkWJcDFC8ZA9agO4Q-U8WdgGpy6kaVz0,4601
|
7
|
-
pyloid/js_api/base.py,sha256=
|
7
|
+
pyloid/js_api/base.py,sha256=T7znBDwUwGduHEt--F6eKQwdXPcJsMrMjBs7Z1XVrPE,8255
|
8
8
|
pyloid/js_api/event_api.py,sha256=w0z1DcmwcmseqfcoZWgsQmFC2iBCgTMVJubTaHeXI1c,957
|
9
|
-
pyloid/js_api/window_api.py,sha256
|
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=
|
11
|
+
pyloid/pyloid.py,sha256=bsD5KgEIK1uApJQHkobljlY8wBkCTCiebV2mVE6MudM,85566
|
12
|
+
pyloid/rpc.py,sha256=ATGIPZnONRNyVXQN27h4j4dbzWWKvr9tJOwHHs501q0,15115
|
12
13
|
pyloid/serve.py,sha256=wJIBqiLr1-8FvBdV3yybeBtVXsu94FfWYKjHL0eQ68s,1444
|
14
|
+
pyloid/store.py,sha256=p0plJj52hQjjtNMVJhy20eNLXfQ3Qmf7LtGHQk7FiPg,4471
|
13
15
|
pyloid/thread_pool.py,sha256=fKOBb8jMfZn_7crA_fJCno8dObBRZE31EIWaNQ759aw,14616
|
14
16
|
pyloid/timer.py,sha256=RqMsChFUd93cxMVgkHWiIKrci0QDTBgJSTULnAtYT8M,8712
|
15
17
|
pyloid/tray.py,sha256=D12opVEc2wc2T4tK9epaN1oOdeziScsIVNM2uCN7C-A,1710
|
18
|
+
pyloid/url_interceptor.py,sha256=AFjPANDELc9-E-1TnVvkNVc-JZBJYf0677dWQ8LDaqw,726
|
16
19
|
pyloid/utils.py,sha256=e866N9uyAGHTMYsqRYY4JL0AEMRCOiY-k1c1zmEpDA4,4686
|
17
|
-
pyloid-0.
|
18
|
-
pyloid-0.
|
19
|
-
pyloid-0.
|
20
|
-
pyloid-0.
|
20
|
+
pyloid-0.23.0.dist-info/LICENSE,sha256=F96EzotgWhhpnQTW2TcdoqrMDir1jyEo6H915tGQ-QE,11524
|
21
|
+
pyloid-0.23.0.dist-info/METADATA,sha256=jkOxOCerLMGqkVGHmye1m8v-TOSugHqxYycC3C3rTHU,3197
|
22
|
+
pyloid-0.23.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
23
|
+
pyloid-0.23.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|