Skip to content

Commit 0995cc0

Browse files
authored
Merge pull request #9 from render-examples/rm/sdk-update
update to new sdk
2 parents c786063 + 34177e4 commit 0995cc0

23 files changed

Lines changed: 275 additions & 193 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ A curated collection of production-ready workflow examples demonstrating various
66

77
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.
88

9-
Render Workflows support both Python and TypeScript. This repo contains Python examples using the `render-sdk` package, deployed as Workflow services on Render.
9+
Render Workflows support both Python and TypeScript. This repo contains Python examples using the `render` package, deployed as Workflow services on Render.
1010

1111
## Examples
1212

@@ -16,7 +16,7 @@ Render Workflows support both Python and TypeScript. This repo contains Python e
1616

1717
| Example | Use Case | Key Patterns | Extra Dependencies |
1818
|---------|----------|--------------|-------------------|
19-
| [**Hello World**](./hello-world/) | Learn workflow basics with simple number processing | Task definition, subtask calling with `await`, basic orchestration | None |
19+
| [**Hello World**](./hello-world/) | Learn workflow basics with simple number processing | Task definition, subtask calling with `ctx.run`, basic orchestration | None |
2020
| [**ETL Job**](./etl-job/) | Process CSV data with validation and statistics | Subtasks, sequential processing, batch operations, data validation | None |
2121
| [**OpenAI Agent**](./openai-agent/) | AI customer support agent with tool calling | Tool calling, nested subtasks (3 levels deep), stateful workflows, dynamic orchestration | `openai` |
2222
| [**File Processing**](./file-processing/) | Batch process multiple file formats in parallel | Parallel execution with `asyncio.gather()`, multi-format handling, aggregation | None |
@@ -28,7 +28,7 @@ Render Workflows support both Python and TypeScript. This repo contains Python e
2828
The simplest possible workflow — learn the fundamentals through simple number processing.
2929

3030
- Ultra-simple task definitions
31-
- Clear subtask calling examples
31+
- Clear `ctx.run` subtask examples
3232
- Subtasks in loops demonstration
3333
- Multi-step workflow orchestration
3434
- Heavily commented code explaining every pattern

data-pipeline/README.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ Input:
179179
Once deployed, trigger the pipeline via the Render API or SDK:
180180

181181
```python
182-
from render_sdk import Render
182+
from render import Render
183183

184184
# Uses RENDER_API_KEY environment variable automatically
185185
render = Render()
@@ -212,18 +212,20 @@ Using `asyncio.gather()` ensures all sources are fetched in parallel for maximum
212212

213213
### Stage 2: Transform
214214

215-
**`transform_user_data`**: Combines data from all sources and enriches each user by calling subtasks:
215+
**`transform_user_data`**: Combines data from all sources and enriches each user by running subtasks:
216216
```python
217217
for user in users:
218218
# SUBTASK CALL: Calculate metrics for this user
219-
user_metrics = await calculate_user_metrics(user, transactions, engagement)
219+
user_metrics = await ctx.run(
220+
calculate_user_metrics, user, transactions, engagement
221+
)
220222

221223
# SUBTASK CALL: Enrich with geographic data
222-
geo_data = await enrich_with_geo_data(user['email'])
224+
geo_data = await ctx.run(enrich_with_geo_data, user['email'])
223225

224226
enriched_users.append({**user_metrics, 'geo': geo_data})
225227
```
226-
This demonstrates **sequential subtask calls per item** in a transformation loop.
228+
This demonstrates **sequential subtask runs per item** in a transformation loop.
227229

228230
**`calculate_user_metrics`**: Calculates per-user metrics:
229231
- Total spent and refunded
@@ -283,9 +285,9 @@ This demonstrates **sequential subtask calls per item** in a transformation loop
283285

284286
```python
285287
# SUBTASK PATTERN: Launch multiple subtasks in parallel
286-
user_task = fetch_user_data(user_ids)
287-
transaction_task = fetch_transaction_data(user_ids)
288-
engagement_task = fetch_engagement_data(user_ids)
288+
user_task = ctx.run(fetch_user_data, user_ids)
289+
transaction_task = ctx.run(fetch_transaction_data, user_ids)
290+
engagement_task = ctx.run(fetch_engagement_data, user_ids)
289291

