crosswind-workers 0.1.0__py3-none-any.whl
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.
- _crosswind_patches/__init__.py +13 -0
- _crosswind_patches/entropy.py +259 -0
- _crosswind_patches/runtime.py +45 -0
- asgi/__init__.py +7 -0
- crosswind_workers-0.1.0.data/purelib/crosswind_workers_patches.pth +1 -0
- crosswind_workers-0.1.0.dist-info/METADATA +9 -0
- crosswind_workers-0.1.0.dist-info/RECORD +35 -0
- crosswind_workers-0.1.0.dist-info/WHEEL +4 -0
- workers/__init__.py +88 -0
- workers/_adapter.py +24 -0
- workers/_body.py +34 -0
- workers/_js.py +121 -0
- workers/asgi.py +523 -0
- workers/bindings/__init__.py +36 -0
- workers/bindings/base.py +135 -0
- workers/bindings/d1.py +47 -0
- workers/bindings/env.py +50 -0
- workers/bindings/kv.py +121 -0
- workers/bindings/queues.py +36 -0
- workers/bindings/r2.py +130 -0
- workers/bindings/services.py +55 -0
- workers/bindings/workflow.py +64 -0
- workers/blob.py +158 -0
- workers/context.py +230 -0
- workers/entrypoints.py +183 -0
- workers/fetch.py +51 -0
- workers/formdata.py +92 -0
- workers/py.typed +0 -0
- workers/request.py +138 -0
- workers/response.py +181 -0
- workers/rpc.py +154 -0
- workers/types.py +57 -0
- workers/utils.py +55 -0
- workers/workflows.py +265 -0
- workers/wsgi.py +426 -0
workers/context.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""The context an entrypoint receives as `ctx`.
|
|
2
|
+
|
|
3
|
+
As in the stock SDK, names these classes don't define pass through raw: the
|
|
4
|
+
JS value comes back unconverted. Python wrappers passed as arguments are
|
|
5
|
+
unwrapped to their JS objects first.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from functools import cached_property
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import js
|
|
15
|
+
from pyodide.ffi import create_once_callable, create_proxy
|
|
16
|
+
|
|
17
|
+
from ._js import js_name
|
|
18
|
+
from .bindings import Binding, _js_options
|
|
19
|
+
from .rpc import python_from_rpc, python_to_rpc
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DurableObjectAbort",
|
|
23
|
+
"DurableObjectContext",
|
|
24
|
+
"DurableObjectStorage",
|
|
25
|
+
"ExecutionContext",
|
|
26
|
+
"js_promise",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DurableObjectAbort(BaseException):
|
|
31
|
+
"""Raised by `ctx.abort()`. A BaseException, so `except Exception` lets it
|
|
32
|
+
through, and the Durable Object is reset."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _RawContext(Binding):
|
|
36
|
+
@property
|
|
37
|
+
def _ctx(self) -> Any:
|
|
38
|
+
# The stock SDK's private name for the JS context, which its tests and
|
|
39
|
+
# the agents port read.
|
|
40
|
+
return self.js_object
|
|
41
|
+
|
|
42
|
+
def _pass_through(self, name: str, value: Any) -> Any:
|
|
43
|
+
if not callable(value):
|
|
44
|
+
return value
|
|
45
|
+
return lambda *args, **kwargs: value(
|
|
46
|
+
*map(_unwrap, args), **{k: _unwrap(v) for k, v in kwargs.items()}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def wait_until(self, awaitable: Awaitable[Any]) -> None:
|
|
50
|
+
"""Keep the invocation alive until `awaitable`, a coroutine, task or
|
|
51
|
+
JS promise, finishes."""
|
|
52
|
+
self.js_object.waitUntil(js_promise(awaitable))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def js_promise(awaitable: Any) -> Any:
|
|
56
|
+
"""A JS promise that settles when `awaitable` does.
|
|
57
|
+
|
|
58
|
+
Handing JS a coroutine loses it once the call returns, unless workerd's
|
|
59
|
+
patchWaitUntil applies, and it applies only to an ExecutionContext. So JS
|
|
60
|
+
gets a real promise that follows a task through a persistent proxy, which
|
|
61
|
+
is destroyed once the promise settles.
|
|
62
|
+
"""
|
|
63
|
+
if js_name(awaitable) == "Promise":
|
|
64
|
+
return awaitable
|
|
65
|
+
proxy = create_proxy(asyncio.ensure_future(awaitable))
|
|
66
|
+
return js.Promise.resolve(proxy).finally_(create_once_callable(proxy.destroy))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _unwrap(value: Any) -> Any:
|
|
70
|
+
return getattr(value, "js_object", value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ExecutionContext(_RawContext):
|
|
74
|
+
"""The `ctx` of a WorkerEntrypoint."""
|
|
75
|
+
|
|
76
|
+
def pass_through_on_exception(self) -> None:
|
|
77
|
+
self.js_object.passThroughOnException()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DurableObjectStorage(Binding):
|
|
81
|
+
"""A Durable Object's storage. Unlisted names, such as `sql`, pass through
|
|
82
|
+
with values converted, as in the stock SDK."""
|
|
83
|
+
|
|
84
|
+
async def get(
|
|
85
|
+
self,
|
|
86
|
+
keys: Any,
|
|
87
|
+
options: Any = None,
|
|
88
|
+
/,
|
|
89
|
+
*,
|
|
90
|
+
allow_concurrency: bool | None = None,
|
|
91
|
+
no_cache: bool | None = None,
|
|
92
|
+
**extra: Any,
|
|
93
|
+
) -> Any:
|
|
94
|
+
"""One value for a key, or a dict for a list of keys."""
|
|
95
|
+
js_options = _js_options(
|
|
96
|
+
options, extra, allow_concurrency=allow_concurrency, no_cache=no_cache
|
|
97
|
+
)
|
|
98
|
+
return python_from_rpc(
|
|
99
|
+
await self.js_object.get(python_to_rpc(keys), js_options)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def put(
|
|
103
|
+
self,
|
|
104
|
+
key_or_entries: Any,
|
|
105
|
+
*args: Any,
|
|
106
|
+
allow_concurrency: bool | None = None,
|
|
107
|
+
allow_unconfirmed: bool | None = None,
|
|
108
|
+
no_cache: bool | None = None,
|
|
109
|
+
**extra: Any,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""`put(key, value[, options])` or `put(entries[, options])`."""
|
|
112
|
+
named = {
|
|
113
|
+
"allow_concurrency": allow_concurrency,
|
|
114
|
+
"allow_unconfirmed": allow_unconfirmed,
|
|
115
|
+
"no_cache": no_cache,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if isinstance(key_or_entries, str):
|
|
119
|
+
if not 1 <= len(args) <= 2:
|
|
120
|
+
raise TypeError("put(key, value[, options]) needs a value")
|
|
121
|
+
value, options = args[0], args[1] if len(args) == 2 else None
|
|
122
|
+
js_value = python_to_rpc(value)
|
|
123
|
+
js_options = _js_options(options, extra, **named)
|
|
124
|
+
await self.js_object.put(key_or_entries, js_value, js_options)
|
|
125
|
+
else:
|
|
126
|
+
if len(args) > 1:
|
|
127
|
+
raise TypeError("put(entries[, options]) takes no value")
|
|
128
|
+
js_options = _js_options(args[0] if args else None, extra, **named)
|
|
129
|
+
await self.js_object.put(python_to_rpc(key_or_entries), js_options)
|
|
130
|
+
|
|
131
|
+
async def delete(
|
|
132
|
+
self,
|
|
133
|
+
keys: Any,
|
|
134
|
+
options: Any = None,
|
|
135
|
+
/,
|
|
136
|
+
*,
|
|
137
|
+
allow_unconfirmed: bool | None = None,
|
|
138
|
+
no_cache: bool | None = None,
|
|
139
|
+
**extra: Any,
|
|
140
|
+
) -> Any:
|
|
141
|
+
"""Whether a key existed, or how many of a list of keys did."""
|
|
142
|
+
js_options = _js_options(
|
|
143
|
+
options, extra, allow_unconfirmed=allow_unconfirmed, no_cache=no_cache
|
|
144
|
+
)
|
|
145
|
+
return await self.js_object.delete(python_to_rpc(keys), js_options)
|
|
146
|
+
|
|
147
|
+
async def delete_all(self, options: Any = None, /, **extra: Any) -> None:
|
|
148
|
+
await self.js_object.deleteAll(_js_options(options, extra))
|
|
149
|
+
|
|
150
|
+
async def list(
|
|
151
|
+
self,
|
|
152
|
+
options: Any = None,
|
|
153
|
+
/,
|
|
154
|
+
*,
|
|
155
|
+
start: str | None = None,
|
|
156
|
+
start_after: str | None = None,
|
|
157
|
+
end: str | None = None,
|
|
158
|
+
prefix: str | None = None,
|
|
159
|
+
reverse: bool | None = None,
|
|
160
|
+
limit: int | None = None,
|
|
161
|
+
**extra: Any,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
js_options = _js_options(
|
|
164
|
+
options,
|
|
165
|
+
extra,
|
|
166
|
+
start=start,
|
|
167
|
+
start_after=start_after,
|
|
168
|
+
end=end,
|
|
169
|
+
prefix=prefix,
|
|
170
|
+
reverse=reverse,
|
|
171
|
+
limit=limit,
|
|
172
|
+
)
|
|
173
|
+
return python_from_rpc(await self.js_object.list(js_options))
|
|
174
|
+
|
|
175
|
+
async def get_alarm(self, options: Any = None, /, **extra: Any) -> int | None:
|
|
176
|
+
"""The scheduled alarm time in milliseconds since the epoch."""
|
|
177
|
+
return python_from_rpc(
|
|
178
|
+
await self.js_object.getAlarm(_js_options(options, extra))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def set_alarm(
|
|
182
|
+
self, scheduled_time: datetime | int, options: Any = None, /, **extra: Any
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Schedule `alarm` at a datetime or at milliseconds since the epoch."""
|
|
185
|
+
if isinstance(scheduled_time, datetime):
|
|
186
|
+
scheduled_time = int(scheduled_time.timestamp() * 1000)
|
|
187
|
+
js_options = _js_options(options, extra)
|
|
188
|
+
await self.js_object.setAlarm(scheduled_time, js_options)
|
|
189
|
+
|
|
190
|
+
async def delete_alarm(self, options: Any = None, /, **extra: Any) -> None:
|
|
191
|
+
await self.js_object.deleteAlarm(_js_options(options, extra))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class DurableObjectContext(_RawContext):
|
|
195
|
+
"""The `ctx` of a Durable Object: the JS DurableObjectState."""
|
|
196
|
+
|
|
197
|
+
@cached_property
|
|
198
|
+
def storage(self) -> DurableObjectStorage:
|
|
199
|
+
return DurableObjectStorage(self.js_object.storage)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def id(self) -> Any:
|
|
203
|
+
return self.js_object.id
|
|
204
|
+
|
|
205
|
+
async def block_concurrency_while(
|
|
206
|
+
self, callback: Callable[[], Awaitable[Any]]
|
|
207
|
+
) -> Any:
|
|
208
|
+
return python_from_rpc(await self.js_object.blockConcurrencyWhile(callback))
|
|
209
|
+
|
|
210
|
+
def accept_web_socket(self, ws: Any, tags: list[str] | None = None) -> None:
|
|
211
|
+
if tags is None:
|
|
212
|
+
self.js_object.acceptWebSocket(_unwrap(ws))
|
|
213
|
+
else:
|
|
214
|
+
self.js_object.acceptWebSocket(_unwrap(ws), python_to_rpc(tags))
|
|
215
|
+
|
|
216
|
+
def get_web_sockets(self, tag: str | None = None) -> list[Binding]:
|
|
217
|
+
sockets = self.js_object.getWebSockets(*([] if tag is None else [tag]))
|
|
218
|
+
return [Binding(socket) for socket in sockets]
|
|
219
|
+
|
|
220
|
+
def abort(self, reason: str | None = None) -> None:
|
|
221
|
+
"""Reset the Durable Object, and stop this call.
|
|
222
|
+
|
|
223
|
+
The JS abort ends execution at once, so it runs in a microtask, after
|
|
224
|
+
Python has unwound. DurableObjectAbort stops the caller's code first.
|
|
225
|
+
"""
|
|
226
|
+
ctx = self.js_object
|
|
227
|
+
args = () if reason is None else (reason,)
|
|
228
|
+
js.queueMicrotask(create_once_callable(lambda: ctx.abort(*args)))
|
|
229
|
+
|
|
230
|
+
raise DurableObjectAbort(reason or "Durable Object abort requested")
|
workers/entrypoints.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
4
|
+
|
|
5
|
+
import _pyodide_entrypoint_helper
|
|
6
|
+
from pyodide.code import relaxed_call
|
|
7
|
+
|
|
8
|
+
from ._js import js_name
|
|
9
|
+
from .bindings import Binding, _convert, _EnvWrapper
|
|
10
|
+
from .context import DurableObjectAbort, DurableObjectContext, ExecutionContext
|
|
11
|
+
from .request import Request
|
|
12
|
+
from .workflows import wrap_run
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DurableObject",
|
|
16
|
+
"DurableObjectAbort",
|
|
17
|
+
"DurableObjectContext",
|
|
18
|
+
"WorkerEntrypoint",
|
|
19
|
+
"WorkflowEntrypoint",
|
|
20
|
+
"handler",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
EnvT = TypeVar("EnvT")
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
|
|
27
|
+
class _Typed[Env]:
|
|
28
|
+
env: Env
|
|
29
|
+
|
|
30
|
+
else:
|
|
31
|
+
|
|
32
|
+
class _Typed:
|
|
33
|
+
# Generic only to type checkers. A class that is generic at runtime
|
|
34
|
+
# can't follow Generic[S] in a base list (Python reports an
|
|
35
|
+
# inconsistent MRO), so subscripting here just returns the class and
|
|
36
|
+
# Generic mixes in before or after an entrypoint.
|
|
37
|
+
def __class_getitem__(cls, item: Any) -> type:
|
|
38
|
+
return cls
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Entrypoint:
|
|
42
|
+
# The class that wraps ctx, or None to leave it raw.
|
|
43
|
+
_context_class: Callable[[Any], Any] | None = None
|
|
44
|
+
# Handlers workerd calls with raw JS arguments, which get converted.
|
|
45
|
+
_raw_handlers: tuple[str, ...] = ()
|
|
46
|
+
# Handlers workerd calls as on_<name> on dates before 2025-08-14, from
|
|
47
|
+
# makeEntrypointProxyHandler in workerd's python-entrypoint-helper.ts.
|
|
48
|
+
_legacy_names: tuple[str, ...] = ()
|
|
49
|
+
|
|
50
|
+
ctx: Any
|
|
51
|
+
env: Any
|
|
52
|
+
|
|
53
|
+
def __init__(self, ctx: Any, env: Any) -> None:
|
|
54
|
+
self.ctx, self.env = _wrap_ctx_and_env(type(self), ctx, env)
|
|
55
|
+
|
|
56
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
57
|
+
super().__init_subclass__(**kwargs)
|
|
58
|
+
if "__init__" in cls.__dict__:
|
|
59
|
+
init = _init_with_wrapped_args(cls.__dict__["__init__"])
|
|
60
|
+
setattr(cls, "__init__", init) # noqa: B010 (ty rejects the plain assignment)
|
|
61
|
+
|
|
62
|
+
# A handler the user names on_<name> for old dates converts too.
|
|
63
|
+
for name in (*cls._raw_handlers, *(f"on_{n}" for n in cls._raw_handlers)):
|
|
64
|
+
handler = getattr(cls, name, None)
|
|
65
|
+
if handler is not None and handler not in _CONVERTING | _ALIASES:
|
|
66
|
+
setattr(cls, name, _converting(handler))
|
|
67
|
+
|
|
68
|
+
# The alias looks the handler up on each call, so it follows subclass
|
|
69
|
+
# overrides and is set once. An on_<name> the user defines wins.
|
|
70
|
+
for name in cls._legacy_names:
|
|
71
|
+
if hasattr(cls, name) and not hasattr(cls, f"on_{name}"):
|
|
72
|
+
setattr(cls, f"on_{name}", _alias(name))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _wrap_ctx_and_env(cls: type[_Entrypoint], ctx: Any, env: Any) -> tuple[Any, Any]:
|
|
76
|
+
if not isinstance(ctx, Binding):
|
|
77
|
+
# Lets ctx.waitUntil take a coroutine, as in the stock SDK. A ctx this
|
|
78
|
+
# wrapped already went through here; workerd ignores a repeat patch.
|
|
79
|
+
_pyodide_entrypoint_helper.patchWaitUntil(ctx)
|
|
80
|
+
if cls._context_class is not None:
|
|
81
|
+
ctx = cls._context_class(ctx)
|
|
82
|
+
|
|
83
|
+
return ctx, _EnvWrapper(env)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _init_with_wrapped_args(init: Callable[..., None]) -> Callable[..., None]:
|
|
87
|
+
# The stock SDK wraps ctx and env before the user's __init__ sees them.
|
|
88
|
+
@functools.wraps(init)
|
|
89
|
+
def wrapped(self: Any, ctx: Any, env: Any, *args: Any, **kwargs: Any) -> None:
|
|
90
|
+
init(self, *_wrap_ctx_and_env(type(self), ctx, env), *args, **kwargs)
|
|
91
|
+
|
|
92
|
+
return wrapped
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Handlers already converting their arguments, and on_<name> aliases, so a
|
|
96
|
+
# subclass doesn't rewrap them.
|
|
97
|
+
_CONVERTING: set[Callable[..., Any]] = set()
|
|
98
|
+
_ALIASES: set[Callable[..., Any]] = set()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _handler_arg(value: Any) -> Any:
|
|
102
|
+
# A binary WebSocket message stays an ArrayBuffer, as in the stock SDK.
|
|
103
|
+
return value if js_name(value) == "ArrayBuffer" else _convert(value)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _converting(handler: Callable[..., Any]) -> Callable[..., Any]:
|
|
107
|
+
# wraps() keeps the handler's signature visible to workerd's relaxed_call.
|
|
108
|
+
@functools.wraps(handler)
|
|
109
|
+
def wrapped(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
110
|
+
return handler(
|
|
111
|
+
self,
|
|
112
|
+
*map(_handler_arg, args),
|
|
113
|
+
**{k: _handler_arg(v) for k, v in kwargs.items()},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
_CONVERTING.add(wrapped)
|
|
117
|
+
return wrapped
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _alias(name: str) -> Callable[..., Any]:
|
|
121
|
+
def alias(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
122
|
+
# workerd passes on_fetch (request, env, ctx). Relaxing drops what the
|
|
123
|
+
# handler's signature doesn't take, as workerd does for a plain fetch.
|
|
124
|
+
return relaxed_call(getattr(self, name), *args, **kwargs)
|
|
125
|
+
|
|
126
|
+
_ALIASES.add(alias)
|
|
127
|
+
return alias
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class WorkerEntrypoint(_Typed[EnvT], _Entrypoint):
|
|
131
|
+
_context_class = ExecutionContext
|
|
132
|
+
_raw_handlers = ("connect", "queue")
|
|
133
|
+
_legacy_names = ("fetch", "connect")
|
|
134
|
+
|
|
135
|
+
ctx: ExecutionContext
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class DurableObject(_Typed[EnvT], _Entrypoint):
|
|
139
|
+
_context_class = DurableObjectContext
|
|
140
|
+
_raw_handlers = (
|
|
141
|
+
"connect",
|
|
142
|
+
"alarm",
|
|
143
|
+
"webSocketMessage",
|
|
144
|
+
"webSocketClose",
|
|
145
|
+
"webSocketError",
|
|
146
|
+
)
|
|
147
|
+
_legacy_names = (
|
|
148
|
+
"fetch",
|
|
149
|
+
"connect",
|
|
150
|
+
"alarm",
|
|
151
|
+
"webSocketMessage",
|
|
152
|
+
"webSocketClose",
|
|
153
|
+
"webSocketError",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
ctx: DurableObjectContext
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class WorkflowEntrypoint(_Typed[EnvT], _Entrypoint):
|
|
160
|
+
# The stock SDK leaves a Workflow's ctx raw.
|
|
161
|
+
|
|
162
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
163
|
+
super().__init_subclass__(**kwargs)
|
|
164
|
+
# Only a run this class defines, so an inherited one isn't rewrapped.
|
|
165
|
+
if "run" in cls.__dict__:
|
|
166
|
+
setattr(cls, "run", wrap_run(cls.__dict__["run"])) # noqa: B010
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
170
|
+
"""For a module-level handler such as `on_fetch`, which workerd calls only
|
|
171
|
+
on dates before 2025-08-14: converts a JS Request argument to a Request
|
|
172
|
+
and wraps the env, as in the stock SDK."""
|
|
173
|
+
|
|
174
|
+
@functools.wraps(func)
|
|
175
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
176
|
+
if args and js_name(args[0]) == "Request":
|
|
177
|
+
args = (Request(args[0]), *args[1:])
|
|
178
|
+
if len(args) > 1:
|
|
179
|
+
args = (args[0], _EnvWrapper(args[1]), *args[2:])
|
|
180
|
+
|
|
181
|
+
return func(*args, **kwargs)
|
|
182
|
+
|
|
183
|
+
return wrapper
|
workers/fetch.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from http import HTTPMethod
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Unpack
|
|
3
|
+
|
|
4
|
+
import js
|
|
5
|
+
from pyodide import __version__ as pyodide_version
|
|
6
|
+
from pyodide.ffi import JsException, to_js
|
|
7
|
+
from pyodide.http import pyfetch
|
|
8
|
+
|
|
9
|
+
from ._js import js_body
|
|
10
|
+
from .request import Request
|
|
11
|
+
from .response import Response
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .types import FetchKwargs
|
|
15
|
+
|
|
16
|
+
__all__ = ["fetch"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _pyfetch_0_26(request: Any, **kwargs: Any) -> Response:
|
|
20
|
+
# Pyodide 0.26.0a2's pyfetch lacks `fetcher`. Copied from Pyodide d3f99e1d.
|
|
21
|
+
custom_fetch = kwargs.pop("fetcher", None) or js.fetch
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
return Response(
|
|
25
|
+
await custom_fetch(
|
|
26
|
+
request, to_js(kwargs, dict_converter=js.Object.fromEntries)
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
except JsException as error:
|
|
30
|
+
raise OSError(error.message) from None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def fetch(
|
|
34
|
+
resource: "str | Request | js.Request", **options: "Unpack[FetchKwargs]"
|
|
35
|
+
) -> Response:
|
|
36
|
+
if isinstance(resource, Request):
|
|
37
|
+
resource = resource.js_object
|
|
38
|
+
kwargs: dict[str, Any] = dict(options)
|
|
39
|
+
if isinstance(kwargs.get("method"), HTTPMethod):
|
|
40
|
+
kwargs["method"] = kwargs["method"].value
|
|
41
|
+
|
|
42
|
+
# The body conversion Request() does: FormData and Blob as their JS
|
|
43
|
+
# objects, and bytes lent as a buffer while the request is sent.
|
|
44
|
+
body = kwargs.pop("body", None)
|
|
45
|
+
send = _pyfetch_0_26 if pyodide_version == "0.26.0a2" else pyfetch
|
|
46
|
+
with js_body(body) as converted:
|
|
47
|
+
if body is not None:
|
|
48
|
+
kwargs["body"] = converted
|
|
49
|
+
response = await send(resource, **kwargs)
|
|
50
|
+
|
|
51
|
+
return Response(response.js_response)
|
workers/formdata.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from collections.abc import Generator, MutableMapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import js
|
|
5
|
+
|
|
6
|
+
from ._js import js_name
|
|
7
|
+
from .blob import Blob, File
|
|
8
|
+
|
|
9
|
+
__all__ = ["FormData", "FormDataValue"]
|
|
10
|
+
|
|
11
|
+
type FormDataValue = str | js.Blob | Blob
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FormData(MutableMapping[str, Any]):
|
|
15
|
+
"""Key/value pairs for forms, following multidict and the JS FormData API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, form_data: "js.FormData | dict[str, Any] | None" = None) -> None:
|
|
18
|
+
if not form_data:
|
|
19
|
+
self._js_form_data = js.FormData.new()
|
|
20
|
+
elif isinstance(form_data, dict):
|
|
21
|
+
self._js_form_data = js.FormData.new()
|
|
22
|
+
for key, value in form_data.items():
|
|
23
|
+
self._js_form_data.append(key, _to_js(value))
|
|
24
|
+
elif js_name(form_data) == "FormData":
|
|
25
|
+
self._js_form_data = form_data
|
|
26
|
+
else:
|
|
27
|
+
raise TypeError(
|
|
28
|
+
"Expected form_data to be a dict or an instance of FormData"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __getitem__(self, key: str) -> Any:
|
|
32
|
+
# The stock SDK returns JS null for a missing key.
|
|
33
|
+
if not self._js_form_data.has(key):
|
|
34
|
+
raise KeyError(key)
|
|
35
|
+
return _to_python(self._js_form_data.get(key))
|
|
36
|
+
|
|
37
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
38
|
+
if isinstance(value, list):
|
|
39
|
+
raise TypeError("Expected single item in arguments to FormData.__setitem__")
|
|
40
|
+
self._js_form_data.set(key, _to_js(value))
|
|
41
|
+
|
|
42
|
+
def __delitem__(self, key: str) -> None:
|
|
43
|
+
self.delete(key)
|
|
44
|
+
|
|
45
|
+
def __contains__(self, key: object) -> bool:
|
|
46
|
+
return self._js_form_data.has(key)
|
|
47
|
+
|
|
48
|
+
def __iter__(self) -> Generator[str]:
|
|
49
|
+
yield from self.keys()
|
|
50
|
+
|
|
51
|
+
def __len__(self) -> int:
|
|
52
|
+
# The stock SDK calls len() on a generator here and raises TypeError.
|
|
53
|
+
return sum(1 for _ in self.keys())
|
|
54
|
+
|
|
55
|
+
def append(self, key: str, value: Any, filename: str | None = None) -> None:
|
|
56
|
+
self._js_form_data.append(key, _to_js(value), filename)
|
|
57
|
+
|
|
58
|
+
def delete(self, key: str) -> None:
|
|
59
|
+
self._js_form_data.delete(key)
|
|
60
|
+
|
|
61
|
+
# keys, values and items return generators, as in the stock SDK, where
|
|
62
|
+
# Mapping promises views.
|
|
63
|
+
def keys(self) -> Generator[str]: # ty: ignore[invalid-method-override]
|
|
64
|
+
yield from self._js_form_data.keys()
|
|
65
|
+
|
|
66
|
+
def values(self) -> Generator[Any]: # ty: ignore[invalid-method-override]
|
|
67
|
+
for value in self._js_form_data.values():
|
|
68
|
+
yield _to_python(value)
|
|
69
|
+
|
|
70
|
+
def items(self) -> Generator[tuple[str, Any]]: # ty: ignore[invalid-method-override]
|
|
71
|
+
for key, value in self._js_form_data.entries():
|
|
72
|
+
yield key, _to_python(value)
|
|
73
|
+
|
|
74
|
+
def get_all(self, key: str) -> list[Any]:
|
|
75
|
+
return [_to_python(value) for value in self._js_form_data.getAll(key)]
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def js_object(self) -> "js.FormData":
|
|
79
|
+
return self._js_form_data
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _to_js(value: Any) -> Any:
|
|
83
|
+
return value.js_object if isinstance(value, Blob) else value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# The Python wrapper for each JS value FormData can hold besides a string.
|
|
87
|
+
_WRAPPERS: dict[str | None, type[Blob]] = {"File": File, "Blob": Blob}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _to_python(value: Any) -> Any:
|
|
91
|
+
wrapper = _WRAPPERS.get(js_name(value))
|
|
92
|
+
return value if wrapper is None else wrapper(value)
|
workers/py.typed
ADDED
|
File without changes
|
workers/request.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from http import HTTPMethod
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Unpack
|
|
4
|
+
|
|
5
|
+
import js
|
|
6
|
+
|
|
7
|
+
from ._body import BodyMixin
|
|
8
|
+
from ._js import js_body, js_name, none_if_null, to_js_headers
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .types import FetchKwargs
|
|
12
|
+
|
|
13
|
+
__all__ = ["Request"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Request(BodyMixin):
|
|
17
|
+
def __init__(
|
|
18
|
+
self, input: "Request | str | js.Request", **options: "Unpack[FetchKwargs]"
|
|
19
|
+
) -> None:
|
|
20
|
+
if js_name(input) == "Request":
|
|
21
|
+
if options:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"Expected no options when constructing Request from a js.Request"
|
|
24
|
+
)
|
|
25
|
+
self._js_request: Any = input
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
js_options: dict[str, Any] = dict(options)
|
|
29
|
+
if isinstance(js_options.get("method"), HTTPMethod):
|
|
30
|
+
js_options["method"] = js_options["method"].value
|
|
31
|
+
if "headers" in js_options:
|
|
32
|
+
js_options["headers"] = to_js_headers(js_options["headers"])
|
|
33
|
+
|
|
34
|
+
body = js_options.pop("body", None)
|
|
35
|
+
with js_body(body) as converted:
|
|
36
|
+
if body is not None:
|
|
37
|
+
js_options["body"] = converted
|
|
38
|
+
self._js_request = js.Request.new(
|
|
39
|
+
input.js_object if isinstance(input, Request) else input,
|
|
40
|
+
**js_options,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return (
|
|
45
|
+
f"Request(method={self._js_request.method!r}, url={self._js_request.url!r})"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def js_object(self) -> "js.Request":
|
|
50
|
+
return self._js_request
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def body(self) -> "js.ReadableStream":
|
|
54
|
+
return self.js_object.body
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def body_used(self) -> bool:
|
|
58
|
+
return self.js_object.bodyUsed
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def cf(self) -> Any:
|
|
62
|
+
"""Cloudflare's properties of an incoming request, or None if absent."""
|
|
63
|
+
return none_if_null(self.js_object.cf)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def cache(self) -> str:
|
|
67
|
+
return self.js_object.cache
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def credentials(self) -> str:
|
|
71
|
+
return self.js_object.credentials
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def destination(self) -> str:
|
|
75
|
+
return self.js_object.destination
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def integrity(self) -> str:
|
|
79
|
+
return self.js_object.integrity
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def is_history_navigation(self) -> bool:
|
|
83
|
+
return self.js_object.isHistoryNavigation
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def keepalive(self) -> bool:
|
|
87
|
+
return self.js_object.keepalive
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def method(self) -> HTTPMethod | str:
|
|
91
|
+
# HTTPMethod for a standard method. The stock SDK raises KeyError on
|
|
92
|
+
# any other; this returns the method string instead.
|
|
93
|
+
method = self.js_object.method
|
|
94
|
+
return HTTPMethod[method] if method in HTTPMethod.__members__ else method
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def mode(self) -> str:
|
|
98
|
+
return self.js_object.mode
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def redirect(self) -> str:
|
|
102
|
+
return self.js_object.redirect
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def referrer(self) -> str:
|
|
106
|
+
return self.js_object.referrer
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def referrer_policy(self) -> str:
|
|
110
|
+
return self.js_object.referrerPolicy
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def url(self) -> str:
|
|
114
|
+
return self.js_object.url
|
|
115
|
+
|
|
116
|
+
def _raise_if_failed(self) -> None:
|
|
117
|
+
if self.body_used:
|
|
118
|
+
raise OSError("Body already used")
|
|
119
|
+
|
|
120
|
+
async def buffer(self) -> "js.ArrayBuffer":
|
|
121
|
+
self._raise_if_failed()
|
|
122
|
+
return await self.js_object.arrayBuffer()
|
|
123
|
+
|
|
124
|
+
async def bytes(self) -> bytes:
|
|
125
|
+
self._raise_if_failed()
|
|
126
|
+
return (await self.buffer()).to_bytes()
|
|
127
|
+
|
|
128
|
+
def clone(self) -> "Request":
|
|
129
|
+
self._raise_if_failed()
|
|
130
|
+
return Request(self.js_object.clone())
|
|
131
|
+
|
|
132
|
+
async def json(self, **kwargs: Any) -> Any:
|
|
133
|
+
self._raise_if_failed()
|
|
134
|
+
return json.loads(await self.text(), **kwargs)
|
|
135
|
+
|
|
136
|
+
async def text(self) -> str:
|
|
137
|
+
self._raise_if_failed()
|
|
138
|
+
return await self.js_object.text()
|