Skip to content

Commit

Permalink
feat: support api_url configuration option (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Feb 7, 2024
2 parents 53db8f2 + fa107a4 commit 89e1228
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 46 deletions.
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ from openfga_sdk import ClientConfiguration, OpenFgaClient

async def main():
configuration = ClientConfiguration(
api_scheme=FGA_API_SCHEME, # optional, defaults to "https"
api_host=FGA_API_HOST, # required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
api_url=FGA_API_URL, # required
store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores`
authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request
)
Expand All @@ -150,8 +149,7 @@ from openfga_sdk.credentials import CredentialConfiguration, Credentials

async def main():
configuration = ClientConfiguration(
api_scheme=FGA_API_SCHEME, # optional, defaults to "https"
api_host=FGA_API_HOST, # required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
api_url=FGA_API_URL, # required
store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores`
authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request
credentials=Credentials(
Expand All @@ -177,8 +175,7 @@ from openfga_sdk.credentials import Credentials, CredentialConfiguration

async def main():
configuration = ClientConfiguration(
api_scheme=FGA_API_SCHEME, # optional, defaults to "https"
api_host=FGA_API_HOST, # required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
api_url=FGA_API_URL, # required
store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores`
authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request
credentials=Credentials(
Expand All @@ -196,7 +193,6 @@ async def main():
api_response = await fga_client.read_authorization_models()
await fga_client.close()
return api_response

```

#### Synchronous Client
Expand All @@ -206,22 +202,19 @@ from `openfga_sdk.sync` that supports all the credential types and calls,
without requiring async/await.

```python
from openfga_sdk import ClientConfiguration
from openfga_sdk.sync import OpenFgaClient
from openfga_sdk.sync import ClientConfiguration, OpenFgaClient


def main():
configuration = ClientConfiguration(
api_scheme=FGA_API_SCHEME, # optional, defaults to "https"
api_host=FGA_API_HOST, # required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
api_url=FGA_API_URL, # required
store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores`
authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # optional, can be overridden per request
)
# Enter a context with an instance of the OpenFgaClient
with OpenFgaClient(configuration) as fga_client:
api_response = fga_client.read_authorization_models()
return api_response

```


Expand Down Expand Up @@ -960,7 +953,6 @@ body = [ClientAssertion(
)]

response = await fga_client.write_assertions(body, options)

```


Expand Down
7 changes: 3 additions & 4 deletions example/example1/example1.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ async def main():
)
)

if os.getenv('FGA_API_HOST') is not None:
if os.getenv('FGA_API_URL') is not None:
configuration = ClientConfiguration(
api_host=os.getenv('FGA_API_HOST'),
api_url=os.getenv('FGA_API_URL'),
credentials=credentials
)
else:
configuration = ClientConfiguration(
api_scheme='http',
api_host='localhost:8080',
api_url='http://localhost:8080',
credentials=credentials
)

Expand Down
5 changes: 4 additions & 1 deletion openfga_sdk/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,10 @@ async def __call_api(

# request url
if _host is None:
url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path
if self.configuration.api_url is not None:
url = self.configuration.api_url + resource_path
else:
url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path
else:
# use server/host defined in path or operation instead
url = self.configuration.api_scheme + '://' + _host + resource_path
Expand Down
4 changes: 3 additions & 1 deletion openfga_sdk/client/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ def __init__(
retry_params=None,
authorization_model_id=None,
ssl_ca_cert=None,
api_url=None, # TODO: restructure when removing api_scheme/api_host
):
super().__init__(api_scheme, api_host, store_id, credentials, retry_params, ssl_ca_cert=ssl_ca_cert)
super().__init__(api_scheme, api_host, store_id, credentials,
retry_params, ssl_ca_cert=ssl_ca_cert, api_url=api_url)
self._authorization_model_id = authorization_model_id

def is_valid(self):
Expand Down
60 changes: 42 additions & 18 deletions openfga_sdk/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ class Configuration(object):
Do not edit the class manually.
:param api_scheme: Whether connection is 'https' or 'http'. Default as 'https'
.. deprecated:: 0.4.1
Use `api_url` instead.
:param api_host: Base url
.. deprecated:: 0.4.1
Use `api_url` instead.
:param store_id: ID of store for API
:param credentials: Configuration for obtaining authentication credential
:param retry_params: Retry parameters upon HTTP too many request
Expand Down Expand Up @@ -132,6 +136,7 @@ class Configuration(object):
The validation of enums is performed for variables with defined enum values before.
: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
"""

_default = None
Expand All @@ -147,9 +152,11 @@ def __init__(self, api_scheme="https", api_host=None,
server_index=None, server_variables=None,
server_operation_index=None, server_operation_variables=None,
ssl_ca_cert=None,
api_url=None, # TODO: restructure when removing api_scheme/api_host
):
"""Constructor
"""
self._url = api_url
self._scheme = api_scheme
self._base_path = api_host
self._store_id = store_id
Expand Down Expand Up @@ -499,28 +506,35 @@ def is_valid(self):
Verify the configuration is valid.
Note that we are only doing basic validation to ensure input is sane.
"""
if self.api_host is None or self.api_host == '':
raise FgaValidationException('api_host is required but not configured.')
if self.api_scheme is None or self.api_scheme == '':
raise FgaValidationException('api_scheme is required but not configured.')
combined_url = self.api_scheme + '://' + self.api_host
combined_url = self.api_url
if self.api_url is None:
if self.api_host is None or self.api_host == '':
raise FgaValidationException('api_host is required but not configured.')
if self.api_scheme is None or self.api_scheme == '':
raise FgaValidationException('api_scheme is required but not configured.')
combined_url = self.api_scheme + '://' + self.api_host
parsed_url = None
try:
parsed_url = urlparse(combined_url)
except ValueError:
raise ApiValueError('Either api_scheme `{}` or api_host `{}` is invalid'.format(
self.api_scheme, self.api_host))
if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'):
raise ApiValueError(
'api_scheme `{}` must be either `http` or `https`'.format(self.api_scheme))
if (parsed_url.netloc == ''):
raise ApiValueError('api_host `{}` is invalid'.format(self.api_host))
if (parsed_url.path != ''):
raise ApiValueError(
'api_host `{}` is not expected to have path specified'.format(self.api_scheme))
if (parsed_url.query != ''):
raise ApiValueError(
'api_host `{}` is not expected to have query specified'.format(self.api_scheme))
if self.api_url is None:
raise ApiValueError('Either api_scheme `{}` or api_host `{}` is invalid'.format(
self.api_scheme, self.api_host))
else:
raise ApiValueError('api_url `{}` is invalid'.format(
self.api_url))
if self.api_url is None:
if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'):
raise ApiValueError(
'api_scheme `{}` must be either `http` or `https`'.format(self.api_scheme))
if (parsed_url.netloc == ''):
raise ApiValueError('api_host `{}` is invalid'.format(self.api_host))
if (parsed_url.path != ''):
raise ApiValueError(
'api_host `{}` is not expected to have path specified'.format(self.api_scheme))
if (parsed_url.query != ''):
raise ApiValueError(
'api_host `{}` is not expected to have query specified'.format(self.api_scheme))

if self.store_id is not None and self.store_id != "" and is_well_formed_ulid_string(self.store_id) is False:
raise FgaValidationException(
Expand Down Expand Up @@ -549,6 +563,16 @@ def api_host(self, value):
"""Update configured host"""
self._base_path = value

@property
def api_url(self):
"""Return api_url"""
return self._url

@api_url.setter
def api_url(self, value):
"""Update configured api_url"""
self._url = value

@property
def store_id(self):
"""Return store id."""
Expand Down
5 changes: 4 additions & 1 deletion openfga_sdk/sync/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ def __call_api(

# request url
if _host is None:
url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path
if self.configuration.api_url is not None:
url = self.configuration.api_url + resource_path
else:
url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path
else:
# use server/host defined in path or operation instead
url = self.configuration.api_scheme + '://' + _host + resource_path
Expand Down
3 changes: 1 addition & 2 deletions test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase):

def setUp(self):
self.configuration = ClientConfiguration(
api_scheme='http',
api_host="api.fga.example",
api_url='http://api.fga.example',
)

def tearDown(self):
Expand Down
3 changes: 1 addition & 2 deletions test/test_client_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase):

def setUp(self):
self.configuration = ClientConfiguration(
api_scheme='http',
api_host="api.fga.example",
api_url='http://api.fga.example',
)

def tearDown(self):
Expand Down
25 changes: 23 additions & 2 deletions test/test_open_fga_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase):

def setUp(self):
self.configuration = openfga_sdk.Configuration(
api_scheme='http',
api_host="api.fga.example",
api_url='http://api.fga.example',
)

def tearDown(self):
Expand Down Expand Up @@ -923,6 +922,28 @@ def test_configuration_store_id_invalid(self):
)
self.assertRaises(FgaValidationException, configuration.is_valid)

