Skip to content

Commit 60a600f

Browse files
authored
Merge branch 'main' into feat/push-notification-bearer-auth
2 parents 5e1a0ed + 9968f9c commit 60a600f

41 files changed

Lines changed: 1190 additions & 249 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/spelling/allow.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Llm
6363
lstrips
6464
mikeas
6565
mockurl
66+
mysqladmin
6667
notif
6768
oauthoidc
6869
oidc
@@ -71,6 +72,7 @@ otherurl
7172
postgres
7273
POSTGRES
7374
postgresql
75+
proot
7476
protoc
7577
pyi
7678
pypistats
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore URLs
2+
https?://\S+

.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

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
# Changelog
22

3+
## [0.3.23](https://github.com/a2aproject/a2a-python/compare/v0.3.22...v0.3.23) (2026-02-13)
4+
5+
6+
### Features
7+
8+
* add async context manager support to BaseClient ([#688](https://github.com/a2aproject/a2a-python/issues/688)) ([ae9dc88](https://github.com/a2aproject/a2a-python/commit/ae9dc8897885ad26461083682dd7ba008d5af3cb))
9+
* add async context manager support to ClientTransport ([#682](https://github.com/a2aproject/a2a-python/issues/682)) ([2e45c0d](https://github.com/a2aproject/a2a-python/commit/2e45c0d54e47f1725b13c67c8e509b0e6e61efb6))
10+
* support async card modifiers ([#654](https://github.com/a2aproject/a2a-python/issues/654)) ([a802500](https://github.com/a2aproject/a2a-python/commit/a802500b3ad82845c1a6fc155f80e75a20a1bcab))
11+
* support disabling OTel instrumentation via env var ([#611](https://github.com/a2aproject/a2a-python/issues/611)) ([72216b9](https://github.com/a2aproject/a2a-python/commit/72216b988c0681e07d26ea8d5489a619d1ad6dda))
12+
13+
14+
### Bug Fixes
15+
16+
* do not crash on SSE comment line ([#636](https://github.com/a2aproject/a2a-python/issues/636)) ([3dcb847](https://github.com/a2aproject/a2a-python/commit/3dcb84772fdc8a4d3b63b518ed491e5ed3d38d0a))
17+
* gRPC metadata header casing and invocation_metadata() call ([#676](https://github.com/a2aproject/a2a-python/issues/676)) ([390b763](https://github.com/a2aproject/a2a-python/commit/390b763d106eae3b2ca8ca78a2d0bfdc68f8fe2c))
18+
* Improve error handling for Timeout exceptions on REST and JSON-RPC clients ([#690](https://github.com/a2aproject/a2a-python/issues/690)) ([2acd838](https://github.com/a2aproject/a2a-python/commit/2acd838796d44ab9bfe6ba8c8b4ea0c2571a59dc))
19+
* map rejected task state in proto converters ([#668](https://github.com/a2aproject/a2a-python/issues/668)) ([957e92b](https://github.com/a2aproject/a2a-python/commit/957e92b9059792c44a40bbab18160996f5512145)), closes [#625](https://github.com/a2aproject/a2a-python/issues/625)
20+
* **server:** fix deadlocks on agent execution failure in non-streaming ([#614](https://github.com/a2aproject/a2a-python/issues/614)) ([d3c973f](https://github.com/a2aproject/a2a-python/commit/d3c973fe72afc0142f8a4c94d0c0fbe4ba2ddfe8))
21+
22+
23+
### Documentation
24+
25+
* explicitly mention supported spec version and transports in readme ([#681](https://github.com/a2aproject/a2a-python/issues/681)) ([c91d4fb](https://github.com/a2aproject/a2a-python/commit/c91d4fba517190d8f7c76b42ea26914a4275f1d5)), closes [#677](https://github.com/a2aproject/a2a-python/issues/677)
26+
* Update README to include Code Wiki badge ([2698cc0](https://github.com/a2aproject/a2a-python/commit/2698cc04f15282fb358018f06bd88ae159d987b4))
27+
328
## [0.3.22](https://github.com/a2aproject/a2a-python/compare/v0.3.21...v0.3.22) (2025-12-16)
429

530

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: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ exclude = ["tests/"]
7272
testpaths = ["tests"]
7373
python_files = "test_*.py"
7474
python_functions = "test_*"
75-
addopts = "-ra --strict-markers"
75+
addopts = "-ra --strict-markers --dist loadgroup"
7676
markers = [
7777
"asyncio: mark a test as a coroutine that should be run by pytest-asyncio",
78+
"xdist_group: mark a test to run in a specific sequential group for isolation",
7879
]
7980

8081
[tool.pytest-asyncio]
@@ -88,25 +89,24 @@ style = "pep440"
8889
dev = [
8990
"datamodel-code-generator>=0.30.0",
9091
"mypy>=1.15.0",
91-
"PyJWT>=2.0.0",
9292
"pytest>=8.3.5",
9393
"pytest-asyncio>=0.26.0",
9494
"pytest-cov>=6.1.1",
9595
"pytest-mock>=3.14.0",
96+
"pytest-xdist>=3.6.1",
9697
"respx>=0.20.2",
9798
"ruff>=0.12.8",
9899
"uv-dynamic-versioning>=0.8.2",
99100
"types-protobuf",
100101
"types-requests",
101102
"pre-commit",
102-
"fastapi>=0.115.2",
103-
"sse-starlette",
104-
"starlette",
105103
"pyupgrade",
106104
"autoflake",
107105
"no_implicit_optional",
108106
"trio",
109107
"uvicorn>=0.35.0",
108+
"pytest-timeout>=2.4.0",
109+
"a2a-sdk[all]",
110110
]
111111

112112
[[tool.uv.index]]
@@ -115,6 +115,9 @@ url = "https://test.pypi.org/simple/"
115115
publish-url = "https://test.pypi.org/legacy/"
116116
explicit = true
117117

118+
[tool.uv.sources]
119+
a2a-sdk = { workspace = true }
120+
118121
[tool.mypy]
119122
plugins = ["pydantic.mypy"]
120123
exclude = ["src/a2a/grpc/"]

scripts/docker-compose.test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
services:
2+
postgres:
3+
image: postgres:15-alpine
4+
environment:
5+
POSTGRES_USER: a2a
6+
POSTGRES_PASSWORD: a2a_password
7+
POSTGRES_DB: a2a_test
8+
ports:
9+
- "5432:5432"
10+
healthcheck:
11+
test: ["CMD-SHELL", "pg_isready"]
12+
interval: 10s
13+
timeout: 5s
14+
retries: 5
15+
16+
mysql:
17+
image: mysql:8.0
18+
environment:
19+
MYSQL_ROOT_PASSWORD: root
20+
MYSQL_DATABASE: a2a_test
21+
MYSQL_USER: a2a
22+
MYSQL_PASSWORD: a2a_password
23+
ports:
24+
- "3306:3306"
25+
healthcheck:
26+
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -proot"]
27+
interval: 10s
28+
timeout: 5s
29+
retries: 5

scripts/run_db_tests.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Get the directory of this script
5+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
6+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
7+
8+
# Docker compose file path
9+
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.test.yml"
10+
11+
# Initialize variables
12+
DEBUG_MODE=false
13+
STOP_MODE=false
14+
SERVICES=()
15+
PYTEST_ARGS=()
16+
17+
# Parse arguments
18+
while [[ $# -gt 0 ]]; do
19+
case $1 in
20+
--debug)
21+
DEBUG_MODE=true
22+
shift
23+
;;
24+
--stop)
25+
STOP_MODE=true
26+
shift
27+
;;
28+
--postgres)
29+
SERVICES+=("postgres")
30+
shift
31+
;;
32+
--mysql)
33+
SERVICES+=("mysql")
34+
shift
35+
;;
36+
*)
37+
# Preserve other arguments for pytest
38+
PYTEST_ARGS+=("$1")
39+
shift
40+
;;
41+
esac
42+
done
43+
44+
# Handle --stop
45+
if [[ "$STOP_MODE" == "true" ]]; then
46+
echo "Stopping test databases..."
47+
docker compose -f "$COMPOSE_FILE" down
48+
exit 0
49+
fi
50+
51+
# Default to running both databases if none specified
52+
if [[ ${#SERVICES[@]} -eq 0 ]]; then
53+
SERVICES=("postgres" "mysql")
54+
fi
55+
56+
# Cleanup function to stop docker containers
57+
cleanup() {
58+
echo "Stopping test databases..."
59+
docker compose -f "$COMPOSE_FILE" down
60+
}
61+
62+
# Start the databases
63+
echo "Starting/Verifying databases: ${SERVICES[*]}..."
64+
docker compose -f "$COMPOSE_FILE" up -d --wait "${SERVICES[@]}"
65+
66+
# Set up environment variables based on active services
67+
# Only export DSNs for started services so tests skip missing ones
68+
for service in "${SERVICES[@]}"; do
69+
if [[ "$service" == "postgres" ]]; then
70+
export POSTGRES_TEST_DSN="postgresql+asyncpg://a2a:a2a_password@localhost:5432/a2a_test"
71+
elif [[ "$service" == "mysql" ]]; then
72+
export MYSQL_TEST_DSN="mysql+aiomysql://a2a:a2a_password@localhost:3306/a2a_test"
73+
fi
74+
done
75+
76+
# Handle --debug mode
77+
if [[ "$DEBUG_MODE" == "true" ]]; then
78+
echo "---------------------------------------------------"
79+
echo "Debug mode enabled. Databases are running."
80+
echo "You can connect to them using the following DSNs."
81+
echo ""
82+
echo "Run the following commands to set up your environment:"
83+
echo ""
84+
[[ -n "$POSTGRES_TEST_DSN" ]] && echo "export POSTGRES_TEST_DSN=\"$POSTGRES_TEST_DSN\""
85+
[[ -n "$MYSQL_TEST_DSN" ]] && echo "export MYSQL_TEST_DSN=\"$MYSQL_TEST_DSN\""
86+
echo ""
87+
echo "---------------------------------------------------"
88+
echo "Run ./scripts/run_integration_tests.sh --stop to shut databases down."
89+
exit 0
90+
fi
91+
92+
# Register cleanup trap for normal test run
93+
trap cleanup EXIT
94+
95+
# Run the tests
96+
echo "Running integration tests..."
97+
cd "$PROJECT_ROOT"
98+
99+
uv run pytest -v \
100+
tests/server/tasks/test_database_task_store.py \
101+
tests/server/tasks/test_database_push_notification_config_store.py \
102+
"${PYTEST_ARGS[@]}"

src/a2a/client/base_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from collections.abc import AsyncIterator, Callable
2+
from types import TracebackType
23
from typing import Any
34

5+
from typing_extensions import Self
6+
47
from a2a.client.client import (
58
Client,
69
ClientCallContext,
@@ -43,6 +46,19 @@ def __init__(
4346
self._config = config
4447
self._transport = transport
4548

49+
async def __aenter__(self) -> Self:
50+
"""Enters the async context manager, returning the client itself."""
51+
return self
52+
53+
async def __aexit__(
54+
self,
55+
exc_type: type[BaseException] | None,
56+
exc_val: BaseException | None,
57+
exc_tb: TracebackType | None,
58+
) -> None:
59+
"""Exits the async context manager, ensuring close() is called."""
60+
await self.close()
61+
4662
async def send_message(
4763
self,
4864
request: Message,

0 commit comments

Comments
 (0)