From b53f5980047d43c3eac89cdb03e104249709e1e7 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 18 Aug 2025 09:21:52 -0400 Subject: [PATCH 1/2] fix: handle empty or incomplete conferencing objects in events - Fixed KeyError when processing events with empty conferencing objects ({}) - Fixed KeyError when conferencing details missing required provider field - Fixed KeyError when conferencing autocreate missing required provider field - Added comprehensive backwards compatibility tests for edge cases - Maintains existing behavior for valid conferencing objects - Returns None for malformed conferencing data instead of raising errors Resolves issue where SDK version 6.11.0 would throw KeyError when processing events containing empty or incomplete conferencing objects. --- CHANGELOG.md | 4 ++ nylas/models/events.py | 24 +++++-- tests/resources/test_events.py | 110 +++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d64ebd..e32f08d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ nylas-python Changelog ====================== +Unreleased +---------- +* Fixed KeyError when processing events with empty or incomplete conferencing objects + v6.11.0 ---------------- * Added `unknown` to ConferencingProvider diff --git a/nylas/models/events.py b/nylas/models/events.py index 0e7929a..d359d31 100644 --- a/nylas/models/events.py +++ b/nylas/models/events.py @@ -227,22 +227,32 @@ class Autocreate: def _decode_conferencing(conferencing: dict) -> Union[Conferencing, None]: """ - Decode a when object into a When object. + Decode a conferencing object into a Conferencing object. Args: - when: The when object to decode. + conferencing: The conferencing object to decode. Returns: - The decoded When object. + The decoded Conferencing object, or None if empty or incomplete. """ if not conferencing: return None + # Handle details case - must have provider to be valid if "details" in conferencing: - return Details.from_dict(conferencing) + if "provider" in conferencing: + return Details.from_dict(conferencing) + else: + # Incomplete details without provider - return None for backwards compatibility + return None + # Handle autocreate case - must have provider to be valid if "autocreate" in conferencing: - return Autocreate.from_dict(conferencing) + if "provider" in conferencing: + return Autocreate.from_dict(conferencing) + else: + # Incomplete autocreate without provider - return None for backwards compatibility + return None # Handle case where provider exists but details/autocreate doesn't if "provider" in conferencing: @@ -257,7 +267,9 @@ def _decode_conferencing(conferencing: dict) -> Union[Conferencing, None]: } return Details.from_dict(details_dict) - raise ValueError(f"Invalid conferencing object, unknown type found: {conferencing}") + # Handle unknown or incomplete conferencing objects by returning None + # This provides backwards compatibility for malformed conferencing data + return None @dataclass_json diff --git a/tests/resources/test_events.py b/tests/resources/test_events.py index 25aba85..14e6288 100644 --- a/tests/resources/test_events.py +++ b/tests/resources/test_events.py @@ -550,3 +550,113 @@ def test_update_event_with_notetaker(self, http_client_response): request_body, overrides=None, ) + + def test_event_with_empty_conferencing_deserialization(self): + """Test event deserialization with empty conferencing object.""" + event_json = { + "id": "test-event-id", + "grant_id": "test-grant-id", + "calendar_id": "test-calendar-id", + "busy": True, + "participants": [ + {"email": "test@example.com", "name": "Test User", "status": "yes"} + ], + "when": { + "start_time": 1497916800, + "end_time": 1497920400, + "object": "timespan" + }, + "conferencing": {}, # Empty conferencing object + "title": "Test Event with Empty Conferencing" + } + + event = Event.from_dict(event_json) + + assert event.id == "test-event-id" + assert event.title == "Test Event with Empty Conferencing" + assert event.conferencing is None + + def test_event_with_incomplete_conferencing_details_deserialization(self): + """Test event deserialization with conferencing details missing provider.""" + event_json = { + "id": "test-event-id", + "grant_id": "test-grant-id", + "calendar_id": "test-calendar-id", + "busy": True, + "participants": [ + {"email": "test@example.com", "name": "Test User", "status": "yes"} + ], + "when": { + "start_time": 1497916800, + "end_time": 1497920400, + "object": "timespan" + }, + "conferencing": { + "details": { + "meeting_code": "code-123456", + "password": "password-123456", + "url": "https://zoom.us/j/1234567890?pwd=1234567890", + } + }, # Details without provider + "title": "Test Event with Incomplete Conferencing Details" + } + + event = Event.from_dict(event_json) + + assert event.id == "test-event-id" + assert event.title == "Test Event with Incomplete Conferencing Details" + assert event.conferencing is None + + def test_event_with_incomplete_conferencing_autocreate_deserialization(self): + """Test event deserialization with conferencing autocreate missing provider.""" + event_json = { + "id": "test-event-id", + "grant_id": "test-grant-id", + "calendar_id": "test-calendar-id", + "busy": True, + "participants": [ + {"email": "test@example.com", "name": "Test User", "status": "yes"} + ], + "when": { + "start_time": 1497916800, + "end_time": 1497920400, + "object": "timespan" + }, + "conferencing": { + "autocreate": {} + }, # Autocreate without provider + "title": "Test Event with Incomplete Conferencing Autocreate" + } + + event = Event.from_dict(event_json) + + assert event.id == "test-event-id" + assert event.title == "Test Event with Incomplete Conferencing Autocreate" + assert event.conferencing is None + + def test_event_with_unknown_conferencing_fields_deserialization(self): + """Test event deserialization with conferencing containing unknown fields.""" + event_json = { + "id": "test-event-id", + "grant_id": "test-grant-id", + "calendar_id": "test-calendar-id", + "busy": True, + "participants": [ + {"email": "test@example.com", "name": "Test User", "status": "yes"} + ], + "when": { + "start_time": 1497916800, + "end_time": 1497920400, + "object": "timespan" + }, + "conferencing": { + "unknown_field": "value" + }, # Unknown conferencing fields + "title": "Test Event with Unknown Conferencing Fields" + } + + event = Event.from_dict(event_json) + + assert event.id == "test-event-id" + assert event.title == "Test Event with Unknown Conferencing Fields" + assert event.conferencing is None From 0f866a3636f3ca8897231a6a471a2466956d8ee2 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 18 Aug 2025 09:25:28 -0400 Subject: [PATCH 2/2] style: fix pylint issues in _decode_conferencing function - Remove unnecessary else statements after return (no-else-return) - Remove trailing whitespace (trailing-whitespace) - Reduce number of return statements to comply with complexity limits (too-many-return-statements) - Consolidate conditional logic for better readability - Maintain identical functionality and test coverage --- nylas/models/events.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/nylas/models/events.py b/nylas/models/events.py index d359d31..118eeaf 100644 --- a/nylas/models/events.py +++ b/nylas/models/events.py @@ -239,20 +239,12 @@ def _decode_conferencing(conferencing: dict) -> Union[Conferencing, None]: return None # Handle details case - must have provider to be valid - if "details" in conferencing: - if "provider" in conferencing: - return Details.from_dict(conferencing) - else: - # Incomplete details without provider - return None for backwards compatibility - return None - - # Handle autocreate case - must have provider to be valid - if "autocreate" in conferencing: - if "provider" in conferencing: - return Autocreate.from_dict(conferencing) - else: - # Incomplete autocreate without provider - return None for backwards compatibility - return None + if "details" in conferencing and "provider" in conferencing: + return Details.from_dict(conferencing) + + # Handle autocreate case - must have provider to be valid + if "autocreate" in conferencing and "provider" in conferencing: + return Autocreate.from_dict(conferencing) # Handle case where provider exists but details/autocreate doesn't if "provider" in conferencing: