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.
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()