Skip to content

Commit 7f5b287

Browse files
committed
fix: rename tests and example
1 parent 785af00 commit 7f5b287

File tree

3 files changed

+221
-48
lines changed

3 files changed

+221
-48
lines changed

example/execute-api-request/execute_api_request_example.py

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# ruff: noqa: E402
22

33
"""
4-
execute_api_request example — calls real OpenFGA endpoints and compares
5-
the results with the regular SDK methods to verify correctness.
4+
execute_api_request example
65
76
Requires a running OpenFGA server (default: http://localhost:8080).
87
export FGA_API_URL=http://localhost:8080 # optional, this is the default
@@ -49,14 +48,12 @@ async def main():
4948
async with OpenFgaClient(configuration) as fga_client:
5049
print("=== Setup ===")
5150

52-
# Create a test store via the SDK
5351
store = await fga_client.create_store(
5452
CreateStoreRequest(name="execute_api_request_test")
5553
)
5654
fga_client.set_store_id(store.id)
5755
print(f"Created store: {store.id}")
5856

59-
# Write an authorization model
6057
model_resp = await fga_client.write_authorization_model(
6158
WriteAuthorizationModelRequest(
6259
schema_version="1.1",
@@ -101,7 +98,6 @@ async def main():
10198
fga_client.set_authorization_model_id(auth_model_id)
10299
print(f"Created model: {auth_model_id}")
103100

104-
# Write a tuple
105101
await fga_client.write(
106102
ClientWriteRequest(
107103
writes=[
@@ -113,10 +109,11 @@ async def main():
113109
]
114110
)
115111
)
116-
print("Wrote tuple: user:anne writer document:roadmap")
112+
print("Wrote tuple: user:anne writer document:roadmap")
117113

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

116+
# 1. ListStores
120117
print("1. ListStores (GET /stores)")
121118
raw = await fga_client.execute_api_request(
122119
operation_name="ListStores",
@@ -128,12 +125,11 @@ async def main():
128125
body = raw.json()
129126
assert raw.status == 200, f"Expected 200, got {raw.status}"
130127
assert "stores" in body
131-
assert len(body["stores"]) == len(sdk.stores), (
132-
f"Count mismatch: {len(body['stores'])} vs {len(sdk.stores)}"
133-
)
134-
print(f" ✅ {len(body['stores'])} stores (status {raw.status})")
128+
assert len(body["stores"]) == len(sdk.stores)
129+
print(f" {len(body['stores'])} stores (status {raw.status})")
135130

136-
print("2. GetStore (GET /stores/{store_id})")
131+
# 2. GetStore
132+
print("2. GetStore (GET /stores/{{store_id}})")
137133
raw = await fga_client.execute_api_request(
138134
operation_name="GetStore",
139135
method="GET",
@@ -145,10 +141,11 @@ async def main():
145141
assert raw.status == 200
146142
assert body["id"] == sdk.id
147143
assert body["name"] == sdk.name
148-
print(f" id={body['id']}, name={body['name']}")
144+
print(f" id={body['id']}, name={body['name']}")
149145

146+
# 3. ReadAuthorizationModels
150147
print(
151-
"3. ReadAuthorizationModels (GET /stores/{store_id}/authorization-models)"
148+
"3. ReadAuthorizationModels (GET /stores/{{store_id}}/authorization-models)"
152149
)
153150
raw = await fga_client.execute_api_request(
154151
operation_name="ReadAuthorizationModels",
@@ -160,9 +157,10 @@ async def main():
160157
body = raw.json()
161158
assert raw.status == 200
162159
assert len(body["authorization_models"]) == len(sdk.authorization_models)
163-
print(f" {len(body['authorization_models'])} models")
160+
print(f" {len(body['authorization_models'])} models")
164161

165-
print("4. Check (POST /stores/{store_id}/check)")
162+
# 4. Check
163+
print("4. Check (POST /stores/{{store_id}}/check)")
166164
raw = await fga_client.execute_api_request(
167165
operation_name="Check",
168166
method="POST",
@@ -187,9 +185,10 @@ async def main():
187185
body = raw.json()
188186
assert raw.status == 200
189187
assert body["allowed"] == sdk.allowed
190-
print(f" allowed={body['allowed']}")
188+
print(f" allowed={body['allowed']}")
191189

192-
print("5. Read (POST /stores/{store_id}/read)")
190+
# 5. Read
191+
print("5. Read (POST /stores/{{store_id}}/read)")
193192
raw = await fga_client.execute_api_request(
194193
operation_name="Read",
195194
method="POST",
@@ -206,32 +205,35 @@ async def main():
206205
assert raw.status == 200
207206
assert "tuples" in body
208207
assert len(body["tuples"]) >= 1
209-
print(f" {len(body['tuples'])} tuples returned")
208+
print(f" {len(body['tuples'])} tuples returned")
210209

210+
# 6. CreateStore
211211
print("6. CreateStore (POST /stores)")
212212
raw = await fga_client.execute_api_request(
213213
operation_name="CreateStore",
214214
method="POST",
215215
path="/stores",
216-
body={"name": "raw_request_test_store"},
216+
body={"name": "executor_test_store"},
217217
)
218218
body = raw.json()
219219
assert raw.status == 201, f"Expected 201, got {raw.status}"
220220
assert "id" in body
221221
new_store_id = body["id"]
222-
print(f" created store: {new_store_id}")
222+
print(f" created store: {new_store_id}")
223223

224-
print("7. DeleteStore (DELETE /stores/{store_id})")
224+
# 7. DeleteStore
225+
print("7. DeleteStore (DELETE /stores/{{store_id}})")
225226
raw = await fga_client.execute_api_request(
226227
operation_name="DeleteStore",
227228
method="DELETE",
228229
path="/stores/{store_id}",
229230
path_params={"store_id": new_store_id},
230231
)
231232
assert raw.status == 204, f"Expected 204, got {raw.status}"
232-
print(f" deleted store: {new_store_id} (status 204 No Content)")
233+
print(f" deleted store: {new_store_id} (status 204)")
233234

234-
print("8. Custom headers (GET /stores/{store_id})")
235+
# 8. Custom headers
236+
print("8. Custom headers (GET /stores/{{store_id}})")
235237
raw = await fga_client.execute_api_request(
236238
operation_name="GetStoreWithHeaders",
237239
method="GET",
@@ -240,9 +242,12 @@ async def main():
240242
headers={"X-Custom-Header": "test-value"},
241243
)
242244
assert raw.status == 200
243-
print(f" custom headers accepted (status {raw.status})")
245+
print(f" custom headers accepted (status {raw.status})")
244246

245-
print("9. StreamedListObjects (POST /stores/{store_id}/streamed-list-objects)")
247+
# 9. StreamedListObjects
248+
print(
249+
"9. StreamedListObjects (POST /stores/{{store_id}}/streamed-list-objects)"
250+
)
246251
chunks = []
247252
async for chunk in fga_client.execute_streamed_api_request(
248253
operation_name="StreamedListObjects",
@@ -258,16 +263,16 @@ async def main():
258263
):
259264
chunks.append(chunk)
260265
assert len(chunks) >= 1, f"Expected at least 1 chunk, got {len(chunks)}"
261-
# Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}}
262266
objects = [c["result"]["object"] for c in chunks if "result" in c]
263267
assert "document:roadmap" in objects, f"Expected document:roadmap in {objects}"
264-
print(f" {len(chunks)} chunks, objects={objects}")
268+
print(f" {len(chunks)} chunks, objects={objects}")
265269

270+
# Cleanup
266271
print("\n=== Cleanup ===")
267272
await fga_client.delete_store()
268273
print(f"Deleted test store: {store.id}")
269274

270-
print("\nAll execute_api_request examples completed successfully.\n")
275+
print("\nAll examples completed successfully.\n")
271276

272277

273278
asyncio.run(main())

test/client/client_test.py

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from openfga_sdk.configuration import RetryParams
3030
from openfga_sdk.exceptions import (
3131
FgaValidationException,
32+
RateLimitExceededError,
3233
UnauthorizedException,
3334
ValidationException,
3435
)
@@ -4174,7 +4175,7 @@ async def test_write_with_conflict_options_both(self, mock_request):
41744175

41754176
@patch.object(rest.RESTClientObject, "request")
41764177
@pytest.mark.asyncio
4177-
async def test_raw_request_post_with_body(self, mock_request):
4178+
async def test_api_executor_post_with_body(self, mock_request):
41784179
"""Test case for execute_api_request
41794180
41804181
Make a POST request with JSON body
@@ -4226,7 +4227,7 @@ async def test_raw_request_post_with_body(self, mock_request):
42264227

