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.
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/CHANGELOG.md +18 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/PKG-INFO +1 -1
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/pyproject.toml +7 -2
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/workers/__init__.py +14 -19
- workers_runtime_sdk-1.5.3/src/workers/_workers.py +602 -0
- workers_runtime_sdk-1.5.3/src/workers/blob.py +154 -0
- workers_runtime_sdk-1.5.3/src/workers/fetch.py +44 -0
- workers_runtime_sdk-1.5.3/src/workers/formdata.py +97 -0
- workers_runtime_sdk-1.5.3/src/workers/request.py +175 -0
- workers_runtime_sdk-1.5.3/src/workers/response.py +216 -0
- workers_runtime_sdk-1.5.3/src/workers/rpc.py +208 -0
- workers_runtime_sdk-1.5.3/src/workers/types.py +49 -0
- workers_runtime_sdk-1.5.3/src/workers/utils.py +246 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/uv.lock +1 -1
- workers_runtime_sdk-1.5.1/src/workers/_workers.py +0 -1685
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/.gitignore +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/AGENTS.md +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/README.md +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_cloudflare_compat_flags.pyi +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_pyodide_entrypoint_helper.pyi +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_workers_sdk_entropy_import_context.pth +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_workers_sdk_entropy_import_context.py +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/asgi.py +0 -0
- {workers_runtime_sdk-1.5.1 → workers_runtime_sdk-1.5.3}/src/workers/py.typed +0 -0
- {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.
|
|
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.
|
|
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 = "
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|