From f4dec148bdf79dae40fabab2c453e77195bae047 Mon Sep 17 00:00:00 2001 From: kcbiradar Date: Thu, 15 Jan 2026 15:23:26 +0530 Subject: [PATCH 1/6] feat: add raw_request method --- README.md | 132 ++++++++++ openfga_sdk/api_client.py | 2 +- openfga_sdk/client/client.py | 167 ++++++++++++ openfga_sdk/client/models/__init__.py | 2 + openfga_sdk/client/models/raw_response.py | 90 +++++++ openfga_sdk/sync/api_client.py | 2 +- openfga_sdk/sync/client/client.py | 169 ++++++++++++ openfga_sdk/telemetry/attributes.py | 2 +- test/client/client_test.py | 308 ++++++++++++++++++++++ test/sync/client/client_test.py | 306 +++++++++++++++++++++ 10 files changed, 1177 insertions(+), 3 deletions(-) create mode 100644 openfga_sdk/client/models/raw_response.py diff --git a/README.md b/README.md index c424a14..03d9531 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ This is an autogenerated python SDK for OpenFGA. It provides a wrapper around th - [Read Assertions](#read-assertions) - [Write Assertions](#write-assertions) - [Retries](#retries) + - [Calling Other Endpoints](#calling-other-endpoints) - [API Endpoints](#api-endpoints) - [Models](#models) - [OpenTelemetry](#opentelemetry) @@ -1260,6 +1261,137 @@ body = [ClientAssertion( response = await fga_client.write_assertions(body, options) ``` +### Calling Other Endpoints + +In certain cases you may want to call other APIs not yet wrapped by the SDK. You can do so by using the `raw_request` method available on the `OpenFgaClient`. The `raw_request` method allows you to make raw HTTP calls to any OpenFGA endpoint by specifying the operation name, HTTP method, path, parameters, body, and headers, while still honoring the client configuration (authentication, telemetry, retries, and error handling). + +This is useful when: +- You want to call a new endpoint that is not yet supported by the SDK +- You are using an earlier version of the SDK that doesn't yet support a particular endpoint +- You have a custom endpoint deployed that extends the OpenFGA API + +In all cases, you initialize the SDK the same way as usual, and then call `raw_request` on the `fga_client` instance. + +```python +from openfga_sdk import ClientConfiguration, OpenFgaClient + +configuration = ClientConfiguration( + api_url=FGA_API_URL, + store_id=FGA_STORE_ID, +) + +async with OpenFgaClient(configuration) as fga_client: + request_body = { + "user": "user:bob", + "action": "custom_action", + "resource": "resource:123", + } + + response = await fga_client.raw_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + query_params={"page_size": "20"}, + body=request_body, + headers={"X-Experimental-Feature": "enabled"}, + ) +``` + +#### Example: Calling a new "Custom Endpoint" endpoint and handling raw response + +```python +# Get raw response without automatic decoding +raw_response = await fga_client.raw_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={"user": "user:bob", "action": "custom_action"}, +) + +# Access the response data +if raw_response.status == 200: + # Manually decode the response + result = raw_response.json() + if result: + print(f"Response: {result}") + + # You can access fields like headers, status code, etc. from raw_response: + print(f"Status Code: {raw_response.status}") + print(f"Headers: {raw_response.headers}") + print(f"Body as text: {raw_response.text()}") +``` + +#### Example: Calling a new "Custom Endpoint" endpoint and decoding response into a dictionary + +```python +# Get raw response decoded into a dictionary +response = await fga_client.raw_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={"user": "user:bob", "action": "custom_action"}, +) + +# The response body is automatically parsed as JSON if possible +result = response.json() # Returns dict or None if not JSON + +if result: + print(f"Response: {result}") + # Access fields from the decoded response + if "allowed" in result: + print(f"Allowed: {result['allowed']}") + +print(f"Status Code: {response.status}") +print(f"Headers: {response.headers}") +``` + +#### Example: Calling an existing endpoint with GET + +```python +# Get a list of stores with query parameters +response = await fga_client.raw_request( + operation_name="ListStores", # Required: descriptive name for the operation + method="GET", + path="/stores", + query_params={ + "page_size": 10, + "continuation_token": "eyJwayI6...", + }, +) + +stores = response.json() +print("Stores:", stores) +``` + +#### Example: Using Path Parameters + +Path parameters are specified in the path using `{param_name}` syntax and are replaced with URL-encoded values from the `path_params` dictionary. If `{store_id}` is present in the path and not provided in `path_params`, it will be automatically replaced with the configured store_id: + +```python +# Using explicit path parameters +response = await fga_client.raw_request( + operation_name="ReadAuthorizationModel", # Required: descriptive name for the operation + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + path_params={ + "store_id": "your-store-id", + "model_id": "your-model-id", + }, +) + +# Using automatic store_id substitution +response = await fga_client.raw_request( + operation_name="ReadAuthorizationModel", # Required: descriptive name for the operation + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + path_params={ + "model_id": "your-model-id", + }, +) +``` ### Retries diff --git a/openfga_sdk/api_client.py b/openfga_sdk/api_client.py index d4cc632..68cd5d2 100644 --- a/openfga_sdk/api_client.py +++ b/openfga_sdk/api_client.py @@ -420,7 +420,7 @@ async def __call_api( if _return_http_data_only: return return_data else: - return (return_data, response_data.status, response_data.headers) + return (return_data, response_data.status, response_data.getheaders()) def _parse_retry_after_header(self, headers) -> int: retry_after_header = headers.get("retry-after") diff --git a/openfga_sdk/client/client.py b/openfga_sdk/client/client.py index aba8943..9984ba2 100644 --- a/openfga_sdk/client/client.py +++ b/openfga_sdk/client/client.py @@ -1,5 +1,8 @@ import asyncio import uuid +import urllib.parse +from typing import Any +import json from openfga_sdk.api.open_fga_api import OpenFgaApi from openfga_sdk.api_client import ApiClient @@ -33,6 +36,7 @@ construct_write_single_response, ) from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.constants import ( CLIENT_BULK_REQUEST_ID_HEADER, CLIENT_MAX_BATCH_SIZE, @@ -40,6 +44,7 @@ CLIENT_METHOD_HEADER, ) from openfga_sdk.exceptions import ( + ApiException, AuthenticationError, FgaValidationException, UnauthorizedException, @@ -68,6 +73,7 @@ ) from openfga_sdk.models.write_request import WriteRequest from openfga_sdk.validation import is_well_formed_ulid_string +from openfga_sdk.rest import RESTResponse def _chuck_array(array, max_size): @@ -1096,3 +1102,164 @@ def map_to_assertion(client_assertion: ClientAssertion): authorization_model_id, api_request_body, **kwargs ) return api_response + + ####################### + # Raw Request + ####################### + async def raw_request( + self, + method: str, + path: str, + query_params: dict[str, str | int | list[str | int]] | None = None, + path_params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + body: dict[str, Any] | list[Any] | str | bytes | None = None, + operation_name: str | None = None, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> RawResponse: + """ + Make a raw HTTP request to any OpenFGA API endpoint. + + :param method: HTTP method (GET, POST, PUT, DELETE, PATCH, etc.) + :param path: API endpoint path (e.g., "/stores/{store_id}/check" or "/stores") + :param query_params: Optional query parameters as a dictionary + :param path_params: Optional path parameters to replace placeholders in path + (e.g., {"store_id": "abc", "model_id": "xyz"}) + :param headers: Optional request headers (will be merged with default headers) + :param body: Optional request body (dict/list will be JSON serialized, str/bytes sent as-is) + :param operation_name: Required operation name for telemetry/logging (e.g., "Check", "Write", "CustomEndpoint") + :param options: Optional request options: + - headers: Additional headers (merged with headers parameter) + - retry_params: Override retry parameters for this request + - authorization_model_id: Not used in raw_request, but kept for consistency + :return: RawResponse object with status, headers, and body + :raises FgaValidationException: If path contains {store_id} but store_id is not configured + :raises ApiException: For HTTP errors (with SDK error handling applied) + """ + + request_headers = dict(headers) if headers else {} + if options and options.get("headers"): + request_headers.update(options["headers"]) + + if not operation_name: + raise FgaValidationException("operation_name is required for raw_request") + + resource_path = path + path_params_dict = dict(path_params) if path_params else {} + + if "{store_id}" in resource_path and "store_id" not in path_params_dict: + store_id = self.get_store_id() + if store_id is None or store_id == "": + raise FgaValidationException( + "Path contains {store_id} but store_id is not configured. " + "Set store_id in ClientConfiguration, use set_store_id(), or provide it in path_params." + ) + path_params_dict["store_id"] = store_id + + for param_name, param_value in path_params_dict.items(): + placeholder = f"{{{param_name}}}" + if placeholder in resource_path: + encoded_value = urllib.parse.quote(str(param_value), safe="") + resource_path = resource_path.replace(placeholder, encoded_value) + if "{" in resource_path or "}" in resource_path: + raise FgaValidationException( + f"Not all path parameters were provided for path: {path}" + ) + + query_params_list = [] + if query_params: + for key, value in query_params.items(): + if value is None: + continue + if isinstance(value, list): + for item in value: + if item is not None: + query_params_list.append((key, str(item))) + continue + query_params_list.append((key, str(value))) + + body_params = None + if body is not None: + if isinstance(body, (dict, list)): + body_params = json.dumps(body) + elif isinstance(body, str): + body_params = body + elif isinstance(body, bytes): + body_params = body + else: + body_params = json.dumps(body) + + retry_params = None + if options and options.get("retry_params"): + retry_params = options["retry_params"] + + if "Content-Type" not in request_headers: + request_headers["Content-Type"] = "application/json" + if "Accept" not in request_headers: + request_headers["Accept"] = "application/json" + + auth_headers = dict(request_headers) if request_headers else {} + await self._api_client.update_params_for_auth( + auth_headers, + query_params_list, + auth_settings=[], + oauth2_client=self._api._oauth2_client, + ) + + telemetry_attributes = None + if operation_name: + from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes + telemetry_attributes = { + TelemetryAttributes.fga_client_request_method: operation_name.lower(), + } + if self.get_store_id(): + telemetry_attributes[TelemetryAttributes.fga_client_request_store_id] = self.get_store_id() + + try: + _, http_status, http_headers = await self._api_client.call_api( + resource_path=resource_path, + method=method.upper(), + query_params=query_params_list if query_params_list else None, + header_params=auth_headers if auth_headers else None, + body=body_params, + response_types_map={}, + auth_settings=[], + _return_http_data_only=False, + _preload_content=True, + _retry_params=retry_params, + _oauth2_client=self._api._oauth2_client, + _telemetry_attributes=telemetry_attributes, + ) + except ApiException as e: + raise + rest_response: RESTResponse | None = getattr( + self._api_client, "last_response", None + ) + + if rest_response is None: + raise RuntimeError("Failed to get response from API client") + + response_body: bytes | str | dict[str, Any] | None = None + if rest_response.data is not None: + if isinstance(rest_response.data, str): + try: + response_body = json.loads(rest_response.data) + except (json.JSONDecodeError, ValueError): + response_body = rest_response.data + elif isinstance(rest_response.data, bytes): + try: + decoded = rest_response.data.decode("utf-8") + try: + response_body = json.loads(decoded) + except (json.JSONDecodeError, ValueError): + response_body = decoded + except UnicodeDecodeError: + response_body = rest_response.data + else: + response_body = rest_response.data + + return RawResponse( + status=http_status, + headers=http_headers if http_headers else {}, + body=response_body, + ) diff --git a/openfga_sdk/client/models/__init__.py b/openfga_sdk/client/models/__init__.py index 528d527..52644a5 100644 --- a/openfga_sdk/client/models/__init__.py +++ b/openfga_sdk/client/models/__init__.py @@ -23,6 +23,7 @@ from openfga_sdk.client.models.write_request import ClientWriteRequest from openfga_sdk.client.models.write_response import ClientWriteResponse from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts +from openfga_sdk.client.models.raw_response import RawResponse __all__ = [ @@ -45,4 +46,5 @@ "ClientWriteRequestOnMissingDeletes", "ConflictOptions", "ClientWriteOptions", + "RawResponse", ] diff --git a/openfga_sdk/client/models/raw_response.py b/openfga_sdk/client/models/raw_response.py new file mode 100644 index 0000000..26503db --- /dev/null +++ b/openfga_sdk/client/models/raw_response.py @@ -0,0 +1,90 @@ +""" +Raw response wrapper for raw_request method. + +This module provides a simple response wrapper for raw HTTP requests +made through the SDK's raw_request method. +""" + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class RawResponse: + """ + Response wrapper for raw HTTP requests. + + This class provides a simple interface to access the response + from a raw_request call, including status code, headers, and body. + + The body is automatically parsed as JSON if possible, otherwise + it's returned as a string or bytes. + """ + + status: int + """HTTP status code""" + + headers: dict[str, str] + """Response headers as a dictionary""" + + body: bytes | str | dict[str, Any] | None = None + """Response body (already parsed as dict if JSON, otherwise str or bytes)""" + + def json(self) -> dict[str, Any] | None: + """ + Return the response body as a JSON dictionary. + + If the body is already a dict (parsed JSON), returns it directly. + If the body is a string or bytes, attempts to parse it as JSON. + Returns None if body is None or cannot be parsed. + + :return: Parsed JSON dictionary or None + """ + if self.body is None: + return None + + if isinstance(self.body, dict): + return self.body + + if isinstance(self.body, str): + import json + + try: + return json.loads(self.body) + except (json.JSONDecodeError, ValueError): + return None + + if isinstance(self.body, bytes): + import json + + try: + return json.loads(self.body.decode("utf-8")) + except (json.JSONDecodeError, ValueError, UnicodeDecodeError): + return None + + return None + + def text(self) -> str | None: + """ + Return the response body as a string. + + :return: Response body as string or None + """ + if self.body is None: + return None + + if isinstance(self.body, str): + return self.body + + if isinstance(self.body, bytes): + try: + return self.body.decode("utf-8") + except UnicodeDecodeError: + return self.body.decode("utf-8", errors="replace") + + if isinstance(self.body, dict): + import json + + return json.dumps(self.body) + + return str(self.body) diff --git a/openfga_sdk/sync/api_client.py b/openfga_sdk/sync/api_client.py index 7990e25..dfcdff9 100644 --- a/openfga_sdk/sync/api_client.py +++ b/openfga_sdk/sync/api_client.py @@ -420,7 +420,7 @@ def __call_api( if _return_http_data_only: return return_data else: - return (return_data, response_data.status, response_data.headers) + return (return_data, response_data.status, response_data.getheaders()) def _parse_retry_after_header(self, headers) -> int: retry_after_header = headers.get("retry-after") diff --git a/openfga_sdk/sync/client/client.py b/openfga_sdk/sync/client/client.py index 1efa33f..078df6f 100644 --- a/openfga_sdk/sync/client/client.py +++ b/openfga_sdk/sync/client/client.py @@ -1,4 +1,7 @@ import uuid +import json +import urllib.parse +from typing import Any from concurrent.futures import ThreadPoolExecutor @@ -32,6 +35,7 @@ construct_write_single_response, ) from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.constants import ( CLIENT_BULK_REQUEST_ID_HEADER, CLIENT_MAX_BATCH_SIZE, @@ -39,6 +43,7 @@ CLIENT_METHOD_HEADER, ) from openfga_sdk.exceptions import ( + ApiException, AuthenticationError, FgaValidationException, UnauthorizedException, @@ -69,6 +74,7 @@ from openfga_sdk.sync.api_client import ApiClient from openfga_sdk.sync.open_fga_api import OpenFgaApi from openfga_sdk.validation import is_well_formed_ulid_string +from openfga_sdk.sync.rest import RESTResponse def _chuck_array(array, max_size): @@ -1095,3 +1101,166 @@ def map_to_assertion(client_assertion: ClientAssertion) -> Assertion: authorization_model_id, api_request_body, **kwargs ) return api_response + + ####################### + # Raw Request + ####################### + def raw_request( + self, + method: str, + path: str, + query_params: dict[str, str | int | list[str | int]] | None = None, + path_params: dict[str, str] | None = None, + headers: dict[str, str] | None = None, + body: dict[str, Any] | list[Any] | str | bytes | None = None, + operation_name: str | None = None, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> RawResponse: + """ + Make a raw HTTP request to any OpenFGA API endpoint. + + :param method: HTTP method (GET, POST, PUT, DELETE, PATCH, etc.) + :param path: API endpoint path (e.g., "/stores/{store_id}/check" or "/stores") + :param query_params: Optional query parameters as a dictionary + :param path_params: Optional path parameters to replace placeholders in path + (e.g., {"store_id": "abc", "model_id": "xyz"}) + :param headers: Optional request headers (will be merged with default headers) + :param body: Optional request body (dict/list will be JSON serialized, str/bytes sent as-is) + :param operation_name: Required operation name for telemetry/logging (e.g., "Check", "Write", "CustomEndpoint") + :param options: Optional request options: + - headers: Additional headers (merged with headers parameter) + - retry_params: Override retry parameters for this request + - authorization_model_id: Not used in raw_request, but kept for consistency + :return: RawResponse object with status, headers, and body + :raises FgaValidationException: If path contains {store_id} but store_id is not configured + :raises ApiException: For HTTP errors (with SDK error handling applied) + """ + + request_headers = dict(headers) if headers else {} + if options and options.get("headers"): + request_headers.update(options["headers"]) + + if not operation_name: + raise FgaValidationException("operation_name is required for raw_request") + + resource_path = path + path_params_dict = dict(path_params) if path_params else {} + + if "{store_id}" in resource_path and "store_id" not in path_params_dict: + store_id = self.get_store_id() + if store_id is None or store_id == "": + raise FgaValidationException( + "Path contains {store_id} but store_id is not configured. " + "Set store_id in ClientConfiguration, use set_store_id(), or provide it in path_params." + ) + path_params_dict["store_id"] = store_id + + for param_name, param_value in path_params_dict.items(): + placeholder = f"{{{param_name}}}" + if placeholder in resource_path: + encoded_value = urllib.parse.quote(str(param_value), safe="") + resource_path = resource_path.replace(placeholder, encoded_value) + + if "{" in resource_path or "}" in resource_path: + raise FgaValidationException( + f"Not all path parameters were provided for path: {path}" + ) + + query_params_list = [] + if query_params: + for key, value in query_params.items(): + if value is None: + continue + if isinstance(value, list): + for item in value: + if item is not None: + query_params_list.append((key, str(item))) + continue + query_params_list.append((key, str(value))) + + body_params = None + if body is not None: + if isinstance(body, (dict, list)): + body_params = json.dumps(body) + elif isinstance(body, str): + body_params = body + elif isinstance(body, bytes): + body_params = body + else: + body_params = json.dumps(body) + + retry_params = None + if options and options.get("retry_params"): + retry_params = options["retry_params"] + + if "Content-Type" not in request_headers: + request_headers["Content-Type"] = "application/json" + if "Accept" not in request_headers: + request_headers["Accept"] = "application/json" + + auth_headers = dict(request_headers) if request_headers else {} + self._api_client.update_params_for_auth( + auth_headers, + query_params_list, + auth_settings=[], + oauth2_client=self._api._oauth2_client, + ) + + telemetry_attributes = None + if operation_name: + from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes + telemetry_attributes = { + TelemetryAttributes.fga_client_request_method: operation_name.lower(), + } + if self.get_store_id(): + telemetry_attributes[TelemetryAttributes.fga_client_request_store_id] = self.get_store_id() + + try: + _, http_status, http_headers = self._api_client.call_api( + resource_path=resource_path, + method=method.upper(), + query_params=query_params_list if query_params_list else None, + header_params=auth_headers if auth_headers else None, + body=body_params, + response_types_map={}, + auth_settings=[], + _return_http_data_only=False, + _preload_content=True, + _retry_params=retry_params, + _oauth2_client=self._api._oauth2_client, + _telemetry_attributes=telemetry_attributes, + ) + except ApiException as e: + raise + + rest_response: RESTResponse | None = getattr( + self._api_client, "last_response", None + ) + + if rest_response is None: + raise RuntimeError("Failed to get response from API client") + + response_body: bytes | str | dict[str, Any] | None = None + if rest_response.data is not None: + if isinstance(rest_response.data, str): + try: + response_body = json.loads(rest_response.data) + except (json.JSONDecodeError, ValueError): + response_body = rest_response.data + elif isinstance(rest_response.data, bytes): + try: + decoded = rest_response.data.decode("utf-8") + try: + response_body = json.loads(decoded) + except (json.JSONDecodeError, ValueError): + response_body = decoded + except UnicodeDecodeError: + response_body = rest_response.data + else: + response_body = rest_response.data + + return RawResponse( + status=http_status, + headers=http_headers if http_headers else {}, + body=response_body, + ) diff --git a/openfga_sdk/telemetry/attributes.py b/openfga_sdk/telemetry/attributes.py index ddda43f..e7b10dd 100644 --- a/openfga_sdk/telemetry/attributes.py +++ b/openfga_sdk/telemetry/attributes.py @@ -295,7 +295,7 @@ def fromResponse( response.status ) - if response.body is not None: + if response.body is not None and isinstance(response.body, dict): response_model_id = response.body.get( "openfga-authorization-model-id" ) or response.body.get("openfga_authorization_model_id") diff --git a/test/client/client_test.py b/test/client/client_test.py index 947134d..c2972be 100644 --- a/test/client/client_test.py +++ b/test/client/client_test.py @@ -25,6 +25,7 @@ from openfga_sdk.client.models.write_request import ClientWriteRequest from openfga_sdk.client.models.write_single_response import ClientWriteSingleResponse from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.configuration import RetryParams from openfga_sdk.exceptions import ( FgaValidationException, @@ -4165,3 +4166,310 @@ async def test_write_with_conflict_options_both(self, mock_request): _preload_content=ANY, _request_timeout=None, ) + + @patch.object(rest.RESTClientObject, "request") + async def test_raw_request_post_with_body(self, mock_request): + """Test case for raw_request + + Make a raw POST request with JSON body + """ + response_body = '{"result": "success", "data": {"id": "123"}}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + response = await api_client.raw_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + body={"user": "user:bob", "action": "custom_action"}, + query_params={"page_size": "20"}, + headers={"X-Experimental-Feature": "enabled"}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.headers) + self.assertIsNotNone(response.body) + self.assertIsInstance(response.body, dict) + self.assertEqual(response.body["result"], "success") + + # Verify response helper methods + json_data = response.json() + self.assertIsNotNone(json_data) + self.assertEqual(json_data["result"], "success") + + text_data = response.text() + self.assertIsNotNone(text_data) + self.assertIn("success", text_data) + + # Verify the API was called with correct parameters + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom-endpoint", + headers=ANY, + body={"user": "user:bob", "action": "custom_action"}, + query_params=[("page_size", "20")], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_raw_request_get_with_query_params(self, mock_request): + """Test case for raw_request + + Make a raw GET request with query parameters + """ + response_body = '{"stores": [{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}]}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + async with OpenFgaClient(configuration) as api_client: + response = await api_client.raw_request( + operation_name="ListStores", + method="GET", + path="/stores", + query_params={ + "page_size": 10, + "continuation_token": "eyJwayI6...", + }, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.body) + self.assertIsInstance(response.body, dict) + self.assertIn("stores", response.body) + + # Verify the API was called with correct parameters + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores", + headers=ANY, + body=None, + query_params=[("page_size", "10"), ("continuation_token", "eyJwayI6...")], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_raw_request_with_path_params(self, mock_request): + """Test case for raw_request + + Make a raw request with path parameters + """ + response_body = '{"authorization_model": {"id": "01G5JAVJ41T49E9TT3SKVS7X1J"}}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + response = await api_client.raw_request( + operation_name="ReadAuthorizationModel", + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + path_params={"model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.body) + + # Verify the API was called with correct path (store_id auto-substituted) + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", + headers=ANY, + body=None, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_raw_request_auto_store_id_substitution(self, mock_request): + """Test case for raw_request + + Test automatic store_id substitution when not provided in path_params + """ + response_body = '{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + response = await api_client.raw_request( + operation_name="GetStore", + method="GET", + path="/stores/{store_id}", + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + + # Verify store_id was automatically substituted + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", + headers=ANY, + body=None, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + async def test_raw_request_missing_operation_name(self): + """Test case for raw_request + + Test that operation_name is required + """ + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + await api_client.raw_request( + method="GET", + path="/stores", + ) + self.assertIn("operation_name is required", str(error.exception)) + await api_client.close() + + async def test_raw_request_missing_store_id(self): + """Test case for raw_request + + Test that store_id is required when path contains {store_id} + """ + configuration = self.configuration + # Don't set store_id + async with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + await api_client.raw_request( + operation_name="GetStore", + method="GET", + path="/stores/{store_id}", + ) + self.assertIn("store_id is not configured", str(error.exception)) + await api_client.close() + + async def test_raw_request_missing_path_params(self): + """Test case for raw_request + + Test that all path parameters must be provided + """ + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + await api_client.raw_request( + operation_name="ReadAuthorizationModel", + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + # Missing model_id in path_params + ) + self.assertIn("Not all path parameters were provided", str(error.exception)) + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_raw_request_with_list_query_params(self, mock_request): + """Test case for raw_request + + Test query parameters with list values + """ + response_body = '{"results": []}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + async with OpenFgaClient(configuration) as api_client: + response = await api_client.raw_request( + operation_name="ListStores", + method="GET", + path="/stores", + query_params={"ids": ["id1", "id2", "id3"]}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + + # Verify list query params are expanded + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores", + headers=ANY, + body=None, + query_params=[("ids", "id1"), ("ids", "id2"), ("ids", "id3")], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_raw_request_default_headers(self, mock_request): + """Test case for raw_request + + Test that default headers (Content-Type, Accept) are set + """ + response_body = '{"result": "success"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + response = await api_client.raw_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + body={"test": "data"}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + + # Verify default headers are set + call_args = mock_request.call_args + headers = call_args[1]["headers"] + self.assertEqual(headers.get("Content-Type"), "application/json") + self.assertEqual(headers.get("Accept"), "application/json") + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_raw_request_url_encoded_path_params(self, mock_request): + """Test case for raw_request + + Test that path parameters are URL encoded + """ + response_body = '{"result": "success"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + response = await api_client.raw_request( + operation_name="CustomEndpoint", + method="GET", + path="/stores/{store_id}/custom/{param}", + path_params={"param": "value with spaces & special chars"}, + ) + + self.assertIsInstance(response, RawResponse) + + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom/value%20with%20spaces%20%26%20special%20chars", + headers=ANY, + body=None, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() diff --git a/test/sync/client/client_test.py b/test/sync/client/client_test.py index dbffbd0..44394b2 100644 --- a/test/sync/client/client_test.py +++ b/test/sync/client/client_test.py @@ -23,6 +23,7 @@ from openfga_sdk.client.models.write_request import ClientWriteRequest from openfga_sdk.client.models.write_single_response import ClientWriteSingleResponse from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.exceptions import ( FgaValidationException, UnauthorizedException, @@ -4146,3 +4147,308 @@ def test_sync_write_with_conflict_options_both(self, mock_request): _preload_content=ANY, _request_timeout=None, ) + + @patch.object(rest.RESTClientObject, "request") + def test_raw_request_post_with_body(self, mock_request): + """Test case for raw_request + + Make a raw POST request with JSON body + """ + response_body = '{"result": "success", "data": {"id": "123"}}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + response = api_client.raw_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + body={"user": "user:bob", "action": "custom_action"}, + query_params={"page_size": "20"}, + headers={"X-Experimental-Feature": "enabled"}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.headers) + self.assertIsNotNone(response.body) + self.assertIsInstance(response.body, dict) + self.assertEqual(response.body["result"], "success") + + # Verify response helper methods + json_data = response.json() + self.assertIsNotNone(json_data) + self.assertEqual(json_data["result"], "success") + + text_data = response.text() + self.assertIsNotNone(text_data) + self.assertIn("success", text_data) + + # Verify the API was called with correct parameters + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom-endpoint", + headers=ANY, + body={"user": "user:bob", "action": "custom_action"}, + query_params=[("page_size", "20")], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_raw_request_get_with_query_params(self, mock_request): + """Test case for raw_request + + Make a raw GET request with query parameters + """ + response_body = '{"stores": [{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}]}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + with OpenFgaClient(configuration) as api_client: + response = api_client.raw_request( + operation_name="ListStores", + method="GET", + path="/stores", + query_params={ + "page_size": 10, + "continuation_token": "eyJwayI6...", + }, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.body) + self.assertIsInstance(response.body, dict) + self.assertIn("stores", response.body) + + # Verify the API was called with correct parameters + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores", + headers=ANY, + body=None, + query_params=[("page_size", "10"), ("continuation_token", "eyJwayI6...")], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_raw_request_with_path_params(self, mock_request): + """Test case for raw_request + + Make a raw request with path parameters + """ + response_body = '{"authorization_model": {"id": "01G5JAVJ41T49E9TT3SKVS7X1J"}}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + response = api_client.raw_request( + operation_name="ReadAuthorizationModel", + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + path_params={"model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + self.assertIsNotNone(response.body) + + # Verify the API was called with correct path (store_id auto-substituted) + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", + headers=ANY, + body=None, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_raw_request_auto_store_id_substitution(self, mock_request): + """Test case for raw_request + + Test automatic store_id substitution when not provided in path_params + """ + response_body = '{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + response = api_client.raw_request( + operation_name="GetStore", + method="GET", + path="/stores/{store_id}", + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + + # Verify store_id was automatically substituted + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", + headers=ANY, + body=None, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + def test_raw_request_missing_operation_name(self): + """Test case for raw_request + + Test that operation_name is required + """ + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + api_client.raw_request( + method="GET", + path="/stores", + ) + self.assertIn("operation_name is required", str(error.exception)) + api_client.close() + + def test_raw_request_missing_store_id(self): + """Test case for raw_request + + Test that store_id is required when path contains {store_id} + """ + configuration = self.configuration + # Don't set store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + api_client.raw_request( + operation_name="GetStore", + method="GET", + path="/stores/{store_id}", + ) + self.assertIn("store_id is not configured", str(error.exception)) + api_client.close() + + def test_raw_request_missing_path_params(self): + """Test case for raw_request + + Test that all path parameters must be provided + """ + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + api_client.raw_request( + operation_name="ReadAuthorizationModel", + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + # Missing model_id in path_params + ) + self.assertIn("Not all path parameters were provided", str(error.exception)) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_raw_request_with_list_query_params(self, mock_request): + """Test case for raw_request + + Test query parameters with list values + """ + response_body = '{"results": []}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + with OpenFgaClient(configuration) as api_client: + response = api_client.raw_request( + operation_name="ListStores", + method="GET", + path="/stores", + query_params={"ids": ["id1", "id2", "id3"]}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores", + headers=ANY, + body=None, + query_params=[("ids", "id1"), ("ids", "id2"), ("ids", "id3")], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_raw_request_default_headers(self, mock_request): + """Test case for raw_request + + Test that default headers (Content-Type, Accept) are set + """ + response_body = '{"result": "success"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + response = api_client.raw_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + body={"test": "data"}, + ) + + self.assertIsInstance(response, RawResponse) + self.assertEqual(response.status, 200) + + call_args = mock_request.call_args + headers = call_args[1]["headers"] + self.assertEqual(headers.get("Content-Type"), "application/json") + self.assertEqual(headers.get("Accept"), "application/json") + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_raw_request_url_encoded_path_params(self, mock_request): + """Test case for raw_request + + Test that path parameters are URL encoded + """ + response_body = '{"result": "success"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + response = api_client.raw_request( + operation_name="CustomEndpoint", + method="GET", + path="/stores/{store_id}/custom/{param}", + path_params={"param": "value with spaces & special chars"}, + ) + + self.assertIsInstance(response, RawResponse) + + mock_request.assert_called_once_with( + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom/value%20with%20spaces%20%26%20special%20chars", + headers=ANY, + body=None, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() From 3c2e66ab7d3f84a39e178c8cfb5d9e1249c26f3f Mon Sep 17 00:00:00 2001 From: kcbiradar Date: Thu, 15 Jan 2026 22:22:12 +0530 Subject: [PATCH 2/6] update: handling double-JSON-encodes --- openfga_sdk/client/client.py | 17 +++++++---------- openfga_sdk/sync/client/client.py | 17 +++++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/openfga_sdk/client/client.py b/openfga_sdk/client/client.py index 9984ba2..110881f 100644 --- a/openfga_sdk/client/client.py +++ b/openfga_sdk/client/client.py @@ -1178,23 +1178,20 @@ async def raw_request( continue query_params_list.append((key, str(value))) - body_params = None - if body is not None: - if isinstance(body, (dict, list)): - body_params = json.dumps(body) + body_params = body + if "Content-Type" not in request_headers: + if isinstance(body, (dict, list)) or body is None: + request_headers["Content-Type"] = "application/json" elif isinstance(body, str): - body_params = body + request_headers["Content-Type"] = "text/plain" elif isinstance(body, bytes): - body_params = body + request_headers["Content-Type"] = "application/octet-stream" else: - body_params = json.dumps(body) + request_headers["Content-Type"] = "application/json" retry_params = None if options and options.get("retry_params"): retry_params = options["retry_params"] - - if "Content-Type" not in request_headers: - request_headers["Content-Type"] = "application/json" if "Accept" not in request_headers: request_headers["Accept"] = "application/json" diff --git a/openfga_sdk/sync/client/client.py b/openfga_sdk/sync/client/client.py index 078df6f..e7d7738 100644 --- a/openfga_sdk/sync/client/client.py +++ b/openfga_sdk/sync/client/client.py @@ -1178,23 +1178,20 @@ def raw_request( continue query_params_list.append((key, str(value))) - body_params = None - if body is not None: - if isinstance(body, (dict, list)): - body_params = json.dumps(body) + body_params = body + if "Content-Type" not in request_headers: + if isinstance(body, (dict, list)) or body is None: + request_headers["Content-Type"] = "application/json" elif isinstance(body, str): - body_params = body + request_headers["Content-Type"] = "text/plain" elif isinstance(body, bytes): - body_params = body + request_headers["Content-Type"] = "application/octet-stream" else: - body_params = json.dumps(body) + request_headers["Content-Type"] = "application/json" retry_params = None if options and options.get("retry_params"): retry_params = options["retry_params"] - - if "Content-Type" not in request_headers: - request_headers["Content-Type"] = "application/json" if "Accept" not in request_headers: request_headers["Accept"] = "application/json" From 3f85dfad753f87ff3ffd212f6fdfba7044aa6f0a Mon Sep 17 00:00:00 2001 From: kcbiradar Date: Sat, 17 Jan 2026 15:40:32 +0530 Subject: [PATCH 3/6] addRawRequestMethod: fix testcases & remove trailing spaces --- openfga_sdk/client/client.py | 62 ++++++++++--------- openfga_sdk/client/models/__init__.py | 2 +- openfga_sdk/client/models/raw_response.py | 4 +- openfga_sdk/sync/client/client.py | 63 +++++++++++--------- test/client/client_test.py | 72 +++++++++++++++-------- test/sync/client/client_test.py | 66 ++++++++++++--------- 6 files changed, 156 insertions(+), 113 deletions(-) diff --git a/openfga_sdk/client/client.py b/openfga_sdk/client/client.py index 110881f..9eba189 100644 --- a/openfga_sdk/client/client.py +++ b/openfga_sdk/client/client.py @@ -1,8 +1,9 @@ import asyncio -import uuid +import json import urllib.parse +import uuid + from typing import Any -import json from openfga_sdk.api.open_fga_api import OpenFgaApi from openfga_sdk.api_client import ApiClient @@ -28,6 +29,7 @@ from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest from openfga_sdk.client.models.list_users_request import ClientListUsersRequest +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest from openfga_sdk.client.models.tuple import ClientTuple, convert_tuple_keys from openfga_sdk.client.models.write_request import ClientWriteRequest @@ -36,7 +38,6 @@ construct_write_single_response, ) from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts -from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.constants import ( CLIENT_BULK_REQUEST_ID_HEADER, CLIENT_MAX_BATCH_SIZE, @@ -44,7 +45,6 @@ CLIENT_METHOD_HEADER, ) from openfga_sdk.exceptions import ( - ApiException, AuthenticationError, FgaValidationException, UnauthorizedException, @@ -72,8 +72,11 @@ WriteAuthorizationModelRequest, ) from openfga_sdk.models.write_request import WriteRequest -from openfga_sdk.validation import is_well_formed_ulid_string from openfga_sdk.rest import RESTResponse +from openfga_sdk.telemetry.attributes import ( + TelemetryAttributes, +) +from openfga_sdk.validation import is_well_formed_ulid_string def _chuck_array(array, max_size): @@ -1146,7 +1149,7 @@ async def raw_request( resource_path = path path_params_dict = dict(path_params) if path_params else {} - + if "{store_id}" in resource_path and "store_id" not in path_params_dict: store_id = self.get_store_id() if store_id is None or store_id == "": @@ -1155,7 +1158,7 @@ async def raw_request( "Set store_id in ClientConfiguration, use set_store_id(), or provide it in path_params." ) path_params_dict["store_id"] = store_id - + for param_name, param_value in path_params_dict.items(): placeholder = f"{{{param_name}}}" if placeholder in resource_path: @@ -1205,36 +1208,39 @@ async def raw_request( telemetry_attributes = None if operation_name: - from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes telemetry_attributes = { TelemetryAttributes.fga_client_request_method: operation_name.lower(), } if self.get_store_id(): - telemetry_attributes[TelemetryAttributes.fga_client_request_store_id] = self.get_store_id() - - try: - _, http_status, http_headers = await self._api_client.call_api( - resource_path=resource_path, - method=method.upper(), - query_params=query_params_list if query_params_list else None, - header_params=auth_headers if auth_headers else None, - body=body_params, - response_types_map={}, - auth_settings=[], - _return_http_data_only=False, - _preload_content=True, - _retry_params=retry_params, - _oauth2_client=self._api._oauth2_client, - _telemetry_attributes=telemetry_attributes, - ) - except ApiException as e: - raise + telemetry_attributes[ + TelemetryAttributes.fga_client_request_store_id + ] = self.get_store_id() + + _, http_status, http_headers = await self._api_client.call_api( + resource_path=resource_path, + method=method.upper(), + query_params=query_params_list if query_params_list else None, + header_params=auth_headers if auth_headers else None, + body=body_params, + response_types_map={}, + auth_settings=[], + _return_http_data_only=False, + _preload_content=True, + _retry_params=retry_params, + _oauth2_client=self._api._oauth2_client, + _telemetry_attributes=telemetry_attributes, + ) rest_response: RESTResponse | None = getattr( self._api_client, "last_response", None ) if rest_response is None: - raise RuntimeError("Failed to get response from API client") + raise RuntimeError( + f"Failed to get response from API client for {method.upper()} " + f"request to '{resource_path}'" + f"{f' (operation: {operation_name})' if operation_name else ''}. " + "This may indicate an internal SDK error, network problem, or client configuration issue." + ) response_body: bytes | str | dict[str, Any] | None = None if rest_response.data is not None: diff --git a/openfga_sdk/client/models/__init__.py b/openfga_sdk/client/models/__init__.py index 52644a5..2d3cde1 100644 --- a/openfga_sdk/client/models/__init__.py +++ b/openfga_sdk/client/models/__init__.py @@ -12,6 +12,7 @@ from openfga_sdk.client.models.expand_request import ClientExpandRequest from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest from openfga_sdk.client.models.tuple import ClientTuple from openfga_sdk.client.models.write_conflict_opts import ( @@ -23,7 +24,6 @@ from openfga_sdk.client.models.write_request import ClientWriteRequest from openfga_sdk.client.models.write_response import ClientWriteResponse from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts -from openfga_sdk.client.models.raw_response import RawResponse __all__ = [ diff --git a/openfga_sdk/client/models/raw_response.py b/openfga_sdk/client/models/raw_response.py index 26503db..e369088 100644 --- a/openfga_sdk/client/models/raw_response.py +++ b/openfga_sdk/client/models/raw_response.py @@ -23,10 +23,10 @@ class RawResponse: status: int """HTTP status code""" - + headers: dict[str, str] """Response headers as a dictionary""" - + body: bytes | str | dict[str, Any] | None = None """Response body (already parsed as dict if JSON, otherwise str or bytes)""" diff --git a/openfga_sdk/sync/client/client.py b/openfga_sdk/sync/client/client.py index e7d7738..18b0a56 100644 --- a/openfga_sdk/sync/client/client.py +++ b/openfga_sdk/sync/client/client.py @@ -1,9 +1,9 @@ -import uuid import json import urllib.parse -from typing import Any +import uuid from concurrent.futures import ThreadPoolExecutor +from typing import Any from openfga_sdk.client.configuration import ClientConfiguration from openfga_sdk.client.models.assertion import ClientAssertion @@ -27,6 +27,7 @@ from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest from openfga_sdk.client.models.list_users_request import ClientListUsersRequest +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest from openfga_sdk.client.models.tuple import ClientTuple, convert_tuple_keys from openfga_sdk.client.models.write_request import ClientWriteRequest @@ -35,7 +36,6 @@ construct_write_single_response, ) from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts -from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.constants import ( CLIENT_BULK_REQUEST_ID_HEADER, CLIENT_MAX_BATCH_SIZE, @@ -43,7 +43,6 @@ CLIENT_METHOD_HEADER, ) from openfga_sdk.exceptions import ( - ApiException, AuthenticationError, FgaValidationException, UnauthorizedException, @@ -73,8 +72,11 @@ from openfga_sdk.models.write_request import WriteRequest from openfga_sdk.sync.api_client import ApiClient from openfga_sdk.sync.open_fga_api import OpenFgaApi -from openfga_sdk.validation import is_well_formed_ulid_string from openfga_sdk.sync.rest import RESTResponse +from openfga_sdk.telemetry.attributes import ( + TelemetryAttributes, +) +from openfga_sdk.validation import is_well_formed_ulid_string def _chuck_array(array, max_size): @@ -1145,7 +1147,7 @@ def raw_request( resource_path = path path_params_dict = dict(path_params) if path_params else {} - + if "{store_id}" in resource_path and "store_id" not in path_params_dict: store_id = self.get_store_id() if store_id is None or store_id == "": @@ -1154,13 +1156,13 @@ def raw_request( "Set store_id in ClientConfiguration, use set_store_id(), or provide it in path_params." ) path_params_dict["store_id"] = store_id - + for param_name, param_value in path_params_dict.items(): placeholder = f"{{{param_name}}}" if placeholder in resource_path: encoded_value = urllib.parse.quote(str(param_value), safe="") resource_path = resource_path.replace(placeholder, encoded_value) - + if "{" in resource_path or "}" in resource_path: raise FgaValidationException( f"Not all path parameters were provided for path: {path}" @@ -1205,37 +1207,40 @@ def raw_request( telemetry_attributes = None if operation_name: - from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes telemetry_attributes = { TelemetryAttributes.fga_client_request_method: operation_name.lower(), } if self.get_store_id(): - telemetry_attributes[TelemetryAttributes.fga_client_request_store_id] = self.get_store_id() - - try: - _, http_status, http_headers = self._api_client.call_api( - resource_path=resource_path, - method=method.upper(), - query_params=query_params_list if query_params_list else None, - header_params=auth_headers if auth_headers else None, - body=body_params, - response_types_map={}, - auth_settings=[], - _return_http_data_only=False, - _preload_content=True, - _retry_params=retry_params, - _oauth2_client=self._api._oauth2_client, - _telemetry_attributes=telemetry_attributes, - ) - except ApiException as e: - raise + telemetry_attributes[ + TelemetryAttributes.fga_client_request_store_id + ] = self.get_store_id() + + _, http_status, http_headers = self._api_client.call_api( + resource_path=resource_path, + method=method.upper(), + query_params=query_params_list if query_params_list else None, + header_params=auth_headers if auth_headers else None, + body=body_params, + response_types_map={}, + auth_settings=[], + _return_http_data_only=False, + _preload_content=True, + _retry_params=retry_params, + _oauth2_client=self._api._oauth2_client, + _telemetry_attributes=telemetry_attributes, + ) rest_response: RESTResponse | None = getattr( self._api_client, "last_response", None ) if rest_response is None: - raise RuntimeError("Failed to get response from API client") + raise RuntimeError( + f"Failed to get response from API client for {method.upper()} " + f"request to '{resource_path}'" + f"{f' (operation: {operation_name})' if operation_name else ''}. " + "This may indicate an internal SDK error, network problem, or client configuration issue." + ) response_body: bytes | str | dict[str, Any] | None = None if rest_response.data is not None: diff --git a/test/client/client_test.py b/test/client/client_test.py index c2972be..52f8b97 100644 --- a/test/client/client_test.py +++ b/test/client/client_test.py @@ -20,12 +20,12 @@ from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest from openfga_sdk.client.models.list_users_request import ClientListUsersRequest +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest from openfga_sdk.client.models.tuple import ClientTuple from openfga_sdk.client.models.write_request import ClientWriteRequest from openfga_sdk.client.models.write_single_response import ClientWriteSingleResponse from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts -from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.configuration import RetryParams from openfga_sdk.exceptions import ( FgaValidationException, @@ -3907,12 +3907,17 @@ def client_configuration(): ) -class TestClientConfigurationHeaders: +class TestClientConfigurationHeaders(IsolatedAsyncioTestCase): """Tests for ClientConfiguration headers parameter""" - def test_client_configuration_headers_default_none(self, client_configuration): + def setUp(self): + self.configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + + def test_client_configuration_headers_default_none(self): """Test that headers default to an empty dict in ClientConfiguration""" - assert client_configuration.headers == {} + assert self.configuration.headers == {} def test_client_configuration_headers_initialization_with_dict(self): """Test initializing ClientConfiguration with headers""" @@ -3937,11 +3942,11 @@ def test_client_configuration_headers_initialization_with_none(self): ) assert config.headers == {} - def test_client_configuration_headers_setter(self, client_configuration): + def test_client_configuration_headers_setter(self): """Test setting headers via property setter""" headers = {"X-Test": "test-value"} - client_configuration.headers = headers - assert client_configuration.headers == headers + self.configuration.headers = headers + assert self.configuration.headers == headers def test_client_configuration_headers_with_authorization_model_id(self): """Test ClientConfiguration with headers and authorization_model_id""" @@ -4168,6 +4173,7 @@ async def test_write_with_conflict_options_both(self, mock_request): ) @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio async def test_raw_request_post_with_body(self, mock_request): """Test case for raw_request @@ -4208,22 +4214,25 @@ async def test_raw_request_post_with_body(self, mock_request): mock_request.assert_called_once_with( "POST", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom-endpoint", + query_params=[("page_size", "20")], headers=ANY, + post_params=None, body={"user": "user:bob", "action": "custom_action"}, - query_params=[("page_size", "20")], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) await api_client.close() @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio async def test_raw_request_get_with_query_params(self, mock_request): """Test case for raw_request Make a raw GET request with query parameters """ - response_body = '{"stores": [{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}]}' + response_body = ( + '{"stores": [{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}]}' + ) mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration @@ -4248,16 +4257,20 @@ async def test_raw_request_get_with_query_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores", + query_params=[ + ("page_size", "10"), + ("continuation_token", "eyJwayI6..."), + ], headers=ANY, + post_params=None, body=None, - query_params=[("page_size", "10"), ("continuation_token", "eyJwayI6...")], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) await api_client.close() @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio async def test_raw_request_with_path_params(self, mock_request): """Test case for raw_request @@ -4284,16 +4297,17 @@ async def test_raw_request_with_path_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", + query_params=None, headers=ANY, + post_params=None, body=None, - query_params=[], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) await api_client.close() @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio async def test_raw_request_auto_store_id_substitution(self, mock_request): """Test case for raw_request @@ -4318,15 +4332,16 @@ async def test_raw_request_auto_store_id_substitution(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", + query_params=None, headers=ANY, + post_params=None, body=None, - query_params=[], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) await api_client.close() + @pytest.mark.asyncio async def test_raw_request_missing_operation_name(self): """Test case for raw_request @@ -4343,6 +4358,7 @@ async def test_raw_request_missing_operation_name(self): self.assertIn("operation_name is required", str(error.exception)) await api_client.close() + @pytest.mark.asyncio async def test_raw_request_missing_store_id(self): """Test case for raw_request @@ -4360,6 +4376,7 @@ async def test_raw_request_missing_store_id(self): self.assertIn("store_id is not configured", str(error.exception)) await api_client.close() + @pytest.mark.asyncio async def test_raw_request_missing_path_params(self): """Test case for raw_request @@ -4379,6 +4396,7 @@ async def test_raw_request_missing_path_params(self): await api_client.close() @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio async def test_raw_request_with_list_query_params(self, mock_request): """Test case for raw_request @@ -4403,16 +4421,17 @@ async def test_raw_request_with_list_query_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores", + query_params=[("ids", "id1"), ("ids", "id2"), ("ids", "id3")], headers=ANY, + post_params=None, body=None, - query_params=[("ids", "id1"), ("ids", "id2"), ("ids", "id3")], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) await api_client.close() @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio async def test_raw_request_default_headers(self, mock_request): """Test case for raw_request @@ -4442,6 +4461,7 @@ async def test_raw_request_default_headers(self, mock_request): await api_client.close() @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio async def test_raw_request_url_encoded_path_params(self, mock_request): """Test case for raw_request @@ -4465,11 +4485,11 @@ async def test_raw_request_url_encoded_path_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom/value%20with%20spaces%20%26%20special%20chars", + query_params=None, headers=ANY, + post_params=None, body=None, - query_params=[], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) await api_client.close() diff --git a/test/sync/client/client_test.py b/test/sync/client/client_test.py index 44394b2..d3fe576 100644 --- a/test/sync/client/client_test.py +++ b/test/sync/client/client_test.py @@ -3,7 +3,7 @@ import uuid from datetime import datetime -from unittest import IsolatedAsyncioTestCase +from unittest import IsolatedAsyncioTestCase, TestCase from unittest.mock import ANY, patch import pytest @@ -18,12 +18,12 @@ from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest from openfga_sdk.client.models.list_users_request import ClientListUsersRequest +from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest from openfga_sdk.client.models.tuple import ClientTuple from openfga_sdk.client.models.write_request import ClientWriteRequest from openfga_sdk.client.models.write_single_response import ClientWriteSingleResponse from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts -from openfga_sdk.client.models.raw_response import RawResponse from openfga_sdk.exceptions import ( FgaValidationException, UnauthorizedException, @@ -3886,12 +3886,19 @@ def client_configuration(): ) -class TestSyncClientConfigurationHeaders: +class TestSyncClientConfigurationHeaders(TestCase): """Tests for ClientConfiguration headers parameter in sync client""" - def test_sync_client_configuration_headers_default_none(self, client_configuration): + def setUp(self): + from openfga_sdk.client.configuration import ClientConfiguration + + self.configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + + def test_sync_client_configuration_headers_default_none(self): """Test that headers default to an empty dict in ClientConfiguration""" - assert client_configuration.headers == {} + assert self.configuration.headers == {} def test_sync_client_configuration_headers_initialization_with_dict(self): """Test initializing ClientConfiguration with headers""" @@ -3920,11 +3927,11 @@ def test_sync_client_configuration_headers_initialization_with_none(self): ) assert config.headers == {} - def test_sync_client_configuration_headers_setter(self, client_configuration): + def test_sync_client_configuration_headers_setter(self): """Test setting headers via property setter""" headers = {"X-Test": "test-value"} - client_configuration.headers = headers - assert client_configuration.headers == headers + self.configuration.headers = headers + assert self.configuration.headers == headers def test_sync_client_configuration_headers_with_authorization_model_id(self): """Test ClientConfiguration with headers and authorization_model_id""" @@ -4189,11 +4196,11 @@ def test_raw_request_post_with_body(self, mock_request): mock_request.assert_called_once_with( "POST", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom-endpoint", + query_params=[("page_size", "20")], headers=ANY, + post_params=None, body={"user": "user:bob", "action": "custom_action"}, - query_params=[("page_size", "20")], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) api_client.close() @@ -4204,7 +4211,9 @@ def test_raw_request_get_with_query_params(self, mock_request): Make a raw GET request with query parameters """ - response_body = '{"stores": [{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}]}' + response_body = ( + '{"stores": [{"id": "01YCP46JKYM8FJCQ37NMBYHE5X", "name": "store1"}]}' + ) mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration @@ -4229,11 +4238,14 @@ def test_raw_request_get_with_query_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores", + query_params=[ + ("page_size", "10"), + ("continuation_token", "eyJwayI6..."), + ], headers=ANY, + post_params=None, body=None, - query_params=[("page_size", "10"), ("continuation_token", "eyJwayI6...")], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) api_client.close() @@ -4265,11 +4277,11 @@ def test_raw_request_with_path_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", + query_params=None, headers=ANY, + post_params=None, body=None, - query_params=[], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) api_client.close() @@ -4299,11 +4311,11 @@ def test_raw_request_auto_store_id_substitution(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", + query_params=None, headers=ANY, + post_params=None, body=None, - query_params=[], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) api_client.close() @@ -4383,11 +4395,11 @@ def test_raw_request_with_list_query_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores", + query_params=[("ids", "id1"), ("ids", "id2"), ("ids", "id3")], headers=ANY, + post_params=None, body=None, - query_params=[("ids", "id1"), ("ids", "id2"), ("ids", "id3")], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) api_client.close() @@ -4444,11 +4456,11 @@ def test_raw_request_url_encoded_path_params(self, mock_request): mock_request.assert_called_once_with( "GET", "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/custom/value%20with%20spaces%20%26%20special%20chars", + query_params=None, headers=ANY, + post_params=None, body=None, - query_params=[], - post_params=[], - _preload_content=ANY, + _preload_content=True, _request_timeout=None, ) api_client.close() From b6b72aa80781912844c3eab68403224f08607f15 Mon Sep 17 00:00:00 2001 From: kcbiradar Date: Sun, 18 Jan 2026 14:02:58 +0530 Subject: [PATCH 4/6] addRawRequestMethod: update-changes-requested --- openfga_sdk/api_client.py | 2 +- openfga_sdk/client/client.py | 8 +++---- openfga_sdk/client/models/raw_response.py | 28 +++-------------------- openfga_sdk/sync/api_client.py | 2 +- openfga_sdk/sync/client/client.py | 8 +++---- 5 files changed, 13 insertions(+), 35 deletions(-) diff --git a/openfga_sdk/api_client.py b/openfga_sdk/api_client.py index 68cd5d2..d4cc632 100644 --- a/openfga_sdk/api_client.py +++ b/openfga_sdk/api_client.py @@ -420,7 +420,7 @@ async def __call_api( if _return_http_data_only: return return_data else: - return (return_data, response_data.status, response_data.getheaders()) + return (return_data, response_data.status, response_data.headers) def _parse_retry_after_header(self, headers) -> int: retry_after_header = headers.get("retry-after") diff --git a/openfga_sdk/client/client.py b/openfga_sdk/client/client.py index 9eba189..09da6f0 100644 --- a/openfga_sdk/client/client.py +++ b/openfga_sdk/client/client.py @@ -1216,7 +1216,7 @@ async def raw_request( TelemetryAttributes.fga_client_request_store_id ] = self.get_store_id() - _, http_status, http_headers = await self._api_client.call_api( + await self._api_client.call_api( resource_path=resource_path, method=method.upper(), query_params=query_params_list if query_params_list else None, @@ -1224,7 +1224,7 @@ async def raw_request( body=body_params, response_types_map={}, auth_settings=[], - _return_http_data_only=False, + _return_http_data_only=True, _preload_content=True, _retry_params=retry_params, _oauth2_client=self._api._oauth2_client, @@ -1262,7 +1262,7 @@ async def raw_request( response_body = rest_response.data return RawResponse( - status=http_status, - headers=http_headers if http_headers else {}, + status=rest_response.status, + headers=dict(rest_response.getheaders()), body=response_body, ) diff --git a/openfga_sdk/client/models/raw_response.py b/openfga_sdk/client/models/raw_response.py index e369088..043e2b4 100644 --- a/openfga_sdk/client/models/raw_response.py +++ b/openfga_sdk/client/models/raw_response.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Any +import json @dataclass @@ -34,34 +35,13 @@ def json(self) -> dict[str, Any] | None: """ Return the response body as a JSON dictionary. - If the body is already a dict (parsed JSON), returns it directly. - If the body is a string or bytes, attempts to parse it as JSON. - Returns None if body is None or cannot be parsed. + The body is already parsed during the request, so this typically + just returns the body if it's a dict, or None otherwise. :return: Parsed JSON dictionary or None """ - if self.body is None: - return None - if isinstance(self.body, dict): return self.body - - if isinstance(self.body, str): - import json - - try: - return json.loads(self.body) - except (json.JSONDecodeError, ValueError): - return None - - if isinstance(self.body, bytes): - import json - - try: - return json.loads(self.body.decode("utf-8")) - except (json.JSONDecodeError, ValueError, UnicodeDecodeError): - return None - return None def text(self) -> str | None: @@ -83,8 +63,6 @@ def text(self) -> str | None: return self.body.decode("utf-8", errors="replace") if isinstance(self.body, dict): - import json - return json.dumps(self.body) return str(self.body) diff --git a/openfga_sdk/sync/api_client.py b/openfga_sdk/sync/api_client.py index dfcdff9..7990e25 100644 --- a/openfga_sdk/sync/api_client.py +++ b/openfga_sdk/sync/api_client.py @@ -420,7 +420,7 @@ def __call_api( if _return_http_data_only: return return_data else: - return (return_data, response_data.status, response_data.getheaders()) + return (return_data, response_data.status, response_data.headers) def _parse_retry_after_header(self, headers) -> int: retry_after_header = headers.get("retry-after") diff --git a/openfga_sdk/sync/client/client.py b/openfga_sdk/sync/client/client.py index 18b0a56..ba6a26a 100644 --- a/openfga_sdk/sync/client/client.py +++ b/openfga_sdk/sync/client/client.py @@ -1215,7 +1215,7 @@ def raw_request( TelemetryAttributes.fga_client_request_store_id ] = self.get_store_id() - _, http_status, http_headers = self._api_client.call_api( + self._api_client.call_api( resource_path=resource_path, method=method.upper(), query_params=query_params_list if query_params_list else None, @@ -1223,7 +1223,7 @@ def raw_request( body=body_params, response_types_map={}, auth_settings=[], - _return_http_data_only=False, + _return_http_data_only=True, _preload_content=True, _retry_params=retry_params, _oauth2_client=self._api._oauth2_client, @@ -1262,7 +1262,7 @@ def raw_request( response_body = rest_response.data return RawResponse( - status=http_status, - headers=http_headers if http_headers else {}, + status=rest_response.status, + headers=dict(rest_response.getheaders()), body=response_body, ) From 6188d9e8ca3954f44973b5a1b379b30c7e5cde89 Mon Sep 17 00:00:00 2001 From: kcbiradar Date: Sun, 18 Jan 2026 14:29:00 +0530 Subject: [PATCH 5/6] updates --- openfga_sdk/client/models/raw_response.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openfga_sdk/client/models/raw_response.py b/openfga_sdk/client/models/raw_response.py index 043e2b4..889182b 100644 --- a/openfga_sdk/client/models/raw_response.py +++ b/openfga_sdk/client/models/raw_response.py @@ -5,9 +5,10 @@ made through the SDK's raw_request method. """ +import json + from dataclasses import dataclass from typing import Any -import json @dataclass From a34de525b07805d3d0ad35003f14ebb3492f4b6e Mon Sep 17 00:00:00 2001 From: kcbiradar Date: Mon, 19 Jan 2026 15:08:23 +0530 Subject: [PATCH 6/6] updated-requested-changes --- openfga_sdk/client/client.py | 9 ++++++--- openfga_sdk/sync/client/client.py | 9 ++++++--- openfga_sdk/telemetry/attributes.py | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/openfga_sdk/client/client.py b/openfga_sdk/client/client.py index 09da6f0..8a7183d 100644 --- a/openfga_sdk/client/client.py +++ b/openfga_sdk/client/client.py @@ -1142,7 +1142,8 @@ async def raw_request( request_headers = dict(headers) if headers else {} if options and options.get("headers"): - request_headers.update(options["headers"]) + if isinstance(options["headers"], dict): + request_headers.update(options["headers"]) if not operation_name: raise FgaValidationException("operation_name is required for raw_request") @@ -1235,10 +1236,12 @@ async def raw_request( ) if rest_response is None: + operation_suffix = ( + f" (operation: {operation_name})" if operation_name else "" + ) raise RuntimeError( f"Failed to get response from API client for {method.upper()} " - f"request to '{resource_path}'" - f"{f' (operation: {operation_name})' if operation_name else ''}. " + f"request to '{resource_path}'{operation_suffix}. " "This may indicate an internal SDK error, network problem, or client configuration issue." ) diff --git a/openfga_sdk/sync/client/client.py b/openfga_sdk/sync/client/client.py index ba6a26a..daac606 100644 --- a/openfga_sdk/sync/client/client.py +++ b/openfga_sdk/sync/client/client.py @@ -1140,7 +1140,8 @@ def raw_request( request_headers = dict(headers) if headers else {} if options and options.get("headers"): - request_headers.update(options["headers"]) + if isinstance(options["headers"], dict): + request_headers.update(options["headers"]) if not operation_name: raise FgaValidationException("operation_name is required for raw_request") @@ -1235,10 +1236,12 @@ def raw_request( ) if rest_response is None: + operation_suffix = ( + f" (operation: {operation_name})" if operation_name else "" + ) raise RuntimeError( f"Failed to get response from API client for {method.upper()} " - f"request to '{resource_path}'" - f"{f' (operation: {operation_name})' if operation_name else ''}. " + f"request to '{resource_path}'{operation_suffix}. " "This may indicate an internal SDK error, network problem, or client configuration issue." ) diff --git a/openfga_sdk/telemetry/attributes.py b/openfga_sdk/telemetry/attributes.py index e7b10dd..ddda43f 100644 --- a/openfga_sdk/telemetry/attributes.py +++ b/openfga_sdk/telemetry/attributes.py @@ -295,7 +295,7 @@ def fromResponse( response.status ) - if response.body is not None and isinstance(response.body, dict): + if response.body is not None: response_model_id = response.body.get( "openfga-authorization-model-id" ) or response.body.get("openfga_authorization_model_id")