Skip to content

Commit 2a4b36d

Browse files
committed
feat: Create helper methods that return a future of the asynchronous tasks scheduled on an internal background loop/thread
1 parent 336b1e5 commit 2a4b36d

File tree

2 files changed

+130
-20
lines changed

2 files changed

+130
-20
lines changed

packages/toolbox-core/src/toolbox_core/sync_client.py

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import asyncio
1717
from asyncio import AbstractEventLoop
18+
from concurrent.futures import Future
1819
from threading import Thread
1920
from typing import Any, Callable, Coroutine, Mapping, Optional, Union
2021

@@ -86,6 +87,103 @@ def close(self):
8687
coro = self.__async_client.close()
8788
asyncio.run_coroutine_threadsafe(coro, self.__loop).result()
8889

90+
def _load_tool_future(
91+
self,
92+
name: str,
93+
auth_token_getters: dict[str, Callable[[], str]] = {},
94+
bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {},
95+
) -> Future[ToolboxSyncTool]:
96+
"""
97+
Asynchronously initiates the loading of a specific tool from the Toolbox service.
98+
99+
This method schedules the tool loading operation on a background event loop
100+
and immediately returns a `concurrent.futures.Future`. This allows other
101+
operations to proceed while the tool is being loaded. To get the actual
102+
`ToolboxSyncTool` instance, call `.result()` on the returned future, which
103+
will block until the tool is available or an error occurs.
104+
105+
Args:
106+
name: The unique name or identifier of the tool to load.
107+
auth_token_getters: A mapping of authentication service names to
108+
callables that return the corresponding authentication token.
109+
bound_params: A mapping of parameter names to bind to specific values or
110+
callables that are called to produce values as needed.
111+
112+
Returns:
113+
A `concurrent.futures.Future` that, upon successful completion, will
114+
yield a `ToolboxSyncTool` instance representing the loaded tool.
115+
116+
Raises:
117+
ValueError: If the background event loop or thread (required for asynchronous
118+
operations) is not running or properly initialized.
119+
120+
"""
121+
122+
async def async_worker() -> ToolboxSyncTool:
123+
if not self.__loop or not self.__thread:
124+
raise ValueError("Background loop or thread cannot be None.")
125+
async_tool = await self.__async_client.load_tool(
126+
name, auth_token_getters, bound_params
127+
)
128+
return ToolboxSyncTool(async_tool, self.__loop, self.__thread)
129+
130+
if not self.__loop or not self.__thread:
131+
raise ValueError("Background loop or thread cannot be None.")
132+
return asyncio.run_coroutine_threadsafe(async_worker(), self.__loop)
133+
134+
def _load_toolset_future(
135+
self,
136+
name: Optional[str] = None,
137+
auth_token_getters: dict[str, Callable[[], str]] = {},
138+
bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {},
139+
strict: bool = False,
140+
) -> Future[list[ToolboxSyncTool]]:
141+
"""
142+
Asynchronously initiates loading of all tools within a specified toolset.
143+
144+
This method schedules the toolset loading operation on a background event
145+
loop and returns a `concurrent.futures.Future` without blocking.
146+
The future's result will be a list of `ToolboxSyncTool` instances.
147+
Call `.result()` on the returned future to wait for completion and get
148+
the list of tools.
149+
150+
Args:
151+
name: Name of the toolset to load tools.
152+
auth_token_getters: A mapping of authentication service names to
153+
callables that return the corresponding authentication token.
154+
bound_params: A mapping of parameter names to bind to specific values or
155+
callables that are called to produce values as needed.
156+
strict: If True, raises an error if *any* loaded tool instance fails
157+
to utilize at least one provided parameter or auth token (if any
158+
provided). If False (default), raises an error only if a
159+
user-provided parameter or auth token cannot be applied to *any*
160+
loaded tool across the set.
161+
162+
Returns:
163+
A `concurrent.futures.Future` that, upon successful completion, will
164+
yield a list of `ToolboxSyncTool` instances.
165+
166+
Raises:
167+
ValueError: If the background event loop or thread is not running,
168+
or if validation fails based on the `strict` flag during
169+
the underlying asynchronous loading process.
170+
"""
171+
172+
async def async_worker() -> list[ToolboxSyncTool]:
173+
if not self.__loop or not self.__thread:
174+
raise ValueError("Background loop or thread cannot be None.")
175+
async_tools = await self.__async_client.load_toolset(
176+
name, auth_token_getters, bound_params, strict
177+
)
178+
return [
179+
ToolboxSyncTool(async_tool, self.__loop, self.__thread)
180+
for async_tool in async_tools
181+
]
182+
183+
if not self.__loop or not self.__thread:
184+
raise ValueError("Background loop or thread cannot be None.")
185+
return asyncio.run_coroutine_threadsafe(async_worker(), self.__loop)
186+
89187
def load_tool(
90188
self,
91189
name: str,
@@ -111,13 +209,7 @@ def load_tool(
111209
for execution. The specific arguments and behavior of the callable
112210
depend on the tool itself.
113211
"""
114-
coro = self.__async_client.load_tool(name, auth_token_getters, bound_params)
115-
116-
if not self.__loop or not self.__thread:
117-
raise ValueError("Background loop or thread cannot be None.")
118-
119-
async_tool = asyncio.run_coroutine_threadsafe(coro, self.__loop).result()
120-
return ToolboxSyncTool(async_tool, self.__loop, self.__thread)
212+
return self._load_tool_future(name, auth_token_getters, bound_params).result()
121213

122214
def load_toolset(
123215
self,
@@ -148,18 +240,9 @@ def load_toolset(
148240
Raises:
149241
ValueError: If validation fails based on the `strict` flag.
150242
"""
151-
coro = self.__async_client.load_toolset(
243+
return self._load_toolset_future(
152244
name, auth_token_getters, bound_params, strict
153-
)
154-
155-
if not self.__loop or not self.__thread:
156-
raise ValueError("Background loop or thread cannot be None.")
157-
158-
async_tools = asyncio.run_coroutine_threadsafe(coro, self.__loop).result()
159-
return [
160-
ToolboxSyncTool(async_tool, self.__loop, self.__thread)
161-
for async_tool in async_tools
162-
]
245+
).result()
163246

164247
def add_headers(
165248
self, headers: Mapping[str, Union[Callable, Coroutine, str]]

packages/toolbox-core/src/toolbox_core/sync_tool.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import asyncio
1717
from asyncio import AbstractEventLoop
18+
from concurrent.futures import Future
1819
from inspect import Signature
1920
from threading import Thread
2021
from typing import Any, Callable, Coroutine, Mapping, Sequence, Union
@@ -129,6 +130,32 @@ def _auth_service_token_getters(self) -> Mapping[str, Callable[[], str]]:
129130
def _client_headers(self) -> Mapping[str, Union[Callable, Coroutine, str]]:
130131
return self.__async_tool._client_headers
131132

133+
def _call_future(self, *args: Any, **kwargs: Any) -> Future[str]:
134+
"""
135+
Asynchronously calls the remote tool with the provided arguments.
136+
137+
This method schedules the actual remote tool invocation on a background
138+
event loop and immediately returns a `concurrent.futures.Future`.
139+
This allows the caller to perform other operations while the remote
140+
tool execution is in progress.
141+
142+
To obtain the result of the tool's execution, call `.result()` on
143+
the returned Future object. This will block until the remote tool
144+
responds or an error occurs.
145+
146+
Args:
147+
*args: Positional arguments for the tool.
148+
**kwargs: Keyword arguments for the tool.
149+
150+
Returns:
151+
A `concurrent.futures.Future` that will eventually resolve to the
152+
string result returned by the remote tool's execution. If the
153+
remote tool call fails, the future will resolve to an exception.
154+
155+
"""
156+
coro = self.__async_tool(*args, **kwargs)
157+
return asyncio.run_coroutine_threadsafe(coro, self.__loop)
158+
132159
def __call__(self, *args: Any, **kwargs: Any) -> str:
133160
"""
134161
Synchronously calls the remote tool with the provided arguments.
@@ -142,9 +169,9 @@ def __call__(self, *args: Any, **kwargs: Any) -> str:
142169
143170
Returns:
144171
The string result returned by the remote tool execution.
172+
145173
"""
146-
coro = self.__async_tool(*args, **kwargs)
147-
return asyncio.run_coroutine_threadsafe(coro, self.__loop).result()
174+
return self._call_future(*args, **kwargs).result()
148175

149176
def add_auth_token_getters(
150177
self,

0 commit comments

Comments
 (0)