diff --git a/README.md b/README.md index 7828c03..03b3bcc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A curated collection of production-ready workflow examples demonstrating various These examples demonstrate how to build robust, scalable workflows using Render's Python SDK. All examples follow best practices for production deployments and include comprehensive documentation. -Render Workflows are Python-only (via `render-sdk`) and must be deployed as Workflow services on Render. +Render Workflows support both Python and TypeScript. This repo contains Python examples using the `render-sdk` package, deployed as Workflow services on Render. ## Examples diff --git a/data-pipeline/main.py b/data-pipeline/main.py index 3ac5e5e..07ea6d8 100644 --- a/data-pipeline/main.py +++ b/data-pipeline/main.py @@ -494,4 +494,5 @@ async def run_data_pipeline(user_ids: list[str]) -> dict: } -app.start() +if __name__ == "__main__": + app.start() diff --git a/etl-job/main.py b/etl-job/main.py index 63f4d48..928754a 100644 --- a/etl-job/main.py +++ b/etl-job/main.py @@ -335,4 +335,5 @@ async def run_etl_pipeline(source_file: str) -> dict: } -app.start() +if __name__ == "__main__": + app.start() diff --git a/file-analyzer/api-service/main.py b/file-analyzer/api-service/main.py index f1eadd2..5a7028b 100644 --- a/file-analyzer/api-service/main.py +++ b/file-analyzer/api-service/main.py @@ -16,7 +16,7 @@ from typing import Any from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi.middleware.cors import CORSMiddleware -from render_sdk import Render +from render_sdk import RenderAsync from render_sdk.client.errors import RenderError, TaskRunError from pydantic import BaseModel @@ -62,9 +62,9 @@ class HealthResponse(BaseModel): # Client SDK helper functions -def get_client() -> Render: +def get_client() -> RenderAsync: """ - Get Render API client. + Get async Render API client. The Client SDK requires a RENDER_API_KEY environment variable. Get your API key from: Render Dashboard → Account Settings → API Keys @@ -76,7 +76,7 @@ def get_client() -> Render: status_code=500, detail="RENDER_API_KEY not configured. Get your API key from Render Dashboard → Account Settings → API Keys" ) - return Render() # Uses RENDER_API_KEY env var automatically + return RenderAsync() # Uses RENDER_API_KEY env var automatically def get_task_identifier(task_name: str) -> str: @@ -191,18 +191,17 @@ async def analyze_file(file: UploadFile = File(...)): logger.info(f"Calling workflow task: {task_identifier}") - # CLIENT SDK CALL: Run the workflow task - # Format: client.workflows.run_task(task_identifier, {"arg": value}) - task_run = await client.workflows.run_task( + # CLIENT SDK CALL: Start the workflow task + # start_task returns an AwaitableTaskRun immediately + started_run = await client.workflows.start_task( task_identifier, {"file_content": file_content_str} ) - logger.info(f"Task started: {task_run.id}") + logger.info(f"Task started: {started_run.id}") - # CLIENT SDK CALL: Await the task completion - # This will block until the task finishes - result = await task_run + # Await the AwaitableTaskRun to wait for completion + result = await started_run logger.info(f"Task completed with status: {result.status}") @@ -210,7 +209,7 @@ async def analyze_file(file: UploadFile = File(...)): task_run_id=result.id, status=result.status, message=f"File '{file.filename}' analyzed successfully", - result=result.results # Task return value + result=result.results ) except TaskRunError as e: @@ -281,16 +280,16 @@ async def analyze_with_custom_task(task_name: str, file: UploadFile = File(...)) logger.info(f"Calling workflow task: {task_identifier}") - # CLIENT SDK CALL: Run the specified workflow task - task_run = await client.workflows.run_task( + # CLIENT SDK CALL: Start the specified workflow task + started_run = await client.workflows.start_task( task_identifier, {"file_content": file_content_str} ) - logger.info(f"Task started: {task_run.id}") + logger.info(f"Task started: {started_run.id}") - # CLIENT SDK CALL: Await the task completion - result = await task_run + # Await the AwaitableTaskRun to wait for completion + result = await started_run logger.info(f"Task '{task_name}' completed with status: {result.status}") @@ -325,7 +324,7 @@ async def analyze_with_custom_task(task_name: str, file: UploadFile = File(...)) import uvicorn logger.info("Starting File Analyzer API Service") - logger.info("This service calls workflow tasks using the Client SDK") + logger.info("This service calls workflow tasks using the async Client SDK (RenderAsync)") logger.info("Required environment variables:") logger.info(" - RENDER_API_KEY: Your Render API key") logger.info(" - WORKFLOW_SERVICE_SLUG: Your workflow service slug (e.g., 'file-analyzer-workflows')") diff --git a/file-analyzer/workflow-service/main.py b/file-analyzer/workflow-service/main.py index ff2112e..ec55246 100644 --- a/file-analyzer/workflow-service/main.py +++ b/file-analyzer/workflow-service/main.py @@ -327,4 +327,5 @@ async def analyze_file(file_content: str) -> dict: } -app.start() +if __name__ == "__main__": + app.start() diff --git a/file-processing/main.py b/file-processing/main.py index b0178ea..0a09a66 100644 --- a/file-processing/main.py +++ b/file-processing/main.py @@ -411,7 +411,6 @@ async def process_file_batch(*file_paths: str) -> dict: Returns: Dictionary with results for all files """ - # Convert to list for easier handling file_paths_list = list(file_paths) logger.info("=" * 80) @@ -421,9 +420,7 @@ async def process_file_batch(*file_paths: str) -> dict: # Process all files in parallel # SUBTASK PATTERN: Call multiple subtasks concurrently using asyncio.gather() logger.info("[BATCH] Launching parallel file processing tasks...") - # Create list of subtask calls (one per file) tasks = [process_single_file(fp) for fp in file_paths_list] - # SUBTASK CALLS: Execute all process_single_file subtasks in parallel results = await asyncio.gather(*tasks) # Aggregate results @@ -513,4 +510,5 @@ async def generate_consolidated_report(batch_result: dict) -> dict: return report -app.start() +if __name__ == "__main__": + app.start() diff --git a/hello-world/main.py b/hello-world/main.py index d84084e..c522110 100644 --- a/hello-world/main.py +++ b/hello-world/main.py @@ -43,18 +43,6 @@ def double(x: int) -> int: Returns: The doubled number """ - # Handle case where x might be a dictionary instead of an integer - if isinstance(x, dict): - if 'x' in x: - x = x['x'] - logger.info(f"[TASK] Extracted x from dictionary: {x}") - else: - logger.error(f"[TASK] Dictionary input missing 'x' key: {x}") - raise ValueError(f"Expected integer or dict with 'x' key, got: {x}") - elif not isinstance(x, int): - logger.error(f"[TASK] Invalid input type: {type(x)}, value: {x}") - raise ValueError(f"Expected integer, got: {type(x)}") - logger.info(f"[TASK] Doubling {x}") result = x * 2 logger.info(f"[TASK] Result: {result}") @@ -131,7 +119,6 @@ async def process_numbers(*numbers: int) -> dict: Returns: Dictionary with original numbers and their doubled values """ - # Convert to list for easier handling numbers_list = list(numbers) logger.info(f"[WORKFLOW] Starting: process_numbers({numbers_list})") @@ -180,7 +167,6 @@ async def calculate_and_process(a: int, b: int, *more_numbers: int) -> dict: Returns: Dictionary with results from multiple workflow steps """ - # Convert to list for easier handling more_numbers_list = list(more_numbers) logger.info("[WORKFLOW] Starting multi-step workflow") @@ -206,4 +192,5 @@ async def calculate_and_process(a: int, b: int, *more_numbers: int) -> dict: return final_result -app.start() +if __name__ == "__main__": + app.start() diff --git a/openai-agent/main.py b/openai-agent/main.py index fdc0faa..bfad56f 100644 --- a/openai-agent/main.py +++ b/openai-agent/main.py @@ -482,7 +482,6 @@ async def multi_turn_conversation(*messages: str) -> dict: Returns: Dictionary with full conversation and all responses """ - # Convert to list for easier handling messages_list = list(messages) logger.info("=" * 80) @@ -522,4 +521,5 @@ async def multi_turn_conversation(*messages: str) -> dict: } -app.start() +if __name__ == "__main__": + app.start()