diff --git a/examples/deployment/cloudflare/.gitignore b/examples/deployment/cloudflare/.gitignore new file mode 100644 index 000000000..08696adc8 --- /dev/null +++ b/examples/deployment/cloudflare/.gitignore @@ -0,0 +1,13 @@ +.venv*/ +.wrangler/ +node_modules/ +python_modules/ +.runtime-cache/ +__pycache__/ +.dev.vars* +.env +output/ +wrangler.*.jsonc +package-lock.json +pylock.toml +uv.lock diff --git a/examples/deployment/cloudflare/README.md b/examples/deployment/cloudflare/README.md new file mode 100644 index 000000000..6dbf3900e --- /dev/null +++ b/examples/deployment/cloudflare/README.md @@ -0,0 +1,95 @@ + + +# Deploy Burr to Cloudflare Workers + +This example deploys the same counter application as the +[Vercel example](../vercel/) using Cloudflare's Python Workers runtime. +`POST /api/counter` runs the Burr application and returns its serialized State. + +## Prerequisites + +- Node.js 22 or later, Python 3.13 or later, and [uv](https://docs.astral.sh/uv/). +- A Cloudflare account for deployment. Local testing needs no account. + +## Local setup + +From this directory: + +```bash +npm install --package-lock=false +uv run pywrangler dev +``` + +`pywrangler` installs the Python dependencies and starts the local Worker at +`http://localhost:8787`. Direct dependency versions are specified in +`package.json` and `pyproject.toml`; generated lock files are ignored by Git. + +In another terminal, test the counter: + +```bash +curl -X POST http://localhost:8787/api/counter \ + -H 'Content-Type: application/json' \ + -d '{"number": 5}' +``` + +Expected response: + +```json +{"counter": 5, "counter_limit": 5, "__SEQUENCE_ID": 5, "__PRIOR_STEP": "result"} +``` + +Each request starts a new counter. Requests with a missing, invalid or +nonpositive `number` return HTTP 400; other HTTP methods return HTTP 405. + +## Deploy + +Log in to Cloudflare: + +```bash +npx wrangler login +``` + +Choose a Worker name in `wrangler.jsonc`, then deploy: + +```bash +uv run pywrangler deploy +``` + +Use the HTTPS URL printed by Wrangler to test the deployed Worker: + +```bash +curl -X POST https://your-worker.your-subdomain.workers.dev/api/counter \ + -H 'Content-Type: application/json' \ + -d '{"number": 5}' +``` + +The response should match the local result above. + +## Troubleshooting + +- For HTTP 404, check that the request path is `/api/counter`. +- For HTTP 405, send a POST request with the JSON body shown above. +- To inspect deployed Worker logs, run `npx wrangler tail`. + +## Resources + +- [Cloudflare Python Workers](https://developers.cloudflare.com/workers/languages/python/) +- [Python package support](https://developers.cloudflare.com/workers/languages/python/packages/) +- [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/) diff --git a/examples/deployment/cloudflare/package.json b/examples/deployment/cloudflare/package.json new file mode 100644 index 000000000..0456efaac --- /dev/null +++ b/examples/deployment/cloudflare/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "devDependencies": { + "wrangler": "4.151.2" + } +} diff --git a/examples/deployment/cloudflare/pyproject.toml b/examples/deployment/cloudflare/pyproject.toml new file mode 100644 index 000000000..ec8dbce15 --- /dev/null +++ b/examples/deployment/cloudflare/pyproject.toml @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[project] +name = "burr-cloudflare-workers" +version = "0.1.0" +description = "Burr counter deployed to Cloudflare Python Workers" +requires-python = ">=3.13" +dependencies = ["apache-burr==0.43.0", "workers-runtime-sdk==1.9.5"] + +[dependency-groups] +dev = ["workers-py==1.17.8"] + +# Use PyPI for these pure-Python packages. +[[tool.uv.index]] +name = "pypi" +url = "https://pypi.org/simple" +explicit = true + +[tool.uv.sources] +apache-burr = { index = "pypi" } +workers-runtime-sdk = { index = "pypi" } diff --git a/examples/deployment/cloudflare/src/app/__init__.py b/examples/deployment/cloudflare/src/app/__init__.py new file mode 100644 index 000000000..13a83393a --- /dev/null +++ b/examples/deployment/cloudflare/src/app/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/examples/deployment/cloudflare/src/app/counter_app.py b/examples/deployment/cloudflare/src/app/counter_app.py new file mode 100644 index 000000000..d588c910e --- /dev/null +++ b/examples/deployment/cloudflare/src/app/counter_app.py @@ -0,0 +1,56 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +This is a very simple counting application. + +It's here to help you get the mechanics of deploying a Burr application to Cloudflare Workers. +""" + +import burr.core +from burr.core import Application, Result, State, default, expr +from burr.core.action import action +from burr.core.graph import GraphBuilder + + +@action(reads=["counter"], writes=["counter"]) +def counter(state: State) -> State: + result = {"counter": state["counter"] + 1} + return state.update(**result) + + +# our graph. +graph = ( + GraphBuilder() + .with_actions(counter=counter, result=Result("counter")) + .with_transitions( + ("counter", "counter", expr("counter < counter_limit")), + ("counter", "result", default), + ) + .build() +) + + +def application(count_up_to: int = 10) -> Application: + """Return a Burr Application that counts from zero to ``count_up_to``.""" + return ( + burr.core.ApplicationBuilder() + .with_graph(graph) + .with_state(**{"counter": 0, "counter_limit": count_up_to}) + .with_entrypoint("counter") + .build() + ) diff --git a/examples/deployment/cloudflare/src/entry.py b/examples/deployment/cloudflare/src/entry.py new file mode 100644 index 000000000..094f1c8c4 --- /dev/null +++ b/examples/deployment/cloudflare/src/entry.py @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""HTTP entrypoint for the Burr counter example in Python Workers.""" + +import json +from urllib.parse import urlparse + +from app.counter_app import application +from workers import Response, WorkerEntrypoint + + +def respond(data, status=200): + """Return JSON ``data`` with HTTP ``status``.""" + return Response(json.dumps(data), status=status, headers={"Content-Type": "application/json"}) + + +class Default(WorkerEntrypoint): + async def fetch(self, request): + """Run the counter for a POST ``request`` and return its serialized Burr State.""" + if urlparse(request.url).path != "/api/counter": + return respond({"error": "Not found"}, 404) + if request.method != "POST": + return respond({"error": "Only POST method is allowed"}, 405) + try: + data = json.loads(await request.text()) + if not isinstance(data, dict): + return respond({"error": "JSON object required"}, 400) + count_up_to = int(data.get("number", 0)) + if count_up_to <= 0: + return respond({"error": "number must be greater than 0"}, 400) + except (TypeError, ValueError): + return respond({"error": "Invalid JSON or number"}, 400) + app = application(count_up_to) + _, _, state = app.run(halt_after=["result"]) + return respond(state.serialize()) diff --git a/examples/deployment/cloudflare/wrangler.jsonc b/examples/deployment/cloudflare/wrangler.jsonc new file mode 100644 index 000000000..6cc4e35df --- /dev/null +++ b/examples/deployment/cloudflare/wrangler.jsonc @@ -0,0 +1,23 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "burr-cloudflare-counter", + "main": "src/entry.py", + "compatibility_date": "2026-10-20", + "compatibility_flags": ["python_workers"] +}