From a16539db7cd52d5e0d17a7eb85a2f9e556ac6fe5 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Fri, 12 Jul 2024 00:57:53 -0500 Subject: [PATCH] release(python-sdk): v0.6.0 --- Makefile | 25 +- config/clients/python/CHANGELOG.md.mustache | 6 + config/clients/python/config.overrides.json | 227 ++- .../python/patches/open_fga_api.py.patch | 19 +- .../python/patches/open_fga_api_sync.py.patch | 19 +- .../.github/workflows/main.yaml.mustache | 10 +- .../template/README_calling_api.mustache | 72 +- config/clients/python/template/api.mustache | 349 +--- .../clients/python/template/api_sync.mustache | 342 +--- .../clients/python/template/api_test.mustache | 1431 +---------------- .../template/client/models/__init__.mustache | 12 - .../python/template/docs/opentelemetry.md | 31 + .../{example1.py => example1.py.mustache} | 10 +- .../example1/{setup.py => setup.py.mustache} | 4 +- .../example/opentelemetry/.env.example | 7 + .../template/example/opentelemetry/.gitignore | 1 + .../template/example/opentelemetry/README.md | 43 + .../example/opentelemetry/main.py.mustache | 151 ++ .../example/opentelemetry/requirements.txt | 3 + .../template/example/opentelemetry/setup.cfg | 2 + .../example/opentelemetry/setup.py.mustache | 30 + .../python/template/requirements.mustache | 2 + config/clients/python/template/setup.mustache | 9 +- .../__init__.py.mustache} | 0 .../python/template/src/api.py.mustache | 386 +++++ .../api/__init__.py.mustache} | 0 .../api_client.py.mustache} | 319 ++-- .../client/__init__.py.mustache} | 0 .../client/client.py.mustache} | 0 .../client/configuration.py.mustache} | 0 .../src/client/models/__init__.py.mustache | 12 + .../client/models/assertion.py.mustache} | 0 .../models/batch_check_response.py.mustache} | 0 .../client/models/check_request.py.mustache} | 0 .../client/models/expand_request.py.mustache} | 0 .../models/list_objects_request.py.mustache} | 0 .../list_relations_request.py.mustache} | 0 .../models/list_users_request.py.mustache} | 0 .../models/read_changes_request.py.mustache} | 0 .../client/models/tuple.py.mustache} | 0 .../client/models/write_request.py.mustache} | 0 .../client/models/write_response.py.mustache} | 0 .../models/write_single_response.py.mustache} | 0 .../write_transaction_opts.py.mustache} | 0 .../configuration.py.mustache} | 0 .../credentials.py.mustache} | 0 .../exceptions.py.mustache} | 0 .../{help.py => src/help.py.mustache} | 10 + .../models/__init__.py.mustache} | 0 .../oauth2.py.mustache} | 9 + .../rest.mustache => src/rest.py.mustache} | 0 .../sync/__init__.py.mustache} | 0 .../python/template/src/sync/api.py.mustache | 376 +++++ .../sync/api_client.py.mustache} | 201 ++- .../sync/client/__init__.py.mustache} | 0 .../sync/client/client.py.mustache} | 0 .../sync/oauth2.py.mustache} | 9 + .../sync/rest.py.mustache} | 0 .../src/telemetry/__init__.py.mustache | 4 + .../src/telemetry/attributes.py.mustache | 82 + .../src/telemetry/counters.py.mustache | 15 + .../src/telemetry/histograms.py.mustache | 20 + .../src/telemetry/metrics.py.mustache | 94 ++ .../src/telemetry/telemetry.py.mustache | 11 + .../validation.py.mustache} | 0 .../template/test-requirements.mustache | 2 +- .../__init__.py.mustache} | 0 .../template/test/api/__init__.py.mustache | 0 .../python/template/test/api_test.py.mustache | 1430 ++++++++++++++++ .../template/test/client/__init__.py.mustache | 0 .../client/client_test.py.mustache} | 0 .../configuration_test.py.mustache} | 0 .../credentials_test.py.mustache} | 0 .../oauth2_test.py.mustache} | 0 .../template/test/sync/__init__.py.mustache | 0 .../sync/api_test.py.mustache} | 0 .../test/sync/client/__init__.py.mustache | 0 .../sync/client/client_test.py.mustache} | 0 .../sync/oauth2_test.py.mustache} | 0 .../validation_test.py.mustache} | 2 +- .../python/template/tornado/rest.mustache | 222 --- config/common/files/README.mustache | 2 +- 82 files changed, 3346 insertions(+), 2665 deletions(-) delete mode 100644 config/clients/python/template/client/models/__init__.mustache create mode 100644 config/clients/python/template/docs/opentelemetry.md rename config/clients/python/template/example/example1/{example1.py => example1.py.mustache} (97%) rename config/clients/python/template/example/example1/{setup.py => setup.py.mustache} (89%) create mode 100644 config/clients/python/template/example/opentelemetry/.env.example create mode 100644 config/clients/python/template/example/opentelemetry/.gitignore create mode 100644 config/clients/python/template/example/opentelemetry/README.md create mode 100644 config/clients/python/template/example/opentelemetry/main.py.mustache create mode 100644 config/clients/python/template/example/opentelemetry/requirements.txt create mode 100644 config/clients/python/template/example/opentelemetry/setup.cfg create mode 100644 config/clients/python/template/example/opentelemetry/setup.py.mustache rename config/clients/python/template/{__init__package.mustache => src/__init__.py.mustache} (100%) create mode 100644 config/clients/python/template/src/api.py.mustache rename config/clients/python/template/{__init__api.mustache => src/api/__init__.py.mustache} (100%) rename config/clients/python/template/{api_client.mustache => src/api_client.py.mustache} (77%) rename config/clients/python/template/{client/__init__.mustache => src/client/__init__.py.mustache} (100%) rename config/clients/python/template/{client/client.mustache => src/client/client.py.mustache} (100%) rename config/clients/python/template/{client/configuration.mustache => src/client/configuration.py.mustache} (100%) create mode 100644 config/clients/python/template/src/client/models/__init__.py.mustache rename config/clients/python/template/{client/models/assertion.mustache => src/client/models/assertion.py.mustache} (100%) rename config/clients/python/template/{client/models/batch_check_response.mustache => src/client/models/batch_check_response.py.mustache} (100%) rename config/clients/python/template/{client/models/check_request.mustache => src/client/models/check_request.py.mustache} (100%) rename config/clients/python/template/{client/models/expand_request.mustache => src/client/models/expand_request.py.mustache} (100%) rename config/clients/python/template/{client/models/list_objects_request.mustache => src/client/models/list_objects_request.py.mustache} (100%) rename config/clients/python/template/{client/models/list_relations_request.mustache => src/client/models/list_relations_request.py.mustache} (100%) rename config/clients/python/template/{client/models/list_users_request.mustache => src/client/models/list_users_request.py.mustache} (100%) rename config/clients/python/template/{client/models/read_changes_request.mustache => src/client/models/read_changes_request.py.mustache} (100%) rename config/clients/python/template/{client/models/tuple.mustache => src/client/models/tuple.py.mustache} (100%) rename config/clients/python/template/{client/models/write_request.mustache => src/client/models/write_request.py.mustache} (100%) rename config/clients/python/template/{client/models/write_response.mustache => src/client/models/write_response.py.mustache} (100%) rename config/clients/python/template/{client/models/write_single_response.mustache => src/client/models/write_single_response.py.mustache} (100%) rename config/clients/python/template/{client/models/write_transaction_opts.mustache => src/client/models/write_transaction_opts.py.mustache} (100%) rename config/clients/python/template/{configuration.mustache => src/configuration.py.mustache} (100%) rename config/clients/python/template/{credentials.mustache => src/credentials.py.mustache} (100%) rename config/clients/python/template/{exceptions.mustache => src/exceptions.py.mustache} (100%) rename config/clients/python/template/{help.py => src/help.py.mustache} (89%) rename config/clients/python/template/{__init__model.mustache => src/models/__init__.py.mustache} (100%) rename config/clients/python/template/{oauth2.mustache => src/oauth2.py.mustache} (90%) rename config/clients/python/template/{asyncio/rest.mustache => src/rest.py.mustache} (100%) rename config/clients/python/template/{__init__sync.mustache => src/sync/__init__.py.mustache} (100%) create mode 100644 config/clients/python/template/src/sync/api.py.mustache rename config/clients/python/template/{api_client_sync.mustache => src/sync/api_client.py.mustache} (86%) rename config/clients/python/template/{__init__sync_client.mustache => src/sync/client/__init__.py.mustache} (100%) rename config/clients/python/template/{client/client_sync.mustache => src/sync/client/client.py.mustache} (100%) rename config/clients/python/template/{oauth2_sync.mustache => src/sync/oauth2.py.mustache} (90%) rename config/clients/python/template/{rest_sync.mustache => src/sync/rest.py.mustache} (100%) create mode 100644 config/clients/python/template/src/telemetry/__init__.py.mustache create mode 100644 config/clients/python/template/src/telemetry/attributes.py.mustache create mode 100644 config/clients/python/template/src/telemetry/counters.py.mustache create mode 100644 config/clients/python/template/src/telemetry/histograms.py.mustache create mode 100644 config/clients/python/template/src/telemetry/metrics.py.mustache create mode 100644 config/clients/python/template/src/telemetry/telemetry.py.mustache rename config/clients/python/template/{validation.mustache => src/validation.py.mustache} (100%) rename config/clients/python/template/{__init__.mustache => test/__init__.py.mustache} (100%) create mode 100644 config/clients/python/template/test/api/__init__.py.mustache create mode 100644 config/clients/python/template/test/api_test.py.mustache create mode 100644 config/clients/python/template/test/client/__init__.py.mustache rename config/clients/python/template/{client/test_client.mustache => test/client/client_test.py.mustache} (100%) rename config/clients/python/template/{test_configuration.mustache => test/configuration_test.py.mustache} (100%) rename config/clients/python/template/{credentials_test.mustache => test/credentials_test.py.mustache} (100%) rename config/clients/python/template/{oauth2_test.mustache => test/oauth2_test.py.mustache} (100%) create mode 100644 config/clients/python/template/test/sync/__init__.py.mustache rename config/clients/python/template/{api_test_sync.mustache => test/sync/api_test.py.mustache} (100%) create mode 100644 config/clients/python/template/test/sync/client/__init__.py.mustache rename config/clients/python/template/{client/test_client_sync.mustache => test/sync/client/client_test.py.mustache} (100%) rename config/clients/python/template/{oauth2_test_sync.mustache => test/sync/oauth2_test.py.mustache} (100%) rename config/clients/python/template/{test_validation.mustache => test/validation_test.py.mustache} (95%) delete mode 100644 config/clients/python/template/tornado/rest.mustache diff --git a/Makefile b/Makefile index 20486285..bc19a962 100644 --- a/Makefile +++ b/Makefile @@ -112,12 +112,25 @@ tag-client-python: test-client-python .PHONY: build-client-python build-client-python: make build-client sdk_language=python tmpdir=${TMP_DIR} library="asyncio" - mv ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/openfga_sdk/api/open_fga_api_sync.py ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/openfga_sdk/sync/open_fga_api.py # TODO: Remove on OpenAPI generator v7.1 or higher - make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/openfga_sdk/api/open_fga_api.py /config/clients/python/patches/open_fga_api.py.patch'" - make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/openfga_sdk/sync/open_fga_api.py /config/clients/python/patches/open_fga_api_sync.py.patch'" - make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/docs/OpenFgaApi.md /config/clients/python/patches/OpenFgaApi.md.patch'" - make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'python -m pip install pyupgrade==3.15.2 isort==5.13.2 black==24.4.2 autoflake==2.3.1; pyupgrade \`find . -name *.py -type f\` --py310-plus --keep-runtime-typing; isort . --profile black; autoflake --exclude=__init__.py --in-place --remove-unused-variables --remove-all-unused-imports -r .; black .'" - make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'pip install setuptools wheel && python setup.py sdist bdist_wheel'" + + 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 + mv ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/test/_/*.py ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/test/ && rm -rf ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/test/_/ + + sort -uo ${CLIENTS_OUTPUT_DIR}/fga-python-sdk/.openapi-generator/FILES{,} + + make run-in-docker sdk_language=python image=busybox:${BUSYBOX_DOCKER_TAG} command="/bin/sh -c 'patch -p1 /module/openfga_sdk/api/open_fga_api.py /config/clients/python/patches/open_fga_api.py.patch && \ + patch -p1 /module/openfga_sdk/sync/open_fga_api.py /config/clients/python/patches/open_fga_api_sync.py.patch && \ + patch -p1 /module/docs/OpenFgaApi.md /config/clients/python/patches/OpenFgaApi.md.patch'" + + make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'python -m pip install --upgrade pip && \ + python -m pip install --upgrade setuptools wheel && \ + python -m pip install -r test-requirements.txt && \ + python -m pyupgrade \`find . -name *.py -type f\` --py310-plus --keep-runtime-typing && \ + python -m isort . --profile black && \ + python -m autoflake --exclude=__init__.py --in-place --remove-unused-variables --remove-all-unused-imports -r . && \ + python -m black . && \ + python setup.py sdist bdist_wheel'" .PHONY: test-client-python test-client-python: build-client-python diff --git a/config/clients/python/CHANGELOG.md.mustache b/config/clients/python/CHANGELOG.md.mustache index 924464cd..4033307c 100644 --- a/config/clients/python/CHANGELOG.md.mustache +++ b/config/clients/python/CHANGELOG.md.mustache @@ -1,5 +1,11 @@ # Changelog +## v0.6.0 + +### [0.6.0](https://github.com/openfga/python-sdk/compare/v0.5.0...v0.6.0) (2024-06-28) + +- feat: add OpenTelemetry metrics reporting + ## v0.5.0 ### [0.5.0](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.4.2...v0.5.0) (2024-06-17) diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index c6e1e777..ff99f772 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -2,189 +2,272 @@ "sdkId": "python", "gitRepoId": "python-sdk", "packageName": "openfga_sdk", - "packageVersion": "0.5.0", + "packageVersion": "0.6.0", "packageDescription": "Python SDK for OpenFGA", "packageDetailedDescription": "This is an autogenerated python SDK for OpenFGA. It provides a wrapper around the [OpenFGA API definition](https://openfga.dev/api).", "fossaComplianceNoticeId": "2f8a8629-b46c-435e-b8cd-1174a674fb4b", "infoName": "OpenFGA", "infoEmail": "community@openfga.dev", "docPrefix": "https://github.com/openfga/python-sdk/blob/main/", + "pythonMinimumRuntime": "3.10", + "supportsOpenTelemetry": true, "files": { ".github/workflows/main.yaml.mustache": { "destinationFilename": ".github/workflows/main.yaml", "templateType": "SupportingFiles" }, + ".snyk": {}, - "credentials.mustache": { - "destinationFilename": "openfga_sdk/credentials.py", + + "api_sync.mustache": { + "folder": "openfga_sdk/sync", + "destinationFilename": "_sync.py", + "templateType": "API" + }, + + "docs/opentelemetry.md": {}, + + "example/Makefile": {}, + "example/README.md": {}, + "example/example1/example1.py.mustache": { + "destinationFilename": "example/example1/example1.py", + "templateType": "SupportingFiles" + }, + "example/example1/requirements.txt.mustache": { + "destinationFilename": "example/example1/requirements.txt", "templateType": "SupportingFiles" }, - "credentials_test.mustache": { - "destinationFilename": "test/test_credentials.py", + "example/example1/setup.py.mustache": { + "destinationFilename": "example/example1/setup.py", "templateType": "SupportingFiles" }, - "client/__init__.mustache": { - "destinationFilename": "openfga_sdk/client/__init__.py", + "example/example1/setup.cfg": {}, + + "example/opentelemetry/.env.example": {}, + "example/opentelemetry/.gitignore": {}, + "example/opentelemetry/README.md": {}, + "example/opentelemetry/requirements.txt": {}, + "example/opentelemetry/setup.cfg": {}, + "example/opentelemetry/setup.py.mustache": { + "destinationFilename": "example/opentelemetry/setup.py", "templateType": "SupportingFiles" }, - "client/client.mustache": { - "destinationFilename": "openfga_sdk/client/client.py", + "example/opentelemetry/main.py.mustache": { + "destinationFilename": "example/opentelemetry/main.py", "templateType": "SupportingFiles" }, - "client/test_client.mustache": { - "destinationFilename": "test/test_client.py", + + "src/api/__init__.py.mustache": { + "destinationFilename": "openfga_sdk/api/__init__.py", "templateType": "SupportingFiles" }, - "client/configuration.mustache": { + "src/client/__init__.py.mustache": { + "destinationFilename": "openfga_sdk/client/__init__.py", + "templateType": "SupportingFiles" + }, + "src/client/client.py.mustache": { + "destinationFilename": "openfga_sdk/client/client.py", + "templateType": "SupportingFiles" + }, + "src/client/configuration.py.mustache": { "destinationFilename": "openfga_sdk/client/configuration.py", "templateType": "SupportingFiles" }, - "client/models/__init__.mustache": { + "src/client/models/__init__.py.mustache": { "destinationFilename": "openfga_sdk/client/models/__init__.py", "templateType": "SupportingFiles" }, - "client/models/assertion.mustache": { + "src/client/models/assertion.py.mustache": { "destinationFilename": "openfga_sdk/client/models/assertion.py", "templateType": "SupportingFiles" }, - "client/models/batch_check_response.mustache": { + "src/client/models/batch_check_response.py.mustache": { "destinationFilename": "openfga_sdk/client/models/batch_check_response.py", "templateType": "SupportingFiles" }, - "client/models/check_request.mustache": { + "src/client/models/check_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/check_request.py", "templateType": "SupportingFiles" }, - "client/models/expand_request.mustache": { + "src/client/models/expand_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/expand_request.py", "templateType": "SupportingFiles" }, - "client/models/list_objects_request.mustache": { + "src/client/models/list_objects_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/list_objects_request.py", "templateType": "SupportingFiles" }, - "client/models/list_relations_request.mustache": { + "src/client/models/list_relations_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/list_relations_request.py", "templateType": "SupportingFiles" }, - "client/models/list_users_request.mustache": { + "src/client/models/list_users_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/list_users_request.py", "templateType": "SupportingFiles" }, - "client/models/read_changes_request.mustache": { + "src/client/models/read_changes_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/read_changes_request.py", "templateType": "SupportingFiles" }, - "client/models/tuple.mustache": { + "src/client/models/tuple.py.mustache": { "destinationFilename": "openfga_sdk/client/models/tuple.py", "templateType": "SupportingFiles" }, - "client/models/write_request.mustache": { + "src/client/models/write_request.py.mustache": { "destinationFilename": "openfga_sdk/client/models/write_request.py", "templateType": "SupportingFiles" }, - "client/models/write_response.mustache": { + "src/client/models/write_response.py.mustache": { "destinationFilename": "openfga_sdk/client/models/write_response.py", "templateType": "SupportingFiles" }, - "client/models/write_single_response.mustache": { + "src/client/models/write_single_response.py.mustache": { "destinationFilename": "openfga_sdk/client/models/write_single_response.py", "templateType": "SupportingFiles" }, - "client/models/write_transaction_opts.mustache": { + "src/client/models/write_transaction_opts.py.mustache": { "destinationFilename": "openfga_sdk/client/models/write_transaction_opts.py", "templateType": "SupportingFiles" }, - "configuration.mustache": { - "destinationFilename": "openfga_sdk/configuration.py", + "/src/models/__init__.py.mustache": { + "destinationFilename": "openfga_sdk/models/__init__.py", "templateType": "SupportingFiles" }, - "test_configuration.mustache": { - "destinationFilename": "test/test_configuration.py", + "/src/sync/client/__init__.py.mustache": { + "destinationFilename": "openfga_sdk/sync/client/__init__.py", "templateType": "SupportingFiles" }, - "validation.mustache": { - "destinationFilename": "openfga_sdk/validation.py", + "src/sync/client/client.py.mustache": { + "destinationFilename": "openfga_sdk/sync/client/client.py", "templateType": "SupportingFiles" }, - "test_validation.mustache": { - "destinationFilename": "test/test_validation.py", + "src/sync/__init__.py.mustache": { + "destinationFilename": "openfga_sdk/sync/__init__.py", "templateType": "SupportingFiles" }, - "__init__sync.mustache": { - "destinationFilename": "openfga_sdk/sync/__init__.py", + "src/sync/api_client.py.mustache": { + "destinationFilename": "openfga_sdk/sync/api_client.py", "templateType": "SupportingFiles" }, - "__init__sync_client.mustache": { - "destinationFilename": "openfga_sdk/sync/client/__init__.py", + "src/sync/oauth2.py.mustache": { + "destinationFilename": "openfga_sdk/sync/oauth2.py", "templateType": "SupportingFiles" }, - "rest_sync.mustache": { + "src/sync/rest.py.mustache": { "destinationFilename": "openfga_sdk/sync/rest.py", "templateType": "SupportingFiles" }, - "api_client_sync.mustache": { - "destinationFilename": "openfga_sdk/sync/api_client.py", + "src/telemetry/__init__.py.mustache": { + "destinationFilename": "openfga_sdk/telemetry/__init__.py", "templateType": "SupportingFiles" }, - "api_sync.mustache": { - "folder": "openfga_sdk/sync", - "destinationFilename": "_sync.py", - "templateType": "API" + "src/telemetry/attributes.py.mustache": { + "destinationFilename": "openfga_sdk/telemetry/attributes.py", + "templateType": "SupportingFiles" }, - "api_test_sync.mustache": { - "destinationFilename": "test/test_open_fga_api_sync.py", + "src/telemetry/counters.py.mustache": { + "destinationFilename": "openfga_sdk/telemetry/counters.py", "templateType": "SupportingFiles" }, - "client/client_sync.mustache": { - "destinationFilename": "openfga_sdk/sync/client/client.py", + "src/telemetry/histograms.py.mustache": { + "destinationFilename": "openfga_sdk/telemetry/histograms.py", + "templateType": "SupportingFiles" + }, + "src/telemetry/metrics.py.mustache": { + "destinationFilename": "openfga_sdk/telemetry/metrics.py", + "templateType": "SupportingFiles" + }, + "src/telemetry/telemetry.py.mustache": { + "destinationFilename": "openfga_sdk/telemetry/telemetry.py", + "templateType": "SupportingFiles" + }, + "src/__init__.py.mustache": { + "destinationFilename": "openfga_sdk/__init__.py", + "templateType": "SupportingFiles" + }, + "src/api_client.py.mustache": { + "destinationFilename": "openfga_sdk/api_client.py", + "templateType": "SupportingFiles" + }, + "src/configuration.py.mustache": { + "destinationFilename": "openfga_sdk/configuration.py", + "templateType": "SupportingFiles" + }, + "src/credentials.py.mustache": { + "destinationFilename": "openfga_sdk/credentials.py", + "templateType": "SupportingFiles" + }, + "src/exceptions.py.mustache": { + "destinationFilename": "openfga_sdk/exceptions.py", "templateType": "SupportingFiles" }, - "client/test_client_sync.mustache": { - "destinationFilename": "test/test_client_sync.py", + "src/help.py.mustache": { + "destinationFilename": "openfga_sdk/help.py", "templateType": "SupportingFiles" }, - "oauth2.mustache": { + "src/oauth2.py.mustache": { "destinationFilename": "openfga_sdk/oauth2.py", "templateType": "SupportingFiles" }, - "oauth2_test.mustache": { - "destinationFilename": "test/test_oauth2.py", + "src/rest.py.mustache": { + "destinationFilename": "openfga_sdk/rest.py", "templateType": "SupportingFiles" }, - "oauth2_sync.mustache": { - "destinationFilename": "openfga_sdk/sync/oauth2.py", + "src/validation.py.mustache": { + "destinationFilename": "openfga_sdk/validation.py", "templateType": "SupportingFiles" }, - "oauth2_test_sync.mustache": { - "destinationFilename": "test/test_oauth2_sync.py", + + "test/api/__init__.py.mustache": { + "destinationFilename": "test/api/__init__.py", "templateType": "SupportingFiles" }, - "help.py": { - "destinationFilename": "openfga_sdk/help.py" + "test/client/__init__.py.mustache": { + "destinationFilename": "test/client/__init__.py", + "templateType": "SupportingFiles" }, - "example/Makefile": { - "destinationFilename": "example/Makefile", + "test/client/client_test.py.mustache": { + "destinationFilename": "test/client/client_test.py", "templateType": "SupportingFiles" }, - "example/README.md": { - "destinationFilename": "example/README.md", + "test/sync/client/__init__.py.mustache": { + "destinationFilename": "test/sync/client/__init__.py", "templateType": "SupportingFiles" }, - "example/example1/example1.py": { - "destinationFilename": "example/example1/example1.py", + "test/sync/client/client_test.py.mustache": { + "destinationFilename": "test/sync/client/client_test.py", "templateType": "SupportingFiles" }, - "example/example1/requirements.txt.mustache": { - "destinationFilename": "example/example1/requirements.txt", + "test/sync/__init__.py.mustache": { + "destinationFilename": "test/sync/__init__.py", "templateType": "SupportingFiles" }, - "example/example1/setup.py": { - "destinationFilename": "example/example1/setup.py", + "test/sync/api_test.py.mustache": { + "destinationFilename": "test/sync/open_fga_api_test.py", + "templateType": "SupportingFiles" + }, + "test/sync/oauth2_test.py.mustache": { + "destinationFilename": "test/sync/oauth2_test.py", + "templateType": "SupportingFiles" + }, + "test/__init__.py.mustache": { + "destinationFilename": "test/__init__.py", + "templateType": "SupportingFiles" + }, + "test/configuration_test.py.mustache": { + "destinationFilename": "test/_/configuration_test.py", + "templateType": "SupportingFiles" + }, + "test/credentials_test.py.mustache": { + "destinationFilename": "test/_/credentials_test.py", + "templateType": "SupportingFiles" + }, + "test/oauth2_test.py.mustache": { + "destinationFilename": "test/_/oauth2_test.py", "templateType": "SupportingFiles" }, - "example/example1/setup.cfg": { - "destinationFilename": "example/example1/setup.cfg", + "test/validation_test.py.mustache": { + "destinationFilename": "test/_/validation_test.py", "templateType": "SupportingFiles" } } diff --git a/config/clients/python/patches/open_fga_api.py.patch b/config/clients/python/patches/open_fga_api.py.patch index 76127bcf..d331bb20 100644 --- a/config/clients/python/patches/open_fga_api.py.patch +++ b/config/clients/python/patches/open_fga_api.py.patch @@ -1,9 +1,6 @@ --- clients/fga-python-sdk/openfga_sdk/api/open_fga_api.py 2022-09-13 14:15:46.000000000 -0400 +++ open_fga_api.py 2022-09-13 14:14:01.000000000 -0400 -@@ -193,13 +193,15 @@ - _request_auth=local_var_params.get('_request_auth'), - _oauth2_client=self._oauth2_client)) - +@@ -229,8 +236,10 @@ - async def create_store(self, **kwargs): + async def create_store(self, body, **kwargs): """Create a store @@ -16,8 +13,6 @@ + :param body: (required) + :type body: CreateStoreRequest :param async_req: Whether to execute the request asynchronously. - :type async_req: bool, optional - :param _preload_content: if False, the urllib3.HTTPResponse object will @@ -216,15 +218,17 @@ :rtype: CreateStoreResponse """ @@ -48,21 +43,13 @@ ] all_params.extend( [ -@@ -312,7 +318,7 @@ - } - +@@ -368,3 +370,3 @@ return await (self.api_client.call_api( - '/stores'.replace('{store_id}', store_id), 'POST', + '/stores', 'POST', path_params, - query_params, - header_params, -@@ -998,7 +1004,7 @@ - } - +@@ -1192,3 +1194,3 @@ return await (self.api_client.call_api( - '/stores'.replace('{store_id}', store_id), 'GET', + '/stores', 'GET', path_params, - query_params, - header_params, diff --git a/config/clients/python/patches/open_fga_api_sync.py.patch b/config/clients/python/patches/open_fga_api_sync.py.patch index e5000b3b..7ebef365 100644 --- a/config/clients/python/patches/open_fga_api_sync.py.patch +++ b/config/clients/python/patches/open_fga_api_sync.py.patch @@ -1,9 +1,6 @@ --- clients/fga-python-sdk/openfga_sdk/sync/open_fga_api.py 2022-09-13 14:15:46.000000000 -0400 +++ open_fga_api.py 2022-09-13 14:14:01.000000000 -0400 -@@ -193,13 +193,15 @@ - _request_auth=local_var_params.get('_request_auth'), - _oauth2_client=self._oauth2_client) - +@@ -229,8 +236,10 @@ - def create_store(self, **kwargs): + def create_store(self, body, **kwargs): """Create a store @@ -16,8 +13,6 @@ + :param body: (required) + :type body: CreateStoreRequest :param async_req: Whether to execute the request asynchronously. - :type async_req: bool, optional - :param _preload_content: if False, the urllib3.HTTPResponse object will @@ -216,15 +218,17 @@ :rtype: CreateStoreResponse """ @@ -48,21 +43,13 @@ ] all_params.extend( [ -@@ -312,7 +318,7 @@ - } - +@@ -368,3 +370,3 @@ return self.api_client.call_api( - '/stores'.replace('{store_id}', store_id), 'POST', + '/stores', 'POST', path_params, - query_params, - header_params, -@@ -998,7 +1004,7 @@ - } - +@@ -1192,3 +1194,3 @@ return self.api_client.call_api( - '/stores'.replace('{store_id}', store_id), 'GET', + '/stores', 'GET', path_params, - query_params, - header_params, diff --git a/config/clients/python/template/.github/workflows/main.yaml.mustache b/config/clients/python/template/.github/workflows/main.yaml.mustache index 7a936518..ddb65b14 100644 --- a/config/clients/python/template/.github/workflows/main.yaml.mustache +++ b/config/clients/python/template/.github/workflows/main.yaml.mustache @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 @@ -60,7 +60,7 @@ jobs: - if: matrix.python-version == '3.10' name: Upload coverage to Codecov - uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 continue-on-error: true with: token: ${{ secrets.CODECOV_TOKEN }} @@ -75,7 +75,7 @@ jobs: id-token: write # Required for PyPI trusted publishing steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 @@ -98,7 +98,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish package - uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 + uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 create-release: runs-on: ubuntu-latest @@ -106,7 +106,7 @@ jobs: needs: [publish] steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index 854c382b..978d4247 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -7,7 +7,7 @@ Get a paginated list of stores. [API Documentation]({{apiDocsUrl}}/docs/api#/Stores/ListStores) ```python -# from openfga_sdk.sync import OpenFgaClient +# from {{packageName}}.sync import OpenFgaClient # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -26,7 +26,7 @@ Create and initialize a store. [API Documentation]({{apiDocsUrl}}/docs/api#/Stores/CreateStore) ```python -# from openfga_sdk import CreateStoreRequest, OpenFgaClient +# from {{packageName}} import CreateStoreRequest, OpenFgaClient # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -48,7 +48,7 @@ Get information about the current store. > Requires a client initialized with a storeId ```python -# from openfga_sdk import OpenFgaClient +# from {{packageName}} import OpenFgaClient # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -67,7 +67,7 @@ Delete a store. > Requires a client initialized with a storeId ```python -# from openfga_sdk import OpenFgaClient +# from {{packageName}} import OpenFgaClient # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -85,7 +85,7 @@ Read all authorization models in the store. [API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModels) ```python -# from openfga_sdk import OpenFgaClient +# from {{packageName}} import OpenFgaClient # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -109,7 +109,7 @@ Create a new authorization model. > You can use the [{{appTitleCaseName}} Syntax Transformer](https://github.com/openfga/syntax-transformer) to convert between the friendly DSL and the JSON authorization model. ```python -# from openfga_sdk import ( +# from {{packageName}} import ( # Condition, ConditionParamTypeRef, Metadata, ObjectRelation, OpenFgaClient, RelationMetadata, # RelationReference, TypeDefinition, Userset, Usersets, WriteAuthorizationModelRequest # ) @@ -189,7 +189,7 @@ Read a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModel) ```python -# from openfga_sdk import OpenFgaClient +# from {{packageName}} import OpenFgaClient # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -210,7 +210,7 @@ Reads the latest authorization model (note: this ignores the model id in configu [API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModel) ```python -# from openfga_sdk import ClientConfiguration, OpenFgaClient +# from {{packageName}} import ClientConfiguration, OpenFgaClient # Create the cofiguration object # configuration = ClientConfiguration( @@ -236,8 +236,8 @@ Reads the list of historical relationship tuple writes and deletes. [API Documentation]({{apiDocsUrl}}#/Relationship%20Tuples/ReadChanges) ```python -# from openfga_sdk import OpenFgaClient -# from openfga_sdk.client.models import ClientReadChangesRequest +# from {{packageName}} import OpenFgaClient +# from {{packageName}}.client.models import ClientReadChangesRequest # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -261,7 +261,7 @@ Reads the relationship tuples stored in the database. It does not evaluate nor e [API Documentation]({{apiDocsUrl}}#/Relationship%20Tuples/Read) ```python -# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey +# from {{packageName}} import OpenFgaClient, ReadRequestTupleKey # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -278,7 +278,7 @@ response = await fga_client.read(body) ``` ```python -# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey +# from {{packageName}} import OpenFgaClient, ReadRequestTupleKey # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -295,7 +295,7 @@ response = await fga_client.read(body) ``` ```python -# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey +# from {{packageName}} import OpenFgaClient, ReadRequestTupleKey # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -312,7 +312,7 @@ response = await fga_client.read(body) ``` ```python -# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey +# from {{packageName}} import OpenFgaClient, ReadRequestTupleKey # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -327,7 +327,7 @@ response = await fga_client.read(body) ``` ```python -# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey +# from {{packageName}} import OpenFgaClient, ReadRequestTupleKey # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -350,8 +350,8 @@ Create and/or delete relationship tuples to update the system state. By default, write runs in a transaction mode where any invalid operation (deleting a non-existing tuple, creating an existing tuple, one of the tuples was invalid) or a server error will fail the entire operation. ```python -# from openfga_sdk import OpenFgaClient, RelationshipCondition -# from openfga_sdk.client.models import ClientTuple, ClientWriteRequest +# from {{packageName}} import OpenFgaClient, RelationshipCondition +# from {{packageName}}.client.models import ClientTuple, ClientWriteRequest # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -399,8 +399,8 @@ Convenience `write_tuples` and `delete_tuples` methods are also available. The SDK will split the writes into separate requests and send them sequentially to avoid violating rate limits. ```python -# from openfga_sdk import OpenFgaClient, RelationshipCondition -# from openfga_sdk.client.models import ClientTuple, ClientWriteRequest, WriteTransactionOpts +# from {{packageName}} import OpenFgaClient, RelationshipCondition +# from {{packageName}}.client.models import ClientTuple, ClientWriteRequest, WriteTransactionOpts # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -455,8 +455,8 @@ Check if a user has a particular relation with an object. [API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/Check) ```python -# from openfga_sdk import OpenFgaClient -# from openfga_sdk.client import ClientCheckRequest +# from {{packageName}} import OpenFgaClient +# from {{packageName}}.client import ClientCheckRequest # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -485,9 +485,9 @@ Run a set of [checks](#check). Batch Check will return `allowed: false` if it en If 429s or 5xxs are encountered, the underlying check will retry up to {{defaultMaxRetry}} times before giving up. ```python -# from openfga_sdk import OpenFgaClient -# from openfga_sdk.client import ClientCheckRequest -# from openfga_sdk.client.models import ClientTuple +# from {{packageName}} import OpenFgaClient +# from {{packageName}}.client import ClientCheckRequest +# from {{packageName}}.client.models import ClientTuple # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -584,8 +584,8 @@ Expands the relationships in userset tree format. [API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/Expand) ```python -# from openfga_sdk import OpenFgaClient -# from openfga_sdk.client.models import ClientExpandRequest +# from {{packageName}} import OpenFgaClient +# from {{packageName}}.client.models import ClientExpandRequest # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -610,8 +610,8 @@ List the objects of a particular type a user has access to. [API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/ListObjects) ```python -# from openfga_sdk import OpenFgaClient -# from openfga_sdk.client.models import ClientListObjectsRequest, ClientTuple +# from {{packageName}} import OpenFgaClient +# from {{packageName}}.client.models import ClientListObjectsRequest, ClientTuple # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -645,8 +645,8 @@ response = await fga_client.list_objects(body) List the relations a user has on an object. ```python -# from openfga_sdk import OpenFgaClient -# from openfga_sdk.client.models import ClientListRelationsRequest +# from {{packageName}} import OpenFgaClient +# from {{packageName}}.client.models import ClientListRelationsRequest # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -683,9 +683,9 @@ List the users who have a certain relation to a particular type. [API Documentation](https://openfga.dev/api/service#/Relationship%20Queries/ListUsers) ```python -from openfga_sdk import OpenFgaClient -from openfga_sdk.models.fga_object import FgaObject -from openfga_sdk.client.models import ClientListUsersRequest, ClientTuple +from {{packageName}} import OpenFgaClient +from {{packageName}}.models.fga_object import FgaObject +from {{packageName}}.client.models import ClientListUsersRequest, ClientTuple configuration = ClientConfiguration( api_url=FGA_API_URL, @@ -733,7 +733,7 @@ Read assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Read%20Assertions) ```python -# from openfga_sdk import OpenFgaClient +# from {{packageName}} import OpenFgaClient # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -753,8 +753,8 @@ Update the assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Write%20Assertions) ```python -# from openfga_sdk import OpenFgaClient -# from openfga_sdk.client.models import ClientAssertion +# from {{packageName}} import OpenFgaClient +# from {{packageName}}.client.models import ClientAssertion # Initialize the fga_client # fga_client = OpenFgaClient(configuration) diff --git a/config/clients/python/template/api.mustache b/config/clients/python/template/api.mustache index efa0c5a5..737a6d46 100644 --- a/config/clients/python/template/api.mustache +++ b/config/clients/python/template/api.mustache @@ -1,348 +1 @@ -{{>partial_header}} - - -from {{packageName}}.exceptions import ApiValueError, FgaValidationException -from {{packageName}}.api_client import ApiClient -from {{packageName}}.oauth2 import OAuth2Client - - -{{#operations}} -class {{classname}}: - """NOTE: This class is auto generated by OpenAPI Generator - Ref: https://openapi-generator.tech - - Do not edit the class manually. - """ - - def __init__(self, api_client=None): - if api_client is None: - api_client = ApiClient() - self.api_client = api_client - - self._oauth2_client = None - if api_client.configuration is not None: - credentials = api_client.configuration.credentials - if credentials is not None and credentials.method == "client_credentials": - self._oauth2_client = OAuth2Client(credentials) - -{{#asyncio}} - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_value, traceback): - await self.close() - - async def close(self): - await self.api_client.close() -{{/asyncio}} -{{^asyncio}} - def __enter__(self): - return self - - def __exit__(self): - self.close() - - def close(self): - self.api_client.close() -{{/asyncio}} - -{{#operation}} - - {{#asyncio}}async {{/asyncio}}def {{operationId}}(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): - """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} - -{{#notes}} - {{{.}}} -{{/notes}} - -{{#sortParamsByRequiredFlag}} - >>> thread = {{#asyncio}}await {{/asyncio}}api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} -{{^sortParamsByRequiredFlag}} - >>> thread = {{#asyncio}}await {{/asyncio}}api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} - -{{#requiredParams}} -{{^-first}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) - :type {{paramName}}: {{dataType}} -{{/-first}} -{{/requiredParams}} -{{#optionalParams}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) - :type {{paramName}}: {{dataType}}, optional -{{/optionalParams}} - :param async_req: Whether to execute the request asynchronously. - :type async_req: bool, optional - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :type _preload_content: bool, optional - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :return: Returns the result object. - If the method is called asynchronously, - returns the request thread. - :rtype: {{returnType}}{{^returnType}}None{{/returnType}} - """ - kwargs["_return_http_data_only"] = True - return {{#asyncio}}await {{/asyncio}}self.{{operationId}}_with_http_info({{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs){{#asyncio}}{{/asyncio}} - - {{#asyncio}}async {{/asyncio}}def {{operationId}}_with_http_info(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): - """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} - -{{#notes}} - {{{.}}} -{{/notes}} - -{{#sortParamsByRequiredFlag}} - >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} -{{^sortParamsByRequiredFlag}} - >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} - -{{#requiredParams}} -{{^-first}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) - :type {{paramName}}: {{dataType}} -{{/-first}} -{{/requiredParams}} -{{#optionalParams}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) - :type {{paramName}}: {{dataType}}, optional -{{/optionalParams}} - :param async_req: Whether to execute the request asynchronously. - :type async_req: bool, optional - :param _return_http_data_only: response data without head status code - and headers - :type _return_http_data_only: bool, optional - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :type _preload_content: bool, optional - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :param _request_auth: set to override the auth_settings for an a single - request; this effectively ignores the authentication - in the spec for a single request. - :param _retry_param: if specified, override the retry parameters specified in configuration - :type _request_auth: dict, optional - :type _content_type: string, optional: force content-type for the request - :return: Returns the result object. - If the method is called asynchronously, - returns the request thread. - :rtype: {{#returnType}}tuple({{.}}, status_code(int), headers(HTTPHeaderDict)){{/returnType}}{{^returnType}}None{{/returnType}} - """ - - {{#servers.0}} - local_var_hosts = [ -{{#servers}} - '{{{url}}}'{{^-last}},{{/-last}} -{{/servers}} - ] - local_var_host = local_var_hosts[0] - if kwargs.get('_host_index'): - _host_index = int(kwargs.get('_host_index')) - if _host_index < 0 or _host_index >= len(local_var_hosts): - raise ApiValueError( - "Invalid host index. Must be 0 <= index < %s" - % len(local_var_host) - ) - local_var_host = local_var_hosts[_host_index] - {{/servers.0}} - local_var_params = locals() - - all_params = [ -{{#requiredParams}}{{^-first}} - '{{paramName}}'{{^-last}},{{/-last}} -{{/-first}}{{/requiredParams}} -{{#optionalParams}} - '{{paramName}}'{{^-last}},{{/-last}} -{{/optionalParams}} - ] - all_params.extend( - [ - 'async_req', - '_return_http_data_only', - '_preload_content', - '_request_timeout', - '_request_auth', - '_content_type', - '_headers', - '_retry_parms' - ] - ) - - for key, val in local_var_params['kwargs'].items(): - if key not in all_params{{#servers.0}} and key != "_host_index"{{/servers.0}}: - raise FgaValidationException( - "Got an unexpected keyword argument '%s'" - " to method {{operationId}}" % key - ) - local_var_params[key] = val - del local_var_params['kwargs'] -{{#allParams}} -{{^isNullable}} -{{#required}} -{{^-first}} - # verify the required parameter '{{paramName}}' is set - if self.api_client.client_side_validation and local_var_params.get('{{paramName}}') is None: - raise ApiValueError( - "Missing the required parameter `{{paramName}}` when calling `{{operationId}}`") -{{/-first}} -{{/required}} -{{/isNullable}} -{{/allParams}} - -{{#allParams}} -{{#hasValidation}} - {{#maxLength}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) > {{maxLength}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be less than or equal to `{{maxLength}}`") - {{/maxLength}} - {{#minLength}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) < {{minLength}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be greater than or equal to `{{minLength}}`") - {{/minLength}} - {{#maximum}} - if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}: - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value less than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}`{{maximum}}`") - {{/maximum}} - {{#minimum}} - if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}: - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}`{{minimum}}`") - {{/minimum}} - {{#pattern}} - if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and not re.search(r'{{{vendorExtensions.x-regex}}}', local_var_params['{{paramName}}']{{#vendorExtensions.x-modifiers}}{{#-first}}, flags={{/-first}}re.{{.}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-modifiers}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must conform to the pattern `{{{pattern}}}`") - {{/pattern}} - {{#maxItems}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) > {{maxItems}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be less than or equal to `{{maxItems}}`") - {{/maxItems}} - {{#minItems}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) < {{minItems}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be greater than or equal to `{{minItems}}`") - {{/minItems}} -{{/hasValidation}} -{{#-last}} -{{/-last}} -{{/allParams}} - collection_formats = {} - - path_params = {} -{{#pathParams}} -{{^-first}} - if '{{paramName}}' in local_var_params: - path_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/-first}} - -{{#-first}} - if self.api_client._get_store_id() is None: - raise ApiValueError( - "Store ID expected in api_client's configuration when calling `{{operationId}}`") - store_id = self.api_client._get_store_id() -{{/-first}} - -{{/pathParams}} - - query_params = [] -{{#queryParams}} - if local_var_params.get('{{paramName}}') is not None: - query_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/queryParams}} - - header_params = dict(local_var_params.get('_headers', {})) -{{#headerParams}} - if '{{paramName}}' in local_var_params: - header_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/headerParams}} - - form_params = [] - local_var_files = {} -{{#formParams}} - if '{{paramName}}' in local_var_params: - {{^isFile}}form_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{/isFile}}{{#isFile}}local_var_files['{{baseName}}'] = local_var_params['{{paramName}}']{{/isFile}}{{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/formParams}} - - body_params = None -{{#bodyParam}} - if '{{paramName}}' in local_var_params: - body_params = local_var_params['{{paramName}}'] -{{/bodyParam}} - {{#hasProduces}} - # HTTP header `Accept` - header_params['Accept'] = self.api_client.select_header_accept( - [{{#produces}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}]) - - {{/hasProduces}} - {{#hasConsumes}} - # HTTP header `Content-Type` - content_types_list = local_var_params.get('_content_type', self.api_client.select_header_content_type([{{#consumes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/consumes}}],'{{httpMethod}}', body_params)) - if content_types_list: - header_params['Content-Type'] = content_types_list - - {{/hasConsumes}} - # Authentication setting - auth_settings = [{{#authMethods}}'{{name}}'{{^-last}}, {{/-last}}{{/authMethods}}] - - {{#returnType}} - {{#responses}} - {{#-first}} - response_types_map = { - {{/-first}} - {{^isWildcard}} - {{code}}: {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}}, - {{/isWildcard}} - {{#-last}} - } - {{/-last}} - {{/responses}} - {{/returnType}} - {{^returnType}} - response_types_map = {} - {{/returnType}} - - return {{#asyncio}}await ({{/asyncio}}self.api_client.call_api( - '{{{path}}}'.replace('{store_id}', store_id), '{{httpMethod}}', - path_params, - query_params, - header_params, - body=body_params, - post_params=form_params, - files=local_var_files, - response_types_map=response_types_map, - auth_settings=auth_settings, - async_req=local_var_params.get('async_req'), - _return_http_data_only=local_var_params.get('_return_http_data_only'), - _preload_content=local_var_params.get('_preload_content', True), - _request_timeout=local_var_params.get('_request_timeout'), - _retry_params=local_var_params.get('_retry_params'), - {{#servers.0}} - _host=local_var_host, - {{/servers.0}} - collection_formats=collection_formats, - _request_auth=local_var_params.get('_request_auth'), - _oauth2_client=self._oauth2_client){{#asyncio}}){{/asyncio}} -{{/operation}} -{{/operations}} +{{>src/api.py}} diff --git a/config/clients/python/template/api_sync.mustache b/config/clients/python/template/api_sync.mustache index d28c1065..cb79be78 100644 --- a/config/clients/python/template/api_sync.mustache +++ b/config/clients/python/template/api_sync.mustache @@ -1,341 +1 @@ -{{>partial_header}} - -import re - -from {{packageName}}.sync.api_client import ApiClient -from {{packageName}}.sync.oauth2 import OAuth2Client -from {{packageName}}.exceptions import ( - FgaValidationException, - ApiValueError -) - - -{{#operations}} -class {{classname}}: - """NOTE: This class is auto generated by OpenAPI Generator - Ref: https://openapi-generator.tech - - Do not edit the class manually. - """ - - def __init__(self, api_client=None): - if api_client is None: - api_client = ApiClient() - self.api_client = api_client - - self._oauth2_client = None - if api_client.configuration is not None: - credentials = api_client.configuration.credentials - if credentials is not None and credentials.method == "client_credentials": - self._oauth2_client = OAuth2Client(credentials) - - - def __enter__(self): - return self - - def __exit__(self): - self.close() - - def close(self): - self.api_client.close() - -{{#operation}} - - def {{operationId}}(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): - """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} - -{{#notes}} - {{{.}}} -{{/notes}} - -{{#sortParamsByRequiredFlag}} - >>> thread = api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} -{{^sortParamsByRequiredFlag}} - >>> thread = api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} - -{{#requiredParams}} -{{^-first}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) - :type {{paramName}}: {{dataType}} -{{/-first}} -{{/requiredParams}} -{{#optionalParams}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) - :type {{paramName}}: {{dataType}}, optional -{{/optionalParams}} - :param async_req: Whether to execute the request asynchronously. - :type async_req: bool, optional - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :type _preload_content: bool, optional - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :return: Returns the result object. - If the method is called asynchronously, - returns the request thread. - :rtype: {{returnType}}{{^returnType}}None{{/returnType}} - """ - kwargs["_return_http_data_only"] = True - return self.{{operationId}}_with_http_info({{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs) - - def {{operationId}}_with_http_info(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): - """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} - -{{#notes}} - {{{.}}} -{{/notes}} - -{{#sortParamsByRequiredFlag}} - >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} -{{^sortParamsByRequiredFlag}} - >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) -{{/sortParamsByRequiredFlag}} - -{{#requiredParams}} -{{^-first}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) - :type {{paramName}}: {{dataType}} -{{/-first}} -{{/requiredParams}} -{{#optionalParams}} - :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) - :type {{paramName}}: {{dataType}}, optional -{{/optionalParams}} - :param async_req: Whether to execute the request asynchronously. - :type async_req: bool, optional - :param _return_http_data_only: response data without head status code - and headers - :type _return_http_data_only: bool, optional - :param _preload_content: if False, the urllib3.HTTPResponse object will - be returned without reading/decoding response - data. Default is True. - :type _preload_content: bool, optional - :param _request_timeout: timeout setting for this request. If one - number provided, it will be total request - timeout. It can also be a pair (tuple) of - (connection, read) timeouts. - :param _request_auth: set to override the auth_settings for an a single - request; this effectively ignores the authentication - in the spec for a single request. - :param _retry_param: if specified, override the retry parameters specified in configuration - :type _request_auth: dict, optional - :type _content_type: string, optional: force content-type for the request - :return: Returns the result object. - If the method is called asynchronously, - returns the request thread. - :rtype: {{#returnType}}tuple({{.}}, status_code(int), headers(HTTPHeaderDict)){{/returnType}}{{^returnType}}None{{/returnType}} - """ - - {{#servers.0}} - local_var_hosts = [ -{{#servers}} - '{{{url}}}'{{^-last}},{{/-last}} -{{/servers}} - ] - local_var_host = local_var_hosts[0] - if kwargs.get('_host_index'): - _host_index = int(kwargs.get('_host_index')) - if _host_index < 0 or _host_index >= len(local_var_hosts): - raise ApiValueError( - "Invalid host index. Must be 0 <= index < %s" - % len(local_var_host) - ) - local_var_host = local_var_hosts[_host_index] - {{/servers.0}} - local_var_params = locals() - - all_params = [ -{{#requiredParams}}{{^-first}} - '{{paramName}}'{{^-last}},{{/-last}} -{{/-first}}{{/requiredParams}} -{{#optionalParams}} - '{{paramName}}'{{^-last}},{{/-last}} -{{/optionalParams}} - ] - all_params.extend( - [ - "async_req", - "_return_http_data_only", - "_preload_content", - "_request_timeout", - "_request_auth", - "_content_type", - "_headers", - "_retry_parms" - ] - ) - - for key, val in local_var_params['kwargs'].items(): - if key not in all_params{{#servers.0}} and key != "_host_index"{{/servers.0}}: - raise FgaValidationException( - "Got an unexpected keyword argument '%s'" - " to method {{operationId}}" % key - ) - local_var_params[key] = val - del local_var_params['kwargs'] -{{#allParams}} -{{^isNullable}} -{{#required}} -{{^-first}} - # verify the required parameter '{{paramName}}' is set - if self.api_client.client_side_validation and local_var_params.get('{{paramName}}') is None: - raise ApiValueError( - "Missing the required parameter `{{paramName}}` when calling `{{operationId}}`") -{{/-first}} -{{/required}} -{{/isNullable}} -{{/allParams}} - -{{#allParams}} -{{#hasValidation}} - {{#maxLength}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) > {{maxLength}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be less than or equal to `{{maxLength}}`") - {{/maxLength}} - {{#minLength}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) < {{minLength}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be greater than or equal to `{{minLength}}`") - {{/minLength}} - {{#maximum}} - if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}: - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value less than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}`{{maximum}}`") - {{/maximum}} - {{#minimum}} - if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}: - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}`{{minimum}}`") - {{/minimum}} - {{#pattern}} - if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and not re.search(r'{{{vendorExtensions.x-regex}}}', local_var_params['{{paramName}}']{{#vendorExtensions.x-modifiers}}{{#-first}}, flags={{/-first}}re.{{.}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-modifiers}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must conform to the pattern `{{{pattern}}}`") - {{/pattern}} - {{#maxItems}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) > {{maxItems}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be less than or equal to `{{maxItems}}`") - {{/maxItems}} - {{#minItems}} - if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and - len(local_var_params['{{paramName}}']) < {{minItems}}): - raise ApiValueError( - "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be greater than or equal to `{{minItems}}`") - {{/minItems}} -{{/hasValidation}} -{{#-last}} -{{/-last}} -{{/allParams}} - collection_formats = {} - - path_params = {} -{{#pathParams}} -{{^-first}} - if '{{paramName}}' in local_var_params: - path_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/-first}} - -{{#-first}} - if self.api_client._get_store_id() is None: - raise ApiValueError("Store ID expected in api_client's configuration when calling `{{operationId}}`") - store_id = self.api_client._get_store_id() -{{/-first}} - -{{/pathParams}} - - query_params = [] -{{#queryParams}} - if local_var_params.get('{{paramName}}') is not None: - query_params.append( - ('{{baseName}}', local_var_params['{{paramName}}'])){{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/queryParams}} - - header_params = dict(local_var_params.get('_headers', {})) -{{#headerParams}} - if '{{paramName}}' in local_var_params: - header_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/headerParams}} - - form_params = [] - local_var_files = {} -{{#formParams}} - if '{{paramName}}' in local_var_params: - {{^isFile}}form_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{/isFile}}{{#isFile}}local_var_files['{{baseName}}'] = local_var_params['{{paramName}}']{{/isFile}}{{#isArray}} - collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} -{{/formParams}} - - body_params = None -{{#bodyParam}} - if '{{paramName}}' in local_var_params: - body_params = local_var_params['{{paramName}}'] -{{/bodyParam}} - {{#hasProduces}} - # HTTP header `Accept` - header_params['Accept'] = self.api_client.select_header_accept( - [{{#produces}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}]) - - {{/hasProduces}} - {{#hasConsumes}} - # HTTP header `Content-Type` - content_types_list = local_var_params.get('_content_type', self.api_client.select_header_content_type([{{#consumes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/consumes}}],'{{httpMethod}}', body_params)) - if content_types_list: - header_params['Content-Type'] = content_types_list - - {{/hasConsumes}} - # Authentication setting - auth_settings = [{{#authMethods}}'{{name}}'{{^-last}}, {{/-last}}{{/authMethods}}] - - {{#returnType}} - {{#responses}} - {{#-first}} - response_types_map = { - {{/-first}} - {{^isWildcard}} - {{code}}: {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}}, - {{/isWildcard}} - {{#-last}} - } - {{/-last}} - {{/responses}} - {{/returnType}} - {{^returnType}} - response_types_map = {} - {{/returnType}} - - return self.api_client.call_api( - '{{{path}}}'.replace('{store_id}', store_id), '{{httpMethod}}', - path_params, - query_params, - header_params, - body=body_params, - post_params=form_params, - files=local_var_files, - response_types_map=response_types_map, - auth_settings=auth_settings, - async_req=local_var_params.get('async_req'), - _return_http_data_only=local_var_params.get('_return_http_data_only'), - _preload_content=local_var_params.get('_preload_content', True), - _request_timeout=local_var_params.get('_request_timeout'), - _retry_params=local_var_params.get('_retry_params'), - {{#servers.0}} - _host=local_var_host, - {{/servers.0}} - collection_formats=collection_formats, - _request_auth=local_var_params.get('_request_auth'), - _oauth2_client=self._oauth2_client) -{{/operation}} -{{/operations}} +{{>src/sync/api.py}} diff --git a/config/clients/python/template/api_test.mustache b/config/clients/python/template/api_test.mustache index 7306304c..61a54b93 100644 --- a/config/clients/python/template/api_test.mustache +++ b/config/clients/python/template/api_test.mustache @@ -1,1430 +1 @@ -{{>partial_header}} - -import unittest -from unittest.mock import ANY -from unittest import IsolatedAsyncioTestCase -from unittest.mock import patch -from datetime import datetime - -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}}.models.assertion import Assertion -from {{packageName}}.models.authorization_model import AuthorizationModel -from {{packageName}}.models.check_request import CheckRequest -from {{packageName}}.models.check_response import CheckResponse -from {{packageName}}.models.create_store_request import CreateStoreRequest -from {{packageName}}.models.create_store_response import CreateStoreResponse -from {{packageName}}.models.error_code import ErrorCode -from {{packageName}}.models.expand_request import ExpandRequest -from {{packageName}}.models.expand_request_tuple_key import ExpandRequestTupleKey -from {{packageName}}.models.expand_response import ExpandResponse -from {{packageName}}.models.get_store_response import GetStoreResponse -from {{packageName}}.models.internal_error_code import InternalErrorCode -from {{packageName}}.models.internal_error_message_response import InternalErrorMessageResponse -from {{packageName}}.models.leaf import Leaf -from {{packageName}}.models.list_objects_request import ListObjectsRequest -from {{packageName}}.models.list_objects_response import ListObjectsResponse -from {{packageName}}.models.list_stores_response import ListStoresResponse -from {{packageName}}.models.list_users_request import ListUsersRequest -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.read_assertions_response import ReadAssertionsResponse -from {{packageName}}.models.read_authorization_model_response import ReadAuthorizationModelResponse -from {{packageName}}.models.read_changes_response import ReadChangesResponse -from {{packageName}}.models.read_request import ReadRequest -from {{packageName}}.models.read_request_tuple_key import ReadRequestTupleKey -from {{packageName}}.models.read_response import ReadResponse -from {{packageName}}.models.store import Store -from {{packageName}}.models.tuple import Tuple -from {{packageName}}.models.tuple_change import TupleChange -from {{packageName}}.models.tuple_key import TupleKey -from {{packageName}}.models.tuple_key_without_condition import TupleKeyWithoutCondition -from {{packageName}}.models.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.write_assertions_request import WriteAssertionsRequest -from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest -from {{packageName}}.models.write_authorization_model_response import WriteAuthorizationModelResponse -from {{packageName}}.models.write_request import WriteRequest - -store_id = '01H0H015178Y2V4CX10C2KGHF4' -request_id = 'x1y2z3' - -# Helper function to construct mock response -def http_mock_response(body, status): - headers = urllib3.response.HTTPHeaderDict({ - 'content-type': 'application/json', - 'Fga-Request-Id': request_id - }) - return urllib3.HTTPResponse( - body.encode('utf-8'), - headers, - status, - preload_content=False - ) - -def mock_response(body, status): - obj = http_mock_response(body, status) - return rest.RESTResponse(obj, obj.data) - -class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): - """{{classname}} unit test stubs""" - - def setUp(self): - self.configuration = {{packageName}}.Configuration( - api_url='http://api.{{sampleApiDomain}}', - ) - - def tearDown(self): - pass - - @patch.object(rest.RESTClientObject, 'request') - async def test_check(self, mock_request): - """Test case for check - - Check whether a user is authorized to access an object - """ - - # First, mock the response - response_body = '{"allowed": true, "resolution": "1234"}' - mock_request.return_value = mock_response(response_body, 200) - - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", - ) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( - body=body, - ) - self.assertIsInstance(api_response, CheckResponse) - self.assertTrue(api_response.allowed) - # Make sure the API was called with the right data - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', - headers=ANY, - query_params=[], - post_params=[], - body={"tuple_key": {"object": "document:2021-budget", - "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, - _preload_content=ANY, - _request_timeout=None - ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - @patch.object(rest.RESTClientObject, 'request') - async def test_create_store(self, mock_request): - """Test case for create_store - - Create a store - """ - 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 {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CreateStoreRequest( - name="test-store", - ) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.create_store( - body=body, - ) - self.assertIsInstance(api_response, CreateStoreResponse) - self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores', - headers=ANY, - query_params=[], - post_params=[], - body={"name": "test-store"}, - _preload_content=ANY, - _request_timeout=None - ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - @patch.object(rest.RESTClientObject, 'request') - async def test_delete_store(self, mock_request): - """Test case for delete_store - - Delete a store - """ - response_body = '' - mock_request.return_value = mock_response(response_body, 201) - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - 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', - headers=ANY, - query_params=[], - body=None, - _preload_content=ANY, - _request_timeout=None - ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - @patch.object(rest.RESTClientObject, 'request') - async def test_expand(self, mock_request): - """Test case for expand - - Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship - """ - 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 {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = ExpandRequest( - tuple_key=ExpandRequestTupleKey( - object="document:budget", - relation="reader", - ), - authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", - ) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.expand( - body=body, - ) - self.assertIsInstance(api_response, ExpandResponse) - cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) - leaf = Leaf(users=cur_users) - node = Node(name="document:budget#reader", leaf=leaf) - userTree = UsersetTree(node) - expected_response = ExpandResponse(userTree) - self.assertEqual(api_response, expected_response) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/expand', - headers=ANY, - query_params=[], - post_params=[], - body={"tuple_key": {"object": "document:budget", "relation": "reader"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, - _preload_content=ANY, - _request_timeout=None - ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - @patch.object(rest.RESTClientObject, 'request') - async def test_get_store(self, mock_request): - """Test case for get_store - - Get a store - """ - 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 - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - # 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') - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - @patch.object(rest.RESTClientObject, 'request') - async def test_list_objects(self, mock_request): - """Test case for list_objects - - List objects - """ - 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 {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = ListObjectsRequest( - authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", - type="document", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) - # Get all stores - api_response = {{#asyncio}}await {{/asyncio}}api_instance.list_objects(body) - self.assertIsInstance(api_response, ListObjectsResponse) - self.assertEqual(api_response.objects, ['document:abcd1234']) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-objects', - headers=ANY, - query_params=[], - post_params=[], - body={'authorization_model_id': '01G5JAVJ41T49E9TT3SKVS7X1J', - 'type': 'document', 'relation': 'reader', 'user': 'user:81684243-9356-4421-8fbf-a4f8d36aa31b'}, - _preload_content=ANY, - _request_timeout=None - ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - @patch.object(rest.RESTClientObject, 'request') - async def test_list_stores(self, mock_request): - """Test case for list_stores - - Get all stores - """ - response_body = ''' -{ - "stores": [ - { - "id": "01YCP46JKYM8FJCQ37NMBYHE5X", - "name": "store1", - "created_at": "2022-07-25T21:15:37.524Z", - "updated_at": "2022-07-25T21:15:37.524Z", - "deleted_at": "2022-07-25T21:15:37.524Z" - }, - { - "id": "01YCP46JKYM8FJCQ37NMBYHE6X", - "name": "store2", - "created_at": "2022-07-25T21:15:37.524Z", - "updated_at": "2022-07-25T21:15:37.524Z", - "deleted_at": "2022-07-25T21:15:37.524Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" -} - ''' - mock_request.return_value = mock_response(response_body, 200) - configuration = self.configuration - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - # Get all stores - api_response = {{#asyncio}}await {{/asyncio}}api_instance.list_stores( - page_size=1, - continuation_token="continuation_token_example", - ) - self.assertIsInstance(api_response, ListStoresResponse) - self.assertEqual(api_response.continuation_token, - "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") - store1 = Store( - id="01YCP46JKYM8FJCQ37NMBYHE5X", - name="store1", - created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), - updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), - deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), - ) - store2 = Store( - id="01YCP46JKYM8FJCQ37NMBYHE6X", - name="store2", - created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), - updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), - deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), - ) - - stores = [store1, store2] - self.assertEqual(api_response.stores, stores) - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores', - headers=ANY, - query_params=[('page_size', 1), ('continuation_token', - 'continuation_token_example')], - _preload_content=ANY, - _request_timeout=None - ) - {{#asyncio}}await {{/asyncio}}api_client.close() - - - @patch.object(rest.RESTClientObject, "request") - {{#asyncio}}async {{/asyncio}}def test_list_users(self, mock_request): - """ - Test case for list_users - """ - - response_body = """{ - "users": [ - { - "object": { - "id": "81684243-9356-4421-8fbf-a4f8d36aa31b", - "type": "user" - } - }, - { - "userset": { - "id": "fga", - "relation": "member", - "type": "team" - } - }, - { - "wildcard": { - "type": "user" - } - } - ] -}""" - - mock_request.return_value = mock_response(response_body, 200) - - configuration = self.configuration - configuration.store_id = store_id - - {{#asyncio}}async {{/asyncio}}with openfga_sdk.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - - request = ListUsersRequest( - authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", - object="document:2021-budget", - relation="can_read", - user_filters=[ - {"type": "user"}, - {"type": "team", "relation": "member"}, - ], - context={}, - contextual_tuples=[ - { - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - "relation": "editor", - "object": "folder:product", - }, - { - "user": "folder:product", - "relation": "parent", - "object": "document:roadmap", - }, - ], - ) - - response = {{#asyncio}}await {{/asyncio}}api_instance.list_users(request) - - self.assertIsInstance(response, ListUsersResponse) - - self.assertEqual(response.users.__len__(), 3) - - self.assertIsNotNone(response.users[0].object) - self.assertEqual( - response.users[0].object.id, "81684243-9356-4421-8fbf-a4f8d36aa31b" - ) - self.assertEqual(response.users[0].object.type, "user") - self.assertIsNone(response.users[0].userset) - self.assertIsNone(response.users[0].wildcard) - - self.assertIsNone(response.users[1].object) - self.assertIsNotNone(response.users[1].userset) - self.assertEqual(response.users[1].userset.id, "fga") - self.assertEqual(response.users[1].userset.relation, "member") - self.assertEqual(response.users[1].userset.type, "team") - self.assertIsNone(response.users[1].wildcard) - - self.assertIsNone(response.users[2].object) - self.assertIsNone(response.users[2].userset) - self.assertIsNotNone(response.users[2].wildcard) - self.assertEqual(response.users[2].wildcard.type, "user") - - mock_request.assert_called_once_with( - "POST", - "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-users", - headers=ANY, - query_params=[], - post_params=[], - body={ - "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", - "object": "document:2021-budget", - "relation": "can_read", - "user_filters": [ - {"type": "user"}, - {"type": "team", "relation": "member"}, - ], - "context": {}, - "contextual_tuples": [ - { - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - "relation": "editor", - "object": "folder:product", - }, - { - "user": "folder:product", - "relation": "parent", - "object": "document:roadmap", - }, - ], - }, - _preload_content=ANY, - _request_timeout=None, - ) - - await api_client.close() - - - @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 = ''' - { - "tuples": [ - { - "key": { - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - "relation": "reader", - "object": "document:2021-budget" - }, - "timestamp": "2021-10-06T15:32:11.128Z" - } - ], - "continuation_token": "" -} - ''' - mock_request.return_value = mock_response(response_body, 200) - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = ReadRequest( - tuple_key=ReadRequestTupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - page_size=50, - continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", - ) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.read( - body=body, - ) - self.assertIsInstance(api_response, ReadResponse) - key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",relation="reader",object="document:2021-budget") - timestamp = datetime.fromisoformat("2021-10-06T15:32:11.128+00:00") - expected_data = ReadResponse( - tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') - self.assertEqual(api_response, expected_data) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/read', - headers=ANY, - query_params=[], - post_params=[], - body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"},"page_size":50,"continuation_token":"eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="}, - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_read_assertions(self, mock_request): - """Test case for read_assertions - - Read assertions for an authorization model ID - """ - response_body = ''' -{ - "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", - "assertions": [ - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" - }, - "expectation": true - } - ] -} - ''' - mock_request.return_value = mock_response(response_body, 200) - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_assertions( - "01G5JAVJ41T49E9TT3SKVS7X1J", - ) - self.assertIsInstance(api_response, ReadAssertionsResponse) - self.assertEqual(api_response.authorization_model_id, '01G5JAVJ41T49E9TT3SKVS7X1J') - assertion=Assertion( - tuple_key=TupleKeyWithoutCondition( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - expectation=True, - ) - self.assertEqual(api_response.assertions, [assertion]) - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_read_authorization_model(self, mock_request): - """Test case for read_authorization_model - - Return a particular version of an authorization model - """ - response_body = ''' -{ - "authorization_model": { - "id": "01G5JAVJ41T49E9TT3SKVS7X1J", - "schema_version":"1.1", - "type_definitions": [ - { - "type": "document", - "relations": { - "reader": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "object": "", - "relation": "writer" - } - } - ] - } - }, - "writer": { - "this": {} - } - } - } - ] - } -} - ''' - mock_request.return_value = mock_response(response_body, 200) - configuration = self.configuration - configuration.store_id = store_id - # Enter a context with an instance of the API client - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = open_fga_api.OpenFgaApi(api_client) - - # Return a particular version of an authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_model( - "01G5JAVJ41T49E9TT3SKVS7X1J", - ) - self.assertIsInstance(api_response, ReadAuthorizationModelResponse) - type_definitions = [ - TypeDefinition( - type="document", - relations=dict( - reader=Userset( - union=Usersets( - child=[ - Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), - ], - ), - ), - writer=Userset( - this=dict(), - ), - ) - ) - ] - authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version = "1.1", - type_definitions=type_definitions) - self.assertEqual(api_response.authorization_model, authorization_model) - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', - headers=ANY, - query_params=[], - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_read_changes(self, mock_request): - """Test case for read_changes - - Return a list of all the tuple changes - """ - response_body = ''' -{ - "changes": [ - { - "tuple_key": { - "object": "document:2021-budget", - "relation": "reader", - "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" - }, - "operation": "TUPLE_OPERATION_WRITE", - "timestamp": "2022-07-26T15:55:55.809Z" - } - ], - "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" -} - ''' - mock_request.return_value = mock_response(response_body, 200) - configuration = self.configuration - configuration.store_id = store_id - # Enter a context with an instance of the API client - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = open_fga_api.OpenFgaApi(api_client) - - # Return a particular version of an authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_changes( - page_size=1, - continuation_token="abcdefg", - type="document" - ) - self.assertIsInstance(api_response, ReadChangesResponse) - changes = TupleChange( - tuple_key=TupleKey(object="document:2021-budget",relation="reader",user="user:81684243-9356-4421-8fbf-a4f8d36aa31b"), - operation=TupleOperation.WRITE, - timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00")) - read_changes = ReadChangesResponse( - continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', - changes=[changes]) - self.assertEqual(api_response, read_changes) - mock_request.assert_called_once_with( - 'GET', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/changes', - headers=ANY, - query_params=[('type', 'document'), ('page_size', 1), ('continuation_token', 'abcdefg') ], - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_write(self, mock_request): - """Test case for write - - Add tuples from the store - """ - response_body = '{}' - mock_request.return_value = mock_response(response_body, 200) - configuration = self.configuration - configuration.store_id = store_id - # Enter a context with an instance of the API client - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = open_fga_api.OpenFgaApi(api_client) - - # example passing only required values which don't have defaults set - - body = WriteRequest( - writes=WriteRequestWrites( - tuple_keys=[ - TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) - ], - ), - authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", - ) - {{#asyncio}}await {{/asyncio}}api_instance.write( - body, - ) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', - headers=ANY, - query_params=[], - post_params=[], - body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_write_delete(self, mock_request): - """Test case for write - - Delete tuples from the store - """ - response_body = '{}' - mock_request.return_value = mock_response(response_body, 200) - configuration = self.configuration - configuration.store_id = store_id - # Enter a context with an instance of the API client - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = open_fga_api.OpenFgaApi(api_client) - - # example passing only required values which don't have defaults set - - body = WriteRequest( - deletes=WriteRequestDeletes( - tuple_keys=[ - TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ) - ], - ), - authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", - ) - {{#asyncio}}await {{/asyncio}}api_instance.write( - body, - ) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', - headers=ANY, - query_params=[], - post_params=[], - body={"deletes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_write_assertions(self, mock_request): - """Test case for write_assertions - - Upsert assertions for an authorization model ID - """ - response_body = '' - mock_request.return_value = mock_response(response_body, 204) - configuration = self.configuration - configuration.store_id = store_id - # Enter a context with an instance of the API client - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = open_fga_api.OpenFgaApi(api_client) - - # example passing only required values which don't have defaults set - body = WriteAssertionsRequest( - assertions=[ - Assertion( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - expectation=True, - ) - ], - ) - # Upsert assertions for an authorization model ID - {{#asyncio}}await {{/asyncio}}api_instance.write_assertions( - authorization_model_id="xyz0123", - body=body, - ) - mock_request.assert_called_once_with( - 'PUT', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/xyz0123', - headers=ANY, - query_params=[], - post_params=[], - body={"assertions":[{"expectation":True,"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}]}, - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_write_authorization_model(self, mock_request): - """Test case for write_authorization_model - - Create a new authorization model - """ - response_body = '{"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}' - mock_request.return_value = mock_response(response_body, 201) - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = open_fga_api.OpenFgaApi(api_client) - - # example passing only required values which don't have defaults set - body = WriteAuthorizationModelRequest( - schema_version = "1.1", - type_definitions=[ - TypeDefinition( - type="document", - relations=dict( - writer=Userset( - this=dict(), - ), - reader=Userset( - union=Usersets( - child=[ - Userset(this=dict()), - Userset(computed_userset=ObjectRelation( - object="", - relation="writer", - )), - ], - ), - ), - ) - ), - ], - ) - # Create a new authorization model - api_response = {{#asyncio}}await {{/asyncio}}api_instance.write_authorization_model( - body - ) - self.assertIsInstance(api_response, WriteAuthorizationModelResponse) - expected_response = WriteAuthorizationModelResponse( - authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' - ) - self.assertEqual(api_response, expected_response) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models', - headers=ANY, - query_params=[], - post_params=[], - body={"schema_version":"1.1","type_definitions":[{"type":"document","relations":{"writer":{"this":{}},"reader":{"union":{"child":[{"this":{}},{"computedUserset":{"object":"","relation":"writer"}}]}}}}]}, - _preload_content=ANY, - _request_timeout=None - ) - - def test_default_scheme(self): - """ - Ensure default scheme is https - """ - configuration = {{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') - - def test_configuration_missing_host(self): - """ - Test whether FgaValidationException is raised if configuration does not have host specified - """ - 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.api_scheme = None - self.assertRaises(FgaValidationException, configuration.is_valid) - - def test_configuration_bad_scheme(self): - """ - Test whether ApiValueError is raised if scheme is bad - """ - configuration = {{packageName}}.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 = {{packageName}}.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 = {{packageName}}.Configuration( - api_host='localhost/mypath', - api_scheme='http' - ) - self.assertRaises(ApiValueError, configuration.is_valid) - - def test_configuration_has_query(self): - """ - Test whether ApiValueError is raised if host has query - """ - configuration = {{packageName}}.Configuration( - api_host='localhost?mypath=foo', - api_scheme='http' - ) - self.assertRaises(ApiValueError, configuration.is_valid) - - def test_configuration_store_id_invalid(self): - """ - Test whether ApiValueError is raised if host has query - """ - configuration = {{packageName}}.Configuration( - api_host='localhost', - api_scheme='http', - store_id="abcd" - ) - self.assertRaises(FgaValidationException, configuration.is_valid) - - def test_url(self): - """ - 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.is_valid() - - def test_url_with_scheme_and_host(self): - """ - 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' - ) - self.assertEqual(configuration.api_url, 'http://localhost:8080') - configuration.is_valid() # Should not throw and complain about scheme being invalid - - async def test_bad_configuration_read_authorization_model(self): - """ - Test whether FgaValidationException is raised for API (reading authorization models) - with configuration is having incorrect API scheme - """ - configuration = {{packageName}}.Configuration( - api_scheme = 'bad', - api_host = "api.{{sampleApiDomain}}", - ) - 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 - api_instance = open_fga_api.OpenFgaApi(api_client) - - # 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" - ) - - async def test_configuration_missing_storeid(self): - """ - Test whether FgaValidationException is raised for API (reading authorization models) - required store ID but configuration is missing store ID - """ - configuration = {{packageName}}.Configuration( - api_scheme = 'http', - api_host = "api.{{sampleApiDomain}}", - ) - # Notice the store_id is not set - # Enter a context with an instance of the API client - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - # Create an instance of the API class - api_instance = open_fga_api.OpenFgaApi(api_client) - - # expects FgaValidationException to be thrown because store_id is not specified - with self.assertRaises(FgaValidationException): - {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_models( - page_size= 1, - continuation_token= "abcdefg" - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_400_error(self, mock_request): - """ - Test to ensure 400 errors are handled properly - """ - response_body = ''' -{ - "code": "validation_error", - "message": "Generic validation error" -} - ''' - mock_request.side_effect = ValidationException(http_resp=http_mock_response(response_body, 400)) - - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - with self.assertRaises(ValidationException) as api_exception: - {{#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) - - - @patch.object(rest.RESTClientObject, 'request') - async def test_404_error(self, mock_request): - """ - Test to ensure 404 errors are handled properly - """ - response_body = ''' -{ - "code": "undefined_endpoint", - "message": "Endpoint not enabled" -} - ''' - mock_request.side_effect = NotFoundException(http_resp=http_mock_response(response_body, 404)) - - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - with self.assertRaises(NotFoundException) as api_exception: - {{#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") - - @patch.object(rest.RESTClientObject, 'request') - async def test_429_error_no_retry(self, mock_request): - """ - Test to ensure 429 errors are handled properly. - For this case, there is no retry configured - """ - response_body = ''' -{ - "code": "rate_limit_exceeded", - "message": "Rate Limit exceeded" -} - ''' - mock_request.side_effect = RateLimitExceededError(http_resp=http_mock_response(response_body, 429)) - - retry = {{packageName}}.configuration.RetryParams(0, 10) - configuration = self.configuration - configuration.store_id = store_id - configuration.retry_params = retry - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - with self.assertRaises(RateLimitExceededError) as api_exception: - {{#asyncio}}await {{/asyncio}}api_instance.check( - body=body, - ) - self.assertIsInstance(api_exception.exception, RateLimitExceededError) - mock_request.assert_called() - self.assertEqual(mock_request.call_count, 1) - - @patch.object(rest.RESTClientObject, 'request') - async def test_429_error_first_error(self, mock_request): - """ - Test to ensure 429 errors are handled properly. - For this case, retry is configured and only the first time has error - """ - response_body = '{"allowed": true, "resolution": "1234"}' - error_response_body = ''' -{ - "code": "rate_limit_exceeded", - "message": "Rate Limit exceeded" -} - ''' - mock_request.side_effect = [RateLimitExceededError(http_resp=http_mock_response(error_response_body, 429)), mock_response(response_body, 200)] - - retry = {{packageName}}.configuration.RetryParams(1, 10) - configuration = self.configuration - configuration.store_id = store_id - configuration.retry_params = retry - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( - body=body, - ) - self.assertIsInstance(api_response, CheckResponse) - self.assertTrue(api_response.allowed) - mock_request.assert_called() - self.assertEqual(mock_request.call_count, 2) - - - @patch.object(rest.RESTClientObject, 'request') - async def test_500_error(self, mock_request): - """ - Test to ensure 500 errors are handled properly - """ - response_body = ''' -{ - "code": "internal_error", - "message": "Internal Server Error" -} - ''' - mock_request.side_effect = ServiceException(http_resp=http_mock_response(response_body, 500)) - - configuration = self.configuration - configuration.store_id = store_id - configuration.retry_params.max_retry = 0 - - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - with self.assertRaises(ServiceException) as api_exception: - {{#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") - 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): - """ - Test to ensure 5xxx retries are handled properly - """ - response_body = """ -{ - "code": "internal_error", - "message": "Internal Server Error" -} - """ - mock_request.side_effect = [ - ServiceException(http_resp=http_mock_response(response_body, 500)), - ServiceException(http_resp=http_mock_response(response_body, 502)), - ServiceException(http_resp=http_mock_response(response_body, 503)), - ServiceException(http_resp=http_mock_response(response_body, 504)), - mock_response(response_body, 200), - ] - - retry = openfga_sdk.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: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - - api_response = await api_instance.check( - body=body, - ) - - self.assertIsInstance(api_response, CheckResponse) - mock_request.assert_called() - self.assertEqual(mock_request.call_count, 5) - - @patch.object(rest.RESTClientObject, "request") - async def test_501_error_retry(self, mock_request): - """ - Test to ensure 501 responses are not auto-retried - """ - response_body = """ -{ - "code": "not_implemented", - "message": "Not Implemented" -} - """ - mock_request.side_effect = [ - ServiceException(http_resp=http_mock_response(response_body, 501)), - ServiceException(http_resp=http_mock_response(response_body, 501)), - ServiceException(http_resp=http_mock_response(response_body, 501)), - mock_response(response_body, 200), - ] - - retry = openfga_sdk.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: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - with self.assertRaises(ServiceException) as api_exception: - await 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): - """Test case for API token - - Check whether API token is send when configuration specifies credential method as api_token - """ - - # First, mock the response - response_body = '{"allowed": true}' - mock_request.return_value = mock_response(response_body, 200) - - configuration = self.configuration - configuration.store_id = store_id - configuration.credentials = Credentials(method='api_token', configuration=CredentialConfiguration(api_token='TOKEN1')) - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( - body=body, - ) - self.assertIsInstance(api_response, CheckResponse) - self.assertTrue(api_response.allowed) - # Make sure the API was called with the right data - expected_headers = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk {{sdkId}}/{{packageVersion}}', 'Authorization': 'Bearer TOKEN1'}) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', - headers=expected_headers, - query_params=[], - post_params=[], - body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, - _preload_content=ANY, - _request_timeout=None - ) - - @patch.object(rest.RESTClientObject, 'request') - async def test_check_custom_header(self, mock_request): - """Test case for custom header - - Check whether custom header can be added - """ - - # First, mock the response - response_body = '{"allowed": true}' - mock_request.return_value = mock_response(response_body, 200) - - configuration = self.configuration - configuration.store_id = store_id - {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: - api_client.set_default_header("Custom Header", "custom value") - api_instance = open_fga_api.OpenFgaApi(api_client) - body = CheckRequest( - tuple_key=TupleKey( - object="document:2021-budget", - relation="reader", - user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", - ), - ) - api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( - body=body, - ) - self.assertIsInstance(api_response, CheckResponse) - self.assertTrue(api_response.allowed) - # Make sure the API was called with the right data - expected_headers = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk {{sdkId}}/{{packageVersion}}', 'Custom Header': 'custom value'}) - mock_request.assert_called_once_with( - 'POST', - 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', - headers=expected_headers, - query_params=[], - post_params=[], - body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, - _preload_content=ANY, - _request_timeout=None - ) - -{{/operations}} - -if __name__ == '__main__': - unittest.main() +{{>test/api_test.py}} diff --git a/config/clients/python/template/client/models/__init__.mustache b/config/clients/python/template/client/models/__init__.mustache deleted file mode 100644 index 69ee4317..00000000 --- a/config/clients/python/template/client/models/__init__.mustache +++ /dev/null @@ -1,12 +0,0 @@ -{{>partial_header}} -from openfga_sdk.client.models.assertion import ClientAssertion -from openfga_sdk.client.models.batch_check_response import BatchCheckResponse -from openfga_sdk.client.models.check_request import ClientCheckRequest -from openfga_sdk.client.models.expand_request import ClientExpandRequest -from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest -from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest -from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest -from openfga_sdk.client.models.tuple import ClientTuple -from openfga_sdk.client.models.write_request import ClientWriteRequest -from openfga_sdk.client.models.write_response import ClientWriteResponse -from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts diff --git a/config/clients/python/template/docs/opentelemetry.md b/config/clients/python/template/docs/opentelemetry.md new file mode 100644 index 00000000..ce32858f --- /dev/null +++ b/config/clients/python/template/docs/opentelemetry.md @@ -0,0 +1,31 @@ +# OpenTelemetry + +This SDK produces [metrics](https://opentelemetry.io/docs/concepts/signals/metrics/) using [OpenTelemetry](https://opentelemetry.io/) that allow you to view data such as request timings. These metrics also include attributes for the model and store ID, as well as the API called to allow you to build reporting. + +When an OpenTelemetry SDK instance is configured, the metrics will be exported and sent to the collector configured as part of your applications configuration. If you are not using OpenTelemetry, the metric functionality is a no-op and the events are never sent. + +In cases when metrics events are sent, they will not be viewable outside of infrastructure configured in your application, and are never available to the OpenFGA team or contributors. + +## Metrics + +### Supported Metrics + +| Metric Name | Type | Description | +| --------------------------------- | --------- | -------------------------------------------------------------------------------- | +| `fga-client.request.duration` | Histogram | The total request time for FGA requests | +| `fga-client.query.duration` | Histogram | The amount of time the FGA server took to process the request | +| ` fga-client.credentials.request` | Counter | The total number of times a new token was requested when using ClientCredentials | + +### Supported attributes + +| Attribute Name | Type | Description | +| ------------------------------ | -------- | ----------------------------------------------------------------------------------- | +| `fga-client.response.model_id` | `string` | The authorization model ID that the FGA server used | +| `fga-client.request.method` | `string` | The FGA method/action that was performed | +| `fga-client.request.store_id` | `string` | The store ID that was sent as part of the request | +| `fga-client.request.model_id` | `string` | The authorization model ID that was sent as part of the request, if any | +| `fga-client.request.client_id` | `string` | The client ID associated with the request, if any | +| `fga-client.user` | `string` | The user that is associated with the action of the request for check and list users | +| `http.status_code ` | `int` | The status code of the response | +| `http.method` | `string` | The HTTP method for the request | +| `http.host` | `string` | Host identifier of the origin the request was sent to | diff --git a/config/clients/python/template/example/example1/example1.py b/config/clients/python/template/example/example1/example1.py.mustache similarity index 97% rename from config/clients/python/template/example/example1/example1.py rename to config/clients/python/template/example/example1/example1.py.mustache index 3ae7f880..a232c9b5 100644 --- a/config/clients/python/template/example/example1/example1.py +++ b/config/clients/python/template/example/example1/example1.py.mustache @@ -1,7 +1,7 @@ import asyncio import os -from openfga_sdk import ( +from {{packageName}} import ( ClientConfiguration, Condition, ConditionParamTypeRef, @@ -18,7 +18,7 @@ Usersets, WriteAuthorizationModelRequest, ) -from openfga_sdk.client.models import ( +from {{packageName}}.client.models import ( ClientAssertion, ClientCheckRequest, ClientListObjectsRequest, @@ -28,9 +28,9 @@ ClientWriteRequest, WriteTransactionOpts, ) -from openfga_sdk.client.models.list_users_request import ClientListUsersRequest -from openfga_sdk.credentials import CredentialConfiguration, Credentials -from openfga_sdk.models.fga_object import FgaObject +from {{packageName}}.client.models.list_users_request import ClientListUsersRequest +from {{packageName}}.credentials import CredentialConfiguration, Credentials +from {{packageName}}.models.fga_object import FgaObject async def main(): diff --git a/config/clients/python/template/example/example1/setup.py b/config/clients/python/template/example/example1/setup.py.mustache similarity index 89% rename from config/clients/python/template/example/example1/setup.py rename to config/clients/python/template/example/example1/setup.py.mustache index 8771d555..0008c28b 100644 --- a/config/clients/python/template/example/example1/setup.py +++ b/config/clients/python/template/example/example1/setup.py.mustache @@ -14,7 +14,7 @@ NAME = "example1" VERSION = "0.0.1" -REQUIRES = ["openfga-sdk >= 0.3"] +REQUIRES = ["openfga-sdk >= {{packageVersion}}"] setup( name=NAME, @@ -24,7 +24,7 @@ author_email="community@openfga.dev", url="https://github.com/openfga/python-sdk", install_requires=REQUIRES, - python_requires=">=3.10", + 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/opentelemetry/.env.example b/config/clients/python/template/example/opentelemetry/.env.example new file mode 100644 index 00000000..55e13eff --- /dev/null +++ b/config/clients/python/template/example/opentelemetry/.env.example @@ -0,0 +1,7 @@ +FGA_CLIENT_ID= +FGA_API_TOKEN_ISSUER= +FGA_API_AUDIENCE= +FGA_CLIENT_SECRET= +FGA_STORE_ID= +FGA_AUTHORIZATION_MODEL_ID= +FGA_API_URL="http://localhost:8080" diff --git a/config/clients/python/template/example/opentelemetry/.gitignore b/config/clients/python/template/example/opentelemetry/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/config/clients/python/template/example/opentelemetry/.gitignore @@ -0,0 +1 @@ +.env diff --git a/config/clients/python/template/example/opentelemetry/README.md b/config/clients/python/template/example/opentelemetry/README.md new file mode 100644 index 00000000..c5b00bc7 --- /dev/null +++ b/config/clients/python/template/example/opentelemetry/README.md @@ -0,0 +1,43 @@ +# OpenTelemetry usage with OpenFGA's Python SDK + +This example demonstrates how you can use OpenTelemetry with OpenFGA's 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 +``` + +You need to have an OpenTelemetry collector running to receive data. A pre-configured collector is available using Docker: + +```bash +git clone https://github.com/ewanharris/opentelemetry-collector-dev-setup.git +cd opentelemetry-collector-dev-setup +docker-compose up -d +``` + +## Configure the example + +You 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: + +```bash +python main.py +``` diff --git a/config/clients/python/template/example/opentelemetry/main.py.mustache b/config/clients/python/template/example/opentelemetry/main.py.mustache new file mode 100644 index 00000000..a9226267 --- /dev/null +++ b/config/clients/python/template/example/opentelemetry/main.py.mustache @@ -0,0 +1,151 @@ +import asyncio +import os +import sys +from operator import attrgetter +from random import randint +from typing import Any + +from dotenv import load_dotenv +from opentelemetry import metrics +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.resources import SERVICE_NAME, Resource + +# For usage convenience of this example, we will import the OpenFGA SDK from the parent directory. +sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) +sys.path.insert(0, sdk_path) + +from {{packageName}} import ( + ClientConfiguration, + OpenFgaClient, + ReadRequestTupleKey, +) +from {{packageName}}.client.models import ClientCheckRequest +from {{packageName}}.credentials import ( + CredentialConfiguration, + Credentials, +) +from {{packageName}}.exceptions import FgaValidationException + + +class app: + """ + An example class to demonstrate how to implement the OpenFGA SDK with OpenTelemetry. + """ + + def __init__( + self, + client: OpenFgaClient = None, + credentials: Credentials = None, + configuration: ClientConfiguration = None, + ): + """ + Initialize the example with the provided client, credentials, and configuration. + """ + + self._client = client + self._credentials = credentials + self._configuration = configuration + + async def fga_client(self, env: dict[str, str] = {}) -> OpenFgaClient: + """ + Build an OpenFGA client with the provided credentials and configuration. If not provided, load from environment variables. + """ + + if not self._client or not self._credentials or not self._configuration: + load_dotenv() + + if not self._credentials: + self._credentials = Credentials( + method="client_credentials", + configuration=CredentialConfiguration( + client_id=os.getenv("FGA_CLIENT_ID"), + client_secret=os.getenv("FGA_CLIENT_SECRET"), + api_issuer=os.getenv("FGA_API_TOKEN_ISSUER"), + api_audience=os.getenv("FGA_API_AUDIENCE"), + ), + ) + + if not self._configuration: + self._configuration = ClientConfiguration( + api_url=os.getenv("FGA_API_URL"), + store_id=os.getenv("FGA_STORE_ID"), + authorization_model_id=os.getenv("FGA_AUTHORIZATION_MODEL_ID"), + credentials=self._credentials, + ) + + if not self._client: + return OpenFgaClient(self._configuration) + + return self._client + + def configure_telemetry(self) -> None: + """ + Configure OpenTelemetry with the provided meter provider. + """ + + exporters = [] + exporters.append(PeriodicExportingMetricReader(OTLPMetricExporter())) + + if os.getenv("OTEL_EXPORTER_CONSOLE") == "true": + exporters.append(PeriodicExportingMetricReader(ConsoleMetricExporter())) + + metrics.set_meter_provider( + MeterProvider( + resource=Resource(attributes={SERVICE_NAME: "openfga-python-example"}), + metric_readers=[exporter for exporter in exporters], + ) + ) + + def unpack( + self, + response, + attr: str, + ) -> Any: + """ + Shortcut to unpack a FGA response and return the desired attribute. + Note: This is a simple example and does not handle errors or exceptions. + """ + + return attrgetter(attr)(response) + + +async def main(): + app().configure_telemetry() + + async with await app().fga_client() as fga_client: + print("Client created successfully.") + + print("Reading authorization model ...", end=" ") + authorization_models = app().unpack( + await fga_client.read_authorization_models(), "authorization_models" + ) + print(f"Done! Models Count: {len(authorization_models)}") + + print("Reading tuples ...", end=" ") + tuples = app().unpack(await fga_client.read(ReadRequestTupleKey()), "tuples") + print(f"Done! Tuples Count: {len(tuples)}") + + checks_requests = randint(1, 10) + + print(f"Making {checks_requests} checks ...", end=" ") + for _ in range(checks_requests): + try: + allowed = app().unpack( + await fga_client.check( + body=ClientCheckRequest( + user="user:anne", relation="owner", object="folder:foo" + ), + ), + "allowed", + ) + except FgaValidationException as error: + print(f"Checked failed due to validation exception: {error}") + print("Done!") + + +asyncio.run(main()) diff --git a/config/clients/python/template/example/opentelemetry/requirements.txt b/config/clients/python/template/example/opentelemetry/requirements.txt new file mode 100644 index 00000000..5a87b5fe --- /dev/null +++ b/config/clients/python/template/example/opentelemetry/requirements.txt @@ -0,0 +1,3 @@ +python-dotenv >= 1, <2 +opentelemetry-sdk >= 1, <2 +opentelemetry-exporter-otlp-proto-grpc >= 1.25, <2 diff --git a/config/clients/python/template/example/opentelemetry/setup.cfg b/config/clients/python/template/example/opentelemetry/setup.cfg new file mode 100644 index 00000000..11433ee8 --- /dev/null +++ b/config/clients/python/template/example/opentelemetry/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=99 diff --git a/config/clients/python/template/example/opentelemetry/setup.py.mustache b/config/clients/python/template/example/opentelemetry/setup.py.mustache new file mode 100644 index 00000000..ddeb10fc --- /dev/null +++ b/config/clients/python/template/example/opentelemetry/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-opentelemetry-example" +VERSION = "0.0.1" +REQUIRES = [""] + +setup( + name=NAME, + version=VERSION, + description="An example of using the OpenFGA Python SDK with OpenTelemetry", + 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/requirements.mustache b/config/clients/python/template/requirements.mustache index ff6a875d..4eef03dd 100644 --- a/config/clients/python/template/requirements.mustache +++ b/config/clients/python/template/requirements.mustache @@ -1,4 +1,6 @@ aiohttp >= 3.9.3, < 4 python-dateutil >= 2.9.0, < 3 setuptools >= 69.1.1 +build >= 1.2.1, < 2 urllib3 >= 1.25.11, < 3 +opentelemetry-api >= 1.25.0, < 2 diff --git a/config/clients/python/template/setup.mustache b/config/clients/python/template/setup.mustache index 53fa9b71..7a1c2ba8 100644 --- a/config/clients/python/template/setup.mustache +++ b/config/clients/python/template/setup.mustache @@ -2,7 +2,8 @@ import pathlib import pkg_resources -import setuptools + +from setuptools import find_packages, setup NAME = "{{{projectName}}}" @@ -19,7 +20,7 @@ with pathlib.Path("requirements.txt").open() as requirements_txt: this_directory = pathlib.Path(__file__).parent long_description = (this_directory / "README.md").read_text() -setuptools.setup( +setup( name=NAME, version=VERSION, description="{{appDescription}}", @@ -40,8 +41,8 @@ setuptools.setup( {{/packageTags}} ], install_requires=REQUIRES, - python_requires=">=3.10", - packages=setuptools.find_packages(exclude=["test", "tests"]), + python_requires=">={{pythonMinimumRuntime}}", + packages=find_packages(exclude=["test"]), include_package_data=True, {{#licenseId}}license="{{.}}", {{/licenseId}}long_description_content_type="text/markdown", diff --git a/config/clients/python/template/__init__package.mustache b/config/clients/python/template/src/__init__.py.mustache similarity index 100% rename from config/clients/python/template/__init__package.mustache rename to config/clients/python/template/src/__init__.py.mustache diff --git a/config/clients/python/template/src/api.py.mustache b/config/clients/python/template/src/api.py.mustache new file mode 100644 index 00000000..d6563b95 --- /dev/null +++ b/config/clients/python/template/src/api.py.mustache @@ -0,0 +1,386 @@ +{{>partial_header}} + + +from {{packageName}}.api_client import ApiClient +from {{packageName}}.exceptions import ApiValueError, FgaValidationException +from {{packageName}}.oauth2 import OAuth2Client +from {{packageName}}.telemetry import Telemetry +from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes + + +{{#operations}} +class {{classname}}: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None): + if api_client is None: + api_client = ApiClient() + self.api_client: ApiClient = api_client + + self._oauth2_client = None + if api_client.configuration is not None: + credentials = api_client.configuration.credentials + if credentials is not None and credentials.method == "client_credentials": + self._oauth2_client = OAuth2Client(credentials) + + self._telemetry = Telemetry() + +{{#asyncio}} + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + await self.close() + + async def close(self): + await self.api_client.close() +{{/asyncio}} +{{^asyncio}} + def __enter__(self): + return self + + def __exit__(self): + self.close() + + def close(self): + self.api_client.close() +{{/asyncio}} + +{{#operation}} + + {{#asyncio}}async {{/asyncio}}def {{operationId}}(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): + """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} + +{{#notes}} + {{{.}}} +{{/notes}} + +{{#sortParamsByRequiredFlag}} + >>> thread = {{#asyncio}}await {{/asyncio}}api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} +{{^sortParamsByRequiredFlag}} + >>> thread = {{#asyncio}}await {{/asyncio}}api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} + +{{#requiredParams}} +{{^-first}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) + :type {{paramName}}: {{dataType}} +{{/-first}} +{{/requiredParams}} +{{#optionalParams}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) + :type {{paramName}}: {{dataType}}, optional +{{/optionalParams}} + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: {{returnType}}{{^returnType}}None{{/returnType}} + """ + kwargs["_return_http_data_only"] = True + return {{#asyncio}}await {{/asyncio}}self.{{operationId}}_with_http_info({{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs){{#asyncio}}{{/asyncio}} + + {{#asyncio}}async {{/asyncio}}def {{operationId}}_with_http_info(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): + """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} + +{{#notes}} + {{{.}}} +{{/notes}} + +{{#sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} +{{^sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} + +{{#requiredParams}} +{{^-first}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) + :type {{paramName}}: {{dataType}} +{{/-first}} +{{/requiredParams}} +{{#optionalParams}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) + :type {{paramName}}: {{dataType}}, optional +{{/optionalParams}} + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _return_http_data_only: response data without head status code + and headers + :type _return_http_data_only: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :param _retry_param: if specified, override the retry parameters specified in configuration + :type _request_auth: dict, optional + :type _content_type: string, optional: force content-type for the request + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: {{#returnType}}tuple({{.}}, status_code(int), headers(HTTPHeaderDict)){{/returnType}}{{^returnType}}None{{/returnType}} + """ + + {{#servers.0}} + local_var_hosts = [ +{{#servers}} + '{{{url}}}'{{^-last}},{{/-last}} +{{/servers}} + ] + local_var_host = local_var_hosts[0] + if kwargs.get('_host_index'): + _host_index = int(kwargs.get('_host_index')) + if _host_index < 0 or _host_index >= len(local_var_hosts): + raise ApiValueError( + "Invalid host index. Must be 0 <= index < %s" + % len(local_var_host) + ) + local_var_host = local_var_hosts[_host_index] + {{/servers.0}} + local_var_params = locals() + + all_params = [ +{{#requiredParams}}{{^-first}} + '{{paramName}}'{{^-last}},{{/-last}} +{{/-first}}{{/requiredParams}} +{{#optionalParams}} + '{{paramName}}'{{^-last}},{{/-last}} +{{/optionalParams}} + ] + all_params.extend( + [ + 'async_req', + '_return_http_data_only', + '_preload_content', + '_request_timeout', + '_request_auth', + '_content_type', + '_headers', + '_retry_parms' + ] + ) + + for key, val in local_var_params['kwargs'].items(): + if key not in all_params{{#servers.0}} and key != "_host_index"{{/servers.0}}: + raise FgaValidationException( + "Got an unexpected keyword argument '%s'" + " to method {{operationId}}" % key + ) + local_var_params[key] = val + del local_var_params['kwargs'] +{{#allParams}} +{{^isNullable}} +{{#required}} +{{^-first}} + # verify the required parameter '{{paramName}}' is set + if self.api_client.client_side_validation and local_var_params.get('{{paramName}}') is None: + raise ApiValueError( + "Missing the required parameter `{{paramName}}` when calling `{{operationId}}`") +{{/-first}} +{{/required}} +{{/isNullable}} +{{/allParams}} + +{{#allParams}} +{{#hasValidation}} + {{#maxLength}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) > {{maxLength}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be less than or equal to `{{maxLength}}`") + {{/maxLength}} + {{#minLength}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) < {{minLength}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be greater than or equal to `{{minLength}}`") + {{/minLength}} + {{#maximum}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}: + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value less than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}`{{maximum}}`") + {{/maximum}} + {{#minimum}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}: + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}`{{minimum}}`") + {{/minimum}} + {{#pattern}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and not re.search(r'{{{vendorExtensions.x-regex}}}', local_var_params['{{paramName}}']{{#vendorExtensions.x-modifiers}}{{#-first}}, flags={{/-first}}re.{{.}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-modifiers}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must conform to the pattern `{{{pattern}}}`") + {{/pattern}} + {{#maxItems}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) > {{maxItems}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be less than or equal to `{{maxItems}}`") + {{/maxItems}} + {{#minItems}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) < {{minItems}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be greater than or equal to `{{minItems}}`") + {{/minItems}} +{{/hasValidation}} +{{#-last}} +{{/-last}} +{{/allParams}} + collection_formats = {} + + path_params = {} + + store_id = None +{{#pathParams}} +{{^-first}} + if '{{paramName}}' in local_var_params: + path_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/-first}} + +{{#-first}} + if self.api_client._get_store_id() is None: + raise ApiValueError( + "Store ID expected in api_client's configuration when calling `{{operationId}}`") + store_id = self.api_client._get_store_id() +{{/-first}} + +{{/pathParams}} + + query_params = [] +{{#queryParams}} + if local_var_params.get('{{paramName}}') is not None: + query_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/queryParams}} + + header_params = dict(local_var_params.get('_headers', {})) +{{#headerParams}} + if '{{paramName}}' in local_var_params: + header_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/headerParams}} + + form_params = [] + local_var_files = {} +{{#formParams}} + if '{{paramName}}' in local_var_params: + {{^isFile}}form_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{/isFile}}{{#isFile}}local_var_files['{{baseName}}'] = local_var_params['{{paramName}}']{{/isFile}}{{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/formParams}} + + body_params = None +{{#bodyParam}} + if '{{paramName}}' in local_var_params: + body_params = local_var_params['{{paramName}}'] +{{/bodyParam}} + {{#hasProduces}} + # HTTP header `Accept` + header_params['Accept'] = self.api_client.select_header_accept( + [{{#produces}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}]) + + {{/hasProduces}} + {{#hasConsumes}} + # HTTP header `Content-Type` + content_types_list = local_var_params.get('_content_type', self.api_client.select_header_content_type([{{#consumes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/consumes}}],'{{httpMethod}}', body_params)) + if content_types_list: + header_params['Content-Type'] = content_types_list + + {{/hasConsumes}} + # Authentication setting + auth_settings = [{{#authMethods}}'{{name}}'{{^-last}}, {{/-last}}{{/authMethods}}] + + {{#returnType}} + {{#responses}} + {{#-first}} + response_types_map = { + {{/-first}} + {{^isWildcard}} + {{code}}: {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}}, + {{/isWildcard}} + {{#-last}} + } + {{/-last}} + {{/responses}} + {{/returnType}} + {{^returnType}} + response_types_map = {} + {{/returnType}} + + telemetry_attributes: dict[TelemetryAttribute, str] = { + TelemetryAttributes.request_method: "{{operationId}}" + } + + try: + if store_id: + telemetry_attributes[TelemetryAttributes.request_store_id] = store_id + except: + pass + + try: + if body_params.tuple_key: + telemetry_attributes[TelemetryAttributes.client_user] = ( + body_params.tuple_key.user + ) + except: + pass + + try: + if "authorization_model_id" in local_var_params: + telemetry_attributes[TelemetryAttributes.request_model_id] = local_var_params[ + "authorization_model_id" + ] + elif body_params.authorization_model_id: + telemetry_attributes[TelemetryAttributes.request_model_id] = ( + body_params.authorization_model_id + ) + except: + pass + + return {{#asyncio}}await ({{/asyncio}}self.api_client.call_api( + '{{{path}}}'.replace('{store_id}', store_id), '{{httpMethod}}', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_types_map=response_types_map, + auth_settings=auth_settings, + async_req=local_var_params.get('async_req'), + _return_http_data_only=local_var_params.get('_return_http_data_only'), + _preload_content=local_var_params.get('_preload_content', True), + _request_timeout=local_var_params.get('_request_timeout'), + _retry_params=local_var_params.get('_retry_params'), + {{#servers.0}} + _host=local_var_host, + {{/servers.0}} + collection_formats=collection_formats, + _request_auth=local_var_params.get('_request_auth'), + _oauth2_client=self._oauth2_client, + _telemetry_attributes=telemetry_attributes{{#asyncio}}){{/asyncio}} + ) +{{/operation}} +{{/operations}} diff --git a/config/clients/python/template/__init__api.mustache b/config/clients/python/template/src/api/__init__.py.mustache similarity index 100% rename from config/clients/python/template/__init__api.mustache rename to config/clients/python/template/src/api/__init__.py.mustache diff --git a/config/clients/python/template/api_client.mustache b/config/clients/python/template/src/api_client.py.mustache similarity index 77% rename from config/clients/python/template/api_client.mustache rename to config/clients/python/template/src/api_client.py.mustache index 82173ea9..a4e6be75 100644 --- a/config/clients/python/template/api_client.mustache +++ b/config/clients/python/template/src/api_client.py.mustache @@ -3,27 +3,33 @@ {{#asyncio}} import asyncio {{/asyncio}} -{{^asyncio}} -import time -{{/asyncio}} import atexit import datetime -from dateutil.parser import parse import json import math -from multiprocessing.pool import ThreadPool import random import re +import time import urllib -import {{modelPackage}} {{#tornado}} import tornado.gen {{/tornado}} +from multiprocessing.pool import ThreadPool -from {{packageName}}.configuration import Configuration -from {{packageName}} import rest, oauth2 -from {{packageName}}.exceptions import ApiValueError, ApiException, FgaValidationException, RateLimitExceededError, ServiceException +from dateutil.parser import parse +import {{modelPackage}} +from {{packageName}} import rest, oauth2 +from {{packageName}}.configuration import Configuration +from {{packageName}}.exceptions import ( + ApiException, + ApiValueError, + FgaValidationException, + RateLimitExceededError, + ServiceException, +) +from {{packageName}}.telemetry import Telemetry +from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes DEFAULT_USER_AGENT = '{{{userAgent}}}' @@ -72,21 +78,32 @@ class ApiClient: } _pool = None - def __init__(self, configuration=None, header_name=None, header_value=None, - cookie=None, pool_threads=1): + def __init__( + self, + configuration=None, + header_name=None, + header_value=None, + cookie=None, + pool_threads=1, + ): if configuration is None: configuration = Configuration.get_default_copy() + self.configuration = configuration self.pool_threads = pool_threads self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} if header_name is not None: self.default_headers[header_name] = header_value + self.cookie = cookie - # Set default User-Agent. + self.user_agent = DEFAULT_USER_AGENT + self.client_side_validation = configuration.client_side_validation + self._telemetry = Telemetry() {{#asyncio}} async def __aenter__(self): @@ -140,16 +157,38 @@ class ApiClient: @tornado.gen.coroutine {{/tornado}} {{#asyncio}}async {{/asyncio}}def __call_api( - self, resource_path, method, path_params=None, - query_params=None, header_params=None, body=None, post_params=None, - response_types_map=None, auth_settings=None, - _return_http_data_only=None, collection_formats=None, - _preload_content=True, _request_timeout=None, _host=None, - _request_auth=None, _retry_params=None, _oauth2_client=None): + self, + resource_path, + method, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + response_types_map=None, + auth_settings=None, + _return_http_data_only=None, + collection_formats=None, + _preload_content=True, + _request_timeout=None, + _host=None, + _request_auth=None, + _retry_params=None, + _oauth2_client=None, + _telemetry_attributes: dict[TelemetryAttribute | str, str] = None, + ): self.configuration.is_valid() config = self.configuration + benchmark: dict[str, float] = {"start": float(time.time())} + _telemetry_attributes = _telemetry_attributes or {} + + _telemetry_attributes[TelemetryAttributes().http_host] = urllib.parse.urlparse( + config.api_url + ).hostname + _telemetry_attributes[TelemetryAttributes().http_method] = method + # header parameters header_params = header_params or {} header_params.update(self.default_headers) @@ -212,7 +251,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 - for x in range(max_retry+1): + for retry in range(max_retry + 1): try: # perform request and return response response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request( @@ -221,9 +260,9 @@ class ApiClient: _preload_content=_preload_content, _request_timeout=_request_timeout) except (RateLimitExceededError, ServiceException) as e: - if x < max_retry and e.status != 501: - {{#asyncio}}await asyncio.sleep(random_time(x, min_wait_in_ms)){{/asyncio}} - {{^asyncio}}time.sleep(random_time(x, min_wait_in_ms)){{/asyncio}} + if retry < max_retry and e.status != 501: + {{#asyncio}}await asyncio.sleep(random_time(retry, min_wait_in_ms)){{/asyncio}} + {{^asyncio}}time.sleep(random_time(retry, min_wait_in_ms)){{/asyncio}} continue e.body = e.body.decode('utf-8') response_type = response_types_map.get(e.status, None) @@ -245,6 +284,31 @@ class ApiClient: return_data = response_data + benchmark["end"] = float(time.time()) + + _telemetry_attributes[TelemetryAttributes().request_retries] = retry + + _telemetry_attributes.update( + TelemetryAttributes().fromResponse(response_data) + ) + + try: + _telemetry_attributes[TelemetryAttributes().request_client_id] = ( + self.configuration.credentials.configuration.client_id + ) + except AttributeError: + pass + + duration = response_data.getheader("fga-query-duration-ms") + attributes = TelemetryAttributes().prepare(_telemetry_attributes) + + if duration is not None: + self._telemetry.metrics().queryDuration(int(duration, 10), attributes) + + self._telemetry.metrics().requestDuration().record( + float(benchmark["end"] - benchmark["start"]), attributes + ) + if not _preload_content: {{^tornado}} return return_data @@ -384,14 +448,29 @@ class ApiClient: else: return self.__deserialize_model(data, klass) - {{#asyncio}}async {{/asyncio}}def call_api(self, resource_path, method, - path_params=None, query_params=None, header_params=None, - body=None, post_params=None, files=None, - response_types_map=None, auth_settings=None, - async_req=None, _return_http_data_only=None, - collection_formats=None,_preload_content=True, - _request_timeout=None, _host=None, _request_auth=None, - _retry_params=None, _oauth2_client=None): + {{#asyncio}}async {{/asyncio}}def call_api( + self, + resource_path, + method, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + files=None, + response_types_map=None, + auth_settings=None, + async_req=None, + _return_http_data_only=None, + collection_formats=None, + _preload_content=True, + _request_timeout=None, + _host=None, + _request_auth=None, + _retry_params=None, + _oauth2_client=None, + _telemetry_attributes: dict[TelemetryAttribute, str] = None, + ): """Makes the HTTP request (synchronous) and returns deserialized data. To make an async_req request, set the async_req parameter. @@ -433,82 +512,126 @@ class ApiClient: then the method will return the response directly. """ if not async_req: - return {{#asyncio}}await ({{/asyncio}}self.__call_api(resource_path, method, - path_params, query_params, header_params, - body, post_params, - response_types_map, auth_settings, - _return_http_data_only, collection_formats, - _preload_content, _request_timeout, _host, - _request_auth, _retry_params, _oauth2_client){{#asyncio}}){{/asyncio}} - - return self.pool.apply_async(self.__call_api, (resource_path, - method, path_params, - query_params, - header_params, body, - post_params, - response_types_map, - auth_settings, - _return_http_data_only, - collection_formats, - _preload_content, - _request_timeout, - _host, _request_auth, - _retry_params, - _oauth2_client)) - - {{#asyncio}}async {{/asyncio}}def request(self, method, url, query_params=None, headers=None, - post_params=None, body=None, _preload_content=True, - _request_timeout=None): + return {{#asyncio}}await ({{/asyncio}}self.__call_api( + resource_path, + method, + path_params, + query_params, + header_params, + body, + post_params, + response_types_map, + auth_settings, + _return_http_data_only, + collection_formats, + _preload_content, + _request_timeout, + _host, + _request_auth, + _retry_params, + _oauth2_client, + _telemetry_attributes, + ){{#asyncio}}){{/asyncio}} + + return self.pool.apply_async( + self.__call_api, + ( + resource_path, + method, + path_params, + query_params, + header_params, + body, + post_params, + response_types_map, + auth_settings, + _return_http_data_only, + collection_formats, + _preload_content, + _request_timeout, + _host, + _request_auth, + _retry_params, + _oauth2_client, + _telemetry_attributes, + ), + ) + + {{#asyncio}}async {{/asyncio}}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 {{#asyncio}}await ({{/asyncio}}self.rest_client.GET(url, - query_params=query_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - headers=headers){{#asyncio}}){{/asyncio}} + 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}} + 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}} + 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}} + 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}} + 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(url, - query_params=query_params, - headers=headers, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body){{#asyncio}}){{/asyncio}} + return {{#asyncio}}await ({{/asyncio}} 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, + ){{#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}} + 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`," diff --git a/config/clients/python/template/client/__init__.mustache b/config/clients/python/template/src/client/__init__.py.mustache similarity index 100% rename from config/clients/python/template/client/__init__.mustache rename to config/clients/python/template/src/client/__init__.py.mustache diff --git a/config/clients/python/template/client/client.mustache b/config/clients/python/template/src/client/client.py.mustache similarity index 100% rename from config/clients/python/template/client/client.mustache rename to config/clients/python/template/src/client/client.py.mustache diff --git a/config/clients/python/template/client/configuration.mustache b/config/clients/python/template/src/client/configuration.py.mustache similarity index 100% rename from config/clients/python/template/client/configuration.mustache rename to config/clients/python/template/src/client/configuration.py.mustache diff --git a/config/clients/python/template/src/client/models/__init__.py.mustache b/config/clients/python/template/src/client/models/__init__.py.mustache new file mode 100644 index 00000000..6033fcc7 --- /dev/null +++ b/config/clients/python/template/src/client/models/__init__.py.mustache @@ -0,0 +1,12 @@ +{{>partial_header}} +from {{packageName}}.client.models.assertion import ClientAssertion +from {{packageName}}.client.models.batch_check_response import BatchCheckResponse +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 +from {{packageName}}.client.models.list_relations_request import ClientListRelationsRequest +from {{packageName}}.client.models.read_changes_request import ClientReadChangesRequest +from {{packageName}}.client.models.tuple import ClientTuple +from {{packageName}}.client.models.write_request import ClientWriteRequest +from {{packageName}}.client.models.write_response import ClientWriteResponse +from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts diff --git a/config/clients/python/template/client/models/assertion.mustache b/config/clients/python/template/src/client/models/assertion.py.mustache similarity index 100% rename from config/clients/python/template/client/models/assertion.mustache rename to config/clients/python/template/src/client/models/assertion.py.mustache diff --git a/config/clients/python/template/client/models/batch_check_response.mustache b/config/clients/python/template/src/client/models/batch_check_response.py.mustache similarity index 100% rename from config/clients/python/template/client/models/batch_check_response.mustache rename to config/clients/python/template/src/client/models/batch_check_response.py.mustache diff --git a/config/clients/python/template/client/models/check_request.mustache b/config/clients/python/template/src/client/models/check_request.py.mustache similarity index 100% rename from config/clients/python/template/client/models/check_request.mustache rename to config/clients/python/template/src/client/models/check_request.py.mustache diff --git a/config/clients/python/template/client/models/expand_request.mustache b/config/clients/python/template/src/client/models/expand_request.py.mustache similarity index 100% rename from config/clients/python/template/client/models/expand_request.mustache rename to config/clients/python/template/src/client/models/expand_request.py.mustache diff --git a/config/clients/python/template/client/models/list_objects_request.mustache b/config/clients/python/template/src/client/models/list_objects_request.py.mustache similarity index 100% rename from config/clients/python/template/client/models/list_objects_request.mustache rename to config/clients/python/template/src/client/models/list_objects_request.py.mustache diff --git a/config/clients/python/template/client/models/list_relations_request.mustache b/config/clients/python/template/src/client/models/list_relations_request.py.mustache similarity index 100% rename from config/clients/python/template/client/models/list_relations_request.mustache rename to config/clients/python/template/src/client/models/list_relations_request.py.mustache diff --git a/config/clients/python/template/client/models/list_users_request.mustache b/config/clients/python/template/src/client/models/list_users_request.py.mustache similarity index 100% rename from config/clients/python/template/client/models/list_users_request.mustache rename to config/clients/python/template/src/client/models/list_users_request.py.mustache diff --git a/config/clients/python/template/client/models/read_changes_request.mustache b/config/clients/python/template/src/client/models/read_changes_request.py.mustache similarity index 100% rename from config/clients/python/template/client/models/read_changes_request.mustache rename to config/clients/python/template/src/client/models/read_changes_request.py.mustache diff --git a/config/clients/python/template/client/models/tuple.mustache b/config/clients/python/template/src/client/models/tuple.py.mustache similarity index 100% rename from config/clients/python/template/client/models/tuple.mustache rename to config/clients/python/template/src/client/models/tuple.py.mustache diff --git a/config/clients/python/template/client/models/write_request.mustache b/config/clients/python/template/src/client/models/write_request.py.mustache similarity index 100% rename from config/clients/python/template/client/models/write_request.mustache rename to config/clients/python/template/src/client/models/write_request.py.mustache diff --git a/config/clients/python/template/client/models/write_response.mustache b/config/clients/python/template/src/client/models/write_response.py.mustache similarity index 100% rename from config/clients/python/template/client/models/write_response.mustache rename to config/clients/python/template/src/client/models/write_response.py.mustache diff --git a/config/clients/python/template/client/models/write_single_response.mustache b/config/clients/python/template/src/client/models/write_single_response.py.mustache similarity index 100% rename from config/clients/python/template/client/models/write_single_response.mustache rename to config/clients/python/template/src/client/models/write_single_response.py.mustache diff --git a/config/clients/python/template/client/models/write_transaction_opts.mustache b/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache similarity index 100% rename from config/clients/python/template/client/models/write_transaction_opts.mustache rename to config/clients/python/template/src/client/models/write_transaction_opts.py.mustache diff --git a/config/clients/python/template/configuration.mustache b/config/clients/python/template/src/configuration.py.mustache similarity index 100% rename from config/clients/python/template/configuration.mustache rename to config/clients/python/template/src/configuration.py.mustache diff --git a/config/clients/python/template/credentials.mustache b/config/clients/python/template/src/credentials.py.mustache similarity index 100% rename from config/clients/python/template/credentials.mustache rename to config/clients/python/template/src/credentials.py.mustache diff --git a/config/clients/python/template/exceptions.mustache b/config/clients/python/template/src/exceptions.py.mustache similarity index 100% rename from config/clients/python/template/exceptions.mustache rename to config/clients/python/template/src/exceptions.py.mustache diff --git a/config/clients/python/template/help.py b/config/clients/python/template/src/help.py.mustache similarity index 89% rename from config/clients/python/template/help.py rename to config/clients/python/template/src/help.py.mustache index 6795d13a..c6017efa 100644 --- a/config/clients/python/template/help.py +++ b/config/clients/python/template/src/help.py.mustache @@ -3,6 +3,8 @@ import sys from collections import OrderedDict +import opentelemetry.version + from . import __version__ as openfga_sdk_version try: @@ -26,6 +28,13 @@ except ModuleNotFoundError: aiohttp_version = "" +try: + import opentelemetry + + opentelemetry_version = opentelemetry.version.__version__ +except ModuleNotFoundError: + opentelemetry_version = "" + def info() -> dict[str, dict[str, str]]: """ @@ -70,6 +79,7 @@ def info() -> dict[str, dict[str, str]]: "urllib3": {"version": urllib3_version}, "python-dateutil": {"version": dateutil_version}, "aiohttp": {"version": aiohttp_version}, + "opentelemetry": {"version": opentelemetry_version}, }, } ) diff --git a/config/clients/python/template/__init__model.mustache b/config/clients/python/template/src/models/__init__.py.mustache similarity index 100% rename from config/clients/python/template/__init__model.mustache rename to config/clients/python/template/src/models/__init__.py.mustache diff --git a/config/clients/python/template/oauth2.mustache b/config/clients/python/template/src/oauth2.py.mustache similarity index 90% rename from config/clients/python/template/oauth2.mustache rename to config/clients/python/template/src/oauth2.py.mustache index caf9a3e1..3ea23250 100644 --- a/config/clients/python/template/oauth2.mustache +++ b/config/clients/python/template/src/oauth2.py.mustache @@ -12,6 +12,8 @@ import urllib3 from {{packageName}}.configuration import Configuration from {{packageName}}.credentials import Credentials from {{packageName}}.exceptions import AuthenticationError +from {{packageName}}.telemetry.attributes import TelemetryAttributes +from {{packageName}}.telemetry.telemetry import Telemetry def jitter(loop_count, min_wait_in_ms): @@ -35,6 +37,7 @@ class OAuth2Client: self._credentials = credentials self._access_token = None self._access_expiry_time = None + self._telemetry = Telemetry() if configuration is None: configuration = Configuration.get_default_copy() @@ -107,6 +110,12 @@ class OAuth2Client: seconds=int(api_response.get("expires_in")) ) self._access_token = api_response.get("access_token") + self._telemetry.metrics().credentialsRequest( + 1, + { + TelemetryAttributes.request_client_id: configuration.client_id + }, + ) break raise AuthenticationError(http_resp=raw_response) diff --git a/config/clients/python/template/asyncio/rest.mustache b/config/clients/python/template/src/rest.py.mustache similarity index 100% rename from config/clients/python/template/asyncio/rest.mustache rename to config/clients/python/template/src/rest.py.mustache diff --git a/config/clients/python/template/__init__sync.mustache b/config/clients/python/template/src/sync/__init__.py.mustache similarity index 100% rename from config/clients/python/template/__init__sync.mustache rename to config/clients/python/template/src/sync/__init__.py.mustache diff --git a/config/clients/python/template/src/sync/api.py.mustache b/config/clients/python/template/src/sync/api.py.mustache new file mode 100644 index 00000000..ec994c61 --- /dev/null +++ b/config/clients/python/template/src/sync/api.py.mustache @@ -0,0 +1,376 @@ +{{>partial_header}} + +import re + +from {{packageName}}.exceptions import ApiValueError, FgaValidationException +from {{packageName}}.sync.api_client import ApiClient +from {{packageName}}.sync.oauth2 import OAuth2Client +from {{packageName}}.telemetry import Telemetry +from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes + + +{{#operations}} +class {{classname}}: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None): + if api_client is None: + api_client = ApiClient() + self.api_client: ApiClient = api_client + + self._oauth2_client = None + if api_client.configuration is not None: + credentials = api_client.configuration.credentials + if credentials is not None and credentials.method == "client_credentials": + self._oauth2_client = OAuth2Client(credentials) + + self._telemetry = Telemetry() + + + def __enter__(self): + return self + + def __exit__(self): + self.close() + + def close(self): + self.api_client.close() + +{{#operation}} + + def {{operationId}}(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): + """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} + +{{#notes}} + {{{.}}} +{{/notes}} + +{{#sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} +{{^sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} + +{{#requiredParams}} +{{^-first}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) + :type {{paramName}}: {{dataType}} +{{/-first}} +{{/requiredParams}} +{{#optionalParams}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) + :type {{paramName}}: {{dataType}}, optional +{{/optionalParams}} + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: {{returnType}}{{^returnType}}None{{/returnType}} + """ + kwargs["_return_http_data_only"] = True + return self.{{operationId}}_with_http_info({{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs) + + def {{operationId}}_with_http_info(self, {{#sortParamsByRequiredFlag}}{{#allParams}}{{#required}}{{^-first}}{{paramName}}, {{/-first}}{{/required}}{{/allParams}}{{/sortParamsByRequiredFlag}}**kwargs): + """{{{summary}}}{{^summary}}{{operationId}}{{/summary}} + +{{#notes}} + {{{.}}} +{{/notes}} + +{{#sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}{{^-last}}, {{/-last}}{{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} +{{^sortParamsByRequiredFlag}} + >>> thread = api.{{operationId}}_with_http_info({{#allParams}}{{#required}}{{^-first}}{{paramName}}={{paramName}}_value{{^-last}}, {{/-last}} {{/-first}}{{/required}}{{/allParams}}) +{{/sortParamsByRequiredFlag}} + +{{#requiredParams}} +{{^-first}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}} (required) + :type {{paramName}}: {{dataType}} +{{/-first}} +{{/requiredParams}} +{{#optionalParams}} + :param {{paramName}}:{{#description}} {{{.}}}{{/description}}(optional) + :type {{paramName}}: {{dataType}}, optional +{{/optionalParams}} + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _return_http_data_only: response data without head status code + and headers + :type _return_http_data_only: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :param _retry_param: if specified, override the retry parameters specified in configuration + :type _request_auth: dict, optional + :type _content_type: string, optional: force content-type for the request + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: {{#returnType}}tuple({{.}}, status_code(int), headers(HTTPHeaderDict)){{/returnType}}{{^returnType}}None{{/returnType}} + """ + + {{#servers.0}} + local_var_hosts = [ +{{#servers}} + '{{{url}}}'{{^-last}},{{/-last}} +{{/servers}} + ] + local_var_host = local_var_hosts[0] + if kwargs.get('_host_index'): + _host_index = int(kwargs.get('_host_index')) + if _host_index < 0 or _host_index >= len(local_var_hosts): + raise ApiValueError( + "Invalid host index. Must be 0 <= index < %s" + % len(local_var_host) + ) + local_var_host = local_var_hosts[_host_index] + {{/servers.0}} + local_var_params = locals() + + all_params = [ +{{#requiredParams}}{{^-first}} + '{{paramName}}'{{^-last}},{{/-last}} +{{/-first}}{{/requiredParams}} +{{#optionalParams}} + '{{paramName}}'{{^-last}},{{/-last}} +{{/optionalParams}} + ] + all_params.extend( + [ + "async_req", + "_return_http_data_only", + "_preload_content", + "_request_timeout", + "_request_auth", + "_content_type", + "_headers", + "_retry_parms" + ] + ) + + for key, val in local_var_params['kwargs'].items(): + if key not in all_params{{#servers.0}} and key != "_host_index"{{/servers.0}}: + raise FgaValidationException( + "Got an unexpected keyword argument '%s'" + " to method {{operationId}}" % key + ) + local_var_params[key] = val + del local_var_params['kwargs'] +{{#allParams}} +{{^isNullable}} +{{#required}} +{{^-first}} + # verify the required parameter '{{paramName}}' is set + if self.api_client.client_side_validation and local_var_params.get('{{paramName}}') is None: + raise ApiValueError( + "Missing the required parameter `{{paramName}}` when calling `{{operationId}}`") +{{/-first}} +{{/required}} +{{/isNullable}} +{{/allParams}} + +{{#allParams}} +{{#hasValidation}} + {{#maxLength}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) > {{maxLength}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be less than or equal to `{{maxLength}}`") + {{/maxLength}} + {{#minLength}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) < {{minLength}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, length must be greater than or equal to `{{minLength}}`") + {{/minLength}} + {{#maximum}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}: + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value less than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}`{{maximum}}`") + {{/maximum}} + {{#minimum}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and local_var_params['{{paramName}}'] <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}: + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must be a value greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}`{{minimum}}`") + {{/minimum}} + {{#pattern}} + if self.api_client.client_side_validation and '{{paramName}}' in local_var_params and not re.search(r'{{{vendorExtensions.x-regex}}}', local_var_params['{{paramName}}']{{#vendorExtensions.x-modifiers}}{{#-first}}, flags={{/-first}}re.{{.}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-modifiers}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, must conform to the pattern `{{{pattern}}}`") + {{/pattern}} + {{#maxItems}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) > {{maxItems}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be less than or equal to `{{maxItems}}`") + {{/maxItems}} + {{#minItems}} + if self.api_client.client_side_validation and ('{{paramName}}' in local_var_params and + len(local_var_params['{{paramName}}']) < {{minItems}}): + raise ApiValueError( + "Invalid value for parameter `{{paramName}}` when calling `{{operationId}}`, number of items must be greater than or equal to `{{minItems}}`") + {{/minItems}} +{{/hasValidation}} +{{#-last}} +{{/-last}} +{{/allParams}} + collection_formats = {} + + path_params = {} + + store_id = None +{{#pathParams}} +{{^-first}} + if '{{paramName}}' in local_var_params: + path_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/-first}} + +{{#-first}} + if self.api_client._get_store_id() is None: + raise ApiValueError("Store ID expected in api_client's configuration when calling `{{operationId}}`") + store_id = self.api_client._get_store_id() +{{/-first}} + +{{/pathParams}} + + query_params = [] +{{#queryParams}} + if local_var_params.get('{{paramName}}') is not None: + query_params.append( + ('{{baseName}}', local_var_params['{{paramName}}'])){{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/queryParams}} + + header_params = dict(local_var_params.get('_headers', {})) +{{#headerParams}} + if '{{paramName}}' in local_var_params: + header_params['{{baseName}}'] = local_var_params['{{paramName}}']{{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/headerParams}} + + form_params = [] + local_var_files = {} +{{#formParams}} + if '{{paramName}}' in local_var_params: + {{^isFile}}form_params.append(('{{baseName}}', local_var_params['{{paramName}}'])){{/isFile}}{{#isFile}}local_var_files['{{baseName}}'] = local_var_params['{{paramName}}']{{/isFile}}{{#isArray}} + collection_formats['{{baseName}}'] = '{{collectionFormat}}'{{/isArray}} +{{/formParams}} + + body_params = None +{{#bodyParam}} + if '{{paramName}}' in local_var_params: + body_params = local_var_params['{{paramName}}'] +{{/bodyParam}} + {{#hasProduces}} + # HTTP header `Accept` + header_params['Accept'] = self.api_client.select_header_accept( + [{{#produces}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/produces}}]) + + {{/hasProduces}} + {{#hasConsumes}} + # HTTP header `Content-Type` + content_types_list = local_var_params.get('_content_type', self.api_client.select_header_content_type([{{#consumes}}'{{{mediaType}}}'{{^-last}}, {{/-last}}{{/consumes}}],'{{httpMethod}}', body_params)) + if content_types_list: + header_params['Content-Type'] = content_types_list + + {{/hasConsumes}} + # Authentication setting + auth_settings = [{{#authMethods}}'{{name}}'{{^-last}}, {{/-last}}{{/authMethods}}] + + {{#returnType}} + {{#responses}} + {{#-first}} + response_types_map = { + {{/-first}} + {{^isWildcard}} + {{code}}: {{#dataType}}"{{.}}"{{/dataType}}{{^dataType}}None{{/dataType}}, + {{/isWildcard}} + {{#-last}} + } + {{/-last}} + {{/responses}} + {{/returnType}} + {{^returnType}} + response_types_map = {} + {{/returnType}} + + telemetry_attributes: dict[TelemetryAttribute, str] = { + TelemetryAttributes.request_method: "{{operationId}}" + } + + try: + if store_id: + telemetry_attributes[TelemetryAttributes.request_store_id] = store_id + except: + pass + + try: + if body_params.tuple_key: + telemetry_attributes[TelemetryAttributes.client_user] = ( + body_params.tuple_key.user + ) + except: + pass + + try: + if "authorization_model_id" in local_var_params: + telemetry_attributes[TelemetryAttributes.request_model_id] = local_var_params[ + "authorization_model_id" + ] + elif body_params.authorization_model_id: + telemetry_attributes[TelemetryAttributes.request_model_id] = ( + body_params.authorization_model_id + ) + except: + pass + + return self.api_client.call_api( + '{{{path}}}'.replace('{store_id}', store_id), '{{httpMethod}}', + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_types_map=response_types_map, + auth_settings=auth_settings, + async_req=local_var_params.get('async_req'), + _return_http_data_only=local_var_params.get('_return_http_data_only'), + _preload_content=local_var_params.get('_preload_content', True), + _request_timeout=local_var_params.get('_request_timeout'), + _retry_params=local_var_params.get('_retry_params'), + {{#servers.0}} + _host=local_var_host, + {{/servers.0}} + collection_formats=collection_formats, + _request_auth=local_var_params.get('_request_auth'), + _oauth2_client=self._oauth2_client, + _telemetry_attributes=telemetry_attributes, + ) +{{/operation}} +{{/operations}} diff --git a/config/clients/python/template/api_client_sync.mustache b/config/clients/python/template/src/sync/api_client.py.mustache similarity index 86% rename from config/clients/python/template/api_client_sync.mustache rename to config/clients/python/template/src/sync/api_client.py.mustache index 5443c146..1788d707 100644 --- a/config/clients/python/template/api_client_sync.mustache +++ b/config/clients/python/template/src/sync/api_client.py.mustache @@ -1,24 +1,32 @@ {{>partial_header}} -import time import atexit import datetime -from dateutil.parser import parse import json import math -from multiprocessing.pool import ThreadPool import random import re +import time import urllib -import {{modelPackage}} {{#tornado}} import tornado.gen {{/tornado}} +from multiprocessing.pool import ThreadPool -from {{packageName}}.configuration import Configuration -from {{packageName}}.sync import rest, oauth2 -from {{packageName}}.exceptions import ApiValueError, ApiException, FgaValidationException, RateLimitExceededError, ServiceException +from dateutil.parser import parse +import {{modelPackage}} +from {{packageName}}.sync import rest, oauth2 +from {{packageName}}.configuration import Configuration +from {{packageName}}.exceptions import ( + ApiException, + ApiValueError, + FgaValidationException, + RateLimitExceededError, + ServiceException, +) +from {{packageName}}.telemetry import Telemetry +from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes DEFAULT_USER_AGENT = '{{{userAgent}}}' @@ -67,21 +75,32 @@ class ApiClient: } _pool = None - def __init__(self, configuration=None, header_name=None, header_value=None, - cookie=None, pool_threads=1): + def __init__( + self, + configuration=None, + header_name=None, + header_value=None, + cookie=None, + pool_threads=1, + ): if configuration is None: configuration = Configuration.get_default_copy() + self.configuration = configuration self.pool_threads = pool_threads self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} if header_name is not None: self.default_headers[header_name] = header_value + self.cookie = cookie - # Set default User-Agent. + self.user_agent = DEFAULT_USER_AGENT + self.client_side_validation = configuration.client_side_validation + self._telemetry = Telemetry() def __enter__(self): return self @@ -123,16 +142,38 @@ class ApiClient: @tornado.gen.coroutine {{/tornado}} def __call_api( - self, resource_path, method, path_params=None, - query_params=None, header_params=None, body=None, post_params=None, - response_types_map=None, auth_settings=None, - _return_http_data_only=None, collection_formats=None, - _preload_content=True, _request_timeout=None, _host=None, - _request_auth=None, _retry_params=None, _oauth2_client=None): + self, + resource_path, + method, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + response_types_map=None, + auth_settings=None, + _return_http_data_only=None, + collection_formats=None, + _preload_content=True, + _request_timeout=None, + _host=None, + _request_auth=None, + _retry_params=None, + _oauth2_client=None, + _telemetry_attributes: dict[TelemetryAttribute | str, str] = None, + ): self.configuration.is_valid() config = self.configuration + benchmark: dict[str, float] = {"start": float(time.time())} + _telemetry_attributes = _telemetry_attributes or {} + + _telemetry_attributes[TelemetryAttributes().http_host] = urllib.parse.urlparse( + config.api_url + ).hostname + _telemetry_attributes[TelemetryAttributes().http_method] = method + # header parameters header_params = header_params or {} header_params.update(self.default_headers) @@ -194,7 +235,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 - for x in range(max_retry+1): + for retry in range(max_retry+1): try: # perform request and return response response_data = {{#tornado}}yield {{/tornado}}self.request( @@ -203,8 +244,8 @@ class ApiClient: _preload_content=_preload_content, _request_timeout=_request_timeout) except (RateLimitExceededError, ServiceException) as e: - if x < max_retry and e.status != 501: - time.sleep(random_time(x, min_wait_in_ms)) + if retry < max_retry and e.status != 501: + time.sleep(random_time(retry, min_wait_in_ms)) continue e.body = e.body.decode('utf-8') response_type = response_types_map.get(e.status, None) @@ -224,6 +265,31 @@ class ApiClient: return_data = response_data + benchmark["end"] = float(time.time()) + + _telemetry_attributes[TelemetryAttributes().request_retries] = retry + + _telemetry_attributes.update( + TelemetryAttributes().fromResponse(response_data) + ) + + try: + _telemetry_attributes[TelemetryAttributes().request_client_id] = ( + self.configuration.credentials.configuration.client_id + ) + except AttributeError: + pass + + duration = response_data.getheader("fga-query-duration-ms") + attributes = TelemetryAttributes().prepare(_telemetry_attributes) + + if duration is not None: + self._telemetry.metrics().queryDuration(int(duration, 10), attributes) + + self._telemetry.metrics().requestDuration().record( + float(benchmark["end"] - benchmark["start"]), attributes + ) + if not _preload_content: {{^tornado}} return return_data @@ -363,14 +429,29 @@ class ApiClient: else: return self.__deserialize_model(data, klass) - def call_api(self, resource_path, method, - path_params=None, query_params=None, header_params=None, - body=None, post_params=None, files=None, - response_types_map=None, auth_settings=None, - async_req=None, _return_http_data_only=None, - collection_formats=None,_preload_content=True, - _request_timeout=None, _host=None, _request_auth=None, - _retry_params=None, _oauth2_client=None): + def call_api( + self, + resource_path, + method, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + files=None, + response_types_map=None, + auth_settings=None, + async_req=None, + _return_http_data_only=None, + collection_formats=None, + _preload_content=True, + _request_timeout=None, + _host=None, + _request_auth=None, + _retry_params=None, + _oauth2_client=None, + _telemetry_attributes: dict[TelemetryAttribute, str] = None, + ): """Makes the HTTP request (synchronous) and returns deserialized data. To make an async_req request, set the async_req parameter. @@ -412,28 +493,50 @@ class ApiClient: then the method will return the response directly. """ if not async_req: - return self.__call_api(resource_path, method, - path_params, query_params, header_params, - body, post_params, - response_types_map, auth_settings, - _return_http_data_only, collection_formats, - _preload_content, _request_timeout, _host, - _request_auth, _retry_params, _oauth2_client) - - return self.pool.apply_async(self.__call_api, (resource_path, - method, path_params, - query_params, - header_params, body, - post_params, - response_types_map, - auth_settings, - _return_http_data_only, - collection_formats, - _preload_content, - _request_timeout, - _host, _request_auth, - _retry_params, - _oauth2_client)) + return self.__call_api( + resource_path, + method, + path_params, + query_params, + header_params, + body, + post_params, + response_types_map, + auth_settings, + _return_http_data_only, + collection_formats, + _preload_content, + _request_timeout, + _host, + _request_auth, + _retry_params, + _oauth2_client, + _telemetry_attributes, + ) + + return self.pool.apply_async( + self.__call_api, + ( + resource_path, + method, + path_params, + query_params, + header_params, + body, + post_params, + response_types_map, + auth_settings, + _return_http_data_only, + collection_formats, + _preload_content, + _request_timeout, + _host, + _request_auth, + _retry_params, + _oauth2_client, + _telemetry_attributes, + ), + ) def request(self, method, url, query_params=None, headers=None, post_params=None, body=None, _preload_content=True, diff --git a/config/clients/python/template/__init__sync_client.mustache b/config/clients/python/template/src/sync/client/__init__.py.mustache similarity index 100% rename from config/clients/python/template/__init__sync_client.mustache rename to config/clients/python/template/src/sync/client/__init__.py.mustache diff --git a/config/clients/python/template/client/client_sync.mustache b/config/clients/python/template/src/sync/client/client.py.mustache similarity index 100% rename from config/clients/python/template/client/client_sync.mustache rename to config/clients/python/template/src/sync/client/client.py.mustache diff --git a/config/clients/python/template/oauth2_sync.mustache b/config/clients/python/template/src/sync/oauth2.py.mustache similarity index 90% rename from config/clients/python/template/oauth2_sync.mustache rename to config/clients/python/template/src/sync/oauth2.py.mustache index 94ddc303..8fb41c78 100644 --- a/config/clients/python/template/oauth2_sync.mustache +++ b/config/clients/python/template/src/sync/oauth2.py.mustache @@ -12,6 +12,8 @@ import urllib3 from {{packageName}}.configuration import Configuration from {{packageName}}.credentials import Credentials from {{packageName}}.exceptions import AuthenticationError +from {{packageName}}.telemetry.attributes import TelemetryAttributes +from {{packageName}}.telemetry.telemetry import Telemetry def jitter(loop_count, min_wait_in_ms): @@ -35,6 +37,7 @@ class OAuth2Client: self._credentials = credentials self._access_token = None self._access_expiry_time = None + self._telemetry = Telemetry() if configuration is None: configuration = Configuration.get_default_copy() @@ -107,6 +110,12 @@ class OAuth2Client: seconds=int(api_response.get("expires_in")) ) self._access_token = api_response.get("access_token") + self._telemetry.metrics().credentialsRequest( + 1, + { + TelemetryAttributes.request_client_id: configuration.client_id + }, + ) break raise AuthenticationError(http_resp=raw_response) diff --git a/config/clients/python/template/rest_sync.mustache b/config/clients/python/template/src/sync/rest.py.mustache similarity index 100% rename from config/clients/python/template/rest_sync.mustache rename to config/clients/python/template/src/sync/rest.py.mustache diff --git a/config/clients/python/template/src/telemetry/__init__.py.mustache b/config/clients/python/template/src/telemetry/__init__.py.mustache new file mode 100644 index 00000000..e23e60a2 --- /dev/null +++ b/config/clients/python/template/src/telemetry/__init__.py.mustache @@ -0,0 +1,4 @@ +from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes +from {{packageName}}.telemetry.histograms import TelemetryHistogram, TelemetryHistograms +from {{packageName}}.telemetry.metrics import MetricsTelemetry +from {{packageName}}.telemetry.telemetry import Telemetry diff --git a/config/clients/python/template/src/telemetry/attributes.py.mustache b/config/clients/python/template/src/telemetry/attributes.py.mustache new file mode 100644 index 00000000..4219da25 --- /dev/null +++ b/config/clients/python/template/src/telemetry/attributes.py.mustache @@ -0,0 +1,82 @@ +from typing import NamedTuple + +from aiohttp import ClientResponse +from urllib3 import HTTPResponse + +from {{packageName}}.credentials import Credentials +from {{packageName}}.rest import RESTResponse + + +class TelemetryAttribute(NamedTuple): + name: str + + +class TelemetryAttributes: + request_model_id: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.model_id", + ) + request_method: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.method", + ) + request_store_id: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.store_id", + ) + request_client_id: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.client_id", + ) + request_retries: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.retries", + ) + response_model_id: TelemetryAttribute = TelemetryAttribute( + name="fga-client.response.model_id", + ) + client_user: TelemetryAttribute = TelemetryAttribute( + name="fga-client.user", + ) + http_host: TelemetryAttribute = TelemetryAttribute( + name="http.host", + ) + http_method: TelemetryAttribute = TelemetryAttribute( + name="http.method", + ) + http_status_code: TelemetryAttribute = TelemetryAttribute( + name="http.status_code", + ) + + def prepare( + self, attributes: dict[TelemetryAttribute | str, str | int] | None + ) -> dict: + response = {} + + if attributes is not None: + for attribute, value in attributes.items(): + if isinstance(attribute, TelemetryAttribute): + response[attribute.name] = value + else: + response[attribute] = value + + return dict(sorted(response.items())) + + def fromResponse( + self, + response: HTTPResponse | RESTResponse | ClientResponse = None, + credentials: Credentials = None, + ): + attributes: dict[TelemetryAttribute | str, str | int] = {} + + if response: + if response.status: + attributes[self.http_status_code] = int(response.status) + + response_model_id = response.getheader("openfga-authorization-model-id") + + if response_model_id is not None: + attributes[self.response_model_id.name] = response_model_id + + if isinstance(credentials, Credentials): + if credentials.method == "client_credentials": + attributes[self.request_client_id.name] = ( + credentials.configuration.client_id + ) + + return attributes diff --git a/config/clients/python/template/src/telemetry/counters.py.mustache b/config/clients/python/template/src/telemetry/counters.py.mustache new file mode 100644 index 00000000..bd856347 --- /dev/null +++ b/config/clients/python/template/src/telemetry/counters.py.mustache @@ -0,0 +1,15 @@ +from typing import NamedTuple + + +class TelemetryCounter(NamedTuple): + name: str + unit: str + description: str + + +class TelemetryCounters: + credentials_request: TelemetryCounter = TelemetryCounter( + name="fga-client.credentials.request", + unit="milliseconds", + description="The number of times an access token is requested.", + ) diff --git a/config/clients/python/template/src/telemetry/histograms.py.mustache b/config/clients/python/template/src/telemetry/histograms.py.mustache new file mode 100644 index 00000000..51966775 --- /dev/null +++ b/config/clients/python/template/src/telemetry/histograms.py.mustache @@ -0,0 +1,20 @@ +from typing import NamedTuple + + +class TelemetryHistogram(NamedTuple): + name: str + unit: str + description: str + + +class TelemetryHistograms: + duration: TelemetryHistogram = TelemetryHistogram( + name="fga-client.request.duration", + unit="milliseconds", + description="How long it took for a request to be fulfilled.", + ) + query_duration: TelemetryHistogram = TelemetryHistogram( + name="fga-client.query.duration", + unit="milliseconds", + description="How long it took to perform a query request.", + ) diff --git a/config/clients/python/template/src/telemetry/metrics.py.mustache b/config/clients/python/template/src/telemetry/metrics.py.mustache new file mode 100644 index 00000000..cb5ed76f --- /dev/null +++ b/config/clients/python/template/src/telemetry/metrics.py.mustache @@ -0,0 +1,94 @@ +from opentelemetry.metrics import Counter, Histogram, Meter, get_meter + +from {{packageName}} import __version__ +from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes +from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters +from {{packageName}}.telemetry.histograms import TelemetryHistogram, TelemetryHistograms + + +class MetricsTelemetry: + _meter: Meter = None + _histograms: dict[str, Histogram] = {} + _counters: dict[str, Counter] = {} + + def __init__(self): + self._meter = get_meter("openfga-sdk", __version__) + self._histograms = {} + + def meter(self) -> Meter: + return self._meter + + def counter( + self, + counter: str | TelemetryCounter, + value: int = None, + attributes: dict[TelemetryAttribute | str, str | int] | None = None, + ) -> Counter: + if isinstance(counter, str): + counter = TelemetryCounters[counter] + + if not isinstance(counter, TelemetryCounter): + raise ValueError( + "counter must be a TelemetryCounter, or a string that is a key in TelemetryCounters" + ) + + if counter.name not in self._counters: + self._counters[counter.name] = self._meter.create_counter( + name=counter.name, unit=counter.unit, description=counter.description + ) + + if value is not None: + self._counters[counter.name].add( + amount=value, attributes=TelemetryAttributes().prepare(attributes) + ) + + return self._counters[counter.name] + + def histogram( + self, + histogram: str | TelemetryHistogram, + value: int | float = None, + attributes: dict[TelemetryAttribute | str, str | int] | None = None, + ) -> Histogram: + if isinstance(histogram, str): + histogram = TelemetryHistograms[histogram] + + if not isinstance(histogram, TelemetryHistogram): + raise ValueError( + "histogram must be a TelemetryHistogram, or a string that is a key in TelemetryHistograms" + ) + + if histogram.name not in self._histograms: + self._histograms[histogram.name] = self._meter.create_histogram( + name=histogram.name, + unit=histogram.unit, + description=histogram.description, + ) + + if value is not None: + self._histograms[histogram.name].record( + amount=value, attributes=TelemetryAttributes().prepare(attributes) + ) + + return self._histograms[histogram.name] + + def credentialsRequest( + self, + value: int | float = None, + attributes: dict[TelemetryAttribute | str, str | int] | None = None, + ) -> Counter: + return self.counter(TelemetryCounters.credentials_request, value, attributes) + + def requestDuration( + self, + value: int | float = None, + attributes: dict[TelemetryAttribute | str, str | int] | None = None, + ) -> Histogram: + return self.histogram(TelemetryHistograms.duration, value, attributes) + + def queryDuration( + self, + value: int | float = None, + attributes: dict[TelemetryAttribute | str, str | int] | None = None, + ) -> Histogram: + return self.histogram(TelemetryHistograms.query_duration, value, attributes) diff --git a/config/clients/python/template/src/telemetry/telemetry.py.mustache b/config/clients/python/template/src/telemetry/telemetry.py.mustache new file mode 100644 index 00000000..d01033b0 --- /dev/null +++ b/config/clients/python/template/src/telemetry/telemetry.py.mustache @@ -0,0 +1,11 @@ +from {{packageName}}.telemetry.metrics import MetricsTelemetry + + +class Telemetry: + _metrics: MetricsTelemetry = None + + def metrics(self) -> MetricsTelemetry: + if not self._metrics: + self._metrics = MetricsTelemetry() + + return self._metrics diff --git a/config/clients/python/template/validation.mustache b/config/clients/python/template/src/validation.py.mustache similarity index 100% rename from config/clients/python/template/validation.mustache rename to config/clients/python/template/src/validation.py.mustache diff --git a/config/clients/python/template/test-requirements.mustache b/config/clients/python/template/test-requirements.mustache index c8243d74..fdc80111 100644 --- a/config/clients/python/template/test-requirements.mustache +++ b/config/clients/python/template/test-requirements.mustache @@ -7,4 +7,4 @@ flake8 >= 7.0.0, < 8 griffe >= 0.41.2, < 1 isort==5.13.2 pytest-cov >= 5, < 6 -pyupgrade==3.15.2 +pyupgrade==3.16.0 diff --git a/config/clients/python/template/__init__.mustache b/config/clients/python/template/test/__init__.py.mustache similarity index 100% rename from config/clients/python/template/__init__.mustache rename to config/clients/python/template/test/__init__.py.mustache diff --git a/config/clients/python/template/test/api/__init__.py.mustache b/config/clients/python/template/test/api/__init__.py.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/python/template/test/api_test.py.mustache b/config/clients/python/template/test/api_test.py.mustache new file mode 100644 index 00000000..7306304c --- /dev/null +++ b/config/clients/python/template/test/api_test.py.mustache @@ -0,0 +1,1430 @@ +{{>partial_header}} + +import unittest +from unittest.mock import ANY +from unittest import IsolatedAsyncioTestCase +from unittest.mock import patch +from datetime import datetime + +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}}.models.assertion import Assertion +from {{packageName}}.models.authorization_model import AuthorizationModel +from {{packageName}}.models.check_request import CheckRequest +from {{packageName}}.models.check_response import CheckResponse +from {{packageName}}.models.create_store_request import CreateStoreRequest +from {{packageName}}.models.create_store_response import CreateStoreResponse +from {{packageName}}.models.error_code import ErrorCode +from {{packageName}}.models.expand_request import ExpandRequest +from {{packageName}}.models.expand_request_tuple_key import ExpandRequestTupleKey +from {{packageName}}.models.expand_response import ExpandResponse +from {{packageName}}.models.get_store_response import GetStoreResponse +from {{packageName}}.models.internal_error_code import InternalErrorCode +from {{packageName}}.models.internal_error_message_response import InternalErrorMessageResponse +from {{packageName}}.models.leaf import Leaf +from {{packageName}}.models.list_objects_request import ListObjectsRequest +from {{packageName}}.models.list_objects_response import ListObjectsResponse +from {{packageName}}.models.list_stores_response import ListStoresResponse +from {{packageName}}.models.list_users_request import ListUsersRequest +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.read_assertions_response import ReadAssertionsResponse +from {{packageName}}.models.read_authorization_model_response import ReadAuthorizationModelResponse +from {{packageName}}.models.read_changes_response import ReadChangesResponse +from {{packageName}}.models.read_request import ReadRequest +from {{packageName}}.models.read_request_tuple_key import ReadRequestTupleKey +from {{packageName}}.models.read_response import ReadResponse +from {{packageName}}.models.store import Store +from {{packageName}}.models.tuple import Tuple +from {{packageName}}.models.tuple_change import TupleChange +from {{packageName}}.models.tuple_key import TupleKey +from {{packageName}}.models.tuple_key_without_condition import TupleKeyWithoutCondition +from {{packageName}}.models.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.write_assertions_request import WriteAssertionsRequest +from {{packageName}}.models.write_authorization_model_request import WriteAuthorizationModelRequest +from {{packageName}}.models.write_authorization_model_response import WriteAuthorizationModelResponse +from {{packageName}}.models.write_request import WriteRequest + +store_id = '01H0H015178Y2V4CX10C2KGHF4' +request_id = 'x1y2z3' + +# Helper function to construct mock response +def http_mock_response(body, status): + headers = urllib3.response.HTTPHeaderDict({ + 'content-type': 'application/json', + 'Fga-Request-Id': request_id + }) + return urllib3.HTTPResponse( + body.encode('utf-8'), + headers, + status, + preload_content=False + ) + +def mock_response(body, status): + obj = http_mock_response(body, status) + return rest.RESTResponse(obj, obj.data) + +class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): + """{{classname}} unit test stubs""" + + def setUp(self): + self.configuration = {{packageName}}.Configuration( + api_url='http://api.{{sampleApiDomain}}', + ) + + def tearDown(self): + pass + + @patch.object(rest.RESTClientObject, 'request') + async def test_check(self, mock_request): + """Test case for check + + Check whether a user is authorized to access an object + """ + + # First, mock the response + response_body = '{"allowed": true, "resolution": "1234"}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", + ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:2021-budget", + "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + {{#asyncio}}await {{/asyncio}}api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_create_store(self, mock_request): + """Test case for create_store + + Create a store + """ + 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 {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CreateStoreRequest( + name="test-store", + ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.create_store( + body=body, + ) + self.assertIsInstance(api_response, CreateStoreResponse) + self.assertEqual(api_response.id, '01YCP46JKYM8FJCQ37NMBYHE5X') + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores', + headers=ANY, + query_params=[], + post_params=[], + body={"name": "test-store"}, + _preload_content=ANY, + _request_timeout=None + ) + {{#asyncio}}await {{/asyncio}}api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_delete_store(self, mock_request): + """Test case for delete_store + + Delete a store + """ + response_body = '' + mock_request.return_value = mock_response(response_body, 201) + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + 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', + headers=ANY, + query_params=[], + body=None, + _preload_content=ANY, + _request_timeout=None + ) + {{#asyncio}}await {{/asyncio}}api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_expand(self, mock_request): + """Test case for expand + + Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship + """ + 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 {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = ExpandRequest( + tuple_key=ExpandRequestTupleKey( + object="document:budget", + relation="reader", + ), + authorization_model_id="01GXSA8YR785C4FYS3C0RTG7B1", + ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.expand( + body=body, + ) + self.assertIsInstance(api_response, ExpandResponse) + cur_users = Users(users=["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]) + leaf = Leaf(users=cur_users) + node = Node(name="document:budget#reader", leaf=leaf) + userTree = UsersetTree(node) + expected_response = ExpandResponse(userTree) + self.assertEqual(api_response, expected_response) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/expand', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key": {"object": "document:budget", "relation": "reader"}, "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + _preload_content=ANY, + _request_timeout=None + ) + {{#asyncio}}await {{/asyncio}}api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_get_store(self, mock_request): + """Test case for get_store + + Get a store + """ + 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 + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + # 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') + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + {{#asyncio}}await {{/asyncio}}api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_list_objects(self, mock_request): + """Test case for list_objects + + List objects + """ + 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 {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = ListObjectsRequest( + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + type="document", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + # Get all stores + api_response = {{#asyncio}}await {{/asyncio}}api_instance.list_objects(body) + self.assertIsInstance(api_response, ListObjectsResponse) + self.assertEqual(api_response.objects, ['document:abcd1234']) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-objects', + headers=ANY, + query_params=[], + post_params=[], + body={'authorization_model_id': '01G5JAVJ41T49E9TT3SKVS7X1J', + 'type': 'document', 'relation': 'reader', 'user': 'user:81684243-9356-4421-8fbf-a4f8d36aa31b'}, + _preload_content=ANY, + _request_timeout=None + ) + {{#asyncio}}await {{/asyncio}}api_client.close() + + @patch.object(rest.RESTClientObject, 'request') + async def test_list_stores(self, mock_request): + """Test case for list_stores + + Get all stores + """ + response_body = ''' +{ + "stores": [ + { + "id": "01YCP46JKYM8FJCQ37NMBYHE5X", + "name": "store1", + "created_at": "2022-07-25T21:15:37.524Z", + "updated_at": "2022-07-25T21:15:37.524Z", + "deleted_at": "2022-07-25T21:15:37.524Z" + }, + { + "id": "01YCP46JKYM8FJCQ37NMBYHE6X", + "name": "store2", + "created_at": "2022-07-25T21:15:37.524Z", + "updated_at": "2022-07-25T21:15:37.524Z", + "deleted_at": "2022-07-25T21:15:37.524Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + # Get all stores + api_response = {{#asyncio}}await {{/asyncio}}api_instance.list_stores( + page_size=1, + continuation_token="continuation_token_example", + ) + self.assertIsInstance(api_response, ListStoresResponse) + self.assertEqual(api_response.continuation_token, + "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==") + store1 = Store( + id="01YCP46JKYM8FJCQ37NMBYHE5X", + name="store1", + created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + ) + store2 = Store( + id="01YCP46JKYM8FJCQ37NMBYHE6X", + name="store2", + created_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + updated_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + deleted_at=datetime.fromisoformat("2022-07-25T21:15:37.524+00:00"), + ) + + stores = [store1, store2] + self.assertEqual(api_response.stores, stores) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores', + headers=ANY, + query_params=[('page_size', 1), ('continuation_token', + 'continuation_token_example')], + _preload_content=ANY, + _request_timeout=None + ) + {{#asyncio}}await {{/asyncio}}api_client.close() + + + @patch.object(rest.RESTClientObject, "request") + {{#asyncio}}async {{/asyncio}}def test_list_users(self, mock_request): + """ + Test case for list_users + """ + + response_body = """{ + "users": [ + { + "object": { + "id": "81684243-9356-4421-8fbf-a4f8d36aa31b", + "type": "user" + } + }, + { + "userset": { + "id": "fga", + "relation": "member", + "type": "team" + } + }, + { + "wildcard": { + "type": "user" + } + } + ] +}""" + + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + + {{#asyncio}}async {{/asyncio}}with openfga_sdk.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + + request = ListUsersRequest( + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + object="document:2021-budget", + relation="can_read", + user_filters=[ + {"type": "user"}, + {"type": "team", "relation": "member"}, + ], + context={}, + contextual_tuples=[ + { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "editor", + "object": "folder:product", + }, + { + "user": "folder:product", + "relation": "parent", + "object": "document:roadmap", + }, + ], + ) + + response = {{#asyncio}}await {{/asyncio}}api_instance.list_users(request) + + self.assertIsInstance(response, ListUsersResponse) + + self.assertEqual(response.users.__len__(), 3) + + self.assertIsNotNone(response.users[0].object) + self.assertEqual( + response.users[0].object.id, "81684243-9356-4421-8fbf-a4f8d36aa31b" + ) + self.assertEqual(response.users[0].object.type, "user") + self.assertIsNone(response.users[0].userset) + self.assertIsNone(response.users[0].wildcard) + + self.assertIsNone(response.users[1].object) + self.assertIsNotNone(response.users[1].userset) + self.assertEqual(response.users[1].userset.id, "fga") + self.assertEqual(response.users[1].userset.relation, "member") + self.assertEqual(response.users[1].userset.type, "team") + self.assertIsNone(response.users[1].wildcard) + + self.assertIsNone(response.users[2].object) + self.assertIsNone(response.users[2].userset) + self.assertIsNotNone(response.users[2].wildcard) + self.assertEqual(response.users[2].wildcard.type, "user") + + mock_request.assert_called_once_with( + "POST", + "http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/list-users", + headers=ANY, + query_params=[], + post_params=[], + body={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "object": "document:2021-budget", + "relation": "can_read", + "user_filters": [ + {"type": "user"}, + {"type": "team", "relation": "member"}, + ], + "context": {}, + "contextual_tuples": [ + { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "editor", + "object": "folder:product", + }, + { + "user": "folder:product", + "relation": "parent", + "object": "document:roadmap", + }, + ], + }, + _preload_content=ANY, + _request_timeout=None, + ) + + await api_client.close() + + + @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 = ''' + { + "tuples": [ + { + "key": { + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + "relation": "reader", + "object": "document:2021-budget" + }, + "timestamp": "2021-10-06T15:32:11.128Z" + } + ], + "continuation_token": "" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = ReadRequest( + tuple_key=ReadRequestTupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + page_size=50, + continuation_token="eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==", + ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read( + body=body, + ) + self.assertIsInstance(api_response, ReadResponse) + key = TupleKey(user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",relation="reader",object="document:2021-budget") + timestamp = datetime.fromisoformat("2021-10-06T15:32:11.128+00:00") + expected_data = ReadResponse( + tuples=[Tuple(key=key, timestamp=timestamp)], continuation_token='') + self.assertEqual(api_response, expected_data) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/read', + headers=ANY, + query_params=[], + post_params=[], + body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"},"page_size":50,"continuation_token":"eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_read_assertions(self, mock_request): + """Test case for read_assertions + + Read assertions for an authorization model ID + """ + response_body = ''' +{ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "assertions": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + }, + "expectation": true + } + ] +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_assertions( + "01G5JAVJ41T49E9TT3SKVS7X1J", + ) + self.assertIsInstance(api_response, ReadAssertionsResponse) + self.assertEqual(api_response.authorization_model_id, '01G5JAVJ41T49E9TT3SKVS7X1J') + assertion=Assertion( + tuple_key=TupleKeyWithoutCondition( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + expectation=True, + ) + self.assertEqual(api_response.assertions, [assertion]) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_read_authorization_model(self, mock_request): + """Test case for read_authorization_model + + Return a particular version of an authorization model + """ + response_body = ''' +{ + "authorization_model": { + "id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "schema_version":"1.1", + "type_definitions": [ + { + "type": "document", + "relations": { + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "object": "", + "relation": "writer" + } + } + ] + } + }, + "writer": { + "this": {} + } + } + } + ] + } +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # Return a particular version of an authorization model + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_model( + "01G5JAVJ41T49E9TT3SKVS7X1J", + ) + self.assertIsInstance(api_response, ReadAuthorizationModelResponse) + type_definitions = [ + TypeDefinition( + type="document", + relations=dict( + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + writer=Userset( + this=dict(), + ), + ) + ) + ] + authorization_model = AuthorizationModel(id='01G5JAVJ41T49E9TT3SKVS7X1J', schema_version = "1.1", + type_definitions=type_definitions) + self.assertEqual(api_response.authorization_model, authorization_model) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J', + headers=ANY, + query_params=[], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_read_changes(self, mock_request): + """Test case for read_changes + + Return a list of all the tuple changes + """ + response_body = ''' +{ + "changes": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b" + }, + "operation": "TUPLE_OPERATION_WRITE", + "timestamp": "2022-07-26T15:55:55.809Z" + } + ], + "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==" +} + ''' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # Return a particular version of an authorization model + api_response = {{#asyncio}}await {{/asyncio}}api_instance.read_changes( + page_size=1, + continuation_token="abcdefg", + type="document" + ) + self.assertIsInstance(api_response, ReadChangesResponse) + changes = TupleChange( + tuple_key=TupleKey(object="document:2021-budget",relation="reader",user="user:81684243-9356-4421-8fbf-a4f8d36aa31b"), + operation=TupleOperation.WRITE, + timestamp=datetime.fromisoformat("2022-07-26T15:55:55.809+00:00")) + read_changes = ReadChangesResponse( + continuation_token='eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ==', + changes=[changes]) + self.assertEqual(api_response, read_changes) + mock_request.assert_called_once_with( + 'GET', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/changes', + headers=ANY, + query_params=[('type', 'document'), ('page_size', 1), ('continuation_token', 'abcdefg') ], + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write(self, mock_request): + """Test case for write + + Add tuples from the store + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + + body = WriteRequest( + writes=WriteRequestWrites( + tuple_keys=[ + TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ), + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + ) + {{#asyncio}}await {{/asyncio}}api_instance.write( + body, + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + headers=ANY, + query_params=[], + post_params=[], + body={"writes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write_delete(self, mock_request): + """Test case for write + + Delete tuples from the store + """ + response_body = '{}' + mock_request.return_value = mock_response(response_body, 200) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + + body = WriteRequest( + deletes=WriteRequestDeletes( + tuple_keys=[ + TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ), + authorization_model_id="01G5JAVJ41T49E9TT3SKVS7X1J", + ) + {{#asyncio}}await {{/asyncio}}api_instance.write( + body, + ) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/write', + headers=ANY, + query_params=[], + post_params=[], + body={"deletes":{"tuple_keys":[{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}]},"authorization_model_id":"01G5JAVJ41T49E9TT3SKVS7X1J"}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write_assertions(self, mock_request): + """Test case for write_assertions + + Upsert assertions for an authorization model ID + """ + response_body = '' + mock_request.return_value = mock_response(response_body, 204) + configuration = self.configuration + configuration.store_id = store_id + # Enter a context with an instance of the API client + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + body = WriteAssertionsRequest( + assertions=[ + Assertion( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + expectation=True, + ) + ], + ) + # Upsert assertions for an authorization model ID + {{#asyncio}}await {{/asyncio}}api_instance.write_assertions( + authorization_model_id="xyz0123", + body=body, + ) + mock_request.assert_called_once_with( + 'PUT', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/assertions/xyz0123', + headers=ANY, + query_params=[], + post_params=[], + body={"assertions":[{"expectation":True,"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}]}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_write_authorization_model(self, mock_request): + """Test case for write_authorization_model + + Create a new authorization model + """ + response_body = '{"authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J"}' + mock_request.return_value = mock_response(response_body, 201) + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # example passing only required values which don't have defaults set + body = WriteAuthorizationModelRequest( + schema_version = "1.1", + type_definitions=[ + TypeDefinition( + type="document", + relations=dict( + writer=Userset( + this=dict(), + ), + reader=Userset( + union=Usersets( + child=[ + Userset(this=dict()), + Userset(computed_userset=ObjectRelation( + object="", + relation="writer", + )), + ], + ), + ), + ) + ), + ], + ) + # Create a new authorization model + api_response = {{#asyncio}}await {{/asyncio}}api_instance.write_authorization_model( + body + ) + self.assertIsInstance(api_response, WriteAuthorizationModelResponse) + expected_response = WriteAuthorizationModelResponse( + authorization_model_id='01G5JAVJ41T49E9TT3SKVS7X1J' + ) + self.assertEqual(api_response, expected_response) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/authorization-models', + headers=ANY, + query_params=[], + post_params=[], + body={"schema_version":"1.1","type_definitions":[{"type":"document","relations":{"writer":{"this":{}},"reader":{"union":{"child":[{"this":{}},{"computedUserset":{"object":"","relation":"writer"}}]}}}}]}, + _preload_content=ANY, + _request_timeout=None + ) + + def test_default_scheme(self): + """ + Ensure default scheme is https + """ + configuration = {{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') + + def test_configuration_missing_host(self): + """ + Test whether FgaValidationException is raised if configuration does not have host specified + """ + 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.api_scheme = None + self.assertRaises(FgaValidationException, configuration.is_valid) + + def test_configuration_bad_scheme(self): + """ + Test whether ApiValueError is raised if scheme is bad + """ + configuration = {{packageName}}.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 = {{packageName}}.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 = {{packageName}}.Configuration( + api_host='localhost/mypath', + api_scheme='http' + ) + self.assertRaises(ApiValueError, configuration.is_valid) + + def test_configuration_has_query(self): + """ + Test whether ApiValueError is raised if host has query + """ + configuration = {{packageName}}.Configuration( + api_host='localhost?mypath=foo', + api_scheme='http' + ) + self.assertRaises(ApiValueError, configuration.is_valid) + + def test_configuration_store_id_invalid(self): + """ + Test whether ApiValueError is raised if host has query + """ + configuration = {{packageName}}.Configuration( + api_host='localhost', + api_scheme='http', + store_id="abcd" + ) + self.assertRaises(FgaValidationException, configuration.is_valid) + + def test_url(self): + """ + 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.is_valid() + + def test_url_with_scheme_and_host(self): + """ + 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' + ) + self.assertEqual(configuration.api_url, 'http://localhost:8080') + configuration.is_valid() # Should not throw and complain about scheme being invalid + + async def test_bad_configuration_read_authorization_model(self): + """ + Test whether FgaValidationException is raised for API (reading authorization models) + with configuration is having incorrect API scheme + """ + configuration = {{packageName}}.Configuration( + api_scheme = 'bad', + api_host = "api.{{sampleApiDomain}}", + ) + 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 + api_instance = open_fga_api.OpenFgaApi(api_client) + + # 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" + ) + + async def test_configuration_missing_storeid(self): + """ + Test whether FgaValidationException is raised for API (reading authorization models) + required store ID but configuration is missing store ID + """ + configuration = {{packageName}}.Configuration( + api_scheme = 'http', + api_host = "api.{{sampleApiDomain}}", + ) + # Notice the store_id is not set + # Enter a context with an instance of the API client + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = open_fga_api.OpenFgaApi(api_client) + + # expects FgaValidationException to be thrown because store_id is not specified + with self.assertRaises(FgaValidationException): + {{#asyncio}}await {{/asyncio}}api_instance.read_authorization_models( + page_size= 1, + continuation_token= "abcdefg" + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_400_error(self, mock_request): + """ + Test to ensure 400 errors are handled properly + """ + response_body = ''' +{ + "code": "validation_error", + "message": "Generic validation error" +} + ''' + mock_request.side_effect = ValidationException(http_resp=http_mock_response(response_body, 400)) + + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(ValidationException) as api_exception: + {{#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) + + + @patch.object(rest.RESTClientObject, 'request') + async def test_404_error(self, mock_request): + """ + Test to ensure 404 errors are handled properly + """ + response_body = ''' +{ + "code": "undefined_endpoint", + "message": "Endpoint not enabled" +} + ''' + mock_request.side_effect = NotFoundException(http_resp=http_mock_response(response_body, 404)) + + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(NotFoundException) as api_exception: + {{#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") + + @patch.object(rest.RESTClientObject, 'request') + async def test_429_error_no_retry(self, mock_request): + """ + Test to ensure 429 errors are handled properly. + For this case, there is no retry configured + """ + response_body = ''' +{ + "code": "rate_limit_exceeded", + "message": "Rate Limit exceeded" +} + ''' + mock_request.side_effect = RateLimitExceededError(http_resp=http_mock_response(response_body, 429)) + + retry = {{packageName}}.configuration.RetryParams(0, 10) + configuration = self.configuration + configuration.store_id = store_id + configuration.retry_params = retry + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(RateLimitExceededError) as api_exception: + {{#asyncio}}await {{/asyncio}}api_instance.check( + body=body, + ) + self.assertIsInstance(api_exception.exception, RateLimitExceededError) + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 1) + + @patch.object(rest.RESTClientObject, 'request') + async def test_429_error_first_error(self, mock_request): + """ + Test to ensure 429 errors are handled properly. + For this case, retry is configured and only the first time has error + """ + response_body = '{"allowed": true, "resolution": "1234"}' + error_response_body = ''' +{ + "code": "rate_limit_exceeded", + "message": "Rate Limit exceeded" +} + ''' + mock_request.side_effect = [RateLimitExceededError(http_resp=http_mock_response(error_response_body, 429)), mock_response(response_body, 200)] + + retry = {{packageName}}.configuration.RetryParams(1, 10) + configuration = self.configuration + configuration.store_id = store_id + configuration.retry_params = retry + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 2) + + + @patch.object(rest.RESTClientObject, 'request') + async def test_500_error(self, mock_request): + """ + Test to ensure 500 errors are handled properly + """ + response_body = ''' +{ + "code": "internal_error", + "message": "Internal Server Error" +} + ''' + mock_request.side_effect = ServiceException(http_resp=http_mock_response(response_body, 500)) + + configuration = self.configuration + configuration.store_id = store_id + configuration.retry_params.max_retry = 0 + + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(ServiceException) as api_exception: + {{#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") + 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): + """ + Test to ensure 5xxx retries are handled properly + """ + response_body = """ +{ + "code": "internal_error", + "message": "Internal Server Error" +} + """ + mock_request.side_effect = [ + ServiceException(http_resp=http_mock_response(response_body, 500)), + ServiceException(http_resp=http_mock_response(response_body, 502)), + ServiceException(http_resp=http_mock_response(response_body, 503)), + ServiceException(http_resp=http_mock_response(response_body, 504)), + mock_response(response_body, 200), + ] + + retry = openfga_sdk.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: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + + api_response = await api_instance.check( + body=body, + ) + + self.assertIsInstance(api_response, CheckResponse) + mock_request.assert_called() + self.assertEqual(mock_request.call_count, 5) + + @patch.object(rest.RESTClientObject, "request") + async def test_501_error_retry(self, mock_request): + """ + Test to ensure 501 responses are not auto-retried + """ + response_body = """ +{ + "code": "not_implemented", + "message": "Not Implemented" +} + """ + mock_request.side_effect = [ + ServiceException(http_resp=http_mock_response(response_body, 501)), + ServiceException(http_resp=http_mock_response(response_body, 501)), + ServiceException(http_resp=http_mock_response(response_body, 501)), + mock_response(response_body, 200), + ] + + retry = openfga_sdk.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: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + with self.assertRaises(ServiceException) as api_exception: + await 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): + """Test case for API token + + Check whether API token is send when configuration specifies credential method as api_token + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + configuration.credentials = Credentials(method='api_token', configuration=CredentialConfiguration(api_token='TOKEN1')) + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + expected_headers = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk {{sdkId}}/{{packageVersion}}', 'Authorization': 'Bearer TOKEN1'}) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + headers=expected_headers, + query_params=[], + post_params=[], + body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + _preload_content=ANY, + _request_timeout=None + ) + + @patch.object(rest.RESTClientObject, 'request') + async def test_check_custom_header(self, mock_request): + """Test case for custom header + + Check whether custom header can be added + """ + + # First, mock the response + response_body = '{"allowed": true}' + mock_request.return_value = mock_response(response_body, 200) + + configuration = self.configuration + configuration.store_id = store_id + {{#asyncio}}async {{/asyncio}}with {{packageName}}.ApiClient(configuration) as api_client: + api_client.set_default_header("Custom Header", "custom value") + api_instance = open_fga_api.OpenFgaApi(api_client) + body = CheckRequest( + tuple_key=TupleKey( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ) + api_response = {{#asyncio}}await {{/asyncio}}api_instance.check( + body=body, + ) + self.assertIsInstance(api_response, CheckResponse) + self.assertTrue(api_response.allowed) + # Make sure the API was called with the right data + expected_headers = urllib3.response.HTTPHeaderDict({'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'openfga-sdk {{sdkId}}/{{packageVersion}}', 'Custom Header': 'custom value'}) + mock_request.assert_called_once_with( + 'POST', + 'http://api.{{sampleApiDomain}}/stores/01H0H015178Y2V4CX10C2KGHF4/check', + headers=expected_headers, + query_params=[], + post_params=[], + body={"tuple_key":{"object":"document:2021-budget","relation":"reader","user":"user:81684243-9356-4421-8fbf-a4f8d36aa31b"}}, + _preload_content=ANY, + _request_timeout=None + ) + +{{/operations}} + +if __name__ == '__main__': + unittest.main() diff --git a/config/clients/python/template/test/client/__init__.py.mustache b/config/clients/python/template/test/client/__init__.py.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/python/template/client/test_client.mustache b/config/clients/python/template/test/client/client_test.py.mustache similarity index 100% rename from config/clients/python/template/client/test_client.mustache rename to config/clients/python/template/test/client/client_test.py.mustache diff --git a/config/clients/python/template/test_configuration.mustache b/config/clients/python/template/test/configuration_test.py.mustache similarity index 100% rename from config/clients/python/template/test_configuration.mustache rename to config/clients/python/template/test/configuration_test.py.mustache diff --git a/config/clients/python/template/credentials_test.mustache b/config/clients/python/template/test/credentials_test.py.mustache similarity index 100% rename from config/clients/python/template/credentials_test.mustache rename to config/clients/python/template/test/credentials_test.py.mustache diff --git a/config/clients/python/template/oauth2_test.mustache b/config/clients/python/template/test/oauth2_test.py.mustache similarity index 100% rename from config/clients/python/template/oauth2_test.mustache rename to config/clients/python/template/test/oauth2_test.py.mustache diff --git a/config/clients/python/template/test/sync/__init__.py.mustache b/config/clients/python/template/test/sync/__init__.py.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/python/template/api_test_sync.mustache b/config/clients/python/template/test/sync/api_test.py.mustache similarity index 100% rename from config/clients/python/template/api_test_sync.mustache rename to config/clients/python/template/test/sync/api_test.py.mustache diff --git a/config/clients/python/template/test/sync/client/__init__.py.mustache b/config/clients/python/template/test/sync/client/__init__.py.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/python/template/client/test_client_sync.mustache b/config/clients/python/template/test/sync/client/client_test.py.mustache similarity index 100% rename from config/clients/python/template/client/test_client_sync.mustache rename to config/clients/python/template/test/sync/client/client_test.py.mustache diff --git a/config/clients/python/template/oauth2_test_sync.mustache b/config/clients/python/template/test/sync/oauth2_test.py.mustache similarity index 100% rename from config/clients/python/template/oauth2_test_sync.mustache rename to config/clients/python/template/test/sync/oauth2_test.py.mustache diff --git a/config/clients/python/template/test_validation.mustache b/config/clients/python/template/test/validation_test.py.mustache similarity index 95% rename from config/clients/python/template/test_validation.mustache rename to config/clients/python/template/test/validation_test.py.mustache index ce8f0dca..9a7938af 100644 --- a/config/clients/python/template/test_validation.mustache +++ b/config/clients/python/template/test/validation_test.py.mustache @@ -2,7 +2,7 @@ import unittest -from openfga_sdk.validation import is_well_formed_ulid_string +from {{packageName}}.validation import is_well_formed_ulid_string class TestValidation(unittest.TestCase): diff --git a/config/clients/python/template/tornado/rest.mustache b/config/clients/python/template/tornado/rest.mustache deleted file mode 100644 index 11920259..00000000 --- a/config/clients/python/template/tornado/rest.mustache +++ /dev/null @@ -1,222 +0,0 @@ -{{>partial_header}} - -import io -import json -import logging -import re - -# python 2 and python 3 compatibility library -from six.moves.urllib.parse import urlencode -import tornado -import tornado.gen -from tornado import httpclient -from urllib3.filepost import encode_multipart_formdata - -from {{packageName}}.exceptions import ApiException, ApiValueError - -logger = logging.getLogger(__name__) - - -class RESTResponse(io.IOBase): - - def __init__(self, resp): - self.tornado_response = resp - self.status = resp.code - self.reason = resp.reason - - if resp.body: - self.data = resp.body - else: - self.data = None - - def getheaders(self): - """Returns a CIMultiDictProxy of the response headers.""" - return self.tornado_response.headers - - def getheader(self, name, default=None): - """Returns a given response header.""" - return self.tornado_response.headers.get(name, default) - - -class RESTClientObject: - - def __init__(self, configuration, pools_size=4, maxsize=4): - # maxsize is number of requests to host that are allowed in parallel - - self.ca_certs = configuration.ssl_ca_cert - self.client_key = configuration.key_file - self.client_cert = configuration.cert_file - - self.proxy_port = self.proxy_host = None - - # https pool manager - if configuration.proxy: - self.proxy_port = 80 - self.proxy_host = configuration.proxy - - self.pool_manager = httpclient.AsyncHTTPClient() - - @tornado.gen.coroutine - 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. - """ - method = method.upper() - 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." - ) - - request = httpclient.HTTPRequest(url) - request.allow_nonstandard_methods = True - request.ca_certs = self.ca_certs - request.client_key = self.client_key - request.client_cert = self.client_cert - request.proxy_host = self.proxy_host - request.proxy_port = self.proxy_port - request.method = method - if headers: - request.headers = headers - if 'Content-Type' not in headers: - request.headers['Content-Type'] = 'application/json' - request.request_timeout = _request_timeout or 5 * 60 - - post_params = post_params or {} - - if query_params: - request.url += '?' + 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 body: - body = json.dumps(body) - request.body = body - elif headers['Content-Type'] == 'application/x-www-form-urlencoded': - request.body = urlencode(post_params) - elif headers['Content-Type'] == 'multipart/form-data': - multipart = encode_multipart_formdata(post_params) - request.body, headers['Content-Type'] = multipart - # 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): - request.body = 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.""" - raise ApiException(status=0, reason=msg) - - r = yield self.pool_manager.fetch(request, raise_error=False) - - if _preload_content: - - r = RESTResponse(r) - - # log response body - logger.debug("response body: %s", r.data) - - if not 200 <= r.status <= 299: - raise ApiException(http_resp=r) - - raise tornado.gen.Return(r) - - @tornado.gen.coroutine - def GET(self, url, headers=None, query_params=None, _preload_content=True, - _request_timeout=None): - result = yield self.request("GET", url, - headers=headers, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - query_params=query_params) - raise tornado.gen.Return(result) - - @tornado.gen.coroutine - def HEAD(self, url, headers=None, query_params=None, _preload_content=True, - _request_timeout=None): - result = yield self.request("HEAD", url, - headers=headers, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - query_params=query_params) - raise tornado.gen.Return(result) - - @tornado.gen.coroutine - def OPTIONS(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - result = yield self.request("OPTIONS", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - raise tornado.gen.Return(result) - - @tornado.gen.coroutine - def DELETE(self, url, headers=None, query_params=None, body=None, - _preload_content=True, _request_timeout=None): - result = yield self.request("DELETE", url, - headers=headers, - query_params=query_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - raise tornado.gen.Return(result) - - @tornado.gen.coroutine - def POST(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - result = yield self.request("POST", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - raise tornado.gen.Return(result) - - @tornado.gen.coroutine - def PUT(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - result = yield self.request("PUT", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - raise tornado.gen.Return(result) - - @tornado.gen.coroutine - def PATCH(self, url, headers=None, query_params=None, post_params=None, - body=None, _preload_content=True, _request_timeout=None): - result = yield self.request("PATCH", url, - headers=headers, - query_params=query_params, - post_params=post_params, - _preload_content=_preload_content, - _request_timeout=_request_timeout, - body=body) - raise tornado.gen.Return(result) diff --git a/config/common/files/README.mustache b/config/common/files/README.mustache index 252dc1d8..54cf7006 100644 --- a/config/common/files/README.mustache +++ b/config/common/files/README.mustache @@ -115,7 +115,7 @@ If your server is configured with [authentication enabled]({{docsUrl}}/getting-s {{#supportsOpenTelemetry}} ### OpenTelemetry -This SDK supports producing metrics that can be consumed as part of an [OpenTelemetry](https://opentelemetry.io/) setup. For more information, please see [the documentation]((https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/blob/main/docs/opentelemetry.md) +This SDK supports producing metrics that can be consumed as part of an [OpenTelemetry](https://opentelemetry.io/) setup. For more information, please see [the documentation](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/blob/main/docs/opentelemetry.md) {{/supportsOpenTelemetry}} ## Contributing