From 01238df50fece7fd7ede837111534f0a106ab9e7 Mon Sep 17 00:00:00 2001 From: echonesis Date: Mon, 1 Dec 2025 14:31:58 +0800 Subject: [PATCH 1/5] feat: deploy Burr in Vercel --- examples/deployment/vercel/.gitignore | 2 + examples/deployment/vercel/README.md | 123 ++++++++++++++++++ examples/deployment/vercel/api/counter.py | 111 ++++++++++++++++ examples/deployment/vercel/app/__init__.py | 0 examples/deployment/vercel/app/counter_app.py | 43 ++++++ examples/deployment/vercel/requirements.txt | 1 + 6 files changed, 280 insertions(+) create mode 100644 examples/deployment/vercel/.gitignore create mode 100644 examples/deployment/vercel/README.md create mode 100644 examples/deployment/vercel/api/counter.py create mode 100644 examples/deployment/vercel/app/__init__.py create mode 100644 examples/deployment/vercel/app/counter_app.py create mode 100644 examples/deployment/vercel/requirements.txt diff --git a/examples/deployment/vercel/.gitignore b/examples/deployment/vercel/.gitignore new file mode 100644 index 000000000..c8a733615 --- /dev/null +++ b/examples/deployment/vercel/.gitignore @@ -0,0 +1,2 @@ +.vercel +.env*.local diff --git a/examples/deployment/vercel/README.md b/examples/deployment/vercel/README.md new file mode 100644 index 000000000..8b95a1523 --- /dev/null +++ b/examples/deployment/vercel/README.md @@ -0,0 +1,123 @@ +# Deploy Burr in Vercel + +[Vercel](https://vercel.com/) - serverless platform for frontend frameworks and serverless functions. + +Here we have an example of how to deploy a Burr application as a Vercel Serverless Function. + +## Prerequisites + +- **Node.js**: Required for Vercel CLI (v14 or higher) +- **Vercel Account**: Sign up at [vercel.com](https://vercel.com/signup) (free tier available) + +## Step-by-Step Guide + +### 1. Install Vercel CLI: + +```bash +npm install -g vercel +``` + +### 2. Local tests: + +Start the local development server: + +```bash +vercel dev +``` + +Send test request to check if the function executes correctly: + +```bash +curl -X POST "http://localhost:3000/api/counter" \ + -H "Content-Type: application/json" \ + -d '{"number": 5}' +``` + +Expected response: + +```json +{"counter": 5, "counter_limit": 5, "__SEQUENCE_ID": 5, "__PRIOR_STEP": "result"} +``` + +### 3. Login to Vercel: + +```bash +vercel login +``` + +This will open your browser to authenticate with your Vercel account. + +### 4. Deploy to Vercel (Preview): + +Deploy to a preview environment for testing: + +```bash +vercel +``` + +### 5. Test Preview Deployment: + +Vercel will provide a preview URL. Test it: + +```bash +curl -X POST "https://your-project-xxx.vercel.app/api/counter" \ + -H "Content-Type: application/json" \ + -d '{"number": 5}' +``` + +### 6. Deploy to Production: + +Once preview testing is successful, deploy to production: + +```bash +vercel --prod +``` + +Your production URL will be: + +``` +https://your-project.vercel.app +``` + +### 7. Test Production Deployment: + +```bash +curl -X POST "https://your-project.vercel.app/api/counter" \ + -H "Content-Type: application/json" \ + -d '{"number": 5}' +``` + +## Alternative: Deploy via Git Integration (Recommended) + +### Import project in Vercel Dashboard: + +- Go to https://vercel.com/new +- Click "Import Git Repository" +- Select your repository +- Click "Deploy" + +## Troubleshooting + +### If deployment fails: + +View detailed logs: + +```bash +vercel logs --follow +``` + +### If function returns 404: + +Ensure your handler file is in the `api/` directory with correct format. + +### If you see deployment URL instead of production URL: + +The production URL is always in the format: `https://your-project.vercel.app` + +Check your Vercel Dashboard → Domains section for the correct URL. + +## Resources + +- [Vercel Documentation](https://vercel.com/docs) +- [Vercel Python Runtime](https://vercel.com/docs/functions/runtimes/python) +- [Vercel CLI Reference](https://vercel.com/docs/cli) \ No newline at end of file diff --git a/examples/deployment/vercel/api/counter.py b/examples/deployment/vercel/api/counter.py new file mode 100644 index 000000000..8a8725ef1 --- /dev/null +++ b/examples/deployment/vercel/api/counter.py @@ -0,0 +1,111 @@ +""" +Vercel Serverless Function for counter application +Endpoint: /api/counter +""" +from http.server import BaseHTTPRequestHandler +import json +from app import counter_app + + +class handler(BaseHTTPRequestHandler): + """Vercel Serverless Function handler. + + This class handles HTTP requests for the counter API endpoint. + Must inherit from BaseHTTPRequestHandler to work with Vercel's Python runtime. + """ + + def do_POST(self): + """Handle POST requests to increment counter. + + Expects JSON body with 'number' field indicating count limit. + Returns serialized application state on success. + """ + try: + # Read request body + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length) + + # Parse JSON payload + data = json.loads(body.decode('utf-8')) + + # Extract parameter (equivalent to Lambda's event["body"]["number"]) + count_up_to = int(data.get("number", 0)) + + # Validate input + if count_up_to <= 0: + self.send_error_response(400, "number must be greater than 0") + return + + # Execute business logic (identical to Lambda implementation) + app = counter_app.application(count_up_to) + action, result, state = app.run(halt_after=["result"]) + + # Return success response with serialized state + self.send_json_response(200, state.serialize()) + + except json.JSONDecodeError: + self.send_error_response(400, "Invalid JSON format") + except ValueError as e: + self.send_error_response(400, f"Invalid number format: {str(e)}") + except KeyError as e: + self.send_error_response(400, f"Missing required field: {str(e)}") + except Exception as e: + # Log error for debugging + print(f"Error in counter handler: {str(e)}") + import traceback + traceback.print_exc() + self.send_error_response(500, "Internal server error") + + def do_GET(self): + """Handle GET requests - not allowed. + + Returns 405 Method Not Allowed error. + """ + self.send_error_response(405, "Only POST method is allowed") + + def do_PUT(self): + """Handle PUT requests - not allowed. + + Returns 405 Method Not Allowed error. + """ + self.send_error_response(405, "Only POST method is allowed") + + def do_DELETE(self): + """Handle DELETE requests - not allowed. + + Returns 405 Method Not Allowed error. + """ + self.send_error_response(405, "Only POST method is allowed") + + def send_json_response(self, status_code, data): + """Send JSON response to client. + + Args: + status_code: HTTP status code + data: Response data (dict, list, or any JSON-serializable object) + """ + self.send_response(status_code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + + if isinstance(data, (dict, list)): + response_body = json.dumps(data, ensure_ascii=False) + else: + response_body = str(data) + + self.wfile.write(response_body.encode('utf-8')) + + def send_error_response(self, status_code, message): + """Send error response to client. + + Args: + status_code: HTTP error status code + message: Error message string to include in response body + """ + self.send_response(status_code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + + error_body = json.dumps({'error': message}) + self.wfile.write(error_body.encode('utf-8')) + \ No newline at end of file diff --git a/examples/deployment/vercel/app/__init__.py b/examples/deployment/vercel/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/deployment/vercel/app/counter_app.py b/examples/deployment/vercel/app/counter_app.py new file mode 100644 index 000000000..76eafabe3 --- /dev/null +++ b/examples/deployment/vercel/app/counter_app.py @@ -0,0 +1,43 @@ +""" +This is a very simple counting application. + +It's here to help you get the mechanics of deploying a Burr application to AWS Lambda. +""" + +import time + +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} + time.sleep(0.5) # sleep to simulate a longer running function + 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: + """function to return a burr application""" + return ( + burr.core.ApplicationBuilder() + .with_graph(graph) + .with_state(**{"counter": 0, "counter_limit": count_up_to}) + .with_entrypoint("counter") + .build() + ) + \ No newline at end of file diff --git a/examples/deployment/vercel/requirements.txt b/examples/deployment/vercel/requirements.txt new file mode 100644 index 000000000..a78cac9dd --- /dev/null +++ b/examples/deployment/vercel/requirements.txt @@ -0,0 +1 @@ +burr From 65215dbe15c724c3239e451e6c768cb408bf9774 Mon Sep 17 00:00:00 2001 From: echonesis Date: Mon, 1 Dec 2025 14:34:01 +0800 Subject: [PATCH 2/5] chore: remove test settings --- examples/deployment/vercel/.gitignore | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 examples/deployment/vercel/.gitignore diff --git a/examples/deployment/vercel/.gitignore b/examples/deployment/vercel/.gitignore deleted file mode 100644 index c8a733615..000000000 --- a/examples/deployment/vercel/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.vercel -.env*.local From 03fb604e5280c764d70de3a1b7351165876faf78 Mon Sep 17 00:00:00 2001 From: echonesis Date: Mon, 1 Dec 2025 14:38:06 +0800 Subject: [PATCH 3/5] chore: add EOL --- examples/deployment/vercel/api/counter.py | 3 ++- examples/deployment/vercel/app/counter_app.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/deployment/vercel/api/counter.py b/examples/deployment/vercel/api/counter.py index 8a8725ef1..4d6ebb4f3 100644 --- a/examples/deployment/vercel/api/counter.py +++ b/examples/deployment/vercel/api/counter.py @@ -108,4 +108,5 @@ def send_error_response(self, status_code, message): error_body = json.dumps({'error': message}) self.wfile.write(error_body.encode('utf-8')) - \ No newline at end of file + + diff --git a/examples/deployment/vercel/app/counter_app.py b/examples/deployment/vercel/app/counter_app.py index 76eafabe3..f6ff11466 100644 --- a/examples/deployment/vercel/app/counter_app.py +++ b/examples/deployment/vercel/app/counter_app.py @@ -40,4 +40,4 @@ def application(count_up_to: int = 10) -> Application: .with_entrypoint("counter") .build() ) - \ No newline at end of file + From fbf89133df0985dd0329e7cec04dc8856f3ab814 Mon Sep 17 00:00:00 2001 From: echonesis Date: Mon, 1 Dec 2025 14:39:18 +0800 Subject: [PATCH 4/5] chore: remove additional line --- examples/deployment/vercel/api/counter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/deployment/vercel/api/counter.py b/examples/deployment/vercel/api/counter.py index 4d6ebb4f3..89055b0c5 100644 --- a/examples/deployment/vercel/api/counter.py +++ b/examples/deployment/vercel/api/counter.py @@ -109,4 +109,3 @@ def send_error_response(self, status_code, message): error_body = json.dumps({'error': message}) self.wfile.write(error_body.encode('utf-8')) - From 2763ecc4b4c1749ced5c151077b29233745c07f5 Mon Sep 17 00:00:00 2001 From: echonesis Date: Wed, 3 Dec 2025 14:36:52 +0800 Subject: [PATCH 5/5] fix: add ASF license section --- examples/deployment/vercel/README.md | 19 +++++++++++++++++++ examples/deployment/vercel/api/counter.py | 17 +++++++++++++++++ examples/deployment/vercel/app/__init__.py | 16 ++++++++++++++++ examples/deployment/vercel/app/counter_app.py | 17 +++++++++++++++++ examples/deployment/vercel/requirements.txt | 1 + 5 files changed, 70 insertions(+) diff --git a/examples/deployment/vercel/README.md b/examples/deployment/vercel/README.md index 8b95a1523..13762fb35 100644 --- a/examples/deployment/vercel/README.md +++ b/examples/deployment/vercel/README.md @@ -1,3 +1,22 @@ + + # Deploy Burr in Vercel [Vercel](https://vercel.com/) - serverless platform for frontend frameworks and serverless functions. diff --git a/examples/deployment/vercel/api/counter.py b/examples/deployment/vercel/api/counter.py index 89055b0c5..fd4e53c32 100644 --- a/examples/deployment/vercel/api/counter.py +++ b/examples/deployment/vercel/api/counter.py @@ -1,3 +1,20 @@ +# 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. + """ Vercel Serverless Function for counter application Endpoint: /api/counter diff --git a/examples/deployment/vercel/app/__init__.py b/examples/deployment/vercel/app/__init__.py index e69de29bb..13a83393a 100644 --- a/examples/deployment/vercel/app/__init__.py +++ b/examples/deployment/vercel/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/vercel/app/counter_app.py b/examples/deployment/vercel/app/counter_app.py index f6ff11466..06697427a 100644 --- a/examples/deployment/vercel/app/counter_app.py +++ b/examples/deployment/vercel/app/counter_app.py @@ -1,3 +1,20 @@ +# 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. diff --git a/examples/deployment/vercel/requirements.txt b/examples/deployment/vercel/requirements.txt index a78cac9dd..c10793cba 100644 --- a/examples/deployment/vercel/requirements.txt +++ b/examples/deployment/vercel/requirements.txt @@ -1 +1,2 @@ burr +# for tracking you'd add extra dependencies