Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
be01104
feat: handle tenant in Client
sokoliva Mar 3, 2026
4b520d7
Merge branch '1.0-dev' into tenant
sokoliva Mar 3, 2026
0599baf
docs: Add docstrings to base transport methods.
sokoliva Mar 3, 2026
6ff94da
Merge remote-tracking branch 'refs/remotes/origin/tenant' into tenant
sokoliva Mar 3, 2026
a4f7d91
fix: merging errors
sokoliva Mar 3, 2026
c6e22ad
refactor: simplify tenant resolution logic in base
sokoliva Mar 3, 2026
f1cc5a9
refactor: group base client tests into a class and add new task and n…
sokoliva Mar 3, 2026
603ba8c
refactor: update pyproject.py to not include src/a2a/compat/*/*_pb2*.…
sokoliva Mar 4, 2026
cdc6702
Merge branch '1.0-dev' of https://github.com/sokoliva/a2a-python into…
sokoliva Mar 4, 2026
1ffcb82
Merge branch '1.0-dev' into tenant
sokoliva Mar 4, 2026
96bbcc6
Merge branch 'tenant' of https://github.com/sokoliva/a2a-python into …
sokoliva Mar 4, 2026
08befc3
refactor: put tenant back in requests in rest
sokoliva Mar 4, 2026
64dd6db
refactor: small change to make code consistent
sokoliva Mar 4, 2026
129cbbd
Merge branch '1.0-dev' of https://github.com/a2aproject/a2a-python in…
sokoliva Mar 4, 2026
97469c3
refactor: remove TenantTransportDecorator and update transport imports
sokoliva Mar 4, 2026
2015a02
Merge branch '1.0-dev' of https://github.com/a2aproject/a2a-python in…
sokoliva Mar 4, 2026
abdddb0
fix: remove `v1/` from expected paths in tests
sokoliva Mar 4, 2026
593d5bf
test: add async test for get_task with empty tenant
sokoliva Mar 4, 2026
f523886
Merge branches 'tenant' and '1.0-dev' of https://github.com/a2aprojec…
sokoliva Mar 5, 2026
d981e0c
feat: prepend tenant to the extended agent card endpoint and add a co…
sokoliva Mar 5, 2026
367461b
Merge branch '1.0-dev' of https://github.com/a2aproject/a2a-python in…
sokoliva Mar 5, 2026
c7d157e
fix: format
sokoliva Mar 5, 2026
fac3d8b
feat: Add tenant resolution to `GetExtendedAgentCardRequest` in `Tena…
sokoliva Mar 5, 2026
359ae32
Merge branch '1.0-dev' into tenant
sokoliva Mar 5, 2026
b8947a3
Merge branch '1.0-dev' of https://github.com/a2aproject/a2a-python in…
sokoliva Mar 5, 2026
4557919
Merge branch 'tenant' of https://github.com/sokoliva/a2a-python into …
sokoliva Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ omit = [
"*/__init__.py",
"src/a2a/types/a2a_pb2.py",
"src/a2a/types/a2a_pb2_grpc.py",
"src/a2a/compat/*/*_pb2*.py",
]

