Skip to content

Commit fe0a174

Browse files
authored
Merge pull request #6 from franccesco/feat/async-bulk-operations
feat: add async bulk operations with concurrent execution
2 parents edd1981 + 440ca89 commit fe0a174

10 files changed

Lines changed: 2160 additions & 429 deletions

File tree

docs/guide/bulk-operations.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,274 @@ total_successful = sum(len(r.successful) for r in all_results)
389389
total_failed = sum(len(r.failed) for r in all_results)
390390
```
391391

392+
## Async Bulk Operations
393+
394+
The Bloomy SDK provides async versions of all bulk operations, enabling concurrent execution for significantly better performance when processing multiple items.
395+
396+
### Benefits of Async Bulk Operations
397+
398+
- **Concurrent Execution**: Process multiple items simultaneously instead of sequentially
399+
- **Better Performance**: Reduce total execution time by up to 80% for large batches
400+
- **Rate Limit Control**: Use `max_concurrent` parameter to control request parallelism
401+
- **Same Error Handling**: Uses the same `BulkCreateResult` structure as sync operations
402+
403+
### Async Todos Example
404+
405+
```python
406+
import asyncio
407+
from bloomy import AsyncClient
408+
from datetime import datetime, timedelta
409+
410+
async def create_todos_async():
411+
async with AsyncClient(api_key="your-api-key") as client:
412+
# Prepare todos
413+
todos = [
414+
{
415+
"meeting_id": 123,
416+
"title": f"Task {i}",
417+
"due_date": (datetime.now() + timedelta(days=i)).date().isoformat()
418+
}
419+
for i in range(1, 21) # Create 20 todos
420+
]
421+
422+
# Create with controlled concurrency
423+
result = await client.todo.create_many(todos, max_concurrent=10)
424+
425+
print(f"Created {len(result.successful)} todos concurrently")
426+
if result.failed:
427+
print(f"Failed: {len(result.failed)}")
428+
429+
# Run the async function
430+
asyncio.run(create_todos_async())
431+
```
432+
433+
### Async Issues Example
434+
435+
```python
436+
async def bulk_create_issues():
437+
async with AsyncClient(api_key="your-api-key") as client:
438+
issues = [
439+
{
440+
"meeting_id": 123,
441+
"title": f"Issue {i}: Performance concern",
442+
"notes": f"Details about issue {i}"
443+
}
444+
for i in range(1, 11)
445+
]
446+
447+
# Higher concurrency for smaller payloads
448+
result = await client.issue.create_many(issues, max_concurrent=15)
449+
450+
# Process results
451+
for issue in result.successful:
452+
print(f"Created issue: {issue.title} (ID: {issue.id})")
453+
```
454+
455+
### Async Goals Example
456+
457+
```python
458+
async def create_quarterly_goals():
459+
async with AsyncClient(api_key="your-api-key") as client:
460+
# Different goals for team members
461+
team_goals = []
462+
for user_id in [456, 789, 101, 112]:
463+
team_goals.extend([
464+
{
465+
"meeting_id": 123,
466+
"title": f"Q1 Goal for User {user_id}",
467+
"user_id": user_id
468+
}
469+
])
470+
471+
# Conservative concurrency for complex operations
472+
result = await client.goal.create_many(team_goals, max_concurrent=5)
473+
474+
return result
475+
```
476+
477+
### Async Meetings Example
478+
479+
```python
480+
async def setup_recurring_meetings():
481+
async with AsyncClient(api_key="your-api-key") as client:
482+
# Create a month of weekly meetings
483+
meetings = [
484+
{
485+
"title": f"Week {week} Team Sync",
486+
"attendees": [456, 789, 321]
487+
}
488+
for week in range(1, 5)
489+
]
490+
491+
# Create meetings concurrently
492+
create_result = await client.meeting.create_many(meetings, max_concurrent=5)
493+
494+
# Then fetch all details concurrently
495+
meeting_ids = [m['id'] for m in create_result.successful]
496+
details_result = await client.meeting.get_many(meeting_ids, max_concurrent=10)
497+
498+
return details_result.successful
499+
```
500+
501+
### Controlling Concurrency with max_concurrent
502+
503+
The `max_concurrent` parameter controls how many requests can be in flight simultaneously:
504+
505+
```python
506+
async def demonstrate_concurrency_control():
507+
async with AsyncClient(api_key="your-api-key") as client:
508+
todos = [{"title": f"Todo {i}", "meeting_id": 123} for i in range(100)]
509+
510+
# Conservative: 3 concurrent requests (slower but safer)
511+
result_conservative = await client.todo.create_many(todos, max_concurrent=3)
512+
513+
# Moderate: 10 concurrent requests (balanced)
514+
result_moderate = await client.todo.create_many(todos, max_concurrent=10)
515+
516+
# Aggressive: 20 concurrent requests (faster but may hit rate limits)
517+
result_aggressive = await client.todo.create_many(todos, max_concurrent=20)
518+
```
519+
520+
!!! tip "Choosing max_concurrent"
521+
- **Small payloads** (todos, issues): 10-20 concurrent requests
522+
- **Complex operations** (meetings with attendees): 5-10 concurrent requests
523+
- **Rate-limited environments**: 3-5 concurrent requests
524+
- **Default value**: 5 (conservative and safe)
525+
526+
### Error Handling with Async Bulk Operations
527+
528+
Error handling works the same as sync operations but with async/await syntax:
529+
530+
```python
531+
async def robust_bulk_create():
532+
async with AsyncClient(api_key="your-api-key") as client:
533+
todos = [
534+
{"title": "Valid todo", "meeting_id": 123},
535+
{"title": "Missing meeting_id"}, # Will fail
536+
{"title": "Another valid todo", "meeting_id": 123}
537+
]
538+
539+
result = await client.todo.create_many(todos)
540+
541+
# Handle successes
542+
successful_ids = [todo.id for todo in result.successful]
543+
print(f"Successfully created todos: {successful_ids}")
544+
545+
# Handle failures
546+
for failure in result.failed:
547+
print(f"Failed at index {failure.index}: {failure.error}")
548+
print(f"Failed data: {failure.input_data}")
549+
550+
# Optionally retry with corrected data
551+
if "meeting_id" in failure.error:
552+
corrected = {**failure.input_data, "meeting_id": 123}
553+
retry_result = await client.todo.create(**corrected)
554+
```
555+
556+
### Performance Comparison: Async vs Sync
557+
558+
Here's a practical example comparing async and sync performance:
559+
560+
```python
561+
import asyncio
562+
import time
563+
from bloomy import Client, AsyncClient
564+
565+
def sync_bulk_create(todos_data):
566+
"""Synchronous bulk creation."""
567+
start = time.time()
568+
569+
with Client(api_key="your-api-key") as client:
570+
result = client.todo.create_many(todos_data)
571+
572+
duration = time.time() - start
573+
return result, duration
574+
575+
async def async_bulk_create(todos_data, max_concurrent=10):
576+
"""Asynchronous bulk creation."""
577+
start = time.time()
578+
579+
async with AsyncClient(api_key="your-api-key") as client:
580+
result = await client.todo.create_many(todos_data, max_concurrent=max_concurrent)
581+
582+
duration = time.time() - start
583+
return result, duration
584+
585+
# Compare performance
586+
async def performance_comparison():
587+
# Create test data
588+
todos_data = [
589+
{"title": f"Todo {i}", "meeting_id": 123}
590+
for i in range(50)
591+
]
592+
593+
# Run sync version
594+
sync_result, sync_time = sync_bulk_create(todos_data)
595+
596+
# Run async version
597+
async_result, async_time = await async_bulk_create(todos_data)
598+
599+
print(f"Sync version: {sync_time:.2f} seconds")
600+
print(f"Async version: {async_time:.2f} seconds")
601+
print(f"Speed improvement: {(sync_time / async_time - 1) * 100:.0f}%")
602+
603+
# Run comparison
604+
asyncio.run(performance_comparison())
605+
```
606+
607+
!!! note "Typical Performance Gains"
608+
- 10 items: 50-60% faster with async
609+
- 50 items: 70-80% faster with async
610+
- 100+ items: 80-85% faster with async (diminishing returns due to rate limiting)
611+
612+
### Combining Multiple Async Bulk Operations
613+
614+
```python
615+
async def complete_meeting_setup():
616+
"""Set up a complete meeting with all components."""
617+
async with AsyncClient(api_key="your-api-key") as client:
618+
# Create meeting first
619+
meeting_result = await client.meeting.create_many([
620+
{"title": "Q1 Planning Session", "attendees": [456, 789]}
621+
])
622+
623+
if not meeting_result.successful:
624+
return None
625+
626+
meeting_id = meeting_result.successful[0]['id']
627+
628+
# Create all meeting components concurrently
629+
todos_task = client.todo.create_many([
630+
{"title": "Prepare Q1 roadmap", "meeting_id": meeting_id},
631+
{"title": "Review budget allocation", "meeting_id": meeting_id}
632+
])
633+
634+
issues_task = client.issue.create_many([
635+
{"title": "Resource constraints", "meeting_id": meeting_id},
636+
{"title": "Timeline concerns", "meeting_id": meeting_id}
637+
])
638+
639+
goals_task = client.goal.create_many([
640+
{"title": "Complete product launch", "meeting_id": meeting_id},
641+
{"title": "Achieve 20% growth", "meeting_id": meeting_id}
642+
])
643+
644+
# Wait for all operations to complete
645+
todos_result, issues_result, goals_result = await asyncio.gather(
646+
todos_task, issues_task, goals_task
647+
)
648+
649+
print(f"Meeting {meeting_id} setup complete:")
650+
print(f" - Todos: {len(todos_result.successful)}")
651+
print(f" - Issues: {len(issues_result.successful)}")
652+
print(f" - Goals: {len(goals_result.successful)}")
653+
654+
return meeting_id
655+
656+
# Run the setup
657+
asyncio.run(complete_meeting_setup())
658+
```
659+
392660
## Best Practices
393661

394662
1. **Validate Input Data**: Check required fields before bulk operations
@@ -397,6 +665,8 @@ total_failed = sum(len(r.failed) for r in all_results)
397665
4. **Use Appropriate Chunk Sizes**: Balance between efficiency and rate limits
398666
5. **Implement Retry Logic**: For transient failures, consider retrying
399667
6. **Monitor Rate Limits**: Watch for 429 errors and adjust accordingly
668+
7. **Choose Async When Appropriate**: Use async for better performance with multiple items
669+
8. **Control Concurrency**: Adjust `max_concurrent` based on your use case and API limits
400670

401671
## Next Steps
402672

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "bloomy-python"
3-
version = "0.17.0"
3+
version = "0.18.0"
44
description = "Python SDK for Bloom Growth API"
55
readme = "README.md"
66
authors = [{ name = "Franccesco Orozco", email = "franccesco@codingdose.info" }]

0 commit comments

Comments
 (0)