diff --git a/README.md b/README.md index 89e6886..cf4e9a2 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,14 @@ new_meeting = client.meeting.create( # Delete a meeting client.meeting.delete(meeting_id=123) + +# Get multiple meetings by ID (batch read) +result = client.meeting.get_many([123, 456, 789]) +for meeting in result.successful: + print(f"{meeting.name} - {meeting.meeting_date}") +# Handle any failed retrievals +for error in result.failed: + print(f"Failed to get meeting: {error.error}") ``` ### Todos diff --git a/docs/api/operations/meetings.md b/docs/api/operations/meetings.md index 89035de..c9d8582 100644 --- a/docs/api/operations/meetings.md +++ b/docs/api/operations/meetings.md @@ -49,6 +49,11 @@ The async version `AsyncMeetingOperations` provides the same methods as above, b # Get metrics for a meeting metrics = client.meeting.metrics(meeting_id=123) + + # Batch retrieve multiple meetings + result = client.meeting.get_many([123, 456, 789]) + for meeting in result.successful: + print(f"{meeting.name}: {len(meeting.attendees)} attendees") ``` === "Async" @@ -72,6 +77,11 @@ The async version `AsyncMeetingOperations` provides the same methods as above, b # Get metrics for a meeting metrics = await client.meeting.metrics(meeting_id=123) + + # Batch retrieve multiple meetings + result = await client.meeting.get_many([123, 456, 789]) + for meeting in result.successful: + print(f"{meeting.name}: {len(meeting.attendees)} attendees") asyncio.run(main()) ``` @@ -82,6 +92,7 @@ The async version `AsyncMeetingOperations` provides the same methods as above, b |--------|-------------|------------| | `list()` | Get all meetings | `include_closed` | | `details()` | Get detailed meeting information with attendees, issues, todos, and metrics | `meeting_id` | +| `get_many()` | Batch retrieve multiple meetings by ID | `meeting_ids` | | `attendees()` | Get meeting attendees | `meeting_id` | | `issues()` | Get issues from a meeting | `meeting_id` | | `todos()` | Get todos from a meeting | `meeting_id` | diff --git a/docs/guide/bulk-operations.md b/docs/guide/bulk-operations.md index eac853d..5248a57 100644 --- a/docs/guide/bulk-operations.md +++ b/docs/guide/bulk-operations.md @@ -1,19 +1,23 @@ # Bulk Operations -The Bloomy SDK provides bulk creation methods for efficiently creating multiple resources at once. These methods use a best-effort approach, processing items sequentially to avoid rate limiting while capturing both successful and failed operations. +The Bloomy SDK provides bulk operations for efficiently working with multiple resources at once. These methods use a best-effort approach, processing items sequentially to avoid rate limiting while capturing both successful and failed operations. ## Overview Bulk operations are available for: +**Creation:** - Issues - Todos - Meetings - Goals (Rocks) +**Reading:** +- Meetings (batch retrieve by ID) + Each bulk operation returns a `BulkCreateResult` containing: -- `successful`: List of successfully created items +- `successful`: List of successfully processed items (created or retrieved) - `failed`: List of `BulkCreateError` objects with failure details !!! note "Best-Effort Processing" @@ -186,6 +190,77 @@ meeting_ids = [m['id'] for m in result.successful] print(f"Created meetings with IDs: {meeting_ids}") ``` +## Batch Reading Operations + +### Retrieving Multiple Meetings + +The SDK supports batch retrieval of meetings by ID, which is useful when you need details for multiple meetings: + +```python +# Get details for multiple meetings +meeting_ids = [123, 456, 789, 999] # IDs to retrieve +result = client.meeting.get_many(meeting_ids) + +# Process successful retrievals +print(f"Successfully retrieved {len(result.successful)} meetings:") +for meeting in result.successful: + print(f" - {meeting.name} on {meeting.meeting_date}") + print(f" Attendees: {len(meeting.attendees)}") + print(f" Todos: {len(meeting.todos)}") + print(f" Issues: {len(meeting.issues)}") + +# Handle failed retrievals +if result.failed: + print(f"\nFailed to retrieve {len(result.failed)} meetings:") + for failure in result.failed: + meeting_id = failure.input_data.get('meeting_id') + print(f" - Meeting ID {meeting_id}: {failure.error}") +``` + +### Use Cases for Batch Reading + +```python +# Example 1: Get details for all meetings from a list operation +meetings_list = client.meeting.list() +meeting_ids = [m.id for m in meetings_list[:10]] # First 10 meetings + +result = client.meeting.get_many(meeting_ids) +meetings_with_details = result.successful + +# Example 2: Aggregate data across multiple meetings +def get_all_open_issues(meeting_ids): + """Get all open issues from multiple meetings.""" + result = client.meeting.get_many(meeting_ids) + + all_issues = [] + for meeting in result.successful: + open_issues = [issue for issue in meeting.issues if not issue.closed] + all_issues.extend(open_issues) + + return all_issues + +# Example 3: Build a dashboard with meeting metrics +def build_meeting_dashboard(meeting_ids): + """Build dashboard data for multiple meetings.""" + result = client.meeting.get_many(meeting_ids) + + dashboard = { + 'total_meetings': len(result.successful), + 'total_attendees': sum(len(m.attendees) for m in result.successful), + 'total_todos': sum(len(m.todos) for m in result.successful), + 'total_open_issues': sum( + len([i for i in m.issues if not i.closed]) + for m in result.successful + ), + 'failed_retrievals': len(result.failed) + } + + return dashboard +``` + +!!! note "Performance Considerations" + The `get_many()` method fetches full meeting details including attendees, issues, todos, and metrics for each meeting. For large batches, this can be data-intensive. Consider chunking if retrieving many meetings. + ## Error Handling Strategies ### Retry Failed Operations diff --git a/pyproject.toml b/pyproject.toml index fe90f63..76ee300 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "bloomy-python" -version = "0.16.0" +version = "0.17.0" description = "Python SDK for Bloom Growth API" readme = "README.md" authors = [{ name = "Franccesco Orozco", email = "franccesco@codingdose.info" }] diff --git a/src/bloomy/operations/meetings.py b/src/bloomy/operations/meetings.py index d7e5269..960655d 100644 --- a/src/bloomy/operations/meetings.py +++ b/src/bloomy/operations/meetings.py @@ -380,3 +380,46 @@ def create_many( ) return BulkCreateResult(successful=successful, failed=failed) + + def get_many(self, meeting_ids: list[int]) -> BulkCreateResult[MeetingDetails]: + """Retrieve details for multiple meetings in a best-effort manner. + + Processes each meeting ID sequentially to avoid rate limiting. + Failed operations are captured and returned alongside successful ones. + + Args: + meeting_ids: List of meeting IDs to retrieve details for + + Returns: + BulkCreateResult containing: + - successful: List of MeetingDetails instances for successfully + retrieved meetings + - failed: List of BulkCreateError instances for failed retrievals + + Example: + ```python + result = client.meeting.get_many([1, 2, 3]) + + print(f"Retrieved {len(result.successful)} meetings") + for error in result.failed: + print(f"Failed at index {error.index}: {error.error}") + ``` + + """ + successful: builtins.list[MeetingDetails] = [] + failed: builtins.list[BulkCreateError] = [] + + for index, meeting_id in enumerate(meeting_ids): + try: + # Use the existing details method to get meeting details + meeting_details = self.details(meeting_id) + successful.append(meeting_details) + + except Exception as e: + failed.append( + BulkCreateError( + index=index, input_data={"meeting_id": meeting_id}, error=str(e) + ) + ) + + return BulkCreateResult(successful=successful, failed=failed) diff --git a/tests/test_bulk_operations.py b/tests/test_bulk_operations.py index 12f3e46..d31935a 100644 --- a/tests/test_bulk_operations.py +++ b/tests/test_bulk_operations.py @@ -331,3 +331,225 @@ def test_create_many_meetings_with_exception(self, mock_http_client: Mock) -> No assert len(result.failed) == 1 assert result.failed[0].index == 0 assert "Network error" in result.failed[0].error + + def test_get_many_all_success( + self, mock_http_client: Mock, mock_user_id: Mock + ) -> None: + """Test retrieving multiple meetings when all succeed.""" + # Mock list response for finding meetings + list_response = Mock() + list_response.json.return_value = [ + {"Id": 456, "Type": "NameId", "Key": "NameId_456", "Name": "Meeting 1"}, + {"Id": 457, "Type": "NameId", "Key": "NameId_457", "Name": "Meeting 2"}, + {"Id": 458, "Type": "NameId", "Key": "NameId_458", "Name": "Meeting 3"}, + ] + + # Mock responses for each meeting's details + attendees_response = Mock() + attendees_response.json.return_value = [ + {"Id": 123, "Name": "John Doe", "ImageUrl": "https://example.com/john.jpg"} + ] + + issues_response = Mock() + issues_response.json.return_value = [] + + todos_response = Mock() + todos_response.json.return_value = [] + + metrics_response = Mock() + metrics_response.json.return_value = [] + + # Set up the side effect for all calls + # Pattern: list, attendees, issues, todos, metrics (repeated for each meeting) + mock_http_client.get.side_effect = [ + list_response, # For finding meeting 456 + attendees_response, + issues_response, + todos_response, + metrics_response, + list_response, # For finding meeting 457 + attendees_response, + issues_response, + todos_response, + metrics_response, + list_response, # For finding meeting 458 + attendees_response, + issues_response, + todos_response, + metrics_response, + ] + + meeting_ops = MeetingOperations(mock_http_client) + + result = meeting_ops.get_many([456, 457, 458]) + + assert isinstance(result, BulkCreateResult) + assert len(result.successful) == 3 + assert len(result.failed) == 0 + + # Check successful meetings + assert result.successful[0].id == 456 + assert result.successful[0].name == "Meeting 1" + assert result.successful[1].id == 457 + assert result.successful[1].name == "Meeting 2" + assert result.successful[2].id == 458 + assert result.successful[2].name == "Meeting 3" + + # Verify all API calls were made (5 calls per meeting) + assert mock_http_client.get.call_count == 15 + + def test_get_many_partial_failure( + self, mock_http_client: Mock, mock_user_id: Mock + ) -> None: + """Test retrieving multiple meetings with some failures.""" + # Mock list response for finding meetings + list_response = Mock() + list_response.json.return_value = [ + {"Id": 456, "Type": "NameId", "Key": "NameId_456", "Name": "Meeting 1"}, + ] + + empty_list_response = Mock() + empty_list_response.json.return_value = [] # Meeting not found + + # Mock responses for meeting details + attendees_response = Mock() + attendees_response.json.return_value = [] + + issues_response = Mock() + issues_response.json.return_value = [] + + todos_response = Mock() + todos_response.json.return_value = [] + + metrics_response = Mock() + metrics_response.json.return_value = [] + + # Set up the side effect + mock_http_client.get.side_effect = [ + list_response, # First meeting found + attendees_response, + issues_response, + todos_response, + metrics_response, + empty_list_response, # Second meeting not found + list_response, # Third meeting found + attendees_response, + issues_response, + todos_response, + metrics_response, + ] + + meeting_ops = MeetingOperations(mock_http_client) + + result = meeting_ops.get_many([456, 999, 456]) # 999 doesn't exist + + assert len(result.successful) == 2 + assert len(result.failed) == 1 + + # Check failure details + assert result.failed[0].index == 1 + assert result.failed[0].input_data == {"meeting_id": 999} + assert "Meeting with ID 999 not found" in result.failed[0].error + + def test_get_many_empty_list( + self, mock_http_client: Mock, mock_user_id: Mock + ) -> None: + """Test retrieving meetings with empty list.""" + meeting_ops = MeetingOperations(mock_http_client) + + result = meeting_ops.get_many([]) + + assert len(result.successful) == 0 + assert len(result.failed) == 0 + assert mock_http_client.get.call_count == 0 + + def test_get_many_network_error( + self, mock_http_client: Mock, mock_user_id: Mock + ) -> None: + """Test retrieving meetings with network errors.""" + # First meeting succeeds + list_response = Mock() + list_response.json.return_value = [ + {"Id": 456, "Type": "NameId", "Key": "NameId_456", "Name": "Meeting 1"}, + ] + + attendees_response = Mock() + attendees_response.json.return_value = [] + + issues_response = Mock() + issues_response.json.return_value = [] + + todos_response = Mock() + todos_response.json.return_value = [] + + metrics_response = Mock() + metrics_response.json.return_value = [] + + # Set up side effect with network error on second meeting + mock_http_client.get.side_effect = [ + list_response, # First meeting list + attendees_response, + issues_response, + todos_response, + metrics_response, + Exception("Network error"), # Network error on second meeting list + ] + + meeting_ops = MeetingOperations(mock_http_client) + + result = meeting_ops.get_many([456, 457]) + + assert len(result.successful) == 1 + assert len(result.failed) == 1 + assert result.successful[0].id == 456 + assert result.failed[0].index == 1 + assert "Network error" in result.failed[0].error + + def test_get_many_duplicate_ids( + self, mock_http_client: Mock, mock_user_id: Mock + ) -> None: + """Test retrieving meetings with duplicate IDs.""" + # Mock list response + list_response = Mock() + list_response.json.return_value = [ + {"Id": 456, "Type": "NameId", "Key": "NameId_456", "Name": "Meeting 1"}, + ] + + # Mock responses for meeting details + attendees_response = Mock() + attendees_response.json.return_value = [] + + issues_response = Mock() + issues_response.json.return_value = [] + + todos_response = Mock() + todos_response.json.return_value = [] + + metrics_response = Mock() + metrics_response.json.return_value = [] + + # Set up the side effect (need responses for two calls to same meeting) + mock_http_client.get.side_effect = [ + list_response, # First call for meeting 456 + attendees_response, + issues_response, + todos_response, + metrics_response, + list_response, # Second call for meeting 456 + attendees_response, + issues_response, + todos_response, + metrics_response, + ] + + meeting_ops = MeetingOperations(mock_http_client) + + result = meeting_ops.get_many([456, 456]) + + assert len(result.successful) == 2 + assert len(result.failed) == 0 + assert result.successful[0].id == 456 + assert result.successful[1].id == 456 + + # Should make 10 calls (5 per meeting retrieval) + assert mock_http_client.get.call_count == 10 diff --git a/uv.lock b/uv.lock index 39d9138..49cb925 100644 --- a/uv.lock +++ b/uv.lock @@ -49,7 +49,7 @@ wheels = [ [[package]] name = "bloomy-python" -version = "0.15.0" +version = "0.16.0" source = { editable = "." } dependencies = [ { name = "httpx" },