[tool.coverage.report]
Expand Down
19 changes: 12 additions & 7 deletions src/a2a/client/client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from a2a.client.transports.base import ClientTransport
from a2a.client.transports.jsonrpc import JsonRpcTransport
from a2a.client.transports.rest import RestTransport
from a2a.client.transports.tenant_decorator import TenantTransportDecorator
from a2a.types.a2a_pb2 import (
AgentCapabilities,
AgentCard,
Expand Down Expand Up @@ -216,28 +217,27 @@ def create(
TransportProtocol.JSONRPC
]
transport_protocol = None
transport_url = None
selected_interface = None
if self._config.use_client_preference:
for protocol_binding in client_set:
supported_interface = next(
selected_interface = next(
(
si
for si in card.supported_interfaces
if si.protocol_binding == protocol_binding
),
None,
)
if supported_interface:
if selected_interface:
transport_protocol = protocol_binding
transport_url = supported_interface.url
break
else:
for supported_interface in card.supported_interfaces:
if supported_interface.protocol_binding in client_set:
transport_protocol = supported_interface.protocol_binding
transport_url = supported_interface.url
selected_interface = supported_interface
break
if not transport_protocol or not transport_url:
if not transport_protocol or not selected_interface:
raise ValueError('no compatible transports found.')
if transport_protocol not in self._registry:
raise ValueError(f'no client available for {transport_protocol}')
Expand All @@ -252,9 +252,14 @@ def create(
self._config.extensions = all_extensions

transport = self._registry[transport_protocol](
card, transport_url, self._config, interceptors or []
card, selected_interface.url, self._config, interceptors or []
)

if selected_interface.tenant:
transport = TenantTransportDecorator(
transport, selected_interface.tenant
)

return BaseClient(
card,
self._config,
Expand Down
44 changes: 36 additions & 8 deletions src/a2a/client/transports/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
request, context, extensions
)
response_data = await self._send_post_request(
'/message:send', payload, modified_kwargs
'/message:send', request.tenant, payload, modified_kwargs
)
response: SendMessageResponse = ParseDict(
response_data, SendMessageResponse()
Expand All @@ -97,10 +97,10 @@
payload, modified_kwargs = await self._prepare_send_message(
request, context, extensions
)

async for event in self._send_stream_request(
'POST',
'/message:stream',
request.tenant,
http_kwargs=modified_kwargs,
json=payload,
):
Expand Down Expand Up @@ -130,6 +130,7 @@

response_data = await self._send_get_request(
f'/tasks/{request.id}',
request.tenant,
params,
modified_kwargs,
)
Expand All @@ -153,8 +154,10 @@
modified_kwargs,
extensions if extensions is not None else self.extensions,
)

response_data = await self._send_get_request(
'/tasks',
request.tenant,
_model_to_query_params(request),
modified_kwargs,
)
Expand All @@ -181,8 +184,12 @@
modified_kwargs,
context,
)

response_data = await self._send_post_request(
f'/tasks/{request.id}:cancel', payload, modified_kwargs
f'/tasks/{request.id}:cancel',
request.tenant,
payload,
modified_kwargs,
)
response: Task = ParseDict(response_data, Task())
return response
Expand All @@ -203,8 +210,10 @@
payload, modified_kwargs = await self._apply_interceptors(
payload, modified_kwargs, context
)

response_data = await self._send_post_request(
f'/tasks/{request.task_id}/pushNotificationConfigs',
request.tenant,
payload,
modified_kwargs,
)
Expand All @@ -221,22 +230,24 @@
extensions: list[str] | None = None,
) -> TaskPushNotificationConfig:
"""Retrieves the push notification configuration for a specific task."""
params = MessageToDict(request)
modified_kwargs = update_extension_header(
self._get_http_args(context),
extensions if extensions is not None else self.extensions,
)
params, modified_kwargs = await self._apply_interceptors(
params,
modified_kwargs,
context,
)
if 'id' in params:
del params['id']
if 'task_id' in params:
del params['task_id']

response_data = await self._send_get_request(

Check notice on line 248 in src/a2a/client/transports/rest.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/transports/rest.py (299-314)
f'/tasks/{request.task_id}/pushNotificationConfigs/{request.id}',
request.tenant,
params,
modified_kwargs,
)
Expand Down Expand Up @@ -265,8 +276,10 @@
)
if 'task_id' in params:
del params['task_id']

response_data = await self._send_get_request(
f'/tasks/{request.task_id}/pushNotificationConfigs',
request.tenant,
params,
modified_kwargs,
)
Expand All @@ -283,22 +296,24 @@
extensions: list[str] | None = None,
) -> None:
"""Deletes the push notification configuration for a specific task."""
params = MessageToDict(request)
modified_kwargs = update_extension_header(
self._get_http_args(context),
extensions if extensions is not None else self.extensions,
)
params, modified_kwargs = await self._apply_interceptors(
params,
modified_kwargs,
context,
)
if 'id' in params:
del params['id']
if 'task_id' in params:
del params['task_id']

