workers-runtime-sdk 1.7.0__tar.gz → 1.8.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.7.0 → workers_runtime_sdk-1.8.1}/CHANGELOG.md +30 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/PKG-INFO +1 -1
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/pyproject.toml +1 -1
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/asgi.py +59 -14
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/utils.py +8 -10
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/wsgi.py +14 -3
- workers_runtime_sdk-1.8.1/tests/test_flask.py +26 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/_client.py +32 -0
- workers_runtime_sdk-1.8.1/tests/web-frameworks-test/fastapi-tests/src/test_platform_boundaries.py +99 -0
- workers_runtime_sdk-1.8.1/tests/web-frameworks-test/fastapi-tests/src/test_platform_lifecycle.py +46 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/worker.py +111 -0
- workers_runtime_sdk-1.8.1/tests/web-frameworks-test/flask-tests/pyproject.toml +5 -0
- workers_runtime_sdk-1.8.1/tests/web-frameworks-test/flask-tests/src/test_response.py +13 -0
- workers_runtime_sdk-1.8.1/tests/web-frameworks-test/flask-tests/src/worker.py +89 -0
- workers_runtime_sdk-1.8.1/tests/web-frameworks-test/flask-tests/wrangler.jsonc +6 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi-ws-disconnect/tests/test_ws_disconnect.py +8 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi-ws-disconnect/worker.py +15 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/wsgi/tests/test_wsgi.py +19 -3
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/wsgi/worker.py +21 -9
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/uv.lock +1 -1
- workers_runtime_sdk-1.7.0/tests/web-frameworks-test/fastapi-tests/src/test_platform_lifecycle.py +0 -12
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/.gitignore +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/AGENTS.md +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/README.md +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/_cloudflare_compat_flags.pyi +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/_pyodide_entrypoint_helper.pyi +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/_workers_sdk_entropy_import_context.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/_workers_sdk_package_patches.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/_workers_sdk_patch_loader.pth +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/_workers_sdk_patch_loader.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/asgi.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/__init__.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/blob.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/entrypoints.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/fetch.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/formdata.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/py.typed +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/request.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/response.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/rpc.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/types.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/src/workers/workflows.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/public/data.json +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/public/hello.txt +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/conftest.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_analytics_engine.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_assets.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_d1.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_do.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_hyperdrive_mysql.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_hyperdrive_postgresql.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_images.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_kv.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_mtls.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_queue.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_r2.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_ratelimit.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_vectorize.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_version_metadata.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/test_workflow.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/worker_durable_object.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/src/worker_workflow.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/bindings-test/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/conftest.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/test_bindings.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/test_fastapi.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/test_in_workerd.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/test_web_frameworks.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/_client.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/conftest.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/__init__.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/api_urls.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/auth_backend.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/drf_auth.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/drf_serializers.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/drf_throttling.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/drf_views.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/forms.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/middleware.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/templatetags/__init__.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/templatetags/custom_tags.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/urls.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/django_app/views.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/templates/base.html +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/templates/context_test.html +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/templates/csrf_form.html +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/templates/hello.html +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_async_utilities.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_auth.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_caching.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_cors.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_csrf.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_drf_auth_permissions.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_drf_content_negotiation.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_drf_serializers.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_drf_throttling.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_drf_versioning.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_drf_views.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_error_handling.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_file_uploads.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_forms.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_middleware.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_pagination.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_request_response.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_sessions.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_signals.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_streaming.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_templates.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_url_routing.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/test_views_basic.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/src/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/django-async/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/public/data.json +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/public/index.html +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/public/style.css +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/conftest.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/static/app.css +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/static/data.json +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/static/hello.txt +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/static/index.html +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_assets.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_error_handling.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_platform_io.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_response_types.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_routing.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_staticfiles.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_sync_handlers.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/src/test_websocket.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/web-frameworks-test/fastapi-tests/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi/asgi.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi/tests/test_asgi.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi-ws-disconnect/asgi-ws-disconnect.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi-ws-disconnect/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/asgi-ws-disconnect/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object/durable-object.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-abort/durable-object-abort.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-abort/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-abort/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-abort/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-inheritance/durable-object-inheritance.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-inheritance/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-inheritance/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-inheritance/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-websocket/durable-object-websocket.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-websocket/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-websocket/tester.js +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-websocket/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/durable-object-websocket/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/entropy-patches.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/tests/test_aiohttp_websocket.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/tests/test_langsmith.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/tests/test_numpy.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/tests/test_pydantic.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/tests/test_rust_packages.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/tests/test_ssl_avoidance.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/entropy-patches/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/python-rpc/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/python-rpc/python-rpc.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/python-rpc/worker.js +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/python-rpc/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/python-rpc/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/sdk/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/sdk/sdk.wd-test +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/sdk/server.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/sdk/tests/test_sdk.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/sdk/worker.py +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/sdk/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/wsgi/pyproject.toml +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/wsgi/wrangler.jsonc +0 -0
- {workers_runtime_sdk-1.7.0 → workers_runtime_sdk-1.8.1}/tests/workerd-test/wsgi/wsgi.wd-test +0 -0
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.8.1 (2026-08-28)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Make Headers subclass Mapping ([#233](https://github.com/cloudflare/workers-py/pull/233),
|
|
10
|
+
[`2e0a47f`](https://github.com/cloudflare/workers-py/commit/2e0a47f7aca2091a0396fcd8b04c03979326698a))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v1.8.0 (2026-08-28)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Defer ASGI lifespan shutdown until request completion
|
|
18
|
+
([`3249184`](https://github.com/cloudflare/workers-py/commit/324918442748a0b87affc6128315ceb190e1b368))
|
|
19
|
+
|
|
20
|
+
- Make wsgi streaming response handlers async
|
|
21
|
+
([#191](https://github.com/cloudflare/workers-py/pull/191),
|
|
22
|
+
[`12168ae`](https://github.com/cloudflare/workers-py/commit/12168aef7599067a910a518f1ce7860a00ae9a58))
|
|
23
|
+
|
|
24
|
+
- **runtime-sdk**: Close the WebSocket transport when the app task ends
|
|
25
|
+
([#166](https://github.com/cloudflare/workers-py/pull/166),
|
|
26
|
+
[`e67e7e7`](https://github.com/cloudflare/workers-py/commit/e67e7e78af7fa6976f1f9966508b9869a6840ce5))
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
- Add wsgi.entrypoint like asgi.entrypoint
|
|
31
|
+
([#232](https://github.com/cloudflare/workers-py/pull/232),
|
|
32
|
+
[`e044561`](https://github.com/cloudflare/workers-py/commit/e04456174b3f5bc9df8b3f8293ced6d08aaffc0f))
|
|
33
|
+
|
|
34
|
+
|
|
5
35
|
## v1.7.0 (2026-08-24)
|
|
6
36
|
|
|
7
37
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: workers-runtime-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.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
|
|
@@ -16,7 +16,7 @@ logger = logging.getLogger("asgi")
|
|
|
16
16
|
background_tasks = set()
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def run_in_background(coro: Awaitable[Any]) ->
|
|
19
|
+
def run_in_background(coro: Awaitable[Any]) -> Future:
|
|
20
20
|
fut = ensure_future(coro)
|
|
21
21
|
background_tasks.add(fut)
|
|
22
22
|
|
|
@@ -27,6 +27,7 @@ def run_in_background(coro: Awaitable[Any]) -> None:
|
|
|
27
27
|
logger.error("Unhandled exception in background task", exc_info=exc)
|
|
28
28
|
|
|
29
29
|
fut.add_done_callback(_on_done)
|
|
30
|
+
return fut
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
async def close_stream_quietly(writer):
|
|
@@ -185,7 +186,7 @@ async def process_request( # noqa: PLR0913
|
|
|
185
186
|
# TODO(later): remove this parameter after unvendoring Python SDK from workerd
|
|
186
187
|
ctx: Context | None,
|
|
187
188
|
state: dict[str, Any] | None = None,
|
|
188
|
-
) -> js.Response:
|
|
189
|
+
) -> tuple[js.Response, Future[None]]:
|
|
189
190
|
from js import Response, TransformStream
|
|
190
191
|
from pyodide.ffi import create_proxy
|
|
191
192
|
|
|
@@ -304,17 +305,22 @@ async def process_request( # noqa: PLR0913
|
|
|
304
305
|
# runtime is waiting on through wait_until.
|
|
305
306
|
run_in_background(close_stream_quietly(writer))
|
|
306
307
|
|
|
307
|
-
|
|
308
|
-
app_task = create_proxy(create_task(run_app()))
|
|
309
|
-
|
|
310
|
-
from workers import wait_until
|
|
311
|
-
|
|
312
|
-
wait_until(app_task)
|
|
308
|
+
request_task = create_task(run_app())
|
|
313
309
|
|
|
314
310
|
try:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
311
|
+
response = await result
|
|
312
|
+
except Exception:
|
|
313
|
+
# The request task handles application exceptions and should be allowed
|
|
314
|
+
# to finish before its error is propagated to fetch().
|
|
315
|
+
await request_task
|
|
316
|
+
raise
|
|
317
|
+
|
|
318
|
+
# Buffered responses do not depend on a consumer, so request cleanup can
|
|
319
|
+
# finish before returning. Streaming responses must run alongside the
|
|
320
|
+
# consumer because writes can be subject to backpressure.
|
|
321
|
+
if writer is None:
|
|
322
|
+
await request_task
|
|
323
|
+
return response, request_task
|
|
318
324
|
|
|
319
325
|
|
|
320
326
|
async def process_websocket(
|
|
@@ -359,7 +365,10 @@ async def process_websocket(
|
|
|
359
365
|
server.onclose = onclose
|
|
360
366
|
server.onmessage = onmessage
|
|
361
367
|
|
|
368
|
+
app_closed = False
|
|
369
|
+
|
|
362
370
|
async def ws_send(got):
|
|
371
|
+
nonlocal app_closed
|
|
363
372
|
if got["type"] == "websocket.accept":
|
|
364
373
|
# The Workers WebSocketPair is accepted before the upgrade response
|
|
365
374
|
# is returned, so there is no deferred accept operation here.
|
|
@@ -376,6 +385,7 @@ async def process_websocket(
|
|
|
376
385
|
server.send(s)
|
|
377
386
|
return
|
|
378
387
|
if got["type"] == "websocket.close":
|
|
388
|
+
app_closed = True
|
|
379
389
|
server.close(got.get("code", 1000), got.get("reason", ""))
|
|
380
390
|
return
|
|
381
391
|
logger.warning(" == Not implemented %s", got["type"])
|
|
@@ -384,13 +394,34 @@ async def process_websocket(
|
|
|
384
394
|
received = await queue.get()
|
|
385
395
|
return received
|
|
386
396
|
|
|
387
|
-
|
|
397
|
+
from pyodide.ffi import create_proxy
|
|
398
|
+
|
|
399
|
+
from workers import wait_until
|
|
400
|
+
|
|
401
|
+
task = run_in_background(
|
|
388
402
|
app(
|
|
389
403
|
request_to_scope(req, env if env is not None else {}, ws=True),
|
|
390
404
|
ws_receive,
|
|
391
405
|
ws_send,
|
|
392
406
|
)
|
|
393
407
|
)
|
|
408
|
+
# wait_until is JS's waitUntil, so the task crosses the boundary as a proxy,
|
|
409
|
+
# destroyed once it settles rather than left to leak.
|
|
410
|
+
task_proxy = create_proxy(task)
|
|
411
|
+
|
|
412
|
+
def _close_transport(finished):
|
|
413
|
+
if not app_closed:
|
|
414
|
+
# The app ended without closing, so a half-open connection would
|
|
415
|
+
# leave the client hanging instead of reconnecting.
|
|
416
|
+
failed = not finished.cancelled() and finished.exception() is not None
|
|
417
|
+
try:
|
|
418
|
+
server.close(1011 if failed else 1000, "")
|
|
419
|
+
except Exception:
|
|
420
|
+
pass # the peer may already have closed the socket
|
|
421
|
+
task_proxy.destroy()
|
|
422
|
+
|
|
423
|
+
task.add_done_callback(_close_transport)
|
|
424
|
+
wait_until(task_proxy)
|
|
394
425
|
|
|
395
426
|
return Response.new(None, status=101, webSocket=client)
|
|
396
427
|
|
|
@@ -401,11 +432,25 @@ async def fetch(
|
|
|
401
432
|
logger.debug("ASGI request: %s %s", req.method, req.url)
|
|
402
433
|
shutdown, state = await start_application(app)
|
|
403
434
|
try:
|
|
404
|
-
result = await process_request(app, req, env, ctx, state=state)
|
|
435
|
+
result, request_task = await process_request(app, req, env, ctx, state=state)
|
|
405
436
|
except Exception:
|
|
406
437
|
logger.exception("ASGI request failed")
|
|
438
|
+
await shutdown()
|
|
407
439
|
raise
|
|
408
|
-
|
|
440
|
+
|
|
441
|
+
if request_task.done():
|
|
442
|
+
await shutdown()
|
|
443
|
+
else:
|
|
444
|
+
from workers import wait_until # noqa: PLC0415
|
|
445
|
+
|
|
446
|
+
async def finalize_request():
|
|
447
|
+
try:
|
|
448
|
+
await request_task
|
|
449
|
+
finally:
|
|
450
|
+
await shutdown()
|
|
451
|
+
|
|
452
|
+
wait_until(run_in_background(finalize_request()))
|
|
453
|
+
|
|
409
454
|
return result
|
|
410
455
|
|
|
411
456
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import http.client
|
|
2
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -201,22 +202,19 @@ def _to_js_headers(headers):
|
|
|
201
202
|
raise TypeError("Received unexpected type for headers argument")
|
|
202
203
|
|
|
203
204
|
|
|
205
|
+
class HTTPMessageMapping(http.client.HTTPMessage, Mapping):
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
|
|
204
209
|
def _js_headers_to_http_message(
|
|
205
210
|
js_headers: dict[str, str],
|
|
206
211
|
):
|
|
207
|
-
# `http.client` is imported here because it costs a lot of CPU time when imported at the
|
|
208
|
-
# top-level. At least it does when we do so in our validator tests, doesn't seem to cause
|
|
209
|
-
# trouble in production. So as a workaround we do the import here.
|
|
210
|
-
#
|
|
211
|
-
# TODO(later): when dedicated snapshots are default we can move this import to the top-level.
|
|
212
|
-
import http.client
|
|
213
|
-
|
|
214
212
|
# Newer Pyodide versions already expose headers as an http.client.HTTPMessage,
|
|
215
213
|
# in which case there is nothing to convert.
|
|
216
|
-
if isinstance(js_headers,
|
|
214
|
+
if isinstance(js_headers, HTTPMessageMapping):
|
|
217
215
|
return js_headers
|
|
218
216
|
|
|
219
|
-
result =
|
|
217
|
+
result = HTTPMessageMapping()
|
|
220
218
|
if not get_compat_flag("python_request_headers_preserve_commas"):
|
|
221
219
|
for key, val in js_headers:
|
|
222
220
|
result[key] = val.strip()
|
|
@@ -7,7 +7,7 @@ from urllib.parse import unquote, urlsplit
|
|
|
7
7
|
|
|
8
8
|
import js
|
|
9
9
|
|
|
10
|
-
from workers import Context, Request
|
|
10
|
+
from workers import Context, Request, WorkerEntrypoint
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger("wsgi")
|
|
13
13
|
NULL_BODY_STATUSES = frozenset({101, 103, 204, 205, 304})
|
|
@@ -262,8 +262,9 @@ def _make_streaming_response(
|
|
|
262
262
|
for proxy in proxies:
|
|
263
263
|
proxy.destroy()
|
|
264
264
|
|
|
265
|
+
# Make proxies async so that it is possible to stack switch inside them.
|
|
265
266
|
@create_proxy
|
|
266
|
-
def pull(controller: Any) -> None:
|
|
267
|
+
async def pull(controller: Any) -> None:
|
|
267
268
|
try:
|
|
268
269
|
chunk = next(chunks, _END)
|
|
269
270
|
except Exception as exc: # noqa: BLE001 - forward app errors to the stream
|
|
@@ -278,7 +279,7 @@ def _make_streaming_response(
|
|
|
278
279
|
controller.enqueue(_to_js_uint8array(chunk))
|
|
279
280
|
|
|
280
281
|
@create_proxy
|
|
281
|
-
def cancel(_reason: Any = None) -> None:
|
|
282
|
+
async def cancel(_reason: Any = None) -> None:
|
|
282
283
|
cleanup()
|
|
283
284
|
|
|
284
285
|
proxies = [pull, cancel]
|
|
@@ -378,3 +379,13 @@ async def fetch(
|
|
|
378
379
|
except Exception:
|
|
379
380
|
logger.exception("WSGI request failed")
|
|
380
381
|
raise
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def entrypoint(app: Any) -> type[WorkerEntrypoint]:
|
|
385
|
+
"""Create the default Worker entrypoint for a WSGI application."""
|
|
386
|
+
|
|
387
|
+
class Default(WorkerEntrypoint):
|
|
388
|
+
async def fetch(self, request):
|
|
389
|
+
return await fetch(app, request, self.env)
|
|
390
|
+
|
|
391
|
+
return Default
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Tests for Flask running against a live pywrangler dev server."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites
|
|
7
|
+
|
|
8
|
+
WEB_FRAMEWORKS_DIR = Path(__file__).parent / "web-frameworks-test" / "flask-tests"
|
|
9
|
+
WEB_FRAMEWORKS_SRC_DIR = WEB_FRAMEWORKS_DIR / "src"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(scope="module")
|
|
13
|
+
def worker_project_dir() -> Path:
|
|
14
|
+
return WEB_FRAMEWORKS_DIR
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(
|
|
18
|
+
scope="module",
|
|
19
|
+
params=COMPAT_CONFIGS,
|
|
20
|
+
ids=[config.python_version for config in COMPAT_CONFIGS],
|
|
21
|
+
)
|
|
22
|
+
def compat_config(request: pytest.FixtureRequest) -> CompatConfig:
|
|
23
|
+
return request.param
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import json as _json
|
|
2
2
|
|
|
3
|
+
import js
|
|
4
|
+
from pyodide.ffi import create_proxy, to_js
|
|
5
|
+
|
|
3
6
|
import asgi
|
|
4
7
|
from workers import Request
|
|
5
8
|
|
|
@@ -63,6 +66,35 @@ def build_multipart_bytes(files, boundary="----WebTestBoundary"):
|
|
|
63
66
|
return bytes(body), f"multipart/form-data; boundary={boundary}"
|
|
64
67
|
|
|
65
68
|
|
|
69
|
+
async def fetch_chunks(app, path, chunks, env=None, method="POST", headers=None):
|
|
70
|
+
"""Send a request body as a JS ReadableStream with explicit chunk boundaries."""
|
|
71
|
+
from js import Object
|
|
72
|
+
|
|
73
|
+
chunks = list(chunks)
|
|
74
|
+
|
|
75
|
+
def start(controller):
|
|
76
|
+
for chunk in chunks:
|
|
77
|
+
controller.enqueue(to_js(chunk))
|
|
78
|
+
controller.close()
|
|
79
|
+
|
|
80
|
+
start_proxy = create_proxy(start)
|
|
81
|
+
try:
|
|
82
|
+
stream = js.ReadableStream.new(
|
|
83
|
+
to_js({"start": start_proxy}, dict_converter=Object.fromEntries)
|
|
84
|
+
)
|
|
85
|
+
hdrs = dict(headers or {})
|
|
86
|
+
hdrs.setdefault("Content-Length", str(sum(len(chunk) for chunk in chunks)))
|
|
87
|
+
request = Request(
|
|
88
|
+
f"{BASE_URL}{path}",
|
|
89
|
+
method=method,
|
|
90
|
+
headers=hdrs,
|
|
91
|
+
body=stream,
|
|
92
|
+
)
|
|
93
|
+
return await asgi.fetch(app, request, env or {})
|
|
94
|
+
finally:
|
|
95
|
+
start_proxy.destroy()
|
|
96
|
+
|
|
97
|
+
|
|
66
98
|
def build_multipart(files, boundary="----WebTestBoundary"):
|
|
67
99
|
"""Build a multipart/form-data body from a list of (name, filename, content) tuples."""
|
|
68
100
|
lines = []
|
workers_runtime_sdk-1.8.1/tests/web-frameworks-test/fastapi-tests/src/test_platform_boundaries.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Tests across the FastAPI, Pyodide, and Workers runtime boundaries."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import gzip
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from _client import fetch, fetch_chunks, read_json
|
|
10
|
+
from worker import reset_platform_concurrency
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_chunked_js_stream_preserves_utf8_and_boundaries(fastapi_app):
|
|
15
|
+
"""A JS ReadableStream survives the JS-to-Python ASGI receive bridge."""
|
|
16
|
+
value = {
|
|
17
|
+
"message": "worker \u0063\u0061\u0066\u00e9 \U0001f680",
|
|
18
|
+
"items": [1, 2, 3],
|
|
19
|
+
}
|
|
20
|
+
body = json.dumps(value, ensure_ascii=False).encode()
|
|
21
|
+
emoji = "\U0001f680".encode()
|
|
22
|
+
emoji_start = body.index(emoji)
|
|
23
|
+
cuts = [7, emoji_start + 1, emoji_start + 3, len(body) - 2]
|
|
24
|
+
chunks = []
|
|
25
|
+
start = 0
|
|
26
|
+
for end in cuts + [len(body)]:
|
|
27
|
+
chunks.append(body[start:end])
|
|
28
|
+
start = end
|
|
29
|
+
|
|
30
|
+
resp = await fetch_chunks(
|
|
31
|
+
fastapi_app,
|
|
32
|
+
"/platform/chunked-json",
|
|
33
|
+
chunks,
|
|
34
|
+
headers={"Content-Type": "application/json"},
|
|
35
|
+
)
|
|
36
|
+
assert resp.status == 200
|
|
37
|
+
data = await read_json(resp)
|
|
38
|
+
assert data["chunk_sizes"] == [len(chunk) for chunk in chunks]
|
|
39
|
+
assert data["sha256"] == hashlib.sha256(body).hexdigest()
|
|
40
|
+
assert data["data"] == value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_concurrent_requests_keep_bindings_and_data_isolated(fastapi_app):
|
|
45
|
+
"""Interleaved requests retain their own env, headers, and bodies."""
|
|
46
|
+
reset_platform_concurrency()
|
|
47
|
+
|
|
48
|
+
async def request(marker):
|
|
49
|
+
resp = await fetch(
|
|
50
|
+
fastapi_app,
|
|
51
|
+
"/platform/concurrent-env",
|
|
52
|
+
env={"marker": marker},
|
|
53
|
+
method="POST",
|
|
54
|
+
headers={"X-Request-Marker": marker},
|
|
55
|
+
body=f"body-{marker}",
|
|
56
|
+
)
|
|
57
|
+
assert resp.status == 200
|
|
58
|
+
return await read_json(resp)
|
|
59
|
+
|
|
60
|
+
first, second = await asyncio.wait_for(
|
|
61
|
+
asyncio.gather(request("first"), request("second")), timeout=5
|
|
62
|
+
)
|
|
63
|
+
assert first == {
|
|
64
|
+
"binding": "first",
|
|
65
|
+
"header": "first",
|
|
66
|
+
"body": "body-first",
|
|
67
|
+
}
|
|
68
|
+
assert second == {
|
|
69
|
+
"binding": "second",
|
|
70
|
+
"header": "second",
|
|
71
|
+
"body": "body-second",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.asyncio
|
|
76
|
+
async def test_multiple_set_cookie_headers_survive_fastapi(fastapi_app):
|
|
77
|
+
"""FastAPI's separate Set-Cookie headers remain distinct through JS Headers."""
|
|
78
|
+
resp = await fetch(fastapi_app, "/platform/multiple-cookies")
|
|
79
|
+
assert resp.status == 200
|
|
80
|
+
cookies = list(resp.headers.getSetCookie())
|
|
81
|
+
assert len(cookies) == 3
|
|
82
|
+
assert cookies[0].startswith("first=1;")
|
|
83
|
+
assert cookies[1].startswith("second=two;")
|
|
84
|
+
assert cookies[2].startswith("dated=3;")
|
|
85
|
+
assert "Wed, 21 Oct 2037 07:28:00 GMT" in cookies[2]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_gzip_middleware_preserves_binary_body(fastapi_app):
|
|
90
|
+
"""GZipMiddleware compresses binary data streamed through a JS Response."""
|
|
91
|
+
expected = bytes(range(256)) * 32
|
|
92
|
+
resp = await fetch(
|
|
93
|
+
fastapi_app,
|
|
94
|
+
"/platform/gzip-binary",
|
|
95
|
+
headers={"Accept-Encoding": "gzip"},
|
|
96
|
+
)
|
|
97
|
+
assert resp.status == 200
|
|
98
|
+
assert resp.headers.get("content-encoding") == "gzip"
|
|
99
|
+
assert gzip.decompress((await resp.bytes()).to_bytes()) == expected
|
workers_runtime_sdk-1.8.1/tests/web-frameworks-test/fastapi-tests/src/test_platform_lifecycle.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""FastAPI lifecycle tests targeting the Workers ASGI streaming boundary."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from _client import fetch, get_json
|
|
7
|
+
from worker import (
|
|
8
|
+
_platform_events,
|
|
9
|
+
_platform_shutdown_complete,
|
|
10
|
+
reset_platform_events,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_lifespan_state_reaches_request(fastapi_app):
|
|
16
|
+
"""State yielded by FastAPI's lifespan context is copied to HTTP scopes."""
|
|
17
|
+
resp, data = await get_json(fastapi_app, "/platform/lifespan-state")
|
|
18
|
+
assert resp.status == 200
|
|
19
|
+
assert data == {"available": True, "closed": False}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_stream_finishes_before_cleanup_and_shutdown(fastapi_app):
|
|
24
|
+
"""Streaming, background work, and dependency cleanup precede shutdown."""
|
|
25
|
+
reset_platform_events()
|
|
26
|
+
|
|
27
|
+
resp = await asyncio.wait_for(
|
|
28
|
+
fetch(fastapi_app, "/platform/lifecycle-stream"), timeout=5
|
|
29
|
+
)
|
|
30
|
+
assert resp.status == 200
|
|
31
|
+
assert await asyncio.wait_for(resp.text(), timeout=5) == (
|
|
32
|
+
"chunk-0\nchunk-1\nchunk-2\n"
|
|
33
|
+
)
|
|
34
|
+
await asyncio.wait_for(_platform_shutdown_complete.wait(), timeout=5)
|
|
35
|
+
|
|
36
|
+
assert _platform_events == [
|
|
37
|
+
"lifespan-startup",
|
|
38
|
+
"dependency-open",
|
|
39
|
+
"handler",
|
|
40
|
+
"chunk-0",
|
|
41
|
+
"chunk-1",
|
|
42
|
+
"chunk-2",
|
|
43
|
+
"background-task",
|
|
44
|
+
"dependency-close",
|
|
45
|
+
"lifespan-shutdown",
|
|
46
|
+
]
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import enum
|
|
3
3
|
import hashlib
|
|
4
4
|
import importlib.util
|
|
5
|
+
import json
|
|
5
6
|
from contextlib import asynccontextmanager
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -32,6 +33,7 @@ from fastapi.staticfiles import StaticFiles
|
|
|
32
33
|
from pydantic import BaseModel
|
|
33
34
|
from pyodide.webloop import WebLoop
|
|
34
35
|
from starlette.background import BackgroundTask
|
|
36
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
35
37
|
|
|
36
38
|
import asgi
|
|
37
39
|
|
|
@@ -51,16 +53,40 @@ class PlatformResource:
|
|
|
51
53
|
self.closed = False
|
|
52
54
|
|
|
53
55
|
|
|
56
|
+
_platform_events: list[str] = []
|
|
57
|
+
_platform_shutdown_complete = asyncio.Event()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def reset_platform_events():
|
|
61
|
+
_platform_events.clear()
|
|
62
|
+
_platform_shutdown_complete.clear()
|
|
63
|
+
|
|
64
|
+
|
|
54
65
|
@asynccontextmanager
|
|
55
66
|
async def _platform_lifespan(_app):
|
|
56
67
|
resource = PlatformResource()
|
|
68
|
+
_platform_events.append("lifespan-startup")
|
|
57
69
|
try:
|
|
58
70
|
yield {"platform_resource": resource}
|
|
59
71
|
finally:
|
|
60
72
|
resource.closed = True
|
|
73
|
+
_platform_events.append("lifespan-shutdown")
|
|
74
|
+
_platform_shutdown_complete.set()
|
|
61
75
|
|
|
62
76
|
|
|
63
77
|
app = FastAPI(lifespan=_platform_lifespan)
|
|
78
|
+
app.add_middleware(GZipMiddleware, minimum_size=1024)
|
|
79
|
+
|
|
80
|
+
_platform_concurrency_ready = asyncio.Event()
|
|
81
|
+
_platform_concurrency_count = 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def reset_platform_concurrency():
|
|
85
|
+
global _platform_concurrency_count
|
|
86
|
+
|
|
87
|
+
_platform_concurrency_count = 0
|
|
88
|
+
_platform_concurrency_ready.clear()
|
|
89
|
+
|
|
64
90
|
|
|
65
91
|
# --------------------------------------------------------------------------- #
|
|
66
92
|
# Shared mutable state used to verify side-effects (e.g. background tasks).
|
|
@@ -463,6 +489,91 @@ async def platform_upload_spooled(file: UploadFile = File(...)): # noqa: B008
|
|
|
463
489
|
}
|
|
464
490
|
|
|
465
491
|
|
|
492
|
+
@app.post("/platform/chunked-json")
|
|
493
|
+
async def platform_chunked_json(request: Request):
|
|
494
|
+
chunks = [chunk async for chunk in request.stream() if chunk]
|
|
495
|
+
body = b"".join(chunks)
|
|
496
|
+
return {
|
|
497
|
+
"chunk_sizes": [len(chunk) for chunk in chunks],
|
|
498
|
+
"sha256": hashlib.sha256(body).hexdigest(),
|
|
499
|
+
"data": json.loads(body),
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@app.post("/platform/concurrent-env")
|
|
504
|
+
async def platform_concurrent_env(
|
|
505
|
+
request: Request,
|
|
506
|
+
bindings=asgi.env, # noqa: B008
|
|
507
|
+
):
|
|
508
|
+
global _platform_concurrency_count
|
|
509
|
+
|
|
510
|
+
_platform_concurrency_count += 1
|
|
511
|
+
if _platform_concurrency_count == 2:
|
|
512
|
+
_platform_concurrency_ready.set()
|
|
513
|
+
await asyncio.wait_for(_platform_concurrency_ready.wait(), timeout=5)
|
|
514
|
+
body = await request.body()
|
|
515
|
+
return {
|
|
516
|
+
"binding": bindings["marker"],
|
|
517
|
+
"header": request.headers["x-request-marker"],
|
|
518
|
+
"body": body.decode(),
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@app.get("/platform/multiple-cookies")
|
|
523
|
+
async def platform_multiple_cookies():
|
|
524
|
+
response = JSONResponse({"ok": True})
|
|
525
|
+
response.set_cookie("first", "1")
|
|
526
|
+
response.set_cookie("second", "two")
|
|
527
|
+
response.set_cookie("dated", "3", expires="Wed, 21 Oct 2037 07:28:00 GMT")
|
|
528
|
+
return response
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@app.get("/platform/gzip-binary")
|
|
532
|
+
async def platform_gzip_binary():
|
|
533
|
+
return FastAPIResponse(
|
|
534
|
+
content=(bytes(range(256)) * 32),
|
|
535
|
+
media_type="application/octet-stream",
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _stream_resource():
|
|
540
|
+
resource = PlatformResource()
|
|
541
|
+
_platform_events.append("dependency-open")
|
|
542
|
+
try:
|
|
543
|
+
yield resource
|
|
544
|
+
finally:
|
|
545
|
+
resource.closed = True
|
|
546
|
+
_platform_events.append("dependency-close")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
async def _platform_background_task():
|
|
550
|
+
await asyncio.sleep(0)
|
|
551
|
+
_platform_events.append("background-task")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@app.get("/platform/lifecycle-stream")
|
|
555
|
+
async def platform_lifecycle_stream(
|
|
556
|
+
request: Request,
|
|
557
|
+
resource: PlatformResource = Depends(_stream_resource), # noqa: B008
|
|
558
|
+
):
|
|
559
|
+
lifespan_resource = request.state.platform_resource
|
|
560
|
+
_platform_events.append("handler")
|
|
561
|
+
|
|
562
|
+
async def chunks():
|
|
563
|
+
for index in range(3):
|
|
564
|
+
assert not resource.closed
|
|
565
|
+
assert not lifespan_resource.closed
|
|
566
|
+
_platform_events.append(f"chunk-{index}")
|
|
567
|
+
yield f"chunk-{index}\n"
|
|
568
|
+
await asyncio.sleep(0)
|
|
569
|
+
|
|
570
|
+
return StreamingResponse(
|
|
571
|
+
chunks(),
|
|
572
|
+
media_type="text/plain",
|
|
573
|
+
background=BackgroundTask(_platform_background_task),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
466
577
|
@app.get("/native-file")
|
|
467
578
|
async def native_file():
|
|
468
579
|
"""Serve a single bundled file with FastAPI's native FileResponse."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from flask import Response as FlaskResponse
|
|
2
|
+
|
|
3
|
+
from workers import FetchResponse, Response
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_fetch_response_headers():
|
|
7
|
+
fetch_response = Response(headers={"X-Workers-Test": "value"})
|
|
8
|
+
|
|
9
|
+
assert isinstance(fetch_response, FetchResponse)
|
|
10
|
+
|
|
11
|
+
flask_response = FlaskResponse(headers=fetch_response.headers)
|
|
12
|
+
|
|
13
|
+
assert flask_response.headers["X-Workers-Test"] == "value"
|