290292
# SUBTASK CALLS: Wait for all three subtasks to complete
291293
user_data, transaction_data, engagement_data = await asyncio.gather(
@@ -304,15 +306,17 @@ Each user is enriched by calling multiple subtasks:
304306
```python
305307
for user in users:
306308
# SUBTASK CALL: Calculate user-specific metrics
307-
metrics = await calculate_user_metrics(user, transactions, engagement)
309+
metrics = await ctx.run(
310+
calculate_user_metrics, user, transactions, engagement
311+
)
308312

309313
# SUBTASK CALL: Enrich with geographic data
310-
geo = await enrich_with_geo_data(user['email'])
314+
geo = await ctx.run(enrich_with_geo_data, user['email'])
311315

312316
enriched_users.append({**metrics, 'geo': geo})
313317
```
314318

315-
This shows **sequential subtask calls** for per-item enrichment.
319+
This shows **sequential subtask runs** for per-item enrichment.
316320

317321
### User Segmentation
318322

@@ -327,7 +331,7 @@ Business logic classifies users into segments:
327331
**Add Real APIs**:
328332
```python
329333
@app.task
330-
async def fetch_user_data_from_api(user_ids: list[str]) -> dict:
334+
async def fetch_user_data_from_api(ctx: TaskContext, user_ids: list[str]) -> dict:
331335
client = get_http_client()
332336
response = await client.post(
333337
"https://api.yourservice.com/users",
@@ -339,7 +343,7 @@ async def fetch_user_data_from_api(user_ids: list[str]) -> dict:
339343
**Add Database Integration**:
340344
```python
341345
@app.task
342-
async def load_to_warehouse(insights: dict) -> dict:
346+
async def load_to_warehouse(ctx: TaskContext, insights: dict) -> dict:
343347
# Connect to data warehouse (Snowflake, BigQuery, etc.)
344348
# Insert aggregated insights
345349
# Return confirmation
@@ -349,7 +353,7 @@ async def load_to_warehouse(insights: dict) -> dict:
349353
**Add Caching**:
350354
```python
351355
@app.task
352-
async def fetch_with_cache(source: str, key: str) -> dict:
356+
async def fetch_with_cache(ctx: TaskContext, source: str, key: str) -> dict:
353357
# Check Redis/Memcached
354358
# If miss, fetch from source and cache
355359
# Return data
@@ -359,7 +363,7 @@ async def fetch_with_cache(source: str, key: str) -> dict:
359363
**Add Notifications**:
360364
```python
361365
@app.task
362-
async def send_pipeline_notification(result: dict) -> dict:
366+
async def send_pipeline_notification(ctx: TaskContext, result: dict) -> dict:
363367
# Send to Slack, email, etc.
364368
# Notify stakeholders of pipeline completion
365369
pass
@@ -375,7 +379,7 @@ async def send_pipeline_notification(result: dict) -> dict:
375379

376380
## Important Notes
377381

378-
- **Python-only**: Workflows are only supported in Python via render-sdk
382+
- **Python-only**: Workflows are only supported in Python via `render`
379383
- **No Blueprint Support**: Workflows don't support render.yaml blueprint configuration
380384
- **Mock Data**: Example uses simulated data; replace with real API calls in production
381385
- **Idempotency**: Design pipeline to be safely re-runnable

data-pipeline/main.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import logging
1818
from datetime import datetime, timedelta
1919

20-
from render_sdk import Retry, Workflows
20+
from render import Retry, TaskContext, Workflows
2121

2222
# Configure logging
2323
logging.basicConfig(
@@ -55,7 +55,7 @@ def get_http_client():
5555
# ============================================================================
5656

5757
@app.task
58-
async def fetch_user_data(user_ids: list[str]) -> dict:
58+
async def fetch_user_data(ctx: TaskContext, user_ids: list[str]) -> dict:
5959
"""
6060
Fetch user profile data from user service.
6161
@@ -90,7 +90,7 @@ async def fetch_user_data(user_ids: list[str]) -> dict:
9090

9191

9292
@app.task
93-
async def fetch_transaction_data(user_ids: list[str], days: int = 30) -> dict:
93+
async def fetch_transaction_data(ctx: TaskContext, user_ids: list[str], days: int = 30) -> dict:
9494
"""
9595
Fetch transaction history for users.
9696
@@ -130,7 +130,7 @@ async def fetch_transaction_data(user_ids: list[str], days: int = 30) -> dict:
130130

131131

132132
@app.task
133-
async def fetch_engagement_data(user_ids: list[str]) -> dict:
133+
async def fetch_engagement_data(ctx: TaskContext, user_ids: list[str]) -> dict:
134134
"""
135135
Fetch user engagement metrics.
136136
@@ -174,7 +174,7 @@ async def fetch_engagement_data(user_ids: list[str]) -> dict:
174174
# ============================================================================
175175

176176
@app.task
177-
async def enrich_with_geo_data(user_email: str) -> dict:
177+
async def enrich_with_geo_data(ctx: TaskContext, user_email: str) -> dict:
178178
"""
179179
Enrich user data with geographic information.
180180
@@ -200,6 +200,7 @@ async def enrich_with_geo_data(user_email: str) -> dict:
200200

201201
@app.task
202202
async def calculate_user_metrics(
203+
ctx: TaskContext,
203204
user: dict,
204205
transactions: list[dict],
205206
engagement: dict
@@ -268,6 +269,7 @@ async def calculate_user_metrics(
268269

269270
@app.task
270271
async def transform_user_data(
272+
ctx: TaskContext,
271273
user_data: dict,
272274
transaction_data: dict,
273275
engagement_data: dict
@@ -303,11 +305,13 @@ async def transform_user_data(
303305
user_engagement = engagement_map.get(user['id'], {})
304306

305307
# Calculate metrics for this user
306-
user_metrics = await calculate_user_metrics(user, transactions, user_engagement)
308+
user_metrics = await ctx.run(
309+
calculate_user_metrics, user, transactions, user_engagement
310+
)
307311

308312
# Enrich with geo data
309313
user_email = user.get('email', f"{user['id']}@example.com")
310-
geo_data = await enrich_with_geo_data(user_email)
314+
geo_data = await ctx.run(enrich_with_geo_data, user_email)
311315
user_metrics['geo'] = geo_data
312316

313317
enriched_users.append(user_metrics)
@@ -326,7 +330,7 @@ async def transform_user_data(
326330
# ============================================================================
327331

328332
@app.task
329-
def aggregate_insights(enriched_data: dict) -> dict:
333+
def aggregate_insights(ctx: TaskContext, enriched_data: dict) -> dict:
330334
"""
331335
Generate aggregate insights from enriched user data.
332336
@@ -397,7 +401,7 @@ def aggregate_insights(enriched_data: dict) -> dict:
397401
# ============================================================================
398402

399403
@app.task
400-
async def run_data_pipeline(user_ids: list[str]) -> dict:
404+
async def run_data_pipeline(ctx: TaskContext, user_ids: list[str]) -> dict:
401405
"""
402406
Execute the complete data pipeline.
403407
@@ -426,9 +430,9 @@ async def run_data_pipeline(user_ids: list[str]) -> dict:
426430
try:
427431
# Stage 1: EXTRACT - Fetch from all sources in parallel
428432
logger.info("[PIPELINE] Stage 1/3: EXTRACT (parallel)")
429-
user_task = fetch_user_data(user_ids)
430-
transaction_task = fetch_transaction_data(user_ids)
431-
engagement_task = fetch_engagement_data(user_ids)
433+
user_task = ctx.run(fetch_user_data, user_ids)
434+
transaction_task = ctx.run(fetch_transaction_data, user_ids)
435+
engagement_task = ctx.run(fetch_engagement_data, user_ids)
432436

433437
# Wait for all extractions to complete
434438
user_data, transaction_data, engagement_data = await asyncio.gather(
@@ -441,7 +445,8 @@ async def run_data_pipeline(user_ids: list[str]) -> dict:
441445

442446
# Stage 2: TRANSFORM - Combine and enrich
443447
logger.info("[PIPELINE] Stage 2/3: TRANSFORM")
444-
enriched_data = await transform_user_data(
448+
enriched_data = await ctx.run(
449+
transform_user_data,
445450
user_data,
446451
transaction_data,
447452
engagement_data
@@ -451,7 +456,7 @@ async def run_data_pipeline(user_ids: list[str]) -> dict:
451456

452457
# Stage 3: LOAD - Generate insights
453458
logger.info("[PIPELINE] Stage 3/3: AGGREGATE")
454-
insights = await aggregate_insights(enriched_data)
459+
insights = await ctx.run(aggregate_insights, enriched_data)
455460

456461
logger.info("[PIPELINE] Insights generated successfully")
457462

data-pipeline/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
render-sdk>=0.5.0
1+
render>=1.0.1
22
httpx>=0.27.0

etl-job/README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Process customer signup data from CSV files with validation, cleaning, and stati
1313

1414
## Features
1515

16-
- **Subtask Execution**: Demonstrates calling tasks from other tasks using `await`
16+
- **Subtask Execution**: Demonstrates running tasks from other tasks with `ctx.run`
1717
- **Extract**: Read data from CSV files (extensible to APIs, databases)
1818
- **Transform**: Validate records with comprehensive error tracking
1919
- **Load**: Compute statistics and prepare aggregated insights
@@ -126,7 +126,7 @@ Input:
126126
Once deployed, trigger the ETL pipeline via the Render API or SDK:
127127

128128
```python
129-
from render_sdk import Render
129+
from render import Render
130130

131131
# Uses RENDER_API_KEY environment variable automatically
132132
render = Render()
@@ -163,23 +163,23 @@ This demonstrates how the pipeline handles data quality issues.
163163
- Validates age range (0-120)
164164
- Returns cleaned data with error tracking
165165

166-
**`transform_batch`**: Processes all records by calling `validate_record` as a subtask for each one:
166+
**`transform_batch`**: Processes all records by running `validate_record` as a subtask for each one:
167167
```python
168168
for record in records:
169-
# Call validate_record as a subtask
170-
validated = await validate_record(record)
169+
# Run validate_record as a subtask on its own compute
170+
validated = await ctx.run(validate_record, record)
171171
```
172-
This demonstrates **calling subtasks in a loop** for batch processing.
172+
This demonstrates **running subtasks in a loop** for batch processing.
173173

174174
**`compute_statistics`**: Aggregates valid records to produce:
175175
- Country distribution
176176
- Age statistics (min, max, average)
177177
- Data quality metrics
178178

179-
**`run_etl_pipeline`**: Main orchestrator that calls three subtasks sequentially:
180-
1. `await extract_csv_data(source_file)` - Extract data
181-
2. `await transform_batch(raw_records)` - Validate records (which calls `validate_record` for each)
182-
3. `await compute_statistics(valid_records)` - Generate insights
179+
**`run_etl_pipeline`**: Main orchestrator that runs three subtasks sequentially:
180+
1. `await ctx.run(extract_csv_data, source_file)` - Extract data
181+
2. `await ctx.run(transform_batch, raw_records)` - Validate records (which runs `validate_record` for each)
182+
3. `await ctx.run(compute_statistics, valid_records)` - Generate insights
183183

184184
This demonstrates **sequential subtask orchestration** for multi-stage pipelines.
185185

@@ -188,7 +188,7 @@ This demonstrates **sequential subtask orchestration** for multi-stage pipelines
188188
**Add Database Loading**:
189189
```python
190190
@app.task
191-
async def load_to_database(records: list[dict]) -> dict:
191+
async def load_to_database(ctx: TaskContext, records: list[dict]) -> dict:
192192
# Connect to database
193193
# Insert records
194194
# Return confirmation
@@ -198,7 +198,7 @@ async def load_to_database(records: list[dict]) -> dict:
198198
**Add API Data Source**:
199199
```python
200200
@app.task
201-
async def extract_from_api(api_url: str) -> list[dict]:
201+
async def extract_from_api(ctx: TaskContext, api_url: str) -> list[dict]:
202202
# Fetch from REST API
203203
# Parse JSON response
204204
# Return records
@@ -210,16 +210,16 @@ async def extract_from_api(api_url: str) -> list[dict]:
210210
import asyncio
211211

212212
@app.task
213-
async def transform_batch_parallel(records: list[dict]) -> dict:
213+
async def transform_batch_parallel(ctx: TaskContext, records: list[dict]) -> dict:
214214
# Validate all records in parallel
215-
tasks = [validate_record(record) for record in records]
215+
tasks = [ctx.run(validate_record, record) for record in records]
216216
results = await asyncio.gather(*tasks)
217217
# Aggregate results
218218
return results
219219
```
220220

221221
## Important Notes
222222

223-
- **Python-only**: Workflows are only supported in Python via render-sdk
223+
- **Python-only**: Workflows are only supported in Python via `render`
224224
- **No Blueprint Support**: Workflows don't support render.yaml blueprint configuration
225225
- **Service Type**: Deploy as a Workflow service on Render (not Background Worker or Web Service)

0 commit comments

Comments
 (0)