workers-runtime-sdk 1.5.3__tar.gz → 1.6.0__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.3 → workers_runtime_sdk-1.6.0}/CHANGELOG.md +17 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/PKG-INFO +2 -2
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/pyproject.toml +4 -4
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/types.py +4 -4
- workers_runtime_sdk-1.6.0/src/workers/wsgi.py +372 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/uv.lock +21 -2
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/.gitignore +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/AGENTS.md +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/README.md +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/_cloudflare_compat_flags.pyi +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/_pyodide_entrypoint_helper.pyi +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/_workers_sdk_entropy_import_context.pth +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/_workers_sdk_entropy_import_context.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/asgi.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/__init__.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/_workers.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/blob.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/fetch.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/formdata.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/py.typed +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/request.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/response.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/rpc.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/utils.py +0 -0
- {workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/workers/workflows.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.6.0 (2026-07-08)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Add wsgi.py analogous to asgi.py ([#145](https://github.com/cloudflare/workers-py/pull/145),
|
|
10
|
+
[`247f9ff`](https://github.com/cloudflare/workers-py/commit/247f9ff5387eb2d286c65c1199f3ff51938578fa))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v1.5.4 (2026-07-08)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Relax required-python version to support Python 3.11
|
|
18
|
+
([#149](https://github.com/cloudflare/workers-py/pull/149),
|
|
19
|
+
[`31d0f69`](https://github.com/cloudflare/workers-py/commit/31d0f696b2f64daf27ef629242957dff96bdde81))
|
|
20
|
+
|
|
21
|
+
|
|
5
22
|
## v1.5.3 (2026-07-03)
|
|
6
23
|
|
|
7
24
|
### Bug Fixes
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workers-runtime-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
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
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
|
|
13
13
|
# workers-runtime-sdk
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "workers-runtime-sdk"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.6.0"
|
|
8
8
|
description = "Python SDK for Cloudflare Workers"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Programming Language :: Python :: 3",
|
|
13
13
|
"License :: OSI Approved :: MIT License",
|
|
@@ -27,7 +27,7 @@ dev = [
|
|
|
27
27
|
"Bug Tracker" = "https://github.com/cloudflare/workers-py/issues"
|
|
28
28
|
|
|
29
29
|
[tool.ruff]
|
|
30
|
-
target-version = "
|
|
30
|
+
target-version = "py311"
|
|
31
31
|
lint.select = [
|
|
32
32
|
"B0", # bugbear (all B0* checks enabled by default)
|
|
33
33
|
"B904", # bugbear (Within an except clause, raise exceptions with raise ... from err)
|
|
@@ -59,7 +59,7 @@ lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments = true
|
|
|
59
59
|
|
|
60
60
|
[tool.mypy]
|
|
61
61
|
packages = ["workers_runtime_sdk"]
|
|
62
|
-
python_version = "3.
|
|
62
|
+
python_version = "3.11"
|
|
63
63
|
|
|
64
64
|
show_error_codes = true
|
|
65
65
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections.abc import Awaitable
|
|
2
2
|
from http import HTTPMethod
|
|
3
|
-
from typing import Any, Protocol, TypedDict
|
|
3
|
+
from typing import Any, Protocol, TypeAlias, TypedDict
|
|
4
4
|
|
|
5
5
|
import js
|
|
6
6
|
from pyodide.ffi import JsBuffer
|
|
@@ -13,11 +13,11 @@ class Context(Protocol):
|
|
|
13
13
|
def waitUntil(self, other: Awaitable[Any]) -> None: ...
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
JSBody: TypeAlias = (
|
|
17
17
|
"js.Blob | JsBuffer | js.FormData | js.ReadableStream | js.URLSearchParams"
|
|
18
18
|
)
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
Body: TypeAlias = "str | FormData | JSBody"
|
|
20
|
+
Headers: TypeAlias = "dict[str, str] | list[tuple[str, str]] | js.Headers"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
# https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Callable, Iterator
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import unquote, urlsplit
|
|
7
|
+
|
|
8
|
+
import js
|
|
9
|
+
|
|
10
|
+
from workers import Context, Request
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("wsgi")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _wsgi_native_string(value: str) -> str:
|
|
16
|
+
"""Convert a (possibly non-ASCII) Python ``str`` into a WSGI "native string".
|
|
17
|
+
|
|
18
|
+
PEP 3333 requires that strings handed to a WSGI application use the
|
|
19
|
+
bytes-in-unicode convention: the string contains characters whose code
|
|
20
|
+
points correspond to the raw bytes (i.e. it is decoded with ``latin-1``).
|
|
21
|
+
Frameworks such as Werkzeug/Flask undo this by re-encoding with ``latin-1``
|
|
22
|
+
and decoding with ``utf-8``.
|
|
23
|
+
"""
|
|
24
|
+
return value.encode("utf-8").decode("latin-1")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _ReadableStreamInput(io.RawIOBase):
|
|
28
|
+
"""A blocking, file-like ``wsgi.input`` backed by an async ``ReadableStream``.
|
|
29
|
+
|
|
30
|
+
WSGI apps read the request body synchronously, but the Workers body is an
|
|
31
|
+
async ``ReadableStream``. We bridge the two lazily using Pyodide's
|
|
32
|
+
``run_sync`` (JSPI stack switching): each ``readinto`` pulls only as much
|
|
33
|
+
from the stream as the application asks for, so the body is never fully
|
|
34
|
+
buffered up-front.
|
|
35
|
+
|
|
36
|
+
This only works while a JSPI suspender is on the stack, which is the case
|
|
37
|
+
here because the WSGI app runs synchronously inside the async ``fetch``
|
|
38
|
+
handler.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, js_body: "js.ReadableStream") -> None:
|
|
42
|
+
self._reader = js_body.getReader()
|
|
43
|
+
self._buf = bytearray()
|
|
44
|
+
self._eof = False
|
|
45
|
+
|
|
46
|
+
def readable(self) -> bool:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def _fill(self) -> None:
|
|
50
|
+
from pyodide.ffi import run_sync
|
|
51
|
+
|
|
52
|
+
while not self._buf and not self._eof:
|
|
53
|
+
result = run_sync(self._reader.read())
|
|
54
|
+
if result.done:
|
|
55
|
+
self._eof = True
|
|
56
|
+
elif result.value is not None:
|
|
57
|
+
self._buf.extend(result.value.to_bytes())
|
|
58
|
+
|
|
59
|
+
def readinto(self, b) -> int:
|
|
60
|
+
self._fill()
|
|
61
|
+
if not self._buf:
|
|
62
|
+
return 0
|
|
63
|
+
n = min(len(b), len(self._buf))
|
|
64
|
+
b[:n] = self._buf[:n]
|
|
65
|
+
del self._buf[:n]
|
|
66
|
+
return n
|
|
67
|
+
|
|
68
|
+
def close(self) -> None:
|
|
69
|
+
if not self.closed:
|
|
70
|
+
self._release_reader()
|
|
71
|
+
super().close()
|
|
72
|
+
|
|
73
|
+
def __del__(self):
|
|
74
|
+
self.close()
|
|
75
|
+
|
|
76
|
+
def _release_reader(self) -> None:
|
|
77
|
+
"""Discard the request body and release the underlying reader.
|
|
78
|
+
|
|
79
|
+
If the body was fully consumed we just release the lock. Otherwise we
|
|
80
|
+
``cancel()`` the reader to drop the remainder; ``cancel()`` returns a JS
|
|
81
|
+
promise that the runtime settles on its own, so we don't await it.
|
|
82
|
+
"""
|
|
83
|
+
from pyodide.ffi import run_sync
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
if self._eof:
|
|
87
|
+
self._reader.releaseLock()
|
|
88
|
+
else:
|
|
89
|
+
run_sync(self._reader.cancel())
|
|
90
|
+
except Exception: # noqa: BLE001 - best-effort cleanup
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _make_wsgi_input(req: "Request | js.Request") -> "io.BufferedIOBase | None":
|
|
95
|
+
"""Build a lazy, stream-backed ``wsgi.input`` if the runtime supports it.
|
|
96
|
+
|
|
97
|
+
Returns ``None`` when lazy streaming is unavailable, signalling the caller
|
|
98
|
+
to fall back to pre-buffering the body.
|
|
99
|
+
"""
|
|
100
|
+
if not req.body:
|
|
101
|
+
return io.BytesIO(b"")
|
|
102
|
+
# `req.body` is a JS ReadableStream for both workers.Request and js.Request.
|
|
103
|
+
return io.BufferedReader(_ReadableStreamInput(req.body))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _read_body(req: "Request | js.Request") -> bytes:
|
|
107
|
+
"""Read the entire request body into memory (fallback when streaming is off)."""
|
|
108
|
+
if not req.body:
|
|
109
|
+
return b""
|
|
110
|
+
chunks = [data.to_bytes() async for data in req.body]
|
|
111
|
+
return b"".join(chunks)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_environ(
|
|
115
|
+
req: "Request | js.Request",
|
|
116
|
+
env: dict[str, Any],
|
|
117
|
+
body: "bytes | io.IOBase",
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
# `body` may be raw bytes or a file-like `wsgi.input` stream
|
|
120
|
+
if isinstance(body, (bytes, bytearray)):
|
|
121
|
+
wsgi_input: Any = io.BytesIO(body)
|
|
122
|
+
content_length_fallback: int | None = len(body)
|
|
123
|
+
else:
|
|
124
|
+
wsgi_input = body
|
|
125
|
+
content_length_fallback = None
|
|
126
|
+
|
|
127
|
+
req_headers = req.headers.items() if isinstance(req, Request) else req.headers
|
|
128
|
+
|
|
129
|
+
url = urlsplit(req.url)
|
|
130
|
+
scheme = url.scheme
|
|
131
|
+
if not scheme:
|
|
132
|
+
raise ValueError(f"Request URL is not absolute: {req.url!r}")
|
|
133
|
+
|
|
134
|
+
# PATH_INFO is the URL-decoded path expressed as a WSGI native string.
|
|
135
|
+
path_info = _wsgi_native_string(unquote(url.path or "/"))
|
|
136
|
+
# QUERY_STRING stays percent-encoded per the spec.
|
|
137
|
+
query_string = url.query
|
|
138
|
+
|
|
139
|
+
server_port = str(url.port) if url.port else ("443" if scheme == "https" else "80")
|
|
140
|
+
|
|
141
|
+
method = req.method
|
|
142
|
+
# `workers.Request.method` is an `http.HTTPMethod` (a str subclass); coerce
|
|
143
|
+
# to a plain string so frameworks comparing against literals behave.
|
|
144
|
+
method = str(method.value) if hasattr(method, "value") else str(method)
|
|
145
|
+
|
|
146
|
+
environ: dict[str, Any] = {
|
|
147
|
+
"REQUEST_METHOD": method,
|
|
148
|
+
"SCRIPT_NAME": "",
|
|
149
|
+
"PATH_INFO": path_info,
|
|
150
|
+
"QUERY_STRING": query_string,
|
|
151
|
+
# A request URL is always absolute here, so hostname is populated; keep a
|
|
152
|
+
# fallback since WSGI requires SERVER_NAME to be non-empty.
|
|
153
|
+
"SERVER_NAME": url.hostname,
|
|
154
|
+
"SERVER_PORT": server_port,
|
|
155
|
+
"SERVER_PROTOCOL": "HTTP/1.1",
|
|
156
|
+
"wsgi.version": (1, 0),
|
|
157
|
+
"wsgi.url_scheme": scheme,
|
|
158
|
+
"wsgi.input": wsgi_input,
|
|
159
|
+
"wsgi.errors": sys.stderr,
|
|
160
|
+
"wsgi.multithread": False,
|
|
161
|
+
"wsgi.multiprocess": False,
|
|
162
|
+
"wsgi.run_once": False,
|
|
163
|
+
# Cloudflare-specific extension so handlers can reach bindings, mirroring
|
|
164
|
+
# the `scope["env"]` convention used by asgi.py.
|
|
165
|
+
"workers.env": env,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for key, value in req_headers:
|
|
169
|
+
name = key.upper().replace("-", "_")
|
|
170
|
+
if name in ("CONTENT_TYPE", "CONTENT_LENGTH"):
|
|
171
|
+
environ[name] = value
|
|
172
|
+
else:
|
|
173
|
+
http_name = "HTTP_" + name
|
|
174
|
+
if http_name in environ:
|
|
175
|
+
# Repeated headers are folded into a single comma-separated value.
|
|
176
|
+
environ[http_name] += "," + value
|
|
177
|
+
else:
|
|
178
|
+
environ[http_name] = value
|
|
179
|
+
|
|
180
|
+
# Only synthesize CONTENT_LENGTH when the body was pre-buffered; when
|
|
181
|
+
# streaming lazily we don't know the length without consuming it.
|
|
182
|
+
if "CONTENT_LENGTH" not in environ and content_length_fallback:
|
|
183
|
+
environ["CONTENT_LENGTH"] = str(content_length_fallback)
|
|
184
|
+
|
|
185
|
+
return environ
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _to_js_uint8array(chunk: bytes) -> "js.Uint8Array":
|
|
189
|
+
"""Copy Python ``bytes`` into a standalone, JS-owned ``Uint8Array``.
|
|
190
|
+
|
|
191
|
+
A copy (rather than a view over the WASM heap) is required because
|
|
192
|
+
``ReadableStreamDefaultController.enqueue`` keeps the chunk by reference in
|
|
193
|
+
the stream's internal queue until the consumer reads it later, by which
|
|
194
|
+
point ``chunk`` has been released. ``to_js`` copies buffer data out of the
|
|
195
|
+
heap into a fresh ``ArrayBuffer`` (pyodide's ``python2js_buffer``), so its
|
|
196
|
+
result stays valid.
|
|
197
|
+
"""
|
|
198
|
+
from pyodide.ffi import to_js
|
|
199
|
+
|
|
200
|
+
return to_js(chunk)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _close_iterable(iterable: Any) -> None:
|
|
204
|
+
# PEP 3333: if the iterable has a close() method, the server must call it.
|
|
205
|
+
close = getattr(iterable, "close", None)
|
|
206
|
+
if close is not None:
|
|
207
|
+
close()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Sentinel marking exhaustion of the response body iterator.
|
|
211
|
+
_END = object()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _make_streaming_response(
|
|
215
|
+
status: str,
|
|
216
|
+
headers: "list[tuple[str, str]]",
|
|
217
|
+
chunks: "Iterator[bytes]",
|
|
218
|
+
on_close: "Callable[[], None]",
|
|
219
|
+
) -> js.Response:
|
|
220
|
+
"""Build a ``js.Response`` whose body is pulled lazily from *chunks*.
|
|
221
|
+
|
|
222
|
+
Uses a ``ReadableStream`` with a synchronous, pull-based source so response
|
|
223
|
+
bytes are produced on demand as the client reads, instead of buffering the
|
|
224
|
+
whole body in memory. ``on_close`` runs exactly once when the stream is
|
|
225
|
+
exhausted, errored, or cancelled.
|
|
226
|
+
"""
|
|
227
|
+
from js import Headers as JsHeaders
|
|
228
|
+
from js import ReadableStream, Response
|
|
229
|
+
from pyodide.ffi import create_proxy
|
|
230
|
+
|
|
231
|
+
# WSGI status is e.g. "200 OK"; split into code + reason phrase.
|
|
232
|
+
code_str, _, reason = status.partition(" ")
|
|
233
|
+
options: dict[str, Any] = {"status": int(code_str)}
|
|
234
|
+
if reason:
|
|
235
|
+
options["statusText"] = reason
|
|
236
|
+
|
|
237
|
+
js_headers = JsHeaders.new()
|
|
238
|
+
for key, value in headers:
|
|
239
|
+
# `append` (not `set`) preserves repeated headers such as Set-Cookie.
|
|
240
|
+
js_headers.append(key, value)
|
|
241
|
+
options["headers"] = js_headers
|
|
242
|
+
|
|
243
|
+
proxies: list[Any] = []
|
|
244
|
+
done = False
|
|
245
|
+
|
|
246
|
+
def cleanup() -> None:
|
|
247
|
+
nonlocal done
|
|
248
|
+
if done:
|
|
249
|
+
return
|
|
250
|
+
done = True
|
|
251
|
+
try:
|
|
252
|
+
on_close()
|
|
253
|
+
finally:
|
|
254
|
+
for proxy in proxies:
|
|
255
|
+
proxy.destroy()
|
|
256
|
+
|
|
257
|
+
@create_proxy
|
|
258
|
+
def pull(controller: Any) -> None:
|
|
259
|
+
try:
|
|
260
|
+
chunk = next(chunks, _END)
|
|
261
|
+
except Exception as exc: # noqa: BLE001 - forward app errors to the stream
|
|
262
|
+
logger.exception("Exception while streaming WSGI response body")
|
|
263
|
+
cleanup()
|
|
264
|
+
controller.error(str(exc))
|
|
265
|
+
return
|
|
266
|
+
if chunk is _END:
|
|
267
|
+
controller.close()
|
|
268
|
+
cleanup()
|
|
269
|
+
return
|
|
270
|
+
controller.enqueue(_to_js_uint8array(chunk))
|
|
271
|
+
|
|
272
|
+
@create_proxy
|
|
273
|
+
def cancel(_reason: Any = None) -> None:
|
|
274
|
+
cleanup()
|
|
275
|
+
|
|
276
|
+
proxies = [pull, cancel]
|
|
277
|
+
return Response.new(ReadableStream.new(pull=pull, cancel=cancel), **options)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def process_request(
|
|
281
|
+
app: Any,
|
|
282
|
+
req: "Request | js.Request",
|
|
283
|
+
env: Any,
|
|
284
|
+
body: "bytes | io.IOBase",
|
|
285
|
+
) -> js.Response:
|
|
286
|
+
environ = build_environ(req, env, body)
|
|
287
|
+
|
|
288
|
+
response_state: dict[str, Any] = {"status": None, "headers": None}
|
|
289
|
+
# Output from the (rarely used) legacy `write()` callable. Modern frameworks
|
|
290
|
+
# (Flask, Werkzeug, Django) return an iterable instead; when they do use
|
|
291
|
+
# write(), that output is emitted before the iterable's chunks.
|
|
292
|
+
write_chunks: list[bytes] = []
|
|
293
|
+
|
|
294
|
+
def write(chunk: bytes) -> None:
|
|
295
|
+
if response_state["status"] is None:
|
|
296
|
+
raise AssertionError("write() called before start_response()")
|
|
297
|
+
if chunk:
|
|
298
|
+
write_chunks.append(chunk)
|
|
299
|
+
|
|
300
|
+
def start_response(status, response_headers, exc_info=None):
|
|
301
|
+
if exc_info is not None:
|
|
302
|
+
try:
|
|
303
|
+
if response_state["status"] is not None:
|
|
304
|
+
# Headers were already sent; re-raise the original error.
|
|
305
|
+
raise exc_info[1].with_traceback(exc_info[2])
|
|
306
|
+
finally:
|
|
307
|
+
exc_info = None
|
|
308
|
+
elif response_state["status"] is not None:
|
|
309
|
+
raise AssertionError("start_response() called more than once")
|
|
310
|
+
|
|
311
|
+
response_state["status"] = status
|
|
312
|
+
response_state["headers"] = response_headers
|
|
313
|
+
return write
|
|
314
|
+
|
|
315
|
+
result = app(environ, start_response)
|
|
316
|
+
result_iter = iter(result)
|
|
317
|
+
|
|
318
|
+
def close_all() -> None:
|
|
319
|
+
_close_iterable(result)
|
|
320
|
+
try:
|
|
321
|
+
environ["wsgi.input"].close()
|
|
322
|
+
except Exception: # noqa: BLE001 - best-effort cleanup
|
|
323
|
+
logger.exception("Failed to close wsgi.input")
|
|
324
|
+
|
|
325
|
+
# WSGI apps must call start_response before yielding the first body chunk,
|
|
326
|
+
# but some defer it until the first non-empty chunk is produced. Pull that
|
|
327
|
+
# chunk now so status/headers are known before we construct the Response.
|
|
328
|
+
try:
|
|
329
|
+
first_chunk: Any = _END
|
|
330
|
+
for chunk in result_iter:
|
|
331
|
+
if chunk:
|
|
332
|
+
first_chunk = chunk
|
|
333
|
+
break
|
|
334
|
+
except Exception:
|
|
335
|
+
close_all()
|
|
336
|
+
raise
|
|
337
|
+
|
|
338
|
+
if response_state["status"] is None:
|
|
339
|
+
close_all()
|
|
340
|
+
raise RuntimeError("The WSGI application did not call start_response()")
|
|
341
|
+
|
|
342
|
+
def body_chunks() -> "Iterator[bytes]":
|
|
343
|
+
yield from write_chunks
|
|
344
|
+
if first_chunk is not _END:
|
|
345
|
+
yield first_chunk
|
|
346
|
+
for chunk in result_iter:
|
|
347
|
+
if chunk:
|
|
348
|
+
yield chunk
|
|
349
|
+
|
|
350
|
+
return _make_streaming_response(
|
|
351
|
+
response_state["status"], response_state["headers"], body_chunks(), close_all
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
async def fetch(
|
|
356
|
+
app: Any,
|
|
357
|
+
req: "Request | js.Request",
|
|
358
|
+
env: Any,
|
|
359
|
+
# Accepted for parity with asgi.fetch; WSGI has no use for it.
|
|
360
|
+
ctx: Context | None = None,
|
|
361
|
+
) -> js.Response:
|
|
362
|
+
logger.debug("WSGI request: %s %s", req.method, req.url)
|
|
363
|
+
# Prefer lazily streaming the body through `wsgi.input` (no full buffering);
|
|
364
|
+
# fall back to pre-buffering when `run_sync`/JSPI isn't available.
|
|
365
|
+
body: bytes | io.IOBase | None = _make_wsgi_input(req)
|
|
366
|
+
if body is None:
|
|
367
|
+
body = await _read_body(req)
|
|
368
|
+
try:
|
|
369
|
+
return process_request(app, req, env, body)
|
|
370
|
+
except Exception:
|
|
371
|
+
logger.exception("WSGI request failed")
|
|
372
|
+
raise
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
version = 1
|
|
2
2
|
revision = 3
|
|
3
|
-
requires-python = ">=3.
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "colorama"
|
|
@@ -26,6 +26,19 @@ version = "0.8.1"
|
|
|
26
26
|
source = { registry = "https://pypi.org/simple" }
|
|
27
27
|
sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" }
|
|
28
28
|
wheels = [
|
|
29
|
+
{ url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" },
|
|
30
|
+
{ url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" },
|
|
31
|
+
{ url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" },
|
|
32
|
+
{ url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" },
|
|
33
|
+
{ url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" },
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" },
|
|
35
|
+
{ url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" },
|
|
36
|
+
{ url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" },
|
|
37
|
+
{ url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" },
|
|
38
|
+
{ url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" },
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" },
|
|
40
|
+
{ url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" },
|
|
41
|
+
{ url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" },
|
|
29
42
|
{ url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" },
|
|
30
43
|
{ url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" },
|
|
31
44
|
{ url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" },
|
|
@@ -92,6 +105,12 @@ dependencies = [
|
|
|
92
105
|
]
|
|
93
106
|
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
|
|
94
107
|
wheels = [
|
|
108
|
+
{ url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
|
|
109
|
+
{ url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
|
|
110
|
+
{ url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
|
|
111
|
+
{ url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
|
|
112
|
+
{ url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
|
|
113
|
+
{ url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
|
|
95
114
|
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
|
|
96
115
|
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
|
|
97
116
|
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
|
|
@@ -210,7 +229,7 @@ wheels = [
|
|
|
210
229
|
|
|
211
230
|
[[package]]
|
|
212
231
|
name = "workers-runtime-sdk"
|
|
213
|
-
version = "1.
|
|
232
|
+
version = "1.6.0"
|
|
214
233
|
source = { editable = "." }
|
|
215
234
|
|
|
216
235
|
[package.dev-dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/_workers_sdk_entropy_import_context.pth
RENAMED
|
File without changes
|
{workers_runtime_sdk-1.5.3 → workers_runtime_sdk-1.6.0}/src/_workers_sdk_entropy_import_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|