From 9ff6150e0cc0f6db940368e849f2abc0de836ec9 Mon Sep 17 00:00:00 2001 From: samLRodrigues Date: Mon, 13 Apr 2026 16:35:08 -0300 Subject: [PATCH 1/2] CUST-5289 [v3] Python SDK ListGrantsQueryParams uses camelCase keys that are silently ignored by the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit License I confirm that this contribution is made under the terms of the MIT license and that I have the authority necessary to make this contribution on behalf of its copyright owner. Fix ListGrantsQueryParams key handling in Python SDK - Fix bug where `sortBy`, `orderBy`, and `grantStatus` are sent as-is and ignored by the API. - Normalize grants query aliases to `sort_by`, `order_by`, and `grant_status` at request serialization time. - Align type hints with snake_case API contract while keeping camelCase aliases for backwards compatibility. - Preserve existing SDK behavior for callers already using snake_case keys. ✅ Tests checks: - `pytest tests/handler/test_http_client.py -k test_build_query_params -q` (100%) - Manual serialization check for grants aliases via `_build_query_params` (verified) Made-with: Cursor --- nylas/handler/http_client.py | 13 ++++++++++--- nylas/models/grants.py | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index 9027736..5d9668e 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -49,15 +49,22 @@ def _validate_response(response: Response) -> Tuple[Dict, CaseInsensitiveDict]: def _build_query_params(base_url: str, query_params: dict = None) -> str: query_param_parts = [] + query_param_key_aliases = { + "sortBy": "sort_by", + "orderBy": "order_by", + "grantStatus": "grant_status", + } + for key, value in query_params.items(): + normalized_key = query_param_key_aliases.get(key, key) if isinstance(value, list): for item in value: - query_param_parts.append(f"{key}={quote(str(item))}") + query_param_parts.append(f"{normalized_key}={quote(str(item))}") elif isinstance(value, dict): for k, v in value.items(): - query_param_parts.append(f"{key}={k}:{quote(str(v))}") + query_param_parts.append(f"{normalized_key}={k}:{quote(str(v))}") else: - query_param_parts.append(f"{key}={quote(str(value))}") + query_param_parts.append(f"{normalized_key}={quote(str(value))}") query_string = "&".join(query_param_parts) return f"{base_url}?{query_string}" diff --git a/nylas/models/grants.py b/nylas/models/grants.py index 4cae4c1..7d793a2 100644 --- a/nylas/models/grants.py +++ b/nylas/models/grants.py @@ -83,23 +83,30 @@ class ListGrantsQueryParams(TypedDict): limit: The maximum number of objects to return. This field defaults to 10. The maximum allowed value is 200. offset: Offset grant results by this number. - sortBy: Sort entries by field name - orderBy: Specify ascending or descending order. + sort_by: Sort entries by field name. + order_by: Specify ascending or descending order. since: Scope grants from a specific point in time by Unix timestamp. before: Scope grants to a specific point in time by Unix timestamp. email: Filtering your query based on grant email address (if applicable) - grantStatus: Filtering your query based on grant email status (if applicable) + grant_status: Filtering your query based on grant email status (if applicable) ip: Filtering your query based on grant IP address provider: Filtering your query based on OAuth provider + sortBy: Deprecated camelCase alias for sort_by. + orderBy: Deprecated camelCase alias for order_by. + grantStatus: Deprecated camelCase alias for grant_status. """ limit: NotRequired[int] offset: NotRequired[int] - sortBy: NotRequired[str] - orderBy: NotRequired[str] + sort_by: NotRequired[str] + order_by: NotRequired[str] since: NotRequired[int] before: NotRequired[int] email: NotRequired[str] - grantStatus: NotRequired[str] + grant_status: NotRequired[str] ip: NotRequired[str] provider: NotRequired[Provider] + # Backward-compatible aliases for callers still passing camelCase keys. + sortBy: NotRequired[str] + orderBy: NotRequired[str] + grantStatus: NotRequired[str] From 1363139a17a2eade94afdeebf5268f6fd9474c88 Mon Sep 17 00:00:00 2001 From: samLRodrigues Date: Mon, 13 Apr 2026 17:05:08 -0300 Subject: [PATCH 2/2] CUST-5289 Scope grants query alias normalization to grants resource Move camelCase-to-snake_case key normalization out of the shared HTTP query builder and into the grants resource so only grants list parameters are transformed. Add grants-specific tests to verify camelCase compatibility and snake_case precedence. Made-with: Cursor --- nylas/handler/http_client.py | 13 ++------ nylas/resources/grants.py | 24 ++++++++++++++- tests/resources/test_grants.py | 55 ++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py index 5d9668e..9027736 100644 --- a/nylas/handler/http_client.py +++ b/nylas/handler/http_client.py @@ -49,22 +49,15 @@ def _validate_response(response: Response) -> Tuple[Dict, CaseInsensitiveDict]: def _build_query_params(base_url: str, query_params: dict = None) -> str: query_param_parts = [] - query_param_key_aliases = { - "sortBy": "sort_by", - "orderBy": "order_by", - "grantStatus": "grant_status", - } - for key, value in query_params.items(): - normalized_key = query_param_key_aliases.get(key, key) if isinstance(value, list): for item in value: - query_param_parts.append(f"{normalized_key}={quote(str(item))}") + query_param_parts.append(f"{key}={quote(str(item))}") elif isinstance(value, dict): for k, v in value.items(): - query_param_parts.append(f"{normalized_key}={k}:{quote(str(v))}") + query_param_parts.append(f"{key}={k}:{quote(str(v))}") else: - query_param_parts.append(f"{normalized_key}={quote(str(value))}") + query_param_parts.append(f"{key}={quote(str(value))}") query_string = "&".join(query_param_parts) return f"{base_url}?{query_string}" diff --git a/nylas/resources/grants.py b/nylas/resources/grants.py index 06a951b..4a7675b 100644 --- a/nylas/resources/grants.py +++ b/nylas/resources/grants.py @@ -13,6 +13,28 @@ from nylas.models.response import Response, ListResponse, DeleteResponse +def _normalize_grants_query_params(query_params: ListGrantsQueryParams = None) -> dict: + if not query_params: + return query_params + + normalized_query_params = dict(query_params) + key_aliases = { + "sortBy": "sort_by", + "orderBy": "order_by", + "grantStatus": "grant_status", + } + + for camel_case_key, snake_case_key in key_aliases.items(): + if camel_case_key in normalized_query_params: + if snake_case_key not in normalized_query_params: + normalized_query_params[snake_case_key] = normalized_query_params[ + camel_case_key + ] + del normalized_query_params[camel_case_key] + + return normalized_query_params + + class Grants( ListableApiResource, FindableApiResource, @@ -47,7 +69,7 @@ def list( return super().list( path="/v3/grants", response_type=Grant, - query_params=query_params, + query_params=_normalize_grants_query_params(query_params), overrides=overrides, ) diff --git a/tests/resources/test_grants.py b/tests/resources/test_grants.py index bcb6e88..c96a675 100644 --- a/tests/resources/test_grants.py +++ b/tests/resources/test_grants.py @@ -68,6 +68,61 @@ def test_list_grants(self, http_client_list_response): "GET", "/v3/grants", None, None, None, overrides=None ) + def test_list_grants_normalizes_camel_case_query_params( + self, http_client_list_response + ): + grants = Grants(http_client_list_response) + + grants.list( + query_params={ + "sortBy": "created_at", + "orderBy": "asc", + "grantStatus": "valid", + "limit": 10, + } + ) + + http_client_list_response._execute.assert_called_once_with( + "GET", + "/v3/grants", + None, + { + "sort_by": "created_at", + "order_by": "asc", + "grant_status": "valid", + "limit": 10, + }, + None, + overrides=None, + ) + + def test_list_grants_prefers_snake_case_query_params(self, http_client_list_response): + grants = Grants(http_client_list_response) + + grants.list( + query_params={ + "sortBy": "updated_at", + "sort_by": "created_at", + "orderBy": "desc", + "order_by": "asc", + "grantStatus": "invalid", + "grant_status": "valid", + } + ) + + http_client_list_response._execute.assert_called_once_with( + "GET", + "/v3/grants", + None, + { + "sort_by": "created_at", + "order_by": "asc", + "grant_status": "valid", + }, + None, + overrides=None, + ) + def test_find_grant(self, http_client_response): grants = Grants(http_client_response)