diff --git a/17-dynamic-py-py/README.md b/17-dynamic-py-py/README.md new file mode 100644 index 0000000..df26429 --- /dev/null +++ b/17-dynamic-py-py/README.md @@ -0,0 +1,47 @@ +# 17-dynamic-py-py — Python Worker loads a Python Dynamic Worker + +This example shows how a Python Worker can use the [Worker Loader](https://developers.cloudflare.com/dynamic-workers/) binding to dynamically load and execute another Python Worker at runtime. + +The parent Python Worker keeps the source of the dynamic Worker as a string (for demonstration purposes — in a real application you would load it from KV, R2, or any other storage). Each request is forwarded to the dynamically-loaded Worker, which runs in its own isolated sandbox. + +## Run it + +```bash +uv run pywrangler dev +``` + +Then press `b` to open a browser, or: + +```bash +curl http://localhost:8787/ +``` + +You should see: + +``` +Hello from a dynamically-loaded Python Worker! +``` + +## How it works + +The parent Worker declares a `worker_loaders` binding in `wrangler.jsonc`: + +```jsonc +"worker_loaders": [ + { "binding": "LOADER" } +] +``` + +In `src/entry.py`, the parent calls `self.env.LOADER.get(id, callback)`. The callback returns a JS object describing the Worker to load — its `compatibilityDate`, `compatibilityFlags`, `mainModule`, and `modules`. + +```python +worker = self.env.LOADER.get( + "hello-python-v1", + create_proxy(_worker_code), +) +return await worker.getEntrypoint().fetch(request) +``` + +The runtime caches loaded isolates by ID, so the callback typically only runs the first time a given `id` is seen. To load updated code, change the `id` (for example, `"hello-python-v2"`). + +`create_proxy()` from `pyodide.ffi` is required to expose the Python callback to the JavaScript runtime. `to_js(..., dict_converter=Object.fromEntries)` converts the Python dict into a plain JS object so the loader receives the shape it expects. diff --git a/17-dynamic-py-py/package.json b/17-dynamic-py-py/package.json new file mode 100644 index 0000000..d61093f --- /dev/null +++ b/17-dynamic-py-py/package.json @@ -0,0 +1,13 @@ +{ + "name": "dynamic-py-py", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "uv run pywrangler deploy", + "dev": "uv run pywrangler dev", + "start": "uv run pywrangler dev" + }, + "devDependencies": { + "wrangler": "^4.84.1" + } +} diff --git a/17-dynamic-py-py/pyproject.toml b/17-dynamic-py-py/pyproject.toml new file mode 100644 index 0000000..b966c85 --- /dev/null +++ b/17-dynamic-py-py/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "dynamic-py-py" +version = "0.1.0" +description = "Python Worker that dynamically loads a Python Worker" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "webtypy>=0.1.7", +] + +[dependency-groups] +dev = [ + "workers-py>=1.9.0", + "workers-runtime-sdk" +] diff --git a/17-dynamic-py-py/src/entry.py b/17-dynamic-py-py/src/entry.py new file mode 100644 index 0000000..91b1dba --- /dev/null +++ b/17-dynamic-py-py/src/entry.py @@ -0,0 +1,50 @@ +from pyodide.ffi import create_proxy, to_js +from js import Object +from workers import WorkerEntrypoint + +# The source code of the dynamically-loaded Python Worker. In a real +# application this could be loaded from KV, R2, or any other storage. +DYNAMIC_WORKER_SOURCE = """ +from workers import Response, WorkerEntrypoint + + +class Default(WorkerEntrypoint): + async def fetch(self, request): + return Response("Hello from a dynamically-loaded Python Worker!") +""" + + +def _worker_code(): + """Return a JS object describing the Worker to load. + + The shape matches the `WorkerCode` interface documented at + https://developers.cloudflare.com/dynamic-workers/api-reference/ + """ + return to_js( + { + "compatibilityDate": "2026-04-01", + "compatibilityFlags": ["python_workers"], + "mainModule": "entry.py", + "modules": {"entry.py": DYNAMIC_WORKER_SOURCE}, + }, + dict_converter=Object.fromEntries, + ) + + +class Default(WorkerEntrypoint): + async def fetch(self, request): + # `env.LOADER` is the Worker Loader binding configured in + # wrangler.jsonc. `get(id, callback)` returns a stub for a Worker + # loaded from the code returned by the callback. The runtime caches + # the loaded isolate by `id` so the callback usually runs only once. + worker = self.env.LOADER.get( + "hello-python-v1", + create_proxy(_worker_code), + ) + + # Forward the incoming request to the dynamic Worker's default + # entrypoint and return its response unchanged. We pass the + # underlying JS Request object so the runtime sees a proper + # `Request`, not the Python wrapper. + entrypoint = worker.getEntrypoint() + return await entrypoint.fetch(request._js_request) diff --git a/17-dynamic-py-py/wrangler.jsonc b/17-dynamic-py-py/wrangler.jsonc new file mode 100644 index 0000000..c0fcd9c --- /dev/null +++ b/17-dynamic-py-py/wrangler.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "dynamic-py-py", + "main": "src/entry.py", + "compatibility_date": "2026-04-01", + "compatibility_flags": [ + "python_workers" + ], + "worker_loaders": [ + { + "binding": "LOADER" + } + ], + "observability": { + "enabled": true + } +}