From 3e6336f109aae41a26ae6656826484a64ddd76d9 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 17:14:19 +0530 Subject: [PATCH 01/33] create sync tool --- .../src/toolbox_core/sync_tool.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 packages/toolbox-core/src/toolbox_core/sync_tool.py diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py new file mode 100644 index 00000000..c0688445 --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -0,0 +1,129 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +from asyncio import AbstractEventLoop +from threading import Thread +from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union + +from .tool import ToolboxTool + +T = TypeVar("T") + + +class ToolboxSyncTool: + """ + A synchronous wrapper around an asynchronous ToolboxTool instance. + + This class allows calling the underlying async tool's __call__ method + synchronously. It also wraps methods like `add_auth_token_getters` and + `bind_parameters` to ensure they return new instances of this synchronous + wrapper. + """ + + def __init__( + self, async_tool: ToolboxTool, loop: AbstractEventLoop, thread: Thread + ): + """ + Initializes the synchronous wrapper. + + Args: + async_tool: An instance of the asynchronous ToolboxTool. + """ + if not isinstance(async_tool, ToolboxTool): + raise TypeError("async_tool must be an instance of ToolboxTool") + + self.__async_tool = async_tool + self.__loop = loop + self.__thread = thread + + # Delegate introspection attributes to the wrapped async tool + self.__name__ = self.__async_tool.__name__ + self.__doc__ = self.__async_tool.__doc__ + self.__signature__ = self.__async_tool.__signature__ + self.__annotations__ = self.__async_tool.__annotations__ + # TODO: self.__qualname__ ?? (Consider if needed) + + def __run_as_sync(self, coro: Awaitable[T]) -> T: + """Run an async coroutine synchronously""" + if not self.__loop: + raise Exception( + "Cannot call synchronous methods before the background loop is initialized." + ) + return asyncio.run_coroutine_threadsafe(coro, self.__loop).result() + + async def __run_as_async(self, coro: Awaitable[T]) -> T: + """Run an async coroutine asynchronously""" + + # If a loop has not been provided, attempt to run in current thread. + if not self.__loop: + return await coro + + # Otherwise, run in the background thread. + return await asyncio.wrap_future( + asyncio.run_coroutine_threadsafe(coro, self.__loop) + ) + + def __call__(self, *args: Any, **kwargs: Any) -> str: + """ + Synchronously calls the underlying remote tool. + + This method blocks until the asynchronous call completes and returns + the result. + + Args: + *args: Positional arguments for the tool. + **kwargs: Keyword arguments for the tool. + + Returns: + The string result returned by the remote tool execution. + + Raises: + Any exception raised by the underlying async tool's __call__ method + or during asyncio execution. + """ + return self.__run_as_sync(self.__async_tool(**kwargs)) + + def add_auth_token_getters( + self, + auth_token_getters: Mapping[str, Callable[[], str]], + ) -> "ToolboxSyncTool": + """ + Registers auth token getters and returns a new SyncToolboxTool instance. + + Args: + auth_token_getters: A mapping of authentication service names to + callables that return the corresponding authentication token. + + Returns: + A new SyncToolboxTool instance wrapping the updated async tool. + """ + new_async_tool = self.__async_tool.add_auth_token_getters(auth_token_getters) + return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) + + def bind_parameters( + self, bound_params: Mapping[str, Union[Callable[[], Any], Any]] + ) -> "ToolboxSyncTool": + """ + Binds parameters and returns a new SyncToolboxTool instance. + + Args: + bound_params: A mapping of parameter names to values or callables that + produce values. + + Returns: + A new SyncToolboxTool instance wrapping the updated async tool. + """ + new_async_tool = self.__async_tool.bind_parameters(bound_params) + return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) From b6442b42ffdaaab9d93436315b0d0059683a59f1 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 17:23:10 +0530 Subject: [PATCH 02/33] create sync client --- .../src/toolbox_core/sync_client.py | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 packages/toolbox-core/src/toolbox_core/sync_client.py diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py new file mode 100644 index 00000000..680a1f9b --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -0,0 +1,237 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Callable, Mapping, Optional, Union, Awaitable, TypeVar +import asyncio +from aiohttp import ClientSession +from threading import Thread + +from .sync_tool import ToolboxSyncTool +from .client import ToolboxClient + +T = TypeVar("T") + + +class ToolboxSyncClient: + __session: Optional[ClientSession] = None + __loop: Optional[asyncio.AbstractEventLoop] = None + __thread: Optional[Thread] = None + """ + A synchronous client for interacting with a Toolbox service. + + Provides methods to discover and load tools defined by a remote Toolbox + service endpoint, returning synchronous tool wrappers (`SyncToolboxTool`). + It manages an underlying asynchronous `ToolboxClient`. + """ + + def __init__( + self, + url: str, + ): + """ + Initializes the SyncToolboxClient. + + Args: + url: The base URL for the Toolbox service API (e.g., "http://localhost:8000"). + """ + # Running a loop in a background thread allows us to support async + # methods from non-async environments. + if ToolboxClient.__loop is None: + loop = asyncio.new_event_loop() + thread = Thread(target=loop.run_forever, daemon=True) + thread.start() + ToolboxClient.__thread = thread + ToolboxClient.__loop = loop + + async def __start_session() -> None: + + # Use a default session if none is provided. This leverages connection + # pooling for better performance by reusing a single session throughout + # the application's lifetime. + if ToolboxClient.__session is None: + ToolboxClient.__session = ClientSession() + + coro = __start_session() + + asyncio.run_coroutine_threadsafe(coro, ToolboxClient.__loop).result() + + if not ToolboxClient.__session: + raise ValueError("Session cannot be None.") + self.__async_client = ToolboxClient(url, ToolboxClient.__session) + + def __run_as_sync(self, coro: Awaitable[T]) -> T: + """Run an async coroutine synchronously""" + if not self.__loop: + raise Exception( + "Cannot call synchronous methods before the background loop is initialized." + ) + return asyncio.run_coroutine_threadsafe(coro, self.__loop).result() + + async def __run_as_async(self, coro: Awaitable[T]) -> T: + """Run an async coroutine asynchronously""" + + # If a loop has not been provided, attempt to run in current thread. + if not self.__loop: + return await coro + + # Otherwise, run in the background thread. + return await asyncio.wrap_future( + asyncio.run_coroutine_threadsafe(coro, self.__loop) + ) + + def load_tool( + self, + tool_name: str, + auth_token_getters: dict[str, Callable[[], str]] = {}, + bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, + ) -> ToolboxSyncTool: + """ + Synchronously loads a tool from the server. + + Retrieves the schema for the specified tool and returns a callable, + synchronous object (`SyncToolboxTool`) that can be used to invoke the + tool remotely. + + Args: + tool_name: The unique name or identifier of the tool to load. + auth_token_getters: A mapping of authentication service names to + callables that return the corresponding authentication token. + bound_params: A mapping of parameter names to bind to specific values or + callables that are called to produce values as needed. + + Returns: + ToolboxSyncTool: A synchronous callable object representing the loaded tool. + """ + async_tool = self.__run_as_sync( + self.__async_client.load_tool(tool_name, auth_token_getters, bound_params) + ) + + if not self.__loop or not self.__thread: + raise ValueError("Background loop or thread cannot be None.") + return ToolboxSyncTool(async_tool, self.__loop, self.__thread) + + def load_toolset( + self, + toolset_name: str, + auth_token_getters: dict[str, Callable[[], str]] = {}, + bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, + ) -> list[ToolboxSyncTool]: + """ + Synchronously fetches a toolset and loads all tools defined within it. + + Args: + toolset_name: Name of the toolset to load tools. + auth_token_getters: A mapping of authentication service names to + callables that return the corresponding authentication token. + bound_params: A mapping of parameter names to bind to specific values or + callables that are called to produce values as needed. + + Returns: + list[ToolboxSyncTool]: A list of synchronous callables, one for each + tool defined in the toolset. + """ + async_tools = self.__run_as_sync( + self.__async_client.load_toolset( + toolset_name, auth_token_getters, bound_params + ) + ) + + if not self.__loop or not self.__thread: + raise ValueError("Background loop or thread cannot be None.") + tools: list[ToolboxSyncTool] = [] + for async_tool in async_tools: + tools.append(ToolboxSyncTool(async_tool, self.__loop, self.__thread)) + return tools + + async def aload_tool( + self, + tool_name: str, + auth_token_getters: dict[str, Callable[[], str]] = {}, + bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, + ) -> ToolboxSyncTool: + """ + Synchronously loads a tool from the server. + + Retrieves the schema for the specified tool and returns a callable, + synchronous object (`SyncToolboxTool`) that can be used to invoke the + tool remotely. + + Args: + tool_name: The unique name or identifier of the tool to load. + auth_token_getters: A mapping of authentication service names to + callables that return the corresponding authentication token. + bound_params: A mapping of parameter names to bind to specific values or + callables that are called to produce values as needed. + + Returns: + ToolboxSyncTool: A synchronous callable object representing the loaded tool. + """ + async_tool = await self.__run_as_async( + self.__async_client.load_tool(tool_name, auth_token_getters, bound_params) + ) + + if not self.__loop or not self.__thread: + raise ValueError("Background loop or thread cannot be None.") + return ToolboxSyncTool(async_tool, self.__loop, self.__thread) + + async def aload_toolset( + self, + toolset_name: str, + auth_token_getters: dict[str, Callable[[], str]] = {}, + bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, + ) -> list[ToolboxSyncTool]: + """ + Synchronously fetches a toolset and loads all tools defined within it. + + Args: + toolset_name: Name of the toolset to load tools. + auth_token_getters: A mapping of authentication service names to + callables that return the corresponding authentication token. + bound_params: A mapping of parameter names to bind to specific values or + callables that are called to produce values as needed. + + Returns: + list[ToolboxSyncTool]: A list of synchronous callables, one for each + tool defined in the toolset. + """ + async_tools = await self.__run_as_async( + self.__async_client.load_toolset( + toolset_name, auth_token_getters, bound_params + ) + ) + + if not self.__loop or not self.__thread: + raise ValueError("Background loop or thread cannot be None.") + tools: list[ToolboxSyncTool] = [] + for async_tool in async_tools: + tools.append(ToolboxSyncTool(async_tool, self.__loop, self.__thread)) + return tools + + def close(self): + """ + Synchronously closes the underlying asynchronous client session if it + was created internally by the client. + """ + # Create the coroutine for closing the async client + coro = self.__async_client.close() + # Run it synchronously + self.__run_as_sync(coro) + + def __enter__(self): + """Enter the runtime context related to this client instance.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit the runtime context and close the client session.""" + self.close() From 2095970861cf6ac5d553949a10ab0986e143779e Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 17:35:34 +0530 Subject: [PATCH 03/33] add sync client and its tests --- .../src/toolbox_core/sync_client.py | 18 +- packages/toolbox-core/tests/test_sync_e2e.py | 181 ++++++++++++++++++ 2 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 packages/toolbox-core/tests/test_sync_e2e.py diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 680a1f9b..84b7aa69 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -47,28 +47,28 @@ def __init__( """ # Running a loop in a background thread allows us to support async # methods from non-async environments. - if ToolboxClient.__loop is None: + if ToolboxSyncClient.__loop is None: loop = asyncio.new_event_loop() thread = Thread(target=loop.run_forever, daemon=True) thread.start() - ToolboxClient.__thread = thread - ToolboxClient.__loop = loop + ToolboxSyncClient.__thread = thread + ToolboxSyncClient.__loop = loop async def __start_session() -> None: # Use a default session if none is provided. This leverages connection # pooling for better performance by reusing a single session throughout # the application's lifetime. - if ToolboxClient.__session is None: - ToolboxClient.__session = ClientSession() + if ToolboxSyncClient.__session is None: + ToolboxSyncClient.__session = ClientSession() coro = __start_session() - asyncio.run_coroutine_threadsafe(coro, ToolboxClient.__loop).result() + asyncio.run_coroutine_threadsafe(coro, ToolboxSyncClient.__loop).result() - if not ToolboxClient.__session: + if not ToolboxSyncClient.__session: raise ValueError("Session cannot be None.") - self.__async_client = ToolboxClient(url, ToolboxClient.__session) + self.__async_client = ToolboxClient(url, ToolboxSyncClient.__session) def __run_as_sync(self, coro: Awaitable[T]) -> T: """Run an async coroutine synchronously""" @@ -224,7 +224,7 @@ def close(self): was created internally by the client. """ # Create the coroutine for closing the async client - coro = self.__async_client.close() + coro = self.__session.close() # Run it synchronously self.__run_as_sync(coro) diff --git a/packages/toolbox-core/tests/test_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py new file mode 100644 index 00000000..9c857a24 --- /dev/null +++ b/packages/toolbox-core/tests/test_sync_e2e.py @@ -0,0 +1,181 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest +from toolbox_core.sync_client import ToolboxSyncClient +from toolbox_core.sync_tool import ToolboxSyncTool + + +# --- Shared Fixtures Defined at Module Level --- +@pytest.fixture(scope="module") +def toolbox(): + """Creates a ToolboxSyncClient instance shared by all tests in this module.""" + toolbox = ToolboxSyncClient("http://localhost:5000") + try: + yield toolbox + finally: + toolbox.close() + + +@pytest.fixture(scope="function") +def get_n_rows_tool(toolbox: ToolboxSyncClient) -> ToolboxSyncTool: + """Load the 'get-n-rows' tool using the shared toolbox client.""" + tool = toolbox.load_tool("get-n-rows") + assert tool.__name__ == "get-n-rows" + return tool + + +# @pytest.mark.usefixtures("toolbox_server") +class TestBasicE2E: + @pytest.mark.parametrize( + "toolset_name, expected_length, expected_tools", + [ + ("my-toolset", 1, ["get-row-by-id"]), + ("my-toolset-2", 2, ["get-n-rows", "get-row-by-id"]), + ], + ) + def test_load_toolset_specific( + self, + toolbox: ToolboxSyncClient, + toolset_name: str, + expected_length: int, + expected_tools: list[str], + ): + """Load a specific toolset""" + toolset = toolbox.load_toolset(toolset_name) + assert len(toolset) == expected_length + tool_names = {tool.__name__ for tool in toolset} + assert tool_names == set(expected_tools) + + def test_run_tool(self, get_n_rows_tool: ToolboxSyncTool): + """Invoke a tool.""" + response = get_n_rows_tool(num_rows="2") + + assert isinstance(response, str) + assert "row1" in response + assert "row2" in response + assert "row3" not in response + + def test_run_tool_missing_params(self, get_n_rows_tool: ToolboxSyncTool): + """Invoke a tool with missing params.""" + with pytest.raises(TypeError, match="missing a required argument: 'num_rows'"): + get_n_rows_tool() + + def test_run_tool_wrong_param_type(self, get_n_rows_tool: ToolboxSyncTool): + """Invoke a tool with wrong param type.""" + with pytest.raises( + Exception, + match='provided parameters were invalid: unable to parse value for "num_rows": .* not type "string"', + ): + get_n_rows_tool(num_rows=2) + + +@pytest.mark.usefixtures("toolbox_server") +class TestBindParams: + def test_bind_params( + self, toolbox: ToolboxSyncClient, get_n_rows_tool: ToolboxSyncTool + ): + """Bind a param to an existing tool.""" + new_tool = get_n_rows_tool.bind_parameters({"num_rows": "3"}) + response = new_tool() + assert isinstance(response, str) + assert "row1" in response + assert "row2" in response + assert "row3" in response + assert "row4" not in response + + def test_bind_params_callable( + self, toolbox: ToolboxSyncClient, get_n_rows_tool: ToolboxSyncTool + ): + """Bind a callable param to an existing tool.""" + new_tool = get_n_rows_tool.bind_parameters({"num_rows": lambda: "3"}) + response = new_tool() + assert isinstance(response, str) + assert "row1" in response + assert "row2" in response + assert "row3" in response + assert "row4" not in response + + +@pytest.mark.usefixtures("toolbox_server") +class TestAuth: + def test_run_tool_unauth_with_auth( + self, toolbox: ToolboxSyncClient, auth_token2: str + ): + """Tests running a tool that doesn't require auth, with auth provided.""" + tool = toolbox.load_tool( + "get-row-by-id", auth_token_getters={"my-test-auth": lambda: auth_token2} + ) + response = tool(id="2") + assert "row2" in response + + def test_run_tool_no_auth(self, toolbox: ToolboxSyncClient): + """Tests running a tool requiring auth without providing auth.""" + tool = toolbox.load_tool("get-row-by-id-auth") + with pytest.raises( + Exception, + match="tool invocation not authorized. Please make sure your specify correct auth headers", + ): + tool(id="2") + + def test_run_tool_wrong_auth(self, toolbox: ToolboxSyncClient, auth_token2: str): + """Tests running a tool with incorrect auth. The tool + requires a different authentication than the one provided.""" + tool = toolbox.load_tool("get-row-by-id-auth") + auth_tool = tool.add_auth_token_getters({"my-test-auth": lambda: auth_token2}) + with pytest.raises( + Exception, + match="tool invocation not authorized", + ): + auth_tool(id="2") + + def test_run_tool_auth(self, toolbox: ToolboxSyncClient, auth_token1: str): + """Tests running a tool with correct auth.""" + tool = toolbox.load_tool("get-row-by-id-auth") + auth_tool = tool.add_auth_token_getters({"my-test-auth": lambda: auth_token1}) + response = auth_tool(id="2") + assert "row2" in response + + def test_run_tool_param_auth_no_auth(self, toolbox: ToolboxSyncClient): + """Tests running a tool with a param requiring auth, without auth.""" + tool = toolbox.load_tool("get-row-by-email-auth") + with pytest.raises( + Exception, + match="One or more of the following authn services are required to invoke this tool: my-test-auth", + ): + tool() + + def test_run_tool_param_auth(self, toolbox: ToolboxSyncClient, auth_token1: str): + """Tests running a tool with a param requiring auth, with correct auth.""" + tool = toolbox.load_tool( + "get-row-by-email-auth", + auth_token_getters={"my-test-auth": lambda: auth_token1}, + ) + response = tool() + assert "row4" in response + assert "row5" in response + assert "row6" in response + + def test_run_tool_param_auth_no_field( + self, toolbox: ToolboxSyncClient, auth_token1: str + ): + """Tests running a tool with a param requiring auth, with insufficient auth.""" + tool = toolbox.load_tool( + "get-row-by-content-auth", + auth_token_getters={"my-test-auth": lambda: auth_token1}, + ) + with pytest.raises( + Exception, + match="no field named row_data in claims", + ): + tool() From 190ae98274fa2621ff29c181fff4898b7de7aad6 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 17:38:11 +0530 Subject: [PATCH 04/33] lint --- packages/toolbox-core/src/toolbox_core/sync_client.py | 7 ++++--- packages/toolbox-core/tests/test_sync_e2e.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 84b7aa69..e7b83bae 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Callable, Mapping, Optional, Union, Awaitable, TypeVar import asyncio -from aiohttp import ClientSession from threading import Thread +from typing import Any, Awaitable, Callable, Mapping, Optional, TypeVar, Union + +from aiohttp import ClientSession -from .sync_tool import ToolboxSyncTool from .client import ToolboxClient +from .sync_tool import ToolboxSyncTool T = TypeVar("T") diff --git a/packages/toolbox-core/tests/test_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py index 9c857a24..e8a62b7b 100644 --- a/packages/toolbox-core/tests/test_sync_e2e.py +++ b/packages/toolbox-core/tests/test_sync_e2e.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import pytest + from toolbox_core.sync_client import ToolboxSyncClient from toolbox_core.sync_tool import ToolboxSyncTool From fe45341bdf77fa61f1d5c4167c8c459923a7fde3 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 17:47:30 +0530 Subject: [PATCH 05/33] small fix --- packages/toolbox-core/tests/test_sync_e2e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py index e8a62b7b..339b40f1 100644 --- a/packages/toolbox-core/tests/test_sync_e2e.py +++ b/packages/toolbox-core/tests/test_sync_e2e.py @@ -36,7 +36,7 @@ def get_n_rows_tool(toolbox: ToolboxSyncClient) -> ToolboxSyncTool: return tool -# @pytest.mark.usefixtures("toolbox_server") +@pytest.mark.usefixtures("toolbox_server") class TestBasicE2E: @pytest.mark.parametrize( "toolset_name, expected_length, expected_tools", From fad580a468f9c74fdc384631cc471ad823de98af Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 17:51:48 +0530 Subject: [PATCH 06/33] docs fix --- .../src/toolbox_core/sync_client.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index e7b83bae..ab14e8bb 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -25,9 +25,6 @@ class ToolboxSyncClient: - __session: Optional[ClientSession] = None - __loop: Optional[asyncio.AbstractEventLoop] = None - __thread: Optional[Thread] = None """ A synchronous client for interacting with a Toolbox service. @@ -35,6 +32,9 @@ class ToolboxSyncClient: service endpoint, returning synchronous tool wrappers (`SyncToolboxTool`). It manages an underlying asynchronous `ToolboxClient`. """ + __session: Optional[ClientSession] = None + __loop: Optional[asyncio.AbstractEventLoop] = None + __thread: Optional[Thread] = None def __init__( self, @@ -56,7 +56,6 @@ def __init__( ToolboxSyncClient.__loop = loop async def __start_session() -> None: - # Use a default session if none is provided. This leverages connection # pooling for better performance by reusing a single session throughout # the application's lifetime. @@ -105,7 +104,7 @@ def load_tool( tool remotely. Args: - tool_name: The unique name or identifier of the tool to load. + tool_name: Name of the tool to load. auth_token_getters: A mapping of authentication service names to callables that return the corresponding authentication token. bound_params: A mapping of parameter names to bind to specific values or @@ -162,7 +161,7 @@ async def aload_tool( bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, ) -> ToolboxSyncTool: """ - Synchronously loads a tool from the server. + Asynchronously loads a tool from the server. Retrieves the schema for the specified tool and returns a callable, synchronous object (`SyncToolboxTool`) that can be used to invoke the @@ -193,7 +192,7 @@ async def aload_toolset( bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, ) -> list[ToolboxSyncTool]: """ - Synchronously fetches a toolset and loads all tools defined within it. + Asynchronously fetches a toolset and loads all tools defined within it. Args: toolset_name: Name of the toolset to load tools. @@ -221,12 +220,9 @@ async def aload_toolset( def close(self): """ - Synchronously closes the underlying asynchronous client session if it - was created internally by the client. + Synchronously closes the client session if it was created internally by the client. """ - # Create the coroutine for closing the async client coro = self.__session.close() - # Run it synchronously self.__run_as_sync(coro) def __enter__(self): From 7737c84a725366ab7a3d9fef9384817eb2828969 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 18:00:02 +0530 Subject: [PATCH 07/33] lint --- packages/toolbox-core/src/toolbox_core/sync_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index ab14e8bb..28836486 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -32,6 +32,7 @@ class ToolboxSyncClient: service endpoint, returning synchronous tool wrappers (`SyncToolboxTool`). It manages an underlying asynchronous `ToolboxClient`. """ + __session: Optional[ClientSession] = None __loop: Optional[asyncio.AbstractEventLoop] = None __thread: Optional[Thread] = None From 5a866b9cb7d90fed05f9184b278d2fd392a36147 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 18:09:02 +0530 Subject: [PATCH 08/33] Export sync as well as async client --- packages/toolbox-core/src/toolbox_core/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/__init__.py b/packages/toolbox-core/src/toolbox_core/__init__.py index 882b2046..7ea4a145 100644 --- a/packages/toolbox-core/src/toolbox_core/__init__.py +++ b/packages/toolbox-core/src/toolbox_core/__init__.py @@ -13,5 +13,6 @@ # limitations under the License. from .client import ToolboxClient +from .sync_client import ToolboxSyncClient -__all__ = ["ToolboxClient"] +__all__ = ["ToolboxClient", "ToolboxSyncClient"] From e3272ae35850c8a3719937f9982e7fdf16715af6 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 18:16:36 +0530 Subject: [PATCH 09/33] remove mixed implementations --- .../src/toolbox_core/sync_client.py | 64 ------------------- 1 file changed, 64 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 28836486..2e0939eb 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -155,70 +155,6 @@ def load_toolset( tools.append(ToolboxSyncTool(async_tool, self.__loop, self.__thread)) return tools - async def aload_tool( - self, - tool_name: str, - auth_token_getters: dict[str, Callable[[], str]] = {}, - bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, - ) -> ToolboxSyncTool: - """ - Asynchronously loads a tool from the server. - - Retrieves the schema for the specified tool and returns a callable, - synchronous object (`SyncToolboxTool`) that can be used to invoke the - tool remotely. - - Args: - tool_name: The unique name or identifier of the tool to load. - auth_token_getters: A mapping of authentication service names to - callables that return the corresponding authentication token. - bound_params: A mapping of parameter names to bind to specific values or - callables that are called to produce values as needed. - - Returns: - ToolboxSyncTool: A synchronous callable object representing the loaded tool. - """ - async_tool = await self.__run_as_async( - self.__async_client.load_tool(tool_name, auth_token_getters, bound_params) - ) - - if not self.__loop or not self.__thread: - raise ValueError("Background loop or thread cannot be None.") - return ToolboxSyncTool(async_tool, self.__loop, self.__thread) - - async def aload_toolset( - self, - toolset_name: str, - auth_token_getters: dict[str, Callable[[], str]] = {}, - bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, - ) -> list[ToolboxSyncTool]: - """ - Asynchronously fetches a toolset and loads all tools defined within it. - - Args: - toolset_name: Name of the toolset to load tools. - auth_token_getters: A mapping of authentication service names to - callables that return the corresponding authentication token. - bound_params: A mapping of parameter names to bind to specific values or - callables that are called to produce values as needed. - - Returns: - list[ToolboxSyncTool]: A list of synchronous callables, one for each - tool defined in the toolset. - """ - async_tools = await self.__run_as_async( - self.__async_client.load_toolset( - toolset_name, auth_token_getters, bound_params - ) - ) - - if not self.__loop or not self.__thread: - raise ValueError("Background loop or thread cannot be None.") - tools: list[ToolboxSyncTool] = [] - for async_tool in async_tools: - tools.append(ToolboxSyncTool(async_tool, self.__loop, self.__thread)) - return tools - def close(self): """ Synchronously closes the client session if it was created internally by the client. From 9b8776fbaf6e36397c079ce7eddd62b6e27d67a3 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 21:24:48 +0530 Subject: [PATCH 10/33] fix toolbox sync client and tool name --- packages/toolbox-core/src/toolbox_core/sync_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 2e0939eb..7f562bde 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -29,7 +29,7 @@ class ToolboxSyncClient: A synchronous client for interacting with a Toolbox service. Provides methods to discover and load tools defined by a remote Toolbox - service endpoint, returning synchronous tool wrappers (`SyncToolboxTool`). + service endpoint, returning synchronous tool wrappers (`ToolboxSyncTool`). It manages an underlying asynchronous `ToolboxClient`. """ @@ -42,7 +42,7 @@ def __init__( url: str, ): """ - Initializes the SyncToolboxClient. + Initializes the ToolboxSyncClient. Args: url: The base URL for the Toolbox service API (e.g., "http://localhost:8000"). @@ -101,7 +101,7 @@ def load_tool( Synchronously loads a tool from the server. Retrieves the schema for the specified tool and returns a callable, - synchronous object (`SyncToolboxTool`) that can be used to invoke the + synchronous object (`ToolboxSyncTool`) that can be used to invoke the tool remotely. Args: From 5d5338d825b24ef44e1904b46f3d8fc899c22251 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 21:25:04 +0530 Subject: [PATCH 11/33] fix default port --- packages/toolbox-core/src/toolbox_core/sync_client.py | 2 +- 1 file changed, 1 insertion(+), 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 7f562bde..85c4d618 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -45,7 +45,7 @@ def __init__( Initializes the ToolboxSyncClient. Args: - url: The base URL for the Toolbox service API (e.g., "http://localhost:8000"). + url: The base URL for the Toolbox service API (e.g., "http://localhost:5000"). """ # Running a loop in a background thread allows us to support async # methods from non-async environments. From c7603f27bb826e70c2a1bfe9e9606e47770c09bf Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 21:28:56 +0530 Subject: [PATCH 12/33] PR comments resolve --- packages/toolbox-core/src/toolbox_core/sync_client.py | 2 +- packages/toolbox-core/src/toolbox_core/sync_tool.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 85c4d618..a9534f9e 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -132,7 +132,7 @@ def load_toolset( Synchronously fetches a toolset and loads all tools defined within it. Args: - toolset_name: Name of the toolset to load tools. + toolset_name: Name of the toolset to load tools from. auth_token_getters: A mapping of authentication service names to callables that return the corresponding authentication token. bound_params: A mapping of parameter names to bind to specific values or diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index c0688445..4721f4b2 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -26,8 +26,8 @@ class ToolboxSyncTool: """ A synchronous wrapper around an asynchronous ToolboxTool instance. - This class allows calling the underlying async tool's __call__ method - synchronously. It also wraps methods like `add_auth_token_getters` and + This class allows calling the underlying async tool synchronously. + It also proxies methods like `add_auth_token_getters` and `bind_parameters` to ensure they return new instances of this synchronous wrapper. """ @@ -79,7 +79,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> str: """ Synchronously calls the underlying remote tool. - This method blocks until the asynchronous call completes and returns + This method blocks until the tool call completes and returns the result. Args: From 9c0d0dbfd5342f2dc89fb7d90e77eef70ef888b7 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 21:29:59 +0530 Subject: [PATCH 13/33] small fix --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 4721f4b2..24f7a1ab 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -24,7 +24,7 @@ class ToolboxSyncTool: """ - A synchronous wrapper around an asynchronous ToolboxTool instance. + A synchronous wrapper proxying asynchronous ToolboxTool instance. This class allows calling the underlying async tool synchronously. It also proxies methods like `add_auth_token_getters` and From 954d7d2b59e0fad8dfa35e77cc1d8b5cc73d47dd Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:34:30 +0530 Subject: [PATCH 14/33] Apply suggestions from code review Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Co-authored-by: Anubhav Dhawan --- packages/toolbox-core/src/toolbox_core/sync_client.py | 4 +++- packages/toolbox-core/src/toolbox_core/sync_tool.py | 3 +++ packages/toolbox-core/tests/test_sync_e2e.py | 1 + 3 files changed, 7 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 a9534f9e..7a536cdd 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -65,7 +65,7 @@ async def __start_session() -> None: coro = __start_session() - asyncio.run_coroutine_threadsafe(coro, ToolboxSyncClient.__loop).result() + asyncio.run_coroutine_threadsafe(__start_session(), ToolboxSyncClient.__loop).result() if not ToolboxSyncClient.__session: raise ValueError("Session cannot be None.") @@ -114,6 +114,7 @@ def load_tool( Returns: ToolboxSyncTool: A synchronous callable object representing the loaded tool. """ + async_tool = self.__run_as_sync( self.__async_client.load_tool(tool_name, auth_token_getters, bound_params) ) @@ -142,6 +143,7 @@ def load_toolset( list[ToolboxSyncTool]: A list of synchronous callables, one for each tool defined in the toolset. """ + async_tools = self.__run_as_sync( self.__async_client.load_toolset( toolset_name, auth_token_getters, bound_params diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 24f7a1ab..0a860ec6 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -41,6 +41,7 @@ def __init__( Args: async_tool: An instance of the asynchronous ToolboxTool. """ + if not isinstance(async_tool, ToolboxTool): raise TypeError("async_tool must be an instance of ToolboxTool") @@ -109,6 +110,7 @@ def add_auth_token_getters( Returns: A new SyncToolboxTool instance wrapping the updated async tool. """ + new_async_tool = self.__async_tool.add_auth_token_getters(auth_token_getters) return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) @@ -125,5 +127,6 @@ def bind_parameters( Returns: A new SyncToolboxTool instance wrapping the updated async tool. """ + new_async_tool = self.__async_tool.bind_parameters(bound_params) return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) diff --git a/packages/toolbox-core/tests/test_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py index 339b40f1..4a3e0345 100644 --- a/packages/toolbox-core/tests/test_sync_e2e.py +++ b/packages/toolbox-core/tests/test_sync_e2e.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import pytest from toolbox_core.sync_client import ToolboxSyncClient From 239143f32e4a8ce34ac87d11f27b99bc22d28d43 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 21:46:09 +0530 Subject: [PATCH 15/33] fix docstrings --- .../src/toolbox_core/sync_client.py | 49 +++++++++++-------- .../src/toolbox_core/sync_tool.py | 38 +++++++------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index a9534f9e..ef6b17a4 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -26,11 +26,10 @@ class ToolboxSyncClient: """ - A synchronous client for interacting with a Toolbox service. + An synchronous client for interacting with a Toolbox service. Provides methods to discover and load tools defined by a remote Toolbox - service endpoint, returning synchronous tool wrappers (`ToolboxSyncTool`). - It manages an underlying asynchronous `ToolboxClient`. + service endpoint. """ __session: Optional[ClientSession] = None @@ -42,7 +41,7 @@ def __init__( url: str, ): """ - Initializes the ToolboxSyncClient. + Initializes the ToolboxClient. Args: url: The base URL for the Toolbox service API (e.g., "http://localhost:5000"). @@ -91,31 +90,45 @@ async def __run_as_async(self, coro: Awaitable[T]) -> T: asyncio.run_coroutine_threadsafe(coro, self.__loop) ) + def close(self): + """ + Synchronously closes the underlying client session. Doing so will cause + any tools created by this Client to cease to function. + + If the session was provided externally during initialization, the caller + is responsible for its lifecycle, but calling close here will still + attempt to close it. + """ + coro = self.__session.close() + self.__run_as_sync(coro) + def load_tool( self, - tool_name: str, + name: str, auth_token_getters: dict[str, Callable[[], str]] = {}, bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, ) -> ToolboxSyncTool: """ Synchronously loads a tool from the server. - Retrieves the schema for the specified tool and returns a callable, - synchronous object (`ToolboxSyncTool`) that can be used to invoke the + Retrieves the schema for the specified tool from the Toolbox server and + returns a callable object (`ToolboxSyncTool`) that can be used to invoke the tool remotely. Args: - tool_name: Name of the tool to load. + name: The unique name or identifier of the tool to load. auth_token_getters: A mapping of authentication service names to callables that return the corresponding authentication token. bound_params: A mapping of parameter names to bind to specific values or callables that are called to produce values as needed. Returns: - ToolboxSyncTool: A synchronous callable object representing the loaded tool. + ToolboxSyncTool: A callable object representing the loaded tool, ready + for execution. The specific arguments and behavior of the callable + depend on the tool itself. """ async_tool = self.__run_as_sync( - self.__async_client.load_tool(tool_name, auth_token_getters, bound_params) + self.__async_client.load_tool(name, auth_token_getters, bound_params) ) if not self.__loop or not self.__thread: @@ -124,7 +137,7 @@ def load_tool( def load_toolset( self, - toolset_name: str, + name: str, auth_token_getters: dict[str, Callable[[], str]] = {}, bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {}, ) -> list[ToolboxSyncTool]: @@ -132,19 +145,19 @@ def load_toolset( Synchronously fetches a toolset and loads all tools defined within it. Args: - toolset_name: Name of the toolset to load tools from. + name: Name of the toolset to load tools. auth_token_getters: A mapping of authentication service names to callables that return the corresponding authentication token. bound_params: A mapping of parameter names to bind to specific values or callables that are called to produce values as needed. Returns: - list[ToolboxSyncTool]: A list of synchronous callables, one for each - tool defined in the toolset. + list[ToolboxSyncTool]: A list of callables, one for each tool defined + in the toolset. """ async_tools = self.__run_as_sync( self.__async_client.load_toolset( - toolset_name, auth_token_getters, bound_params + name, auth_token_getters, bound_params ) ) @@ -155,12 +168,6 @@ def load_toolset( tools.append(ToolboxSyncTool(async_tool, self.__loop, self.__thread)) return tools - def close(self): - """ - Synchronously closes the client session if it was created internally by the client. - """ - coro = self.__session.close() - self.__run_as_sync(coro) def __enter__(self): """Enter the runtime context related to this client instance.""" diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 24f7a1ab..5ed3d275 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -24,22 +24,28 @@ class ToolboxSyncTool: """ - A synchronous wrapper proxying asynchronous ToolboxTool instance. + A callable proxy object representing a specific tool on a remote Toolbox server. - This class allows calling the underlying async tool synchronously. - It also proxies methods like `add_auth_token_getters` and - `bind_parameters` to ensure they return new instances of this synchronous - wrapper. + Instances of this class behave like synchronous functions. When called, they + send a request to the corresponding tool's endpoint on the Toolbox server with + the provided arguments. + + It utilizes Python's introspection features (`__name__`, `__doc__`, + `__signature__`, `__annotations__`) so that standard tools like `help()` + and `inspect` work as expected. """ def __init__( self, async_tool: ToolboxTool, loop: AbstractEventLoop, thread: Thread ): """ - Initializes the synchronous wrapper. + Initializes a callable that will trigger the tool invocation through the + Toolbox server. Args: async_tool: An instance of the asynchronous ToolboxTool. + loop: The event loop used to run asynchronous tasks. + thread: The thread to run blocking operations in. """ if not isinstance(async_tool, ToolboxTool): raise TypeError("async_tool must be an instance of ToolboxTool") @@ -77,10 +83,10 @@ async def __run_as_async(self, coro: Awaitable[T]) -> T: def __call__(self, *args: Any, **kwargs: Any) -> str: """ - Synchronously calls the underlying remote tool. + Synchronously calls the remote tool with the provided arguments. - This method blocks until the tool call completes and returns - the result. + Validates arguments against the tool's signature, then sends them + as a JSON payload in a POST request to the tool's invoke URL. Args: *args: Positional arguments for the tool. @@ -88,10 +94,6 @@ def __call__(self, *args: Any, **kwargs: Any) -> str: Returns: The string result returned by the remote tool execution. - - Raises: - Any exception raised by the underlying async tool's __call__ method - or during asyncio execution. """ return self.__run_as_sync(self.__async_tool(**kwargs)) @@ -100,14 +102,16 @@ def add_auth_token_getters( auth_token_getters: Mapping[str, Callable[[], str]], ) -> "ToolboxSyncTool": """ - Registers auth token getters and returns a new SyncToolboxTool instance. + Registers an auth token getter function that is used for AuthServices when tools + are invoked. Args: auth_token_getters: A mapping of authentication service names to callables that return the corresponding authentication token. Returns: - A new SyncToolboxTool instance wrapping the updated async tool. + A new ToolboxSyncTool instance with the specified authentication token + getters registered. """ new_async_tool = self.__async_tool.add_auth_token_getters(auth_token_getters) return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) @@ -116,14 +120,14 @@ def bind_parameters( self, bound_params: Mapping[str, Union[Callable[[], Any], Any]] ) -> "ToolboxSyncTool": """ - Binds parameters and returns a new SyncToolboxTool instance. + Binds parameters to values or callables that produce values. Args: bound_params: A mapping of parameter names to values or callables that produce values. Returns: - A new SyncToolboxTool instance wrapping the updated async tool. + A new ToolboxSyncTool instance with the specified parameters bound. """ new_async_tool = self.__async_tool.bind_parameters(bound_params) return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread) From 351dd112b38cea7a8fa3b870da7ded5922468f5f Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 21:47:22 +0530 Subject: [PATCH 16/33] lint --- packages/toolbox-core/src/toolbox_core/sync_client.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 3d048016..6c2fc558 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -64,7 +64,9 @@ async def __start_session() -> None: coro = __start_session() - asyncio.run_coroutine_threadsafe(__start_session(), ToolboxSyncClient.__loop).result() + asyncio.run_coroutine_threadsafe( + __start_session(), ToolboxSyncClient.__loop + ).result() if not ToolboxSyncClient.__session: raise ValueError("Session cannot be None.") @@ -158,9 +160,7 @@ def load_toolset( """ async_tools = self.__run_as_sync( - self.__async_client.load_toolset( - name, auth_token_getters, bound_params - ) + self.__async_client.load_toolset(name, auth_token_getters, bound_params) ) if not self.__loop or not self.__thread: @@ -170,7 +170,6 @@ def load_toolset( tools.append(ToolboxSyncTool(async_tool, self.__loop, self.__thread)) return tools - def __enter__(self): """Enter the runtime context related to this client instance.""" return self From 27aa9d8e98c131e30ef2d0fc53bbb6d637ee812a Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:01:35 +0530 Subject: [PATCH 17/33] small fix --- .../src/toolbox_core/sync_client.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index 6c2fc558..f1a5845b 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -48,29 +48,27 @@ def __init__( """ # Running a loop in a background thread allows us to support async # methods from non-async environments. - if ToolboxSyncClient.__loop is None: + if self.__class__.__loop is None: loop = asyncio.new_event_loop() thread = Thread(target=loop.run_forever, daemon=True) thread.start() - ToolboxSyncClient.__thread = thread - ToolboxSyncClient.__loop = loop + self.__class__.__thread = thread + self.__class__.__loop = loop async def __start_session() -> None: # Use a default session if none is provided. This leverages connection # pooling for better performance by reusing a single session throughout # the application's lifetime. - if ToolboxSyncClient.__session is None: - ToolboxSyncClient.__session = ClientSession() - - coro = __start_session() + if self.__class__.__session is None: + self.__class__.__session = ClientSession() asyncio.run_coroutine_threadsafe( - __start_session(), ToolboxSyncClient.__loop + __start_session(), self.__class__.__loop ).result() - if not ToolboxSyncClient.__session: + if not self.__class__.__session: raise ValueError("Session cannot be None.") - self.__async_client = ToolboxClient(url, ToolboxSyncClient.__session) + self.__async_client = ToolboxClient(url, self.__class__.__session) def __run_as_sync(self, coro: Awaitable[T]) -> T: """Run an async coroutine synchronously""" From 9ace5753381a32092b660306b46acca17750838b Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:03:17 +0530 Subject: [PATCH 18/33] resolve comment --- packages/toolbox-core/src/toolbox_core/sync_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index f1a5845b..ce09f40d 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -163,10 +163,10 @@ def load_toolset( if not self.__loop or not self.__thread: raise ValueError("Background loop or thread cannot be None.") - tools: list[ToolboxSyncTool] = [] - for async_tool in async_tools: - tools.append(ToolboxSyncTool(async_tool, self.__loop, self.__thread)) - return tools + return [ + ToolboxSyncTool(async_tool, self.__loop, self.__thread) + for async_tool in async_tools + ] def __enter__(self): """Enter the runtime context related to this client instance.""" From 360b4be25be36517466f48cfbbf082244e18fb6c Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:05:25 +0530 Subject: [PATCH 19/33] fix --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index d86c8ae1..b91d948a 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -96,7 +96,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> str: Returns: The string result returned by the remote tool execution. """ - return self.__run_as_sync(self.__async_tool(**kwargs)) + return self.__run_as_sync(self.__async_tool(*args, **kwargs)) def add_auth_token_getters( self, From 033c85df17821c85c245881283876c9b18e2e043 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:14:27 +0530 Subject: [PATCH 20/33] remove run_as_sync and run_as_async methods --- .../src/toolbox_core/sync_client.py | 34 +++---------------- .../src/toolbox_core/sync_tool.py | 23 ++----------- 2 files changed, 7 insertions(+), 50 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index ce09f40d..7617fd73 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -70,26 +70,6 @@ async def __start_session() -> None: raise ValueError("Session cannot be None.") self.__async_client = ToolboxClient(url, self.__class__.__session) - def __run_as_sync(self, coro: Awaitable[T]) -> T: - """Run an async coroutine synchronously""" - if not self.__loop: - raise Exception( - "Cannot call synchronous methods before the background loop is initialized." - ) - return asyncio.run_coroutine_threadsafe(coro, self.__loop).result() - - async def __run_as_async(self, coro: Awaitable[T]) -> T: - """Run an async coroutine asynchronously""" - - # If a loop has not been provided, attempt to run in current thread. - if not self.__loop: - return await coro - - # Otherwise, run in the background thread. - return await asyncio.wrap_future( - asyncio.run_coroutine_threadsafe(coro, self.__loop) - ) - def close(self): """ Synchronously closes the underlying client session. Doing so will cause @@ -100,7 +80,7 @@ def close(self): attempt to close it. """ coro = self.__session.close() - self.__run_as_sync(coro) + asyncio.run_coroutine_threadsafe(coro, self.__loop).result() def load_tool( self, @@ -127,10 +107,8 @@ def load_tool( for execution. The specific arguments and behavior of the callable depend on the tool itself. """ - - async_tool = self.__run_as_sync( - self.__async_client.load_tool(name, auth_token_getters, bound_params) - ) + coro = self.__async_client.load_tool(name, auth_token_getters, bound_params) + async_tool = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() if not self.__loop or not self.__thread: raise ValueError("Background loop or thread cannot be None.") @@ -156,10 +134,8 @@ def load_toolset( list[ToolboxSyncTool]: A list of callables, one for each tool defined in the toolset. """ - - async_tools = self.__run_as_sync( - self.__async_client.load_toolset(name, auth_token_getters, bound_params) - ) + coro = self.__async_client.load_toolset(name, auth_token_getters, bound_params) + async_tools = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() if not self.__loop or not self.__thread: raise ValueError("Background loop or thread cannot be None.") diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index b91d948a..a798a8a5 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -62,26 +62,6 @@ def __init__( self.__annotations__ = self.__async_tool.__annotations__ # TODO: self.__qualname__ ?? (Consider if needed) - def __run_as_sync(self, coro: Awaitable[T]) -> T: - """Run an async coroutine synchronously""" - if not self.__loop: - raise Exception( - "Cannot call synchronous methods before the background loop is initialized." - ) - return asyncio.run_coroutine_threadsafe(coro, self.__loop).result() - - async def __run_as_async(self, coro: Awaitable[T]) -> T: - """Run an async coroutine asynchronously""" - - # If a loop has not been provided, attempt to run in current thread. - if not self.__loop: - return await coro - - # Otherwise, run in the background thread. - return await asyncio.wrap_future( - asyncio.run_coroutine_threadsafe(coro, self.__loop) - ) - def __call__(self, *args: Any, **kwargs: Any) -> str: """ Synchronously calls the remote tool with the provided arguments. @@ -96,7 +76,8 @@ def __call__(self, *args: Any, **kwargs: Any) -> str: Returns: The string result returned by the remote tool execution. """ - return self.__run_as_sync(self.__async_tool(*args, **kwargs)) + coro = self.__async_tool(*args, **kwargs) + return asyncio.run_coroutine_threadsafe(coro, self.__loop).result() def add_auth_token_getters( self, From cacec3590b2b19a8d89b01f6a52fb5de453010a7 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:21:51 +0530 Subject: [PATCH 21/33] lint --- packages/toolbox-core/src/toolbox_core/sync_client.py | 8 ++++++-- 1 file changed, 6 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 7617fd73..c1d16491 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -108,7 +108,9 @@ def load_tool( depend on the tool itself. """ coro = self.__async_client.load_tool(name, auth_token_getters, bound_params) - async_tool = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() + + # We have already created a new loop in the init method in case it does not already exist + async_tool = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore if not self.__loop or not self.__thread: raise ValueError("Background loop or thread cannot be None.") @@ -135,7 +137,9 @@ def load_toolset( in the toolset. """ coro = self.__async_client.load_toolset(name, auth_token_getters, bound_params) - async_tools = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() + + # We have already created a new loop in the init method in case it does not already exist + async_tools = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore if not self.__loop or not self.__thread: raise ValueError("Background loop or thread cannot be None.") From 9bab4c08657641f1dce0d90ecd5c1e7972f3b994 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:27:48 +0530 Subject: [PATCH 22/33] lint --- 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 c1d16491..eb52fa7f 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -110,7 +110,7 @@ def load_tool( coro = self.__async_client.load_tool(name, auth_token_getters, bound_params) # We have already created a new loop in the init method in case it does not already exist - async_tool = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore + async_tool = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore if not self.__loop or not self.__thread: raise ValueError("Background loop or thread cannot be None.") @@ -139,7 +139,7 @@ def load_toolset( coro = self.__async_client.load_toolset(name, auth_token_getters, bound_params) # We have already created a new loop in the init method in case it does not already exist - async_tools = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore + async_tools = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore if not self.__loop or not self.__thread: raise ValueError("Background loop or thread cannot be None.") From d81a790aabe1c43738d946337cbde8f547054d73 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:31:36 +0530 Subject: [PATCH 23/33] change from function attributes to properties --- .../src/toolbox_core/sync_tool.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index a798a8a5..47fb0c07 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -14,8 +14,9 @@ import asyncio from asyncio import AbstractEventLoop +from inspect import Signature from threading import Thread -from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union +from typing import Any, Callable, Mapping, TypeVar, Union from .tool import ToolboxTool @@ -54,14 +55,24 @@ def __init__( self.__async_tool = async_tool self.__loop = loop self.__thread = thread - - # Delegate introspection attributes to the wrapped async tool - self.__name__ = self.__async_tool.__name__ - self.__doc__ = self.__async_tool.__doc__ - self.__signature__ = self.__async_tool.__signature__ - self.__annotations__ = self.__async_tool.__annotations__ # TODO: self.__qualname__ ?? (Consider if needed) + @property + def __name__(self) -> str: + return self.__async_tool.__name__ + + @property + def __doc__(self) -> str | None: # Docstring can be None + return self.__async_tool.__doc__ + + @property + def __signature__(self) -> Signature: + return self.__async_tool.__signature__ + + @property + def __annotations__(self) -> dict[str, Any]: + return self.__async_tool.__annotations__ + def __call__(self, *args: Any, **kwargs: Any) -> str: """ Synchronously calls the remote tool with the provided arguments. From dd4ebd9111f073dda0caf681a8e8d5b97d613260 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:37:45 +0530 Subject: [PATCH 24/33] fix mypy issue --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 47fb0c07..56b813e8 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -62,8 +62,11 @@ def __name__(self) -> str: return self.__async_tool.__name__ @property - def __doc__(self) -> str | None: # Docstring can be None - return self.__async_tool.__doc__ + def __doc__(self) -> Union[str, None]: + # Standard Python object attributes like __doc__ are technically "writable". + # But not defining a setter function makes this a read-only property. + # Mypy flags this issue in the type checks. + return self.__async_tool.__doc__ # type: ignore[override] @property def __signature__(self) -> Signature: @@ -71,7 +74,10 @@ def __signature__(self) -> Signature: @property def __annotations__(self) -> dict[str, Any]: - return self.__async_tool.__annotations__ + # Standard Python object attributes like __doc__ are technically "writable". + # But not defining a setter function makes this a read-only property. + # Mypy flags this issue in the type checks. + return self.__async_tool.__annotations__ # type: ignore[override] def __call__(self, *args: Any, **kwargs: Any) -> str: """ From dfe776814bea40e8d7d529c4c0e14ee3e0ce29ca Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:41:10 +0530 Subject: [PATCH 25/33] fix mypy issue --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 56b813e8..df4fd934 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -62,22 +62,22 @@ def __name__(self) -> str: return self.__async_tool.__name__ @property - def __doc__(self) -> Union[str, None]: + def __doc__(self) -> Union[str, None]: # type: ignore[override] # Standard Python object attributes like __doc__ are technically "writable". # But not defining a setter function makes this a read-only property. # Mypy flags this issue in the type checks. - return self.__async_tool.__doc__ # type: ignore[override] + return self.__async_tool.__doc__ @property def __signature__(self) -> Signature: return self.__async_tool.__signature__ @property - def __annotations__(self) -> dict[str, Any]: + def __annotations__(self) -> dict[str, Any]: # type: ignore[override] # Standard Python object attributes like __doc__ are technically "writable". # But not defining a setter function makes this a read-only property. # Mypy flags this issue in the type checks. - return self.__async_tool.__annotations__ # type: ignore[override] + return self.__async_tool.__annotations__ def __call__(self, *args: Any, **kwargs: Any) -> str: """ From a8054552240ba2599ecb0d20ce8b9692275477f5 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:44:18 +0530 Subject: [PATCH 26/33] lint --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index df4fd934..2fd9e3ad 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -62,7 +62,7 @@ def __name__(self) -> str: return self.__async_tool.__name__ @property - def __doc__(self) -> Union[str, None]: # type: ignore[override] + def __doc__(self) -> Union[str, None]: # type: ignore[override] # Standard Python object attributes like __doc__ are technically "writable". # But not defining a setter function makes this a read-only property. # Mypy flags this issue in the type checks. @@ -73,7 +73,7 @@ def __signature__(self) -> Signature: return self.__async_tool.__signature__ @property - def __annotations__(self) -> dict[str, Any]: # type: ignore[override] + def __annotations__(self) -> dict[str, Any]: # type: ignore[override] # Standard Python object attributes like __doc__ are technically "writable". # But not defining a setter function makes this a read-only property. # Mypy flags this issue in the type checks. From c5a0cf5ff036967706e89ea03795732ebebf8258 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:48:20 +0530 Subject: [PATCH 27/33] remove session variable --- .../src/toolbox_core/sync_client.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_client.py b/packages/toolbox-core/src/toolbox_core/sync_client.py index eb52fa7f..5ff4b504 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -32,7 +32,6 @@ class ToolboxSyncClient: service endpoint. """ - __session: Optional[ClientSession] = None __loop: Optional[asyncio.AbstractEventLoop] = None __thread: Optional[Thread] = None @@ -55,21 +54,13 @@ def __init__( self.__class__.__thread = thread self.__class__.__loop = loop - async def __start_session() -> None: - # Use a default session if none is provided. This leverages connection - # pooling for better performance by reusing a single session throughout - # the application's lifetime. - if self.__class__.__session is None: - self.__class__.__session = ClientSession() + async def create_client(): + return ToolboxClient(url) - asyncio.run_coroutine_threadsafe( - __start_session(), self.__class__.__loop + self.__async_client = asyncio.run_coroutine_threadsafe( + create_client(), ToolboxSyncClient.__loop ).result() - if not self.__class__.__session: - raise ValueError("Session cannot be None.") - self.__async_client = ToolboxClient(url, self.__class__.__session) - def close(self): """ Synchronously closes the underlying client session. Doing so will cause @@ -79,7 +70,7 @@ def close(self): is responsible for its lifecycle, but calling close here will still attempt to close it. """ - coro = self.__session.close() + coro = self.__async_client.close() asyncio.run_coroutine_threadsafe(coro, self.__loop).result() def load_tool( From eb284533460ee751a3e225e95256aff71355b1dc Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:53:08 +0530 Subject: [PATCH 28/33] lint --- packages/toolbox-core/src/toolbox_core/sync_client.py | 3 ++- 1 file changed, 2 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 5ff4b504..f1900f17 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -57,8 +57,9 @@ def __init__( async def create_client(): return ToolboxClient(url) + # Ignoring type since we're already checking the existing of a loop above. self.__async_client = asyncio.run_coroutine_threadsafe( - create_client(), ToolboxSyncClient.__loop + create_client(), ToolboxSyncClient.__loop # type: ignore ).result() def close(self): From d620c2565271db384f4b622d64e1c790c21d64d5 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 22:55:30 +0530 Subject: [PATCH 29/33] added qualname --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 2fd9e3ad..4a8c2471 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -55,7 +55,6 @@ def __init__( self.__async_tool = async_tool self.__loop = loop self.__thread = thread - # TODO: self.__qualname__ ?? (Consider if needed) @property def __name__(self) -> str: @@ -79,6 +78,10 @@ def __annotations__(self) -> dict[str, Any]: # type: ignore[override] # Mypy flags this issue in the type checks. return self.__async_tool.__annotations__ + @property + def __qualname__(self) -> str: + return f"{self.__class__.__qualname__}.{self.__name__}" + def __call__(self, *args: Any, **kwargs: Any) -> str: """ Synchronously calls the remote tool with the provided arguments. From 9e1da13e85f39285f001441911d14b85e178ac0c Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 23:06:47 +0530 Subject: [PATCH 30/33] fix qualname --- packages/toolbox-core/src/toolbox_core/sync_tool.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/sync_tool.py b/packages/toolbox-core/src/toolbox_core/sync_tool.py index 4a8c2471..ad24e8e3 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_tool.py +++ b/packages/toolbox-core/src/toolbox_core/sync_tool.py @@ -56,6 +56,15 @@ def __init__( self.__loop = loop self.__thread = thread + # NOTE: We cannot define __qualname__ as a @property here. + # Properties are designed to compute values dynamically when accessed on an *instance* (using 'self'). + # However, Python needs the class's __qualname__ attribute to be a plain string + # *before* any instances exist, specifically when the 'class ToolboxSyncTool:' statement + # itself is being processed during module import or class definition. + # Defining __qualname__ as a property leads to a TypeError because the class object needs + # a string value immediately, not a descriptor that evaluates later. + self.__qualname__ = f"{self.__class__.__qualname__}.{self.__name__}" + @property def __name__(self) -> str: return self.__async_tool.__name__ @@ -78,10 +87,6 @@ def __annotations__(self) -> dict[str, Any]: # type: ignore[override] # Mypy flags this issue in the type checks. return self.__async_tool.__annotations__ - @property - def __qualname__(self) -> str: - return f"{self.__class__.__qualname__}.{self.__name__}" - def __call__(self, *args: Any, **kwargs: Any) -> str: """ Synchronously calls the remote tool with the provided arguments. From 5aaf20ace08df9cd70172eaa1e5f4e0abffa5957 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 23:13:27 +0530 Subject: [PATCH 31/33] nit --- 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 f1900f17..1cb2d868 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -40,7 +40,7 @@ def __init__( url: str, ): """ - Initializes the ToolboxClient. + Initializes the ToolboxSyncClient. Args: url: The base URL for the Toolbox service API (e.g., "http://localhost:5000"). @@ -59,7 +59,7 @@ async def create_client(): # Ignoring type since we're already checking the existing of a loop above. self.__async_client = asyncio.run_coroutine_threadsafe( - create_client(), ToolboxSyncClient.__loop # type: ignore + create_client(), self.__class__.__loop # type: ignore ).result() def close(self): From 4939a182e72204ed71a1757421c585b842cfe47b Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 23:21:07 +0530 Subject: [PATCH 32/33] fix comment --- packages/toolbox-core/src/toolbox_core/sync_client.py | 2 +- 1 file changed, 1 insertion(+), 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 1cb2d868..36877223 100644 --- a/packages/toolbox-core/src/toolbox_core/sync_client.py +++ b/packages/toolbox-core/src/toolbox_core/sync_client.py @@ -57,7 +57,7 @@ def __init__( async def create_client(): return ToolboxClient(url) - # Ignoring type since we're already checking the existing of a loop above. + # Ignoring type since we're already checking the existence of a loop above. self.__async_client = asyncio.run_coroutine_threadsafe( create_client(), self.__class__.__loop # type: ignore ).result() From d650e1c86b8d4554715d3d686479b9617eb60f63 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 3 Apr 2025 23:46:52 +0530 Subject: [PATCH 33/33] fix error message --- packages/toolbox-core/tests/test_sync_e2e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/tests/test_sync_e2e.py b/packages/toolbox-core/tests/test_sync_e2e.py index 4a3e0345..055ca977 100644 --- a/packages/toolbox-core/tests/test_sync_e2e.py +++ b/packages/toolbox-core/tests/test_sync_e2e.py @@ -77,7 +77,7 @@ def test_run_tool_wrong_param_type(self, get_n_rows_tool: ToolboxSyncTool): """Invoke a tool with wrong param type.""" with pytest.raises( Exception, - match='provided parameters were invalid: unable to parse value for "num_rows": .* not type "string"', + match=r"num_rows\s+Input should be a valid string\s+\[type=string_type,\s+input_value=2,\s+input_type=int\]", ): get_n_rows_tool(num_rows=2)