diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index d1763b3..06aeac2 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -253,8 +253,6 @@ openfga_sdk/telemetry/metrics.py openfga_sdk/telemetry/telemetry.py openfga_sdk/validation.py pyproject.toml -README.md -test/__init__.py test/_/configuration_test.py test/_/credentials_test.py test/_/oauth2_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7082413..c05fca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Changelog -## [Unreleased](https://github.com/openfga/python-sdk/compare/v0.9.5...HEAD) +## [Unreleased](https://github.com/openfga/python-sdk/compare/v0.9.6...HEAD) -### [0.9.5](https://github.com/openfga/python-sdk/compare/v0.9.4...0.9.5) (2025-07-09) +### [0.9.6](https://github.com/openfga/python-sdk/compare/v0.9.5...0.9.6) (2025-09-15) + +- fix: reuse ssl context in the sync client (#222) - thanks @wadells! +- feat: add OAuth2 scopes parameter support to CredentialConfiguration (#213) - thanks @SoulPancake + +### [v0.9.5](https://github.com/openfga/python-sdk/compare/v0.9.4...v0.9.5) (2025-07-09) - fix: aiohttp.ClientResponse.data should be awaited (#197) - thanks @cmbernard333 diff --git a/VERSION.txt b/VERSION.txt index b0bb878..85b7c69 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.9.5 +0.9.6 diff --git a/example/example1/requirements.txt b/example/example1/requirements.txt index 64a7064..e947271 100644 --- a/example/example1/requirements.txt +++ b/example/example1/requirements.txt @@ -4,7 +4,7 @@ attrs >= 25.3.0 frozenlist >= 1.7.0 idna >= 3.10 multidict >= 6.6.4 -openfga-sdk >= 0.9.5 +openfga-sdk >= 0.9.6 python-dateutil >= 2.9.0.post0 urllib3 >= 1.26.19, != 2.0.*, != 2.1.*, != 2.2.0, != 2.2.1, < 3 yarl >= 1.20.1 diff --git a/example/example1/setup.py b/example/example1/setup.py index 6a2f4a5..858d741 100644 --- a/example/example1/setup.py +++ b/example/example1/setup.py @@ -15,7 +15,7 @@ NAME = "example1" VERSION = "0.0.1" -REQUIRES = ["openfga-sdk >= 0.9.5"] +REQUIRES = ["openfga-sdk >= 0.9.6"] setup( name=NAME, diff --git a/openfga_sdk/__init__.py b/openfga_sdk/__init__.py index caeb2cf..e7c13f3 100644 --- a/openfga_sdk/__init__.py +++ b/openfga_sdk/__init__.py @@ -10,7 +10,7 @@ NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. """ -__version__ = "0.9.5" +__version__ = "0.9.6" from openfga_sdk.api.open_fga_api import OpenFgaApi from openfga_sdk.api_client import ApiClient diff --git a/openfga_sdk/api_client.py b/openfga_sdk/api_client.py index f8d6651..5a54500 100644 --- a/openfga_sdk/api_client.py +++ b/openfga_sdk/api_client.py @@ -39,7 +39,7 @@ from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes -DEFAULT_USER_AGENT = "openfga-sdk python/0.9.5" +DEFAULT_USER_AGENT = "openfga-sdk python/0.9.6" def random_time(loop_count, min_wait_in_ms) -> float: diff --git a/openfga_sdk/configuration.py b/openfga_sdk/configuration.py index 2fc81e6..84aedee 100644 --- a/openfga_sdk/configuration.py +++ b/openfga_sdk/configuration.py @@ -538,7 +538,7 @@ def to_debug_report(self): f"OS: {sys.platform}\n" f"Python Version: {sys.version}\n" "Version of the API: 1.x\n" - "SDK Package Version: 0.9.5" + "SDK Package Version: 0.9.6" ) def get_host_settings(self): diff --git a/openfga_sdk/oauth2.py b/openfga_sdk/oauth2.py index 060acf5..ddba92f 100644 --- a/openfga_sdk/oauth2.py +++ b/openfga_sdk/oauth2.py @@ -90,7 +90,7 @@ async def _obtain_token(self, client): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) diff --git a/openfga_sdk/sync/api_client.py b/openfga_sdk/sync/api_client.py index db4a5c7..a588092 100644 --- a/openfga_sdk/sync/api_client.py +++ b/openfga_sdk/sync/api_client.py @@ -38,7 +38,7 @@ from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes -DEFAULT_USER_AGENT = "openfga-sdk python/0.9.5" +DEFAULT_USER_AGENT = "openfga-sdk python/0.9.6" def random_time(loop_count, min_wait_in_ms) -> float: diff --git a/openfga_sdk/sync/oauth2.py b/openfga_sdk/sync/oauth2.py index ad4336c..3a8a8c7 100644 --- a/openfga_sdk/sync/oauth2.py +++ b/openfga_sdk/sync/oauth2.py @@ -70,7 +70,7 @@ def _obtain_token(self, client): """ configuration = self._credentials.configuration - token_url = f"https://{configuration.api_issuer}/oauth/token" + token_url = self._credentials._parse_issuer(configuration.api_issuer) post_params = { "client_id": configuration.client_id, @@ -90,7 +90,7 @@ def _obtain_token(self, client): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) diff --git a/openfga_sdk/sync/rest.py b/openfga_sdk/sync/rest.py index 4626152..134c532 100644 --- a/openfga_sdk/sync/rest.py +++ b/openfga_sdk/sync/rest.py @@ -155,6 +155,7 @@ def __init__( :param pools_size: The number of connection pools to use. :param maxsize: The maximum number of connections per pool. """ + # Reuse SSL context to mitigate OpenSSL 3.0+ performance issues # See: https://github.com/openssl/openssl/issues/17064 ssl_context = ssl.create_default_context(cafile=configuration.ssl_ca_cert) diff --git a/pyproject.toml b/pyproject.toml index aa3e8bb..605902b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ [project] name = "openfga-sdk" -version = "0.9.5" +version = "0.9.6" description="A high performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar." authors = [ { name = "OpenFGA (https://openfga.dev)", email = "community@openfga.dev"} diff --git a/test/api/open_fga_api_test.py b/test/api/open_fga_api_test.py index f8e5a0c..42a43cf 100644 --- a/test/api/open_fga_api_test.py +++ b/test/api/open_fga_api_test.py @@ -1782,7 +1782,7 @@ async def test_check_api_token(self, mock_request): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.5", + "User-Agent": "openfga-sdk python/0.9.6", "Authorization": "Bearer TOKEN1", } ) @@ -1836,7 +1836,7 @@ async def test_check_custom_header(self, mock_request): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.5", + "User-Agent": "openfga-sdk python/0.9.6", "Custom Header": "custom value", } ) diff --git a/test/oauth2_test.py b/test/oauth2_test.py index 2afad9b..3b0baae 100644 --- a/test/oauth2_test.py +++ b/test/oauth2_test.py @@ -84,7 +84,7 @@ async def test_get_authentication_obtain_client_credentials(self, mock_request): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -310,7 +310,7 @@ async def test_get_authentication_keep_full_url(self, mock_request): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -365,7 +365,7 @@ async def test_get_authentication_add_scheme(self, mock_request): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -420,7 +420,7 @@ async def test_get_authentication_add_path(self, mock_request): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -475,7 +475,7 @@ async def test_get_authentication_add_scheme_and_path(self, mock_request): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -533,7 +533,7 @@ async def test_get_authentication_obtain_client_credentials_with_scopes_list( { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -592,7 +592,7 @@ async def test_get_authentication_obtain_client_credentials_with_scopes_string( { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( diff --git a/test/sync/oauth2_test.py b/test/sync/oauth2_test.py index cf8aea3..5da3525 100644 --- a/test/sync/oauth2_test.py +++ b/test/sync/oauth2_test.py @@ -84,7 +84,7 @@ def test_get_authentication_obtain_client_credentials(self, mock_request): { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -142,7 +142,7 @@ def test_get_authentication_obtain_client_credentials_with_scopes_list( { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( @@ -201,7 +201,7 @@ def test_get_authentication_obtain_client_credentials_with_scopes_string( { "Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "openfga-sdk (python) 0.9.5", + "User-Agent": "openfga-sdk (python) 0.9.6", } ) mock_request.assert_called_once_with( diff --git a/test/sync/open_fga_api_test.py b/test/sync/open_fga_api_test.py index 000cbf8..22f1261 100644 --- a/test/sync/open_fga_api_test.py +++ b/test/sync/open_fga_api_test.py @@ -1841,7 +1841,7 @@ def test_check_api_token(self, mock_request): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.5", + "User-Agent": "openfga-sdk python/0.9.6", "Authorization": "Bearer TOKEN1", } ) @@ -1895,7 +1895,7 @@ def test_check_custom_header(self, mock_request): { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": "openfga-sdk python/0.9.5", + "User-Agent": "openfga-sdk python/0.9.6", "Custom Header": "custom value", } ) diff --git a/test/sync/rest_test.py b/test/sync/rest_test.py index 9942d69..ebfe8ec 100644 --- a/test/sync/rest_test.py +++ b/test/sync/rest_test.py @@ -535,8 +535,8 @@ def release_conn(self): # Tests for SSL Context Reuse (fix for OpenSSL 3.0+ performance issues) -@patch('ssl.create_default_context') -@patch('urllib3.PoolManager') +@patch("ssl.create_default_context") +@patch("urllib3.PoolManager") def test_ssl_context_created_with_ca_cert(mock_pool_manager, mock_create_context): """Test that SSL context is created with CA certificate file.""" mock_ssl_context = MagicMock() @@ -559,11 +559,11 @@ def test_ssl_context_created_with_ca_cert(mock_pool_manager, mock_create_context # Verify SSL context was passed to PoolManager mock_pool_manager.assert_called_once() call_kwargs = mock_pool_manager.call_args[1] - assert call_kwargs['ssl_context'] == mock_ssl_context + assert call_kwargs["ssl_context"] == mock_ssl_context -@patch('ssl.create_default_context') -@patch('urllib3.PoolManager') +@patch("ssl.create_default_context") +@patch("urllib3.PoolManager") def test_ssl_context_loads_client_certificate(mock_pool_manager, mock_create_context): """Test that SSL context loads client certificate and key when provided.""" mock_ssl_context = MagicMock() @@ -591,12 +591,14 @@ def test_ssl_context_loads_client_certificate(mock_pool_manager, mock_create_con # Verify SSL context was passed to PoolManager mock_pool_manager.assert_called_once() call_kwargs = mock_pool_manager.call_args[1] - assert call_kwargs['ssl_context'] == mock_ssl_context + assert call_kwargs["ssl_context"] == mock_ssl_context -@patch('ssl.create_default_context') -@patch('urllib3.PoolManager') -def test_ssl_context_disables_verification_when_verify_ssl_false(mock_pool_manager, mock_create_context): +@patch("ssl.create_default_context") +@patch("urllib3.PoolManager") +def test_ssl_context_disables_verification_when_verify_ssl_false( + mock_pool_manager, mock_create_context +): """Test that SSL context disables verification when verify_ssl=False.""" mock_ssl_context = MagicMock() mock_create_context.return_value = mock_ssl_context @@ -622,11 +624,11 @@ def test_ssl_context_disables_verification_when_verify_ssl_false(mock_pool_manag # Verify SSL context was passed to PoolManager mock_pool_manager.assert_called_once() call_kwargs = mock_pool_manager.call_args[1] - assert call_kwargs['ssl_context'] == mock_ssl_context + assert call_kwargs["ssl_context"] == mock_ssl_context -@patch('ssl.create_default_context') -@patch('urllib3.ProxyManager') +@patch("ssl.create_default_context") +@patch("urllib3.ProxyManager") def test_ssl_context_used_with_proxy_manager(mock_proxy_manager, mock_create_context): """Test that SSL context is passed to ProxyManager when proxy is configured.""" mock_ssl_context = MagicMock() @@ -655,14 +657,16 @@ def test_ssl_context_used_with_proxy_manager(mock_proxy_manager, mock_create_con # Verify SSL context was passed to ProxyManager mock_proxy_manager.assert_called_once() call_kwargs = mock_proxy_manager.call_args[1] - assert call_kwargs['ssl_context'] == mock_ssl_context - assert call_kwargs['proxy_url'] == "http://proxy:8080" - assert call_kwargs['proxy_headers'] == {"Proxy-Auth": "token"} + assert call_kwargs["ssl_context"] == mock_ssl_context + assert call_kwargs["proxy_url"] == "http://proxy:8080" + assert call_kwargs["proxy_headers"] == {"Proxy-Auth": "token"} -@patch('ssl.create_default_context') -@patch('urllib3.PoolManager') -def test_ssl_context_reuse_performance_optimization(mock_pool_manager, mock_create_context): +@patch("ssl.create_default_context") +@patch("urllib3.PoolManager") +def test_ssl_context_reuse_performance_optimization( + mock_pool_manager, mock_create_context +): """Test that SSL context creation is called only once per client instance.""" mock_ssl_context = MagicMock() mock_create_context.return_value = mock_ssl_context @@ -685,7 +689,7 @@ def test_ssl_context_reuse_performance_optimization(mock_pool_manager, mock_crea # Verify the same SSL context instance is reused mock_pool_manager.assert_called_once() call_kwargs = mock_pool_manager.call_args[1] - assert call_kwargs['ssl_context'] is mock_ssl_context + assert call_kwargs["ssl_context"] is mock_ssl_context # Verify context was not created again during subsequent operations mock_create_context.reset_mock() @@ -697,8 +701,8 @@ def test_ssl_context_reuse_performance_optimization(mock_pool_manager, mock_crea mock_create_context.assert_not_called() -@patch('ssl.create_default_context') -@patch('urllib3.PoolManager') +@patch("ssl.create_default_context") +@patch("urllib3.PoolManager") def test_ssl_context_with_all_ssl_options(mock_pool_manager, mock_create_context): """Test SSL context creation with all SSL configuration options set.""" mock_ssl_context = MagicMock() @@ -725,11 +729,17 @@ def test_ssl_context_with_all_ssl_options(mock_pool_manager, mock_create_context # Verify SSL verification settings were NOT modified (verify_ssl=True) # check_hostname and verify_mode should remain at their default secure values - assert not hasattr(mock_ssl_context, 'check_hostname') or mock_ssl_context.check_hostname - assert not hasattr(mock_ssl_context, 'verify_mode') or mock_ssl_context.verify_mode != ssl.CERT_NONE + assert ( + not hasattr(mock_ssl_context, "check_hostname") + or mock_ssl_context.check_hostname + ) + assert ( + not hasattr(mock_ssl_context, "verify_mode") + or mock_ssl_context.verify_mode != ssl.CERT_NONE + ) # Verify SSL context was passed to PoolManager mock_pool_manager.assert_called_once() call_kwargs = mock_pool_manager.call_args[1] - assert call_kwargs['ssl_context'] == mock_ssl_context - assert call_kwargs['maxsize'] == 8 + assert call_kwargs["ssl_context"] == mock_ssl_context + assert call_kwargs["maxsize"] == 8 diff --git a/uv.lock b/uv.lock index 39f3e93..4fa7063 100644 --- a/uv.lock +++ b/uv.lock @@ -551,7 +551,7 @@ wheels = [ [[package]] name = "openfga-sdk" -version = "0.9.5" +version = "0.9.6" source = { editable = "." } dependencies = [ { name = "aiohttp" },