Skip to content

Commit 33a3168

Browse files
feat: add execute_api_request method for calling arbitrary endpoints (#252)
* feat: add raw_request method * update: handling double-JSON-encodes * addRawRequestMethod: fix testcases & remove trailing spaces * addRawRequestMethod: update-changes-requested * updates * updated-requested-changes * update-suggested-changes * feat: refactor method to execute api method * fix: tetss and refactor * feat: example for existing endpoints * fix: lint * fix: cleanup * feat: execute api request api layer refactor * fix: changelog and ruff lint * feat: remove unused import * feat: address comments * fix: streaming stuff * fix: rename tests and example * feat: address comments * fix: refactor * fix: _retry_params * fix: lint --------- Co-authored-by: Anurag Bandyopadhyay <angbpy@gmail.com>
1 parent ba2fda2 commit 33a3168

File tree

14 files changed

+2809
-3256
lines changed

14 files changed

+2809
-3256
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## [Unreleased](https://github.com/openfga/python-sdk/compare/v0.9.9...HEAD)
44

5+
- feat: add `execute_api_request` and `execute_streamed_api_request` methods to `OpenFgaClient` and `OpenFgaApi` for making arbitrary HTTP requests to any OpenFGA API endpoint with full auth, retry, and telemetry support (#252) - thanks @kcbiradar
6+
7+
### Breaking Changes
8+
9+
- The `_return_http_data_only`, `_preload_content`, `_request_auth`, `async_req`, and `_request_timeout` kwargs have been removed from all `OpenFgaApi` and `SyncOpenFgaApi` endpoint methods. These were internal implementation details not intended for external use. `_return_http_data_only` is now hardcoded to `True`; all endpoint methods return the deserialized response object directly. Users relying on `_with_http_info` methods returning a `(data, status, headers)` tuple should use `execute_api_request` instead.
10+
511
### [0.9.9](https://github.com/openfga/python-sdk/compare/v0.9.8...v0.9.9) (2025-12-09)
612
- feat: improve error messaging (#245)
713

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ This is an autogenerated python SDK for OpenFGA. It provides a wrapper around th
4848
- [Read Assertions](#read-assertions)
4949
- [Write Assertions](#write-assertions)
5050
- [Retries](#retries)
51+
- [Calling Other Endpoints](#calling-other-endpoints)
5152
- [API Endpoints](#api-endpoints)
5253
- [Models](#models)
5354
- [OpenTelemetry](#opentelemetry)
@@ -1260,6 +1261,96 @@ body = [ClientAssertion(
12601261
response = await fga_client.write_assertions(body, options)
12611262
```
12621263

1264+
### Calling Other Endpoints
1265+
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).
1267+
1268+
For streaming endpoints (e.g. `streamed-list-objects`), use `execute_streamed_api_request` instead. It returns an `AsyncIterator` (or `Iterator` in the sync client) that yields one parsed JSON object per chunk.
1269+
1270+
This is useful when:
1271+
- You want to call a new endpoint that is not yet supported by the SDK
1272+
- You are using an earlier version of the SDK that doesn't yet support a particular endpoint
1273+
- You have a custom endpoint deployed that extends the OpenFGA API
1274+
1275+
#### Example: Calling a Custom Endpoint with POST
1276+
1277+
```python
1278+
# Call a custom endpoint using path parameters
1279+
response = await fga_client.execute_api_request(
1280+
operation_name="CustomEndpoint", # For telemetry/logging
1281+
method="POST",
1282+
path="/stores/{store_id}/custom-endpoint",
1283+
path_params={"store_id": FGA_STORE_ID},
1284+
body={
1285+
"user": "user:bob",
1286+
"action": "custom_action",
1287+
"resource": "resource:123",
1288+
},
1289+
query_params={
1290+
"page_size": 20,
1291+
},
1292+
)
1293+
1294+
# Access the response data
1295+
if response.status == 200:
1296+
result = response.json()
1297+
print(f"Response: {result}")
1298+
```
1299+
1300+
#### Example: Calling an existing endpoint with GET
1301+
1302+
```python
1303+
# Get a list of stores with query parameters
1304+
stores_response = await fga_client.execute_api_request(
1305+
operation_name="ListStores",
1306+
method="GET",
1307+
path="/stores",
1308+
query_params={
1309+
"page_size": 10,
1310+
"continuation_token": "eyJwayI6...",
1311+
},
1312+
)
1313+
1314+
stores = stores_response.json()
1315+
print("Stores:", stores)
1316+
```
1317+
1318+
#### Example: Calling a Streaming Endpoint
1319+
1320+
```python
1321+
# Stream objects visible to a user
1322+
async for chunk in fga_client.execute_streamed_api_request(
1323+
operation_name="StreamedListObjects",
1324+
method="POST",
1325+
path="/stores/{store_id}/streamed-list-objects",
1326+
path_params={"store_id": FGA_STORE_ID},
1327+
body={
1328+
"type": "document",
1329+
"relation": "viewer",
1330+
"user": "user:anne",
1331+
"authorization_model_id": FGA_MODEL_ID,
1332+
},
1333+
):
1334+
# Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}}
1335+
if "result" in chunk:
1336+
print(chunk["result"]["object"]) # e.g. "document:roadmap"
1337+
```
1338+
1339+
#### Example: Using Path Parameters
1340+
1341+
Path parameters are specified in the path using `{param_name}` syntax and must all be provided explicitly via `path_params` (URL-encoded automatically):
1342+
1343+
```python
1344+
response = await fga_client.execute_api_request(
1345+
operation_name="GetAuthorizationModel",
1346+
method="GET",
1347+
path="/stores/{store_id}/authorization-models/{model_id}",
1348+
path_params={
1349+
"store_id": "your-store-id",
1350+
"model_id": "your-model-id",
1351+
},
1352+
)
1353+
```
12631354

12641355
### Retries
12651356

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# ruff: noqa: E402
2+
3+
"""
4+
execute_api_request example
5+
6+
Requires a running OpenFGA server (default: http://localhost:8080).
7+
export FGA_API_URL=http://localhost:8080 # optional, this is the default
8+
python3 execute_api_request_example.py
9+
"""
10+
11+
import asyncio
12+
import os
13+
import sys
14+
15+
16+
sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", ".."))
17+
sys.path.insert(0, sdk_path)
18+
19+
from openfga_sdk import (
20+
ClientConfiguration,
21+
CreateStoreRequest,
22+
Metadata,
23+
ObjectRelation,
24+
OpenFgaClient,
25+
RelationMetadata,
26+
RelationReference,
27+
TypeDefinition,
28+
Userset,
29+
Usersets,
30+
WriteAuthorizationModelRequest,
31+
)
32+
from openfga_sdk.client.models import (
33+
ClientCheckRequest,
34+
ClientTuple,
35+
ClientWriteRequest,
36+
)
37+
from openfga_sdk.credentials import Credentials
38+
39+
40+
async def main():
41+
api_url = os.getenv("FGA_API_URL", "http://localhost:8080")
42+
43+
configuration = ClientConfiguration(
44+
api_url=api_url,
45+
credentials=Credentials(),
46+
)
47+
48+
async with OpenFgaClient(configuration) as fga_client:
49+
print("=== Setup ===")
50+
51+
store = await fga_client.create_store(
52+
CreateStoreRequest(name="execute_api_request_test")
53+
)
54+
fga_client.set_store_id(store.id)
55+
print(f"Created store: {store.id}")
56+
57+
model_resp = await fga_client.write_authorization_model(
58+
WriteAuthorizationModelRequest(
59+
schema_version="1.1",
60+
type_definitions=[
61+
TypeDefinition(type="user"),
62+
TypeDefinition(
63+
type="document",
64+
relations=dict(
65+
writer=Userset(this=dict()),
66+
viewer=Userset(
67+
union=Usersets(
68+
child=[
69+
Userset(this=dict()),
70+
Userset(
71+
computed_userset=ObjectRelation(
72+
object="", relation="writer"
73+
)
74+
),
75+
]
76+
)
77+
),
78+
),
79+
metadata=Metadata(
80+
relations=dict(
81+
writer=RelationMetadata(
82+
directly_related_user_types=[
83+
RelationReference(type="user"),
84+
]
85+
),
86+
viewer=RelationMetadata(
87+
directly_related_user_types=[
88+
RelationReference(type="user"),
89+
]
90+
),
91+
)
92+
),
93+
),
94+
],
95+
)
96+
)
97+
auth_model_id = model_resp.authorization_model_id
98+
fga_client.set_authorization_model_id(auth_model_id)
99+
print(f"Created model: {auth_model_id}")
100+
101+
await fga_client.write(
102+
ClientWriteRequest(
103+
writes=[
104+
ClientTuple(
105+
user="user:anne",
106+
relation="writer",
107+
object="document:roadmap",
108+
),
109+
]
110+
)
111+
)
112+
print("Wrote tuple: user:anne writer document:roadmap")
113+
114+
print("\n=== execute_api_request ===\n")
115+
116+
# 1. ListStores
117+
print("1. ListStores (GET /stores)")
118+
raw = await fga_client.execute_api_request(
119+
operation_name="ListStores",
120+
method="GET",
121+
path="/stores",
122+
query_params={"page_size": 100},
123+
)
124+
sdk = await fga_client.list_stores()
125+
body = raw.json()
126+
assert raw.status == 200, f"Expected 200, got {raw.status}"
127+
assert "stores" in body
128+
assert len(body["stores"]) == len(sdk.stores)
129+
print(f" {len(body['stores'])} stores (status {raw.status})")
130+
131+
# 2. GetStore
132+
print("2. GetStore (GET /stores/{{store_id}})")
133+
raw = await fga_client.execute_api_request(
134+
operation_name="GetStore",
135+
method="GET",
136+
path="/stores/{store_id}",
137+
path_params={"store_id": store.id},
138+
)
139+
sdk = await fga_client.get_store()
140+
body = raw.json()
141+
assert raw.status == 200
142+
assert body["id"] == sdk.id
143+
assert body["name"] == sdk.name
144+
print(f" id={body['id']}, name={body['name']}")
145+
146+
# 3. ReadAuthorizationModels
147+
print(
148+
"3. ReadAuthorizationModels (GET /stores/{{store_id}}/authorization-models)"
149+
)
150+
raw = await fga_client.execute_api_request(
151+
operation_name="ReadAuthorizationModels",
152+
method="GET",
153+
path="/stores/{store_id}/authorization-models",
154+
path_params={"store_id": store.id},
155+
)
156+
sdk = await fga_client.read_authorization_models()
157+
body = raw.json()
158+
assert raw.status == 200
159+
assert len(body["authorization_models"]) == len(sdk.authorization_models)
160+
print(f" {len(body['authorization_models'])} models")
161+
162+
# 4. Check
163+
print("4. Check (POST /stores/{{store_id}}/check)")
164+
raw = await fga_client.execute_api_request(
165+
operation_name="Check",
166+
method="POST",
167+
path="/stores/{store_id}/check",
168+
path_params={"store_id": store.id},
169+
body={
170+
"tuple_key": {
171+
"user": "user:anne",
172+
"relation": "viewer",
173+
"object": "document:roadmap",
174+
},
175+
"authorization_model_id": auth_model_id,
176+
},
177+
)
178+
sdk = await fga_client.check(
179+
ClientCheckRequest(
180+
user="user:anne",
181+
relation="viewer",
182+
object="document:roadmap",
183+
)
184+
)
185+
body = raw.json()
186+
assert raw.status == 200
187+
assert body["allowed"] == sdk.allowed
188+
print(f" allowed={body['allowed']}")
189+
190+
# 5. Read
191+
print("5. Read (POST /stores/{{store_id}}/read)")
192+
raw = await fga_client.execute_api_request(
193+
operation_name="Read",
194+
method="POST",
195+
path="/stores/{store_id}/read",
196+
path_params={"store_id": store.id},
197+
body={
198+
"tuple_key": {
199+
"user": "user:anne",
200+
"object": "document:",
201+
},
202+
},
203+
)
204+
body = raw.json()
205+
assert raw.status == 200
206+
assert "tuples" in body
207+
assert len(body["tuples"]) >= 1
208+
print(f" {len(body['tuples'])} tuples returned")
209+
210+
# 6. CreateStore
211+
print("6. CreateStore (POST /stores)")
212+
raw = await fga_client.execute_api_request(
213+
operation_name="CreateStore",
214+
method="POST",
215+
path="/stores",
216+
body={"name": "executor_test_store"},
217+
)
218+
body = raw.json()
219+
assert raw.status == 201, f"Expected 201, got {raw.status}"
220+
assert "id" in body
221+
new_store_id = body["id"]
222+
print(f" created store: {new_store_id}")
223+
224+
# 7. DeleteStore
225+
print("7. DeleteStore (DELETE /stores/{{store_id}})")
226+
raw = await fga_client.execute_api_request(
227+
operation_name="DeleteStore",
228+
method="DELETE",
229+
path="/stores/{store_id}",
230+
path_params={"store_id": new_store_id},
231+
)
232+
assert raw.status == 204, f"Expected 204, got {raw.status}"
233+
print(f" deleted store: {new_store_id} (status 204)")
234+
235+
# 8. Custom headers
236+
print("8. Custom headers (GET /stores/{{store_id}})")
237+
raw = await fga_client.execute_api_request(
238+
operation_name="GetStoreWithHeaders",
239+
method="GET",
240+
path="/stores/{store_id}",
241+
path_params={"store_id": store.id},
242+
headers={"X-Custom-Header": "test-value"},
243+
)
244+
assert raw.status == 200
245+
print(f" custom headers accepted (status {raw.status})")
246+
247+
# 9. StreamedListObjects
248+
print(
249+
"9. StreamedListObjects (POST /stores/{{store_id}}/streamed-list-objects)"
250+
)
251+
chunks = []
252+
async for chunk in fga_client.execute_streamed_api_request(
253+
operation_name="StreamedListObjects",
254+
method="POST",
255+
path="/stores/{store_id}/streamed-list-objects",
256+
path_params={"store_id": store.id},
257+
body={
258+
"type": "document",
259+
"relation": "viewer",
260+
"user": "user:anne",
261+
"authorization_model_id": auth_model_id,
262+
},
263+
):
264+
chunks.append(chunk)
265+
assert len(chunks) >= 1, f"Expected at least 1 chunk, got {len(chunks)}"
266+
objects = [c["result"]["object"] for c in chunks if "result" in c]
267+
assert "document:roadmap" in objects, f"Expected document:roadmap in {objects}"
268+
print(f" {len(chunks)} chunks, objects={objects}")
269+
270+
# Cleanup
271+
print("\n=== Cleanup ===")
272+
await fga_client.delete_store()
273+
print(f"Deleted test store: {store.id}")
274+
275+
print("\nAll examples completed successfully.\n")
276+
277+
278+
asyncio.run(main())

0 commit comments

Comments
 (0)