Skip to content

Commit c205b96

Browse files
committed
deprecate instead of remove
1 parent 9751394 commit c205b96

File tree

4 files changed

+154
-0
lines changed

4 files changed

+154
-0
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Any, Awaitable, Callable, Mapping, Optional, Union
1818

1919
from aiohttp import ClientSession
20+
from deprecated import deprecated
2021

2122
from .protocol import ManifestSchema, ToolSchema
2223
from .tool import ToolboxTool
@@ -339,3 +340,26 @@ async def load_toolset(
339340
)
340341

341342
return tools
343+
344+
@deprecated("Please add client level headers during client initialization.")
345+
def add_headers(
346+
self,
347+
headers: Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]]]],
348+
) -> None:
349+
"""
350+
Add headers to be included in each request sent through this client.
351+
Args:
352+
headers: Headers to include in each request sent through this client.
353+
Raises:
354+
ValueError: If any of the headers are already registered in the client.
355+
"""
356+
existing_headers = self.__client_headers.keys()
357+
incoming_headers = headers.keys()
358+
duplicates = existing_headers & incoming_headers
359+
if duplicates:
360+
raise ValueError(
361+
f"Client header(s) `{', '.join(duplicates)}` already registered in the client."
362+
)
363+
364+
merged_headers = {**self.__client_headers, **headers}
365+
self.__client_headers = merged_headers

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from threading import Thread
1818
from typing import Any, Awaitable, Callable, Mapping, Optional, Union
1919

20+
from deprecated import deprecated
21+
2022
from .client import ToolboxClient
2123
from .sync_tool import ToolboxSyncTool
2224

@@ -153,6 +155,24 @@ def load_toolset(
153155
for async_tool in async_tools
154156
]
155157

158+
@deprecated("Please add client level headers during client initialization.")
159+
def add_headers(
160+
self,
161+
headers: Mapping[
162+
str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]
163+
],
164+
) -> None:
165+
"""
166+
Add headers to be included in each request sent through this client.
167+
168+
Args:
169+
headers: Headers to include in each request sent through this client.
170+
171+
Raises:
172+
ValueError: If any of the headers are already registered in the client.
173+
"""
174+
self.__async_client.add_headers(headers)
175+
156176
def __enter__(self):
157177
"""Enter the runtime context related to this client instance."""
158178
return self