42274228
@patch.object(rest.RESTClientObject, "request")
42284229
@pytest.mark.asyncio
4229-
async def test_raw_request_get_with_query_params(self, mock_request):
4230+
async def test_api_executor_get_with_query_params(self, mock_request):
42304231
"""Test case for execute_api_request
42314232
42324233
Make a GET request with query parameters
@@ -4272,7 +4273,7 @@ async def test_raw_request_get_with_query_params(self, mock_request):
42724273

42734274
@patch.object(rest.RESTClientObject, "request")
42744275
@pytest.mark.asyncio
4275-
async def test_raw_request_with_path_params(self, mock_request):
4276+
async def test_api_executor_with_path_params(self, mock_request):
42764277
"""Test case for execute_api_request
42774278
42784279
Make a request with path parameters
@@ -4312,7 +4313,7 @@ async def test_raw_request_with_path_params(self, mock_request):
43124313

43134314
@patch.object(rest.RESTClientObject, "request")
43144315
@pytest.mark.asyncio
4315-
async def test_raw_request_explicit_store_id_in_path_params(self, mock_request):
4316+
async def test_api_executor_explicit_store_id_in_path_params(self, mock_request):
43164317
"""Test case for execute_api_request
43174318
43184319
Test that store_id must be provided explicitly in path_params
@@ -4347,7 +4348,7 @@ async def test_raw_request_explicit_store_id_in_path_params(self, mock_request):
43474348
await api_client.close()
43484349

43494350
@pytest.mark.asyncio
4350-
async def test_raw_request_missing_operation_name(self):
4351+
async def test_api_executor_missing_operation_name(self):
43514352
"""Test case for execute_api_request
43524353
43534354
Test that operation_name is required
@@ -4365,7 +4366,7 @@ async def test_raw_request_missing_operation_name(self):
43654366
await api_client.close()
43664367

