workers-runtime-sdk 1.1.0__tar.gz → 1.1.2__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.
@@ -2,6 +2,27 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.1.2 (2026-04-21)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Make top level asgi import work with snapshots
10
+ ([#93](https://github.com/cloudflare/workers-py/pull/93),
11
+ [`3dd4115`](https://github.com/cloudflare/workers-py/commit/3dd41151d201aca4e1b895638fd3926eb1c68756))
12
+
13
+
14
+ ## v1.1.1 (2026-03-18)
15
+
16
+ ### Bug Fixes
17
+
18
+ - Fix Python ASGI adaptor to handle streaming responses correctly
19
+ ([#82](https://github.com/cloudflare/workers-py/pull/82),
20
+ [`d3ea87a`](https://github.com/cloudflare/workers-py/commit/d3ea87aff37c7a833f0602cc2a8018f1d5dde91b))
21
+
22
+ - Fix streaming responses in asgi module ([#82](https://github.com/cloudflare/workers-py/pull/82),
23
+ [`d3ea87a`](https://github.com/cloudflare/workers-py/commit/d3ea87aff37c7a833f0602cc2a8018f1d5dde91b))
24
+
25
+
5
26
  ## v1.1.0 (2026-03-12)
6
27
 
7
28
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workers-runtime-sdk
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Python SDK for Cloudflare Workers
5
5
  Project-URL: Homepage, https://github.com/cloudflare/workers-py
6
6
  Project-URL: Bug Tracker, https://github.com/cloudflare/workers-py/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "workers-runtime-sdk"
7
- version = "1.1.0"
7
+ version = "1.1.2"
8
8
  description = "Python SDK for Cloudflare Workers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -116,7 +116,12 @@ async def start_application(app):
116
116
 
117
117
 
118
118
  async def process_request(
119
- app: Any, req: "Request | js.Request", env: Any, ctx: Context
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 is_sse
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
- # Convert body to JS buffer
185
- px = create_proxy(body)
186
- buf = px.getBuffer()
187
- px.destroy()
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 to handle SSE
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
- await writer.close() # Close the writer
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,16 @@ 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())
223
+ app_task = create_proxy(create_task(run_app()))
224
224
 
225
- # Wait for the result (the response)
226
- response = await result
225
+ from workers import wait_until
227
226
 
228
- # For non-SSE responses, we need to wait for the application to complete
229
- if not is_sse:
230
- await app_task
231
- else: # noqa: PLR5501
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
227
+ wait_until(app_task)
228
+
229
+ try:
230
+ return await result
231
+ finally:
232
+ app_task.destroy()
239
233
 
240
234
 
241
235
  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 == "waitUntil":
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}")
@@ -210,7 +210,7 @@ wheels = [
210
210
 
211
211
  [[package]]
212
212
  name = "workers-runtime-sdk"
213
- version = "1.0.1"
213
+ version = "1.1.0"
214
214
  source = { editable = "." }
215
215
 
216
216
  [package.dev-dependencies]