workers-runtime-sdk 1.5.1__tar.gz → 1.5.3__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 (26) hide show
  1. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/CHANGELOG.md +18 -0
  2. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/PKG-INFO +1 -1
  3. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/pyproject.toml +7 -2
  4. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/workers/__init__.py +14 -19
  5. workers_runtime_sdk-1.5.3/src/workers/_workers.py +602 -0
  6. workers_runtime_sdk-1.5.3/src/workers/blob.py +154 -0
  7. workers_runtime_sdk-1.5.3/src/workers/fetch.py +44 -0
  8. workers_runtime_sdk-1.5.3/src/workers/formdata.py +97 -0
  9. workers_runtime_sdk-1.5.3/src/workers/request.py +175 -0
  10. workers_runtime_sdk-1.5.3/src/workers/response.py +216 -0
  11. workers_runtime_sdk-1.5.3/src/workers/rpc.py +208 -0
  12. workers_runtime_sdk-1.5.3/src/workers/types.py +49 -0
  13. workers_runtime_sdk-1.5.3/src/workers/utils.py +246 -0
  14. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/uv.lock +1 -1
  15. workers_runtime_sdk-1.5.1/src/workers/_workers.py +0 -1685
  16. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/.gitignore +0 -0
  17. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/AGENTS.md +0 -0
  18. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/README.md +0 -0
  19. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_cloudflare_compat_flags.pyi +0 -0
  20. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_pyodide_entrypoint_helper.pyi +0 -0
  21. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_workers_sdk_entropy_import_context.pth +0 -0
  22. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_workers_sdk_entropy_import_context.py +0 -0
  23. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
  24. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/asgi.py +0 -0
  25. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/workers/py.typed +0 -0
  26. {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/workers/workflows.py +0 -0
@@ -2,6 +2,24 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.5.3 (2026-07-03)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Update Workflows wrapper to work more natively with Python objects
10
+ ([#138](https://github.com/cloudflare/workers-py/pull/138),
11
+ [`63ea6a0`](https://github.com/cloudflare/workers-py/commit/63ea6a0842875e04f3883bd050a097a3ef7152bd))
12
+
13
+
14
+ ## v1.5.2 (2026-07-01)
15
+
16
+ ### Bug Fixes
17
+
18
+ - Ensure that ctx and env __init__ arguments are always wrapped
19
+ ([#131](https://github.com/cloudflare/workers-py/pull/131),
20
+ [`465c702`](https://github.com/cloudflare/workers-py/commit/465c7029d7b7d5ca75afb1648d9a96433a8a9a13))
21
+
22
+
5
23
  ## v1.5.1 (2026-06-29)
6
24
 
7
25
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workers-runtime-sdk
3
- Version: 1.5.1
3
+ Version: 1.5.3
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.5.1"
7
+ version = "1.5.3"
8
8
  description = "Python SDK for Cloudflare Workers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -88,7 +88,12 @@ allow_zero_version = false
88
88
  no_git_verify = false
89
89
  tag_format = "workers-runtime-sdk-v{version}"
90
90
  version_toml = ["pyproject.toml:project.version"]
91
- build_command = "pip install uv && uv build"
91
+ build_command = """
92
+ pip install uv
93
+ uv lock --upgrade-package "$PACKAGE_NAME"
94
+ git add uv.lock
95
+ uv build
96
+ """
92
97
 
93
98
  [tool.semantic_release.branches.main]
94
99
  match = "(main|master)"
@@ -1,30 +1,25 @@
1
1
  from ._workers import (
2
- Blob,
3
- BlobEnding,
4
- BlobValue,
2
+ DurableObject,
3
+ WorkerEntrypoint,
4
+ WorkflowEntrypoint,
5
+ _EnvWrapper,
6
+ handler,
7
+ )
8
+ from .blob import Blob, BlobEnding, BlobValue, File
9
+ from .fetch import fetch
10
+ from .formdata import FormData, FormDataValue
11
+ from .request import Request
12
+ from .response import FetchResponse, Response
13
+ from .rpc import python_from_rpc, python_to_rpc
14
+ from .types import (
5
15
  Body,
6
16
  Context,
7
- DurableObject,
8
17
  FetchKwargs,
9
- FetchResponse,
10
- File,
11
- FormData,
12
- FormDataValue,
13
18
  Headers,
14
19
  JSBody,
15
- Request,
16
20
  RequestInitCfProperties,
17
- Response,
18
- WorkerEntrypoint,
19
- WorkflowEntrypoint,
20
- _EnvWrapper,
21
- fetch,
22
- handler,
23
- import_from_javascript,
24
- patch_env,
25
- python_from_rpc,
26
- python_to_rpc,
27
21
  )
22
+ from .utils import import_from_javascript, patch_env
28
23
 
29
24
  __all__ = [
30
25
  "Blob",
@@ -0,0 +1,602 @@
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
+ import functools
5
+ import inspect
6
+ from asyncio import create_task, gather
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import _cloudflare_compat_flags
10
+
11
+ # Get globals modules and import function from the entrypoint-helper
12
+ import _pyodide_entrypoint_helper
13
+ import js
14
+ from js import Object
15
+ from pyodide.ffi import (
16
+ JsProxy,
17
+ create_once_callable,
18
+ to_js,
19
+ )
20
+
21
+ from .fetch import fetch
22
+ 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,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from js import DurableObjectState, Env, ExecutionContext
33
+
34
+
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
+ class DurableObjectAbort(BaseException):
171
+ pass
172
+
173
+
174
+ class DurableObjectContext:
175
+ # __new__ and __init__ set up to make sure that the following passes:
176
+ #
177
+ # a = DurableObjectContext(x)
178
+ # assert DurableObjectContext(a) is a
179
+ # assert a._ctx is x
180
+ __new__ = _idempotent_new
181
+
182
+ def __init__(self, ctx: "DurableObjectState"):
183
+ if ctx is self:
184
+ return
185
+ self._ctx = ctx
186
+
187
+ def __getattr__(self, name: str):
188
+ result = getattr(self._ctx, name)
189
+ if _is_js_instance(result, "DurableObjectStorage"):
190
+ # durable_object.ctx.storage
191
+ result = _BindingWrapper(result)
192
+ setattr(self, name, result)
193
+ return result
194
+
195
+ def abort(self, reason: str | None = None):
196
+ # DurableObjectState.abort() terminates JS execution immediately. If Python
197
+ # calls it synchronously while asyncio is still running the task in the event loop,
198
+ # V8 unwinds the stack before asyncio can run its task-exit cleanup, leaving
199
+ # stale task state behind for the next request.
200
+ #
201
+ # Therefore, we queue the real abort into a microtask so Python can unwind first,
202
+ # then raise BaseException to stop user code without being swallowed by
203
+ # `except Exception` handlers.
204
+ ctx = self._ctx
205
+
206
+ if reason is None:
207
+ callback = create_once_callable(lambda: ctx.abort())
208
+ else:
209
+ callback = create_once_callable(lambda: ctx.abort(reason))
210
+
211
+ js.queueMicrotask(callback)
212
+ raise DurableObjectAbort(reason or "Durable Object abort requested")
213
+
214
+
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
+ class _WorkflowStepWrapper:
313
+ __new__ = _idempotent_new
314
+
315
+ def __init__(self, js_step):
316
+ if js_step is self:
317
+ return
318
+ self._js_step = js_step
319
+ self._memoized_dependencies = {}
320
+ self._in_flight = {}
321
+ self.step_closures = {}
322
+
323
+ # Assign the appropriate method based on compat flag
324
+ if _cloudflare_compat_flags.python_workflows_implicit_dependencies:
325
+ self.do = self._do_implicit
326
+ else:
327
+ self.do = self._do_legacy
328
+
329
+ def _do_legacy(self, name, depends=None, concurrent=False, config=None):
330
+ """Original signature - positional args allowed, explicit depends parameter."""
331
+ return self._create_step_decorator(
332
+ name=name,
333
+ depends=depends,
334
+ concurrent=concurrent,
335
+ config=config,
336
+ implicit=False,
337
+ )
338
+
339
+ def _do_implicit(self, name=None, *, concurrent=False, config=None):
340
+ """New signature - keyword-only args, dependencies resolved from param names."""
341
+ return self._create_step_decorator(
342
+ name=name,
343
+ depends=None,
344
+ concurrent=concurrent,
345
+ config=config,
346
+ implicit=True,
347
+ )
348
+
349
+ def _create_step_decorator(self, name, depends, concurrent, config, implicit):
350
+ """Shared decorator factory for both legacy and implicit modes."""
351
+
352
+ def decorator(func):
353
+ step_name = func.__name__ if name is None else name
354
+
355
+ async def wrapper():
356
+ results_future_list = self._build_dependency_list(
357
+ func, depends, implicit
358
+ )
359
+ results = await self._gather_results(results_future_list, concurrent)
360
+ return await _do_call(self, step_name, config, func, *results)
361
+
362
+ wrapper._step_name = step_name
363
+ self.step_closures[step_name] = wrapper
364
+ return wrapper
365
+
366
+ return decorator
367
+
368
+ def _build_dependency_list(self, func, depends, implicit):
369
+ """Build the dependency list based on mode (implicit vs legacy)."""
370
+ sig = inspect.signature(func)
371
+ results_future_list = []
372
+
373
+ if implicit:
374
+ # Implicit mode: resolve dependencies from parameter names
375
+ for p in sig.parameters.values():
376
+ if p.name in self.step_closures:
377
+ results_future_list.append(self.step_closures[p.name])
378
+ elif p.name == "ctx":
379
+ results_future_list.append(p)
380
+ else:
381
+ raise TypeError(f"Received unexpected parameter {p.name}")
382
+ else:
383
+ # Legacy mode: use explicit depends list, support ctx parameter
384
+ non_ctx_params = [p for p in sig.parameters.values() if p.name != "ctx"]
385
+
386
+ if depends is None and len(non_ctx_params) > 0:
387
+ raise TypeError(
388
+ f"Step has {len(non_ctx_params)} non-ctx parameter(s) but no 'depends' list provided"
389
+ )
390
+
391
+ elif depends is not None and len(depends) != len(non_ctx_params):
392
+ raise TypeError(
393
+ f"Step declares {len(non_ctx_params)} non-ctx parameter(s) but 'depends' has {len(depends)} item(s)"
394
+ )
395
+
396
+ curr = 0
397
+ for p in sig.parameters.values():
398
+ if p.name == "ctx":
399
+ results_future_list.append(p)
400
+ else:
401
+ results_future_list.append(depends[curr])
402
+ curr += 1
403
+
404
+ return results_future_list
405
+
406
+ async def _gather_results(self, results_future_list, concurrent):
407
+ """Resolve dependencies concurrently or sequentially."""
408
+ if concurrent:
409
+ return await gather(
410
+ *[self._resolve_dependency(dep) for dep in results_future_list or []]
411
+ )
412
+ else:
413
+ return [
414
+ await self._resolve_dependency(dep) for dep in results_future_list or []
415
+ ]
416
+
417
+ def sleep(self, *args, **kwargs):
418
+ return self._js_step.sleep(*args, **kwargs)
419
+
420
+ def sleep_until(self, name, timestamp):
421
+ if not isinstance(timestamp, str):
422
+ timestamp = python_to_rpc(timestamp)
423
+
424
+ return self._js_step.sleepUntil(name, timestamp)
425
+
426
+ async def wait_for_event(self, name, event_type, /, timeout="24 hours"):
427
+ return python_from_rpc(
428
+ await self._js_step.waitForEvent(
429
+ name,
430
+ to_js(
431
+ {"type": event_type, "timeout": timeout},
432
+ dict_converter=Object.fromEntries,
433
+ ),
434
+ )
435
+ )
436
+
437
+ async def _resolve_dependency(self, dep):
438
+ if hasattr(dep, "name") and dep.name == "ctx":
439
+ return dep
440
+ elif dep._step_name in self._memoized_dependencies:
441
+ return self._memoized_dependencies[dep._step_name]
442
+ elif dep._step_name in self._in_flight:
443
+ return await self._in_flight[dep._step_name]
444
+
445
+ return await dep()
446
+
447
+
448
+ async def _do_call(entrypoint, name, config, callback, *results):
449
+ async def _callback(ctx=None):
450
+ # deconstruct the actual ctx object
451
+ resolved_results = tuple(
452
+ python_from_rpc(ctx)
453
+ if isinstance(r, inspect.Parameter) and r.name == "ctx"
454
+ else r
455
+ for r in results
456
+ )
457
+ result = callback(*resolved_results)
458
+
459
+ if inspect.iscoroutine(result):
460
+ result = await result
461
+ return to_js(result, dict_converter=Object.fromEntries)
462
+
463
+ async def _closure():
464
+ try:
465
+ if config is None:
466
+ coroutine = await entrypoint._js_step.do(name, _callback)
467
+ else:
468
+ coroutine = await entrypoint._js_step.do(
469
+ name, to_js(config, dict_converter=Object.fromEntries), _callback
470
+ )
471
+
472
+ return python_from_rpc(coroutine)
473
+ except Exception as exc:
474
+ raise _from_js_error(exc) from exc
475
+
476
+ task = create_task(_closure())
477
+ entrypoint._in_flight[name] = task
478
+
479
+ try:
480
+ result = await task
481
+ entrypoint._memoized_dependencies[name] = result
482
+ finally:
483
+ del entrypoint._in_flight[name]
484
+
485
+ return result
486
+
487
+
488
+ def _wrap_class(cls):
489
+ # Override the class __init__ so that we can wrap the `env` in the constructor.
490
+ original_init = cls.__dict__.get("__init__")
491
+ if original_init is None:
492
+ return cls
493
+
494
+ def wrapped_init(self, *args, **kwargs):
495
+ args = list(args)
496
+ if len(args) > 0:
497
+ _pyodide_entrypoint_helper.patchWaitUntil(args[0])
498
+ if issubclass(cls, DurableObject):
499
+ args[0] = DurableObjectContext(args[0])
500
+ if len(args) > 1:
501
+ args[1] = _EnvWrapper(args[1])
502
+
503
+ original_init(self, *args, **kwargs)
504
+
505
+ cls.__init__ = wrapped_init
506
+ return cls
507
+
508
+
509
+ def _wrap_workflow_step(cls):
510
+ run_fn = cls.__dict__.get("run")
511
+ if run_fn is None:
512
+ return
513
+
514
+ @functools.wraps(run_fn)
515
+ async def wrapped_run(self, event=None, step=None, /, *args, **kwargs):
516
+ if event is not None:
517
+ event = python_from_rpc(event)
518
+ if step is not None:
519
+ step = _WorkflowStepWrapper(step)
520
+
521
+ result = run_fn(self, event, step, *args, **kwargs)
522
+
523
+ if inspect.iscoroutine(result):
524
+ result = await result
525
+
526
+ if result is None:
527
+ return result
528
+
529
+ # This should be wrapped again to js object
530
+ # as the value will go through the RPC boundary
531
+ return python_to_rpc(result)
532
+
533
+ cls.run = wrapped_run
534
+
535
+
536
+ @_wrap_class
537
+ class DurableObject:
538
+ """
539
+ Base class used to define a Durable Object.
540
+ """
541
+
542
+ ctx: "DurableObjectContext"
543
+ env: "Env"
544
+
545
+ def __init__(self, ctx: "DurableObjectState", env: "Env"):
546
+ self.ctx = ctx
547
+ self.env = env
548
+
549
+ def __init_subclass__(cls, **_kwargs):
550
+ _wrap_class(cls)
551
+
552
+
553
+ @_wrap_class
554
+ class WorkerEntrypoint:
555
+ """
556
+ Base class used to define a Worker Entrypoint.
557
+ """
558
+
559
+ ctx: "ExecutionContext"
560
+ env: "Env"
561
+
562
+ def __init__(self, ctx: "ExecutionContext", env: "Env"):
563
+ self.ctx = ctx
564
+ self.env = env
565
+
566
+ def __init_subclass__(cls, **_kwargs: Any):
567
+ _wrap_class(cls)
568
+ _wrap_queue_handler(cls)
569
+
570
+
571
+ @_wrap_class
572
+ class WorkflowEntrypoint:
573
+ """
574
+ Base class used to define a Workflow Entrypoint.
575
+ """
576
+
577
+ ctx: "ExecutionContext"
578
+ env: "Env"
579
+
580
+ def __init__(self, ctx: "ExecutionContext", env: "Env"):
581
+ self.ctx = ctx
582
+ self.env = env
583
+
584
+ def __init_subclass__(cls, **_kwargs: Any):
585
+ _wrap_class(cls)
586
+ _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