From 753d4bfb5675d8fe4c9901cac3d14a20f9519510 Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Fri, 5 Sep 2025 14:40:45 +0300 Subject: [PATCH 01/10] Prevent race condition during JWT obtaining --- .../adapters/api_client_adapter.py | 48 ++++++++++++++----- .../configuration/configuration.py | 25 +++++----- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/conductor/asyncio_client/adapters/api_client_adapter.py b/src/conductor/asyncio_client/adapters/api_client_adapter.py index 4fe809cb..9f786495 100644 --- a/src/conductor/asyncio_client/adapters/api_client_adapter.py +++ b/src/conductor/asyncio_client/adapters/api_client_adapter.py @@ -1,5 +1,7 @@ +import asyncio import json import logging +import time from conductor.asyncio_client.adapters.models import GenerateTokenRequest from conductor.asyncio_client.http import rest @@ -10,6 +12,10 @@ class ApiClientAdapter(ApiClient): + def __init__(self, *args, **kwargs): + self._token_lock = asyncio.Lock() + super().__init__(*args, **kwargs) + async def call_api( self, method, @@ -40,17 +46,34 @@ async def call_api( post_params=post_params, _request_timeout=_request_timeout, ) - if response_data.status == 401: # noqa: PLR2004 (Unauthorized status code) - token = await self.refresh_authorization_token() - header_params["X-Authorization"] = token - response_data = await self.rest_client.request( - method, - url, - headers=header_params, - body=body, - post_params=post_params, - _request_timeout=_request_timeout, - ) + if ( + response_data.status == 401 + and url != self.configuration.host + "/token" + ): # noqa: PLR2004 (Unauthorized status code) + async with self._token_lock: + token_expired = ( + self.configuration.token_update_time > 0 + and time.time() + >= self.configuration.token_update_time + + self.configuration.auth_token_ttl_sec + ) + invalid_token = not self.configuration._http_config.api_key.get( + "api_key" + ) + + if invalid_token or token_expired: + token = await self.refresh_authorization_token() + else: + token = self.configuration._http_config.api_key["api_key"] + header_params["X-Authorization"] = token + response_data = await self.rest_client.request( + method, + url, + headers=header_params, + body=body, + post_params=post_params, + _request_timeout=_request_timeout, + ) except ApiException as e: raise e @@ -59,7 +82,8 @@ async def call_api( async def refresh_authorization_token(self): obtain_new_token_response = await self.obtain_new_token() token = obtain_new_token_response.get("token") - self.configuration.api_key["api_key"] = token + self.configuration._http_config.api_key["api_key"] = token + self.configuration.token_update_time = time.time() return token async def obtain_new_token(self): diff --git a/src/conductor/asyncio_client/configuration/configuration.py b/src/conductor/asyncio_client/configuration/configuration.py index cf1edf94..daefc9b9 100644 --- a/src/conductor/asyncio_client/configuration/configuration.py +++ b/src/conductor/asyncio_client/configuration/configuration.py @@ -4,8 +4,9 @@ import os from typing import Any, Dict, Optional, Union -from conductor.asyncio_client.http.configuration import \ - Configuration as HttpConfiguration +from conductor.asyncio_client.http.configuration import ( + Configuration as HttpConfiguration, +) class Configuration: @@ -55,6 +56,7 @@ def __init__( auth_key: Optional[str] = None, auth_secret: Optional[str] = None, debug: bool = False, + auth_token_ttl_min: int = 45, # Worker properties default_polling_interval: Optional[float] = None, default_domain: Optional[str] = None, @@ -128,10 +130,6 @@ def __init__( if api_key is None: api_key = {} - if self.auth_key and self.auth_secret: - # Use the auth_key as the API key for X-Authorization header - api_key["api_key"] = self.auth_key - self.__ui_host = os.getenv("CONDUCTOR_UI_SERVER_URL") if self.__ui_host is None: self.__ui_host = self.server_url.replace("/api", "") @@ -172,6 +170,10 @@ def __init__( if debug: self.logger.setLevel(logging.DEBUG) + # Orkes Conductor auth token properties + self.token_update_time = 0 + self.auth_token_ttl_sec = auth_token_ttl_min * 60 + def _get_env_float(self, env_var: str, default: float) -> float: """Get float value from environment variable with default fallback.""" try: @@ -450,16 +452,13 @@ def log_level(self) -> int: """Get log level.""" return self.__log_level - def apply_logging_config(self, log_format : Optional[str] = None, level = None): + def apply_logging_config(self, log_format: Optional[str] = None, level=None): """Apply logging configuration for the application.""" if log_format is None: log_format = self.logger_format if level is None: level = self.__log_level - logging.basicConfig( - format=log_format, - level=level - ) + logging.basicConfig(format=log_format, level=level) @staticmethod def get_logging_formatted_name(name): @@ -474,5 +473,7 @@ def ui_host(self): def __getattr__(self, name: str) -> Any: """Delegate attribute access to underlying HTTP configuration.""" if "_http_config" not in self.__dict__ or self._http_config is None: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) return getattr(self._http_config, name) From a9e313da3c1ce2e8c393bae132698fae3b6657d4 Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Tue, 16 Sep 2025 17:22:38 +0300 Subject: [PATCH 02/10] Testcov for an async Configuration --- .../configuration/test_async_configuration.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/unit/configuration/test_async_configuration.py diff --git a/tests/unit/configuration/test_async_configuration.py b/tests/unit/configuration/test_async_configuration.py new file mode 100644 index 00000000..d35d59ba --- /dev/null +++ b/tests/unit/configuration/test_async_configuration.py @@ -0,0 +1,25 @@ +import pytest + +from conductor.asyncio_client.configuration import Configuration + + +def test_initialization_default(monkeypatch): + monkeypatch.setenv("CONDUCTOR_SERVER_URL", "http://localhost:8080/api") + configuration = Configuration() + assert configuration.host == "http://localhost:8080/api" + + +def test_initialization_with_base_url(): + configuration = Configuration(server_url="https://play.orkes.io/api") + assert configuration.host == "https://play.orkes.io/api" + + +def test_missed_http_config(): + configuration = Configuration() + configuration._http_config = None + with pytest.raises(AttributeError) as ctx: + _ = configuration.api_key + assert ( + f"'{Configuration.__class__.__name__}' object has no attribute 'api_key'" + in ctx.value + ) From 8674b2772c6ab587a5f57ffd11f56b776c1f2e8b Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Tue, 16 Sep 2025 19:18:42 +0300 Subject: [PATCH 03/10] Testcov for async ApiClientAdapter --- .../unit/api_client/test_async_api_client.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 tests/unit/api_client/test_async_api_client.py diff --git a/tests/unit/api_client/test_async_api_client.py b/tests/unit/api_client/test_async_api_client.py new file mode 100644 index 00000000..698081af --- /dev/null +++ b/tests/unit/api_client/test_async_api_client.py @@ -0,0 +1,221 @@ +import asyncio +import time + +import pytest + +from conductor.asyncio_client.adapters import ApiClient +from conductor.asyncio_client.configuration.configuration import Configuration +from conductor.asyncio_client.http.rest import RESTResponse + + +@pytest.fixture +def api_client(): + configuration = Configuration( + server_url="http://localhost:8080/api", + auth_key="test_key", + auth_secret="test_secret", + ) + return ApiClient(configuration) + + +@pytest.fixture +def mock_success_response(mocker): + response = mocker.Mock(spec=RESTResponse) + response.status = 200 + response.data = b'{"token": "test_token"}' + response.read = mocker.Mock() + return response + + +@pytest.fixture +def mock_401_response(mocker): + response = mocker.Mock(spec=RESTResponse) + response.status = 401 + response.data = b'{"message":"Token cannot be null or empty","error":"INVALID_TOKEN","timestamp":1758039192168}' + response.read = mocker.AsyncMock() + return response + + +@pytest.mark.asyncio +async def test_refresh_authorization_token_called_on_invalid_token( + mocker, api_client, mock_401_response, mock_success_response +): + api_client.configuration._http_config.api_key = {} + + api_client.rest_client = mocker.AsyncMock() + api_client.rest_client.request.side_effect = [ + mock_401_response, + mock_success_response, + ] + + mock_refresh = mocker.patch.object( + api_client, "refresh_authorization_token", new_callable=mocker.AsyncMock + ) + mock_refresh.return_value = "new_token" + + mock_obtain = mocker.patch.object( + api_client, "obtain_new_token", new_callable=mocker.AsyncMock + ) + mock_obtain.return_value = {"token": "new_token"} + + await api_client.call_api( + method="GET", url="http://localhost:8080/api/test", header_params={} + ) + + mock_refresh.assert_called_once() + + +@pytest.mark.asyncio +async def test_refresh_authorization_token_called_on_expired_token( + mocker, api_client, mock_401_response, mock_success_response +): + current_time = time.time() + api_client.configuration.token_update_time = current_time - 3600 + api_client.configuration.auth_token_ttl_sec = 1800 + api_client.configuration._http_config.api_key = {"api_key": "old_token"} + + api_client.rest_client = mocker.AsyncMock() + api_client.rest_client.request.side_effect = [ + mock_401_response, + mock_success_response, + ] + + mock_refresh = mocker.patch.object( + api_client, "refresh_authorization_token", new_callable=mocker.AsyncMock + ) + mock_refresh.return_value = "new_token" + + mock_obtain = mocker.patch.object( + api_client, "obtain_new_token", new_callable=mocker.AsyncMock + ) + mock_obtain.return_value = {"token": "new_token"} + + await api_client.call_api( + method="GET", url="http://localhost:8080/api/test", header_params={} + ) + + mock_refresh.assert_called_once() + + +@pytest.mark.asyncio +async def test_token_lock_prevents_concurrent_refresh( + mocker, api_client, mock_401_response, mock_success_response +): + api_client.configuration._http_config.api_key = {} + + refresh_calls = [] + + async def mock_refresh(): + refresh_calls.append(time.time()) + await asyncio.sleep(0.1) + return "new_token" + + mocker.patch.object( + api_client, "refresh_authorization_token", side_effect=mock_refresh + ) + + mock_obtain = mocker.patch.object( + api_client, "obtain_new_token", new_callable=mocker.AsyncMock + ) + mock_obtain.return_value = {"token": "new_token"} + + api_client.rest_client = mocker.AsyncMock() + api_client.rest_client.request.side_effect = [ + mock_401_response, + mock_success_response, + mock_401_response, + mock_success_response, + ] + + tasks = [ + api_client.call_api( + method="GET", + url="http://localhost:8080/api/test1", + header_params={}, + ), + api_client.call_api( + method="GET", + url="http://localhost:8080/api/test2", + header_params={}, + ), + ] + + await asyncio.gather(*tasks) + + assert len(refresh_calls) == 1 + + +@pytest.mark.asyncio +async def test_no_refresh_when_token_valid_and_not_expired( + mocker, api_client, mock_success_response +): + current_time = time.time() + api_client.configuration.token_update_time = current_time - 100 + api_client.configuration.auth_token_ttl_sec = 1800 + api_client.configuration._http_config.api_key = {"api_key": "valid_token"} + + api_client.rest_client = mocker.AsyncMock() + api_client.rest_client.request.return_value = mock_success_response + + mock_refresh = mocker.patch.object( + api_client, "refresh_authorization_token", new_callable=mocker.AsyncMock + ) + + await api_client.call_api( + method="GET", url="http://localhost:8080/api/test", header_params={} + ) + + mock_refresh.assert_not_called() + + +@pytest.mark.asyncio +async def test_no_refresh_for_token_endpoint(mocker, api_client, mock_401_response): + api_client.configuration._http_config.api_key = {} + + api_client.rest_client = mocker.AsyncMock() + api_client.rest_client.request.return_value = mock_401_response + + mock_refresh = mocker.patch.object( + api_client, "refresh_authorization_token", new_callable=mocker.AsyncMock + ) + + await api_client.call_api( + method="POST", url="http://localhost:8080/api/token", header_params={} + ) + + mock_refresh.assert_not_called() + + +@pytest.mark.asyncio +async def test_401_response_triggers_retry_with_new_token( + mocker, api_client, mock_401_response, mock_success_response +): + api_client.configuration._http_config.api_key = {} + + api_client.rest_client = mocker.AsyncMock() + api_client.rest_client.request.side_effect = [ + mock_401_response, + mock_success_response, + ] + + mock_refresh = mocker.patch.object( + api_client, "refresh_authorization_token", new_callable=mocker.AsyncMock + ) + mock_refresh.return_value = "new_token" + + mock_obtain = mocker.patch.object( + api_client, "obtain_new_token", new_callable=mocker.AsyncMock + ) + mock_obtain.return_value = {"token": "new_token"} + + header_params = {} + await api_client.call_api( + method="GET", + url="http://localhost:8080/api/test", + header_params=header_params, + ) + + assert api_client.rest_client.request.call_count == 2 + + second_call_args = api_client.rest_client.request.call_args_list[1] + assert second_call_args[1]["headers"]["X-Authorization"] == "new_token" From eaf4dfaabd07013590fcae05238fa37eac77de6c Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Wed, 24 Sep 2025 12:29:59 +0300 Subject: [PATCH 04/10] Added lock explanation comment --- src/conductor/asyncio_client/adapters/api_client_adapter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/conductor/asyncio_client/adapters/api_client_adapter.py b/src/conductor/asyncio_client/adapters/api_client_adapter.py index 9f786495..7474a01a 100644 --- a/src/conductor/asyncio_client/adapters/api_client_adapter.py +++ b/src/conductor/asyncio_client/adapters/api_client_adapter.py @@ -51,6 +51,9 @@ async def call_api( and url != self.configuration.host + "/token" ): # noqa: PLR2004 (Unauthorized status code) async with self._token_lock: + # The lock is intentionally broad (covers the whole block including the token state) + # to avoid race conditions: without it, other coroutines could mis-evaluate + # token state during a context switch and trigger redundant refreshes token_expired = ( self.configuration.token_update_time > 0 and time.time() From 4b4862536286219fc4aaffee89a3f1f82bc597a5 Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Wed, 24 Sep 2025 12:31:54 +0300 Subject: [PATCH 05/10] Moved noqa directive for ruff --- src/conductor/asyncio_client/adapters/api_client_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conductor/asyncio_client/adapters/api_client_adapter.py b/src/conductor/asyncio_client/adapters/api_client_adapter.py index 7474a01a..181950c4 100644 --- a/src/conductor/asyncio_client/adapters/api_client_adapter.py +++ b/src/conductor/asyncio_client/adapters/api_client_adapter.py @@ -47,9 +47,9 @@ async def call_api( _request_timeout=_request_timeout, ) if ( - response_data.status == 401 + response_data.status == 401 # noqa: PLR2004 (Unauthorized status code) and url != self.configuration.host + "/token" - ): # noqa: PLR2004 (Unauthorized status code) + ): async with self._token_lock: # The lock is intentionally broad (covers the whole block including the token state) # to avoid race conditions: without it, other coroutines could mis-evaluate From 928bbb516fee10c1c727e9f710b7ec78566e6319 Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Wed, 24 Sep 2025 12:36:15 +0300 Subject: [PATCH 06/10] Run black --- src/conductor/asyncio_client/adapters/api_client_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conductor/asyncio_client/adapters/api_client_adapter.py b/src/conductor/asyncio_client/adapters/api_client_adapter.py index 181950c4..23d59416 100644 --- a/src/conductor/asyncio_client/adapters/api_client_adapter.py +++ b/src/conductor/asyncio_client/adapters/api_client_adapter.py @@ -47,7 +47,7 @@ async def call_api( _request_timeout=_request_timeout, ) if ( - response_data.status == 401 # noqa: PLR2004 (Unauthorized status code) + response_data.status == 401 # noqa: PLR2004 (Unauthorized status code) and url != self.configuration.host + "/token" ): async with self._token_lock: From d81b16a674d3e97e5ea91cb013a86213ddd69cdf Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Thu, 25 Sep 2025 09:45:44 +0300 Subject: [PATCH 07/10] Apply formatting --- .../adapters/api_client_adapter.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/conductor/asyncio_client/adapters/api_client_adapter.py b/src/conductor/asyncio_client/adapters/api_client_adapter.py index d8948bc2..fcebf870 100644 --- a/src/conductor/asyncio_client/adapters/api_client_adapter.py +++ b/src/conductor/asyncio_client/adapters/api_client_adapter.py @@ -43,7 +43,9 @@ async def call_api( """ try: - logger.debug("HTTP request method: %s; url: %s; header_params: %s", method, url, header_params) + logger.debug( + "HTTP request method: %s; url: %s; header_params: %s", method, url, header_params + ) response_data = await self.rest_client.request( method, url, @@ -56,7 +58,9 @@ async def call_api( response_data.status == 401 # noqa: PLR2004 (Unauthorized status code) and url != self.configuration.host + "/token" ): - logger.warning("HTTP response from: %s; status code: 401 - obtaining new token", url) + logger.warning( + "HTTP response from: %s; status code: 401 - obtaining new token", url + ) async with self._token_lock: # The lock is intentionally broad (covers the whole block including the token state) # to avoid race conditions: without it, other coroutines could mis-evaluate @@ -67,9 +71,7 @@ async def call_api( >= self.configuration.token_update_time + self.configuration.auth_token_ttl_sec ) - invalid_token = not self.configuration._http_config.api_key.get( - "api_key" - ) + invalid_token = not self.configuration._http_config.api_key.get("api_key") if invalid_token or token_expired: token = await self.refresh_authorization_token() @@ -85,7 +87,9 @@ async def call_api( _request_timeout=_request_timeout, ) except ApiException as e: - logger.error("HTTP request failed url: %s status: %s; reason: %s", url, e.status, e.reason) + logger.error( + "HTTP request failed url: %s status: %s; reason: %s", url, e.status, e.reason + ) raise e return response_data @@ -111,9 +115,7 @@ def response_deserialize( and 100 <= response_data.status <= 599 ): # if not found, look for '1XX', '2XX', etc. - response_type = response_types_map.get( - str(response_data.status)[0] + "XX", None - ) + response_type = response_types_map.get(str(response_data.status)[0] + "XX", None) # deserialize response data response_text = None @@ -130,9 +132,7 @@ def response_deserialize( match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" response_text = response_data.data.decode(encoding) - return_data = self.deserialize( - response_text, response_type, content_type - ) + return_data = self.deserialize(response_text, response_type, content_type) finally: if not 200 <= response_data.status <= 299: logger.error(f"Unexpected response status code: {response_data.status}") From e0ed82f8250ef9e12cdc239eb2e39c80ee465a75 Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Thu, 25 Sep 2025 13:26:06 +0300 Subject: [PATCH 08/10] Post merge fixes --- .github/workflows/pull_request.yml | 6 +++--- .../asyncio_client/configuration/configuration.py | 8 ++++---- tests/unit/asyncio_client/test_api_client_adapter.py | 4 ++-- tests/unit/asyncio_client/test_configuration.py | 6 ------ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c33c81e5..cb2c5d84 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -81,7 +81,7 @@ jobs: /bin/sh -c "cd /package && COVERAGE_FILE=/package/${{ env.COVERAGE_DIR }}/.coverage.integration coverage run -m pytest -m v4 tests/integration -v" - name: Run asyncio integration tests - id: integration_tests + id: asyncio_integration_tests continue-on-error: true run: | docker run --rm \ @@ -90,7 +90,7 @@ jobs: -e CONDUCTOR_SERVER_URL=${{ env.CONDUCTOR_SERVER_URL }} \ -v ${{ github.workspace }}/${{ env.COVERAGE_DIR }}:/package/${{ env.COVERAGE_DIR }}:rw \ conductor-sdk-test:latest \ - /bin/sh -c "cd /package && COVERAGE_FILE=/package/${{ env.COVERAGE_DIR }}/.coverage.integration coverage run -m pytest -m v4 tests/integration -v" + /bin/sh -c "cd /package && COVERAGE_FILE=/package/${{ env.COVERAGE_DIR }}/.coverage.asyncio_integration coverage run -m pytest -m v4 tests/integration/async -v" - name: Generate coverage report id: coverage_report @@ -124,4 +124,4 @@ jobs: - name: Check test results if: steps.unit_tests.outcome == 'failure' || steps.bc_tests.outcome == 'failure' || steps.serdeser_tests.outcome == 'failure' - run: exit 1 \ No newline at end of file + run: exit 1 diff --git a/src/conductor/asyncio_client/configuration/configuration.py b/src/conductor/asyncio_client/configuration/configuration.py index 2451e787..f35c06f4 100644 --- a/src/conductor/asyncio_client/configuration/configuration.py +++ b/src/conductor/asyncio_client/configuration/configuration.py @@ -502,14 +502,14 @@ def log_level(self) -> int: def apply_logging_config(self, log_format: Optional[str] = None, level=None): """Apply logging configuration for the application.""" + if self.is_logger_config_applied: + return if log_format is None: log_format = self.logger_format if level is None: level = self.__log_level - logging.basicConfig( - format=log_format, - level=level - ) + logging.basicConfig(format=log_format, level=level) + self.is_logger_config_applied = True @staticmethod def get_logging_formatted_name(name): diff --git a/tests/unit/asyncio_client/test_api_client_adapter.py b/tests/unit/asyncio_client/test_api_client_adapter.py index aecde358..1234d26c 100644 --- a/tests/unit/asyncio_client/test_api_client_adapter.py +++ b/tests/unit/asyncio_client/test_api_client_adapter.py @@ -1,15 +1,15 @@ import pytest from unittest.mock import AsyncMock, MagicMock, patch from conductor.asyncio_client.adapters.api_client_adapter import ApiClientAdapter +from conductor.asyncio_client.configuration import Configuration from conductor.asyncio_client.http.exceptions import ApiException from conductor.asyncio_client.http.api_response import ApiResponse @pytest.fixture def mock_config(): - config = MagicMock() + config = Configuration() config.host = "http://test.com" - config.api_key = {"api_key": "test_token"} config.auth_key = "test_key" config.auth_secret = "test_secret" return config diff --git a/tests/unit/asyncio_client/test_configuration.py b/tests/unit/asyncio_client/test_configuration.py index a4be3d5c..8cdab6d1 100644 --- a/tests/unit/asyncio_client/test_configuration.py +++ b/tests/unit/asyncio_client/test_configuration.py @@ -378,12 +378,6 @@ def test_getattr_no_http_config(): _ = config.nonexistent_attr -def test_auth_setup_with_credentials(): - config = Configuration(auth_key="key", auth_secret="secret") - assert "api_key" in config.api_key - assert config.api_key["api_key"] == "key" - - def test_worker_properties_dict_initialization(): config = Configuration() assert isinstance(config._worker_properties, dict) From 353c1386ad1de815789fe20d7e6fc2cd3fb4437f Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Thu, 25 Sep 2025 13:28:58 +0300 Subject: [PATCH 09/10] Formatting --- .../asyncio_client/configuration/configuration.py | 8 ++------ tests/unit/asyncio_client/test_api_client_adapter.py | 12 +++--------- tests/unit/asyncio_client/test_configuration.py | 7 +++---- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/conductor/asyncio_client/configuration/configuration.py b/src/conductor/asyncio_client/configuration/configuration.py index f35c06f4..8177242f 100644 --- a/src/conductor/asyncio_client/configuration/configuration.py +++ b/src/conductor/asyncio_client/configuration/configuration.py @@ -269,9 +269,7 @@ def _convert_property_value(self, property_name: str, value: str) -> Any: # For other properties, return as string return value - def set_worker_property( - self, task_type: str, property_name: str, value: Any - ) -> None: + def set_worker_property(self, task_type: str, property_name: str, value: Any) -> None: """ Set worker property for a specific task type. @@ -524,7 +522,5 @@ def ui_host(self): def __getattr__(self, name: str) -> Any: """Delegate attribute access to underlying HTTP configuration.""" if "_http_config" not in self.__dict__ or self._http_config is None: - raise AttributeError( - f"'{self.__class__.__name__}' object has no attribute '{name}'" - ) + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") return getattr(self._http_config, name) diff --git a/tests/unit/asyncio_client/test_api_client_adapter.py b/tests/unit/asyncio_client/test_api_client_adapter.py index 1234d26c..494b74d4 100644 --- a/tests/unit/asyncio_client/test_api_client_adapter.py +++ b/tests/unit/asyncio_client/test_api_client_adapter.py @@ -50,9 +50,7 @@ async def test_call_api_401_retry(adapter): adapter.rest_client.request = AsyncMock(return_value=mock_response) adapter.refresh_authorization_token = AsyncMock(return_value="new_token") - result = await adapter.call_api( - "GET", "http://test.com/api", {"X-Authorization": "old_token"} - ) + result = await adapter.call_api("GET", "http://test.com/api", {"X-Authorization": "old_token"}) assert result == mock_response assert adapter.rest_client.request.call_count == 2 @@ -215,9 +213,7 @@ async def test_obtain_new_token_with_patch(): client_adapter.configuration = MagicMock() client_adapter.configuration.auth_key = "test_key" client_adapter.configuration.auth_secret = "test_secret" - client_adapter.param_serialize = MagicMock( - return_value=("POST", "/token", {}, {}) - ) + client_adapter.param_serialize = MagicMock(return_value=("POST", "/token", {}, {})) mock_response = MagicMock() mock_response.data = b'{"token": "test_token"}' @@ -227,9 +223,7 @@ async def test_obtain_new_token_with_patch(): result = await client_adapter.obtain_new_token() assert result == {"token": "test_token"} - mock_generate_token.assert_called_once_with( - key_id="test_key", key_secret="test_secret" - ) + mock_generate_token.assert_called_once_with(key_id="test_key", key_secret="test_secret") def test_response_deserialize_encoding_detection(adapter): diff --git a/tests/unit/asyncio_client/test_configuration.py b/tests/unit/asyncio_client/test_configuration.py index 8cdab6d1..db4f427a 100644 --- a/tests/unit/asyncio_client/test_configuration.py +++ b/tests/unit/asyncio_client/test_configuration.py @@ -50,6 +50,7 @@ def test_initialization_with_env_vars(monkeypatch): assert config.domain == "env_domain" assert config.polling_interval_seconds == 10 + def test_initialization_env_vars_override_params(monkeypatch): monkeypatch.setenv("CONDUCTOR_SERVER_URL", "https://env.com/api") monkeypatch.setenv("CONDUCTOR_AUTH_KEY", "env_key") @@ -146,7 +147,7 @@ def test_get_worker_property_value_poll_interval_seconds(): result = config.get_worker_property_value("poll_interval_seconds", "mytask") assert result == 0 - + def test_convert_property_value_polling_interval(): config = Configuration() result = config._convert_property_value("polling_interval", "250") @@ -392,9 +393,7 @@ def test_get_worker_property_value_unknown_property(): def test_get_poll_interval_with_task_type_none_value(): config = Configuration() - with patch.dict( - os.environ, {"CONDUCTOR_WORKER_MYTASK_POLLING_INTERVAL": "invalid"} - ): + with patch.dict(os.environ, {"CONDUCTOR_WORKER_MYTASK_POLLING_INTERVAL": "invalid"}): result = config.get_poll_interval("mytask") assert result == 100 From 4f9ce11e33a184e45a8a1f22aa2a39175ced16a8 Mon Sep 17 00:00:00 2001 From: IgorChvyrov-sm Date: Thu, 25 Sep 2025 13:32:57 +0300 Subject: [PATCH 10/10] Ruff fixes --- .../asyncio_client/adapters/api_client_adapter.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/conductor/asyncio_client/adapters/api_client_adapter.py b/src/conductor/asyncio_client/adapters/api_client_adapter.py index fcebf870..2eb91344 100644 --- a/src/conductor/asyncio_client/adapters/api_client_adapter.py +++ b/src/conductor/asyncio_client/adapters/api_client_adapter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import json import logging @@ -112,7 +114,7 @@ def response_deserialize( if ( not response_type and isinstance(response_data.status, int) - and 100 <= response_data.status <= 599 + and 100 <= response_data.status <= 599 # noqa: PLR2004 ): # if not found, look for '1XX', '2XX', etc. response_type = response_types_map.get(str(response_data.status)[0] + "XX", None) @@ -134,8 +136,8 @@ def response_deserialize( response_text = response_data.data.decode(encoding) return_data = self.deserialize(response_text, response_type, content_type) finally: - if not 200 <= response_data.status <= 299: - logger.error(f"Unexpected response status code: {response_data.status}") + if not 200 <= response_data.status <= 299: # noqa: PLR2004 + logger.error("Unexpected response status code: %s", response_data.status) raise ApiException.from_response( http_resp=response_data, body=response_text, @@ -154,7 +156,7 @@ async def refresh_authorization_token(self): token = obtain_new_token_response.get("token") self.configuration._http_config.api_key["api_key"] = token self.configuration.token_update_time = time.time() - logger.debug(f"New auth token been set") + logger.debug("New auth token been set") return token async def obtain_new_token(self):