diff --git a/examples/deployment/vercel/README.md b/examples/deployment/vercel/README.md new file mode 100644 index 000000000..13762fb35 --- /dev/null +++ b/examples/deployment/vercel/README.md @@ -0,0 +1,142 @@ + + +# 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..fd4e53c32 --- /dev/null +++ b/examples/deployment/vercel/api/counter.py @@ -0,0 +1,128 @@ +# 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 +""" +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')) + diff --git a/examples/deployment/vercel/app/__init__.py b/examples/deployment/vercel/app/__init__.py new file mode 100644 index 000000000..13a83393a --- /dev/null +++ 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 new file mode 100644 index 000000000..06697427a --- /dev/null +++ b/examples/deployment/vercel/app/counter_app.py @@ -0,0 +1,60 @@ +# 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 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() + ) + diff --git a/examples/deployment/vercel/requirements.txt b/examples/deployment/vercel/requirements.txt new file mode 100644 index 000000000..c10793cba --- /dev/null +++ b/examples/deployment/vercel/requirements.txt @@ -0,0 +1,2 @@ +burr +# for tracking you'd add extra dependencies