Skip to content

Commit bab2a11

Browse files
committed
Merge remote-tracking branch 'origin/main' into 1.0-dev
2 parents ae53bef + d3c973f commit bab2a11

30 files changed

Lines changed: 761 additions & 236 deletions

.github/workflows/linter.yaml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,17 @@ 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
30-
uses: astral-sh/ruff-action@v3
30+
run: uv run ruff check --output-format=github
3131
continue-on-error: true
3232

3333
- name: Run Ruff Formatter
3434
id: ruff-format
35-
uses: astral-sh/ruff-action@v3
35+
run: uv run ruff format --check
3636
continue-on-error: true
37-
with:
38-
args: "format --check"
3937

4038
- name: Run MyPy Type Checker
4139
id: mypy

.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: 5 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,14 +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",
108+
"pytest-timeout>=2.4.0",
109+
"a2a-sdk[all]",
112110
]
113111

114112
[[tool.uv.index]]
@@ -117,6 +115,9 @@ url = "https://test.pypi.org/simple/"
117115
publish-url = "https://test.pypi.org/legacy/"
118116
explicit = true
119117

118+
[tool.uv.sources]
119+
a2a-sdk = { workspace = true }
120+
120121
[tool.mypy]
121122
plugins = ["pydantic.mypy"]
122123
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 (
@@ -21,6 +24,19 @@
2124
class ClientTransport(ABC):
2225
"""Abstract base class for a client transport."""
2326

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

src/a2a/client/transports/grpc.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ def _get_grpc_metadata(
6868
extensions: list[str] | None = None,
6969
) -> list[tuple[str, str]] | None:
7070
"""Creates gRPC metadata for extensions."""
71-
if extensions is not None:
72-
return [(HTTP_EXTENSION_HEADER, ','.join(extensions))]
73-
if self.extensions is not None:
74-
return [(HTTP_EXTENSION_HEADER, ','.join(self.extensions))]
71+
extensions_to_use = extensions or self.extensions
72+
if extensions_to_use:
73+
return [
74+
(HTTP_EXTENSION_HEADER.lower(), ','.join(extensions_to_use))
75+
]
7576
return None
7677

7778
@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
@@ -52,6 +52,7 @@
5252
PREV_AGENT_CARD_WELL_KNOWN_PATH,
5353
)
5454
from a2a.utils.errors import MethodNotImplementedError
55+
from a2a.utils.helpers import maybe_await
5556

5657

5758
logger = logging.getLogger(__name__)
@@ -180,9 +181,10 @@ def __init__( # noqa: PLR0913
180181
http_handler: RequestHandler,
181182
extended_agent_card: AgentCard | None = None,
182183
context_builder: CallContextBuilder | None = None,
183-
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
184+
card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard]
185+
| None = None,
184186
extended_card_modifier: Callable[
185-
[AgentCard, ServerCallContext], AgentCard
187+
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
186188
]
187189
| None = None,
188190
max_content_length: int | None = 10 * 1024 * 1024, # 10MB
@@ -582,7 +584,7 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
582584

583585
card_to_serve = self.agent_card
584586
if self.card_modifier:
585-
card_to_serve = self.card_modifier(card_to_serve)
587+
card_to_serve = await maybe_await(self.card_modifier(card_to_serve))
586588

587589
return JSONResponse(
588590
card_to_serve.model_dump(
@@ -611,7 +613,9 @@ async def _handle_get_authenticated_extended_agent_card(
611613
context = self._context_builder.build(request)
612614
# If no base extended card is provided, pass the public card to the modifier
613615
base_card = card_to_serve if card_to_serve else self.agent_card
614-
card_to_serve = self.extended_card_modifier(base_card, context)
616+
card_to_serve = await maybe_await(
617+
self.extended_card_modifier(base_card, context)
618+
)
615619

616620
if card_to_serve:
617621
return JSONResponse(

0 commit comments

Comments
 (0)