2929from openfga_sdk .configuration import RetryParams
3030from 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