workers-runtime-sdk 1.6.3__tar.gz → 1.6.4__tar.gz
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.
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/CHANGELOG.md +17 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/PKG-INFO +1 -1
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/pyproject.toml +1 -1
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/asgi.py +5 -2
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/__init__.py +3 -4
- workers_runtime_sdk-1.6.3/src/workers/_workers.py → workers_runtime_sdk-1.6.4/src/workers/entrypoints.py +46 -266
- workers_runtime_sdk-1.6.4/src/workers/rpc.py +421 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/uv.lock +1 -1
- workers_runtime_sdk-1.6.3/src/workers/rpc.py +0 -208
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/.gitignore +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/AGENTS.md +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/README.md +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/_cloudflare_compat_flags.pyi +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/_pyodide_entrypoint_helper.pyi +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context.pth +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/blob.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/fetch.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/formdata.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/py.typed +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/request.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/response.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/types.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/utils.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/workflows.py +0 -0
- {workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/workers/wsgi.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.6.4 (2026-07-30)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **runtime-sdk**: Deliver client WebSocket close events to the ASGI app
|
|
10
|
+
([#158](https://github.com/cloudflare/workers-py/pull/158),
|
|
11
|
+
[`6f53155`](https://github.com/cloudflare/workers-py/commit/6f53155b5620137c749a64dc4ee785505cede74e))
|
|
12
|
+
|
|
13
|
+
- **runtime-sdk**: Register the WebSocket close handler on onclose
|
|
14
|
+
([#158](https://github.com/cloudflare/workers-py/pull/158),
|
|
15
|
+
[`6f53155`](https://github.com/cloudflare/workers-py/commit/6f53155b5620137c749a64dc4ee785505cede74e))
|
|
16
|
+
|
|
17
|
+
- **runtime-sdk**: Surface client closes as websocket.disconnect
|
|
18
|
+
([#158](https://github.com/cloudflare/workers-py/pull/158),
|
|
19
|
+
[`6f53155`](https://github.com/cloudflare/workers-py/commit/6f53155b5620137c749a64dc4ee785505cede74e))
|
|
20
|
+
|
|
21
|
+
|
|
5
22
|
## v1.6.3 (2026-07-17)
|
|
6
23
|
|
|
7
24
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workers-runtime-sdk
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.4
|
|
4
4
|
Summary: Python SDK for Cloudflare Workers
|
|
5
5
|
Project-URL: Homepage, https://github.com/cloudflare/workers-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/cloudflare/workers-py/issues
|
|
@@ -300,7 +300,10 @@ async def process_websocket(app: Any, req: "Request | js.Request") -> js.Respons
|
|
|
300
300
|
onopen(1)
|
|
301
301
|
|
|
302
302
|
def onclose(evt):
|
|
303
|
-
|
|
303
|
+
# Client-initiated closes surface to the app as "websocket.disconnect"
|
|
304
|
+
# per the ASGI spec ("websocket.close" is the app->server direction);
|
|
305
|
+
# frameworks raise their disconnect exceptions only on this type.
|
|
306
|
+
msg = {"type": "websocket.disconnect", "code": evt.code, "reason": evt.reason}
|
|
304
307
|
queue.put_nowait(msg)
|
|
305
308
|
|
|
306
309
|
def onmessage(evt):
|
|
@@ -308,7 +311,7 @@ async def process_websocket(app: Any, req: "Request | js.Request") -> js.Respons
|
|
|
308
311
|
queue.put_nowait(msg)
|
|
309
312
|
|
|
310
313
|
server.onopen = onopen
|
|
311
|
-
server.
|
|
314
|
+
server.onclose = onclose
|
|
312
315
|
server.onmessage = onmessage
|
|
313
316
|
|
|
314
317
|
async def ws_send(got):
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .blob import Blob, BlobEnding, BlobValue, File
|
|
2
|
+
from .entrypoints import (
|
|
2
3
|
DurableObject,
|
|
3
4
|
WorkerEntrypoint,
|
|
4
5
|
WorkflowEntrypoint,
|
|
5
|
-
_EnvWrapper,
|
|
6
6
|
handler,
|
|
7
7
|
)
|
|
8
|
-
from .blob import Blob, BlobEnding, BlobValue, File
|
|
9
8
|
from .fetch import fetch
|
|
10
9
|
from .formdata import FormData, FormDataValue
|
|
11
10
|
from .request import Request
|
|
12
11
|
from .response import FetchResponse, Response
|
|
13
|
-
from .rpc import python_from_rpc, python_to_rpc
|
|
12
|
+
from .rpc import _EnvWrapper, python_from_rpc, python_to_rpc
|
|
14
13
|
from .types import (
|
|
15
14
|
Body,
|
|
16
15
|
Context,
|
|
@@ -1,172 +1,28 @@
|
|
|
1
|
-
# This module defines a Workers API for Python. It is similar to the API provided by
|
|
2
|
-
# JS Workers, but with changes and additions to be more idiomatic to the Python
|
|
3
|
-
# programming language.
|
|
4
1
|
import functools
|
|
5
2
|
import inspect
|
|
6
3
|
from asyncio import create_task, gather
|
|
7
4
|
from typing import TYPE_CHECKING, Any
|
|
8
5
|
|
|
9
6
|
import _cloudflare_compat_flags
|
|
10
|
-
|
|
11
|
-
# Get globals modules and import function from the entrypoint-helper
|
|
12
7
|
import _pyodide_entrypoint_helper
|
|
13
8
|
import js
|
|
14
9
|
from js import Object
|
|
15
|
-
from pyodide.ffi import
|
|
16
|
-
JsProxy,
|
|
17
|
-
create_once_callable,
|
|
18
|
-
to_js,
|
|
19
|
-
)
|
|
10
|
+
from pyodide.ffi import create_once_callable, to_js
|
|
20
11
|
|
|
21
|
-
from .fetch import fetch
|
|
22
12
|
from .request import Request
|
|
23
|
-
from .rpc import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
from .rpc import (
|
|
14
|
+
_BindingWrapper,
|
|
15
|
+
_EnvWrapper,
|
|
16
|
+
_idempotent_new,
|
|
17
|
+
python_from_rpc,
|
|
18
|
+
python_to_rpc,
|
|
29
19
|
)
|
|
20
|
+
from .utils import _from_js_error, _is_js_instance
|
|
30
21
|
|
|
31
22
|
if TYPE_CHECKING:
|
|
32
23
|
from js import DurableObjectState, Env, ExecutionContext
|
|
33
24
|
|
|
34
25
|
|
|
35
|
-
def _idempotent_new(cls, obj):
|
|
36
|
-
"""Set __new__ on a class to this so cls is idempotent:
|
|
37
|
-
|
|
38
|
-
>>> a = A(x)
|
|
39
|
-
>>> b = A(a)
|
|
40
|
-
>>> assert a is b
|
|
41
|
-
|
|
42
|
-
For this to work, start the __init__ function with:
|
|
43
|
-
|
|
44
|
-
if obj is self:
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
to prevent double-init.
|
|
48
|
-
"""
|
|
49
|
-
if isinstance(obj, cls):
|
|
50
|
-
return obj
|
|
51
|
-
return object.__new__(cls)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class _BindingWrapper:
|
|
55
|
-
__new__ = _idempotent_new
|
|
56
|
-
|
|
57
|
-
def __init__(self, binding):
|
|
58
|
-
if binding is self:
|
|
59
|
-
return
|
|
60
|
-
self._binding = binding
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def _real_name(self):
|
|
64
|
-
js_name = _get_js_constructor_name(self._binding)
|
|
65
|
-
if not js_name:
|
|
66
|
-
# Should not happen, but just in case
|
|
67
|
-
return type(self).__name__
|
|
68
|
-
return js_name
|
|
69
|
-
|
|
70
|
-
def _should_wrap_nested_attribute(self, jsobj) -> bool:
|
|
71
|
-
if not isinstance(jsobj, JsProxy):
|
|
72
|
-
return False
|
|
73
|
-
|
|
74
|
-
# TODO: This allowlist approach is a workaround. The long-term fix is to
|
|
75
|
-
# add dedicated Python wrappers for these types in python_from_rpc so they
|
|
76
|
-
# never reach _BindingWrapper in the first place.
|
|
77
|
-
js_type = _get_js_constructor_name(jsobj)
|
|
78
|
-
return js_type and js_type not in _JS_PASSTHROUGH_TYPES
|
|
79
|
-
|
|
80
|
-
def _convert_result(self, result):
|
|
81
|
-
converted = python_from_rpc(result)
|
|
82
|
-
|
|
83
|
-
# After python_from_rpc, some objects may still be JsProxy objects.
|
|
84
|
-
# We need to wrap them with _BindingWrapper (or a subclass of it) again
|
|
85
|
-
# to ensure that accessing attributes on them will be properly converted.
|
|
86
|
-
if self._should_wrap_nested_attribute(converted):
|
|
87
|
-
return self.__class__(converted)
|
|
88
|
-
if isinstance(converted, list):
|
|
89
|
-
return [
|
|
90
|
-
self.__class__(item)
|
|
91
|
-
if self._should_wrap_nested_attribute(item)
|
|
92
|
-
else item
|
|
93
|
-
for item in converted
|
|
94
|
-
]
|
|
95
|
-
return converted
|
|
96
|
-
|
|
97
|
-
@staticmethod
|
|
98
|
-
def _convert_args(args, kwargs):
|
|
99
|
-
js_args = [python_to_rpc(arg) for arg in args]
|
|
100
|
-
js_kwargs = {k: python_to_rpc(v) for k, v in kwargs.items()}
|
|
101
|
-
return js_args, js_kwargs
|
|
102
|
-
|
|
103
|
-
def _getattr_helper(self, name):
|
|
104
|
-
attr = getattr(self._binding, name)
|
|
105
|
-
|
|
106
|
-
if not callable(attr):
|
|
107
|
-
return self._convert_result(attr)
|
|
108
|
-
|
|
109
|
-
def wrapper(*args, **kwargs):
|
|
110
|
-
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
111
|
-
result = attr(*js_args, **js_kwargs)
|
|
112
|
-
if hasattr(result, "then") and callable(result.then):
|
|
113
|
-
|
|
114
|
-
async def await_and_convert():
|
|
115
|
-
return self._convert_result(await result)
|
|
116
|
-
|
|
117
|
-
return await_and_convert()
|
|
118
|
-
return self._convert_result(result)
|
|
119
|
-
|
|
120
|
-
return wrapper
|
|
121
|
-
|
|
122
|
-
def __getattr__(self, name):
|
|
123
|
-
result = self._getattr_helper(name)
|
|
124
|
-
setattr(self, name, result)
|
|
125
|
-
return result
|
|
126
|
-
|
|
127
|
-
def __getitem__(self, key):
|
|
128
|
-
if isinstance(key, int):
|
|
129
|
-
return self._convert_result(self._binding[key])
|
|
130
|
-
return self._convert_result(getattr(self._binding, key))
|
|
131
|
-
|
|
132
|
-
def __iter__(self):
|
|
133
|
-
binding = self._binding
|
|
134
|
-
if not hasattr(binding, "__iter__"):
|
|
135
|
-
raise TypeError(f"'{self._real_name}' object is not iterable")
|
|
136
|
-
for item in binding:
|
|
137
|
-
yield self._convert_result(item)
|
|
138
|
-
|
|
139
|
-
def __len__(self):
|
|
140
|
-
binding = self._binding
|
|
141
|
-
if not hasattr(binding, "length"):
|
|
142
|
-
raise TypeError(f"'{self._real_name}' object has no len()")
|
|
143
|
-
return binding.length
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class _FetcherWrapper(_BindingWrapper):
|
|
147
|
-
def fetch(self, *args, **kwargs):
|
|
148
|
-
return fetch(*args, fetcher=self._binding.fetch, **kwargs)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class _DurableObjectNamespaceWrapper:
|
|
152
|
-
def __init__(self, binding):
|
|
153
|
-
self._binding = binding
|
|
154
|
-
|
|
155
|
-
def __getattr__(self, name):
|
|
156
|
-
return getattr(self._binding, name)
|
|
157
|
-
|
|
158
|
-
def get(self, *args, **kwargs):
|
|
159
|
-
return _FetcherWrapper(self._binding.get(*args, **kwargs))
|
|
160
|
-
|
|
161
|
-
def getByName(self, *args, **kwargs):
|
|
162
|
-
return _FetcherWrapper(self._binding.getByName(*args, **kwargs))
|
|
163
|
-
|
|
164
|
-
def jurisdiction(self, *args, **kwargs):
|
|
165
|
-
return _DurableObjectNamespaceWrapper(
|
|
166
|
-
self._binding.jurisdiction(*args, **kwargs)
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
26
|
class DurableObjectAbort(BaseException):
|
|
171
27
|
pass
|
|
172
28
|
|
|
@@ -212,104 +68,6 @@ class DurableObjectContext:
|
|
|
212
68
|
raise DurableObjectAbort(reason or "Durable Object abort requested")
|
|
213
69
|
|
|
214
70
|
|
|
215
|
-
class _WorkflowInstanceWrapper(_BindingWrapper):
|
|
216
|
-
# status/pause/resume/restart/terminate share their JS names and are handled by
|
|
217
|
-
# the _BindingWrapper, which already converts arguments and results.
|
|
218
|
-
# Only send_event needs the snake_case -> camelCase mapping for backward compatibility
|
|
219
|
-
|
|
220
|
-
async def send_event(self, *args, **kwargs):
|
|
221
|
-
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
222
|
-
return self._convert_result(
|
|
223
|
-
await self._binding.sendEvent(*js_args, **js_kwargs)
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
class _WorkflowBindingWrapper(_BindingWrapper):
|
|
228
|
-
async def get(self, *args, **kwargs):
|
|
229
|
-
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
230
|
-
return _WorkflowInstanceWrapper(await self._binding.get(*js_args, **js_kwargs))
|
|
231
|
-
|
|
232
|
-
async def create(self, *args, **kwargs):
|
|
233
|
-
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
234
|
-
return _WorkflowInstanceWrapper(
|
|
235
|
-
await self._binding.create(*js_args, **js_kwargs)
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
async def create_batch(self, *args, **kwargs):
|
|
239
|
-
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
240
|
-
return [
|
|
241
|
-
_WorkflowInstanceWrapper(w)
|
|
242
|
-
for w in await self._binding.createBatch(*js_args, **js_kwargs)
|
|
243
|
-
]
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
class _EnvWrapper:
|
|
247
|
-
_BINDING_TYPES = {
|
|
248
|
-
"KvNamespace",
|
|
249
|
-
"R2Bucket",
|
|
250
|
-
"D1Database",
|
|
251
|
-
"WorkerQueue",
|
|
252
|
-
"Ai",
|
|
253
|
-
"VectorizeIndexImpl",
|
|
254
|
-
"AnalyticsEngine",
|
|
255
|
-
"AnalyticsEngineDataset",
|
|
256
|
-
"LocalAnalyticsEngineDataset",
|
|
257
|
-
"ImagesBindingImpl",
|
|
258
|
-
"HostedImagesBindingImpl",
|
|
259
|
-
"Ratelimit",
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
__new__ = _idempotent_new
|
|
263
|
-
|
|
264
|
-
def __init__(self, env: Any):
|
|
265
|
-
if env is self:
|
|
266
|
-
return
|
|
267
|
-
self._env = env
|
|
268
|
-
|
|
269
|
-
def _getattr_helper(self, name):
|
|
270
|
-
binding = getattr(self._env, name)
|
|
271
|
-
if _is_js_instance(binding, "Fetcher"):
|
|
272
|
-
return _FetcherWrapper(binding)
|
|
273
|
-
|
|
274
|
-
if _is_js_instance(binding, "DurableObjectNamespace"):
|
|
275
|
-
return _DurableObjectNamespaceWrapper(binding)
|
|
276
|
-
|
|
277
|
-
if _is_js_instance(binding, "WorkflowImpl"):
|
|
278
|
-
return _WorkflowBindingWrapper(binding)
|
|
279
|
-
|
|
280
|
-
if _is_js_instance(binding, self._BINDING_TYPES):
|
|
281
|
-
return _BindingWrapper(binding)
|
|
282
|
-
|
|
283
|
-
return binding
|
|
284
|
-
|
|
285
|
-
def __getattr__(self, name):
|
|
286
|
-
result = self._getattr_helper(name)
|
|
287
|
-
setattr(self, name, result)
|
|
288
|
-
return result
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def handler(func):
|
|
292
|
-
"""
|
|
293
|
-
When applied to handlers such as `on_fetch` it will rewrite arguments passed in to native Python
|
|
294
|
-
types defined in this module. For example, the `request` argument to `on_fetch` gets converted
|
|
295
|
-
to an instance of the Request class defined in this module.
|
|
296
|
-
"""
|
|
297
|
-
|
|
298
|
-
@functools.wraps(func)
|
|
299
|
-
def wrapper(*args, **kwargs):
|
|
300
|
-
# TODO: support transforming kwargs
|
|
301
|
-
if len(args) > 0 and _is_js_instance(args[0], "Request"):
|
|
302
|
-
args = (Request(args[0]), *args[1:])
|
|
303
|
-
|
|
304
|
-
# Wrap `env` so that bindings can be used without to_js.
|
|
305
|
-
if len(args) > 1:
|
|
306
|
-
args = (args[0], _EnvWrapper(args[1]), *args[2:])
|
|
307
|
-
|
|
308
|
-
return func(*args, **kwargs)
|
|
309
|
-
|
|
310
|
-
return wrapper
|
|
311
|
-
|
|
312
|
-
|
|
313
71
|
class _WorkflowStepWrapper:
|
|
314
72
|
__new__ = _idempotent_new
|
|
315
73
|
|
|
@@ -486,6 +244,28 @@ async def _do_call(entrypoint, name, config, callback, *results):
|
|
|
486
244
|
return result
|
|
487
245
|
|
|
488
246
|
|
|
247
|
+
def handler(func):
|
|
248
|
+
"""
|
|
249
|
+
When applied to handlers such as `on_fetch` it will rewrite arguments passed in to native Python
|
|
250
|
+
types defined in this module. For example, the `request` argument to `on_fetch` gets converted
|
|
251
|
+
to an instance of the Request class defined in this module.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
@functools.wraps(func)
|
|
255
|
+
def wrapper(*args, **kwargs):
|
|
256
|
+
# TODO: support transforming kwargs
|
|
257
|
+
if len(args) > 0 and _is_js_instance(args[0], "Request"):
|
|
258
|
+
args = (Request(args[0]), *args[1:])
|
|
259
|
+
|
|
260
|
+
# Wrap `env` so that bindings can be used without to_js.
|
|
261
|
+
if len(args) > 1:
|
|
262
|
+
args = (args[0], _EnvWrapper(args[1]), *args[2:])
|
|
263
|
+
|
|
264
|
+
return func(*args, **kwargs)
|
|
265
|
+
|
|
266
|
+
return wrapper
|
|
267
|
+
|
|
268
|
+
|
|
489
269
|
def _wrap_class(cls):
|
|
490
270
|
# Override the class __init__ so that we can wrap the `env` in the constructor.
|
|
491
271
|
original_init = cls.__dict__.get("__init__")
|
|
@@ -534,6 +314,22 @@ def _wrap_workflow_step(cls):
|
|
|
534
314
|
cls.run = wrapped_run
|
|
535
315
|
|
|
536
316
|
|
|
317
|
+
def _wrap_queue_handler(cls):
|
|
318
|
+
queue_fn = getattr(cls, "queue", None)
|
|
319
|
+
if queue_fn is None:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
@functools.wraps(queue_fn)
|
|
323
|
+
async def wrapped_queue(self, batch, *args, **kwargs):
|
|
324
|
+
wrapped_batch = _BindingWrapper(batch)
|
|
325
|
+
result = queue_fn(self, wrapped_batch, *args, **kwargs)
|
|
326
|
+
if inspect.iscoroutine(result):
|
|
327
|
+
result = await result
|
|
328
|
+
return result
|
|
329
|
+
|
|
330
|
+
cls.queue = wrapped_queue
|
|
331
|
+
|
|
332
|
+
|
|
537
333
|
@_wrap_class
|
|
538
334
|
class DurableObject:
|
|
539
335
|
"""
|
|
@@ -585,19 +381,3 @@ class WorkflowEntrypoint:
|
|
|
585
381
|
def __init_subclass__(cls, **_kwargs: Any):
|
|
586
382
|
_wrap_class(cls)
|
|
587
383
|
_wrap_workflow_step(cls)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def _wrap_queue_handler(cls):
|
|
591
|
-
queue_fn = getattr(cls, "queue", None)
|
|
592
|
-
if queue_fn is None:
|
|
593
|
-
return
|
|
594
|
-
|
|
595
|
-
@functools.wraps(queue_fn)
|
|
596
|
-
async def wrapped_queue(self, batch, *args, **kwargs):
|
|
597
|
-
wrapped_batch = _BindingWrapper(batch)
|
|
598
|
-
result = queue_fn(self, wrapped_batch, *args, **kwargs)
|
|
599
|
-
if inspect.iscoroutine(result):
|
|
600
|
-
result = await result
|
|
601
|
-
return result
|
|
602
|
-
|
|
603
|
-
cls.queue = wrapped_queue
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
import js
|
|
5
|
+
from js import Object
|
|
6
|
+
from pyodide.ffi import JsProxy, create_proxy, to_js
|
|
7
|
+
|
|
8
|
+
from .blob import Blob, File
|
|
9
|
+
from .fetch import fetch
|
|
10
|
+
from .formdata import FormData
|
|
11
|
+
from .request import Request
|
|
12
|
+
from .response import Response
|
|
13
|
+
from .utils import (
|
|
14
|
+
_JS_PASSTHROUGH_TYPES,
|
|
15
|
+
_get_js_constructor_name,
|
|
16
|
+
_is_iterable,
|
|
17
|
+
_is_js_instance,
|
|
18
|
+
jsnull,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _python_from_rpc_default_converter(value, convert, cache):
|
|
23
|
+
if value is jsnull:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
if not hasattr(value, "constructor"):
|
|
27
|
+
# Assume that the object doesn't need conversion as it's not a JS object.
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
if value.constructor.name == "Response":
|
|
31
|
+
return Response(value)
|
|
32
|
+
elif value.constructor.name == "FormData":
|
|
33
|
+
return FormData(value)
|
|
34
|
+
elif value.constructor.name == "Blob":
|
|
35
|
+
return Blob(value)
|
|
36
|
+
elif value.constructor.name == "File":
|
|
37
|
+
return File(value)
|
|
38
|
+
elif value.constructor.name == "Request":
|
|
39
|
+
return Request(value)
|
|
40
|
+
elif value.constructor.name == "Date":
|
|
41
|
+
# TODO: Pyodide should gain support for this, we should upstream this.
|
|
42
|
+
return datetime.datetime.fromtimestamp(value.getTime() / 1000)
|
|
43
|
+
elif value.constructor.name == "Error":
|
|
44
|
+
return Exception(value.toString())
|
|
45
|
+
elif value.constructor.name == "Number":
|
|
46
|
+
return value.valueOf()
|
|
47
|
+
|
|
48
|
+
# We used to throw an error here, but since these conversions are now automatic when the default
|
|
49
|
+
# entrypoint is being used, it makes sense to be less loud about it and just pass through the
|
|
50
|
+
# JS value un-modified.
|
|
51
|
+
#
|
|
52
|
+
# This does mean that in the future we need to be careful when adding type wrappers for new
|
|
53
|
+
# types here, so if you're doing this make sure to do so behind a compat flag.
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class JsDict(dict):
|
|
58
|
+
"""
|
|
59
|
+
Python dictionary that allows attribute access to keys.
|
|
60
|
+
|
|
61
|
+
This is used to convert JS objects to Python dictionaries while maintaining
|
|
62
|
+
the ability to access keys as attributes.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __getattr__(self, name):
|
|
66
|
+
# The limitation of this approach is that if there is a key that conflicts with a built-in
|
|
67
|
+
# method or attribute of the dict class, it will not be accessible through attribute access.
|
|
68
|
+
# But that is a reasonable trade-off for the convenience of being able to access keys as
|
|
69
|
+
# attributes.
|
|
70
|
+
try:
|
|
71
|
+
return self[name]
|
|
72
|
+
except KeyError:
|
|
73
|
+
raise AttributeError(name) from None
|
|
74
|
+
|
|
75
|
+
def __setattr__(self, name, value):
|
|
76
|
+
self[name] = value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _replace_jsnull_with_none(obj):
|
|
80
|
+
"""
|
|
81
|
+
Recursively converts JS objects to Python objects.
|
|
82
|
+
"""
|
|
83
|
+
if obj is jsnull:
|
|
84
|
+
return None
|
|
85
|
+
if isinstance(obj, dict):
|
|
86
|
+
return JsDict({k: _replace_jsnull_with_none(v) for k, v in obj.items()})
|
|
87
|
+
if isinstance(obj, list):
|
|
88
|
+
return [_replace_jsnull_with_none(v) for v in obj]
|
|
89
|
+
return obj
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def python_from_rpc(obj: "JsProxy"):
|
|
93
|
+
"""
|
|
94
|
+
Converts JS objects like Response, Request, Blob, etc. to equivalent Python objects defined in
|
|
95
|
+
this module and also other JS objects like Map, Set, etc. to equivalent Python stdlib objects.
|
|
96
|
+
|
|
97
|
+
This method is used for Workers RPC in Python to convert JavaScript objects to Python. As such
|
|
98
|
+
it does not support serializing all JS object types.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
if obj is jsnull:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
if not hasattr(obj, "constructor"):
|
|
105
|
+
return obj
|
|
106
|
+
|
|
107
|
+
if obj.constructor.name == "TestController":
|
|
108
|
+
# This object currently has no methods defined on it. If this changes we should
|
|
109
|
+
# implement a Python wrapper for it, but for now we'll just pass in None.
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
result = obj.to_py(default_converter=_python_from_rpc_default_converter)
|
|
113
|
+
|
|
114
|
+
return _replace_jsnull_with_none(result)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _raise_on_disabled_type(value):
|
|
118
|
+
if isinstance(value, _BindingWrapper):
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
if callable(value) and not isinstance(value, type):
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
if _is_js_instance(value, "RegExp"):
|
|
125
|
+
raise TypeError(f"{value.constructor.name} cannot be sent over RPC.")
|
|
126
|
+
|
|
127
|
+
if isinstance(value, (tuple, bytearray)):
|
|
128
|
+
raise TypeError(f"{type(value)} cannot be sent over RPC.")
|
|
129
|
+
|
|
130
|
+
if inspect.isawaitable(value):
|
|
131
|
+
# The caller is expected to await the value prior to conversion.
|
|
132
|
+
raise TypeError(f"Awaitable {type(value)} cannot be sent over RPC.")
|
|
133
|
+
|
|
134
|
+
if _is_iterable(value):
|
|
135
|
+
if isinstance(value, dict):
|
|
136
|
+
for v in value.values():
|
|
137
|
+
_raise_on_disabled_type(v)
|
|
138
|
+
else:
|
|
139
|
+
for v in value:
|
|
140
|
+
_raise_on_disabled_type(v)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _python_to_rpc_default_converter(obj, convert, cache):
|
|
144
|
+
if obj is None:
|
|
145
|
+
return jsnull
|
|
146
|
+
|
|
147
|
+
if isinstance(obj, _BindingWrapper):
|
|
148
|
+
return obj._binding
|
|
149
|
+
|
|
150
|
+
if hasattr(obj, "js_object"):
|
|
151
|
+
return obj.js_object
|
|
152
|
+
|
|
153
|
+
if isinstance(obj, datetime.datetime):
|
|
154
|
+
# TODO: Pyodide should gain support for this, we should upstream this.
|
|
155
|
+
return js.Date.new(obj.timestamp() * 1000)
|
|
156
|
+
|
|
157
|
+
if isinstance(obj, Exception):
|
|
158
|
+
return js.Error.new(str(obj))
|
|
159
|
+
|
|
160
|
+
if callable(obj) and not isinstance(obj, type):
|
|
161
|
+
# Wrap function with create_proxy so that
|
|
162
|
+
# it doesn't get garbage collected
|
|
163
|
+
return create_proxy(obj)
|
|
164
|
+
|
|
165
|
+
_raise_on_disabled_type(obj)
|
|
166
|
+
|
|
167
|
+
return obj
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _replace_none_with_jsnull(value):
|
|
171
|
+
if value is None:
|
|
172
|
+
return jsnull
|
|
173
|
+
if isinstance(value, dict):
|
|
174
|
+
return {k: _replace_none_with_jsnull(v) for k, v in value.items()}
|
|
175
|
+
if isinstance(value, list):
|
|
176
|
+
return [_replace_none_with_jsnull(v) for v in value]
|
|
177
|
+
return value
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def python_to_rpc(value) -> JsProxy:
|
|
181
|
+
"""
|
|
182
|
+
Converts Python objects defined in this module (Response, Request, etc) and native Python types
|
|
183
|
+
like Map, Set, datetime to equivalent JavaScript types.
|
|
184
|
+
|
|
185
|
+
This method is used for Workers RPC in Python to convert Python objects to JavaScript. As such
|
|
186
|
+
it does not support serializing all Python object types.
|
|
187
|
+
"""
|
|
188
|
+
if value is None:
|
|
189
|
+
return jsnull
|
|
190
|
+
|
|
191
|
+
if isinstance(value, _BindingWrapper):
|
|
192
|
+
return value._binding
|
|
193
|
+
|
|
194
|
+
value = _replace_none_with_jsnull(value)
|
|
195
|
+
|
|
196
|
+
# `to_js` won't always call the default_converter, for example when a list of tuples is passed
|
|
197
|
+
_raise_on_disabled_type(value)
|
|
198
|
+
|
|
199
|
+
result = to_js(
|
|
200
|
+
value,
|
|
201
|
+
default_converter=_python_to_rpc_default_converter,
|
|
202
|
+
dict_converter=Object.fromEntries,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Binding wrappers
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _idempotent_new(cls, obj):
|
|
214
|
+
"""Set __new__ on a class to this so cls is idempotent:
|
|
215
|
+
|
|
216
|
+
>>> a = A(x)
|
|
217
|
+
>>> b = A(a)
|
|
218
|
+
>>> assert a is b
|
|
219
|
+
|
|
220
|
+
For this to work, start the __init__ function with:
|
|
221
|
+
|
|
222
|
+
if obj is self:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
to prevent double-init.
|
|
226
|
+
"""
|
|
227
|
+
if isinstance(obj, cls):
|
|
228
|
+
return obj
|
|
229
|
+
return object.__new__(cls)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class _BindingWrapper:
|
|
233
|
+
__new__ = _idempotent_new
|
|
234
|
+
|
|
235
|
+
def __init__(self, binding):
|
|
236
|
+
if binding is self:
|
|
237
|
+
return
|
|
238
|
+
self._binding = binding
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def _real_name(self):
|
|
242
|
+
js_name = _get_js_constructor_name(self._binding)
|
|
243
|
+
if not js_name:
|
|
244
|
+
# Should not happen, but just in case
|
|
245
|
+
return type(self).__name__
|
|
246
|
+
return js_name
|
|
247
|
+
|
|
248
|
+
def _should_wrap_nested_attribute(self, jsobj) -> bool:
|
|
249
|
+
if not isinstance(jsobj, JsProxy):
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
# TODO: This allowlist approach is a workaround. The long-term fix is to
|
|
253
|
+
# add dedicated Python wrappers for these types in python_from_rpc so they
|
|
254
|
+
# never reach _BindingWrapper in the first place.
|
|
255
|
+
js_type = _get_js_constructor_name(jsobj)
|
|
256
|
+
return js_type and js_type not in _JS_PASSTHROUGH_TYPES
|
|
257
|
+
|
|
258
|
+
def _convert_result(self, result):
|
|
259
|
+
converted = python_from_rpc(result)
|
|
260
|
+
|
|
261
|
+
# After python_from_rpc, some objects may still be JsProxy objects.
|
|
262
|
+
# We need to wrap them with _BindingWrapper (or a subclass of it) again
|
|
263
|
+
# to ensure that accessing attributes on them will be properly converted.
|
|
264
|
+
if self._should_wrap_nested_attribute(converted):
|
|
265
|
+
return self.__class__(converted)
|
|
266
|
+
if isinstance(converted, list):
|
|
267
|
+
return [
|
|
268
|
+
self.__class__(item)
|
|
269
|
+
if self._should_wrap_nested_attribute(item)
|
|
270
|
+
else item
|
|
271
|
+
for item in converted
|
|
272
|
+
]
|
|
273
|
+
return converted
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def _convert_args(args, kwargs):
|
|
277
|
+
js_args = [python_to_rpc(arg) for arg in args]
|
|
278
|
+
js_kwargs = {k: python_to_rpc(v) for k, v in kwargs.items()}
|
|
279
|
+
return js_args, js_kwargs
|
|
280
|
+
|
|
281
|
+
def _getattr_helper(self, name):
|
|
282
|
+
attr = getattr(self._binding, name)
|
|
283
|
+
|
|
284
|
+
if not callable(attr):
|
|
285
|
+
return self._convert_result(attr)
|
|
286
|
+
|
|
287
|
+
def wrapper(*args, **kwargs):
|
|
288
|
+
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
289
|
+
result = attr(*js_args, **js_kwargs)
|
|
290
|
+
if hasattr(result, "then") and callable(result.then):
|
|
291
|
+
|
|
292
|
+
async def await_and_convert():
|
|
293
|
+
return self._convert_result(await result)
|
|
294
|
+
|
|
295
|
+
return await_and_convert()
|
|
296
|
+
return self._convert_result(result)
|
|
297
|
+
|
|
298
|
+
return wrapper
|
|
299
|
+
|
|
300
|
+
def __getattr__(self, name):
|
|
301
|
+
result = self._getattr_helper(name)
|
|
302
|
+
setattr(self, name, result)
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
def __getitem__(self, key):
|
|
306
|
+
if isinstance(key, int):
|
|
307
|
+
return self._convert_result(self._binding[key])
|
|
308
|
+
return self._convert_result(getattr(self._binding, key))
|
|
309
|
+
|
|
310
|
+
def __iter__(self):
|
|
311
|
+
binding = self._binding
|
|
312
|
+
if not hasattr(binding, "__iter__"):
|
|
313
|
+
raise TypeError(f"'{self._real_name}' object is not iterable")
|
|
314
|
+
for item in binding:
|
|
315
|
+
yield self._convert_result(item)
|
|
316
|
+
|
|
317
|
+
def __len__(self):
|
|
318
|
+
binding = self._binding
|
|
319
|
+
if not hasattr(binding, "length"):
|
|
320
|
+
raise TypeError(f"'{self._real_name}' object has no len()")
|
|
321
|
+
return binding.length
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class _FetcherWrapper(_BindingWrapper):
|
|
325
|
+
def fetch(self, *args, **kwargs):
|
|
326
|
+
return fetch(*args, fetcher=self._binding.fetch, **kwargs)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class _DurableObjectNamespaceWrapper:
|
|
330
|
+
def __init__(self, binding):
|
|
331
|
+
self._binding = binding
|
|
332
|
+
|
|
333
|
+
def __getattr__(self, name):
|
|
334
|
+
return getattr(self._binding, name)
|
|
335
|
+
|
|
336
|
+
def get(self, *args, **kwargs):
|
|
337
|
+
return _FetcherWrapper(self._binding.get(*args, **kwargs))
|
|
338
|
+
|
|
339
|
+
def getByName(self, *args, **kwargs):
|
|
340
|
+
return _FetcherWrapper(self._binding.getByName(*args, **kwargs))
|
|
341
|
+
|
|
342
|
+
def jurisdiction(self, *args, **kwargs):
|
|
343
|
+
return _DurableObjectNamespaceWrapper(
|
|
344
|
+
self._binding.jurisdiction(*args, **kwargs)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class _WorkflowInstanceWrapper(_BindingWrapper):
|
|
349
|
+
# status/pause/resume/restart/terminate share their JS names and are handled by
|
|
350
|
+
# the _BindingWrapper, which already converts arguments and results.
|
|
351
|
+
# Only send_event needs the snake_case -> camelCase mapping for backward compatibility
|
|
352
|
+
|
|
353
|
+
async def send_event(self, *args, **kwargs):
|
|
354
|
+
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
355
|
+
return self._convert_result(
|
|
356
|
+
await self._binding.sendEvent(*js_args, **js_kwargs)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class _WorkflowBindingWrapper(_BindingWrapper):
|
|
361
|
+
async def get(self, *args, **kwargs):
|
|
362
|
+
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
363
|
+
return _WorkflowInstanceWrapper(await self._binding.get(*js_args, **js_kwargs))
|
|
364
|
+
|
|
365
|
+
async def create(self, *args, **kwargs):
|
|
366
|
+
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
367
|
+
return _WorkflowInstanceWrapper(
|
|
368
|
+
await self._binding.create(*js_args, **js_kwargs)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def create_batch(self, *args, **kwargs):
|
|
372
|
+
js_args, js_kwargs = self._convert_args(args, kwargs)
|
|
373
|
+
return [
|
|
374
|
+
_WorkflowInstanceWrapper(w)
|
|
375
|
+
for w in await self._binding.createBatch(*js_args, **js_kwargs)
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class _EnvWrapper:
|
|
380
|
+
_BINDING_TYPES = {
|
|
381
|
+
"KvNamespace",
|
|
382
|
+
"R2Bucket",
|
|
383
|
+
"D1Database",
|
|
384
|
+
"WorkerQueue",
|
|
385
|
+
"Ai",
|
|
386
|
+
"VectorizeIndexImpl",
|
|
387
|
+
"AnalyticsEngine",
|
|
388
|
+
"AnalyticsEngineDataset",
|
|
389
|
+
"LocalAnalyticsEngineDataset",
|
|
390
|
+
"ImagesBindingImpl",
|
|
391
|
+
"HostedImagesBindingImpl",
|
|
392
|
+
"Ratelimit",
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
__new__ = _idempotent_new
|
|
396
|
+
|
|
397
|
+
def __init__(self, env):
|
|
398
|
+
if env is self:
|
|
399
|
+
return
|
|
400
|
+
self._env = env
|
|
401
|
+
|
|
402
|
+
def _getattr_helper(self, name):
|
|
403
|
+
binding = getattr(self._env, name)
|
|
404
|
+
if _is_js_instance(binding, "Fetcher"):
|
|
405
|
+
return _FetcherWrapper(binding)
|
|
406
|
+
|
|
407
|
+
if _is_js_instance(binding, "DurableObjectNamespace"):
|
|
408
|
+
return _DurableObjectNamespaceWrapper(binding)
|
|
409
|
+
|
|
410
|
+
if _is_js_instance(binding, "WorkflowImpl"):
|
|
411
|
+
return _WorkflowBindingWrapper(binding)
|
|
412
|
+
|
|
413
|
+
if _is_js_instance(binding, self._BINDING_TYPES):
|
|
414
|
+
return _BindingWrapper(binding)
|
|
415
|
+
|
|
416
|
+
return binding
|
|
417
|
+
|
|
418
|
+
def __getattr__(self, name):
|
|
419
|
+
result = self._getattr_helper(name)
|
|
420
|
+
setattr(self, name, result)
|
|
421
|
+
return result
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import inspect
|
|
3
|
-
|
|
4
|
-
import js
|
|
5
|
-
from js import Object
|
|
6
|
-
from pyodide.ffi import JsProxy, create_proxy, to_js
|
|
7
|
-
|
|
8
|
-
from .blob import Blob, File
|
|
9
|
-
from .formdata import FormData
|
|
10
|
-
from .request import Request
|
|
11
|
-
from .response import Response
|
|
12
|
-
from .utils import _is_iterable, _is_js_instance, jsnull
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _python_from_rpc_default_converter(value, convert, cache):
|
|
16
|
-
if value is jsnull:
|
|
17
|
-
return None
|
|
18
|
-
|
|
19
|
-
if not hasattr(value, "constructor"):
|
|
20
|
-
# Assume that the object doesn't need conversion as it's not a JS object.
|
|
21
|
-
return value
|
|
22
|
-
|
|
23
|
-
if value.constructor.name == "Response":
|
|
24
|
-
return Response(value)
|
|
25
|
-
elif value.constructor.name == "FormData":
|
|
26
|
-
return FormData(value)
|
|
27
|
-
elif value.constructor.name == "Blob":
|
|
28
|
-
return Blob(value)
|
|
29
|
-
elif value.constructor.name == "File":
|
|
30
|
-
return File(value)
|
|
31
|
-
elif value.constructor.name == "Request":
|
|
32
|
-
return Request(value)
|
|
33
|
-
elif value.constructor.name == "Date":
|
|
34
|
-
# TODO: Pyodide should gain support for this, we should upstream this.
|
|
35
|
-
return datetime.datetime.fromtimestamp(value.getTime() / 1000)
|
|
36
|
-
elif value.constructor.name == "Error":
|
|
37
|
-
return Exception(value.toString())
|
|
38
|
-
elif value.constructor.name == "Number":
|
|
39
|
-
return value.valueOf()
|
|
40
|
-
|
|
41
|
-
# We used to throw an error here, but since these conversions are now automatic when the default
|
|
42
|
-
# entrypoint is being used, it makes sense to be less loud about it and just pass through the
|
|
43
|
-
# JS value un-modified.
|
|
44
|
-
#
|
|
45
|
-
# This does mean that in the future we need to be careful when adding type wrappers for new
|
|
46
|
-
# types here, so if you're doing this make sure to do so behind a compat flag.
|
|
47
|
-
return value
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class JsDict(dict):
|
|
51
|
-
"""
|
|
52
|
-
Python dictionary that allows attribute access to keys.
|
|
53
|
-
|
|
54
|
-
This is used to convert JS objects to Python dictionaries while maintaining
|
|
55
|
-
the ability to access keys as attributes.
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
def __getattr__(self, name):
|
|
59
|
-
# The limitation of this approach is that if there is a key that conflicts with a built-in
|
|
60
|
-
# method or attribute of the dict class, it will not be accessible through attribute access.
|
|
61
|
-
# But that is a reasonable trade-off for the convenience of being able to access keys as
|
|
62
|
-
# attributes.
|
|
63
|
-
try:
|
|
64
|
-
return self[name]
|
|
65
|
-
except KeyError:
|
|
66
|
-
raise AttributeError(name) from None
|
|
67
|
-
|
|
68
|
-
def __setattr__(self, name, value):
|
|
69
|
-
self[name] = value
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _replace_jsnull_with_none(obj):
|
|
73
|
-
"""
|
|
74
|
-
Recursively converts JS objects to Python objects.
|
|
75
|
-
"""
|
|
76
|
-
if obj is jsnull:
|
|
77
|
-
return None
|
|
78
|
-
if isinstance(obj, dict):
|
|
79
|
-
return JsDict({k: _replace_jsnull_with_none(v) for k, v in obj.items()})
|
|
80
|
-
if isinstance(obj, list):
|
|
81
|
-
return [_replace_jsnull_with_none(v) for v in obj]
|
|
82
|
-
return obj
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def python_from_rpc(obj: "JsProxy"):
|
|
86
|
-
"""
|
|
87
|
-
Converts JS objects like Response, Request, Blob, etc. to equivalent Python objects defined in
|
|
88
|
-
this module and also other JS objects like Map, Set, etc. to equivalent Python stdlib objects.
|
|
89
|
-
|
|
90
|
-
This method is used for Workers RPC in Python to convert JavaScript objects to Python. As such
|
|
91
|
-
it does not support serializing all JS object types.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
if obj is jsnull:
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
if not hasattr(obj, "constructor"):
|
|
98
|
-
return obj
|
|
99
|
-
|
|
100
|
-
if obj.constructor.name == "TestController":
|
|
101
|
-
# This object currently has no methods defined on it. If this changes we should
|
|
102
|
-
# implement a Python wrapper for it, but for now we'll just pass in None.
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
result = obj.to_py(default_converter=_python_from_rpc_default_converter)
|
|
106
|
-
|
|
107
|
-
return _replace_jsnull_with_none(result)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _raise_on_disabled_type(value):
|
|
111
|
-
# Lazy import: _BindingWrapper is defined in _workers.py
|
|
112
|
-
from ._workers import _BindingWrapper
|
|
113
|
-
|
|
114
|
-
if isinstance(value, _BindingWrapper):
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
if callable(value) and not isinstance(value, type):
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
if _is_js_instance(value, "RegExp"):
|
|
121
|
-
raise TypeError(f"{value.constructor.name} cannot be sent over RPC.")
|
|
122
|
-
|
|
123
|
-
if isinstance(value, (tuple, bytearray)):
|
|
124
|
-
raise TypeError(f"{type(value)} cannot be sent over RPC.")
|
|
125
|
-
|
|
126
|
-
if inspect.isawaitable(value):
|
|
127
|
-
# The caller is expected to await the value prior to conversion.
|
|
128
|
-
raise TypeError(f"Awaitable {type(value)} cannot be sent over RPC.")
|
|
129
|
-
|
|
130
|
-
if _is_iterable(value):
|
|
131
|
-
if isinstance(value, dict):
|
|
132
|
-
for v in value.values():
|
|
133
|
-
_raise_on_disabled_type(v)
|
|
134
|
-
else:
|
|
135
|
-
for v in value:
|
|
136
|
-
_raise_on_disabled_type(v)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _python_to_rpc_default_converter(obj, convert, cache):
|
|
140
|
-
# Lazy import: _BindingWrapper is defined in _workers.py
|
|
141
|
-
from ._workers import _BindingWrapper
|
|
142
|
-
|
|
143
|
-
if obj is None:
|
|
144
|
-
return jsnull
|
|
145
|
-
|
|
146
|
-
if isinstance(obj, _BindingWrapper):
|
|
147
|
-
return obj._binding
|
|
148
|
-
|
|
149
|
-
if hasattr(obj, "js_object"):
|
|
150
|
-
return obj.js_object
|
|
151
|
-
|
|
152
|
-
if isinstance(obj, datetime.datetime):
|
|
153
|
-
# TODO: Pyodide should gain support for this, we should upstream this.
|
|
154
|
-
return js.Date.new(obj.timestamp() * 1000)
|
|
155
|
-
|
|
156
|
-
if isinstance(obj, Exception):
|
|
157
|
-
return js.Error.new(str(obj))
|
|
158
|
-
|
|
159
|
-
if callable(obj) and not isinstance(obj, type):
|
|
160
|
-
# Wrap function with create_proxy so that
|
|
161
|
-
# it doesn't get garbage collected
|
|
162
|
-
return create_proxy(obj)
|
|
163
|
-
|
|
164
|
-
_raise_on_disabled_type(obj)
|
|
165
|
-
|
|
166
|
-
return obj
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _replace_none_with_jsnull(value):
|
|
170
|
-
if value is None:
|
|
171
|
-
return jsnull
|
|
172
|
-
if isinstance(value, dict):
|
|
173
|
-
return {k: _replace_none_with_jsnull(v) for k, v in value.items()}
|
|
174
|
-
if isinstance(value, list):
|
|
175
|
-
return [_replace_none_with_jsnull(v) for v in value]
|
|
176
|
-
return value
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def python_to_rpc(value) -> JsProxy:
|
|
180
|
-
"""
|
|
181
|
-
Converts Python objects defined in this module (Response, Request, etc) and native Python types
|
|
182
|
-
like Map, Set, datetime to equivalent JavaScript types.
|
|
183
|
-
|
|
184
|
-
This method is used for Workers RPC in Python to convert Python objects to JavaScript. As such
|
|
185
|
-
it does not support serializing all Python object types.
|
|
186
|
-
"""
|
|
187
|
-
# Lazy import: _BindingWrapper is defined in _workers.py
|
|
188
|
-
# TODO: refactor more to avoid circular imports
|
|
189
|
-
from ._workers import _BindingWrapper
|
|
190
|
-
|
|
191
|
-
if value is None:
|
|
192
|
-
return jsnull
|
|
193
|
-
|
|
194
|
-
if isinstance(value, _BindingWrapper):
|
|
195
|
-
return value._binding
|
|
196
|
-
|
|
197
|
-
value = _replace_none_with_jsnull(value)
|
|
198
|
-
|
|
199
|
-
# `to_js` won't always call the default_converter, for example when a list of tuples is passed
|
|
200
|
-
_raise_on_disabled_type(value)
|
|
201
|
-
|
|
202
|
-
result = to_js(
|
|
203
|
-
value,
|
|
204
|
-
default_converter=_python_to_rpc_default_converter,
|
|
205
|
-
dict_converter=Object.fromEntries,
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context.pth
RENAMED
|
File without changes
|
{workers_runtime_sdk-1.6.3 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|