From 84b0d1c3bb2f4dec48b8d41968b80a5b324cde01 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Tue, 6 May 2025 17:11:47 +0530 Subject: [PATCH 01/19] chore: Add unit test cases --- packages/toolbox-core/tests/test_client.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index 1b36fe0d..1a4860d3 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -398,6 +398,35 @@ async def test_constructor_getters_missing_fail(self, tool_name, client): await client.load_tool(tool_name, auth_token_getters={AUTH_SERVICE: {}}) + @pytest.mark.asyncio + async def test_add_auth_token_getters_missing_fail(self, tool_name, client): + """ + Tests that adding a missing auth token getter raises ValueError. + """ + AUTH_SERVICE = "xmy-auth-service" + + tool = await client.load_tool(tool_name) + + with pytest.raises( + ValueError, + match=f"Authentication source\(s\) \`{AUTH_SERVICE}\` unused by tool \`{tool_name}\`.", + ): + tool.add_auth_token_getters({AUTH_SERVICE: {}}) + + @pytest.mark.asyncio + async def test_constructor_getters_missing_fail(self, tool_name, client): + """ + Tests that adding a missing auth token getter raises ValueError. + """ + AUTH_SERVICE = "xmy-auth-service" + + with pytest.raises( + ValueError, + match=f"Validation failed for tool '{tool_name}': unused auth tokens: {AUTH_SERVICE}.", + ): + await client.load_tool(tool_name, auth_token_getters={AUTH_SERVICE: {}}) + + class TestBoundParameter: @pytest.fixture From 89fa0107c4946180beab27e4d409ffdb29c429cd Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Tue, 6 May 2025 17:12:37 +0530 Subject: [PATCH 02/19] chore: Delint --- packages/toolbox-core/tests/test_client.py | 29 ---------------------- 1 file changed, 29 deletions(-) diff --git a/packages/toolbox-core/tests/test_client.py b/packages/toolbox-core/tests/test_client.py index 1a4860d3..1b36fe0d 100644 --- a/packages/toolbox-core/tests/test_client.py +++ b/packages/toolbox-core/tests/test_client.py @@ -398,35 +398,6 @@ async def test_constructor_getters_missing_fail(self, tool_name, client): await client.load_tool(tool_name, auth_token_getters={AUTH_SERVICE: {}}) - @pytest.mark.asyncio - async def test_add_auth_token_getters_missing_fail(self, tool_name, client): - """ - Tests that adding a missing auth token getter raises ValueError. - """ - AUTH_SERVICE = "xmy-auth-service" - - tool = await client.load_tool(tool_name) - - with pytest.raises( - ValueError, - match=f"Authentication source\(s\) \`{AUTH_SERVICE}\` unused by tool \`{tool_name}\`.", - ): - tool.add_auth_token_getters({AUTH_SERVICE: {}}) - - @pytest.mark.asyncio - async def test_constructor_getters_missing_fail(self, tool_name, client): - """ - Tests that adding a missing auth token getter raises ValueError. - """ - AUTH_SERVICE = "xmy-auth-service" - - with pytest.raises( - ValueError, - match=f"Validation failed for tool '{tool_name}': unused auth tokens: {AUTH_SERVICE}.", - ): - await client.load_tool(tool_name, auth_token_getters={AUTH_SERVICE: {}}) - - class TestBoundParameter: @pytest.fixture From 3acd5d3023f2c841a7be746bbe673e520371475e Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Tue, 6 May 2025 17:40:02 +0530 Subject: [PATCH 03/19] feat: Warn on insecure tool invocation with authentication This change introduces a warning that is displayed immediately before a tool invocation if: 1. The invocation includes an authentication header. 2. The connection is being made over non-secure HTTP. > [!IMPORTANT] The purpose of this warning is to alert the user to the security risk of sending credentials over an unencrypted channel and to encourage the use of HTTPS. --- packages/toolbox-core/src/toolbox_core/tool.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index 5a9bca6e..a28135f9 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -257,18 +257,27 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str: payload[param] = await resolve_value(value) # create headers for auth services - headers = {} + auth_headers = {} for auth_service, token_getter in self.__auth_service_token_getters.items(): - headers[self.__get_auth_header(auth_service)] = await resolve_value( + auth_headers[self.__get_auth_header(auth_service)] = await resolve_value( token_getter ) for client_header_name, client_header_val in self.__client_headers.items(): - headers[client_header_name] = await resolve_value(client_header_val) + auth_headers[client_header_name] = await resolve_value(client_header_val) + + # ID tokens contain sensitive user information (claims). Transmitting + # these over HTTP exposes the data to interception and unauthorized + # access. Always use HTTPS to ensure secure communication and protect + # user privacy. + if auth_headers and not self.__url.startswith("https://"): + warn( + "Sending ID token over HTTP. User data may be exposed. Use HTTPS for secure communication." + ) async with self.__session.post( self.__url, json=payload, - headers=headers, + headers=auth_headers, ) as resp: body = await resp.json() if resp.status < 200 or resp.status >= 300: From 7fa72f68a5d0c08586f4e9baea17947f6cd6e3af Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Tue, 13 May 2025 15:57:46 +0530 Subject: [PATCH 04/19] fix!: Warn about https only during tool initialization --- packages/toolbox-core/src/toolbox_core/tool.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index a28135f9..5a9bca6e 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -257,27 +257,18 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str: payload[param] = await resolve_value(value) # create headers for auth services - auth_headers = {} + headers = {} for auth_service, token_getter in self.__auth_service_token_getters.items(): - auth_headers[self.__get_auth_header(auth_service)] = await resolve_value( + headers[self.__get_auth_header(auth_service)] = await resolve_value( token_getter ) for client_header_name, client_header_val in self.__client_headers.items(): - auth_headers[client_header_name] = await resolve_value(client_header_val) - - # ID tokens contain sensitive user information (claims). Transmitting - # these over HTTP exposes the data to interception and unauthorized - # access. Always use HTTPS to ensure secure communication and protect - # user privacy. - if auth_headers and not self.__url.startswith("https://"): - warn( - "Sending ID token over HTTP. User data may be exposed. Use HTTPS for secure communication." - ) + headers[client_header_name] = await resolve_value(client_header_val) async with self.__session.post( self.__url, json=payload, - headers=auth_headers, + headers=headers, ) as resp: body = await resp.json() if resp.status < 200 or resp.status >= 300: From 2750100382cbb3e917d6b3603d74a45b676ade0f Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 10 May 2025 00:56:49 +0530 Subject: [PATCH 05/19] fix: Make protected members private --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 1b21d722..e01f4dd0 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -67,6 +67,18 @@ def __init__( f"{self.__class__.__qualname__}.{self.__async_tool.__name__}" ) + @property + def _async_tool(self) -> ToolboxTool: + return self.__async_tool + + @property + def _loop(self) -> AbstractEventLoop: + return self.__loop + + @property + def _thread(self) -> Thread: + return self.__thread + @property def __name__(self) -> str: return self.__async_tool.__name__ From 242066d0115624c6561a8f926ebe210a3fdc0844 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 10 May 2025 01:06:08 +0530 Subject: [PATCH 06/19] chore: Expose members of sync client as well --- .../toolbox-core/src/toolbox_core/sync_client.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 6bb54b45..d2aca1dc 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -14,6 +14,7 @@ import asyncio +from asyncio import AbstractEventLoop from threading import Thread from typing import Any, Callable, Coroutine, Mapping, Optional, Union @@ -29,7 +30,7 @@ class ToolboxSyncClient: service endpoint. """ - __loop: Optional[asyncio.AbstractEventLoop] = None + __loop: Optional[AbstractEventLoop] = None __thread: Optional[Thread] = None def __init__( @@ -61,6 +62,18 @@ async def create_client(): create_client(), self.__class__.__loop ).result() + @property + def _async_client(self) -> ToolboxClient: + return self.__async_client + + @property + def _loop(self) -> AbstractEventLoop: + return self.__class__.__loop + + @property + def _thread(self) -> Thread: + return self.__class__.__thread + def close(self): """ Synchronously closes the underlying client session. Doing so will cause From f2eaf3fc31924b749fecf84907e8a32f6a95aeba Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 10 May 2025 01:08:44 +0530 Subject: [PATCH 07/19] chore: Fix types --- packages/toolbox-core/src/toolbox_core/sync_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index d2aca1dc..64b7165e 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -63,11 +63,11 @@ async def create_client(): ).result() @property - def _async_client(self) -> ToolboxClient: + def _async_client(self) -> Optional[ToolboxClient]: return self.__async_client @property - def _loop(self) -> AbstractEventLoop: + def _loop(self) -> Optional[AbstractEventLoop]: return self.__class__.__loop @property From 54e40a05063fa986e2a8ed9fa2d97a6b7d5fc1eb Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 10 May 2025 01:11:43 +0530 Subject: [PATCH 08/19] chore: Fix types --- packages/toolbox-core/src/toolbox_core/sync_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 64b7165e..e3c2a626 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -63,7 +63,7 @@ async def create_client(): ).result() @property - def _async_client(self) -> Optional[ToolboxClient]: + def _async_client(self) -> ToolboxClient: return self.__async_client @property @@ -71,7 +71,7 @@ def _loop(self) -> Optional[AbstractEventLoop]: return self.__class__.__loop @property - def _thread(self) -> Thread: + def _thread(self) -> Optional[Thread]: return self.__class__.__thread def close(self): From 62a4459be698dfcc047cd13452beb361dfd755d9 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 10 May 2025 01:25:35 +0530 Subject: [PATCH 09/19] chore: Expose pydantic model from async tool --- packages/toolbox-core/src/toolbox_core/tool.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index 5a9bca6e..ad997d40 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -19,6 +19,7 @@ from warnings import warn from aiohttp import ClientSession +from pydantic import BaseModel from .protocol import ParameterSchema from .utils import ( @@ -158,6 +159,10 @@ def _auth_service_token_getters(self) -> Mapping[str, Callable[[], str]]: def _client_headers(self) -> Mapping[str, Union[Callable, Coroutine, str]]: return MappingProxyType(self.__client_headers) + @property + def _pydantic_model(self) -> type[BaseModel]: + return self.__pydantic_model + def __copy( self, session: Optional[ClientSession] = None, From ee8185b0dbe423f335ca7ae61de3a3fa0434059b Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Tue, 13 May 2025 03:00:36 +0530 Subject: [PATCH 10/19] chore: Add test for pydantic_model property --- packages/toolbox-core/tests/test_tool.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_tool.py b/packages/toolbox-core/tests/test_tool.py index c64149f5..7dffad91 100644 --- a/packages/toolbox-core/tests/test_tool.py +++ b/packages/toolbox-core/tests/test_tool.py @@ -23,7 +23,7 @@ import pytest_asyncio from aiohttp import ClientSession from aioresponses import aioresponses -from pydantic import ValidationError +from pydantic import BaseModel, ValidationError from toolbox_core.protocol import ParameterSchema from toolbox_core.tool import ToolboxTool, create_func_docstring, resolve_value @@ -578,6 +578,24 @@ def test_toolbox_tool_underscore_client_headers_property(toolbox_tool: ToolboxTo client_headers["new_header"] = "new_value" +def test_toolbox_tool_underscore_pydantic_model_property(toolbox_tool: ToolboxTool): + """Tests the _pydantic_model property returns the correct Pydantic model.""" + pydantic_model = toolbox_tool._pydantic_model + assert issubclass(pydantic_model, BaseModel) + assert pydantic_model.__name__ == TEST_TOOL_NAME + + # Test that the model can validate expected data + valid_data = {"message": "test", "count": 10} + validated_data = pydantic_model.model_validate(valid_data) + assert validated_data.message == "test" + assert validated_data.count == 10 + + # Test that the model raises ValidationError for invalid data + invalid_data = {"message": 123, "count": "not_an_int"} + with pytest.raises(ValidationError): + pydantic_model.model_validate(invalid_data) + + # --- Test for the HTTP Warning --- @pytest.mark.parametrize( "trigger_condition_params", From a80d94d689d639878c44d602315e571200e55e59 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Tue, 13 May 2025 03:10:59 +0530 Subject: [PATCH 11/19] chore: Add test coverage for additional @property methods --- .../toolbox-core/tests/test_sync_client.py | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index 28f2b27b..472121a0 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -14,7 +14,9 @@ import inspect -from typing import Any, Callable, Mapping, Optional +from asyncio import AbstractEventLoop +from threading import Thread +from typing import Any, Callable, Generator, Mapping, Optional from unittest.mock import AsyncMock, Mock, patch import pytest @@ -605,3 +607,53 @@ def test_constructor_getters_missing_fail( tool_name_auth, auth_token_getters={UNUSED_AUTH_SERVICE: lambda: "token"}, ) + + +# --- Tests for @property methods of ToolboxSyncClient --- + + +@pytest.fixture +def sync_client_with_mocks() -> Generator[ToolboxSyncClient, Any, Any]: + """ + Fixture to create a ToolboxSyncClient with mocked internal async client + without relying on actual network calls during init. + """ + with patch( + "toolbox_core.sync_client.ToolboxClient", autospec=True + ) as MockToolboxClient: + # Mock the async client's constructor to return an AsyncMock instance + mock_async_client_instance = AsyncMock(spec=ToolboxClient) + MockToolboxClient.return_value = mock_async_client_instance + + # Mock the run_coroutine_threadsafe and its result() + with patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe: + mock_future = Mock() + mock_future.result.return_value = mock_async_client_instance + mock_run_coroutine_threadsafe.return_value = mock_future + + client = ToolboxSyncClient(TEST_BASE_URL) + yield client + + +def test_sync_client_underscore_async_client_property( + sync_client_with_mocks: ToolboxSyncClient, +): + """Tests the _async_client property.""" + assert isinstance(sync_client_with_mocks._async_client, AsyncMock) + + +def test_sync_client_underscore_loop_property( + sync_client_with_mocks: ToolboxSyncClient, +): + """Tests the _loop property.""" + assert sync_client_with_mocks._loop is not None + assert isinstance(sync_client_with_mocks._loop, AbstractEventLoop) + + +def test_sync_client_underscore_thread_property( + sync_client_with_mocks: ToolboxSyncClient, +): + """Tests the _thread property.""" + assert sync_client_with_mocks._thread is not None + assert isinstance(sync_client_with_mocks._thread, Thread) + assert sync_client_with_mocks._thread.is_alive() From 080706c2fac86ff24c39146af5434edad8eefdbe Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Wed, 14 May 2025 17:42:04 +0530 Subject: [PATCH 12/19] chore: Add unit test coverage for internal properties --- .../toolbox-core/tests/test_sync_client.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index 472121a0..5bbedb7d 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -26,6 +26,7 @@ from toolbox_core.protocol import ManifestSchema, ParameterSchema, ToolSchema from toolbox_core.sync_client import ToolboxSyncClient from toolbox_core.sync_tool import ToolboxSyncTool +from toolbox_core.tool import ToolboxTool TEST_BASE_URL = "http://toolbox.example.com" @@ -234,6 +235,41 @@ def test_sync_load_toolset_success( assert result1 == f"{TOOL1_NAME}_ok" +def test_sync_tool_internal_properties(aioresponses, tool_schema_minimal, sync_client): + """ + Tests that the internal properties _async_tool, _loop, and _thread + of a ToolboxSyncTool instance are correctly initialized and accessible. + This directly covers the respective @property methods in ToolboxSyncTool. + """ + TOOL_NAME = "test_tool_for_internal_properties" + mock_tool_load(aioresponses, TOOL_NAME, tool_schema_minimal) + + loaded_sync_tool = sync_client.load_tool(TOOL_NAME) + + assert isinstance(loaded_sync_tool, ToolboxSyncTool) + + # 1. Test the _async_tool property + internal_async_tool = loaded_sync_tool._async_tool + assert isinstance(internal_async_tool, ToolboxTool) + assert internal_async_tool.__name__ == TOOL_NAME + + # 2. Test the _loop property + internal_loop = loaded_sync_tool._loop + assert isinstance(internal_loop, AbstractEventLoop) + assert internal_loop is sync_client._ToolboxSyncClient__loop + assert ( + internal_loop.is_running() + ), "The event loop used by ToolboxSyncTool should be running." + + # 3. Test the _thread property + internal_thread = loaded_sync_tool._thread + assert isinstance(internal_thread, Thread) + assert internal_thread is sync_client._ToolboxSyncClient__thread + assert ( + internal_thread.is_alive() + ), "The thread used by ToolboxSyncTool should be alive." + + def test_sync_invoke_tool_server_error(aioresponses, test_tool_str_schema, sync_client): TOOL_NAME = "sync_server_error_tool" ERROR_MESSAGE = "Simulated Server Error for Sync Client" From 132d58b61317906dc8e63e48798858ad6ec64ba5 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 16 May 2025 20:14:16 +0530 Subject: [PATCH 13/19] fix: Remove properties exposing internal state from sync client --- .../src/toolbox_core/sync_client.py | 12 ---------- .../toolbox-core/tests/test_sync_client.py | 24 ------------------- 2 files changed, 36 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index e3c2a626..46c73b39 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -62,18 +62,6 @@ async def create_client(): create_client(), self.__class__.__loop ).result() - @property - def _async_client(self) -> ToolboxClient: - return self.__async_client - - @property - def _loop(self) -> Optional[AbstractEventLoop]: - return self.__class__.__loop - - @property - def _thread(self) -> Optional[Thread]: - return self.__class__.__thread - def close(self): """ Synchronously closes the underlying client session. Doing so will cause diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index 5bbedb7d..d069c6c1 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -669,27 +669,3 @@ def sync_client_with_mocks() -> Generator[ToolboxSyncClient, Any, Any]: client = ToolboxSyncClient(TEST_BASE_URL) yield client - - -def test_sync_client_underscore_async_client_property( - sync_client_with_mocks: ToolboxSyncClient, -): - """Tests the _async_client property.""" - assert isinstance(sync_client_with_mocks._async_client, AsyncMock) - - -def test_sync_client_underscore_loop_property( - sync_client_with_mocks: ToolboxSyncClient, -): - """Tests the _loop property.""" - assert sync_client_with_mocks._loop is not None - assert isinstance(sync_client_with_mocks._loop, AbstractEventLoop) - - -def test_sync_client_underscore_thread_property( - sync_client_with_mocks: ToolboxSyncClient, -): - """Tests the _thread property.""" - assert sync_client_with_mocks._thread is not None - assert isinstance(sync_client_with_mocks._thread, Thread) - assert sync_client_with_mocks._thread.is_alive() From e2f57dd8124f1bdaf014efc4739366e1252f36a6 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 16 May 2025 20:16:04 +0530 Subject: [PATCH 14/19] fix: Remove properties exposing internal state from sync tool --- .../src/toolbox_core/sync_tool.py | 12 ------- .../toolbox-core/tests/test_sync_client.py | 35 ------------------- 2 files changed, 47 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index e01f4dd0..1b21d722 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -67,18 +67,6 @@ def __init__( f"{self.__class__.__qualname__}.{self.__async_tool.__name__}" ) - @property - def _async_tool(self) -> ToolboxTool: - return self.__async_tool - - @property - def _loop(self) -> AbstractEventLoop: - return self.__loop - - @property - def _thread(self) -> Thread: - return self.__thread - @property def __name__(self) -> str: return self.__async_tool.__name__ diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index d069c6c1..264eef5c 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -235,41 +235,6 @@ def test_sync_load_toolset_success( assert result1 == f"{TOOL1_NAME}_ok" -def test_sync_tool_internal_properties(aioresponses, tool_schema_minimal, sync_client): - """ - Tests that the internal properties _async_tool, _loop, and _thread - of a ToolboxSyncTool instance are correctly initialized and accessible. - This directly covers the respective @property methods in ToolboxSyncTool. - """ - TOOL_NAME = "test_tool_for_internal_properties" - mock_tool_load(aioresponses, TOOL_NAME, tool_schema_minimal) - - loaded_sync_tool = sync_client.load_tool(TOOL_NAME) - - assert isinstance(loaded_sync_tool, ToolboxSyncTool) - - # 1. Test the _async_tool property - internal_async_tool = loaded_sync_tool._async_tool - assert isinstance(internal_async_tool, ToolboxTool) - assert internal_async_tool.__name__ == TOOL_NAME - - # 2. Test the _loop property - internal_loop = loaded_sync_tool._loop - assert isinstance(internal_loop, AbstractEventLoop) - assert internal_loop is sync_client._ToolboxSyncClient__loop - assert ( - internal_loop.is_running() - ), "The event loop used by ToolboxSyncTool should be running." - - # 3. Test the _thread property - internal_thread = loaded_sync_tool._thread - assert isinstance(internal_thread, Thread) - assert internal_thread is sync_client._ToolboxSyncClient__thread - assert ( - internal_thread.is_alive() - ), "The thread used by ToolboxSyncTool should be alive." - - def test_sync_invoke_tool_server_error(aioresponses, test_tool_str_schema, sync_client): TOOL_NAME = "sync_server_error_tool" ERROR_MESSAGE = "Simulated Server Error for Sync Client" From 7b12cc5ffea85bd5161b455036d4a2a696a17d92 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 16 May 2025 20:16:55 +0530 Subject: [PATCH 15/19] chore: Remove unused imports --- packages/toolbox-core/tests/test_sync_client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index 264eef5c..676e1432 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -14,8 +14,6 @@ import inspect -from asyncio import AbstractEventLoop -from threading import Thread from typing import Any, Callable, Generator, Mapping, Optional from unittest.mock import AsyncMock, Mock, patch @@ -26,7 +24,6 @@ from toolbox_core.protocol import ManifestSchema, ParameterSchema, ToolSchema from toolbox_core.sync_client import ToolboxSyncClient from toolbox_core.sync_tool import ToolboxSyncTool -from toolbox_core.tool import ToolboxTool TEST_BASE_URL = "http://toolbox.example.com" From 5b2cc8d024f9e52e1b4b9a82938200a362bef0a7 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 16 May 2025 21:17:03 +0530 Subject: [PATCH 16/19] chore: Remove now unnecessary fixture --- .../toolbox-core/tests/test_sync_client.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index 676e1432..90362716 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -605,29 +605,3 @@ def test_constructor_getters_missing_fail( tool_name_auth, auth_token_getters={UNUSED_AUTH_SERVICE: lambda: "token"}, ) - - -# --- Tests for @property methods of ToolboxSyncClient --- - - -@pytest.fixture -def sync_client_with_mocks() -> Generator[ToolboxSyncClient, Any, Any]: - """ - Fixture to create a ToolboxSyncClient with mocked internal async client - without relying on actual network calls during init. - """ - with patch( - "toolbox_core.sync_client.ToolboxClient", autospec=True - ) as MockToolboxClient: - # Mock the async client's constructor to return an AsyncMock instance - mock_async_client_instance = AsyncMock(spec=ToolboxClient) - MockToolboxClient.return_value = mock_async_client_instance - - # Mock the run_coroutine_threadsafe and its result() - with patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe: - mock_future = Mock() - mock_future.result.return_value = mock_async_client_instance - mock_run_coroutine_threadsafe.return_value = mock_future - - client = ToolboxSyncClient(TEST_BASE_URL) - yield client From eb48ffddc22ee8bd6c0021ac574e744a0e2e5cc9 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 16 May 2025 22:15:01 +0530 Subject: [PATCH 17/19] chore: Remove property exposing internal pydantic model --- packages/toolbox-core/src/toolbox_core/tool.py | 4 ---- packages/toolbox-core/tests/test_tool.py | 18 ------------------ 2 files changed, 22 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index ad997d40..1088e935 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -159,10 +159,6 @@ def _auth_service_token_getters(self) -> Mapping[str, Callable[[], str]]: def _client_headers(self) -> Mapping[str, Union[Callable, Coroutine, str]]: return MappingProxyType(self.__client_headers) - @property - def _pydantic_model(self) -> type[BaseModel]: - return self.__pydantic_model - def __copy( self, session: Optional[ClientSession] = None, diff --git a/packages/toolbox-core/tests/test_tool.py b/packages/toolbox-core/tests/test_tool.py index 7dffad91..9d5e5478 100644 --- a/packages/toolbox-core/tests/test_tool.py +++ b/packages/toolbox-core/tests/test_tool.py @@ -578,24 +578,6 @@ def test_toolbox_tool_underscore_client_headers_property(toolbox_tool: ToolboxTo client_headers["new_header"] = "new_value" -def test_toolbox_tool_underscore_pydantic_model_property(toolbox_tool: ToolboxTool): - """Tests the _pydantic_model property returns the correct Pydantic model.""" - pydantic_model = toolbox_tool._pydantic_model - assert issubclass(pydantic_model, BaseModel) - assert pydantic_model.__name__ == TEST_TOOL_NAME - - # Test that the model can validate expected data - valid_data = {"message": "test", "count": 10} - validated_data = pydantic_model.model_validate(valid_data) - assert validated_data.message == "test" - assert validated_data.count == 10 - - # Test that the model raises ValidationError for invalid data - invalid_data = {"message": 123, "count": "not_an_int"} - with pytest.raises(ValidationError): - pydantic_model.model_validate(invalid_data) - - # --- Test for the HTTP Warning --- @pytest.mark.parametrize( "trigger_condition_params", From 0754b99b6c324054a09cff57d947bb337d393a18 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Fri, 16 May 2025 22:22:43 +0530 Subject: [PATCH 18/19] chore: remove unused imports --- packages/toolbox-core/src/toolbox_core/tool.py | 1 - packages/toolbox-core/tests/test_sync_client.py | 2 +- packages/toolbox-core/tests/test_tool.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.py b/packages/toolbox-core/src/toolbox_core/tool.py index 1088e935..5a9bca6e 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.py +++ b/packages/toolbox-core/src/toolbox_core/tool.py @@ -19,7 +19,6 @@ from warnings import warn from aiohttp import ClientSession -from pydantic import BaseModel from .protocol import ParameterSchema from .utils import ( diff --git a/packages/toolbox-core/tests/test_sync_client.py b/packages/toolbox-core/tests/test_sync_client.py index 90362716..28f2b27b 100644 --- a/packages/toolbox-core/tests/test_sync_client.py +++ b/packages/toolbox-core/tests/test_sync_client.py @@ -14,7 +14,7 @@ import inspect -from typing import Any, Callable, Generator, Mapping, Optional +from typing import Any, Callable, Mapping, Optional from unittest.mock import AsyncMock, Mock, patch import pytest diff --git a/packages/toolbox-core/tests/test_tool.py b/packages/toolbox-core/tests/test_tool.py index 9d5e5478..c64149f5 100644 --- a/packages/toolbox-core/tests/test_tool.py +++ b/packages/toolbox-core/tests/test_tool.py @@ -23,7 +23,7 @@ import pytest_asyncio from aiohttp import ClientSession from aioresponses import aioresponses -from pydantic import BaseModel, ValidationError +from pydantic import ValidationError from toolbox_core.protocol import ParameterSchema from toolbox_core.tool import ToolboxTool, create_func_docstring, resolve_value From f03b2930a80ee5e29fd7170c72caa2ec99c4ae99 Mon Sep 17 00:00:00 2001 From: Anubhav Dhawan Date: Sat, 17 May 2025 01:54:49 +0530 Subject: [PATCH 19/19] chore: Revert minor change --- packages/toolbox-core/src/toolbox_core/sync_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 46c73b39..6bb54b45 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -14,7 +14,6 @@ import asyncio -from asyncio import AbstractEventLoop from threading import Thread from typing import Any, Callable, Coroutine, Mapping, Optional, Union @@ -30,7 +29,7 @@ class ToolboxSyncClient: service endpoint. """ - __loop: Optional[AbstractEventLoop] = None + __loop: Optional[asyncio.AbstractEventLoop] = None __thread: Optional[Thread] = None def __init__(