workers-runtime-sdk 1.9.4__tar.gz → 1.9.6__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.
Files changed (212) hide show
  1. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/AGENTS.md +1 -0
  2. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/CHANGELOG.md +18 -0
  3. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/PKG-INFO +1 -1
  4. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/pyproject.toml +1 -1
  5. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/asgi.py +35 -7
  6. workers_runtime_sdk-1.9.6/src/workers/gateway/__init__.py +5 -0
  7. workers_runtime_sdk-1.9.6/src/workers/gateway/options.py +30 -0
  8. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/wsgi.py +44 -11
  9. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/flask-tests/pyproject.toml +7 -1
  10. workers_runtime_sdk-1.9.6/tests/web-frameworks-test/flask-tests/src/test_stream_with_context.py +70 -0
  11. workers_runtime_sdk-1.9.6/tests/workerd-test/asgi/asgi.wd-test +36 -0
  12. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi/tests/test_asgi.py +39 -1
  13. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi/worker.py +60 -0
  14. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi-ws-disconnect/tests/test_ws_disconnect.py +6 -0
  15. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/sdk/tests/test_sdk.py +1 -2
  16. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/wsgi/tests/test_wsgi.py +86 -0
  17. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/wsgi/worker.py +71 -0
  18. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/wsgi/wsgi.wd-test +18 -1
  19. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/uv.lock +1 -1
  20. workers_runtime_sdk-1.9.4/tests/workerd-test/asgi/asgi.wd-test +0 -19
  21. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/.gitignore +0 -0
  22. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/LICENSE +0 -0
  23. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/README.md +0 -0
  24. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/_cloudflare_compat_flags.pyi +0 -0
  25. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/_pyodide_entrypoint_helper.pyi +0 -0
  26. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/_workers_sdk_entropy_import_context.py +0 -0
  27. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/_workers_sdk_package_patches.py +0 -0
  28. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/_workers_sdk_patch_loader.pth +0 -0
  29. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/_workers_sdk_patch_loader.py +0 -0
  30. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/asgi.py +0 -0
  31. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/__init__.py +0 -0
  32. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/blob.py +0 -0
  33. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/entrypoints.py +0 -0
  34. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/fetch.py +0 -0
  35. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/formdata.py +0 -0
  36. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/py.typed +0 -0
  37. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/request.py +0 -0
  38. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/response.py +0 -0
  39. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/rpc.py +0 -0
  40. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/types.py +0 -0
  41. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/utils.py +0 -0
  42. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/workflows.js +0 -0
  43. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/src/workers/workflows.py +0 -0
  44. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/public/data.json +0 -0
  45. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/public/hello.txt +0 -0
  46. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/pyproject.toml +0 -0
  47. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/conftest.py +0 -0
  48. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_analytics_engine.py +0 -0
  49. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_assets.py +0 -0
  50. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_d1.py +0 -0
  51. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_do.py +0 -0
  52. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_hyperdrive_mysql.py +0 -0
  53. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_hyperdrive_postgresql.py +0 -0
  54. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_images.py +0 -0
  55. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_kv.py +0 -0
  56. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_mtls.py +0 -0
  57. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_queue.py +0 -0
  58. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_r2.py +0 -0
  59. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_ratelimit.py +0 -0
  60. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_vectorize.py +0 -0
  61. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_version_metadata.py +0 -0
  62. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/test_workflow.py +0 -0
  63. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/worker.py +0 -0
  64. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/worker_durable_object.py +0 -0
  65. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/src/worker_workflow.py +0 -0
  66. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/bindings-test/wrangler.jsonc +0 -0
  67. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/conftest.py +0 -0
  68. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/test_bindings.py +0 -0
  69. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/test_django.py +0 -0
  70. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/test_django_databases.py +0 -0
  71. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/test_fastapi.py +0 -0
  72. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/test_flask.py +0 -0
  73. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/test_in_workerd.py +0 -0
  74. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/pyproject.toml +0 -0
  75. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/_client.py +0 -0
  76. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/conftest.py +0 -0
  77. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/__init__.py +0 -0
  78. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/api_urls.py +0 -0
  79. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/auth_backend.py +0 -0
  80. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/drf_auth.py +0 -0
  81. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/drf_serializers.py +0 -0
  82. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/drf_throttling.py +0 -0
  83. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/drf_views.py +0 -0
  84. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/forms.py +0 -0
  85. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/middleware.py +0 -0
  86. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/templatetags/__init__.py +0 -0
  87. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/templatetags/custom_tags.py +0 -0
  88. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/urls.py +0 -0
  89. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/django_app/views.py +0 -0
  90. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/templates/base.html +0 -0
  91. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/templates/context_test.html +0 -0
  92. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/templates/csrf_form.html +0 -0
  93. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/templates/hello.html +0 -0
  94. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_async_utilities.py +0 -0
  95. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_auth.py +0 -0
  96. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_caching.py +0 -0
  97. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_cors.py +0 -0
  98. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_csrf.py +0 -0
  99. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_drf_auth_permissions.py +0 -0
  100. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_drf_content_negotiation.py +0 -0
  101. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_drf_serializers.py +0 -0
  102. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_drf_throttling.py +0 -0
  103. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_drf_versioning.py +0 -0
  104. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_drf_views.py +0 -0
  105. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_error_handling.py +0 -0
  106. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_file_uploads.py +0 -0
  107. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_forms.py +0 -0
  108. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_middleware.py +0 -0
  109. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_pagination.py +0 -0
  110. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_request_response.py +0 -0
  111. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_sessions.py +0 -0
  112. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_signals.py +0 -0
  113. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_streaming.py +0 -0
  114. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_templates.py +0 -0
  115. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_url_routing.py +0 -0
  116. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/test_views_basic.py +0 -0
  117. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/src/worker.py +0 -0
  118. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-async/wrangler.jsonc +0 -0
  119. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-databases/pyproject.toml +0 -0
  120. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-databases/src/django_db_config.py +0 -0
  121. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-databases/src/django_db_helpers.py +0 -0
  122. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-databases/src/test_mysql.py +0 -0
  123. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-databases/src/test_postgresql.py +0 -0
  124. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-databases/src/worker.py +0 -0
  125. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/django-databases/wrangler.jsonc +0 -0
  126. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/public/data.json +0 -0
  127. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/public/index.html +0 -0
  128. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/public/style.css +0 -0
  129. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/pyproject.toml +0 -0
  130. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/_client.py +0 -0
  131. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/conftest.py +0 -0
  132. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/static/app.css +0 -0
  133. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/static/data.json +0 -0
  134. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/static/hello.txt +0 -0
  135. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/static/index.html +0 -0
  136. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_assets.py +0 -0
  137. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_error_handling.py +0 -0
  138. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_platform_boundaries.py +0 -0
  139. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_platform_io.py +0 -0
  140. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_platform_lifecycle.py +0 -0
  141. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_response_types.py +0 -0
  142. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_routing.py +0 -0
  143. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_staticfiles.py +0 -0
  144. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_sync_handlers.py +0 -0
  145. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/test_websocket.py +0 -0
  146. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/src/worker.py +0 -0
  147. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/fastapi-tests/wrangler.jsonc +0 -0
  148. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/flask-tests/src/test_response.py +0 -0
  149. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/flask-tests/src/worker.py +0 -0
  150. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/web-frameworks-test/flask-tests/wrangler.jsonc +0 -0
  151. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi/pyproject.toml +0 -0
  152. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi/wrangler.jsonc +0 -0
  153. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi-ws-disconnect/asgi-ws-disconnect.wd-test +0 -0
  154. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi-ws-disconnect/pyproject.toml +0 -0
  155. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi-ws-disconnect/worker.py +0 -0
  156. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/asgi-ws-disconnect/wrangler.jsonc +0 -0
  157. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object/durable-object.wd-test +0 -0
  158. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object/pyproject.toml +0 -0
  159. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object/worker.py +0 -0
  160. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object/wrangler.jsonc +0 -0
  161. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-abort/durable-object-abort.wd-test +0 -0
  162. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-abort/pyproject.toml +0 -0
  163. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-abort/worker.py +0 -0
  164. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-abort/wrangler.jsonc +0 -0
  165. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-inheritance/durable-object-inheritance.wd-test +0 -0
  166. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-inheritance/pyproject.toml +0 -0
  167. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-inheritance/worker.py +0 -0
  168. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-inheritance/wrangler.jsonc +0 -0
  169. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-websocket/durable-object-websocket.wd-test +0 -0
  170. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-websocket/pyproject.toml +0 -0
  171. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-websocket/tester.js +0 -0
  172. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-websocket/worker.py +0 -0
  173. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/durable-object-websocket/wrangler.jsonc +0 -0
  174. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/entropy-patches.wd-test +0 -0
  175. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/pyproject.toml +0 -0
  176. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_aiohttp_websocket.py +0 -0
  177. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_boto3.py +0 -0
  178. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_jsonschema.py +0 -0
  179. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_langsmith.py +0 -0
  180. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_litestar.py +0 -0
  181. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_mcp.py +0 -0
  182. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_numpy.py +0 -0
  183. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_opentelemetry.py +0 -0
  184. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_pydantic.py +0 -0
  185. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_rust_packages.py +0 -0
  186. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/tests/test_ssl_avoidance.py +0 -0
  187. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/worker.py +0 -0
  188. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/entropy-patches/wrangler.jsonc +0 -0
  189. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/http-client/http-client.wd-test +0 -0
  190. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/http-client/origin.py +0 -0
  191. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/http-client/pyproject.toml +0 -0
  192. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/http-client/tests/test_http_client.py +0 -0
  193. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/http-client/tests/test_urllib_request.py +0 -0
  194. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/http-client/worker.py +0 -0
  195. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/http-client/wrangler.jsonc +0 -0
  196. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/python-rpc/pyproject.toml +0 -0
  197. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/python-rpc/python-rpc.wd-test +0 -0
  198. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/python-rpc/worker.js +0 -0
  199. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/python-rpc/worker.py +0 -0
  200. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/python-rpc/wrangler.jsonc +0 -0
  201. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/sdk/pyproject.toml +0 -0
  202. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/sdk/sdk.wd-test +0 -0
  203. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/sdk/server.py +0 -0
  204. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/sdk/worker.py +0 -0
  205. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/sdk/wrangler.jsonc +0 -0
  206. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/workflow/pyproject.toml +0 -0
  207. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/workflow/worker.js +0 -0
  208. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/workflow/worker.py +0 -0
  209. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/workflow/workflow.wd-test +0 -0
  210. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/workflow/wrangler.jsonc +0 -0
  211. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/wsgi/pyproject.toml +0 -0
  212. {workers_runtime_sdk-1.9.4 → workers_runtime_sdk-1.9.6}/tests/workerd-test/wsgi/wrangler.jsonc +0 -0
