Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f4dec14
feat: add raw_request method
kcbiradar Jan 15, 2026
3c2e66a
update: handling double-JSON-encodes
kcbiradar Jan 15, 2026
3f85dfa
addRawRequestMethod: fix testcases & remove trailing spaces
kcbiradar Jan 17, 2026
e9c47d9
Merge branch 'main' into feat/add-raw-request-method
SoulPancake Jan 18, 2026
b6b72aa
addRawRequestMethod: update-changes-requested
kcbiradar Jan 18, 2026
6188d9e
updates
kcbiradar Jan 18, 2026
a34de52
updated-requested-changes
kcbiradar Jan 19, 2026
5cc73ea
update-suggested-changes
kcbiradar Jan 20, 2026
3eacc7b
Merge branch 'main' into feat/add-raw-request-method
SoulPancake Jan 28, 2026
07e354d
Merge branch 'main' into feat/add-raw-request-method
SoulPancake Mar 6, 2026
256d6e1
feat: refactor method to execute api method
SoulPancake Mar 6, 2026
1e71e79
fix: tetss and refactor
SoulPancake Mar 6, 2026
1ba7b3d
feat: example for existing endpoints
SoulPancake Mar 6, 2026
51b92ae
fix: lint
SoulPancake Mar 6, 2026
4569a6a
fix: cleanup
SoulPancake Mar 6, 2026
b79b235
feat: execute api request api layer refactor
SoulPancake Mar 13, 2026
9946cf8
fix: changelog and ruff lint
SoulPancake Mar 13, 2026
826f1d9
feat: remove unused import
SoulPancake Mar 16, 2026
88b86fb
Merge branch 'main' into feat/add-raw-request-method
SoulPancake Mar 17, 2026
0535b1c
feat: address comments
SoulPancake Mar 18, 2026
785af00
fix: streaming stuff
SoulPancake Mar 18, 2026
7f5b287
fix: rename tests and example
SoulPancake Mar 23, 2026
1450cff
feat: address comments
SoulPancake Mar 23, 2026
1d03009
fix: refactor
SoulPancake Mar 23, 2026
294c69c
fix: _retry_params
SoulPancake Mar 23, 2026
f5d93a3
fix: lint
SoulPancake Mar 23, 2026
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
81 changes: 81 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,86 @@ 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, use `execute_streamed_api_request` instead.

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: 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.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",
},
)

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

### Retries

Expand Down
271 changes: 271 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,271 @@
# 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:
# ─── Setup: create a store, model, and tuple ─────────────
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")

# ─── Tests ────────────────────────────────────────────────
print("\n=== execute_api_request tests ===\n")

# ── 1. GET /stores ────────────────────────────────────────
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})")

# ── 2. GET /stores/{store_id} (auto-substitution) ────────
print("2. GetStore (GET /stores/{store_id})")
raw = await fga_client.execute_api_request(
operation_name="GetStore",
method="GET",
path="/stores/{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']}")

# ── 3. GET /stores/{store_id}/authorization-models ────────
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",
)
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")

# ── 4. POST /stores/{store_id}/check ──────────────────────
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",
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']}")

# ── 5. POST /stores/{store_id}/read ───────────────────────
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",
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")

# ── 6. POST /stores — create store via raw request ────────
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}")

# ── 7. DELETE /stores/{store_id} — clean up ───────────────
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)")

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

# ── 9. Explicit path_params override for store_id ─────────
print("9. Explicit store_id in path_params")
raw = await fga_client.execute_api_request(
operation_name="GetStore",
method="GET",
path="/stores/{store_id}",
path_params={"store_id": store.id},
)
body = raw.json()
assert raw.status == 200
assert body["id"] == store.id
print(f" ✅ explicit store_id matched: {body['id']}")

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

print("\n All execute_api_request integration tests passed!\n")


asyncio.run(main())
Loading
Loading