workers-runtime-sdk 1.1.0__tar.gz → 1.1.1__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.1.0 → workers_runtime_sdk-1.1.1}/CHANGELOG.md +12 -0
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/PKG-INFO +1 -1
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/pyproject.toml +1 -1
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/src/asgi.py +41 -50
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/src/workers/__init__.py +2 -1
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/uv.lock +1 -1
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/.gitignore +0 -0
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/README.md +0 -0
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/src/_cloudflare_compat_flags.pyi +0 -0
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/src/_pyodide_entrypoint_helper.pyi +0 -0
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/src/workers/_workers.py +0 -0
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/src/workers/py.typed +0 -0
- {workers_runtime_sdk-1.1.0 → workers_runtime_sdk-1.1.1}/src/workers/workflows.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.1.1 (2026-03-18)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Fix Python ASGI adaptor to handle streaming responses correctly
|
|
10
|
+
([#82](https://github.com/cloudflare/workers-py/pull/82),
|
|
11
|
+
[`d3ea87a`](https://github.com/cloudflare/workers-py/commit/d3ea87aff37c7a833f0602cc2a8018f1d5dde91b))
|
|
12
|
+
|
|
13
|
+
- Fix streaming responses in asgi module ([#82](https://github.com/cloudflare/workers-py/pull/82),
|
|
14
|
+
[`d3ea87a`](https://github.com/cloudflare/workers-py/commit/d3ea87aff37c7a833f0602cc2a8018f1d5dde91b))
|
|
15
|
+
|
|
16
|
+
|
|
5
17
|
## v1.1.0 (2026-03-12)
|
|
6
18
|
|
|
7
19
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workers-runtime-sdk
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
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 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import js
|
|
9
9
|
|
|
10
|
-
from workers import Context, Request
|
|
10
|
+
from workers import Context, Request, wait_until
|
|
11
11
|
|
|
12
12
|
ASGI = {"spec_version": "2.0", "version": "3.0"}
|
|
13
13
|
logger = logging.getLogger("asgi")
|
|
@@ -116,7 +116,12 @@ async def start_application(app):
|
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
async def process_request(
|
|
119
|
-
app: Any,
|
|
119
|
+
app: Any,
|
|
120
|
+
req: "Request | js.Request",
|
|
121
|
+
env: Any,
|
|
122
|
+
# added for waitUntil, but not used anymore
|
|
123
|
+
# TODO(later): remove this parameter after unvendoring Python SDK from workerd
|
|
124
|
+
ctx: Context | None,
|
|
120
125
|
) -> js.Response:
|
|
121
126
|
from js import Object, Response, TransformStream
|
|
122
127
|
from pyodide.ffi import create_proxy
|
|
@@ -124,9 +129,11 @@ async def process_request(
|
|
|
124
129
|
status = None
|
|
125
130
|
headers = None
|
|
126
131
|
result = Future()
|
|
127
|
-
is_sse = False
|
|
128
132
|
finished_response = Event()
|
|
129
133
|
|
|
134
|
+
# Streaming state — initialized lazily on first body chunk with more_body=True.
|
|
135
|
+
writer = None
|
|
136
|
+
|
|
130
137
|
receive_queue = Queue()
|
|
131
138
|
if req.body:
|
|
132
139
|
async for data in req.body:
|
|
@@ -148,60 +155,52 @@ async def process_request(
|
|
|
148
155
|
message = {"type": "http.disconnect"}
|
|
149
156
|
return message
|
|
150
157
|
|
|
151
|
-
# Create a transform stream for handling streaming responses
|
|
152
|
-
transform_stream = TransformStream.new()
|
|
153
|
-
readable = transform_stream.readable
|
|
154
|
-
writable = transform_stream.writable
|
|
155
|
-
writer = writable.getWriter()
|
|
156
|
-
|
|
157
158
|
async def send(got):
|
|
158
159
|
nonlocal status
|
|
159
160
|
nonlocal headers
|
|
160
|
-
nonlocal
|
|
161
|
+
nonlocal writer
|
|
161
162
|
|
|
162
163
|
if got["type"] == "http.response.start":
|
|
163
164
|
status = got["status"]
|
|
164
165
|
# Like above, we need to convert byte-pairs into string explicitly.
|
|
165
166
|
headers = [(k.decode(), v.decode()) for k, v in got["headers"]]
|
|
166
|
-
# Check if this is a server-sent events response
|
|
167
|
-
for k, v in headers:
|
|
168
|
-
if k.lower() == "content-type" and v.lower().startswith(
|
|
169
|
-
"text/event-stream"
|
|
170
|
-
):
|
|
171
|
-
is_sse = True
|
|
172
|
-
break
|
|
173
|
-
if is_sse:
|
|
174
|
-
# For SSE, create and return the response immediately after http.response.start
|
|
175
|
-
resp = Response.new(
|
|
176
|
-
readable, headers=Object.fromEntries(headers), status=status
|
|
177
|
-
)
|
|
178
|
-
result.set_result(resp)
|
|
179
167
|
|
|
180
168
|
elif got["type"] == "http.response.body":
|
|
181
169
|
body = got["body"]
|
|
182
170
|
more_body = got.get("more_body", False)
|
|
183
171
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if is_sse:
|
|
190
|
-
# For SSE, write chunk to the stream
|
|
191
|
-
await writer.write(buf.data)
|
|
192
|
-
# If this is the last chunk, close the writer
|
|
172
|
+
if writer is not None:
|
|
173
|
+
# Already in streaming mode — write chunk to the stream.
|
|
174
|
+
with acquire_js_buffer(body) as jsbytes:
|
|
175
|
+
await writer.write(jsbytes.slice())
|
|
193
176
|
if not more_body:
|
|
194
177
|
await writer.close()
|
|
195
178
|
finished_response.set()
|
|
179
|
+
elif more_body:
|
|
180
|
+
# First body chunk with more data coming — switch to streaming.
|
|
181
|
+
# Create a TransformStream so the runtime can start consuming
|
|
182
|
+
# body chunks as they are written.
|
|
183
|
+
transform_stream = TransformStream.new()
|
|
184
|
+
readable = transform_stream.readable
|
|
185
|
+
writer = transform_stream.writable.getWriter()
|
|
186
|
+
resp = Response.new(
|
|
187
|
+
readable, headers=Object.fromEntries(headers), status=status
|
|
188
|
+
)
|
|
189
|
+
result.set_result(resp)
|
|
190
|
+
with acquire_js_buffer(body) as jsbytes:
|
|
191
|
+
await writer.write(jsbytes.slice())
|
|
196
192
|
else:
|
|
193
|
+
# Complete body in a single chunk
|
|
194
|
+
px = create_proxy(body)
|
|
195
|
+
buf = px.getBuffer()
|
|
196
|
+
px.destroy()
|
|
197
197
|
resp = Response.new(
|
|
198
198
|
buf.data, headers=Object.fromEntries(headers), status=status
|
|
199
199
|
)
|
|
200
200
|
result.set_result(resp)
|
|
201
|
-
await writer.close()
|
|
202
201
|
finished_response.set()
|
|
203
202
|
|
|
204
|
-
# Run the application in the background
|
|
203
|
+
# Run the application in the background
|
|
205
204
|
async def run_app():
|
|
206
205
|
try:
|
|
207
206
|
await app(request_to_scope(req, env), receive, send)
|
|
@@ -212,7 +211,8 @@ async def process_request(
|
|
|
212
211
|
except Exception as e:
|
|
213
212
|
if not result.done():
|
|
214
213
|
result.set_exception(e)
|
|
215
|
-
|
|
214
|
+
if writer is not None:
|
|
215
|
+
await writer.close()
|
|
216
216
|
finished_response.set()
|
|
217
217
|
else:
|
|
218
218
|
# Response already sent — exception can't be propagated to the
|
|
@@ -220,22 +220,13 @@ async def process_request(
|
|
|
220
220
|
logger.exception("Exception in ASGI application after response started")
|
|
221
221
|
|
|
222
222
|
# Create task to run the application in the background
|
|
223
|
-
app_task = create_task(run_app())
|
|
224
|
-
|
|
225
|
-
# Wait for the result (the response)
|
|
226
|
-
response = await result
|
|
223
|
+
app_task = create_proxy(create_task(run_app()))
|
|
224
|
+
wait_until(app_task)
|
|
227
225
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if ctx is not None:
|
|
233
|
-
ctx.waitUntil(create_proxy(app_task))
|
|
234
|
-
else:
|
|
235
|
-
raise RuntimeError(
|
|
236
|
-
"Server-Side-Events require ctx to be passed to asgi.fetch"
|
|
237
|
-
)
|
|
238
|
-
return response
|
|
226
|
+
try:
|
|
227
|
+
return await result
|
|
228
|
+
finally:
|
|
229
|
+
app_task.destroy()
|
|
239
230
|
|
|
240
231
|
|
|
241
232
|
async def process_websocket(app: Any, req: "Request | js.Request") -> js.Response:
|
|
@@ -52,6 +52,7 @@ __all__ = [
|
|
|
52
52
|
"python_from_rpc",
|
|
53
53
|
"python_to_rpc",
|
|
54
54
|
"waitUntil",
|
|
55
|
+
"wait_until",
|
|
55
56
|
]
|
|
56
57
|
|
|
57
58
|
|
|
@@ -59,7 +60,7 @@ def __getattr__(key):
|
|
|
59
60
|
if key == "env":
|
|
60
61
|
cloudflare_workers = import_from_javascript("cloudflare:workers")
|
|
61
62
|
return cloudflare_workers.env
|
|
62
|
-
if key
|
|
63
|
+
if key in ("wait_until", "waitUntil"):
|
|
63
64
|
cloudflare_workers = import_from_javascript("cloudflare:workers")
|
|
64
65
|
return cloudflare_workers.waitUntil
|
|
65
66
|
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|