|
| 1 | +# ruff: noqa: E402 |
| 2 | + |
| 3 | +""" |
| 4 | +execute_api_request example — calls real OpenFGA endpoints and compares |
| 5 | +the results with the regular SDK methods to verify correctness. |
| 6 | +
|
| 7 | +Requires a running OpenFGA server (default: http://localhost:8080). |
| 8 | + export FGA_API_URL=http://localhost:8080 # optional, this is the default |
| 9 | + python3 execute_api_request_example.py |
| 10 | +""" |
| 11 | + |
| 12 | +import asyncio |
| 13 | +import os |
| 14 | +import sys |
| 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 | + |
| 50 | + # ─── Setup: create a store, model, and tuple ───────────── |
| 51 | + print("=== Setup ===") |
| 52 | + |
| 53 | + # Create a test store via the SDK |
| 54 | + store = await fga_client.create_store( |
| 55 | + CreateStoreRequest(name="execute_api_request_test") |
| 56 | + ) |
| 57 | + fga_client.set_store_id(store.id) |
| 58 | + print(f"Created store: {store.id}") |
| 59 | + |
| 60 | + # Write an authorization model |
| 61 | + model_resp = await fga_client.write_authorization_model( |
| 62 | + WriteAuthorizationModelRequest( |
| 63 | + schema_version="1.1", |
| 64 | + type_definitions=[ |
| 65 | + TypeDefinition(type="user"), |
| 66 | + TypeDefinition( |
| 67 | + type="document", |
| 68 | + relations=dict( |
| 69 | + writer=Userset(this=dict()), |
| 70 | + viewer=Userset( |
| 71 | + union=Usersets( |
| 72 | + child=[ |
| 73 | + Userset(this=dict()), |
| 74 | + Userset( |
| 75 | + computed_userset=ObjectRelation( |
| 76 | + object="", relation="writer" |
| 77 | + ) |
| 78 | + ), |
| 79 | + ] |
| 80 | + ) |
| 81 | + ), |
| 82 | + ), |
| 83 | + metadata=Metadata( |
| 84 | + relations=dict( |
| 85 | + writer=RelationMetadata( |
| 86 | + directly_related_user_types=[ |
| 87 | + RelationReference(type="user"), |
| 88 | + ] |
| 89 | + ), |
| 90 | + viewer=RelationMetadata( |
| 91 | + directly_related_user_types=[ |
| 92 | + RelationReference(type="user"), |
| 93 | + ] |
| 94 | + ), |
| 95 | + ) |
| 96 | + ), |
| 97 | + ), |
| 98 | + ], |
| 99 | + ) |
| 100 | + ) |
| 101 | + auth_model_id = model_resp.authorization_model_id |
| 102 | + fga_client.set_authorization_model_id(auth_model_id) |
| 103 | + print(f"Created model: {auth_model_id}") |
| 104 | + |
| 105 | + # Write a tuple |
| 106 | + await fga_client.write( |
| 107 | + ClientWriteRequest( |
| 108 | + writes=[ |
| 109 | + ClientTuple( |
| 110 | + user="user:anne", |
| 111 | + relation="writer", |
| 112 | + object="document:roadmap", |
| 113 | + ), |
| 114 | + ] |
| 115 | + ) |
| 116 | + ) |
| 117 | + print("Wrote tuple: user:anne → writer → document:roadmap") |
| 118 | + |
| 119 | + # ─── Tests ──────────────────────────────────────────────── |
| 120 | + print("\n=== execute_api_request tests ===\n") |
| 121 | + |
| 122 | + # ── 1. GET /stores ──────────────────────────────────────── |
| 123 | + print("1. ListStores (GET /stores)") |
| 124 | + raw = await fga_client.execute_api_request( |
| 125 | + operation_name="ListStores", |
| 126 | + method="GET", |
| 127 | + path="/stores", |
| 128 | + query_params={"page_size": 100}, |
| 129 | + ) |
| 130 | + sdk = await fga_client.list_stores() |
| 131 | + body = raw.json() |
| 132 | + assert raw.status == 200, f"Expected 200, got {raw.status}" |
| 133 | + assert "stores" in body |
| 134 | + assert len(body["stores"]) == len(sdk.stores), ( |
| 135 | + f"Count mismatch: {len(body['stores'])} vs {len(sdk.stores)}" |
| 136 | + ) |
| 137 | + print(f" ✅ {len(body['stores'])} stores (status {raw.status})") |
| 138 | + |
| 139 | + # ── 2. GET /stores/{store_id} (auto-substitution) ──────── |
| 140 | + print("2. GetStore (GET /stores/{store_id})") |
| 141 | + raw = await fga_client.execute_api_request( |
| 142 | + operation_name="GetStore", |
| 143 | + method="GET", |
| 144 | + path="/stores/{store_id}", |
| 145 | + ) |
| 146 | + sdk = await fga_client.get_store() |
| 147 | + body = raw.json() |
| 148 | + assert raw.status == 200 |
| 149 | + assert body["id"] == sdk.id |
| 150 | + assert body["name"] == sdk.name |
| 151 | + print(f" ✅ id={body['id']}, name={body['name']}") |
| 152 | + |
| 153 | + # ── 3. GET /stores/{store_id}/authorization-models ──────── |
| 154 | + print("3. ReadAuthorizationModels (GET /stores/{store_id}/authorization-models)") |
| 155 | + raw = await fga_client.execute_api_request( |
| 156 | + operation_name="ReadAuthorizationModels", |
| 157 | + method="GET", |
| 158 | + path="/stores/{store_id}/authorization-models", |
| 159 | + ) |
| 160 | + sdk = await fga_client.read_authorization_models() |
| 161 | + body = raw.json() |
| 162 | + assert raw.status == 200 |
| 163 | + assert len(body["authorization_models"]) == len(sdk.authorization_models) |
| 164 | + print(f" ✅ {len(body['authorization_models'])} models") |
| 165 | + |
| 166 | + # ── 4. POST /stores/{store_id}/check ────────────────────── |
| 167 | + print("4. Check (POST /stores/{store_id}/check)") |
| 168 | + raw = await fga_client.execute_api_request( |
| 169 | + operation_name="Check", |
| 170 | + method="POST", |
| 171 | + path="/stores/{store_id}/check", |
| 172 | + body={ |
| 173 | + "tuple_key": { |
| 174 | + "user": "user:anne", |
| 175 | + "relation": "viewer", |
| 176 | + "object": "document:roadmap", |
| 177 | + }, |
| 178 | + "authorization_model_id": auth_model_id, |
| 179 | + }, |
| 180 | + ) |
| 181 | + sdk = await fga_client.check( |
| 182 | + ClientCheckRequest( |
| 183 | + user="user:anne", |
| 184 | + relation="viewer", |
| 185 | + object="document:roadmap", |
| 186 | + ) |
| 187 | + ) |
| 188 | + body = raw.json() |
| 189 | + assert raw.status == 200 |
| 190 | + assert body["allowed"] == sdk.allowed |
| 191 | + print(f" ✅ allowed={body['allowed']}") |
| 192 | + |
| 193 | + # ── 5. POST /stores/{store_id}/read ─────────────────────── |
| 194 | + print("5. Read (POST /stores/{store_id}/read)") |
| 195 | + raw = await fga_client.execute_api_request( |
| 196 | + operation_name="Read", |
| 197 | + method="POST", |
| 198 | + path="/stores/{store_id}/read", |
| 199 | + body={ |
| 200 | + "tuple_key": { |
| 201 | + "user": "user:anne", |
| 202 | + "object": "document:", |
| 203 | + }, |
| 204 | + }, |
| 205 | + ) |
| 206 | + body = raw.json() |
| 207 | + assert raw.status == 200 |
| 208 | + assert "tuples" in body |
| 209 | + assert len(body["tuples"]) >= 1 |
| 210 | + print(f" ✅ {len(body['tuples'])} tuples returned") |
| 211 | + |
| 212 | + # ── 6. POST /stores — create store via raw request ──────── |
| 213 | + print("6. CreateStore (POST /stores)") |
| 214 | + raw = await fga_client.execute_api_request( |
| 215 | + operation_name="CreateStore", |
| 216 | + method="POST", |
| 217 | + path="/stores", |
| 218 | + body={"name": "raw_request_test_store"}, |
| 219 | + ) |
| 220 | + body = raw.json() |
| 221 | + assert raw.status == 201, f"Expected 201, got {raw.status}" |
| 222 | + assert "id" in body |
| 223 | + new_store_id = body["id"] |
| 224 | + print(f" ✅ created store: {new_store_id}") |
| 225 | + |
| 226 | + # ── 7. DELETE /stores/{store_id} — clean up ─────────────── |
| 227 | + print("7. DeleteStore (DELETE /stores/{store_id})") |
| 228 | + raw = await fga_client.execute_api_request( |
| 229 | + operation_name="DeleteStore", |
| 230 | + method="DELETE", |
| 231 | + path="/stores/{store_id}", |
| 232 | + path_params={"store_id": new_store_id}, |
| 233 | + ) |
| 234 | + assert raw.status == 204, f"Expected 204, got {raw.status}" |
| 235 | + print(f" ✅ deleted store: {new_store_id} (status 204 No Content)") |
| 236 | + |
| 237 | + # ── 8. Custom headers ───────────────────────────────────── |
| 238 | + print("8. Custom headers (GET /stores/{store_id})") |
| 239 | + raw = await fga_client.execute_api_request( |
| 240 | + operation_name="GetStoreWithHeaders", |
| 241 | + method="GET", |
| 242 | + path="/stores/{store_id}", |
| 243 | + headers={"X-Custom-Header": "test-value"}, |
| 244 | + ) |
| 245 | + assert raw.status == 200 |
| 246 | + print(f" ✅ custom headers accepted (status {raw.status})") |
| 247 | + |
| 248 | + # ── 9. Explicit path_params override for store_id ───────── |
| 249 | + print("9. Explicit store_id in path_params") |
| 250 | + raw = await fga_client.execute_api_request( |
| 251 | + operation_name="GetStore", |
| 252 | + method="GET", |
| 253 | + path="/stores/{store_id}", |
| 254 | + path_params={"store_id": store.id}, |
| 255 | + ) |
| 256 | + body = raw.json() |
| 257 | + assert raw.status == 200 |
| 258 | + assert body["id"] == store.id |
| 259 | + print(f" ✅ explicit store_id matched: {body['id']}") |
| 260 | + |
| 261 | + # ─── Cleanup ───────────────────────────────────────────── |
| 262 | + print("\n=== Cleanup ===") |
| 263 | + await fga_client.delete_store() |
| 264 | + print(f"Deleted test store: {store.id}") |
| 265 | + |
| 266 | + print("\n All execute_api_request integration tests passed!\n") |
| 267 | + |
| 268 | + |
| 269 | +asyncio.run(main()) |
| 270 | + |
| 271 | + |
0 commit comments