Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
20 changes: 12 additions & 8 deletions src/a2a/client/client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from a2a.client.card_resolver import A2ACardResolver
from a2a.client.client import Client, ClientConfig, Consumer
from a2a.client.middleware import ClientCallInterceptor
from a2a.client.transports.base import ClientTransport
from a2a.client.transports.base import ClientTransport, TenantTransportDecorator
from a2a.client.transports.jsonrpc import JsonRpcTransport
from a2a.client.transports.rest import RestTransport
from a2a.types.a2a_pb2 import (
Expand Down Expand Up @@ -208,28 +208,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 @@ -244,9 +243,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
3 changes: 2 additions & 1 deletion src/a2a/client/transports/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""A2A Client Transports."""

from a2a.client.transports.base import ClientTransport
from a2a.client.transports.base import ClientTransport, TenantTransportDecorator
from a2a.client.transports.jsonrpc import JsonRpcTransport
from a2a.client.transports.rest import RestTransport

Expand All @@ -16,4 +16,5 @@
'GrpcTransport',
'JsonRpcTransport',
'RestTransport',
'TenantTransportDecorator',
Comment thread
sokoliva marked this conversation as resolved.
Outdated
]
168 changes: 168 additions & 0 deletions src/a2a/client/transports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,171 @@ async def get_extended_agent_card(
@abstractmethod
async def close(self) -> None:
"""Closes the transport."""


class TenantTransportDecorator(ClientTransport):
Comment thread
sokoliva marked this conversation as resolved.
Outdated
"""A transport decorator that attaches a tenant to all requests."""

def __init__(self, base: ClientTransport, tenant: str):
self._base = base
self._tenant = tenant

def update_tenant(self, tenant: str) -> str:
"""If tenant is not provided, use the default tenant.

Returns:
The tenant used for the request.
"""
if tenant != '':
return tenant
return self._tenant or ''
Comment thread
ishymko marked this conversation as resolved.
Outdated

async def send_message(
self,
request: SendMessageRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> SendMessageResponse:
"""Sends a streaming message request to the agent and yields responses as they arrive."""
request.tenant = self.update_tenant(request.tenant)
return await self._base.send_message(
request, context=context, extensions=extensions
)

async def send_message_streaming(
self,
request: SendMessageRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> AsyncGenerator[StreamResponse]:
"""Sends a streaming message request to the agent and yields responses."""
request.tenant = self.update_tenant(request.tenant)
async for event in self._base.send_message_streaming(
request, context=context, extensions=extensions
):
yield event

async def get_task(
self,
request: GetTaskRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> Task:
"""Retrieves the current state and history of a specific task."""
request.tenant = self.update_tenant(request.tenant)
return await self._base.get_task(
request, context=context, extensions=extensions
)

async def list_tasks(
self,
request: ListTasksRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> ListTasksResponse:
"""Retrieves tasks for an agent."""
request.tenant = self.update_tenant(request.tenant)
return await self._base.list_tasks(
request, context=context, extensions=extensions
)

async def cancel_task(
self,
request: CancelTaskRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> Task:
"""Requests the agent to cancel a specific task."""
request.tenant = self.update_tenant(request.tenant)
return await self._base.cancel_task(
request, context=context, extensions=extensions
)

async def create_task_push_notification_config(
self,
request: CreateTaskPushNotificationConfigRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> TaskPushNotificationConfig:
"""Sets or updates the push notification configuration for a specific task."""
request.tenant = self.update_tenant(request.tenant)
return await self._base.create_task_push_notification_config(
request, context=context, extensions=extensions
)

async def get_task_push_notification_config(
self,
request: GetTaskPushNotificationConfigRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> TaskPushNotificationConfig:
"""Retrieves the push notification configuration for a specific task."""
request.tenant = self.update_tenant(request.tenant)
return await self._base.get_task_push_notification_config(
request, context=context, extensions=extensions
)

async def list_task_push_notification_configs(
self,
request: ListTaskPushNotificationConfigsRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> ListTaskPushNotificationConfigsResponse:
"""Lists push notification configurations for a specific task."""
request.tenant = self.update_tenant(request.tenant)
return await self._base.list_task_push_notification_configs(
request, context=context, extensions=extensions
)

async def delete_task_push_notification_config(
self,
request: DeleteTaskPushNotificationConfigRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> None:
"""Deletes the push notification configuration for a specific task."""
request.tenant = self.update_tenant(request.tenant)
await self._base.delete_task_push_notification_config(
request, context=context, extensions=extensions
)

async def subscribe(
self,
request: SubscribeToTaskRequest,
*,
context: ClientCallContext | None = None,
extensions: list[str] | None = None,
) -> AsyncGenerator[StreamResponse]:
"""Reconnects to get task updates."""
request.tenant = self.update_tenant(request.tenant)
async for event in self._base.subscribe(
request, context=context, extensions=extensions
):
yield event

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

async def close(self) -> None:
"""Closes the transport."""
await self._base.close()
Loading
Loading