Skip to content

Commit 4569a6a

Browse files
committed
fix: cleanup
1 parent 51b92ae commit 4569a6a

File tree

6 files changed

+50
-174
lines changed

6 files changed

+50
-174
lines changed

README.md

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,7 +1263,7 @@ response = await fga_client.write_assertions(body, options)
12631263

12641264
### Calling Other Endpoints
12651265

1266-
In certain cases you may want to call other APIs not yet wrapped by the SDK. You can do so by using the `execute_api_request` method available on the `OpenFgaClient`. The `execute_api_request` method allows you to make raw HTTP calls to any OpenFGA endpoint by specifying the HTTP method, path, body, query parameters, and path parameters, while still honoring the client configuration (authentication, telemetry, retries, and error handling).
1266+
In certain cases you may want to call other APIs not yet wrapped by the SDK. You can do so by using the `execute_api_request` method available on the `OpenFgaClient`. It allows you to make raw HTTP calls to any OpenFGA endpoint by specifying the HTTP method, path, body, query parameters, and path parameters, while still honoring the client configuration (authentication, telemetry, retries, and error handling).
12671267

12681268
For streaming endpoints, use `execute_streamed_api_request` instead.
12691269

@@ -1272,35 +1272,7 @@ This is useful when:
12721272
- You are using an earlier version of the SDK that doesn't yet support a particular endpoint
12731273
- You have a custom endpoint deployed that extends the OpenFGA API
12741274

1275-
In all cases, you initialize the SDK the same way as usual, and then call `execute_api_request` on the `fga_client` instance.
1276-
1277-
```python
1278-
from openfga_sdk import ClientConfiguration, OpenFgaClient
1279-
1280-
configuration = ClientConfiguration(
1281-
api_url=FGA_API_URL,
1282-
store_id=FGA_STORE_ID,
1283-
)
1284-
1285-
async with OpenFgaClient(configuration) as fga_client:
1286-
request_body = {
1287-
"user": "user:bob",
1288-
"action": "custom_action",
1289-
"resource": "resource:123",
1290-
}
1291-
1292-
response = await fga_client.execute_api_request(
1293-
operation_name="CustomEndpoint",
1294-
method="POST",
1295-
path="/stores/{store_id}/custom-endpoint",
1296-
path_params={"store_id": FGA_STORE_ID},
1297-
query_params={"page_size": "20"},
1298-
body=request_body,
1299-
headers={"X-Experimental-Feature": "enabled"},
1300-
)
1301-
```
1302-
1303-
#### Example: Calling a custom endpoint with POST
1275+
#### Example: Calling a Custom Endpoint with POST
13041276

