workers-runtime-sdk 1.5.0__tar.gz → 1.5.2__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.0 → workers_runtime_sdk-1.5.2}/CHANGELOG.md +22 -0
  2. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/PKG-INFO +1 -1
  3. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/pyproject.toml +1 -1
  4. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/workers/__init__.py +15 -19
  5. workers_runtime_sdk-1.5.2/src/workers/_workers.py +605 -0
  6. workers_runtime_sdk-1.5.2/src/workers/blob.py +154 -0
  7. workers_runtime_sdk-1.5.2/src/workers/fetch.py +44 -0
  8. workers_runtime_sdk-1.5.2/src/workers/formdata.py +97 -0
  9. workers_runtime_sdk-1.5.2/src/workers/request.py +175 -0
  10. workers_runtime_sdk-1.5.2/src/workers/response.py +216 -0
  11. workers_runtime_sdk-1.5.2/src/workers/rpc.py +208 -0
  12. workers_runtime_sdk-1.5.2/src/workers/types.py +49 -0
  13. workers_runtime_sdk-1.5.2/src/workers/utils.py +246 -0
  14. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/uv.lock +1 -1
  15. workers_runtime_sdk-1.5.0/src/workers/_workers.py +0 -1673
  16. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/.gitignore +0 -0
  17. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/AGENTS.md +0 -0
  18. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/README.md +0 -0
  19. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/_cloudflare_compat_flags.pyi +0 -0
  20. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/_pyodide_entrypoint_helper.pyi +0 -0
  21. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/_workers_sdk_entropy_import_context.pth +0 -0
  22. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/_workers_sdk_entropy_import_context.py +0 -0
  23. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
  24. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/asgi.py +0 -0
  25. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/workers/py.typed +0 -0
  26. {workers_runtime_sdk-1.5.0 → workers_runtime_sdk-1.5.2}/src/workers/workflows.py +0 -0