43674368
@pytest.mark.asyncio
4368-
async def test_raw_request_missing_store_id(self):
4369+
async def test_api_executor_missing_store_id(self):
43694370
"""Test case for execute_api_request
43704371
43714372
Test that store_id must be provided in path_params when path contains {store_id}
@@ -4382,7 +4383,7 @@ async def test_raw_request_missing_store_id(self):
43824383
await api_client.close()
43834384

43844385
@pytest.mark.asyncio
4385-
async def test_raw_request_missing_path_params(self):
4386+
async def test_api_executor_missing_path_params(self):
43864387
"""Test case for execute_api_request
43874388
43884389
Test that all path parameters must be provided
@@ -4403,7 +4404,7 @@ async def test_raw_request_missing_path_params(self):
44034404

44044405
@patch.object(rest.RESTClientObject, "request")
44054406
@pytest.mark.asyncio
4406-
async def test_raw_request_with_list_query_params(self, mock_request):
4407+
async def test_api_executor_with_list_query_params(self, mock_request):
44074408
"""Test case for execute_api_request
44084409
44094410
Test query parameters with list values
@@ -4438,7 +4439,7 @@ async def test_raw_request_with_list_query_params(self, mock_request):
44384439

44394440
@patch.object(rest.RESTClientObject, "request")
44404441
@pytest.mark.asyncio
4441-
async def test_raw_request_default_headers(self, mock_request):
4442+
async def test_api_executor_default_headers(self, mock_request):
44424443
"""Test case for execute_api_request
44434444
44444445
Test that default headers (Content-Type, Accept) are set
@@ -4469,7 +4470,7 @@ async def test_raw_request_default_headers(self, mock_request):
44694470