@@ -16,6 +16,7 @@ This package provides utilities for:
16
16
  - `_pyodide_entrypoint_helper` and `_cloudflare_compat_flags` are internal values that are imported from `workerd`.
17
17
  - `workerd` (https://github.com/cloudflare/workerd) is the runtime that Cloudflare Workers use to run Python code.
18
18
  - When using a feature from `workerd`, always check if we can implement it in this package instead to avoid tight coupling with the runtime.
19
+ - Code shared by the ASGI (`workers/asgi.py`) and WSGI (`workers/wsgi.py`) adapters lives in the `workers/gateway/` directory.
19
20
 
20
21
  ## Testing
21
22
 
@@ -2,6 +2,24 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.9.6 (2026-11-04)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **runtime-sdk**: Fix contextvars not being properly shared in WSGI streaming response
10
+ ([#288](https://github.com/cloudflare/workers-py/pull/288),
11
+ [`7cc6cbd`](https://github.com/cloudflare/workers-py/commit/7cc6cbd9253cc581dbb53e9254aa063b643f1183))
12
+
13
+
14
+ ## v1.9.5 (2026-10-20)
15
+
16
+ ### Bug Fixes
17
+
18
+ - **runtime-sdk**: Add options argument to asgi and wsgi
19
+ ([#289](https://github.com/cloudflare/workers-py/pull/289),
20
+ [`eec1e54`](https://github.com/cloudflare/workers-py/commit/eec1e54fa0348e6c90161d7f1f022344e4e6e867))
21
+
22
+
5
23
  ## v1.9.4 (2026-10-16)
6
24
 
7
25
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: workers-runtime-sdk
3
- Version: 1.9.4
3
+ Version: 1.9.6
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.9.4"
7
+ version = "1.9.6"
8
8
  description = "Python SDK for Cloudflare Workers"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,12 +2,14 @@ import logging
2
2
  from asyncio import Event, Future, Queue, create_task, ensure_future
3
3
  from collections.abc import Awaitable
4
4
  from contextlib import contextmanager
5
+ from dataclasses import dataclass
5
6
  from typing import Any
6
7
  from urllib.parse import unquote
7
8
 
8
9
  import js
9
10
 
10
11
  from workers import Context, Request, WorkerEntrypoint
12
+ from workers.gateway import BaseOptions
11
13
  from workers.utils import _to_js_headers
12
14
 
13
15
  ASGI = {"spec_version": "2.0", "version": "3.0"}
@@ -16,6 +18,14 @@ logger = logging.getLogger("asgi")
16
18
  background_tasks = set()
17
19
 
18
20
 
21
+ @dataclass(frozen=True, kw_only=True)
22
+ class Options(BaseOptions):
23
+ """Options for the ASGI adapter"""
24
+
25
+
26
+ DEFAULT_OPTIONS = Options()
27
+
28
+
19
29
  def run_in_background(coro: Awaitable[Any]) -> Future:
20
30
  fut = ensure_future(coro)
21
31
  background_tasks.add(fut)
@@ -195,10 +205,12 @@ async def process_request( # noqa: PLR0913
195
205
  # TODO(later): remove this parameter after unvendoring Python SDK from workerd
196
206
  ctx: Context | None,
197
207
  state: dict[str, Any] | None = None,
208
+ options: Options = DEFAULT_OPTIONS,
198
209
  ) -> tuple[js.Response, Future[None]]:
199
210
  from js import Response, TransformStream
200
211
  from pyodide.ffi import create_proxy
201
212
 
213
+ response_init = options.response_init()
202
214
  status = None
203
215
  headers = None
204
216
  result = Future()
@@ -262,7 +274,10 @@ async def process_request( # noqa: PLR0913
262
274
  readable = transform_stream.readable
263
275
  writer = transform_stream.writable.getWriter()
264
276
  resp = Response.new(
265
- readable, headers=_to_js_headers(headers), status=status
277
+ readable,
278
+ headers=_to_js_headers(headers),
279
+ status=status,
280
+ **response_init,
266
281
  )
267
282
  result.set_result(resp)
268
283
  with acquire_js_buffer(body) as jsbytes:
@@ -271,7 +286,10 @@ async def process_request( # noqa: PLR0913
271
286
  # 101/103/204/205/304 must not carry a body per the Fetch spec.
272
287
  # https://fetch.spec.whatwg.org/#null-body-status
273
288
  resp = Response.new(
274
- None, headers=_to_js_headers(headers), status=status
289
+ None,
290
+ headers=_to_js_headers(headers),
291
+ status=status,
292
+ **response_init,
275
293
  )
276
294
  result.set_result(resp)
277
295
  finished_response.set()
@@ -281,7 +299,10 @@ async def process_request( # noqa: PLR0913
281
299
  buf = px.getBuffer()
282
300
  px.destroy()
283
301
  resp = Response.new(
284
- buf.data, headers=_to_js_headers(headers), status=status
302
+ buf.data,
303
+ headers=_to_js_headers(headers),
304
+ status=status,
305
+ **response_init,
285
306
  )
286
307
  result.set_result(resp)
287
308
  finished_response.set()
@@ -436,12 +457,19 @@ async def process_websocket(
436
457
 
437
458
 
438
459
  async def fetch(
439
- app: Any, req: "Request | js.Request", env: Any, ctx: Context | None = None
460
+ app: Any,
461
+ req: "Request | js.Request",
462
+ env: Any,
463
+ ctx: Context | None = None,
464
+ *,
465
+ options: Options | None = None,
440
466
  ) -> js.Response:
441
467
  logger.debug("ASGI request: %s %s", req.method, req.url)
442
468
  shutdown, state = await start_application(app)
443
469
  try:
444
- result, request_task = await process_request(app, req, env, ctx, state=state)
470
+ result, request_task = await process_request(
471
+ app, req, env, ctx, state=state, options=options or DEFAULT_OPTIONS
472
+ )
445
473
  except Exception:
446
474
  logger.exception("ASGI request failed")
447
475
  await shutdown()
@@ -469,14 +497,14 @@ async def websocket(
469
497
  return await process_websocket(app, req, env)
470
498
 
471
499
 
472
- def entrypoint(app: Any) -> type[WorkerEntrypoint]:
500
+ def entrypoint(app: Any, *, options: Options | None = None) -> type[WorkerEntrypoint]:
473
501
  """Create the default Worker entrypoint for an ASGI application."""
474
502
 
475
503
  class Default(WorkerEntrypoint):
476
504
  async def fetch(self, request):
477
505
  if (request.headers.get("upgrade") or "").lower() == "websocket":
478
506
  return await websocket(app, request, self.env)
479
- return await fetch(app, request, self.env, self.ctx)
507
+ return await fetch(app, request, self.env, self.ctx, options=options)
480
508
 
481
509
  return Default
482
510
 
@@ -0,0 +1,5 @@
1
+ """Shared building blocks for the ASGI and WSGI adapters."""
2
+
3
+ from .options import BaseOptions, EncodeBody
4
+
5
+ __all__ = ["BaseOptions", "EncodeBody"]
@@ -0,0 +1,30 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Literal, get_args
3
+
4
+ EncodeBody = Literal["automatic", "manual"]
5
+
6
+
7
+ @dataclass(frozen=True, kw_only=True)
8
+ class BaseOptions:
9
+ """Options shared by the ASGI and WSGI adapters.
10
+
11
+ Downstream adapters should inherit from this class to define their own options.
12
+ """
13
+
14
+ # Whether to encode the body according to the ``Content-Encoding`` header
15
+ # See https://developers.cloudflare.com/workers/runtime-apis/response/#the-encodebody-option
16
+ encode_body: EncodeBody = "automatic"
17
+
18
+ def __post_init__(self) -> None:
19
+ if self.encode_body not in get_args(EncodeBody):
20
+ raise ValueError(
21
+ f"encode_body must be one of {get_args(EncodeBody)}, "
22
+ f"got {self.encode_body!r}"
23
+ )
24
+
25
+ def response_init(self) -> dict[str, Any]:
26
+ """Extra keyword arguments for ``Response.new``."""
27
+ init: dict[str, Any] = {}
28
+ if self.encode_body != "automatic":
29
+ init["encodeBody"] = self.encode_body
30
+ return init
@@ -1,18 +1,29 @@
1
+ import contextvars
1
2
  import io
2
3
  import logging
3
4
  import sys
4
5
  from collections.abc import Callable, Iterator
6
+ from dataclasses import dataclass
5
7
  from typing import Any
6
8
  from urllib.parse import unquote, urlsplit
7
9
 
8
10
  import js
9
11
 
10
12
  from workers import Context, Request, WorkerEntrypoint
13
+ from workers.gateway import BaseOptions
11
14
 
12
15
  logger = logging.getLogger("wsgi")
13
16
  NULL_BODY_STATUSES = frozenset({101, 103, 204, 205, 304})
14
17
 
15
18
 
19
+ @dataclass(frozen=True, kw_only=True)
20
+ class Options(BaseOptions):
21
+ """Options for the WSGI adapter, see ``BaseOptions`` for shared fields."""
22
+
23
+
24
+ DEFAULT_OPTIONS = Options()
25
+
26
+
16
27
  def _wsgi_native_string(value: str) -> str:
17
28
  """Convert a (possibly non-ASCII) Python ``str`` into a WSGI "native string".
18
29
 
@@ -212,11 +223,20 @@ def _close_iterable(iterable: Any) -> None:
212
223
  _END = object()
213
224
 
214
225
 
226
+ def _iter_in_context(
227
+ context: contextvars.Context, iterator: "Iterator[Any]"
228
+ ) -> "Iterator[Any]":
229
+ """Make sure every chunk is produced in the given context."""
230
+ while (chunk := context.run(next, iterator, _END)) is not _END:
231
+ yield chunk
232
+
233
+
215
234
  def _make_streaming_response(
216
235
  status: str,
217
236
  headers: "list[tuple[str, str]]",
218
237
  chunks: "Iterator[bytes]",
219
238
  on_close: "Callable[[], None]",
239
+ response_init: dict[str, Any],
220
240
  ) -> js.Response:
221
241
  """Build a ``js.Response`` whose body is pulled lazily from *chunks*.
222
242
 
@@ -232,7 +252,7 @@ def _make_streaming_response(
232
252
  # WSGI status is e.g. "200 OK"; split into code + reason phrase.
233
253
  code_str, _, reason = status.partition(" ")
234
254
  code = int(code_str)
235
- options: dict[str, Any] = {"status": code}
255
+ options: dict[str, Any] = {"status": code, **response_init}
236
256
  if reason:
237
257
  options["statusText"] = reason
238
258
 
@@ -291,6 +311,7 @@ def process_request(
291
311
  req: "Request | js.Request",
292
312
  env: Any,
293
313
  body: "bytes | io.IOBase",
314
+ options: Options = DEFAULT_OPTIONS,
294
315
  ) -> js.Response:
295
316
  environ = build_environ(req, env, body)
296
317
 
@@ -321,15 +342,21 @@ def process_request(
321
342
  response_state["headers"] = response_headers
322
343
  return write
323
344
 
324
- result = app(environ, start_response)
325
- result_iter = iter(result)
345
+ # Make sure a single context is used per response.
346
+ # Otherwise, stack switching could cause context variables set by the app
347
+ # to be lost when the iterable is consumed.
348
+ context = contextvars.copy_context()
349
+ result = context.run(app, environ, start_response)
350
+ result_iter = _iter_in_context(context, context.run(iter, result))
326
351
 
327
352
  def close_all() -> None:
328
- _close_iterable(result)
329
353
  try:
330
- environ["wsgi.input"].close()
331
- except Exception: # noqa: BLE001 - best-effort cleanup
332
- logger.exception("Failed to close wsgi.input")
354
+ context.run(_close_iterable, result)
355
+ finally:
356
+ try:
357
+ environ["wsgi.input"].close()
358
+ except Exception: # noqa: BLE001 - best-effort cleanup
359
+ logger.exception("Failed to close wsgi.input")
333
360
 
334
361
  # WSGI apps must call start_response before yielding the first body chunk,
335
362
  # but some defer it until the first non-empty chunk is produced. Pull that
@@ -357,7 +384,11 @@ def process_request(
357
384
  yield chunk
358
385
 
359
386
  return _make_streaming_response(
360
- response_state["status"], response_state["headers"], body_chunks(), close_all
387
+ response_state["status"],
388
+ response_state["headers"],
389
+ body_chunks(),
390
+ close_all,
391
+ options.response_init(),
361
392
  )
362
393
 
363
394
 
@@ -367,6 +398,8 @@ async def fetch(
367
398
  env: Any,
368
399
  # Accepted for parity with asgi.fetch; WSGI has no use for it.
369
400
  ctx: Context | None = None,
401
+ *,
402
+ options: Options | None = None,
370
403
  ) -> js.Response:
371
404
  logger.debug("WSGI request: %s %s", req.method, req.url)
372
405
  # Prefer lazily streaming the body through `wsgi.input` (no full buffering);
@@ -375,17 +408,17 @@ async def fetch(
375
408
  if body is None:
376
409
  body = await _read_body(req)
377
410
  try:
378
- return process_request(app, req, env, body)
411
+ return process_request(app, req, env, body, options or DEFAULT_OPTIONS)
379
412
  except Exception:
380
413
  logger.exception("WSGI request failed")
381
414
  raise
382
415
 
383
416
 
384
- def entrypoint(app: Any) -> type[WorkerEntrypoint]:
417
+ def entrypoint(app: Any, *, options: Options | None = None) -> type[WorkerEntrypoint]:
385
418
  """Create the default Worker entrypoint for a WSGI application."""
386
419
 
387
420
  class Default(WorkerEntrypoint):
388
421
  async def fetch(self, request):
389
- return await fetch(app, request, self.env)
422
+ return await fetch(app, request, self.env, options=options)
390
423
 
391
424
  return Default
@@ -2,7 +2,13 @@
2
2
  name = "flask-tests"
3
3
  version = "0.1.0"
4
4
  requires-python = ">=3.12"
5
- dependencies = ["flask", "pytest", "testlib", "workers-runtime-sdk"]
5
+ dependencies = [
6
+ "flask",
7
+ "pytest",
8
+ "pytest-asyncio<1.2.0",
9
+ "testlib",
10
+ "workers-runtime-sdk",
11
+ ]
6
12
 
7
13
  [tool.uv.sources]
8
14
  testlib = { path = "../packages/testlib" }
@@ -0,0 +1,70 @@
1
+ import asyncio
2
+
3
+ import js
4
+ import pytest
5
+ from flask import (
6
+ Flask,
7
+ Response,
8
+ has_request_context,
9
+ request,
10
+ stream_with_context,
11
+ )
12
+
13
+ from workers import wsgi
14
+
15
+ app = Flask(__name__)
16
+ # Request values whose stream_with_context generator has run its cleanup.
17
+ closed: list[str] = []
18
+
19
+
20
+ @app.get("/stream")
21
+ def stream():
22
+ @stream_with_context
23
+ def body():
24
+ value = request.args["value"]
25
+ try:
26
+ yield "flask:"
27
+ yield request.args["value"]
28
+ finally:
29
+ closed.append(value)
30
+
31
+ return Response(body(), content_type="text/plain")
32
+
33
+
34
+ async def fetch(value):
35
+ req = js.Request.new(f"http://example.com/stream?value={value}")
36
+ return await wsgi.fetch(app, req, {})
37
+
38
+
39
+ @pytest.fixture(autouse=True)
40
+ def clear_closed():
41
+ closed.clear()
42
+
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_stream_with_context():
46
+ response = await fetch("stream-body")
47
+ assert response.status == 200
48
+ # The request context pushed while priming the body must stay in the
49
+ # response's own context instead of leaking into the caller's.
50
+ assert not has_request_context()
51
+ assert await response.text() == "flask:stream-body"
52
+ assert closed == ["stream-body"]
53
+
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_concurrent_stream_with_context():
57
+ responses = await asyncio.gather(fetch("first"), fetch("second"))
58
+ bodies = await asyncio.gather(*(response.text() for response in responses))
59
+ assert bodies == ["flask:first", "flask:second"]
60
+ assert sorted(closed) == ["first", "second"]
61
+
62
+
63
+ @pytest.mark.asyncio
64
+ async def test_cancelled_stream_with_context():
65
+ response = await fetch("cancel")
66
+ reader = response.body.getReader()
67
+ first = await reader.read()
68
+ assert first.value.to_bytes() == b"flask:"
69
+ await reader.cancel()
70
+ assert closed == ["cancel"]
@@ -0,0 +1,36 @@
1
+ using Workerd = import "/workerd/workerd.capnp";
2
+
3
+ const unitTests :Workerd.Config = (
4
+ services = [
5
+ ( name = "python-asgi",
6
+ worker = (
7
+ modules = [
8
+ (name = "worker.py", pythonModule = embed "worker.py"),
9
+ %PYTHON_MODULES
10
+ ],
11
+ bindings = [
12
+ ( name = "SELF", service = "python-asgi" ),
13
+ # SELF.fetch does not serialize the response over HTTP, so it cannot
14
+ # observe the runtime encoding the body. ASGI_HTTP goes through the
15
+ # loopback HTTP socket below instead.
16
+ ( name = "ASGI_HTTP", service = "asgi-http" ),
17
+ ],
18
+ compatibilityDate = "%COMPAT_DATE",
19
+ compatibilityFlags = ["python_workers"],
20
+ )
21
+ ),
22
+ ( name = "asgi-http",
23
+ external = (
24
+ address = "loopback:asgi-http",
25
+ http = (),
26
+ )
27
+ ),
28
+ ],
29
+ sockets = [
30
+ ( name = "asgi-http",
31
+ address = "loopback:asgi-http",
32
+ http = (),
33
+ service = "python-asgi",
34
+ ),
35
+ ],
36
+ );
@@ -1,11 +1,17 @@
1
1
  import asyncio
2
+ import gzip
2
3
  import json
3
4
  import logging
4
5
 
5
6
  import js
6
7
  import pytest
7
8
  from pyodide.ffi import to_js
8
- from worker import STREAMING_CHUNK_SIZE, STREAMING_NUM_CHUNKS, example_hdr
9
+ from worker import (
10
+ ENCODED_RESPONSE_PAYLOAD,
11
+ STREAMING_CHUNK_SIZE,
12
+ STREAMING_NUM_CHUNKS,
13
+ example_hdr,
14
+ )
9
15
 
10
16
  import asgi
11
17
  from workers import Request, env
@@ -474,3 +480,35 @@ async def test_lifespan_preack_crash_is_logged_and_treated_as_unsupported():
474
480
  )
475
481
  finally:
476
482
  _remove_handler(handler)
483
+
484
+
485
+ def test_options_validate_encode_body():
486
+ assert asgi.Options().response_init() == {}
487
+ assert asgi.Options(encode_body="manual").response_init() == {
488
+ "encodeBody": "manual"
489
+ }
490
+ with pytest.raises(ValueError, match="encode_body"):
491
+ asgi.Options(encode_body="gzip")
492
+
493
+
494
+ @pytest.mark.asyncio
495
+ @pytest.mark.parametrize("mode", ["buffered", "streaming"])
496
+ @pytest.mark.parametrize("encoding", ["gzip", "identity"])
497
+ @pytest.mark.parametrize("encode_body", ["automatic", "manual"])
498
+ async def test_encode_body_option(encode_body, encoding, mode):
499
+ # SELF.fetch does not serialize the response over HTTP, so it cannot
500
+ # observe the runtime encoding the body; ASGI_HTTP goes over a socket.
501
+ response = await env.ASGI_HTTP.fetch(
502
+ f"http://example.com/encoded/{encoding}/{mode}/{encode_body}",
503
+ headers={"accept-encoding": "gzip"},
504
+ )
505
+ assert response.status == 200
506
+ assert response.headers["content-encoding"] == encoding
507
+
508
+ # fetch() decodes the declared Content-Encoding once, like a browser.
509
+ body = await response.bytes()
510
+ if encoding == "gzip" and encode_body == "automatic":
511
+ # The runtime gzips the app's already-gzipped bytes a second time.
512
+ assert gzip.decompress(body) == ENCODED_RESPONSE_PAYLOAD
513
+ else:
514
+ assert body == ENCODED_RESPONSE_PAYLOAD
@@ -1,9 +1,11 @@
1
+ import gzip
1
2
  import os
2
3
 
3
4
  from testlib.entrypoint import run_pytest
4
5
 
5
6
  import asgi
6
7
  from workers import WorkerEntrypoint
8
+ from workers.asgi import Options
7
9
 
8
10
  # ---------------------------------------------------------------------------
9
11
  # ASGI apps
@@ -238,6 +240,54 @@ class MultiCookieApp:
238
240
  await send({"type": "http.response.body", "body": b"ok"})
239
241
 
240
242
 
243
+ ENCODED_RESPONSE_PAYLOAD = b'{"message":"already encoded by the app"}' * 64
244
+
245
+
246
+ class EncodedResponseApp:
247
+ """Sends a body the app has already encoded to match Content-Encoding."""
248
+
249
+ def __init__(self, encoding, streaming):
250
+ self.encoding = encoding
251
+ self.streaming = streaming
252
+ self.body = (
253
+ gzip.compress(ENCODED_RESPONSE_PAYLOAD, mtime=0)
254
+ if encoding == "gzip"
255
+ else ENCODED_RESPONSE_PAYLOAD
256
+ )
257
+
258
+ async def __call__(self, scope, receive, send):
259
+ if scope["type"] == "lifespan":
260
+ message = await receive()
261
+ if message["type"] == "lifespan.startup":
262
+ await send({"type": "lifespan.startup.complete"})
263
+ return
264
+ await receive()
265
+ await send(
266
+ {
267
+ "type": "http.response.start",
268
+ "status": 200,
269
+ "headers": [
270
+ (b"content-type", b"application/json"),
271
+ (b"content-encoding", self.encoding.encode()),
272
+ ],
273
+ }
274
+ )
275
+ if not self.streaming:
276
+ await send({"type": "http.response.body", "body": self.body})
277
+ return
278
+ # Split one gzip member across chunks (inside its header and before
279
+ # its footer) and finish with an empty terminal chunk.
280
+ chunks = (self.body[:7], self.body[7:-8], self.body[-8:], b"")
281
+ for i, chunk in enumerate(chunks):
282
+ await send(
283
+ {
284
+ "type": "http.response.body",
285
+ "body": chunk,
286
+ "more_body": i < len(chunks) - 1,
287
+ }
288
+ )
289
+
290
+
241
291
  app = HeaderEchoApp()
242
292
  sse_app = SSEApp()
243
293
  streaming_app = StreamingApp()
@@ -267,6 +317,16 @@ class Default(WorkerEntrypoint):
267
317
  )
268
318
  elif path == "/multi-cookie":
269
319
  return await asgi.fetch(multi_cookie_app, request, self.env, self.ctx)
320
+ elif path.startswith("/encoded/"):
321
+ # /encoded/<gzip|identity>/<buffered|streaming>/<automatic|manual>
322
+ _, _, encoding, mode, encode_body = path.split("/")
323
+ return await asgi.fetch(
324
+ EncodedResponseApp(encoding, streaming=mode == "streaming"),
325
+ request,
326
+ self.env,
327
+ self.ctx,
328
+ options=Options(encode_body=encode_body),
329
+ )
270
330
 
271
331
  return await asgi.fetch(app, request, self.env, self.ctx)
272
332
 
@@ -99,10 +99,16 @@ async def _ws_session(path):
99
99
  # Without this the payload arrives as a Blob, which cannot be read
100
100
  # synchronously in the message callback.
101
101
  ws.binaryType = "arraybuffer"
102
+ # The app runs on this test's event loop, which pytest-asyncio closes when
103
+ # the test ends, cancelling an app still awaiting the disconnect. Its
104
+ # server socket then never closes, and workerd keeps the request open
105
+ # waiting for it, so wait for the server's close before returning.
106
+ closed = _listen(ws, "close")
102
107
  try:
103
108
  yield ws
104
109
  finally:
105
110
  ws.close()
111
+ await asyncio.wait_for(closed, TIMEOUT_S)
106
112
 
107
113
 
108
114
  @pytest.mark.asyncio
@@ -498,8 +498,7 @@ async def test_response_unit_tests():
498
498
  response_json = Response.json([1, 2, 3])
499
499
  assert await response_json.text() == "[1, 2, 3]"
500
500
  assert (
501
- repr(response_json)
502
- == "Response(status=200, status_text='OK', content_type='application/json')"
501
+ repr(response_json) == "Response(status=200, content_type='application/json')"
503
502
  )
504
503
 
505
504
  response_json = Response.from_json([1, 2, 3])