def test_url(self):
"""
Ensure that api_url is set and validated
"""
configuration = openfga_sdk.Configuration(
api_url='http://localhost:8080'
)
self.assertEqual(configuration.api_url, 'http://localhost:8080')
configuration.is_valid()

def test_url_with_scheme_and_host(self):
"""
Ensure that api_url takes precedence over api_host and scheme
"""
configuration = openfga_sdk.Configuration(
api_url='http://localhost:8080',
api_host='localhost:8080',
api_scheme='foo'
)
self.assertEqual(configuration.api_url, 'http://localhost:8080')
configuration.is_valid() # Should not throw and complain about scheme being invalid

async def test_bad_configuration_read_authorization_model(self):
"""
Test whether FgaValidationException is raised for API (reading authorization models)
Expand Down
25 changes: 23 additions & 2 deletions test/test_open_fga_api_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase):

def setUp(self):
self.configuration = Configuration(
api_scheme='http',
api_host="api.fga.example",
api_url='http://api.fga.example',
)

def tearDown(self):
Expand Down Expand Up @@ -924,6 +923,28 @@ def test_configuration_store_id_invalid(self):
)
self.assertRaises(FgaValidationException, configuration.is_valid)

def test_url(self):
"""
Ensure that api_url is set and validated
"""
configuration = Configuration(
api_url='http://localhost:8080'
)
self.assertEqual(configuration.api_url, 'http://localhost:8080')
configuration.is_valid()

def test_url_with_scheme_and_host(self):
"""
Ensure that api_url takes precedence over api_host and scheme
"""
configuration = Configuration(
api_url='http://localhost:8080',
api_host='localhost:8080',
api_scheme='foo'
)
self.assertEqual(configuration.api_url, 'http://localhost:8080')
configuration.is_valid() # Should not throw and complain about scheme being invalid

async def test_bad_configuration_read_authorization_model(self):
"""
Test whether FgaValidationException is raised for API (reading authorization models)
Expand Down

0 comments on commit 89e1228

Please sign in to comment.