diff --git a/Makefile b/Makefile index b8b0f6ce..e2368e62 100644 --- a/Makefile +++ b/Makefile @@ -115,7 +115,9 @@ tag-client-python: test-client-python .PHONY: build-client-python build-client-python: make build-client sdk_language=python tmpdir=${TMP_DIR} library="asyncio" + mv ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/openfga_sdk/api/open_fga_api_sync.py ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/openfga_sdk/sync/open_fga_api.py # TODO: Remove on OpenAPI generator v7.1 or higher make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/openfga_sdk/api/open_fga_api.py /config/clients/python/patches/open_fga_api.py.patch'" + make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/openfga_sdk/sync/open_fga_api.py /config/clients/python/patches/open_fga_api_sync.py.patch'" make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/docs/OpenFgaApi.md /config/clients/python/patches/OpenFgaApi.md.patch'" make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'python -m pip install pycodestyle==2.10.0 autopep8==2.0.2; autopep8 --in-place --ignore E402 --recursive openfga_sdk; autopep8 --in-place --recursive test'" make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'pip install setuptools wheel && python setup.py sdist bdist_wheel'" @@ -161,7 +163,7 @@ run-in-docker: .EXPORT_ALL_VARIABLES: .PHONY: build-client build-client: build-openapi - SDK_LANGUAGE="${sdk_language}" TMP_DIR="${tmpdir}" LIBRARY_TEMPLATE="${library}" \ + SDK_LANGUAGE="${sdk_language}" TMP_DIR="${tmpdir}" LIBRARY_TEMPLATE="${library}"\ ./scripts/build_client.sh .PHONY: build-openapi diff --git a/config/clients/python/.openapi-generator-ignore b/config/clients/python/.openapi-generator-ignore index e277ddc8..a014ba4a 100644 --- a/config/clients/python/.openapi-generator-ignore +++ b/config/clients/python/.openapi-generator-ignore @@ -4,6 +4,12 @@ test/* !test/test_open_fga_api.py !test/test_credentials.py !test/test_client.py +!test/test_client_sync.py +!test/test_open_fga_api_sync.py +!test/test_validation.py +!test/test_oauth2.py +!test/test_oauth2_sync.py +.github/workflows/python.yml .gitlab-ci.yml .travis.yml tox.ini diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 2bd781c3..456b95db 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -97,6 +97,55 @@ "test_validation.mustache": { "destinationFilename": "test/test_validation.py", "templateType": "SupportingFiles" + }, + "__init__sync.mustache": { + "destinationFilename": "openfga_sdk/sync/__init__.py", + "templateType": "SupportingFiles" + }, + "__init__sync_client.mustache": { + "destinationFilename": "openfga_sdk/sync/client/__init__.py", + "templateType": "SupportingFiles" + }, + "rest_sync.mustache": { + "destinationFilename": "openfga_sdk/sync/rest.py", + "templateType": "SupportingFiles" + }, + "api_client_sync.mustache": { + "destinationFilename": "openfga_sdk/sync/api_client.py", + "templateType": "SupportingFiles" + }, + "api_sync.mustache": { + "folder": "openfga_sdk/sync", + "destinationFilename": "_sync.py", + "templateType": "API" + }, + "api_test_sync.mustache": { + "destinationFilename": "test/test_open_fga_api_sync.py", + "templateType": "SupportingFiles" + }, + "client/client_sync.mustache": { + "destinationFilename": "openfga_sdk/sync/client/client.py", + "templateType": "SupportingFiles" + }, + "client/test_client_sync.mustache": { + "destinationFilename": "test/test_client_sync.py", + "templateType": "SupportingFiles" + }, + "oauth2.mustache": { + "destinationFilename": "openfga_sdk/oauth2.py", + "templateType": "SupportingFiles" + }, + "oauth2_test.mustache": { + "destinationFilename": "test/test_oauth2.py", + "templateType": "SupportingFiles" + }, + "oauth2_sync.mustache": { + "destinationFilename": "openfga_sdk/sync/oauth2.py", + "templateType": "SupportingFiles" + }, + "oauth2_test_sync.mustache": { + "destinationFilename": "test/test_oauth2_sync.py", + "templateType": "SupportingFiles" } } } diff --git a/config/clients/python/patches/open_fga_api.py.patch b/config/clients/python/patches/open_fga_api.py.patch index 489d9804..2d82b8b2 100644 --- a/config/clients/python/patches/open_fga_api.py.patch +++ b/config/clients/python/patches/open_fga_api.py.patch @@ -1,8 +1,8 @@ --- clients/fga-python-sdk/openfga_sdk/api/open_fga_api.py 2022-09-13 14:15:46.000000000 -0400 +++ open_fga_api.py 2022-09-13 14:14:01.000000000 -0400 @@ -193,13 +193,15 @@ - collection_formats=collection_formats, - _request_auth=local_var_params.get('_request_auth'))) + _request_auth=local_var_params.get('_request_auth'), + _oauth2_client=self._oauth2_client)) - async def create_store(self, **kwargs): # noqa: E501 + async def create_store(self, body, **kwargs): # noqa: E501 diff --git a/config/clients/python/patches/open_fga_api_sync.py.patch b/config/clients/python/patches/open_fga_api_sync.py.patch new file mode 100644 index 00000000..3c07ed7b --- /dev/null +++ b/config/clients/python/patches/open_fga_api_sync.py.patch @@ -0,0 +1,68 @@ +--- clients/fga-python-sdk/openfga_sdk/sync/open_fga_api.py 2022-09-13 14:15:46.000000000 -0400 ++++ open_fga_api.py 2022-09-13 14:14:01.000000000 -0400 +@@ -193,13 +193,15 @@ + _request_auth=local_var_params.get('_request_auth'), + _oauth2_client=self._oauth2_client) + +- def create_store(self, **kwargs): # noqa: E501 ++ def create_store(self, body, **kwargs): # noqa: E501 + """Create a store # noqa: E501 + + Create a unique OpenFGA store which will be used to store authorization models and relationship tuples. # noqa: E501 + +- >>> thread = api.create_store() ++ >>> thread = api.create_store(body) + ++ :param body: (required) ++ :type body: CreateStoreRequest + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will +@@ -216,15 +218,17 @@ + :rtype: CreateStoreResponse + """ + kwargs['_return_http_data_only'] = True +- return self.create_store_with_http_info(**kwargs) # noqa: E501 ++ return self.create_store_with_http_info(body, **kwargs) # noqa: E501 + +- def create_store_with_http_info(self, **kwargs): # noqa: E501 ++ def create_store_with_http_info(self, body, **kwargs): # noqa: E501 + """Create a store # noqa: E501 + + Create a unique OpenFGA store which will be used to store authorization models and relationship tuples. # noqa: E501 + +- >>> thread = api.create_store_with_http_info() ++ >>> thread = api.create_store_with_http_info(body) + ++ :param body: (required) ++ :type body: CreateStoreRequest + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _return_http_data_only: response data without head status code +@@ -253,6 +257,8 @@ + + all_params = [ + ++ 'body' ++ + ] + all_params.extend( + [ +@@ -312,7 +318,7 @@ + } + + return self.api_client.call_api( +- '/stores'.replace('{store_id}', store_id), 'POST', ++ '/stores', 'POST', + path_params, + query_params, + header_params, +@@ -998,7 +1004,7 @@ + } + + return self.api_client.call_api( +- '/stores'.replace('{store_id}', store_id), 'GET', ++ '/stores', 'GET', + path_params, + query_params, + header_params, diff --git a/config/clients/python/template/__init__sync.mustache b/config/clients/python/template/__init__sync.mustache new file mode 100644 index 00000000..cf7c1af2 --- /dev/null +++ b/config/clients/python/template/__init__sync.mustache @@ -0,0 +1,7 @@ +# coding: utf-8 + +# flake8: noqa +{{>partial_header}} + +from {{packageName}}.sync.client.client import OpenFgaClient +from {{packageName}}.sync.api_client import ApiClient diff --git a/config/clients/python/template/__init__sync_client.mustache b/config/clients/python/template/__init__sync_client.mustache new file mode 100644 index 00000000..6e4fd774 --- /dev/null +++ b/config/clients/python/template/__init__sync_client.mustache @@ -0,0 +1,4 @@ +# coding: utf-8 + +# flake8: noqa +{{>partial_header}} diff --git a/config/clients/python/template/api.mustache b/config/clients/python/template/api.mustache index 6f3b88ed..a74fe7d4 100644 --- a/config/clients/python/template/api.mustache +++ b/config/clients/python/template/api.mustache @@ -9,6 +9,7 @@ import re # noqa: F401 import six from {{packageName}}.api_client import ApiClient +from {{packageName}}.oauth2 import OAuth2Client from {{packageName}}.exceptions import ( # noqa: F401 FgaValidationException, ApiValueError @@ -28,7 +29,13 @@ class {{classname}}(object): api_client = ApiClient() self.api_client = api_client - {{#asyncio}} + self._oauth2_client = None + if api_client.configuration is not None: + credentials = api_client.configuration.credentials + if credentials is not None and credentials.method == 'client_credentials': + self._oauth2_client = OAuth2Client(credentials) + +{{#asyncio}} async def __aenter__(self): return self @@ -37,7 +44,17 @@ class {{classname}}(object): async def close(self): await self.api_client.close() - {{/asyncio}} +{{/asyncio}} +{{^asyncio}} + def __enter__(self): + return self + + def __exit__(self): + self.close() + + def close(self): + self.api_client.close() +{{/asyncio}} {{#operation}} @@ -49,10 +66,10 @@ class {{classname}}(object): {{/notes}} {{#sortParamsByRequiredFlag}} - >>> thread = await api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) + >>> thread = {{#asyncio}}await {{/asyncio}}api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) {{/sortParamsByRequiredFlag}} {{^sortParamsByRequiredFlag}} - >>> thread = await api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) + >>> thread = {{#asyncio}}await {{/asyncio}}api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) {{/sortParamsByRequiredFlag}} {{#requiredParams}} @@ -326,6 +343,7 @@ class {{classname}}(object): _host=local_var_host, {{/servers.0}} collection_formats=collection_formats, - _request_auth=local_var_params.get('_request_auth')){{#asyncio}}){{/asyncio}} + _request_auth=local_var_params.get('_request_auth'), + _oauth2_client=self._oauth2_client){{#asyncio}}){{/asyncio}} {{/operation}} {{/operations}} diff --git a/config/clients/python/template/api_client.mustache b/config/clients/python/template/api_client.mustache index 93c2c4c5..0279caab 100644 --- a/config/clients/python/template/api_client.mustache +++ b/config/clients/python/template/api_client.mustache @@ -4,6 +4,9 @@ {{#asyncio}} import asyncio {{/asyncio}} +{{^asyncio}} +import time +{{/asyncio}} import atexit import datetime from dateutil.parser import parse @@ -25,7 +28,7 @@ import tornado.gen from {{packageName}}.configuration import Configuration import {{modelPackage}} -from {{packageName}} import rest +from {{packageName}} import rest, oauth2 from {{packageName}}.exceptions import ApiValueError, ApiException, FgaValidationException, RateLimitExceededError @@ -149,7 +152,7 @@ class ApiClient(object): response_types_map=None, auth_settings=None, _return_http_data_only=None, collection_formats=None, _preload_content=True, _request_timeout=None, _host=None, - _request_auth=None, _retry_params=None): + _request_auth=None, _retry_params=None, _oauth2_client=None): self.configuration.is_valid() config = self.configuration @@ -192,7 +195,7 @@ class ApiClient(object): # auth setting {{#asyncio}}await {{/asyncio}}self.update_params_for_auth( header_params, query_params, auth_settings, - request_auth=_request_auth) + request_auth=_request_auth, oauth2_client=_oauth2_client) # body if body: @@ -222,7 +225,8 @@ class ApiClient(object): _request_timeout=_request_timeout) except RateLimitExceededError as e: if x < max_retry: - {{#asyncio}}await asyncio{{/asyncio}}.sleep(random_time(x, min_wait_in_ms)) + {{#asyncio}}await asyncio.sleep(random_time(x, min_wait_in_ms)){{/asyncio}} + {{^asyncio}}time.sleep(random_time(x, min_wait_in_ms)){{/asyncio}} continue e.body = e.body.decode('utf-8') if six.PY3 else e.body response_type = response_types_map.get(e.status, None) @@ -389,7 +393,7 @@ class ApiClient(object): async_req=None, _return_http_data_only=None, collection_formats=None,_preload_content=True, _request_timeout=None, _host=None, _request_auth=None, - _retry_params=None): + _retry_params=None, _oauth2_client=None): """Makes the HTTP request (synchronous) and returns deserialized data. To make an async_req request, set the async_req parameter. @@ -437,7 +441,7 @@ class ApiClient(object): response_types_map, auth_settings, _return_http_data_only, collection_formats, _preload_content, _request_timeout, _host, - _request_auth, _retry_params){{#asyncio}}){{/asyncio}} + _request_auth, _retry_params, _oauth2_client){{#asyncio}}){{/asyncio}} return self.pool.apply_async(self.__call_api, (resource_path, method, path_params, @@ -450,7 +454,9 @@ class ApiClient(object): collection_formats, _preload_content, _request_timeout, - _host, _request_auth, _retry_params)) + _host, _request_auth, + _retry_params, + _oauth2_client)) {{#asyncio}}async {{/asyncio}}def request(self, method, url, query_params=None, headers=None, post_params=None, body=None, _preload_content=True, @@ -581,7 +587,7 @@ class ApiClient(object): return content_types[0] {{#asyncio}}async {{/asyncio}}def update_params_for_auth(self, headers, queries, auth_settings, - request_auth=None): + request_auth=None, oauth2_client=None): """Updates header and query params based on authentication setting. :param headers: Header parameters dict to be updated. @@ -589,11 +595,20 @@ class ApiClient(object): :param auth_settings: Authentication setting identifiers list. :param request_auth: if set, the provided settings will override the token in the configuration. + :param oauth2_client: if set, will be used for credential exchange. """ - if self.configuration.credentials is not None: - added_headers = {{#asyncio}}await {{/asyncio}}self.configuration.credentials.get_authentication_header(self.rest_client) - for key, value in added_headers.items(): - headers[key] = value + credentials = self.configuration.credentials + if credentials is not None: + if credentials.method == 'none': + pass + if credentials.method == 'api_token': + headers['Authorization'] = 'Bearer {}'.format(credentials.configuration.api_token) + if credentials.method == 'client_credentials': + if oauth2_client is None: + oauth2_client = oauth2.OAuth2Client(credentials) + oauth2_headers = await oauth2_client.get_authentication_header(self.rest_client) + for key, value in oauth2_headers.items(): + headers[key] = value if not auth_settings: return diff --git a/config/clients/python/template/api_client_sync.mustache b/config/clients/python/template/api_client_sync.mustache new file mode 100644 index 00000000..991944fe --- /dev/null +++ b/config/clients/python/template/api_client_sync.mustache @@ -0,0 +1,739 @@ +# coding: utf-8 +{{>partial_header}} + +import time +import atexit +import datetime +from dateutil.parser import parse +import json +import math +import mimetypes +from multiprocessing.pool import ThreadPool +import os +import random +import re +import tempfile + +# python 2 and python 3 compatibility library +import six +from six.moves.urllib.parse import quote +{{#tornado}} +import tornado.gen +{{/tornado}} + +from {{packageName}}.configuration import Configuration +import {{modelPackage}} +from {{packageName}}.sync import rest, oauth2 +from {{packageName}}.exceptions import ApiValueError, ApiException, FgaValidationException, RateLimitExceededError + + +DEFAULT_USER_AGENT = '{{{userAgent}}}' + + +def random_time(loop_count, min_wait_in_ms): + """ + Helper function to return the time (in s) to wait before retry + """ + minimum = math.ceil(2 ** loop_count * min_wait_in_ms) + maximum = math.ceil(2 ** (loop_count + 1) * min_wait_in_ms) + return random.randrange(minimum, maximum) / 1000 + + +class ApiClient(object): + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + Do not edit the class manually. + + :param configuration: .Configuration object for this client + :param header_name: a header to pass when making calls to the API. + :param header_value: a header value to pass when making calls to + the API. + :param cookie: a cookie to include in the header when making calls + to the API + :param pool_threads: The number of threads to use for async requests + to the API. More threads means more concurrent API requests. + """ + + PRIMITIVE_TYPES = (float, bool, bytes, six.text_type) + six.integer_types + NATIVE_TYPES_MAPPING = { + 'int': int, + 'long': int if six.PY3 else long, # noqa: F821 + 'float': float, + 'str': str, + 'bool': bool, + 'date': datetime.date, + 'datetime': datetime.datetime, + 'object': object, + } + _pool = None + + def __init__(self, configuration=None, header_name=None, header_value=None, + cookie=None, pool_threads=1): + if configuration is None: + configuration = Configuration.get_default_copy() + self.configuration = configuration + self.pool_threads = pool_threads + + self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} + if header_name is not None: + self.default_headers[header_name] = header_value + self.cookie = cookie + # Set default User-Agent. + self.user_agent = DEFAULT_USER_AGENT + self.client_side_validation = configuration.client_side_validation + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + if self._pool: + self._pool.close() + self._pool.join() + self._pool = None + if hasattr(atexit, 'unregister'): + atexit.unregister(self.close) + + @property + def pool(self): + """Create thread pool on first request + avoids instantiating unused threadpool for blocking clients. + """ + if self._pool is None: + atexit.register(self.close) + self._pool = ThreadPool(self.pool_threads) + return self._pool + + @property + def user_agent(self): + """User agent for this API client""" + return self.default_headers['User-Agent'] + + @user_agent.setter + def user_agent(self, value): + self.default_headers['User-Agent'] = value + + def set_default_header(self, header_name, header_value): + self.default_headers[header_name] = header_value + + {{#tornado}} + @tornado.gen.coroutine + {{/tornado}} + def __call_api( + self, resource_path, method, path_params=None, + query_params=None, header_params=None, body=None, post_params=None, + response_types_map=None, auth_settings=None, + _return_http_data_only=None, collection_formats=None, + _preload_content=True, _request_timeout=None, _host=None, + _request_auth=None, _retry_params=None, _oauth2_client=None): + + self.configuration.is_valid() + config = self.configuration + + # header parameters + header_params = header_params or {} + header_params.update(self.default_headers) + if self.cookie: + header_params['Cookie'] = self.cookie + if header_params: + header_params = self.sanitize_for_serialization(header_params) + header_params = dict(self.parameters_to_tuples(header_params, + collection_formats)) + + # path parameters + if path_params: + path_params = self.sanitize_for_serialization(path_params) + path_params = self.parameters_to_tuples(path_params, + collection_formats) + for k, v in path_params: + # specified safe chars, encode everything + resource_path = resource_path.replace( + '{%s}' % k, + quote(str(v), safe=config.safe_chars_for_path_param) + ) + + # query parameters + if query_params: + query_params = self.sanitize_for_serialization(query_params) + query_params = self.parameters_to_tuples(query_params, + collection_formats) + + # post parameters + if post_params: + post_params = post_params if post_params else [] + post_params = self.sanitize_for_serialization(post_params) + post_params = self.parameters_to_tuples(post_params, + collection_formats) + + # auth setting + self.update_params_for_auth( + header_params, query_params, auth_settings, + request_auth=_request_auth, oauth2_client=_oauth2_client) + + # body + if body: + body = self.sanitize_for_serialization(body) + + # request url + if _host is None: + 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 + + max_retry = self.configuration.retry_params.max_retry if (self.configuration.retry_params is not None and self.configuration.retry_params.max_retry is not None) else 0 + min_wait_in_ms = self.configuration.retry_params.min_wait_in_ms if (self.configuration.retry_params is not None and self.configuration.retry_params.min_wait_in_ms is not None) else 0 + if _retry_params is not None: + if _retry_params.max_retry is not None: + max_retry = _retry_params.max_retry + if _retry_params.min_wait_in_ms is not None: + max_retry = _retry_params.min_wait_in_ms + for x in range(max_retry+1): + try: + # perform request and return response + response_data = {{#tornado}}yield {{/tornado}}self.request( + method, url, query_params=query_params, headers=header_params, + post_params=post_params, body=body, + _preload_content=_preload_content, + _request_timeout=_request_timeout) + except RateLimitExceededError as e: + if x < max_retry: + time.sleep(random_time(x, min_wait_in_ms)) + continue + e.body = e.body.decode('utf-8') if six.PY3 else e.body + response_type = response_types_map.get(e.status, None) + if response_type is not None: + e.parsed_exception = self.__deserialize(json.loads(e.body), response_type) + e.body = None + raise e + except ApiException as e: + e.body = e.body.decode('utf-8') if six.PY3 else e.body + response_type = response_types_map.get(e.status, None) + if response_type is not None: + e.parsed_exception = self.__deserialize(json.loads(e.body), response_type) + e.body = None + raise e + + self.last_response = response_data + + return_data = response_data + + if not _preload_content: + {{^tornado}} + return return_data + {{/tornado}} + {{#tornado}} + raise tornado.gen.Return(return_data) + {{/tornado}} + + response_type = response_types_map.get(response_data.status, None) + + if six.PY3 and response_type not in ["file", "bytes"]: + match = None + content_type = response_data.getheader('content-type') + if content_type is not None: + match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type) + encoding = match.group(1) if match else "utf-8" + if response_data.data is not None: + response_data.data = response_data.data.decode(encoding) + + # deserialize response data + + if response_type: + return_data = self.deserialize(response_data, response_type) + else: + return_data = None + +{{^tornado}} + if _return_http_data_only: + return (return_data) + else: + return (return_data, response_data.status, + response_data.getheaders()) +{{/tornado}} +{{#tornado}} + if _return_http_data_only: + raise tornado.gen.Return(return_data) + else: + raise tornado.gen.Return((return_data, response_data.status, + response_data.getheaders())) +{{/tornado}} + + def sanitize_for_serialization(self, obj): + """Builds a JSON POST object. + + If obj is None, return None. + If obj is str, int, long, float, bool, return directly. + If obj is datetime.datetime, datetime.date + convert to string in iso8601 format. + If obj is list, sanitize each element in the list. + If obj is dict, return the dict. + If obj is OpenAPI model, return the properties dict. + + :param obj: The data to serialize. + :return: The serialized form of data. + """ + if obj is None: + return None + elif isinstance(obj, self.PRIMITIVE_TYPES): + return obj + elif isinstance(obj, list): + return [self.sanitize_for_serialization(sub_obj) + for sub_obj in obj] + elif isinstance(obj, tuple): + return tuple(self.sanitize_for_serialization(sub_obj) + for sub_obj in obj) + elif isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + + if isinstance(obj, dict): + obj_dict = obj + else: + # Convert model obj to dict except + # attributes `openapi_types`, `attribute_map` + # and attributes which value is not None. + # Convert attribute name to json key in + # model definition for request. + obj_dict = {obj.attribute_map[attr]: getattr(obj, attr) + for attr, _ in six.iteritems(obj.openapi_types) + if getattr(obj, attr) is not None} + + return {key: self.sanitize_for_serialization(val) + for key, val in six.iteritems(obj_dict)} + + def deserialize(self, response, response_type): + """Deserializes response into an object. + + :param response: RESTResponse object to be deserialized. + :param response_type: class literal for + deserialized object, or string of class name. + + :return: deserialized object. + """ + + # fetch data from response object + try: + data = json.loads(response.data) + except ValueError: + data = response.data + + return self.__deserialize(data, response_type) + + def __deserialize(self, data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if type(klass) == str: + if klass.startswith('list['): + sub_kls = re.match(r'list\[(.*)\]', klass).group(1) + return [self.__deserialize(sub_data, sub_kls) + for sub_data in data] + + if klass.startswith('dict['): + sub_kls = re.match(r'dict\[([^,]*), (.*)\]', klass).group(2) + return {k: self.__deserialize(v, sub_kls) + for k, v in six.iteritems(data)} + + # convert str to class + if klass in self.NATIVE_TYPES_MAPPING: + klass = self.NATIVE_TYPES_MAPPING[klass] + else: + klass = getattr({{modelPackage}}, klass) + + if klass in self.PRIMITIVE_TYPES: + return self.__deserialize_primitive(data, klass) + elif klass == object: + return self.__deserialize_object(data) + elif klass == datetime.date: + return self.__deserialize_date(data) + elif klass == datetime.datetime: + return self.__deserialize_datetime(data) + else: + return self.__deserialize_model(data, klass) + + def call_api(self, resource_path, method, + path_params=None, query_params=None, header_params=None, + body=None, post_params=None, files=None, + response_types_map=None, auth_settings=None, + async_req=None, _return_http_data_only=None, + collection_formats=None,_preload_content=True, + _request_timeout=None, _host=None, _request_auth=None, + _retry_params=None, _oauth2_client=None): + """Makes the HTTP request (synchronous) and returns deserialized data. + + To make an async_req request, set the async_req parameter. + + :param resource_path: Path to method endpoint. + :param method: Method to call. + :param path_params: Path parameters in the url. + :param query_params: Query parameters in the url. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param auth_settings list: Auth Settings names for the request. + :param response: Response data type. + :param files dict: it will not be used + :param async_req bool: execute request asynchronously + :param _return_http_data_only: response data without head status code + and headers + :param collection_formats: dict of collection formats for path, query, + header, and post parameters. + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :param _retry_params: If specified, override the default retry parameters + :type _request_token: dict, optional + :return: + If async_req parameter is True, + the request will be called asynchronously. + The method will return the request thread. + If parameter async_req is False or missing, + then the method will return the response directly. + """ + if not async_req: + return self.__call_api(resource_path, method, + path_params, query_params, header_params, + body, post_params, + response_types_map, auth_settings, + _return_http_data_only, collection_formats, + _preload_content, _request_timeout, _host, + _request_auth, _retry_params, _oauth2_client) + + return self.pool.apply_async(self.__call_api, (resource_path, + method, path_params, + query_params, + header_params, body, + post_params, + response_types_map, + auth_settings, + _return_http_data_only, + collection_formats, + _preload_content, + _request_timeout, + _host, _request_auth, + _retry_params, + _oauth2_client)) + + def request(self, method, url, query_params=None, headers=None, + post_params=None, body=None, _preload_content=True, + _request_timeout=None): + """Makes the HTTP request using RESTClient.""" + if method == "GET": + return self.rest_client.GET(url, + query_params=query_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + headers=headers) + elif method == "HEAD": + return self.rest_client.HEAD(url, + query_params=query_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + headers=headers) + elif method == "OPTIONS": + return self.rest_client.OPTIONS(url, + query_params=query_params, + headers=headers, + _preload_content=_preload_content, + _request_timeout=_request_timeout) + elif method == "POST": + return self.rest_client.POST(url, + query_params=query_params, + headers=headers, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + elif method == "PUT": + return self.rest_client.PUT(url, + query_params=query_params, + headers=headers, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + elif method == "PATCH": + return self.rest_client.PATCH(url, + query_params=query_params, + headers=headers, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + elif method == "DELETE": + return self.rest_client.DELETE(url, + query_params=query_params, + headers=headers, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + body=body) + else: + raise ApiValueError( + "http method must be `GET`, `HEAD`, `OPTIONS`," + " `POST`, `PATCH`, `PUT` or `DELETE`." + ) + + def parameters_to_tuples(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: Parameters as list of tuples, collections formatted + """ + new_params = [] + if collection_formats is None: + collection_formats = {} + for k, v in six.iteritems(params) if isinstance(params, dict) else params: # noqa: E501 + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, value) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(str(value) for value in v))) + else: + new_params.append((k, v)) + return new_params + + def select_header_accept(self, accepts): + """Returns `Accept` based on an array of accepts provided. + + :param accepts: List of headers. + :return: Accept (e.g. application/json). + """ + if not accepts: + return + + accepts = [x.lower() for x in accepts] + + if 'application/json' in accepts: + return 'application/json' + else: + return ', '.join(accepts) + + def select_header_content_type(self, content_types, method=None, body=None): + """Returns `Content-Type` based on an array of content_types provided. + + :param content_types: List of content-types. + :param method: http method (e.g. POST, PATCH). + :param body: http body to send. + :return: Content-Type (e.g. application/json). + """ + if not content_types: + return None + + content_types = [x.lower() for x in content_types] + + if (method == 'PATCH' and + 'application/json-patch+json' in content_types and + isinstance(body, list)): + return 'application/json-patch+json' + + if 'application/json' in content_types or '*/*' in content_types: + return 'application/json' + else: + return content_types[0] + + def update_params_for_auth(self, headers, queries, auth_settings, + request_auth=None, oauth2_client=None): + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :param auth_settings: Authentication setting identifiers list. + :param request_auth: if set, the provided settings will + override the token in the configuration. + :param oauth2_client: if set, will be used for credential exchange. + """ + credentials = self.configuration.credentials + if credentials is not None: + if credentials.method == 'none': + pass + if credentials.method == 'api_token': + headers['Authorization'] = 'Bearer {}'.format(credentials.configuration.api_token) + if credentials.method == 'client_credentials': + if oauth2_client is None: + oauth2_client = oauth2.OAuth2Client(credentials) + oauth2_headers = oauth2_client.get_authentication_header(self.rest_client) + for key, value in oauth2_headers.items(): + headers[key] = value + + if not auth_settings: + return + + if request_auth: + self._apply_auth_params(headers, queries, request_auth) + return + + for auth in auth_settings: + auth_setting = self.configuration.auth_settings().get(auth) + if auth_setting: + self._apply_auth_params(headers, queries, auth_setting) + + def _apply_auth_params(self, headers, queries, auth_setting): + """Updates the request parameters based on a single auth_setting + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :param auth_setting: auth settings for the endpoint + """ + if auth_setting['in'] == 'cookie': + headers['Cookie'] = auth_setting['value'] + elif auth_setting['in'] == 'header': + headers[auth_setting['key']] = auth_setting['value'] + elif auth_setting['in'] == 'query': + queries.append((auth_setting['key'], auth_setting['value'])) + else: + raise ApiValueError( + 'Authentication token must be in `query` or `header`' + ) + + def __deserialize_primitive(self, data, klass): + """Deserializes string to primitive type. + + :param data: str. + :param klass: class literal. + + :return: int, long, float, str, bool. + """ + try: + return klass(data) + except UnicodeEncodeError: + return six.text_type(data) + except TypeError: + return data + + def __deserialize_object(self, value): + """Return an original value. + + :return: object. + """ + return value + + def __deserialize_date(self, string): + """Deserializes string to date. + + :param string: str. + :return: date. + """ + try: + return parse(string).date() + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason="Failed to parse `{0}` as date object".format(string) + ) + + def __deserialize_datetime(self, string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :return: datetime. + """ + try: + return parse(string) + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as datetime object" + .format(string) + ) + ) + + def __deserialize_model(self, data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :param klass: class literal. + :return: model object. + """ + has_discriminator = False + if (hasattr(klass, 'get_real_child_model') and klass.discriminator_value_class_map): + has_discriminator = True + + if not klass.openapi_types and has_discriminator is False: + return data + + kwargs = {} + if (data is not None and + klass.openapi_types is not None and + isinstance(data, (list, dict))): + for attr, attr_type in six.iteritems(klass.openapi_types): + if klass.attribute_map[attr] in data: + value = data[klass.attribute_map[attr]] + kwargs[attr] = self.__deserialize(value, attr_type) + + kwargs["local_vars_configuration"] = self.configuration + instance = klass(**kwargs) + + if has_discriminator: + klass_name = instance.get_real_child_model(data) + if klass_name: + instance = self.__deserialize(data, klass_name) + return instance + + def _get_store_id(self): + """ + Verify that the store id has been configured and not empty string. + It will return the store ID. + Otherwise, raise FgaValidationException + """ + configuration = self.configuration + if configuration.store_id is None or configuration.store_id == '': + raise FgaValidationException( + 'store_id is required but not configured' + ) + return configuration.store_id + + def set_store_id(self, value): + """ + Update the store ID in the configuration + """ + self.configuration.store_id = value + + def get_store_id(self): + """ + Return the store id (if any) store in the configuration + """ + return self.configuration.store_id diff --git a/config/clients/python/template/api_sync.mustache b/config/clients/python/template/api_sync.mustache new file mode 100644 index 00000000..ce401b68 --- /dev/null +++ b/config/clients/python/template/api_sync.mustache @@ -0,0 +1,338 @@ +# coding: utf-8 + +{{>partial_header}} + + +import re # noqa: F401 + +# python 2 and python 3 compatibility library +import six + +from {{packageName}}.sync.api_client import ApiClient +from {{packageName}}.sync.oauth2 import OAuth2Client +from {{packageName}}.exceptions import ( # noqa: F401 + FgaValidationException, + ApiValueError +) + + +{{#operations}} +class {{classname}}(object): + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None): + if api_client is None: + api_client = ApiClient() + self.api_client = api_client + + self._oauth2_client = None + if api_client.configuration is not None: + credentials = api_client.configuration.credentials + if credentials is not None and credentials.method == 'client_credentials': + self._oauth2_client = OAuth2Client(credentials) + + + def __enter__(self): + return self + + def __exit__(self): + self.close() + + def close(self): + self.api_client.close() + +{{#operation}} + + def {{operationId}}(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): # noqa: E501 + """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} # noqa: E501 + +{{#notes}} + {{{.}}} # noqa: E501 +{{/notes}} + +{{#sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} +{{^sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} + +{{#requiredParams}} +{{^-first}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) + :type {{paramName}}: {{dataType}} +{{/-first}} +{{/requiredParams}} +{{#optionalParams}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) + :type {{paramName}}: {{dataType}}, optional +{{/optionalParams}} + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: {{returnType}}{{^returnType}}None{{/returnType}} + """ + kwargs['_return_http_data_only'] = True + return self.{{operationId}}_with_http_info({{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs) # noqa: E501 + + def {{operationId}}_with_http_info(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): # noqa: E501 + """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} # noqa: E501 + +{{#notes}} + {{{.}}} # noqa: E501 +{{/notes}} + +{{#sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} +{{^sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} + +{{#requiredParams}} +{{^-first}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) + :type {{paramName}}: {{dataType}} +{{/-first}} +{{/requiredParams}} +{{#optionalParams}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) + :type {{paramName}}: {{dataType}}, optional +{{/optionalParams}} + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _return_http_data_only: response data without head status code + and headers + :type _return_http_data_only: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :param _retry_param: if specified, override the retry parameters specified in configuration + :type _request_auth: dict, optional + :type _content_type: string, optional: force content-type for the request + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: {{#returnType}}tuple({{.}}, status_code(int), headers(HTTPHeaderDict)){{/returnType}}{{^returnType}}None{{/returnType}} + """ + + {{#servers.0}} + local_var_hosts = [ +{{#servers}} + '{{{url}}}'{{^-last}},{{/-last}} +{{/servers}} + ] + local_var_host = local_var_hosts[0] + if kwargs.get('_host_index'): + _host_index = int(kwargs.get('_host_index')) + if _host_index < 0 or _host_index >= len(local_var_hosts): + raise ApiValueError( + "Invalid host index. Must be 0 <= index < %s" + % len(local_var_host) + ) + local_var_host = local_var_hosts[_host_index] + {{/servers.0}} + local_var_params = locals() + + all_params = [ +{{#requiredParams}}{{^-first}} + '{{paramName}}'{{^-last}},{{/-last}} +{{/-first}}{{/requiredParams}} +{{#optionalParams}} + '{{paramName}}'{{^-last}},{{/-last}} +{{/optionalParams}} + ] + all_params.extend( + [ + 'async_req', + '_return_http_data_only', + '_preload_content', + '_request_timeout', + '_request_auth', + '_content_type', + '_headers', + '_retry_parms' + ] + ) + + for key, val in six.iteritems(local_var_params['kwargs']): + if key not in all_params{{#servers.0}} and key != "_host_index"{{/servers.0}}: + raise FgaValidationException( + "Got an unexpected keyword argument '%s'" + " to method {{operationId}}" % key + ) + local_var_params[key] = val + del local_var_params['kwargs'] +{{#allParams}} +{{^isNullable}} +{{#required}} +{{^-first}} + # verify the required parameter '{{paramName}}' is set + if self.api_client.client_side_validation and local_var_params.get('{{paramName}}') is None: # noqa: E501 + raise ApiValueError("Missing the required parameter `{{paramName}}` when calling `{{operationId}}`") # noqa: E501 +{{/-first}} +{{/required}} +{{/isNullable}} +{{/allParams}} + +{{#allParams}} +{{#hasValidation}} + {{#maxLength}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and # noqa: E501 + len(local_var_params['{{paramName}}']) > {{maxLength}}): # noqa: E501 + raise ApiValueError("Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be less than or equal to `{{maxLength}}`") # noqa: E501 + {{/maxLength}} + {{#minLength}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and # noqa: E501 + len(local_var_params['{{paramName}}']) < {{minLength}}): # noqa: E501 + raise ApiValueError("Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be greater than or equal to `{{minLength}}`") # noqa: E501 + {{/minLength}} + {{#maximum}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}: # noqa: E501 + raise ApiValueError("Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value less than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}`{{maximum}}`") # noqa: E501 + {{/maximum}} + {{#minimum}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}: # noqa: E501 + raise ApiValueError("Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}`{{minimum}}`") # noqa: E501 + {{/minimum}} + {{#pattern}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and not re.search(r'{{{vendorExtensions.x-regex}}}', local_var_params['{{paramName}}']{{#vendorExtensions.x-modifiers}}{{#-first}}, flags={{/-first}}re.{{.}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-modifiers}}): # noqa: E501 + raise ApiValueError("Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must conform to the pattern `{{{pattern}}}`") # noqa: E501 + {{/pattern}} + {{#maxItems}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and # noqa: E501 + len(local_var_params['{{paramName}}']) > {{maxItems}}): # noqa: E501 + raise ApiValueError("Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be less than or equal to `{{maxItems}}`") # noqa: E501 + {{/maxItems}} + {{#minItems}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and # noqa: E501 + len(local_var_params['{{paramName}}']) < {{minItems}}): # noqa: E501 + raise ApiValueError("Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be greater than or equal to `{{minItems}}`") # noqa: E501 + {{/minItems}} +{{/hasValidation}} +{{#-last}} +{{/-last}} +{{/allParams}} + collection_formats = {} + + path_params = {} +{{#pathParams}} +{{^-first}} + if '{{paramName}}' in local_var_params: + path_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} # noqa: E501 + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} # noqa: E501 +{{/-first}} + +{{#-first}} + if self.api_client._get_store_id() is None: + raise ApiValueError("Store ID expected in api_client's configuration when calling `{{operationId}}`") # noqa: E501 + store_id = self.api_client._get_store_id() +{{/-first}} + +{{/pathParams}} + + query_params = [] +{{#queryParams}} + if local_var_params.get('{{paramName}}') is not None: # noqa: E501 + query_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{#isArray}} # noqa: E501 + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} # noqa: E501 +{{/queryParams}} + + header_params = dict(local_var_params.get('_headers', {})) +{{#headerParams}} + if '{{paramName}}' in local_var_params: + header_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} # noqa: E501 + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} # noqa: E501 +{{/headerParams}} + + form_params = [] + local_var_files = {} +{{#formParams}} + if '{{paramName}}' in local_var_params: + {{^isFile}}form_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{/isFile}}{{#isFile}}local_var_files['{{baseName}}'] = local_var_params['{{paramName}}']{{/isFile}}{{#isArray}} # noqa: E501 + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} # noqa: E501 +{{/formParams}} + + body_params = None +{{#bodyParam}} + if '{{paramName}}' in local_var_params: + body_params = local_var_params['{{paramName}}'] +{{/bodyParam}} + {{#hasProduces}} + # HTTP header `Accept` + header_params['Accept'] = self.api_client.select_header_accept( + [{{#produces}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}]) # noqa: E501 + + {{/hasProduces}} + {{#hasConsumes}} + # HTTP header `Content-Type` + content_types_list = local_var_params.get('_content_type', self.api_client.select_header_content_type([{{#consumes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/consumes}}],'{{httpMethod}}', body_params)) # noqa: E501 + if content_types_list: + header_params['Content-Type'] = content_types_list + + {{/hasConsumes}} + # Authentication setting + auth_settings = [{{#authMethods}}'{{name}}'{{^-last}}, {{/-last}}{{/authMethods}}] # noqa: E501 + + {{#returnType}} + {{#responses}} + {{#-first}} + response_types_map = { + {{/-first}} + {{^isWildcard}} + {{code}}: {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}}, + {{/isWildcard}} + {{#-last}} + } + {{/-last}} + {{/responses}} + {{/returnType}} + {{^returnType}} + response_types_map = {} + {{/returnType}} + + return self.api_client.call_api( + '{{{path}}}'.replace('{store_id}', store_id), '{{httpMethod}}', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_types_map=response_types_map, + auth_settings=auth_settings, + async_req=local_var_params.get('async_req'), + _return_http_data_only=local_var_params.get('_return_http_data_only'), # noqa: E501 + _preload_content=local_var_params.get('_preload_content', True), + _request_timeout=local_var_params.get('_request_timeout'), + _retry_params=local_var_params.get('_retry_params'), + {{#servers.0}} + _host=local_var_host, + {{/servers.0}} + collection_formats=collection_formats, + _request_auth=local_var_params.get('_request_auth'), + _oauth2_client=self._oauth2_client) +{{/operation}} +{{/operations}} diff --git a/config/clients/python/template/api_test.mustache b/config/clients/python/template/api_test.mustache index c81cf442..6b1ba38b 100644 --- a/config/clients/python/template/api_test.mustache +++ b/config/clients/python/template/api_test.mustache @@ -102,7 +102,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( tuple_key=TupleKey( @@ -112,7 +112,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", ) - api_response = await api_instance.check( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_response, CheckResponse) @@ -129,7 +129,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') async def test_create_store(self, mock_request): @@ -146,12 +146,12 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CreateStoreRequest( name="test-store", ) - api_response = await api_instance.create_store( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.create_store( body=body, ) self.assertIsInstance(api_response, CreateStoreResponse) @@ -166,7 +166,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') async def test_delete_store(self, mock_request): @@ -178,9 +178,9 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) - await api_instance.delete_store() + {{#asyncio}}await {{/asyncio}}api_instance.delete_store() mock_request.assert_called_once_with( 'DELETE', 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4', @@ -190,7 +190,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') async def test_expand(self, mock_request): @@ -204,7 +204,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = ExpandRequest( tuple_key=TupleKey( @@ -213,7 +213,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", ) - api_response = await api_instance.expand( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.expand( body=body, ) self.assertIsInstance(api_response, ExpandResponse) @@ -233,7 +233,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') async def test_get_store(self, mock_request): @@ -251,10 +251,10 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) # Get a store - api_response = await api_instance.get_store() + api_response = {{#asyncio}}await {{/asyncio}}api_instance.get_store() self.assertIsInstance(api_response, GetStoreResponse) self.assertEqual(api_response.id, '01H0H015178Y2V4CX10C2KGHF4') self.assertEqual(api_response.name, 'test_store') @@ -266,7 +266,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') async def test_list_objects(self, mock_request): @@ -284,7 +284,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = ListObjectsRequest( authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", @@ -293,7 +293,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) # Get all stores - api_response = await api_instance.list_objects(body) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.list_objects(body) self.assertIsInstance(api_response, ListObjectsResponse) self.assertEqual(api_response.objects, ['document:abcd1234']) mock_request.assert_called_once_with( @@ -307,7 +307,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') async def test_list_stores(self, mock_request): @@ -338,10 +338,10 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ''' mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) # Get all stores - api_response = await api_instance.list_stores( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.list_stores( page_size=1, continuation_token="continuation_token_example", ) @@ -374,7 +374,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') async def test_read(self, mock_request): @@ -399,7 +399,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = ReadRequest( tuple_key=TupleKey( @@ -410,7 +410,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): page_size=50, continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", ) - api_response = await api_instance.read( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read( body=body, ) self.assertIsInstance(api_response, ReadResponse) @@ -453,9 +453,9 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) - api_response = await api_instance.read_assertions( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_assertions( "01G5JAVJ41T49E9TT3SKVS7X1J", ) self.assertIsInstance(api_response, ReadAssertionsResponse) @@ -521,12 +521,12 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) # Return a particular version of an authorization model - api_response = await api_instance.read_authorization_model( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_model( "01G5JAVJ41T49E9TT3SKVS7X1J", ) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) @@ -589,12 +589,12 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) # Return a particular version of an authorization model - api_response = await api_instance.read_changes( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_changes( page_size=1, continuation_token="abcdefg", type="document" @@ -628,7 +628,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) @@ -646,7 +646,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", ) - await api_instance.write( + {{#asyncio}}await {{/asyncio}}api_instance.write( body, ) mock_request.assert_called_once_with( @@ -671,7 +671,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) @@ -689,7 +689,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", ) - await api_instance.write( + {{#asyncio}}await {{/asyncio}}api_instance.write( body, ) mock_request.assert_called_once_with( @@ -714,7 +714,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) @@ -732,7 +732,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ], ) # Upsert assertions for an authorization model ID - await api_instance.write_assertions( + {{#asyncio}}await {{/asyncio}}api_instance.write_assertions( authorization_model_id="xyz0123", body=body, ) @@ -757,7 +757,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration configuration.store_id = store_id - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) @@ -787,7 +787,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ], ) # Create a new authorization model - api_response = await api_instance.write_authorization_model( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.write_authorization_model( body ) self.assertIsInstance(api_response, WriteAuthorizationModelResponse) @@ -887,7 +887,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): """ Test whether ApiValueError is raised if host has query """ - configuration = openfga_sdk.Configuration( + configuration = {{packageName}}.Configuration( api_host='localhost', api_scheme='http', store_id="abcd" @@ -905,13 +905,13 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ) configuration.store_id = 'xyz123' # Enter a context with an instance of the API client - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) # expects FgaValidationException to be thrown because api_scheme is bad with self.assertRaises(ApiValueError): - await api_instance.read_authorization_models( + {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_models( page_size= 1, continuation_token= "abcdefg" ) @@ -927,13 +927,13 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ) # Notice the store_id is not set # Enter a context with an instance of the API client - async with openfga_sdk.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = open_fga_api.OpenFgaApi(api_client) # expects FgaValidationException to be thrown because store_id is not specified with self.assertRaises(FgaValidationException): - await api_instance.read_authorization_models( + {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_models( page_size= 1, continuation_token= "abcdefg" ) @@ -953,7 +953,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( tuple_key=TupleKey( @@ -963,7 +963,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), ) with self.assertRaises(ValidationException) as api_exception: - await api_instance.check( + {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_exception.exception.parsed_exception, ValidationErrorMessageResponse) @@ -987,7 +987,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( tuple_key=TupleKey( @@ -997,7 +997,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), ) with self.assertRaises(NotFoundException) as api_exception: - await api_instance.check( + {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_exception.exception.parsed_exception, PathUnknownErrorMessageResponse) @@ -1022,7 +1022,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id configuration.retry_params = retry - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( tuple_key=TupleKey( @@ -1032,7 +1032,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), ) with self.assertRaises(RateLimitExceededError) as api_exception: - await api_instance.check( + {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_exception.exception, RateLimitExceededError) @@ -1058,7 +1058,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id configuration.retry_params = retry - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( tuple_key=TupleKey( @@ -1067,7 +1067,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), ) - api_response = await api_instance.check( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_response, CheckResponse) @@ -1091,7 +1091,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( tuple_key=TupleKey( @@ -1101,7 +1101,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), ) with self.assertRaises(ServiceException) as api_exception: - await api_instance.check( + {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_exception.exception.parsed_exception, InternalErrorMessageResponse) @@ -1122,7 +1122,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id configuration.credentials = Credentials(method='api_token', configuration=CredentialConfiguration(api_token='TOKEN1')) - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( tuple_key=TupleKey( @@ -1131,7 +1131,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), ) - api_response = await api_instance.check( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_response, CheckResponse) @@ -1162,7 +1162,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - async with {{packageName}}.ApiClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_client.set_default_header("Custom Header", "custom value") api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( @@ -1172,7 +1172,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), ) - api_response = await api_instance.check( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) self.assertIsInstance(api_response, CheckResponse) diff --git a/config/clients/python/template/api_test_sync.mustache b/config/clients/python/template/api_test_sync.mustache new file mode 100644 index 00000000..30a0ad44 --- /dev/null +++ b/config/clients/python/template/api_test_sync.mustache @@ -0,0 +1,1195 @@ +# coding: utf-8 + +{{>partial_header}} + +import unittest +from unittest.mock import ANY +from unittest import IsolatedAsyncioTestCase +from mock import patch +from datetime import datetime + +import urllib3 + +import {{packageName}}.sync +from {{packageName}}.sync import rest, open_fga_api +from {{packageName}}.sync.api_client import ApiClient +from {{packageName}}.credentials import Credentials, CredentialConfiguration +from {{packageName}}.configuration import Configuration +from {{packageName}}.exceptions import FgaValidationException, ApiValueError, NotFoundException, RateLimitExceededError, ServiceException, ValidationException, FGA_REQUEST_ID +from {{packageName}}.models.assertion import Assertion +from {{packageName}}.models.authorization_model import AuthorizationModel +from {{packageName}}.models.check_request import CheckRequest +from {{packageName}}.models.check_response import CheckResponse +from {{packageName}}.models.create_store_request import CreateStoreRequest +from {{packageName}}.models.create_store_response import CreateStoreResponse +from {{packageName}}.models.error_code import ErrorCode +from {{packageName}}.models.expand_request import ExpandRequest +from {{packageName}}.models.expand_response import ExpandResponse +from {{packageName}}.models.get_store_response import GetStoreResponse +from {{packageName}}.models.internal_error_code import InternalErrorCode +from {{packageName}}.models.internal_error_message_response import InternalErrorMessageResponse +from {{packageName}}.models.leaf import Leaf +from {{packageName}}.models.list_objects_request import ListObjectsRequest +from {{packageName}}.models.list_objects_response import ListObjectsResponse +from {{packageName}}.models.list_stores_response import ListStoresResponse +from {{packageName}}.models.node import Node +from {{packageName}}.models.not_found_error_code import NotFoundErrorCode +from {{packageName}}.models.object_relation import ObjectRelation +from {{packageName}}.models.path_unknown_error_message_response import PathUnknownErrorMessageResponse +from {{packageName}}.models.read_assertions_response import ReadAssertionsResponse +from {{packageName}}.models.read_authorization_model_response import ReadAuthorizationModelResponse +from {{packageName}}.models.read_changes_response import ReadChangesResponse +from {{packageName}}.models.read_request import ReadRequest +from {{packageName}}.models.read_response import ReadResponse +from {{packageName}}.models.store import Store +from {{packageName}}.models.tuple import Tuple +from {{packageName}}.models.tuple_change import TupleChange +from {{packageName}}.models.tuple_key import TupleKey +from {{packageName}}.models.tuple_keys import TupleKeys +from {{packageName}}.models.tuple_operation import TupleOperation +from {{packageName}}.models.type_definition import TypeDefinition +from {{packageName}}.models.users import Users +from {{packageName}}.models.userset import Userset +from {{packageName}}.models.userset_tree import UsersetTree +from {{packageName}}.models.usersets import Usersets +from {{packageName}}.models.validation_error_message_response import ValidationErrorMessageResponse +from {{packageName}}.models.write_assertions_request import WriteAssertionsRequest +from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest +from {{packageName}}.models.write_authorization_model_response import WriteAuthorizationModelResponse +from {{packageName}}.models.write_request import WriteRequest + +store_id = '01H0H015178Y2V4CX10C2KGHF4' +request_id = 'x1y2z3' + +# Helper function to construct mock response +def http_mock_response(body, status): + headers = urllib3.response.HTTPHeaderDict({ + 'content-type': 'application/json', + 'Fga-Request-Id': request_id + }) + return urllib3.HTTPResponse( + body.encode('utf-8'), + headers, + status, + preload_content=False + ) + +def mock_response(body, status): + obj = http_mock_response(body, status) + return rest.RESTResponse(obj, obj.data) + +class TestOpenFgaApiSync(IsolatedAsyncioTestCase): + """openfga_sdk.sync.OpenFgaApi unit test stubs""" + + def setUp(self): + self.configuration = Configuration( + api_scheme='http', + api_host="api.{{sampleApiDomain}}", + ) + + def tearDown(self): + pass + + @patch.object(rest.RESTClientObject, 'request') + async def test_check(self, mock_request): + """Test case for check + + Check whether a user is authorized to access an object # noqa: E501 + """ + + # First, mock the response + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", + ) + api_response = api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_create_store(self, mock_request): + """Test case for create_store + + Create a store # noqa: E501 + """ + response_body = '''{ + "id": "01YCP46JKYM8FJCQ37NMBYHE5X", + "name": "test_store", + "created_at": "2022-07-25T17:41:26.607Z", + "updated_at": "2022-07-25T17:41:26.607Z"} + ''' + mock_request.return_value = mock_response(response_body, 201) + + configuration = self.configuration + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CreateStoreRequest( + name="test-store", + ) + api_response = api_instance.create_store( + body=body, + ) + self.assertIsInstance(api_response, CreateStoreResponse) + self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores', + headers=ANY, + query_params=[], + post_params=[], + body={"name": "test-store"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_delete_store(self, mock_request): + """Test case for delete_store + + Delete a store # noqa: E501 + """ + response_body = '' + mock_request.return_value = mock_response(response_body, 201) + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + api_instance.delete_store() + mock_request.assert_called_once_with( + 'DELETE', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4', + headers=ANY, + query_params=[], + body=None, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_expand(self, mock_request): + """Test case for expand + + Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship # noqa: E501 + """ + response_body = '''{ + "tree": {"root": {"name": "document:budget#reader", "leaf": {"users": {"users": ["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]}}}}} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = ExpandRequest( + tuple_key=TupleKey( + object="document:budget", + relation="reader", + ), + authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", + ) + api_response = api_instance.expand( + body=body, + ) + self.assertIsInstance(api_response, ExpandResponse) + curUsers = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) + leaf = Leaf(users=curUsers) + node = Node(name="document:budget#reader", leaf=leaf) + userTree = UsersetTree(node) + expected_response = ExpandResponse(userTree) + self.assertEqual(api_response, expected_response) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/expand', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:budget", "relation": "reader"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_get_store(self, mock_request): + """Test case for get_store + + Get a store # noqa: E501 + """ + response_body = '''{ + "id": "01H0H015178Y2V4CX10C2KGHF4", + "name": "test_store", + "created_at": "2022-07-25T20:45:10.485Z", + "updated_at": "2022-07-25T20:45:10.485Z" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + # Get a store + api_response = api_instance.get_store() + self.assertIsInstance(api_response, GetStoreResponse) + self.assertEqual(api_response.id, '01H0H015178Y2V4CX10C2KGHF4') + self.assertEqual(api_response.name, 'test_store') + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_list_objects(self, mock_request): + """Test case for list_objects + + List objects # noqa: E501 + """ + response_body = ''' +{ + "objects": [ + "document:abcd1234" + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = ListObjectsRequest( + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + type="document", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + # Get all stores + api_response = api_instance.list_objects(body) + self.assertIsInstance(api_response, ListObjectsResponse) + self.assertEqual(api_response.objects, ['document:abcd1234']) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-objects', + headers=ANY, + query_params=[], + post_params=[], + body={'authorization_model_id': '01G5JAVJ41T49E9TT3SKVS7X1J', + 'type': 'document', 'relation': 'reader', 'user': 'user:81684243-9356-4421-8fbf-a4f8d36aa31b'}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_list_stores(self, mock_request): + """Test case for list_stores + + Get all stores # noqa: E501 + """ + response_body = ''' +{ + "stores": [ + { + "id": "01YCP46JKYM8FJCQ37NMBYHE5X", + "name": "store1", + "created_at": "2022-07-25T21:15:37.524Z", + "updated_at": "2022-07-25T21:15:37.524Z", + "deleted_at": "2022-07-25T21:15:37.524Z" + }, + { + "id": "01YCP46JKYM8FJCQ37NMBYHE6X", + "name": "store2", + "created_at": "2022-07-25T21:15:37.524Z", + "updated_at": "2022-07-25T21:15:37.524Z", + "deleted_at": "2022-07-25T21:15:37.524Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + # Get all stores + api_response = api_instance.list_stores( + page_size=1, + continuation_token="continuation_token_example", + ) + self.assertIsInstance(api_response, ListStoresResponse) + self.assertEqual(api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") + store1 = Store( + id="01YCP46JKYM8FJCQ37NMBYHE5X", + name="store1", + created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + ) + store2 = Store( + id="01YCP46JKYM8FJCQ37NMBYHE6X", + name="store2", + created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + ) + + stores = [store1, store2] + self.assertEqual(api_response.stores, stores) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores', + headers=ANY, + query_params=[('page_size', 1), ('continuation_token', + 'continuation_token_example')], + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_read(self, mock_request): + """Test case for read + + Get tuples from the store that matches a query, without following userset rewrite rules # noqa: E501 + """ + response_body = ''' + { + "tuples": [ + { + "key": { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = ReadRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + page_size=50, + continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) + api_response = api_instance.read( + body=body, + ) + self.assertIsInstance(api_response, ReadResponse) + key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",relation="reader",object="document:2021-budget") + timestamp = datetime.fromisoformat("2021-10-06T15:32:11.128+00:00") + expected_data = ReadResponse(tuples=[Tuple(key=key, timestamp=timestamp)]) + self.assertEqual(api_response, expected_data) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/read', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"},"page_size":50,"continuation_token":"eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_read_assertions(self, mock_request): + """Test case for read_assertions + + Read assertions for an authorization model ID # noqa: E501 + """ + response_body = ''' +{ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "assertions": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + }, + "expectation": true + } + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + api_response = api_instance.read_assertions( + "01G5JAVJ41T49E9TT3SKVS7X1J", + ) + self.assertIsInstance(api_response, ReadAssertionsResponse) + self.assertEqual(api_response.authorization_model_id, '01G5JAVJ41T49E9TT3SKVS7X1J') + assertion=Assertion( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + expectation=True, + ) + self.assertEqual(api_response.assertions, [assertion]) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_read_authorization_model(self, mock_request): + """Test case for read_authorization_model + + Return a particular version of an authorization model # noqa: E501 + """ + response_body = ''' +{ + "authorization_model": { + "id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "schema_version":"1.1", + "type_definitions": [ + { + "type": "document", + "relations": { + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "object": "", + "relation": "writer" + } + } + ] + } + }, + "writer": { + "this": {} + } + } + } + ] + } +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # Return a particular version of an authorization model + api_response = api_instance.read_authorization_model( + "01G5JAVJ41T49E9TT3SKVS7X1J", + ) + self.assertIsInstance(api_response, ReadAuthorizationModelResponse) + type_definitions = [ + TypeDefinition( + type="document", + relations=dict( + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + writer=Userset( + this=dict(), + ), + ) + ) + ] + authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version = "1.1", + type_definitions=type_definitions) + self.assertEqual(api_response.authorization_model, authorization_model) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_read_changes(self, mock_request): + """Test case for read_changes + + Return a list of all the tuple changes # noqa: E501 + """ + response_body = ''' +{ + "changes": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + }, + "operation": "TUPLE_OPERATION_WRITE", + "timestamp": "2022-07-26T15:55:55.809Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # Return a particular version of an authorization model + api_response = api_instance.read_changes( + page_size=1, + continuation_token="abcdefg", + type="document" + ) + self.assertIsInstance(api_response, ReadChangesResponse) + changes = TupleChange( + tuple_key=TupleKey(object="document:2021-budget",relation="reader",user="user:81684243-9356-4421-8fbf-a4f8d36aa31b"), + operation=TupleOperation.WRITE, + timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00")) + read_changes = ReadChangesResponse( + continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', + changes=[changes]) + self.assertEqual(api_response, read_changes) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/changes', + headers=ANY, + query_params=[('type', 'document'), ('page_size', 1), ('continuation_token', 'abcdefg') ], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write(self, mock_request): + """Test case for write + + Add tuples from the store # noqa: E501 + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + + body = WriteRequest( + writes=TupleKeys( + tuple_keys=[ + TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ), + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + ) + api_instance.write( + body, + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write_delete(self, mock_request): + """Test case for write + + Delete tuples from the store # noqa: E501 + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + + body = WriteRequest( + deletes=TupleKeys( + tuple_keys=[ + TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ), + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + ) + api_instance.write( + body, + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + headers=ANY, + query_params=[], + post_params=[], + body={"deletes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write_assertions(self, mock_request): + """Test case for write_assertions + + Upsert assertions for an authorization model ID # noqa: E501 + """ + response_body = '' + mock_request.return_value = mock_response(response_body, 204) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + body = WriteAssertionsRequest( + assertions=[ + Assertion( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + expectation=True, + ) + ], + ) + # Upsert assertions for an authorization model ID + api_instance.write_assertions( + authorization_model_id="xyz0123", + body=body, + ) + mock_request.assert_called_once_with( + 'PUT', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/xyz0123', + headers=ANY, + query_params=[], + post_params=[], + body={"assertions":[{"expectation":True,"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}]}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write_authorization_model(self, mock_request): + """Test case for write_authorization_model + + Create a new authorization model # noqa: E501 + """ + response_body = '{"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}' + mock_request.return_value = mock_response(response_body, 201) + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + body = WriteAuthorizationModelRequest( + schema_version = "1.1", + type_definitions=[ + TypeDefinition( + type="document", + relations=dict( + writer=Userset( + this=dict(), + ), + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + ) + ), + ], + ) + # Create a new authorization model + api_response = api_instance.write_authorization_model( + body + ) + self.assertIsInstance(api_response, WriteAuthorizationModelResponse) + expected_response = WriteAuthorizationModelResponse( + authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' + ) + self.assertEqual(api_response, expected_response) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models', + headers=ANY, + query_params=[], + post_params=[], + body={"schema_version":"1.1","type_definitions":[{"type":"document","relations":{"writer":{"this":{}},"reader":{"union":{"child":[{"this":{}},{"computedUserset":{"object":"","relation":"writer"}}]}}}}]}, + _preload_content=ANY, + _request_timeout=None + ) + + def test_default_scheme(self): + """ + Ensure default scheme is https + """ + configuration = Configuration( + api_host='localhost' + ) + self.assertEqual(configuration.api_scheme, 'https') + + def test_host_port(self): + """ + Ensure host has port will not raise error + """ + configuration = Configuration( + api_host='localhost:3000' + ) + self.assertEqual(configuration.api_host, 'localhost:3000') + + def test_configuration_missing_host(self): + """ + Test whether FgaValidationException is raised if configuration does not have host specified + """ + configuration = Configuration( + api_scheme='http' + ) + self.assertRaises(FgaValidationException, configuration.is_valid) + + def test_configuration_missing_scheme(self): + """ + Test whether FgaValidationException is raised if configuration does not have scheme specified + """ + configuration = Configuration( + api_host='localhost' + ) + configuration.api_scheme = None + self.assertRaises(FgaValidationException, configuration.is_valid) + + def test_configuration_bad_scheme(self): + """ + Test whether ApiValueError is raised if scheme is bad + """ + configuration = Configuration( + api_host='localhost', + api_scheme='foo' + ) + self.assertRaises(ApiValueError, configuration.is_valid) + + def test_configuration_bad_host(self): + """ + Test whether ApiValueError is raised if host is bad + """ + configuration = Configuration( + api_host='/', + api_scheme='foo' + ) + self.assertRaises(ApiValueError, configuration.is_valid) + + def test_configuration_has_path(self): + """ + Test whether ApiValueError is raised if host has path + """ + configuration = Configuration( + api_host='localhost/mypath', + api_scheme='http' + ) + self.assertRaises(ApiValueError, configuration.is_valid) + + def test_configuration_has_query(self): + """ + Test whether ApiValueError is raised if host has query + """ + configuration = Configuration( + api_host='localhost?mypath=foo', + api_scheme='http' + ) + self.assertRaises(ApiValueError, configuration.is_valid) + + def test_configuration_store_id_invalid(self): + """ + Test whether ApiValueError is raised if host has query + """ + configuration = Configuration( + api_host='localhost', + api_scheme='http', + store_id="abcd" + ) + self.assertRaises(FgaValidationException, configuration.is_valid) + + async def test_bad_configuration_read_authorization_model(self): + """ + Test whether FgaValidationException is raised for API (reading authorization models) + with configuration is having incorrect API scheme + """ + configuration = Configuration( + api_scheme = 'bad', + api_host = "api.{{sampleApiDomain}}", + ) + configuration.store_id = 'xyz123' + # Enter a context with an instance of the API client + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # expects FgaValidationException to be thrown because api_scheme is bad + with self.assertRaises(ApiValueError): + api_instance.read_authorization_models( + page_size= 1, + continuation_token= "abcdefg" + ) + + async def test_configuration_missing_storeid(self): + """ + Test whether FgaValidationException is raised for API (reading authorization models) + required store ID but configuration is missing store ID + """ + configuration = Configuration( + api_scheme = 'http', + api_host = "api.{{sampleApiDomain}}", + ) + # Notice the store_id is not set + # Enter a context with an instance of the API client + with ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # expects FgaValidationException to be thrown because store_id is not specified + with self.assertRaises(FgaValidationException): + api_instance.read_authorization_models( + page_size= 1, + continuation_token= "abcdefg" + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_400_error(self, mock_request): + """ + Test to ensure 400 errors are handled properly + """ + response_body = ''' +{ + "code": "validation_error", + "message": "Generic validation error" +} + ''' + mock_request.side_effect = ValidationException(http_resp=http_mock_response(response_body, 400)) + + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(ValidationException) as api_exception: + api_instance.check( + body=body, + ) + self.assertIsInstance(api_exception.exception.parsed_exception, ValidationErrorMessageResponse) + self.assertEqual(api_exception.exception.parsed_exception.code, ErrorCode.VALIDATION_ERROR) + self.assertEqual(api_exception.exception.parsed_exception.message, "Generic validation error") + self.assertEqual(api_exception.exception.header.get(FGA_REQUEST_ID), request_id) + + + @patch.object(rest.RESTClientObject, 'request') + async def test_404_error(self, mock_request): + """ + Test to ensure 404 errors are handled properly + """ + response_body = ''' +{ + "code": "undefined_endpoint", + "message": "Endpoint not enabled" +} + ''' + mock_request.side_effect = NotFoundException(http_resp=http_mock_response(response_body, 404)) + + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(NotFoundException) as api_exception: + api_instance.check( + body=body, + ) + self.assertIsInstance(api_exception.exception.parsed_exception, PathUnknownErrorMessageResponse) + self.assertEqual(api_exception.exception.parsed_exception.code, NotFoundErrorCode.UNDEFINED_ENDPOINT) + self.assertEqual(api_exception.exception.parsed_exception.message, "Endpoint not enabled") + + @patch.object(rest.RESTClientObject, 'request') + async def test_429_error_no_retry(self, mock_request): + """ + Test to ensure 429 errors are handled properly. + For this case, there is no retry configured + """ + response_body = ''' +{ + "code": "rate_limit_exceeded", + "message": "Rate Limit exceeded" +} + ''' + mock_request.side_effect = RateLimitExceededError(http_resp=http_mock_response(response_body, 429)) + + retry = {{packageName}}.configuration.RetryParams(0, 10) + configuration = self.configuration + configuration.store_id = store_id + configuration.retry_params = retry + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(RateLimitExceededError) as api_exception: + api_instance.check( + body=body, + ) + self.assertIsInstance(api_exception.exception, RateLimitExceededError) + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 1) + + @patch.object(rest.RESTClientObject, 'request') + async def test_429_error_first_error(self, mock_request): + """ + Test to ensure 429 errors are handled properly. + For this case, retry is configured and only the first time has error + """ + response_body = '{"allowed": true, "resolution": "1234"}' + error_response_body = ''' +{ + "code": "rate_limit_exceeded", + "message": "Rate Limit exceeded" +} + ''' + mock_request.side_effect = [RateLimitExceededError(http_resp=http_mock_response(error_response_body, 429)), mock_response(response_body, 200)] + + retry = {{packageName}}.configuration.RetryParams(1, 10) + configuration = self.configuration + configuration.store_id = store_id + configuration.retry_params = retry + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + api_response = api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 2) + + + @patch.object(rest.RESTClientObject, 'request') + async def test_500_error(self, mock_request): + """ + Test to ensure 500 errors are handled properly + """ + response_body = ''' +{ + "code": "internal_error", + "message": "Internal Server Error" +} + ''' + mock_request.side_effect = ServiceException(http_resp=http_mock_response(response_body, 500)) + + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(ServiceException) as api_exception: + api_instance.check( + body=body, + ) + self.assertIsInstance(api_exception.exception.parsed_exception, InternalErrorMessageResponse) + self.assertEqual(api_exception.exception.parsed_exception.code, InternalErrorCode.INTERNAL_ERROR) + self.assertEqual(api_exception.exception.parsed_exception.message, "Internal Server Error") + + @patch.object(rest.RESTClientObject, 'request') + async def test_check_api_token(self, mock_request): + """Test case for API token + + Check whether API token is send when configuration specifies credential method as api_token + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + configuration.credentials = Credentials(method='api_token', configuration=CredentialConfiguration(api_token='TOKEN1')) + with ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + api_response = api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + expectedHeader = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk {{sdkId}}/{{packageVersion}}', 'Authorization': 'Bearer TOKEN1'}) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + headers=expectedHeader, + query_params=[], + post_params=[], + body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_check_custom_header(self, mock_request): + """Test case for custom header + + Check whether custom header can be added + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + with ApiClient(configuration) as api_client: + api_client.set_default_header("Custom Header", "custom value") + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + api_response = api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + expectedHeader = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk {{sdkId}}/{{packageVersion}}', 'Custom Header': 'custom value'}) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + headers=expectedHeader, + query_params=[], + post_params=[], + body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + _preload_content=ANY, + _request_timeout=None + ) + +if __name__ == '__main__': + unittest.main() diff --git a/config/clients/python/template/client/client.mustache b/config/clients/python/template/client/client.mustache index 466c36cb..5ab18417 100644 --- a/config/clients/python/template/client/client.mustache +++ b/config/clients/python/template/client/client.mustache @@ -30,7 +30,12 @@ from {{packageName}}.models.write_authorization_model_request import WriteAuthor from {{packageName}}.models.write_request import WriteRequest from {{packageName}}.validation import is_well_formed_ulid_string +{{#asyncio}} import asyncio +{{/asyncio}} +{{^asyncio}} +import time +{{/asyncio}} import uuid from typing import List @@ -100,14 +105,23 @@ class OpenFgaClient(): self._api_client = ApiClient(configuration) self._api = OpenFgaApi(self._api_client) + {{#asyncio}} async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_value, traceback): await self.close() + {{/asyncio}} + {{^asyncio}} + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + {{/asyncio}} - async def close(self): - await self._api.close() + {{#asyncio}}async {{/asyncio}}def close(self): + {{#asyncio}}await {{/asyncio}}self._api.close() def _get_authorization_model_id(self, options: object) -> str | None: """ @@ -148,21 +162,21 @@ class OpenFgaClient(): """ return self._client_configuration.authorization_model_id - async def _check_valid_api_connection(self, options: dict[str, int | str]): + {{#asyncio}}async {{/asyncio}}def _check_valid_api_connection(self, options: dict[str, int | str]): """ Checks that a connection with the given configuration can be established """ authorization_model_id = self._get_authorization_model_id(options) if authorization_model_id is not None and authorization_model_id != "": - await self.read_authorization_model(options) + {{#asyncio}}await {{/asyncio}}self.read_authorization_model(options) else: - await self.read_latest_authorization_model(options) + {{#asyncio}}await {{/asyncio}}self.read_latest_authorization_model(options) ################# # Stores ################# - async def list_stores(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def list_stores(self, options: dict[str, int|str] = None): """ List the stores in the system :param page_size(options) - Number of items returned per request @@ -175,12 +189,12 @@ class OpenFgaClient(): # convert options to kargs options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListStores") kwargs = options_to_kwargs(options) - api_response = await self._api.list_stores( + api_response = {{#asyncio}}await {{/asyncio}}self._api.list_stores( **kwargs, ) return api_response - async def create_store(self, body: CreateStoreRequest, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def create_store(self, body: CreateStoreRequest, options: dict[str, int|str] = None): """ Create the stores in the system :param header(options) - Custom headers to send alongside the request @@ -190,13 +204,13 @@ class OpenFgaClient(): """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "CreateStore") kwargs = options_to_kwargs(options) - api_response = await self._api.create_store( + api_response = {{#asyncio}}await {{/asyncio}}self._api.create_store( body, **kwargs ) return api_response - async def get_store(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def get_store(self, options: dict[str, int|str] = None): """ Get the store info in the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -206,12 +220,12 @@ class OpenFgaClient(): """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "GetStore") kwargs = options_to_kwargs(options) - api_response = await self._api.get_store( + api_response = {{#asyncio}}await {{/asyncio}}self._api.get_store( **kwargs, ) return api_response - async def delete_store(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def delete_store(self, options: dict[str, int|str] = None): """ Delete the store from the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -221,7 +235,7 @@ class OpenFgaClient(): """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "DeleteStore") kwargs = options_to_kwargs(options) - api_response = await self._api.delete_store( + api_response = {{#asyncio}}await {{/asyncio}}self._api.delete_store( **kwargs, ) return api_response @@ -230,7 +244,7 @@ class OpenFgaClient(): # Authorization Models ####################### - async def read_authorization_models(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_authorization_models(self, options: dict[str, int|str] = None): """ Return all the authorization models for a particular store. :param header(options) - Custom headers to send alongside the request @@ -240,12 +254,12 @@ class OpenFgaClient(): """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadAuthorizationModels") kwargs = options_to_kwargs(options) - api_response = await self._api.read_authorization_models( + api_response = {{#asyncio}}await {{/asyncio}}self._api.read_authorization_models( **kwargs, ) return api_response - async def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int|str] = None): """ Write authorization model. :param body - WriteAuthorizationModelRequest @@ -256,13 +270,13 @@ class OpenFgaClient(): """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "WriteAuthorizationModel") kwargs = options_to_kwargs(options) - api_response = await self._api.write_authorization_model( + api_response = {{#asyncio}}await {{/asyncio}}self._api.write_authorization_model( body, **kwargs, ) return api_response - async def read_authorization_model(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_authorization_model(self, options: dict[str, int|str] = None): """ Read an authorization model. :param header(options) - Custom headers to send alongside the request @@ -273,13 +287,13 @@ class OpenFgaClient(): options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadAuthorizationModel") kwargs = options_to_kwargs(options) authorization_model_id=self._get_authorization_model_id(options) - api_response = await self._api.read_authorization_model( + api_response = {{#asyncio}}await {{/asyncio}}self._api.read_authorization_model( authorization_model_id, **kwargs, ) return api_response - async def read_latest_authorization_model(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_latest_authorization_model(self, options: dict[str, int|str] = None): """ Convenient method of reading the latest authorizaiton model :param header(options) - Custom headers to send alongside the request @@ -289,14 +303,14 @@ class OpenFgaClient(): """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadLatestAuthoriationModel") options["page_size"] = 1 - api_response = await self.read_authorization_models(options) + api_response = {{#asyncio}}await {{/asyncio}}self.read_authorization_models(options) return ReadAuthorizationModelResponse(api_response.authorization_models[0]) ####################### # Relationship Tuples ####################### - async def read_changes(self, body: ClientReadChangesRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read_changes(self, body: ClientReadChangesRequest, options: dict[str, str] = None): """ Read changes for specified type :param body - the type we want to look for change @@ -310,12 +324,12 @@ class OpenFgaClient(): options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadChanges") kwargs = options_to_kwargs(options) kwargs["type"] = body.type - api_response = await self._api.read_changes( + api_response = {{#asyncio}}await {{/asyncio}}self._api.read_changes( **kwargs, ) return api_response - async def read(self, body: TupleKey, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read(self, body: TupleKey, options: dict[str, str] = None): """ Read changes for specified type :param body - the tuples we want to read @@ -338,7 +352,7 @@ class OpenFgaClient(): options.pop("continuation_token") kwargs = options_to_kwargs(options) - api_response = await self._api.read( + api_response = {{#asyncio}}await {{/asyncio}}self._api.read( ReadRequest( tuple_key=body, page_size=page_size, @@ -348,7 +362,7 @@ class OpenFgaClient(): ) return api_response - async def _write_single_batch(self, batch: List[ClientTuple], is_write: bool, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_single_batch(self, batch: List[ClientTuple], is_write: bool, options: dict[str, str] = None): try: write_batch = None delete_batch = None @@ -356,12 +370,12 @@ class OpenFgaClient(): write_batch = batch else: delete_batch = batch - await self._write_with_transaction(ClientWriteRequest(writes=write_batch, deletes=delete_batch), options) + {{#asyncio}}await {{/asyncio}}self._write_with_transaction(ClientWriteRequest(writes=write_batch, deletes=delete_batch), options) return [construct_write_single_response(i, True, None) for i in batch] except Exception as err: return [construct_write_single_response(i, False, err) for i in batch] - async def _write_batches(self, tuple_keys: List[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_batches(self, tuple_keys: List[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, str] = None): """ Internal function for write/delete batches """ @@ -370,14 +384,19 @@ class OpenFgaClient(): write_batches = _chuck_array(chunks, transaction.max_parallel_requests) batch_write_responses = [] for write_batch in write_batches: + {{#asyncio}} request = [self._write_single_batch(i, is_write, options) for i in write_batch] response = await asyncio.gather(*request) + {{/asyncio}} + {{^asyncio}} + response = [self._write_single_batch(i, is_write, options) for i in write_batch] + {{/asyncio}} flatten_list = [item for batch_single_response in response for item in batch_single_response] batch_write_responses.extend(flatten_list) return batch_write_responses - async def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, str] = None): """ Write or deletes tuples """ @@ -389,7 +408,7 @@ class OpenFgaClient(): if body.deletes_tuple_keys: deletes_tuple_keys=body.deletes_tuple_keys - await self._api.write( + {{#asyncio}}await {{/asyncio}}self._api.write( WriteRequest( writes=writes_tuple_keys, deletes=deletes_tuple_keys, @@ -406,7 +425,7 @@ class OpenFgaClient(): deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - async def write(self, body: ClientWriteRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write(self, body: ClientWriteRequest, options: dict[str, str] = None): """ Write or deletes tuples :param body - the write request @@ -418,23 +437,23 @@ class OpenFgaClient(): options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "Writes") transaction = options_to_transaction_info(options) if not transaction.disabled: - results = await self._write_with_transaction(body, options) + results = {{#asyncio}}await {{/asyncio}}self._write_with_transaction(body, options) return results options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) # TODO: this should be run in parallel - await self._check_valid_api_connection(options) + {{#asyncio}}await {{/asyncio}}self._check_valid_api_connection(options) # otherwise, it is not a transaction and it is a batch write requests writes_response = None if body.writes: - writes_response = await self._write_batches(body.writes, transaction, True, options) + writes_response = {{#asyncio}}await {{/asyncio}}self._write_batches(body.writes, transaction, True, options) deletes_response = None if body.deletes: - deletes_response = await self._write_batches(body.deletes, transaction, False, options) + deletes_response = {{#asyncio}}await {{/asyncio}}self._write_batches(body.deletes, transaction, False, options) return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - async def write_tuples(self, body: List[ClientTuple], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write_tuples(self, body: List[ClientTuple], options: dict[str, str] = None): """ Convenient method for writing tuples :param body - the list of tuples we want to write @@ -444,10 +463,10 @@ class OpenFgaClient(): :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "WriteTuples") - result = await self.write(ClientWriteRequest(body, None), options) + result = {{#asyncio}}await {{/asyncio}}self.write(ClientWriteRequest(body, None), options) return result - async def delete_tuples(self, body: List[ClientTuple], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def delete_tuples(self, body: List[ClientTuple], options: dict[str, str] = None): """ Convenient method for deleteing tuples :param body - the list of tuples we want to delete @@ -457,13 +476,13 @@ class OpenFgaClient(): :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated """ options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "DeleteTuples") - result = await self.write(ClientWriteRequest(None, body), options) + result = {{#asyncio}}await {{/asyncio}}self.write(ClientWriteRequest(None, body), options) return result ####################### # Relationship Queries ####################### - async def check(self, body: ClientCheckRequest, options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def check(self, body: ClientCheckRequest, options: dict[str, str] = None): # noqa: E501 """ Check whether a user is authorized to access an object :param body - ClientCheckRequest defining check request @@ -489,25 +508,25 @@ class OpenFgaClient(): req_body.contextual_tuples = ContextualTupleKeys( tuple_keys=convert_tuple_keys(body.contextual_tuples) ) - api_response = await self._api.check( + api_response = {{#asyncio}}await {{/asyncio}}self._api.check( body=req_body, **kwargs ) return api_response - async def _single_batch_check(self, body: ClientCheckRequest, options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def _single_batch_check(self, body: ClientCheckRequest, options: dict[str, str] = None): # noqa: E501 """ Run a single batch request and return body in a SingleBatchCheckResponse :param body - ClientCheckRequest defining check request :param authorization_model_id(options) - Overrides the authorization model id in the configuration """ try: - api_response = await self.check(body, options) + api_response = {{#asyncio}}await {{/asyncio}}self.check(body, options) return BatchCheckResponse(allowed=api_response.allowed, request=body, response=api_response, error=None) except Exception as err: return BatchCheckResponse(allowed=False, request=body, response=None, error=err) - async def batch_check(self, body: List[ClientCheckRequest], options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def batch_check(self, body: List[ClientCheckRequest], options: dict[str, str] = None): # noqa: E501 """ Run a set of checks :param body - list of ClientCheckRequest defining check request @@ -522,7 +541,7 @@ class OpenFgaClient(): options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) # TODO: this should be run in parallel - await self._check_valid_api_connection(options) + {{#asyncio}}await {{/asyncio}}self._check_valid_api_connection(options) max_parallel_requests = {{ clientMaxMethodParallelRequests }} if options is not None and "max_parallel_requests" in options: @@ -531,12 +550,17 @@ class OpenFgaClient(): request_batches = _chuck_array(body, max_parallel_requests) batch_check_response = [] for request_batch in request_batches: + {{#asyncio}} request = [self._single_batch_check(i, options) for i in request_batch] response = await asyncio.gather(*request) + {{/asyncio}} + {{^asyncio}} + response = [self._single_batch_check(i, options) for i in request_batch] + {{/asyncio}} batch_check_response.extend(response) return batch_check_response - async def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): # noqa: E501 """ Run expand request :param body - list of ClientExpandRequest defining expand request @@ -556,13 +580,13 @@ class OpenFgaClient(): ), authorization_model_id=self._get_authorization_model_id(options), ) - api_response = await self._api.expand( + api_response = {{#asyncio}}await {{/asyncio}}self._api.expand( body=req_body, **kwargs ) return api_response - async def list_objects(self, body: ClientListObjectsRequest, options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def list_objects(self, body: ClientListObjectsRequest, options: dict[str, str] = None): # noqa: E501 """ Run list object request :param body - list object parameters @@ -585,13 +609,13 @@ class OpenFgaClient(): req_body.contextual_tuples = ContextualTupleKeys( tuple_keys=convert_tuple_keys(body.contextual_tuples) ) - api_response = await self._api.list_objects( + api_response = {{#asyncio}}await {{/asyncio}}self._api.list_objects( body=req_body, **kwargs ) return api_response - async def list_relations(self, body: ClientListObjectsRequest, options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def list_relations(self, body: ClientListObjectsRequest, options: dict[str, str] = None): # noqa: E501 """ Return all the relations for which user has a relationship with the object :param body - list relation request @@ -605,7 +629,7 @@ class OpenFgaClient(): options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples) for i in body.relations] - result = await self.batch_check(request_body, options) + result = {{#asyncio}}await {{/asyncio}}self.batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) result_list = list(result_iterator) @@ -615,7 +639,7 @@ class OpenFgaClient(): ####################### # Assertions ####################### - async def read_assertions(self, options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def read_assertions(self, options: dict[str, str] = None): # noqa: E501 """ Return the assertions :param authorization_model_id(options) - Overrides the authorization model id in the configuration @@ -628,10 +652,10 @@ class OpenFgaClient(): kwargs = options_to_kwargs(options) authorization_model_id=self._get_authorization_model_id(options) - api_response = await self._api.read_assertions(authorization_model_id, **kwargs) + api_response = {{#asyncio}}await {{/asyncio}}self._api.read_assertions(authorization_model_id, **kwargs) return api_response - async def write_assertions(self, body:List[ClientAssertion], options: dict[str, str] = None): # noqa: E501 + {{#asyncio}}async {{/asyncio}}def write_assertions(self, body:List[ClientAssertion], options: dict[str, str] = None): # noqa: E501 """ Upsert the assertions :param body - Write assertion request @@ -653,5 +677,5 @@ class OpenFgaClient(): ), client_assertion.expectation) api_request_body = WriteAssertionsRequest([map_to_assertion(client_assertion) for client_assertion in body]) - api_response = await self._api.write_assertions(authorization_model_id, api_request_body, **kwargs) + api_response = {{#asyncio}}await {{/asyncio}}self._api.write_assertions(authorization_model_id, api_request_body, **kwargs) return api_response diff --git a/config/clients/python/template/client/client_sync.mustache b/config/clients/python/template/client/client_sync.mustache new file mode 100644 index 00000000..2b3655cf --- /dev/null +++ b/config/clients/python/template/client/client_sync.mustache @@ -0,0 +1,655 @@ +# coding: utf-8 +{{>partial_header}} + +from {{packageName}}.sync.api_client import ApiClient +from {{packageName}}.sync.open_fga_api import OpenFgaApi +from {{packageName}}.client.configuration import ClientConfiguration +from {{packageName}}.client.models.assertion import ClientAssertion +from {{packageName}}.client.models.check_request import ClientCheckRequest, construct_check_request +from {{packageName}}.client.models.batch_check_response import BatchCheckResponse +from {{packageName}}.client.models.tuple import ClientTuple, convert_tuple_keys +from {{packageName}}.client.models.write_request import ClientWriteRequest +from {{packageName}}.client.models.write_response import ClientWriteResponse +from {{packageName}}.client.models.expand_request import ClientExpandRequest +from {{packageName}}.client.models.list_objects_request import ClientListObjectsRequest +from {{packageName}}.client.models.write_single_response import construct_write_single_response +from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts +from {{packageName}}.client.models.read_changes_request import ClientReadChangesRequest +from {{packageName}}.exceptions import FgaValidationException +from {{packageName}}.models.assertion import Assertion +from {{packageName}}.models.check_request import CheckRequest +from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys +from {{packageName}}.models.create_store_request import CreateStoreRequest +from {{packageName}}.models.expand_request import ExpandRequest +from {{packageName}}.models.list_objects_request import ListObjectsRequest +from {{packageName}}.models.read_authorization_model_response import ReadAuthorizationModelResponse +from {{packageName}}.models.read_request import ReadRequest +from {{packageName}}.models.tuple_key import TupleKey +from {{packageName}}.models.write_assertions_request import WriteAssertionsRequest +from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest +from {{packageName}}.models.write_request import WriteRequest +from {{packageName}}.validation import is_well_formed_ulid_string + +import time +import uuid +from typing import List + +CLIENT_METHOD_HEADER = "X-OpenFGA-Client-Method" +CLIENT_BULK_REQUEST_ID_HEADER = "X-OpenFGA-Client-Bulk-Request-Id" + +def _chuck_array(array, max_size): + """ + Helper function to chuck array into arrays of max_size + """ + return [array[i * max_size:(i + 1) * max_size] for i in range((len(array) + max_size - 1) // max_size )] + + +def set_heading_if_not_set(options: dict[str, int|str], name: str, value: str): + """ + Set heading to the value if it is not set + """ + if options is None: + options = {} + headers = options.get("headers") + if headers is None: + headers = {} + if headers.get(name) is None: + headers[name] = value + options["headers"] = headers + return options + + +def options_to_kwargs(options: dict[str, int|str] = None): + """ + Return kargs with continuation_token and page_size + """ + kwargs = {} + if options is not None: + if options.get("page_size"): + kwargs["page_size"] = options["page_size"] + if options.get("continuation_token"): + kwargs["continuation_token"] = options["continuation_token"] + if options.get("headers"): + kwargs["_headers"] = options["headers"] + if options.get("retry_params"): + kwargs["_retry_params"] = options["retry_params"] + return kwargs + +def options_to_transaction_info(options: dict[str, int|str] = None): + """ + Return the transaction info + """ + if options is not None and options.get("transaction"): + return options["transaction"] + return WriteTransactionOpts() + +def _check_allowed(response:BatchCheckResponse): + """ + Helper function to return whether the response is check is allowed + """ + return response.allowed + + +class OpenFgaClient(): + """ + OpenFgaClient is the entry point for invoking calls against the OpenFGA API. + """ + + def __init__(self, configuration: ClientConfiguration): + self._client_configuration = configuration + self._api_client = ApiClient(configuration) + self._api = OpenFgaApi(self._api_client) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + self._api.close() + + def _get_authorization_model_id(self, options: object) -> str | None: + """ + Return the authorization model ID if specified in the options. + Otherwise return the authorization model ID stored in the client's configuration + """ + authorization_model_id = self._client_configuration.authorization_model_id + if options is not None and "authorization_model_id" in options: + authorization_model_id = options["authorization_model_id"] + if authorization_model_id is None or authorization_model_id == "": + return None + if is_well_formed_ulid_string(authorization_model_id) is False: + raise FgaValidationException( + "authorization_model_id ('%s') is not in a valid ulid format" % authorization_model_id) + return authorization_model_id + + def set_store_id(self, value): + """ + Update the store ID in the configuration + """ + self._api_client.set_store_id(value) + + def get_store_id(self): + """ + Return the store id (if any) store in the configuration + """ + return self._api_client.get_store_id() + + def set_authorization_model_id(self, value): + """ + Update the authorizaiton model id in the configuration + """ + self._client_configuration.authorization_model_id = value + + def get_authorization_model_id(self): + """ + Return the authorizaiton model id + """ + return self._client_configuration.authorization_model_id + + def _check_valid_api_connection(self, options: dict[str, int | str]): + """ + Checks that a connection with the given configuration can be established + """ + authorization_model_id = self._get_authorization_model_id(options) + if authorization_model_id is not None and authorization_model_id != "": + self.read_authorization_model(options) + else: + self.read_latest_authorization_model(options) + + ################# + # Stores + ################# + + def list_stores(self, options: dict[str, int|str] = None): + """ + List the stores in the system + :param page_size(options) - Number of items returned per request + :param continuation_token(options) - No continuation_token by default + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + # convert options to kargs + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListStores") + kwargs = options_to_kwargs(options) + api_response = self._api.list_stores( + **kwargs, + ) + return api_response + + def create_store(self, body: CreateStoreRequest, options: dict[str, int|str] = None): + """ + Create the stores in the system + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "CreateStore") + kwargs = options_to_kwargs(options) + api_response = self._api.create_store( + body, + **kwargs + ) + return api_response + + def get_store(self, options: dict[str, int|str] = None): + """ + Get the store info in the system. Store id is from the configuration. + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "GetStore") + kwargs = options_to_kwargs(options) + api_response = self._api.get_store( + **kwargs, + ) + return api_response + + def delete_store(self, options: dict[str, int|str] = None): + """ + Delete the store from the system. Store id is from the configuration. + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "DeleteStore") + kwargs = options_to_kwargs(options) + api_response = self._api.delete_store( + **kwargs, + ) + return api_response + + ####################### + # Authorization Models + ####################### + + def read_authorization_models(self, options: dict[str, int|str] = None): + """ + Return all the authorization models for a particular store. + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadAuthorizationModels") + kwargs = options_to_kwargs(options) + api_response = self._api.read_authorization_models( + **kwargs, + ) + return api_response + + def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int|str] = None): + """ + Write authorization model. + :param body - WriteAuthorizationModelRequest + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "WriteAuthorizationModel") + kwargs = options_to_kwargs(options) + api_response = self._api.write_authorization_model( + body, + **kwargs, + ) + return api_response + + def read_authorization_model(self, options: dict[str, int|str] = None): + """ + Read an authorization model. + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadAuthorizationModel") + kwargs = options_to_kwargs(options) + authorization_model_id=self._get_authorization_model_id(options) + api_response = self._api.read_authorization_model( + authorization_model_id, + **kwargs, + ) + return api_response + + def read_latest_authorization_model(self, options: dict[str, int|str] = None): + """ + Convenient method of reading the latest authorizaiton model + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadLatestAuthoriationModel") + options["page_size"] = 1 + api_response = self.read_authorization_models(options) + return ReadAuthorizationModelResponse(api_response.authorization_models[0]) + + ####################### + # Relationship Tuples + ####################### + + def read_changes(self, body: ClientReadChangesRequest, options: dict[str, str] = None): + """ + Read changes for specified type + :param body - the type we want to look for change + :param page_size(options) - Number of items returned per request + :param continuation_token(options) - No continuation_token by default + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadChanges") + kwargs = options_to_kwargs(options) + kwargs["type"] = body.type + api_response = self._api.read_changes( + **kwargs, + ) + return api_response + + def read(self, body: TupleKey, options: dict[str, str] = None): + """ + Read changes for specified type + :param body - the tuples we want to read + :param page_size(options) - Number of items returned per request + :param continuation_token(options) - No continuation_token by default + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "Read") + page_size = None + continuation_token = None + if options: + if options.get("page_size"): + page_size = options.get("page_size") + options.pop("page_size") + if options.get("continuation_token"): + continuation_token = options.get("continuation_token") + options.pop("continuation_token") + kwargs = options_to_kwargs(options) + + api_response = self._api.read( + ReadRequest( + tuple_key=body, + page_size=page_size, + continuation_token=continuation_token, + ), + **kwargs, + ) + return api_response + + def _write_single_batch(self, batch: List[ClientTuple], is_write: bool, options: dict[str, str] = None): + try: + write_batch = None + delete_batch = None + if is_write: + write_batch = batch + else: + delete_batch = batch + self._write_with_transaction(ClientWriteRequest(writes=write_batch, deletes=delete_batch), options) + return [construct_write_single_response(i, True, None) for i in batch] + except Exception as err: + return [construct_write_single_response(i, False, err) for i in batch] + + def _write_batches(self, tuple_keys: List[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, str] = None): + """ + Internal function for write/delete batches + """ + chunks = _chuck_array(tuple_keys, transaction.max_per_chunk) + + write_batches = _chuck_array(chunks, transaction.max_parallel_requests) + batch_write_responses = [] + for write_batch in write_batches: + response = [self._write_single_batch(i, is_write, options) for i in write_batch] + flatten_list = [item for batch_single_response in response for item in batch_single_response] + batch_write_responses.extend(flatten_list) + + return batch_write_responses + + def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, str] = None): + """ + Write or deletes tuples + """ + kwargs = options_to_kwargs(options) + writes_tuple_keys = None + deletes_tuple_keys = None + if body.writes_tuple_keys: + writes_tuple_keys=body.writes_tuple_keys + if body.deletes_tuple_keys: + deletes_tuple_keys=body.deletes_tuple_keys + + self._api.write( + WriteRequest( + writes=writes_tuple_keys, + deletes=deletes_tuple_keys, + authorization_model_id=self._get_authorization_model_id(options), + ), + **kwargs, + ) + # any error will result in exception being thrown and not reached below code + writes_response = None + if body.writes: + writes_response = [construct_write_single_response(i, True, None) for i in body.writes] + deletes_response = None + if body.deletes: + deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] + return ClientWriteResponse(writes=writes_response, deletes=deletes_response) + + def write(self, body: ClientWriteRequest, options: dict[str, str] = None): + """ + Write or deletes tuples + :param body - the write request + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "Writes") + transaction = options_to_transaction_info(options) + if not transaction.disabled: + results = self._write_with_transaction(body, options) + return results + + options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) + # TODO: this should be run in parallel + self._check_valid_api_connection(options) + + # otherwise, it is not a transaction and it is a batch write requests + writes_response = None + if body.writes: + writes_response = self._write_batches(body.writes, transaction, True, options) + deletes_response = None + if body.deletes: + deletes_response = self._write_batches(body.deletes, transaction, False, options) + return ClientWriteResponse(writes=writes_response, deletes=deletes_response) + + def write_tuples(self, body: List[ClientTuple], options: dict[str, str] = None): + """ + Convenient method for writing tuples + :param body - the list of tuples we want to write + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "WriteTuples") + result = self.write(ClientWriteRequest(body, None), options) + return result + + def delete_tuples(self, body: List[ClientTuple], options: dict[str, str] = None): + """ + Convenient method for deleteing tuples + :param body - the list of tuples we want to delete + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "DeleteTuples") + result = self.write(ClientWriteRequest(None, body), options) + return result + + ####################### + # Relationship Queries + ####################### + def check(self, body: ClientCheckRequest, options: dict[str, str] = None): # noqa: E501 + """ + Check whether a user is authorized to access an object + :param body - ClientCheckRequest defining check request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "Check") + + kwargs = options_to_kwargs(options) + + req_body = CheckRequest( + tuple_key=TupleKey( + user=body.user, + relation=body.relation, + object=body.object, + ), + authorization_model_id=self._get_authorization_model_id(options), + ) + if body.contextual_tuples: + req_body.contextual_tuples = ContextualTupleKeys( + tuple_keys=convert_tuple_keys(body.contextual_tuples) + ) + api_response = self._api.check( + body=req_body, + **kwargs + ) + return api_response + + def _single_batch_check(self, body: ClientCheckRequest, options: dict[str, str] = None): # noqa: E501 + """ + Run a single batch request and return body in a SingleBatchCheckResponse + :param body - ClientCheckRequest defining check request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + """ + try: + api_response = self.check(body, options) + return BatchCheckResponse(allowed=api_response.allowed, request=body, response=api_response, error=None) + except Exception as err: + return BatchCheckResponse(allowed=False, request=body, response=None, error=err) + + def batch_check(self, body: List[ClientCheckRequest], options: dict[str, str] = None): # noqa: E501 + """ + Run a set of checks + :param body - list of ClientCheckRequest defining check request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param max_parallel_requests(options) - Max number of requests to issue in parallel. Defaults to {{ clientMaxMethodParallelRequests }} + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "BatchCheck") + options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) + + # TODO: this should be run in parallel + self._check_valid_api_connection(options) + + max_parallel_requests = {{ clientMaxMethodParallelRequests }} + if options is not None and "max_parallel_requests" in options: + max_parallel_requests = options["max_parallel_requests"] + # Break the batch into chunks + request_batches = _chuck_array(body, max_parallel_requests) + batch_check_response = [] + for request_batch in request_batches: + response = [self._single_batch_check(i, options) for i in request_batch] + batch_check_response.extend(response) + return batch_check_response + + def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): # noqa: E501 + """ + Run expand request + :param body - list of ClientExpandRequest defining expand request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "Expand") + kwargs = options_to_kwargs(options) + + req_body = ExpandRequest( + tuple_key=TupleKey( + relation=body.relation, + object=body.object, + ), + authorization_model_id=self._get_authorization_model_id(options), + ) + api_response = self._api.expand( + body=req_body, + **kwargs + ) + return api_response + + def list_objects(self, body: ClientListObjectsRequest, options: dict[str, str] = None): # noqa: E501 + """ + Run list object request + :param body - list object parameters + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListObjects") + kwargs = options_to_kwargs(options) + + req_body = ListObjectsRequest( + authorization_model_id=self._get_authorization_model_id(options), + user=body.user, + relation=body.relation, + type=body.type, + ) + if body.contextual_tuples: + req_body.contextual_tuples = ContextualTupleKeys( + tuple_keys=convert_tuple_keys(body.contextual_tuples) + ) + api_response = self._api.list_objects( + body=req_body, + **kwargs + ) + return api_response + + def list_relations(self, body: ClientListObjectsRequest, options: dict[str, str] = None): # noqa: E501 + """ + Return all the relations for which user has a relationship with the object + :param body - list relation request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListRelations") + options = set_heading_if_not_set(options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())) + + request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples) for i in body.relations] + result = self.batch_check(request_body, options) + # need to filter with the allowed response + result_iterator = filter(_check_allowed, result) + result_list = list(result_iterator) + return [i.request.relation for i in result_list] + + + ####################### + # Assertions + ####################### + def read_assertions(self, options: dict[str, str] = None): # noqa: E501 + """ + Return the assertions + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ReadAssertions") + + kwargs = options_to_kwargs(options) + authorization_model_id=self._get_authorization_model_id(options) + api_response = self._api.read_assertions(authorization_model_id, **kwargs) + return api_response + + def write_assertions(self, body:List[ClientAssertion], options: dict[str, str] = None): # noqa: E501 + """ + Upsert the assertions + :param body - Write assertion request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "WriteAssertions") + kwargs = options_to_kwargs(options) + authorization_model_id=self._get_authorization_model_id(options) + + def map_to_assertion(client_assertion: ClientAssertion): + return Assertion(TupleKey( + user=client_assertion.user, + relation=client_assertion.relation, + object=client_assertion.object, + ), client_assertion.expectation) + + api_request_body = WriteAssertionsRequest([map_to_assertion(client_assertion) for client_assertion in body]) + api_response = self._api.write_assertions(authorization_model_id, api_request_body, **kwargs) + return api_response diff --git a/config/clients/python/template/client/test_client.mustache b/config/clients/python/template/client/test_client.mustache index a82a1df5..65549779 100644 --- a/config/clients/python/template/client/test_client.mustache +++ b/config/clients/python/template/client/test_client.mustache @@ -88,7 +88,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - async def test_list_stores(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_list_stores(self, mock_request): """Test case for list_stores Get all stores # noqa: E501 @@ -116,8 +116,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ''' mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.list_stores( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.list_stores( options={"page_size": 1, "continuation_token": "continuation_token_example"} ) self.assertIsInstance(api_response, ListStoresResponse) @@ -149,10 +149,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_create_store(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_create_store(self, mock_request): """Test case for create_store Create a store # noqa: E501 @@ -165,8 +165,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ''' mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.create_store( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.create_store( CreateStoreRequest(name="test-store"), options={} ) @@ -182,11 +182,11 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_get_store(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_get_store(self, mock_request): """Test case for get_store Get all stores # noqa: E501 @@ -202,8 +202,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.get_store( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.get_store( options={} ) self.assertIsInstance(api_response, GetStoreResponse) @@ -217,10 +217,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_delete_store(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_delete_store(self, mock_request): """Test case for delete_store Get all stores # noqa: E501 @@ -228,8 +228,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response('', 201) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - await api_client.delete_store( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + {{#asyncio}}await {{/asyncio}}api_client.delete_store( options={} ) mock_request.assert_called_once_with( @@ -241,10 +241,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_read_authorization_models(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_read_authorization_models(self, mock_request): """Test case for read_authorization_models Return all authorization models configured for the store # noqa: E501 @@ -287,10 +287,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = await api_client.read_authorization_models( + api_response = {{#asyncio}}await {{/asyncio}}api_client.read_authorization_models( options={} ) self.assertIsInstance(api_response, ReadAuthorizationModelsResponse) @@ -329,7 +329,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write_authorization_model(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_authorization_model(self, mock_request): """Test case for write_authorization_model Create a new authorization model # noqa: E501 @@ -338,7 +338,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: # example passing only required values which don't have defaults set body = WriteAuthorizationModelRequest( schema_version="1.1", @@ -365,7 +365,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) # Create a new authorization model - api_response = await api_client.write_authorization_model( + api_response = {{#asyncio}}await {{/asyncio}}api_client.write_authorization_model( body, options={} ) @@ -388,7 +388,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - async def test_read_authorization_model(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_read_authorization_model(self, mock_request): """Test case for read_authorization_model Return a particular version of an authorization model # noqa: E501 @@ -430,10 +430,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = await api_client.read_authorization_model( + api_response = {{#asyncio}}await {{/asyncio}}api_client.read_authorization_model( options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) @@ -471,7 +471,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_read_latest_authorization_model(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_read_latest_authorization_model(self, mock_request): """Test case for read_latest_authorization_model Return the latest authorization models configured for the store # noqa: E501 @@ -514,10 +514,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = await api_client.read_latest_authorization_model( + api_response = {{#asyncio}}await {{/asyncio}}api_client.read_latest_authorization_model( options={} ) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) @@ -556,7 +556,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - async def test_read_changes(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_read_changes(self, mock_request): """Test case for read_changes Return a list of all the tuple changes # noqa: E501 @@ -581,10 +581,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = await api_client.read_changes( + api_response = {{#asyncio}}await {{/asyncio}}api_client.read_changes( ClientReadChangesRequest("document"), options={"page_size":1, "continuation_token":"abcdefg"} ) @@ -610,7 +610,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_read(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_read(self, mock_request): """Test case for read Get tuples from the store that matches a query, without following userset rewrite rules # noqa: E501 @@ -633,13 +633,13 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = TupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) - api_response = await api_client.read( + api_response = {{#asyncio}}await {{/asyncio}}api_client.read( body=body, options={"page_size":50, "continuation_token":"eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="} ) @@ -663,7 +663,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - async def test_read_empty_options(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_read_empty_options(self, mock_request): """Test case for read with empty options Get tuples from the store that matches a query, without following userset rewrite rules # noqa: E501 @@ -686,13 +686,13 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = TupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) - api_response = await api_client.read( + api_response = {{#asyncio}}await {{/asyncio}}api_client.read( body=body, options={} ) @@ -714,7 +714,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write(self, mock_request): """Test case for write Add tuples from the store with transaction enabled @@ -723,7 +723,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( writes= @@ -745,7 +745,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) ], ) - await api_client.write( + {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) @@ -761,7 +761,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_delete(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_delete(self, mock_request): """Test case for delete Delete tuples from the store with transaction enabled @@ -770,7 +770,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( deletes= @@ -782,7 +782,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) ], ) - await api_client.write( + {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) @@ -798,7 +798,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write_batch(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_batch(self, mock_request): """Test case for write Add tuples from the store with transaction disabled @@ -811,7 +811,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( writes= @@ -834,7 +834,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) - response = await api_client.write( + response = {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} ) @@ -909,7 +909,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write_batch_min_parallel(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_batch_min_parallel(self, mock_request): """Test case for write Add tuples from the store with transaction disabled and minimum parallel request @@ -922,7 +922,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( writes= @@ -945,7 +945,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=1) - response = await api_client.write( + response = {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} ) @@ -1021,7 +1021,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - async def test_write_batch_larger_chunk(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_batch_larger_chunk(self, mock_request): """Test case for write Add tuples from the store with transaction disabled and minimum parallel request @@ -1033,7 +1033,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( writes= @@ -1056,7 +1056,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) transaction = WriteTransactionOpts(disabled=True, max_per_chunk=2, max_parallel_requests=2) - response = await api_client.write( + response = {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} ) @@ -1121,7 +1121,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write_batch_failed(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_batch_failed(self, mock_request): """Test case for write Add tuples from the store with transaction disabled where one of the request failed @@ -1141,7 +1141,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( writes= @@ -1164,7 +1164,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) - response = await api_client.write( + response = {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} ) @@ -1239,7 +1239,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_delete_batch(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_delete_batch(self, mock_request): """Test case for delete Delete tuples from the store with transaction disabled but there is only 1 relationship tuple @@ -1250,7 +1250,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( deletes= @@ -1263,7 +1263,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) - await api_client.write( + {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} ) @@ -1287,7 +1287,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write_tuples(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_tuples(self, mock_request): """Test case for write tuples Add tuples from the store with transaction enabled @@ -1296,9 +1296,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - await api_client.write_tuples( + {{#asyncio}}await {{/asyncio}}api_client.write_tuples( [ ClientTuple( object="document:2021-budget", @@ -1330,7 +1330,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_delete_tuples(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_delete_tuples(self, mock_request): """Test case for delete tuples Add tuples from the store with transaction enabled @@ -1339,9 +1339,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - await api_client.delete_tuples( + {{#asyncio}}await {{/asyncio}}api_client.delete_tuples( [ ClientTuple( object="document:2021-budget", @@ -1373,7 +1373,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write_batch_unauthorized(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_batch_unauthorized(self, mock_request): """Test case for write with 401 response """ @@ -1382,7 +1382,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: with self.assertRaises(UnauthorizedException) as api_exception: body = ClientWriteRequest( writes=[ @@ -1395,7 +1395,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) transaction = WriteTransactionOpts( disabled=True, max_per_chunk=1, max_parallel_requests=10) - await api_client.write( + {{#asyncio}}await {{/asyncio}}api_client.write( body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} @@ -1413,10 +1413,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_check(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_check(self, mock_request): """Test case for check Check whether a user is authorized to access an object # noqa: E501 @@ -1439,8 +1439,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.check( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.check( body=body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} ) @@ -1459,10 +1459,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_check_config_auth_model(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_check_config_auth_model(self, mock_request): """Test case for check Check whether a user is authorized to access an object and the auth model is already encoded in store # noqa: E501 @@ -1479,8 +1479,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.check( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.check( body=body, options={} ) @@ -1498,10 +1498,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_batch_check_single_request(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_batch_check_single_request(self, mock_request): """Test case for check with single request Check whether a user is authorized to access an object # noqa: E501 @@ -1520,8 +1520,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.batch_check( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.batch_check( body=[body], options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} ) @@ -1550,10 +1550,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_batch_check_multiple_request(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_batch_check_multiple_request(self, mock_request): """Test case for check with multiple request Check whether a user is authorized to access an object # noqa: E501 @@ -1583,8 +1583,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.batch_check( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.batch_check( body=[body1, body2, body3], options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} ) @@ -1641,11 +1641,11 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_batch_check_multiple_request_fail(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_batch_check_multiple_request_fail(self, mock_request): """Test case for check with multiple request with one request failed Check whether a user is authorized to access an object # noqa: E501 @@ -1681,8 +1681,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.batch_check( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.batch_check( body=[body1, body2, body3], options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} ) @@ -1740,11 +1740,11 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_expand(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_expand(self, mock_request): """Test case for expand Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship # noqa: E501 @@ -1755,12 +1755,12 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientExpandRequest( object="document:budget", relation="reader", ) - api_response = await api_client.expand( + api_response = {{#asyncio}}await {{/asyncio}}api_client.expand( body=body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} ) @@ -1782,10 +1782,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_list_objects(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_list_objects(self, mock_request): """Test case for list_objects List objects # noqa: E501 @@ -1800,14 +1800,14 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientListObjectsRequest( type="document", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) # Get all stores - api_response = await api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) + api_response = {{#asyncio}}await {{/asyncio}}api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) self.assertIsInstance(api_response, ListObjectsResponse) self.assertEqual(api_response.objects, ['document:abcd1234']) mock_request.assert_called_once_with( @@ -1821,11 +1821,11 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_list_objects_contextual_tuples(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_list_objects_contextual_tuples(self, mock_request): """Test case for list_objects List objects # noqa: E501 @@ -1840,7 +1840,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: body = ClientListObjectsRequest( type="document", relation="reader", @@ -1854,7 +1854,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) # Get all stores - api_response = await api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) + api_response = {{#asyncio}}await {{/asyncio}}api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) self.assertIsInstance(api_response, ListObjectsResponse) self.assertEqual(api_response.objects, ['document:abcd1234']) mock_request.assert_called_once_with( @@ -1869,10 +1869,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_list_relations(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_list_relations(self, mock_request): """Test case for list relations Check whether a user is authorized to access an object # noqa: E501 @@ -1887,8 +1887,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.list_relations( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.list_relations( body=ClientListRelationsRequest(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relations=["reader", "owner", "viewer"], object="document:2021-budget"), @@ -1938,10 +1938,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_list_relations_unauthorized(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_list_relations_unauthorized(self, mock_request): """Test case for list relations with 401 response """ @@ -1950,9 +1950,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: with self.assertRaises(UnauthorizedException) as api_exception: - await api_client.list_relations( + {{#asyncio}}await {{/asyncio}}api_client.list_relations( body=ClientListRelationsRequest(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relations=["reader", "owner", "viewer"], object="document:2021-budget"), @@ -1971,10 +1971,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=None ) - await api_client.close() + {{#asyncio}}await {{/asyncio}}api_client.close() @patch.object(rest.RESTClientObject, 'request') - async def test_read_assertions(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_read_assertions(self, mock_request): """Test case for read assertions """ @@ -1997,8 +1997,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id # Enter a context with an instance of the API client - async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.read_assertions( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + api_response = {{#asyncio}}await {{/asyncio}}api_client.read_assertions( options={"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"} ) self.assertEqual(api_response, ReadAssertionsResponse( @@ -2019,7 +2019,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_write_assertions(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_write_assertions(self, mock_request): """Test case for write assertions Get all stores # noqa: E501 @@ -2027,8 +2027,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response('', 204) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: - await api_client.write_assertions( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + {{#asyncio}}await {{/asyncio}}api_client.write_assertions( [ClientAssertion(user="user:anne", relation="reader", object="document:2021-budget", expectation=True)], options={"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"} ) @@ -2044,7 +2044,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) @patch.object(rest.RESTClientObject, 'request') - async def test_set_store_id(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_set_store_id(self, mock_request): """Test case for write assertions Get all stores # noqa: E501 @@ -2052,10 +2052,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response('', 204) configuration = self.configuration configuration.store_id = store_id - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: api_client.set_store_id("01YCP46JKYM8FJCQ37NMBYHE5Y") - await api_client.write_assertions( + {{#asyncio}}await {{/asyncio}}api_client.write_assertions( [ClientAssertion(user="user:anne", relation="reader", object="document:2021-budget", expectation=True)], options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} @@ -2074,7 +2074,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - async def test_config_auth_model(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_config_auth_model(self, mock_request): """Test case for write assertions Get all stores # noqa: E501 @@ -2083,8 +2083,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id configuration.authorization_model_id = "01G5JAVJ41T49E9TT3SKVS7X1J" - async with OpenFgaClient(configuration) as api_client: - await api_client.write_assertions( + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + {{#asyncio}}await {{/asyncio}}api_client.write_assertions( [ClientAssertion(user="user:anne", relation="reader", object="document:2021-budget", expectation=True)], options={} @@ -2103,7 +2103,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): @patch.object(rest.RESTClientObject, 'request') - async def test_update_auth_model(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_update_auth_model(self, mock_request): """Test case for write assertions Get all stores # noqa: E501 @@ -2112,10 +2112,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id configuration.authorization_model_id = "01G5JAVJ41T49E9TT3SKVS7X1J" - async with OpenFgaClient(configuration) as api_client: + {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: api_client.set_authorization_model_id("01G5JAVJ41T49E9TT3SKVS7X2J") - await api_client.write_assertions( + {{#asyncio}}await {{/asyncio}}api_client.write_assertions( [ClientAssertion(user="user:anne", relation="reader", object="document:2021-budget", expectation=True)], options={} diff --git a/config/clients/python/template/client/test_client_sync.mustache b/config/clients/python/template/client/test_client_sync.mustache new file mode 100644 index 00000000..180c6e8d --- /dev/null +++ b/config/clients/python/template/client/test_client_sync.mustache @@ -0,0 +1,2155 @@ +# coding: utf-8 +{{>partial_header}} + +from unittest.mock import ANY +from unittest import IsolatedAsyncioTestCase +from mock import patch +from datetime import datetime + +import urllib3 + +from {{packageName}}.sync.client.client import OpenFgaClient +from {{packageName}}.sync import rest +from {{packageName}}.client import ClientConfiguration +from {{packageName}}.client.models.assertion import ClientAssertion +from {{packageName}}.client.models.check_request import ClientCheckRequest +from {{packageName}}.client.models.tuple import ClientTuple +from {{packageName}}.client.models.write_request import ClientWriteRequest +from {{packageName}}.client.models.expand_request import ClientExpandRequest +from {{packageName}}.client.models.list_objects_request import ClientListObjectsRequest +from {{packageName}}.client.models.list_relations_request import ClientListRelationsRequest +from {{packageName}}.client.models.read_changes_request import ClientReadChangesRequest +from {{packageName}}.client.models.write_single_response import ClientWriteSingleResponse +from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts +from {{packageName}}.exceptions import ValidationException, FgaValidationException, UnauthorizedException +from {{packageName}}.models.assertion import Assertion +from {{packageName}}.models.authorization_model import AuthorizationModel +from {{packageName}}.models.check_response import CheckResponse +from {{packageName}}.models.create_store_request import CreateStoreRequest +from {{packageName}}.models.create_store_response import CreateStoreResponse +from {{packageName}}.models.expand_response import ExpandResponse +from {{packageName}}.models.get_store_response import GetStoreResponse +from {{packageName}}.models.leaf import Leaf +from {{packageName}}.models.list_objects_response import ListObjectsResponse +from {{packageName}}.models.list_stores_response import ListStoresResponse +from {{packageName}}.models.node import Node +from {{packageName}}.models.object_relation import ObjectRelation +from {{packageName}}.models.read_assertions_response import ReadAssertionsResponse +from {{packageName}}.models.read_authorization_model_response import ReadAuthorizationModelResponse +from {{packageName}}.models.read_authorization_models_response import ReadAuthorizationModelsResponse +from {{packageName}}.models.read_changes_response import ReadChangesResponse +from {{packageName}}.models.read_response import ReadResponse +from {{packageName}}.models.store import Store +from {{packageName}}.models.tuple import Tuple +from {{packageName}}.models.tuple_change import TupleChange +from {{packageName}}.models.tuple_key import TupleKey +from {{packageName}}.models.tuple_operation import TupleOperation +from {{packageName}}.models.type_definition import TypeDefinition +from {{packageName}}.models.users import Users +from {{packageName}}.models.userset import Userset +from {{packageName}}.models.usersets import Usersets +from {{packageName}}.models.userset_tree import UsersetTree +from {{packageName}}.models.validation_error_message_response import ValidationErrorMessageResponse +from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest +from {{packageName}}.models.write_authorization_model_response import WriteAuthorizationModelResponse + + +store_id = '01YCP46JKYM8FJCQ37NMBYHE5X' +request_id = 'x1y2z3' + +# Helper function to construct mock response +def http_mock_response(body, status): + headers = urllib3.response.HTTPHeaderDict({ + 'content-type': 'application/json', + 'Fga-Request-Id': request_id + }) + return urllib3.HTTPResponse( + body.encode('utf-8'), + headers, + status, + preload_content=False + ) + +def mock_response(body, status): + obj = http_mock_response(body, status) + return rest.RESTResponse(obj, obj.data) + +class TestOpenFgaClient(IsolatedAsyncioTestCase): + """Test for OpenFGA Client""" + + def setUp(self): + self.configuration = ClientConfiguration( + api_scheme='http', + api_host="api.{{sampleApiDomain}}", + ) + + def tearDown(self): + pass + + + @patch.object(rest.RESTClientObject, 'request') + def test_list_stores(self, mock_request): + """Test case for list_stores + + Get all stores # noqa: E501 + """ + response_body = ''' +{ + "stores": [ + { + "id": "01YCP46JKYM8FJCQ37NMBYHE5X", + "name": "store1", + "created_at": "2022-07-25T21:15:37.524Z", + "updated_at": "2022-07-25T21:15:37.524Z", + "deleted_at": "2022-07-25T21:15:37.524Z" + }, + { + "id": "01YCP46JKYM8FJCQ37NMBYHE6X", + "name": "store2", + "created_at": "2022-07-25T21:15:37.524Z", + "updated_at": "2022-07-25T21:15:37.524Z", + "deleted_at": "2022-07-25T21:15:37.524Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + with OpenFgaClient(configuration) as api_client: + api_response = api_client.list_stores( + options={"page_size": 1, "continuation_token": "continuation_token_example"} + ) + self.assertIsInstance(api_response, ListStoresResponse) + self.assertEqual(api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") + store1 = Store( + id="01YCP46JKYM8FJCQ37NMBYHE5X", + name="store1", + created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + ) + store2 = Store( + id="01YCP46JKYM8FJCQ37NMBYHE6X", + name="store2", + created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + ) + + stores = [store1, store2] + self.assertEqual(api_response.stores, stores) + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores', + headers=ANY, + query_params=[('page_size', 1), ('continuation_token', + 'continuation_token_example')], + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_create_store(self, mock_request): + """Test case for create_store + + Create a store # noqa: E501 + """ + response_body = '''{ + "id": "01YCP46JKYM8FJCQ37NMBYHE5X", + "name": "test_store", + "created_at": "2022-07-25T17:41:26.607Z", + "updated_at": "2022-07-25T17:41:26.607Z"} + ''' + mock_request.return_value = mock_response(response_body, 201) + configuration = self.configuration + with OpenFgaClient(configuration) as api_client: + api_response = api_client.create_store( + CreateStoreRequest(name="test-store"), + options={} + ) + self.assertIsInstance(api_response, CreateStoreResponse) + self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores', + headers=ANY, + query_params=[], + post_params=[], + body={"name": "test-store"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + + @patch.object(rest.RESTClientObject, 'request') + def test_get_store(self, mock_request): + """Test case for get_store + + Get all stores # noqa: E501 + """ + response_body = ''' +{ + "id": "01YCP46JKYM8FJCQ37NMBYHE5X", + "name": "store1", + "created_at": "2022-07-25T21:15:37.524Z", + "updated_at": "2022-07-25T21:15:37.524Z" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.get_store( + options={} + ) + self.assertIsInstance(api_response, GetStoreResponse) + self.assertEqual(api_response.id, "01YCP46JKYM8FJCQ37NMBYHE5X") + self.assertEqual(api_response.name, "store1") + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_delete_store(self, mock_request): + """Test case for delete_store + + Get all stores # noqa: E501 + """ + mock_request.return_value = mock_response('', 201) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_client.delete_store( + options={} + ) + mock_request.assert_called_once_with( + 'DELETE', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X', + headers=ANY, + query_params=[], + body=None, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_read_authorization_models(self, mock_request): + """Test case for read_authorization_models + + Return all authorization models configured for the store # noqa: E501 + """ + response_body = ''' +{ + "authorization_models": [{ + "id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "schema_version":"1.1", + "type_definitions": [ + { + "type": "document", + "relations": { + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "object": "", + "relation": "writer" + } + } + ] + } + }, + "writer": { + "this": {} + } + } + } + ] + }], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with OpenFgaClient(configuration) as api_client: + + # Return a particular version of an authorization model + api_response = api_client.read_authorization_models( + options={} + ) + self.assertIsInstance(api_response, ReadAuthorizationModelsResponse) + type_definitions = [ + TypeDefinition( + type="document", + relations=dict( + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + writer=Userset( + this=dict(), + ), + ) + ) + ] + authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", + type_definitions=type_definitions) + self.assertEqual(api_response.authorization_models, [authorization_model]) + self.assertEqual(api_response.continuation_token, "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write_authorization_model(self, mock_request): + """Test case for write_authorization_model + + Create a new authorization model # noqa: E501 + """ + response_body = '{"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}' + mock_request.return_value = mock_response(response_body, 201) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + # example passing only required values which don't have defaults set + body = WriteAuthorizationModelRequest( + schema_version="1.1", + type_definitions=[ + TypeDefinition( + type="document", + relations=dict( + writer=Userset( + this=dict(), + ), + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + ) + ), + ], + ) + # Create a new authorization model + api_response = api_client.write_authorization_model( + body, + options={} + ) + self.assertIsInstance(api_response, WriteAuthorizationModelResponse) + expected_response = WriteAuthorizationModelResponse( + authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' + ) + self.assertEqual(api_response, expected_response) + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + headers=ANY, + query_params=[], + post_params=[], + body={"schema_version": "1.1", "type_definitions": [{"type": "document", "relations": {"writer": {"this": { + }}, "reader": {"union": {"child": [{"this": {}}, {"computedUserset": {"object": "", "relation": "writer"}}]}}}}]}, + _preload_content=ANY, + _request_timeout=None + ) + + + @patch.object(rest.RESTClientObject, 'request') + def test_read_authorization_model(self, mock_request): + """Test case for read_authorization_model + + Return a particular version of an authorization model # noqa: E501 + """ + response_body = ''' +{ + "authorization_model": { + "id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "schema_version":"1.1", + "type_definitions": [ + { + "type": "document", + "relations": { + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "object": "", + "relation": "writer" + } + } + ] + } + }, + "writer": { + "this": {} + } + } + } + ] + } +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with OpenFgaClient(configuration) as api_client: + + # Return a particular version of an authorization model + api_response = api_client.read_authorization_model( + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + self.assertIsInstance(api_response, ReadAuthorizationModelResponse) + type_definitions = [ + TypeDefinition( + type="document", + relations=dict( + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + writer=Userset( + this=dict(), + ), + ) + ) + ] + authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", + type_definitions=type_definitions) + self.assertEqual(api_response.authorization_model, authorization_model) + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_read_latest_authorization_model(self, mock_request): + """Test case for read_latest_authorization_model + + Return the latest authorization models configured for the store # noqa: E501 + """ + response_body = ''' +{ + "authorization_models": [{ + "id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "schema_version":"1.1", + "type_definitions": [ + { + "type": "document", + "relations": { + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "object": "", + "relation": "writer" + } + } + ] + } + }, + "writer": { + "this": {} + } + } + } + ] + }], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with OpenFgaClient(configuration) as api_client: + + # Return a particular version of an authorization model + api_response = api_client.read_latest_authorization_model( + options={} + ) + self.assertIsInstance(api_response, ReadAuthorizationModelResponse) + type_definitions = [ + TypeDefinition( + type="document", + relations=dict( + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + writer=Userset( + this=dict(), + ), + ) + ) + ] + authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", + type_definitions=type_definitions) + self.assertEqual(api_response.authorization_model, authorization_model) + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + headers=ANY, + query_params=[('page_size', 1)], + _preload_content=ANY, + _request_timeout=None + ) + + + @patch.object(rest.RESTClientObject, 'request') + def test_read_changes(self, mock_request): + """Test case for read_changes + + Return a list of all the tuple changes # noqa: E501 + """ + response_body = ''' +{ + "changes": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + }, + "operation": "TUPLE_OPERATION_WRITE", + "timestamp": "2022-07-26T15:55:55.809Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with OpenFgaClient(configuration) as api_client: + + # Return a particular version of an authorization model + api_response = api_client.read_changes( + ClientReadChangesRequest("document"), + options={"page_size":1, "continuation_token":"abcdefg"} + ) + + self.assertIsInstance(api_response, ReadChangesResponse) + changes = TupleChange( + tuple_key=TupleKey(object="document:2021-budget", relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b"), + operation=TupleOperation.WRITE, + timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00")) + read_changes = ReadChangesResponse( + continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', + changes=[changes]) + self.assertEqual(api_response, read_changes) + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes', + headers=ANY, + query_params=[('type', 'document'), ('page_size', 1), + ('continuation_token', 'abcdefg')], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_read(self, mock_request): + """Test case for read + + Get tuples from the store that matches a query, without following userset rewrite rules # noqa: E501 + """ + response_body = ''' + { + "tuples": [ + { + "key": { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with OpenFgaClient(configuration) as api_client: + body = TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + api_response = api_client.read( + body=body, + options={"page_size":50, "continuation_token":"eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="} + ) + self.assertIsInstance(api_response, ReadResponse) + key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relation="reader", object="document:2021-budget") + timestamp = datetime.fromisoformat("2021-10-06T15:32:11.128+00:00") + expected_data = ReadResponse(tuples=[Tuple(key=key, timestamp=timestamp)]) + self.assertEqual(api_response, expected_data) + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, + "page_size": 50, "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="}, + _preload_content=ANY, + _request_timeout=None + ) + + + @patch.object(rest.RESTClientObject, 'request') + def test_read_empty_options(self, mock_request): + """Test case for read with empty options + + Get tuples from the store that matches a query, without following userset rewrite rules # noqa: E501 + """ + response_body = ''' + { + "tuples": [ + { + "key": { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with OpenFgaClient(configuration) as api_client: + body = TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + api_response = api_client.read( + body=body, + options={} + ) + self.assertIsInstance(api_response, ReadResponse) + key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relation="reader", object="document:2021-budget") + timestamp = datetime.fromisoformat("2021-10-06T15:32:11.128+00:00") + expected_data = ReadResponse(tuples=[Tuple(key=key, timestamp=timestamp)]) + self.assertEqual(api_response, expected_data) + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write(self, mock_request): + """Test case for write + + Add tuples from the store with transaction enabled + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + body = ClientWriteRequest( + writes= + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + ], + ) + api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"},{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31c"},{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31d"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_delete(self, mock_request): + """Test case for delete + + Delete tuples from the store with transaction enabled + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + body = ClientWriteRequest( + deletes= + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ) + api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"deletes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write_batch(self, mock_request): + """Test case for write + + Add tuples from the store with transaction disabled + """ + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{}', 200), + mock_response('{}', 200), + mock_response('{}', 200), + ] + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + body = ClientWriteRequest( + writes= + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + ], + ) + transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) + response = api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + ) + + self.assertEqual(response.deletes, None) + self.assertEqual(response.writes, + [ + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + success=True, + error=None), + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + success=True, + error=None), + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), + success=True, + error=None) + ] + ) + self.assertEqual(mock_request.call_count, 4) + mock_request.assert_any_call( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31c"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31d"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write_batch_min_parallel(self, mock_request): + """Test case for write + + Add tuples from the store with transaction disabled and minimum parallel request + """ + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{}', 200), + mock_response('{}', 200), + mock_response('{}', 200), + ] + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + body = ClientWriteRequest( + writes= + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + ], + ) + transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=1) + response = api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + ) + + self.assertEqual(response.deletes, None) + self.assertEqual(response.writes, + [ + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + success=True, + error=None), + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + success=True, + error=None), + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), + success=True, + error=None) + ] + ) + self.assertEqual(mock_request.call_count, 4) + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31c"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31d"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + + @patch.object(rest.RESTClientObject, 'request') + def test_write_batch_larger_chunk(self, mock_request): + """Test case for write + + Add tuples from the store with transaction disabled and minimum parallel request + """ + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{}', 200), + mock_response('{}', 200), + ] + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + body = ClientWriteRequest( + writes= + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + ], + ) + transaction = WriteTransactionOpts(disabled=True, max_per_chunk=2, max_parallel_requests=2) + response = api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + ) + + self.assertEqual(response.deletes, None) + self.assertEqual(response.writes, + [ + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + success=True, + error=None), + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + success=True, + error=None), + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), + success=True, + error=None) + ] + ) + self.assertEqual(mock_request.call_count, 3) + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"},{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31c"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31d"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write_batch_failed(self, mock_request): + """Test case for write + + Add tuples from the store with transaction disabled where one of the request failed + """ + response_body = ''' +{ + "code": "validation_error", + "message": "Generic validation error" +} + ''' + + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{}', 200), + ValidationException(http_resp=http_mock_response(response_body, 400)), + mock_response('{}', 200), + ] + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + body = ClientWriteRequest( + writes= + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + ], + ) + transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) + response = api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + ) + + self.assertEqual(response.deletes, None) + self.assertEqual(len(response.writes), 3) + self.assertEqual(response.writes[0], + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + success=True, + error=None)) + self.assertEqual(response.writes[1].tuple_key, + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + )) + self.assertFalse(response.writes[1].success) + self.assertIsInstance(response.writes[1].error, ValidationException) + self.assertIsInstance(response.writes[1].error.parsed_exception, ValidationErrorMessageResponse) + self.assertEqual(response.writes[2], + ClientWriteSingleResponse( + tuple_key=ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), + success=True, + error=None)) + self.assertEqual(mock_request.call_count, 4) + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31c"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31d"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_delete_batch(self, mock_request): + """Test case for delete + + Delete tuples from the store with transaction disabled but there is only 1 relationship tuple + """ + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{}', 200), + ] + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + body = ClientWriteRequest( + deletes= + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ) + transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) + api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + ) + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"deletes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write_tuples(self, mock_request): + """Test case for write tuples + + Add tuples from the store with transaction enabled + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + api_client.write_tuples( + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + ], + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"},{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31c"},{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31d"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_delete_tuples(self, mock_request): + """Test case for delete tuples + + Add tuples from the store with transaction enabled + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + + api_client.delete_tuples( + [ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ), + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + ], + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + headers=ANY, + query_params=[], + post_params=[], + body={"deletes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"},{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31c"},{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31d"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write_batch_unauthorized(self, mock_request): + """Test case for write with 401 response + """ + + mock_request.side_effect = UnauthorizedException( + http_resp=http_mock_response('{}', 401) + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(UnauthorizedException) as api_exception: + body = ClientWriteRequest( + writes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ) + transaction = WriteTransactionOpts( + disabled=True, max_per_chunk=1, max_parallel_requests=10) + api_client.write( + body, + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction} + ) + + self.assertIsInstance(api_exception.exception, UnauthorizedException) + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 1) + + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_check(self, mock_request): + """Test case for check + + Check whether a user is authorized to access an object # noqa: E501 + """ + + # First, mock the response + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + body = ClientCheckRequest( + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relation="reader", + object="document:budget", + contextual_tuples=[ + ClientTuple( + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relation="writer", + object="document:budget", + ), + ], + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", "relation": "reader", "object": "document:budget"}, + "contextual_tuples": {"tuple_keys": [{"user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", "relation": "writer", "object": "document:budget"}]}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_check_config_auth_model(self, mock_request): + """Test case for check + + Check whether a user is authorized to access an object and the auth model is already encoded in store # noqa: E501 + """ + + # First, mock the response + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + body = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01GXSA8YR785C4FYS3C0RTG7B1" + with OpenFgaClient(configuration) as api_client: + api_response = api_client.check( + body=body, + options={} + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_batch_check_single_request(self, mock_request): + """Test case for check with single request + + Check whether a user is authorized to access an object # noqa: E501 + """ + + # First, mock the response + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response(response_body, 200), + ] + body = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.batch_check( + body=[body], + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + ) + self.assertIsInstance(api_response, list) + self.assertEqual(len(api_response), 1) + self.assertEqual(api_response[0].error, None) + self.assertTrue(api_response[0].allowed) + self.assertEqual(api_response[0].request, body) + # Make sure the API was called with the right data + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_batch_check_multiple_request(self, mock_request): + """Test case for check with multiple request + + Check whether a user is authorized to access an object # noqa: E501 + """ + + # First, mock the response + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{"allowed": true, "resolution": "1234"}', 200), + mock_response('{"allowed": false, "resolution": "1234"}', 200), + mock_response('{"allowed": true, "resolution": "1234"}', 200), + ] + body1 = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + body2 = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ) + body3 = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.batch_check( + body=[body1, body2, body3], + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} + ) + self.assertIsInstance(api_response, list) + self.assertEqual(len(api_response), 3) + self.assertEqual(api_response[0].error, None) + self.assertTrue(api_response[0].allowed) + self.assertEqual(api_response[0].request, body1) + self.assertEqual(api_response[1].error, None) + self.assertFalse(api_response[1].allowed) + self.assertEqual(api_response[1].request, body2) + self.assertEqual(api_response[2].error, None) + self.assertTrue(api_response[2].allowed) + self.assertEqual(api_response[2].request, body3) + # Make sure the API was called with the right data + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + + @patch.object(rest.RESTClientObject, 'request') + def test_batch_check_multiple_request_fail(self, mock_request): + """Test case for check with multiple request with one request failed + + Check whether a user is authorized to access an object # noqa: E501 + """ + response_body = ''' +{ + "code": "validation_error", + "message": "Generic validation error" +} + ''' + + # First, mock the response + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{"allowed": true, "resolution": "1234"}', 200), + ValidationException(http_resp=http_mock_response(response_body, 400)), + mock_response('{"allowed": false, "resolution": "1234"}', 200), + ] + body1 = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + body2 = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ) + body3 = ClientCheckRequest( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.batch_check( + body=[body1, body2, body3], + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} + ) + self.assertIsInstance(api_response, list) + self.assertEqual(len(api_response), 3) + self.assertEqual(api_response[0].error, None) + self.assertTrue(api_response[0].allowed) + self.assertEqual(api_response[0].request, body1) + self.assertFalse(api_response[1].allowed) + self.assertEqual(api_response[1].request, body2) + self.assertIsInstance(api_response[1].error, ValidationException) + self.assertIsInstance(api_response[1].error.parsed_exception, ValidationErrorMessageResponse) + self.assertEqual(api_response[2].error, None) + self.assertFalse(api_response[2].allowed) + self.assertEqual(api_response[2].request, body3) + # Make sure the API was called with the right data + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + + @patch.object(rest.RESTClientObject, 'request') + def test_expand(self, mock_request): + """Test case for expand + + Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship # noqa: E501 + """ + response_body = '''{ + "tree": {"root": {"name": "document:budget#reader", "leaf": {"users": {"users": ["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]}}}}} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + body = ClientExpandRequest( + object="document:budget", + relation="reader", + ) + api_response = api_client.expand( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + ) + self.assertIsInstance(api_response, ExpandResponse) + curUsers = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) + leaf = Leaf(users=curUsers) + node = Node(name="document:budget#reader", leaf=leaf) + userTree = UsersetTree(node) + expected_response = ExpandResponse(userTree) + self.assertEqual(api_response, expected_response) + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:budget", "relation": "reader"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_list_objects(self, mock_request): + """Test case for list_objects + + List objects # noqa: E501 + """ + response_body = ''' +{ + "objects": [ + "document:abcd1234" + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + body = ClientListObjectsRequest( + type="document", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + # Get all stores + api_response = api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) + self.assertIsInstance(api_response, ListObjectsResponse) + self.assertEqual(api_response.objects, ['document:abcd1234']) + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects', + headers=ANY, + query_params=[], + post_params=[], + body={'authorization_model_id': '01GXSA8YR785C4FYS3C0RTG7B1', + 'type': 'document', 'relation': 'reader', 'user': 'user:81684243-9356-4421-8fbf-a4f8d36aa31b'}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + + @patch.object(rest.RESTClientObject, 'request') + def test_list_objects_contextual_tuples(self, mock_request): + """Test case for list_objects + + List objects # noqa: E501 + """ + response_body = ''' +{ + "objects": [ + "document:abcd1234" + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + body = ClientListObjectsRequest( + type="document", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + contextual_tuples=[ + ClientTuple( + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relation="writer", + object="document:budget", + ), + ], + ) + # Get all stores + api_response = api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) + self.assertIsInstance(api_response, ListObjectsResponse) + self.assertEqual(api_response.objects, ['document:abcd1234']) + mock_request.assert_called_once_with( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects', + headers=ANY, + query_params=[], + post_params=[], + body={'authorization_model_id': '01GXSA8YR785C4FYS3C0RTG7B1', + 'type': 'document', 'relation': 'reader', 'user': 'user:81684243-9356-4421-8fbf-a4f8d36aa31b', + 'contextual_tuples': {'tuple_keys': [{'object': 'document:budget','relation': 'writer','user': 'user:81684243-9356-4421-8fbf-a4f8d36aa31b'}]}}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_list_relations(self, mock_request): + """Test case for list relations + + Check whether a user is authorized to access an object # noqa: E501 + """ + + # First, mock the response + mock_request.side_effect = [ + mock_response('{}', 200), + mock_response('{"allowed": true, "resolution": "1234"}', 200), + mock_response('{"allowed": false, "resolution": "1234"}', 200), + mock_response('{"allowed": true, "resolution": "1234"}', 200), + ] + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.list_relations( + body=ClientListRelationsRequest(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relations=["reader", "owner", "viewer"], + object="document:2021-budget"), + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + ) + self.assertEqual(api_response, ["reader", "viewer"]) + + # Make sure the API was called with the right data + mock_request.assert_any_call( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "owner", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + mock_request.assert_any_call( + 'POST', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "viewer", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_list_relations_unauthorized(self, mock_request): + """Test case for list relations with 401 response + """ + + mock_request.side_effect = UnauthorizedException( + http_resp=http_mock_response('{}', 401) + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(UnauthorizedException) as api_exception: + api_client.list_relations( + body=ClientListRelationsRequest(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relations=["reader", "owner", "viewer"], + object="document:2021-budget"), + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + ) + + self.assertIsInstance(api_exception.exception, UnauthorizedException) + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 1) + + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01GXSA8YR785C4FYS3C0RTG7B1', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_read_assertions(self, mock_request): + """Test case for read assertions + + """ + response_body = ''' +{ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "assertions": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:anne" + }, + "expectation": true + } + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + with OpenFgaClient(configuration) as api_client: + api_response = api_client.read_assertions( + options={"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + self.assertEqual(api_response, ReadAssertionsResponse( + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + assertions=[Assertion( + tuple_key=TupleKey(object="document:2021-budget", relation="reader", + user="user:anne"), + expectation=True, + )] + )) + mock_request.assert_called_once_with( + 'GET', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_write_assertions(self, mock_request): + """Test case for write assertions + + Get all stores # noqa: E501 + """ + mock_request.return_value = mock_response('', 204) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_client.write_assertions( + [ClientAssertion(user="user:anne", relation="reader", object="document:2021-budget", expectation=True)], + options={"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + mock_request.assert_called_once_with( + 'PUT', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + body={"assertions": [{"tuple_key": {"object": "document:2021-budget","relation": "reader","user": "user:anne"},"expectation": True}]}, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + def test_set_store_id(self, mock_request): + """Test case for write assertions + + Get all stores # noqa: E501 + """ + mock_request.return_value = mock_response('', 204) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_client.set_store_id("01YCP46JKYM8FJCQ37NMBYHE5Y") + + api_client.write_assertions( + [ClientAssertion(user="user:anne", relation="reader", + object="document:2021-budget", expectation=True)], + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + self.assertEqual(api_client.get_store_id(), "01YCP46JKYM8FJCQ37NMBYHE5Y") + mock_request.assert_called_once_with( + 'PUT', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5Y/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + body={"assertions": [{"tuple_key": {"object": "document:2021-budget","relation": "reader","user": "user:anne"},"expectation": True}]}, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + + @patch.object(rest.RESTClientObject, 'request') + def test_config_auth_model(self, mock_request): + """Test case for write assertions + + Get all stores # noqa: E501 + """ + mock_request.return_value = mock_response('', 204) + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01G5JAVJ41T49E9TT3SKVS7X1J" + with OpenFgaClient(configuration) as api_client: + api_client.write_assertions( + [ClientAssertion(user="user:anne", relation="reader", + object="document:2021-budget", expectation=True)], + options={} + ) + self.assertEqual(api_client.get_authorization_model_id(), "01G5JAVJ41T49E9TT3SKVS7X1J") + mock_request.assert_called_once_with( + 'PUT', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + body={"assertions": [{"tuple_key": {"object": "document:2021-budget","relation": "reader","user": "user:anne"},"expectation": True}]}, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + + @patch.object(rest.RESTClientObject, 'request') + def test_update_auth_model(self, mock_request): + """Test case for write assertions + + Get all stores # noqa: E501 + """ + mock_request.return_value = mock_response('', 204) + configuration = self.configuration + configuration.store_id = store_id + configuration.authorization_model_id = "01G5JAVJ41T49E9TT3SKVS7X1J" + with OpenFgaClient(configuration) as api_client: + api_client.set_authorization_model_id("01G5JAVJ41T49E9TT3SKVS7X2J") + + api_client.write_assertions( + [ClientAssertion(user="user:anne", relation="reader", + object="document:2021-budget", expectation=True)], + options={} + ) + mock_request.assert_called_once_with( + 'PUT', + 'http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X2J', + headers=ANY, + body={"assertions": [{"tuple_key": {"object": "document:2021-budget","relation": "reader","user": "user:anne"},"expectation": True}]}, + query_params=[], + post_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + def test_configuration_store_id_invalid(self): + """ + Test whether ApiValueError is raised if host has query + """ + configuration = ClientConfiguration( + api_host='localhost', + api_scheme='http', + store_id="abcd" + ) + self.assertRaises(FgaValidationException, configuration.is_valid) + + def test_configuration_authorization_model_id_invalid(self): + """ + Test whether ApiValueError is raised if host has query + """ + configuration = ClientConfiguration( + api_host='localhost', + api_scheme='http', + store_id="01H15K9J85050XTEDPVM8DJM78", + authorization_model_id="abcd" + ) + self.assertRaises(FgaValidationException, configuration.is_valid) diff --git a/config/clients/python/template/credentials.mustache b/config/clients/python/template/credentials.mustache index 9e73f536..e9593fd6 100644 --- a/config/clients/python/template/credentials.mustache +++ b/config/clients/python/template/credentials.mustache @@ -1,13 +1,10 @@ {{>partial_header}} from dataclasses import dataclass -from datetime import datetime, timedelta -import json import typing -import urllib3 from urllib.parse import urlparse -from {{packageName}}.exceptions import FgaValidationException, ApiValueError, AuthenticationError +from {{packageName}}.exceptions import ApiValueError def none_or_empty(value): """ @@ -39,7 +36,7 @@ class CredentialConfiguration: self._api_audience = api_audience self._api_issuer = api_issuer self._api_token = api_token - + @property def client_id(self): @@ -126,8 +123,6 @@ class Credentials: ): self._method = method self._configuration = configuration - self._access_token = None - self._access_expiry_time = None @property def method(self): @@ -177,53 +172,3 @@ class Credentials: raise ApiValueError('api_issuer `{}` is invalid'.format(self.configuration.api_issuer)) if (parsed_url.netloc == ''): raise ApiValueError('api_issuer `{}` is invalid'.format(self.configuration.api_issuer)) - - def _token_valid(self): - """ - Return whether token is valid - """ - if self._access_token is None or self._access_expiry_time is None: - return False - if self._access_expiry_time < datetime.now(): - return False - return True - - async def _obtain_token(self, client): - """ - Perform OAuth2 and obtain token - """ - token_url = 'https://{}/oauth/token'.format(self.configuration.api_issuer) - body = { - 'client_id': self.configuration.client_id, - 'client_secret': self.configuration.client_secret, - 'audience': self.configuration.api_audience, - 'grant_type': "client_credentials", - } - headers = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) - raw_response = await client.POST(token_url, headers=headers, body=body); - if 200 <= raw_response.status <= 299: - try: - api_response = json.loads(raw_response.data) - except: # noqa: E722 - raise AuthenticationError(http_resp=raw_response) - if not api_response.get('expires_in') or not api_response.get('access_token'): - raise AuthenticationError(http_resp=raw_response) - self._access_expiry_time = datetime.now() + timedelta(seconds=int(api_response.get('expires_in'))) - self._access_token = api_response.get('access_token') - else: - raise AuthenticationError(http_resp=raw_response) - - async def get_authentication_header(self, client): - """ - If configured, return the header for authentication - """ - if self._method == 'none': - return {} - if self._method == 'api_token': - return {'Authorization': 'Bearer {}'.format(self.configuration.api_token)} - # check to see token is valid - if not self._token_valid(): - # In this case, the token is not valid, we need to get the refresh the token - await self._obtain_token(client) - return {'Authorization': 'Bearer {}'.format(self._access_token)} - diff --git a/config/clients/python/template/credentials_test.mustache b/config/clients/python/template/credentials_test.mustache index e635aff8..f8e4e206 100644 --- a/config/clients/python/template/credentials_test.mustache +++ b/config/clients/python/template/credentials_test.mustache @@ -4,30 +4,9 @@ from unittest import IsolatedAsyncioTestCase -from mock import patch -from datetime import datetime, timedelta +import {{packageName}}{{^asyncio}} as openfga_sdk{{/asyncio}} -import {{packageName}} -import urllib3 - -from {{packageName}} import rest from {{packageName}}.credentials import CredentialConfiguration, Credentials -from {{packageName}}.configuration import Configuration -from {{packageName}}.exceptions import AuthenticationError - - -# Helper function to construct mock response -def mock_response(body, status): - headers = urllib3.response.HTTPHeaderDict({ - 'content-type': 'application/json' - }) - obj = urllib3.HTTPResponse( - body, - headers, - status, - preload_content=False - ) - return rest.RESTResponse(obj, obj.data) class TestCredentials(IsolatedAsyncioTestCase): @@ -154,83 +133,3 @@ class TestCredentials(IsolatedAsyncioTestCase): with self.assertRaises(openfga_sdk.ApiValueError): credential.validate_credentials_config() - async def test_get_authentication_header(self): - """ - Test getting authentication header when method is none - """ - credential = Credentials() - auth_header = await credential.get_authentication_header(None) - self.assertEqual(auth_header, {}) - - async def test_get_authentication_api_token(self): - """ - Test getting authentication header when method is api token - """ - credential = Credentials(method="api_token", configuration=CredentialConfiguration(api_token='ABCDEFG')) - auth_header = await credential.get_authentication_header(None) - self.assertEqual(auth_header, {'Authorization': 'Bearer ABCDEFG'}) - - async def test_get_authentication_valid_client_credentials(self): - """ - Test getting authentication header when method is client credentials - """ - credential = Credentials(method="client_credentials", - configuration=CredentialConfiguration(client_id='myclientid', - client_secret='mysecret', api_issuer='www.testme.com', api_audience='myaudience')) - credential._access_token = 'XYZ123' - credential._access_expiry_time = datetime.now() + timedelta(seconds=60) - auth_header = await credential.get_authentication_header(None) - self.assertEqual(auth_header, {'Authorization': 'Bearer XYZ123'}) - - @patch.object(rest.RESTClientObject, 'request') - async def test_get_authentication_obtain_client_credentials(self, mock_request): - """ - Test getting authentication header when method is client credential and we need to obtain token - """ - response_body = ''' -{ - "expires_in": 120, - "access_token": "AABBCCDD" -} - ''' - mock_request.return_value = mock_response(response_body, 200) - - credential = Credentials(method="client_credentials", - configuration=CredentialConfiguration(client_id='myclientid', - client_secret='mysecret', api_issuer='www.testme.com', api_audience='myaudience')) - client = rest.RESTClientObject(Configuration()) - current_time = datetime.now() - auth_header = await credential.get_authentication_header(client) - self.assertEqual(auth_header, {'Authorization': 'Bearer AABBCCDD'}) - self.assertEqual(credential._access_token, 'AABBCCDD') - self.assertGreaterEqual(credential._access_expiry_time, current_time + timedelta(seconds=int(120))) - expected_header = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) - mock_request.assert_called_once_with( - 'POST', - 'https://www.testme.com/oauth/token', - headers=expected_header, - query_params=None, post_params=None, _preload_content=True, _request_timeout=None, - body={"client_id": "myclientid", "client_secret": "mysecret", "audience": "myaudience", "grant_type": "client_credentials"} - ) - await client.close() - - @patch.object(rest.RESTClientObject, 'request') - async def test_get_authentication_obtain_client_credentials_failed(self, mock_request): - """ - Test getting authentication header when method is client credential and we fail to obtain token - """ - response_body = ''' -{ - "reason": "Unauthorized" -} - ''' - mock_request.return_value = mock_response(response_body, 403) - - credential = Credentials(method="client_credentials", - configuration=CredentialConfiguration(client_id='myclientid', - client_secret='mysecret', api_issuer='www.testme.com', api_audience='myaudience')) - client = rest.RESTClientObject(Configuration()) - with self.assertRaises(AuthenticationError): - await credential.get_authentication_header(client) - await client.close() - diff --git a/config/clients/python/template/model_test.mustache b/config/clients/python/template/model_test.mustache index f5f4b415..4e669592 100644 --- a/config/clients/python/template/model_test.mustache +++ b/config/clients/python/template/model_test.mustache @@ -7,7 +7,7 @@ import datetime {{#models}} {{#model}} -import {{packageName}} +import {{packageName}}{{^asyncio}} as openfga_sdk{{/asyncio}} from {{modelPackage}}.{{classFilename}} import {{classname}} # noqa: E501 from {{packageName}}.rest import ApiException diff --git a/config/clients/python/template/oauth2.mustache b/config/clients/python/template/oauth2.mustache new file mode 100644 index 00000000..d9532654 --- /dev/null +++ b/config/clients/python/template/oauth2.mustache @@ -0,0 +1,67 @@ +{{>partial_header}} + +from dataclasses import dataclass +from datetime import datetime, timedelta +import json +import typing +import urllib3 +from urllib.parse import urlparse + +from {{packageName}}.credentials import Credentials +from {{packageName}}.exceptions import AuthenticationError + +class OAuth2Client: + + def __init__( + self, + credentials: Credentials + ): + self._credentials = credentials + self._access_token = None + self._access_expiry_time = None + + def _token_valid(self): + """ + Return whether token is valid + """ + if self._access_token is None or self._access_expiry_time is None: + return False + if self._access_expiry_time < datetime.now(): + return False + return True + + async def _obtain_token(self, client): + """ + Perform OAuth2 and obtain token + """ + configuration = self._credentials.configuration + token_url = 'https://{}/oauth/token'.format(configuration.api_issuer) + body = { + 'client_id': configuration.client_id, + 'client_secret': configuration.client_secret, + 'audience': configuration.api_audience, + 'grant_type': "client_credentials", + } + headers = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) + raw_response = await client.POST(token_url, headers=headers, body=body); + if 200 <= raw_response.status <= 299: + try: + api_response = json.loads(raw_response.data) + except: # noqa: E722 + raise AuthenticationError(http_resp=raw_response) + if not api_response.get('expires_in') or not api_response.get('access_token'): + raise AuthenticationError(http_resp=raw_response) + self._access_expiry_time = datetime.now() + timedelta(seconds=int(api_response.get('expires_in'))) + self._access_token = api_response.get('access_token') + else: + raise AuthenticationError(http_resp=raw_response) + + async def get_authentication_header(self, client): + """ + If configured, return the header for authentication + """ + # check to see token is valid + if not self._token_valid(): + # In this case, the token is not valid, we need to get the refresh the token + await self._obtain_token(client) + return {'Authorization': 'Bearer {}'.format(self._access_token)} diff --git a/config/clients/python/template/oauth2_sync.mustache b/config/clients/python/template/oauth2_sync.mustache new file mode 100644 index 00000000..0978d5a4 --- /dev/null +++ b/config/clients/python/template/oauth2_sync.mustache @@ -0,0 +1,67 @@ +{{>partial_header}} + +from dataclasses import dataclass +from datetime import datetime, timedelta +import json +import typing +import urllib3 +from urllib.parse import urlparse + +from {{packageName}}.credentials import Credentials +from {{packageName}}.exceptions import AuthenticationError + +class OAuth2Client: + + def __init__( + self, + credentials: Credentials + ): + self._credentials = credentials + self._access_token = None + self._access_expiry_time = None + + def _token_valid(self): + """ + Return whether token is valid + """ + if self._access_token is None or self._access_expiry_time is None: + return False + if self._access_expiry_time < datetime.now(): + return False + return True + + def _obtain_token(self, client): + """ + Perform OAuth2 and obtain token + """ + configuration = self._credentials.configuration + token_url = 'https://{}/oauth/token'.format(configuration.api_issuer) + body = { + 'client_id': configuration.client_id, + 'client_secret': configuration.client_secret, + 'audience': configuration.api_audience, + 'grant_type': "client_credentials", + } + headers = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) + raw_response = client.POST(token_url, headers=headers, body=body); + if 200 <= raw_response.status <= 299: + try: + api_response = json.loads(raw_response.data) + except: # noqa: E722 + raise AuthenticationError(http_resp=raw_response) + if not api_response.get('expires_in') or not api_response.get('access_token'): + raise AuthenticationError(http_resp=raw_response) + self._access_expiry_time = datetime.now() + timedelta(seconds=int(api_response.get('expires_in'))) + self._access_token = api_response.get('access_token') + else: + raise AuthenticationError(http_resp=raw_response) + + def get_authentication_header(self, client): + """ + If configured, return the header for authentication + """ + # check to see token is valid + if not self._token_valid(): + # In this case, the token is not valid, we need to get the refresh the token + self._obtain_token(client) + return {'Authorization': 'Bearer {}'.format(self._access_token)} diff --git a/config/clients/python/template/oauth2_test.mustache b/config/clients/python/template/oauth2_test.mustache new file mode 100644 index 00000000..a1bad033 --- /dev/null +++ b/config/clients/python/template/oauth2_test.mustache @@ -0,0 +1,101 @@ +# coding: utf-8 + +{{>partial_header}} + +import urllib3 + +from unittest import IsolatedAsyncioTestCase +from mock import patch +from datetime import datetime, timedelta +from {{packageName}}.oauth2 import OAuth2Client +from {{packageName}} import rest +from {{packageName}}.credentials import CredentialConfiguration, Credentials +from {{packageName}}.configuration import Configuration +from {{packageName}}.exceptions import AuthenticationError + + +# Helper function to construct mock response +def mock_response(body, status): + headers = urllib3.response.HTTPHeaderDict({ + 'content-type': 'application/json' + }) + obj = urllib3.HTTPResponse( + body, + headers, + status, + preload_content=False + ) + return rest.RESTResponse(obj, obj.data) + +class TestOAuth2Client(IsolatedAsyncioTestCase): + """TestOAuth2Client unit test""" + + def setUp(self): + pass + + def tearDown(self): + pass + + async def test_get_authentication_valid_client_credentials(self): + """ + Test getting authentication header when method is client credentials + """ + client = OAuth2Client(None) + client._access_token = 'XYZ123' + client._access_expiry_time = datetime.now() + timedelta(seconds=60) + auth_header = await client.get_authentication_header(None) + self.assertEqual(auth_header, {'Authorization': 'Bearer XYZ123'}) + + @patch.object(rest.RESTClientObject, 'request') + async def test_get_authentication_obtain_client_credentials(self, mock_request): + """ + Test getting authentication header when method is client credential and we need to obtain token + """ + response_body = ''' +{ + "expires_in": 120, + "access_token": "AABBCCDD" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + + credentials = Credentials(method="client_credentials", + configuration=CredentialConfiguration(client_id='myclientid', + client_secret='mysecret', api_issuer='www.testme.com', api_audience='myaudience')) + rest_client = rest.RESTClientObject(Configuration()) + current_time = datetime.now() + client = OAuth2Client(credentials) + auth_header = await client.get_authentication_header(rest_client) + self.assertEqual(auth_header, {'Authorization': 'Bearer AABBCCDD'}) + self.assertEqual(client._access_token, 'AABBCCDD') + self.assertGreaterEqual(client._access_expiry_time, current_time + timedelta(seconds=int(120))) + expected_header = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) + mock_request.assert_called_once_with( + 'POST', + 'https://www.testme.com/oauth/token', + headers=expected_header, + query_params=None, post_params=None, _preload_content=True, _request_timeout=None, + body={"client_id": "myclientid", "client_secret": "mysecret", "audience": "myaudience", "grant_type": "client_credentials"} + ) + await rest_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_get_authentication_obtain_client_credentials_failed(self, mock_request): + """ + Test getting authentication header when method is client credential and we fail to obtain token + """ + response_body = ''' +{ + "reason": "Unauthorized" +} + ''' + mock_request.return_value = mock_response(response_body, 403) + + credentials = Credentials(method="client_credentials", + configuration=CredentialConfiguration(client_id='myclientid', + client_secret='mysecret', api_issuer='www.testme.com', api_audience='myaudience')) + rest_client = rest.RESTClientObject(Configuration()) + client = OAuth2Client(credentials) + with self.assertRaises(AuthenticationError): + await client.get_authentication_header(rest_client) + await rest_client.close() diff --git a/config/clients/python/template/oauth2_test_sync.mustache b/config/clients/python/template/oauth2_test_sync.mustache new file mode 100644 index 00000000..4bb1be65 --- /dev/null +++ b/config/clients/python/template/oauth2_test_sync.mustache @@ -0,0 +1,102 @@ +# coding: utf-8 + +{{>partial_header}} + + +import urllib3 + +from unittest import IsolatedAsyncioTestCase +from mock import patch +from datetime import datetime, timedelta +from {{packageName}}.sync.oauth2 import OAuth2Client +from {{packageName}}.sync import rest +from {{packageName}}.credentials import CredentialConfiguration, Credentials +from {{packageName}}.configuration import Configuration +from {{packageName}}.exceptions import AuthenticationError + + +# Helper function to construct mock response +def mock_response(body, status): + headers = urllib3.response.HTTPHeaderDict({ + 'content-type': 'application/json' + }) + obj = urllib3.HTTPResponse( + body, + headers, + status, + preload_content=False + ) + return rest.RESTResponse(obj, obj.data) + +class TestOAuth2Client(IsolatedAsyncioTestCase): + """TestOAuth2Client unit test""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_get_authentication_valid_client_credentials(self): + """ + Test getting authentication header when method is client credentials + """ + client = OAuth2Client(None) + client._access_token = 'XYZ123' + client._access_expiry_time = datetime.now() + timedelta(seconds=60) + auth_header = client.get_authentication_header(None) + self.assertEqual(auth_header, {'Authorization': 'Bearer XYZ123'}) + + @patch.object(rest.RESTClientObject, 'request') + def test_get_authentication_obtain_client_credentials(self, mock_request): + """ + Test getting authentication header when method is client credential and we need to obtain token + """ + response_body = ''' +{ + "expires_in": 120, + "access_token": "AABBCCDD" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + + credentials = Credentials(method="client_credentials", + configuration=CredentialConfiguration(client_id='myclientid', + client_secret='mysecret', api_issuer='www.testme.com', api_audience='myaudience')) + rest_client = rest.RESTClientObject(Configuration()) + current_time = datetime.now() + client = OAuth2Client(credentials) + auth_header = client.get_authentication_header(rest_client) + self.assertEqual(auth_header, {'Authorization': 'Bearer AABBCCDD'}) + self.assertEqual(client._access_token, 'AABBCCDD') + self.assertGreaterEqual(client._access_expiry_time, current_time + timedelta(seconds=int(120))) + expected_header = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) + mock_request.assert_called_once_with( + 'POST', + 'https://www.testme.com/oauth/token', + headers=expected_header, + query_params=None, post_params=None, _preload_content=True, _request_timeout=None, + body={"client_id": "myclientid", "client_secret": "mysecret", "audience": "myaudience", "grant_type": "client_credentials"} + ) + rest_client.close() + + @patch.object(rest.RESTClientObject, 'request') + def test_get_authentication_obtain_client_credentials_failed(self, mock_request): + """ + Test getting authentication header when method is client credential and we fail to obtain token + """ + response_body = ''' +{ + "reason": "Unauthorized" +} + ''' + mock_request.return_value = mock_response(response_body, 403) + + credentials = Credentials(method="client_credentials", + configuration=CredentialConfiguration(client_id='myclientid', + client_secret='mysecret', api_issuer='www.testme.com', api_audience='myaudience')) + rest_client = rest.RESTClientObject(Configuration()) + client = OAuth2Client(credentials) + with self.assertRaises(AuthenticationError): + client.get_authentication_header(rest_client) + rest_client.close() diff --git a/config/clients/python/template/rest.mustache b/config/clients/python/template/rest_sync.mustache similarity index 98% rename from config/clients/python/template/rest.mustache rename to config/clients/python/template/rest_sync.mustache index 28332e13..d547fb20 100644 --- a/config/clients/python/template/rest.mustache +++ b/config/clients/python/template/rest_sync.mustache @@ -13,7 +13,7 @@ import six from six.moves.urllib.parse import urlencode import urllib3 -from {{packageName}}.exceptions import ApiException, UnauthorizedException, ForbiddenException, NotFoundException, ServiceException, ApiValueError +from {{packageName}}.exceptions import ApiException, UnauthorizedException, ForbiddenException, NotFoundException, ServiceException, ApiValueError, ValidationException logger = logging.getLogger(__name__) @@ -21,15 +21,15 @@ logger = logging.getLogger(__name__) class RESTResponse(io.IOBase): - def __init__(self, resp): + def __init__(self, resp, data): self.urllib3_response = resp self.status = resp.status self.reason = resp.reason - self.data = resp.data + self.data = data def getheaders(self): """Returns a dictionary of the response headers.""" - return self.urllib3_response.getheaders() + return self.urllib3_response.headers def getheader(self, name, default=None): """Returns a given response header.""" @@ -91,6 +91,9 @@ class RESTClientObject(object): **addition_pool_args ) + def close(self): + self.pool_manager.clear() + def request(self, method, url, query_params=None, headers=None, body=None, post_params=None, _preload_content=True, _request_timeout=None): @@ -200,7 +203,7 @@ class RESTClientObject(object): raise ApiException(status=0, reason=msg) if _preload_content: - r = RESTResponse(r) + r = RESTResponse(r, r.data) # log response body logger.debug("response body: %s", r.data) diff --git a/docs/openapi/.gitkeep b/docs/openapi/.gitkeep deleted file mode 100644 index e69de29b..00000000