44704471
@patch.object(rest.RESTClientObject, "request")
44714472
@pytest.mark.asyncio
4472-
async def test_raw_request_url_encoded_path_params(self, mock_request):
4473+
async def test_api_executor_url_encoded_path_params(self, mock_request):
44734474
"""Test case for execute_api_request
44744475
44754476
Test that path parameters are URL encoded
@@ -4603,3 +4604,87 @@ async def mock_gen():
46034604
self.assertIn(store_id, call_args[0][1])
46044605
self.assertNotIn("{store_id}", call_args[0][1])
46054606
await api_client.close()
4607+
4608+
@patch("asyncio.sleep")
4609+
@patch.object(rest.RESTClientObject, "stream")
4610+
@pytest.mark.asyncio
4611+
async def test_execute_streamed_api_request_retry_on_connection(
4612+
self, mock_stream, mock_sleep
4613+
):
4614+
"""Test that the streaming executor retries on connection-phase errors (e.g. 429)
4615+
but does NOT retry once streaming has begun."""
4616+
4617+
async def mock_gen():
4618+
yield {"result": {"object": "document:roadmap"}}
4619+
yield {"result": {"object": "document:budget"}}
4620+
4621+
mock_stream.side_effect = [
4622+
RateLimitExceededError(
4623+
http_resp=http_mock_response(
4624+
'{"code": "rate_limit_exceeded", "message": "Rate Limit exceeded"}',
4625+
429,
4626+
)
4627+
),
4628+
mock_gen(),
4629+
]
4630+
4631+
configuration = self.configuration
4632+
configuration.store_id = store_id
4633+
configuration.retry_params = RetryParams(max_retry=3, min_wait_in_ms=10)
4634+
async with OpenFgaClient(configuration) as api_client:
4635+
chunks = []
4636+
async for chunk in api_client.execute_streamed_api_request(
4637+
operation_name="StreamedListObjects",
4638+
method="POST",
4639+
path="/stores/{store_id}/streamed-list-objects",
4640+
path_params={"store_id": store_id},
4641+
body={
4642+
"type": "document",
4643+
"relation": "viewer",
4644+
"user": "user:anne",
4645+
},
4646+
):
4647+
chunks.append(chunk)
4648+
4649+
self.assertEqual(len(chunks), 2)
4650+
self.assertEqual(chunks[0], {"result": {"object": "document:roadmap"}})
4651+
self.assertEqual(chunks[1], {"result": {"object": "document:budget"}})
4652+
4653+
# stream() was called twice: first raised 429, second succeeded
4654+
self.assertEqual(mock_stream.call_count, 2)
4655+
await api_client.close()
4656+
4657+
@patch.object(rest.RESTClientObject, "stream")
4658+
@pytest.mark.asyncio
4659+
async def test_execute_streamed_api_request_no_retry_without_config(
4660+
self, mock_stream
4661+
):
4662+
"""Test that without retry config, a 429 on connection is raised immediately."""
4663+
4664+
mock_stream.side_effect = RateLimitExceededError(
4665+
http_resp=http_mock_response(
4666+
'{"code": "rate_limit_exceeded", "message": "Rate Limit exceeded"}',
4667+
429,
4668+
)
4669+
)
4670+
4671+
configuration = self.configuration
4672+
configuration.store_id = store_id
4673+
configuration.retry_params = RetryParams(max_retry=0)
4674+
async with OpenFgaClient(configuration) as api_client:
4675+
with self.assertRaises(RateLimitExceededError):
4676+
async for _chunk in api_client.execute_streamed_api_request(
4677+
operation_name="StreamedListObjects",
4678+
method="POST",
4679+
path="/stores/{store_id}/streamed-list-objects",
4680+
path_params={"store_id": store_id},
4681+
body={
4682+
"type": "document",
4683+
"relation": "viewer",
4684+
"user": "user:anne",
4685+
},
4686+
):
4687+
pass # should not reach here
4688+
4689+
self.assertEqual(mock_stream.call_count, 1)
4690+
await api_client.close()

0 commit comments

Comments
 (0)