From 3b6eb17b9e47eaf0158e81322e90be85c87f5ba4 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 27 Jan 2025 10:34:01 -0500 Subject: [PATCH 1/6] feat: add response headers to responses and add demo example --- examples/response_headers_demo/README.md | 107 ++++++++++++++ .../response_headers_example.py | 139 ++++++++++++++++++ nylas/handler/api_resources.py | 24 +-- nylas/handler/http_client.py | 14 +- nylas/models/errors.py | 18 ++- nylas/models/response.py | 53 ++++++- tests/conftest.py | 40 ++--- tests/handler/test_api_resources.py | 105 +++++++++++++ tests/handler/test_http_client.py | 105 ++++++++++++- 9 files changed, 543 insertions(+), 62 deletions(-) create mode 100644 examples/response_headers_demo/README.md create mode 100644 examples/response_headers_demo/response_headers_example.py diff --git a/examples/response_headers_demo/README.md b/examples/response_headers_demo/README.md new file mode 100644 index 00000000..8ba6959d --- /dev/null +++ b/examples/response_headers_demo/README.md @@ -0,0 +1,107 @@ +# Response Headers Demo + +This example demonstrates how to access and use response headers from various Nylas API responses. It shows how headers are available in different types of responses: + +1. List responses (from methods like `list()`) +2. Single-item responses (from methods like `find()`) +3. Error responses (when API calls fail) + +## What You'll Learn + +- How to access response headers from successful API calls +- How to access headers from error responses +- Common headers you'll encounter in Nylas API responses +- How headers differ between list and single-item responses + +## Headers Demonstrated + +The example will show various headers that Nylas includes in responses, such as: + +- `request-id`: Unique identifier for the API request +- `x-ratelimit-limit`: Your rate limit for the endpoint +- `x-ratelimit-remaining`: Remaining requests within the current window +- `x-ratelimit-reset`: When the rate limit window resets +- And more... + +## Prerequisites + +Before running this example, make sure you have: + +1. A Nylas API key +2. A Nylas grant ID +3. Python 3.7 or later installed +4. The Nylas Python SDK installed + +## Setup + +1. First, install the SDK in development mode: + ```bash + cd /path/to/nylas-python + pip install -e . + ``` + +2. Set up your environment variables: + ```bash + export NYLAS_API_KEY="your_api_key" + export NYLAS_GRANT_ID="your_grant_id" + ``` + +## Running the Example + +Run the example with: +```bash +python examples/response_headers_demo/response_headers_example.py +``` + +The script will: +1. Demonstrate headers from a list response by fetching messages +2. Show headers from a single-item response by fetching one message +3. Trigger and catch an error to show error response headers + +## Example Output + +You'll see output similar to this: + +``` +Demonstrating Response Headers +============================ + +Demonstrating List Response Headers +---------------------------------- +✓ Successfully retrieved messages + +Response Headers: +------------------------ +request-id: req_abcd1234 +x-ratelimit-limit: 1000 +x-ratelimit-remaining: 999 +... + +Demonstrating Find Response Headers +---------------------------------- +✓ Successfully retrieved single message + +Response Headers: +------------------------ +request-id: req_efgh5678 +... + +Demonstrating Error Response Headers +--------------------------------- +✓ Successfully caught expected error +✗ Error Type: invalid_request +✗ Request ID: req_ijkl9012 +✗ Status Code: 404 + +Error Response Headers: +------------------------ +request-id: req_ijkl9012 +... +``` + +## Error Handling + +The example includes proper error handling and will show you how to: +- Catch `NylasApiError` exceptions +- Access error details and headers +- Handle different types of API errors gracefully \ No newline at end of file diff --git a/examples/response_headers_demo/response_headers_example.py b/examples/response_headers_demo/response_headers_example.py new file mode 100644 index 00000000..a33a0c12 --- /dev/null +++ b/examples/response_headers_demo/response_headers_example.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Nylas SDK Example: Response Headers Demo + +This example demonstrates how to access and use response headers from various Nylas API +responses, including successful responses and error cases. + +Required Environment Variables: + NYLAS_API_KEY: Your Nylas API key + NYLAS_GRANT_ID: Your Nylas grant ID + +Usage: + First, install the SDK in development mode: + cd /path/to/nylas-python + pip install -e . + + Then set environment variables and run: + export NYLAS_API_KEY="your_api_key" + export NYLAS_GRANT_ID="your_grant_id" + python examples/response_headers_demo/response_headers_example.py +""" + +import os +import sys +from typing import Optional + +from nylas import Client +from nylas.models.errors import NylasApiError + + +def get_env_or_exit(var_name: str) -> str: + """Get an environment variable or exit if not found.""" + value = os.getenv(var_name) + if not value: + print(f"Error: {var_name} environment variable is required") + sys.exit(1) + return value + + +def print_response_headers(headers: dict, prefix: str = "") -> None: + """Helper function to print response headers.""" + print(f"\n{prefix} Response Headers:") + print("------------------------") + for key, value in headers.items(): + print(f"{key}: {value}") + + +def demonstrate_list_response_headers(client: Client, grant_id: str) -> None: + """Demonstrate headers in list responses.""" + print("\nDemonstrating List Response Headers") + print("----------------------------------") + + try: + # List messages to get a ListResponse + messages = client.messages.list(identifier=grant_id) + + print("✓ Successfully retrieved messages") + print_response_headers(messages.headers) + print(f"Total messages count: {len(messages.data)}") + + except NylasApiError as e: + print("\nError occurred while listing messages:") + print(f"✗ Error Type: {e.type}") + print(f"✗ Provider Error: {e.provider_error}") + print(f"✗ Request ID: {e.request_id}") + print_response_headers(e.headers, "Error") + + +def demonstrate_find_response_headers(client: Client, grant_id: str) -> None: + """Demonstrate headers in find/single-item responses.""" + print("\nDemonstrating Find Response Headers") + print("----------------------------------") + + try: + # Get the first message to demonstrate single-item response + messages = client.messages.list(identifier=grant_id) + if not messages.data: + print("No messages found to demonstrate find response") + return + + message_id = messages.data[0].id + message = client.messages.find(identifier=grant_id, message_id=message_id) + + print("✓ Successfully retrieved single message") + print_response_headers(message.headers) + + except NylasApiError as e: + print("\nError occurred while finding message:") + print(f"✗ Error Type: {e.type}") + print(f"✗ Provider Error: {e.provider_error}") + print(f"✗ Request ID: {e.request_id}") + print_response_headers(e.headers, "Error") + + +def demonstrate_error_response_headers(client: Client, grant_id: str) -> None: + """Demonstrate headers in error responses.""" + print("\nDemonstrating Error Response Headers") + print("---------------------------------") + + try: + # Attempt to find a non-existent message + message = client.messages.find( + identifier=grant_id, + message_id="non-existent-id-123" + ) + + except NylasApiError as e: + print("✓ Successfully caught expected error") + print(f"✗ Error Type: {e.type}") + print(f"✗ Provider Error: {e.provider_error}") + print(f"✗ Request ID: {e.request_id}") + print(f"✗ Status Code: {e.status_code}") + print_response_headers(e.headers, "Error") + + +def main(): + """Main function demonstrating response headers.""" + # Get required environment variables + api_key = get_env_or_exit("NYLAS_API_KEY") + grant_id = get_env_or_exit("NYLAS_GRANT_ID") + + # Initialize Nylas client + client = Client( + api_key=api_key, + ) + + print("\nDemonstrating Response Headers") + print("============================") + + # Demonstrate different types of responses and their headers + demonstrate_list_response_headers(client, grant_id) + demonstrate_find_response_headers(client, grant_id) + demonstrate_error_response_headers(client, grant_id) + + print("\nExample completed!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/nylas/handler/api_resources.py b/nylas/handler/api_resources.py index 35c6dce2..546bc8b5 100644 --- a/nylas/handler/api_resources.py +++ b/nylas/handler/api_resources.py @@ -16,11 +16,11 @@ def list( request_body=None, overrides=None, ) -> ListResponse: - response_json = self._http_client._execute( + response_json, response_headers = self._http_client._execute( "GET", path, headers, query_params, request_body, overrides=overrides ) - return ListResponse.from_dict(response_json, response_type) + return ListResponse.from_dict(response_json, response_type, response_headers) class FindableApiResource(Resource): @@ -33,11 +33,11 @@ def find( request_body=None, overrides=None, ) -> Response: - response_json = self._http_client._execute( + response_json, response_headers = self._http_client._execute( "GET", path, headers, query_params, request_body, overrides=overrides ) - return Response.from_dict(response_json, response_type) + return Response.from_dict(response_json, response_type, response_headers) class CreatableApiResource(Resource): @@ -50,11 +50,11 @@ def create( request_body=None, overrides=None, ) -> Response: - response_json = self._http_client._execute( + response_json, response_headers = self._http_client._execute( "POST", path, headers, query_params, request_body, overrides=overrides ) - return Response.from_dict(response_json, response_type) + return Response.from_dict(response_json, response_type, response_headers) class UpdatableApiResource(Resource): @@ -68,11 +68,11 @@ def update( method="PUT", overrides=None, ): - response_json = self._http_client._execute( + response_json, response_headers = self._http_client._execute( method, path, headers, query_params, request_body, overrides=overrides ) - return Response.from_dict(response_json, response_type) + return Response.from_dict(response_json, response_type, response_headers) class UpdatablePatchApiResource(Resource): @@ -86,11 +86,11 @@ def patch( method="PATCH", overrides=None, ): - response_json = self._http_client._execute( + response_json, response_headers = self._http_client._execute( method, path, headers, query_params, request_body, overrides=overrides ) - return Response.from_dict(response_json, response_type) + return Response.from_dict(response_json, response_type, response_headers) class DestroyableApiResource(Resource): @@ -106,7 +106,7 @@ def destroy( if response_type is None: response_type = DeleteResponse - response_json = self._http_client._execute( + response_json, response_headers = self._http_client._execute( "DELETE", path, headers, query_params, request_body, overrides=overrides ) - return response_type.from_dict(response_json) + return response_type.from_dict(response_json, headers=response_headers) diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index a2a7be2f..9769cb3c 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -1,9 +1,10 @@ import sys -from typing import Union +from typing import Union, Tuple, Dict from urllib.parse import urlparse, quote import requests from requests import Response +from requests.structures import CaseInsensitiveDict from nylas._client_sdk_version import __VERSION__ from nylas.models.errors import ( @@ -16,7 +17,7 @@ ) -def _validate_response(response: Response) -> dict: +def _validate_response(response: Response) -> Tuple[Dict, CaseInsensitiveDict]: json = response.json() if response.status_code >= 400: parsed_url = urlparse(response.url) @@ -26,10 +27,10 @@ def _validate_response(response: Response) -> dict: or "connect/revoke" in parsed_url.path ): parsed_error = NylasOAuthErrorResponse.from_dict(json) - raise NylasOAuthError(parsed_error, response.status_code) + raise NylasOAuthError(parsed_error, response.status_code, response.headers) parsed_error = NylasApiErrorResponse.from_dict(json) - raise NylasApiError(parsed_error, response.status_code) + raise NylasApiError(parsed_error, response.status_code, response.headers) except (KeyError, TypeError) as exc: request_id = json.get("request_id", None) raise NylasApiError( @@ -41,9 +42,10 @@ def _validate_response(response: Response) -> dict: ), ), status_code=response.status_code, + headers=response.headers, ) from exc - - return json + + return (json, response.headers) def _build_query_params(base_url: str, query_params: dict = None) -> str: query_param_parts = [] diff --git a/nylas/models/errors.py b/nylas/models/errors.py index 83ff3def..dc71ea92 100644 --- a/nylas/models/errors.py +++ b/nylas/models/errors.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import Optional +from requests.structures import CaseInsensitiveDict from dataclasses_json import dataclass_json @@ -11,6 +12,7 @@ class AbstractNylasApiError(Exception): Attributes: request_id: The unique identifier of the request. status_code: The HTTP status code of the error response. + headers: The headers returned from the API. """ def __init__( @@ -18,6 +20,7 @@ def __init__( message: str, request_id: Optional[str] = None, status_code: Optional[int] = None, + headers: Optional[CaseInsensitiveDict] = None, ): """ Args: @@ -27,6 +30,7 @@ def __init__( """ self.request_id: str = request_id self.status_code: int = status_code + self.headers: CaseInsensitiveDict = headers super().__init__(message) @@ -96,22 +100,24 @@ class NylasApiError(AbstractNylasApiError): Attributes: type: Error type. provider_error: Provider Error. + headers: The headers returned from the API. """ def __init__( self, api_error: NylasApiErrorResponse, status_code: Optional[int] = None, + headers: Optional[CaseInsensitiveDict] = None, ): """ Args: api_error: The error details from the API. status_code: The HTTP status code of the error response. """ - super().__init__(api_error.error.message, api_error.request_id, status_code) + super().__init__(api_error.error.message, api_error.request_id, status_code, headers) self.type: str = api_error.error.type self.provider_error: Optional[dict] = api_error.error.provider_error - + self.headers: CaseInsensitiveDict = headers class NylasOAuthError(AbstractNylasApiError): """ @@ -128,18 +134,19 @@ def __init__( self, oauth_error: NylasOAuthErrorResponse, status_code: Optional[int] = None, + headers: Optional[CaseInsensitiveDict] = None, ): """ Args: oauth_error: The error details from the API. status_code: The HTTP status code of the error response. """ - super().__init__(oauth_error.error_description, None, status_code) + super().__init__(oauth_error.error_description, None, status_code, headers) self.error: str = oauth_error.error self.error_code: int = oauth_error.error_code self.error_description: str = oauth_error.error_description self.error_uri: str = oauth_error.error_uri - + self.headers: CaseInsensitiveDict = headers class NylasSdkTimeoutError(AbstractNylasSdkError): """ @@ -150,7 +157,7 @@ class NylasSdkTimeoutError(AbstractNylasSdkError): timeout: The timeout value set in the Nylas SDK, in seconds. """ - def __init__(self, url: str, timeout: int): + def __init__(self, url: str, timeout: int, headers: Optional[CaseInsensitiveDict] = None): """ Args: url: The URL that timed out. @@ -161,3 +168,4 @@ def __init__(self, url: str, timeout: int): ) self.url: str = url self.timeout: int = timeout + self.headers: CaseInsensitiveDict = headers \ No newline at end of file diff --git a/nylas/models/response.py b/nylas/models/response.py index f8a91652..62ae765c 100644 --- a/nylas/models/response.py +++ b/nylas/models/response.py @@ -3,6 +3,8 @@ from dataclasses_json import DataClassJsonMixin, dataclass_json +from requests.structures import CaseInsensitiveDict + T = TypeVar("T", bound=DataClassJsonMixin) @@ -17,36 +19,41 @@ class Response(tuple, Generic[T]): data: T request_id: str + headers: Optional[CaseInsensitiveDict] = None - def __new__(cls, data: T, request_id: str): + def __new__(cls, data: T, request_id: str, headers: Optional[CaseInsensitiveDict] = None): """ Initialize the response object. Args: data: The requested data object. request_id: The request ID. + headers: The headers returned from the API. """ # Initialize the tuple for destructuring support - instance = super().__new__(cls, (data, request_id)) + instance = super().__new__(cls, (data, request_id, headers)) instance.data = data instance.request_id = request_id + instance.headers = headers return instance @classmethod - def from_dict(cls, resp: dict, generic_type): + def from_dict(cls, resp: dict, generic_type, headers: Optional[CaseInsensitiveDict] = None): """ Convert a dictionary to a response object. Args: resp: The dictionary to convert. generic_type: The type to deserialize the data object into. + headers: The headers returned from the API. """ return cls( data=generic_type.from_dict(resp["data"]), request_id=resp["request_id"], + headers=headers, ) @@ -58,13 +65,15 @@ class ListResponse(tuple, Generic[T]): data: The list of requested data objects. request_id: The request ID. next_cursor: The cursor to use to get the next page of data. + headers: The headers returned from the API. """ data: List[T] request_id: str next_cursor: Optional[str] = None + headers: Optional[CaseInsensitiveDict] = None - def __new__(cls, data: List[T], request_id: str, next_cursor: Optional[str] = None): + def __new__(cls, data: List[T], request_id: str, next_cursor: Optional[str] = None, headers: Optional[CaseInsensitiveDict] = None): """ Initialize the response object. @@ -72,24 +81,27 @@ def __new__(cls, data: List[T], request_id: str, next_cursor: Optional[str] = No data: The list of requested data objects. request_id: The request ID. next_cursor: The cursor to use to get the next page of data. + headers: The headers returned from the API. """ # Initialize the tuple for destructuring support - instance = super().__new__(cls, (data, request_id, next_cursor)) + instance = super().__new__(cls, (data, request_id, next_cursor, headers)) instance.data = data instance.request_id = request_id instance.next_cursor = next_cursor + instance.headers = headers return instance @classmethod - def from_dict(cls, resp: dict, generic_type): + def from_dict(cls, resp: dict, generic_type, headers: Optional[CaseInsensitiveDict] = None): """ Convert a dictionary to a response object. Args: resp: The dictionary to convert. generic_type: The type to deserialize the data objects into. + headers: The headers returned from the API. """ converted_data = [] @@ -100,10 +112,10 @@ def from_dict(cls, resp: dict, generic_type): data=converted_data, request_id=resp["request_id"], next_cursor=resp.get("next_cursor", None), + headers=headers, ) -@dataclass_json @dataclass class DeleteResponse: """ @@ -111,12 +123,24 @@ class DeleteResponse: Attributes: request_id: The request ID returned from the API. + headers: The headers returned from the API. """ request_id: str + headers: Optional[CaseInsensitiveDict] = None + + @classmethod + def from_dict(cls, resp: dict, headers: Optional[CaseInsensitiveDict] = None): + """ + Convert a dictionary to a response object. + + Args: + resp: The dictionary to convert. + headers: The headers returned from the API. + """ + return cls(request_id=resp["request_id"], headers=headers) -@dataclass_json @dataclass class RequestIdOnlyResponse: """ @@ -124,6 +148,19 @@ class RequestIdOnlyResponse: Attributes: request_id: The request ID returned from the API. + headers: The headers returned from the API. """ request_id: str + headers: Optional[CaseInsensitiveDict] = None + + @classmethod + def from_dict(cls, resp: dict, headers: Optional[CaseInsensitiveDict] = None): + """ + Convert a dictionary to a response object. + + Args: + resp: The dictionary to convert. + headers: The headers returned from the API. + """ + return cls(request_id=resp["request_id"], headers=headers) diff --git a/tests/conftest.py b/tests/conftest.py index 196695a3..d588618e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,33 +53,23 @@ def mock_session_timeout(): @pytest.fixture def http_client_list_response(): with patch( - "nylas.models.response.ListResponse.from_dict", - return_value=ListResponse([], "bar"), + "nylas.models.response.ListResponse.from_dict", return_value=ListResponse([], "bar", None, {"X-Test-Header": "test"}) ): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "abc-123", - "data": [ - { - "id": "calendar-123", - "grant_id": "grant-123", - "name": "Mock Calendar", - "read_only": False, - "is_owned_by_user": True, - "object": "calendar", - } - ], - } + "data": [], + }, {"X-Test-Header": "test"}) yield mock_http_client @pytest.fixture def http_client_response(): with patch( - "nylas.models.response.Response.from_dict", return_value=Response({}, "bar") + "nylas.models.response.Response.from_dict", return_value=Response({}, "bar", {"X-Test-Header": "test"}) ): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "abc-123", "data": { "id": "calendar-123", @@ -89,23 +79,23 @@ def http_client_response(): "is_owned_by_user": True, "object": "calendar", }, - } + }, {"X-Test-Header": "test"}) yield mock_http_client @pytest.fixture def http_client_delete_response(): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "abc-123", - } + }, {"X-Test-Header": "test"}) return mock_http_client @pytest.fixture def http_client_token_exchange(): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "access_token": "nylas_access_token", "expires_in": 3600, "id_token": "jwt_token", @@ -114,7 +104,7 @@ def http_client_token_exchange(): "token_type": "Bearer", "grant_id": "grant_123", "provider": "google", - } + }, {"X-Test-Header": "test"}) return mock_http_client @@ -172,7 +162,7 @@ def http_client_free_busy(): @pytest.fixture def http_client_list_scheduled_messages(): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "dd3ec9a2-8f15-403d-b269-32b1f1beb9f5", "data": [ { @@ -188,14 +178,14 @@ def http_client_list_scheduled_messages(): "close_time": 1690579819, }, ], - } + }, {"X-Test-Header": "test"}) return mock_http_client @pytest.fixture def http_client_clean_messages(): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "dd3ec9a2-8f15-403d-b269-32b1f1beb9f5", "data": [ { @@ -217,5 +207,5 @@ def http_client_clean_messages(): "conversation": "another example", }, ], - } + }, {"X-Test-Header": "test"}) return mock_http_client diff --git a/tests/handler/test_api_resources.py b/tests/handler/test_api_resources.py index e40aadf6..80ae1d5f 100644 --- a/tests/handler/test_api_resources.py +++ b/tests/handler/test_api_resources.py @@ -160,3 +160,108 @@ def test_destroy_resource_default_type(self, http_client_delete_response): {"foo": "bar"}, overrides=None, ) + + def test_list_resource_with_headers(self, http_client_list_response): + resource = MockResource(http_client_list_response) + + response = resource.list( + path="/foo", + response_type=Calendar, + headers={"test": "header"}, + query_params={"query": "param"}, + request_body={"foo": "bar"}, + ) + + assert response.headers == {"X-Test-Header": "test"} + http_client_list_response._execute.assert_called_once_with( + "GET", + "/foo", + {"test": "header"}, + {"query": "param"}, + {"foo": "bar"}, + overrides=None, + ) + + def test_find_resource_with_headers(self, http_client_response): + resource = MockResource(http_client_response) + + response = resource.find( + path="/foo", + response_type=Calendar, + headers={"test": "header"}, + query_params={"query": "param"}, + request_body={"foo": "bar"}, + ) + + assert response.headers == {"X-Test-Header": "test"} + http_client_response._execute.assert_called_once_with( + "GET", + "/foo", + {"test": "header"}, + {"query": "param"}, + {"foo": "bar"}, + overrides=None, + ) + + def test_create_resource_with_headers(self, http_client_response): + resource = MockResource(http_client_response) + + response = resource.create( + path="/foo", + response_type=Calendar, + headers={"test": "header"}, + query_params={"query": "param"}, + request_body={"foo": "bar"}, + ) + + assert response.headers == {"X-Test-Header": "test"} + http_client_response._execute.assert_called_once_with( + "POST", + "/foo", + {"test": "header"}, + {"query": "param"}, + {"foo": "bar"}, + overrides=None, + ) + + def test_update_resource_with_headers(self, http_client_response): + resource = MockResource(http_client_response) + + response = resource.update( + path="/foo", + response_type=Calendar, + headers={"test": "header"}, + query_params={"query": "param"}, + request_body={"foo": "bar"}, + ) + + assert response.headers == {"X-Test-Header": "test"} + http_client_response._execute.assert_called_once_with( + "PUT", + "/foo", + {"test": "header"}, + {"query": "param"}, + {"foo": "bar"}, + overrides=None, + ) + + def test_destroy_resource_with_headers(self, http_client_delete_response): + resource = MockResource(http_client_delete_response) + + response = resource.destroy( + path="/foo", + response_type=RequestIdOnlyResponse, + headers={"test": "header"}, + query_params={"query": "param"}, + request_body={"foo": "bar"}, + ) + + assert response.headers == {"X-Test-Header": "test"} + http_client_delete_response._execute.assert_called_once_with( + "DELETE", + "/foo", + {"test": "header"}, + {"query": "param"}, + {"foo": "bar"}, + overrides=None, + ) diff --git a/tests/handler/test_http_client.py b/tests/handler/test_http_client.py index e6a61576..9fb0684a 100644 --- a/tests/handler/test_http_client.py +++ b/tests/handler/test_http_client.py @@ -211,9 +211,11 @@ def test_validate_response(self): response.status_code = 200 response.json.return_value = {"foo": "bar"} response.url = "https://test.nylas.com/foo" + response.headers = {"X-Test-Header": "test"} - validation = _validate_response(response) - assert validation == {"foo": "bar"} + response_json, response_headers = _validate_response(response) + assert response_json == {"foo": "bar"} + assert response_headers == {"X-Test-Header": "test"} def test_validate_response_400_error(self): response = Mock() @@ -274,7 +276,13 @@ def test_validate_response_400_keyerror(self): assert e.value.status_code == 400 def test_execute(self, http_client, patched_version_and_sys, patched_request): - response = http_client._execute( + mock_response = Mock() + mock_response.json.return_value = {"foo": "bar"} + mock_response.headers = {"X-Test-Header": "test"} + mock_response.status_code = 200 + patched_request.return_value = mock_response + + response_json, response_headers = http_client._execute( method="GET", path="/foo", headers={"test": "header"}, @@ -282,7 +290,8 @@ def test_execute(self, http_client, patched_version_and_sys, patched_request): request_body={"foo": "bar"}, ) - assert response == {"foo": "bar"} + assert response_json == {"foo": "bar"} + assert response_headers == {"X-Test-Header": "test"} patched_request.assert_called_once_with( "GET", "https://test.nylas.com/foo?query=param", @@ -301,7 +310,13 @@ def test_execute(self, http_client, patched_version_and_sys, patched_request): def test_execute_override_timeout( self, http_client, patched_version_and_sys, patched_request ): - response = http_client._execute( + mock_response = Mock() + mock_response.json.return_value = {"foo": "bar"} + mock_response.headers = {"X-Test-Header": "test"} + mock_response.status_code = 200 + patched_request.return_value = mock_response + + response_json, response_headers = http_client._execute( method="GET", path="/foo", headers={"test": "header"}, @@ -310,7 +325,8 @@ def test_execute_override_timeout( overrides={"timeout": 60}, ) - assert response == {"foo": "bar"} + assert response_json == {"foo": "bar"} + assert response_headers == {"X-Test-Header": "test"} patched_request.assert_called_once_with( "GET", "https://test.nylas.com/foo?query=param", @@ -339,3 +355,80 @@ def test_execute_timeout(self, http_client, mock_session_timeout): str(e.value) == "Nylas SDK timed out before receiving a response from the server." ) + + def test_validate_response_with_headers(self): + response = Mock() + response.status_code = 200 + response.json.return_value = {"foo": "bar"} + response.url = "https://test.nylas.com/foo" + response.headers = {"X-Test-Header": "test"} + + json_response, headers = _validate_response(response) + assert json_response == {"foo": "bar"} + assert headers == {"X-Test-Header": "test"} + + def test_validate_response_400_error_with_headers(self): + response = Mock() + response.status_code = 400 + response.json.return_value = { + "request_id": "123", + "error": { + "type": "api_error", + "message": "The request is invalid.", + "provider_error": {"foo": "bar"}, + }, + } + response.url = "https://test.nylas.com/foo" + response.headers = {"X-Test-Header": "test"} + + with pytest.raises(NylasApiError) as e: + _validate_response(response) + assert e.value.headers == {"X-Test-Header": "test"} + + def test_validate_response_auth_error_with_headers(self): + response = Mock() + response.status_code = 401 + response.json.return_value = { + "error": "invalid_request", + "error_description": "The request is invalid.", + "error_uri": "https://docs.nylas.com/reference#authentication-errors", + "error_code": 100241, + } + response.url = "https://test.nylas.com/connect/token" + response.headers = {"X-Test-Header": "test"} + + with pytest.raises(NylasOAuthError) as e: + _validate_response(response) + assert e.value.headers == {"X-Test-Header": "test"} + + def test_execute_with_headers(self, http_client, patched_version_and_sys, patched_request): + mock_response = Mock() + mock_response.json.return_value = {"foo": "bar"} + mock_response.headers = {"X-Test-Header": "test"} + mock_response.status_code = 200 + patched_request.return_value = mock_response + + response_json, response_headers = http_client._execute( + method="GET", + path="/foo", + headers={"test": "header"}, + query_params={"query": "param"}, + request_body={"foo": "bar"}, + ) + + assert response_json == {"foo": "bar"} + assert response_headers == {"X-Test-Header": "test"} + patched_request.assert_called_once_with( + "GET", + "https://test.nylas.com/foo?query=param", + headers={ + "X-Nylas-API-Wrapper": "python", + "User-Agent": "Nylas Python SDK 2.0.0 - 1.2.3", + "Authorization": "Bearer test-key", + "Content-type": "application/json", + "test": "header", + }, + json={"foo": "bar"}, + timeout=30, + data=None, + ) From 19003e0e277f5ee1c4d0e30344537e4ec2a2368d Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 27 Jan 2025 10:34:33 -0500 Subject: [PATCH 2/6] Updated the changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b892bd..b84572d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ nylas-python Changelog ====================== +Unreleased +---------------- +* Added response headers to all responses from the Nylas API + v6.5.0 ---------------- * Added support for Scheduler APIs From 9a2745121e59ecb689785960992a669756055611 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 27 Jan 2025 10:53:55 -0500 Subject: [PATCH 3/6] Fix tests --- nylas/handler/api_resources.py | 4 ++++ nylas/resources/applications.py | 4 ++-- nylas/resources/auth.py | 14 +++++++------- nylas/resources/calendars.py | 8 ++++---- nylas/resources/configurations.py | 1 - nylas/resources/contacts.py | 4 ++-- nylas/resources/drafts.py | 4 ++-- nylas/resources/events.py | 9 +++++---- nylas/resources/messages.py | 22 +++++++++++----------- nylas/resources/smart_compose.py | 8 ++++---- nylas/resources/webhooks.py | 8 ++++---- tests/conftest.py | 8 ++++---- tests/resources/test_applications.py | 4 ++-- tests/resources/test_auth.py | 8 ++++---- 14 files changed, 55 insertions(+), 51 deletions(-) diff --git a/nylas/handler/api_resources.py b/nylas/handler/api_resources.py index 546bc8b5..474e47dc 100644 --- a/nylas/handler/api_resources.py +++ b/nylas/handler/api_resources.py @@ -109,4 +109,8 @@ def destroy( response_json, response_headers = self._http_client._execute( "DELETE", path, headers, query_params, request_body, overrides=overrides ) + + # Check if the response type is a dataclass_json class + if hasattr(response_type, "from_dict") and not hasattr(response_type, "headers"): + return response_type.from_dict(response_json) return response_type.from_dict(response_json, headers=response_headers) diff --git a/nylas/resources/applications.py b/nylas/resources/applications.py index 9510cf32..ed20c5ae 100644 --- a/nylas/resources/applications.py +++ b/nylas/resources/applications.py @@ -34,7 +34,7 @@ def info(self, overrides: RequestOverrides = None) -> Response[ApplicationDetail Response: The application information. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="GET", path="/v3/applications", overrides=overrides ) - return Response.from_dict(json_response, ApplicationDetails) + return Response.from_dict(json_response, ApplicationDetails, headers) diff --git a/nylas/resources/auth.py b/nylas/resources/auth.py index cccc35f1..b6691573 100644 --- a/nylas/resources/auth.py +++ b/nylas/resources/auth.py @@ -114,13 +114,13 @@ def custom_authentication( The created Grant. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path="/v3/connect/custom", request_body=request_body, overrides=overrides, ) - return Response.from_dict(json_response, Grant) + return Response.from_dict(json_response, Grant, headers) def refresh_access_token( self, request: TokenExchangeRequest, overrides: RequestOverrides = None @@ -248,13 +248,13 @@ def detect_provider( The detected provider, if found. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path="/v3/providers/detect", query_params=params, overrides=overrides, ) - return Response.from_dict(json_response, ProviderDetectResponse) + return Response.from_dict(json_response, ProviderDetectResponse, headers) def _url_auth_builder(self, query: dict) -> str: base = f"{self._http_client.api_server}/v3/connect/auth" @@ -263,7 +263,7 @@ def _url_auth_builder(self, query: dict) -> str: def _get_token( self, request_body: dict, overrides: RequestOverrides ) -> CodeExchangeResponse: - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path="/v3/connect/token", request_body=request_body, @@ -274,10 +274,10 @@ def _get_token( def _get_token_info( self, query_params: dict, overrides: RequestOverrides ) -> Response[TokenInfoResponse]: - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="GET", path="/v3/connect/tokeninfo", query_params=query_params, overrides=overrides, ) - return Response.from_dict(json_response, TokenInfoResponse) + return Response.from_dict(json_response, TokenInfoResponse, headers) diff --git a/nylas/resources/calendars.py b/nylas/resources/calendars.py index 145da03a..14db916c 100644 --- a/nylas/resources/calendars.py +++ b/nylas/resources/calendars.py @@ -169,14 +169,14 @@ def get_availability( Returns: Response: The availability response from the API. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path="/v3/calendars/availability", request_body=request_body, overrides=overrides, ) - return Response.from_dict(json_response, GetAvailabilityResponse) + return Response.from_dict(json_response, GetAvailabilityResponse, headers) def get_free_busy( self, @@ -195,7 +195,7 @@ def get_free_busy( Returns: Response: The free/busy response from the API. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path=f"/v3/grants/{identifier}/calendars/free-busy", request_body=request_body, @@ -210,4 +210,4 @@ def get_free_busy( else: data.append(FreeBusy.from_dict(item)) - return Response(data, request_id) + return Response(data, request_id, headers) diff --git a/nylas/resources/configurations.py b/nylas/resources/configurations.py index 85c47ec6..103b0c54 100644 --- a/nylas/resources/configurations.py +++ b/nylas/resources/configurations.py @@ -70,7 +70,6 @@ def list( response_type=Configuration, query_params=query_params, ) - print("What's this", res) return res def find( diff --git a/nylas/resources/contacts.py b/nylas/resources/contacts.py index 66c45557..de78a9fd 100644 --- a/nylas/resources/contacts.py +++ b/nylas/resources/contacts.py @@ -172,11 +172,11 @@ def list_groups( Returns: The list of contact groups. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="GET", path=f"/v3/grants/{identifier}/contacts/groups", query_params=query_params, overrides=overrides, ) - return ListResponse.from_dict(json_response, ContactGroup) + return ListResponse.from_dict(json_response, ContactGroup, headers) diff --git a/nylas/resources/drafts.py b/nylas/resources/drafts.py index 23146d88..222ccdc4 100644 --- a/nylas/resources/drafts.py +++ b/nylas/resources/drafts.py @@ -215,10 +215,10 @@ def send( draft_id: The identifier of the draft to send. overrides: The request overrides to use for the request. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path=f"/v3/grants/{identifier}/drafts/{urllib.parse.quote(draft_id, safe='')}", overrides=overrides, ) - return Response.from_dict(json_response, Message) + return Response.from_dict(json_response, Message, headers) diff --git a/nylas/resources/events.py b/nylas/resources/events.py index 416e8529..a9563006 100644 --- a/nylas/resources/events.py +++ b/nylas/resources/events.py @@ -183,10 +183,11 @@ def send_rsvp( query_params: SendRsvpQueryParams, overrides: RequestOverrides = None, ) -> RequestIdOnlyResponse: - """Send RSVP for an event. + """ + Send an RSVP for an event. Args: - identifier: The grant ID or email account to send RSVP for. + identifier: The grant ID to send RSVP for. event_id: The event ID to send RSVP for. query_params: The query parameters to send to the API. request_body: The request body to send to the API. @@ -195,7 +196,7 @@ def send_rsvp( Returns: Response: The RSVP response from the API. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path=f"/v3/grants/{identifier}/events/{event_id}/send-rsvp", query_params=query_params, @@ -203,4 +204,4 @@ def send_rsvp( overrides=overrides, ) - return RequestIdOnlyResponse.from_dict(json_response) + return RequestIdOnlyResponse.from_dict(json_response, headers) diff --git a/nylas/resources/messages.py b/nylas/resources/messages.py index 310f3682..a61f037f 100644 --- a/nylas/resources/messages.py +++ b/nylas/resources/messages.py @@ -189,7 +189,7 @@ def send( json_body = request_body - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="POST", path=path, request_body=json_body, @@ -197,7 +197,7 @@ def send( overrides=overrides, ) - return Response.from_dict(json_response, Message) + return Response.from_dict(json_response, Message, headers) def list_scheduled_messages( self, identifier: str, overrides: RequestOverrides = None @@ -212,7 +212,7 @@ def list_scheduled_messages( Returns: Response: The list of scheduled messages. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="GET", path=f"/v3/grants/{identifier}/messages/schedules", overrides=overrides, @@ -223,7 +223,7 @@ def list_scheduled_messages( for item in json_response["data"]: data.append(ScheduledMessage.from_dict(item)) - return Response(data, request_id) + return Response(data, request_id, headers) def find_scheduled_message( self, identifier: str, schedule_id: str, overrides: RequestOverrides = None @@ -239,13 +239,13 @@ def find_scheduled_message( Returns: Response: The scheduled message. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="GET", path=f"/v3/grants/{identifier}/messages/schedules/{schedule_id}", overrides=overrides, ) - return Response.from_dict(json_response, ScheduledMessage) + return Response.from_dict(json_response, ScheduledMessage, headers) def stop_scheduled_message( self, identifier: str, schedule_id: str, overrides: RequestOverrides = None @@ -261,13 +261,13 @@ def stop_scheduled_message( Returns: Response: The confirmation of the stopped scheduled message. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="DELETE", path=f"/v3/grants/{identifier}/messages/schedules/{schedule_id}", overrides=overrides, ) - return Response.from_dict(json_response, StopScheduledMessageResponse) + return Response.from_dict(json_response, StopScheduledMessageResponse, headers) def clean_messages( self, @@ -284,13 +284,13 @@ def clean_messages( overrides: The request overrides to apply to the request. Returns: - The list of cleaned messages. + ListResponse: The list of cleaned messages. """ - json_response = self._http_client._execute( + json_response, headers = self._http_client._execute( method="PUT", path=f"/v3/grants/{identifier}/messages/clean", request_body=request_body, overrides=overrides, ) - return ListResponse.from_dict(json_response, CleanMessagesResponse) + return ListResponse.from_dict(json_response, CleanMessagesResponse, headers) diff --git a/nylas/resources/smart_compose.py b/nylas/resources/smart_compose.py index 9f54135b..1d266d89 100644 --- a/nylas/resources/smart_compose.py +++ b/nylas/resources/smart_compose.py @@ -29,14 +29,14 @@ def compose_message( Returns: The generated message. """ - res = self._http_client._execute( + res, headers = self._http_client._execute( method="POST", path=f"/v3/grants/{identifier}/messages/smart-compose", request_body=request_body, overrides=overrides, ) - return Response.from_dict(res, ComposeMessageResponse) + return Response.from_dict(res, ComposeMessageResponse, headers) def compose_message_reply( self, @@ -57,11 +57,11 @@ def compose_message_reply( Returns: The generated message reply. """ - res = self._http_client._execute( + res, headers = self._http_client._execute( method="POST", path=f"/v3/grants/{identifier}/messages/{message_id}/smart-compose", request_body=request_body, overrides=overrides, ) - return Response.from_dict(res, ComposeMessageResponse) + return Response.from_dict(res, ComposeMessageResponse, headers) diff --git a/nylas/resources/webhooks.py b/nylas/resources/webhooks.py index b649e5ba..443c159d 100644 --- a/nylas/resources/webhooks.py +++ b/nylas/resources/webhooks.py @@ -139,13 +139,13 @@ def rotate_secret( Returns: The updated webhook destination """ - res = self._http_client._execute( + res, headers = self._http_client._execute( method="PUT", path=f"/v3/webhooks/{webhook_id}/rotate-secret", request_body={}, overrides=overrides, ) - return Response.from_dict(res, WebhookWithSecret) + return Response.from_dict(res, WebhookWithSecret, headers) def ip_addresses( self, overrides: RequestOverrides = None @@ -159,10 +159,10 @@ def ip_addresses( Returns: The list of IP addresses that Nylas sends webhooks from """ - res = self._http_client._execute( + res, headers = self._http_client._execute( method="GET", path="/v3/webhooks/ip-addresses", overrides=overrides ) - return Response.from_dict(res, WebhookIpAddressesResponse) + return Response.from_dict(res, WebhookIpAddressesResponse, headers) def extract_challenge_parameter(url: str) -> str: diff --git a/tests/conftest.py b/tests/conftest.py index d588618e..35bca720 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -111,7 +111,7 @@ def http_client_token_exchange(): @pytest.fixture def http_client_token_info(): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "abc-123", "data": { "iss": "https://nylas.com", @@ -121,14 +121,14 @@ def http_client_token_info(): "iat": 1692094848, "exp": 1692095173, }, - } + }, {"X-Test-Header": "test"}) return mock_http_client @pytest.fixture def http_client_free_busy(): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "dd3ec9a2-8f15-403d-b269-32b1f1beb9f5", "data": [ { @@ -155,7 +155,7 @@ def http_client_free_busy(): "object": "error", }, ], - } + }, {"X-Test-Header": "test"}) return mock_http_client diff --git a/tests/resources/test_applications.py b/tests/resources/test_applications.py index 43c65971..9de17fb4 100644 --- a/tests/resources/test_applications.py +++ b/tests/resources/test_applications.py @@ -14,7 +14,7 @@ def test_redirect_uris_property(self, http_client): def test_info(self): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "req-123", "data": { "application_id": "ad410018-d306-43f9-8361-fa5d7b2172e0", @@ -51,7 +51,7 @@ def test_info(self): } ], }, - } + }, {"X-Test-Header": "test"}) app = Applications(mock_http_client) res = app.info() diff --git a/tests/resources/test_auth.py b/tests/resources/test_auth.py index 8d3ecd38..db88ca56 100644 --- a/tests/resources/test_auth.py +++ b/tests/resources/test_auth.py @@ -187,7 +187,7 @@ def test_exchange_code_for_token_no_secret(self, http_client_token_exchange): def test_custom_authentication(self): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "abc-123", "data": { "id": "e19f8e1a-eb1c-41c0-b6a6-d2e59daf7f47", @@ -201,7 +201,7 @@ def test_custom_authentication(self): "created_at": 1617817109, "updated_at": 1617817109, }, - } + }, {"X-Test-Header": "test"}) auth = Auth(mock_http_client) res = auth.custom_authentication( @@ -358,7 +358,7 @@ def test_revoke(self, http_client_response): def test_detect_provider(self): mock_http_client = Mock() - mock_http_client._execute.return_value = { + mock_http_client._execute.return_value = ({ "request_id": "abc-123", "data": { "email_address": "test@gmail.com", @@ -366,7 +366,7 @@ def test_detect_provider(self): "provider": "google", "type": "string", }, - } + }, {"X-Test-Header": "test"}) auth = Auth(mock_http_client) req = {"email": "test@gmail.com", "all_provider_types": True} From 1e398a82d2c4e999aaba32445ec9a6796a23dd53 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Mon, 27 Jan 2025 10:58:09 -0500 Subject: [PATCH 4/6] Fix: lint errors --- nylas/handler/api_resources.py | 2 +- nylas/handler/http_client.py | 1 - nylas/models/errors.py | 2 +- nylas/models/response.py | 10 ++++++++-- nylas/resources/auth.py | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/nylas/handler/api_resources.py b/nylas/handler/api_resources.py index 474e47dc..ce2d3efa 100644 --- a/nylas/handler/api_resources.py +++ b/nylas/handler/api_resources.py @@ -109,7 +109,7 @@ def destroy( response_json, response_headers = self._http_client._execute( "DELETE", path, headers, query_params, request_body, overrides=overrides ) - + # Check if the response type is a dataclass_json class if hasattr(response_type, "from_dict") and not hasattr(response_type, "headers"): return response_type.from_dict(response_json) diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index 9769cb3c..76bcd7f1 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -44,7 +44,6 @@ def _validate_response(response: Response) -> Tuple[Dict, CaseInsensitiveDict]: status_code=response.status_code, headers=response.headers, ) from exc - return (json, response.headers) def _build_query_params(base_url: str, query_params: dict = None) -> str: diff --git a/nylas/models/errors.py b/nylas/models/errors.py index dc71ea92..43e02d4b 100644 --- a/nylas/models/errors.py +++ b/nylas/models/errors.py @@ -168,4 +168,4 @@ def __init__(self, url: str, timeout: int, headers: Optional[CaseInsensitiveDict ) self.url: str = url self.timeout: int = timeout - self.headers: CaseInsensitiveDict = headers \ No newline at end of file + self.headers: CaseInsensitiveDict = headers diff --git a/nylas/models/response.py b/nylas/models/response.py index 62ae765c..bcb0fba1 100644 --- a/nylas/models/response.py +++ b/nylas/models/response.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import TypeVar, Generic, Optional, List -from dataclasses_json import DataClassJsonMixin, dataclass_json +from dataclasses_json import DataClassJsonMixin from requests.structures import CaseInsensitiveDict @@ -73,7 +73,13 @@ class ListResponse(tuple, Generic[T]): next_cursor: Optional[str] = None headers: Optional[CaseInsensitiveDict] = None - def __new__(cls, data: List[T], request_id: str, next_cursor: Optional[str] = None, headers: Optional[CaseInsensitiveDict] = None): + def __new__( + cls, + data: List[T], + request_id: str, + next_cursor: Optional[str] = None, + headers: Optional[CaseInsensitiveDict] = None + ): """ Initialize the response object. diff --git a/nylas/resources/auth.py b/nylas/resources/auth.py index b6691573..18dfc658 100644 --- a/nylas/resources/auth.py +++ b/nylas/resources/auth.py @@ -263,7 +263,7 @@ def _url_auth_builder(self, query: dict) -> str: def _get_token( self, request_body: dict, overrides: RequestOverrides ) -> CodeExchangeResponse: - json_response, headers = self._http_client._execute( + json_response, _ = self._http_client._execute( method="POST", path="/v3/connect/token", request_body=request_body, From ebecf9f329faeb47cdd38b80215093b553b789be Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Tue, 28 Jan 2025 11:38:35 -0500 Subject: [PATCH 5/6] Brought back previous doc comment --- nylas/resources/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nylas/resources/events.py b/nylas/resources/events.py index a9563006..95f05bdd 100644 --- a/nylas/resources/events.py +++ b/nylas/resources/events.py @@ -187,7 +187,7 @@ def send_rsvp( Send an RSVP for an event. Args: - identifier: The grant ID to send RSVP for. + identifier: The grant ID or email account to send RSVP for. event_id: The event ID to send RSVP for. query_params: The query parameters to send to the API. request_body: The request body to send to the API. From 78540f8650ac9dd177e8c05e66eb4e3e418d12b9 Mon Sep 17 00:00:00 2001 From: Aaron de Mello Date: Tue, 28 Jan 2025 11:46:18 -0500 Subject: [PATCH 6/6] Updated the example --- .../response_headers_example.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/examples/response_headers_demo/response_headers_example.py b/examples/response_headers_demo/response_headers_example.py index a33a0c12..d93d8b04 100644 --- a/examples/response_headers_demo/response_headers_example.py +++ b/examples/response_headers_demo/response_headers_example.py @@ -66,6 +66,27 @@ def demonstrate_list_response_headers(client: Client, grant_id: str) -> None: print_response_headers(e.headers, "Error") +def demonstrate_list_response_headers_with_pagination(client: Client, grant_id: str) -> None: + """Demonstrate headers in list responses with pagination.""" + print("\nDemonstrating List Response Headers with Pagination") + print("--------------------------------------------------") + + try: + # List messages to get a ListResponse + threads = client.threads.list(identifier=grant_id, query_params={"limit": 1}) + + print("✓ Successfully retrieved threads") + print_response_headers(threads.headers) + print(f"Total threads count: {len(threads.data)}") + + except NylasApiError as e: + print("\nError occurred while listing threads:") + print(f"✗ Error Type: {e.type}") + print(f"✗ Provider Error: {e.provider_error}") + print(f"✗ Request ID: {e.request_id}") + print_response_headers(e.headers, "Error") + + def demonstrate_find_response_headers(client: Client, grant_id: str) -> None: """Demonstrate headers in find/single-item responses.""" print("\nDemonstrating Find Response Headers") @@ -129,6 +150,7 @@ def main(): # Demonstrate different types of responses and their headers demonstrate_list_response_headers(client, grant_id) + demonstrate_list_response_headers_with_pagination(client, grant_id) demonstrate_find_response_headers(client, grant_id) demonstrate_error_response_headers(client, grant_id)