Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.9.5
0.9.6
2 changes: 1 addition & 1 deletion example/example1/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion example/example1/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion openfga_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion openfga_sdk/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

DEFAULT_USER_AGENT updated — good.

To prevent future PR churn, derive from version.

Apply:

-DEFAULT_USER_AGENT = "openfga-sdk python/0.9.6" +DEFAULT_USER_AGENT = f"openfga-sdk python/{__version__}"

Add (outside this hunk) near imports:

from openfga_sdk import __version__

Check all UA strings reference the current version:


🏁 Script executed:

#!/bin/bash rg -nP 'User-Agent.*0\.9\.\d+' openfga_sdk | cat

Length of output: 174


DEFAULT_USER_AGENT updated — good. Remaining hardcoded User-Agent strings found; update them to derive from version.

Apply (api_client.py):

-DEFAULT_USER_AGENT = "openfga-sdk python/0.9.6" +DEFAULT_USER_AGENT = f"openfga-sdk python/{__version__}"

Add near imports:

from openfga_sdk import __version__

Files to fix (from verification):

  • openfga_sdk/oauth2.py:93
  • openfga_sdk/sync/oauth2.py:93

Replace:

"User-Agent": "openfga-sdk (python) 0.9.6",

with:

"User-Agent": f"openfga-sdk (python) {__version__}",

and add from openfga_sdk import __version__ to those modules' imports.

🤖 Prompt for AI Agents
In openfga_sdk/oauth2.py and openfga_sdk/sync/oauth2.py around line 93, replace the hardcoded User-Agent header value with one derived from the package version: add an import line `from openfga_sdk import __version__` to each module's imports and change the header value from the literal "User-Agent": "openfga-sdk (python) 0.9.6" to use f"openfga-sdk (python) {__version__}" so the User-Agent reflects the package __version__ dynamically. 


def random_time(loop_count, min_wait_in_ms) -> float:
Expand Down
2 changes: 1 addition & 1 deletion openfga_sdk/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion openfga_sdk/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
)

Expand Down
2 changes: 1 addition & 1 deletion openfga_sdk/sync/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions openfga_sdk/sync/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
}
)

Expand Down
1 change: 1 addition & 0 deletions openfga_sdk/sync/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
4 changes: 2 additions & 2 deletions test/api/open_fga_api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
)
Expand Down Expand Up @@ -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",
}
)
Expand Down
14 changes: 7 additions & 7 deletions test/oauth2_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions test/sync/oauth2_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions test/sync/open_fga_api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
)
Expand Down Expand Up @@ -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",
}
)
Expand Down
60 changes: 35 additions & 25 deletions test/sync/rest_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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")
Comment on lines +704 to +705
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fragile assertion: MagicMock makes the "secure defaults not modified" check vacuous.

hasattr() on MagicMock returns True and attributes are truthy Mocks, so the assertions would pass even if code regressed. Assert concrete values instead and pre-set expected defaults.

Apply this diff to make the assertions meaningful:

- # 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 - ) + # Verify SSL verification settings were NOT modified (verify_ssl=True) + assert mock_ssl_context.check_hostname is True + assert mock_ssl_context.verify_mode == ssl.CERT_REQUIRED

And initialize the defaults on the mocked context (outside the changed lines, right after creating mock_ssl_context):

# after: mock_ssl_context = MagicMock() mock_ssl_context.check_hostname = True mock_ssl_context.verify_mode = ssl.CERT_REQUIRED

Also applies to: 732-739, 744-745

🤖 Prompt for AI Agents
In test/sync/rest_test.py around lines 704-705 (and similarly for ranges 732-739 and 744-745), the test relies on hasattr() and truthy MagicMock attributes which makes the "secure defaults not modified" check vacuous; after creating mock_ssl_context (right after mock_ssl_context = MagicMock()) set concrete default attributes like check_hostname = True and verify_mode = ssl.CERT_REQUIRED on the mock, then replace hasattr()/truthy checks with assertions that those attributes equal the concrete expected values so the test fails if the code mutates them. 
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()
Expand All @@ -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
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.