From 3a75ff5a57b44d73e635e9a3531ed8d948dea553 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 27 Jan 2025 19:22:02 -0600 Subject: [PATCH 1/8] chore(python-sdk): add `ruff` and remove `pyupgrade`, `isort`, `autoflake`, and `black` --- Makefile | 7 +-- .../clients/python/.openapi-generator-ignore | 1 + config/clients/python/config.overrides.json | 1 + .../example/example1/example1.py.mustache | 4 ++ .../example/example1/setup.py.mustache | 12 +--- .../example/opentelemetry/main.py.mustache | 15 +++-- .../example/opentelemetry/setup.py.mustache | 12 +--- .../asynchronous.py.mustache | 8 ++- .../streamed-list-objects/setup.py.mustache | 12 +--- .../synchronous.py.mustache | 8 ++- .../python/template/gitignore_custom.mustache | 1 + config/clients/python/template/model.mustache | 1 - config/clients/python/template/pyproject.toml | 61 +++++++++++++++++++ .../python/template/setup_cfg.mustache | 20 ------ .../python/template/src/__init__.py.mustache | 23 +++++++ .../python/template/src/api.py.mustache | 1 - .../template/src/api/__init__.py.mustache | 7 +++ .../template/src/api_client.py.mustache | 13 ++-- .../template/src/client/__init__.py.mustache | 7 +++ .../template/src/client/client.py.mustache | 3 +- .../src/client/models/__init__.py.mustache | 19 ++++++ .../models/batch_check_item.py.mustache | 2 + .../models/batch_check_request.py.mustache | 1 + .../models/batch_check_response.py.mustache | 1 + .../batch_check_single_response.py.mustache | 2 +- .../client/models/check_request.py.mustache | 1 + .../client_batch_check_response.py.mustache | 1 + .../models/list_objects_request.py.mustache | 1 + .../models/list_relations_request.py.mustache | 1 + .../src/client/models/tuple.py.mustache | 1 + .../client/models/write_request.py.mustache | 1 + .../client/models/write_response.py.mustache | 1 + .../models/write_single_response.py.mustache | 2 + .../models/write_transaction_opts.py.mustache | 1 - .../template/src/configuration.py.mustache | 2 +- .../template/src/credentials.py.mustache | 4 +- .../python/template/src/help.py.mustache | 3 + .../template/src/models/__init__.py.mustache | 5 ++ .../python/template/src/oauth2.py.mustache | 2 +- .../template/src/sync/__init__.py.mustache | 6 ++ .../python/template/src/sync/api.py.mustache | 2 - .../template/src/sync/api_client.py.mustache | 13 ++-- .../template/src/sync/oauth2.py.mustache | 2 +- .../src/telemetry/__init__.py.mustache | 17 ++++++ .../src/telemetry/attributes.py.mustache | 3 + .../src/telemetry/configuration.py.mustache | 3 + .../src/telemetry/counters.py.mustache | 2 + .../src/telemetry/histograms.py.mustache | 2 + .../src/telemetry/metrics.py.mustache | 2 + .../src/telemetry/telemetry.py.mustache | 2 + .../src/telemetry/utilities.py.mustache | 4 +- .../template/test-requirements.mustache | 10 +-- .../python/template/test/__init__.py.mustache | 1 + .../template/test/api/__init__.py.mustache | 1 + .../python/template/test/api_test.py.mustache | 2 +- .../template/test/client/__init__.py.mustache | 1 + .../test/configuration_test.py.mustache | 8 --- .../template/test/oauth2_test.py.mustache | 2 - .../template/test/sync/__init__.py.mustache | 1 + .../template/test/sync/api_test.py.mustache | 4 +- .../test/sync/client/__init__.py.mustache | 1 + .../telemetry/attributes_test.py.mustache | 6 +- .../telemetry/configuration_test.py.mustache | 4 +- .../test/telemetry/counters_test.py.mustache | 4 +- .../telemetry/histograms_test.py.mustache | 4 +- .../test/telemetry/metrics_test.py.mustache | 15 +---- .../test/telemetry/telemetry_test.py.mustache | 4 +- .../test/telemetry/utilities_test.py.mustache | 2 + 68 files changed, 266 insertions(+), 130 deletions(-) create mode 100644 config/clients/python/template/pyproject.toml delete mode 100644 config/clients/python/template/setup_cfg.mustache diff --git a/Makefile b/Makefile index 824d1237..034b8215 100644 --- a/Makefile +++ b/Makefile @@ -125,17 +125,14 @@ build-client-python: 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 -m ruff format . && \ python setup.py sdist bdist_wheel'" .PHONY: test-client-python test-client-python: build-client-python make run-in-docker sdk_language=python image=python:${PYTHON_DOCKER_TAG} command="/bin/sh -c 'python -m pip install -r test-requirements.txt && \ pytest --cov-report term-missing --cov=openfga_sdk test/ && \ - flake8 . --count --show-source --statistics'" + ruff check .'" ### Java .PHONY: tag-client-java diff --git a/config/clients/python/.openapi-generator-ignore b/config/clients/python/.openapi-generator-ignore index 2e6a0ecd..56a5dff4 100644 --- a/config/clients/python/.openapi-generator-ignore +++ b/config/clients/python/.openapi-generator-ignore @@ -14,3 +14,4 @@ test/* .gitlab-ci.yml .travis.yml tox.ini +setup.cfg diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 3fe89010..13e71723 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -14,6 +14,7 @@ "supportsStreamedListObjects": "streamed_list_objects", "files": { ".snyk": {}, + "pyproject.toml": {}, "api_sync.mustache": { "folder": "openfga_sdk/sync", diff --git a/config/clients/python/template/example/example1/example1.py.mustache b/config/clients/python/template/example/example1/example1.py.mustache index dd924a15..cc72fb4d 100644 --- a/config/clients/python/template/example/example1/example1.py.mustache +++ b/config/clients/python/template/example/example1/example1.py.mustache @@ -1,3 +1,7 @@ +# ruff: noqa: E402 + +{{>partial_header}} + import asyncio import os import sys diff --git a/config/clients/python/template/example/example1/setup.py.mustache b/config/clients/python/template/example/example1/setup.py.mustache index 0008c28b..4b25531d 100644 --- a/config/clients/python/template/example/example1/setup.py.mustache +++ b/config/clients/python/template/example/example1/setup.py.mustache @@ -1,14 +1,4 @@ -""" - 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. -""" +{{>partial_header}} from setuptools import find_packages, setup diff --git a/config/clients/python/template/example/opentelemetry/main.py.mustache b/config/clients/python/template/example/opentelemetry/main.py.mustache index 63c34678..5483c5a4 100644 --- a/config/clients/python/template/example/opentelemetry/main.py.mustache +++ b/config/clients/python/template/example/opentelemetry/main.py.mustache @@ -1,3 +1,7 @@ +# ruff: noqa: E402 + +{{>partial_header}} + import asyncio import os import sys @@ -198,16 +202,15 @@ async def main(): 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" - ), + 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!") diff --git a/config/clients/python/template/example/opentelemetry/setup.py.mustache b/config/clients/python/template/example/opentelemetry/setup.py.mustache index ddeb10fc..fb220881 100644 --- a/config/clients/python/template/example/opentelemetry/setup.py.mustache +++ b/config/clients/python/template/example/opentelemetry/setup.py.mustache @@ -1,14 +1,4 @@ -""" - 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. -""" +{{>partial_header}} from setuptools import find_packages, setup diff --git a/config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache b/config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache index 2968454e..19cb0b7e 100644 --- a/config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache +++ b/config/clients/python/template/example/streamed-list-objects/asynchronous.py.mustache @@ -1,3 +1,7 @@ +# ruff: noqa: E402 + +{{>partial_header}} + import asyncio import json import os @@ -72,7 +76,7 @@ async def main(): ) print(f"Created temporary authorization model ({model})") - print(f"Writing 100 mock tuples to store.") + print("Writing 100 mock tuples to store.") # Write mock data writes = [] @@ -111,7 +115,7 @@ async def main(): try: await fga_client.delete_store() print(f"Deleted temporary store ({store})") - except: + except Exception: pass print("Finished.") diff --git a/config/clients/python/template/example/streamed-list-objects/setup.py.mustache b/config/clients/python/template/example/streamed-list-objects/setup.py.mustache index 7a381fff..68b2ace3 100644 --- a/config/clients/python/template/example/streamed-list-objects/setup.py.mustache +++ b/config/clients/python/template/example/streamed-list-objects/setup.py.mustache @@ -1,14 +1,4 @@ -""" - 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. -""" +{{>partial_header}} from setuptools import find_packages, setup diff --git a/config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache b/config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache index d3db0d29..e6bc688f 100644 --- a/config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache +++ b/config/clients/python/template/example/streamed-list-objects/synchronous.py.mustache @@ -1,3 +1,7 @@ +# ruff: noqa: E402 + +{{>partial_header}} + import json import os import sys @@ -69,7 +73,7 @@ def main(): ) print(f"Created temporary authorization model ({model})") - print(f"Writing 100 mock tuples to store.") + print("Writing 100 mock tuples to store.") # Write mock data writes = [] @@ -108,7 +112,7 @@ def main(): try: fga_client.delete_store() print(f"Deleted temporary store ({store})") - except: + except Exception: pass print("Finished.") diff --git a/config/clients/python/template/gitignore_custom.mustache b/config/clients/python/template/gitignore_custom.mustache index ae5ff0ce..1657fc69 100644 --- a/config/clients/python/template/gitignore_custom.mustache +++ b/config/clients/python/template/gitignore_custom.mustache @@ -50,6 +50,7 @@ venv/ .venv/ .python-version .pytest_cache +.ruff_cache test/__pycache__/ # Translations diff --git a/config/clients/python/template/model.mustache b/config/clients/python/template/model.mustache index d620f96f..7a01ae67 100644 --- a/config/clients/python/template/model.mustache +++ b/config/clients/python/template/model.mustache @@ -5,7 +5,6 @@ try: except ImportError: from inspect import getargspec as getfullargspec import pprint -import re from {{packageName}}.configuration import Configuration diff --git a/config/clients/python/template/pyproject.toml b/config/clients/python/template/pyproject.toml new file mode 100644 index 00000000..74966dc4 --- /dev/null +++ b/config/clients/python/template/pyproject.toml @@ -0,0 +1,61 @@ +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +line-length = 88 +indent-width = 4 + +target-version = "py310" + +[lint] +select = ["E4", "E7", "E9", "F"] +ignore = [] + +fixable = ["ALL"] +unfixable = [] + +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.pytest.ini_options] +testpaths = [ + "tests", + "integration", +] + +addopts = "--cov=openfga_sdk --cov-report term-missing --cov-report xml --cov-report html" + +asyncio_mode = "strict" +asyncio_default_fixture_loop_scope = "function" +asyncio_default_test_loop_scope = "function" diff --git a/config/clients/python/template/setup_cfg.mustache b/config/clients/python/template/setup_cfg.mustache deleted file mode 100644 index 719e295b..00000000 --- a/config/clients/python/template/setup_cfg.mustache +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -ignore=F401,E203,E402,E501,E722,W291,W503 -exclude=.pytest_cache,build,docs,example,test -max-line-length=88 -extend-select = B950 -extend-ignore = E203,E501,E701 - -[isort] -profile=black -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -line_length = 88 -split_on_trailing_comma = True - -[tool:pytest] -testpaths=test -addopts=--cov=openfga_sdk --cov-report term-missing --cov-report xml --cov-report html diff --git a/config/clients/python/template/src/__init__.py.mustache b/config/clients/python/template/src/__init__.py.mustache index 13699158..f7b0141c 100644 --- a/config/clients/python/template/src/__init__.py.mustache +++ b/config/clients/python/template/src/__init__.py.mustache @@ -1,4 +1,5 @@ {{>partial_header}} + __version__ = "{{packageVersion}}" from {{packageName}}.client.client import OpenFgaClient @@ -29,3 +30,25 @@ from {{packageName}}.telemetry.configuration import ( __import__('sys').setrecursionlimit({{{.}}}) {{/recursionLimit}} + +__all__ = [ + "OpenFgaClient", + "ClientConfiguration", + {{#apiInfo}}{{#apis}}"{{classname}}", + {{/apis}}{{/apiInfo}} + "ApiClient", + "Configuration", + "OpenApiException", + "FgaValidationException", + "ApiValueError", + "ApiKeyError", + "ApiAttributeError", + "ApiException", + {{#models}}{{#model}}"{{classname}}", + {{/model}}{{/models}} + "TelemetryConfiguration", + "TelemetryConfigurations", + "TelemetryConfigurationType", + "TelemetryMetricConfiguration", + "TelemetryMetricsConfiguration", +] diff --git a/config/clients/python/template/src/api.py.mustache b/config/clients/python/template/src/api.py.mustache index fba48d63..7bb4fa76 100644 --- a/config/clients/python/template/src/api.py.mustache +++ b/config/clients/python/template/src/api.py.mustache @@ -1,6 +1,5 @@ {{>partial_header}} - from {{packageName}}.api_client import ApiClient from {{packageName}}.exceptions import ApiValueError, FgaValidationException from {{packageName}}.oauth2 import OAuth2Client diff --git a/config/clients/python/template/src/api/__init__.py.mustache b/config/clients/python/template/src/api/__init__.py.mustache index e134de02..f6dd869f 100644 --- a/config/clients/python/template/src/api/__init__.py.mustache +++ b/config/clients/python/template/src/api/__init__.py.mustache @@ -1,2 +1,9 @@ +{{>partial_header}} + {{#apiInfo}}{{#apis}}from {{apiPackage}}.{{classFilename}} import {{classname}} {{/apis}}{{/apiInfo}} + +__all__ = [ + {{#apiInfo}}{{#apis}}"{{classname}}", + {{/apis}}{{/apiInfo}} +] diff --git a/config/clients/python/template/src/api_client.py.mustache b/config/clients/python/template/src/api_client.py.mustache index 2e9413ea..391a6b5d 100644 --- a/config/clients/python/template/src/api_client.py.mustache +++ b/config/clients/python/template/src/api_client.py.mustache @@ -481,14 +481,17 @@ class ApiClient: if klass in self.PRIMITIVE_TYPES: return self.__deserialize_primitive(data, klass) - elif klass == object: + + if klass is object: return self.__deserialize_object(data) - elif klass == datetime.date: + + if klass is datetime.date: return self.__deserialize_date(data) - elif klass == datetime.datetime: + + if klass is datetime.datetime: return self.__deserialize_datetime(data) - else: - return self.__deserialize_model(data, klass) + + return self.__deserialize_model(data, klass) {{#asyncio}}async {{/asyncio}}def call_api( self, diff --git a/config/clients/python/template/src/client/__init__.py.mustache b/config/clients/python/template/src/client/__init__.py.mustache index 1e85e02c..7e2ef67b 100644 --- a/config/clients/python/template/src/client/__init__.py.mustache +++ b/config/clients/python/template/src/client/__init__.py.mustache @@ -1,4 +1,11 @@ {{>partial_header}} + from {{packageName}}.client.client import OpenFgaClient from {{packageName}}.client.configuration import ClientConfiguration from {{packageName}}.client.models.check_request import ClientCheckRequest + +__all__ = [ + "OpenFgaClient", + "ClientConfiguration", + "ClientCheckRequest", +] diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index 0747639b..55795fbe 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -14,11 +14,10 @@ from {{packageName}}.client.configuration import ClientConfiguration from {{packageName}}.client.models.assertion import ClientAssertion from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem, construct_batch_item from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest -from {{packageName}}.client.models.batch_check_response import BatchCheckResponse, ClientBatchCheckResponse +from {{packageName}}.client.models.batch_check_response import ClientBatchCheckResponse from {{packageName}}.client.models.batch_check_single_response import ClientBatchCheckSingleResponse from {{packageName}}.client.models.client_batch_check_response import ClientBatchCheckClientResponse from {{packageName}}.client.models.check_request import ClientCheckRequest, construct_check_request -from {{packageName}}.client.models.batch_check_response import BatchCheckResponse from {{packageName}}.client.models.tuple import ClientTuple, convert_tuple_keys from {{packageName}}.client.models.write_request import ClientWriteRequest from {{packageName}}.client.models.write_response import ClientWriteResponse diff --git a/config/clients/python/template/src/client/models/__init__.py.mustache b/config/clients/python/template/src/client/models/__init__.py.mustache index 5f61f417..7edc3e50 100644 --- a/config/clients/python/template/src/client/models/__init__.py.mustache +++ b/config/clients/python/template/src/client/models/__init__.py.mustache @@ -1,4 +1,5 @@ {{>partial_header}} + from {{packageName}}.client.models.assertion import ClientAssertion from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem from {{packageName}}.client.models.batch_check_request import ClientBatchCheckRequest @@ -14,3 +15,21 @@ 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 + +__all__ = [ + "ClientAssertion", + "ClientBatchCheckItem", + "ClientBatchCheckRequest", + "ClientBatchCheckResponse", + "ClientBatchCheckSingleResponse", + "ClientCheckRequest", + "ClientBatchCheckClientResponse", + "ClientExpandRequest", + "ClientListObjectsRequest", + "ClientListRelationsRequest", + "ClientReadChangesRequest", + "ClientTuple", + "ClientWriteRequest", + "ClientWriteResponse", + "WriteTransactionOpts", +] diff --git a/config/clients/python/template/src/client/models/batch_check_item.py.mustache b/config/clients/python/template/src/client/models/batch_check_item.py.mustache index 139767cb..6229ab62 100644 --- a/config/clients/python/template/src/client/models/batch_check_item.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_item.py.mustache @@ -5,6 +5,7 @@ from {{packageName}}.models.batch_check_item import BatchCheckItem from {{packageName}}.models.check_request_tuple_key import CheckRequestTupleKey from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys + def construct_batch_item(check): batch_item = BatchCheckItem( tuple_key=CheckRequestTupleKey( @@ -23,6 +24,7 @@ def construct_batch_item(check): return batch_item + class ClientBatchCheckItem: def __init__( self, diff --git a/config/clients/python/template/src/client/models/batch_check_request.py.mustache b/config/clients/python/template/src/client/models/batch_check_request.py.mustache index 4c1572df..efe9b321 100644 --- a/config/clients/python/template/src/client/models/batch_check_request.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_request.py.mustache @@ -2,6 +2,7 @@ from {{packageName}}.client.models.batch_check_item import ClientBatchCheckItem + class ClientBatchCheckRequest: """ ClientBatchCheckRequest encapsulates the parameters for a BatchCheck request diff --git a/config/clients/python/template/src/client/models/batch_check_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_response.py.mustache index 532c4588..018f288a 100644 --- a/config/clients/python/template/src/client/models/batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_response.py.mustache @@ -2,6 +2,7 @@ from {{packageName}}.client.models.batch_check_single_response import ClientBatchCheckSingleResponse + class ClientBatchCheckResponse: def __init__(self, result: list[ClientBatchCheckSingleResponse]): self._result = result diff --git a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache index 3fe78d32..3a46eb29 100644 --- a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache @@ -1,9 +1,9 @@ {{>partial_header}} from {{packageName}}.client.models.tuple import ClientTuple -from {{packageName}}.models.batch_check_single_result import BatchCheckSingleResult from {{packageName}}.models.check_error import CheckError + class ClientBatchCheckSingleResponse: def __init__( self, diff --git a/config/clients/python/template/src/client/models/check_request.py.mustache b/config/clients/python/template/src/client/models/check_request.py.mustache index 7a0dce90..8f9dc0a7 100644 --- a/config/clients/python/template/src/client/models/check_request.py.mustache +++ b/config/clients/python/template/src/client/models/check_request.py.mustache @@ -2,6 +2,7 @@ from {{packageName}}.client.models.tuple import ClientTuple + def construct_check_request(user: str, relation: str, object: str, contextual_tuples: list[ClientTuple] = None, context: object = None): """ helper function to construct the check request body diff --git a/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache index 52c9765c..33c72e25 100644 --- a/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache @@ -3,6 +3,7 @@ from {{packageName}}.client.models.check_request import ClientCheckRequest from {{packageName}}.models.check_response import CheckResponse + class ClientBatchCheckClientResponse: """ ClientBatchCheckClientResponse encapsulates the response for a single batch check diff --git a/config/clients/python/template/src/client/models/list_objects_request.py.mustache b/config/clients/python/template/src/client/models/list_objects_request.py.mustache index 748eeb46..674d5b9f 100644 --- a/config/clients/python/template/src/client/models/list_objects_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_objects_request.py.mustache @@ -2,6 +2,7 @@ from {{packageName}}.client.models.tuple import ClientTuple + class ClientListObjectsRequest: """ ClientListObjectsRequest encapsulates the parameters required for list objects diff --git a/config/clients/python/template/src/client/models/list_relations_request.py.mustache b/config/clients/python/template/src/client/models/list_relations_request.py.mustache index 02a9650c..eb668d30 100644 --- a/config/clients/python/template/src/client/models/list_relations_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_relations_request.py.mustache @@ -2,6 +2,7 @@ from {{packageName}}.client.models.tuple import ClientTuple + class ClientListRelationsRequest: """ ClientListRelationsRequest encapsulates the parameters required for list all relations user have with object diff --git a/config/clients/python/template/src/client/models/tuple.py.mustache b/config/clients/python/template/src/client/models/tuple.py.mustache index d5e75def..823c706b 100644 --- a/config/clients/python/template/src/client/models/tuple.py.mustache +++ b/config/clients/python/template/src/client/models/tuple.py.mustache @@ -3,6 +3,7 @@ from {{packageName}}.models.relationship_condition import RelationshipCondition from {{packageName}}.models.tuple_key import TupleKey + class ClientTuple: """ ClientTuple encapsulates the client tuple diff --git a/config/clients/python/template/src/client/models/write_request.py.mustache b/config/clients/python/template/src/client/models/write_request.py.mustache index 2ffa0fbd..56335d15 100644 --- a/config/clients/python/template/src/client/models/write_request.py.mustache +++ b/config/clients/python/template/src/client/models/write_request.py.mustache @@ -4,6 +4,7 @@ from {{packageName}}.client.models.tuple import ClientTuple, convert_tuple_keys from {{packageName}}.models.write_request_writes import WriteRequestWrites from {{packageName}}.models.write_request_deletes import WriteRequestDeletes + class ClientWriteRequest: """ ClientWriteRequest encapsulates the parameters required to write diff --git a/config/clients/python/template/src/client/models/write_response.py.mustache b/config/clients/python/template/src/client/models/write_response.py.mustache index e8979971..7302bf17 100644 --- a/config/clients/python/template/src/client/models/write_response.py.mustache +++ b/config/clients/python/template/src/client/models/write_response.py.mustache @@ -2,6 +2,7 @@ from {{packageName}}.client.models.write_single_response import ClientWriteSingleResponse + class ClientWriteResponse: """ ClientWriteResponse returns the set of responses and their statuses diff --git a/config/clients/python/template/src/client/models/write_single_response.py.mustache b/config/clients/python/template/src/client/models/write_single_response.py.mustache index c79e8ed9..dcf76956 100644 --- a/config/clients/python/template/src/client/models/write_single_response.py.mustache +++ b/config/clients/python/template/src/client/models/write_single_response.py.mustache @@ -2,12 +2,14 @@ from {{packageName}}.client.models.tuple import ClientTuple + def construct_write_single_response(tuple_key: ClientTuple, success: bool, error: Exception=None): """ Helper function to return a single write response """ return ClientWriteSingleResponse(tuple_key, success, error) + class ClientWriteSingleResponse: """ ClientWriteSingleResponse encapsulates the response of a single write diff --git a/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache b/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache index 4777b063..b74f493b 100644 --- a/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache +++ b/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache @@ -1,6 +1,5 @@ {{>partial_header}} - class WriteTransactionOpts: """ OpenFGA client write transaction info diff --git a/config/clients/python/template/src/configuration.py.mustache b/config/clients/python/template/src/configuration.py.mustache index 910ecccc..c8d4e993 100644 --- a/config/clients/python/template/src/configuration.py.mustache +++ b/config/clients/python/template/src/configuration.py.mustache @@ -22,6 +22,7 @@ from {{packageName}}.telemetry.counters import TelemetryCounter from {{packageName}}.telemetry.histograms import TelemetryHistogram from {{packageName}}.validation import is_well_formed_ulid_string + class RetryParams: """NOTE: This class is auto generated by OpenAPI Generator @@ -617,7 +618,6 @@ class Configuration: raise FgaValidationException(f"timeout_millisec not within reasonable range (0,60000), {self._timeout_millisec}") - @property def api_scheme(self): """Return connection is https or http.""" diff --git a/config/clients/python/template/src/credentials.py.mustache b/config/clients/python/template/src/credentials.py.mustache index d8d4df22..e0440a8e 100644 --- a/config/clients/python/template/src/credentials.py.mustache +++ b/config/clients/python/template/src/credentials.py.mustache @@ -188,7 +188,7 @@ class Credentials: if self.method == 'client_credentials': if self.configuration is None or none_or_empty(self.configuration.client_id) or none_or_empty(self.configuration.client_secret) or none_or_empty(self.configuration.api_audience) or none_or_empty(self.configuration.api_issuer): raise ApiValueError('configuration `{}` requires client_id, client_secret, api_audience and api_issuer defined for client_credentials method.') - # validate token issuer - parsed_issuer_url = self._parse_issuer(self.configuration.api_issuer) + # validate token issuer + self._parse_issuer(self.configuration.api_issuer) diff --git a/config/clients/python/template/src/help.py.mustache b/config/clients/python/template/src/help.py.mustache index c6017efa..659398f4 100644 --- a/config/clients/python/template/src/help.py.mustache +++ b/config/clients/python/template/src/help.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + import json import platform import sys @@ -7,6 +9,7 @@ import opentelemetry.version from . import __version__ as openfga_sdk_version + try: import urllib3 diff --git a/config/clients/python/template/src/models/__init__.py.mustache b/config/clients/python/template/src/models/__init__.py.mustache index c9c421a2..8de0949d 100644 --- a/config/clients/python/template/src/models/__init__.py.mustache +++ b/config/clients/python/template/src/models/__init__.py.mustache @@ -1,3 +1,8 @@ {{>partial_header}} {{#models}}{{#model}}from {{modelPackage}}.{{classFilename}} import {{classname}}{{/model}} {{/models}} + +__all__ = [ +{{#models}}{{#model}}"{{classname}}", +{{/model}}{{/models}} +] diff --git a/config/clients/python/template/src/oauth2.py.mustache b/config/clients/python/template/src/oauth2.py.mustache index 03356e25..9a8ffd52 100644 --- a/config/clients/python/template/src/oauth2.py.mustache +++ b/config/clients/python/template/src/oauth2.py.mustache @@ -109,7 +109,7 @@ class OAuth2Client: if 200 <= raw_response.status <= 299: try: api_response = json.loads(raw_response.data) - except: + except Exception: raise AuthenticationError(http_resp=raw_response) if api_response.get("expires_in") and api_response.get("access_token"): diff --git a/config/clients/python/template/src/sync/__init__.py.mustache b/config/clients/python/template/src/sync/__init__.py.mustache index 09cad624..e4bd82fa 100644 --- a/config/clients/python/template/src/sync/__init__.py.mustache +++ b/config/clients/python/template/src/sync/__init__.py.mustache @@ -2,3 +2,9 @@ from {{packageName}}.sync.client.client import OpenFgaClient from {{packageName}}.sync.api_client import ApiClient + + +__all__ = [ + "ApiClient", + "OpenFgaClient", +] diff --git a/config/clients/python/template/src/sync/api.py.mustache b/config/clients/python/template/src/sync/api.py.mustache index 23e25544..5feedbb6 100644 --- a/config/clients/python/template/src/sync/api.py.mustache +++ b/config/clients/python/template/src/sync/api.py.mustache @@ -1,7 +1,5 @@ {{>partial_header}} -import re - from {{packageName}}.exceptions import ApiValueError, FgaValidationException from {{packageName}}.sync.api_client import ApiClient from {{packageName}}.sync.oauth2 import OAuth2Client diff --git a/config/clients/python/template/src/sync/api_client.py.mustache b/config/clients/python/template/src/sync/api_client.py.mustache index 68171661..cc455a50 100644 --- a/config/clients/python/template/src/sync/api_client.py.mustache +++ b/config/clients/python/template/src/sync/api_client.py.mustache @@ -464,14 +464,17 @@ class ApiClient: if klass in self.PRIMITIVE_TYPES: return self.__deserialize_primitive(data, klass) - elif klass == object: + + if klass is object: return self.__deserialize_object(data) - elif klass == datetime.date: + + if klass is datetime.date: return self.__deserialize_date(data) - elif klass == datetime.datetime: + + if klass is datetime.datetime: return self.__deserialize_datetime(data) - else: - return self.__deserialize_model(data, klass) + + return self.__deserialize_model(data, klass) def call_api( self, diff --git a/config/clients/python/template/src/sync/oauth2.py.mustache b/config/clients/python/template/src/sync/oauth2.py.mustache index 032a9c96..e4a39dec 100644 --- a/config/clients/python/template/src/sync/oauth2.py.mustache +++ b/config/clients/python/template/src/sync/oauth2.py.mustache @@ -109,7 +109,7 @@ class OAuth2Client: if 200 <= raw_response.status <= 299: try: api_response = json.loads(raw_response.data) - except: + except Exception: raise AuthenticationError(http_resp=raw_response) if api_response.get("expires_in") and api_response.get("access_token"): diff --git a/config/clients/python/template/src/telemetry/__init__.py.mustache b/config/clients/python/template/src/telemetry/__init__.py.mustache index 73c1c462..b7df2c2e 100644 --- a/config/clients/python/template/src/telemetry/__init__.py.mustache +++ b/config/clients/python/template/src/telemetry/__init__.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes from {{packageName}}.telemetry.configuration import ( TelemetryConfiguration, @@ -9,3 +11,18 @@ from {{packageName}}.telemetry.configuration import ( from {{packageName}}.telemetry.histograms import TelemetryHistogram, TelemetryHistograms from {{packageName}}.telemetry.metrics import TelemetryMetrics from {{packageName}}.telemetry.telemetry import Telemetry + + +__all__ = [ + "Telemetry", + "TelemetryAttribute", + "TelemetryAttributes", + "TelemetryConfiguration", + "TelemetryConfigurations", + "TelemetryConfigurationType", + "TelemetryMetricConfiguration", + "TelemetryMetricsConfiguration", + "TelemetryHistogram", + "TelemetryHistograms", + "TelemetryMetrics", +] diff --git a/config/clients/python/template/src/telemetry/attributes.py.mustache b/config/clients/python/template/src/telemetry/attributes.py.mustache index 9cd697c2..ff35b006 100644 --- a/config/clients/python/template/src/telemetry/attributes.py.mustache +++ b/config/clients/python/template/src/telemetry/attributes.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + import time import urllib from typing import Any, NamedTuple @@ -12,6 +14,7 @@ from {{packageName}}.telemetry.utilities import ( doesInstanceHaveCallable, ) + class TelemetryAttribute(NamedTuple): name: str format: str = "string" diff --git a/config/clients/python/template/src/telemetry/configuration.py.mustache b/config/clients/python/template/src/telemetry/configuration.py.mustache index 9ba43cdf..5cb354e8 100644 --- a/config/clients/python/template/src/telemetry/configuration.py.mustache +++ b/config/clients/python/template/src/telemetry/configuration.py.mustache @@ -1,9 +1,12 @@ +{{>partial_header}} + from typing import NamedTuple from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters from {{packageName}}.telemetry.histograms import TelemetryHistogram, TelemetryHistograms + class TelemetryMetricConfiguration: """ Telemetry configuration for an individual histogram or counter. When instantiated directly, all attributes are disabled by default. diff --git a/config/clients/python/template/src/telemetry/counters.py.mustache b/config/clients/python/template/src/telemetry/counters.py.mustache index 3e16ce49..96bbe8a1 100644 --- a/config/clients/python/template/src/telemetry/counters.py.mustache +++ b/config/clients/python/template/src/telemetry/counters.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from typing import NamedTuple diff --git a/config/clients/python/template/src/telemetry/histograms.py.mustache b/config/clients/python/template/src/telemetry/histograms.py.mustache index 482fd3ae..26a1ca78 100644 --- a/config/clients/python/template/src/telemetry/histograms.py.mustache +++ b/config/clients/python/template/src/telemetry/histograms.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from typing import NamedTuple diff --git a/config/clients/python/template/src/telemetry/metrics.py.mustache b/config/clients/python/template/src/telemetry/metrics.py.mustache index 865b7dd0..3aecae99 100644 --- a/config/clients/python/template/src/telemetry/metrics.py.mustache +++ b/config/clients/python/template/src/telemetry/metrics.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from opentelemetry.metrics import Counter, Histogram, Meter, get_meter from {{packageName}}.telemetry.attributes import ( diff --git a/config/clients/python/template/src/telemetry/telemetry.py.mustache b/config/clients/python/template/src/telemetry/telemetry.py.mustache index 25d78cfb..bb1e5b1b 100644 --- a/config/clients/python/template/src/telemetry/telemetry.py.mustache +++ b/config/clients/python/template/src/telemetry/telemetry.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from {{packageName}}.telemetry.metrics import TelemetryMetrics diff --git a/config/clients/python/template/src/telemetry/utilities.py.mustache b/config/clients/python/template/src/telemetry/utilities.py.mustache index de52ec7c..69204c35 100644 --- a/config/clients/python/template/src/telemetry/utilities.py.mustache +++ b/config/clients/python/template/src/telemetry/utilities.py.mustache @@ -1,7 +1,9 @@ +{{>partial_header}} + def doesInstanceHaveCallable(instance: object, callableName: str) -> bool: instanceCallable = getattr(instance, callableName, None) if instanceCallable is None: return False - return callable(instanceCallable) \ No newline at end of file + return callable(instanceCallable) diff --git a/config/clients/python/template/test-requirements.mustache b/config/clients/python/template/test-requirements.mustache index aae4461c..27570111 100644 --- a/config/clients/python/template/test-requirements.mustache +++ b/config/clients/python/template/test-requirements.mustache @@ -1,11 +1,7 @@ -r requirements.txt -mock >= 5.1.0, < 6 -autoflake==2.3.1 -black==24.10.0 -flake8 >= 7.0.0, < 8 griffe >= 0.41.2, < 2 -isort==5.13.2 -pytest-cov >= 5, < 7 -pyupgrade==3.19.1 +mock >= 5.1.0, < 6 pytest-asyncio >= 0.25, < 1 +pytest-cov >= 5, < 7 +ruff >= 0.9, < 1 diff --git a/config/clients/python/template/test/__init__.py.mustache b/config/clients/python/template/test/__init__.py.mustache index e69de29b..96e69920 100644 --- a/config/clients/python/template/test/__init__.py.mustache +++ b/config/clients/python/template/test/__init__.py.mustache @@ -0,0 +1 @@ +{{>partial_header}} diff --git a/config/clients/python/template/test/api/__init__.py.mustache b/config/clients/python/template/test/api/__init__.py.mustache index e69de29b..96e69920 100644 --- a/config/clients/python/template/test/api/__init__.py.mustache +++ b/config/clients/python/template/test/api/__init__.py.mustache @@ -0,0 +1 @@ +{{>partial_header}} diff --git a/config/clients/python/template/test/api_test.py.mustache b/config/clients/python/template/test/api_test.py.mustache index a620ca7e..b017face 100644 --- a/config/clients/python/template/test/api_test.py.mustache +++ b/config/clients/python/template/test/api_test.py.mustache @@ -1491,7 +1491,7 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), ) - with self.assertRaises(ServiceException) as api_exception: + with self.assertRaises(ServiceException): {{#asyncio}}await {{/asyncio}}api_instance.check( body=body, ) diff --git a/config/clients/python/template/test/client/__init__.py.mustache b/config/clients/python/template/test/client/__init__.py.mustache index e69de29b..96e69920 100644 --- a/config/clients/python/template/test/client/__init__.py.mustache +++ b/config/clients/python/template/test/client/__init__.py.mustache @@ -0,0 +1 @@ +{{>partial_header}} diff --git a/config/clients/python/template/test/configuration_test.py.mustache b/config/clients/python/template/test/configuration_test.py.mustache index 751c820d..d0352ee3 100644 --- a/config/clients/python/template/test/configuration_test.py.mustache +++ b/config/clients/python/template/test/configuration_test.py.mustache @@ -271,14 +271,6 @@ class TestConfigurationHostSettings: ) def test_configuration_get_host_from_settings_invalid_value(self, configuration): - servers = [ - { - "url": "https://{region}.{{sampleApiDomain}}", - "description": "Example", - "variables": {"region": {"default_value": "us"}}, - } - ] - variables = {"region": "invalid"} with pytest.raises(ValueError): configuration.get_host_from_settings(999, variables={"var": "value"}) diff --git a/config/clients/python/template/test/oauth2_test.py.mustache b/config/clients/python/template/test/oauth2_test.py.mustache index fcd0be87..51062700 100644 --- a/config/clients/python/template/test/oauth2_test.py.mustache +++ b/config/clients/python/template/test/oauth2_test.py.mustache @@ -1,7 +1,5 @@ {{>partial_header}} -import urllib3 - from datetime import datetime, timedelta from unittest import IsolatedAsyncioTestCase from unittest.mock import patch diff --git a/config/clients/python/template/test/sync/__init__.py.mustache b/config/clients/python/template/test/sync/__init__.py.mustache index e69de29b..96e69920 100644 --- a/config/clients/python/template/test/sync/__init__.py.mustache +++ b/config/clients/python/template/test/sync/__init__.py.mustache @@ -0,0 +1 @@ +{{>partial_header}} diff --git a/config/clients/python/template/test/sync/api_test.py.mustache b/config/clients/python/template/test/sync/api_test.py.mustache index a8d0dc7f..96df6f15 100644 --- a/config/clients/python/template/test/sync/api_test.py.mustache +++ b/config/clients/python/template/test/sync/api_test.py.mustache @@ -1418,7 +1418,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): self.assertEqual(mock_request.call_count, 1) @patch.object(rest.RESTClientObject, "request") - async def test_500_error(self, mock_request): + async def test_500_error_retry(self, mock_request): """ Test to ensure 5xx retries are handled properly """ @@ -1491,7 +1491,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", ), ) - with self.assertRaises(ServiceException) as api_exception: + with self.assertRaises(ServiceException): api_instance.check( body=body, ) diff --git a/config/clients/python/template/test/sync/client/__init__.py.mustache b/config/clients/python/template/test/sync/client/__init__.py.mustache index e69de29b..96e69920 100644 --- a/config/clients/python/template/test/sync/client/__init__.py.mustache +++ b/config/clients/python/template/test/sync/client/__init__.py.mustache @@ -0,0 +1 @@ +{{>partial_header}} diff --git a/config/clients/python/template/test/telemetry/attributes_test.py.mustache b/config/clients/python/template/test/telemetry/attributes_test.py.mustache index 28086afa..9a4783e7 100644 --- a/config/clients/python/template/test/telemetry/attributes_test.py.mustache +++ b/config/clients/python/template/test/telemetry/attributes_test.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + import time from unittest.mock import MagicMock @@ -8,7 +10,7 @@ from {{packageName}}.credentials import CredentialConfiguration, Credentials from {{packageName}}.models.batch_check_request import BatchCheckRequest from {{packageName}}.models.check_request import CheckRequest from {{packageName}}.rest import RESTResponse -from {{packageName}}.rest import RESTResponse + from {{packageName}}.telemetry.attributes import ( TelemetryAttributes, ) @@ -168,4 +170,4 @@ def test_from_body_with_other_body(telemetry_attributes): attributes = telemetry_attributes.fromBody(body=body) - assert attributes == {} \ No newline at end of file + assert attributes == {} diff --git a/config/clients/python/template/test/telemetry/configuration_test.py.mustache b/config/clients/python/template/test/telemetry/configuration_test.py.mustache index 2607c433..74b384ba 100644 --- a/config/clients/python/template/test/telemetry/configuration_test.py.mustache +++ b/config/clients/python/template/test/telemetry/configuration_test.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from {{packageName}}.telemetry.attributes import TelemetryAttributes from {{packageName}}.telemetry.configuration import ( TelemetryConfiguration, @@ -312,4 +314,4 @@ def test_default_telemetry_metric_configuration(): assert metric_config[TelemetryAttributes.http_server_request_duration] is False assert metric_config[TelemetryAttributes.url_scheme] is True assert metric_config[TelemetryAttributes.url_full] is True - assert metric_config[TelemetryAttributes.user_agent_original] is True \ No newline at end of file + assert metric_config[TelemetryAttributes.user_agent_original] is True diff --git a/config/clients/python/template/test/telemetry/counters_test.py.mustache b/config/clients/python/template/test/telemetry/counters_test.py.mustache index 1dff0f33..34707495 100644 --- a/config/clients/python/template/test/telemetry/counters_test.py.mustache +++ b/config/clients/python/template/test/telemetry/counters_test.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters @@ -32,4 +34,4 @@ def test_telemetry_counters_custom_counter(): assert custom_counter.name == "fga-client.custom.counter" assert custom_counter.unit == "operations" - assert custom_counter.description == "A custom counter for specific operations." \ No newline at end of file + assert custom_counter.description == "A custom counter for specific operations." diff --git a/config/clients/python/template/test/telemetry/histograms_test.py.mustache b/config/clients/python/template/test/telemetry/histograms_test.py.mustache index df700568..4f0f614a 100644 --- a/config/clients/python/template/test/telemetry/histograms_test.py.mustache +++ b/config/clients/python/template/test/telemetry/histograms_test.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from {{packageName}}.telemetry.histograms import TelemetryHistogram, TelemetryHistograms @@ -40,4 +42,4 @@ def test_telemetry_histograms_custom_histogram(): assert custom_histogram.name == "fga-client.custom.histogram" assert custom_histogram.unit == "operations" - assert custom_histogram.description == "A custom histogram for specific operations." \ No newline at end of file + assert custom_histogram.description == "A custom histogram for specific operations." diff --git a/config/clients/python/template/test/telemetry/metrics_test.py.mustache b/config/clients/python/template/test/telemetry/metrics_test.py.mustache index aa4d2b24..79f6719b 100644 --- a/config/clients/python/template/test/telemetry/metrics_test.py.mustache +++ b/config/clients/python/template/test/telemetry/metrics_test.py.mustache @@ -1,9 +1,10 @@ +{{>partial_header}} + from unittest.mock import MagicMock, patch import pytest from opentelemetry.metrics import Counter, Histogram, Meter -from {{packageName}}.telemetry.attributes import TelemetryAttributes from {{packageName}}.telemetry.counters import TelemetryCounters from {{packageName}}.telemetry.histograms import TelemetryHistograms from {{packageName}}.telemetry.metrics import TelemetryMetrics @@ -39,11 +40,6 @@ def test_counter_creation(mock_get_meter): telemetry = TelemetryMetrics() - attributes = { - TelemetryAttributes.fga_client_request_model_id.name: "model_123", - "custom_attribute": "custom_value", - } - counter = telemetry.counter(TelemetryCounters.fga_client_credentials_request) assert counter == mock_counter @@ -64,11 +60,6 @@ def test_histogram_creation(mock_get_meter): telemetry = TelemetryMetrics() - attributes = { - TelemetryAttributes.fga_client_request_model_id.name: "model_123", - "custom_attribute": "custom_value", - } - histogram = telemetry.histogram(TelemetryHistograms.fga_client_request_duration) assert histogram == mock_histogram @@ -89,4 +80,4 @@ def test_invalid_counter_key(): def test_invalid_histogram_key(): telemetry = TelemetryMetrics() with pytest.raises(ValueError): - telemetry.histogram("invalid_histogram_key") \ No newline at end of file + telemetry.histogram("invalid_histogram_key") diff --git a/config/clients/python/template/test/telemetry/telemetry_test.py.mustache b/config/clients/python/template/test/telemetry/telemetry_test.py.mustache index 3a3122a1..508e9ce3 100644 --- a/config/clients/python/template/test/telemetry/telemetry_test.py.mustache +++ b/config/clients/python/template/test/telemetry/telemetry_test.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from unittest.mock import patch from {{packageName}}.telemetry.metrics import ( @@ -42,4 +44,4 @@ def test_metrics_initialization_without_patch(): # Access the metrics property again, no new instance should be created metrics_again = telemetry.metrics - assert metrics_again == metrics \ No newline at end of file + assert metrics_again == metrics diff --git a/config/clients/python/template/test/telemetry/utilities_test.py.mustache b/config/clients/python/template/test/telemetry/utilities_test.py.mustache index 7a3a23df..1a958a6d 100644 --- a/config/clients/python/template/test/telemetry/utilities_test.py.mustache +++ b/config/clients/python/template/test/telemetry/utilities_test.py.mustache @@ -1,3 +1,5 @@ +{{>partial_header}} + from mock import MagicMock from {{packageName}}.telemetry.utilities import doesInstanceHaveCallable From ade989f4aea5d94f3f6dbb39e4946c03245bbfdd Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Wed, 29 Jan 2025 21:23:17 -0600 Subject: [PATCH 2/8] feat(python-sdk): type hinting improvements --- config/clients/python/config.overrides.json | 8 - .../example1/requirements.txt.mustache | 1 + config/clients/python/template/model.mustache | 9 +- config/clients/python/template/pyproject.toml | 17 +- .../python/template/requirements.mustache | 1 + .../python/template/src/api.py.mustache | 2 +- .../template/src/api_client.py.mustache | 6 +- .../template/src/client/client.py.mustache | 138 +++++--- .../src/client/models/assertion.py.mustache | 44 ++- .../models/batch_check_item.py.mustache | 93 +++-- .../models/batch_check_request.py.mustache | 10 +- .../models/batch_check_response.py.mustache | 9 +- .../batch_check_single_response.py.mustache | 58 +-- .../client/models/check_request.py.mustache | 96 ++--- .../client_batch_check_response.py.mustache | 44 ++- .../client/models/expand_request.py.mustache | 16 +- .../models/list_objects_request.py.mustache | 73 ++-- .../models/list_relations_request.py.mustache | 73 ++-- .../models/list_users_request.py.mustache | 81 ++--- .../models/read_changes_request.py.mustache | 30 +- .../src/client/models/tuple.py.mustache | 82 ++--- .../client/models/write_request.py.mustache | 44 ++- .../client/models/write_response.py.mustache | 22 +- .../models/write_single_response.py.mustache | 52 ++- .../models/write_transaction_opts.py.mustache | 50 ++- .../template/src/configuration.py.mustache | 233 +++++++------ .../python/template/src/help.py.mustache | 117 ++++--- .../python/template/src/rest.py.mustache | 107 +++--- .../python/template/src/sync/api.py.mustache | 2 +- .../template/src/sync/api_client.py.mustache | 14 +- .../src/sync/client/client.py.mustache | 141 +++++--- .../python/template/src/sync/rest.py.mustache | 109 +++--- .../src/telemetry/attributes.py.mustache | 185 +++++----- .../src/telemetry/configuration.py.mustache | 330 ++++++++++++------ .../src/telemetry/counters.py.mustache | 9 +- .../src/telemetry/histograms.py.mustache | 9 +- .../src/telemetry/metrics.py.mustache | 137 ++++++-- .../src/telemetry/utilities.py.mustache | 9 - .../template/test-requirements.mustache | 1 + .../template/test/rest_test.py.mustache | 4 +- .../template/test/sync/rest_test.py.mustache | 5 +- .../test/telemetry/utilities_test.py.mustache | 17 - 42 files changed, 1495 insertions(+), 993 deletions(-) delete mode 100644 config/clients/python/template/src/telemetry/utilities.py.mustache delete mode 100644 config/clients/python/template/test/telemetry/utilities_test.py.mustache diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 13e71723..3c20c4de 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -219,10 +219,6 @@ "destinationFilename": "openfga_sdk/telemetry/telemetry.py", "templateType": "SupportingFiles" }, - "src/telemetry/utilities.py.mustache": { - "destinationFilename": "openfga_sdk/telemetry/utilities.py", - "templateType": "SupportingFiles" - }, "src/__init__.py.mustache": { "destinationFilename": "openfga_sdk/__init__.py", "templateType": "SupportingFiles" @@ -320,10 +316,6 @@ "destinationFilename": "test/telemetry/telemetry_test.py", "templateType": "SupportingFiles" }, - "test/telemetry/utilities_test.py.mustache": { - "destinationFilename": "test/telemetry/utilities_test.py", - "templateType": "SupportingFiles" - }, "test/__init__.py.mustache": { "destinationFilename": "test/__init__.py", "templateType": "SupportingFiles" diff --git a/config/clients/python/template/example/example1/requirements.txt.mustache b/config/clients/python/template/example/example1/requirements.txt.mustache index f0a60d32..23c9af82 100644 --- a/config/clients/python/template/example/example1/requirements.txt.mustache +++ b/config/clients/python/template/example/example1/requirements.txt.mustache @@ -9,3 +9,4 @@ python-dateutil >= 2.8.2 urllib3 >= 2.1.0 yarl >= 1.9.4 python-dotenv >= 1, <2 + diff --git a/config/clients/python/template/model.mustache b/config/clients/python/template/model.mustache index 7a01ae67..73469059 100644 --- a/config/clients/python/template/model.mustache +++ b/config/clients/python/template/model.mustache @@ -1,9 +1,6 @@ {{>partial_header}} -try: - from inspect import getfullargspec -except ImportError: - from inspect import getargspec as getfullargspec +from inspect import getfullargspec import pprint from {{packageName}}.configuration import Configuration @@ -37,13 +34,13 @@ class {{classname}}: attribute_map (dict): The key is attribute name and the value is json key in definition. """ - openapi_types = { + openapi_types: dict[str, str] = { {{#vars}} '{{name}}': '{{{dataType}}}'{{^-last}},{{/-last}} {{/vars}} } - attribute_map = { + attribute_map: dict[str, str] = { {{#vars}} '{{name}}': '{{baseName}}'{{^-last}},{{/-last}} {{/vars}} diff --git a/config/clients/python/template/pyproject.toml b/config/clients/python/template/pyproject.toml index 74966dc4..ca06cb1a 100644 --- a/config/clients/python/template/pyproject.toml +++ b/config/clients/python/template/pyproject.toml @@ -33,7 +33,7 @@ indent-width = 4 target-version = "py310" -[lint] +[tool.ruff.lint] select = ["E4", "E7", "E9", "F"] ignore = [] @@ -42,7 +42,7 @@ unfixable = [] dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -[format] +[tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false @@ -50,7 +50,7 @@ line-ending = "auto" [tool.pytest.ini_options] testpaths = [ - "tests", + "test", "integration", ] @@ -58,4 +58,13 @@ addopts = "--cov=openfga_sdk --cov-report term-missing --cov-report xml --cov-re asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" -asyncio_default_test_loop_scope = "function" + +[tool.mypy] +python_version = "3.10" +packages = "openfga_sdk" +exclude = [ + "openfa_sdk/models", +] +#warn_return_any = "True" +#warn_unused_configs = "True" +#disallow_untyped_defs = "True" diff --git a/config/clients/python/template/requirements.mustache b/config/clients/python/template/requirements.mustache index 4eef03dd..8cda1952 100644 --- a/config/clients/python/template/requirements.mustache +++ b/config/clients/python/template/requirements.mustache @@ -4,3 +4,4 @@ setuptools >= 69.1.1 build >= 1.2.1, < 2 urllib3 >= 1.25.11, < 3 opentelemetry-api >= 1.25.0, < 2 +python-dateutil >= 2.9.0, < 3 diff --git a/config/clients/python/template/src/api.py.mustache b/config/clients/python/template/src/api.py.mustache index 7bb4fa76..b9a278c2 100644 --- a/config/clients/python/template/src/api.py.mustache +++ b/config/clients/python/template/src/api.py.mustache @@ -290,7 +290,7 @@ class {{classname}}: response_types_map = {} {{/returnType}} - telemetry_attributes: dict[TelemetryAttribute, str | int] = { + telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] = { TelemetryAttributes.fga_client_request_method: "{{operationId}}", TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), TelemetryAttributes.fga_client_request_model_id: local_var_params.get( diff --git a/config/clients/python/template/src/api_client.py.mustache b/config/clients/python/template/src/api_client.py.mustache index 391a6b5d..89a2a159 100644 --- a/config/clients/python/template/src/api_client.py.mustache +++ b/config/clients/python/template/src/api_client.py.mustache @@ -16,7 +16,7 @@ import tornado.gen {{/tornado}} from multiprocessing.pool import ThreadPool -from dateutil.parser import parse +from dateutil.parser import parse # type: ignore[import-untyped] import {{modelPackage}} from {{packageName}} import rest, oauth2 @@ -175,7 +175,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): @@ -514,7 +514,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): """Makes the HTTP request (synchronous) and returns deserialized data. diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index 55795fbe..59aed0de 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -62,22 +62,31 @@ def _chuck_array(array, max_size): return [array[i * max_size:(i + 1) * max_size] for i in range((len(array) + max_size - 1) // max_size )] -def set_heading_if_not_set(options: dict[str, int|str], name: str, value: str): +def set_heading_if_not_set( + options: dict[str, int | str | dict[str, int | str]] | None, + name: str, + value: str, +) -> dict[str, int | str | dict[str, int | str]]: """ Set heading to the value if it is not set """ - if options is None: - options = {} - headers = options.get("headers") - if headers is None: - headers = {} - if headers.get(name) is None: - headers[name] = value - options["headers"] = headers - return options - - -def options_to_kwargs(options: dict[str, int|str] = None): + _options: dict[str, int | str | dict[str, int | str]] = ( + options if options is not None else {} + ) + + if type(_options.get("headers")) is not dict: + _options["headers"] = {} + + if type(_options["headers"]) is dict: + if type(_options["headers"].get(name)) not in [int, str]: + _options["headers"][name] = value + + return _options + + +def options_to_kwargs( + options: dict[str, int | str | dict[str, int | str]] | None = None, +) -> dict[str, int | str | dict[str, int | str]]: """ Return kwargs with continuation_token and page_size """ @@ -93,7 +102,7 @@ def options_to_kwargs(options: dict[str, int|str] = None): kwargs["_retry_params"] = options["retry_params"] return kwargs -def options_to_transaction_info(options: dict[str, int|str] = None): +def options_to_transaction_info(options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the transaction info """ @@ -136,7 +145,10 @@ class OpenFgaClient: {{#asyncio}}async {{/asyncio}}def close(self): {{#asyncio}}await {{/asyncio}}self._api.close() - def _get_authorization_model_id(self, options: object) -> str | None: + def _get_authorization_model_id( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Return the authorization model ID if specified in the options. Otherwise, return the authorization model ID stored in the client's configuration @@ -151,13 +163,21 @@ class OpenFgaClient: "authorization_model_id ('%s') is not in a valid ulid format" % authorization_model_id) return authorization_model_id - def _get_consistency(self, options: object) -> str | None: + def _get_consistency( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Returns the consistency requested if specified in the options. Otherwise, returns None. """ - if options is not None and "consistency" in options: - return options["consistency"] + consistency: int | str | dict[str, int | str] | None = ( + options.get("consistency", None) if options is not None else None + ) + + if type(consistency) is str: + return consistency + return None def set_store_id(self, value): @@ -188,7 +208,7 @@ class OpenFgaClient: # Stores ################# - {{#asyncio}}async {{/asyncio}}def list_stores(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def list_stores(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ List the stores in the system :param page_size(options) - Number of items returned per request @@ -205,7 +225,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def create_store(self, body: CreateStoreRequest, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def create_store(self, body: CreateStoreRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Create the stores in the system :param header(options) - Custom headers to send alongside the request @@ -220,7 +240,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def get_store(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def get_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Get the store info in the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -234,7 +254,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def delete_store(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def delete_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Delete the store from the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -252,7 +272,7 @@ class OpenFgaClient: # Authorization Models ####################### - {{#asyncio}}async {{/asyncio}}def read_authorization_models(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_authorization_models(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the authorization models for a particular store. :param header(options) - Custom headers to send alongside the request @@ -266,7 +286,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write authorization model. :param body - WriteAuthorizationModelRequest @@ -282,7 +302,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def read_authorization_model(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read an authorization model. :param header(options) - Custom headers to send alongside the request @@ -298,7 +318,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def read_latest_authorization_model(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_latest_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method of reading the latest authorization model :param header(options) - Custom headers to send alongside the request @@ -316,7 +336,7 @@ class OpenFgaClient: # Relationship Tuples ####################### - {{#asyncio}}async {{/asyncio}}def read_changes(self, body: ClientReadChangesRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read_changes(self, body: ClientReadChangesRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the type we want to look for change @@ -328,14 +348,19 @@ class OpenFgaClient: :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated """ kwargs = options_to_kwargs(options) - kwargs["type"] = body.type - kwargs["start_time"] = body.start_time + + if body.type is not None: + kwargs["type"] = body.type + + if body.start_time is not None: + kwargs["start_time"] = body.start_time + api_response = {{#asyncio}}await {{/asyncio}}self._api.read_changes( **kwargs, ) return api_response - {{#asyncio}}async {{/asyncio}}def read(self, body: ReadRequestTupleKey, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read(self, body: ReadRequestTupleKey, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the tuples we want to read @@ -374,7 +399,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): try: write_batch = None delete_batch = None @@ -389,7 +414,7 @@ class OpenFgaClient: except Exception as err: return [construct_write_single_response(i, False, err) for i in batch] - {{#asyncio}}async {{/asyncio}}def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Internal function for write/delete batches """ @@ -410,7 +435,7 @@ class OpenFgaClient: return batch_write_responses - {{#asyncio}}async {{/asyncio}}def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples """ @@ -439,7 +464,7 @@ class OpenFgaClient: deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - {{#asyncio}}async {{/asyncio}}def write(self, body: ClientWriteRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples :param body - the write request @@ -465,7 +490,7 @@ class OpenFgaClient: deletes_response = {{#asyncio}}await {{/asyncio}}self._write_batches(body.deletes, transaction, False, options) return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - {{#asyncio}}async {{/asyncio}}def write_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for writing tuples :param body - the list of tuples we want to write @@ -478,7 +503,7 @@ class OpenFgaClient: result = {{#asyncio}}await {{/asyncio}}self.write(ClientWriteRequest(body, None), options) return result - {{#asyncio}}async {{/asyncio}}def delete_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def delete_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for deleteing tuples :param body - the list of tuples we want to delete @@ -494,7 +519,11 @@ class OpenFgaClient: ####################### # Relationship Queries ####################### - {{#asyncio}}async {{/asyncio}}def check(self, body: ClientCheckRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def check( + self, + body: ClientCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Check whether a user is authorized to access an object :param body - ClientCheckRequest defining check request @@ -527,7 +556,12 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def _single_client_batch_check(self, body: ClientCheckRequest, semaphore: asyncio.Semaphore, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _single_client_batch_check( + self, + body: ClientCheckRequest, + semaphore: asyncio.Semaphore, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a single batch request and return body in a SingleBatchCheckResponse :param body - ClientCheckRequest defining check request @@ -546,7 +580,11 @@ class OpenFgaClient: semaphore.release() {{/asyncio}} - {{#asyncio}}async {{/asyncio}}def client_batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): + {{#asyncio}}async {{/asyncio}}def client_batch_check( + self, + body: list[ClientCheckRequest], + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a set of checks :param body - list of ClientCheckRequest defining check request @@ -590,7 +628,7 @@ class OpenFgaClient: self, body: BatchCheckRequest, semaphore: asyncio.Semaphore, - options: dict[str, str] = None, + options: dict[str, int | str | dict[str, int | str]] | None = None, ): """ Run a single BatchCheck request @@ -607,7 +645,11 @@ class OpenFgaClient: finally: semaphore.release() - async def batch_check(self, body: ClientBatchCheckRequest, options = None): + async def batch_check( + self, + body: ClientBatchCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a batchcheck request :param body - BatchCheck request @@ -691,7 +733,7 @@ class OpenFgaClient: return ClientBatchCheckResponse(result) - {{#asyncio}}async {{/asyncio}}def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def expand(self, body: ClientExpandRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run expand request :param body - list of ClientExpandRequest defining expand request @@ -722,7 +764,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def list_objects(self, body: ClientListObjectsRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def list_objects(self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run list object request :param body - list object parameters @@ -754,7 +796,7 @@ class OpenFgaClient: return api_response {{#asyncio}}async {{/asyncio}}def streamed_list_objects( - self, body: ClientListObjectsRequest, options: dict[str, str] = None + self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None ): """ Retrieve all objects of the given type that the user has a relation with, using the streaming ListObjects API. @@ -790,7 +832,7 @@ class OpenFgaClient: if response and "result" in response and "object" in response["result"]: yield StreamedListObjectsResponse(response["result"]["object"]) - {{#asyncio}}async {{/asyncio}}def list_relations(self, body: ClientListRelationsRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def list_relations(self, body: ClientListRelationsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the relations for which user has a relationship with the object :param body - list relation request @@ -812,7 +854,7 @@ class OpenFgaClient: return [i.request.relation for i in result_list] {{#asyncio}}async {{/asyncio}}def list_users( - self, body: ClientListUsersRequest, options: dict[str, str] = None + self, body: ClientListUsersRequest, options: dict[str, int | str | dict[str, int | str]] | None = None ): """ Run list users request @@ -846,7 +888,7 @@ class OpenFgaClient: ####################### # Assertions ####################### - {{#asyncio}}async {{/asyncio}}def read_assertions(self, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read_assertions(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the assertions :param authorization_model_id(options) - Overrides the authorization model id in the configuration @@ -861,7 +903,7 @@ class OpenFgaClient: api_response = {{#asyncio}}await {{/asyncio}}self._api.read_assertions(authorization_model_id, **kwargs) return api_response - {{#asyncio}}async {{/asyncio}}def write_assertions(self, body: list[ClientAssertion], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write_assertions(self, body: list[ClientAssertion], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Upsert the assertions :param body - Write assertion request diff --git a/config/clients/python/template/src/client/models/assertion.py.mustache b/config/clients/python/template/src/client/models/assertion.py.mustache index ca189609..4baa97e7 100644 --- a/config/clients/python/template/src/client/models/assertion.py.mustache +++ b/config/clients/python/template/src/client/models/assertion.py.mustache @@ -5,36 +5,70 @@ class ClientAssertion: ClientAssertion flattens the input necessary for an Assertion """ - def __init__(self, user: str, relation: str, object: str, expectation: bool): + def __init__( + self, + user: str, + relation: str, + object: str, + expectation: bool, + ) -> None: self._user = user self._relation = relation self._object = object self._expectation = expectation @property - def user(self): + def user(self) -> str: """ Return user """ return self._user + @user.setter + def user(self, user: str) -> None: + """ + Set user + """ + self._user = user + @property - def relation(self): + def relation(self) -> str: """ Return relation """ return self._relation + @relation.setter + def relation(self, relation: str) -> None: + """ + Set relation + """ + self._relation = relation + @property - def object(self): + def object(self) -> str: """ Return object """ return self._object + @object.setter + def object(self, object: str) -> None: + """ + Set object + """ + self._object = object + @property - def expectation(self): + def expectation(self) -> bool: """ Return expectation """ return self._expectation + + @expectation.setter + def expectation(self, expectation: bool) -> None: + """ + Set expectation + """ + self._expectation = expectation diff --git a/config/clients/python/template/src/client/models/batch_check_item.py.mustache b/config/clients/python/template/src/client/models/batch_check_item.py.mustache index 6229ab62..415374c0 100644 --- a/config/clients/python/template/src/client/models/batch_check_item.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_item.py.mustache @@ -6,7 +6,7 @@ from {{packageName}}.models.check_request_tuple_key import CheckRequestTupleKey from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys -def construct_batch_item(check): +def construct_batch_item(check) -> BatchCheckItem: batch_item = BatchCheckItem( tuple_key=CheckRequestTupleKey( user=check.user, @@ -31,100 +31,97 @@ class ClientBatchCheckItem: user: str, relation: str, object: str, - correlation_id: str = None, - contextual_tuples: list[ClientTuple] = None, - context: object = None, - ): + correlation_id: str | None = None, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._user = user self._relation = relation self._object = object self._correlation_id = correlation_id - self._contextual_tuples = None - if contextual_tuples: - self._contextual_tuples = contextual_tuples + self._contextual_tuples = contextual_tuples self._context = context - @property - def user(self): + def user(self) -> str: """ Return user """ return self._user + @user.setter + def user(self, value: str) -> None: + """ + Set user + """ + self._user = value + @property - def relation(self): + def relation(self) -> str: """ Return relation """ return self._relation - @property - def object(self): + @relation.setter + def relation(self, value: str) -> None: """ - Return object + Set relation """ - return self._object - + self._relation = value @property - def contextual_tuples(self): + def object(self) -> str: """ - Return contextual tuples + Return object """ - return self._contextual_tuples + return self._object - @property - def context(self): + @object.setter + def object(self, value: str) -> None: """ - Return context + Set object """ - return self._context + self._object = value @property - def correlation_id(self): + def correlation_id(self) -> str | None: """ + Return correlation id """ return self._correlation_id - - @user.setter - def user(self, value): - """ - Set user - """ - self._user = value - - @relation.setter - def relation(self, value): + @correlation_id.setter + def correlation_id(self, value: str | None) -> None: """ - Set relation + Set correlation id """ - self._relation = value + self._correlation_id = value - @object.setter - def object(self, value): + @property + def contextual_tuples(self) -> list[ClientTuple] | None: """ - Set object + Return contextual tuples """ - self._object = value + return self._contextual_tuples @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value - @context.setter - def context(self, value): + @property + def context(self) -> dict[str, int | str] | None: """ - Set context + Return context """ - self._context = value + return self._context - @correlation_id.setter - def correlation_id(self, value): + @context.setter + def context(self, value: dict[str, int | str] | None) -> None: """ + Set context """ - self._correlation_id = value + self._context = value diff --git a/config/clients/python/template/src/client/models/batch_check_request.py.mustache b/config/clients/python/template/src/client/models/batch_check_request.py.mustache index efe9b321..40b082e5 100644 --- a/config/clients/python/template/src/client/models/batch_check_request.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_request.py.mustache @@ -7,18 +7,22 @@ class ClientBatchCheckRequest: """ ClientBatchCheckRequest encapsulates the parameters for a BatchCheck request """ - def __init__(self, checks: list[ClientBatchCheckItem]): + + def __init__( + self, + checks: list[ClientBatchCheckItem], + ) -> None: self._checks = checks @property - def checks(self): + def checks(self) -> list[ClientBatchCheckItem]: """ Return checks """ return self._checks @checks.setter - def checks(self, checks): + def checks(self, checks: list[ClientBatchCheckItem]) -> None: """ Set checks """ diff --git a/config/clients/python/template/src/client/models/batch_check_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_response.py.mustache index 018f288a..479484d7 100644 --- a/config/clients/python/template/src/client/models/batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_response.py.mustache @@ -4,18 +4,21 @@ from {{packageName}}.client.models.batch_check_single_response import ClientBatc class ClientBatchCheckResponse: - def __init__(self, result: list[ClientBatchCheckSingleResponse]): + def __init__( + self, + result: list[ClientBatchCheckSingleResponse], + ) -> None: self._result = result @property - def result(self): + def result(self) -> list[ClientBatchCheckSingleResponse]: """ Return result """ return self._result @result.setter - def result(self, result): + def result(self, result: list[ClientBatchCheckSingleResponse]) -> None: """ Set result """ diff --git a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache index 3a46eb29..5f9252f3 100644 --- a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache @@ -10,68 +10,68 @@ class ClientBatchCheckSingleResponse: allowed: bool, request: ClientTuple, correlation_id: str, - error: CheckError = None, - ): + error: CheckError | None = None, + ) -> None: self._allowed = allowed self._request = request self._correlation_id = correlation_id self._error = error - # Set "false" if there was an error and allowed isn't set + + # Set `allowed` to `false` if there was an error and allowed isn't otherwise set. if error is not None and allowed is None: self._allowed = False - @property - def allowed(self): + def allowed(self) -> bool: """ Return allowed """ return self._allowed - @property - def request(self): - """ - Return request - """ - return self._request - - @property - def correlation_id(self): + @allowed.setter + def allowed(self, allowed: bool) -> None: """ - Return correlation_id + Set allowed """ - return self._correlation_id + self._allowed = allowed @property - def error(self): - """ - Return error - """ - return self._error - - @allowed.setter - def allowed(self, allowed): + def request(self) -> ClientTuple: """ - Set allowed + Return request """ - self._allowed = allowed + return self._request @request.setter - def request(self, request): + def request(self, request: ClientTuple) -> None: """ Set request """ self._request = request + @property + def correlation_id(self) -> str: + """ + Return correlation_id + """ + return self._correlation_id + @correlation_id.setter - def correlation_id(self, correlation_id): + def correlation_id(self, correlation_id: str) -> None: """ Set correlation_id """ self._correlation_id = correlation_id + @property + def error(self) -> CheckError | None: + """ + Return error + """ + return self._error + @error.setter - def error(self, error): + def error(self, error: CheckError | None) -> None: """ Set error """ diff --git a/config/clients/python/template/src/client/models/check_request.py.mustache b/config/clients/python/template/src/client/models/check_request.py.mustache index 8f9dc0a7..2be3824b 100644 --- a/config/clients/python/template/src/client/models/check_request.py.mustache +++ b/config/clients/python/template/src/client/models/check_request.py.mustache @@ -3,93 +3,107 @@ from {{packageName}}.client.models.tuple import ClientTuple -def construct_check_request(user: str, relation: str, object: str, contextual_tuples: list[ClientTuple] = None, context: object = None): - """ - helper function to construct the check request body - """ - return ClientCheckRequest(user, relation, object, contextual_tuples, context) - - class ClientCheckRequest: """ ClientCheckRequest encapsulates the parameters for check request """ - def __init__(self, user: str, relation: str, object: str, contextual_tuples: list[ClientTuple] = None, context: object = None): + def __init__( + self, + user: str, + relation: str, + object: str, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._user = user self._relation = relation self._object = object self._contextual_tuples = None + self._context = context + if contextual_tuples: self._contextual_tuples = contextual_tuples - self._context = context @property - def user(self): + def user(self) -> str: """ Return user """ return self._user - @property - def relation(self): + @user.setter + def user(self, value: str) -> None: """ - Return relation + Set user """ - return self._relation + self._user = value @property - def object(self): + def relation(self) -> str: """ - Return object + Return relation """ - return self._object + return self._relation - @property - def contextual_tuples(self): + @relation.setter + def relation(self, value: str) -> None: """ - Return contextual tuples + Set relation """ - return self._contextual_tuples + self._relation = value @property - def context(self): - """ - Return context + def object(self) -> str: """ - return self._context - - @user.setter - def user(self, value): - """ - Set user - """ - self._user = value - - @relation.setter - def relation(self, value): - """ - Set relation + Return object """ - self._relation = value + return self._object @object.setter - def object(self, value): + def object(self, value: str) -> None: """ Set object """ self._object = value + @property + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Return contextual tuples + """ + return self._contextual_tuples + @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value + @property + def context(self) -> dict[str, int | str] | None: + """ + Return context + """ + return self._context + @context.setter - def context(self, value): + def context(self, value: dict[str, int | str] | None) -> None: """ Set context """ self._context = value + + +def construct_check_request( + user: str, + relation: str, + object: str, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, +) -> ClientCheckRequest: + """ + helper function to construct the check request body + """ + return ClientCheckRequest(user, relation, object, contextual_tuples, context) diff --git a/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache index 33c72e25..aa3ab7c8 100644 --- a/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache @@ -9,40 +9,74 @@ class ClientBatchCheckClientResponse: ClientBatchCheckClientResponse encapsulates the response for a single batch check """ - def __init__(self, allowed: bool, request: ClientCheckRequest, response: CheckResponse, error: Exception=None): + def __init__( + self, + allowed: bool, + request: ClientCheckRequest, + response: CheckResponse | None = None, + error: Exception | None = None, + ) -> None: self._allowed = allowed self._request = request self._response = response self._error = error @property - def allowed(self): + def allowed(self) -> bool: """ Return whether request is allowed """ return self._allowed + @allowed.setter + def allowed(self, value: bool) -> None: + """ + Set whether request is allowed + """ + self._allowed = value + @property - def request(self): + def request(self) -> ClientCheckRequest: """ Return original request """ return self._request + @request.setter + def request(self, value: ClientCheckRequest) -> None: + """ + Set original request + """ + self._request = value + @property - def response(self): + def response(self) -> CheckResponse | None: """ Return original request """ return self._response + @response.setter + def response(self, value: CheckResponse | None) -> None: + """ + Set original request + """ + self._response = value + @property - def error(self): + def error(self) -> Exception | None: """ Return error associated with batch request (if any) """ return self._error + @error.setter + def error(self, value: Exception | None) -> None: + """ + Set error associated with batch request + """ + self._error = value + def __str__(self): """ Return the class string diff --git a/config/clients/python/template/src/client/models/expand_request.py.mustache b/config/clients/python/template/src/client/models/expand_request.py.mustache index 6a315bc7..9dbf8590 100644 --- a/config/clients/python/template/src/client/models/expand_request.py.mustache +++ b/config/clients/python/template/src/client/models/expand_request.py.mustache @@ -12,49 +12,49 @@ class ClientExpandRequest: self, relation: str, object: str, - contextual_tuples: list[ClientTuple] = None, - ): + contextual_tuples: list[ClientTuple] | None = None, + ) -> None: self._relation = relation self._object = object self._contextual_tuples = contextual_tuples @property - def relation(self): + def relation(self) -> str: """ Return relation """ return self._relation @relation.setter - def relation(self, value): + def relation(self, value: str) -> None: """ Set relation """ self._relation = value @property - def object(self): + def object(self) -> str: """ Return object """ return self._object @object.setter - def object(self, value): + def object(self, value: str) -> None: """ Set object """ self._object = value @property - def contextual_tuples(self): + def contextual_tuples(self) -> list[ClientTuple] | None: """ Return contextual_tuples """ return self._contextual_tuples @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ diff --git a/config/clients/python/template/src/client/models/list_objects_request.py.mustache b/config/clients/python/template/src/client/models/list_objects_request.py.mustache index 674d5b9f..bc2d0097 100644 --- a/config/clients/python/template/src/client/models/list_objects_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_objects_request.py.mustache @@ -8,7 +8,14 @@ class ClientListObjectsRequest: ClientListObjectsRequest encapsulates the parameters required for list objects """ - def __init__(self, user: str, relation: str, type: str, contextual_tuples: list[ClientTuple] = None, context: object = None): + def __init__( + self, + user: str, + relation: str, + type: str, + contextual_tuples: list[ClientTuple] | None = None, + context: object | None = None, + ) -> None: self._user = user self._relation = relation self._type = type @@ -16,70 +23,70 @@ class ClientListObjectsRequest: self._context = context @property - def user(self): + def user(self) -> str: """ Return user """ return self._user - @property - def relation(self): + @user.setter + def user(self, value: str) -> None: """ - Return relation + Set user """ - return self._relation + self._user = value @property - def type(self): + def relation(self) -> str: """ - Return type + Return relation """ - return self._type + return self._relation - @property - def contextual_tuples(self): + @relation.setter + def relation(self, value: str) -> None: """ - Return contextual_tuples + Set relation """ - return self._contextual_tuples + self._relation = value @property - def context(self): - """ - Return context - """ - return self._context - - @user.setter - def user(self, value): + def type(self) -> str: """ - Set user - """ - self._user = value - - @relation.setter - def relation(self, value): - """ - Set relation + Return type """ - self._relation = value + return self._type @type.setter - def type(self, value): + def type(self, value: str) -> None: """ Set type """ self._type = value + @property + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Return contextual_tuples + """ + return self._contextual_tuples + @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value + @property + def context(self) -> object | None: + """ + Return context + """ + return self._context + @context.setter - def context(self, value): + def context(self, value: object | None) -> None: """ Set context """ diff --git a/config/clients/python/template/src/client/models/list_relations_request.py.mustache b/config/clients/python/template/src/client/models/list_relations_request.py.mustache index eb668d30..24d54299 100644 --- a/config/clients/python/template/src/client/models/list_relations_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_relations_request.py.mustache @@ -8,7 +8,14 @@ class ClientListRelationsRequest: ClientListRelationsRequest encapsulates the parameters required for list all relations user have with object """ - def __init__(self, user: str, relations: list[str], object: str, contextual_tuples: list[ClientTuple] = None, context: object = None): + def __init__( + self, + user: str, + relations: list[str], + object: str, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._user = user self._relations = relations self._object = object @@ -16,70 +23,70 @@ class ClientListRelationsRequest: self._context = context @property - def user(self): + def user(self) -> str: """ Return user """ return self._user - @property - def relations(self): + @user.setter + def user(self, value: str) -> None: """ - Return relations + Set user """ - return self._relations + self._user = value @property - def object(self): + def relations(self) -> list[str]: """ - Return object + Return relations """ - return self._object + return self._relations - @property - def contextual_tuples(self): + @relations.setter + def relations(self, value: list[str]) -> None: """ - Return contextual_tuples + Set relations """ - return self._contextual_tuples + self._relations = value @property - def context(self): - """ - Return context - """ - return self._context - - @user.setter - def user(self, value): + def object(self) -> str: """ - Set user - """ - self._user = value - - @relations.setter - def relations(self, value): - """ - Set relations + Return object """ - self._relations = value + return self._object @object.setter - def object(self, value): + def object(self, value: str) -> None: """ Set object """ self._object = value + @property + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Return contextual_tuples + """ + return self._contextual_tuples + @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value + @property + def context(self) -> dict[str, int | str] | None: + """ + Return context + """ + return self._context + @context.setter - def context(self, value): + def context(self, value: dict[str, int | str] | None) -> None: """ Set context """ diff --git a/config/clients/python/template/src/client/models/list_users_request.py.mustache b/config/clients/python/template/src/client/models/list_users_request.py.mustache index 3c83e3e1..9944be77 100644 --- a/config/clients/python/template/src/client/models/list_users_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_users_request.py.mustache @@ -12,12 +12,12 @@ class ClientListUsersRequest: def __init__( self, - object: FgaObject = None, - relation: str = None, - user_filters: list[UserTypeFilter] = None, - contextual_tuples: list[ClientTuple] = None, - context: object = None, - ): + object: FgaObject | None = None, + relation: str | None = None, + user_filters: list[UserTypeFilter] | None = None, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._object = object self._relation = relation self._user_filters = user_filters @@ -25,9 +25,9 @@ class ClientListUsersRequest: self._context = context @property - def object(self) -> FgaObject: - """Gets the object of this ClientListUsersRequest. - + def object(self) -> FgaObject | None: + """ + Gets the object of this ClientListUsersRequest. :return: The object of this ClientListUsersRequest. :rtype: str @@ -35,20 +35,19 @@ class ClientListUsersRequest: return self._object @object.setter - def object(self, object: FgaObject): - """Sets the object of this ClientListUsersRequest. - + def object(self, object: FgaObject | None) -> None: + """ + Sets the object of this ClientListUsersRequest. :param object: The object of this ClientListUsersRequest. :type object: str """ - self._object = object @property - def relation(self) -> str: - """Gets the relation of this ClientListUsersRequest. - + def relation(self) -> str | None: + """ + Gets the relation of this ClientListUsersRequest. :return: The relation of this ClientListUsersRequest. :rtype: str @@ -56,20 +55,19 @@ class ClientListUsersRequest: return self._relation @relation.setter - def relation(self, relation: str): - """Sets the relation of this ClientListUsersRequest. - + def relation(self, relation: str | None) -> None: + """ + Sets the relation of this ClientListUsersRequest. :param relation: The relation of this ClientListUsersRequest. :type relation: str """ - self._relation = relation @property - def user_filters(self) -> list[UserTypeFilter]: - """Gets the user_filters of this ClientListUsersRequest. - + def user_filters(self) -> list[UserTypeFilter] | None: + """ + Gets the user_filters of this ClientListUsersRequest. :return: The user_filters of this ClientListUsersRequest. :rtype: str @@ -77,20 +75,19 @@ class ClientListUsersRequest: return self._user_filters @user_filters.setter - def user_filters(self, user_filters: list[UserTypeFilter]): - """Sets the user_filters of this ClientListUsersRequest. - + def user_filters(self, user_filters: list[UserTypeFilter] | None) -> None: + """ + Sets the user_filters of this ClientListUsersRequest. :param user_filters: The user_filters of this ClientListUsersRequest. :type user_filters: str """ - self._user_filters = user_filters @property - def contextual_tuples(self) -> list[ClientTuple]: - """Gets the contextual_tuples of this ClientListUsersRequest. - + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Gets the contextual_tuples of this ClientListUsersRequest. :return: The contextual_tuples of this ClientListUsersRequest. :rtype: ContextualTupleKeys @@ -98,35 +95,29 @@ class ClientListUsersRequest: return self._contextual_tuples @contextual_tuples.setter - def contextual_tuples(self, contextual_tuples: list[ClientTuple]): - """Sets the contextual_tuples of this ClientListUsersRequest. - + def contextual_tuples(self, contextual_tuples: list[ClientTuple] | None) -> None: + """ + Sets the contextual_tuples of this ClientListUsersRequest. :param contextual_tuples: The contextual_tuples of this ClientListUsersRequest. :type contextual_tuples: ContextualTupleKeys """ - self._contextual_tuples = contextual_tuples @property - def context(self) -> object: - """Gets the context of this ClientListUsersRequest. + def context(self) -> dict[str, int | str] | None: + """ + Gets the context of this ClientListUsersRequest. Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. - - :return: The context of this ClientListUsersRequest. - :rtype: object """ return self._context @context.setter - def context(self, context: object): - """Sets the context of this ClientListUsersRequest. + def context(self, context: dict[str, int | str] | None) -> None: + """ + Sets the context of this ClientListUsersRequest. Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. - - :param context: The context of this ClientListUsersRequest. - :type context: object """ - self._context = context diff --git a/config/clients/python/template/src/client/models/read_changes_request.py.mustache b/config/clients/python/template/src/client/models/read_changes_request.py.mustache index 5aafb89a..e0a0351b 100644 --- a/config/clients/python/template/src/client/models/read_changes_request.py.mustache +++ b/config/clients/python/template/src/client/models/read_changes_request.py.mustache @@ -5,20 +5,44 @@ class ClientReadChangesRequest: ClientReadChangesRequest encapsulates the parameters required to read changes """ - def __init__(self, type: str, start_time: str=None): + def __init__( + self, + type: str, + start_time: str | None = None, + ): self._type = type self._startTime = start_time @property - def type(self): + def type(self) -> str: """ Return type """ return self._type + @type.setter + def type( + self, + value: str, + ) -> None: + """ + Set type + """ + self._type = value + @property - def start_time(self): + def start_time(self) -> str | None: """ Return startTime """ return self._startTime + + @start_time.setter + def start_time( + self, + value: str | None, + ) -> None: + """ + Set startTime + """ + self._startTime = value diff --git a/config/clients/python/template/src/client/models/tuple.py.mustache b/config/clients/python/template/src/client/models/tuple.py.mustache index 823c706b..2ec2594f 100644 --- a/config/clients/python/template/src/client/models/tuple.py.mustache +++ b/config/clients/python/template/src/client/models/tuple.py.mustache @@ -15,75 +15,59 @@ class ClientTuple: relation: str, object: str, condition: RelationshipCondition | None = None, - ): + ) -> None: self._user = user self._relation = relation self._object = object self._condition = condition - def __eq__(self, other): - return self.user == other.user and self.relation == other.relation and self.object == other.object + def __eq__(self, other) -> bool: + if ( + self.user == other.user + and self.relation == other.relation + and self.object == other.object + and self.condition == other.condition + ): + return True - @property - def user(self): - """ - Return user - """ - return self._user - - @property - def relation(self): - """ - Return relation - """ - return self._relation + return False @property - def object(self): - """ - Return object - """ - return self._object - - @property - def condition(self): - """ - Return condition - """ - return self._condition + def user(self) -> str: + return self._user @user.setter - def user(self, value): - """ - Set user - """ + def user(self, value: str) -> None: self._user = value + @property + def relation(self) -> str: + return self._relation + @relation.setter - def relation(self, value): - """ - Set relation - """ + def relation(self, value: str) -> None: self._relation = value + @property + def object(self) -> str: + return self._object + @object.setter - def object(self, value): - """ - Set object - """ + def object(self, value: str) -> None: self._object = value + @property + def condition(self) -> RelationshipCondition | None: + return self._condition + @condition.setter - def condition(self, value): - """ - Set condition - """ + def condition(self, value: RelationshipCondition | None) -> None: self._condition = value @property - def tuple_key(self): + def tuple_key(self) -> TupleKey: """ - Return the tuple as tuple_key + Return the ClientTuple as TupleKey """ return TupleKey( object=self.object, @@ -93,11 +77,11 @@ class ClientTuple: ) -def convert_tuple_keys(lists: list[ClientTuple]): +def convert_tuple_keys(lists: list[ClientTuple]) -> list[TupleKey] | None: """ Return the items as tuple_keys """ if lists is None: return None - items=map(lambda item: item.tuple_key, lists) - return list(items) + + return list(map(lambda item: item.tuple_key, lists)) diff --git a/config/clients/python/template/src/client/models/write_request.py.mustache b/config/clients/python/template/src/client/models/write_request.py.mustache index 56335d15..bfba1e97 100644 --- a/config/clients/python/template/src/client/models/write_request.py.mustache +++ b/config/clients/python/template/src/client/models/write_request.py.mustache @@ -10,54 +10,68 @@ class ClientWriteRequest: ClientWriteRequest encapsulates the parameters required to write """ - def __init__(self, writes: list[ClientTuple]=None, deletes: list[ClientTuple]=None): + def __init__( + self, + writes: list[ClientTuple] | None = None, + deletes: list[ClientTuple] | None = None, + ) -> None: self._writes = writes self._deletes = deletes @property - def writes(self): + def writes(self) -> list[ClientTuple] | None: """ Return writes """ return self._writes - @property - def deletes(self): - """ - Return deletes - """ - return self._deletes - @writes.setter - def writes(self, value): + def writes(self, value: list[ClientTuple] | None) -> None: """ Set writes """ self._writes = value + @property + def deletes(self) -> list[ClientTuple] | None: + """ + Return deletes + """ + return self._deletes + @deletes.setter - def deletes(self, value): + def deletes(self, value: list[ClientTuple] | None) -> None: """ Set deletes """ self._deletes = value @property - def writes_tuple_keys(self): + def writes_tuple_keys(self) -> WriteRequestWrites | None: """ Return the writes as tuple keys """ - keys = convert_tuple_keys(self.writes) + if self._writes is None: + return None + + keys = convert_tuple_keys(self._writes) + if keys is None: return None + return WriteRequestWrites(tuple_keys=keys) @property - def deletes_tuple_keys(self): + def deletes_tuple_keys(self) -> WriteRequestDeletes | None: """ Return the delete as tuple keys """ - keys = convert_tuple_keys(self.deletes) + if self._deletes is None: + return None + + keys = convert_tuple_keys(self._deletes) + if keys is None: return None + return WriteRequestDeletes(tuple_keys=keys) diff --git a/config/clients/python/template/src/client/models/write_response.py.mustache b/config/clients/python/template/src/client/models/write_response.py.mustache index 7302bf17..bf3eef9c 100644 --- a/config/clients/python/template/src/client/models/write_response.py.mustache +++ b/config/clients/python/template/src/client/models/write_response.py.mustache @@ -8,20 +8,38 @@ class ClientWriteResponse: ClientWriteResponse returns the set of responses and their statuses """ - def __init__(self, writes: list[ClientWriteSingleResponse], deletes: list[ClientWriteSingleResponse]): + def __init__( + self, + writes: list[ClientWriteSingleResponse] | None = None, + deletes: list[ClientWriteSingleResponse] | None = None, + ) -> None: self._writes = writes self._deletes = deletes @property - def writes(self): + def writes(self) -> list[ClientWriteSingleResponse] | None: """ Return the writes response """ return self._writes + @writes.setter + def writes(self, value: list[ClientWriteSingleResponse] | None) -> None: + """ + Set the writes response + """ + self._writes = value + @property def deletes(self): """ Return the delete response """ return self._deletes + + @deletes.setter + def deletes(self, value: list[ClientWriteSingleResponse] | None) -> None: + """ + Set the delete response + """ + self._deletes = value diff --git a/config/clients/python/template/src/client/models/write_single_response.py.mustache b/config/clients/python/template/src/client/models/write_single_response.py.mustache index dcf76956..69c42c3f 100644 --- a/config/clients/python/template/src/client/models/write_single_response.py.mustache +++ b/config/clients/python/template/src/client/models/write_single_response.py.mustache @@ -3,19 +3,17 @@ from {{packageName}}.client.models.tuple import ClientTuple -def construct_write_single_response(tuple_key: ClientTuple, success: bool, error: Exception=None): - """ - Helper function to return a single write response - """ - return ClientWriteSingleResponse(tuple_key, success, error) - - class ClientWriteSingleResponse: """ ClientWriteSingleResponse encapsulates the response of a single write """ - def __init__(self, tuple_key: ClientTuple, success: bool, error: Exception=None): + def __init__( + self, + tuple_key: ClientTuple, + success: bool, + error: Exception | None = None, + ) -> None: self._tuple_key = tuple_key self._success = success self._error = error @@ -24,22 +22,54 @@ class ClientWriteSingleResponse: return self.tuple_key == other.tuple_key and self.success == other.success and self.error == other.error @property - def tuple_key(self): + def tuple_key(self) -> ClientTuple: """ Return tuple_key """ return self._tuple_key + @tuple_key.setter + def tuple_key(self, value: ClientTuple) -> None: + """ + Set tuple_key + """ + self._tuple_key = value + @property - def success(self): + def success(self) -> bool: """ Return success """ return self._success + @success.setter + def success(self, value: bool) -> None: + """ + Set success + """ + self._success = value + @property - def error(self): + def error(self) -> Exception | None: """ Return error """ return self._error + + @error.setter + def error(self, value: Exception | None) -> None: + """ + Set error + """ + self._error = value + + +def construct_write_single_response( + tuple_key: ClientTuple, + success: bool, + error: Exception | None = None, +) -> ClientWriteSingleResponse: + """ + Helper function to return a single write response + """ + return ClientWriteSingleResponse(tuple_key, success, error) diff --git a/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache b/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache index b74f493b..6c630a8b 100644 --- a/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache +++ b/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache @@ -5,48 +5,62 @@ class WriteTransactionOpts: OpenFGA client write transaction info """ - def __init__(self, disabled: bool=False, max_per_chunk: int=1, max_parallel_requests: int=10): + def __init__( + self, + disabled: bool = False, + max_per_chunk: int = 1, + max_parallel_requests: int = 10, + ) -> None: self._disabled = disabled self._max_per_chunk = max_per_chunk self._max_parallel_requests = max_parallel_requests @property - def disabled(self): + def disabled(self) -> bool: """ Return disabled """ return self._disabled - @property - def max_per_chunk(self): + @disabled.setter + def disabled( + self, + value: bool, + ) -> None: """ - Return max per chunk + Set disabled """ - return self._max_per_chunk + self._disabled = value @property - def max_parallel_requests(self): - """ - Return max parallel requests - """ - return self._max_parallel_requests - - @disabled.setter - def disabled(self, value): + def max_per_chunk(self) -> int: """ - Set disabled + Return max per chunk """ - self._disabled = value + return self._max_per_chunk @max_per_chunk.setter - def max_per_chunk(self, value): + def max_per_chunk( + self, + value: int, + ) -> None: """ Set max_per_chunk """ self._max_per_chunk = value + @property + def max_parallel_requests(self) -> int: + """ + Return max parallel requests + """ + return self._max_parallel_requests + @max_parallel_requests.setter - def max_parallel_requests(self, value): + def max_parallel_requests( + self, + value: int, + ) -> None: """ Set max_parallel_requests """ diff --git a/config/clients/python/template/src/configuration.py.mustache b/config/clients/python/template/src/configuration.py.mustache index c8d4e993..d4a1d3cf 100644 --- a/config/clients/python/template/src/configuration.py.mustache +++ b/config/clients/python/template/src/configuration.py.mustache @@ -34,7 +34,8 @@ class RetryParams: :param max_retry: Maximum number of retry :param min_wait_in_ms: Minimum wait (in ms) between retry """ - def __init__(self, max_retry={{{defaultMaxRetry}}}, min_wait_in_ms={{{defaultMinWaitInMs}}}): + + def __init__(self, max_retry=3, min_wait_in_ms=100): self._max_retry = max_retry self._min_wait_in_ms = min_wait_in_ms @@ -43,8 +44,10 @@ class RetryParams: """ Return the maximum number of retry """ - if self._max_retry > {{{retryMaxAllowedNumber}}}: - raise FgaValidationException("RetryParams.max_retry exceeds maximum allowed limit of {{retryMaxAllowedNumber}}"); + if self._max_retry > 15: + raise FgaValidationException( + "RetryParams.max_retry exceeds maximum allowed limit of 15" + ) return self._max_retry @@ -54,10 +57,14 @@ class RetryParams: Update the maximum number of retry """ if not isinstance(value, int) or value < 0: - raise FgaValidationException("RetryParams.max_retry must be an integer greater than or equal to 0"); + raise FgaValidationException( + "RetryParams.max_retry must be an integer greater than or equal to 0" + ) - if value > {{{retryMaxAllowedNumber}}}: - raise FgaValidationException("RetryParams.max_retry exceeds maximum allowed limit of {{retryMaxAllowedNumber}}"); + if value > 15: + raise FgaValidationException( + "RetryParams.max_retry exceeds maximum allowed limit of 15" + ) self._max_retry = value @@ -133,23 +140,41 @@ class Configuration: _default = None - def __init__(self, api_scheme="https", api_host=None, - store_id=None, - credentials=None, - retry_params=None, - api_key=None, api_key_prefix=None, - username=None, password=None, - discard_unknown_keys=False, - server_index=None, server_variables=None, - server_operation_index=None, server_operation_variables=None, - ssl_ca_cert=None, - api_url=None, # TODO: restructure when removing api_scheme/api_host - telemetry: dict[TelemetryConfigurationType | str, TelemetryMetricsConfiguration | dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | dict[TelemetryAttribute, bool] | None] | None] | None = None, - timeout_millisec: int | None = None, + def __init__( + self, + api_scheme="https", + api_host=None, + store_id=None, + credentials=None, + retry_params=None, + api_key=None, + api_key_prefix=None, + username=None, + password=None, + discard_unknown_keys=False, + server_index=None, + server_variables=None, + server_operation_index=None, + server_operation_variables=None, + ssl_ca_cert=None, + api_url=None, # TODO: restructure when removing api_scheme/api_host + telemetry: ( + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None, + ] + | None + ) = None, + timeout_millisec: int | None = None, ): - - """Constructor - """ + """Constructor""" self._url = api_url self._scheme = api_scheme self._base_path = api_host @@ -199,9 +224,9 @@ class Configuration: self.logger = {} """Logging Settings """ - self.logger["package_logger"] = logging.getLogger("{{packageName}}") + self.logger["package_logger"] = logging.getLogger("openfga_sdk") self.logger["urllib3_logger"] = logging.getLogger("urllib3") - self.logger_format = '%(asctime)s %(levelname)s %(message)s' + self.logger_format = "%(asctime)s %(levelname)s %(message)s" """Log format """ self.logger_stream_handler = None @@ -235,21 +260,10 @@ class Configuration: """Set this to True/False to enable/disable SSL hostname verification. """ - {{#asyncio}} self.connection_pool_maxsize = 100 """This value is passed to the aiohttp to limit simultaneous connections. Default values is 100, None means no-limit. """ - {{/asyncio}} - {{^asyncio}} - self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 - """urllib3 connection pool's maximum number of connections saved - per pool. urllib3 uses 1 connection as default value, but this is - not the best value when you are making a lot of possibly parallel - requests to the same host, which is often the case here. - cpu_count * 5 is used as default value to increase performance. - """ - {{/asyncio}} self.proxy = None """Proxy URL @@ -257,7 +271,7 @@ class Configuration: self.proxy_headers = None """Proxy headers """ - self.safe_chars_for_path_param = '' + self.safe_chars_for_path_param = "" """Safe chars for path_param """ self.retries = None @@ -272,7 +286,7 @@ class Configuration: self._telemetry: TelemetryConfiguration | None = None if telemetry is None: - self._telemetry = TelemetryConfiguration( + self._telemetry = TelemetryConfiguration( TelemetryConfiguration.getSdkDefaults() ) elif isinstance(telemetry, dict): @@ -288,7 +302,7 @@ class Configuration: result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): - if k not in ('logger', 'logger_file_handler'): + if k not in ("logger", "logger_file_handler"): setattr(result, k, copy.deepcopy(v, memo)) # shallow copy of loggers result.logger = copy.copy(self.logger) @@ -416,13 +430,14 @@ class Configuration: def telemetry( self, value: ( - dict[ + TelemetryConfiguration + | dict[ TelemetryConfigurationType | str, TelemetryMetricsConfiguration | dict[ - TelemetryHistogram | TelemetryCounter, + TelemetryHistogram | TelemetryCounter | str, TelemetryMetricConfiguration - | dict[TelemetryAttribute, bool] + | dict[TelemetryAttribute | str, bool] | None, ] | None, @@ -432,16 +447,16 @@ class Configuration: ) -> None: """Set the telemetry configuration""" if value is not None: + if isinstance(value, TelemetryConfiguration): + self._telemetry = value + return + if isinstance(value, dict): self._telemetry = TelemetryConfiguration(value) return - elif isinstance(value, TelemetryConfiguration): - self._telemetry = value - return self._telemetry = None - def get_api_key_with_prefix(self, identifier, alias=None): """Gets API key (with prefix if set). @@ -451,7 +466,9 @@ class Configuration: """ if self.refresh_api_key_hook is not None: self.refresh_api_key_hook(self) - key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None) + key = self.api_key.get( + identifier, self.api_key.get(alias) if alias is not None else None + ) if key: prefix = self.api_key_prefix.get(identifier) if prefix: @@ -470,9 +487,9 @@ class Configuration: password = "" if self.password is not None: password = self.password - return urllib3.util.make_headers( - basic_auth=username + ':' + password - ).get('authorization') + return urllib3.util.make_headers(basic_auth=username + ":" + password).get( + "authorization" + ) def auth_settings(self): """Gets Auth Settings dict for api client. @@ -487,12 +504,13 @@ class Configuration: :return: The report for debugging. """ - return "Python SDK Debug Report:\n"\ - "OS: {env}\n"\ - "Python Version: {pyversion}\n"\ - "Version of the API: {{version}}\n"\ - "SDK Package Version: {{packageVersion}}".\ - format(env=sys.platform, pyversion=sys.version) + return ( + "Python SDK Debug Report:\n" + "OS: {env}\n" + "Python Version: {pyversion}\n" + "Version of the API: 1.x\n" + "SDK Package Version: 0.9.1".format(env=sys.platform, pyversion=sys.version) + ) def get_host_settings(self): """Gets an array of host settings @@ -500,33 +518,10 @@ class Configuration: :return: An array of host settings """ return [ - {{#servers}} { - 'url': "{{{url}}}", - 'description': "{{{description}}}{{^description}}No description provided{{/description}}", - {{#variables}} - {{#-first}} - 'variables': { - {{/-first}} - '{{{name}}}': { - 'description': "{{{description}}}{{^description}}No description provided{{/description}}", - 'default_value': "{{{defaultValue}}}", - {{#enumValues}} - {{#-first}} - 'enum_values': [ - {{/-first}} - "{{{.}}}"{{^-last}},{{/-last}} - {{#-last}} - ] - {{/-last}} - {{/enumValues}} - }{{^-last}},{{/-last}} - {{#-last}} - } - {{/-last}} - {{/variables}} - }{{^-last}},{{/-last}} - {{/servers}} + "url": "", + "description": "No description provided", + } ] def get_host_from_settings(self, index, variables=None, servers=None): @@ -547,22 +542,22 @@ class Configuration: except IndexError: raise ValueError( "Invalid index {} when selecting the host settings. " - "Must be less than {}".format(index, len(servers))) + "Must be less than {}".format(index, len(servers)) + ) - url = server['url'] + url = server["url"] # go through variables and replace placeholders - for variable_name, variable in server.get('variables', {}).items(): - used_value = variables.get( - variable_name, variable['default_value']) + for variable_name, variable in server.get("variables", {}).items(): + used_value = variables.get(variable_name, variable["default_value"]) - if 'enum_values' in variable \ - and used_value not in variable['enum_values']: + if "enum_values" in variable and used_value not in variable["enum_values"]: raise ValueError( "The variable `{}` in the host URL has invalid value " "{}. Must be {}.".format( - variable_name, variables[variable_name], - variable['enum_values'])) + variable_name, variables[variable_name], variable["enum_values"] + ) + ) url = url.replace("{" + variable_name + "}", used_value) @@ -575,48 +570,64 @@ class Configuration: """ combined_url = self.api_url if self.api_url is None: - if self.api_host is None or self.api_host == '': - raise FgaValidationException('api_host is required but not configured.') - if self.api_scheme is None or self.api_scheme == '': - raise FgaValidationException('api_scheme is required but not configured.') - combined_url = self.api_scheme + '://' + self.api_host + if self.api_host is None or self.api_host == "": + raise FgaValidationException("api_host is required but not configured.") + if self.api_scheme is None or self.api_scheme == "": + raise FgaValidationException( + "api_scheme is required but not configured." + ) + combined_url = self.api_scheme + "://" + self.api_host parsed_url = None try: parsed_url = urllib.parse.urlparse(combined_url) except ValueError: if self.api_url is None: - raise ApiValueError('Either api_scheme `{}` or api_host `{}` is invalid'.format( - self.api_scheme, self.api_host)) + raise ApiValueError( + "Either api_scheme `{}` or api_host `{}` is invalid".format( + self.api_scheme, self.api_host + ) + ) else: raise ApiValueError(f"api_url `{self.api_url}` is invalid") if self.api_url is None: - if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'): + if parsed_url.scheme != "http" and parsed_url.scheme != "https": raise ApiValueError( - f'api_scheme `{self.api_scheme}` must be either `http` or `https`') - if (parsed_url.netloc == ''): - raise ApiValueError(f'api_host `{self.api_host}` is invalid') - if (parsed_url.path != ''): + f"api_scheme `{self.api_scheme}` must be either `http` or `https`" + ) + if parsed_url.netloc == "": + raise ApiValueError(f"api_host `{self.api_host}` is invalid") + if parsed_url.path != "": raise ApiValueError( - f'api_host `{self.api_scheme}` is not expected to have path specified') - if (parsed_url.query != ''): + f"api_host `{self.api_scheme}` is not expected to have path specified" + ) + if parsed_url.query != "": raise ApiValueError( - f'api_host `{self.api_scheme}` is not expected to have query specified') - - if self.store_id is not None and self.store_id != "" and is_well_formed_ulid_string(self.store_id) is False: + f"api_host `{self.api_scheme}` is not expected to have query specified" + ) + + if ( + self.store_id is not None + and self.store_id != "" + and is_well_formed_ulid_string(self.store_id) is False + ): raise FgaValidationException( - "store_id ('%s') is not in a valid ulid format" % self.store_id) + "store_id ('%s') is not in a valid ulid format" % self.store_id + ) if self._credentials is not None: self._credentials.validate_credentials_config() if self._timeout_millisec is not None: if not isinstance(self._timeout_millisec, int): - raise FgaValidationException(f"timeout_millisec unexpected type {self._timeout_millisec}") + raise FgaValidationException( + f"timeout_millisec unexpected type {self._timeout_millisec}" + ) ten_minutes = 10000 * 60 if self._timeout_millisec < 0 or self._timeout_millisec > ten_minutes: - raise FgaValidationException(f"timeout_millisec not within reasonable range (0,60000), {self._timeout_millisec}") - + raise FgaValidationException( + f"timeout_millisec not within reasonable range (0,60000), {self._timeout_millisec}" + ) @property def api_scheme(self): diff --git a/config/clients/python/template/src/help.py.mustache b/config/clients/python/template/src/help.py.mustache index 659398f4..73ac80c3 100644 --- a/config/clients/python/template/src/help.py.mustache +++ b/config/clients/python/template/src/help.py.mustache @@ -3,89 +3,104 @@ import json import platform import sys -from collections import OrderedDict - -import opentelemetry.version from . import __version__ as openfga_sdk_version -try: - import urllib3 +def get_urllib3_version() -> str: + try: + import urllib3 + + return urllib3.__version__ + except ModuleNotFoundError: + return "" + + +def get_dateutil_version() -> str: + try: + import dateutil # type: ignore[import-untyped] + version = dateutil.__version__ + + if type(version) is not str: + try: + version = str(version) + except Exception: + pass + + if type(version) is str: + return version - urllib3_version = urllib3.__version__ -except ModuleNotFoundError: - urllib3_version = "" + except ModuleNotFoundError: + pass -try: - import dateutil + return "" - dateutil_version = dateutil.__version__ -except ModuleNotFoundError: - dateutil_version = "" -try: - import aiohttp +def get_aiohttp_version() -> str: + try: + import aiohttp + + return aiohttp.__version__ + except ModuleNotFoundError: + return "" - aiohttp_version = aiohttp.__version__ -except ModuleNotFoundError: - aiohttp_version = "" -try: - import opentelemetry +def get_opentelemetry_version() -> str: + try: + import opentelemetry.version - opentelemetry_version = opentelemetry.version.__version__ -except ModuleNotFoundError: - opentelemetry_version = "" + return opentelemetry.version.__version__ + except ModuleNotFoundError: + return "" -def info() -> dict[str, dict[str, str]]: +def info() -> dict[str, str | dict[str, str] | dict[str, dict[str, str]]]: """ Generate information for a bug report. Based on the requests package help utility module. """ + platform_info: dict[str, str] = {"system": "Unknown", "release": "Unknown"} + implementation_version: str = "Unknown" + try: - platform_info = { - "system": platform.system(), - "release": platform.release(), - } - except OSError: - platform_info = {"system": "Unknown", "release": "Unknown"} + platform_info["system"] = platform.system() + platform_info["release"] = platform.release() + except Exception: + pass - implementation = platform.python_implementation() + implementation: str = platform.python_implementation() if implementation == "CPython": implementation_version = platform.python_version() - elif implementation == "PyPy": + + if implementation == "PyPy": pypy_version_info = sys.pypy_version_info # type: ignore[attr-defined] + implementation_version = ( f"{pypy_version_info.major}." f"{pypy_version_info.minor}." f"{pypy_version_info.micro}" ) + if pypy_version_info.releaselevel != "final": implementation_version = "".join( [implementation_version, pypy_version_info.releaselevel] ) - else: - implementation_version = "Unknown" - - return OrderedDict( - { - "platform": platform_info, - "implementation": { - "name": implementation, - "version": implementation_version, - }, - "openfga_sdk": {"version": openfga_sdk_version}, - "dependencies": { - "urllib3": {"version": urllib3_version}, - "python-dateutil": {"version": dateutil_version}, - "aiohttp": {"version": aiohttp_version}, - "opentelemetry": {"version": opentelemetry_version}, - }, - } - ) + + return { + "platform": platform_info, + "implementation": { + "name": implementation, + "version": implementation_version, + }, + "openfga_sdk": {"version": openfga_sdk_version}, + "dependencies": { + "urllib3": {"version": get_urllib3_version()}, + "python-dateutil": {"version": get_dateutil_version()}, + "aiohttp": {"version": get_aiohttp_version()}, + "opentelemetry": {"version": get_opentelemetry_version()}, + }, + } def main() -> None: diff --git a/config/clients/python/template/src/rest.py.mustache b/config/clients/python/template/src/rest.py.mustache index ca6924a7..af2e8516 100644 --- a/config/clients/python/template/src/rest.py.mustache +++ b/config/clients/python/template/src/rest.py.mustache @@ -6,7 +6,7 @@ import logging import re import ssl import urllib -from typing import Any, List, Optional, Tuple +from typing import Any import aiohttp @@ -26,28 +26,37 @@ logger = logging.getLogger(__name__) class RESTResponse(io.IOBase): """ - Represents an HTTP response object. + Represents an HTTP response object in the asynchronous client. """ - def __init__(self, resp: aiohttp.ClientResponse, data: bytes) -> None: + response: aiohttp.ClientResponse + status: int + reason: str | None + data: bytes + + def __init__( + self, + resp: aiohttp.ClientResponse, + data: bytes, + ) -> None: """ - Initializes a RESTResponse with an aiohttp response and corresponding data. + Initializes a RESTResponse with an aiohttp.ClientResponse and corresponding data. :param resp: The aiohttp.ClientResponse object. :param data: The raw byte data read from the response. """ - self.aiohttp_response = resp + self.response = resp self.status = resp.status self.reason = resp.reason self.data = data - def getheaders(self) -> aiohttp.typedefs.LooseHeaders: + def getheaders(self) -> dict[str, str]: """ Returns the response headers. """ - return self.aiohttp_response.headers + return dict(self.response.headers) - def getheader(self, name: str, default: Optional[str] = None) -> Optional[str]: + def getheader(self, name: str, default: str | None = None) -> str | None: """ Returns a specific header value by name. @@ -55,7 +64,7 @@ class RESTResponse(io.IOBase): :param default: The default value if header is not found. :return: The header value, or default if not present. """ - return self.aiohttp_response.headers.get(name, default) + return self.response.headers.get(name, default) class RESTClientObject: @@ -64,7 +73,7 @@ class RESTClientObject: """ def __init__( - self, configuration: Any, pools_size: int = 4, maxsize: Optional[int] = None + self, configuration: Any, pools_size: int = 4, maxsize: int | None = None ) -> None: """ Creates a new RESTClientObject. @@ -92,22 +101,22 @@ class RESTClientObject: self._timeout_millisec = configuration.timeout_millisec self.pool_manager = aiohttp.ClientSession(connector=connector, trust_env=True) - {{#asyncio}}async {{/asyncio}}def close(self) -> None: + async def close(self) -> None: """ Closes the underlying aiohttp.ClientSession. """ - {{#asyncio}}await {{/asyncio}}self.pool_manager.close() + await self.pool_manager.close() - {{#asyncio}}async {{/asyncio}}def build_request( + async def build_request( self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[List[Tuple[str, Any]]] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: list[tuple[str, Any]] | None = None, _preload_content: bool = True, - _request_timeout: Optional[float] = None, + _request_timeout: float | None = None, ) -> dict: """ Builds a dictionary of request arguments suitable for aiohttp. @@ -150,7 +159,8 @@ class RESTClientObject: args["proxy_headers"] = self.proxy_headers if query_params: - args["url"] += "?" + urllib.parse.urlencode(query_params) + encoded_qs = urllib.parse.urlencode(query_params) + args["url"] = f"{url}?{encoded_qs}" if method in ["POST", "PUT", "PATCH", "OPTIONS", "DELETE"]: if re.search("json", headers["Content-Type"], re.IGNORECASE): @@ -180,7 +190,7 @@ class RESTClientObject: return args - {{#asyncio}}async {{/asyncio}}def handle_response_exception( + async def handle_response_exception( self, response: RESTResponse | aiohttp.ClientResponse ) -> None: """ @@ -216,7 +226,7 @@ class RESTClientObject: def _accumulate_json_lines( self, leftover: bytes, data: bytes, buffer: bytearray - ) -> Tuple[bytes, List[Any]]: + ) -> tuple[bytes, list[Any]]: """ Processes a chunk of data and leftover bytes. Splits on newlines, decodes valid JSON, and returns leftover bytes and a list of decoded JSON objects. @@ -226,7 +236,7 @@ class RESTClientObject: :param buffer: The main bytearray buffer for all data. :return: Updated leftover bytes and a list of decoded JSON objects. """ - objects: List[Any] = [] + objects: list[Any] = [] leftover += data lines = leftover.split( b"\n" @@ -241,15 +251,15 @@ class RESTClientObject: logger.warning("Skipping invalid JSON segment: %s", e) return leftover, objects - {{#asyncio}}async {{/asyncio}}def stream( + async def stream( self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[List[Tuple[str, Any]]] = None, - _request_timeout: Optional[float] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: list[tuple[str, Any]] | None = None, + _request_timeout: float | None = None, ): """ Streams JSON objects from a specified endpoint, handling partial chunks @@ -266,7 +276,7 @@ class RESTClientObject: """ # Build our request payload - args = {{#asyncio}}await {{/asyncio}}self.build_request( + args = await self.build_request( method, url, query_params=query_params, @@ -280,15 +290,15 @@ class RESTClientObject: # Initialize buffers for data chunks buffer = bytearray() leftover = b"" - response: Optional[aiohttp.ClientResponse] = None + response: aiohttp.ClientResponse | None = None try: # Send request, collect response handler - {{#asyncio}}async {{/asyncio}}with self.pool_manager.request(**args) as resp: + async with self.pool_manager.request(**args) as resp: response = resp try: # Iterate over streamed/chunked response data - {{#asyncio}}async {{/asyncio}}for data, _ in resp.content.iter_chunks(): + async for data, _ in resp.content.iter_chunks(): if data: # Process data chunk leftover, decoded_objects = self._accumulate_json_lines( @@ -321,27 +331,27 @@ class RESTClientObject: # Decode the complete/buffered data for logging purposes if isinstance(response, aiohttp.ClientResponse): - response.data = buffer.decode("utf-8") + logger.debug("response body: %s", buffer.decode("utf-8")) # Handle any HTTP errors that may have occurred - {{#asyncio}}await {{/asyncio}}self.handle_response_exception(response) + await self.handle_response_exception(response) # Release the response object (required!) response.release() # Release the connection back to the pool - {{#asyncio}}await {{/asyncio}}self.close() + await self.close() - {{#asyncio}}async {{/asyncio}}def request( + async def request( self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[List[Tuple[str, Any]]] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: list[tuple[str, Any]] | None = None, _preload_content: bool = True, - _request_timeout: Optional[float] = None, + _request_timeout: float | None = None, ) -> RESTResponse | aiohttp.ClientResponse: """ Executes a request and returns the response object. @@ -358,7 +368,7 @@ class RESTClientObject: """ # Build our request payload - args = {{#asyncio}}await {{/asyncio}}self.build_request( + args = await self.build_request( method, url, query_params=query_params, @@ -370,20 +380,21 @@ class RESTClientObject: ) # Send request, collect response handler - resp = {{#asyncio}}await {{/asyncio}}self.pool_manager.request(**args) + wrapped_response: RESTResponse | None = None + raw_response: aiohttp.ClientResponse = await self.pool_manager.request(**args) # If we want to preload the response, read it if _preload_content: # Collect response data - data = {{#asyncio}}await {{/asyncio}}resp.read() + data = await raw_response.read() # Transform response JSON data into RESTResponse object - resp = RESTResponse(resp, data) + wrapped_response = RESTResponse(raw_response, data) # Log the response body - logger.debug(f"response body: {resp.data}") + logger.debug("response body: %s", data.decode("utf-8")) # Handle any errors that may have occurred - {{#asyncio}}await {{/asyncio}}self.handle_response_exception(resp) + await self.handle_response_exception(raw_response) - return resp + return wrapped_response or raw_response diff --git a/config/clients/python/template/src/sync/api.py.mustache b/config/clients/python/template/src/sync/api.py.mustache index 5feedbb6..4b2ef225 100644 --- a/config/clients/python/template/src/sync/api.py.mustache +++ b/config/clients/python/template/src/sync/api.py.mustache @@ -277,7 +277,7 @@ class {{classname}}: response_types_map = {} {{/returnType}} - telemetry_attributes: dict[TelemetryAttribute, str | int] = { + telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] = { TelemetryAttributes.fga_client_request_method: "{{operationId}}", TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), TelemetryAttributes.fga_client_request_model_id: local_var_params.get( diff --git a/config/clients/python/template/src/sync/api_client.py.mustache b/config/clients/python/template/src/sync/api_client.py.mustache index cc455a50..e0a5beb7 100644 --- a/config/clients/python/template/src/sync/api_client.py.mustache +++ b/config/clients/python/template/src/sync/api_client.py.mustache @@ -13,7 +13,7 @@ import tornado.gen {{/tornado}} from multiprocessing.pool import ThreadPool -from dateutil.parser import parse +from dateutil.parser import parse # type: ignore[import-untyped] import {{modelPackage}} from {{packageName}}.sync import rest, oauth2 @@ -31,12 +31,13 @@ from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAt DEFAULT_USER_AGENT = '{{{userAgent}}}' -def random_time(loop_count, min_wait_in_ms): +def random_time(loop_count, min_wait_in_ms) -> float: """ Helper function to return the time (in s) to wait before retry """ minimum = math.ceil(2 ** loop_count * min_wait_in_ms) maximum = math.ceil(2 ** (loop_count + 1) * min_wait_in_ms) + return random.randrange(minimum, maximum) / 1000 @@ -113,11 +114,12 @@ class ApiClient: self._pool.close() self._pool.join() self._pool = None - if hasattr(atexit, 'unregister'): + + if hasattr(atexit, 'unregister') and callable(atexit.unregister): atexit.unregister(self.close) @property - def pool(self): + def pool(self) -> ThreadPool: """Create thread pool on first request avoids instantiating unused threadpool for blocking clients. """ @@ -160,7 +162,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): @@ -497,7 +499,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): """Makes the HTTP request (synchronous) and returns deserialized data. diff --git a/config/clients/python/template/src/sync/client/client.py.mustache b/config/clients/python/template/src/sync/client/client.py.mustache index c1a15f3d..49252e60 100644 --- a/config/clients/python/template/src/sync/client/client.py.mustache +++ b/config/clients/python/template/src/sync/client/client.py.mustache @@ -62,22 +62,31 @@ def _chuck_array(array, max_size): return [array[i * max_size:(i + 1) * max_size] for i in range((len(array) + max_size - 1) // max_size )] -def set_heading_if_not_set(options: dict[str, int|str], name: str, value: str): +def set_heading_if_not_set( + options: dict[str, int | str | dict[str, int | str]] | None, + name: str, + value: str, +) -> dict[str, int | str | dict[str, int | str]]: """ Set heading to the value if it is not set """ - if options is None: - options = {} - headers = options.get("headers") - if headers is None: - headers = {} - if headers.get(name) is None: - headers[name] = value - options["headers"] = headers - return options - - -def options_to_kwargs(options: dict[str, int|str] = None): + _options: dict[str, int | str | dict[str, int | str]] = ( + options if options is not None else {} + ) + + if type(_options.get("headers")) is not dict: + _options["headers"] = {} + + if type(_options["headers"]) is dict: + if type(_options["headers"].get(name)) not in [int, str]: + _options["headers"][name] = value + + return _options + + +def options_to_kwargs( + options: dict[str, int | str | dict[str, int | str]] | None = None, +) -> dict[str, int | str | dict[str, int | str]]: """ Return kwargs with continuation_token and page_size """ @@ -93,7 +102,7 @@ def options_to_kwargs(options: dict[str, int|str] = None): kwargs["_retry_params"] = options["retry_params"] return kwargs -def options_to_transaction_info(options: dict[str, int|str] = None): +def options_to_transaction_info(options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the transaction info """ @@ -113,7 +122,7 @@ class OpenFgaClient: OpenFgaClient is the entry point for invoking calls against the OpenFGA API. """ - def __init__(self, configuration: ClientConfiguration): + def __init__(self, configuration: ClientConfiguration) -> None: self._client_configuration = configuration self._api_client = ApiClient(configuration) self._api = OpenFgaApi(self._api_client) @@ -121,13 +130,16 @@ class OpenFgaClient: def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: self.close() - def close(self): + def close(self) -> None: self._api.close() - def _get_authorization_model_id(self, options: object) -> str | None: + def _get_authorization_model_id( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Return the authorization model ID if specified in the options. Otherwise, return the authorization model ID stored in the client's configuration @@ -142,13 +154,21 @@ class OpenFgaClient: "authorization_model_id ('%s') is not in a valid ulid format" % authorization_model_id) return authorization_model_id - def _get_consistency(self, options: object) -> str | None: + def _get_consistency( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Returns the consistency requested if specified in the options. Otherwise, returns None. """ - if options is not None and "consistency" in options: - return options["consistency"] + consistency: int | str | dict[str, int | str] | None = ( + options.get("consistency", None) if options is not None else None + ) + + if type(consistency) is str: + return consistency + return None def set_store_id(self, value): @@ -179,7 +199,7 @@ class OpenFgaClient: # Stores ################# - def list_stores(self, options: dict[str, int|str] = None): + def list_stores(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ List the stores in the system :param page_size(options) - Number of items returned per request @@ -196,7 +216,7 @@ class OpenFgaClient: ) return api_response - def create_store(self, body: CreateStoreRequest, options: dict[str, int|str] = None): + def create_store(self, body: CreateStoreRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Create the stores in the system :param header(options) - Custom headers to send alongside the request @@ -211,7 +231,7 @@ class OpenFgaClient: ) return api_response - def get_store(self, options: dict[str, int|str] = None): + def get_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Get the store info in the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -225,7 +245,7 @@ class OpenFgaClient: ) return api_response - def delete_store(self, options: dict[str, int|str] = None): + def delete_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Delete the store from the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -243,7 +263,7 @@ class OpenFgaClient: # Authorization Models ####################### - def read_authorization_models(self, options: dict[str, int|str] = None): + def read_authorization_models(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the authorization models for a particular store. :param header(options) - Custom headers to send alongside the request @@ -257,7 +277,7 @@ class OpenFgaClient: ) return api_response - def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int|str] = None): + def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write authorization model. :param body - WriteAuthorizationModelRequest @@ -273,7 +293,7 @@ class OpenFgaClient: ) return api_response - def read_authorization_model(self, options: dict[str, int|str] = None): + def read_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read an authorization model. :param header(options) - Custom headers to send alongside the request @@ -289,7 +309,7 @@ class OpenFgaClient: ) return api_response - def read_latest_authorization_model(self, options: dict[str, int|str] = None): + def read_latest_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method of reading the latest authorization model :param header(options) - Custom headers to send alongside the request @@ -307,7 +327,7 @@ class OpenFgaClient: # Relationship Tuples ####################### - def read_changes(self, body: ClientReadChangesRequest, options: dict[str, str] = None): + def read_changes(self, body: ClientReadChangesRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the type we want to look for change @@ -319,14 +339,19 @@ class OpenFgaClient: :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated """ kwargs = options_to_kwargs(options) - kwargs["type"] = body.type - kwargs["start_time"] = body.start_time + + if body.type is not None: + kwargs["type"] = body.type + + if body.start_time is not None: + kwargs["start_time"] = body.start_time + api_response = self._api.read_changes( **kwargs, ) return api_response - def read(self, body: ReadRequestTupleKey, options: dict[str, str] = None): + def read(self, body: ReadRequestTupleKey, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the tuples we want to read @@ -365,7 +390,7 @@ class OpenFgaClient: ) return api_response - def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, str] = None): + def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): try: write_batch = None delete_batch = None @@ -380,7 +405,7 @@ class OpenFgaClient: except Exception as err: return [construct_write_single_response(i, False, err) for i in batch] - def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, str] = None): + def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Internal function for write/delete batches """ @@ -395,7 +420,7 @@ class OpenFgaClient: return batch_write_responses - def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, str] = None): + def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples """ @@ -424,7 +449,7 @@ class OpenFgaClient: deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - def write(self, body: ClientWriteRequest, options: dict[str, str] = None): + def write(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples :param body - the write request @@ -450,7 +475,7 @@ class OpenFgaClient: deletes_response = self._write_batches(body.deletes, transaction, False, options) return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - def write_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + def write_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for writing tuples :param body - the list of tuples we want to write @@ -463,7 +488,7 @@ class OpenFgaClient: result = self.write(ClientWriteRequest(body, None), options) return result - def delete_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + def delete_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for deleteing tuples :param body - the list of tuples we want to delete @@ -479,7 +504,7 @@ class OpenFgaClient: ####################### # Relationship Queries ####################### - def check(self, body: ClientCheckRequest, options: dict[str, str] = None): + def check(self, body: ClientCheckRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Check whether a user is authorized to access an object :param body - ClientCheckRequest defining check request @@ -512,7 +537,11 @@ class OpenFgaClient: ) return api_response - def _single_client_batch_check(self, body: ClientCheckRequest, options: dict[str, str] = None): + def _single_client_batch_check( + self, + body: ClientCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a single batch request and return body in a SingleBatchCheckResponse :param body - ClientCheckRequest defining check request @@ -526,7 +555,11 @@ class OpenFgaClient: except Exception as err: return ClientBatchCheckClientResponse(allowed=False, request=body, response=None, error=err) - def client_batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): + def client_batch_check( + self, + body: list[ClientCheckRequest], + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a set of checks :param body - list of ClientCheckRequest defining check request @@ -565,7 +598,7 @@ class OpenFgaClient: def _single_batch_check( self, body: BatchCheckRequest, - options: dict[str, str] = None, + options: dict[str, int | str | dict[str, int | str]] | None = None, ): """ Run a single BatchCheck request @@ -580,7 +613,11 @@ class OpenFgaClient: except Exception as err: raise err - def batch_check(self, body: ClientBatchCheckRequest, options=None): + def batch_check( + self, + body: ClientBatchCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a batchcheck request :param body - BatchCheck request @@ -670,7 +707,7 @@ class OpenFgaClient: return ClientBatchCheckResponse(result) - def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): + def expand(self, body: ClientExpandRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run expand request :param body - list of ClientExpandRequest defining expand request @@ -701,7 +738,7 @@ class OpenFgaClient: ) return api_response - def list_objects(self, body: ClientListObjectsRequest, options: dict[str, str] = None): + def list_objects(self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run list object request :param body - list object parameters @@ -733,7 +770,7 @@ class OpenFgaClient: return api_response def streamed_list_objects( - self, body: ClientListObjectsRequest, options: dict[str, str] = None + self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None ): """ Retrieve all objects of the given type that the user has a relation with, using the streaming ListObjects API. @@ -769,7 +806,7 @@ class OpenFgaClient: return - def list_relations(self, body: ClientListRelationsRequest, options: dict[str, str] = None): + def list_relations(self, body: ClientListRelationsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the relations for which user has a relationship with the object :param body - list relation request @@ -791,7 +828,7 @@ class OpenFgaClient: return [i.request.relation for i in result_list] - def list_users(self, body: ClientListUsersRequest, options: dict[str, str] = None): + def list_users(self, body: ClientListUsersRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run list users request :param body - list user parameters @@ -825,7 +862,7 @@ class OpenFgaClient: ####################### # Assertions ####################### - def read_assertions(self, options: dict[str, str] = None): + def read_assertions(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the assertions :param authorization_model_id(options) - Overrides the authorization model id in the configuration @@ -840,7 +877,7 @@ class OpenFgaClient: api_response = self._api.read_assertions(authorization_model_id, **kwargs) return api_response - def write_assertions(self, body: list[ClientAssertion], options: dict[str, str] = None): + def write_assertions(self, body: list[ClientAssertion], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Upsert the assertions :param body - Write assertion request @@ -853,7 +890,7 @@ class OpenFgaClient: kwargs = options_to_kwargs(options) authorization_model_id=self._get_authorization_model_id(options) - def map_to_assertion(client_assertion: ClientAssertion): + def map_to_assertion(client_assertion: ClientAssertion) -> Assertion: return Assertion(TupleKey( user=client_assertion.user, relation=client_assertion.relation, diff --git a/config/clients/python/template/src/sync/rest.py.mustache b/config/clients/python/template/src/sync/rest.py.mustache index 7febe958..8b7eaf71 100644 --- a/config/clients/python/template/src/sync/rest.py.mustache +++ b/config/clients/python/template/src/sync/rest.py.mustache @@ -6,7 +6,7 @@ import logging import re import ssl import urllib -from typing import Any, List, Optional, Tuple +from typing import Any import urllib3 @@ -26,28 +26,37 @@ logger = logging.getLogger(__name__) class RESTResponse(io.IOBase): """ - Represents an HTTP response object in the non-async client. + Represents an HTTP response object in the synchronous client. """ - def __init__(self, resp: urllib3.HTTPResponse, data: bytes) -> None: + response: urllib3.BaseHTTPResponse + status: int + reason: str | None + data: bytes + + def __init__( + self, + resp: urllib3.BaseHTTPResponse, + data: bytes, + ) -> None: """ - Initializes a RESTResponse with a urllib3.HTTPResponse and corresponding data. + Initializes a RESTResponse with a urllib3.BaseHTTPResponse and corresponding data. - :param resp: The urllib3.HTTPResponse object. + :param resp: The urllib3.BaseHTTPResponse object. :param data: The raw byte data read from the response. """ - self.urllib3_response = resp + self.response = resp self.status = resp.status self.reason = resp.reason self.data = data - def getheaders(self) -> dict: + def getheaders(self) -> dict[str, str]: """ Returns a dictionary of the response headers. """ - return self.urllib3_response.headers + return dict(self.response.headers) - def getheader(self, name: str, default: Optional[str] = None) -> Optional[str]: + def getheader(self, name: str, default: str | None = None) -> str | None: """ Returns a specific header value by name. @@ -55,7 +64,7 @@ class RESTResponse(io.IOBase): :param default: The default value if header is not found. :return: The header value, or default if not present. """ - return self.urllib3_response.headers.get(name, default) + return self.response.headers.get(name, default) class RESTClientObject: @@ -64,7 +73,10 @@ class RESTClientObject: """ def __init__( - self, configuration: Any, pools_size: int = 4, maxsize: Optional[int] = None + self, + configuration: Any, + pools_size: int = 4, + maxsize: int | None = None, ) -> None: """ Creates a new RESTClientObject using urllib3. @@ -107,16 +119,18 @@ class RESTClientObject: self._timeout_millisec = configuration.timeout_millisec if hasattr(configuration, "proxy") and configuration.proxy is not None: - self.pool_manager = urllib3.ProxyManager( - num_pools=pools_size, - maxsize=maxsize, - cert_reqs=cert_reqs, - ca_certs=configuration.ssl_ca_cert, - cert_file=configuration.cert_file, - key_file=configuration.key_file, - proxy_url=configuration.proxy, - proxy_headers=configuration.proxy_headers, - **addition_pool_args, + self.pool_manager: urllib3.ProxyManager | urllib3.PoolManager = ( + urllib3.ProxyManager( + num_pools=pools_size, + maxsize=maxsize, + cert_reqs=cert_reqs, + ca_certs=configuration.ssl_ca_cert, + cert_file=configuration.cert_file, + key_file=configuration.key_file, + proxy_url=configuration.proxy, + proxy_headers=configuration.proxy_headers, + **addition_pool_args, + ) ) return @@ -141,12 +155,12 @@ class RESTClientObject: self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[dict] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: dict | None = None, _preload_content: bool = True, - _request_timeout: Optional[float | tuple] = None, + _request_timeout: float | tuple | None = None, ) -> dict: """ Builds a dictionary of request arguments suitable for urllib3. @@ -234,7 +248,7 @@ class RESTClientObject: return args def handle_response_exception( - self, response: RESTResponse | urllib3.HTTPResponse + self, response: RESTResponse | urllib3.BaseHTTPResponse ) -> None: """ Raises exceptions if response status indicates an error. @@ -262,7 +276,7 @@ class RESTClientObject: def _accumulate_json_lines( self, leftover: bytes, data: bytes, buffer: bytearray - ) -> Tuple[bytes, List[Any]]: + ) -> tuple[bytes, list[Any]]: """ Processes a chunk of data plus any leftover bytes from a previous iteration. Splits on newlines, decodes valid JSON lines, and returns updated leftover bytes @@ -273,7 +287,7 @@ class RESTClientObject: :param buffer: The main bytearray buffer for all data in this request. :return: A tuple of (updated leftover bytes, list of decoded objects). """ - objects: List[Any] = [] + objects: list[Any] = [] leftover += data lines = leftover.split( b"\n" @@ -295,11 +309,11 @@ class RESTClientObject: self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[dict] = None, - _request_timeout: Optional[float | tuple] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: dict | None = None, + _request_timeout: float | tuple | None = None, ): """ Streams JSON objects from a specified endpoint, reassembling partial chunks @@ -335,9 +349,9 @@ class RESTClientObject: response = self.pool_manager.request(**args) try: - # Iterate over streamed/chunked response data + # Iterate over streamed/chunked response data for chunk in response.stream(1024): - # Process data chunk + # Process data chunk leftover, decoded_objects = self._accumulate_json_lines( leftover, chunk, buffer ) @@ -376,13 +390,13 @@ class RESTClientObject: self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[dict] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: dict | None = None, _preload_content: bool = True, - _request_timeout: Optional[float | tuple] = None, - ) -> RESTResponse | urllib3.HTTPResponse: + _request_timeout: float | tuple | None = None, + ) -> RESTResponse | urllib3.BaseHTTPResponse: """ Executes a request and returns the response object. @@ -412,20 +426,21 @@ class RESTClientObject: ) # Send request, collect response handler - resp = self.pool_manager.request(**args) + wrapped_response: RESTResponse | None = None + raw_response: urllib3.BaseHTTPResponse = self.pool_manager.request(**args) # If we want to preload the response, read it if _preload_content: # Collect response data and transform response (JSON) into RESTResponse object - resp = RESTResponse(resp, resp.data) + wrapped_response = RESTResponse(raw_response, raw_response.data) # Log the response body - logger.debug("response body: %s", resp.data) + logger.debug("response body: %s", wrapped_response.data.decode("utf-8")) # Handle any errors that may have occurred - self.handle_response_exception(resp) + self.handle_response_exception(raw_response) # Release the connection back to the pool self.close() - return resp + return wrapped_response or raw_response diff --git a/config/clients/python/template/src/telemetry/attributes.py.mustache b/config/clients/python/template/src/telemetry/attributes.py.mustache index ff35b006..5477c461 100644 --- a/config/clients/python/template/src/telemetry/attributes.py.mustache +++ b/config/clients/python/template/src/telemetry/attributes.py.mustache @@ -10,9 +10,6 @@ from urllib3 import HTTPResponse from {{packageName}}.credentials import Credentials from {{packageName}}.exceptions import ApiException from {{packageName}}.rest import RESTResponse -from {{packageName}}.telemetry.utilities import ( - doesInstanceHaveCallable, -) class TelemetryAttribute(NamedTuple): @@ -22,8 +19,7 @@ class TelemetryAttribute(NamedTuple): class TelemetryAttributes: fga_client_request_batch_check_size: TelemetryAttribute = TelemetryAttribute( - name="fga-client.request.batch_check_size", - format="int" + name="fga-client.request.batch_check_size", format="int" ) fga_client_request_client_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.client_id", @@ -94,13 +90,14 @@ class TelemetryAttributes: user_agent_original, ] + @staticmethod + def getAll() -> list[TelemetryAttribute]: + return TelemetryAttributes._attributes + @staticmethod def get( name: str | None = None, - ) -> list[TelemetryAttribute] | TelemetryAttribute | None: - if name is None: - return TelemetryAttributes._attributes - + ) -> TelemetryAttribute | None: for attribute in TelemetryAttributes._attributes: if attribute.name == name: return attribute @@ -109,10 +106,10 @@ class TelemetryAttributes: @staticmethod def prepare( - attributes: dict[TelemetryAttribute, str | int] | None, - filter: list[TelemetryAttribute] | None = None, - ) -> dict[str, str | int]: - response = {} + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + filter: list[TelemetryAttribute] | dict[TelemetryAttribute, bool] | None = None, + ) -> dict[str, str | bool | int | float]: + response: dict[str, str | bool | int | float] = {} if filter is None or filter == []: return response @@ -173,8 +170,11 @@ class TelemetryAttributes: return response @staticmethod - def fromBody(body: Any, attributes: dict[TelemetryAttribute, str | int] = None): - from {{packageName}}.models.batch_check_request import BatchCheckRequest + def fromBody( + body: Any, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ): + from openfga_sdk.models.batch_check_request import BatchCheckRequest if attributes is None: attributes = {} @@ -191,86 +191,104 @@ class TelemetryAttributes: @staticmethod def fromRequest( - user_agent: str = None, - fga_method: str = None, - http_method: str = None, - url: str = None, - resend_count: int = None, - start: float = None, - credentials: Credentials = None, - attributes: dict[TelemetryAttribute, str | int] = None, - ) -> dict[TelemetryAttribute, str | int]: - if attributes is None: - attributes = {} + user_agent: str | None = None, + fga_method: str | None = None, + http_method: str | None = None, + url: str | None = None, + resend_count: int | None = None, + start: float | None = None, + credentials: Credentials | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ) -> dict[TelemetryAttribute, str | bool | int | float]: + _attributes: dict[TelemetryAttribute, str | bool | int | float] = {} + + if attributes is not None: + _attributes = attributes if ( - TelemetryAttributes.fga_client_request_method not in attributes + TelemetryAttributes.fga_client_request_method not in _attributes and fga_method is not None ): fga_method = fga_method.rsplit("/", 1)[-1] if fga_method: - attributes[TelemetryAttributes.fga_client_request_method] = ( + _attributes[TelemetryAttributes.fga_client_request_method] = ( fga_method.rsplit("/", 1)[-1] ) - if TelemetryAttributes.fga_client_request_method in attributes: - fga_method = attributes[TelemetryAttributes.fga_client_request_method] - fga_method = ( - fga_method.lower().replace("_", " ").title().replace(" ", "").strip() - ) + if TelemetryAttributes.fga_client_request_method in _attributes: + _attr_fga_method = _attributes[ + TelemetryAttributes.fga_client_request_method + ] + + if type(_attr_fga_method) is str: + _attr_fga_method = ( + _attr_fga_method.lower() + .replace("_", " ") + .title() + .replace(" ", "") + .strip() + ) - if fga_method: - attributes[TelemetryAttributes.fga_client_request_method] = fga_method - else: - del attributes[TelemetryAttributes.fga_client_request_method] + if _attr_fga_method: + _attributes[TelemetryAttributes.fga_client_request_method] = ( + _attr_fga_method + ) + else: + del _attributes[TelemetryAttributes.fga_client_request_method] if user_agent is not None: - attributes[TelemetryAttributes.user_agent_original] = user_agent + _attributes[TelemetryAttributes.user_agent_original] = user_agent if http_method is not None: - attributes[TelemetryAttributes.http_request_method] = http_method + _attributes[TelemetryAttributes.http_request_method] = http_method if url is not None: - attributes[TelemetryAttributes.http_host] = urllib.parse.urlparse( - url - ).hostname - attributes[TelemetryAttributes.url_scheme] = urllib.parse.urlparse( - url - ).scheme - attributes[TelemetryAttributes.url_full] = url + _hostname = urllib.parse.urlparse(url).hostname + _scheme = urllib.parse.urlparse(url).scheme + + if type(_hostname) is str: + _attributes[TelemetryAttributes.http_host] = _hostname + + if type(_scheme) is str: + _attributes[TelemetryAttributes.url_scheme] = _scheme + + _attributes[TelemetryAttributes.url_full] = url if start is not None and start > 0: - attributes[TelemetryAttributes.http_client_request_duration] = int( + _attributes[TelemetryAttributes.http_client_request_duration] = int( (time.time() - start) * 1000 ) if resend_count is not None and resend_count > 0: - attributes[TelemetryAttributes.http_request_resend_count] = resend_count + _attributes[TelemetryAttributes.http_request_resend_count] = resend_count if credentials is not None: if credentials.method == "client_credentials": - attributes[TelemetryAttributes.fga_client_request_client_id] = ( + _attributes[TelemetryAttributes.fga_client_request_client_id] = ( credentials.configuration.client_id ) - return attributes + return _attributes @staticmethod def fromResponse( - response: HTTPResponse | RESTResponse | ClientResponse | ApiException | None = None, + response: ( + HTTPResponse | RESTResponse | ClientResponse | ApiException | None + ) = None, credentials: Credentials | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, - ) -> dict[TelemetryAttribute, str | int]: + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ) -> dict[TelemetryAttribute, str | bool | int | float]: response_model_id = None response_query_duration = None + _attributes: dict[TelemetryAttribute, str | bool | int | float] = {} - if attributes is None: - attributes = {} + if attributes is not None: + _attributes = attributes if isinstance(response, ApiException): if response.status is not None: - attributes[TelemetryAttributes.http_response_status_code] = int( + _attributes[TelemetryAttributes.http_response_status_code] = int( response.status ) @@ -282,62 +300,67 @@ class TelemetryAttributes: if response is not None: if hasattr(response, "status"): - attributes[TelemetryAttributes.http_response_status_code] = int( + _attributes[TelemetryAttributes.http_response_status_code] = int( response.status ) - if doesInstanceHaveCallable(response, "getheader"): + if hasattr(response, "getheader") and callable(response.getheader): response_model_id = response.getheader("openfga-authorization-model-id") response_query_duration = response.getheader("fga-query-duration-ms") - if doesInstanceHaveCallable(response, "headers"): + if hasattr(response, "headers"): response_model_id = response.headers.get( "openfga-authorization-model-id" ) response_query_duration = response.headers.get("fga-query-duration-ms") if response_model_id is not None: - attributes[TelemetryAttributes.fga_client_response_model_id] = ( + _attributes[TelemetryAttributes.fga_client_response_model_id] = ( response_model_id ) if response_query_duration is not None: - attributes[TelemetryAttributes.http_server_request_duration] = ( + _attributes[TelemetryAttributes.http_server_request_duration] = ( response_query_duration ) if isinstance(credentials, Credentials): if credentials.method == "client_credentials": - attributes[TelemetryAttributes.fga_client_request_client_id] = ( + _attributes[TelemetryAttributes.fga_client_request_client_id] = ( credentials.configuration.client_id ) - return attributes + return _attributes @staticmethod def coalesceAttributeValue( attribute: TelemetryAttribute, - value: int | float | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, - ) -> int | float | None: - if value is None: - if attribute in attributes: - value = attributes[attribute] + value: str | bool | int | float | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ) -> str | bool | int | float | None: + _value: str | bool | int | float | None = None if value is not None: + _value = value + else: + if attributes is not None and attribute in attributes.keys(): + _value = attributes.get(attribute) + + if _value is not None: if attribute.format == "int": try: - value = int(value) - except ValueError: - value = None - - if attribute.format == "float": + return int(_value) + except Exception: + pass + elif attribute.format == "float": try: - value = float(value) - except ValueError: - value = None - - if attribute.format == "string": - value = str(value) + return float(_value) + except Exception: + pass + elif attribute.format == "string": + try: + return str(_value) + except Exception: + pass - return value + return None diff --git a/config/clients/python/template/src/telemetry/configuration.py.mustache b/config/clients/python/template/src/telemetry/configuration.py.mustache index 5cb354e8..0e75500b 100644 --- a/config/clients/python/template/src/telemetry/configuration.py.mustache +++ b/config/clients/python/template/src/telemetry/configuration.py.mustache @@ -1,6 +1,6 @@ {{>partial_header}} -from typing import NamedTuple +from typing import NamedTuple, Protocol, Type, runtime_checkable from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters @@ -18,7 +18,7 @@ class TelemetryMetricConfiguration: def __init__( self, - config: dict[TelemetryAttribute, bool] | None = None, + config: dict[TelemetryAttribute | str, bool] | None = None, fga_client_request_client_id: bool | None = None, fga_client_request_method: bool | None = None, fga_client_request_model_id: bool | None = None, @@ -511,10 +511,14 @@ class TelemetryMetricConfiguration: # Apply an incoming configuration, if provided if isinstance(config, dict): for attribute, enabled in config.items(): - if isinstance(attribute, str): - attribute = TelemetryAttributes.get(name=attribute) + _attribute: TelemetryAttribute | None = None + + if isinstance(attribute, TelemetryAttribute): + _attribute = attribute + elif isinstance(attribute, str): + _attribute = TelemetryAttributes.get(name=attribute) - if not isinstance(attribute, TelemetryAttribute): + if not isinstance(_attribute, TelemetryAttribute): raise ValueError( f"Invalid attribute type provided in `TelemetryMetricConfiguration`; `TelemetryAttribute` expected, but `{type(attribute)}` was provided.", attribute, @@ -526,13 +530,13 @@ class TelemetryMetricConfiguration: attribute, ) - if attribute not in self._state: + if _attribute not in self._state: raise ValueError( - f"Invalid attribute provided in `TelemetryMetricConfiguration`; `{attribute.name}` is not a supported attribute type for this context.", - attribute, + f"Invalid attribute provided in `TelemetryMetricConfiguration`; `{_attribute.name}` is not a supported attribute type for this context.", + _attribute, ) - self._state[attribute] = enabled + self._state[_attribute] = enabled # Reset the validation state self._valid = None @@ -551,11 +555,11 @@ class TelemetryMetricConfiguration: attributes = self._state if filter_enabled is True: - return [ - attribute + return { + attribute: enabled for attribute, enabled in attributes.items() if enabled is True - ] + } return attributes @@ -600,7 +604,7 @@ class TelemetryMetricConfiguration: return self._valid @staticmethod - def getSdkDefaults() -> dict[TelemetryAttribute, bool]: + def getSdkDefaults() -> dict[TelemetryAttribute | str, bool]: """ Get the default SDK configuration for the telemetry metric. @@ -626,15 +630,54 @@ class TelemetryMetricConfiguration: } -class TelemetryMetricsConfiguration: +@runtime_checkable +class TelemetryMetricsConfigurationProtocol(Protocol): + def clear(self) -> None: ... + + def configure( + self, + config: ( + dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None + ) = None, + clear: bool = False, + ) -> None: ... + + def getMetrics(self, filter_enabled: bool = True) -> dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, + ]: ... + + def isEnabled( + self, metric: TelemetryCounter | TelemetryHistogram | None = None + ) -> bool: ... + + def isValid(self, raise_exception: bool = False) -> bool: ... + + +class TelemetryMetricsConfiguration(TelemetryMetricsConfigurationProtocol): _state: dict[ - TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, ] = {} _valid: bool | None = None def __init__( self, - config: dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None] | None = None, + config: ( + dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None + ) = None, fga_client_credentials_request: TelemetryMetricConfiguration | None = None, fga_client_request_duration: TelemetryMetricConfiguration | None = None, fga_client_query_duration: TelemetryMetricConfiguration | None = None, @@ -681,8 +724,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.request` counter. """ + state = self._state[TelemetryCounters.fga_client_request] + + if isinstance(state, TelemetryMetricConfiguration): + return state - return self._state[TelemetryCounters.fga_client_request] + return None @fga_client_request.setter def fga_client_request(self, value: TelemetryMetricConfiguration | None): @@ -702,8 +749,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.credentials.request` counter. """ + state = self._state[TelemetryCounters.fga_client_credentials_request] + + if isinstance(state, TelemetryMetricConfiguration): + return state - return self._state[TelemetryCounters.fga_client_credentials_request] + return None @fga_client_credentials_request.setter def fga_client_credentials_request( @@ -725,8 +776,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.query.duration` histogram. """ + state = self._state[TelemetryHistograms.fga_client_request_duration] - return self._state[TelemetryHistograms.fga_client_request_duration] + if isinstance(state, TelemetryMetricConfiguration): + return state + + return None @fga_client_request_duration.setter def fga_client_request_duration(self, value: TelemetryMetricConfiguration | None): @@ -746,8 +801,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.request.duration` histogram. """ + state = self._state[TelemetryHistograms.fga_client_query_duration] - return self._state[TelemetryHistograms.fga_client_query_duration] + if isinstance(state, TelemetryMetricConfiguration): + return state + + return None @fga_client_query_duration.setter def fga_client_query_duration(self, value: TelemetryMetricConfiguration | None): @@ -774,7 +833,15 @@ class TelemetryMetricsConfiguration: def configure( self, - config: dict[TelemetryHistogram | TelemetryCounter | str, TelemetryMetricConfiguration | dict[TelemetryAttribute, bool] | None] | None = None, + config: ( + dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None + ) = None, clear: bool = False, ) -> None: """ @@ -785,13 +852,19 @@ class TelemetryMetricsConfiguration: if isinstance(config, dict): for metric, configuration in config.items(): - if isinstance(metric, str): - metric = TelemetryCounters.get( - name=metric - ) or TelemetryHistograms.get(name=metric) + _metric: TelemetryHistogram | TelemetryCounter | None = None - if not isinstance(metric, TelemetryCounter) and not isinstance( + if isinstance(metric, TelemetryCounter) or isinstance( metric, TelemetryHistogram + ): + _metric = metric + elif isinstance(metric, str): + _metric = TelemetryCounters.get(metric) or TelemetryHistograms.get( + metric + ) + + if not isinstance(_metric, TelemetryCounter) and not isinstance( + _metric, TelemetryHistogram ): raise ValueError( f"Invalid metric type provided in `TelemetryMetricsConfiguration`; `TelemetryHistogram` or `TelemetryCounter` was expected, but `{type(metric)}` was provided.", @@ -810,20 +883,19 @@ class TelemetryMetricsConfiguration: configuration, ) - if metric not in self._state: + if _metric not in self._state: raise ValueError( - f"Invalid metric provided in `TelemetryMetricsConfiguration`; `{metric.name}` is not a supported metric type for this context.", - metric, + f"Invalid metric provided in `TelemetryMetricsConfiguration`; `{_metric.name}` is not a supported metric type for this context.", + _metric, ) - self._state[metric] = configuration + self._state[_metric] = configuration self._valid = None - def getMetrics( - self, filter_enabled: bool = True - ) -> dict[ - TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None + def getMetrics(self, filter_enabled: bool = True) -> dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, ]: """ Returns a list of supported metrics. If `filter_enabled` is `True`, only enabled metrics are returned. @@ -836,11 +908,12 @@ class TelemetryMetricsConfiguration: metrics = self._state if filter_enabled is True: - return [ - metric + return { + metric: configuration for metric, configuration in metrics.items() - if configuration is not None and configuration.isEnabled() - ] + if isinstance(configuration, TelemetryMetricConfiguration) + and configuration.isEnabled() + } return metrics @@ -859,9 +932,11 @@ class TelemetryMetricsConfiguration: # Check if the specified metric is enabled if metric in self._state: + state = self._state[metric] + if ( - self._state[metric] is not None - and self._state[metric].isEnabled() is True + isinstance(state, TelemetryMetricConfiguration) + and state.isEnabled() is True ): return True @@ -875,27 +950,35 @@ class TelemetryMetricsConfiguration: :return: A boolean indicating whether the metrics configuration is valid, including all sub-configurations. """ - - # Check if the validation state is already cached if self._valid is not None: return self._valid enabled = self.getMetrics(filter_enabled=True) # Validate all sub-configurations and cache the result - self._valid = [configuration.isValid() for configuration in enabled.values()] + for configuration in enabled.values(): + if ( + isinstance(configuration, TelemetryMetricConfiguration) + and not configuration.isValid() + ): + self._valid = False + break # If requested, raise an exception if the configuration is invalid if self._valid is False and raise_exception is True: raise ValueError("Invalid TelemetryMetricsConfiguration.") + if self._valid is None: + self._valid = True + # Return the validation state return self._valid @staticmethod - def getSdkDefaults() -> ( - dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None] - ): + def getSdkDefaults() -> dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, + ]: """ Get the default SDK configuration for telemetry metrics. @@ -910,7 +993,7 @@ class TelemetryMetricsConfiguration: class TelemetryConfigurationType(NamedTuple): name: str - configurationClass: object + configurationClass: Type[TelemetryMetricsConfigurationProtocol] class TelemetryConfigurations: @@ -922,12 +1005,13 @@ class TelemetryConfigurations: _configurations: list[TelemetryConfigurationType] = [metrics] @staticmethod - def get( - name: str | None = None, - ) -> list[TelemetryConfigurationType] | TelemetryConfigurationType | None: - if name is None: - return TelemetryConfigurations._configurations + def getAll() -> list[TelemetryConfigurationType]: + return TelemetryConfigurations._configurations + @staticmethod + def get( + name: str, + ) -> TelemetryConfigurationType | None: for configuration in TelemetryConfigurations._configurations: if configuration.name == name: return configuration @@ -936,12 +1020,25 @@ class TelemetryConfigurations: class TelemetryConfiguration: - _state: dict[str, TelemetryMetricsConfiguration | None] = {} + _state: dict[TelemetryConfigurationType, TelemetryMetricsConfiguration | None] = {} _valid: bool | None = None def __init__( self, - config: dict[str, TelemetryMetricsConfiguration | None] | None = None, + config: ( + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None, + ] + | None + ) = None, metrics: TelemetryMetricsConfiguration | None = None, ): """ @@ -950,7 +1047,6 @@ class TelemetryConfiguration: :param config: A dictionary containing the configuration for telemetry. :param metrics: Customize which metrics and attributes are included in telemetry collection. """ - # Instantiate with default state, and apply the incoming configuration, if one was provided self.configure(config=config, clear=True) @@ -964,7 +1060,6 @@ class TelemetryConfiguration: :return: The metrics configuration for telemetry. """ - return self._state[TelemetryConfigurations.metrics] @metrics.setter @@ -974,7 +1069,6 @@ class TelemetryConfiguration: :param value: The metrics configuration for telemetry. """ - if value is not None and not isinstance(value, TelemetryMetricsConfiguration): raise ValueError( "A `metrics` configuration must be an instance of `TelemetryMetricsConfiguration` or `None`." @@ -994,7 +1088,20 @@ class TelemetryConfiguration: def configure( self, - config: dict[TelemetryConfigurationType | str, TelemetryMetricsConfiguration | dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | dict[TelemetryAttribute, bool] | None] | None] | None = None, + config: ( + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None, + ] + | None + ) = None, clear: bool = False, ) -> None: """ @@ -1005,34 +1112,39 @@ class TelemetryConfiguration: if isinstance(config, dict): for context, configuration in config.items(): - if isinstance(context, str): - context = TelemetryConfigurations.get(context) - - if not isinstance(context, TelemetryConfigurationType): - raise ValueError( - f"Invalid context provided in `TelemetryConfiguration`; a valid string or an `TelemetryConfigurationType` instance was expected, but `{type(context)}` was provided.", - context, - ) - - if isinstance(configuration, dict): - configuration = TelemetryMetricsConfiguration(configuration) - - if ( - not isinstance(configuration, context.configurationClass) - and configuration is not None - ): - raise ValueError( - f"Invalid context configuration provided in `TelemetryConfiguration`; a {type(context.configurationClass)} was expected, but `{type(configuration)}` was provided.", - configuration, - ) - - if context not in self._state: - raise ValueError( - f"Invalid context provided in `TelemetryConfiguration`; `{context.name}` is not a supported context type for this configuration context.", - context, - ) - - self._state[context] = configuration + _context: TelemetryConfigurationType | None = None + + if isinstance(context, TelemetryConfigurationType): + _context = context + elif isinstance(context, str): + _context = TelemetryConfigurations.get(context) + + if not isinstance(_context, TelemetryConfigurationType): + raise ValueError( + f"Invalid context provided in `TelemetryConfiguration`; a valid string or an `TelemetryConfigurationType` instance was expected, but `{context}` was provided.", + context, + ) + + if isinstance(_context, TelemetryConfigurationType): + if isinstance(configuration, dict): + configuration = TelemetryMetricsConfiguration(configuration) + + if ( + not isinstance(configuration, _context.configurationClass) + and configuration is not None + ): + raise ValueError( + f"Invalid context configuration provided in `TelemetryConfiguration`; a {type(_context.configurationClass)} was expected, but `{type(configuration)}` was provided.", + configuration, + ) + + if _context not in self._state: + raise ValueError( + f"Invalid context provided in `TelemetryConfiguration`; `{_context.name}` is not a supported context type for this configuration context.", + _context, + ) + + self._state[_context] = configuration self._valid = None @@ -1046,7 +1158,6 @@ class TelemetryConfiguration: :return: A list of enabled contexts. """ - contexts = self._state if filter_enabled is True: @@ -1066,16 +1177,25 @@ class TelemetryConfiguration: :return: A boolean indicating whether telemetry is enabled. """ + _configuration: TelemetryConfigurationType | None = None - if configuration is None: + if isinstance(configuration, TelemetryConfigurationType): + _configuration = configuration + + if isinstance(configuration, str): + _configuration = TelemetryConfigurations.get(name=configuration) + + if _configuration is None: return True if any(self.getConfigurations(filter_enabled=True)) else False - if configuration in self._state: + if _configuration in self._state: + state = self._state[_configuration] + if ( - self._state[configuration] is not None - or self._state[configuration].isEnabled() is True + isinstance(state, TelemetryMetricsConfiguration) + and state.isEnabled() is True ): - return self._state[configuration].isEnabled() + return True return False @@ -1087,25 +1207,34 @@ class TelemetryConfiguration: :return: A boolean indicating whether the telemetry configuration is valid, including all sub-configurations. """ - if self._valid is not None: return self._valid enabled = self.getConfigurations(filter_enabled=True) - self._valid = all( - [configuration.isValid() for configuration in enabled.values()] - ) + for configuration in enabled.values(): + if configuration is not None and not configuration.isValid(): + self._valid = False + break if self._valid is False and raise_exception is True: raise ValueError("Invalid TelemetryConfiguration.") + if self._valid is None: + self._valid = True + return self._valid @staticmethod - def getSdkDefaults() -> ( - dict[TelemetryConfigurationType, TelemetryMetricsConfiguration | None] - ): + def getSdkDefaults() -> dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, + ] + | None, + ]: """ Get the default SDK configuration for telemetry. @@ -1117,13 +1246,12 @@ class TelemetryConfiguration: def isMetricEnabled( - config: TelemetryConfiguration | TelemetryMetricsConfiguration, + config: TelemetryConfiguration | TelemetryMetricsConfiguration | None, metric: TelemetryCounter | TelemetryHistogram, ) -> bool: """ Check if a particular metric is enabled for telemetry collection. """ - if config is not None and metric is not None: if isinstance(config, TelemetryConfiguration): config = config.metrics diff --git a/config/clients/python/template/src/telemetry/counters.py.mustache b/config/clients/python/template/src/telemetry/counters.py.mustache index 96bbe8a1..e6a19136 100644 --- a/config/clients/python/template/src/telemetry/counters.py.mustache +++ b/config/clients/python/template/src/telemetry/counters.py.mustache @@ -25,13 +25,14 @@ class TelemetryCounters: fga_client_request, ] + @staticmethod + def getAll() -> list[TelemetryCounter]: + return TelemetryCounters._counters + @staticmethod def get( name: str | None = None, - ) -> list[TelemetryCounter] | TelemetryCounter | None: - if name is None: - return TelemetryCounters._counters - + ) -> TelemetryCounter | None: for counter in TelemetryCounters._counters: if counter.name == name: return counter diff --git a/config/clients/python/template/src/telemetry/histograms.py.mustache b/config/clients/python/template/src/telemetry/histograms.py.mustache index 26a1ca78..8db0ac86 100644 --- a/config/clients/python/template/src/telemetry/histograms.py.mustache +++ b/config/clients/python/template/src/telemetry/histograms.py.mustache @@ -24,13 +24,14 @@ class TelemetryHistograms: fga_client_query_duration, ] + @staticmethod + def getAll() -> list[TelemetryHistogram]: + return TelemetryHistograms._histograms + @staticmethod def get( name: str | None = None, - ) -> list[TelemetryHistogram] | TelemetryHistogram | None: - if name is None: - return TelemetryHistograms._histograms - + ) -> TelemetryHistogram | None: for histogram in TelemetryHistograms._histograms: if histogram.name == name: return histogram diff --git a/config/clients/python/template/src/telemetry/metrics.py.mustache b/config/clients/python/template/src/telemetry/metrics.py.mustache index 3aecae99..5b34fb66 100644 --- a/config/clients/python/template/src/telemetry/metrics.py.mustache +++ b/config/clients/python/template/src/telemetry/metrics.py.mustache @@ -8,6 +8,7 @@ from {{packageName}}.telemetry.attributes import ( ) from {{packageName}}.telemetry.configuration import ( TelemetryConfiguration, + TelemetryMetricConfiguration, isMetricEnabled, ) from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters @@ -15,7 +16,7 @@ from {{packageName}}.telemetry.histograms import TelemetryHistogram, TelemetryHi class TelemetryMetrics: - _meter: Meter = None + _meter: Meter | None = None _histograms: dict[str, Histogram] = {} _counters: dict[str, Counter] = {} @@ -66,7 +67,7 @@ class TelemetryMetrics: def request( self, value: int = 1, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Counter: """ @@ -75,20 +76,33 @@ class TelemetryMetrics: counter = self.counter(TelemetryCounters.fga_client_request) if isMetricEnabled(configuration, TelemetryCounters.fga_client_request): - attributes = TelemetryAttributes.prepare( + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_request, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = ( + configuration.metrics.fga_client_request.getAttributes() + ) + + prepared_attributes = TelemetryAttributes.prepare( attributes, - filter=configuration.metrics.fga_client_request.getAttributes(), + filter=attribute_filters, ) - if value is not None: - counter.add(amount=value, attributes=attributes) + counter.add(amount=value, attributes=prepared_attributes) return counter def credentialsRequest( self, value: int = 1, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Counter: """ @@ -99,76 +113,125 @@ class TelemetryMetrics: if isMetricEnabled( configuration, TelemetryCounters.fga_client_credentials_request ): - attributes = TelemetryAttributes.prepare( + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_credentials_request, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = ( + configuration.metrics.fga_client_credentials_request.getAttributes() + ) + + prepared_attributes = TelemetryAttributes.prepare( attributes, - filter=configuration.metrics.fga_client_credentials_request.getAttributes(), + filter=attribute_filters, ) - if value is not None: - counter.add(amount=value, attributes=attributes) + counter.add(amount=value, attributes=prepared_attributes) # type: ignore[arg-type] return counter def requestDuration( self, value: int | float | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Histogram: """ Record the duration of a request made by the client. """ histogram = self.histogram(TelemetryHistograms.fga_client_request_duration) + _attributes: dict[TelemetryAttribute, str | bool | int | float] = ( + attributes or {} + ) if isMetricEnabled( configuration, TelemetryHistograms.fga_client_request_duration ): - attributes[TelemetryAttributes.http_client_request_duration] = value = ( - TelemetryAttributes.coalesceAttributeValue( - TelemetryAttributes.http_client_request_duration, - value, - attributes, - ) + coalesced_value = TelemetryAttributes.coalesceAttributeValue( + TelemetryAttributes.http_client_request_duration, + value, + _attributes, ) - attributes = TelemetryAttributes.prepare( - attributes, - filter=configuration.metrics.fga_client_request_duration.getAttributes(), - ) + if type(coalesced_value) is int or type(coalesced_value) is float: + _attributes[TelemetryAttributes.http_client_request_duration] = ( + value + ) = coalesced_value + + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_request_duration, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = configuration.metrics.fga_client_request_duration.getAttributes() + + prepared_attributes = TelemetryAttributes.prepare( + _attributes, + filter=attribute_filters, + ) - if value is not None: - histogram.record(amount=value, attributes=attributes) + histogram.record(amount=value, attributes=prepared_attributes) # type: ignore[arg-type] return histogram def queryDuration( self, value: int | float | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Histogram: """ Record the duration of a query made by the client, as reported by the server. """ histogram = self.histogram(TelemetryHistograms.fga_client_query_duration) + _attributes: dict[TelemetryAttribute, str | bool | int | float] = ( + attributes or {} + ) if isMetricEnabled( configuration, TelemetryHistograms.fga_client_query_duration ): - attributes[TelemetryAttributes.http_server_request_duration] = value = ( - TelemetryAttributes.coalesceAttributeValue( - TelemetryAttributes.http_server_request_duration, - value, - attributes, - ) + coalesced_value = TelemetryAttributes.coalesceAttributeValue( + TelemetryAttributes.http_server_request_duration, + value, + _attributes, ) - attributes = TelemetryAttributes.prepare( - attributes, - filter=configuration.metrics.fga_client_query_duration.getAttributes(), - ) + if type(coalesced_value) is int or type(coalesced_value) is float: + _attributes[TelemetryAttributes.http_server_request_duration] = ( + value + ) = coalesced_value + + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_query_duration, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = ( + configuration.metrics.fga_client_query_duration.getAttributes() + ) + + prepared_attributes = TelemetryAttributes.prepare( + _attributes, + filter=attribute_filters, + ) - if value is not None: - histogram.record(amount=value, attributes=attributes) + histogram.record(amount=value, attributes=prepared_attributes) # type: ignore[arg-type] return histogram diff --git a/config/clients/python/template/src/telemetry/utilities.py.mustache b/config/clients/python/template/src/telemetry/utilities.py.mustache deleted file mode 100644 index 69204c35..00000000 --- a/config/clients/python/template/src/telemetry/utilities.py.mustache +++ /dev/null @@ -1,9 +0,0 @@ -{{>partial_header}} - -def doesInstanceHaveCallable(instance: object, callableName: str) -> bool: - instanceCallable = getattr(instance, callableName, None) - - if instanceCallable is None: - return False - - return callable(instanceCallable) diff --git a/config/clients/python/template/test-requirements.mustache b/config/clients/python/template/test-requirements.mustache index 27570111..b39447e1 100644 --- a/config/clients/python/template/test-requirements.mustache +++ b/config/clients/python/template/test-requirements.mustache @@ -5,3 +5,4 @@ mock >= 5.1.0, < 6 pytest-asyncio >= 0.25, < 1 pytest-cov >= 5, < 7 ruff >= 0.9, < 1 +mypy >= 1.14.1, < 2 diff --git a/config/clients/python/template/test/rest_test.py.mustache b/config/clients/python/template/test/rest_test.py.mustache index 803a02e9..5fa4c331 100644 --- a/config/clients/python/template/test/rest_test.py.mustache +++ b/config/clients/python/template/test/rest_test.py.mustache @@ -22,12 +22,14 @@ async def test_restresponse_init(): mock_resp = MagicMock() mock_resp.status = 200 mock_resp.reason = "OK" + resp_data = b'{"test":"data"}' rest_resp = RESTResponse(mock_resp, resp_data) + assert rest_resp.status == 200 assert rest_resp.reason == "OK" assert rest_resp.data == resp_data - assert rest_resp.aiohttp_response == mock_resp + assert rest_resp.response == mock_resp def test_restresponse_getheaders(): diff --git a/config/clients/python/template/test/sync/rest_test.py.mustache b/config/clients/python/template/test/sync/rest_test.py.mustache index 92bac941..4f3797de 100644 --- a/config/clients/python/template/test/sync/rest_test.py.mustache +++ b/config/clients/python/template/test/sync/rest_test.py.mustache @@ -21,13 +21,14 @@ def test_restresponse_init(): mock_resp = MagicMock() mock_resp.status = 200 mock_resp.reason = "OK" - resp_data = b'{"test":"data"}' + resp_data = b'{"test":"data"}' rest_resp = RESTResponse(mock_resp, resp_data) + assert rest_resp.status == 200 assert rest_resp.reason == "OK" assert rest_resp.data == resp_data - assert rest_resp.urllib3_response == mock_resp + assert rest_resp.response == mock_resp def test_restresponse_getheaders(): diff --git a/config/clients/python/template/test/telemetry/utilities_test.py.mustache b/config/clients/python/template/test/telemetry/utilities_test.py.mustache deleted file mode 100644 index 1a958a6d..00000000 --- a/config/clients/python/template/test/telemetry/utilities_test.py.mustache +++ /dev/null @@ -1,17 +0,0 @@ -{{>partial_header}} - -from mock import MagicMock - -from {{packageName}}.telemetry.utilities import doesInstanceHaveCallable - - -def test_instance_has_callable(): - mock_instance = MagicMock(spec_set=["some_callable", "some_attribute"]) - mock_instance.some_callable = lambda: "I am callable" - - assert doesInstanceHaveCallable(mock_instance, "some_callable") - - assert not doesInstanceHaveCallable(mock_instance, "missing_callable") - - mock_instance.some_attribute = "not callable" - assert not doesInstanceHaveCallable(mock_instance, "some_attribute") From 072483b6e9e2aba0272e6fa2f446bec8a1d35e89 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 27 Jan 2025 19:22:02 -0600 Subject: [PATCH 3/8] chore(python-sdk): add `ruff` and remove `pyupgrade`, `isort`, `autoflake`, and `black` --- Makefile | 1 - config/clients/python/template/pyproject.toml | 25 ++++--------------- .../test/telemetry/utilities_test.py.mustache | 2 +- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 014ebb9d..034b8215 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,6 @@ build-client-python: 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 ruff check --select I --fix . && \ python -m ruff format . && \ python setup.py sdist bdist_wheel'" diff --git a/config/clients/python/template/pyproject.toml b/config/clients/python/template/pyproject.toml index 52f77385..74966dc4 100644 --- a/config/clients/python/template/pyproject.toml +++ b/config/clients/python/template/pyproject.toml @@ -33,20 +33,8 @@ indent-width = 4 target-version = "py310" -[tool.ruff.lint] -extend-select = [ - #"B", # flake8-bugbear - #"C4", # flake8-comprehensions - #"C9", # mccabe - "I", # isort - #"PGH", # pygrep-hooks - #"RUF", # ruff - #"UP", # pyupgrade - #"W", # pycodestyle - #"YTT", # flake8-2020 - #"TRY", # tryceratops - #"EM", # flake8-errmsg -] +[lint] +select = ["E4", "E7", "E9", "F"] ignore = [] fixable = ["ALL"] @@ -54,11 +42,7 @@ unfixable = [] dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -[tool.ruff.lint.isort] -lines-between-types = 1 -lines-after-imports = 2 - -[tool.ruff.format] +[format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false @@ -66,7 +50,7 @@ line-ending = "auto" [tool.pytest.ini_options] testpaths = [ - "test", + "tests", "integration", ] @@ -74,3 +58,4 @@ addopts = "--cov=openfga_sdk --cov-report term-missing --cov-report xml --cov-re asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" +asyncio_default_test_loop_scope = "function" diff --git a/config/clients/python/template/test/telemetry/utilities_test.py.mustache b/config/clients/python/template/test/telemetry/utilities_test.py.mustache index 79a34fd0..1a958a6d 100644 --- a/config/clients/python/template/test/telemetry/utilities_test.py.mustache +++ b/config/clients/python/template/test/telemetry/utilities_test.py.mustache @@ -1,6 +1,6 @@ {{>partial_header}} -from unittest.mock import MagicMock +from mock import MagicMock from {{packageName}}.telemetry.utilities import doesInstanceHaveCallable From 907e9813e453dc78a1e6b647a37cc1fe07b1c4de Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Wed, 29 Jan 2025 21:23:17 -0600 Subject: [PATCH 4/8] feat(python-sdk): type hinting improvements --- config/clients/python/config.overrides.json | 8 - .../example1/requirements.txt.mustache | 1 + config/clients/python/template/model.mustache | 9 +- config/clients/python/template/pyproject.toml | 17 +- .../python/template/requirements.mustache | 1 + .../python/template/src/api.py.mustache | 2 +- .../template/src/api_client.py.mustache | 6 +- .../template/src/client/client.py.mustache | 138 +++++--- .../src/client/models/assertion.py.mustache | 44 ++- .../models/batch_check_item.py.mustache | 93 +++-- .../models/batch_check_request.py.mustache | 10 +- .../models/batch_check_response.py.mustache | 9 +- .../batch_check_single_response.py.mustache | 58 +-- .../client/models/check_request.py.mustache | 96 ++--- .../client_batch_check_response.py.mustache | 44 ++- .../client/models/expand_request.py.mustache | 16 +- .../models/list_objects_request.py.mustache | 73 ++-- .../models/list_relations_request.py.mustache | 73 ++-- .../models/list_users_request.py.mustache | 81 ++--- .../models/read_changes_request.py.mustache | 30 +- .../src/client/models/tuple.py.mustache | 82 ++--- .../client/models/write_request.py.mustache | 44 ++- .../client/models/write_response.py.mustache | 22 +- .../models/write_single_response.py.mustache | 52 ++- .../models/write_transaction_opts.py.mustache | 50 ++- .../template/src/configuration.py.mustache | 233 +++++++------ .../python/template/src/help.py.mustache | 117 ++++--- .../python/template/src/rest.py.mustache | 107 +++--- .../python/template/src/sync/api.py.mustache | 2 +- .../template/src/sync/api_client.py.mustache | 14 +- .../src/sync/client/client.py.mustache | 141 +++++--- .../python/template/src/sync/rest.py.mustache | 109 +++--- .../src/telemetry/attributes.py.mustache | 185 +++++----- .../src/telemetry/configuration.py.mustache | 330 ++++++++++++------ .../src/telemetry/counters.py.mustache | 9 +- .../src/telemetry/histograms.py.mustache | 9 +- .../src/telemetry/metrics.py.mustache | 137 ++++++-- .../src/telemetry/utilities.py.mustache | 9 - .../template/test-requirements.mustache | 1 + .../template/test/rest_test.py.mustache | 4 +- .../template/test/sync/rest_test.py.mustache | 5 +- .../test/telemetry/utilities_test.py.mustache | 17 - 42 files changed, 1495 insertions(+), 993 deletions(-) delete mode 100644 config/clients/python/template/src/telemetry/utilities.py.mustache delete mode 100644 config/clients/python/template/test/telemetry/utilities_test.py.mustache diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 13e71723..3c20c4de 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -219,10 +219,6 @@ "destinationFilename": "openfga_sdk/telemetry/telemetry.py", "templateType": "SupportingFiles" }, - "src/telemetry/utilities.py.mustache": { - "destinationFilename": "openfga_sdk/telemetry/utilities.py", - "templateType": "SupportingFiles" - }, "src/__init__.py.mustache": { "destinationFilename": "openfga_sdk/__init__.py", "templateType": "SupportingFiles" @@ -320,10 +316,6 @@ "destinationFilename": "test/telemetry/telemetry_test.py", "templateType": "SupportingFiles" }, - "test/telemetry/utilities_test.py.mustache": { - "destinationFilename": "test/telemetry/utilities_test.py", - "templateType": "SupportingFiles" - }, "test/__init__.py.mustache": { "destinationFilename": "test/__init__.py", "templateType": "SupportingFiles" diff --git a/config/clients/python/template/example/example1/requirements.txt.mustache b/config/clients/python/template/example/example1/requirements.txt.mustache index f0a60d32..23c9af82 100644 --- a/config/clients/python/template/example/example1/requirements.txt.mustache +++ b/config/clients/python/template/example/example1/requirements.txt.mustache @@ -9,3 +9,4 @@ python-dateutil >= 2.8.2 urllib3 >= 2.1.0 yarl >= 1.9.4 python-dotenv >= 1, <2 + diff --git a/config/clients/python/template/model.mustache b/config/clients/python/template/model.mustache index 7a01ae67..73469059 100644 --- a/config/clients/python/template/model.mustache +++ b/config/clients/python/template/model.mustache @@ -1,9 +1,6 @@ {{>partial_header}} -try: - from inspect import getfullargspec -except ImportError: - from inspect import getargspec as getfullargspec +from inspect import getfullargspec import pprint from {{packageName}}.configuration import Configuration @@ -37,13 +34,13 @@ class {{classname}}: attribute_map (dict): The key is attribute name and the value is json key in definition. """ - openapi_types = { + openapi_types: dict[str, str] = { {{#vars}} '{{name}}': '{{{dataType}}}'{{^-last}},{{/-last}} {{/vars}} } - attribute_map = { + attribute_map: dict[str, str] = { {{#vars}} '{{name}}': '{{baseName}}'{{^-last}},{{/-last}} {{/vars}} diff --git a/config/clients/python/template/pyproject.toml b/config/clients/python/template/pyproject.toml index 74966dc4..ca06cb1a 100644 --- a/config/clients/python/template/pyproject.toml +++ b/config/clients/python/template/pyproject.toml @@ -33,7 +33,7 @@ indent-width = 4 target-version = "py310" -[lint] +[tool.ruff.lint] select = ["E4", "E7", "E9", "F"] ignore = [] @@ -42,7 +42,7 @@ unfixable = [] dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -[format] +[tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false @@ -50,7 +50,7 @@ line-ending = "auto" [tool.pytest.ini_options] testpaths = [ - "tests", + "test", "integration", ] @@ -58,4 +58,13 @@ addopts = "--cov=openfga_sdk --cov-report term-missing --cov-report xml --cov-re asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" -asyncio_default_test_loop_scope = "function" + +[tool.mypy] +python_version = "3.10" +packages = "openfga_sdk" +exclude = [ + "openfa_sdk/models", +] +#warn_return_any = "True" +#warn_unused_configs = "True" +#disallow_untyped_defs = "True" diff --git a/config/clients/python/template/requirements.mustache b/config/clients/python/template/requirements.mustache index 4eef03dd..8cda1952 100644 --- a/config/clients/python/template/requirements.mustache +++ b/config/clients/python/template/requirements.mustache @@ -4,3 +4,4 @@ setuptools >= 69.1.1 build >= 1.2.1, < 2 urllib3 >= 1.25.11, < 3 opentelemetry-api >= 1.25.0, < 2 +python-dateutil >= 2.9.0, < 3 diff --git a/config/clients/python/template/src/api.py.mustache b/config/clients/python/template/src/api.py.mustache index 7bb4fa76..b9a278c2 100644 --- a/config/clients/python/template/src/api.py.mustache +++ b/config/clients/python/template/src/api.py.mustache @@ -290,7 +290,7 @@ class {{classname}}: response_types_map = {} {{/returnType}} - telemetry_attributes: dict[TelemetryAttribute, str | int] = { + telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] = { TelemetryAttributes.fga_client_request_method: "{{operationId}}", TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), TelemetryAttributes.fga_client_request_model_id: local_var_params.get( diff --git a/config/clients/python/template/src/api_client.py.mustache b/config/clients/python/template/src/api_client.py.mustache index 391a6b5d..89a2a159 100644 --- a/config/clients/python/template/src/api_client.py.mustache +++ b/config/clients/python/template/src/api_client.py.mustache @@ -16,7 +16,7 @@ import tornado.gen {{/tornado}} from multiprocessing.pool import ThreadPool -from dateutil.parser import parse +from dateutil.parser import parse # type: ignore[import-untyped] import {{modelPackage}} from {{packageName}} import rest, oauth2 @@ -175,7 +175,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): @@ -514,7 +514,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): """Makes the HTTP request (synchronous) and returns deserialized data. diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index 55795fbe..59aed0de 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -62,22 +62,31 @@ def _chuck_array(array, max_size): return [array[i * max_size:(i + 1) * max_size] for i in range((len(array) + max_size - 1) // max_size )] -def set_heading_if_not_set(options: dict[str, int|str], name: str, value: str): +def set_heading_if_not_set( + options: dict[str, int | str | dict[str, int | str]] | None, + name: str, + value: str, +) -> dict[str, int | str | dict[str, int | str]]: """ Set heading to the value if it is not set """ - if options is None: - options = {} - headers = options.get("headers") - if headers is None: - headers = {} - if headers.get(name) is None: - headers[name] = value - options["headers"] = headers - return options - - -def options_to_kwargs(options: dict[str, int|str] = None): + _options: dict[str, int | str | dict[str, int | str]] = ( + options if options is not None else {} + ) + + if type(_options.get("headers")) is not dict: + _options["headers"] = {} + + if type(_options["headers"]) is dict: + if type(_options["headers"].get(name)) not in [int, str]: + _options["headers"][name] = value + + return _options + + +def options_to_kwargs( + options: dict[str, int | str | dict[str, int | str]] | None = None, +) -> dict[str, int | str | dict[str, int | str]]: """ Return kwargs with continuation_token and page_size """ @@ -93,7 +102,7 @@ def options_to_kwargs(options: dict[str, int|str] = None): kwargs["_retry_params"] = options["retry_params"] return kwargs -def options_to_transaction_info(options: dict[str, int|str] = None): +def options_to_transaction_info(options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the transaction info """ @@ -136,7 +145,10 @@ class OpenFgaClient: {{#asyncio}}async {{/asyncio}}def close(self): {{#asyncio}}await {{/asyncio}}self._api.close() - def _get_authorization_model_id(self, options: object) -> str | None: + def _get_authorization_model_id( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Return the authorization model ID if specified in the options. Otherwise, return the authorization model ID stored in the client's configuration @@ -151,13 +163,21 @@ class OpenFgaClient: "authorization_model_id ('%s') is not in a valid ulid format" % authorization_model_id) return authorization_model_id - def _get_consistency(self, options: object) -> str | None: + def _get_consistency( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Returns the consistency requested if specified in the options. Otherwise, returns None. """ - if options is not None and "consistency" in options: - return options["consistency"] + consistency: int | str | dict[str, int | str] | None = ( + options.get("consistency", None) if options is not None else None + ) + + if type(consistency) is str: + return consistency + return None def set_store_id(self, value): @@ -188,7 +208,7 @@ class OpenFgaClient: # Stores ################# - {{#asyncio}}async {{/asyncio}}def list_stores(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def list_stores(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ List the stores in the system :param page_size(options) - Number of items returned per request @@ -205,7 +225,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def create_store(self, body: CreateStoreRequest, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def create_store(self, body: CreateStoreRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Create the stores in the system :param header(options) - Custom headers to send alongside the request @@ -220,7 +240,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def get_store(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def get_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Get the store info in the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -234,7 +254,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def delete_store(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def delete_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Delete the store from the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -252,7 +272,7 @@ class OpenFgaClient: # Authorization Models ####################### - {{#asyncio}}async {{/asyncio}}def read_authorization_models(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_authorization_models(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the authorization models for a particular store. :param header(options) - Custom headers to send alongside the request @@ -266,7 +286,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write authorization model. :param body - WriteAuthorizationModelRequest @@ -282,7 +302,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def read_authorization_model(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read an authorization model. :param header(options) - Custom headers to send alongside the request @@ -298,7 +318,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def read_latest_authorization_model(self, options: dict[str, int|str] = None): + {{#asyncio}}async {{/asyncio}}def read_latest_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method of reading the latest authorization model :param header(options) - Custom headers to send alongside the request @@ -316,7 +336,7 @@ class OpenFgaClient: # Relationship Tuples ####################### - {{#asyncio}}async {{/asyncio}}def read_changes(self, body: ClientReadChangesRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read_changes(self, body: ClientReadChangesRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the type we want to look for change @@ -328,14 +348,19 @@ class OpenFgaClient: :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated """ kwargs = options_to_kwargs(options) - kwargs["type"] = body.type - kwargs["start_time"] = body.start_time + + if body.type is not None: + kwargs["type"] = body.type + + if body.start_time is not None: + kwargs["start_time"] = body.start_time + api_response = {{#asyncio}}await {{/asyncio}}self._api.read_changes( **kwargs, ) return api_response - {{#asyncio}}async {{/asyncio}}def read(self, body: ReadRequestTupleKey, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read(self, body: ReadRequestTupleKey, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the tuples we want to read @@ -374,7 +399,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): try: write_batch = None delete_batch = None @@ -389,7 +414,7 @@ class OpenFgaClient: except Exception as err: return [construct_write_single_response(i, False, err) for i in batch] - {{#asyncio}}async {{/asyncio}}def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Internal function for write/delete batches """ @@ -410,7 +435,7 @@ class OpenFgaClient: return batch_write_responses - {{#asyncio}}async {{/asyncio}}def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples """ @@ -439,7 +464,7 @@ class OpenFgaClient: deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - {{#asyncio}}async {{/asyncio}}def write(self, body: ClientWriteRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples :param body - the write request @@ -465,7 +490,7 @@ class OpenFgaClient: deletes_response = {{#asyncio}}await {{/asyncio}}self._write_batches(body.deletes, transaction, False, options) return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - {{#asyncio}}async {{/asyncio}}def write_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for writing tuples :param body - the list of tuples we want to write @@ -478,7 +503,7 @@ class OpenFgaClient: result = {{#asyncio}}await {{/asyncio}}self.write(ClientWriteRequest(body, None), options) return result - {{#asyncio}}async {{/asyncio}}def delete_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def delete_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for deleteing tuples :param body - the list of tuples we want to delete @@ -494,7 +519,11 @@ class OpenFgaClient: ####################### # Relationship Queries ####################### - {{#asyncio}}async {{/asyncio}}def check(self, body: ClientCheckRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def check( + self, + body: ClientCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Check whether a user is authorized to access an object :param body - ClientCheckRequest defining check request @@ -527,7 +556,12 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def _single_client_batch_check(self, body: ClientCheckRequest, semaphore: asyncio.Semaphore, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def _single_client_batch_check( + self, + body: ClientCheckRequest, + semaphore: asyncio.Semaphore, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a single batch request and return body in a SingleBatchCheckResponse :param body - ClientCheckRequest defining check request @@ -546,7 +580,11 @@ class OpenFgaClient: semaphore.release() {{/asyncio}} - {{#asyncio}}async {{/asyncio}}def client_batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): + {{#asyncio}}async {{/asyncio}}def client_batch_check( + self, + body: list[ClientCheckRequest], + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a set of checks :param body - list of ClientCheckRequest defining check request @@ -590,7 +628,7 @@ class OpenFgaClient: self, body: BatchCheckRequest, semaphore: asyncio.Semaphore, - options: dict[str, str] = None, + options: dict[str, int | str | dict[str, int | str]] | None = None, ): """ Run a single BatchCheck request @@ -607,7 +645,11 @@ class OpenFgaClient: finally: semaphore.release() - async def batch_check(self, body: ClientBatchCheckRequest, options = None): + async def batch_check( + self, + body: ClientBatchCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a batchcheck request :param body - BatchCheck request @@ -691,7 +733,7 @@ class OpenFgaClient: return ClientBatchCheckResponse(result) - {{#asyncio}}async {{/asyncio}}def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def expand(self, body: ClientExpandRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run expand request :param body - list of ClientExpandRequest defining expand request @@ -722,7 +764,7 @@ class OpenFgaClient: ) return api_response - {{#asyncio}}async {{/asyncio}}def list_objects(self, body: ClientListObjectsRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def list_objects(self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run list object request :param body - list object parameters @@ -754,7 +796,7 @@ class OpenFgaClient: return api_response {{#asyncio}}async {{/asyncio}}def streamed_list_objects( - self, body: ClientListObjectsRequest, options: dict[str, str] = None + self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None ): """ Retrieve all objects of the given type that the user has a relation with, using the streaming ListObjects API. @@ -790,7 +832,7 @@ class OpenFgaClient: if response and "result" in response and "object" in response["result"]: yield StreamedListObjectsResponse(response["result"]["object"]) - {{#asyncio}}async {{/asyncio}}def list_relations(self, body: ClientListRelationsRequest, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def list_relations(self, body: ClientListRelationsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the relations for which user has a relationship with the object :param body - list relation request @@ -812,7 +854,7 @@ class OpenFgaClient: return [i.request.relation for i in result_list] {{#asyncio}}async {{/asyncio}}def list_users( - self, body: ClientListUsersRequest, options: dict[str, str] = None + self, body: ClientListUsersRequest, options: dict[str, int | str | dict[str, int | str]] | None = None ): """ Run list users request @@ -846,7 +888,7 @@ class OpenFgaClient: ####################### # Assertions ####################### - {{#asyncio}}async {{/asyncio}}def read_assertions(self, options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def read_assertions(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the assertions :param authorization_model_id(options) - Overrides the authorization model id in the configuration @@ -861,7 +903,7 @@ class OpenFgaClient: api_response = {{#asyncio}}await {{/asyncio}}self._api.read_assertions(authorization_model_id, **kwargs) return api_response - {{#asyncio}}async {{/asyncio}}def write_assertions(self, body: list[ClientAssertion], options: dict[str, str] = None): + {{#asyncio}}async {{/asyncio}}def write_assertions(self, body: list[ClientAssertion], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Upsert the assertions :param body - Write assertion request diff --git a/config/clients/python/template/src/client/models/assertion.py.mustache b/config/clients/python/template/src/client/models/assertion.py.mustache index ca189609..4baa97e7 100644 --- a/config/clients/python/template/src/client/models/assertion.py.mustache +++ b/config/clients/python/template/src/client/models/assertion.py.mustache @@ -5,36 +5,70 @@ class ClientAssertion: ClientAssertion flattens the input necessary for an Assertion """ - def __init__(self, user: str, relation: str, object: str, expectation: bool): + def __init__( + self, + user: str, + relation: str, + object: str, + expectation: bool, + ) -> None: self._user = user self._relation = relation self._object = object self._expectation = expectation @property - def user(self): + def user(self) -> str: """ Return user """ return self._user + @user.setter + def user(self, user: str) -> None: + """ + Set user + """ + self._user = user + @property - def relation(self): + def relation(self) -> str: """ Return relation """ return self._relation + @relation.setter + def relation(self, relation: str) -> None: + """ + Set relation + """ + self._relation = relation + @property - def object(self): + def object(self) -> str: """ Return object """ return self._object + @object.setter + def object(self, object: str) -> None: + """ + Set object + """ + self._object = object + @property - def expectation(self): + def expectation(self) -> bool: """ Return expectation """ return self._expectation + + @expectation.setter + def expectation(self, expectation: bool) -> None: + """ + Set expectation + """ + self._expectation = expectation diff --git a/config/clients/python/template/src/client/models/batch_check_item.py.mustache b/config/clients/python/template/src/client/models/batch_check_item.py.mustache index 6229ab62..415374c0 100644 --- a/config/clients/python/template/src/client/models/batch_check_item.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_item.py.mustache @@ -6,7 +6,7 @@ from {{packageName}}.models.check_request_tuple_key import CheckRequestTupleKey from {{packageName}}.models.contextual_tuple_keys import ContextualTupleKeys -def construct_batch_item(check): +def construct_batch_item(check) -> BatchCheckItem: batch_item = BatchCheckItem( tuple_key=CheckRequestTupleKey( user=check.user, @@ -31,100 +31,97 @@ class ClientBatchCheckItem: user: str, relation: str, object: str, - correlation_id: str = None, - contextual_tuples: list[ClientTuple] = None, - context: object = None, - ): + correlation_id: str | None = None, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._user = user self._relation = relation self._object = object self._correlation_id = correlation_id - self._contextual_tuples = None - if contextual_tuples: - self._contextual_tuples = contextual_tuples + self._contextual_tuples = contextual_tuples self._context = context - @property - def user(self): + def user(self) -> str: """ Return user """ return self._user + @user.setter + def user(self, value: str) -> None: + """ + Set user + """ + self._user = value + @property - def relation(self): + def relation(self) -> str: """ Return relation """ return self._relation - @property - def object(self): + @relation.setter + def relation(self, value: str) -> None: """ - Return object + Set relation """ - return self._object - + self._relation = value @property - def contextual_tuples(self): + def object(self) -> str: """ - Return contextual tuples + Return object """ - return self._contextual_tuples + return self._object - @property - def context(self): + @object.setter + def object(self, value: str) -> None: """ - Return context + Set object """ - return self._context + self._object = value @property - def correlation_id(self): + def correlation_id(self) -> str | None: """ + Return correlation id """ return self._correlation_id - - @user.setter - def user(self, value): - """ - Set user - """ - self._user = value - - @relation.setter - def relation(self, value): + @correlation_id.setter + def correlation_id(self, value: str | None) -> None: """ - Set relation + Set correlation id """ - self._relation = value + self._correlation_id = value - @object.setter - def object(self, value): + @property + def contextual_tuples(self) -> list[ClientTuple] | None: """ - Set object + Return contextual tuples """ - self._object = value + return self._contextual_tuples @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value - @context.setter - def context(self, value): + @property + def context(self) -> dict[str, int | str] | None: """ - Set context + Return context """ - self._context = value + return self._context - @correlation_id.setter - def correlation_id(self, value): + @context.setter + def context(self, value: dict[str, int | str] | None) -> None: """ + Set context """ - self._correlation_id = value + self._context = value diff --git a/config/clients/python/template/src/client/models/batch_check_request.py.mustache b/config/clients/python/template/src/client/models/batch_check_request.py.mustache index efe9b321..40b082e5 100644 --- a/config/clients/python/template/src/client/models/batch_check_request.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_request.py.mustache @@ -7,18 +7,22 @@ class ClientBatchCheckRequest: """ ClientBatchCheckRequest encapsulates the parameters for a BatchCheck request """ - def __init__(self, checks: list[ClientBatchCheckItem]): + + def __init__( + self, + checks: list[ClientBatchCheckItem], + ) -> None: self._checks = checks @property - def checks(self): + def checks(self) -> list[ClientBatchCheckItem]: """ Return checks """ return self._checks @checks.setter - def checks(self, checks): + def checks(self, checks: list[ClientBatchCheckItem]) -> None: """ Set checks """ diff --git a/config/clients/python/template/src/client/models/batch_check_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_response.py.mustache index 018f288a..479484d7 100644 --- a/config/clients/python/template/src/client/models/batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_response.py.mustache @@ -4,18 +4,21 @@ from {{packageName}}.client.models.batch_check_single_response import ClientBatc class ClientBatchCheckResponse: - def __init__(self, result: list[ClientBatchCheckSingleResponse]): + def __init__( + self, + result: list[ClientBatchCheckSingleResponse], + ) -> None: self._result = result @property - def result(self): + def result(self) -> list[ClientBatchCheckSingleResponse]: """ Return result """ return self._result @result.setter - def result(self, result): + def result(self, result: list[ClientBatchCheckSingleResponse]) -> None: """ Set result """ diff --git a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache index 3a46eb29..5f9252f3 100644 --- a/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache +++ b/config/clients/python/template/src/client/models/batch_check_single_response.py.mustache @@ -10,68 +10,68 @@ class ClientBatchCheckSingleResponse: allowed: bool, request: ClientTuple, correlation_id: str, - error: CheckError = None, - ): + error: CheckError | None = None, + ) -> None: self._allowed = allowed self._request = request self._correlation_id = correlation_id self._error = error - # Set "false" if there was an error and allowed isn't set + + # Set `allowed` to `false` if there was an error and allowed isn't otherwise set. if error is not None and allowed is None: self._allowed = False - @property - def allowed(self): + def allowed(self) -> bool: """ Return allowed """ return self._allowed - @property - def request(self): - """ - Return request - """ - return self._request - - @property - def correlation_id(self): + @allowed.setter + def allowed(self, allowed: bool) -> None: """ - Return correlation_id + Set allowed """ - return self._correlation_id + self._allowed = allowed @property - def error(self): - """ - Return error - """ - return self._error - - @allowed.setter - def allowed(self, allowed): + def request(self) -> ClientTuple: """ - Set allowed + Return request """ - self._allowed = allowed + return self._request @request.setter - def request(self, request): + def request(self, request: ClientTuple) -> None: """ Set request """ self._request = request + @property + def correlation_id(self) -> str: + """ + Return correlation_id + """ + return self._correlation_id + @correlation_id.setter - def correlation_id(self, correlation_id): + def correlation_id(self, correlation_id: str) -> None: """ Set correlation_id """ self._correlation_id = correlation_id + @property + def error(self) -> CheckError | None: + """ + Return error + """ + return self._error + @error.setter - def error(self, error): + def error(self, error: CheckError | None) -> None: """ Set error """ diff --git a/config/clients/python/template/src/client/models/check_request.py.mustache b/config/clients/python/template/src/client/models/check_request.py.mustache index 8f9dc0a7..2be3824b 100644 --- a/config/clients/python/template/src/client/models/check_request.py.mustache +++ b/config/clients/python/template/src/client/models/check_request.py.mustache @@ -3,93 +3,107 @@ from {{packageName}}.client.models.tuple import ClientTuple -def construct_check_request(user: str, relation: str, object: str, contextual_tuples: list[ClientTuple] = None, context: object = None): - """ - helper function to construct the check request body - """ - return ClientCheckRequest(user, relation, object, contextual_tuples, context) - - class ClientCheckRequest: """ ClientCheckRequest encapsulates the parameters for check request """ - def __init__(self, user: str, relation: str, object: str, contextual_tuples: list[ClientTuple] = None, context: object = None): + def __init__( + self, + user: str, + relation: str, + object: str, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._user = user self._relation = relation self._object = object self._contextual_tuples = None + self._context = context + if contextual_tuples: self._contextual_tuples = contextual_tuples - self._context = context @property - def user(self): + def user(self) -> str: """ Return user """ return self._user - @property - def relation(self): + @user.setter + def user(self, value: str) -> None: """ - Return relation + Set user """ - return self._relation + self._user = value @property - def object(self): + def relation(self) -> str: """ - Return object + Return relation """ - return self._object + return self._relation - @property - def contextual_tuples(self): + @relation.setter + def relation(self, value: str) -> None: """ - Return contextual tuples + Set relation """ - return self._contextual_tuples + self._relation = value @property - def context(self): - """ - Return context + def object(self) -> str: """ - return self._context - - @user.setter - def user(self, value): - """ - Set user - """ - self._user = value - - @relation.setter - def relation(self, value): - """ - Set relation + Return object """ - self._relation = value + return self._object @object.setter - def object(self, value): + def object(self, value: str) -> None: """ Set object """ self._object = value + @property + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Return contextual tuples + """ + return self._contextual_tuples + @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value + @property + def context(self) -> dict[str, int | str] | None: + """ + Return context + """ + return self._context + @context.setter - def context(self, value): + def context(self, value: dict[str, int | str] | None) -> None: """ Set context """ self._context = value + + +def construct_check_request( + user: str, + relation: str, + object: str, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, +) -> ClientCheckRequest: + """ + helper function to construct the check request body + """ + return ClientCheckRequest(user, relation, object, contextual_tuples, context) diff --git a/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache index 33c72e25..aa3ab7c8 100644 --- a/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache +++ b/config/clients/python/template/src/client/models/client_batch_check_response.py.mustache @@ -9,40 +9,74 @@ class ClientBatchCheckClientResponse: ClientBatchCheckClientResponse encapsulates the response for a single batch check """ - def __init__(self, allowed: bool, request: ClientCheckRequest, response: CheckResponse, error: Exception=None): + def __init__( + self, + allowed: bool, + request: ClientCheckRequest, + response: CheckResponse | None = None, + error: Exception | None = None, + ) -> None: self._allowed = allowed self._request = request self._response = response self._error = error @property - def allowed(self): + def allowed(self) -> bool: """ Return whether request is allowed """ return self._allowed + @allowed.setter + def allowed(self, value: bool) -> None: + """ + Set whether request is allowed + """ + self._allowed = value + @property - def request(self): + def request(self) -> ClientCheckRequest: """ Return original request """ return self._request + @request.setter + def request(self, value: ClientCheckRequest) -> None: + """ + Set original request + """ + self._request = value + @property - def response(self): + def response(self) -> CheckResponse | None: """ Return original request """ return self._response + @response.setter + def response(self, value: CheckResponse | None) -> None: + """ + Set original request + """ + self._response = value + @property - def error(self): + def error(self) -> Exception | None: """ Return error associated with batch request (if any) """ return self._error + @error.setter + def error(self, value: Exception | None) -> None: + """ + Set error associated with batch request + """ + self._error = value + def __str__(self): """ Return the class string diff --git a/config/clients/python/template/src/client/models/expand_request.py.mustache b/config/clients/python/template/src/client/models/expand_request.py.mustache index 6a315bc7..9dbf8590 100644 --- a/config/clients/python/template/src/client/models/expand_request.py.mustache +++ b/config/clients/python/template/src/client/models/expand_request.py.mustache @@ -12,49 +12,49 @@ class ClientExpandRequest: self, relation: str, object: str, - contextual_tuples: list[ClientTuple] = None, - ): + contextual_tuples: list[ClientTuple] | None = None, + ) -> None: self._relation = relation self._object = object self._contextual_tuples = contextual_tuples @property - def relation(self): + def relation(self) -> str: """ Return relation """ return self._relation @relation.setter - def relation(self, value): + def relation(self, value: str) -> None: """ Set relation """ self._relation = value @property - def object(self): + def object(self) -> str: """ Return object """ return self._object @object.setter - def object(self, value): + def object(self, value: str) -> None: """ Set object """ self._object = value @property - def contextual_tuples(self): + def contextual_tuples(self) -> list[ClientTuple] | None: """ Return contextual_tuples """ return self._contextual_tuples @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ diff --git a/config/clients/python/template/src/client/models/list_objects_request.py.mustache b/config/clients/python/template/src/client/models/list_objects_request.py.mustache index 674d5b9f..bc2d0097 100644 --- a/config/clients/python/template/src/client/models/list_objects_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_objects_request.py.mustache @@ -8,7 +8,14 @@ class ClientListObjectsRequest: ClientListObjectsRequest encapsulates the parameters required for list objects """ - def __init__(self, user: str, relation: str, type: str, contextual_tuples: list[ClientTuple] = None, context: object = None): + def __init__( + self, + user: str, + relation: str, + type: str, + contextual_tuples: list[ClientTuple] | None = None, + context: object | None = None, + ) -> None: self._user = user self._relation = relation self._type = type @@ -16,70 +23,70 @@ class ClientListObjectsRequest: self._context = context @property - def user(self): + def user(self) -> str: """ Return user """ return self._user - @property - def relation(self): + @user.setter + def user(self, value: str) -> None: """ - Return relation + Set user """ - return self._relation + self._user = value @property - def type(self): + def relation(self) -> str: """ - Return type + Return relation """ - return self._type + return self._relation - @property - def contextual_tuples(self): + @relation.setter + def relation(self, value: str) -> None: """ - Return contextual_tuples + Set relation """ - return self._contextual_tuples + self._relation = value @property - def context(self): - """ - Return context - """ - return self._context - - @user.setter - def user(self, value): + def type(self) -> str: """ - Set user - """ - self._user = value - - @relation.setter - def relation(self, value): - """ - Set relation + Return type """ - self._relation = value + return self._type @type.setter - def type(self, value): + def type(self, value: str) -> None: """ Set type """ self._type = value + @property + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Return contextual_tuples + """ + return self._contextual_tuples + @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value + @property + def context(self) -> object | None: + """ + Return context + """ + return self._context + @context.setter - def context(self, value): + def context(self, value: object | None) -> None: """ Set context """ diff --git a/config/clients/python/template/src/client/models/list_relations_request.py.mustache b/config/clients/python/template/src/client/models/list_relations_request.py.mustache index eb668d30..24d54299 100644 --- a/config/clients/python/template/src/client/models/list_relations_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_relations_request.py.mustache @@ -8,7 +8,14 @@ class ClientListRelationsRequest: ClientListRelationsRequest encapsulates the parameters required for list all relations user have with object """ - def __init__(self, user: str, relations: list[str], object: str, contextual_tuples: list[ClientTuple] = None, context: object = None): + def __init__( + self, + user: str, + relations: list[str], + object: str, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._user = user self._relations = relations self._object = object @@ -16,70 +23,70 @@ class ClientListRelationsRequest: self._context = context @property - def user(self): + def user(self) -> str: """ Return user """ return self._user - @property - def relations(self): + @user.setter + def user(self, value: str) -> None: """ - Return relations + Set user """ - return self._relations + self._user = value @property - def object(self): + def relations(self) -> list[str]: """ - Return object + Return relations """ - return self._object + return self._relations - @property - def contextual_tuples(self): + @relations.setter + def relations(self, value: list[str]) -> None: """ - Return contextual_tuples + Set relations """ - return self._contextual_tuples + self._relations = value @property - def context(self): - """ - Return context - """ - return self._context - - @user.setter - def user(self, value): + def object(self) -> str: """ - Set user - """ - self._user = value - - @relations.setter - def relations(self, value): - """ - Set relations + Return object """ - self._relations = value + return self._object @object.setter - def object(self, value): + def object(self, value: str) -> None: """ Set object """ self._object = value + @property + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Return contextual_tuples + """ + return self._contextual_tuples + @contextual_tuples.setter - def contextual_tuples(self, value): + def contextual_tuples(self, value: list[ClientTuple] | None) -> None: """ Set contextual tuples """ self._contextual_tuples = value + @property + def context(self) -> dict[str, int | str] | None: + """ + Return context + """ + return self._context + @context.setter - def context(self, value): + def context(self, value: dict[str, int | str] | None) -> None: """ Set context """ diff --git a/config/clients/python/template/src/client/models/list_users_request.py.mustache b/config/clients/python/template/src/client/models/list_users_request.py.mustache index 3c83e3e1..9944be77 100644 --- a/config/clients/python/template/src/client/models/list_users_request.py.mustache +++ b/config/clients/python/template/src/client/models/list_users_request.py.mustache @@ -12,12 +12,12 @@ class ClientListUsersRequest: def __init__( self, - object: FgaObject = None, - relation: str = None, - user_filters: list[UserTypeFilter] = None, - contextual_tuples: list[ClientTuple] = None, - context: object = None, - ): + object: FgaObject | None = None, + relation: str | None = None, + user_filters: list[UserTypeFilter] | None = None, + contextual_tuples: list[ClientTuple] | None = None, + context: dict[str, int | str] | None = None, + ) -> None: self._object = object self._relation = relation self._user_filters = user_filters @@ -25,9 +25,9 @@ class ClientListUsersRequest: self._context = context @property - def object(self) -> FgaObject: - """Gets the object of this ClientListUsersRequest. - + def object(self) -> FgaObject | None: + """ + Gets the object of this ClientListUsersRequest. :return: The object of this ClientListUsersRequest. :rtype: str @@ -35,20 +35,19 @@ class ClientListUsersRequest: return self._object @object.setter - def object(self, object: FgaObject): - """Sets the object of this ClientListUsersRequest. - + def object(self, object: FgaObject | None) -> None: + """ + Sets the object of this ClientListUsersRequest. :param object: The object of this ClientListUsersRequest. :type object: str """ - self._object = object @property - def relation(self) -> str: - """Gets the relation of this ClientListUsersRequest. - + def relation(self) -> str | None: + """ + Gets the relation of this ClientListUsersRequest. :return: The relation of this ClientListUsersRequest. :rtype: str @@ -56,20 +55,19 @@ class ClientListUsersRequest: return self._relation @relation.setter - def relation(self, relation: str): - """Sets the relation of this ClientListUsersRequest. - + def relation(self, relation: str | None) -> None: + """ + Sets the relation of this ClientListUsersRequest. :param relation: The relation of this ClientListUsersRequest. :type relation: str """ - self._relation = relation @property - def user_filters(self) -> list[UserTypeFilter]: - """Gets the user_filters of this ClientListUsersRequest. - + def user_filters(self) -> list[UserTypeFilter] | None: + """ + Gets the user_filters of this ClientListUsersRequest. :return: The user_filters of this ClientListUsersRequest. :rtype: str @@ -77,20 +75,19 @@ class ClientListUsersRequest: return self._user_filters @user_filters.setter - def user_filters(self, user_filters: list[UserTypeFilter]): - """Sets the user_filters of this ClientListUsersRequest. - + def user_filters(self, user_filters: list[UserTypeFilter] | None) -> None: + """ + Sets the user_filters of this ClientListUsersRequest. :param user_filters: The user_filters of this ClientListUsersRequest. :type user_filters: str """ - self._user_filters = user_filters @property - def contextual_tuples(self) -> list[ClientTuple]: - """Gets the contextual_tuples of this ClientListUsersRequest. - + def contextual_tuples(self) -> list[ClientTuple] | None: + """ + Gets the contextual_tuples of this ClientListUsersRequest. :return: The contextual_tuples of this ClientListUsersRequest. :rtype: ContextualTupleKeys @@ -98,35 +95,29 @@ class ClientListUsersRequest: return self._contextual_tuples @contextual_tuples.setter - def contextual_tuples(self, contextual_tuples: list[ClientTuple]): - """Sets the contextual_tuples of this ClientListUsersRequest. - + def contextual_tuples(self, contextual_tuples: list[ClientTuple] | None) -> None: + """ + Sets the contextual_tuples of this ClientListUsersRequest. :param contextual_tuples: The contextual_tuples of this ClientListUsersRequest. :type contextual_tuples: ContextualTupleKeys """ - self._contextual_tuples = contextual_tuples @property - def context(self) -> object: - """Gets the context of this ClientListUsersRequest. + def context(self) -> dict[str, int | str] | None: + """ + Gets the context of this ClientListUsersRequest. Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. - - :return: The context of this ClientListUsersRequest. - :rtype: object """ return self._context @context.setter - def context(self, context: object): - """Sets the context of this ClientListUsersRequest. + def context(self, context: dict[str, int | str] | None) -> None: + """ + Sets the context of this ClientListUsersRequest. Additional request context that will be used to evaluate any ABAC conditions encountered in the query evaluation. - - :param context: The context of this ClientListUsersRequest. - :type context: object """ - self._context = context diff --git a/config/clients/python/template/src/client/models/read_changes_request.py.mustache b/config/clients/python/template/src/client/models/read_changes_request.py.mustache index 5aafb89a..e0a0351b 100644 --- a/config/clients/python/template/src/client/models/read_changes_request.py.mustache +++ b/config/clients/python/template/src/client/models/read_changes_request.py.mustache @@ -5,20 +5,44 @@ class ClientReadChangesRequest: ClientReadChangesRequest encapsulates the parameters required to read changes """ - def __init__(self, type: str, start_time: str=None): + def __init__( + self, + type: str, + start_time: str | None = None, + ): self._type = type self._startTime = start_time @property - def type(self): + def type(self) -> str: """ Return type """ return self._type + @type.setter + def type( + self, + value: str, + ) -> None: + """ + Set type + """ + self._type = value + @property - def start_time(self): + def start_time(self) -> str | None: """ Return startTime """ return self._startTime + + @start_time.setter + def start_time( + self, + value: str | None, + ) -> None: + """ + Set startTime + """ + self._startTime = value diff --git a/config/clients/python/template/src/client/models/tuple.py.mustache b/config/clients/python/template/src/client/models/tuple.py.mustache index 823c706b..2ec2594f 100644 --- a/config/clients/python/template/src/client/models/tuple.py.mustache +++ b/config/clients/python/template/src/client/models/tuple.py.mustache @@ -15,75 +15,59 @@ class ClientTuple: relation: str, object: str, condition: RelationshipCondition | None = None, - ): + ) -> None: self._user = user self._relation = relation self._object = object self._condition = condition - def __eq__(self, other): - return self.user == other.user and self.relation == other.relation and self.object == other.object + def __eq__(self, other) -> bool: + if ( + self.user == other.user + and self.relation == other.relation + and self.object == other.object + and self.condition == other.condition + ): + return True - @property - def user(self): - """ - Return user - """ - return self._user - - @property - def relation(self): - """ - Return relation - """ - return self._relation + return False @property - def object(self): - """ - Return object - """ - return self._object - - @property - def condition(self): - """ - Return condition - """ - return self._condition + def user(self) -> str: + return self._user @user.setter - def user(self, value): - """ - Set user - """ + def user(self, value: str) -> None: self._user = value + @property + def relation(self) -> str: + return self._relation + @relation.setter - def relation(self, value): - """ - Set relation - """ + def relation(self, value: str) -> None: self._relation = value + @property + def object(self) -> str: + return self._object + @object.setter - def object(self, value): - """ - Set object - """ + def object(self, value: str) -> None: self._object = value + @property + def condition(self) -> RelationshipCondition | None: + return self._condition + @condition.setter - def condition(self, value): - """ - Set condition - """ + def condition(self, value: RelationshipCondition | None) -> None: self._condition = value @property - def tuple_key(self): + def tuple_key(self) -> TupleKey: """ - Return the tuple as tuple_key + Return the ClientTuple as TupleKey """ return TupleKey( object=self.object, @@ -93,11 +77,11 @@ class ClientTuple: ) -def convert_tuple_keys(lists: list[ClientTuple]): +def convert_tuple_keys(lists: list[ClientTuple]) -> list[TupleKey] | None: """ Return the items as tuple_keys """ if lists is None: return None - items=map(lambda item: item.tuple_key, lists) - return list(items) + + return list(map(lambda item: item.tuple_key, lists)) diff --git a/config/clients/python/template/src/client/models/write_request.py.mustache b/config/clients/python/template/src/client/models/write_request.py.mustache index 56335d15..bfba1e97 100644 --- a/config/clients/python/template/src/client/models/write_request.py.mustache +++ b/config/clients/python/template/src/client/models/write_request.py.mustache @@ -10,54 +10,68 @@ class ClientWriteRequest: ClientWriteRequest encapsulates the parameters required to write """ - def __init__(self, writes: list[ClientTuple]=None, deletes: list[ClientTuple]=None): + def __init__( + self, + writes: list[ClientTuple] | None = None, + deletes: list[ClientTuple] | None = None, + ) -> None: self._writes = writes self._deletes = deletes @property - def writes(self): + def writes(self) -> list[ClientTuple] | None: """ Return writes """ return self._writes - @property - def deletes(self): - """ - Return deletes - """ - return self._deletes - @writes.setter - def writes(self, value): + def writes(self, value: list[ClientTuple] | None) -> None: """ Set writes """ self._writes = value + @property + def deletes(self) -> list[ClientTuple] | None: + """ + Return deletes + """ + return self._deletes + @deletes.setter - def deletes(self, value): + def deletes(self, value: list[ClientTuple] | None) -> None: """ Set deletes """ self._deletes = value @property - def writes_tuple_keys(self): + def writes_tuple_keys(self) -> WriteRequestWrites | None: """ Return the writes as tuple keys """ - keys = convert_tuple_keys(self.writes) + if self._writes is None: + return None + + keys = convert_tuple_keys(self._writes) + if keys is None: return None + return WriteRequestWrites(tuple_keys=keys) @property - def deletes_tuple_keys(self): + def deletes_tuple_keys(self) -> WriteRequestDeletes | None: """ Return the delete as tuple keys """ - keys = convert_tuple_keys(self.deletes) + if self._deletes is None: + return None + + keys = convert_tuple_keys(self._deletes) + if keys is None: return None + return WriteRequestDeletes(tuple_keys=keys) diff --git a/config/clients/python/template/src/client/models/write_response.py.mustache b/config/clients/python/template/src/client/models/write_response.py.mustache index 7302bf17..bf3eef9c 100644 --- a/config/clients/python/template/src/client/models/write_response.py.mustache +++ b/config/clients/python/template/src/client/models/write_response.py.mustache @@ -8,20 +8,38 @@ class ClientWriteResponse: ClientWriteResponse returns the set of responses and their statuses """ - def __init__(self, writes: list[ClientWriteSingleResponse], deletes: list[ClientWriteSingleResponse]): + def __init__( + self, + writes: list[ClientWriteSingleResponse] | None = None, + deletes: list[ClientWriteSingleResponse] | None = None, + ) -> None: self._writes = writes self._deletes = deletes @property - def writes(self): + def writes(self) -> list[ClientWriteSingleResponse] | None: """ Return the writes response """ return self._writes + @writes.setter + def writes(self, value: list[ClientWriteSingleResponse] | None) -> None: + """ + Set the writes response + """ + self._writes = value + @property def deletes(self): """ Return the delete response """ return self._deletes + + @deletes.setter + def deletes(self, value: list[ClientWriteSingleResponse] | None) -> None: + """ + Set the delete response + """ + self._deletes = value diff --git a/config/clients/python/template/src/client/models/write_single_response.py.mustache b/config/clients/python/template/src/client/models/write_single_response.py.mustache index dcf76956..69c42c3f 100644 --- a/config/clients/python/template/src/client/models/write_single_response.py.mustache +++ b/config/clients/python/template/src/client/models/write_single_response.py.mustache @@ -3,19 +3,17 @@ from {{packageName}}.client.models.tuple import ClientTuple -def construct_write_single_response(tuple_key: ClientTuple, success: bool, error: Exception=None): - """ - Helper function to return a single write response - """ - return ClientWriteSingleResponse(tuple_key, success, error) - - class ClientWriteSingleResponse: """ ClientWriteSingleResponse encapsulates the response of a single write """ - def __init__(self, tuple_key: ClientTuple, success: bool, error: Exception=None): + def __init__( + self, + tuple_key: ClientTuple, + success: bool, + error: Exception | None = None, + ) -> None: self._tuple_key = tuple_key self._success = success self._error = error @@ -24,22 +22,54 @@ class ClientWriteSingleResponse: return self.tuple_key == other.tuple_key and self.success == other.success and self.error == other.error @property - def tuple_key(self): + def tuple_key(self) -> ClientTuple: """ Return tuple_key """ return self._tuple_key + @tuple_key.setter + def tuple_key(self, value: ClientTuple) -> None: + """ + Set tuple_key + """ + self._tuple_key = value + @property - def success(self): + def success(self) -> bool: """ Return success """ return self._success + @success.setter + def success(self, value: bool) -> None: + """ + Set success + """ + self._success = value + @property - def error(self): + def error(self) -> Exception | None: """ Return error """ return self._error + + @error.setter + def error(self, value: Exception | None) -> None: + """ + Set error + """ + self._error = value + + +def construct_write_single_response( + tuple_key: ClientTuple, + success: bool, + error: Exception | None = None, +) -> ClientWriteSingleResponse: + """ + Helper function to return a single write response + """ + return ClientWriteSingleResponse(tuple_key, success, error) diff --git a/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache b/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache index b74f493b..6c630a8b 100644 --- a/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache +++ b/config/clients/python/template/src/client/models/write_transaction_opts.py.mustache @@ -5,48 +5,62 @@ class WriteTransactionOpts: OpenFGA client write transaction info """ - def __init__(self, disabled: bool=False, max_per_chunk: int=1, max_parallel_requests: int=10): + def __init__( + self, + disabled: bool = False, + max_per_chunk: int = 1, + max_parallel_requests: int = 10, + ) -> None: self._disabled = disabled self._max_per_chunk = max_per_chunk self._max_parallel_requests = max_parallel_requests @property - def disabled(self): + def disabled(self) -> bool: """ Return disabled """ return self._disabled - @property - def max_per_chunk(self): + @disabled.setter + def disabled( + self, + value: bool, + ) -> None: """ - Return max per chunk + Set disabled """ - return self._max_per_chunk + self._disabled = value @property - def max_parallel_requests(self): - """ - Return max parallel requests - """ - return self._max_parallel_requests - - @disabled.setter - def disabled(self, value): + def max_per_chunk(self) -> int: """ - Set disabled + Return max per chunk """ - self._disabled = value + return self._max_per_chunk @max_per_chunk.setter - def max_per_chunk(self, value): + def max_per_chunk( + self, + value: int, + ) -> None: """ Set max_per_chunk """ self._max_per_chunk = value + @property + def max_parallel_requests(self) -> int: + """ + Return max parallel requests + """ + return self._max_parallel_requests + @max_parallel_requests.setter - def max_parallel_requests(self, value): + def max_parallel_requests( + self, + value: int, + ) -> None: """ Set max_parallel_requests """ diff --git a/config/clients/python/template/src/configuration.py.mustache b/config/clients/python/template/src/configuration.py.mustache index c8d4e993..d4a1d3cf 100644 --- a/config/clients/python/template/src/configuration.py.mustache +++ b/config/clients/python/template/src/configuration.py.mustache @@ -34,7 +34,8 @@ class RetryParams: :param max_retry: Maximum number of retry :param min_wait_in_ms: Minimum wait (in ms) between retry """ - def __init__(self, max_retry={{{defaultMaxRetry}}}, min_wait_in_ms={{{defaultMinWaitInMs}}}): + + def __init__(self, max_retry=3, min_wait_in_ms=100): self._max_retry = max_retry self._min_wait_in_ms = min_wait_in_ms @@ -43,8 +44,10 @@ class RetryParams: """ Return the maximum number of retry """ - if self._max_retry > {{{retryMaxAllowedNumber}}}: - raise FgaValidationException("RetryParams.max_retry exceeds maximum allowed limit of {{retryMaxAllowedNumber}}"); + if self._max_retry > 15: + raise FgaValidationException( + "RetryParams.max_retry exceeds maximum allowed limit of 15" + ) return self._max_retry @@ -54,10 +57,14 @@ class RetryParams: Update the maximum number of retry """ if not isinstance(value, int) or value < 0: - raise FgaValidationException("RetryParams.max_retry must be an integer greater than or equal to 0"); + raise FgaValidationException( + "RetryParams.max_retry must be an integer greater than or equal to 0" + ) - if value > {{{retryMaxAllowedNumber}}}: - raise FgaValidationException("RetryParams.max_retry exceeds maximum allowed limit of {{retryMaxAllowedNumber}}"); + if value > 15: + raise FgaValidationException( + "RetryParams.max_retry exceeds maximum allowed limit of 15" + ) self._max_retry = value @@ -133,23 +140,41 @@ class Configuration: _default = None - def __init__(self, api_scheme="https", api_host=None, - store_id=None, - credentials=None, - retry_params=None, - api_key=None, api_key_prefix=None, - username=None, password=None, - discard_unknown_keys=False, - server_index=None, server_variables=None, - server_operation_index=None, server_operation_variables=None, - ssl_ca_cert=None, - api_url=None, # TODO: restructure when removing api_scheme/api_host - telemetry: dict[TelemetryConfigurationType | str, TelemetryMetricsConfiguration | dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | dict[TelemetryAttribute, bool] | None] | None] | None = None, - timeout_millisec: int | None = None, + def __init__( + self, + api_scheme="https", + api_host=None, + store_id=None, + credentials=None, + retry_params=None, + api_key=None, + api_key_prefix=None, + username=None, + password=None, + discard_unknown_keys=False, + server_index=None, + server_variables=None, + server_operation_index=None, + server_operation_variables=None, + ssl_ca_cert=None, + api_url=None, # TODO: restructure when removing api_scheme/api_host + telemetry: ( + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None, + ] + | None + ) = None, + timeout_millisec: int | None = None, ): - - """Constructor - """ + """Constructor""" self._url = api_url self._scheme = api_scheme self._base_path = api_host @@ -199,9 +224,9 @@ class Configuration: self.logger = {} """Logging Settings """ - self.logger["package_logger"] = logging.getLogger("{{packageName}}") + self.logger["package_logger"] = logging.getLogger("openfga_sdk") self.logger["urllib3_logger"] = logging.getLogger("urllib3") - self.logger_format = '%(asctime)s %(levelname)s %(message)s' + self.logger_format = "%(asctime)s %(levelname)s %(message)s" """Log format """ self.logger_stream_handler = None @@ -235,21 +260,10 @@ class Configuration: """Set this to True/False to enable/disable SSL hostname verification. """ - {{#asyncio}} self.connection_pool_maxsize = 100 """This value is passed to the aiohttp to limit simultaneous connections. Default values is 100, None means no-limit. """ - {{/asyncio}} - {{^asyncio}} - self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 - """urllib3 connection pool's maximum number of connections saved - per pool. urllib3 uses 1 connection as default value, but this is - not the best value when you are making a lot of possibly parallel - requests to the same host, which is often the case here. - cpu_count * 5 is used as default value to increase performance. - """ - {{/asyncio}} self.proxy = None """Proxy URL @@ -257,7 +271,7 @@ class Configuration: self.proxy_headers = None """Proxy headers """ - self.safe_chars_for_path_param = '' + self.safe_chars_for_path_param = "" """Safe chars for path_param """ self.retries = None @@ -272,7 +286,7 @@ class Configuration: self._telemetry: TelemetryConfiguration | None = None if telemetry is None: - self._telemetry = TelemetryConfiguration( + self._telemetry = TelemetryConfiguration( TelemetryConfiguration.getSdkDefaults() ) elif isinstance(telemetry, dict): @@ -288,7 +302,7 @@ class Configuration: result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): - if k not in ('logger', 'logger_file_handler'): + if k not in ("logger", "logger_file_handler"): setattr(result, k, copy.deepcopy(v, memo)) # shallow copy of loggers result.logger = copy.copy(self.logger) @@ -416,13 +430,14 @@ class Configuration: def telemetry( self, value: ( - dict[ + TelemetryConfiguration + | dict[ TelemetryConfigurationType | str, TelemetryMetricsConfiguration | dict[ - TelemetryHistogram | TelemetryCounter, + TelemetryHistogram | TelemetryCounter | str, TelemetryMetricConfiguration - | dict[TelemetryAttribute, bool] + | dict[TelemetryAttribute | str, bool] | None, ] | None, @@ -432,16 +447,16 @@ class Configuration: ) -> None: """Set the telemetry configuration""" if value is not None: + if isinstance(value, TelemetryConfiguration): + self._telemetry = value + return + if isinstance(value, dict): self._telemetry = TelemetryConfiguration(value) return - elif isinstance(value, TelemetryConfiguration): - self._telemetry = value - return self._telemetry = None - def get_api_key_with_prefix(self, identifier, alias=None): """Gets API key (with prefix if set). @@ -451,7 +466,9 @@ class Configuration: """ if self.refresh_api_key_hook is not None: self.refresh_api_key_hook(self) - key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None) + key = self.api_key.get( + identifier, self.api_key.get(alias) if alias is not None else None + ) if key: prefix = self.api_key_prefix.get(identifier) if prefix: @@ -470,9 +487,9 @@ class Configuration: password = "" if self.password is not None: password = self.password - return urllib3.util.make_headers( - basic_auth=username + ':' + password - ).get('authorization') + return urllib3.util.make_headers(basic_auth=username + ":" + password).get( + "authorization" + ) def auth_settings(self): """Gets Auth Settings dict for api client. @@ -487,12 +504,13 @@ class Configuration: :return: The report for debugging. """ - return "Python SDK Debug Report:\n"\ - "OS: {env}\n"\ - "Python Version: {pyversion}\n"\ - "Version of the API: {{version}}\n"\ - "SDK Package Version: {{packageVersion}}".\ - format(env=sys.platform, pyversion=sys.version) + return ( + "Python SDK Debug Report:\n" + "OS: {env}\n" + "Python Version: {pyversion}\n" + "Version of the API: 1.x\n" + "SDK Package Version: 0.9.1".format(env=sys.platform, pyversion=sys.version) + ) def get_host_settings(self): """Gets an array of host settings @@ -500,33 +518,10 @@ class Configuration: :return: An array of host settings """ return [ - {{#servers}} { - 'url': "{{{url}}}", - 'description': "{{{description}}}{{^description}}No description provided{{/description}}", - {{#variables}} - {{#-first}} - 'variables': { - {{/-first}} - '{{{name}}}': { - 'description': "{{{description}}}{{^description}}No description provided{{/description}}", - 'default_value': "{{{defaultValue}}}", - {{#enumValues}} - {{#-first}} - 'enum_values': [ - {{/-first}} - "{{{.}}}"{{^-last}},{{/-last}} - {{#-last}} - ] - {{/-last}} - {{/enumValues}} - }{{^-last}},{{/-last}} - {{#-last}} - } - {{/-last}} - {{/variables}} - }{{^-last}},{{/-last}} - {{/servers}} + "url": "", + "description": "No description provided", + } ] def get_host_from_settings(self, index, variables=None, servers=None): @@ -547,22 +542,22 @@ class Configuration: except IndexError: raise ValueError( "Invalid index {} when selecting the host settings. " - "Must be less than {}".format(index, len(servers))) + "Must be less than {}".format(index, len(servers)) + ) - url = server['url'] + url = server["url"] # go through variables and replace placeholders - for variable_name, variable in server.get('variables', {}).items(): - used_value = variables.get( - variable_name, variable['default_value']) + for variable_name, variable in server.get("variables", {}).items(): + used_value = variables.get(variable_name, variable["default_value"]) - if 'enum_values' in variable \ - and used_value not in variable['enum_values']: + if "enum_values" in variable and used_value not in variable["enum_values"]: raise ValueError( "The variable `{}` in the host URL has invalid value " "{}. Must be {}.".format( - variable_name, variables[variable_name], - variable['enum_values'])) + variable_name, variables[variable_name], variable["enum_values"] + ) + ) url = url.replace("{" + variable_name + "}", used_value) @@ -575,48 +570,64 @@ class Configuration: """ combined_url = self.api_url if self.api_url is None: - if self.api_host is None or self.api_host == '': - raise FgaValidationException('api_host is required but not configured.') - if self.api_scheme is None or self.api_scheme == '': - raise FgaValidationException('api_scheme is required but not configured.') - combined_url = self.api_scheme + '://' + self.api_host + if self.api_host is None or self.api_host == "": + raise FgaValidationException("api_host is required but not configured.") + if self.api_scheme is None or self.api_scheme == "": + raise FgaValidationException( + "api_scheme is required but not configured." + ) + combined_url = self.api_scheme + "://" + self.api_host parsed_url = None try: parsed_url = urllib.parse.urlparse(combined_url) except ValueError: if self.api_url is None: - raise ApiValueError('Either api_scheme `{}` or api_host `{}` is invalid'.format( - self.api_scheme, self.api_host)) + raise ApiValueError( + "Either api_scheme `{}` or api_host `{}` is invalid".format( + self.api_scheme, self.api_host + ) + ) else: raise ApiValueError(f"api_url `{self.api_url}` is invalid") if self.api_url is None: - if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'): + if parsed_url.scheme != "http" and parsed_url.scheme != "https": raise ApiValueError( - f'api_scheme `{self.api_scheme}` must be either `http` or `https`') - if (parsed_url.netloc == ''): - raise ApiValueError(f'api_host `{self.api_host}` is invalid') - if (parsed_url.path != ''): + f"api_scheme `{self.api_scheme}` must be either `http` or `https`" + ) + if parsed_url.netloc == "": + raise ApiValueError(f"api_host `{self.api_host}` is invalid") + if parsed_url.path != "": raise ApiValueError( - f'api_host `{self.api_scheme}` is not expected to have path specified') - if (parsed_url.query != ''): + f"api_host `{self.api_scheme}` is not expected to have path specified" + ) + if parsed_url.query != "": raise ApiValueError( - f'api_host `{self.api_scheme}` is not expected to have query specified') - - if self.store_id is not None and self.store_id != "" and is_well_formed_ulid_string(self.store_id) is False: + f"api_host `{self.api_scheme}` is not expected to have query specified" + ) + + if ( + self.store_id is not None + and self.store_id != "" + and is_well_formed_ulid_string(self.store_id) is False + ): raise FgaValidationException( - "store_id ('%s') is not in a valid ulid format" % self.store_id) + "store_id ('%s') is not in a valid ulid format" % self.store_id + ) if self._credentials is not None: self._credentials.validate_credentials_config() if self._timeout_millisec is not None: if not isinstance(self._timeout_millisec, int): - raise FgaValidationException(f"timeout_millisec unexpected type {self._timeout_millisec}") + raise FgaValidationException( + f"timeout_millisec unexpected type {self._timeout_millisec}" + ) ten_minutes = 10000 * 60 if self._timeout_millisec < 0 or self._timeout_millisec > ten_minutes: - raise FgaValidationException(f"timeout_millisec not within reasonable range (0,60000), {self._timeout_millisec}") - + raise FgaValidationException( + f"timeout_millisec not within reasonable range (0,60000), {self._timeout_millisec}" + ) @property def api_scheme(self): diff --git a/config/clients/python/template/src/help.py.mustache b/config/clients/python/template/src/help.py.mustache index 659398f4..73ac80c3 100644 --- a/config/clients/python/template/src/help.py.mustache +++ b/config/clients/python/template/src/help.py.mustache @@ -3,89 +3,104 @@ import json import platform import sys -from collections import OrderedDict - -import opentelemetry.version from . import __version__ as openfga_sdk_version -try: - import urllib3 +def get_urllib3_version() -> str: + try: + import urllib3 + + return urllib3.__version__ + except ModuleNotFoundError: + return "" + + +def get_dateutil_version() -> str: + try: + import dateutil # type: ignore[import-untyped] + version = dateutil.__version__ + + if type(version) is not str: + try: + version = str(version) + except Exception: + pass + + if type(version) is str: + return version - urllib3_version = urllib3.__version__ -except ModuleNotFoundError: - urllib3_version = "" + except ModuleNotFoundError: + pass -try: - import dateutil + return "" - dateutil_version = dateutil.__version__ -except ModuleNotFoundError: - dateutil_version = "" -try: - import aiohttp +def get_aiohttp_version() -> str: + try: + import aiohttp + + return aiohttp.__version__ + except ModuleNotFoundError: + return "" - aiohttp_version = aiohttp.__version__ -except ModuleNotFoundError: - aiohttp_version = "" -try: - import opentelemetry +def get_opentelemetry_version() -> str: + try: + import opentelemetry.version - opentelemetry_version = opentelemetry.version.__version__ -except ModuleNotFoundError: - opentelemetry_version = "" + return opentelemetry.version.__version__ + except ModuleNotFoundError: + return "" -def info() -> dict[str, dict[str, str]]: +def info() -> dict[str, str | dict[str, str] | dict[str, dict[str, str]]]: """ Generate information for a bug report. Based on the requests package help utility module. """ + platform_info: dict[str, str] = {"system": "Unknown", "release": "Unknown"} + implementation_version: str = "Unknown" + try: - platform_info = { - "system": platform.system(), - "release": platform.release(), - } - except OSError: - platform_info = {"system": "Unknown", "release": "Unknown"} + platform_info["system"] = platform.system() + platform_info["release"] = platform.release() + except Exception: + pass - implementation = platform.python_implementation() + implementation: str = platform.python_implementation() if implementation == "CPython": implementation_version = platform.python_version() - elif implementation == "PyPy": + + if implementation == "PyPy": pypy_version_info = sys.pypy_version_info # type: ignore[attr-defined] + implementation_version = ( f"{pypy_version_info.major}." f"{pypy_version_info.minor}." f"{pypy_version_info.micro}" ) + if pypy_version_info.releaselevel != "final": implementation_version = "".join( [implementation_version, pypy_version_info.releaselevel] ) - else: - implementation_version = "Unknown" - - return OrderedDict( - { - "platform": platform_info, - "implementation": { - "name": implementation, - "version": implementation_version, - }, - "openfga_sdk": {"version": openfga_sdk_version}, - "dependencies": { - "urllib3": {"version": urllib3_version}, - "python-dateutil": {"version": dateutil_version}, - "aiohttp": {"version": aiohttp_version}, - "opentelemetry": {"version": opentelemetry_version}, - }, - } - ) + + return { + "platform": platform_info, + "implementation": { + "name": implementation, + "version": implementation_version, + }, + "openfga_sdk": {"version": openfga_sdk_version}, + "dependencies": { + "urllib3": {"version": get_urllib3_version()}, + "python-dateutil": {"version": get_dateutil_version()}, + "aiohttp": {"version": get_aiohttp_version()}, + "opentelemetry": {"version": get_opentelemetry_version()}, + }, + } def main() -> None: diff --git a/config/clients/python/template/src/rest.py.mustache b/config/clients/python/template/src/rest.py.mustache index ca6924a7..af2e8516 100644 --- a/config/clients/python/template/src/rest.py.mustache +++ b/config/clients/python/template/src/rest.py.mustache @@ -6,7 +6,7 @@ import logging import re import ssl import urllib -from typing import Any, List, Optional, Tuple +from typing import Any import aiohttp @@ -26,28 +26,37 @@ logger = logging.getLogger(__name__) class RESTResponse(io.IOBase): """ - Represents an HTTP response object. + Represents an HTTP response object in the asynchronous client. """ - def __init__(self, resp: aiohttp.ClientResponse, data: bytes) -> None: + response: aiohttp.ClientResponse + status: int + reason: str | None + data: bytes + + def __init__( + self, + resp: aiohttp.ClientResponse, + data: bytes, + ) -> None: """ - Initializes a RESTResponse with an aiohttp response and corresponding data. + Initializes a RESTResponse with an aiohttp.ClientResponse and corresponding data. :param resp: The aiohttp.ClientResponse object. :param data: The raw byte data read from the response. """ - self.aiohttp_response = resp + self.response = resp self.status = resp.status self.reason = resp.reason self.data = data - def getheaders(self) -> aiohttp.typedefs.LooseHeaders: + def getheaders(self) -> dict[str, str]: """ Returns the response headers. """ - return self.aiohttp_response.headers + return dict(self.response.headers) - def getheader(self, name: str, default: Optional[str] = None) -> Optional[str]: + def getheader(self, name: str, default: str | None = None) -> str | None: """ Returns a specific header value by name. @@ -55,7 +64,7 @@ class RESTResponse(io.IOBase): :param default: The default value if header is not found. :return: The header value, or default if not present. """ - return self.aiohttp_response.headers.get(name, default) + return self.response.headers.get(name, default) class RESTClientObject: @@ -64,7 +73,7 @@ class RESTClientObject: """ def __init__( - self, configuration: Any, pools_size: int = 4, maxsize: Optional[int] = None + self, configuration: Any, pools_size: int = 4, maxsize: int | None = None ) -> None: """ Creates a new RESTClientObject. @@ -92,22 +101,22 @@ class RESTClientObject: self._timeout_millisec = configuration.timeout_millisec self.pool_manager = aiohttp.ClientSession(connector=connector, trust_env=True) - {{#asyncio}}async {{/asyncio}}def close(self) -> None: + async def close(self) -> None: """ Closes the underlying aiohttp.ClientSession. """ - {{#asyncio}}await {{/asyncio}}self.pool_manager.close() + await self.pool_manager.close() - {{#asyncio}}async {{/asyncio}}def build_request( + async def build_request( self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[List[Tuple[str, Any]]] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: list[tuple[str, Any]] | None = None, _preload_content: bool = True, - _request_timeout: Optional[float] = None, + _request_timeout: float | None = None, ) -> dict: """ Builds a dictionary of request arguments suitable for aiohttp. @@ -150,7 +159,8 @@ class RESTClientObject: args["proxy_headers"] = self.proxy_headers if query_params: - args["url"] += "?" + urllib.parse.urlencode(query_params) + encoded_qs = urllib.parse.urlencode(query_params) + args["url"] = f"{url}?{encoded_qs}" if method in ["POST", "PUT", "PATCH", "OPTIONS", "DELETE"]: if re.search("json", headers["Content-Type"], re.IGNORECASE): @@ -180,7 +190,7 @@ class RESTClientObject: return args - {{#asyncio}}async {{/asyncio}}def handle_response_exception( + async def handle_response_exception( self, response: RESTResponse | aiohttp.ClientResponse ) -> None: """ @@ -216,7 +226,7 @@ class RESTClientObject: def _accumulate_json_lines( self, leftover: bytes, data: bytes, buffer: bytearray - ) -> Tuple[bytes, List[Any]]: + ) -> tuple[bytes, list[Any]]: """ Processes a chunk of data and leftover bytes. Splits on newlines, decodes valid JSON, and returns leftover bytes and a list of decoded JSON objects. @@ -226,7 +236,7 @@ class RESTClientObject: :param buffer: The main bytearray buffer for all data. :return: Updated leftover bytes and a list of decoded JSON objects. """ - objects: List[Any] = [] + objects: list[Any] = [] leftover += data lines = leftover.split( b"\n" @@ -241,15 +251,15 @@ class RESTClientObject: logger.warning("Skipping invalid JSON segment: %s", e) return leftover, objects - {{#asyncio}}async {{/asyncio}}def stream( + async def stream( self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[List[Tuple[str, Any]]] = None, - _request_timeout: Optional[float] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: list[tuple[str, Any]] | None = None, + _request_timeout: float | None = None, ): """ Streams JSON objects from a specified endpoint, handling partial chunks @@ -266,7 +276,7 @@ class RESTClientObject: """ # Build our request payload - args = {{#asyncio}}await {{/asyncio}}self.build_request( + args = await self.build_request( method, url, query_params=query_params, @@ -280,15 +290,15 @@ class RESTClientObject: # Initialize buffers for data chunks buffer = bytearray() leftover = b"" - response: Optional[aiohttp.ClientResponse] = None + response: aiohttp.ClientResponse | None = None try: # Send request, collect response handler - {{#asyncio}}async {{/asyncio}}with self.pool_manager.request(**args) as resp: + async with self.pool_manager.request(**args) as resp: response = resp try: # Iterate over streamed/chunked response data - {{#asyncio}}async {{/asyncio}}for data, _ in resp.content.iter_chunks(): + async for data, _ in resp.content.iter_chunks(): if data: # Process data chunk leftover, decoded_objects = self._accumulate_json_lines( @@ -321,27 +331,27 @@ class RESTClientObject: # Decode the complete/buffered data for logging purposes if isinstance(response, aiohttp.ClientResponse): - response.data = buffer.decode("utf-8") + logger.debug("response body: %s", buffer.decode("utf-8")) # Handle any HTTP errors that may have occurred - {{#asyncio}}await {{/asyncio}}self.handle_response_exception(response) + await self.handle_response_exception(response) # Release the response object (required!) response.release() # Release the connection back to the pool - {{#asyncio}}await {{/asyncio}}self.close() + await self.close() - {{#asyncio}}async {{/asyncio}}def request( + async def request( self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[List[Tuple[str, Any]]] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: list[tuple[str, Any]] | None = None, _preload_content: bool = True, - _request_timeout: Optional[float] = None, + _request_timeout: float | None = None, ) -> RESTResponse | aiohttp.ClientResponse: """ Executes a request and returns the response object. @@ -358,7 +368,7 @@ class RESTClientObject: """ # Build our request payload - args = {{#asyncio}}await {{/asyncio}}self.build_request( + args = await self.build_request( method, url, query_params=query_params, @@ -370,20 +380,21 @@ class RESTClientObject: ) # Send request, collect response handler - resp = {{#asyncio}}await {{/asyncio}}self.pool_manager.request(**args) + wrapped_response: RESTResponse | None = None + raw_response: aiohttp.ClientResponse = await self.pool_manager.request(**args) # If we want to preload the response, read it if _preload_content: # Collect response data - data = {{#asyncio}}await {{/asyncio}}resp.read() + data = await raw_response.read() # Transform response JSON data into RESTResponse object - resp = RESTResponse(resp, data) + wrapped_response = RESTResponse(raw_response, data) # Log the response body - logger.debug(f"response body: {resp.data}") + logger.debug("response body: %s", data.decode("utf-8")) # Handle any errors that may have occurred - {{#asyncio}}await {{/asyncio}}self.handle_response_exception(resp) + await self.handle_response_exception(raw_response) - return resp + return wrapped_response or raw_response diff --git a/config/clients/python/template/src/sync/api.py.mustache b/config/clients/python/template/src/sync/api.py.mustache index 5feedbb6..4b2ef225 100644 --- a/config/clients/python/template/src/sync/api.py.mustache +++ b/config/clients/python/template/src/sync/api.py.mustache @@ -277,7 +277,7 @@ class {{classname}}: response_types_map = {} {{/returnType}} - telemetry_attributes: dict[TelemetryAttribute, str | int] = { + telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] = { TelemetryAttributes.fga_client_request_method: "{{operationId}}", TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), TelemetryAttributes.fga_client_request_model_id: local_var_params.get( diff --git a/config/clients/python/template/src/sync/api_client.py.mustache b/config/clients/python/template/src/sync/api_client.py.mustache index cc455a50..e0a5beb7 100644 --- a/config/clients/python/template/src/sync/api_client.py.mustache +++ b/config/clients/python/template/src/sync/api_client.py.mustache @@ -13,7 +13,7 @@ import tornado.gen {{/tornado}} from multiprocessing.pool import ThreadPool -from dateutil.parser import parse +from dateutil.parser import parse # type: ignore[import-untyped] import {{modelPackage}} from {{packageName}}.sync import rest, oauth2 @@ -31,12 +31,13 @@ from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAt DEFAULT_USER_AGENT = '{{{userAgent}}}' -def random_time(loop_count, min_wait_in_ms): +def random_time(loop_count, min_wait_in_ms) -> float: """ Helper function to return the time (in s) to wait before retry """ minimum = math.ceil(2 ** loop_count * min_wait_in_ms) maximum = math.ceil(2 ** (loop_count + 1) * min_wait_in_ms) + return random.randrange(minimum, maximum) / 1000 @@ -113,11 +114,12 @@ class ApiClient: self._pool.close() self._pool.join() self._pool = None - if hasattr(atexit, 'unregister'): + + if hasattr(atexit, 'unregister') and callable(atexit.unregister): atexit.unregister(self.close) @property - def pool(self): + def pool(self) -> ThreadPool: """Create thread pool on first request avoids instantiating unused threadpool for blocking clients. """ @@ -160,7 +162,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): @@ -497,7 +499,7 @@ class ApiClient: _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, _streaming: bool = False, ): """Makes the HTTP request (synchronous) and returns deserialized data. diff --git a/config/clients/python/template/src/sync/client/client.py.mustache b/config/clients/python/template/src/sync/client/client.py.mustache index c1a15f3d..49252e60 100644 --- a/config/clients/python/template/src/sync/client/client.py.mustache +++ b/config/clients/python/template/src/sync/client/client.py.mustache @@ -62,22 +62,31 @@ def _chuck_array(array, max_size): return [array[i * max_size:(i + 1) * max_size] for i in range((len(array) + max_size - 1) // max_size )] -def set_heading_if_not_set(options: dict[str, int|str], name: str, value: str): +def set_heading_if_not_set( + options: dict[str, int | str | dict[str, int | str]] | None, + name: str, + value: str, +) -> dict[str, int | str | dict[str, int | str]]: """ Set heading to the value if it is not set """ - if options is None: - options = {} - headers = options.get("headers") - if headers is None: - headers = {} - if headers.get(name) is None: - headers[name] = value - options["headers"] = headers - return options - - -def options_to_kwargs(options: dict[str, int|str] = None): + _options: dict[str, int | str | dict[str, int | str]] = ( + options if options is not None else {} + ) + + if type(_options.get("headers")) is not dict: + _options["headers"] = {} + + if type(_options["headers"]) is dict: + if type(_options["headers"].get(name)) not in [int, str]: + _options["headers"][name] = value + + return _options + + +def options_to_kwargs( + options: dict[str, int | str | dict[str, int | str]] | None = None, +) -> dict[str, int | str | dict[str, int | str]]: """ Return kwargs with continuation_token and page_size """ @@ -93,7 +102,7 @@ def options_to_kwargs(options: dict[str, int|str] = None): kwargs["_retry_params"] = options["retry_params"] return kwargs -def options_to_transaction_info(options: dict[str, int|str] = None): +def options_to_transaction_info(options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the transaction info """ @@ -113,7 +122,7 @@ class OpenFgaClient: OpenFgaClient is the entry point for invoking calls against the OpenFGA API. """ - def __init__(self, configuration: ClientConfiguration): + def __init__(self, configuration: ClientConfiguration) -> None: self._client_configuration = configuration self._api_client = ApiClient(configuration) self._api = OpenFgaApi(self._api_client) @@ -121,13 +130,16 @@ class OpenFgaClient: def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: self.close() - def close(self): + def close(self) -> None: self._api.close() - def _get_authorization_model_id(self, options: object) -> str | None: + def _get_authorization_model_id( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Return the authorization model ID if specified in the options. Otherwise, return the authorization model ID stored in the client's configuration @@ -142,13 +154,21 @@ class OpenFgaClient: "authorization_model_id ('%s') is not in a valid ulid format" % authorization_model_id) return authorization_model_id - def _get_consistency(self, options: object) -> str | None: + def _get_consistency( + self, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ) -> str | None: """ Returns the consistency requested if specified in the options. Otherwise, returns None. """ - if options is not None and "consistency" in options: - return options["consistency"] + consistency: int | str | dict[str, int | str] | None = ( + options.get("consistency", None) if options is not None else None + ) + + if type(consistency) is str: + return consistency + return None def set_store_id(self, value): @@ -179,7 +199,7 @@ class OpenFgaClient: # Stores ################# - def list_stores(self, options: dict[str, int|str] = None): + def list_stores(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ List the stores in the system :param page_size(options) - Number of items returned per request @@ -196,7 +216,7 @@ class OpenFgaClient: ) return api_response - def create_store(self, body: CreateStoreRequest, options: dict[str, int|str] = None): + def create_store(self, body: CreateStoreRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Create the stores in the system :param header(options) - Custom headers to send alongside the request @@ -211,7 +231,7 @@ class OpenFgaClient: ) return api_response - def get_store(self, options: dict[str, int|str] = None): + def get_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Get the store info in the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -225,7 +245,7 @@ class OpenFgaClient: ) return api_response - def delete_store(self, options: dict[str, int|str] = None): + def delete_store(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Delete the store from the system. Store id is from the configuration. :param header(options) - Custom headers to send alongside the request @@ -243,7 +263,7 @@ class OpenFgaClient: # Authorization Models ####################### - def read_authorization_models(self, options: dict[str, int|str] = None): + def read_authorization_models(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the authorization models for a particular store. :param header(options) - Custom headers to send alongside the request @@ -257,7 +277,7 @@ class OpenFgaClient: ) return api_response - def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int|str] = None): + def write_authorization_model(self, body: WriteAuthorizationModelRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write authorization model. :param body - WriteAuthorizationModelRequest @@ -273,7 +293,7 @@ class OpenFgaClient: ) return api_response - def read_authorization_model(self, options: dict[str, int|str] = None): + def read_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read an authorization model. :param header(options) - Custom headers to send alongside the request @@ -289,7 +309,7 @@ class OpenFgaClient: ) return api_response - def read_latest_authorization_model(self, options: dict[str, int|str] = None): + def read_latest_authorization_model(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method of reading the latest authorization model :param header(options) - Custom headers to send alongside the request @@ -307,7 +327,7 @@ class OpenFgaClient: # Relationship Tuples ####################### - def read_changes(self, body: ClientReadChangesRequest, options: dict[str, str] = None): + def read_changes(self, body: ClientReadChangesRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the type we want to look for change @@ -319,14 +339,19 @@ class OpenFgaClient: :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated """ kwargs = options_to_kwargs(options) - kwargs["type"] = body.type - kwargs["start_time"] = body.start_time + + if body.type is not None: + kwargs["type"] = body.type + + if body.start_time is not None: + kwargs["start_time"] = body.start_time + api_response = self._api.read_changes( **kwargs, ) return api_response - def read(self, body: ReadRequestTupleKey, options: dict[str, str] = None): + def read(self, body: ReadRequestTupleKey, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Read changes for specified type :param body - the tuples we want to read @@ -365,7 +390,7 @@ class OpenFgaClient: ) return api_response - def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, str] = None): + def _write_single_batch(self, batch: list[ClientTuple], is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): try: write_batch = None delete_batch = None @@ -380,7 +405,7 @@ class OpenFgaClient: except Exception as err: return [construct_write_single_response(i, False, err) for i in batch] - def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, str] = None): + def _write_batches(self, tuple_keys: list[ClientTuple], transaction: WriteTransactionOpts, is_write: bool, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Internal function for write/delete batches """ @@ -395,7 +420,7 @@ class OpenFgaClient: return batch_write_responses - def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, str] = None): + def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples """ @@ -424,7 +449,7 @@ class OpenFgaClient: deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - def write(self, body: ClientWriteRequest, options: dict[str, str] = None): + def write(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Write or deletes tuples :param body - the write request @@ -450,7 +475,7 @@ class OpenFgaClient: deletes_response = self._write_batches(body.deletes, transaction, False, options) return ClientWriteResponse(writes=writes_response, deletes=deletes_response) - def write_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + def write_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for writing tuples :param body - the list of tuples we want to write @@ -463,7 +488,7 @@ class OpenFgaClient: result = self.write(ClientWriteRequest(body, None), options) return result - def delete_tuples(self, body: list[ClientTuple], options: dict[str, str] = None): + def delete_tuples(self, body: list[ClientTuple], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Convenient method for deleteing tuples :param body - the list of tuples we want to delete @@ -479,7 +504,7 @@ class OpenFgaClient: ####################### # Relationship Queries ####################### - def check(self, body: ClientCheckRequest, options: dict[str, str] = None): + def check(self, body: ClientCheckRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Check whether a user is authorized to access an object :param body - ClientCheckRequest defining check request @@ -512,7 +537,11 @@ class OpenFgaClient: ) return api_response - def _single_client_batch_check(self, body: ClientCheckRequest, options: dict[str, str] = None): + def _single_client_batch_check( + self, + body: ClientCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a single batch request and return body in a SingleBatchCheckResponse :param body - ClientCheckRequest defining check request @@ -526,7 +555,11 @@ class OpenFgaClient: except Exception as err: return ClientBatchCheckClientResponse(allowed=False, request=body, response=None, error=err) - def client_batch_check(self, body: list[ClientCheckRequest], options: dict[str, str | int] = None): + def client_batch_check( + self, + body: list[ClientCheckRequest], + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a set of checks :param body - list of ClientCheckRequest defining check request @@ -565,7 +598,7 @@ class OpenFgaClient: def _single_batch_check( self, body: BatchCheckRequest, - options: dict[str, str] = None, + options: dict[str, int | str | dict[str, int | str]] | None = None, ): """ Run a single BatchCheck request @@ -580,7 +613,11 @@ class OpenFgaClient: except Exception as err: raise err - def batch_check(self, body: ClientBatchCheckRequest, options=None): + def batch_check( + self, + body: ClientBatchCheckRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ Run a batchcheck request :param body - BatchCheck request @@ -670,7 +707,7 @@ class OpenFgaClient: return ClientBatchCheckResponse(result) - def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): + def expand(self, body: ClientExpandRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run expand request :param body - list of ClientExpandRequest defining expand request @@ -701,7 +738,7 @@ class OpenFgaClient: ) return api_response - def list_objects(self, body: ClientListObjectsRequest, options: dict[str, str] = None): + def list_objects(self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run list object request :param body - list object parameters @@ -733,7 +770,7 @@ class OpenFgaClient: return api_response def streamed_list_objects( - self, body: ClientListObjectsRequest, options: dict[str, str] = None + self, body: ClientListObjectsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None ): """ Retrieve all objects of the given type that the user has a relation with, using the streaming ListObjects API. @@ -769,7 +806,7 @@ class OpenFgaClient: return - def list_relations(self, body: ClientListRelationsRequest, options: dict[str, str] = None): + def list_relations(self, body: ClientListRelationsRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return all the relations for which user has a relationship with the object :param body - list relation request @@ -791,7 +828,7 @@ class OpenFgaClient: return [i.request.relation for i in result_list] - def list_users(self, body: ClientListUsersRequest, options: dict[str, str] = None): + def list_users(self, body: ClientListUsersRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Run list users request :param body - list user parameters @@ -825,7 +862,7 @@ class OpenFgaClient: ####################### # Assertions ####################### - def read_assertions(self, options: dict[str, str] = None): + def read_assertions(self, options: dict[str, int | str | dict[str, int | str]] | None = None): """ Return the assertions :param authorization_model_id(options) - Overrides the authorization model id in the configuration @@ -840,7 +877,7 @@ class OpenFgaClient: api_response = self._api.read_assertions(authorization_model_id, **kwargs) return api_response - def write_assertions(self, body: list[ClientAssertion], options: dict[str, str] = None): + def write_assertions(self, body: list[ClientAssertion], options: dict[str, int | str | dict[str, int | str]] | None = None): """ Upsert the assertions :param body - Write assertion request @@ -853,7 +890,7 @@ class OpenFgaClient: kwargs = options_to_kwargs(options) authorization_model_id=self._get_authorization_model_id(options) - def map_to_assertion(client_assertion: ClientAssertion): + def map_to_assertion(client_assertion: ClientAssertion) -> Assertion: return Assertion(TupleKey( user=client_assertion.user, relation=client_assertion.relation, diff --git a/config/clients/python/template/src/sync/rest.py.mustache b/config/clients/python/template/src/sync/rest.py.mustache index 7febe958..8b7eaf71 100644 --- a/config/clients/python/template/src/sync/rest.py.mustache +++ b/config/clients/python/template/src/sync/rest.py.mustache @@ -6,7 +6,7 @@ import logging import re import ssl import urllib -from typing import Any, List, Optional, Tuple +from typing import Any import urllib3 @@ -26,28 +26,37 @@ logger = logging.getLogger(__name__) class RESTResponse(io.IOBase): """ - Represents an HTTP response object in the non-async client. + Represents an HTTP response object in the synchronous client. """ - def __init__(self, resp: urllib3.HTTPResponse, data: bytes) -> None: + response: urllib3.BaseHTTPResponse + status: int + reason: str | None + data: bytes + + def __init__( + self, + resp: urllib3.BaseHTTPResponse, + data: bytes, + ) -> None: """ - Initializes a RESTResponse with a urllib3.HTTPResponse and corresponding data. + Initializes a RESTResponse with a urllib3.BaseHTTPResponse and corresponding data. - :param resp: The urllib3.HTTPResponse object. + :param resp: The urllib3.BaseHTTPResponse object. :param data: The raw byte data read from the response. """ - self.urllib3_response = resp + self.response = resp self.status = resp.status self.reason = resp.reason self.data = data - def getheaders(self) -> dict: + def getheaders(self) -> dict[str, str]: """ Returns a dictionary of the response headers. """ - return self.urllib3_response.headers + return dict(self.response.headers) - def getheader(self, name: str, default: Optional[str] = None) -> Optional[str]: + def getheader(self, name: str, default: str | None = None) -> str | None: """ Returns a specific header value by name. @@ -55,7 +64,7 @@ class RESTResponse(io.IOBase): :param default: The default value if header is not found. :return: The header value, or default if not present. """ - return self.urllib3_response.headers.get(name, default) + return self.response.headers.get(name, default) class RESTClientObject: @@ -64,7 +73,10 @@ class RESTClientObject: """ def __init__( - self, configuration: Any, pools_size: int = 4, maxsize: Optional[int] = None + self, + configuration: Any, + pools_size: int = 4, + maxsize: int | None = None, ) -> None: """ Creates a new RESTClientObject using urllib3. @@ -107,16 +119,18 @@ class RESTClientObject: self._timeout_millisec = configuration.timeout_millisec if hasattr(configuration, "proxy") and configuration.proxy is not None: - self.pool_manager = urllib3.ProxyManager( - num_pools=pools_size, - maxsize=maxsize, - cert_reqs=cert_reqs, - ca_certs=configuration.ssl_ca_cert, - cert_file=configuration.cert_file, - key_file=configuration.key_file, - proxy_url=configuration.proxy, - proxy_headers=configuration.proxy_headers, - **addition_pool_args, + self.pool_manager: urllib3.ProxyManager | urllib3.PoolManager = ( + urllib3.ProxyManager( + num_pools=pools_size, + maxsize=maxsize, + cert_reqs=cert_reqs, + ca_certs=configuration.ssl_ca_cert, + cert_file=configuration.cert_file, + key_file=configuration.key_file, + proxy_url=configuration.proxy, + proxy_headers=configuration.proxy_headers, + **addition_pool_args, + ) ) return @@ -141,12 +155,12 @@ class RESTClientObject: self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[dict] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: dict | None = None, _preload_content: bool = True, - _request_timeout: Optional[float | tuple] = None, + _request_timeout: float | tuple | None = None, ) -> dict: """ Builds a dictionary of request arguments suitable for urllib3. @@ -234,7 +248,7 @@ class RESTClientObject: return args def handle_response_exception( - self, response: RESTResponse | urllib3.HTTPResponse + self, response: RESTResponse | urllib3.BaseHTTPResponse ) -> None: """ Raises exceptions if response status indicates an error. @@ -262,7 +276,7 @@ class RESTClientObject: def _accumulate_json_lines( self, leftover: bytes, data: bytes, buffer: bytearray - ) -> Tuple[bytes, List[Any]]: + ) -> tuple[bytes, list[Any]]: """ Processes a chunk of data plus any leftover bytes from a previous iteration. Splits on newlines, decodes valid JSON lines, and returns updated leftover bytes @@ -273,7 +287,7 @@ class RESTClientObject: :param buffer: The main bytearray buffer for all data in this request. :return: A tuple of (updated leftover bytes, list of decoded objects). """ - objects: List[Any] = [] + objects: list[Any] = [] leftover += data lines = leftover.split( b"\n" @@ -295,11 +309,11 @@ class RESTClientObject: self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[dict] = None, - _request_timeout: Optional[float | tuple] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: dict | None = None, + _request_timeout: float | tuple | None = None, ): """ Streams JSON objects from a specified endpoint, reassembling partial chunks @@ -335,9 +349,9 @@ class RESTClientObject: response = self.pool_manager.request(**args) try: - # Iterate over streamed/chunked response data + # Iterate over streamed/chunked response data for chunk in response.stream(1024): - # Process data chunk + # Process data chunk leftover, decoded_objects = self._accumulate_json_lines( leftover, chunk, buffer ) @@ -376,13 +390,13 @@ class RESTClientObject: self, method: str, url: str, - query_params: Optional[dict] = None, - headers: Optional[dict] = None, - body: Optional[Any] = None, - post_params: Optional[dict] = None, + query_params: dict | None = None, + headers: dict | None = None, + body: Any | None = None, + post_params: dict | None = None, _preload_content: bool = True, - _request_timeout: Optional[float | tuple] = None, - ) -> RESTResponse | urllib3.HTTPResponse: + _request_timeout: float | tuple | None = None, + ) -> RESTResponse | urllib3.BaseHTTPResponse: """ Executes a request and returns the response object. @@ -412,20 +426,21 @@ class RESTClientObject: ) # Send request, collect response handler - resp = self.pool_manager.request(**args) + wrapped_response: RESTResponse | None = None + raw_response: urllib3.BaseHTTPResponse = self.pool_manager.request(**args) # If we want to preload the response, read it if _preload_content: # Collect response data and transform response (JSON) into RESTResponse object - resp = RESTResponse(resp, resp.data) + wrapped_response = RESTResponse(raw_response, raw_response.data) # Log the response body - logger.debug("response body: %s", resp.data) + logger.debug("response body: %s", wrapped_response.data.decode("utf-8")) # Handle any errors that may have occurred - self.handle_response_exception(resp) + self.handle_response_exception(raw_response) # Release the connection back to the pool self.close() - return resp + return wrapped_response or raw_response diff --git a/config/clients/python/template/src/telemetry/attributes.py.mustache b/config/clients/python/template/src/telemetry/attributes.py.mustache index ff35b006..5477c461 100644 --- a/config/clients/python/template/src/telemetry/attributes.py.mustache +++ b/config/clients/python/template/src/telemetry/attributes.py.mustache @@ -10,9 +10,6 @@ from urllib3 import HTTPResponse from {{packageName}}.credentials import Credentials from {{packageName}}.exceptions import ApiException from {{packageName}}.rest import RESTResponse -from {{packageName}}.telemetry.utilities import ( - doesInstanceHaveCallable, -) class TelemetryAttribute(NamedTuple): @@ -22,8 +19,7 @@ class TelemetryAttribute(NamedTuple): class TelemetryAttributes: fga_client_request_batch_check_size: TelemetryAttribute = TelemetryAttribute( - name="fga-client.request.batch_check_size", - format="int" + name="fga-client.request.batch_check_size", format="int" ) fga_client_request_client_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.client_id", @@ -94,13 +90,14 @@ class TelemetryAttributes: user_agent_original, ] + @staticmethod + def getAll() -> list[TelemetryAttribute]: + return TelemetryAttributes._attributes + @staticmethod def get( name: str | None = None, - ) -> list[TelemetryAttribute] | TelemetryAttribute | None: - if name is None: - return TelemetryAttributes._attributes - + ) -> TelemetryAttribute | None: for attribute in TelemetryAttributes._attributes: if attribute.name == name: return attribute @@ -109,10 +106,10 @@ class TelemetryAttributes: @staticmethod def prepare( - attributes: dict[TelemetryAttribute, str | int] | None, - filter: list[TelemetryAttribute] | None = None, - ) -> dict[str, str | int]: - response = {} + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + filter: list[TelemetryAttribute] | dict[TelemetryAttribute, bool] | None = None, + ) -> dict[str, str | bool | int | float]: + response: dict[str, str | bool | int | float] = {} if filter is None or filter == []: return response @@ -173,8 +170,11 @@ class TelemetryAttributes: return response @staticmethod - def fromBody(body: Any, attributes: dict[TelemetryAttribute, str | int] = None): - from {{packageName}}.models.batch_check_request import BatchCheckRequest + def fromBody( + body: Any, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ): + from openfga_sdk.models.batch_check_request import BatchCheckRequest if attributes is None: attributes = {} @@ -191,86 +191,104 @@ class TelemetryAttributes: @staticmethod def fromRequest( - user_agent: str = None, - fga_method: str = None, - http_method: str = None, - url: str = None, - resend_count: int = None, - start: float = None, - credentials: Credentials = None, - attributes: dict[TelemetryAttribute, str | int] = None, - ) -> dict[TelemetryAttribute, str | int]: - if attributes is None: - attributes = {} + user_agent: str | None = None, + fga_method: str | None = None, + http_method: str | None = None, + url: str | None = None, + resend_count: int | None = None, + start: float | None = None, + credentials: Credentials | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ) -> dict[TelemetryAttribute, str | bool | int | float]: + _attributes: dict[TelemetryAttribute, str | bool | int | float] = {} + + if attributes is not None: + _attributes = attributes if ( - TelemetryAttributes.fga_client_request_method not in attributes + TelemetryAttributes.fga_client_request_method not in _attributes and fga_method is not None ): fga_method = fga_method.rsplit("/", 1)[-1] if fga_method: - attributes[TelemetryAttributes.fga_client_request_method] = ( + _attributes[TelemetryAttributes.fga_client_request_method] = ( fga_method.rsplit("/", 1)[-1] ) - if TelemetryAttributes.fga_client_request_method in attributes: - fga_method = attributes[TelemetryAttributes.fga_client_request_method] - fga_method = ( - fga_method.lower().replace("_", " ").title().replace(" ", "").strip() - ) + if TelemetryAttributes.fga_client_request_method in _attributes: + _attr_fga_method = _attributes[ + TelemetryAttributes.fga_client_request_method + ] + + if type(_attr_fga_method) is str: + _attr_fga_method = ( + _attr_fga_method.lower() + .replace("_", " ") + .title() + .replace(" ", "") + .strip() + ) - if fga_method: - attributes[TelemetryAttributes.fga_client_request_method] = fga_method - else: - del attributes[TelemetryAttributes.fga_client_request_method] + if _attr_fga_method: + _attributes[TelemetryAttributes.fga_client_request_method] = ( + _attr_fga_method + ) + else: + del _attributes[TelemetryAttributes.fga_client_request_method] if user_agent is not None: - attributes[TelemetryAttributes.user_agent_original] = user_agent + _attributes[TelemetryAttributes.user_agent_original] = user_agent if http_method is not None: - attributes[TelemetryAttributes.http_request_method] = http_method + _attributes[TelemetryAttributes.http_request_method] = http_method if url is not None: - attributes[TelemetryAttributes.http_host] = urllib.parse.urlparse( - url - ).hostname - attributes[TelemetryAttributes.url_scheme] = urllib.parse.urlparse( - url - ).scheme - attributes[TelemetryAttributes.url_full] = url + _hostname = urllib.parse.urlparse(url).hostname + _scheme = urllib.parse.urlparse(url).scheme + + if type(_hostname) is str: + _attributes[TelemetryAttributes.http_host] = _hostname + + if type(_scheme) is str: + _attributes[TelemetryAttributes.url_scheme] = _scheme + + _attributes[TelemetryAttributes.url_full] = url if start is not None and start > 0: - attributes[TelemetryAttributes.http_client_request_duration] = int( + _attributes[TelemetryAttributes.http_client_request_duration] = int( (time.time() - start) * 1000 ) if resend_count is not None and resend_count > 0: - attributes[TelemetryAttributes.http_request_resend_count] = resend_count + _attributes[TelemetryAttributes.http_request_resend_count] = resend_count if credentials is not None: if credentials.method == "client_credentials": - attributes[TelemetryAttributes.fga_client_request_client_id] = ( + _attributes[TelemetryAttributes.fga_client_request_client_id] = ( credentials.configuration.client_id ) - return attributes + return _attributes @staticmethod def fromResponse( - response: HTTPResponse | RESTResponse | ClientResponse | ApiException | None = None, + response: ( + HTTPResponse | RESTResponse | ClientResponse | ApiException | None + ) = None, credentials: Credentials | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, - ) -> dict[TelemetryAttribute, str | int]: + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ) -> dict[TelemetryAttribute, str | bool | int | float]: response_model_id = None response_query_duration = None + _attributes: dict[TelemetryAttribute, str | bool | int | float] = {} - if attributes is None: - attributes = {} + if attributes is not None: + _attributes = attributes if isinstance(response, ApiException): if response.status is not None: - attributes[TelemetryAttributes.http_response_status_code] = int( + _attributes[TelemetryAttributes.http_response_status_code] = int( response.status ) @@ -282,62 +300,67 @@ class TelemetryAttributes: if response is not None: if hasattr(response, "status"): - attributes[TelemetryAttributes.http_response_status_code] = int( + _attributes[TelemetryAttributes.http_response_status_code] = int( response.status ) - if doesInstanceHaveCallable(response, "getheader"): + if hasattr(response, "getheader") and callable(response.getheader): response_model_id = response.getheader("openfga-authorization-model-id") response_query_duration = response.getheader("fga-query-duration-ms") - if doesInstanceHaveCallable(response, "headers"): + if hasattr(response, "headers"): response_model_id = response.headers.get( "openfga-authorization-model-id" ) response_query_duration = response.headers.get("fga-query-duration-ms") if response_model_id is not None: - attributes[TelemetryAttributes.fga_client_response_model_id] = ( + _attributes[TelemetryAttributes.fga_client_response_model_id] = ( response_model_id ) if response_query_duration is not None: - attributes[TelemetryAttributes.http_server_request_duration] = ( + _attributes[TelemetryAttributes.http_server_request_duration] = ( response_query_duration ) if isinstance(credentials, Credentials): if credentials.method == "client_credentials": - attributes[TelemetryAttributes.fga_client_request_client_id] = ( + _attributes[TelemetryAttributes.fga_client_request_client_id] = ( credentials.configuration.client_id ) - return attributes + return _attributes @staticmethod def coalesceAttributeValue( attribute: TelemetryAttribute, - value: int | float | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, - ) -> int | float | None: - if value is None: - if attribute in attributes: - value = attributes[attribute] + value: str | bool | int | float | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, + ) -> str | bool | int | float | None: + _value: str | bool | int | float | None = None if value is not None: + _value = value + else: + if attributes is not None and attribute in attributes.keys(): + _value = attributes.get(attribute) + + if _value is not None: if attribute.format == "int": try: - value = int(value) - except ValueError: - value = None - - if attribute.format == "float": + return int(_value) + except Exception: + pass + elif attribute.format == "float": try: - value = float(value) - except ValueError: - value = None - - if attribute.format == "string": - value = str(value) + return float(_value) + except Exception: + pass + elif attribute.format == "string": + try: + return str(_value) + except Exception: + pass - return value + return None diff --git a/config/clients/python/template/src/telemetry/configuration.py.mustache b/config/clients/python/template/src/telemetry/configuration.py.mustache index 5cb354e8..0e75500b 100644 --- a/config/clients/python/template/src/telemetry/configuration.py.mustache +++ b/config/clients/python/template/src/telemetry/configuration.py.mustache @@ -1,6 +1,6 @@ {{>partial_header}} -from typing import NamedTuple +from typing import NamedTuple, Protocol, Type, runtime_checkable from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters @@ -18,7 +18,7 @@ class TelemetryMetricConfiguration: def __init__( self, - config: dict[TelemetryAttribute, bool] | None = None, + config: dict[TelemetryAttribute | str, bool] | None = None, fga_client_request_client_id: bool | None = None, fga_client_request_method: bool | None = None, fga_client_request_model_id: bool | None = None, @@ -511,10 +511,14 @@ class TelemetryMetricConfiguration: # Apply an incoming configuration, if provided if isinstance(config, dict): for attribute, enabled in config.items(): - if isinstance(attribute, str): - attribute = TelemetryAttributes.get(name=attribute) + _attribute: TelemetryAttribute | None = None + + if isinstance(attribute, TelemetryAttribute): + _attribute = attribute + elif isinstance(attribute, str): + _attribute = TelemetryAttributes.get(name=attribute) - if not isinstance(attribute, TelemetryAttribute): + if not isinstance(_attribute, TelemetryAttribute): raise ValueError( f"Invalid attribute type provided in `TelemetryMetricConfiguration`; `TelemetryAttribute` expected, but `{type(attribute)}` was provided.", attribute, @@ -526,13 +530,13 @@ class TelemetryMetricConfiguration: attribute, ) - if attribute not in self._state: + if _attribute not in self._state: raise ValueError( - f"Invalid attribute provided in `TelemetryMetricConfiguration`; `{attribute.name}` is not a supported attribute type for this context.", - attribute, + f"Invalid attribute provided in `TelemetryMetricConfiguration`; `{_attribute.name}` is not a supported attribute type for this context.", + _attribute, ) - self._state[attribute] = enabled + self._state[_attribute] = enabled # Reset the validation state self._valid = None @@ -551,11 +555,11 @@ class TelemetryMetricConfiguration: attributes = self._state if filter_enabled is True: - return [ - attribute + return { + attribute: enabled for attribute, enabled in attributes.items() if enabled is True - ] + } return attributes @@ -600,7 +604,7 @@ class TelemetryMetricConfiguration: return self._valid @staticmethod - def getSdkDefaults() -> dict[TelemetryAttribute, bool]: + def getSdkDefaults() -> dict[TelemetryAttribute | str, bool]: """ Get the default SDK configuration for the telemetry metric. @@ -626,15 +630,54 @@ class TelemetryMetricConfiguration: } -class TelemetryMetricsConfiguration: +@runtime_checkable +class TelemetryMetricsConfigurationProtocol(Protocol): + def clear(self) -> None: ... + + def configure( + self, + config: ( + dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None + ) = None, + clear: bool = False, + ) -> None: ... + + def getMetrics(self, filter_enabled: bool = True) -> dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, + ]: ... + + def isEnabled( + self, metric: TelemetryCounter | TelemetryHistogram | None = None + ) -> bool: ... + + def isValid(self, raise_exception: bool = False) -> bool: ... + + +class TelemetryMetricsConfiguration(TelemetryMetricsConfigurationProtocol): _state: dict[ - TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, ] = {} _valid: bool | None = None def __init__( self, - config: dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None] | None = None, + config: ( + dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None + ) = None, fga_client_credentials_request: TelemetryMetricConfiguration | None = None, fga_client_request_duration: TelemetryMetricConfiguration | None = None, fga_client_query_duration: TelemetryMetricConfiguration | None = None, @@ -681,8 +724,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.request` counter. """ + state = self._state[TelemetryCounters.fga_client_request] + + if isinstance(state, TelemetryMetricConfiguration): + return state - return self._state[TelemetryCounters.fga_client_request] + return None @fga_client_request.setter def fga_client_request(self, value: TelemetryMetricConfiguration | None): @@ -702,8 +749,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.credentials.request` counter. """ + state = self._state[TelemetryCounters.fga_client_credentials_request] + + if isinstance(state, TelemetryMetricConfiguration): + return state - return self._state[TelemetryCounters.fga_client_credentials_request] + return None @fga_client_credentials_request.setter def fga_client_credentials_request( @@ -725,8 +776,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.query.duration` histogram. """ + state = self._state[TelemetryHistograms.fga_client_request_duration] - return self._state[TelemetryHistograms.fga_client_request_duration] + if isinstance(state, TelemetryMetricConfiguration): + return state + + return None @fga_client_request_duration.setter def fga_client_request_duration(self, value: TelemetryMetricConfiguration | None): @@ -746,8 +801,12 @@ class TelemetryMetricsConfiguration: :return: The configuration for the `fga-client.request.duration` histogram. """ + state = self._state[TelemetryHistograms.fga_client_query_duration] - return self._state[TelemetryHistograms.fga_client_query_duration] + if isinstance(state, TelemetryMetricConfiguration): + return state + + return None @fga_client_query_duration.setter def fga_client_query_duration(self, value: TelemetryMetricConfiguration | None): @@ -774,7 +833,15 @@ class TelemetryMetricsConfiguration: def configure( self, - config: dict[TelemetryHistogram | TelemetryCounter | str, TelemetryMetricConfiguration | dict[TelemetryAttribute, bool] | None] | None = None, + config: ( + dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None + ) = None, clear: bool = False, ) -> None: """ @@ -785,13 +852,19 @@ class TelemetryMetricsConfiguration: if isinstance(config, dict): for metric, configuration in config.items(): - if isinstance(metric, str): - metric = TelemetryCounters.get( - name=metric - ) or TelemetryHistograms.get(name=metric) + _metric: TelemetryHistogram | TelemetryCounter | None = None - if not isinstance(metric, TelemetryCounter) and not isinstance( + if isinstance(metric, TelemetryCounter) or isinstance( metric, TelemetryHistogram + ): + _metric = metric + elif isinstance(metric, str): + _metric = TelemetryCounters.get(metric) or TelemetryHistograms.get( + metric + ) + + if not isinstance(_metric, TelemetryCounter) and not isinstance( + _metric, TelemetryHistogram ): raise ValueError( f"Invalid metric type provided in `TelemetryMetricsConfiguration`; `TelemetryHistogram` or `TelemetryCounter` was expected, but `{type(metric)}` was provided.", @@ -810,20 +883,19 @@ class TelemetryMetricsConfiguration: configuration, ) - if metric not in self._state: + if _metric not in self._state: raise ValueError( - f"Invalid metric provided in `TelemetryMetricsConfiguration`; `{metric.name}` is not a supported metric type for this context.", - metric, + f"Invalid metric provided in `TelemetryMetricsConfiguration`; `{_metric.name}` is not a supported metric type for this context.", + _metric, ) - self._state[metric] = configuration + self._state[_metric] = configuration self._valid = None - def getMetrics( - self, filter_enabled: bool = True - ) -> dict[ - TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None + def getMetrics(self, filter_enabled: bool = True) -> dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, ]: """ Returns a list of supported metrics. If `filter_enabled` is `True`, only enabled metrics are returned. @@ -836,11 +908,12 @@ class TelemetryMetricsConfiguration: metrics = self._state if filter_enabled is True: - return [ - metric + return { + metric: configuration for metric, configuration in metrics.items() - if configuration is not None and configuration.isEnabled() - ] + if isinstance(configuration, TelemetryMetricConfiguration) + and configuration.isEnabled() + } return metrics @@ -859,9 +932,11 @@ class TelemetryMetricsConfiguration: # Check if the specified metric is enabled if metric in self._state: + state = self._state[metric] + if ( - self._state[metric] is not None - and self._state[metric].isEnabled() is True + isinstance(state, TelemetryMetricConfiguration) + and state.isEnabled() is True ): return True @@ -875,27 +950,35 @@ class TelemetryMetricsConfiguration: :return: A boolean indicating whether the metrics configuration is valid, including all sub-configurations. """ - - # Check if the validation state is already cached if self._valid is not None: return self._valid enabled = self.getMetrics(filter_enabled=True) # Validate all sub-configurations and cache the result - self._valid = [configuration.isValid() for configuration in enabled.values()] + for configuration in enabled.values(): + if ( + isinstance(configuration, TelemetryMetricConfiguration) + and not configuration.isValid() + ): + self._valid = False + break # If requested, raise an exception if the configuration is invalid if self._valid is False and raise_exception is True: raise ValueError("Invalid TelemetryMetricsConfiguration.") + if self._valid is None: + self._valid = True + # Return the validation state return self._valid @staticmethod - def getSdkDefaults() -> ( - dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None] - ): + def getSdkDefaults() -> dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, + ]: """ Get the default SDK configuration for telemetry metrics. @@ -910,7 +993,7 @@ class TelemetryMetricsConfiguration: class TelemetryConfigurationType(NamedTuple): name: str - configurationClass: object + configurationClass: Type[TelemetryMetricsConfigurationProtocol] class TelemetryConfigurations: @@ -922,12 +1005,13 @@ class TelemetryConfigurations: _configurations: list[TelemetryConfigurationType] = [metrics] @staticmethod - def get( - name: str | None = None, - ) -> list[TelemetryConfigurationType] | TelemetryConfigurationType | None: - if name is None: - return TelemetryConfigurations._configurations + def getAll() -> list[TelemetryConfigurationType]: + return TelemetryConfigurations._configurations + @staticmethod + def get( + name: str, + ) -> TelemetryConfigurationType | None: for configuration in TelemetryConfigurations._configurations: if configuration.name == name: return configuration @@ -936,12 +1020,25 @@ class TelemetryConfigurations: class TelemetryConfiguration: - _state: dict[str, TelemetryMetricsConfiguration | None] = {} + _state: dict[TelemetryConfigurationType, TelemetryMetricsConfiguration | None] = {} _valid: bool | None = None def __init__( self, - config: dict[str, TelemetryMetricsConfiguration | None] | None = None, + config: ( + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None, + ] + | None + ) = None, metrics: TelemetryMetricsConfiguration | None = None, ): """ @@ -950,7 +1047,6 @@ class TelemetryConfiguration: :param config: A dictionary containing the configuration for telemetry. :param metrics: Customize which metrics and attributes are included in telemetry collection. """ - # Instantiate with default state, and apply the incoming configuration, if one was provided self.configure(config=config, clear=True) @@ -964,7 +1060,6 @@ class TelemetryConfiguration: :return: The metrics configuration for telemetry. """ - return self._state[TelemetryConfigurations.metrics] @metrics.setter @@ -974,7 +1069,6 @@ class TelemetryConfiguration: :param value: The metrics configuration for telemetry. """ - if value is not None and not isinstance(value, TelemetryMetricsConfiguration): raise ValueError( "A `metrics` configuration must be an instance of `TelemetryMetricsConfiguration` or `None`." @@ -994,7 +1088,20 @@ class TelemetryConfiguration: def configure( self, - config: dict[TelemetryConfigurationType | str, TelemetryMetricsConfiguration | dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | dict[TelemetryAttribute, bool] | None] | None] | None = None, + config: ( + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration + | dict[TelemetryAttribute | str, bool] + | None, + ] + | None, + ] + | None + ) = None, clear: bool = False, ) -> None: """ @@ -1005,34 +1112,39 @@ class TelemetryConfiguration: if isinstance(config, dict): for context, configuration in config.items(): - if isinstance(context, str): - context = TelemetryConfigurations.get(context) - - if not isinstance(context, TelemetryConfigurationType): - raise ValueError( - f"Invalid context provided in `TelemetryConfiguration`; a valid string or an `TelemetryConfigurationType` instance was expected, but `{type(context)}` was provided.", - context, - ) - - if isinstance(configuration, dict): - configuration = TelemetryMetricsConfiguration(configuration) - - if ( - not isinstance(configuration, context.configurationClass) - and configuration is not None - ): - raise ValueError( - f"Invalid context configuration provided in `TelemetryConfiguration`; a {type(context.configurationClass)} was expected, but `{type(configuration)}` was provided.", - configuration, - ) - - if context not in self._state: - raise ValueError( - f"Invalid context provided in `TelemetryConfiguration`; `{context.name}` is not a supported context type for this configuration context.", - context, - ) - - self._state[context] = configuration + _context: TelemetryConfigurationType | None = None + + if isinstance(context, TelemetryConfigurationType): + _context = context + elif isinstance(context, str): + _context = TelemetryConfigurations.get(context) + + if not isinstance(_context, TelemetryConfigurationType): + raise ValueError( + f"Invalid context provided in `TelemetryConfiguration`; a valid string or an `TelemetryConfigurationType` instance was expected, but `{context}` was provided.", + context, + ) + + if isinstance(_context, TelemetryConfigurationType): + if isinstance(configuration, dict): + configuration = TelemetryMetricsConfiguration(configuration) + + if ( + not isinstance(configuration, _context.configurationClass) + and configuration is not None + ): + raise ValueError( + f"Invalid context configuration provided in `TelemetryConfiguration`; a {type(_context.configurationClass)} was expected, but `{type(configuration)}` was provided.", + configuration, + ) + + if _context not in self._state: + raise ValueError( + f"Invalid context provided in `TelemetryConfiguration`; `{_context.name}` is not a supported context type for this configuration context.", + _context, + ) + + self._state[_context] = configuration self._valid = None @@ -1046,7 +1158,6 @@ class TelemetryConfiguration: :return: A list of enabled contexts. """ - contexts = self._state if filter_enabled is True: @@ -1066,16 +1177,25 @@ class TelemetryConfiguration: :return: A boolean indicating whether telemetry is enabled. """ + _configuration: TelemetryConfigurationType | None = None - if configuration is None: + if isinstance(configuration, TelemetryConfigurationType): + _configuration = configuration + + if isinstance(configuration, str): + _configuration = TelemetryConfigurations.get(name=configuration) + + if _configuration is None: return True if any(self.getConfigurations(filter_enabled=True)) else False - if configuration in self._state: + if _configuration in self._state: + state = self._state[_configuration] + if ( - self._state[configuration] is not None - or self._state[configuration].isEnabled() is True + isinstance(state, TelemetryMetricsConfiguration) + and state.isEnabled() is True ): - return self._state[configuration].isEnabled() + return True return False @@ -1087,25 +1207,34 @@ class TelemetryConfiguration: :return: A boolean indicating whether the telemetry configuration is valid, including all sub-configurations. """ - if self._valid is not None: return self._valid enabled = self.getConfigurations(filter_enabled=True) - self._valid = all( - [configuration.isValid() for configuration in enabled.values()] - ) + for configuration in enabled.values(): + if configuration is not None and not configuration.isValid(): + self._valid = False + break if self._valid is False and raise_exception is True: raise ValueError("Invalid TelemetryConfiguration.") + if self._valid is None: + self._valid = True + return self._valid @staticmethod - def getSdkDefaults() -> ( - dict[TelemetryConfigurationType, TelemetryMetricsConfiguration | None] - ): + def getSdkDefaults() -> dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration | dict[TelemetryAttribute | str, bool] | None, + ] + | None, + ]: """ Get the default SDK configuration for telemetry. @@ -1117,13 +1246,12 @@ class TelemetryConfiguration: def isMetricEnabled( - config: TelemetryConfiguration | TelemetryMetricsConfiguration, + config: TelemetryConfiguration | TelemetryMetricsConfiguration | None, metric: TelemetryCounter | TelemetryHistogram, ) -> bool: """ Check if a particular metric is enabled for telemetry collection. """ - if config is not None and metric is not None: if isinstance(config, TelemetryConfiguration): config = config.metrics diff --git a/config/clients/python/template/src/telemetry/counters.py.mustache b/config/clients/python/template/src/telemetry/counters.py.mustache index 96bbe8a1..e6a19136 100644 --- a/config/clients/python/template/src/telemetry/counters.py.mustache +++ b/config/clients/python/template/src/telemetry/counters.py.mustache @@ -25,13 +25,14 @@ class TelemetryCounters: fga_client_request, ] + @staticmethod + def getAll() -> list[TelemetryCounter]: + return TelemetryCounters._counters + @staticmethod def get( name: str | None = None, - ) -> list[TelemetryCounter] | TelemetryCounter | None: - if name is None: - return TelemetryCounters._counters - + ) -> TelemetryCounter | None: for counter in TelemetryCounters._counters: if counter.name == name: return counter diff --git a/config/clients/python/template/src/telemetry/histograms.py.mustache b/config/clients/python/template/src/telemetry/histograms.py.mustache index 26a1ca78..8db0ac86 100644 --- a/config/clients/python/template/src/telemetry/histograms.py.mustache +++ b/config/clients/python/template/src/telemetry/histograms.py.mustache @@ -24,13 +24,14 @@ class TelemetryHistograms: fga_client_query_duration, ] + @staticmethod + def getAll() -> list[TelemetryHistogram]: + return TelemetryHistograms._histograms + @staticmethod def get( name: str | None = None, - ) -> list[TelemetryHistogram] | TelemetryHistogram | None: - if name is None: - return TelemetryHistograms._histograms - + ) -> TelemetryHistogram | None: for histogram in TelemetryHistograms._histograms: if histogram.name == name: return histogram diff --git a/config/clients/python/template/src/telemetry/metrics.py.mustache b/config/clients/python/template/src/telemetry/metrics.py.mustache index 3aecae99..5b34fb66 100644 --- a/config/clients/python/template/src/telemetry/metrics.py.mustache +++ b/config/clients/python/template/src/telemetry/metrics.py.mustache @@ -8,6 +8,7 @@ from {{packageName}}.telemetry.attributes import ( ) from {{packageName}}.telemetry.configuration import ( TelemetryConfiguration, + TelemetryMetricConfiguration, isMetricEnabled, ) from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters @@ -15,7 +16,7 @@ from {{packageName}}.telemetry.histograms import TelemetryHistogram, TelemetryHi class TelemetryMetrics: - _meter: Meter = None + _meter: Meter | None = None _histograms: dict[str, Histogram] = {} _counters: dict[str, Counter] = {} @@ -66,7 +67,7 @@ class TelemetryMetrics: def request( self, value: int = 1, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Counter: """ @@ -75,20 +76,33 @@ class TelemetryMetrics: counter = self.counter(TelemetryCounters.fga_client_request) if isMetricEnabled(configuration, TelemetryCounters.fga_client_request): - attributes = TelemetryAttributes.prepare( + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_request, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = ( + configuration.metrics.fga_client_request.getAttributes() + ) + + prepared_attributes = TelemetryAttributes.prepare( attributes, - filter=configuration.metrics.fga_client_request.getAttributes(), + filter=attribute_filters, ) - if value is not None: - counter.add(amount=value, attributes=attributes) + counter.add(amount=value, attributes=prepared_attributes) return counter def credentialsRequest( self, value: int = 1, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Counter: """ @@ -99,76 +113,125 @@ class TelemetryMetrics: if isMetricEnabled( configuration, TelemetryCounters.fga_client_credentials_request ): - attributes = TelemetryAttributes.prepare( + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_credentials_request, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = ( + configuration.metrics.fga_client_credentials_request.getAttributes() + ) + + prepared_attributes = TelemetryAttributes.prepare( attributes, - filter=configuration.metrics.fga_client_credentials_request.getAttributes(), + filter=attribute_filters, ) - if value is not None: - counter.add(amount=value, attributes=attributes) + counter.add(amount=value, attributes=prepared_attributes) # type: ignore[arg-type] return counter def requestDuration( self, value: int | float | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Histogram: """ Record the duration of a request made by the client. """ histogram = self.histogram(TelemetryHistograms.fga_client_request_duration) + _attributes: dict[TelemetryAttribute, str | bool | int | float] = ( + attributes or {} + ) if isMetricEnabled( configuration, TelemetryHistograms.fga_client_request_duration ): - attributes[TelemetryAttributes.http_client_request_duration] = value = ( - TelemetryAttributes.coalesceAttributeValue( - TelemetryAttributes.http_client_request_duration, - value, - attributes, - ) + coalesced_value = TelemetryAttributes.coalesceAttributeValue( + TelemetryAttributes.http_client_request_duration, + value, + _attributes, ) - attributes = TelemetryAttributes.prepare( - attributes, - filter=configuration.metrics.fga_client_request_duration.getAttributes(), - ) + if type(coalesced_value) is int or type(coalesced_value) is float: + _attributes[TelemetryAttributes.http_client_request_duration] = ( + value + ) = coalesced_value + + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_request_duration, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = configuration.metrics.fga_client_request_duration.getAttributes() + + prepared_attributes = TelemetryAttributes.prepare( + _attributes, + filter=attribute_filters, + ) - if value is not None: - histogram.record(amount=value, attributes=attributes) + histogram.record(amount=value, attributes=prepared_attributes) # type: ignore[arg-type] return histogram def queryDuration( self, value: int | float | None = None, - attributes: dict[TelemetryAttribute, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Histogram: """ Record the duration of a query made by the client, as reported by the server. """ histogram = self.histogram(TelemetryHistograms.fga_client_query_duration) + _attributes: dict[TelemetryAttribute, str | bool | int | float] = ( + attributes or {} + ) if isMetricEnabled( configuration, TelemetryHistograms.fga_client_query_duration ): - attributes[TelemetryAttributes.http_server_request_duration] = value = ( - TelemetryAttributes.coalesceAttributeValue( - TelemetryAttributes.http_server_request_duration, - value, - attributes, - ) + coalesced_value = TelemetryAttributes.coalesceAttributeValue( + TelemetryAttributes.http_server_request_duration, + value, + _attributes, ) - attributes = TelemetryAttributes.prepare( - attributes, - filter=configuration.metrics.fga_client_query_duration.getAttributes(), - ) + if type(coalesced_value) is int or type(coalesced_value) is float: + _attributes[TelemetryAttributes.http_server_request_duration] = ( + value + ) = coalesced_value + + attribute_filters = None + + if ( + isinstance(configuration, TelemetryConfiguration) + and isinstance(configuration.metrics, TelemetryMetricConfiguration) + and isinstance( + configuration.metrics.fga_client_query_duration, + TelemetryMetricConfiguration, + ) + ): + attribute_filters = ( + configuration.metrics.fga_client_query_duration.getAttributes() + ) + + prepared_attributes = TelemetryAttributes.prepare( + _attributes, + filter=attribute_filters, + ) - if value is not None: - histogram.record(amount=value, attributes=attributes) + histogram.record(amount=value, attributes=prepared_attributes) # type: ignore[arg-type] return histogram diff --git a/config/clients/python/template/src/telemetry/utilities.py.mustache b/config/clients/python/template/src/telemetry/utilities.py.mustache deleted file mode 100644 index 69204c35..00000000 --- a/config/clients/python/template/src/telemetry/utilities.py.mustache +++ /dev/null @@ -1,9 +0,0 @@ -{{>partial_header}} - -def doesInstanceHaveCallable(instance: object, callableName: str) -> bool: - instanceCallable = getattr(instance, callableName, None) - - if instanceCallable is None: - return False - - return callable(instanceCallable) diff --git a/config/clients/python/template/test-requirements.mustache b/config/clients/python/template/test-requirements.mustache index 27570111..b39447e1 100644 --- a/config/clients/python/template/test-requirements.mustache +++ b/config/clients/python/template/test-requirements.mustache @@ -5,3 +5,4 @@ mock >= 5.1.0, < 6 pytest-asyncio >= 0.25, < 1 pytest-cov >= 5, < 7 ruff >= 0.9, < 1 +mypy >= 1.14.1, < 2 diff --git a/config/clients/python/template/test/rest_test.py.mustache b/config/clients/python/template/test/rest_test.py.mustache index 803a02e9..5fa4c331 100644 --- a/config/clients/python/template/test/rest_test.py.mustache +++ b/config/clients/python/template/test/rest_test.py.mustache @@ -22,12 +22,14 @@ async def test_restresponse_init(): mock_resp = MagicMock() mock_resp.status = 200 mock_resp.reason = "OK" + resp_data = b'{"test":"data"}' rest_resp = RESTResponse(mock_resp, resp_data) + assert rest_resp.status == 200 assert rest_resp.reason == "OK" assert rest_resp.data == resp_data - assert rest_resp.aiohttp_response == mock_resp + assert rest_resp.response == mock_resp def test_restresponse_getheaders(): diff --git a/config/clients/python/template/test/sync/rest_test.py.mustache b/config/clients/python/template/test/sync/rest_test.py.mustache index 92bac941..4f3797de 100644 --- a/config/clients/python/template/test/sync/rest_test.py.mustache +++ b/config/clients/python/template/test/sync/rest_test.py.mustache @@ -21,13 +21,14 @@ def test_restresponse_init(): mock_resp = MagicMock() mock_resp.status = 200 mock_resp.reason = "OK" - resp_data = b'{"test":"data"}' + resp_data = b'{"test":"data"}' rest_resp = RESTResponse(mock_resp, resp_data) + assert rest_resp.status == 200 assert rest_resp.reason == "OK" assert rest_resp.data == resp_data - assert rest_resp.urllib3_response == mock_resp + assert rest_resp.response == mock_resp def test_restresponse_getheaders(): diff --git a/config/clients/python/template/test/telemetry/utilities_test.py.mustache b/config/clients/python/template/test/telemetry/utilities_test.py.mustache deleted file mode 100644 index 1a958a6d..00000000 --- a/config/clients/python/template/test/telemetry/utilities_test.py.mustache +++ /dev/null @@ -1,17 +0,0 @@ -{{>partial_header}} - -from mock import MagicMock - -from {{packageName}}.telemetry.utilities import doesInstanceHaveCallable - - -def test_instance_has_callable(): - mock_instance = MagicMock(spec_set=["some_callable", "some_attribute"]) - mock_instance.some_callable = lambda: "I am callable" - - assert doesInstanceHaveCallable(mock_instance, "some_callable") - - assert not doesInstanceHaveCallable(mock_instance, "missing_callable") - - mock_instance.some_attribute = "not callable" - assert not doesInstanceHaveCallable(mock_instance, "some_attribute") From e76193a93fdcfbe1713eea7ef944cc83a4a384c0 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 3 Feb 2025 09:33:42 -0600 Subject: [PATCH 5/8] Update Makefile --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 034b8215..014ebb9d 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,7 @@ build-client-python: 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 ruff check --select I --fix . && \ python -m ruff format . && \ python setup.py sdist bdist_wheel'" From 2217b1c4cc968066fe0b6fc35e1c82b598f1a7e6 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 3 Feb 2025 10:15:38 -0600 Subject: [PATCH 6/8] feat(python-sdk): type hinting improvements --- Makefile | 1 + config/clients/python/template/pyproject.toml | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 034b8215..014ebb9d 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,7 @@ build-client-python: 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 ruff check --select I --fix . && \ python -m ruff format . && \ python setup.py sdist bdist_wheel'" diff --git a/config/clients/python/template/pyproject.toml b/config/clients/python/template/pyproject.toml index ca06cb1a..636e825e 100644 --- a/config/clients/python/template/pyproject.toml +++ b/config/clients/python/template/pyproject.toml @@ -34,7 +34,19 @@ indent-width = 4 target-version = "py310" [tool.ruff.lint] -select = ["E4", "E7", "E9", "F"] +extend-select = [ + #"B", # flake8-bugbear + #"C4", # flake8-comprehensions + #"C9", # mccabe + "I", # isort + #"PGH", # pygrep-hooks + #"RUF", # ruff + #"UP", # pyupgrade + #"W", # pycodestyle + #"YTT", # flake8-2020 + #"TRY", # tryceratops + #"EM", # flake8-errmsg +] ignore = [] fixable = ["ALL"] @@ -42,6 +54,10 @@ unfixable = [] dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +[tool.ruff.lint.isort] +lines-between-types = 1 +lines-after-imports = 2 + [tool.ruff.format] quote-style = "double" indent-style = "space" From 920c8916c96bb54c4630fbfe347659809f46c949 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 3 Feb 2025 10:30:55 -0600 Subject: [PATCH 7/8] feat(python-sdk): type hinting improvements --- config/clients/python/template/pyproject.toml | 8 +- .../python/template/src/api.py.mustache | 3 +- .../template/src/api_client.py.mustache | 11 ++- .../template/src/client/client.py.mustache | 3 +- .../src/client/configuration.py.mustache | 3 +- .../template/src/configuration.py.mustache | 16 ++-- .../template/src/exceptions.py.mustache | 3 +- .../python/template/src/rest.py.mustache | 76 +++++++++++++++-- .../python/template/src/sync/api.py.mustache | 3 +- .../template/src/sync/api_client.py.mustache | 11 ++- .../src/sync/client/client.py.mustache | 3 +- .../python/template/src/sync/rest.py.mustache | 83 ++++++++++++++++--- .../src/telemetry/attributes.py.mustache | 4 +- .../src/telemetry/configuration.py.mustache | 4 +- .../template/test/oauth2_test.py.mustache | 2 +- .../test/sync/oauth2_test.py.mustache | 2 +- 16 files changed, 170 insertions(+), 65 deletions(-) diff --git a/config/clients/python/template/pyproject.toml b/config/clients/python/template/pyproject.toml index 636e825e..ce973317 100644 --- a/config/clients/python/template/pyproject.toml +++ b/config/clients/python/template/pyproject.toml @@ -39,11 +39,11 @@ extend-select = [ #"C4", # flake8-comprehensions #"C9", # mccabe "I", # isort - #"PGH", # pygrep-hooks + "PGH", # pygrep-hooks #"RUF", # ruff - #"UP", # pyupgrade - #"W", # pycodestyle - #"YTT", # flake8-2020 + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 #"TRY", # tryceratops #"EM", # flake8-errmsg ] diff --git a/config/clients/python/template/src/api.py.mustache b/config/clients/python/template/src/api.py.mustache index b9a278c2..7ba58f99 100644 --- a/config/clients/python/template/src/api.py.mustache +++ b/config/clients/python/template/src/api.py.mustache @@ -187,8 +187,7 @@ class {{classname}}: 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 + f"Got an unexpected keyword argument '{key}' to method {{operationId}}" ) local_var_params[key] = val del local_var_params['kwargs'] diff --git a/config/clients/python/template/src/api_client.py.mustache b/config/clients/python/template/src/api_client.py.mustache index 89a2a159..35405f98 100644 --- a/config/clients/python/template/src/api_client.py.mustache +++ b/config/clients/python/template/src/api_client.py.mustache @@ -200,10 +200,9 @@ class ApiClient: collection_formats) for k, v in path_params: # specified safe chars, encode everything - resource_path = resource_path.replace( - '{%s}' % k, - urllib.parse.quote(str(v), safe=config.safe_chars_for_path_param) - ) + _k = urllib.parse.quote(str(k), safe=config.safe_chars_for_path_param) + _v = urllib.parse.quote(str(v), safe=config.safe_chars_for_path_param) + resource_path = resource_path.replace("{" + str(k) + "}", _v) # query parameters if query_params: @@ -415,7 +414,7 @@ class ApiClient: elif isinstance(obj, tuple): return tuple(self.sanitize_for_serialization(sub_obj) for sub_obj in obj) - elif isinstance(obj, (datetime.datetime, datetime.date)): + elif isinstance(obj, datetime.datetime | datetime.date): return obj.isoformat() if isinstance(obj, dict): @@ -841,7 +840,7 @@ class ApiClient: kwargs = {} if (data is not None and klass.openapi_types is not None - and isinstance(data, (list, dict))): + and isinstance(data, list | dict)): for attr, attr_type in klass.openapi_types.items(): if klass.attribute_map[attr] in data: value = data[klass.attribute_map[attr]] diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index 59aed0de..63fdbe01 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -159,8 +159,7 @@ class OpenFgaClient: if authorization_model_id is None or authorization_model_id == "": return None if is_well_formed_ulid_string(authorization_model_id) is False: - raise FgaValidationException( - "authorization_model_id ('%s') is not in a valid ulid format" % authorization_model_id) + raise FgaValidationException(f"authorization_model_id ('{authorization_model_id}') is not in a valid ulid format") return authorization_model_id def _get_consistency( diff --git a/config/clients/python/template/src/client/configuration.py.mustache b/config/clients/python/template/src/client/configuration.py.mustache index ba26ca5f..eea054c9 100644 --- a/config/clients/python/template/src/client/configuration.py.mustache +++ b/config/clients/python/template/src/client/configuration.py.mustache @@ -28,8 +28,7 @@ class ClientConfiguration(Configuration): super().is_valid() if self.authorization_model_id is not None and self.authorization_model_id != "" and is_well_formed_ulid_string(self.authorization_model_id) is False: - raise FgaValidationException( - "authorization_model_id ('%s') is not in a valid ulid format" % self.authorization_model_id) + raise FgaValidationException(f"authorization_model_id ('{self.authorization_model_id}') is not in a valid ulid format") @property def authorization_model_id(self): diff --git a/config/clients/python/template/src/configuration.py.mustache b/config/clients/python/template/src/configuration.py.mustache index d4a1d3cf..7c574095 100644 --- a/config/clients/python/template/src/configuration.py.mustache +++ b/config/clients/python/template/src/configuration.py.mustache @@ -506,10 +506,10 @@ class Configuration: """ return ( "Python SDK Debug Report:\n" - "OS: {env}\n" - "Python Version: {pyversion}\n" + f"OS: {sys.platform}\n" + f"Python Version: {sys.version}\n" "Version of the API: 1.x\n" - "SDK Package Version: 0.9.1".format(env=sys.platform, pyversion=sys.version) + "SDK Package Version: 0.9.1" ) def get_host_settings(self): @@ -541,8 +541,8 @@ class Configuration: server = servers[index] except IndexError: raise ValueError( - "Invalid index {} when selecting the host settings. " - "Must be less than {}".format(index, len(servers)) + f"Invalid index {index} when selecting the host settings. " + f"Must be less than {len(servers)}" ) url = server["url"] @@ -583,9 +583,7 @@ class Configuration: except ValueError: if self.api_url is None: raise ApiValueError( - "Either api_scheme `{}` or api_host `{}` is invalid".format( - self.api_scheme, self.api_host - ) + f"Either api_scheme `{self.api_scheme}` or api_host `{self.api_host}` is invalid" ) else: raise ApiValueError(f"api_url `{self.api_url}` is invalid") @@ -611,7 +609,7 @@ class Configuration: and is_well_formed_ulid_string(self.store_id) is False ): raise FgaValidationException( - "store_id ('%s') is not in a valid ulid format" % self.store_id + f"store_id ('{self.store_id}') is not in a valid ulid format" ) if self._credentials is not None: diff --git a/config/clients/python/template/src/exceptions.py.mustache b/config/clients/python/template/src/exceptions.py.mustache index 27c32960..e347912e 100644 --- a/config/clients/python/template/src/exceptions.py.mustache +++ b/config/clients/python/template/src/exceptions.py.mustache @@ -124,8 +124,7 @@ class ApiException(OpenApiException): def __str__(self): """Custom error messages for exception""" - error_message = "({})\n"\ - "Reason: {}\n".format(self.status, self.reason) + error_message = f"({self.status})\nReason: {self.reason}\n" if self.body: error_message += f"HTTP response body: {self.body}\n" diff --git a/config/clients/python/template/src/rest.py.mustache b/config/clients/python/template/src/rest.py.mustache index af2e8516..589ffd22 100644 --- a/config/clients/python/template/src/rest.py.mustache +++ b/config/clients/python/template/src/rest.py.mustache @@ -29,15 +29,17 @@ class RESTResponse(io.IOBase): Represents an HTTP response object in the asynchronous client. """ - response: aiohttp.ClientResponse - status: int - reason: str | None - data: bytes + _response: aiohttp.ClientResponse + _data: bytes + _status: int + _reason: str | None def __init__( self, - resp: aiohttp.ClientResponse, + response: aiohttp.ClientResponse, data: bytes, + status: int | None = None, + reason: str | None = None, ) -> None: """ Initializes a RESTResponse with an aiohttp.ClientResponse and corresponding data. @@ -45,10 +47,66 @@ class RESTResponse(io.IOBase): :param resp: The aiohttp.ClientResponse object. :param data: The raw byte data read from the response. """ - self.response = resp - self.status = resp.status - self.reason = resp.reason - self.data = data + self._response = response + self._data = data + self._status = status or response.status + self._reason = reason or response.reason + + @property + def response(self) -> aiohttp.ClientResponse: + """ + Returns the underlying aiohttp.ClientResponse object. + """ + return self._response + + @response.setter + def response(self, value: aiohttp.ClientResponse) -> None: + """ + Sets the underlying aiohttp.ClientResponse object. + """ + self._response = value + + @property + def data(self) -> bytes: + """ + Returns the raw byte data of the response. + """ + return self._data + + @data.setter + def data(self, value: bytes) -> None: + """ + Sets the raw byte data of the response. + """ + self._data = value + + @property + def status(self) -> int: + """ + Returns the HTTP status code of the response. + """ + return self._status + + @status.setter + def status(self, value: int) -> None: + """ + Sets the HTTP status code of the response. + """ + self._status = value + + @property + def reason(self) -> str | None: + """ + Returns the HTTP reason phrase of the response. + """ + return self._reason + + @reason.setter + def reason(self, value: str | None) -> None: + """ + Sets the HTTP reason phrase of the response. + """ + self._reason = value def getheaders(self) -> dict[str, str]: """ diff --git a/config/clients/python/template/src/sync/api.py.mustache b/config/clients/python/template/src/sync/api.py.mustache index 4b2ef225..8437fd47 100644 --- a/config/clients/python/template/src/sync/api.py.mustache +++ b/config/clients/python/template/src/sync/api.py.mustache @@ -174,8 +174,7 @@ class {{classname}}: 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 + f"Got an unexpected keyword argument '{key}' to method {{operationId}}" ) local_var_params[key] = val del local_var_params['kwargs'] diff --git a/config/clients/python/template/src/sync/api_client.py.mustache b/config/clients/python/template/src/sync/api_client.py.mustache index e0a5beb7..3edd1e61 100644 --- a/config/clients/python/template/src/sync/api_client.py.mustache +++ b/config/clients/python/template/src/sync/api_client.py.mustache @@ -187,10 +187,9 @@ class ApiClient: collection_formats) for k, v in path_params: # specified safe chars, encode everything - resource_path = resource_path.replace( - '{%s}' % k, - urllib.parse.quote(str(v), safe=config.safe_chars_for_path_param) - ) + _k = urllib.parse.quote(str(k), safe=config.safe_chars_for_path_param) + _v = urllib.parse.quote(str(v), safe=config.safe_chars_for_path_param) + resource_path = resource_path.replace("{" + str(k) + "}", _v) # query parameters if query_params: @@ -400,7 +399,7 @@ class ApiClient: elif isinstance(obj, tuple): return tuple(self.sanitize_for_serialization(sub_obj) for sub_obj in obj) - elif isinstance(obj, (datetime.datetime, datetime.date)): + elif isinstance(obj, datetime.datetime | datetime.date): return obj.isoformat() if isinstance(obj, dict): @@ -826,7 +825,7 @@ class ApiClient: kwargs = {} if (data is not None and klass.openapi_types is not None - and isinstance(data, (list, dict))): + and isinstance(data, list | dict)): for attr, attr_type in klass.openapi_types.items(): if klass.attribute_map[attr] in data: value = data[klass.attribute_map[attr]] diff --git a/config/clients/python/template/src/sync/client/client.py.mustache b/config/clients/python/template/src/sync/client/client.py.mustache index 49252e60..35e0ad44 100644 --- a/config/clients/python/template/src/sync/client/client.py.mustache +++ b/config/clients/python/template/src/sync/client/client.py.mustache @@ -150,8 +150,7 @@ class OpenFgaClient: if authorization_model_id is None or authorization_model_id == "": return None if is_well_formed_ulid_string(authorization_model_id) is False: - raise FgaValidationException( - "authorization_model_id ('%s') is not in a valid ulid format" % authorization_model_id) + raise FgaValidationException(f"authorization_model_id ('{authorization_model_id}') is not in a valid ulid format") return authorization_model_id def _get_consistency( diff --git a/config/clients/python/template/src/sync/rest.py.mustache b/config/clients/python/template/src/sync/rest.py.mustache index 8b7eaf71..b7444f84 100644 --- a/config/clients/python/template/src/sync/rest.py.mustache +++ b/config/clients/python/template/src/sync/rest.py.mustache @@ -29,15 +29,17 @@ class RESTResponse(io.IOBase): Represents an HTTP response object in the synchronous client. """ - response: urllib3.BaseHTTPResponse - status: int - reason: str | None - data: bytes + _response: urllib3.BaseHTTPResponse + _data: bytes + _status: int + _reason: str | None def __init__( self, - resp: urllib3.BaseHTTPResponse, + response: urllib3.BaseHTTPResponse, data: bytes, + status: int | None = None, + reason: str | None = None, ) -> None: """ Initializes a RESTResponse with a urllib3.BaseHTTPResponse and corresponding data. @@ -45,10 +47,66 @@ class RESTResponse(io.IOBase): :param resp: The urllib3.BaseHTTPResponse object. :param data: The raw byte data read from the response. """ - self.response = resp - self.status = resp.status - self.reason = resp.reason - self.data = data + self._response = response + self._data = data + self._status = status or response.status + self._reason = reason or response.reason + + @property + def response(self) -> urllib3.BaseHTTPResponse: + """ + Returns the underlying urllib3.BaseHTTPResponse object. + """ + return self._response + + @response.setter + def response(self, value: urllib3.BaseHTTPResponse) -> None: + """ + Sets the underlying urllib3.BaseHTTPResponse object. + """ + self._response = value + + @property + def data(self) -> bytes: + """ + Returns the raw byte data of the response. + """ + return self._data + + @data.setter + def data(self, value: bytes) -> None: + """ + Sets the raw byte data of the response. + """ + self._data = value + + @property + def status(self) -> int: + """ + Returns the HTTP status code of the response. + """ + return self._status + + @status.setter + def status(self, value: int) -> None: + """ + Sets the HTTP status code of the response. + """ + self._status = value + + @property + def reason(self) -> str | None: + """ + Returns the HTTP reason phrase of the response. + """ + return self._reason + + @reason.setter + def reason(self, value: str | None) -> None: + """ + Sets the HTTP reason phrase of the response. + """ + self._reason = value def getheaders(self) -> dict[str, str]: """ @@ -187,7 +245,7 @@ class RESTClientObject: post_params = post_params or {} timeout_val = _request_timeout or self._timeout_millisec - if isinstance(timeout_val, (float, int)): + if isinstance(timeout_val, float | int): if timeout_val > 100: timeout_val /= 1000 timeout = urllib3.Timeout(total=timeout_val) @@ -232,7 +290,7 @@ class RESTClientObject: args["fields"] = post_params args["encode_multipart"] = True - elif isinstance(body, (str, bytes)): + elif isinstance(body, str | bytes): args["body"] = body else: msg = ( @@ -357,8 +415,7 @@ class RESTClientObject: ) # Yield any complete objects - for obj in decoded_objects: - yield obj + yield from decoded_objects except Exception as e: logger.exception("Stream error: %s", e) diff --git a/config/clients/python/template/src/telemetry/attributes.py.mustache b/config/clients/python/template/src/telemetry/attributes.py.mustache index 5477c461..2aec69a4 100644 --- a/config/clients/python/template/src/telemetry/attributes.py.mustache +++ b/config/clients/python/template/src/telemetry/attributes.py.mustache @@ -128,13 +128,13 @@ class TelemetryAttributes: ) if attributeInstance is None: - raise ValueError("Invalid attribute specified: %s" % attribute) + raise ValueError(f"Invalid attribute specified: {attribute}") attribute = attributeInstance if not isinstance(attribute, TelemetryAttribute): raise ValueError( - "Invalid attribute specified: %s" % type(attribute) + f"Invalid attribute specified: {type(attribute)}" ) if filter is not None and attribute not in filter: diff --git a/config/clients/python/template/src/telemetry/configuration.py.mustache b/config/clients/python/template/src/telemetry/configuration.py.mustache index 0e75500b..a45f8c83 100644 --- a/config/clients/python/template/src/telemetry/configuration.py.mustache +++ b/config/clients/python/template/src/telemetry/configuration.py.mustache @@ -1,6 +1,6 @@ {{>partial_header}} -from typing import NamedTuple, Protocol, Type, runtime_checkable +from typing import NamedTuple, Protocol, runtime_checkable from {{packageName}}.telemetry.attributes import TelemetryAttribute, TelemetryAttributes from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters @@ -993,7 +993,7 @@ class TelemetryMetricsConfiguration(TelemetryMetricsConfigurationProtocol): class TelemetryConfigurationType(NamedTuple): name: str - configurationClass: Type[TelemetryMetricsConfigurationProtocol] + configurationClass: type[TelemetryMetricsConfigurationProtocol] class TelemetryConfigurations: diff --git a/config/clients/python/template/test/oauth2_test.py.mustache b/config/clients/python/template/test/oauth2_test.py.mustache index 51062700..c7986a77 100644 --- a/config/clients/python/template/test/oauth2_test.py.mustache +++ b/config/clients/python/template/test/oauth2_test.py.mustache @@ -68,7 +68,7 @@ class TestOAuth2Client(IsolatedAsyncioTestCase): self.assertEqual(auth_header, {"Authorization": "Bearer AABBCCDD"}) self.assertEqual(client._access_token, "AABBCCDD") self.assertGreaterEqual( - client._access_expiry_time, current_time + timedelta(seconds=int(120)) + client._access_expiry_time, current_time + timedelta(seconds=120) ) expected_header = urllib3.response.HTTPHeaderDict( { diff --git a/config/clients/python/template/test/sync/oauth2_test.py.mustache b/config/clients/python/template/test/sync/oauth2_test.py.mustache index 2e1a8380..614f60c3 100644 --- a/config/clients/python/template/test/sync/oauth2_test.py.mustache +++ b/config/clients/python/template/test/sync/oauth2_test.py.mustache @@ -68,7 +68,7 @@ class TestOAuth2Client(IsolatedAsyncioTestCase): self.assertEqual(auth_header, {"Authorization": "Bearer AABBCCDD"}) self.assertEqual(client._access_token, "AABBCCDD") self.assertGreaterEqual( - client._access_expiry_time, current_time + timedelta(seconds=int(120)) + client._access_expiry_time, current_time + timedelta(seconds=120) ) expected_header = urllib3.response.HTTPHeaderDict( { From 49c5cf886f6dbe1e64c65ac85b4e0e3252bae85a Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 3 Feb 2025 11:04:47 -0600 Subject: [PATCH 8/8] feat(python-sdk): type hinting improvements --- config/clients/python/template/requirements.mustache | 1 - 1 file changed, 1 deletion(-) diff --git a/config/clients/python/template/requirements.mustache b/config/clients/python/template/requirements.mustache index 8cda1952..4eef03dd 100644 --- a/config/clients/python/template/requirements.mustache +++ b/config/clients/python/template/requirements.mustache @@ -4,4 +4,3 @@ setuptools >= 69.1.1 build >= 1.2.1, < 2 urllib3 >= 1.25.11, < 3 opentelemetry-api >= 1.25.0, < 2 -python-dateutil >= 2.9.0, < 3