13051277
```python
13061278
# Call a custom endpoint using path parameters

openfga_sdk/client/client.py

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,28 +1126,19 @@ async def execute_api_request(
11261126
"""
11271127
Execute an arbitrary HTTP request to any OpenFGA API endpoint.
11281128
1129-
Useful when you need to call a new or experimental API that doesn't
1130-
yet have a built-in method in the SDK. You still get the benefits of
1131-
the SDK: authentication, configuration, retries, and error handling.
1132-
1133-
:param operation_name: Required. Operation name for telemetry/logging
1134-
(e.g., "CustomCheck", "CustomEndpoint")
1135-
:param method: Required. HTTP method (GET, POST, PUT, DELETE, PATCH)
1136-
:param path: Required. API path with optional template parameters
1137-
(e.g., "/stores/{store_id}/my-endpoint")
1138-
:param path_params: Path parameters to replace template variables.
1139-
If {store_id} is in the path and not provided here, it will be
1140-
automatically substituted from the client configuration.
1141-
:param body: Request body for POST/PUT/PATCH requests
1129+
Useful for calling endpoints not yet wrapped by the SDK while
1130+
still getting authentication, retries, and error handling.
1131+
1132+
:param operation_name: Operation name for telemetry (e.g., "CustomCheck")
1133+
:param method: HTTP method (GET, POST, PUT, DELETE, PATCH)
1134+
:param path: API path, e.g. "/stores/{store_id}/my-endpoint".
1135+
{store_id} is auto-substituted from config if not in path_params.
1136+
:param path_params: Path parameter substitutions (URL-encoded automatically)
1137+
:param body: Request body for POST/PUT/PATCH
11421138
:param query_params: Query string parameters
1143-
:param headers: Custom request headers. SDK always enforces
1144-
Content-Type and Accept as application/json.
1145-
:param options: Additional request options:
1146-
- headers: Extra headers (merged; options headers take precedence)
1147-
- retry_params: Override retry parameters for this request
1139+
:param headers: Custom headers (SDK enforces Content-Type and Accept)
1140+
:param options: Extra options (headers, retry_params)
11481141
:return: RawResponse with status, headers, and body
1149-
:raises FgaValidationException: If required parameters are missing
1150-
:raises ApiException: For HTTP errors
11511142
"""
11521143
return await self._execute_api_request_internal(
11531144
operation_name=operation_name,
@@ -1204,8 +1195,7 @@ async def _execute_api_request_internal(
12041195
options: dict[str, int | str | dict[str, int | str]] | None = None,
12051196
streaming: bool = False,
12061197
) -> RawResponse:
1207-
"""Internal implementation for execute_api_request and execute_streamed_api_request."""
1208-
# 1. Validate and build request
1198+
"""Shared implementation for execute_api_request and execute_streamed_api_request."""
12091199
builder = ExecuteApiRequestBuilder(
12101200
operation_name=operation_name,
12111201
method=method,
@@ -1217,7 +1207,6 @@ async def _execute_api_request_internal(
12171207
)
12181208
builder.validate()
12191209

1220-
# 2. Build path, query params, and headers
12211210
resource_path = builder.build_path(self.get_store_id())
12221211
query_params_list = builder.build_query_params_list()
12231212

@@ -1226,7 +1215,6 @@ async def _execute_api_request_internal(
12261215
options_headers = options["headers"]
12271216
final_headers = builder.build_headers(options_headers)
12281217

1229-
# 3. Apply authentication
12301218
auth_headers = dict(final_headers)
12311219
await self._api_client.update_params_for_auth(
12321220
auth_headers,
@@ -1235,7 +1223,6 @@ async def _execute_api_request_internal(
12351223
oauth2_client=self._api._oauth2_client,
12361224
)
12371225

1238-
# 4. Build telemetry attributes
12391226
telemetry_attributes = {
12401227
TelemetryAttributes.fga_client_request_method: operation_name.lower(),
12411228
}
@@ -1244,12 +1231,10 @@ async def _execute_api_request_internal(
12441231
self.get_store_id()
12451232
)
12461233

1247-
# 5. Extract retry params
12481234
retry_params = None
12491235
if options and options.get("retry_params"):
12501236
retry_params = options["retry_params"]
12511237

1252-
# 6. Make API request
12531238
await self._api_client.call_api(
12541239
resource_path=resource_path,
12551240
method=method.upper(),
@@ -1266,17 +1251,13 @@ async def _execute_api_request_internal(
12661251
_streaming=streaming,
12671252
)
12681253

1269-
# 7. Parse response
12701254
rest_response: RESTResponse | None = getattr(
12711255
self._api_client, "last_response", None
12721256
)
1273-
12741257
if rest_response is None:
12751258
raise RuntimeError(
1276-
f"Failed to get response from API client for {method.upper()} "
1277-
f"request to '{resource_path}' (operation: {operation_name}). "
1278-
"This may indicate an internal SDK error, network problem, "
1279-
"or client configuration issue."
1259+
f"No response for {method.upper()} {resource_path} "
1260+
f"(operation: {operation_name})"
12801261
)
12811262

12821263
return RawResponse(

openfga_sdk/client/execute_api_request_builder.py

Lines changed: 10 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
"""
2-
Builders and utilities for execute_api_request functionality.
3-
4-
This module provides reusable utilities for request building, header management,
5-
and response parsing to reduce code duplication across sync and async clients.
6-
"""
1+
"""Utilities for execute_api_request: request building, header management, response parsing."""
72

83
import json
94
import re
@@ -15,12 +10,7 @@
1510

1611

1712
class ExecuteApiRequestBuilder:
18-
"""
19-
Builder for API request execution.
20-
21-
Encapsulates request validation, parameter building, and path construction
22-
to eliminate duplication and improve maintainability.
23-
"""
13+
"""Builds and validates parameters for execute_api_request calls."""
2414

2515
def __init__(
2616
self,
@@ -43,11 +33,7 @@ def __init__(
4333
self.headers = headers
4434

4535
def validate(self) -> None:
46-
"""
47-
Validate all required parameters are present.
48-
49-
:raises FgaValidationException: If any required parameter is missing
50-
"""
36+
"""Validate that all required parameters are present."""
5137
if not self.operation_name:
5238
raise FgaValidationException(
5339
"operation_name is required for execute_api_request"
@@ -59,14 +45,9 @@ def validate(self) -> None:
5945

6046
def build_path(self, configured_store_id: str | None = None) -> str:
6147
"""
62-
Build and validate final resource path with parameter substitution.
63-
64-
Automatically substitutes {store_id} with configured store_id if not
65-
explicitly provided in path_params.
48+
Build resource path with parameter substitution.
6649
67-
:param configured_store_id: Store ID from client configuration
68-
:return: Final resource path with all parameters substituted
69-
:raises FgaValidationException: If path construction fails
50+
Auto-substitutes {store_id} from client config if not in path_params.
7051
"""
7152
path = self.path
7253
params = dict(self.path_params) if self.path_params else {}
@@ -99,16 +80,7 @@ def build_path(self, configured_store_id: str | None = None) -> str:
9980
return result
10081

10182
def build_query_params_list(self) -> list[tuple[str, str]]:
102-
"""
103-
Convert query_params dict to list of tuples for the API client.
104-
105-
Handles:
106-
- List values (expanded to multiple tuples with same key)
107-
- None values (filtered out)
108-
- Type conversion to string
109-
110-
:return: List of (key, value) tuples for query parameters
111-
"""
83+
"""Convert query_params dict to list of (key, value) tuples. Expands lists, filters None."""
11284
if not self.query_params:
11385
return []
11486

@@ -127,15 +99,9 @@ def build_headers(
12799
options_headers: dict[str, str] | None = None,
128100
) -> dict[str, str]:
129101
"""
130-
Build final headers with proper precedence and SDK enforcement.
102+
Merge request headers, options headers, and SDK-enforced defaults.
131103
132-
Header precedence (highest to lowest):
133-
1. SDK-enforced headers (Content-Type, Accept — always application/json)
134-
2. Options headers (from method options parameter)
135-
3. Request headers (from headers parameter)
136-
137-
:param options_headers: Headers from method options
138-
:return: Final merged headers with SDK enforcement
104+
SDK always enforces Content-Type and Accept as application/json.
139105
"""
140106
result = dict(self.headers) if self.headers else {}
141107
if options_headers:
@@ -147,24 +113,13 @@ def build_headers(
147113

148114

149115
class ResponseParser:
150-
"""Parse raw REST responses to appropriate Python types."""
116+
"""Parse raw REST responses into Python types."""
151117

152118
@staticmethod
153119
def parse_body(
154120
data: bytes | str | dict[str, Any] | None,
155121
) -> bytes | str | dict[str, Any] | None:
156-
"""
157-
Parse response body, attempting JSON deserialization.
158-
159-
Handles:
160-
- None values (returned as-is)
161-
- Dict objects (returned as-is)
162-
- String values (attempts JSON parsing, falls back to string)
163-
- Bytes values (attempts UTF-8 decode + JSON, falls back gracefully)
164-
165-
:param data: Raw response data from REST client
166-
:return: Parsed response (dict if JSON, else original type)
167-
"""
122+
"""Parse response data, attempting JSON deserialization."""
168123
if data is None:
169124
return None
170125

openfga_sdk/client/models/raw_response.py

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
"""
2-
Raw response wrapper for raw_request method.
3-
4-
This module provides a simple response wrapper for raw HTTP requests
5-
made through the SDK's raw_request method.
6-
"""
1+
"""Response wrapper for execute_api_request."""
72

83
import json
94

@@ -14,32 +9,26 @@
149
@dataclass
1510
class RawResponse:
1611
"""
17-
Response wrapper for raw HTTP requests.
18-
19-
This class provides a simple interface to access the response
20-
from a raw_request call, including status code, headers, and body.
12+
Response from execute_api_request / execute_streamed_api_request.
2113
22-
The body is automatically parsed as JSON if possible, otherwise
23-
it's returned as a string or bytes.
14+
The body is automatically parsed as JSON if possible,
15+
otherwise returned as a string or bytes.
2416
"""
2517

2618
status: int
2719
"""HTTP status code"""
2820

2921
headers: dict[str, str]
30-
"""Response headers as a dictionary"""
22+
"""Response headers"""
3123

3224
body: bytes | str | dict[str, Any] | None = None
33-
"""Response body (already parsed as dict if JSON, otherwise str or bytes)"""
25+
"""Response body (dict if JSON, otherwise str or bytes)"""
3426

3527
def json(self) -> dict[str, Any] | None:
3628
"""
37-
Return the response body as a JSON dictionary.
38-
39-
The body is already parsed during the request, so this typically
40-
just returns the body if it's a dict, or None otherwise.
29+
Return the response body as a parsed JSON dictionary.
4130
42-
:return: Parsed JSON dictionary or None
31+
:return: Parsed dict or None
4332
"""
4433
if isinstance(self.body, dict):
4534
return self.body
@@ -49,7 +38,7 @@ def text(self) -> str | None:
4938
"""
5039
Return the response body as a string.
5140
52-
:return: Response body as string or None
41+
:return: Body as string, or None
5342
"""
5443
if self.body is None:
5544
return None

0 commit comments

Comments
 (0)