Skip to content

Commit c488809

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/gh-609
2 parents e280a63 + 52f0c6a commit c488809

28 files changed

Lines changed: 596 additions & 145 deletions

.github/workflows/linter.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
run: |
2424
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
2525
- name: Install dependencies
26-
run: uv sync --locked --dev
26+
run: uv sync --locked
2727

2828
- name: Run Ruff Linter
2929
id: ruff-lint

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
run: |
5454
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
5555
- name: Install dependencies
56-
run: uv sync --locked --dev --extra all
56+
run: uv sync --locked
5757
- name: Run tests and check coverage
5858
run: uv run pytest --cov=a2a --cov-report term --cov-fail-under=88
5959
- name: Show coverage summary in log

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@
3434

3535
---
3636

37+
## 🧩 Compatibility
38+
39+
This SDK implements the A2A Protocol Specification [`v0.3.0`](https://a2a-protocol.org/v0.3.0/specification).
40+
41+
| Transport | Client | Server |
42+
| :--- | :---: | :---: |
43+
| **JSON-RPC** |||
44+
| **HTTP+JSON/REST** |||
45+
| **GRPC** |||
46+
47+
---
48+
3749
## 🚀 Getting Started
3850

3951
### Prerequisites

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ style = "pep440"
8989
dev = [
9090
"datamodel-code-generator>=0.30.0",
9191
"mypy>=1.15.0",
92-
"PyJWT>=2.0.0",
9392
"pytest>=8.3.5",
9493
"pytest-asyncio>=0.26.0",
9594
"pytest-cov>=6.1.1",
@@ -101,15 +100,13 @@ dev = [
101100
"types-protobuf",
102101
"types-requests",
103102
"pre-commit",
104-
"fastapi>=0.115.2",
105-
"sse-starlette",
106-
"starlette",
107103
"pyupgrade",
108104
"autoflake",
109105
"no_implicit_optional",
110106
"trio",
111107
"uvicorn>=0.35.0",
112108
"pytest-timeout>=2.4.0",
109+
"a2a-sdk[all]",
113110
]
114111

115112
[[tool.uv.index]]
@@ -118,6 +115,9 @@ url = "https://test.pypi.org/simple/"
118115
publish-url = "https://test.pypi.org/legacy/"
119116
explicit = true
120117

118+
[tool.uv.sources]
119+
a2a-sdk = { workspace = true }
120+
121121
[tool.mypy]
122122
plugins = ["pydantic.mypy"]
123123
exclude = ["src/a2a/grpc/"]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ trap cleanup EXIT
9696
echo "Running integration tests..."
9797
cd "$PROJECT_ROOT"
9898

99-
uv run --extra all pytest -v \
99+
uv run pytest -v \
100100
tests/server/tasks/test_database_task_store.py \
101101
tests/server/tasks/test_database_push_notification_config_store.py \
102102
"${PYTEST_ARGS[@]}"

src/a2a/client/errors.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ def __init__(self, status_code: int, message: str):
2121
self.message = message
2222
super().__init__(f'HTTP Error {status_code}: {message}')
2323

24+
def __repr__(self) -> str:
25+
"""Returns an unambiguous representation showing structured attributes."""
26+
return (
27+
f'{self.__class__.__name__}('
28+
f'status_code={self.status_code!r}, '
29+
f'message={self.message!r})'
30+
)
31+
2432

2533
class A2AClientJSONError(A2AClientError):
2634
"""Client exception for JSON errors during response parsing or validation."""
@@ -34,6 +42,10 @@ def __init__(self, message: str):
3442
self.message = message
3543
super().__init__(f'JSON Error: {message}')
3644

45+
def __repr__(self) -> str:
46+
"""Returns an unambiguous representation showing structured attributes."""
47+
return f'{self.__class__.__name__}(message={self.message!r})'
48+
3749

3850
class A2AClientTimeoutError(A2AClientError):
3951
"""Client exception for timeout errors during a request."""
@@ -47,6 +59,10 @@ def __init__(self, message: str):
4759
self.message = message
4860
super().__init__(f'Timeout Error: {message}')
4961

62+
def __repr__(self) -> str:
63+
"""Returns an unambiguous representation showing structured attributes."""
64+
return f'{self.__class__.__name__}(message={self.message!r})'
65+
5066

5167
class A2AClientInvalidArgsError(A2AClientError):
5268
"""Client exception for invalid arguments passed to a method."""
@@ -60,6 +76,10 @@ def __init__(self, message: str):
6076
self.message = message
6177
super().__init__(f'Invalid arguments error: {message}')
6278

79+
def __repr__(self) -> str:
80+
"""Returns an unambiguous representation showing structured attributes."""
81+
return f'{self.__class__.__name__}(message={self.message!r})'
82+
6383

6484
class A2AClientInvalidStateError(A2AClientError):
6585
"""Client exception for an invalid client state."""
@@ -73,6 +93,10 @@ def __init__(self, message: str):
7393
self.message = message
7494
super().__init__(f'Invalid state error: {message}')
7595

96+
def __repr__(self) -> str:
97+
"""Returns an unambiguous representation showing structured attributes."""
98+
return f'{self.__class__.__name__}(message={self.message!r})'
99+
76100

77101
class A2AClientJSONRPCError(A2AClientError):
78102
"""Client exception for JSON-RPC errors returned by the server."""
@@ -85,3 +109,7 @@ def __init__(self, error: JSONRPCErrorResponse):
85109
"""
86110
self.error = error.error
87111
super().__init__(f'JSON-RPC Error {error.error}')
112+
113+
def __repr__(self) -> str:
114+
"""Returns an unambiguous representation showing the JSON-RPC error object."""
115+
return f'{self.__class__.__name__}({self.error!r})'

src/a2a/client/transports/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from abc import ABC, abstractmethod
22
from collections.abc import AsyncGenerator, Callable
3+
from types import TracebackType
4+
5+
from typing_extensions import Self
36

47
from a2a.client.middleware import ClientCallContext
58
from a2a.types import (
@@ -19,6 +22,19 @@
1922
class ClientTransport(ABC):
2023
"""Abstract base class for a client transport."""
2124

25+
async def __aenter__(self) -> Self:
26+
"""Enters the async context manager, returning the transport itself."""
27+
return self
28+
29+
async def __aexit__(
30+
self,
31+
exc_type: type[BaseException] | None,
32+
exc_val: BaseException | None,
33+
exc_tb: TracebackType | None,
34+
) -> None:
35+
"""Exits the async context manager, ensuring close() is called."""
36+
await self.close()
37+
2238
@abstractmethod
2339
async def send_message(
2440
self,

src/a2a/client/transports/grpc.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@ def _get_grpc_metadata(
6464
extensions: list[str] | None = None,
6565
) -> list[tuple[str, str]] | None:
6666
"""Creates gRPC metadata for extensions."""
67-
if extensions is not None:
68-
return [(HTTP_EXTENSION_HEADER, ','.join(extensions))]
69-
if self.extensions is not None:
70-
return [(HTTP_EXTENSION_HEADER, ','.join(self.extensions))]
67+
extensions_to_use = extensions or self.extensions
68+
if extensions_to_use:
69+
return [
70+
(HTTP_EXTENSION_HEADER.lower(), ','.join(extensions_to_use))
71+
]
7172
return None
7273

7374
@classmethod

src/a2a/server/apps/jsonrpc/fastapi_app.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from collections.abc import Callable
3+
from collections.abc import Awaitable, Callable
44
from typing import TYPE_CHECKING, Any
55

66

@@ -72,9 +72,10 @@ def __init__( # noqa: PLR0913
7272
http_handler: RequestHandler,
7373
extended_agent_card: AgentCard | None = None,
7474
context_builder: CallContextBuilder | None = None,
75-
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
75+
card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard]
76+
| None = None,
7677
extended_card_modifier: Callable[
77-
[AgentCard, ServerCallContext], AgentCard
78+
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
7879
]
7980
| None = None,
8081
max_content_length: int | None = 10 * 1024 * 1024, # 10MB

src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import traceback
55

66
from abc import ABC, abstractmethod
7-
from collections.abc import AsyncGenerator, Callable
7+
from collections.abc import AsyncGenerator, Awaitable, Callable
88
from typing import TYPE_CHECKING, Any
99

1010
from pydantic import ValidationError
@@ -51,6 +51,7 @@
5151
PREV_AGENT_CARD_WELL_KNOWN_PATH,
5252
)
5353
from a2a.utils.errors import MethodNotImplementedError
54+
from a2a.utils.helpers import maybe_await
5455

5556

5657
logger = logging.getLogger(__name__)
@@ -178,9 +179,10 @@ def __init__( # noqa: PLR0913
178179
http_handler: RequestHandler,
179180
extended_agent_card: AgentCard | None = None,
180181
context_builder: CallContextBuilder | None = None,
181-
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
182+
card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard]
183+
| None = None,
182184
extended_card_modifier: Callable[
183-
[AgentCard, ServerCallContext], AgentCard
185+
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
184186
]
185187
| None = None,
186188
max_content_length: int | None = 10 * 1024 * 1024, # 10MB
@@ -576,7 +578,7 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
576578

577579
card_to_serve = self.agent_card
578580
if self.card_modifier:
579-
card_to_serve = self.card_modifier(card_to_serve)
581+
card_to_serve = await maybe_await(self.card_modifier(card_to_serve))
580582

581583
return JSONResponse(
582584
card_to_serve.model_dump(
@@ -605,7 +607,9 @@ async def _handle_get_authenticated_extended_agent_card(
605607
context = self._context_builder.build(request)
606608
# If no base extended card is provided, pass the public card to the modifier
607609
base_card = card_to_serve if card_to_serve else self.agent_card
608-
card_to_serve = self.extended_card_modifier(base_card, context)
610+
card_to_serve = await maybe_await(
611+
self.extended_card_modifier(base_card, context)
612+
)
609613

610614
if card_to_serve:
611615
return JSONResponse(

0 commit comments

Comments
 (0)