packages/toolbox-core/tests/test_client.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,50 @@ async def test_load_toolset_with_headers(
14171417
assert len(tools) == 1
14181418
assert tools[0].__name__ == tool_name
14191419

1420+
@pytest.mark.asyncio
1421+
async def test_add_headers_success(
1422+
self, aioresponses, test_tool_str, static_header
1423+
):
1424+
"""Tests adding headers after client initialization."""
1425+
tool_name = "tool_after_add_headers"
1426+
manifest = ManifestSchema(
1427+
serverVersion="0.0.0", tools={tool_name: test_tool_str}
1428+
)
1429+
expected_payload = {"result": "added_ok"}
1430+
1431+
get_callback = self.create_callback_factory(
1432+
expected_header=static_header,
1433+
callback_payload=manifest.model_dump(),
1434+
)
1435+
aioresponses.get(f"{TEST_BASE_URL}/api/tool/{tool_name}", callback=get_callback)
1436+
1437+
post_callback = self.create_callback_factory(
1438+
expected_header=static_header,
1439+
callback_payload=expected_payload,
1440+
)
1441+
aioresponses.post(
1442+
f"{TEST_BASE_URL}/api/tool/{tool_name}/invoke", callback=post_callback
1443+
)
1444+
1445+
async with ToolboxClient(TEST_BASE_URL) as client:
1446+
client.add_headers(static_header)
1447+
assert client._ToolboxClient__client_headers == static_header
1448+
1449+
tool = await client.load_tool(tool_name)
1450+
result = await tool(param1="test")
1451+
assert result == expected_payload["result"]
1452+
1453+
@pytest.mark.asyncio
1454+
async def test_add_headers_duplicate_fail(self, static_header):
1455+
"""Tests that adding a duplicate header via add_headers raises
1456+
ValueError."""
1457+
async with ToolboxClient(TEST_BASE_URL, client_headers=static_header) as client:
1458+
with pytest.raises(
1459+
ValueError,
1460+
match=f"Client header\\(s\\) `X-Static-Header` already registered",
1461+
):
1462+
await client.add_headers(static_header)
1463+
14201464
@pytest.mark.asyncio
14211465
async def test_client_header_auth_token_conflict_fail(
14221466
self, aioresponses, test_tool_auth

packages/toolbox-core/tests/test_sync_client.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,72 @@ def test_load_tool_raises_if_loop_or_thread_none(self):
350350
# sync_client_environment will handle the final cleanup of original_class_loop/thread.
351351

352352

353+
class TestSyncClientHeaders:
354+
"""Additive tests for client header functionality specific to ToolboxSyncClient if any,
355+
or counterparts to async client header tests."""
356+
357+
def test_sync_add_headers_success(
358+
self, aioresponses, test_tool_str_schema, sync_client
359+
):
360+
tool_name = "tool_after_add_headers_sync"
361+
manifest = ManifestSchema(
362+
serverVersion="0.0.0", tools={tool_name: test_tool_str_schema}
363+
)
364+
expected_payload = {"result": "added_sync_ok"}
365+
headers_to_add = {"X-Custom-SyncHeader": "sync_value"}
366+
367+
def get_callback(url, **kwargs):
368+
# The sync_client might have default headers. Check ours are present.
369+
assert kwargs.get("headers") is not None
370+
for key, value in headers_to_add.items():
371+
assert kwargs["headers"].get(key) == value
372+
return CallbackResult(status=200, payload=manifest.model_dump())
373+
374+
aioresponses.get(f"{TEST_BASE_URL}/api/tool/{tool_name}", callback=get_callback)
375+
376+
def post_callback(url, **kwargs):
377+
assert kwargs.get("headers") is not None
378+
for key, value in headers_to_add.items():
379+
assert kwargs["headers"].get(key) == value
380+
return CallbackResult(status=200, payload=expected_payload)
381+
382+
aioresponses.post(
383+
f"{TEST_BASE_URL}/api/tool/{tool_name}/invoke", callback=post_callback
384+
)
385+
386+
sync_client.add_headers(headers_to_add)
387+
tool = sync_client.load_tool(tool_name)
388+
result = tool(param1="test")
389+
assert result == expected_payload["result"]
390+
391+
def test_sync_add_headers_duplicate_fail(self):
392+
"""Tests that adding a duplicate header via add_headers raises ValueError (from async client)."""
393+
initial_headers = {"X-Initial-Header": "initial_value"}
394+
mock_async_client = AsyncMock(spec=ToolboxClient)
395+
396+
# Configure add_headers to simulate the ValueError from ToolboxClient
397+
def mock_add_headers(headers):
398+
# Simulate ToolboxClient's check
399+
if "X-Initial-Header" in headers:
400+
raise ValueError(
401+
"Client header(s) `X-Initial-Header` already registered"
402+
)
403+
404+
mock_async_client.add_headers = Mock(side_effect=mock_add_headers)
405+
406+
with patch(
407+
"toolbox_core.sync_client.ToolboxClient", return_value=mock_async_client
408+
):
409+
with ToolboxSyncClient(
410+
TEST_BASE_URL, client_headers=initial_headers
411+
) as client:
412+
with pytest.raises(
413+
ValueError,
414+
match="Client header\\(s\\) `X-Initial-Header` already registered",
415+
):
416+
client.add_headers({"X-Initial-Header": "another_value"})
417+
418+
353419
class TestSyncAuth:
354420
@pytest.fixture
355421
def expected_header_token(self):

0 commit comments

Comments
 (0)