From 19eb50166dbef4de3d04c9156ae9e1a07523edae Mon Sep 17 00:00:00 2001 From: "J.R. Hill" Date: Wed, 3 Jan 2024 16:58:06 -0800 Subject: [PATCH 1/3] feat(python-sdk): add condition support --- .github/workflows/main.yaml | 2 +- .../clients/python/template/api_test.mustache | 34 +++++++++++-------- .../python/template/api_test_sync.mustache | 34 +++++++++++-------- .../python/template/client/client.mustache | 18 +++++----- .../template/client/client_sync.mustache | 18 +++++----- .../client/models/write_request.mustache | 7 ++-- .../template/client/test_client.mustache | 32 ++++++++++------- .../template/client/test_client_sync.mustache | 32 ++++++++++------- 8 files changed, 105 insertions(+), 72 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 8c3ed411..2a5b77f1 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -162,7 +162,7 @@ jobs: - name: Run All Tests run: |- - make test-client-python OPEN_API_REF=0f1d73e766d26ac5c004383d741ee0f815c9b1e6 # TODO: Remove OPEN_API_REF after support for conditions + make test-client-python - name: Check for SDK changes run: ./scripts/commit_push_changes.sh diff --git a/config/clients/python/template/api_test.mustache b/config/clients/python/template/api_test.mustache index 6b1ba38b..f735242a 100644 --- a/config/clients/python/template/api_test.mustache +++ b/config/clients/python/template/api_test.mustache @@ -23,6 +23,7 @@ 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_request_tuple_key import ExpandRequestTupleKey from {{packageName}}.models.expand_response import ExpandResponse from {{packageName}}.models.get_store_response import GetStoreResponse from {{packageName}}.models.internal_error_code import InternalErrorCode @@ -39,12 +40,15 @@ from {{packageName}}.models.read_assertions_response import ReadAssertionsRespon 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_request_tuple_key import ReadRequestTupleKey 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_key_without_condition import TupleKeyWithoutCondition +from {{packageName}}.models.write_request_writes import WriteRequestWrites +from {{packageName}}.models.write_request_deletes import WriteRequestDeletes from {{packageName}}.models.tuple_operation import TupleOperation from {{packageName}}.models.type_definition import TypeDefinition from {{packageName}}.models.users import Users @@ -207,7 +211,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = ExpandRequest( - tuple_key=TupleKey( + tuple_key=ExpandRequestTupleKey( object="document:budget", relation="reader", ), @@ -217,8 +221,8 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): body=body, ) self.assertIsInstance(api_response, ExpandResponse) - curUsers = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) - leaf = Leaf(users=curUsers) + cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) + leaf = Leaf(users=cur_users) node = Node(name="document:budget#reader", leaf=leaf) userTree = UsersetTree(node) expected_response = ExpandResponse(userTree) @@ -393,7 +397,8 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -402,7 +407,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = ReadRequest( - tuple_key=TupleKey( + tuple_key=ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -416,7 +421,8 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -461,7 +467,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): self.assertIsInstance(api_response, ReadAssertionsResponse) self.assertEqual(api_response.authorization_model_id, '01G5JAVJ41T49E9TT3SKVS7X1J') assertion=Assertion( - tuple_key=TupleKey( + tuple_key=TupleKeyWithoutCondition( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -635,7 +641,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # example passing only required values which don't have defaults set body = WriteRequest( - writes=TupleKeys( + writes=WriteRequestWrites( tuple_keys=[ TupleKey( object="document:2021-budget", @@ -678,7 +684,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # example passing only required values which don't have defaults set body = WriteRequest( - deletes=TupleKeys( + deletes=WriteRequestDeletes( tuple_keys=[ TupleKey( object="document:2021-budget", @@ -1137,11 +1143,11 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): 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'}) + expected_headers = 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, + headers=expected_headers, query_params=[], post_params=[], body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, @@ -1178,11 +1184,11 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): 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'}) + expected_headers = 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, + headers=expected_headers, query_params=[], post_params=[], body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, diff --git a/config/clients/python/template/api_test_sync.mustache b/config/clients/python/template/api_test_sync.mustache index 30a0ad44..30536f4b 100644 --- a/config/clients/python/template/api_test_sync.mustache +++ b/config/clients/python/template/api_test_sync.mustache @@ -24,6 +24,7 @@ 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_request_tuple_key import ExpandRequestTupleKey from {{packageName}}.models.expand_response import ExpandResponse from {{packageName}}.models.get_store_response import GetStoreResponse from {{packageName}}.models.internal_error_code import InternalErrorCode @@ -40,12 +41,15 @@ from {{packageName}}.models.read_assertions_response import ReadAssertionsRespon 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_request_tuple_key import ReadRequestTupleKey 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_key_without_condition import TupleKeyWithoutCondition +from {{packageName}}.models.write_request_writes import WriteRequestWrites +from {{packageName}}.models.write_request_deletes import WriteRequestDeletes from {{packageName}}.models.tuple_operation import TupleOperation from {{packageName}}.models.type_definition import TypeDefinition from {{packageName}}.models.users import Users @@ -208,7 +212,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): with ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = ExpandRequest( - tuple_key=TupleKey( + tuple_key=ExpandRequestTupleKey( object="document:budget", relation="reader", ), @@ -218,8 +222,8 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): body=body, ) self.assertIsInstance(api_response, ExpandResponse) - curUsers = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) - leaf = Leaf(users=curUsers) + cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) + leaf = Leaf(users=cur_users) node = Node(name="document:budget#reader", leaf=leaf) userTree = UsersetTree(node) expected_response = ExpandResponse(userTree) @@ -394,7 +398,8 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -403,7 +408,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): with ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = ReadRequest( - tuple_key=TupleKey( + tuple_key=ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -417,7 +422,8 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -462,7 +468,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): self.assertIsInstance(api_response, ReadAssertionsResponse) self.assertEqual(api_response.authorization_model_id, '01G5JAVJ41T49E9TT3SKVS7X1J') assertion=Assertion( - tuple_key=TupleKey( + tuple_key=TupleKeyWithoutCondition( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -636,7 +642,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): # example passing only required values which don't have defaults set body = WriteRequest( - writes=TupleKeys( + writes=WriteRequestWrites( tuple_keys=[ TupleKey( object="document:2021-budget", @@ -679,7 +685,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): # example passing only required values which don't have defaults set body = WriteRequest( - deletes=TupleKeys( + deletes=WriteRequestDeletes( tuple_keys=[ TupleKey( object="document:2021-budget", @@ -1138,11 +1144,11 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): 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'}) + expected_headers = 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, + headers=expected_headers, query_params=[], post_params=[], body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, @@ -1179,11 +1185,11 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): 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'}) + expected_headers = 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, + headers=expected_headers, query_params=[], post_params=[], body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, diff --git a/config/clients/python/template/client/client.mustache b/config/clients/python/template/client/client.mustache index ebe8d333..5c278790 100644 --- a/config/clients/python/template/client/client.mustache +++ b/config/clients/python/template/client/client.mustache @@ -31,9 +31,11 @@ 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.expand_request_tuple_key import ExpandRequestTupleKey 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.read_request_tuple_key import ReadRequestTupleKey from {{packageName}}.models.tuple_key import TupleKey from {{packageName}}.models.write_assertions_request import WriteAssertionsRequest from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest @@ -67,7 +69,7 @@ def set_heading_if_not_set(options: dict[str, int|str], name: str, value: str): def options_to_kwargs(options: dict[str, int|str] = None): """ - Return kargs with continuation_token and page_size + Return kwargs with continuation_token and page_size """ kwargs = {} if options is not None: @@ -127,7 +129,7 @@ class OpenFgaClient(): 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 + 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: @@ -153,13 +155,13 @@ class OpenFgaClient(): def set_authorization_model_id(self, value): """ - Update the authorizaiton model id in the configuration + Update the authorization model id in the configuration """ self._client_configuration.authorization_model_id = value def get_authorization_model_id(self): """ - Return the authorizaiton model id + Return the authorization model id """ return self._client_configuration.authorization_model_id @@ -187,7 +189,7 @@ class OpenFgaClient(): :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 + # convert options to kwargs options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListStores") kwargs = options_to_kwargs(options) api_response = {{#asyncio}}await {{/asyncio}}self._api.list_stores( @@ -296,7 +298,7 @@ class OpenFgaClient(): {{#asyncio}}async {{/asyncio}}def read_latest_authorization_model(self, options: dict[str, int|str] = None): """ - Convenient method of reading the latest authorizaiton model + Convenient method of reading the latest 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 @@ -330,7 +332,7 @@ class OpenFgaClient(): ) return api_response - {{#asyncio}}async {{/asyncio}}def read(self, body: TupleKey, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read(self, body: ReadRequestTupleKey, options: dict[str, str] = None): """ Read changes for specified type :param body - the tuples we want to read @@ -588,7 +590,7 @@ class OpenFgaClient(): kwargs = options_to_kwargs(options) req_body = ExpandRequest( - tuple_key=TupleKey( + tuple_key=ExpandRequestTupleKey( relation=body.relation, object=body.object, ), diff --git a/config/clients/python/template/client/client_sync.mustache b/config/clients/python/template/client/client_sync.mustache index e526ab21..c25901c3 100644 --- a/config/clients/python/template/client/client_sync.mustache +++ b/config/clients/python/template/client/client_sync.mustache @@ -22,9 +22,11 @@ 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.expand_request_tuple_key import ExpandRequestTupleKey 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.read_request_tuple_key import ReadRequestTupleKey from {{packageName}}.models.tuple_key import TupleKey from {{packageName}}.models.write_assertions_request import WriteAssertionsRequest from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest @@ -62,7 +64,7 @@ def set_heading_if_not_set(options: dict[str, int|str], name: str, value: str): def options_to_kwargs(options: dict[str, int|str] = None): """ - Return kargs with continuation_token and page_size + Return kwargs with continuation_token and page_size """ kwargs = {} if options is not None: @@ -113,7 +115,7 @@ class OpenFgaClient(): 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 + 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: @@ -139,13 +141,13 @@ class OpenFgaClient(): def set_authorization_model_id(self, value): """ - Update the authorizaiton model id in the configuration + Update the authorization model id in the configuration """ self._client_configuration.authorization_model_id = value def get_authorization_model_id(self): """ - Return the authorizaiton model id + Return the authorization model id """ return self._client_configuration.authorization_model_id @@ -173,7 +175,7 @@ class OpenFgaClient(): :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 + # convert options to kwargs options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListStores") kwargs = options_to_kwargs(options) api_response = self._api.list_stores( @@ -282,7 +284,7 @@ class OpenFgaClient(): def read_latest_authorization_model(self, options: dict[str, int|str] = None): """ - Convenient method of reading the latest authorizaiton model + Convenient method of reading the latest 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 @@ -316,7 +318,7 @@ class OpenFgaClient(): ) return api_response - def read(self, body: TupleKey, options: dict[str, str] = None): + def read(self, body: ReadRequestTupleKey, options: dict[str, str] = None): """ Read changes for specified type :param body - the tuples we want to read @@ -558,7 +560,7 @@ class OpenFgaClient(): kwargs = options_to_kwargs(options) req_body = ExpandRequest( - tuple_key=TupleKey( + tuple_key=ExpandRequestTupleKey( relation=body.relation, object=body.object, ), diff --git a/config/clients/python/template/client/models/write_request.mustache b/config/clients/python/template/client/models/write_request.mustache index 996b9d1f..598cbe95 100644 --- a/config/clients/python/template/client/models/write_request.mustache +++ b/config/clients/python/template/client/models/write_request.mustache @@ -2,7 +2,8 @@ {{>partial_header}} from {{packageName}}.client.models.tuple import ClientTuple, convert_tuple_keys -from {{packageName}}.models.tuple_keys import TupleKeys +from {{packageName}}.models.write_request_writes import WriteRequestWrites +from {{packageName}}.models.write_request_deletes import WriteRequestDeletes from typing import List @@ -51,7 +52,7 @@ class ClientWriteRequest(): keys = convert_tuple_keys(self.writes) if keys is None: return None - return TupleKeys(tuple_keys=keys) + return WriteRequestWrites(tuple_keys=keys) @property def deletes_tuple_keys(self): @@ -61,4 +62,4 @@ class ClientWriteRequest(): keys = convert_tuple_keys(self.deletes) if keys is None: return None - return TupleKeys(tuple_keys=keys) + return WriteRequestDeletes(tuple_keys=keys) diff --git a/config/clients/python/template/client/test_client.mustache b/config/clients/python/template/client/test_client.mustache index 12b3b0a9..37391f0d 100644 --- a/config/clients/python/template/client/test_client.mustache +++ b/config/clients/python/template/client/test_client.mustache @@ -38,11 +38,13 @@ from {{packageName}}.models.read_assertions_response import ReadAssertionsRespon 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_request_tuple_key import ReadRequestTupleKey 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_key_without_condition import TupleKeyWithoutCondition from {{packageName}}.models.tuple_operation import TupleOperation from {{packageName}}.models.type_definition import TypeDefinition from {{packageName}}.models.users import Users @@ -626,7 +628,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -634,7 +637,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration.store_id = store_id # Enter a context with an instance of the API client {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - body = TupleKey( + body = ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -647,7 +650,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -679,7 +683,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -687,7 +692,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration.store_id = store_id # Enter a context with an instance of the API client {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - body = TupleKey( + body = ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -700,7 +705,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -731,7 +737,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -739,7 +746,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration.store_id = store_id # Enter a context with an instance of the API client {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - body = TupleKey() + body = ReadRequestTupleKey() api_response = {{#asyncio}}await {{/asyncio}}api_client.read( body=body, options={} @@ -748,7 +755,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -1813,8 +1821,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} ) self.assertIsInstance(api_response, ExpandResponse) - curUsers = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) - leaf = Leaf(users=curUsers) + cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) + leaf = Leaf(users=cur_users) node = Node(name="document:budget#reader", leaf=leaf) userTree = UsersetTree(node) expected_response = ExpandResponse(userTree) @@ -2052,7 +2060,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response, ReadAssertionsResponse( authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", assertions=[Assertion( - tuple_key=TupleKey(object="document:2021-budget", relation="reader", + tuple_key=TupleKeyWithoutCondition(object="document:2021-budget", relation="reader", user="user:anne"), expectation=True, )] diff --git a/config/clients/python/template/client/test_client_sync.mustache b/config/clients/python/template/client/test_client_sync.mustache index e7608a5d..7219403e 100644 --- a/config/clients/python/template/client/test_client_sync.mustache +++ b/config/clients/python/template/client/test_client_sync.mustache @@ -38,11 +38,13 @@ from {{packageName}}.models.read_assertions_response import ReadAssertionsRespon 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_request_tuple_key import ReadRequestTupleKey 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_key_without_condition import TupleKeyWithoutCondition from {{packageName}}.models.tuple_operation import TupleOperation from {{packageName}}.models.type_definition import TypeDefinition from {{packageName}}.models.users import Users @@ -626,7 +628,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -634,7 +637,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration.store_id = store_id # Enter a context with an instance of the API client with OpenFgaClient(configuration) as api_client: - body = TupleKey( + body = ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -647,7 +650,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -679,7 +683,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -687,7 +692,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration.store_id = store_id # Enter a context with an instance of the API client with OpenFgaClient(configuration) as api_client: - body = TupleKey( + body = ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -700,7 +705,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -731,7 +737,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, "timestamp": "2021-10-06T15:32:11.128Z" } - ] + ], + "continuation_token": "" } ''' mock_request.return_value = mock_response(response_body, 200) @@ -739,7 +746,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration.store_id = store_id # Enter a context with an instance of the API client with OpenFgaClient(configuration) as api_client: - body = TupleKey() + body = ReadRequestTupleKey() api_response = api_client.read( body=body, options={} @@ -748,7 +755,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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)]) + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( 'POST', @@ -1813,8 +1821,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} ) self.assertIsInstance(api_response, ExpandResponse) - curUsers = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) - leaf = Leaf(users=curUsers) + cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) + leaf = Leaf(users=cur_users) node = Node(name="document:budget#reader", leaf=leaf) userTree = UsersetTree(node) expected_response = ExpandResponse(userTree) @@ -2052,7 +2060,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response, ReadAssertionsResponse( authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", assertions=[Assertion( - tuple_key=TupleKey(object="document:2021-budget", relation="reader", + tuple_key=TupleKeyWithoutCondition(object="document:2021-budget", relation="reader", user="user:anne"), expectation=True, )] From 14ef33da465b9ef2fa48a3972e69de5b180a6ac2 Mon Sep 17 00:00:00 2001 From: "J.R. Hill" Date: Fri, 5 Jan 2024 16:37:17 -0800 Subject: [PATCH 2/3] feat(python-sdk): add condition support to client --- .../python/template/client/client.mustache | 2 ++ .../template/client/client_sync.mustache | 2 ++ .../client/models/check_request.mustache | 25 ++++++++++++++++--- .../models/list_objects_request.mustache | 19 ++++++++++++-- .../template/client/models/tuple.mustache | 19 +++++++++++++- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/config/clients/python/template/client/client.mustache b/config/clients/python/template/client/client.mustache index 5c278790..c95da928 100644 --- a/config/clients/python/template/client/client.mustache +++ b/config/clients/python/template/client/client.mustache @@ -510,6 +510,7 @@ class OpenFgaClient(): relation=body.relation, object=body.object, ), + context=body.context, authorization_model_id=self._get_authorization_model_id(options), ) if body.contextual_tuples: @@ -620,6 +621,7 @@ class OpenFgaClient(): user=body.user, relation=body.relation, type=body.type, + context=body.context, ) if body.contextual_tuples: req_body.contextual_tuples = ContextualTupleKeys( diff --git a/config/clients/python/template/client/client_sync.mustache b/config/clients/python/template/client/client_sync.mustache index c25901c3..4ae66936 100644 --- a/config/clients/python/template/client/client_sync.mustache +++ b/config/clients/python/template/client/client_sync.mustache @@ -490,6 +490,7 @@ class OpenFgaClient(): relation=body.relation, object=body.object, ), + context=body.context, authorization_model_id=self._get_authorization_model_id(options), ) if body.contextual_tuples: @@ -590,6 +591,7 @@ class OpenFgaClient(): user=body.user, relation=body.relation, type=body.type, + context=body.context, ) if body.contextual_tuples: req_body.contextual_tuples = ContextualTupleKeys( diff --git a/config/clients/python/template/client/models/check_request.mustache b/config/clients/python/template/client/models/check_request.mustache index 3d40d6a8..340403d9 100644 --- a/config/clients/python/template/client/models/check_request.mustache +++ b/config/clients/python/template/client/models/check_request.mustache @@ -3,23 +3,26 @@ from {{packageName}}.client.models.tuple import ClientTuple -from typing import List +from typing import List, Any -def construct_check_request(user: str, relation: str, object: str, contextual_tuples: List[ClientTuple]=None): + +def construct_check_request(user: str, relation: str, object: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): """ helper function to construct the check request body """ - return ClientCheckRequest(user, relation, object, contextual_tuples) + return ClientCheckRequest(user, relation, object, context, contextual_tuples) + class ClientCheckRequest(): """ ClientCheckRequest encapsulates the parameters for check request """ - def __init__(self, user: str, relation: str, object: str, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relation: str, object: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): self._user = user self._relation = relation self._object = object + self._context = context self._contextual_tuples = None if contextual_tuples: self._contextual_tuples = contextual_tuples @@ -45,6 +48,13 @@ class ClientCheckRequest(): """ return self._object + @property + def context(self): + """ + Return context + """ + return self._context + @property def contextual_tuples(self): """ @@ -73,6 +83,13 @@ class ClientCheckRequest(): """ self._object = value + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value + @contextual_tuples.setter def contextual_tuples(self, value): """ diff --git a/config/clients/python/template/client/models/list_objects_request.mustache b/config/clients/python/template/client/models/list_objects_request.mustache index 5dddf5cb..2cc03b67 100644 --- a/config/clients/python/template/client/models/list_objects_request.mustache +++ b/config/clients/python/template/client/models/list_objects_request.mustache @@ -3,17 +3,18 @@ from {{packageName}}.client.models.tuple import ClientTuple -from typing import List +from typing import List, Any class ClientListObjectsRequest(): """ ClientListObjectsRequest encapsulates the parameters required for list objects """ - def __init__(self, user: str, relation: str, type: str, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relation: str, type: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): self._user = user self._relation = relation self._type = type + self._context = context self._contextual_tuples = contextual_tuples @property @@ -37,6 +38,13 @@ class ClientListObjectsRequest(): """ return self._type + @property + def context(self): + """ + Return context + """ + return self._context + @property def contextual_tuples(self): """ @@ -65,6 +73,13 @@ class ClientListObjectsRequest(): """ self._type = value + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value + @contextual_tuples.setter def contextual_tuples(self, value): """ diff --git a/config/clients/python/template/client/models/tuple.mustache b/config/clients/python/template/client/models/tuple.mustache index d2ba4f92..99fd3dfc 100644 --- a/config/clients/python/template/client/models/tuple.mustache +++ b/config/clients/python/template/client/models/tuple.mustache @@ -1,6 +1,7 @@ # coding: utf-8 {{>partial_header}} +from {{packageName}}.models.relationship_condition import RelationshipCondition from {{packageName}}.models.tuple_key import TupleKey from typing import List @@ -10,10 +11,11 @@ class ClientTuple(): ClientTuple encapsulates the client tuple """ - def __init__(self, user: str, relation: str, object: str): + def __init__(self, user: str, relation: str, object: str, condition: RelationshipCondition = None): self._user = user self._relation = relation self._object = object + self._condition = condition def __eq__(self, other): return self.user == other.user and self.relation == other.relation and self.object == other.object @@ -39,6 +41,13 @@ class ClientTuple(): """ return self._object + @property + def condition(self): + """ + Return condition + """ + return self._condition + @user.setter def user(self, value): """ @@ -60,6 +69,13 @@ class ClientTuple(): """ self._object = value + @condition.setter + def condition(self, value): + """ + Set condition + """ + self._condition = value + @property def tuple_key(self): """ @@ -69,6 +85,7 @@ class ClientTuple(): object=self.object, relation=self.relation, user=self.user, + condition=self.condition ) From bb574bb20b8f6412e8731468504da66520ddfb6f Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Mon, 8 Jan 2024 23:06:58 -0500 Subject: [PATCH 3/3] release(python): v0.3.4 with conditions support --- config/clients/go/CHANGELOG.md.mustache | 9 + config/clients/python/config.overrides.json | 8 +- .../template/README_calling_api.mustache | 93 +++++++-- .../python/template/client/client.mustache | 2 +- .../template/client/client_sync.mustache | 2 +- .../client/models/check_request.mustache | 36 ++-- .../models/list_objects_request.mustache | 32 +-- .../models/list_relations_request.mustache | 18 +- .../clients/python/template/example/Makefile | 7 +- .../template/example/example1/auth-model.json | 73 ------- .../template/example/example1/example1.py | 182 +++++++++++++++--- ...irements.txt => requirements.txt.mustache} | 2 +- 12 files changed, 307 insertions(+), 157 deletions(-) delete mode 100644 config/clients/python/template/example/example1/auth-model.json rename config/clients/python/template/example/example1/{requirements.txt => requirements.txt.mustache} (82%) diff --git a/config/clients/go/CHANGELOG.md.mustache b/config/clients/go/CHANGELOG.md.mustache index abe17fc2..57e6f3fa 100644 --- a/config/clients/go/CHANGELOG.md.mustache +++ b/config/clients/go/CHANGELOG.md.mustache @@ -1,5 +1,14 @@ # Changelog +## v0.3.4 + +### [0.3.4](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.3.3...v0.3.4) (2023-12-21) + +- feat: support for conditions +- chore: use latest API interfaces for type info +- chore: add [example project](./example) +- chore: dependency updates + ## v0.3.3 ### [0.3.3](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.3.2...v0.3.3) (2023-12-21) diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index ee36f5ea..c48e7b69 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -2,7 +2,7 @@ "sdkId": "python", "gitRepoId": "python-sdk", "packageName": "openfga_sdk", - "packageVersion": "0.3.3", + "packageVersion": "0.3.4", "packageDescription": "Python SDK for OpenFGA", "packageDetailedDescription": "This is an autogenerated python SDK for OpenFGA. It provides a wrapper around the [OpenFGA API definition](https://openfga.dev/api).", "fossaComplianceNoticeId": "2f8a8629-b46c-435e-b8cd-1174a674fb4b", @@ -158,15 +158,11 @@ "destinationFilename": "example/README.md", "templateType": "SupportingFiles" }, - "example/example1/auth-model.json": { - "destinationFilename": "example/example1/auth-model.json", - "templateType": "SupportingFiles" - }, "example/example1/example1.py": { "destinationFilename": "example/example1/example1.py", "templateType": "SupportingFiles" }, - "example/example1/requirements.txt": { + "example/example1/requirements.txt.mustache": { "destinationFilename": "example/example1/requirements.txt", "templateType": "SupportingFiles" }, diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index 42bdff13..a00e7fe0 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -85,10 +85,10 @@ Create a new authorization model. ```python body = WriteAuthorizationModelRequest( - schema_version = "1.1", + schema_version="1.1", type_definitions=[ TypeDefinition( - type="user", + type="user" ), TypeDefinition( type="document", @@ -107,9 +107,41 @@ body = WriteAuthorizationModelRequest( ], ), ), + ), + metadata=Metadata( + relations=dict( + writer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + RelationReference(type="user", condition="ViewCountLessThan200"), + ] + ), + viewer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + ] + ) + ) ) - ), + ) ], + conditions=dict( + ViewCountLessThan200=Condition( + name="ViewCountLessThan200", + expression="ViewCount < 200", + parameters=dict( + ViewCount=ConditionParamTypeRef( + type_name="TYPE_NAME_INT" + ), + Type=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + Name=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + ) + ) + ) ) response = await fga_client.write_authorization_model(body) @@ -129,7 +161,7 @@ options = { "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" } -response = await fga_client.read_authorization_model(id) +response = await fga_client.read_authorization_model(options) # response.authorization_model = AuthorizationModel(id='01GXSA8YR785C4FYS3C0RTG7B1', schema_version = '1.1', type_definitions=type_definitions[...]) ``` @@ -159,7 +191,7 @@ options = { "page_size": "25", "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } -body = ClientReadChangesRequest("document") +body = ClientReadChangesRequest(type="document") response = await fga_client.read_changes(body, options) # response.continuation_token = ... @@ -174,7 +206,7 @@ Reads the relationship tuples stored in the database. It does not evaluate nor e ```python # Find if a relationship tuple stating that a certain user is a viewer of certain document -body = TupleKey( +body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:roadmap", @@ -186,7 +218,7 @@ response = await fga_client.read(body) ```python # Find all relationship tuples where a certain user has a relationship as any relation to a certain document -body = TupleKey( +body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", object="document:roadmap", ) @@ -198,7 +230,7 @@ response = await fga_client.read(body) ```python # Find all relationship tuples where a certain user is a viewer of any document -body = TupleKey( +body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:", @@ -210,7 +242,7 @@ response = await fga_client.read(body) ```python # Find all relationship tuples where any user has a relationship as any relation with a particular document -body = TupleKey( +body = ReadRequestTupleKey( object="document:roadmap", ) @@ -220,7 +252,7 @@ response = await fga_client.read(body) ```python # Read all stored relationship tuples -body = TupleKey() +body = ReadRequestTupleKey() response = await api_instance.read(body) # response = ReadResponse({"tuples": [Tuple({"key": TupleKey({"user":"...","relation":"...","object":"..."}), "timestamp": datetime.fromisoformat("...") })]}) @@ -247,6 +279,13 @@ body = ClientWriteRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:roadmap", + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), ), ClientTuple( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -293,6 +332,13 @@ body = ClientWriteRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:budget", + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), ), ], deletes=[ @@ -324,6 +370,9 @@ body = ClientCheckRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="writer", object="document:roadmap", + context=dict( + ViewCount=100 + ), ) response = await fga_client.check(body, options) @@ -351,7 +400,10 @@ body = [ClientCheckRequest( relation="editor", object="document:roadmap", ), - ] + ], + context=dict( + ViewCount=100 + ) ), ClientCheckRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="admin", @@ -384,7 +436,10 @@ response = await fga_client.batch_check(body, options) # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "editor", # object: "document:roadmap" -# }] +# }], +# context=dict( +# ViewCount=100 +# ) # } # }, { # allowed: false, @@ -458,7 +513,10 @@ body = ClientListObjectsRequest( relation="writer", object="document:budget", ), - ] + ], + context=dict( + ViewCount=100 + ) ) response = await fga_client.list_objects(body) @@ -484,7 +542,10 @@ body = ClientListRelationsRequest( relation="writer", object="document:budget", ), - ] + ], + context=dict( + ViewCount=100 + ) ) var response = await fga_client.list_relations(body, options); @@ -499,7 +560,7 @@ Read assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Read%20Assertions) -```csharp +```python options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -513,7 +574,7 @@ Update the assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Write%20Assertions) -```csharp +```python options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" diff --git a/config/clients/python/template/client/client.mustache b/config/clients/python/template/client/client.mustache index c95da928..1ccf767d 100644 --- a/config/clients/python/template/client/client.mustache +++ b/config/clients/python/template/client/client.mustache @@ -646,7 +646,7 @@ class OpenFgaClient(): 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] + request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples, context=body.context) for i in body.relations] result = {{#asyncio}}await {{/asyncio}}self.batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) diff --git a/config/clients/python/template/client/client_sync.mustache b/config/clients/python/template/client/client_sync.mustache index 4ae66936..3e5a8229 100644 --- a/config/clients/python/template/client/client_sync.mustache +++ b/config/clients/python/template/client/client_sync.mustache @@ -616,7 +616,7 @@ class OpenFgaClient(): 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] + request_body = [construct_check_request(user=body.user, relation=i, object=body.object, contextual_tuples=body.contextual_tuples, context=body.context) 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) diff --git a/config/clients/python/template/client/models/check_request.mustache b/config/clients/python/template/client/models/check_request.mustache index 340403d9..fc734373 100644 --- a/config/clients/python/template/client/models/check_request.mustache +++ b/config/clients/python/template/client/models/check_request.mustache @@ -3,14 +3,14 @@ from {{packageName}}.client.models.tuple import ClientTuple -from typing import List, Any +from typing import List -def construct_check_request(user: str, relation: str, object: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): +def construct_check_request(user: str, relation: str, object: str, contextual_tuples: List[ClientTuple] = None, context: object = None): """ helper function to construct the check request body """ - return ClientCheckRequest(user, relation, object, context, contextual_tuples) + return ClientCheckRequest(user, relation, object, contextual_tuples, context) class ClientCheckRequest(): @@ -18,14 +18,14 @@ class ClientCheckRequest(): ClientCheckRequest encapsulates the parameters for check request """ - def __init__(self, user: str, relation: str, object: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relation: str, object: str, contextual_tuples: List[ClientTuple] = None, context: object = None): self._user = user self._relation = relation self._object = object - self._context = context self._contextual_tuples = None if contextual_tuples: self._contextual_tuples = contextual_tuples + self._context = context @property def user(self): @@ -49,18 +49,18 @@ class ClientCheckRequest(): return self._object @property - def context(self): + def contextual_tuples(self): """ - Return context + Return contextual tuples """ - return self._context + return self._contextual_tuples @property - def contextual_tuples(self): + def context(self): """ - Return contextual tuples + Return context """ - return self._contextual_tuples + return self._context @user.setter def user(self, value): @@ -83,16 +83,16 @@ class ClientCheckRequest(): """ self._object = value - @context.setter - def context(self, value): - """ - Set context - """ - self._context = value - @contextual_tuples.setter def contextual_tuples(self, value): """ Set contextual tuples """ self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value diff --git a/config/clients/python/template/client/models/list_objects_request.mustache b/config/clients/python/template/client/models/list_objects_request.mustache index 2cc03b67..67dc64cd 100644 --- a/config/clients/python/template/client/models/list_objects_request.mustache +++ b/config/clients/python/template/client/models/list_objects_request.mustache @@ -3,19 +3,19 @@ from {{packageName}}.client.models.tuple import ClientTuple -from typing import List, Any +from typing import List class ClientListObjectsRequest(): """ ClientListObjectsRequest encapsulates the parameters required for list objects """ - def __init__(self, user: str, relation: str, type: str, context: Any = None, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relation: str, type: str, contextual_tuples: List[ClientTuple] = None, context: object = None): self._user = user self._relation = relation self._type = type - self._context = context self._contextual_tuples = contextual_tuples + self._context = context @property def user(self): @@ -39,18 +39,18 @@ class ClientListObjectsRequest(): return self._type @property - def context(self): + def contextual_tuples(self): """ - Return context + Return contextual_tuples """ - return self._context + return self._contextual_tuples @property - def contextual_tuples(self): + def context(self): """ - Return contextual_tuples + Return context """ - return self._contextual_tuples + return self._context @user.setter def user(self, value): @@ -73,16 +73,16 @@ class ClientListObjectsRequest(): """ self._type = value - @context.setter - def context(self, value): - """ - Set context - """ - self._context = value - @contextual_tuples.setter def contextual_tuples(self, value): """ Set contextual tuples """ self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value diff --git a/config/clients/python/template/client/models/list_relations_request.mustache b/config/clients/python/template/client/models/list_relations_request.mustache index faad11a7..afabed7d 100644 --- a/config/clients/python/template/client/models/list_relations_request.mustache +++ b/config/clients/python/template/client/models/list_relations_request.mustache @@ -5,16 +5,18 @@ from {{packageName}}.client.models.tuple import ClientTuple from typing import List + class ClientListRelationsRequest(): """ ClientListRelationsRequest encapsulates the parameters required for list all relations user have with object """ - def __init__(self, user: str, relations: List[str], object: str, contextual_tuples: List[ClientTuple]=None): + def __init__(self, user: str, relations: List[str], object: str, contextual_tuples: List[ClientTuple] = None, context: object = None): self._user = user self._relations = relations self._object = object self._contextual_tuples = contextual_tuples + self._context = context @property def user(self): @@ -44,6 +46,13 @@ class ClientListRelationsRequest(): """ return self._contextual_tuples + @property + def context(self): + """ + Return context + """ + return self._context + @user.setter def user(self, value): """ @@ -71,3 +80,10 @@ class ClientListRelationsRequest(): Set contextual tuples """ self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value diff --git a/config/clients/python/template/example/Makefile b/config/clients/python/template/example/Makefile index 161efbae..007c2d81 100644 --- a/config/clients/python/template/example/Makefile +++ b/config/clients/python/template/example/Makefile @@ -3,8 +3,11 @@ all: run project_name=example1 openfga_version=latest -run: - python example1/example1.py +setup: + cd "${project_name}/" && pip3 install -r requirements.txt && cd - + +run: setup + python3 "${project_name}/${project_name}.py" run-openfga: docker pull docker.io/openfga/openfga:${openfga_version} && \ diff --git a/config/clients/python/template/example/example1/auth-model.json b/config/clients/python/template/example/example1/auth-model.json deleted file mode 100644 index e1361656..00000000 --- a/config/clients/python/template/example/example1/auth-model.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "schema_version": "1.1", - "type_definitions": [ - { - "type": "user" - }, - { - "type": "document", - "relations": { - "reader": { - "this": {} - }, - "writer": { - "this": {} - }, - "owner": { - "this": {} - } - }, - "metadata": { - "relations": { - "reader": { - "directly_related_user_types": [ - { - "type": "user" - } - ] - }, - "writer": { - "directly_related_user_types": [ - { - "type": "user" - } - ] - }, - "owner": { - "directly_related_user_types": [ - { - "type": "user" - } - ] - }, - "conditional_reader": { - "directly_related_user_types": [ - { - "condition": "name_starts_with_a", - "type": "user" - } - ] - } - } - } - } - ], - "conditions": { - "ViewCountLessThan200": { - "name": "ViewCountLessThan200", - "expression": "ViewCount < 200", - "parameters": { - "ViewCount": { - "type_name": "TYPE_NAME_INT" - }, - "Type": { - "type_name": "TYPE_NAME_STRING" - }, - "Name": { - "type_name": "TYPE_NAME_STRING" - } - } - } - } - } - \ No newline at end of file diff --git a/config/clients/python/template/example/example1/example1.py b/config/clients/python/template/example/example1/example1.py index 3687c806..c06f3c5e 100644 --- a/config/clients/python/template/example/example1/example1.py +++ b/config/clients/python/template/example/example1/example1.py @@ -1,10 +1,10 @@ import asyncio -import json -import openfga_sdk -from openfga_sdk.client.models import ClientAssertion, ClientCheckRequest, ClientReadChangesRequest, ClientTuple, ClientWriteRequest -from openfga_sdk.models import CreateStoreRequest, Metadata, ObjectRelation, RelationMetadata, TupleKey, TypeDefinition, Userset, Usersets, WriteAuthorizationModelRequest -from openfga_sdk import ClientConfiguration, OpenFgaClient +from openfga_sdk.client.models import ClientAssertion, ClientCheckRequest, ClientReadChangesRequest, ClientTuple, \ + ClientWriteRequest, ClientListRelationsRequest, ClientListObjectsRequest, WriteTransactionOpts +from openfga_sdk import ClientConfiguration, OpenFgaClient, RelationReference, RelationshipCondition, \ + ConditionParamTypeRef, Condition, ReadRequestTupleKey, CreateStoreRequest, Metadata, ObjectRelation, \ + RelationMetadata, TypeDefinition, Userset, Usersets, WriteAuthorizationModelRequest from openfga_sdk.credentials import CredentialConfiguration, Credentials import os @@ -76,9 +76,65 @@ async def main(): # WriteAuthorizationModel print('Writing an Authorization Model') - with open(os.path.join(os.path.dirname(__file__), 'auth-model.json')) as f: - auth_model_request = json.load(f) - response = await fga_client.write_authorization_model(auth_model_request) + response = await fga_client.write_authorization_model(WriteAuthorizationModelRequest( + schema_version="1.1", + type_definitions=[ + TypeDefinition( + type="user" + ), + TypeDefinition( + type="document", + relations=dict( + writer=Userset( + this=dict(), + ), + viewer=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + ), + metadata=Metadata( + relations=dict( + writer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + RelationReference(type="user", condition="ViewCountLessThan200"), + ] + ), + viewer=RelationMetadata( + directly_related_user_types=[ + RelationReference(type="user"), + ] + ) + ) + ) + ) + ], + conditions=dict( + ViewCountLessThan200=Condition( + name="ViewCountLessThan200", + expression="ViewCount < 200", + parameters=dict( + ViewCount=ConditionParamTypeRef( + type_name="TYPE_NAME_INT" + ), + Type=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + Name=ConditionParamTypeRef( + type_name="TYPE_NAME_STRING" + ), + ) + ) + ) + )) print(f"Authorization Model ID: {response.authorization_model_id}") # ReadAuthorizationModels (after write) @@ -101,13 +157,13 @@ async def main(): user='user:anne', relation='writer', object='document:roadmap', - # condition=RelationshipCondition( - # name='ViewCountLessThan200', - # context=dict( - # Name='Roadmap', - # Type='Document', - # ), - # ), + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), ), ], ) @@ -118,31 +174,113 @@ async def main(): await fga_client.write(body, options) print('Done Writing Tuples') + # Write + print('Writing Tuples - non txn') + body = ClientWriteRequest( + writes=[ + ClientTuple( + user='user:beth', + relation='writer', + object='document:1', + condition=RelationshipCondition( + name='ViewCountLessThan200', + context=dict( + Name='Roadmap', + Type='Document', + ), + ), + ), + ClientTuple( + user='user:beth', + relation='viewer', + object='document:2' + ), + ], + ) + options = { + # You can rely on the model id set in the configuration or override it for this specific request + "authorization_model_id": auth_model_id, + "transaction": WriteTransactionOpts( + max_per_chunk=1 + ) + } + await fga_client.write(body, options) + print('Done Writing Tuples') + # Set the model ID fga_client.set_authorization_model_id(auth_model_id) # Read print('Reading Tuples') - response = await fga_client.read(TupleKey(user='user:anne', object='document:')) + response = await fga_client.read(ReadRequestTupleKey(user='user:anne', object='document:')) print(f"Read Tuples: {response.tuples}") # ReadChanges print('Reading Tuple Changes') - body = ClientReadChangesRequest('document') + body = ClientReadChangesRequest(type='document') response = await fga_client.read_changes(body) print(f"Read Changes Tuples: {response.changes}") # Check - print('Checking for access') + print('Checking for access w/o context') + try: + response = await fga_client.check(ClientCheckRequest( + user='user:anne', + relation='viewer', + object='document:roadmap' + )) + print(f"Allowed: {response.allowed}") + except Exception as err: + print(f"Failed due to: {err}") + + # Checking for access with context + print('Checking for access with context') + response = await fga_client.check(ClientCheckRequest( user='user:anne', - relation='reader', + relation='viewer', object='document:roadmap', + context=dict( + ViewCount=100 + ) )) print(f"Allowed: {response.allowed}") - # Checking for access with context - # TODO + # List objects with context + print('Listing objects for access with context') + + response = await fga_client.list_objects(ClientListObjectsRequest( + user='user:anne', + relation='viewer', + type='document', + context=dict( + ViewCount=100 + ) + )) + print(f"Objects: {response.objects}") + + # List relations w/o context + print('Listing relations for access w/o context') + + response = await fga_client.list_relations(ClientListRelationsRequest( + user='user:anne', + relations=['viewer', 'writer'], + object='document:roadmap' + )) + print(f"Relations: {response}") + + # List relations with context + print('Listing relations for access with context') + + response = await fga_client.list_relations(ClientListRelationsRequest( + user='user:anne', + relations=['viewer', 'writer'], + object='document:roadmap', + context=dict( + ViewCount=100 + ) + )) + print(f"Relations: {response}") # WriteAssertions await fga_client.write_assertions([ @@ -154,7 +292,7 @@ async def main(): ), ClientAssertion( user='user:anne', - relation='reader', + relation='viewer', object='document:roadmap', expectation=False, ), diff --git a/config/clients/python/template/example/example1/requirements.txt b/config/clients/python/template/example/example1/requirements.txt.mustache similarity index 82% rename from config/clients/python/template/example/example1/requirements.txt rename to config/clients/python/template/example/example1/requirements.txt.mustache index f400ac51..7bb741ac 100644 --- a/config/clients/python/template/example/example1/requirements.txt +++ b/config/clients/python/template/example/example1/requirements.txt.mustache @@ -4,7 +4,7 @@ attrs==23.1.0 frozenlist==1.4.1 idna==3.6 multidict==6.0.4 -openfga-sdk==0.3.2 +openfga-sdk=={{packageVersion}} python-dateutil==2.8.2 six==1.16.0 urllib3==2.1.0