From 6325bbe299ba271776236a3b4fa64af4beb813ff Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 13 Jan 2025 19:56:43 -0600 Subject: [PATCH] feat(python): add streamed-list-objects endpoint --- Makefile | 17 +- config/clients/python/config.overrides.json | 29 +- .../template/README_calling_api.mustache | 27 + .../example/example1/example1.py.mustache | 8 + .../example1/requirements.txt.mustache | 1 + .../streamed-list-objects/.env.example | 1 + .../example/streamed-list-objects/.gitignore | 1 + .../example/streamed-list-objects/README.md | 39 + .../asynchronous.py.mustache | 121 + .../streamed-list-objects/requirements.txt | 1 + .../example/streamed-list-objects/setup.cfg | 2 + .../streamed-list-objects/setup.py.mustache | 30 + .../synchronous.py.mustache | 118 + .../python/template/src/api.py.mustache | 6 +- .../template/src/api_client.py.mustache | 103 +- .../template/src/client/client.py.mustache | 38 + .../src/client/models/__init__.py.mustache | 10 +- .../models/batch_check_item.py.mustache | 16 +- .../models/batch_check_request.py.mustache | 6 +- .../models/batch_check_response.py.mustache | 6 +- .../batch_check_single_response.py.mustache | 6 +- .../python/template/src/oauth2.py.mustache | 11 +- .../python/template/src/rest.py.mustache | 467 ++-- .../python/template/src/sync/api.py.mustache | 4 +- .../template/src/sync/api_client.py.mustache | 109 +- .../src/sync/client/client.py.mustache | 38 + .../template/src/sync/oauth2.py.mustache | 11 +- .../python/template/src/sync/rest.py.mustache | 581 +++-- .../src/telemetry/attributes.py.mustache | 4 +- .../template/test-requirements.mustache | 3 +- .../python/template/test/api_test.py.mustache | 743 +++--- .../test/client/client_test.py.mustache | 2075 ++++++++++------- .../template/test/oauth2_test.py.mustache | 105 +- .../template/test/rest_test.py.mustache | 401 ++++ .../template/test/sync/api_test.py.mustache | 688 ++++-- .../test/sync/client/client_test.py.mustache | 1594 ++++++++----- .../test/sync/oauth2_test.py.mustache | 105 +- .../template/test/sync/rest_test.py.mustache | 525 +++++ config/common/files/README.mustache | 5 +- 39 files changed, 5481 insertions(+), 2574 deletions(-) create mode 100644 config/clients/python/template/example/streamed-list-objects/.env.example create mode 100644 config/clients/python/template/example/streamed-list-objects/.gitignore create mode 100644 config/clients/python/template/example/streamed-list-objects/README.md create mode 100644 config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache create mode 100644 config/clients/python/template/example/streamed-list-objects/requirements.txt create mode 100644 config/clients/python/template/example/streamed-list-objects/setup.cfg create mode 100644 config/clients/python/template/example/streamed-list-objects/setup.py.mustache create mode 100644 config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache create mode 100644 config/clients/python/template/test/rest_test.py.mustache create mode 100644 config/clients/python/template/test/sync/rest_test.py.mustache diff --git a/Makefile b/Makefile index 4ba5ab93..824d1237 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,7 @@ tag-client-python: test-client-python .PHONY: build-client-python build-client-python: - make build-client sdk_language=python tmpdir=${TMP_DIR} library="asyncio" + make build-client-streamed 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 mv ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/test/test_open_fga_api.py ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/test/api/open_fga_api_test.py @@ -182,6 +182,21 @@ build-openapi: init get-openapi-doc sed -i -e 's/#\/definitions\/Object"/#\/definitions\/FgaObject"/g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json sed -i -e 's/v1.//g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json +.EXPORT_ALL_VARIABLES: +.PHONY: build-client-streamed +build-client-streamed: build-openapi-streamed + SDK_LANGUAGE="${sdk_language}" TMP_DIR="${tmpdir}" LIBRARY_TEMPLATE="${library}"\ + ./scripts/build_client.sh + +.PHONY: build-openapi-streamed +build-openapi-streamed: init get-openapi-doc + cat "${DOCS_CACHE_DIR}/openfga.openapiv2.raw.json" | \ + jq '(.. | .tags? | select(.)) |= ["OpenFga"] | (.tags? | select(.)) |= [{"name":"OpenFga"}] | del(.definitions.ReadTuplesParams, .definitions.ReadTuplesResponse, .paths."/stores/{store_id}/read-tuples")' > \ + ${DOCS_CACHE_DIR}/openfga.openapiv2.json + sed -i -e 's/"Object"/"FgaObject"/g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json + sed -i -e 's/#\/definitions\/Object"/#\/definitions\/FgaObject"/g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json + sed -i -e 's/v1.//g' ${DOCS_CACHE_DIR}/openfga.openapiv2.json + .PHONY: get-openapi-doc get-openapi-doc: mkdir -p "${DOCS_CACHE_DIR}" diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 93e421c9..09df4e90 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -11,6 +11,7 @@ "docPrefix": "https://github.com/openfga/python-sdk/blob/main/", "pythonMinimumRuntime": "3.10", "openTelemetryDocumentation": "opentelemetry.md", + "supportsStreamedListObjects": "streamed_list_objects", "files": { ".snyk": {}, @@ -55,6 +56,24 @@ "templateType": "SupportingFiles" }, + "example/streamed-list-objects/.env.example": {}, + "example/streamed-list-objects/.gitignore": {}, + "example/streamed-list-objects/README.md": {}, + "example/streamed-list-objects/requirements.txt": {}, + "example/streamed-list-objects/setup.cfg": {}, + "example/streamed-list-objects/setup.py.mustache": { + "destinationFilename": "example/streamed-list-objects/setup.py", + "templateType": "SupportingFiles" + }, + "example/streamed-list-objects/asynchronous.py.mustache": { + "destinationFilename": "example/streamed-list-objects/asynchronous.py", + "templateType": "SupportingFiles" + }, + "example/streamed-list-objects/synchronous.py.mustache": { + "destinationFilename": "example/streamed-list-objects/synchronous.py", + "templateType": "SupportingFiles" + }, + "src/api/__init__.py.mustache": { "destinationFilename": "openfga_sdk/api/__init__.py", "templateType": "SupportingFiles" @@ -83,7 +102,7 @@ "destinationFilename": "openfga_sdk/client/models/batch_check_item.py", "templateType": "SupportingFiles" }, - "src/client/models/batch_check_request.py.mustache": { + "src/client/models/batch_check_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/batch_check_request.py", "templateType": "SupportingFiles" }, @@ -272,6 +291,10 @@ "destinationFilename": "test/sync/oauth2_test.py", "templateType": "SupportingFiles" }, + "test/sync/rest_test.py.mustache": { + "destinationFilename": "test/sync/rest_test.py", + "templateType": "SupportingFiles" + }, "test/telemetry/attributes_test.py.mustache": { "destinationFilename": "test/telemetry/attributes_test.py", "templateType": "SupportingFiles" @@ -316,6 +339,10 @@ "destinationFilename": "test/_/oauth2_test.py", "templateType": "SupportingFiles" }, + "test/rest_test.py.mustache": { + "destinationFilename": "test/_/rest_test.py", + "templateType": "SupportingFiles" + }, "test/validation_test.py.mustache": { "destinationFilename": "test/_/validation_test.py", "templateType": "SupportingFiles" diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index f95383c9..60e3850b 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -753,6 +753,33 @@ response = await fga_client.list_objects(body) # response.objects = ["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"] ``` +#### Streamed List Objects + +List the objects of a particular type a user has access to, using the streaming API. + +[API Documentation](https://openfga.dev/api/service#/Relationship%20Queries/StreamedListObjects) + +```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client.models import ClientListObjectsRequest + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + +results = [] + +documents = ClientListObjectsRequest( + type="document", + relation="writer", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", +) + +async for response in fga_client.streamed_list_objects(request): + results.append(response) + +# results = ["document:...", ...] +``` + #### List Relations List the relations a user has on an object. diff --git a/config/clients/python/template/example/example1/example1.py.mustache b/config/clients/python/template/example/example1/example1.py.mustache index 66e931fa..dd924a15 100644 --- a/config/clients/python/template/example/example1/example1.py.mustache +++ b/config/clients/python/template/example/example1/example1.py.mustache @@ -1,7 +1,13 @@ import asyncio import os +import sys import uuid +from dotenv import load_dotenv + +sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) +sys.path.insert(0, sdk_path) + from {{packageName}} import ( ClientConfiguration, Condition, @@ -38,6 +44,8 @@ from {{packageName}}.models.fga_object import FgaObject async def main(): + load_dotenv() + credentials = Credentials() if os.getenv("FGA_CLIENT_ID") is not None: credentials = Credentials( diff --git a/config/clients/python/template/example/example1/requirements.txt.mustache b/config/clients/python/template/example/example1/requirements.txt.mustache index d24792d2..f0a60d32 100644 --- a/config/clients/python/template/example/example1/requirements.txt.mustache +++ b/config/clients/python/template/example/example1/requirements.txt.mustache @@ -8,3 +8,4 @@ openfga-sdk >= {{packageVersion}} python-dateutil >= 2.8.2 urllib3 >= 2.1.0 yarl >= 1.9.4 +python-dotenv >= 1, <2 diff --git a/config/clients/python/template/example/streamed-list-objects/.env.example b/config/clients/python/template/example/streamed-list-objects/.env.example new file mode 100644 index 00000000..2bc57571 --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/.env.example @@ -0,0 +1 @@ +FGA_API_URL="http://localhost:8080" diff --git a/config/clients/python/template/example/streamed-list-objects/.gitignore b/config/clients/python/template/example/streamed-list-objects/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/.gitignore @@ -0,0 +1 @@ +.env diff --git a/config/clients/python/template/example/streamed-list-objects/README.md b/config/clients/python/template/example/streamed-list-objects/README.md new file mode 100644 index 00000000..97508484 --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/README.md @@ -0,0 +1,39 @@ +# Streamed List Objects example for OpenFGA's Python SDK + +This example demonstrates working with the `POST` `/stores/:id/streamed-list-objects` endpoint in OpenFGA using the Python SDK. + +## Prerequisites + +If you do not already have an OpenFGA instance running, you can start one using the following command: + +```bash +docker run -d -p 8080:8080 openfga/openfga +``` + +## Configure the example + +You may need to configure the example for your environment: + +```bash +cp .env.example .env +``` + +Now edit the `.env` file and set the values as appropriate. + +## Running the example + +Begin by installing the required dependencies: + +```bash +pip install -r requirements.txt +``` + +Next, run the example. You can use either the synchronous or asynchronous client: + +```bash +python asynchronous.py +``` + +```bash +python synchronous.py +``` diff --git a/config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache b/config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache new file mode 100644 index 00000000..2968454e --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache @@ -0,0 +1,121 @@ +import asyncio +import json +import os +import sys +from operator import attrgetter +from typing import Any + +from dotenv import load_dotenv + +sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) +sys.path.insert(0, sdk_path) + +from {{packageName}} import ( + ClientConfiguration, + OpenFgaClient, +) +from {{packageName}}.client.models import ( + ClientListObjectsRequest, + ClientTuple, + ClientWriteRequest, +) +from {{packageName}}.models import CreateStoreRequest + + +class app: + def __init__( + self, + client: OpenFgaClient = None, + configuration: ClientConfiguration = None, + ): + self._client = client + self._configuration = configuration + + async def fga_client(self, env: dict[str, str] = {}) -> OpenFgaClient: + if not self._client or not self._configuration: + load_dotenv() + + if not self._configuration: + self._configuration = ClientConfiguration( + api_url=os.getenv("FGA_API_URL"), + ) + + self._client = OpenFgaClient(self._configuration) + return self._client + + +def unpack( + response, + attr: str, +) -> Any: + return attrgetter(attr)(response) + + +async def main(): + async with await app().fga_client() as fga_client: + # Create a temporary store + store = unpack( + await fga_client.create_store(CreateStoreRequest(name="Test Store")), + "id", + ) + print(f"Created temporary store ({store})") + fga_client.set_store_id(store) + + # Create a temporary authorization model + model = unpack( + await fga_client.write_authorization_model( + json.loads( + '{"schema_version":"1.1","type_definitions":[{"type":"user","relations":{}},{"type":"group","relations":{"member":{"this":{}}},"metadata":{"relations":{"member":{"directly_related_user_types":[{"type":"user"}]}}}},{"type":"folder","relations":{"can_create_file":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"viewer":{"union":{"child":[{"this":{}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}}},"metadata":{"relations":{"can_create_file":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]}}}},{"type":"document","relations":{"can_change_owner":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"can_read":{"union":{"child":[{"computedUserset":{"object":"","relation":"viewer"}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}},"can_share":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}},"viewer":{"this":{}},"can_write":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}}},"metadata":{"relations":{"can_change_owner":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"can_read":{"directly_related_user_types":[]},"can_share":{"directly_related_user_types":[]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]},"can_write":{"directly_related_user_types":[]}}}}]}' + ) + ), + "authorization_model_id", + ) + print(f"Created temporary authorization model ({model})") + + print(f"Writing 100 mock tuples to store.") + + # Write mock data + writes = [] + for x in range(0, 100): + writes.append( + ClientTuple( + user="user:anne", + relation="owner", + object=f"document:{x}", + ) + ) + + await fga_client.write( + ClientWriteRequest(writes), + { + "authorization_model_id": model, + }, + ) + + print("Listing objects using streaming endpoint:") + results = [] + + request = ClientListObjectsRequest( + type="document", + relation="owner", + user="user:anne", + ) + + async for response in fga_client.streamed_list_objects(request): + print(f" {response}") + results.append(response) + + print(f"API returned {results.__len__()} objects.") + + # Delete the temporary store + try: + await fga_client.delete_store() + print(f"Deleted temporary store ({store})") + except: + pass + + print("Finished.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/config/clients/python/template/example/streamed-list-objects/requirements.txt b/config/clients/python/template/example/streamed-list-objects/requirements.txt new file mode 100644 index 00000000..78af7ab4 --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/requirements.txt @@ -0,0 +1 @@ +python-dotenv >= 1, <2 diff --git a/config/clients/python/template/example/streamed-list-objects/setup.cfg b/config/clients/python/template/example/streamed-list-objects/setup.cfg new file mode 100644 index 00000000..11433ee8 --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=99 diff --git a/config/clients/python/template/example/streamed-list-objects/setup.py.mustache b/config/clients/python/template/example/streamed-list-objects/setup.py.mustache new file mode 100644 index 00000000..7a381fff --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/setup.py.mustache @@ -0,0 +1,30 @@ +""" + Python SDK for OpenFGA + + API version: 0.1 + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://discord.gg/8naAwJfWN6 + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +from setuptools import find_packages, setup + +NAME = "openfga-streamed-list-objects-example" +VERSION = "0.0.1" +REQUIRES = [""] + +setup( + name=NAME, + version=VERSION, + description="An example of using the OpenFGA Python SDK with the Streamed List Objects endpoint.", + author="OpenFGA (https://openfga.dev)", + author_email="community@openfga.dev", + url="https://github.com/openfga/python-sdk", + python_requires=">={{pythonMinimumRuntime}}", + packages=find_packages(exclude=["test", "tests"]), + include_package_data=True, + license="Apache-2.0", +) diff --git a/config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache b/config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache new file mode 100644 index 00000000..d3db0d29 --- /dev/null +++ b/config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache @@ -0,0 +1,118 @@ +import json +import os +import sys +from operator import attrgetter +from typing import Any + +from dotenv import load_dotenv + +sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) +sys.path.insert(0, sdk_path) + +from {{packageName}} import ClientConfiguration +from {{packageName}}.client.models import ( + ClientListObjectsRequest, + ClientTuple, + ClientWriteRequest, +) +from {{packageName}}.models.create_store_request import CreateStoreRequest +from {{packageName}}.sync import OpenFgaClient + + +class app: + def __init__( + self, + client: OpenFgaClient = None, + configuration: ClientConfiguration = None, + ): + self._client = client + self._configuration = configuration + + def fga_client(self, env: dict[str, str] = {}) -> OpenFgaClient: + if not self._client or not self._configuration: + load_dotenv() + + if not self._configuration: + self._configuration = ClientConfiguration( + api_url=os.getenv("FGA_API_URL"), + ) + + self._client = OpenFgaClient(self._configuration) + return self._client + + +def unpack( + response, + attr: str, +) -> Any: + return attrgetter(attr)(response) + + +def main(): + with app().fga_client() as fga_client: + # Create a temporary store + store = unpack( + fga_client.create_store(CreateStoreRequest(name="Test Store")), + "id", + ) + print(f"Created temporary store ({store})") + fga_client.set_store_id(store) + + # Create a temporary authorization model + model = unpack( + fga_client.write_authorization_model( + json.loads( + '{"schema_version":"1.1","type_definitions":[{"type":"user","relations":{}},{"type":"group","relations":{"member":{"this":{}}},"metadata":{"relations":{"member":{"directly_related_user_types":[{"type":"user"}]}}}},{"type":"folder","relations":{"can_create_file":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"viewer":{"union":{"child":[{"this":{}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}}},"metadata":{"relations":{"can_create_file":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]}}}},{"type":"document","relations":{"can_change_owner":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"can_read":{"union":{"child":[{"computedUserset":{"object":"","relation":"viewer"}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}},"can_share":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}},"viewer":{"this":{}},"can_write":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}}},"metadata":{"relations":{"can_change_owner":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"can_read":{"directly_related_user_types":[]},"can_share":{"directly_related_user_types":[]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]},"can_write":{"directly_related_user_types":[]}}}}]}' + ) + ), + "authorization_model_id", + ) + print(f"Created temporary authorization model ({model})") + + print(f"Writing 100 mock tuples to store.") + + # Write mock data + writes = [] + for x in range(0, 100): + writes.append( + ClientTuple( + user="user:anne", + relation="owner", + object=f"document:{x}", + ) + ) + + fga_client.write( + ClientWriteRequest(writes), + { + "authorization_model_id": model, + }, + ) + + print("Listing objects using streaming endpoint:") + results = [] + + request = ClientListObjectsRequest( + type="document", + relation="owner", + user="user:anne", + ) + + for response in fga_client.streamed_list_objects(request): + print(f" {response}") + results.append(response) + + print(f"API returned {results.__len__()} objects.") + + # Delete the temporary store + try: + fga_client.delete_store() + print(f"Deleted temporary store ({store})") + except: + pass + + print("Finished.") + + +if __name__ == "__main__": + main() diff --git a/config/clients/python/template/src/api.py.mustache b/config/clients/python/template/src/api.py.mustache index ceac6233..fba48d63 100644 --- a/config/clients/python/template/src/api.py.mustache +++ b/config/clients/python/template/src/api.py.mustache @@ -180,7 +180,8 @@ class {{classname}}: '_request_auth', '_content_type', '_headers', - '_retry_params' + '_retry_params', + '_streaming', ] ) @@ -324,7 +325,8 @@ class {{classname}}: collection_formats=collection_formats, _request_auth=local_var_params.get('_request_auth'), _oauth2_client=self._oauth2_client, - _telemetry_attributes=telemetry_attributes{{#asyncio}}){{/asyncio}} + _telemetry_attributes=telemetry_attributes, + _streaming=local_var_params.get('_streaming', False){{#asyncio}}){{/asyncio}} ) {{/operation}} {{/operations}} diff --git a/config/clients/python/template/src/api_client.py.mustache b/config/clients/python/template/src/api_client.py.mustache index 07509f95..2e9413ea 100644 --- a/config/clients/python/template/src/api_client.py.mustache +++ b/config/clients/python/template/src/api_client.py.mustache @@ -176,6 +176,7 @@ class ApiClient: _retry_params=None, _oauth2_client=None, _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _streaming: bool = False, ): self.configuration.is_valid() @@ -262,10 +263,16 @@ class ApiClient: try: # perform request and return response response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request( - method, url, query_params=query_params, headers=header_params, - post_params=post_params, body=body, + method, + url, + query_params=query_params, + headers=header_params, + post_params=post_params, + body=body, _preload_content=_preload_content, - _request_timeout=_request_timeout) + _request_timeout=_request_timeout, + _streaming=_streaming, + ) except (RateLimitExceededError, ServiceException) as e: if retry < max_retry and e.status != 501: _telemetry_attributes = TelemetryAttributes.fromResponse( @@ -331,7 +338,7 @@ class ApiClient: self._telemetry.metrics.request( attributes=_telemetry_attributes, - configuration=self.configuration.telemetry, + configuration=self.configuration.telemetry, ) self._telemetry.metrics.queryDuration( @@ -344,7 +351,7 @@ class ApiClient: configuration=self.configuration.telemetry, ) - if not _preload_content: + if not _preload_content or _streaming: {{^tornado}} return return_data {{/tornado}} @@ -505,6 +512,7 @@ class ApiClient: _retry_params=None, _oauth2_client=None, _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _streaming: bool = False, ): """Makes the HTTP request (synchronous) and returns deserialized data. @@ -566,6 +574,7 @@ class ApiClient: _retry_params, _oauth2_client, _telemetry_attributes, + _streaming, ){{#asyncio}}){{/asyncio}} return self.pool.apply_async( @@ -589,6 +598,7 @@ class ApiClient: _retry_params, _oauth2_client, _telemetry_attributes, + _streaming, ), ) @@ -602,77 +612,36 @@ class ApiClient: body=None, _preload_content=True, _request_timeout=None, + _streaming: bool = False, ): - """Makes the HTTP request using RESTClient.""" - if method == "GET": - return {{#asyncio}}await ({{/asyncio}} self.rest_client.GET( - url, - query_params=query_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - headers=headers, - ){{#asyncio}}){{/asyncio}} - elif method == "HEAD": - return {{#asyncio}}await ({{/asyncio}} self.rest_client.HEAD( - url, - query_params=query_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - headers=headers, - ){{#asyncio}}){{/asyncio}} - elif method == "OPTIONS": - return {{#asyncio}}await ({{/asyncio}} self.rest_client.OPTIONS( - url, - query_params=query_params, - headers=headers, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - ){{#asyncio}}){{/asyncio}} - elif method == "POST": - return {{#asyncio}}await ({{/asyncio}} 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, - ){{#asyncio}}){{/asyncio}} - elif method == "PUT": - return {{#asyncio}}await ({{/asyncio}} 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, - ){{#asyncio}}){{/asyncio}} - elif method == "PATCH": - return {{#asyncio}}await ({{/asyncio}} self.rest_client.PATCH( + if method not in ["GET", "HEAD", "OPTIONS", "POST", "PATCH", "PUT", "DELETE"]: + raise ApiValueError( + "http method must be `GET`, `HEAD`, `OPTIONS`," + " `POST`, `PATCH`, `PUT` or `DELETE`." + ) + + if _streaming: + return self.rest_client.stream( + method, url, query_params=query_params, headers=headers, post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, body=body, - ){{#asyncio}}){{/asyncio}} - elif method == "DELETE": - return {{#asyncio}}await ({{/asyncio}} self.rest_client.DELETE( - url, - query_params=query_params, - headers=headers, - _preload_content=_preload_content, _request_timeout=_request_timeout, - body=body, - ){{#asyncio}}){{/asyncio}} - else: - raise ApiValueError( - "http method must be `GET`, `HEAD`, `OPTIONS`," - " `POST`, `PATCH`, `PUT` or `DELETE`." ) + return {{#asyncio}}await {{/asyncio}}self.rest_client.request( + method, + url, + query_params=query_params, + headers=headers, + post_params=post_params, + body=body, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + ) + def parameters_to_tuples(self, params, collection_formats): """Get parameters as list of tuples, formatting collections. diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index 9b569f5d..1242fac0 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -46,6 +46,7 @@ from {{packageName}}.models.list_users_request import ListUsersRequest 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.streamed_list_objects_response import StreamedListObjectsResponse from {{packageName}}.models.tuple_key import TupleKey from {{packageName}}.models.write_assertions_request import WriteAssertionsRequest from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest @@ -749,6 +750,43 @@ class OpenFgaClient: ) return api_response + {{#asyncio}}async {{/asyncio}}def streamed_list_objects( + self, body: ClientListObjectsRequest, options: dict[str, str] = None + ): + """ + Retrieve all objects of the given type that the user has a relation with, using the streaming ListObjects API. + + :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 + :param consistency(options) - The type of consistency preferred for the request11 + """ + kwargs = options_to_kwargs(options) + kwargs["_streaming"] = True + + req_body = ListObjectsRequest( + authorization_model_id=self._get_authorization_model_id(options), + user=body.user, + relation=body.relation, + type=body.type, + context=body.context, + consistency=self._get_consistency(options), + ) + + if body.contextual_tuples: + req_body.contextual_tuples = ContextualTupleKeys( + tuple_keys=convert_tuple_keys(body.contextual_tuples) + ) + + {{#asyncio}}async {{/asyncio}}for response in {{#asyncio}}await {{/asyncio}}self._api.streamed_list_objects( + body=req_body, **kwargs + ): + if response and "result" in response and "object" in response["result"]: + yield StreamedListObjectsResponse(response["result"]["object"]) + {{#asyncio}}async {{/asyncio}}def list_relations(self, body: ClientListRelationsRequest, options: dict[str, str] = None): """ Return all the relations for which user has a relationship with the object diff --git a/config/clients/python/template/src/client/models/__init__.py.mustache b/config/clients/python/template/src/client/models/__init__.py.mustache index b07e3194..5f61f417 100644 --- a/config/clients/python/template/src/client/models/__init__.py.mustache +++ b/config/clients/python/template/src/client/models/__init__.py.mustache @@ -1,10 +1,10 @@ {{>partial_header}} from {{packageName}}.client.models.assertion import ClientAssertion -from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem -from openfga_sdk.client.models.batch_check_request import ClientBatchCheckRequest -from openfga_sdk.client.models.batch_check_response import ClientBatchCheckResponse -from openfga_sdk.client.models.batch_check_single_response import ClientBatchCheckSingleResponse -from openfga_sdk.client.models.client_batch_check_response import ClientBatchCheckClientResponse +from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem +from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest +from {{packageName}}.client.models.batch_check_response import ClientBatchCheckResponse +from {{packageName}}.client.models.batch_check_single_response import ClientBatchCheckSingleResponse +from {{packageName}}.client.models.client_batch_check_response import ClientBatchCheckClientResponse from {{packageName}}.client.models.check_request import ClientCheckRequest from {{packageName}}.client.models.expand_request import ClientExpandRequest from {{packageName}}.client.models.list_objects_request import ClientListObjectsRequest diff --git a/config/clients/python/template/src/client/models/batch_check_item.py.mustache b/config/clients/python/template/src/client/models/batch_check_item.py.mustache index 70f2ea65..139767cb 100644 --- a/config/clients/python/template/src/client/models/batch_check_item.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_item.py.mustache @@ -1,9 +1,9 @@ {{>partial_header}} -from openfga_sdk.client.models.tuple import ClientTuple, convert_tuple_keys -from openfga_sdk.models.batch_check_item import BatchCheckItem -from openfga_sdk.models.check_request_tuple_key import CheckRequestTupleKey -from openfga_sdk.models.contextual_tuple_keys import ContextualTupleKeys +from {{packageName}}.client.models.tuple import ClientTuple, convert_tuple_keys +from {{packageName}}.models.batch_check_item import BatchCheckItem +from {{packageName}}.models.check_request_tuple_key import CheckRequestTupleKey +from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys def construct_batch_item(check): batch_item = BatchCheckItem( @@ -63,7 +63,7 @@ class ClientBatchCheckItem: Return object """ return self._object - + @property def contextual_tuples(self): @@ -78,7 +78,7 @@ class ClientBatchCheckItem: Return context """ return self._context - + @property def correlation_id(self): """ @@ -120,9 +120,9 @@ class ClientBatchCheckItem: Set context """ self._context = value - + @correlation_id.setter def correlation_id(self, value): """ """ - self._correlation_id = value \ No newline at end of file + self._correlation_id = value diff --git a/config/clients/python/template/src/client/models/batch_check_request.py.mustache b/config/clients/python/template/src/client/models/batch_check_request.py.mustache index 3a7c7059..4c1572df 100644 --- a/config/clients/python/template/src/client/models/batch_check_request.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_request.py.mustache @@ -1,6 +1,6 @@ {{>partial_header}} -from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem +from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem class ClientBatchCheckRequest: """ @@ -15,10 +15,10 @@ class ClientBatchCheckRequest: Return checks """ return self._checks - + @checks.setter def checks(self, checks): """ Set checks """ - self._checks = checks \ No newline at end of file + self._checks = checks diff --git a/config/clients/python/template/src/client/models/batch_check_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_response.py.mustache index 4c957757..532c4588 100644 --- a/config/clients/python/template/src/client/models/batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_response.py.mustache @@ -1,6 +1,6 @@ {{>partial_header}} -from openfga_sdk.client.models.batch_check_single_response import ClientBatchCheckSingleResponse +from {{packageName}}.client.models.batch_check_single_response import ClientBatchCheckSingleResponse class ClientBatchCheckResponse: def __init__(self, result: list[ClientBatchCheckSingleResponse]): @@ -12,10 +12,10 @@ class ClientBatchCheckResponse: Return result """ return self._result - + @result.setter def result(self, result): """ Set result """ - self._result = result \ No newline at end of file + self._result = result diff --git a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache index 8a45ec24..3fe78d32 100644 --- a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache @@ -1,8 +1,8 @@ {{>partial_header}} -from openfga_sdk.client.models.tuple import ClientTuple -from openfga_sdk.models.batch_check_single_result import BatchCheckSingleResult -from openfga_sdk.models.check_error import CheckError +from {{packageName}}.client.models.tuple import ClientTuple +from {{packageName}}.models.batch_check_single_result import BatchCheckSingleResult +from {{packageName}}.models.check_error import CheckError class ClientBatchCheckSingleResponse: def __init__( diff --git a/config/clients/python/template/src/oauth2.py.mustache b/config/clients/python/template/src/oauth2.py.mustache index dc75bc29..03356e25 100644 --- a/config/clients/python/template/src/oauth2.py.mustache +++ b/config/clients/python/template/src/oauth2.py.mustache @@ -90,8 +90,15 @@ class OAuth2Client: ) for attempt in range(max_retry + 1): - raw_response = await client.POST( - token_url, headers=headers, post_params=post_params + raw_response = await client.request( + method="POST", + url=token_url, + headers=headers, + query_params=None, + body=None, + _preload_content=True, + _request_timeout=None, + post_params=post_params ) if 500 <= raw_response.status <= 599 or raw_response.status == 429: diff --git a/config/clients/python/template/src/rest.py.mustache b/config/clients/python/template/src/rest.py.mustache index 5cdbe8d4..ca6924a7 100644 --- a/config/clients/python/template/src/rest.py.mustache +++ b/config/clients/python/template/src/rest.py.mustache @@ -6,35 +6,73 @@ import logging import re import ssl import urllib +from typing import Any, List, Optional, Tuple + import aiohttp -from {{packageName}}.exceptions import ApiException, UnauthorizedException, ForbiddenException, NotFoundException, RateLimitExceededError, ServiceException, ApiValueError, ValidationException +from {{packageName}}.exceptions import ( + ApiException, + ApiValueError, + ForbiddenException, + NotFoundException, + RateLimitExceededError, + ServiceException, + UnauthorizedException, + ValidationException, +) logger = logging.getLogger(__name__) class RESTResponse(io.IOBase): + """ + Represents an HTTP response object. + """ + + def __init__(self, resp: aiohttp.ClientResponse, data: bytes) -> None: + """ + Initializes a RESTResponse with an aiohttp response and corresponding data. - def __init__(self, resp, data): + :param resp: The aiohttp.ClientResponse object. + :param data: The raw byte data read from the response. + """ self.aiohttp_response = resp self.status = resp.status self.reason = resp.reason self.data = data - def getheaders(self): - """Returns a CIMultiDictProxy of the response headers.""" + def getheaders(self) -> aiohttp.typedefs.LooseHeaders: + """ + Returns the response headers. + """ return self.aiohttp_response.headers - def getheader(self, name, default=None): - """Returns a given response header.""" + def getheader(self, name: str, default: Optional[str] = None) -> Optional[str]: + """ + Returns a specific header value by name. + + :param name: The name of the header. + :param default: The default value if header is not found. + :return: The header value, or default if not present. + """ return self.aiohttp_response.headers.get(name, default) class RESTClientObject: + """ + A client object that manages HTTP interactions. + """ - def __init__(self, configuration, pools_size=4, maxsize=None): + def __init__( + self, configuration: Any, pools_size: int = 4, maxsize: Optional[int] = None + ) -> None: + """ + Creates a new RESTClientObject. - # maxsize is number of requests to host that are allowed in parallel + :param configuration: A configuration object with necessary parameters. + :param pools_size: The size of the connection pool (unused, present for compatibility). + :param maxsize: Maximum number of connections to allow. + """ if maxsize is None: maxsize = configuration.connection_pool_maxsize @@ -48,65 +86,62 @@ class RESTClientObject: ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE - connector = aiohttp.TCPConnector( - limit=maxsize, - ssl=ssl_context - ) - + connector = aiohttp.TCPConnector(limit=maxsize, ssl=ssl_context) self.proxy = configuration.proxy self.proxy_headers = configuration.proxy_headers self._timeout_millisec = configuration.timeout_millisec + self.pool_manager = aiohttp.ClientSession(connector=connector, trust_env=True) - # https pool manager - self.pool_manager = aiohttp.ClientSession( - connector=connector, - trust_env=True - ) - - async def close(self): - await self.pool_manager.close() - - async def request(self, method, url, query_params=None, headers=None, - body=None, post_params=None, _preload_content=True, - _request_timeout=None): - """Execute request - - :param method: http request method - :param url: http request url - :param query_params: query parameters in the url - :param headers: http request headers - :param body: request json body, for `application/json` - :param post_params: request post parameters, - `application/x-www-form-urlencoded` - and `multipart/form-data` - :param _preload_content: this is a non-applicable field for - the AiohttpClient. - :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. + {{#asyncio}}async {{/asyncio}}def close(self) -> None: + """ + Closes the underlying aiohttp.ClientSession. + """ + {{#asyncio}}await {{/asyncio}}self.pool_manager.close() + + {{#asyncio}}async {{/asyncio}}def build_request( + self, + method: str, + url: str, + query_params: Optional[dict] = None, + headers: Optional[dict] = None, + body: Optional[Any] = None, + post_params: Optional[List[Tuple[str, Any]]] = None, + _preload_content: bool = True, + _request_timeout: Optional[float] = None, + ) -> dict: + """ + Builds a dictionary of request arguments suitable for aiohttp. + + :param method: The HTTP method. + :param url: The URL endpoint. + :param query_params: Optional query parameters. + :param headers: Optional request headers. + :param body: The request body, if any. + :param post_params: Form or multipart parameters, if any. + :param _preload_content: If True, content will be loaded immediately (not used here). + :param _request_timeout: Request timeout in seconds. + :return: A dictionary of request arguments. """ method = method.upper() - assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT', - 'PATCH', 'OPTIONS'] + assert method in ["GET", "HEAD", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"] if post_params and body: raise ApiValueError( "body parameter cannot be used with post_params parameter." ) - post_params = post_params or {} + post_params = post_params or [] headers = headers or {} - timeout = _request_timeout or self._timeout_millisec / 1000 + timeout = _request_timeout or (self._timeout_millisec / 1000) - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json" args = { "method": method, "url": url, "timeout": timeout, - "headers": headers + "headers": headers, } if self.proxy: @@ -115,140 +150,240 @@ class RESTClientObject: args["proxy_headers"] = self.proxy_headers if query_params: - args["url"] += '?' + urllib.parse.urlencode(query_params) + args["url"] += "?" + urllib.parse.urlencode(query_params) - # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` - if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: - if re.search('json', headers['Content-Type'], re.IGNORECASE): + if method in ["POST", "PUT", "PATCH", "OPTIONS", "DELETE"]: + if re.search("json", headers["Content-Type"], re.IGNORECASE): if body is not None: body = json.dumps(body) args["data"] = body - elif headers['Content-Type'] == 'application/x-www-form-urlencoded': + elif headers["Content-Type"] == "application/x-www-form-urlencoded": args["data"] = aiohttp.FormData(post_params) - elif headers['Content-Type'] == 'multipart/form-data': - # must del headers['Content-Type'], or the correct - # Content-Type which generated by aiohttp - del headers['Content-Type'] + elif headers["Content-Type"] == "multipart/form-data": + del headers["Content-Type"] data = aiohttp.FormData() for param in post_params: k, v = param if isinstance(v, tuple) and len(v) == 3: - data.add_field(k, - value=v[1], - filename=v[0], - content_type=v[2]) + data.add_field(k, value=v[1], filename=v[0], content_type=v[2]) else: data.add_field(k, v) args["data"] = data - - # Pass a `bytes` parameter directly in the body to support - # other content types than Json when `body` argument is provided - # in serialized form elif isinstance(body, bytes): args["data"] = body else: - # Cannot generate the request from given parameters - msg = """Cannot prepare a request message for provided - arguments. Please check that your arguments match - declared content type.""" + msg = ( + "Cannot prepare a request message for provided arguments. " + "Please check that your arguments match declared content type." + ) raise ApiException(status=0, reason=msg) - r = await self.pool_manager.request(**args) + return args + + {{#asyncio}}async {{/asyncio}}def handle_response_exception( + self, response: RESTResponse | aiohttp.ClientResponse + ) -> None: + """ + Raises exceptions if response status indicates an error. + + :param response: The response to check. + :raises ValidationException: If status is 400. + :raises UnauthorizedException: If status is 401. + :raises ForbiddenException: If status is 403. + :raises NotFoundException: If status is 404. + :raises RateLimitExceededError: If status is 429. + :raises ServiceException: If status is 5xx. + :raises ApiException: For other non-2xx statuses. + """ + if 200 <= response.status <= 299: + return + + match response.status: + case 400: + raise ValidationException(http_resp=response) + case 401: + raise UnauthorizedException(http_resp=response) + case 403: + raise ForbiddenException(http_resp=response) + case 404: + raise NotFoundException(http_resp=response) + case 429: + raise RateLimitExceededError(http_resp=response) + case _ if 500 <= response.status <= 599: + raise ServiceException(http_resp=response) + case _: + raise ApiException(http_resp=response) + + def _accumulate_json_lines( + self, leftover: bytes, data: bytes, buffer: bytearray + ) -> Tuple[bytes, List[Any]]: + """ + Processes a chunk of data and leftover bytes. Splits on newlines, decodes valid JSON, + and returns leftover bytes and a list of decoded JSON objects. + + :param leftover: Any leftover bytes from previous chunks. + :param data: The new chunk of data. + :param buffer: The main bytearray buffer for all data. + :return: Updated leftover bytes and a list of decoded JSON objects. + """ + objects: List[Any] = [] + leftover += data + lines = leftover.split( + b"\n" + ) # Objects are received as one-per-line, so split at newlines + leftover = lines.pop() + buffer.extend(data) + for line in lines: + try: + decoded = json.loads(line.decode("utf-8")) + objects.append(decoded) + except json.JSONDecodeError as e: + logger.warning("Skipping invalid JSON segment: %s", e) + return leftover, objects + + {{#asyncio}}async {{/asyncio}}def stream( + self, + method: str, + url: str, + query_params: Optional[dict] = None, + headers: Optional[dict] = None, + body: Optional[Any] = None, + post_params: Optional[List[Tuple[str, Any]]] = None, + _request_timeout: Optional[float] = None, + ): + """ + Streams JSON objects from a specified endpoint, handling partial chunks + and leftover data at the end of the stream. + + :param method: The HTTP method (GET, POST, etc.). + :param url: The endpoint URL. + :param query_params: Query parameters to be appended to the URL. + :param headers: Optional headers to include in the request. + :param body: Optional body for the request. + :param post_params: Optional form/multipart parameters. + :param _request_timeout: An optional request timeout in seconds. + :yields: Parsed JSON objects as Python data structures. + """ + + # Build our request payload + args = {{#asyncio}}await {{/asyncio}}self.build_request( + method, + url, + query_params=query_params, + headers=headers, + body=body, + post_params=post_params, + _preload_content=False, + _request_timeout=_request_timeout, + ) + + # Initialize buffers for data chunks + buffer = bytearray() + leftover = b"" + response: Optional[aiohttp.ClientResponse] = None + + try: + # Send request, collect response handler + {{#asyncio}}async {{/asyncio}}with self.pool_manager.request(**args) as resp: + response = resp + try: + # Iterate over streamed/chunked response data + {{#asyncio}}async {{/asyncio}}for data, _ in resp.content.iter_chunks(): + if data: + # Process data chunk + leftover, decoded_objects = self._accumulate_json_lines( + leftover, data, buffer + ) + + # Yield any complete objects + for obj in decoded_objects: + yield obj + + except Exception as e: + logger.exception("Stream reading error: %s", e) + + except Exception as conn_err: + logger.exception("Connection or request setup error: %s", conn_err) + + # Handle any remaining data after stream ends + if response is not None: + # Check for any leftover data + if leftover: + try: + # Attempt to decode and yield any remaining JSON object + final_str = leftover.decode("utf-8") + final_obj = json.loads(final_str) + buffer.extend(leftover) + yield final_obj + + except json.JSONDecodeError: + logger.debug("Incomplete leftover data at end of stream.") + + # Decode the complete/buffered data for logging purposes + if isinstance(response, aiohttp.ClientResponse): + response.data = buffer.decode("utf-8") + + # Handle any HTTP errors that may have occurred + {{#asyncio}}await {{/asyncio}}self.handle_response_exception(response) + + # Release the response object (required!) + response.release() + + # Release the connection back to the pool + {{#asyncio}}await {{/asyncio}}self.close() + + {{#asyncio}}async {{/asyncio}}def request( + self, + method: str, + url: str, + query_params: Optional[dict] = None, + headers: Optional[dict] = None, + body: Optional[Any] = None, + post_params: Optional[List[Tuple[str, Any]]] = None, + _preload_content: bool = True, + _request_timeout: Optional[float] = None, + ) -> RESTResponse | aiohttp.ClientResponse: + """ + Executes a request and returns the response object. + + :param method: The HTTP method. + :param url: The endpoint URL. + :param query_params: Query parameters to be appended to the URL. + :param headers: Optional request headers. + :param body: A request body for JSON or other content types. + :param post_params: Form/multipart parameters for the request. + :param _preload_content: If True, the response body is read immediately. + :param _request_timeout: An optional request timeout in seconds. + :return: A RESTResponse if _preload_content is True, otherwise an aiohttp.ClientResponse. + """ + + # Build our request payload + args = {{#asyncio}}await {{/asyncio}}self.build_request( + method, + url, + query_params=query_params, + headers=headers, + body=body, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + ) + + # Send request, collect response handler + resp = {{#asyncio}}await {{/asyncio}}self.pool_manager.request(**args) + + # If we want to preload the response, read it if _preload_content: + # Collect response data + data = {{#asyncio}}await {{/asyncio}}resp.read() + + # Transform response JSON data into RESTResponse object + resp = RESTResponse(resp, data) + + # Log the response body + logger.debug(f"response body: {resp.data}") + + # Handle any errors that may have occurred + {{#asyncio}}await {{/asyncio}}self.handle_response_exception(resp) - data = await r.read() - r = RESTResponse(r, data) - - # log response body - logger.debug("response body: %s", r.data) - - if not 200 <= r.status <= 299: - if r.status == 400: - raise ValidationException(http_resp=r) - - if r.status == 401: - raise UnauthorizedException(http_resp=r) - - if r.status == 403: - raise ForbiddenException(http_resp=r) - - if r.status == 404: - raise NotFoundException(http_resp=r) - - if r.status == 429: - raise RateLimitExceededError(http_resp=r) - - if 500 <= r.status <= 599: - raise ServiceException(http_resp=r) - - raise ApiException(http_resp=r) - - return r - - async def GET(self, url, headers=None, query_params=None, - _preload_content=True, _request_timeout=None): - return (await self.request("GET", url, - headers=headers, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - query_params=query_params)) - - async def HEAD(self, url, headers=None, query_params=None, - _preload_content=True, _request_timeout=None): - return (await self.request("HEAD", url, - headers=headers, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - query_params=query_params)) - - async def OPTIONS(self, url, headers=None, query_params=None, - post_params=None, body=None, _preload_content=True, - _request_timeout=None): - return (await self.request("OPTIONS", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body)) - - async def DELETE(self, url, headers=None, query_params=None, body=None, - _preload_content=True, _request_timeout=None): - return (await self.request("DELETE", url, - headers=headers, - query_params=query_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body)) - - async def POST(self, url, headers=None, query_params=None, - post_params=None, body=None, _preload_content=True, - _request_timeout=None): - return (await self.request("POST", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body)) - - async def PUT(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - return (await self.request("PUT", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body)) - - async def PATCH(self, url, headers=None, query_params=None, - post_params=None, body=None, _preload_content=True, - _request_timeout=None): - return (await self.request("PATCH", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body)) + return resp diff --git a/config/clients/python/template/src/sync/api.py.mustache b/config/clients/python/template/src/sync/api.py.mustache index 3da0e7cc..23e25544 100644 --- a/config/clients/python/template/src/sync/api.py.mustache +++ b/config/clients/python/template/src/sync/api.py.mustache @@ -168,7 +168,8 @@ class {{classname}}: "_request_auth", "_content_type", "_headers", - "_retry_params" + "_retry_params", + "_streaming", ] ) @@ -313,6 +314,7 @@ class {{classname}}: _request_auth=local_var_params.get('_request_auth'), _oauth2_client=self._oauth2_client, _telemetry_attributes=telemetry_attributes, + _streaming=local_var_params.get("_streaming", False), ) {{/operation}} {{/operations}} diff --git a/config/clients/python/template/src/sync/api_client.py.mustache b/config/clients/python/template/src/sync/api_client.py.mustache index ecc16100..68171661 100644 --- a/config/clients/python/template/src/sync/api_client.py.mustache +++ b/config/clients/python/template/src/sync/api_client.py.mustache @@ -161,6 +161,7 @@ class ApiClient: _retry_params=None, _oauth2_client=None, _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _streaming: bool = False, ): self.configuration.is_valid() @@ -228,7 +229,7 @@ class ApiClient: max_retry = _retry_params.max_retry if _retry_params.min_wait_in_ms is not None: max_retry = _retry_params.min_wait_in_ms - + _telemetry_attributes = TelemetryAttributes.fromRequest( user_agent=self.user_agent, fga_method=resource_path, @@ -246,10 +247,16 @@ class ApiClient: 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, + method, + url, + query_params=query_params, + headers=header_params, + post_params=post_params, + body=body, _preload_content=_preload_content, - _request_timeout=_request_timeout) + _request_timeout=_request_timeout, + _streaming=_streaming, + ) except (RateLimitExceededError, ServiceException) as e: if retry < max_retry and e.status != 501: _telemetry_attributes = TelemetryAttributes.fromResponse( @@ -327,7 +334,7 @@ class ApiClient: configuration=self.configuration.telemetry, ) - if not _preload_content: + if not _preload_content or _streaming: {{^tornado}} return return_data {{/tornado}} @@ -488,6 +495,7 @@ class ApiClient: _retry_params=None, _oauth2_client=None, _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _streaming: bool = False, ): """Makes the HTTP request (synchronous) and returns deserialized data. @@ -549,6 +557,7 @@ class ApiClient: _retry_params, _oauth2_client, _telemetry_attributes, + _streaming, ) return self.pool.apply_async( @@ -572,68 +581,50 @@ class ApiClient: _retry_params, _oauth2_client, _telemetry_attributes, + _streaming, ), ) - 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: + def request( + self, + method, + url, + query_params=None, + headers=None, + post_params=None, + body=None, + _preload_content=True, + _request_timeout=None, + _streaming: bool = False, + ): + if method not in ["GET", "HEAD", "OPTIONS", "POST", "PATCH", "PUT", "DELETE"]: raise ApiValueError( "http method must be `GET`, `HEAD`, `OPTIONS`," " `POST`, `PATCH`, `PUT` or `DELETE`." ) + if _streaming: + return self.rest_client.stream( + method, + url, + query_params=query_params, + headers=headers, + post_params=post_params, + body=body, + _request_timeout=_request_timeout, + ) + + return self.rest_client.request( + method, + url, + query_params=query_params, + headers=headers, + post_params=post_params, + body=body, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + ) + def parameters_to_tuples(self, params, collection_formats): """Get parameters as list of tuples, formatting collections. diff --git a/config/clients/python/template/src/sync/client/client.py.mustache b/config/clients/python/template/src/sync/client/client.py.mustache index a96a321e..1e87d742 100644 --- a/config/clients/python/template/src/sync/client/client.py.mustache +++ b/config/clients/python/template/src/sync/client/client.py.mustache @@ -42,6 +42,7 @@ from {{packageName}}.models.list_users_request import ListUsersRequest 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.streamed_list_objects_response import StreamedListObjectsResponse from {{packageName}}.models.tuple_key import TupleKey from {{packageName}}.models.write_assertions_request import WriteAssertionsRequest from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest @@ -727,6 +728,43 @@ class OpenFgaClient: ) return api_response + def streamed_list_objects( + self, body: ClientListObjectsRequest, options: dict[str, str] = None + ): + """ + Retrieve all objects of the given type that the user has a relation with, using the streaming ListObjects API. + + :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 + :param consistency(options) - The type of consistency preferred for the request + """ + kwargs = options_to_kwargs(options) + kwargs["_streaming"] = True + + req_body = ListObjectsRequest( + authorization_model_id=self._get_authorization_model_id(options), + user=body.user, + relation=body.relation, + type=body.type, + context=body.context, + consistency=self._get_consistency(options), + ) + + if body.contextual_tuples: + req_body.contextual_tuples = ContextualTupleKeys( + tuple_keys=convert_tuple_keys(body.contextual_tuples) + ) + + for response in self._api.streamed_list_objects(body=req_body, **kwargs): + if response and "result" in response and "object" in response["result"]: + yield StreamedListObjectsResponse(response["result"]["object"]) + + return + def list_relations(self, body: ClientListRelationsRequest, options: dict[str, str] = None): """ Return all the relations for which user has a relationship with the object diff --git a/config/clients/python/template/src/sync/oauth2.py.mustache b/config/clients/python/template/src/sync/oauth2.py.mustache index 57faf83d..032a9c96 100644 --- a/config/clients/python/template/src/sync/oauth2.py.mustache +++ b/config/clients/python/template/src/sync/oauth2.py.mustache @@ -90,8 +90,15 @@ class OAuth2Client: ) for attempt in range(max_retry + 1): - raw_response = client.POST( - token_url, headers=headers, post_params=post_params + raw_response = client.request( + method="POST", + url=token_url, + headers=headers, + query_params=None, + body=None, + _preload_content=True, + _request_timeout=None, + post_params=post_params ) if 500 <= raw_response.status <= 599 or raw_response.status == 429: diff --git a/config/clients/python/template/src/sync/rest.py.mustache b/config/clients/python/template/src/sync/rest.py.mustache index b28edafa..7febe958 100644 --- a/config/clients/python/template/src/sync/rest.py.mustache +++ b/config/clients/python/template/src/sync/rest.py.mustache @@ -6,66 +6,107 @@ import logging import re import ssl import urllib -import urllib3 +from typing import Any, List, Optional, Tuple -from {{packageName}}.exceptions import ApiException, UnauthorizedException, ForbiddenException, NotFoundException, ServiceException, ApiValueError, ValidationException, RateLimitExceededError +import urllib3 +from {{packageName}}.exceptions import ( + ApiException, + ApiValueError, + ForbiddenException, + NotFoundException, + RateLimitExceededError, + ServiceException, + UnauthorizedException, + ValidationException, +) logger = logging.getLogger(__name__) class RESTResponse(io.IOBase): + """ + Represents an HTTP response object in the non-async client. + """ + + def __init__(self, resp: urllib3.HTTPResponse, data: bytes) -> None: + """ + Initializes a RESTResponse with a urllib3.HTTPResponse and corresponding data. - def __init__(self, resp, data): + :param resp: The urllib3.HTTPResponse object. + :param data: The raw byte data read from the response. + """ self.urllib3_response = resp self.status = resp.status self.reason = resp.reason self.data = data - def getheaders(self): - """Returns a dictionary of the response headers.""" + def getheaders(self) -> dict: + """ + Returns a dictionary of the response headers. + """ return self.urllib3_response.headers - def getheader(self, name, default=None): - """Returns a given response header.""" + def getheader(self, name: str, default: Optional[str] = None) -> Optional[str]: + """ + Returns a specific header value by name. + + :param name: The name of the header. + :param default: The default value if header is not found. + :return: The header value, or default if not present. + """ return self.urllib3_response.headers.get(name, default) class RESTClientObject: + """ + A synchronous client object that manages HTTP interactions using urllib3. + """ - def __init__(self, configuration, pools_size=4, maxsize=None): - # urllib3.PoolManager will pass all kw parameters to connectionpool - # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 - # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 - # maxsize is the number of requests to host that are allowed in parallel - # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html + def __init__( + self, configuration: Any, pools_size: int = 4, maxsize: Optional[int] = None + ) -> None: + """ + Creates a new RESTClientObject using urllib3. - # cert_reqs - if configuration.verify_ssl: + :param configuration: A configuration object with necessary parameters. + :param pools_size: The number of connection pools to use. + :param maxsize: The maximum number of connections per pool. + """ + if hasattr(configuration, "verify_ssl") and configuration.verify_ssl: cert_reqs = ssl.CERT_REQUIRED else: cert_reqs = ssl.CERT_NONE addition_pool_args = {} - if configuration.assert_hostname is not None: - addition_pool_args['assert_hostname'] = configuration.assert_hostname - if configuration.retries is not None: - addition_pool_args['retries'] = configuration.retries + if ( + hasattr(configuration, "assert_hostname") + and configuration.assert_hostname is not None + ): + addition_pool_args["assert_hostname"] = configuration.assert_hostname - if configuration.socket_options is not None: - addition_pool_args['socket_options'] = configuration.socket_options + if hasattr(configuration, "retries") and configuration.retries is not None: + addition_pool_args["retries"] = configuration.retries + + if ( + hasattr(configuration, "socket_options") + and configuration.socket_options is not None + ): + addition_pool_args["socket_options"] = configuration.socket_options if maxsize is None: - if configuration.connection_pool_maxsize is not None: + if ( + hasattr(configuration, "connection_pool_maxsize") + and configuration.connection_pool_maxsize is not None + ): maxsize = configuration.connection_pool_maxsize else: maxsize = 4 self._timeout_millisec = configuration.timeout_millisec - # https pool manager - if configuration.proxy: + if hasattr(configuration, "proxy") and configuration.proxy is not None: self.pool_manager = urllib3.ProxyManager( num_pools=pools_size, maxsize=maxsize, @@ -75,220 +116,316 @@ class RESTClientObject: key_file=configuration.key_file, proxy_url=configuration.proxy, proxy_headers=configuration.proxy_headers, - **addition_pool_args - ) - else: - self.pool_manager = urllib3.PoolManager( - num_pools=pools_size, - maxsize=maxsize, - cert_reqs=cert_reqs, - ca_certs=configuration.ssl_ca_cert, - cert_file=configuration.cert_file, - key_file=configuration.key_file, - **addition_pool_args + **addition_pool_args, ) - def close(self): + return + + self.pool_manager = urllib3.PoolManager( + num_pools=pools_size, + maxsize=maxsize, + cert_reqs=cert_reqs, + ca_certs=configuration.ssl_ca_cert, + cert_file=configuration.cert_file, + key_file=configuration.key_file, + **addition_pool_args, + ) + + def close(self) -> None: + """ + Closes all pooled connections. + """ 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): - """Perform requests. - - :param method: http request method - :param url: http request url - :param query_params: query parameters in the url - :param headers: http request headers - :param body: request json body, for `application/json` - :param post_params: request post parameters, - `application/x-www-form-urlencoded` - and `multipart/form-data` - :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. + def build_request( + self, + method: str, + url: str, + query_params: Optional[dict] = None, + headers: Optional[dict] = None, + body: Optional[Any] = None, + post_params: Optional[dict] = None, + _preload_content: bool = True, + _request_timeout: Optional[float | tuple] = None, + ) -> dict: + """ + Builds a dictionary of request arguments suitable for urllib3. + + :param method: The HTTP method (GET, POST, etc.). + :param url: The URL endpoint. + :param query_params: Optional query parameters. + :param headers: Optional request headers. + :param body: The request body, if any. + :param post_params: Form or multipart parameters, if any. + :param _preload_content: If True, response data is read immediately (by urllib3). + :param _request_timeout: Timeout setting, in seconds or a (connect, read) tuple. + :return: A dictionary of request arguments for urllib3. """ method = method.upper() - assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT', - 'PATCH', 'OPTIONS'] + assert method in ["GET", "HEAD", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"] if post_params and body: raise ApiValueError( "body parameter cannot be used with post_params parameter." ) - post_params = post_params or {} headers = headers or {} + post_params = post_params or {} + timeout_val = _request_timeout or self._timeout_millisec + + if isinstance(timeout_val, (float, int)): + if timeout_val > 100: + timeout_val /= 1000 + timeout = urllib3.Timeout(total=timeout_val) + elif isinstance(timeout_val, tuple) and len(timeout_val) == 2: + connect_t, read_t = timeout_val + if connect_t > 100: + connect_t /= 1000 + if read_t > 100: + read_t /= 1000 + timeout = urllib3.Timeout(connect=connect_t, read=read_t) + else: + timeout = urllib3.Timeout(total=None) # fallback + + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json" + + args = { + "method": method, + "url": url, + "timeout": timeout, + "headers": headers, + "preload_content": _preload_content, + } + + if query_params: + encoded_qs = urllib.parse.urlencode(query_params) + args["url"] = f"{url}?{encoded_qs}" + + # Handle body/post_params for methods that send payloads + if method in ["POST", "PUT", "PATCH", "OPTIONS", "DELETE"]: + if re.search("json", headers["Content-Type"], re.IGNORECASE): + if body is not None: + body = json.dumps(body) + args["body"] = body + + elif headers["Content-Type"] == "application/x-www-form-urlencoded": + args["fields"] = post_params + args["encode_multipart"] = False + + elif headers["Content-Type"] == "multipart/form-data": + del headers["Content-Type"] + args["fields"] = post_params + args["encode_multipart"] = True + + elif isinstance(body, (str, bytes)): + args["body"] = body + else: + msg = ( + "Cannot prepare a request message for provided arguments. " + "Please check that your arguments match declared content type." + ) + raise ApiException(status=0, reason=msg) + else: + # For GET, HEAD, etc., we can pass query_params as fields if needed + # but we've already appended them to the URL above + pass - timeout = urllib3.Timeout(total=self._timeout_millisec / 1000) - if _request_timeout: - if isinstance(_request_timeout, (float, int)): - timeout = urllib3.Timeout(total=_request_timeout) - elif (isinstance(_request_timeout, tuple) - and len(_request_timeout) == 2): - timeout = urllib3.Timeout( - connect=_request_timeout[0], read=_request_timeout[1]) + return args - if 'Content-Type' not in headers: - headers['Content-Type'] = 'application/json' + def handle_response_exception( + self, response: RESTResponse | urllib3.HTTPResponse + ) -> None: + """ + Raises exceptions if response status indicates an error. + + :param response: The response to check (could be RESTResponse or raw urllib3.HTTPResponse). + """ + if 200 <= response.status <= 299: + return + + match response.status: + case 400: + raise ValidationException(http_resp=response) + case 401: + raise UnauthorizedException(http_resp=response) + case 403: + raise ForbiddenException(http_resp=response) + case 404: + raise NotFoundException(http_resp=response) + case 429: + raise RateLimitExceededError(http_resp=response) + case _ if 500 <= response.status <= 599: + raise ServiceException(http_resp=response) + case _: + raise ApiException(http_resp=response) + + def _accumulate_json_lines( + self, leftover: bytes, data: bytes, buffer: bytearray + ) -> Tuple[bytes, List[Any]]: + """ + Processes a chunk of data plus any leftover bytes from a previous iteration. + Splits on newlines, decodes valid JSON lines, and returns updated leftover bytes + plus a list of decoded JSON objects. + + :param leftover: Any leftover bytes from previous chunks. + :param data: The new chunk of data. + :param buffer: The main bytearray buffer for all data in this request. + :return: A tuple of (updated leftover bytes, list of decoded objects). + """ + objects: List[Any] = [] + leftover += data + lines = leftover.split( + b"\n" + ) # Objects are received as one-per-line, so split at newlines + leftover = lines.pop() + buffer.extend(data) + + for line in lines: + line_str = line.decode("utf-8") + try: + decoded = json.loads(line_str) + objects.append(decoded) + except json.JSONDecodeError as e: + logger.warning("Skipping invalid JSON segment: %s", e) + + return leftover, objects + + def stream( + self, + method: str, + url: str, + query_params: Optional[dict] = None, + headers: Optional[dict] = None, + body: Optional[Any] = None, + post_params: Optional[dict] = None, + _request_timeout: Optional[float | tuple] = None, + ): + """ + Streams JSON objects from a specified endpoint, reassembling partial chunks + and yielding one decoded object at a time. + + :param method: The HTTP method (GET, POST, etc.). + :param url: The endpoint URL. + :param query_params: Query parameters to be appended to the URL. + :param headers: Optional headers to include in the request. + :param body: Optional body for the request. + :param post_params: Optional form/multipart parameters. + :param _request_timeout: An optional request timeout in seconds or (connect, read) tuple. + :yields: Parsed JSON objects as Python data structures. + """ + + # Build our request payload + args = self.build_request( + method, + url, + query_params=query_params, + headers=headers, + body=body, + post_params=post_params, + _preload_content=False, + _request_timeout=_request_timeout, + ) + + # Initialize buffers for data chunks + buffer = bytearray() + leftover = b"" + + # Send request, collect response handler + response = self.pool_manager.request(**args) try: - # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` - if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: - if query_params: - url += '?' + urllib.parse.urlencode(query_params) - if re.search('json', headers['Content-Type'], re.IGNORECASE): - request_body = None - if body is not None: - request_body = json.dumps(body) - r = self.pool_manager.request( - method, url, - body=request_body, - preload_content=_preload_content, - timeout=timeout, - headers=headers) - elif headers['Content-Type'] == 'application/x-www-form-urlencoded': - r = self.pool_manager.request( - method, url, - fields=post_params, - encode_multipart=False, - preload_content=_preload_content, - timeout=timeout, - headers=headers) - elif headers['Content-Type'] == 'multipart/form-data': - # must del headers['Content-Type'], or the correct - # Content-Type which generated by urllib3 will be - # overwritten. - del headers['Content-Type'] - r = self.pool_manager.request( - method, url, - fields=post_params, - encode_multipart=True, - preload_content=_preload_content, - timeout=timeout, - headers=headers) - # Pass a `string` parameter directly in the body to support - # other content types than Json when `body` argument is - # provided in serialized form - elif isinstance(body, str) or isinstance(body, bytes): - request_body = body - r = self.pool_manager.request( - method, url, - body=request_body, - preload_content=_preload_content, - timeout=timeout, - headers=headers) - else: - # Cannot generate the request from given parameters - msg = """Cannot prepare a request message for provided - arguments. Please check that your arguments match - declared content type.""" - raise ApiException(status=0, reason=msg) - # For `GET`, `HEAD` - else: - r = self.pool_manager.request(method, url, - fields=query_params, - preload_content=_preload_content, - timeout=timeout, - headers=headers) - except urllib3.exceptions.SSLError as e: - msg = f"{type(e).__name__}\n{str(e)}" - raise ApiException(status=0, reason=msg) + # Iterate over streamed/chunked response data + for chunk in response.stream(1024): + # Process data chunk + leftover, decoded_objects = self._accumulate_json_lines( + leftover, chunk, buffer + ) + + # Yield any complete objects + for obj in decoded_objects: + yield obj + + except Exception as e: + logger.exception("Stream error: %s", e) + + # Handle any remaining data after stream ends + if response is not None: + # Check for any leftover data + if leftover: + try: + # Attempt to decode and yield any remaining JSON object + final_str = leftover.decode("utf-8") + final_obj = json.loads(final_str) + buffer.extend(leftover) + yield final_obj + + except json.JSONDecodeError: + logger.debug("Incomplete leftover data at end of stream.") + + # Handle any HTTP errors that may have occurred + self.handle_response_exception(response) + + # Release the response object (required!) + response.release_conn() + + # Release the connection back to the pool + self.close() + + def request( + self, + method: str, + url: str, + query_params: Optional[dict] = None, + headers: Optional[dict] = None, + body: Optional[Any] = None, + post_params: Optional[dict] = None, + _preload_content: bool = True, + _request_timeout: Optional[float | tuple] = None, + ) -> RESTResponse | urllib3.HTTPResponse: + """ + Executes a request and returns the response object. + + :param method: The HTTP method. + :param url: The endpoint URL. + :param query_params: Query parameters to be appended to the URL. + :param headers: Optional request headers. + :param body: A request body for JSON or other content types. + :param post_params: Form/multipart parameters for the request. + :param _preload_content: If True, the response body is read immediately + and wrapped in a RESTResponse. Otherwise, + an un-consumed urllib3.HTTPResponse is returned. + :param _request_timeout: Timeout in seconds or a (connect, read) tuple. + :return: A RESTResponse if _preload_content=True, otherwise a raw HTTPResponse. + """ + # Build our request payload + args = self.build_request( + method, + url, + query_params=query_params, + headers=headers, + body=body, + post_params=post_params, + _preload_content=_preload_content, + _request_timeout=_request_timeout, + ) + + # Send request, collect response handler + resp = self.pool_manager.request(**args) + + # If we want to preload the response, read it if _preload_content: - r = RESTResponse(r, r.data) - - # log response body - logger.debug("response body: %s", r.data) - - if not 200 <= r.status <= 299: - if r.status == 400: - raise ValidationException(http_resp=r) - - if r.status == 401: - raise UnauthorizedException(http_resp=r) - - if r.status == 403: - raise ForbiddenException(http_resp=r) - - if r.status == 404: - raise NotFoundException(http_resp=r) - - if r.status == 429: - raise RateLimitExceededError(http_resp=r) - - if 500 <= r.status <= 599: - raise ServiceException(http_resp=r) - - raise ApiException(http_resp=r) - - return r - - def GET(self, url, headers=None, query_params=None, _preload_content=True, - _request_timeout=None): - return self.request("GET", url, - headers=headers, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - query_params=query_params) - - def HEAD(self, url, headers=None, query_params=None, _preload_content=True, - _request_timeout=None): - return self.request("HEAD", url, - headers=headers, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - query_params=query_params) - - def OPTIONS(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - return self.request("OPTIONS", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - - def DELETE(self, url, headers=None, query_params=None, body=None, - _preload_content=True, _request_timeout=None): - return self.request("DELETE", url, - headers=headers, - query_params=query_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - - def POST(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - return self.request("POST", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - - def PUT(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - return self.request("PUT", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - - def PATCH(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - return self.request("PATCH", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) + # Collect response data and transform response (JSON) into RESTResponse object + resp = RESTResponse(resp, resp.data) + + # Log the response body + logger.debug("response body: %s", resp.data) + + # Handle any errors that may have occurred + self.handle_response_exception(resp) + + # Release the connection back to the pool + self.close() + + return resp diff --git a/config/clients/python/template/src/telemetry/attributes.py.mustache b/config/clients/python/template/src/telemetry/attributes.py.mustache index 2b31af84..9cd697c2 100644 --- a/config/clients/python/template/src/telemetry/attributes.py.mustache +++ b/config/clients/python/template/src/telemetry/attributes.py.mustache @@ -8,7 +8,7 @@ from urllib3 import HTTPResponse from {{packageName}}.credentials import Credentials from {{packageName}}.exceptions import ApiException from {{packageName}}.rest import RESTResponse -from openfga_sdk.telemetry.utilities import ( +from {{packageName}}.telemetry.utilities import ( doesInstanceHaveCallable, ) @@ -171,7 +171,7 @@ class TelemetryAttributes: @staticmethod def fromBody(body: Any, attributes: dict[TelemetryAttribute, str | int] = None): - from openfga_sdk.models.batch_check_request import BatchCheckRequest + from {{packageName}}.models.batch_check_request import BatchCheckRequest if attributes is None: attributes = {} diff --git a/config/clients/python/template/test-requirements.mustache b/config/clients/python/template/test-requirements.mustache index 543e9bf6..aae4461c 100644 --- a/config/clients/python/template/test-requirements.mustache +++ b/config/clients/python/template/test-requirements.mustache @@ -7,4 +7,5 @@ flake8 >= 7.0.0, < 8 griffe >= 0.41.2, < 2 isort==5.13.2 pytest-cov >= 5, < 7 -pyupgrade==3.19.0 +pyupgrade==3.19.1 +pytest-asyncio >= 0.25, < 1 diff --git a/config/clients/python/template/test/api_test.py.mustache b/config/clients/python/template/test/api_test.py.mustache index 2938bbc7..2fac190e 100644 --- a/config/clients/python/template/test/api_test.py.mustache +++ b/config/clients/python/template/test/api_test.py.mustache @@ -1,18 +1,25 @@ {{>partial_header}} import unittest -from unittest.mock import ANY -from unittest import IsolatedAsyncioTestCase -from unittest.mock import patch from datetime import datetime +from unittest import IsolatedAsyncioTestCase +from unittest.mock import ANY, patch import urllib3 import {{packageName}} from {{packageName}} import rest from {{packageName}}.api import open_fga_api -from {{packageName}}.credentials import Credentials, CredentialConfiguration -from {{packageName}}.exceptions import FgaValidationException, ApiValueError, NotFoundException, RateLimitExceededError, ServiceException, ValidationException, FGA_REQUEST_ID +from {{packageName}}.credentials import CredentialConfiguration, Credentials +from {{packageName}}.exceptions import ( + FGA_REQUEST_ID, + ApiValueError, + FgaValidationException, + NotFoundException, + RateLimitExceededError, + ServiceException, + ValidationException, +) from {{packageName}}.models.assertion import Assertion from {{packageName}}.models.authorization_model import AuthorizationModel from {{packageName}}.models.check_request import CheckRequest @@ -25,7 +32,9 @@ from {{packageName}}.models.expand_request_tuple_key import ExpandRequestTupleKe 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.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 @@ -35,9 +44,13 @@ from {{packageName}}.models.list_users_response import ListUsersResponse 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.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_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 @@ -47,53 +60,58 @@ 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.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 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.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_authorization_model_request import ( + WriteAuthorizationModelRequest, +) +from {{packageName}}.models.write_authorization_model_response import ( + WriteAuthorizationModelResponse, +) from {{packageName}}.models.write_request import WriteRequest +from {{packageName}}.models.write_request_deletes import WriteRequestDeletes +from {{packageName}}.models.write_request_writes import WriteRequestWrites + +store_id = "01H0H015178Y2V4CX10C2KGHF4" +request_id = "x1y2z3" -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 - }) + 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 + 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 {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): - """{{classname}} unit test stubs""" + +class TestOpenFgaApi(IsolatedAsyncioTestCase): + """OpenFgaApi unit test stubs""" def setUp(self): self.configuration = {{packageName}}.Configuration( - api_url='http://api.{{sampleApiDomain}}', + api_url="http://api.{{sampleApiDomain}}", ) def tearDown(self): pass - @patch.object(rest.RESTClientObject, 'request') - async def test_check(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_check(self, mock_request): """Test case for check Check whether a user is authorized to access an object @@ -122,30 +140,36 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): 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', + "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"}, + 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 + _request_timeout=None, ) {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, 'request') - async def test_create_store(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_create_store(self, mock_request): """Test case for create_store Create a store """ - response_body = '''{ + 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 @@ -158,26 +182,26 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): body=body, ) self.assertIsInstance(api_response, CreateStoreResponse) - self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') + self.assertEqual(api_response.id, "01YCP46JKYM8FJCQ37NMBYHE5X") mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores', + "POST", + "http://api.{{sampleApiDomain}}/stores", headers=ANY, query_params=[], post_params=[], body={"name": "test-store"}, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, 'request') - async def test_delete_store(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_delete_store(self, mock_request): """Test case for delete_store Delete a store """ - response_body = '' + response_body = "" mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration configuration.store_id = store_id @@ -185,25 +209,26 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): api_instance = open_fga_api.OpenFgaApi(api_client) {{#asyncio}}await {{/asyncio}}api_instance.delete_store() mock_request.assert_called_once_with( - 'DELETE', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4', + "DELETE", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4", headers=ANY, - query_params=[], body=None, + query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, 'request') - async def test_expand(self, mock_request): + @patch.object(rest.RESTClientObject, "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 """ - response_body = '''{ + 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 @@ -227,30 +252,33 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): expected_response = ExpandResponse(userTree) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/expand', + "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"}, + body={ + "tuple_key": {"object": "document:budget", "relation": "reader"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, 'request') - async def test_get_store(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_get_store(self, mock_request): """Test case for get_store Get a store """ - response_body = '''{ + 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 @@ -259,31 +287,33 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # Get a 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') + 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', + "GET", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, 'request') - async def test_list_objects(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_list_objects(self, mock_request): """Test case for list_objects List objects """ - response_body = ''' + response_body = """ { "objects": [ "document:abcd1234" ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -298,27 +328,31 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # Get all stores api_response = {{#asyncio}}await {{/asyncio}}api_instance.list_objects(body) self.assertIsInstance(api_response, ListObjectsResponse) - self.assertEqual(api_response.objects, ['document:abcd1234']) + self.assertEqual(api_response.objects, ["document:abcd1234"]) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-objects', + "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'}, + body={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "type": "document", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, 'request') - async def test_list_stores(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_list_stores(self, mock_request): """Test case for list_stores Get all stores """ - response_body = ''' + response_body = """ { "stores": [ { @@ -338,7 +372,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: @@ -349,8 +383,10 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): continuation_token="continuation_token_example", ) self.assertIsInstance(api_response, ListStoresResponse) - self.assertEqual(api_response.continuation_token, - "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") + self.assertEqual( + api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) store1 = Store( id="01YCP46JKYM8FJCQ37NMBYHE5X", name="store1", @@ -369,17 +405,20 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): stores = [store1, store2] self.assertEqual(api_response.stores, stores) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores', + "GET", + "http://api.{{sampleApiDomain}}/stores", headers=ANY, - query_params=[('page_size', 1), ('continuation_token', - 'continuation_token_example')], + body=None, + query_params=[ + ("page_size", 1), + ("continuation_token", "continuation_token_example"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, "request") {{#asyncio}}async {{/asyncio}}def test_list_users(self, mock_request): """ @@ -414,7 +453,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}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) request = ListUsersRequest( @@ -496,16 +535,15 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): _request_timeout=None, ) - await api_client.close() - + {{#asyncio}}await {{/asyncio}}api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "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 """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -519,7 +557,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -538,29 +576,42 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): body=body, ) self.assertIsInstance(api_response, ReadResponse) - key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",relation="reader",object="document:2021-budget") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/read', + "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=="}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_read_assertions(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_read_assertions(self, mock_request): """Test case for read_assertions Read assertions for an authorization model ID """ - response_body = ''' + response_body = """ { "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "assertions": [ @@ -574,7 +625,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): } ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -584,8 +635,10 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): "01G5JAVJ41T49E9TT3SKVS7X1J", ) self.assertIsInstance(api_response, ReadAssertionsResponse) - self.assertEqual(api_response.authorization_model_id, '01G5JAVJ41T49E9TT3SKVS7X1J') - assertion=Assertion( + self.assertEqual( + api_response.authorization_model_id, "01G5JAVJ41T49E9TT3SKVS7X1J" + ) + assertion = Assertion( tuple_key=TupleKeyWithoutCondition( object="document:2021-budget", relation="reader", @@ -595,21 +648,23 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ) self.assertEqual(api_response.assertions, [assertion]) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "GET", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_read_authorization_model(self, mock_request): + @patch.object(rest.RESTClientObject, "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 """ - response_body = ''' + response_body = """ { "authorization_model": { "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -641,7 +696,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ] } } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -663,38 +718,45 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), - writer=Userset( + writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version = "1.1", - type_definitions=type_definitions) + 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', + "GET", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_read_changes(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_read_changes(self, mock_request): """Test case for read_changes Return a list of all the tuple changes """ - response_body = ''' + response_body = """ { "changes": [ { @@ -709,7 +771,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -718,39 +780,51 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # 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 = {{#asyncio}}await {{/asyncio}}api_instance.read_changes( page_size=1, continuation_token="abcdefg", start_time="2022-01-01T00:00:00+00:00", - type="document" + 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"), + 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")) + timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00"), + ) read_changes = ReadChangesResponse( - continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', - changes=[changes]) + continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + changes=[changes], + ) self.assertEqual(api_response, read_changes) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/changes', + "GET", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/changes", headers=ANY, - query_params=[('type', 'document'), ('page_size', 1), ('continuation_token', 'abcdefg'), ('start_time', '2022-01-01T00:00:00+00:00') ], + body=None, + query_params=[ + ("type", "document"), + ("page_size", 1), + ("continuation_token", "abcdefg"), + ("start_time", "2022-01-01T00:00:00+00:00"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_write(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_write(self, mock_request): """Test case for write Add tuples from the store """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -777,23 +851,34 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): body, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + "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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_write_delete(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_write_delete(self, mock_request): """Test case for write Delete tuples from the store """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -820,23 +905,34 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): body, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + "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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_write_assertions(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_write_assertions(self, mock_request): """Test case for write_assertions Upsert assertions for an authorization model ID """ - response_body = '' + response_body = "" mock_request.return_value = mock_response(response_body, 204) configuration = self.configuration configuration.store_id = store_id @@ -864,18 +960,29 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): body=body, ) mock_request.assert_called_once_with( - 'PUT', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/xyz0123', + "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"}}]}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_write_authorization_model(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_write_authorization_model(self, mock_request): """Test case for write_authorization_model Create a new authorization model @@ -890,83 +997,98 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # example passing only required values which don't have defaults set body = WriteAuthorizationModelRequest( - schema_version = "1.1", + schema_version="1.1", type_definitions=[ TypeDefinition( type="document", relations=dict( - writer=Userset( + writer=Userset( this=dict(), ), reader=Userset( union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), - ) + ), ), ], ) # Create a new authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_instance.write_authorization_model( - body - ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.write_authorization_model(body) self.assertIsInstance(api_response, WriteAuthorizationModelResponse) expected_response = WriteAuthorizationModelResponse( - authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J" ) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models', + "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"}}]}}}}]}, + 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 + _request_timeout=None, ) def test_default_scheme(self): """ Ensure default scheme is https """ - configuration = {{packageName}}.Configuration( - api_host='localhost' - ) - self.assertEqual(configuration.api_scheme, 'https') + configuration = {{packageName}}.Configuration(api_host="localhost") + self.assertEqual(configuration.api_scheme, "https") def test_host_port(self): """ Ensure host has port will not raise error """ - configuration = {{packageName}}.Configuration( - api_host='localhost:3000' - ) - self.assertEqual(configuration.api_host, 'localhost:3000') + configuration = {{packageName}}.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 = {{packageName}}.Configuration( - api_scheme='http' - ) + configuration = {{packageName}}.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 = {{packageName}}.Configuration( - api_host='localhost' - ) + configuration = {{packageName}}.Configuration(api_host="localhost") configuration.api_scheme = None self.assertRaises(FgaValidationException, configuration.is_valid) @@ -975,8 +1097,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if scheme is bad """ configuration = {{packageName}}.Configuration( - api_host='localhost', - api_scheme='foo' + api_host="localhost", api_scheme="foo" ) self.assertRaises(ApiValueError, configuration.is_valid) @@ -984,10 +1105,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): """ Test whether ApiValueError is raised if host is bad """ - configuration = {{packageName}}.Configuration( - api_host='/', - api_scheme='foo' - ) + configuration = {{packageName}}.Configuration(api_host="/", api_scheme="foo") self.assertRaises(ApiValueError, configuration.is_valid) def test_configuration_has_path(self): @@ -995,8 +1113,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has path """ configuration = {{packageName}}.Configuration( - api_host='localhost/mypath', - api_scheme='http' + api_host="localhost/mypath", api_scheme="http" ) self.assertRaises(ApiValueError, configuration.is_valid) @@ -1005,8 +1122,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = {{packageName}}.Configuration( - api_host='localhost?mypath=foo', - api_scheme='http' + api_host="localhost?mypath=foo", api_scheme="http" ) self.assertRaises(ApiValueError, configuration.is_valid) @@ -1015,9 +1131,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = {{packageName}}.Configuration( - api_host='localhost', - api_scheme='http', - store_id="abcd" + api_host="localhost", api_scheme="http", store_id="abcd" ) self.assertRaises(FgaValidationException, configuration.is_valid) @@ -1025,10 +1139,8 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): """ Ensure that api_url is set and validated """ - configuration = {{packageName}}.Configuration( - api_url='http://localhost:8080' - ) - self.assertEqual(configuration.api_url, 'http://localhost:8080') + configuration = {{packageName}}.Configuration(api_url="http://localhost:8080") + self.assertEqual(configuration.api_url, "http://localhost:8080") configuration.is_valid() def test_url_with_scheme_and_host(self): @@ -1036,34 +1148,32 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): Ensure that api_url takes precedence over api_host and scheme """ configuration = {{packageName}}.Configuration( - api_url='http://localhost:8080', - api_host='localhost:8080', - api_scheme='foo' + api_url="http://localhost:8080", api_host="localhost:8080", api_scheme="foo" ) - self.assertEqual(configuration.api_url, 'http://localhost:8080') - configuration.is_valid() # Should not throw and complain about scheme being invalid + self.assertEqual(configuration.api_url, "http://localhost:8080") + configuration.is_valid() # Should not throw and complain about scheme being invalid def test_timeout_millisec(self): """ Ensure that timeout_seconds is set and validated """ configuration = {{packageName}}.Configuration( - api_url='http://localhost:8080', + api_url="http://localhost:8080", timeout_millisec=10000, ) self.assertEqual(configuration.timeout_millisec, 10000) configuration.is_valid() - async def test_bad_configuration_read_authorization_model(self): + {{#asyncio}}async {{/asyncio}}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 = {{packageName}}.Configuration( - api_scheme = 'bad', - api_host = "api.{{sampleApiDomain}}", + api_scheme="bad", + api_host="api.{{sampleApiDomain}}", ) - configuration.store_id = 'xyz123' + configuration.store_id = "xyz123" # Enter a context with an instance of the API client {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: # Create an instance of the API class @@ -1072,18 +1182,17 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # expects FgaValidationException to be thrown because api_scheme is bad with self.assertRaises(ApiValueError): {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_models( - page_size= 1, - continuation_token= "abcdefg" + page_size=1, continuation_token="abcdefg" ) - async def test_configuration_missing_storeid(self): + {{#asyncio}}async {{/asyncio}}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 = {{packageName}}.Configuration( - api_scheme = 'http', - api_host = "api.{{sampleApiDomain}}", + api_scheme="http", + api_host="api.{{sampleApiDomain}}", ) # Notice the store_id is not set # Enter a context with an instance of the API client @@ -1094,22 +1203,23 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): # expects FgaValidationException to be thrown because store_id is not specified with self.assertRaises(FgaValidationException): {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_models( - page_size= 1, - continuation_token= "abcdefg" + page_size=1, continuation_token="abcdefg" ) - @patch.object(rest.RESTClientObject, 'request') - async def test_400_error(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_400_error(self, mock_request): """ Test to ensure 400 errors are handled properly """ - response_body = ''' + response_body = """ { "code": "validation_error", "message": "Generic validation error" } - ''' - mock_request.side_effect = ValidationException(http_resp=http_mock_response(response_body, 400)) + """ + mock_request.side_effect = ValidationException( + http_resp=http_mock_response(response_body, 400) + ) configuration = self.configuration configuration.store_id = store_id @@ -1126,24 +1236,35 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): {{#asyncio}}await {{/asyncio}}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) - + 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): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_404_error(self, mock_request): """ Test to ensure 404 errors are handled properly """ - response_body = ''' + response_body = """ { "code": "undefined_endpoint", "message": "Endpoint not enabled" } - ''' - mock_request.side_effect = NotFoundException(http_resp=http_mock_response(response_body, 404)) + """ + mock_request.side_effect = NotFoundException( + http_resp=http_mock_response(response_body, 404) + ) configuration = self.configuration configuration.store_id = store_id @@ -1160,23 +1281,33 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): {{#asyncio}}await {{/asyncio}}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") + 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): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}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 = ''' + response_body = """ { "code": "rate_limit_exceeded", "message": "Rate Limit exceeded" } - ''' - mock_request.side_effect = RateLimitExceededError(http_resp=http_mock_response(response_body, 429)) + """ + mock_request.side_effect = RateLimitExceededError( + http_resp=http_mock_response(response_body, 429) + ) retry = {{packageName}}.configuration.RetryParams(0, 10) configuration = self.configuration @@ -1199,20 +1330,25 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): 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): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}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 = ''' + 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)] + """ + 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 @@ -1235,19 +1371,20 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_request.assert_called() self.assertEqual(mock_request.call_count, 2) - - @patch.object(rest.RESTClientObject, 'request') - async def test_500_error(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_500_error(self, mock_request): """ Test to ensure 500 errors are handled properly """ - response_body = ''' + response_body = """ { "code": "internal_error", "message": "Internal Server Error" } - ''' - mock_request.side_effect = ServiceException(http_resp=http_mock_response(response_body, 500)) + """ + mock_request.side_effect = ServiceException( + http_resp=http_mock_response(response_body, 500) + ) configuration = self.configuration configuration.store_id = store_id @@ -1266,14 +1403,22 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): {{#asyncio}}await {{/asyncio}}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") + 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", + ) mock_request.assert_called() self.assertEqual(mock_request.call_count, 1) @patch.object(rest.RESTClientObject, "request") - async def test_500_error_retry(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_500_error_retry(self, mock_request): """ Test to ensure 5xxx retries are handled properly """ @@ -1291,12 +1436,12 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_response(response_body, 200), ] - retry = openfga_sdk.configuration.RetryParams(5, 10) + retry = {{packageName}}.configuration.RetryParams(5, 10) configuration = self.configuration configuration.store_id = store_id configuration.retry_params = retry - 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 = CheckRequest( tuple_key=TupleKey( @@ -1306,7 +1451,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), ) - api_response = await api_instance.check( + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) @@ -1315,7 +1460,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): self.assertEqual(mock_request.call_count, 5) @patch.object(rest.RESTClientObject, "request") - async def test_501_error_retry(self, mock_request): + {{#asyncio}}async {{/asyncio}}def test_501_error_retry(self, mock_request): """ Test to ensure 501 responses are not auto-retried """ @@ -1332,12 +1477,12 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): mock_response(response_body, 200), ] - retry = openfga_sdk.configuration.RetryParams(5, 10) + retry = {{packageName}}.configuration.RetryParams(5, 10) configuration = self.configuration configuration.store_id = store_id configuration.retry_params = retry - 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 = CheckRequest( tuple_key=TupleKey( @@ -1347,14 +1492,14 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ), ) with self.assertRaises(ServiceException) as api_exception: - await api_instance.check( + {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) mock_request.assert_called() self.assertEqual(mock_request.call_count, 1) - @patch.object(rest.RESTClientObject, 'request') - async def test_check_api_token(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}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 @@ -1366,7 +1511,10 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - configuration.credentials = Credentials(method='api_token', configuration=CredentialConfiguration(api_token='TOKEN1')) + configuration.credentials = Credentials( + method="api_token", + configuration=CredentialConfiguration(api_token="TOKEN1"), + ) {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: api_instance = open_fga_api.OpenFgaApi(api_client) body = CheckRequest( @@ -1382,20 +1530,33 @@ 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 - expected_headers = 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 python/0.9.0", + "Authorization": "Bearer TOKEN1", + } + ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + "POST", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check", headers=expected_headers, query_params=[], post_params=[], - body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - async def test_check_custom_header(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_check_custom_header(self, mock_request): """Test case for custom header Check whether custom header can be added @@ -1423,19 +1584,31 @@ 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 - expected_headers = 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 python/0.9.0", + "Custom Header": "custom value", + } + ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + "POST", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check", headers=expected_headers, query_params=[], post_params=[], - body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) -{{/operations}} -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/config/clients/python/template/test/client/client_test.py.mustache b/config/clients/python/template/test/client/client_test.py.mustache index 06c8d56f..a7996424 100644 --- a/config/clients/python/template/test/client/client_test.py.mustache +++ b/config/clients/python/template/test/client/client_test.py.mustache @@ -1,12 +1,11 @@ {{>partial_header}} -from unittest.mock import ANY -from unittest import IsolatedAsyncioTestCase -from unittest.mock import patch +import uuid from datetime import datetime +from unittest import IsolatedAsyncioTestCase +from unittest.mock import ANY, patch import urllib3 -import uuid from {{packageName}} import rest from {{packageName}}.client import ClientConfiguration @@ -25,7 +24,11 @@ from {{packageName}}.client.models.write_request import ClientWriteRequest from {{packageName}}.client.models.write_single_response import ClientWriteSingleResponse from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts from {{packageName}}.configuration import RetryParams -from {{packageName}}.exceptions import ValidationException, FgaValidationException, UnauthorizedException +from {{packageName}}.exceptions import ( + FgaValidationException, + UnauthorizedException, + ValidationException, +) from {{packageName}}.models.assertion import Assertion from {{packageName}}.models.authorization_model import AuthorizationModel from {{packageName}}.models.check_response import CheckResponse @@ -42,8 +45,12 @@ from {{packageName}}.models.list_users_response import ListUsersResponse 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_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 @@ -59,50 +66,53 @@ 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_authorization_model_request import WriteAuthorizationModelRequest -from {{packageName}}.models.write_authorization_model_response import WriteAuthorizationModelResponse +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" -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 - }) + 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 + 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_url='http://api.{{sampleApiDomain}}', + api_url="http://api.fga.example", ) def tearDown(self): pass - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_list_stores(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_list_stores(self, mock_request): """Test case for list_stores Get all stores """ - response_body = ''' + response_body = """ { "stores": [ { @@ -122,16 +132,21 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration - {{#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"} + async with OpenFgaClient(configuration) as api_client: + api_response = await 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==") + self.assertEqual( + api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) store1 = Store( id="01YCP46JKYM8FJCQ37NMBYHE5X", name="store1", @@ -150,115 +165,116 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): stores = [store1, store2] self.assertEqual(api_response.stores, stores) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores', + "GET", + "http://api.fga.example/stores", headers=ANY, - query_params=[('page_size', 1), ('continuation_token', - 'continuation_token_example')], + body=None, + query_params=[ + ("page_size", 1), + ("continuation_token", "continuation_token_example"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_create_store(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_create_store(self, mock_request): """Test case for create_store Create a store """ - response_body = '''{ + 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.create_store( - CreateStoreRequest(name="test-store"), - options={} + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.create_store( + CreateStoreRequest(name="test-store"), options={} ) self.assertIsInstance(api_response, CreateStoreResponse) - self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') + self.assertEqual(api_response.id, "01YCP46JKYM8FJCQ37NMBYHE5X") mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores', + "POST", + "http://api.fga.example/stores", headers=ANY, query_params=[], post_params=[], body={"name": "test-store"}, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() - + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_get_store(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_get_store(self, mock_request): """Test case for get_store Get all stores """ - response_body = ''' + 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.get_store( - options={} - ) + async with OpenFgaClient(configuration) as api_client: + api_response = await 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_delete_store(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_delete_store(self, mock_request): """Test case for delete_store Get all stores """ - mock_request.return_value = mock_response('', 201) + mock_request.return_value = mock_response("", 201) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - {{#asyncio}}await {{/asyncio}}api_client.delete_store( - options={} - ) + async with OpenFgaClient(configuration) as api_client: + await api_client.delete_store(options={}) mock_request.assert_called_once_with( - 'DELETE', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X', + "DELETE", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", headers=ANY, - query_params=[], body=None, + query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_authorization_models(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_read_authorization_models(self, mock_request): """Test case for read_authorization_models Return all authorization models configured for the store """ - response_body = ''' + response_body = """ { "authorization_models": [{ "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -291,17 +307,15 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }], "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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_client.read_authorization_models( - options={} - ) + api_response = await api_client.read_authorization_models(options={}) self.assertIsInstance(api_response, ReadAuthorizationModelsResponse) type_definitions = [ TypeDefinition( @@ -311,34 +325,44 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", - type_definitions=type_definitions) + 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==") + self.assertEqual( + api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_authorization_model(self, mock_request): + @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 @@ -347,7 +371,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: # example passing only required values which don't have defaults set body = WriteAuthorizationModelRequest( schema_version="1.1", @@ -362,47 +386,67 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), - ) + ), ), ], ) # Create a new authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_client.write_authorization_model( - body, - options={} - ) + api_response = await api_client.write_authorization_model(body, options={}) self.assertIsInstance(api_response, WriteAuthorizationModelResponse) expected_response = WriteAuthorizationModelResponse( - authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J" ) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "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"}}]}}}}]}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_authorization_model(self, mock_request): + @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 """ - response_body = ''' + response_body = """ { "authorization_model": { "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -434,15 +478,15 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] } } - ''' + """ 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_client.read_authorization_model( + api_response = await api_client.read_authorization_model( options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) @@ -454,38 +498,45 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", - type_definitions=type_definitions) + 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/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_latest_authorization_model(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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 """ - response_body = ''' + response_body = """ { "authorization_models": [{ "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -518,17 +569,15 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }], "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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_client.read_latest_authorization_model( - options={} - ) + api_response = await api_client.read_latest_authorization_model(options={}) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) type_definitions = [ TypeDefinition( @@ -538,72 +587,77 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", - type_definitions=type_definitions) + 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/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models", headers=ANY, - query_params=[('page_size', 1)], + body=None, + query_params=[("page_size", 1)], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_latest_authorization_model_with_no_models(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_read_latest_authorization_model_with_no_models(self, mock_request): """Test case for read_latest_authorization_model when no models are in the store Return the latest authorization models configured for the store """ - response_body = ''' + response_body = """ { "authorization_models": [] } - ''' + """ 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_client.read_latest_authorization_model( - options={} - ) + api_response = await api_client.read_latest_authorization_model(options={}) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) self.assertIsNone(api_response.authorization_model) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models", headers=ANY, - query_params=[('page_size', 1)], + body=None, + query_params=[("page_size", 1)], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_changes(self, mock_request): + @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 """ - response_body = ''' + response_body = """ { "changes": [ { @@ -618,46 +672,57 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_client.read_changes( - ClientReadChangesRequest("document","2022-01-01T00:00:00+00:00"), - options={"page_size":1, "continuation_token":"abcdefg"} + api_response = await api_client.read_changes( + ClientReadChangesRequest("document", "2022-01-01T00:00:00+00:00"), + 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"), + 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")) + timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00"), + ) read_changes = ReadChangesResponse( - continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', - changes=[changes]) + continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + changes=[changes], + ) self.assertEqual(api_response, read_changes) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes", headers=ANY, - query_params=[('type', 'document'), ('page_size', 1), - ('continuation_token', 'abcdefg'), ('start_time', '2022-01-01T00:00:00+00:00')], + body=None, + query_params=[ + ("type", "document"), + ("page_size", 1), + ("continuation_token", "abcdefg"), + ("start_time", "2022-01-01T00:00:00+00:00"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read(self, mock_request): + @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 """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -671,36 +736,40 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) - api_response = {{#asyncio}}await {{/asyncio}}api_client.read( + api_response = await api_client.read( body=body, options={ - "page_size":50, - "continuation_token":"eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + "page_size": 50, + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, "retry_params": RetryParams(max_retry=3, min_wait_in_ms=1000), - } + }, ) self.assertIsInstance(api_response, ReadResponse) - key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - relation="reader", object="document:2021-budget") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read", headers=ANY, query_params=[], post_params=[], @@ -708,24 +777,23 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "reader", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", }, "page_size": 50, "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_empty_options(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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 """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -739,47 +807,53 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ReadRequestTupleKey( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) - api_response = {{#asyncio}}await {{/asyncio}}api_client.read( - body=body, - options={} - ) + api_response = await 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") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + "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"}}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_empty_body(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_read_empty_body(self, mock_request): """Test case for read with empty body Get tuples from the store that matches a query, without following userset rewrite rules """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -793,162 +867,196 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ReadRequestTupleKey() - api_response = {{#asyncio}}await {{/asyncio}}api_client.read( - body=body, - options={} - ) + api_response = await 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") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read", headers=ANY, + body={}, query_params=[], post_params=[], - body={}, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_write(self, mock_request): """Test case for write Add tuples from the store with transaction enabled """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async 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", - ) - ], + 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", + ), + ], ) - {{#asyncio}}await {{/asyncio}}api_client.write( - body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + await api_client.write( + body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_delete(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_delete(self, mock_request): """Test case for delete Delete tuples from the store with transaction enabled """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( - deletes= - [ - ClientTuple( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) - ], + deletes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], ) - {{#asyncio}}await {{/asyncio}}api_client.write( - body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + await api_client.write( + body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_batch(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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), + mock_response("{}", 200), + mock_response("{}", 200), ] configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async 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", - ) - ], + 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 ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) - response = {{#asyncio}}await {{/asyncio}}api_client.write( + response = await api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) - self.assertEqual(response.writes, + self.assertEqual( + response.writes, [ ClientWriteSingleResponse( tuple_key=ClientTuple( @@ -957,7 +1065,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -965,7 +1074,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -973,84 +1083,123 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None) - ] + error=None, + ), + ], ) self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_batch_min_parallel(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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), + mock_response("{}", 200), + mock_response("{}", 200), ] configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async 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", - ) - ], + 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 = {{#asyncio}}await {{/asyncio}}api_client.write( + transaction = WriteTransactionOpts( + disabled=True, max_per_chunk=1, max_parallel_requests=1 + ) + response = await api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) - self.assertEqual(response.writes, + self.assertEqual( + response.writes, [ ClientWriteSingleResponse( tuple_key=ClientTuple( @@ -1059,7 +1208,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1067,7 +1217,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1075,84 +1226,122 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None) - ] + error=None, + ), + ], ) self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_batch_larger_chunk(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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), + mock_response("{}", 200), ] configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async 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", - ) - ], + 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 ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=2, max_parallel_requests=2) - response = {{#asyncio}}await {{/asyncio}}api_client.write( + response = await api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) - self.assertEqual(response.writes, + self.assertEqual( + response.writes, [ ClientWriteSingleResponse( tuple_key=ClientTuple( @@ -1161,7 +1350,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1169,7 +1359,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1177,82 +1368,115 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None) - ] + error=None, + ), + ], ) self.assertEqual(mock_request.call_count, 2) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_batch_failed(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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 = ''' + response_body = """ { "code": "validation_error", "message": "Generic validation error" } - ''' - - mock_request.side_effect = [ - mock_response('{}', 200), - ValidationException(http_resp=http_mock_response(response_body, 400)), - mock_response('{}', 200), - ] - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}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", - ) - ], + mock_request.side_effect = [ + mock_response("{}", 200), + ValidationException(http_resp=http_mock_response(response_body, 400)), + mock_response("{}", 200), + ] + configuration = self.configuration + configuration.store_id = store_id + async 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 = {{#asyncio}}await {{/asyncio}}api_client.write( + transaction = WriteTransactionOpts( + disabled=True, max_per_chunk=1, max_parallel_requests=10 + ) + response = await api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) self.assertEqual(len(response.writes), 3) - self.assertEqual(response.writes[0], + self.assertEqual( + response.writes[0], ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1260,17 +1484,25 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None)) - self.assertEqual(response.writes[1].tuple_key, + 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], + self.assertIsInstance( + response.writes[1].error.parsed_exception, + ValidationErrorMessageResponse, + ) + self.assertEqual( + response.writes[2], ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1278,92 +1510,142 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None)) + error=None, + ), + ) self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_delete_batch(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ClientWriteRequest( - deletes= - [ - ClientTuple( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) - ], + deletes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], writes=[], ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) - {{#asyncio}}await {{/asyncio}}api_client.write( + transaction = WriteTransactionOpts( + disabled=True, max_per_chunk=1, max_parallel_requests=10 + ) + await api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_tuples(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_write_tuples(self, mock_request): """Test case for write tuples Add tuples from the store with transaction enabled """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: - {{#asyncio}}await {{/asyncio}}api_client.write_tuples( + await api_client.write_tuples( [ ClientTuple( object="document:2021-budget", @@ -1379,34 +1661,55 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + ), ], - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_delete_tuples(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_delete_tuples(self, mock_request): """Test case for delete tuples Add tuples from the store with transaction enabled """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: - {{#asyncio}}await {{/asyncio}}api_client.delete_tuples( + await api_client.delete_tuples( [ ClientTuple( object="document:2021-budget", @@ -1422,32 +1725,52 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + ), ], - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_batch_unauthorized(self, mock_request): - """Test case for write with 401 response - """ + @patch.object(rest.RESTClientObject, "request") + async 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) + http_resp=http_mock_response("{}", 401) ) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: with self.assertRaises(UnauthorizedException): body = ClientWriteRequest( writes=[ @@ -1459,11 +1782,14 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) transaction = WriteTransactionOpts( - disabled=True, max_per_chunk=1, max_parallel_requests=10) - {{#asyncio}}await {{/asyncio}}api_client.write( + disabled=True, max_per_chunk=1, max_parallel_requests=10 + ) + await api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", - "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) mock_request.assert_called() @@ -1490,10 +1816,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _preload_content=ANY, _request_timeout=ANY, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_check(self, mock_request): + @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 @@ -1513,23 +1839,23 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:budget", ), ], - ) + ) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.check( + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.check( body=body, options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) 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/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -1537,14 +1863,14 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", "relation": "reader", - "object": "document:budget" + "object": "document:budget", }, "contextual_tuples": { "tuple_keys": [ { "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", "relation": "writer", - "object": "document:budget" + "object": "document:budget", } ] }, @@ -1552,12 +1878,12 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_check_config_auth_model(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async 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 @@ -1567,36 +1893,39 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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", - ) + 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" - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.check( - body=body, - options={} - ) + async with OpenFgaClient(configuration) as api_client: + api_response = await 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_client_batch_check_single_request(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_client_batch_check_single_request(self, mock_request): """Test case for check with single request Check whether a user is authorized to access an object @@ -1608,16 +1937,16 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response(response_body, 200), ] body = ClientCheckRequest( - 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-a4f8d36aa31b", + ) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.client_batch_check( + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.client_batch_check( body=[body], - options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, ) self.assertIsInstance(api_response, list) self.assertEqual(len(api_response), 1) @@ -1626,20 +1955,26 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response[0].request, body) # Make sure the API was called with the right data mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_client_batch_check_multiple_request(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_client_batch_check_multiple_request(self, mock_request): """Test case for check with multiple request Check whether a user is authorized to access an object @@ -1652,26 +1987,29 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{"allowed": true, "resolution": "1234"}', 200), ] body1 = ClientCheckRequest( - 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-a4f8d36aa31b", + ) body2 = ClientCheckRequest( - 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-a4f8d36aa31c", + ) body3 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.client_batch_check( + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.client_batch_check( body=[body1, body2, body3], - options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 2, + }, ) self.assertIsInstance(api_response, list) self.assertEqual(len(api_response), 3) @@ -1686,53 +2024,70 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(api_response[2].request, body3) # Make sure the API was called with the right data mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() - + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_client_batch_check_multiple_request_fail(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_client_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 """ - response_body = ''' + response_body = """ { "code": "validation_error", "message": "Generic validation error" } - ''' + """ # First, mock the response mock_request.side_effect = [ @@ -1741,26 +2096,29 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_response('{"allowed": false, "resolution": "1234"}', 200), ] body1 = ClientCheckRequest( - 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-a4f8d36aa31b", + ) body2 = ClientCheckRequest( - 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-a4f8d36aa31c", + ) body3 = ClientCheckRequest( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.client_batch_check( + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.client_batch_check( body=[body1, body2, body3], - options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "max_parallel_requests": 2} + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 2, + }, ) self.assertIsInstance(api_response, list) self.assertEqual(len(api_response), 3) @@ -1770,45 +2128,66 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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.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( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() + @patch.object(rest.RESTClientObject, "request") async def test_batch_check_single_request(self, mock_request): """Test case for check with single request @@ -1836,7 +2215,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - correlation_id='1' + correlation_id="1", ), ] ) @@ -1850,7 +2229,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): self.assertEqual(len(api_response.result), 1) self.assertEqual(api_response.result[0].error, None) self.assertTrue(api_response.result[0].allowed) - self.assertEqual(api_response.result[0].correlation_id, '1') + self.assertEqual(api_response.result[0].correlation_id, "1") self.assertEqual(api_response.result[0].request, body.checks[0]) # Make sure the API was called with the right data mock_request.assert_any_call( @@ -1909,7 +2288,6 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): } }""" - # First, mock the response mock_request.side_effect = [ mock_response(first_response_body, 200), @@ -1919,10 +2297,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): def mock_v4(val: str): return val - mock_uuid.side_effect = [ - mock_v4("batch-id-header"), - mock_v4("fake-uuid") - ] + mock_uuid.side_effect = [mock_v4("batch-id-header"), mock_v4("fake-uuid")] body = ClientBatchCheckRequest( checks=[ @@ -1930,19 +2305,19 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - correlation_id='1' + correlation_id="1", ), ClientBatchCheckItem( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", - correlation_id='2' + correlation_id="2", ), ClientBatchCheckItem( object="doc:2021-budget", relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31d" - ) + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), ] ) @@ -1967,7 +2342,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) self.assertFalse(api_response.result[2].allowed) # value generated from the uuid mock - self.assertEqual(api_response.result[2].correlation_id, 'fake-uuid') + self.assertEqual(api_response.result[2].correlation_id, "fake-uuid") # Make sure the API was called with the right data mock_request.assert_any_call( "POST", @@ -2024,8 +2399,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): await api_client.close() async def test_batch_check_errors_dupe_cor_id(self): - """Test case for duplicate correlation_id being provided to batch_check - """ + """Test case for duplicate correlation_id being provided to batch_check""" body = ClientBatchCheckRequest( checks=[ @@ -2033,13 +2407,13 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - correlation_id='1' + correlation_id="1", ), ClientBatchCheckItem( object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - correlation_id='1' + correlation_id="1", ), ] ) @@ -2052,8 +2426,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, ) self.assertEqual( - "Duplicate correlation_id (1) provided", - str(error.exception) + "Duplicate correlation_id (1) provided", str(error.exception) ) await api_client.close() @@ -2076,9 +2449,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ mock_response(first_response_body, 200), - UnauthorizedException( - http_resp=http_mock_response("{}", 401) - ), + UnauthorizedException(http_resp=http_mock_response("{}", 401)), ] body = ClientBatchCheckRequest( @@ -2099,7 +2470,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - correlation_id="3" + correlation_id="3", ), ] ) @@ -2172,29 +2543,29 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_expand(self, mock_request): + @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 """ - response_body = '''{ + 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 - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ClientExpandRequest( object="document:budget", relation="reader", ) - api_response = {{#asyncio}}await {{/asyncio}}api_client.expand( + api_response = await api_client.expand( body=body, options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertIsInstance(api_response, ExpandResponse) cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) @@ -2204,48 +2575,45 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): expected_response = ExpandResponse(userTree) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand", headers=ANY, query_params=[], post_params=[], body={ - "tuple_key": { - "object": "document:budget", - "relation": "reader" - }, + "tuple_key": {"object": "document:budget", "relation": "reader"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_list_objects(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_list_objects(self, mock_request): """Test case for list_objects List objects """ - response_body = ''' + response_body = """ { "objects": [ "document:abcd1234" ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ClientListObjectsRequest( type="document", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) # Get all stores - api_response = {{#asyncio}}await {{/asyncio}}api_client.list_objects( + api_response = await api_client.list_objects( body, options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", @@ -2253,10 +2621,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, ) self.assertIsInstance(api_response, ListObjectsResponse) - self.assertEqual(api_response.objects, ['document:abcd1234']) + self.assertEqual(api_response.objects, ["document:abcd1234"]) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects", headers=ANY, query_params=[], post_params=[], @@ -2268,28 +2636,27 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() - + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_list_objects_contextual_tuples(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_list_objects_contextual_tuples(self, mock_request): """Test case for list_objects List objects """ - response_body = ''' + response_body = """ { "objects": [ "document:abcd1234" ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ClientListObjectsRequest( type="document", relation="reader", @@ -2303,34 +2670,48 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) # Get all stores - api_response = {{#asyncio}}await {{/asyncio}}api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) + api_response = await api_client.list_objects( + body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + ) self.assertIsInstance(api_response, ListObjectsResponse) - self.assertEqual(api_response.objects, ['document:abcd1234']) + self.assertEqual(api_response.objects, ["document:abcd1234"]) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects', + "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'}]}}, + 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 + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_list_relations(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_list_relations(self, mock_request): """Test case for list relations Check whether a user is authorized to access an object """ def mock_check_requests(*args, **kwargs): - body = kwargs.get('body') - tuple_key = body.get('tuple_key') - if tuple_key['relation'] == "owner": + body = kwargs.get("body") + tuple_key = body.get("tuple_key") + if tuple_key["relation"] == "owner": return mock_response('{"allowed": false, "resolution": "1234"}', 200) return mock_response('{"allowed": true, "resolution": "1234"}', 200) @@ -2339,24 +2720,24 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: - api_response = {{#asyncio}}await {{/asyncio}}api_client.list_relations( + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.list_relations( body=ClientListRelationsRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relations=["reader", "owner", "viewer"], - object="document:2021-budget" + object="document:2021-budget", ), options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertEqual(api_response, ["reader", "viewer"]) # Make sure the API was called with the right data mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -2364,17 +2745,17 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "reader", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", }, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -2382,17 +2763,17 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "owner", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" - }, + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -2400,41 +2781,41 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "viewer", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", }, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() + await api_client.close() - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_list_relations_unauthorized(self, mock_request): - """Test case for list relations with 401 response - """ + @patch.object(rest.RESTClientObject, "request") + async 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) + http_resp=http_mock_response("{}", 401) ) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: with self.assertRaises(UnauthorizedException): - {{#asyncio}}await {{/asyncio}}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"} + await 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"}, ) mock_request.assert_called() - {{#asyncio}}await {{/asyncio}}api_client.close() - + await api_client.close() @patch.object(rest.RESTClientObject, "request") - {{#asyncio}}async {{/asyncio}}def test_list_users(self, mock_request): + async def test_list_users(self, mock_request): """ Test case for list_users """ @@ -2467,7 +2848,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: body = ClientListUsersRequest( object=FgaObject(type="document", id="2021-budget"), relation="can_read", @@ -2489,12 +2870,12 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) - response = {{#asyncio}}await {{/asyncio}}api_client.list_users( + response = await api_client.list_users( body, options={ "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertIsInstance(response, ListUsersResponse) @@ -2523,7 +2904,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.assert_called_once_with( "POST", - "http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-users", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-users", headers=ANY, query_params=[], post_params=[], @@ -2553,15 +2934,12 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): _request_timeout=None, ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_read_assertions(self, mock_request): - """Test case for read assertions + await api_client.close() - """ - response_body = ''' + @patch.object(rest.RESTClientObject, "request") + async def test_read_assertions(self, mock_request): + """Test case for read assertions""" + response_body = """ { "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "assertions": [ @@ -2575,143 +2953,222 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): } ] } - ''' + """ 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 - {{#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( - authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", - assertions=[Assertion( - tuple_key=TupleKeyWithoutCondition(object="document:2021-budget", relation="reader", - user="user:anne"), - expectation=True, - )] - )) + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.read_assertions( + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + self.assertEqual( + api_response, + ReadAssertionsResponse( + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + assertions=[ + Assertion( + tuple_key=TupleKeyWithoutCondition( + object="document:2021-budget", + relation="reader", + user="user:anne", + ), + expectation=True, + ) + ], + ), + ) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_write_assertions(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_write_assertions(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + mock_request.return_value = mock_response("", 204) configuration = self.configuration configuration.store_id = store_id - {{#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"} + async with OpenFgaClient(configuration) as api_client: + await 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "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}]}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_set_store_id(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_set_store_id(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + mock_request.return_value = mock_response("", 204) configuration = self.configuration configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: api_client.set_store_id("01YCP46JKYM8FJCQ37NMBYHE5Y") - {{#asyncio}}await {{/asyncio}}api_client.write_assertions( - [ClientAssertion(user="user:anne", relation="reader", - object="document:2021-budget", expectation=True)], - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + await 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5Y/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "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}]}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_config_auth_model(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_config_auth_model(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + mock_request.return_value = mock_response("", 204) configuration = self.configuration configuration.store_id = store_id configuration.authorization_model_id = "01G5JAVJ41T49E9TT3SKVS7X1J" - {{#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={} + async with OpenFgaClient(configuration) as api_client: + await 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" ) - self.assertEqual(api_client.get_authorization_model_id(), "01G5JAVJ41T49E9TT3SKVS7X1J") mock_request.assert_called_once_with( - 'PUT', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "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}]}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') - {{#asyncio}}async {{/asyncio}}def test_update_auth_model(self, mock_request): + @patch.object(rest.RESTClientObject, "request") + async def test_update_auth_model(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + mock_request.return_value = mock_response("", 204) configuration = self.configuration configuration.store_id = store_id configuration.authorization_model_id = "01G5JAVJ41T49E9TT3SKVS7X1J" - {{#asyncio}}async {{/asyncio}}with OpenFgaClient(configuration) as api_client: + async with OpenFgaClient(configuration) as api_client: api_client.set_authorization_model_id("01G5JAVJ41T49E9TT3SKVS7X2J") - {{#asyncio}}await {{/asyncio}}api_client.write_assertions( - [ClientAssertion(user="user:anne", relation="reader", - object="document:2021-budget", expectation=True)], - options={} + await 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X2J', + "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}]}, + 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 + _request_timeout=None, ) def test_configuration_store_id_invalid(self): @@ -2719,9 +3176,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = ClientConfiguration( - api_host='localhost', - api_scheme='http', - store_id="abcd" + api_host="localhost", api_scheme="http", store_id="abcd" ) self.assertRaises(FgaValidationException, configuration.is_valid) @@ -2730,9 +3185,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = ClientConfiguration( - api_host='localhost', - api_scheme='http', + api_host="localhost", + api_scheme="http", store_id="01H15K9J85050XTEDPVM8DJM78", - authorization_model_id="abcd" + authorization_model_id="abcd", ) self.assertRaises(FgaValidationException, configuration.is_valid) diff --git a/config/clients/python/template/test/oauth2_test.py.mustache b/config/clients/python/template/test/oauth2_test.py.mustache index ac83776e..0d5c7def 100644 --- a/config/clients/python/template/test/oauth2_test.py.mustache +++ b/config/clients/python/template/test/oauth2_test.py.mustache @@ -2,29 +2,26 @@ import urllib3 +from datetime import datetime, timedelta from unittest import IsolatedAsyncioTestCase from unittest.mock import patch -from datetime import datetime, timedelta -from {{packageName}}.oauth2 import OAuth2Client + +import urllib3 + from {{packageName}} import rest -from {{packageName}}.credentials import CredentialConfiguration, Credentials from {{packageName}}.configuration import Configuration +from {{packageName}}.credentials import CredentialConfiguration, Credentials from {{packageName}}.exceptions import AuthenticationError +from {{packageName}}.oauth2 import OAuth2Client # 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 - ) + 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""" @@ -39,59 +36,89 @@ class TestOAuth2Client(IsolatedAsyncioTestCase): Test getting authentication header when method is client credentials """ client = OAuth2Client(None) - client._access_token = 'XYZ123' + 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'}) + self.assertEqual(auth_header, {"Authorization": "Bearer XYZ123"}) - @patch.object(rest.RESTClientObject, 'request') + @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 = ''' + 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='issuer.{{sampleApiDomain}}', api_audience='myaudience')) + credentials = Credentials( + method="client_credentials", + configuration=CredentialConfiguration( + client_id="myclientid", + client_secret="mysecret", + api_issuer="issuer.fga.example", + 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/x-www-form-urlencoded', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) + 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/x-www-form-urlencoded", + "User-Agent": "openfga-sdk (python) 0.9.0", + } + ) mock_request.assert_called_once_with( - 'POST', - 'https://issuer.{{sampleApiDomain}}/oauth/token', + method="POST", + url="https://issuer.fga.example/oauth/token", headers=expected_header, - query_params=None, body=None, _preload_content=True, _request_timeout=None, - post_params={"client_id": "myclientid", "client_secret": "mysecret", "audience": "myaudience", "grant_type": "client_credentials"} + query_params=None, + body=None, + _preload_content=True, + _request_timeout=None, + post_params={ + "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): + @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 = ''' + 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='issuer.{{sampleApiDomain}}', api_audience='myaudience')) + credentials = Credentials( + method="client_credentials", + configuration=CredentialConfiguration( + client_id="myclientid", + client_secret="mysecret", + api_issuer="issuer.fga.example", + api_audience="myaudience", + ), + ) rest_client = rest.RESTClientObject(Configuration()) client = OAuth2Client(credentials) with self.assertRaises(AuthenticationError): @@ -118,7 +145,7 @@ class TestOAuth2Client(IsolatedAsyncioTestCase): configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) @@ -148,7 +175,7 @@ This is not a JSON response configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) @@ -177,7 +204,7 @@ This is not a JSON response configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) @@ -220,7 +247,7 @@ This is not a JSON response configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) diff --git a/config/clients/python/template/test/rest_test.py.mustache b/config/clients/python/template/test/rest_test.py.mustache new file mode 100644 index 00000000..803a02e9 --- /dev/null +++ b/config/clients/python/template/test/rest_test.py.mustache @@ -0,0 +1,401 @@ +{{>partial_header}} + +import json +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from {{packageName}}.exceptions import ( + ApiException, + ForbiddenException, + NotFoundException, + RateLimitExceededError, + ServiceException, + UnauthorizedException, + ValidationException, +) +from {{packageName}}.rest import RESTClientObject, RESTResponse + + +@pytest.mark.asyncio +async def test_restresponse_init(): + mock_resp = MagicMock() + mock_resp.status = 200 + mock_resp.reason = "OK" + resp_data = b'{"test":"data"}' + rest_resp = RESTResponse(mock_resp, resp_data) + assert rest_resp.status == 200 + assert rest_resp.reason == "OK" + assert rest_resp.data == resp_data + assert rest_resp.aiohttp_response == mock_resp + + +def test_restresponse_getheaders(): + mock_resp = MagicMock() + mock_resp.headers = {"Content-Type": "application/json", "X-Testing": "true"} + rest_resp = RESTResponse(mock_resp, b"") + headers = rest_resp.getheaders() + assert headers["Content-Type"] == "application/json" + assert headers["X-Testing"] == "true" + + +def test_restresponse_getheader(): + mock_resp = MagicMock() + mock_resp.headers = {"Content-Type": "application/json"} + rest_resp = RESTResponse(mock_resp, b"") + val = rest_resp.getheader("Content-Type") + missing = rest_resp.getheader("X-Not-Here", default="fallback") + assert val == "application/json" + assert missing == "fallback" + + +@pytest.mark.asyncio +async def test_build_request_json_body(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + req_args = await client.build_request( + method="POST", + url="http://example.com/test", + body={"foo": "bar"}, + headers={"Content-Type": "application/json"}, + ) + assert req_args["method"] == "POST" + assert req_args["url"] == "http://example.com/test" + assert req_args["headers"]["Content-Type"] == "application/json" + assert json.loads(req_args["data"]) == {"foo": "bar"} + + +@pytest.mark.asyncio +async def test_build_request_form_data(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + req_args = await client.build_request( + method="POST", + url="http://example.com/upload", + post_params=[("file", ("filename.txt", b"contents", "text/plain"))], + headers={"Content-Type": "multipart/form-data"}, + ) + assert req_args["method"] == "POST" + assert req_args["url"] == "http://example.com/upload" + assert "Content-Type" not in req_args["headers"] + assert "data" in req_args + + +@pytest.mark.asyncio +async def test_build_request_timeout(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + req_args = await client.build_request( + method="GET", + url="http://example.com", + _request_timeout=10.0, + ) + assert req_args["timeout"] == 10.0 + + +@pytest.mark.asyncio +async def test_handle_response_exception_success(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + mock_response = MagicMock() + mock_response.status = 200 + await client.handle_response_exception(mock_response) + + +@pytest.mark.parametrize( + "status, exc", + [ + (400, ValidationException), + (401, UnauthorizedException), + (403, ForbiddenException), + (404, NotFoundException), + (429, RateLimitExceededError), + (500, ServiceException), + (418, ApiException), + ], +) +@pytest.mark.asyncio +async def test_handle_response_exception_error(status, exc): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + mock_response = MagicMock() + mock_response.status = status + + with pytest.raises(exc): + await client.handle_response_exception(mock_response) + + +@pytest.mark.asyncio +async def test_close(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + + mock_session = MagicMock() + mock_session.close = AsyncMock() + client.pool_manager = mock_session + + await client.close() + + mock_session.close.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_request_preload_content(): + # Mock config + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + + mock_session = MagicMock() + client.pool_manager = mock_session + + mock_raw_response = MagicMock() + mock_raw_response.status = 200 + mock_raw_response.reason = "OK" + mock_raw_response.read = AsyncMock(return_value=b'{"some":"data"}') + mock_session.request = AsyncMock(return_value=mock_raw_response) + + resp = await client.request( + method="GET", url="http://example.com", _preload_content=True + ) + + mock_session.request.assert_awaited_once() + mock_raw_response.read.assert_awaited_once() + + assert resp.status == 200 + assert resp.reason == "OK" + assert resp.data == b'{"some":"data"}' + + +@pytest.mark.asyncio +async def test_request_no_preload_content(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + + mock_session = MagicMock() + client.pool_manager = mock_session + + mock_raw_response = MagicMock() + mock_raw_response.status = 200 + mock_raw_response.reason = "OK" + mock_raw_response.read = AsyncMock(return_value=b"unused") + mock_session.request = AsyncMock(return_value=mock_raw_response) + + resp = await client.request( + method="GET", url="http://example.com", _preload_content=False + ) + + mock_session.request.assert_awaited_once() + + assert resp == mock_raw_response + assert resp.status == 200 + assert resp.reason == "OK" + + +@pytest.mark.asyncio +async def test_stream_happy_path(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + mock_session = MagicMock() + client.pool_manager = mock_session + + class FakeContent: + async def iter_chunks(self): + yield (b'{"foo":"bar"}\n{"hello":"world"}', None) + + mock_response = MagicMock() + mock_response.status = 200 + mock_response.reason = "OK" + mock_response.data = None + mock_response.content = FakeContent() + + mock_context_manager = AsyncMock() + mock_context_manager.__aenter__.return_value = mock_response + mock_context_manager.__aexit__.return_value = None + + mock_session.request.return_value = mock_context_manager + + client.handle_response_exception = AsyncMock() + client.close = AsyncMock() + + results = [] + async for item in client.stream("GET", "http://example.com"): + results.append(item) + + assert results == [{"foo": "bar"}, {"hello": "world"}] + + client.handle_response_exception.assert_awaited_once() + mock_response.release.assert_called_once() + client.close.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_stream_exception_in_chunks(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + mock_session = MagicMock() + client.pool_manager = mock_session + + class FakeContent: + async def iter_chunks(self): + raise ValueError("Boom!") + + mock_response = MagicMock() + mock_response.status = 200 + mock_response.reason = "OK" + mock_response.data = None + mock_response.content = FakeContent() + + mock_context_manager = AsyncMock() + mock_context_manager.__aenter__.return_value = mock_response + mock_context_manager.__aexit__.return_value = None + + mock_session.request.return_value = mock_context_manager + + client.handle_response_exception = AsyncMock() + client.close = AsyncMock() + + results = [] + async for item in client.stream("GET", "http://example.com"): + results.append(item) + + assert results == [] + client.handle_response_exception.assert_awaited_once() + mock_response.release.assert_called_once() + client.close.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_stream_partial_chunks(): + mock_config = MagicMock() + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.proxy = None + mock_config.proxy_headers = None + mock_config.timeout_millisec = 5000 + + client = RESTClientObject(configuration=mock_config) + mock_session = MagicMock() + client.pool_manager = mock_session + + class FakeContent: + async def iter_chunks(self): + yield (b'{"foo":"b', None) + yield (b'ar"}\n{"hello":"world"}', None) + + mock_response = MagicMock() + mock_response.status = 200 + mock_response.reason = "OK" + mock_response.data = None + mock_response.content = FakeContent() + + mock_context_manager = AsyncMock() + mock_context_manager.__aenter__.return_value = mock_response + mock_context_manager.__aexit__.return_value = None + + mock_session.request.return_value = mock_context_manager + + client.handle_response_exception = AsyncMock() + client.close = AsyncMock() + + results = [] + async for item in client.stream("GET", "http://example.com"): + results.append(item) + + assert results == [{"foo": "bar"}, {"hello": "world"}] + + client.handle_response_exception.assert_awaited_once() + mock_response.release.assert_called_once() + client.close.assert_awaited_once() diff --git a/config/clients/python/template/test/sync/api_test.py.mustache b/config/clients/python/template/test/sync/api_test.py.mustache index b77dde57..99286132 100644 --- a/config/clients/python/template/test/sync/api_test.py.mustache +++ b/config/clients/python/template/test/sync/api_test.py.mustache @@ -1,19 +1,24 @@ {{>partial_header}} import unittest -from unittest.mock import ANY -from unittest import IsolatedAsyncioTestCase -from unittest.mock import patch from datetime import datetime +from unittest import IsolatedAsyncioTestCase +from unittest.mock import ANY, patch 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}}.credentials import CredentialConfiguration, Credentials +from {{packageName}}.exceptions import ( + FGA_REQUEST_ID, + ApiValueError, + FgaValidationException, + NotFoundException, + RateLimitExceededError, + ServiceException, + ValidationException, +) from {{packageName}}.models.assertion import Assertion from {{packageName}}.models.authorization_model import AuthorizationModel from {{packageName}}.models.check_request import CheckRequest @@ -26,7 +31,9 @@ from {{packageName}}.models.expand_request_tuple_key import ExpandRequestTupleKe 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.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 @@ -36,9 +43,13 @@ from {{packageName}}.models.list_users_response import ListUsersResponse 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.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_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 @@ -48,52 +59,59 @@ 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.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 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.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_authorization_model_request import ( + WriteAuthorizationModelRequest, +) +from {{packageName}}.models.write_authorization_model_response import ( + WriteAuthorizationModelResponse, +) from {{packageName}}.models.write_request import WriteRequest +from {{packageName}}.models.write_request_deletes import WriteRequestDeletes +from {{packageName}}.models.write_request_writes import WriteRequestWrites +from {{packageName}}.sync import open_fga_api, rest +from {{packageName}}.sync.api_client import ApiClient + +store_id = "01H0H015178Y2V4CX10C2KGHF4" +request_id = "x1y2z3" -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 - }) + 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 + 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""" + """{{packageName}}.sync.OpenFgaApi unit test stubs""" def setUp(self): self.configuration = Configuration( - api_url='http://api.{{sampleApiDomain}}', + api_url="http://api.fga.example", ) def tearDown(self): pass - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_check(self, mock_request): """Test case for check @@ -123,30 +141,36 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): 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', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_create_store(self, mock_request): """Test case for create_store Create a store """ - response_body = '''{ + 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 @@ -159,26 +183,26 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): body=body, ) self.assertIsInstance(api_response, CreateStoreResponse) - self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') + self.assertEqual(api_response.id, "01YCP46JKYM8FJCQ37NMBYHE5X") mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores', + "POST", + "http://api.fga.example/stores", headers=ANY, query_params=[], post_params=[], body={"name": "test-store"}, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_delete_store(self, mock_request): """Test case for delete_store Delete a store """ - response_body = '' + response_body = "" mock_request.return_value = mock_response(response_body, 201) configuration = self.configuration configuration.store_id = store_id @@ -186,25 +210,26 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): api_instance = open_fga_api.OpenFgaApi(api_client) api_instance.delete_store() mock_request.assert_called_once_with( - 'DELETE', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4', + "DELETE", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4", headers=ANY, query_params=[], + post_params=[], body=None, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = '''{ + 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 @@ -228,30 +253,33 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): expected_response = ExpandResponse(userTree) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/expand', + "POST", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/expand", headers=ANY, query_params=[], post_params=[], - body={"tuple_key": {"object": "document:budget", "relation": "reader"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + body={ + "tuple_key": {"object": "document:budget", "relation": "reader"}, + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_get_store(self, mock_request): """Test case for get_store Get a store """ - response_body = '''{ + 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 @@ -260,31 +288,33 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): # 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') + 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', + "GET", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_list_objects(self, mock_request): """Test case for list_objects List objects """ - response_body = ''' + response_body = """ { "objects": [ "document:abcd1234" ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -299,27 +329,31 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): # Get all stores api_response = api_instance.list_objects(body) self.assertIsInstance(api_response, ListObjectsResponse) - self.assertEqual(api_response.objects, ['document:abcd1234']) + self.assertEqual(api_response.objects, ["document:abcd1234"]) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-objects', + "POST", + "http://api.fga.example/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'}, + body={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "type": "document", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_list_stores(self, mock_request): """Test case for list_stores Get all stores """ - response_body = ''' + response_body = """ { "stores": [ { @@ -339,7 +373,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): ], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration with ApiClient(configuration) as api_client: @@ -350,8 +384,10 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): continuation_token="continuation_token_example", ) self.assertIsInstance(api_response, ListStoresResponse) - self.assertEqual(api_response.continuation_token, - "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") + self.assertEqual( + api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) store1 = Store( id="01YCP46JKYM8FJCQ37NMBYHE5X", name="store1", @@ -370,19 +406,20 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): stores = [store1, store2] self.assertEqual(api_response.stores, stores) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores', + "GET", + "http://api.fga.example/stores", headers=ANY, - query_params=[('page_size', 1), ('continuation_token', - 'continuation_token_example')], + body=None, + query_params=[ + ("page_size", 1), + ("continuation_token", "continuation_token_example"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - - - @patch.object(rest.RESTClientObject, "request") async def test_list_users(self, mock_request): """ @@ -473,7 +510,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): mock_request.assert_called_once_with( "POST", - "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-users", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/list-users", headers=ANY, query_params=[], post_params=[], @@ -504,14 +541,13 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): api_client.close() - - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -525,7 +561,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -544,29 +580,42 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): body=body, ) self.assertIsInstance(api_response, ReadResponse) - key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",relation="reader",object="document:2021-budget") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/read', + "POST", + "http://api.fga.example/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=="}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "assertions": [ @@ -580,7 +629,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): } ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -590,8 +639,10 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): "01G5JAVJ41T49E9TT3SKVS7X1J", ) self.assertIsInstance(api_response, ReadAssertionsResponse) - self.assertEqual(api_response.authorization_model_id, '01G5JAVJ41T49E9TT3SKVS7X1J') - assertion=Assertion( + self.assertEqual( + api_response.authorization_model_id, "01G5JAVJ41T49E9TT3SKVS7X1J" + ) + assertion = Assertion( tuple_key=TupleKeyWithoutCondition( object="document:2021-budget", relation="reader", @@ -601,21 +652,23 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): ) self.assertEqual(api_response.assertions, [assertion]) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "GET", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "authorization_model": { "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -647,7 +700,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): ] } } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -669,38 +722,45 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), - writer=Userset( + writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version = "1.1", - type_definitions=type_definitions) + 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', + "GET", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "changes": [ { @@ -715,7 +775,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): ], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -729,33 +789,46 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): page_size=1, continuation_token="abcdefg", start_time="2022-01-01T00:00:00+00:00", - type="document" + 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"), + 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")) + timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00"), + ) read_changes = ReadChangesResponse( - continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', - changes=[changes]) + continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + changes=[changes], + ) self.assertEqual(api_response, read_changes) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/changes', + "GET", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/changes", headers=ANY, - query_params=[('type', 'document'), ('page_size', 1), ('continuation_token', 'abcdefg'), ('start_time', '2022-01-01T00:00:00+00:00')], + body=None, + query_params=[ + ("type", "document"), + ("page_size", 1), + ("continuation_token", "abcdefg"), + ("start_time", "2022-01-01T00:00:00+00:00"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_write(self, mock_request): """Test case for write Add tuples from the store """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -782,23 +855,34 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): body, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_write_delete(self, mock_request): """Test case for write Delete tuples from the store """ - response_body = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -825,23 +909,34 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): body, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = '' + response_body = "" mock_request.return_value = mock_response(response_body, 204) configuration = self.configuration configuration.store_id = store_id @@ -869,17 +964,28 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): body=body, ) mock_request.assert_called_once_with( - 'PUT', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/xyz0123', + "PUT", + "http://api.fga.example/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"}}]}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_write_authorization_model(self, mock_request): """Test case for write_authorization_model @@ -895,83 +1001,98 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): # example passing only required values which don't have defaults set body = WriteAuthorizationModelRequest( - schema_version = "1.1", + schema_version="1.1", type_definitions=[ TypeDefinition( type="document", relations=dict( - writer=Userset( + writer=Userset( this=dict(), ), reader=Userset( union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), - ) + ), ), ], ) # Create a new authorization model - api_response = api_instance.write_authorization_model( - body - ) + api_response = api_instance.write_authorization_model(body) self.assertIsInstance(api_response, WriteAuthorizationModelResponse) expected_response = WriteAuthorizationModelResponse( - authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J" ) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models', + "POST", + "http://api.fga.example/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"}}]}}}}]}, + 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 + _request_timeout=None, ) def test_default_scheme(self): """ Ensure default scheme is https """ - configuration = Configuration( - api_host='localhost' - ) - self.assertEqual(configuration.api_scheme, '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') + 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' - ) + 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 = Configuration(api_host="localhost") configuration.api_scheme = None self.assertRaises(FgaValidationException, configuration.is_valid) @@ -979,30 +1100,21 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): """ Test whether ApiValueError is raised if scheme is bad """ - configuration = Configuration( - api_host='localhost', - api_scheme='foo' - ) + 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' - ) + 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' - ) + configuration = Configuration(api_host="localhost/mypath", api_scheme="http") self.assertRaises(ApiValueError, configuration.is_valid) def test_configuration_has_query(self): @@ -1010,8 +1122,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = Configuration( - api_host='localhost?mypath=foo', - api_scheme='http' + api_host="localhost?mypath=foo", api_scheme="http" ) self.assertRaises(ApiValueError, configuration.is_valid) @@ -1020,9 +1131,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = Configuration( - api_host='localhost', - api_scheme='http', - store_id="abcd" + api_host="localhost", api_scheme="http", store_id="abcd" ) self.assertRaises(FgaValidationException, configuration.is_valid) @@ -1030,10 +1139,8 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): """ Ensure that api_url is set and validated """ - configuration = Configuration( - api_url='http://localhost:8080' - ) - self.assertEqual(configuration.api_url, 'http://localhost:8080') + configuration = Configuration(api_url="http://localhost:8080") + self.assertEqual(configuration.api_url, "http://localhost:8080") configuration.is_valid() def test_url_with_scheme_and_host(self): @@ -1041,19 +1148,17 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): Ensure that api_url takes precedence over api_host and scheme """ configuration = Configuration( - api_url='http://localhost:8080', - api_host='localhost:8080', - api_scheme='foo' + api_url="http://localhost:8080", api_host="localhost:8080", api_scheme="foo" ) - self.assertEqual(configuration.api_url, 'http://localhost:8080') - configuration.is_valid() # Should not throw and complain about scheme being invalid + self.assertEqual(configuration.api_url, "http://localhost:8080") + configuration.is_valid() # Should not throw and complain about scheme being invalid def test_timeout_millisec(self): """ Ensure that timeout_millisec is set and validated """ configuration = Configuration( - api_url='http://localhost:8080', + api_url="http://localhost:8080", timeout_millisec=10000, ) self.assertEqual(configuration.timeout_millisec, 10000) @@ -1065,10 +1170,10 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): with configuration is having incorrect API scheme """ configuration = Configuration( - api_scheme = 'bad', - api_host = "api.{{sampleApiDomain}}", + api_scheme="bad", + api_host="api.fga.example", ) - configuration.store_id = 'xyz123' + 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 @@ -1077,8 +1182,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): # 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" + page_size=1, continuation_token="abcdefg" ) async def test_configuration_missing_storeid(self): @@ -1087,8 +1191,8 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): required store ID but configuration is missing store ID """ configuration = Configuration( - api_scheme = 'http', - api_host = "api.{{sampleApiDomain}}", + api_scheme="http", + api_host="api.fga.example", ) # Notice the store_id is not set # Enter a context with an instance of the API client @@ -1099,22 +1203,23 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): # 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" + page_size=1, continuation_token="abcdefg" ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_400_error(self, mock_request): """ Test to ensure 400 errors are handled properly """ - response_body = ''' + response_body = """ { "code": "validation_error", "message": "Generic validation error" } - ''' - mock_request.side_effect = ValidationException(http_resp=http_mock_response(response_body, 400)) + """ + mock_request.side_effect = ValidationException( + http_resp=http_mock_response(response_body, 400) + ) configuration = self.configuration configuration.store_id = store_id @@ -1131,24 +1236,35 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): 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) - + 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') + @patch.object(rest.RESTClientObject, "request") async def test_404_error(self, mock_request): """ Test to ensure 404 errors are handled properly """ - response_body = ''' + response_body = """ { "code": "undefined_endpoint", "message": "Endpoint not enabled" } - ''' - mock_request.side_effect = NotFoundException(http_resp=http_mock_response(response_body, 404)) + """ + mock_request.side_effect = NotFoundException( + http_resp=http_mock_response(response_body, 404) + ) configuration = self.configuration configuration.store_id = store_id @@ -1165,23 +1281,33 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): 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") + 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') + @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 = ''' + response_body = """ { "code": "rate_limit_exceeded", "message": "Rate Limit exceeded" } - ''' - mock_request.side_effect = RateLimitExceededError(http_resp=http_mock_response(response_body, 429)) + """ + mock_request.side_effect = RateLimitExceededError( + http_resp=http_mock_response(response_body, 429) + ) retry = {{packageName}}.configuration.RetryParams(0, 10) configuration = self.configuration @@ -1204,20 +1330,25 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): mock_request.assert_called() self.assertEqual(mock_request.call_count, 1) - @patch.object(rest.RESTClientObject, 'request') + @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 = ''' + 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)] + """ + 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 @@ -1240,19 +1371,20 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): mock_request.assert_called() self.assertEqual(mock_request.call_count, 2) - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_500_error(self, mock_request): """ Test to ensure 500 errors are handled properly """ - response_body = ''' + response_body = """ { "code": "internal_error", "message": "Internal Server Error" } - ''' - mock_request.side_effect = ServiceException(http_resp=http_mock_response(response_body, 500)) + """ + mock_request.side_effect = ServiceException( + http_resp=http_mock_response(response_body, 500) + ) configuration = self.configuration configuration.store_id = store_id @@ -1271,9 +1403,17 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): 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") + 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", + ) mock_request.assert_called() self.assertEqual(mock_request.call_count, 1) @@ -1296,7 +1436,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): mock_response(response_body, 200), ] - retry = openfga_sdk.configuration.RetryParams(5, 10) + retry = {{packageName}}.configuration.RetryParams(5, 10) configuration = self.configuration configuration.store_id = store_id configuration.retry_params = retry @@ -1337,7 +1477,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): mock_response(response_body, 200), ] - retry = openfga_sdk.configuration.RetryParams(5, 10) + retry = {{packageName}}.configuration.RetryParams(5, 10) configuration = self.configuration configuration.store_id = store_id configuration.retry_params = retry @@ -1358,7 +1498,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): mock_request.assert_called() self.assertEqual(mock_request.call_count, 1) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_check_api_token(self, mock_request): """Test case for API token @@ -1371,7 +1511,10 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): configuration = self.configuration configuration.store_id = store_id - configuration.credentials = Credentials(method='api_token', configuration=CredentialConfiguration(api_token='TOKEN1')) + 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( @@ -1387,19 +1530,32 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): self.assertIsInstance(api_response, CheckResponse) self.assertTrue(api_response.allowed) # Make sure the API was called with the right data - expected_headers = 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 python/0.9.0", + "Authorization": "Bearer TOKEN1", + } + ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + "POST", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/check", headers=expected_headers, query_params=[], post_params=[], - body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") async def test_check_custom_header(self, mock_request): """Test case for custom header @@ -1428,17 +1584,31 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): self.assertIsInstance(api_response, CheckResponse) self.assertTrue(api_response.allowed) # Make sure the API was called with the right data - expected_headers = 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 python/0.9.0", + "Custom Header": "custom value", + } + ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + "POST", + "http://api.fga.example/stores/01H0H015178Y2V4CX10C2KGHF4/check", headers=expected_headers, query_params=[], post_params=[], - body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/config/clients/python/template/test/sync/client/client_test.py.mustache b/config/clients/python/template/test/sync/client/client_test.py.mustache index 9d6e934a..b5b01852 100644 --- a/config/clients/python/template/test/sync/client/client_test.py.mustache +++ b/config/clients/python/template/test/sync/client/client_test.py.mustache @@ -1,12 +1,11 @@ {{>partial_header}} -from unittest.mock import ANY -from unittest import IsolatedAsyncioTestCase -from unittest.mock import patch +import uuid from datetime import datetime +from unittest import IsolatedAsyncioTestCase +from unittest.mock import ANY, patch import urllib3 -import uuid from {{packageName}}.client import ClientConfiguration from {{packageName}}.client.models.assertion import ClientAssertion @@ -22,11 +21,15 @@ from {{packageName}}.client.models.tuple import ClientTuple from {{packageName}}.client.models.write_request import ClientWriteRequest 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}}.exceptions import ( + FgaValidationException, + UnauthorizedException, + ValidationException, +) from {{packageName}}.models.assertion import Assertion from {{packageName}}.models.authorization_model import AuthorizationModel from {{packageName}}.models.check_response import CheckResponse -from openfga_sdk.models.consistency_preference import ConsistencyPreference +from {{packageName}}.models.consistency_preference import ConsistencyPreference from {{packageName}}.models.create_store_request import CreateStoreRequest from {{packageName}}.models.create_store_response import CreateStoreResponse from {{packageName}}.models.expand_response import ExpandResponse @@ -39,8 +42,12 @@ from {{packageName}}.models.list_users_response import ListUsersResponse 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_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 @@ -56,52 +63,55 @@ 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_authorization_model_request import WriteAuthorizationModelRequest -from {{packageName}}.models.write_authorization_model_response import WriteAuthorizationModelResponse +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, +) from {{packageName}}.sync import rest from {{packageName}}.sync.client.client import OpenFgaClient +store_id = "01YCP46JKYM8FJCQ37NMBYHE5X" +request_id = "x1y2z3" -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 - }) + 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 + 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_url='http://api.{{sampleApiDomain}}', + api_url="http://api.fga.example", ) def tearDown(self): pass - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_list_stores(self, mock_request): """Test case for list_stores Get all stores """ - response_body = ''' + response_body = """ { "stores": [ { @@ -121,16 +131,21 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "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"} + options={ + "page_size": 1, + "continuation_token": "continuation_token_example", + } ) self.assertIsInstance(api_response, ListStoresResponse) - self.assertEqual(api_response.continuation_token, - "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") + self.assertEqual( + api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) store1 = Store( id="01YCP46JKYM8FJCQ37NMBYHE5X", name="store1", @@ -149,115 +164,116 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): stores = [store1, store2] self.assertEqual(api_response.stores, stores) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores', + "GET", + "http://api.fga.example/stores", headers=ANY, - query_params=[('page_size', 1), ('continuation_token', - 'continuation_token_example')], + body=None, + query_params=[ + ("page_size", 1), + ("continuation_token", "continuation_token_example"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_create_store(self, mock_request): """Test case for create_store Create a store """ - response_body = '''{ + 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={} + CreateStoreRequest(name="test-store"), options={} ) self.assertIsInstance(api_response, CreateStoreResponse) - self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') + self.assertEqual(api_response.id, "01YCP46JKYM8FJCQ37NMBYHE5X") mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores', + "POST", + "http://api.fga.example/stores", headers=ANY, query_params=[], post_params=[], body={"name": "test-store"}, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_get_store(self, mock_request): """Test case for get_store Get all stores """ - response_body = ''' + 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={} - ) + 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_delete_store(self, mock_request): """Test case for delete_store Get all stores """ - mock_request.return_value = mock_response('', 201) + 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={} - ) + api_client.delete_store(options={}) mock_request.assert_called_once_with( - 'DELETE', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X', + "DELETE", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X", headers=ANY, - query_params=[], body=None, + query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "authorization_models": [{ "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -290,7 +306,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -298,9 +314,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = api_client.read_authorization_models( - options={} - ) + api_response = api_client.read_authorization_models(options={}) self.assertIsInstance(api_response, ReadAuthorizationModelsResponse) type_definitions = [ TypeDefinition( @@ -310,33 +324,43 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", - type_definitions=type_definitions) + 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==") + self.assertEqual( + api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_write_authorization_model(self, mock_request): """Test case for write_authorization_model @@ -361,47 +385,67 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), - ) + ), ), ], ) # Create a new authorization model - api_response = api_client.write_authorization_model( - body, - options={} - ) + api_response = api_client.write_authorization_model(body, options={}) self.assertIsInstance(api_response, WriteAuthorizationModelResponse) expected_response = WriteAuthorizationModelResponse( - authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J" ) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "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"}}]}}}}]}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "authorization_model": { "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -433,7 +477,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ] } } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -453,38 +497,45 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", - type_definitions=type_definitions) + 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/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "authorization_models": [{ "id": "01G5JAVJ41T49E9TT3SKVS7X1J", @@ -517,7 +568,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -525,9 +576,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = api_client.read_latest_authorization_model( - options={} - ) + api_response = api_client.read_latest_authorization_model(options={}) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) type_definitions = [ TypeDefinition( @@ -537,43 +586,49 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): union=Usersets( child=[ Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), + Userset( + computed_userset=ObjectRelation( + object="", + relation="writer", + ) + ), ], ), ), writer=Userset( this=dict(), ), - ) + ), ) ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version="1.1", - type_definitions=type_definitions) + 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/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models", headers=ANY, - query_params=[('page_size', 1)], + body=None, + query_params=[("page_size", 1)], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_read_latest_authorization_model_with_no_models(self, mock_request): """Test case for read_latest_authorization_model when no models are in the store Return the latest authorization models configured for the store """ - response_body = ''' + response_body = """ { "authorization_models": [] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -581,27 +636,27 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): with OpenFgaClient(configuration) as api_client: # Return a particular version of an authorization model - api_response = api_client.read_latest_authorization_model( - options={} - ) + api_response = api_client.read_latest_authorization_model(options={}) self.assertIsInstance(api_response, ReadAuthorizationModelResponse) self.assertIsNone(api_response.authorization_model) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models", headers=ANY, - query_params=[('page_size', 1)], + body=None, + query_params=[("page_size", 1)], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "changes": [ { @@ -616,7 +671,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -625,37 +680,48 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # Return a particular version of an authorization model api_response = api_client.read_changes( - ClientReadChangesRequest("document","2022-01-01T00:00:00+00:00"), - options={"page_size":1, "continuation_token":"abcdefg"} + ClientReadChangesRequest("document", "2022-01-01T00:00:00+00:00"), + 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"), + 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")) + timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00"), + ) read_changes = ReadChangesResponse( - continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', - changes=[changes]) + continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + changes=[changes], + ) self.assertEqual(api_response, read_changes) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes", headers=ANY, - query_params=[('type', 'document'), ('page_size', 1), - ('continuation_token', 'abcdefg'), ('start_time', '2022-01-01T00:00:00+00:00')], + body=None, + query_params=[ + ("type", "document"), + ("page_size", 1), + ("continuation_token", "abcdefg"), + ("start_time", "2022-01-01T00:00:00+00:00"), + ], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -669,7 +735,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -683,21 +749,25 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): api_response = api_client.read( body=body, options={ - "page_size":50, - "continuation_token":"eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + "page_size": 50, + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertIsInstance(api_response, ReadResponse) - key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - relation="reader", object="document:2021-budget") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read", headers=ANY, query_params=[], post_params=[], @@ -705,24 +775,23 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "reader", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", }, "page_size": 50, "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -736,7 +805,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -747,36 +816,42 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ) - api_response = api_client.read( - body=body, - options={} - ) + 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") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + "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"}}, + body={ + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + } + }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_read_empty_body(self, mock_request): """Test case for read with empty body Get tuples from the store that matches a query, without following userset rewrite rules """ - response_body = ''' + response_body = """ { "tuples": [ { @@ -790,162 +865,196 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], "continuation_token": "" } - ''' + """ 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 = ReadRequestTupleKey() - api_response = api_client.read( - body=body, - options={} - ) + 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") + 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)], continuation_token='') + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token="" + ) self.assertEqual(api_response, expected_data) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read", headers=ANY, query_params=[], post_params=[], body={}, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 = '{}' + 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", - ) - ], + 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"} + body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 = '{}' + 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", - ) - ], + deletes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], ) api_client.write( - body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + body, options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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), + 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", - ) - ], + 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 ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) response = api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) - self.assertEqual(response.writes, + self.assertEqual( + response.writes, [ ClientWriteSingleResponse( tuple_key=ClientTuple( @@ -954,7 +1063,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -962,7 +1072,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -970,84 +1081,123 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None) - ] + error=None, + ), + ], ) self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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), + 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", - ) - ], + 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 ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=1) response = api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) - self.assertEqual(response.writes, + self.assertEqual( + response.writes, [ ClientWriteSingleResponse( tuple_key=ClientTuple( @@ -1056,7 +1206,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1064,7 +1215,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1072,85 +1224,123 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None) - ] + error=None, + ), + ], ) self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') + @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), + 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", - ) - ], + 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 ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=2, max_parallel_requests=2) response = api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) - self.assertEqual(response.writes, + self.assertEqual( + response.writes, [ ClientWriteSingleResponse( tuple_key=ClientTuple( @@ -1159,7 +1349,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1167,7 +1358,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", ), success=True, - error=None), + error=None, + ), ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1175,82 +1367,115 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None) - ] + error=None, + ), + ], ) self.assertEqual(mock_request.call_count, 2) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 = ''' + 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), + 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", - ) - ], + 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 ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) response = api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) self.assertEqual(response.deletes, None) self.assertEqual(len(response.writes), 3) - self.assertEqual(response.writes[0], + self.assertEqual( + response.writes[0], ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1258,17 +1483,25 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), success=True, - error=None)) - self.assertEqual(response.writes[1].tuple_key, + 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], + self.assertIsInstance( + response.writes[1].error.parsed_exception, + ValidationErrorMessageResponse, + ) + self.assertEqual( + response.writes[2], ClientWriteSingleResponse( tuple_key=ClientTuple( object="document:2021-budget", @@ -1276,86 +1509,136 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", ), success=True, - error=None)) + error=None, + ), + ) self.assertEqual(mock_request.call_count, 3) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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", - ) - ], + deletes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], writes=[], ) - transaction = WriteTransactionOpts(disabled=True, max_per_chunk=1, max_parallel_requests=10) + transaction = WriteTransactionOpts( + disabled=True, max_per_chunk=1, max_parallel_requests=10 + ) api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -1377,28 +1660,49 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + ), ], - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @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 = '{}' + response_body = "{}" mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -1420,28 +1724,48 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - ) + ), ], - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}, ) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write', + "POST", + "http://api.fga.example/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"}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_write_batch_unauthorized(self, mock_request): - """Test case for write with 401 response - """ + """Test case for write with 401 response""" mock_request.side_effect = UnauthorizedException( - http_resp=http_mock_response('{}', 401) + http_resp=http_mock_response("{}", 401) ) configuration = self.configuration configuration.store_id = store_id @@ -1457,11 +1781,14 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) transaction = WriteTransactionOpts( - disabled=True, max_per_chunk=1, max_parallel_requests=10) + disabled=True, max_per_chunk=1, max_parallel_requests=10 + ) api_client.write( body, - options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", - "transaction": transaction} + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "transaction": transaction, + }, ) mock_request.assert_called() @@ -1490,7 +1817,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_check(self, mock_request): """Test case for check @@ -1511,7 +1838,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:budget", ), ], - ) + ) configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: @@ -1520,14 +1847,14 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) 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/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -1535,14 +1862,14 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", "relation": "reader", - "object": "document:budget" + "object": "document:budget", }, "contextual_tuples": { "tuple_keys": [ { "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", "relation": "writer", - "object": "document:budget" + "object": "document:budget", } ] }, @@ -1550,11 +1877,11 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_check_config_auth_model(self, mock_request): """Test case for check @@ -1565,31 +1892,34 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): 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", - ) + 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={} - ) + 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "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"}, + 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 + _request_timeout=None, ) api_client.close() @@ -2098,8 +2428,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, ) self.assertEqual( - "Duplicate correlation_id (1) provided", - str(error.exception) + "Duplicate correlation_id (1) provided", str(error.exception) ) api_client.close() @@ -2122,9 +2451,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # First, mock the response mock_request.side_effect = [ mock_response(first_response_body, 200), - UnauthorizedException( - http_resp=http_mock_response("{}", 401) - ), + UnauthorizedException(http_resp=http_mock_response("{}", 401)), ] body = ClientBatchCheckRequest( @@ -2145,7 +2472,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): object="document:2021-budget", relation="reader", user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", - correlation_id="3" + correlation_id="3", ), ] ) @@ -2218,15 +2545,15 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @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 """ - response_body = '''{ + 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 @@ -2240,7 +2567,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertIsInstance(api_response, ExpandResponse) cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) @@ -2250,37 +2577,34 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): expected_response = ExpandResponse(userTree) self.assertEqual(api_response, expected_response) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand", headers=ANY, query_params=[], post_params=[], body={ - "tuple_key":{ - "object": "document:budget", - "relation": "reader" - }, + "tuple_key": {"object": "document:budget", "relation": "reader"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_list_objects(self, mock_request): """Test case for list_objects List objects """ - response_body = ''' + response_body = """ { "objects": [ "document:abcd1234" ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -2299,10 +2623,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): }, ) self.assertIsInstance(api_response, ListObjectsResponse) - self.assertEqual(api_response.objects, ['document:abcd1234']) + self.assertEqual(api_response.objects, ["document:abcd1234"]) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects", headers=ANY, query_params=[], post_params=[], @@ -2314,24 +2638,23 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_list_objects_contextual_tuples(self, mock_request): """Test case for list_objects List objects """ - response_body = ''' + response_body = """ { "objects": [ "document:abcd1234" ] } - ''' + """ mock_request.return_value = mock_response(response_body, 200) configuration = self.configuration configuration.store_id = store_id @@ -2349,33 +2672,48 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): ], ) # Get all stores - api_response = api_client.list_objects(body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}) + api_response = api_client.list_objects( + body, options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"} + ) self.assertIsInstance(api_response, ListObjectsResponse) - self.assertEqual(api_response.objects, ['document:abcd1234']) + self.assertEqual(api_response.objects, ["document:abcd1234"]) mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects', + "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'}]}}, + 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 + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @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 """ + def mock_check_requests(*args, **kwargs): - body = kwargs.get('body') - tuple_key = body.get('tuple_key') - if tuple_key['relation'] == "owner": + body = kwargs.get("body") + tuple_key = body.get("tuple_key") + if tuple_key["relation"] == "owner": return mock_response('{"allowed": false, "resolution": "1234"}', 200) return mock_response('{"allowed": true, "resolution": "1234"}', 200) @@ -2389,19 +2727,19 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): body=ClientListRelationsRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relations=["reader", "owner", "viewer"], - object="document:2021-budget" + object="document:2021-budget", ), options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertEqual(api_response, ["reader", "viewer"]) # Make sure the API was called with the right data mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -2409,17 +2747,17 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "reader", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", }, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -2427,17 +2765,17 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "owner", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" - }, + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) mock_request.assert_any_call( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check', + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check", headers=ANY, query_params=[], post_params=[], @@ -2445,40 +2783,40 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "tuple_key": { "object": "document:2021-budget", "relation": "viewer", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", }, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", "consistency": "MINIMIZE_LATENCY", }, _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) api_client.close() - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_list_relations_unauthorized(self, mock_request): - """Test case for list relations with 401 response - """ + """Test case for list relations with 401 response""" mock_request.side_effect = UnauthorizedException( - http_resp=http_mock_response('{}', 401) + 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"} + 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() api_client.close() - @patch.object(rest.RESTClientObject, "request") def test_list_users(self, mock_request): """ @@ -2539,7 +2877,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): options={ "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "consistency": ConsistencyPreference.MINIMIZE_LATENCY, - } + }, ) self.assertIsInstance(response, ListUsersResponse) @@ -2568,7 +2906,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): mock_request.assert_called_once_with( "POST", - "http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-users", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-users", headers=ANY, query_params=[], post_params=[], @@ -2600,13 +2938,10 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): api_client.close() - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_read_assertions(self, mock_request): - """Test case for read assertions - - """ - response_body = ''' + """Test case for read assertions""" + response_body = """ { "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", "assertions": [ @@ -2620,123 +2955,185 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): } ] } - ''' + """ 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=TupleKeyWithoutCondition(object="document:2021-budget", relation="reader", - user="user:anne"), - expectation=True, - )] - )) + options={"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"} + ) + self.assertEqual( + api_response, + ReadAssertionsResponse( + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + assertions=[ + Assertion( + tuple_key=TupleKeyWithoutCondition( + object="document:2021-budget", + relation="reader", + user="user:anne", + ), + expectation=True, + ) + ], + ), + ) mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "GET", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J", headers=ANY, + body=None, query_params=[], + post_params=[], _preload_content=ANY, - _request_timeout=None + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_write_assertions(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + 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"} + [ + 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "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}]}, + 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 + _request_timeout=None, ) - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_set_store_id(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + 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"} + [ + 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.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5Y/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "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}]}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_config_auth_model(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + 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={} + [ + ClientAssertion( + user="user:anne", + relation="reader", + object="document:2021-budget", + expectation=True, + ) + ], + options={}, + ) + self.assertEqual( + api_client.get_authorization_model_id(), "01G5JAVJ41T49E9TT3SKVS7X1J" ) - self.assertEqual(api_client.get_authorization_model_id(), "01G5JAVJ41T49E9TT3SKVS7X1J") mock_request.assert_called_once_with( - 'PUT', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + "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}]}, + 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 + _request_timeout=None, ) - - @patch.object(rest.RESTClientObject, 'request') + @patch.object(rest.RESTClientObject, "request") def test_update_auth_model(self, mock_request): """Test case for write assertions Get all stores """ - mock_request.return_value = mock_response('', 204) + mock_request.return_value = mock_response("", 204) configuration = self.configuration configuration.store_id = store_id configuration.authorization_model_id = "01G5JAVJ41T49E9TT3SKVS7X1J" @@ -2744,19 +3141,36 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): api_client.set_authorization_model_id("01G5JAVJ41T49E9TT3SKVS7X2J") api_client.write_assertions( - [ClientAssertion(user="user:anne", relation="reader", - object="document:2021-budget", expectation=True)], - options={} + [ + ClientAssertion( + user="user:anne", + relation="reader", + object="document:2021-budget", + expectation=True, + ) + ], + options={}, ) mock_request.assert_called_once_with( - 'PUT', - 'http://api.{{sampleApiDomain}}/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X2J', + "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}]}, + 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 + _request_timeout=None, ) def test_configuration_store_id_invalid(self): @@ -2764,9 +3178,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = ClientConfiguration( - api_host='localhost', - api_scheme='http', - store_id="abcd" + api_host="localhost", api_scheme="http", store_id="abcd" ) self.assertRaises(FgaValidationException, configuration.is_valid) @@ -2775,9 +3187,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): Test whether ApiValueError is raised if host has query """ configuration = ClientConfiguration( - api_host='localhost', - api_scheme='http', + api_host="localhost", + api_scheme="http", store_id="01H15K9J85050XTEDPVM8DJM78", - authorization_model_id="abcd" + authorization_model_id="abcd", ) self.assertRaises(FgaValidationException, configuration.is_valid) diff --git a/config/clients/python/template/test/sync/oauth2_test.py.mustache b/config/clients/python/template/test/sync/oauth2_test.py.mustache index 4a784d86..78a652f5 100644 --- a/config/clients/python/template/test/sync/oauth2_test.py.mustache +++ b/config/clients/python/template/test/sync/oauth2_test.py.mustache @@ -1,30 +1,25 @@ {{>partial_header}} -import urllib3 - +from datetime import datetime, timedelta from unittest import IsolatedAsyncioTestCase from unittest.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 + +import urllib3 + from {{packageName}}.configuration import Configuration +from {{packageName}}.credentials import CredentialConfiguration, Credentials from {{packageName}}.exceptions import AuthenticationError +from {{packageName}}.sync import rest +from {{packageName}}.sync.oauth2 import OAuth2Client # 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 - ) + 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""" @@ -39,59 +34,87 @@ class TestOAuth2Client(IsolatedAsyncioTestCase): Test getting authentication header when method is client credentials """ client = OAuth2Client(None) - client._access_token = 'XYZ123' + 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'}) + self.assertEqual(auth_header, {"Authorization": "Bearer XYZ123"}) - @patch.object(rest.RESTClientObject, 'request') + @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 = ''' + 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='issuer.{{sampleApiDomain}}', api_audience='myaudience')) + credentials = Credentials( + method="client_credentials", + configuration=CredentialConfiguration( + client_id="myclientid", + client_secret="mysecret", + api_issuer="issuer.fga.example", + 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/x-www-form-urlencoded', 'User-Agent': 'openfga-sdk (python) {{packageVersion}}'}) + 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/x-www-form-urlencoded", + "User-Agent": "openfga-sdk (python) 0.9.0", + } + ) mock_request.assert_called_once_with( - 'POST', - 'https://issuer.{{sampleApiDomain}}/oauth/token', + method="POST", + url="https://issuer.fga.example/oauth/token", headers=expected_header, - query_params=None, body=None, _preload_content=True, _request_timeout=None, - post_params={"client_id": "myclientid", "client_secret": "mysecret", "audience": "myaudience", "grant_type": "client_credentials"} + query_params=None, + body=None, + _preload_content=True, + _request_timeout=None, + post_params={ + "client_id": "myclientid", + "client_secret": "mysecret", + "audience": "myaudience", + "grant_type": "client_credentials", + }, ) rest_client.close() - @patch.object(rest.RESTClientObject, 'request') + @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 = ''' + 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='issuer.{{sampleApiDomain}}', api_audience='myaudience')) + credentials = Credentials( + method="client_credentials", + configuration=CredentialConfiguration( + client_id="myclientid", + client_secret="mysecret", + api_issuer="issuer.fga.example", + api_audience="myaudience", + ), + ) rest_client = rest.RESTClientObject(Configuration()) client = OAuth2Client(credentials) with self.assertRaises(AuthenticationError): @@ -118,7 +141,7 @@ class TestOAuth2Client(IsolatedAsyncioTestCase): configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) @@ -148,7 +171,7 @@ This is not a JSON response configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) @@ -177,7 +200,7 @@ This is not a JSON response configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) @@ -220,7 +243,7 @@ This is not a JSON response configuration=CredentialConfiguration( client_id="myclientid", client_secret="mysecret", - api_issuer="issuer.{{sampleApiDomain}}", + api_issuer="issuer.fga.example", api_audience="myaudience", ), ) diff --git a/config/clients/python/template/test/sync/rest_test.py.mustache b/config/clients/python/template/test/sync/rest_test.py.mustache new file mode 100644 index 00000000..92bac941 --- /dev/null +++ b/config/clients/python/template/test/sync/rest_test.py.mustache @@ -0,0 +1,525 @@ +{{>partial_header}} + +import json +from unittest.mock import MagicMock + +import pytest + +from {{packageName}}.exceptions import ( + ApiException, + ForbiddenException, + NotFoundException, + RateLimitExceededError, + ServiceException, + UnauthorizedException, + ValidationException, +) +from {{packageName}}.sync.rest import RESTClientObject, RESTResponse + + +def test_restresponse_init(): + mock_resp = MagicMock() + mock_resp.status = 200 + mock_resp.reason = "OK" + resp_data = b'{"test":"data"}' + + rest_resp = RESTResponse(mock_resp, resp_data) + assert rest_resp.status == 200 + assert rest_resp.reason == "OK" + assert rest_resp.data == resp_data + assert rest_resp.urllib3_response == mock_resp + + +def test_restresponse_getheaders(): + mock_resp = MagicMock() + mock_resp.headers = {"Content-Type": "application/json", "X-Testing": "true"} + + rest_resp = RESTResponse(mock_resp, b"") + headers = rest_resp.getheaders() + + assert headers["Content-Type"] == "application/json" + assert headers["X-Testing"] == "true" + + +def test_restresponse_getheader(): + mock_resp = MagicMock() + mock_resp.headers = {"Content-Type": "application/json"} + + rest_resp = RESTResponse(mock_resp, b"") + val = rest_resp.getheader("Content-Type") + missing = rest_resp.getheader("X-Not-Here", default="fallback") + + assert val == "application/json" + assert missing == "fallback" + + +def test_build_request_json_body(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + req_args = client.build_request( + method="POST", + url="http://example.com/test", + body={"foo": "bar"}, + headers={"Content-Type": "application/json"}, + ) + + assert req_args["method"] == "POST" + assert req_args["url"] == "http://example.com/test" + assert req_args["headers"]["Content-Type"] == "application/json" + assert json.loads(req_args["body"]) == {"foo": "bar"} + + +def test_build_request_multipart(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + req_args = client.build_request( + method="POST", + url="http://example.com/upload", + post_params={"file": ("filename.txt", b"contents", "text/plain")}, + headers={"Content-Type": "multipart/form-data"}, + ) + + assert req_args["method"] == "POST" + assert req_args["url"] == "http://example.com/upload" + assert "Content-Type" not in req_args["headers"] + assert req_args["encode_multipart"] is True + assert req_args["fields"] == {"file": ("filename.txt", b"contents", "text/plain")} + + +def test_build_request_timeout(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + req_args = client.build_request( + method="GET", + url="http://example.com", + _request_timeout=10.0, + ) + + # We'll just confirm that the "timeout" object was set to 10.0 + # A deeper check might be verifying urllib3.Timeout, but this suffices. + assert req_args["timeout"].total == 10.0 + + +def test_handle_response_exception_success(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_response = MagicMock() + mock_response.status = 200 + + client.handle_response_exception(mock_response) # no exception + + +@pytest.mark.parametrize( + "status,exc", + [ + (400, ValidationException), + (401, UnauthorizedException), + (403, ForbiddenException), + (404, NotFoundException), + (429, RateLimitExceededError), + (500, ServiceException), + (418, ApiException), + ], +) +def test_handle_response_exception_error(status, exc): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_response = MagicMock() + mock_response.status = status + + with pytest.raises(exc): + client.handle_response_exception(mock_response) + + +def test_close(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_pool_manager = MagicMock() + client.pool_manager = mock_pool_manager + + client.close() + mock_pool_manager.clear.assert_called_once() + + +def test_request_preload_content(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_pool_manager = MagicMock() + client.pool_manager = mock_pool_manager + + mock_raw_response = MagicMock() + mock_raw_response.status = 200 + mock_raw_response.reason = "OK" + mock_raw_response.data = b'{"some":"data"}' + + mock_pool_manager.request.return_value = mock_raw_response + + resp = client.request(method="GET", url="http://example.com", _preload_content=True) + + mock_pool_manager.request.assert_called_once() + assert isinstance(resp, RESTResponse) + assert resp.status == 200 + assert resp.data == b'{"some":"data"}' + mock_pool_manager.clear.assert_called_once() + + +def test_request_no_preload_content(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_pool_manager = MagicMock() + client.pool_manager = mock_pool_manager + + mock_raw_response = MagicMock() + mock_raw_response.status = 200 + mock_raw_response.reason = "OK" + mock_raw_response.data = b"unused" + + mock_pool_manager.request.return_value = mock_raw_response + + resp = client.request( + method="GET", url="http://example.com", _preload_content=False + ) + + mock_pool_manager.request.assert_called_once() + # We expect the raw HTTPResponse + assert resp == mock_raw_response + assert resp.status == 200 + mock_pool_manager.clear.assert_called_once() + + +def test_stream_happy_path(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_pool_manager = MagicMock() + client.pool_manager = mock_pool_manager + + class FakeHTTPResponse: + def __init__(self): + self.status = 200 + self.reason = "OK" + + def stream(self, chunk_size): + # Single chunk with two JSON lines + yield b'{"foo":"bar"}\n{"hello":"world"}' + + def release_conn(self): + pass + + mock_response = FakeHTTPResponse() + mock_pool_manager.request.return_value = mock_response + + results = list(client.stream("GET", "http://example.com")) + + assert results == [{"foo": "bar"}, {"hello": "world"}] + mock_pool_manager.request.assert_called_once() + mock_pool_manager.clear.assert_called_once() + + +def test_stream_partial_chunks(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_pool_manager = MagicMock() + client.pool_manager = mock_pool_manager + + class FakeHTTPResponse: + def __init__(self): + self.status = 200 + self.reason = "OK" + + def stream(self, chunk_size): + # Two partial chunks that form "foo":"bar" plus a second object + yield b'{"foo":"b' + yield b'ar"}\n{"hello":"world"}' + + def release_conn(self): + pass + + mock_response = FakeHTTPResponse() + mock_pool_manager.request.return_value = mock_response + + results = list(client.stream("GET", "http://example.com")) + + assert results == [{"foo": "bar"}, {"hello": "world"}] + mock_pool_manager.request.assert_called_once() + mock_pool_manager.clear.assert_called_once() + + +def test_stream_exception_in_chunks(): + mock_config = MagicMock( + spec=[ + "verify_ssl", + "ssl_ca_cert", + "cert_file", + "key_file", + "assert_hostname", + "retries", + "socket_options", + "connection_pool_maxsize", + "timeout_millisec", + "proxy", + "proxy_headers", + ] + ) + mock_config.ssl_ca_cert = None + mock_config.cert_file = None + mock_config.key_file = None + mock_config.verify_ssl = True + mock_config.connection_pool_maxsize = 4 + mock_config.timeout_millisec = 5000 + mock_config.proxy = None + mock_config.proxy_headers = None + + client = RESTClientObject(configuration=mock_config) + mock_pool_manager = MagicMock() + client.pool_manager = mock_pool_manager + + class FakeHTTPResponse: + def __init__(self): + self.status = 200 + self.reason = "OK" + + def stream(self, chunk_size): + # Raise an exception while streaming + raise ValueError("Boom!") + + def release_conn(self): + pass + + mock_response = FakeHTTPResponse() + mock_pool_manager.request.return_value = mock_response + + results = list(client.stream("GET", "http://example.com")) + # Exception is logged, we yield nothing + assert results == [] + mock_pool_manager.request.assert_called_once() + mock_pool_manager.clear.assert_called_once() diff --git a/config/common/files/README.mustache b/config/common/files/README.mustache index 56a56d61..fba1e885 100644 --- a/config/common/files/README.mustache +++ b/config/common/files/README.mustache @@ -38,6 +38,9 @@ - [Batch Check](#batch-check) - [Expand](#expand) - [List Objects](#list-objects) + {{#supportsStreamedListObjects}} + - [Streamed List Objects](#streamed-list-objects) + {{/supportsStreamedListObjects}} - [List Relations](#list-relations) - [List Users](#list-users) - [Assertions](#assertions) @@ -126,7 +129,7 @@ If you have found a bug or if you have a feature request, please report them on ### Pull Requests -While we accept Pull Requests on this repository, the SDKs are autogenerated so please consider additionally submitting your Pull Requests to the [sdk-generator](https://{{gitHost}}/{{gitUserId}}/sdk-generator) and linking the two PRs together and to the corresponding issue. This will greatly assist the OpenFGA team in being able to give timely reviews as well as deploying fixes and updates to our other SDKs as well. +While we accept Pull Requests on this repository, the SDKs are autogenerated so please consider additionally submitting your Pull Requests to the [sdk-generator](https://{{gitHost}}/{{gitUserId}}/sdk-generator) and linking the two PRs together and to the corresponding issue. This will greatly assist the OpenFGA team in being able to give timely reviews as well as deploying fixes and updates to our other SDKs as well. ## Author