diff --git a/README.md b/README.md index 0fdc2a6..9dd6d47 100644 --- a/README.md +++ b/README.md @@ -349,48 +349,46 @@ Create a new authorization model. # Initialize the fga_client # fga_client = OpenFgaClient(configuration) -body = WriteAuthorizationModelRequest( - schema_version="1.1", - type_definitions=[ - TypeDefinition( - type="user" +user_type = TypeDefinition(type="user") + +document_relations = dict( + writer=Userset(this=dict()), + viewer=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], ), - TypeDefinition( - type="document", - relations=dict( - writer=Userset( - this=dict(), - ), - viewer=Userset( - union=Usersets( - child=[ - Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), - ], - ), - ), + ), +) + +document_metadata = Metadata( + relations=dict( + writer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + RelationReference(type="user", condition="ViewCountLessThan200"), + ] ), - metadata=Metadata( - relations=dict( - writer=RelationMetadata( - directly_related_user_types=[ - RelationReference(type="user"), - RelationReference(type="user", condition="ViewCountLessThan200"), - ] - ), - viewer=RelationMetadata( - directly_related_user_types=[ - RelationReference(type="user"), - ] - ) - ) + viewer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + ] ) ) - ], - conditions=dict( +) + +document_type = TypeDefinition( + type="document", + relations=document_relations, + metadata=document_metadata +) + +conditions = dict( ViewCountLessThan200=Condition( name="ViewCountLessThan200", expression="ViewCount < 200", @@ -407,6 +405,14 @@ body = WriteAuthorizationModelRequest( ) ) ) + +body = WriteAuthorizationModelRequest( + schema_version="1.1", + type_definitions=[ + user_type, + document_type + ], + conditions=conditions ) response = await fga_client.write_authorization_model(body) diff --git a/openfga_sdk/client/configuration.py b/openfga_sdk/client/configuration.py index 0d8a3a0..fa68fbe 100644 --- a/openfga_sdk/client/configuration.py +++ b/openfga_sdk/client/configuration.py @@ -30,6 +30,7 @@ def __init__( authorization_model_id=None, ssl_ca_cert=None, api_url=None, # TODO: restructure when removing api_scheme/api_host + timeout_millisec: int | None = None, ): super().__init__( api_scheme, @@ -39,6 +40,7 @@ def __init__( retry_params, ssl_ca_cert=ssl_ca_cert, api_url=api_url, + timeout_millisec=timeout_millisec, ) self._authorization_model_id = authorization_model_id diff --git a/openfga_sdk/configuration.py b/openfga_sdk/configuration.py index 4168b87..eef52e4 100644 --- a/openfga_sdk/configuration.py +++ b/openfga_sdk/configuration.py @@ -169,6 +169,7 @@ class Configuration: :param ssl_ca_cert: str - the path to a file of concatenated CA certificates in PEM format :param api_url: str - the URL of the FGA server + :param timeout_millisec: int | None - the default timeout in milliseconds for requests """ _default = None @@ -206,6 +207,7 @@ def __init__( ] | None ) = None, + timeout_millisec: int | None = None, ): """Constructor""" self._url = api_url @@ -218,6 +220,8 @@ def __init__( else: # use the default parameters self._retry_params = RetryParams() + + self._timeout_millisec = timeout_millisec or 5000 * 60 """Default Base url """ self.server_index = 0 @@ -647,6 +651,18 @@ def is_valid(self): if self._credentials is not None: self._credentials.validate_credentials_config() + if self._timeout_millisec is not None: + if not isinstance(self._timeout_millisec, int): + raise FgaValidationException( + f"timeout_millisec unexpected type {self._timeout_millisec}" + ) + + ten_minutes = 10000 * 60 + if self._timeout_millisec < 0 or self._timeout_millisec > ten_minutes: + raise FgaValidationException( + f"timeout_millisec not within reasonable range (0,60000), {self._timeout_millisec}" + ) + @property def api_scheme(self): """Return connection is https or http.""" @@ -718,6 +734,20 @@ def retry_params(self, value): """ self._retry_params = value + @property + def timeout_millisec(self): + """ + Return timeout milliseconds + """ + return self._timeout_millisec + + @timeout_millisec.setter + def timeout_millisec(self, value): + """ + Update timeout milliseconds + """ + self._timeout_millisec = value + @property def disabled_client_side_validations(self): """Return disable_client_side_validations.""" diff --git a/openfga_sdk/rest.py b/openfga_sdk/rest.py index d227e6a..726c438 100644 --- a/openfga_sdk/rest.py +++ b/openfga_sdk/rest.py @@ -72,6 +72,7 @@ def __init__(self, configuration, pools_size=4, maxsize=None): self.proxy = configuration.proxy self.proxy_headers = configuration.proxy_headers + self._timeout_millisec = configuration.timeout_millisec # https pool manager self.pool_manager = aiohttp.ClientSession(connector=connector, trust_env=True) @@ -117,7 +118,7 @@ async def request( post_params = post_params or {} headers = headers or {} - timeout = _request_timeout or 5 * 60 + timeout = _request_timeout or self._timeout_millisec / 1000 if "Content-Type" not in headers: headers["Content-Type"] = "application/json" diff --git a/openfga_sdk/sync/rest.py b/openfga_sdk/sync/rest.py index afad041..caab5ef 100644 --- a/openfga_sdk/sync/rest.py +++ b/openfga_sdk/sync/rest.py @@ -81,6 +81,8 @@ def __init__(self, configuration, pools_size=4, maxsize=None): else: maxsize = 4 + self._timeout_millisec = configuration.timeout_millisec + # https pool manager if configuration.proxy: self.pool_manager = urllib3.ProxyManager( @@ -148,7 +150,7 @@ def request( post_params = post_params or {} headers = headers or {} - timeout = None + timeout = urllib3.Timeout(total=self._timeout_millisec / 1000) if _request_timeout: if isinstance(_request_timeout, (float, int)): timeout = urllib3.Timeout(total=_request_timeout) diff --git a/test/api/open_fga_api_test.py b/test/api/open_fga_api_test.py index a051a67..532cdda 100644 --- a/test/api/open_fga_api_test.py +++ b/test/api/open_fga_api_test.py @@ -1148,6 +1148,17 @@ def test_url_with_scheme_and_host(self): self.assertEqual(configuration.api_url, "http://localhost:8080") configuration.is_valid() # Should not throw and complain about scheme being invalid + def test_timeout_millisec(self): + """ + Ensure that timeout_seconds is set and validated + """ + configuration = openfga_sdk.Configuration( + api_url="http://localhost:8080", + timeout_millisec=10000, + ) + self.assertEqual(configuration.timeout_millisec, 10000) + configuration.is_valid() + async def test_bad_configuration_read_authorization_model(self): """ Test whether FgaValidationException is raised for API (reading authorization models) diff --git a/test/configuration_test.py b/test/configuration_test.py index 7be3338..64046b8 100644 --- a/test/configuration_test.py +++ b/test/configuration_test.py @@ -136,6 +136,7 @@ def test_configuration_set_default(self, configuration): } default_config.ssl_ca_cert = "/path/to/ca_cert.pem" default_config.api_url = "https://fga.example/api" + default_config.timeout_millisec = 10000 Configuration.set_default(default_config) assert Configuration._default.api_scheme == "https" @@ -183,6 +184,7 @@ def test_configuration_set_default(self, configuration): } assert Configuration._default.ssl_ca_cert == "/path/to/ca_cert.pem" assert Configuration._default.api_url == "https://fga.example/api" + assert Configuration._default.timeout_millisec == 10000 def test_configuration_get_default_copy(self, configuration): default_config = Configuration() @@ -212,6 +214,7 @@ def test_configuration_get_default_copy(self, configuration): } default_config.ssl_ca_cert = "/path/to/ca_cert.pem" default_config.api_url = "https://fga.example/api" + default_config.timeout_millisec = 10000 Configuration.set_default(default_config) copied_config = Configuration.get_default_copy() @@ -229,6 +232,7 @@ def test_configuration_get_default_copy(self, configuration): assert copied_config.credentials._api_audience == "audience123" assert copied_config.credentials._api_issuer == "issuer123" assert copied_config.credentials._api_token == "token123" + assert Configuration._default.timeout_millisec == 10000 class TestConfigurationValidityChecks: @@ -361,6 +365,7 @@ def test_configuration_deepcopy(self, configuration): }, ssl_ca_cert="/path/to/ca_cert.pem", api_url="https://fga.example/api", + timeout_millisec=10000, ) # Perform deep copy @@ -394,3 +399,4 @@ def test_configuration_deepcopy(self, configuration): ) assert copied_config.ssl_ca_cert == config.ssl_ca_cert assert copied_config.api_url == config.api_url + assert copied_config.timeout_millisec == config.timeout_millisec diff --git a/test/sync/open_fga_api_test.py b/test/sync/open_fga_api_test.py index 4f12ed1..c8a9552 100644 --- a/test/sync/open_fga_api_test.py +++ b/test/sync/open_fga_api_test.py @@ -1148,6 +1148,17 @@ def test_url_with_scheme_and_host(self): self.assertEqual(configuration.api_url, "http://localhost:8080") configuration.is_valid() # Should not throw and complain about scheme being invalid + def test_timeout_millisec(self): + """ + Ensure that timeout_millisec is set and validated + """ + configuration = Configuration( + api_url="http://localhost:8080", + timeout_millisec=10000, + ) + self.assertEqual(configuration.timeout_millisec, 10000) + configuration.is_valid() + async def test_bad_configuration_read_authorization_model(self): """ Test whether FgaValidationException is raised for API (reading authorization models)