pyloid 0.22.1__py3-none-any.whl → 0.23.1__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 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))
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyloid
3
- Version: 0.22.1
3
+ Version: 0.23.1
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=jPvx1EPq3VAKwtMnr9wL9FrmQJLz9rIUfP4I8B2BKwQ,99056
4
+ pyloid/browser_window.py,sha256=Bf0rtdOsZb6fM55xQkDQQfdDATashnVlRYvGPM93X0M,99696
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=v2Su9errEY4oBRSXmLZps8dn9qCGbEqwp-hs-MduQRc,826
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=YnnniHEBGO1qfyziQiJ5ssYOTUxaHe3Wqj1sUZZdem0,8939
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=KKAdAifbDVo247NNTsvvdGob0mi4QTtfewNFnOZadQo,72964
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.22.1.dist-info/LICENSE,sha256=MTYF-6xpRekyTUglRweWtbfbwBL1I_3Bgfbm_SNOuI8,11525
18
- pyloid-0.22.1.dist-info/METADATA,sha256=Ing9aadKgH6AOvjuRJhc_-P9hpLGOXF8WVZjLU4jt5g,3066
19
- pyloid-0.22.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
20
- pyloid-0.22.1.dist-info/RECORD,,
20
+ pyloid-0.23.1.dist-info/LICENSE,sha256=F96EzotgWhhpnQTW2TcdoqrMDir1jyEo6H915tGQ-QE,11524
21
+ pyloid-0.23.1.dist-info/METADATA,sha256=I6lKViC1XGADQp_MuuZKr86kUEzHsgHU8jjZ7IeAb7k,3197
22
+ pyloid-0.23.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
23
+ pyloid-0.23.1.dist-info/RECORD,,