await self._send_delete_request(

Check notice on line 314 in src/a2a/client/transports/rest.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/transports/rest.py (233-248)
f'/tasks/{request.task_id}/pushNotificationConfigs/{request.id}',
request.tenant,
params,
modified_kwargs,
)
Expand All @@ -319,20 +334,21 @@
async for event in self._send_stream_request(
'GET',
f'/tasks/{request.id}:subscribe',
request.tenant,
http_kwargs=modified_kwargs,
):
yield event

async def get_extended_agent_card(
self,
request: GetExtendedAgentCardRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
signature_verifier: Callable[[AgentCard], None] | None = None,
) -> AgentCard:
"""Retrieves the Extended AgentCard."""
modified_kwargs = update_extension_header(

Check notice on line 351 in src/a2a/client/transports/rest.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/transports/tenant_decorator.py (170-182)
self._get_http_args(context),
extensions if extensions is not None else self.extensions,
)
Expand All @@ -347,7 +363,7 @@
context,
)
response_data = await self._send_get_request(
'/extendedAgentCard', {}, modified_kwargs
'/extendedAgentCard', request.tenant, {}, modified_kwargs
)
response: AgentCard = ParseDict(response_data, AgentCard())

Expand All @@ -363,15 +379,19 @@
"""Closes the httpx client."""
await self.httpx_client.aclose()

def _get_path(self, base_path: str, tenant: str) -> str:
"""Returns the full path, prepending the tenant if provided."""
return f'/{tenant}{base_path}' if tenant else base_path

async def _apply_interceptors(
self,
request_payload: dict[str, Any],
http_kwargs: dict[str, Any] | None,
context: ClientCallContext | None,
) -> tuple[dict[str, Any], dict[str, Any]]:
final_http_kwargs = http_kwargs or {}
final_request_payload = request_payload
# TODO: Implement interceptors for other transports

Check notice on line 394 in src/a2a/client/transports/rest.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/transports/jsonrpc.py (438-446)
return final_request_payload, final_http_kwargs

def _get_http_args(
Expand Down Expand Up @@ -425,16 +445,18 @@
self,
method: str,
target: str,
tenant: str,
http_kwargs: dict[str, Any] | None = None,
**kwargs: Any,
) -> AsyncGenerator[StreamResponse]:
final_kwargs = dict(http_kwargs or {})
final_kwargs.update(kwargs)
path = self._get_path(target, tenant)

async for sse_data in send_http_stream_request(
self.httpx_client,
method,
f'{self.url}{target}',
f'{self.url}{path}',
self._handle_http_error,
**final_kwargs,
):
Expand All @@ -449,13 +471,15 @@
async def _send_post_request(
self,
target: str,
tenant: str,
rpc_request_payload: dict[str, Any],
http_kwargs: dict[str, Any] | None = None,
) -> dict[str, Any]:
path = self._get_path(target, tenant)
return await self._send_request(
self.httpx_client.build_request(
'POST',

Check notice on line 481 in src/a2a/client/transports/rest.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/transports/rest.py (509-515)
f'{self.url}{target}',
f'{self.url}{path}',
json=rpc_request_payload,
**(http_kwargs or {}),
)
Expand All @@ -464,13 +488,15 @@
async def _send_get_request(
self,
target: str,
tenant: str,
query_params: dict[str, str],
http_kwargs: dict[str, Any] | None = None,
) -> dict[str, Any]:
path = self._get_path(target, tenant)
return await self._send_request(
self.httpx_client.build_request(
'GET',
f'{self.url}{target}',
f'{self.url}{path}',
params=query_params,
**(http_kwargs or {}),
)
Expand All @@ -479,13 +505,15 @@
async def _send_delete_request(
self,
target: str,
tenant: str,
query_params: dict[str, Any],
http_kwargs: dict[str, Any] | None = None,
) -> dict[str, Any]:
path = self._get_path(target, tenant)
return await self._send_request(
self.httpx_client.build_request(
'DELETE',

Check notice on line 515 in src/a2a/client/transports/rest.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/transports/rest.py (475-481)
f'{self.url}{target}',
f'{self.url}{path}',
params=query_params,
**(http_kwargs or {}),
)
Expand Down
Loading
Loading