workers-runtime-sdk 1.6.2__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.
Files changed (27) hide show
  1. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/CHANGELOG.md +26 -0
  2. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/PKG-INFO +1 -1
  3. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/pyproject.toml +1 -1
  4. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/asgi.py +5 -2
  5. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/__init__.py +3 -4
  6. workers_runtime_sdk-1.6.2/src/workers/_workers.py → workers_runtime_sdk-1.6.4/src/workers/entrypoints.py +46 -265
  7. workers_runtime_sdk-1.6.4/src/workers/rpc.py +421 -0
  8. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/uv.lock +1 -1
  9. workers_runtime_sdk-1.6.2/src/workers/rpc.py +0 -208
  10. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/.gitignore +0 -0
  11. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/AGENTS.md +0 -0
  12. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/README.md +0 -0
  13. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/_cloudflare_compat_flags.pyi +0 -0
  14. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/_pyodide_entrypoint_helper.pyi +0 -0
  15. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context.pth +0 -0
  16. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context.py +0 -0
  17. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
  18. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/blob.py +0 -0
  19. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/fetch.py +0 -0
  20. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/formdata.py +0 -0
  21. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/py.typed +0 -0
  22. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/request.py +0 -0
  23. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/response.py +0 -0
  24. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/types.py +0 -0
  25. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/utils.py +0 -0
  26. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/workflows.py +0 -0
  27. {workers_runtime_sdk-1.6.2 → workers_runtime_sdk-1.6.4}/src/workers/wsgi.py +0 -0
@@ -2,6 +2,32 @@
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
+
22
+ ## v1.6.3 (2026-07-17)
23
+
24
+ ### Bug Fixes
25
+
26
+ - Wrap AnalyticsEngine, which is used in production
27
+ ([#155](https://github.com/cloudflare/workers-py/pull/155),
28
+ [`91e4e50`](https://github.com/cloudflare/workers-py/commit/91e4e500ddfe849cae0bd75481e0e62ff01de97d))
29
+
30
+
5
31
  ## v1.6.2 (2026-07-09)
6
32
 
7
33
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workers-runtime-sdk
3
- Version: 1.6.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "workers-runtime-sdk"
7
- version = "1.6.2"
7
+ version = "1.6.4"
8
8
  description = "Python SDK for Cloudflare Workers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
- msg = {"type": "websocket.close", "code": evt.code, "reason": evt.reason}
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.onopen = onclose
314
+ server.onclose = onclose
312
315
  server.onmessage = onmessage
313
316
 
314
317
  async def ws_send(got):
@@ -1,16 +1,15 @@
1
- from ._workers import (
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 python_from_rpc, python_to_rpc
24
- from .utils import (
25
- _JS_PASSTHROUGH_TYPES,
26
- _from_js_error,
27
- _get_js_constructor_name,
28
- _is_js_instance,
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,103 +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
- "AnalyticsEngineDataset",
255
- "LocalAnalyticsEngineDataset",
256
- "ImagesBindingImpl",
257
- "HostedImagesBindingImpl",
258
- "Ratelimit",
259
- }
260
-
261
- __new__ = _idempotent_new
262
-
263
- def __init__(self, env: Any):
264
- if env is self:
265
- return
266
- self._env = env
267
-
268
- def _getattr_helper(self, name):
269
- binding = getattr(self._env, name)
270
- if _is_js_instance(binding, "Fetcher"):
271
- return _FetcherWrapper(binding)
272
-
273
- if _is_js_instance(binding, "DurableObjectNamespace"):
274
- return _DurableObjectNamespaceWrapper(binding)
275
-
276
- if _is_js_instance(binding, "WorkflowImpl"):
277
- return _WorkflowBindingWrapper(binding)
278
-
279
- if _is_js_instance(binding, self._BINDING_TYPES):
280
- return _BindingWrapper(binding)
281
-
282
- return binding
283
-
284
- def __getattr__(self, name):
285
- result = self._getattr_helper(name)
286
- setattr(self, name, result)
287
- return result
288
-
289
-
290
- def handler(func):
291
- """
292
- When applied to handlers such as `on_fetch` it will rewrite arguments passed in to native Python
293
- types defined in this module. For example, the `request` argument to `on_fetch` gets converted
294
- to an instance of the Request class defined in this module.
295
- """
296
-
297
- @functools.wraps(func)
298
- def wrapper(*args, **kwargs):
299
- # TODO: support transforming kwargs
300
- if len(args) > 0 and _is_js_instance(args[0], "Request"):
301
- args = (Request(args[0]), *args[1:])
302
-
303
- # Wrap `env` so that bindings can be used without to_js.
304
- if len(args) > 1:
305
- args = (args[0], _EnvWrapper(args[1]), *args[2:])
306
-
307
- return func(*args, **kwargs)
308
-
309
- return wrapper
310
-
311
-
312
71
  class _WorkflowStepWrapper:
313
72
  __new__ = _idempotent_new
314
73
 
@@ -485,6 +244,28 @@ async def _do_call(entrypoint, name, config, callback, *results):
485
244
  return result
486
245
 
487
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
+
488
269
  def _wrap_class(cls):
489
270
  # Override the class __init__ so that we can wrap the `env` in the constructor.
490
271
  original_init = cls.__dict__.get("__init__")
@@ -533,6 +314,22 @@ def _wrap_workflow_step(cls):
533
314
  cls.run = wrapped_run
534
315
 
535
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
+
536
333
  @_wrap_class
537
334
  class DurableObject:
538
335
  """
@@ -584,19 +381,3 @@ class WorkflowEntrypoint:
584
381
  def __init_subclass__(cls, **_kwargs: Any):
585
382
  _wrap_class(cls)
586
383
  _wrap_workflow_step(cls)
587
-
588
-
589
- def _wrap_queue_handler(cls):
590
- queue_fn = getattr(cls, "queue", None)
591
- if queue_fn is None:
592
- return
593
-
594
- @functools.wraps(queue_fn)
595
- async def wrapped_queue(self, batch, *args, **kwargs):
596
- wrapped_batch = _BindingWrapper(batch)
597
- result = queue_fn(self, wrapped_batch, *args, **kwargs)
598
- if inspect.iscoroutine(result):
599
- result = await result
600
- return result
601
-
602
- 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
@@ -229,7 +229,7 @@ wheels = [
229
229
 
230
230
  [[package]]
231
231
  name = "workers-runtime-sdk"
232
- version = "1.6.2"
232
+ version = "1.6.4"
233
233
  source = { editable = "." }
234
234
 
235
235
  [package.dev-dependencies]
@@ -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