Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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

- 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

### Breaking Changes

- 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.

### [0.9.9](https://github.com/openfga/python-sdk/compare/v0.9.8...v0.9.9) (2025-12-09)
- feat: improve error messaging (#245)

Expand Down
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1260,6 +1261,96 @@ 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 `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).

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.

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

#### Example: Calling a Custom Endpoint with POST

```python
# Call a custom endpoint using path parameters
response = await fga_client.execute_api_request(
operation_name="CustomEndpoint", # For telemetry/logging
method="POST",
path="/stores/{store_id}/custom-endpoint",
path_params={"store_id": FGA_STORE_ID},
body={
"user": "user:bob",
"action": "custom_action",
"resource": "resource:123",
},
query_params={
"page_size": 20,
},
)

# Access the response data
if response.status == 200:
result = response.json()
print(f"Response: {result}")
```

#### Example: Calling an existing endpoint with GET

```python
# Get a list of stores with query parameters
stores_response = await fga_client.execute_api_request(
operation_name="ListStores",
method="GET",
path="/stores",
query_params={
"page_size": 10,
"continuation_token": "eyJwayI6...",
},
)

stores = stores_response.json()
print("Stores:", stores)
```

#### Example: Calling a Streaming Endpoint

```python
# Stream objects visible to a user
async for chunk in fga_client.execute_streamed_api_request(
operation_name="StreamedListObjects",
method="POST",
path="/stores/{store_id}/streamed-list-objects",
path_params={"store_id": FGA_STORE_ID},
body={
"type": "document",
"relation": "viewer",
"user": "user:anne",
"authorization_model_id": FGA_MODEL_ID,
},
):
# Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}}
if "result" in chunk:
print(chunk["result"]["object"]) # e.g. "document:roadmap"
```

#### Example: Using Path Parameters

Path parameters are specified in the path using `{param_name}` syntax and must all be provided explicitly via `path_params` (URL-encoded automatically):

```python
response = await fga_client.execute_api_request(
operation_name="GetAuthorizationModel",
method="GET",
path="/stores/{store_id}/authorization-models/{model_id}",
path_params={
"store_id": "your-store-id",
"model_id": "your-model-id",
},
)
```

### Retries

Expand Down
273 changes: 273 additions & 0 deletions example/execute-api-request/execute_api_request_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
# ruff: noqa: E402

"""
execute_api_request example — calls real OpenFGA endpoints and compares
the results with the regular SDK methods to verify correctness.

Requires a running OpenFGA server (default: http://localhost:8080).
export FGA_API_URL=http://localhost:8080 # optional, this is the default
python3 execute_api_request_example.py
"""

import asyncio
import os
import sys


sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", ".."))
sys.path.insert(0, sdk_path)

from openfga_sdk import (
ClientConfiguration,
CreateStoreRequest,
Metadata,
ObjectRelation,
OpenFgaClient,
RelationMetadata,
RelationReference,
TypeDefinition,
Userset,
Usersets,
WriteAuthorizationModelRequest,
)
from openfga_sdk.client.models import (
ClientCheckRequest,
ClientTuple,
ClientWriteRequest,
)
from openfga_sdk.credentials import Credentials


async def main():
api_url = os.getenv("FGA_API_URL", "http://localhost:8080")

configuration = ClientConfiguration(
api_url=api_url,
credentials=Credentials(),
)

async with OpenFgaClient(configuration) as fga_client:
print("=== Setup ===")

# Create a test store via the SDK
store = await fga_client.create_store(
CreateStoreRequest(name="execute_api_request_test")
)
fga_client.set_store_id(store.id)
print(f"Created store: {store.id}")

# Write an authorization model
model_resp = await fga_client.write_authorization_model(
WriteAuthorizationModelRequest(
schema_version="1.1",
type_definitions=[
TypeDefinition(type="user"),
TypeDefinition(
type="document",
relations=dict(
writer=Userset(this=dict()),
viewer=Userset(
union=Usersets(
child=[
Userset(this=dict()),
Userset(
computed_userset=ObjectRelation(
object="", relation="writer"
)
),
]
)
),
),
metadata=Metadata(
relations=dict(
writer=RelationMetadata(
directly_related_user_types=[
RelationReference(type="user"),
]
),
viewer=RelationMetadata(
directly_related_user_types=[
RelationReference(type="user"),
]
),
)
),
),
],
)
)
auth_model_id = model_resp.authorization_model_id
fga_client.set_authorization_model_id(auth_model_id)
print(f"Created model: {auth_model_id}")

# Write a tuple
await fga_client.write(
ClientWriteRequest(
writes=[
ClientTuple(
user="user:anne",
relation="writer",
object="document:roadmap",
),
]
)
)
print("Wrote tuple: user:anne → writer → document:roadmap")

print("\n=== execute_api_request ===\n")

print("1. ListStores (GET /stores)")
raw = await fga_client.execute_api_request(
operation_name="ListStores",
method="GET",
path="/stores",
query_params={"page_size": 100},
)
sdk = await fga_client.list_stores()
body = raw.json()
assert raw.status == 200, f"Expected 200, got {raw.status}"
assert "stores" in body
assert len(body["stores"]) == len(sdk.stores), (
f"Count mismatch: {len(body['stores'])} vs {len(sdk.stores)}"
)
print(f" ✅ {len(body['stores'])} stores (status {raw.status})")

print("2. GetStore (GET /stores/{store_id})")
raw = await fga_client.execute_api_request(
operation_name="GetStore",
method="GET",
path="/stores/{store_id}",
path_params={"store_id": store.id},
)
sdk = await fga_client.get_store()
body = raw.json()
assert raw.status == 200
assert body["id"] == sdk.id
assert body["name"] == sdk.name
print(f" ✅ id={body['id']}, name={body['name']}")

print(
"3. ReadAuthorizationModels (GET /stores/{store_id}/authorization-models)"
)
raw = await fga_client.execute_api_request(
operation_name="ReadAuthorizationModels",
method="GET",
path="/stores/{store_id}/authorization-models",
path_params={"store_id": store.id},
)
sdk = await fga_client.read_authorization_models()
body = raw.json()
assert raw.status == 200
assert len(body["authorization_models"]) == len(sdk.authorization_models)
print(f" ✅ {len(body['authorization_models'])} models")

print("4. Check (POST /stores/{store_id}/check)")
raw = await fga_client.execute_api_request(
operation_name="Check",
method="POST",
path="/stores/{store_id}/check",
path_params={"store_id": store.id},
body={
"tuple_key": {
"user": "user:anne",
"relation": "viewer",
"object": "document:roadmap",
},
"authorization_model_id": auth_model_id,
},
)
sdk = await fga_client.check(
ClientCheckRequest(
user="user:anne",
relation="viewer",
object="document:roadmap",
)
)
body = raw.json()
assert raw.status == 200
assert body["allowed"] == sdk.allowed
print(f" ✅ allowed={body['allowed']}")

print("5. Read (POST /stores/{store_id}/read)")
raw = await fga_client.execute_api_request(
operation_name="Read",
method="POST",
path="/stores/{store_id}/read",
path_params={"store_id": store.id},
body={
"tuple_key": {
"user": "user:anne",
"object": "document:",
},
},
)
body = raw.json()
assert raw.status == 200
assert "tuples" in body
assert len(body["tuples"]) >= 1
print(f" ✅ {len(body['tuples'])} tuples returned")

print("6. CreateStore (POST /stores)")
raw = await fga_client.execute_api_request(
operation_name="CreateStore",
method="POST",
path="/stores",
body={"name": "raw_request_test_store"},
)
body = raw.json()
assert raw.status == 201, f"Expected 201, got {raw.status}"
assert "id" in body
new_store_id = body["id"]
print(f" ✅ created store: {new_store_id}")

print("7. DeleteStore (DELETE /stores/{store_id})")
raw = await fga_client.execute_api_request(
operation_name="DeleteStore",
method="DELETE",
path="/stores/{store_id}",
path_params={"store_id": new_store_id},
)
assert raw.status == 204, f"Expected 204, got {raw.status}"
print(f" ✅ deleted store: {new_store_id} (status 204 No Content)")

print("8. Custom headers (GET /stores/{store_id})")
raw = await fga_client.execute_api_request(
operation_name="GetStoreWithHeaders",
method="GET",
path="/stores/{store_id}",
path_params={"store_id": store.id},
headers={"X-Custom-Header": "test-value"},
)
assert raw.status == 200
print(f" ✅ custom headers accepted (status {raw.status})")

print("9. StreamedListObjects (POST /stores/{store_id}/streamed-list-objects)")
chunks = []
async for chunk in fga_client.execute_streamed_api_request(
operation_name="StreamedListObjects",
method="POST",
path="/stores/{store_id}/streamed-list-objects",
path_params={"store_id": store.id},
body={
"type": "document",
"relation": "viewer",
"user": "user:anne",
"authorization_model_id": auth_model_id,
},
):
chunks.append(chunk)
assert len(chunks) >= 1, f"Expected at least 1 chunk, got {len(chunks)}"
# Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}}
objects = [c["result"]["object"] for c in chunks if "result" in c]
assert "document:roadmap" in objects, f"Expected document:roadmap in {objects}"
print(f" ✅ {len(chunks)} chunks, objects={objects}")

print("\n=== Cleanup ===")
await fga_client.delete_store()
print(f"Deleted test store: {store.id}")

print("\nAll execute_api_request examples completed successfully.\n")


asyncio.run(main())
Loading
Loading