@@ -2,6 +2,28 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.5.2 (2026-07-01)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Ensure that ctx and env __init__ arguments are always wrapped
10
+ ([#131](https://github.com/cloudflare/workers-py/pull/131),
11
+ [`465c702`](https://github.com/cloudflare/workers-py/commit/465c7029d7b7d5ca75afb1648d9a96433a8a9a13))
12
+
13
+
14
+ ## v1.5.1 (2026-06-29)
15
+
16
+ ### Bug Fixes
17
+
18
+ - Ensure self.env and top-level env uses a same class
19
+ ([#136](https://github.com/cloudflare/workers-py/pull/136),
20
+ [`e627c11`](https://github.com/cloudflare/workers-py/commit/e627c11f58c572f6ee5df97e423928ee4423d2e9))
21
+
22
+ - Update FetchResponse.headers to return HTTPMessage
23
+ ([#136](https://github.com/cloudflare/workers-py/pull/136),
24
+ [`e627c11`](https://github.com/cloudflare/workers-py/commit/e627c11f58c572f6ee5df97e423928ee4423d2e9))
25
+
26
+
5
27
  ## v1.5.0 (2026-06-23)
6
28
 
7
29
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workers-runtime-sdk
3
- Version: 1.5.0
3
+ Version: 1.5.2
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.0"
7
+ version = "1.5.2"
8
8
  description = "Python SDK for Cloudflare Workers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,29 +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
- fetch,
21
- handler,
22
- import_from_javascript,
23
- patch_env,
24
- python_from_rpc,
25
- python_to_rpc,
26
21
  )
22
+ from .utils import import_from_javascript, patch_env
27
23
 
28
24
  __all__ = [
29
25
  "Blob",
@@ -59,7 +55,7 @@ __all__ = [
59
55
  def __getattr__(key):
60
56
  if key == "env":
61
57
  cloudflare_workers = import_from_javascript("cloudflare:workers")
62
- return cloudflare_workers.env
58
+ return _EnvWrapper(cloudflare_workers.env)
63
59
  if key in ("wait_until", "waitUntil"):
64
60
  cloudflare_workers = import_from_javascript("cloudflare:workers")
65
61
  return cloudflare_workers.waitUntil
@@ -0,0 +1,605 @@
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
+ def _getattr_helper(self, name):
98
+ attr = getattr(self._binding, name)
99
+
100
+ if not callable(attr):
101
+ return self._convert_result(attr)
102
+
103
+ def wrapper(*args, **kwargs):
104
+ js_args = [python_to_rpc(arg) for arg in args]
105
+ js_kwargs = {k: python_to_rpc(v) for k, v in kwargs.items()}
106
+ result = attr(*js_args, **js_kwargs)
107
+ if hasattr(result, "then") and callable(result.then):
108
+
109
+ async def await_and_convert():
110
+ return self._convert_result(await result)
111
+
112
+ return await_and_convert()
113
+ return self._convert_result(result)
114
+
115
+ return wrapper
116
+
117
+ def __getattr__(self, name):
118
+ result = self._getattr_helper(name)
119
+ setattr(self, name, result)
120
+ return result
121
+
122
+ def __getitem__(self, key):
123
+ if isinstance(key, int):
124
+ return self._convert_result(self._binding[key])
125
+ return self._convert_result(getattr(self._binding, key))
126
+
127
+ def __iter__(self):
128
+ binding = self._binding
129
+ if not hasattr(binding, "__iter__"):
130
+ raise TypeError(f"'{self._real_name}' object is not iterable")
131
+ for item in binding:
132
+ yield self._convert_result(item)
133
+
134
+ def __len__(self):
135
+ binding = self._binding
136
+ if not hasattr(binding, "length"):
137
+ raise TypeError(f"'{self._real_name}' object has no len()")
138
+ return binding.length
139
+
140
+
141
+ class _FetcherWrapper(_BindingWrapper):
142
+ def fetch(self, *args, **kwargs):
143
+ return fetch(*args, fetcher=self._binding.fetch, **kwargs)
144
+
145
+
146
+ class _DurableObjectNamespaceWrapper:
147
+ def __init__(self, binding):
148
+ self._binding = binding
149
+
150
+ def __getattr__(self, name):
151
+ return getattr(self._binding, name)
152
+
153
+ def get(self, *args, **kwargs):
154
+ return _FetcherWrapper(self._binding.get(*args, **kwargs))
155
+
156
+ def getByName(self, *args, **kwargs):
157
+ return _FetcherWrapper(self._binding.getByName(*args, **kwargs))
158
+
159
+ def jurisdiction(self, *args, **kwargs):
160
+ return _DurableObjectNamespaceWrapper(
161
+ self._binding.jurisdiction(*args, **kwargs)
162
+ )
163
+
164
+
165
+ class DurableObjectAbort(BaseException):
166
+ pass
167
+
168
+
169
+ class DurableObjectContext:
170
+ # __new__ and __init__ set up to make sure that the following passes:
171
+ #
172
+ # a = DurableObjectContext(x)
173
+ # assert DurableObjectContext(a) is a
174
+ # assert a._ctx is x
175
+ __new__ = _idempotent_new
176
+
177
+ def __init__(self, ctx: "DurableObjectState"):
178
+ if ctx is self:
179
+ return
180
+ self._ctx = ctx
181
+
182
+ def __getattr__(self, name: str):
183
+ result = getattr(self._ctx, name)
184
+ if _is_js_instance(result, "DurableObjectStorage"):
185
+ # durable_object.ctx.storage
186
+ result = _BindingWrapper(result)
187
+ setattr(self, name, result)
188
+ return result
189
+
190
+ def abort(self, reason: str | None = None):
191
+ # DurableObjectState.abort() terminates JS execution immediately. If Python
192
+ # calls it synchronously while asyncio is still running the task in the event loop,
193
+ # V8 unwinds the stack before asyncio can run its task-exit cleanup, leaving
194
+ # stale task state behind for the next request.
195
+ #
196
+ # Therefore, we queue the real abort into a microtask so Python can unwind first,
197
+ # then raise BaseException to stop user code without being swallowed by
198
+ # `except Exception` handlers.
199
+ ctx = self._ctx
200
+
201
+ if reason is None:
202
+ callback = create_once_callable(lambda: ctx.abort())
203
+ else:
204
+ callback = create_once_callable(lambda: ctx.abort(reason))
205
+
206
+ js.queueMicrotask(callback)
207
+ raise DurableObjectAbort(reason or "Durable Object abort requested")
208
+
209
+
210
+ class _WorkflowInstanceWrapper:
211
+ def __init__(self, binding):
212
+ self._binding = binding
213
+
214
+ def __getattr__(self, name):
215
+ return getattr(self._binding, name)
216
+
217
+ async def send_event(self, *args, **kwargs):
218
+ return self._binding.sendEvent(*args, **kwargs)
219
+
220
+ async def pause(self, *args, **kwargs):
221
+ return self._binding.pause(*args, **kwargs)
222
+
223
+ async def resume(self, *args, **kwargs):
224
+ return self._binding.resume(*args, **kwargs)
225
+
226
+ async def terminate(self, *args, **kwargs):
227
+ return self._binding.terminate(*args, **kwargs)
228
+
229
+ async def restart(self, *args, **kwargs):
230
+ return self._binding.restart(*args, **kwargs)
231
+
232
+ async def status(self, *args, **kwargs):
233
+ return self._binding.status(*args, **kwargs)
234
+
235
+
236
+ class _WorkflowBindingWrapper:
237
+ def __init__(self, binding):
238
+ self._binding = binding
239
+
240
+ def __getattr__(self, name):
241
+ return getattr(self._binding, name)
242
+
243
+ async def get(self, *args, **kwargs):
244
+ return _WorkflowInstanceWrapper(await self._binding.get(*args, **kwargs))
245
+
246
+ async def create(self, *args, **kwargs):
247
+ return _WorkflowInstanceWrapper(await self._binding.create(*args, **kwargs))
248
+
249
+ async def create_batch(self, *args, **kwargs):
250
+ return [
251
+ _WorkflowInstanceWrapper(w)
252
+ for w in await self._binding.createBatch(*args, **kwargs)
253
+ ]
254
+
255
+
256
+ class _EnvWrapper:
257
+ _BINDING_TYPES = {
258
+ "KvNamespace",
259
+ "R2Bucket",
260
+ "D1Database",
261
+ "WorkerQueue",
262
+ "Ai",
263
+ "VectorizeIndexImpl",
264
+ "AnalyticsEngineDataset",
265
+ "LocalAnalyticsEngineDataset",
266
+ "ImagesBindingImpl",
267
+ "HostedImagesBindingImpl",
268
+ "Ratelimit",
269
+ }
270
+
271
+ __new__ = _idempotent_new
272
+
273
+ def __init__(self, env: Any):
274
+ if env is self:
275
+ return
276
+ self._env = env
277
+
278
+ def _getattr_helper(self, name):
279
+ binding = getattr(self._env, name)
280
+ if _is_js_instance(binding, "Fetcher"):
281
+ return _FetcherWrapper(binding)
282
+
283
+ if _is_js_instance(binding, "DurableObjectNamespace"):
284
+ return _DurableObjectNamespaceWrapper(binding)
285
+
286
+ if _is_js_instance(binding, "WorkflowImpl"):
287
+ return _WorkflowBindingWrapper(binding)
288
+
289
+ if _is_js_instance(binding, self._BINDING_TYPES):
290
+ return _BindingWrapper(binding)
291
+
292
+ return binding
293
+
294
+ def __getattr__(self, name):
295
+ result = self._getattr_helper(name)
296
+ setattr(self, name, result)
297
+ return result
298
+
299
+
300
+ def handler(func):
301
+ """
302
+ When applied to handlers such as `on_fetch` it will rewrite arguments passed in to native Python
303
+ types defined in this module. For example, the `request` argument to `on_fetch` gets converted
304
+ to an instance of the Request class defined in this module.
305
+ """
306
+
307
+ @functools.wraps(func)
308
+ def wrapper(*args, **kwargs):
309
+ # TODO: support transforming kwargs
310
+ if len(args) > 0 and _is_js_instance(args[0], "Request"):
311
+ args = (Request(args[0]), *args[1:])
312
+
313
+ # Wrap `env` so that bindings can be used without to_js.
314
+ if len(args) > 1:
315
+ args = (args[0], _EnvWrapper(args[1]), *args[2:])
316
+
317
+ return func(*args, **kwargs)
318
+
319
+ return wrapper
320
+
321
+
322
+ class _WorkflowStepWrapper:
323
+ __new__ = _idempotent_new
324
+
325
+ def __init__(self, js_step):
326
+ if js_step is self:
327
+ return
328
+ self._js_step = js_step
329
+ self._memoized_dependencies = {}
330
+ self._in_flight = {}
331
+ self.step_closures = {}
332
+
333
+ # Assign the appropriate method based on compat flag
334
+ if _cloudflare_compat_flags.python_workflows_implicit_dependencies:
335
+ self.do = self._do_implicit
336
+ else:
337
+ self.do = self._do_legacy
338
+
339
+ def _do_legacy(self, name, depends=None, concurrent=False, config=None):
340
+ """Original signature - positional args allowed, explicit depends parameter."""
341
+ return self._create_step_decorator(
342
+ name=name,
343
+ depends=depends,
344
+ concurrent=concurrent,
345
+ config=config,
346
+ implicit=False,
347
+ )
348
+
349
+ def _do_implicit(self, name=None, *, concurrent=False, config=None):
350
+ """New signature - keyword-only args, dependencies resolved from param names."""
351
+ return self._create_step_decorator(
352
+ name=name,
353
+ depends=None,
354
+ concurrent=concurrent,
355
+ config=config,
356
+ implicit=True,
357
+ )
358
+
359
+ def _create_step_decorator(self, name, depends, concurrent, config, implicit):
360
+ """Shared decorator factory for both legacy and implicit modes."""
361
+
362
+ def decorator(func):
363
+ step_name = func.__name__ if name is None else name
364
+
365
+ async def wrapper():
366
+ results_future_list = self._build_dependency_list(
367
+ func, depends, implicit
368
+ )
369
+ results = await self._gather_results(results_future_list, concurrent)
370
+ return await _do_call(self, step_name, config, func, *results)
371
+
372
+ wrapper._step_name = step_name
373
+ self.step_closures[step_name] = wrapper
374
+ return wrapper
375
+
376
+ return decorator
377
+
378
+ def _build_dependency_list(self, func, depends, implicit):
379
+ """Build the dependency list based on mode (implicit vs legacy)."""
380
+ sig = inspect.signature(func)
381
+ results_future_list = []
382
+
383
+ if implicit:
384
+ # Implicit mode: resolve dependencies from parameter names
385
+ for p in sig.parameters.values():
386
+ if p.name in self.step_closures:
387
+ results_future_list.append(self.step_closures[p.name])
388
+ elif p.name == "ctx":
389
+ results_future_list.append(p)
390
+ else:
391
+ raise TypeError(f"Received unexpected parameter {p.name}")
392
+ else:
393
+ # Legacy mode: use explicit depends list, support ctx parameter
394
+ non_ctx_params = [p for p in sig.parameters.values() if p.name != "ctx"]
395
+
396
+ if depends is None and len(non_ctx_params) > 0:
397
+ raise TypeError(
398
+ f"Step has {len(non_ctx_params)} non-ctx parameter(s) but no 'depends' list provided"
399
+ )
400
+
401
+ elif depends is not None and len(depends) != len(non_ctx_params):
402
+ raise TypeError(
403
+ f"Step declares {len(non_ctx_params)} non-ctx parameter(s) but 'depends' has {len(depends)} item(s)"
404
+ )
405
+
406
+ curr = 0
407
+ for p in sig.parameters.values():
408
+ if p.name == "ctx":
409
+ results_future_list.append(p)
410
+ else:
411
+ results_future_list.append(depends[curr])
412
+ curr += 1
413
+
414
+ return results_future_list
415
+
416
+ async def _gather_results(self, results_future_list, concurrent):
417
+ """Resolve dependencies concurrently or sequentially."""
418
+ if concurrent:
419
+ return await gather(
420
+ *[self._resolve_dependency(dep) for dep in results_future_list or []]
421
+ )
422
+ else:
423
+ return [
424
+ await self._resolve_dependency(dep) for dep in results_future_list or []
425
+ ]
426
+
427
+ def sleep(self, *args, **kwargs):
428
+ return self._js_step.sleep(*args, **kwargs)
429
+
430
+ def sleep_until(self, name, timestamp):
431
+ if not isinstance(timestamp, str):
432
+ timestamp = python_to_rpc(timestamp)
433
+
434
+ return self._js_step.sleepUntil(name, timestamp)
435
+
436
+ def wait_for_event(self, name, event_type, /, timeout="24 hours"):
437
+ return self._js_step.waitForEvent(
438
+ name,
439
+ to_js(
440
+ {"type": event_type, "timeout": timeout},
441
+ dict_converter=Object.fromEntries,
442
+ ),
443
+ )
444
+
445
+ async def _resolve_dependency(self, dep):
446
+ if hasattr(dep, "name") and dep.name == "ctx":
447
+ return dep
448
+ elif dep._step_name in self._memoized_dependencies:
449
+ return self._memoized_dependencies[dep._step_name]
450
+ elif dep._step_name in self._in_flight:
451
+ return await self._in_flight[dep._step_name]
452
+
453
+ return await dep()
454
+
455
+
456
+ async def _do_call(entrypoint, name, config, callback, *results):
457
+ async def _callback(ctx=None):
458
+ # deconstruct the actual ctx object
459
+ resolved_results = tuple(
460
+ python_from_rpc(ctx)
461
+ if isinstance(r, inspect.Parameter) and r.name == "ctx"
462
+ else r
463
+ for r in results
464
+ )
465
+ result = callback(*resolved_results)
466
+
467
+ if inspect.iscoroutine(result):
468
+ result = await result
469
+ return to_js(result, dict_converter=Object.fromEntries)
470
+
471
+ async def _closure():
472
+ try:
473
+ if config is None:
474
+ coroutine = await entrypoint._js_step.do(name, _callback)
475
+ else:
476
+ coroutine = await entrypoint._js_step.do(
477
+ name, to_js(config, dict_converter=Object.fromEntries), _callback
478
+ )
479
+
480
+ return python_from_rpc(coroutine)
481
+ except Exception as exc:
482
+ raise _from_js_error(exc) from exc
483
+
484
+ task = create_task(_closure())
485
+ entrypoint._in_flight[name] = task
486
+
487
+ try:
488
+ result = await task
489
+ entrypoint._memoized_dependencies[name] = result
490
+ finally:
491
+ del entrypoint._in_flight[name]
492
+
493
+ return result
494
+
495
+
496
+ def _wrap_class(cls):
497
+ # Override the class __init__ so that we can wrap the `env` in the constructor.
498
+ original_init = cls.__dict__.get("__init__")
499
+ if original_init is None:
500
+ return cls
501
+
502
+ def wrapped_init(self, *args, **kwargs):
503
+ args = list(args)
504
+ if len(args) > 0:
505
+ _pyodide_entrypoint_helper.patchWaitUntil(args[0])
506
+ if issubclass(cls, DurableObject):
507
+ args[0] = DurableObjectContext(args[0])
508
+ if len(args) > 1:
509
+ args[1] = _EnvWrapper(args[1])
510
+
511
+ original_init(self, *args, **kwargs)
512
+
513
+ cls.__init__ = wrapped_init
514
+ return cls
515
+
516
+
517
+ def _wrap_workflow_step(cls):
518
+ run_fn = cls.__dict__.get("run")
519
+ if run_fn is None:
520
+ return
521
+
522
+ @functools.wraps(run_fn)
523
+ async def wrapped_run(self, event=None, step=None, /, *args, **kwargs):
524
+ if event is not None:
525
+ event = python_from_rpc(event)
526
+ if step is not None:
527
+ step = _WorkflowStepWrapper(step)
528
+
529
+ result = run_fn(self, event, step, *args, **kwargs)
530
+
531
+ if inspect.iscoroutine(result):
532
+ result = await result
533
+
534
+ return result
535
+
536
+ cls.run = wrapped_run
537
+
538
+
539
+ @_wrap_class
540
+ class DurableObject:
541
+ """
542
+ Base class used to define a Durable Object.
543
+ """
544
+
545
+ ctx: "DurableObjectContext"
546
+ env: "Env"
547
+
548
+ def __init__(self, ctx: "DurableObjectState", env: "Env"):
549
+ self.ctx = ctx
550
+ self.env = env
551
+
552
+ def __init_subclass__(cls, **_kwargs):
553
+ _wrap_class(cls)
554
+
555
+
556
+ @_wrap_class
557
+ class WorkerEntrypoint:
558
+ """
559
+ Base class used to define a Worker Entrypoint.
560
+ """
561
+
562
+ ctx: "ExecutionContext"
563
+ env: "Env"
564
+
565
+ def __init__(self, ctx: "ExecutionContext", env: "Env"):
566
+ self.ctx = ctx
567
+ self.env = env
568
+
569
+ def __init_subclass__(cls, **_kwargs: Any):
570
+ _wrap_class(cls)
571
+ _wrap_queue_handler(cls)
572
+
573
+
574
+ @_wrap_class
575
+ class WorkflowEntrypoint:
576
+ """
577
+ Base class used to define a Workflow Entrypoint.
578
+ """
579
+
580
+ ctx: "ExecutionContext"
581
+ env: "Env"
582
+
583
+ def __init__(self, ctx: "ExecutionContext", env: "Env"):
584
+ self.ctx = ctx
585
+ self.env = env
586
+
587
+ def __init_subclass__(cls, **_kwargs: Any):
588
+ _wrap_class(cls)
589
+ _wrap_workflow_step(cls)
590
+
591
+
592
+ def _wrap_queue_handler(cls):
593
+ queue_fn = getattr(cls, "queue", None)
594
+ if queue_fn is None:
595
+ return
596
+
597
+ @functools.wraps(queue_fn)
598
+ async def wrapped_queue(self, batch, *args, **kwargs):
599
+ wrapped_batch = _BindingWrapper(batch)
600
+ result = queue_fn(self, wrapped_batch, *args, **kwargs)
601
+ if inspect.iscoroutine(result):
602
+ result = await result
603
+ return result
604
+
605
+ cls.queue = wrapped_queue