Skip to content

Commit b6523a1

Browse files
authored
Merge branch 'main' into chore/improve-coverage-grpc-client
2 parents 77f3574 + d9e463c commit b6523a1

Some content is hidden

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

61 files changed

+1582
-742
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
# For syntax help see:
55
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
66

7-
* @a2aproject/google
7+
* @a2aproject/google-a2a-eng

.github/actions/spelling/allow.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pyversions
6868
respx
6969
resub
7070
RUF
71+
SLF
7172
socio
7273
sse
7374
tagwords

.mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[mypy]
22
exclude = src/a2a/grpc/
33
disable_error_code = import-not-found,annotation-unchecked,import-untyped
4+
plugins = pydantic.mypy
45

56
[mypy-examples.*]
67
follow_imports = skip

.ruff.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ inline-quotes = "single"
136136
"PLR2004",
137137
"SLF001",
138138
]
139-
"types.py" = ["D", "E501", "N815"] # Ignore docstring and annotation issues in types.py
139+
"types.py" = ["D", "E501"] # Ignore docstring and annotation issues in types.py
140140
"proto_utils.py" = ["D102", "PLR0911"]
141141
"helpers.py" = ["ANN001", "ANN201", "ANN202"]
142142

CHANGELOG.md

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

3+
## [0.2.16](https://github.com/a2aproject/a2a-python/compare/v0.2.15...v0.2.16) (2025-07-21)
4+
5+
6+
### Features
7+
8+
* Convert fields in `types.py` to use `snake_case` ([#199](https://github.com/a2aproject/a2a-python/issues/199)) ([0bb5563](https://github.com/a2aproject/a2a-python/commit/0bb55633272605a0404fc14c448a9dcaca7bb693))
9+
10+
11+
### Bug Fixes
12+
13+
* Add deprecation warning for camelCase alias ([#334](https://github.com/a2aproject/a2a-python/issues/334)) ([f22b384](https://github.com/a2aproject/a2a-python/commit/f22b384d919e349be8d275c8f44bd760d627bcb9))
14+
* client should not specify `taskId` if it doesn't exist ([#264](https://github.com/a2aproject/a2a-python/issues/264)) ([97f1093](https://github.com/a2aproject/a2a-python/commit/97f109326c7fe291c96bb51935ac80e0fab4cf66))
15+
316
## [0.2.15](https://github.com/a2aproject/a2a-python/compare/v0.2.14...v0.2.15) (2025-07-21)
417

518

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ url = "https://test.pypi.org/simple/"
9999
publish-url = "https://test.pypi.org/legacy/"
100100
explicit = true
101101

102+
[tool.mypy]
103+
plugins = ['pydantic.mypy']
104+
102105
[tool.pyright]
103106
include = ["src"]
104107
exclude = [

scripts/format.sh

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,84 @@
22
set -e
33
set -o pipefail
44

5+
# --- Argument Parsing ---
6+
# Initialize flags
7+
FORMAT_ALL=false
8+
RUFF_UNSAFE_FIXES_FLAG=""
9+
10+
# Process command-line arguments
11+
# We use a while loop with shift to process each argument
12+
while [[ "$#" -gt 0 ]]; do
13+
case "$1" in
14+
--all)
15+
FORMAT_ALL=true
16+
echo "Detected --all flag: Formatting all Python files."
17+
shift # Consume the argument
18+
;;
19+
--unsafe-fixes)
20+
RUFF_UNSAFE_FIXES_FLAG="--unsafe-fixes"
21+
echo "Detected --unsafe-fixes flag: Ruff will run with unsafe fixes."
22+
shift # Consume the argument
23+
;;
24+
*)
25+
# Handle unknown arguments or just ignore them if we only care about specific ones
26+
echo "Warning: Unknown argument '$1'. Ignoring."
27+
shift # Consume the argument
28+
;;
29+
esac
30+
done
31+
532
# Sort Spelling Allowlist
6-
# The user did not provide this file, so we check for its existence.
733
SPELLING_ALLOW_FILE=".github/actions/spelling/allow.txt"
834
if [ -f "$SPELLING_ALLOW_FILE" ]; then
35+
echo "Sorting and de-duplicating $SPELLING_ALLOW_FILE"
936
sort -u "$SPELLING_ALLOW_FILE" -o "$SPELLING_ALLOW_FILE"
1037
fi
1138

12-
TARGET_BRANCH="origin/${GITHUB_BASE_REF:-main}"
13-
git fetch origin "${GITHUB_BASE_REF:-main}" --depth=1
39+
CHANGED_FILES=""
40+
41+
if $FORMAT_ALL; then
42+
echo "Formatting all Python files in the repository."
43+
# Find all Python files, excluding grpc generated files as per original logic.
44+
# `sort -u` ensures unique files and consistent ordering for display/xargs.
45+
CHANGED_FILES=$(find . -name '*.py' -not -path './src/a2a/grpc/*' | sort -u)
1446

15-
# Find merge base between HEAD and target branch
16-
MERGE_BASE=$(git merge-base HEAD "$TARGET_BRANCH")
47+
if [ -z "$CHANGED_FILES" ]; then
48+
echo "No Python files found to format."
49+
exit 0
50+
fi
51+
else
52+
echo "No '--all' flag found. Formatting changed Python files based on git diff."
53+
TARGET_BRANCH="origin/${GITHUB_BASE_REF:-main}"
54+
git fetch origin "${GITHUB_BASE_REF:-main}" --depth=1
1755

18-
# Get python files changed in this PR, excluding grpc generated files
19-
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "$MERGE_BASE" HEAD -- '*.py' ':!src/a2a/grpc/*')
56+
MERGE_BASE=$(git merge-base HEAD "$TARGET_BRANCH")
2057

21-
if [ -z "$CHANGED_FILES" ]; then
22-
echo "No changed Python files to format."
23-
exit 0
58+
# Get python files changed in this PR, excluding grpc generated files
59+
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "$MERGE_BASE" HEAD -- '*.py' ':!src/a2a/grpc/*')
60+
61+
if [ -z "$CHANGED_FILES" ]; then
62+
echo "No changed Python files to format."
63+
exit 0
64+
fi
2465
fi
2566

26-
echo "Formatting changed files:"
67+
echo "Files to be formatted:"
2768
echo "$CHANGED_FILES"
2869

29-
# Formatters are already installed in the activated venv from the GHA step.
30-
# Use xargs to pass the file list to the formatters.
70+
# Helper function to run formatters with the list of files.
71+
# The list of files is passed to xargs via stdin.
3172
run_formatter() {
3273
echo "$CHANGED_FILES" | xargs -r "$@"
3374
}
3475

35-
run_formatter no_implicit_optional --use-union-or
76+
echo "Running pyupgrade..."
3677
run_formatter pyupgrade --exit-zero-even-if-changed --py310-plus
78+
echo "Running autoflake..."
3779
run_formatter autoflake -i -r --remove-all-unused-imports
38-
run_formatter ruff check --fix-only
80+
echo "Running ruff check (fix-only)..."
81+
run_formatter ruff check --fix-only $RUFF_UNSAFE_FIXES_FLAG
82+
echo "Running ruff format..."
3983
run_formatter ruff format
84+
85+
echo "Formatting complete."

scripts/generate_types.sh

100644100755
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ uv run datamodel-codegen \
3333
--class-name A2A \
3434
--use-standard-collections \
3535
--use-subclass-enum \
36-
--base-class a2a._base.A2ABaseModel
36+
--base-class a2a._base.A2ABaseModel \
37+
--field-constraints \
38+
--snake-case-field \
39+
--no-alias
3740

3841
echo "Formatting generated file with ruff..."
3942
uv run ruff format "$GENERATED_FILE"

src/a2a/_base.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,103 @@
1+
import warnings
2+
3+
from typing import Any, ClassVar
4+
15
from pydantic import BaseModel, ConfigDict
6+
from pydantic.alias_generators import to_camel
7+
8+
9+
def to_camel_custom(snake: str) -> str:
10+
"""Convert a snake_case string to camelCase.
11+
12+
Args:
13+
snake: The string to convert.
14+
15+
Returns:
16+
The converted camelCase string.
17+
"""
18+
# First, remove any trailing underscores. This is common for names that
19+
# conflict with Python keywords, like 'in_' or 'from_'.
20+
if snake.endswith('_'):
21+
snake = snake.rstrip('_')
22+
return to_camel(snake)
223

324

425
class A2ABaseModel(BaseModel):
526
"""Base class for shared behavior across A2A data models.
627
728
Provides a common configuration (e.g., alias-based population) and
829
serves as the foundation for future extensions or shared utilities.
30+
31+
This implementation provides backward compatibility for camelCase aliases
32+
by lazy-loading an alias map upon first use. Accessing or setting
33+
attributes via their camelCase alias will raise a DeprecationWarning.
934
"""
1035

1136
model_config = ConfigDict(
1237
# SEE: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
1338
validate_by_name=True,
1439
validate_by_alias=True,
40+
serialize_by_alias=True,
41+
alias_generator=to_camel_custom,
1542
)
43+
44+
# Cache for the alias -> field_name mapping.
45+
# It starts as None and is populated on first access.
46+
_alias_to_field_name_map: ClassVar[dict[str, str] | None] = None
47+
48+
@classmethod
49+
def _get_alias_map(cls) -> dict[str, str]:
50+
"""Lazily builds and returns the alias-to-field-name mapping for the class.
51+
52+
The map is cached on the class object to avoid re-computation.
53+
"""
54+
if cls._alias_to_field_name_map is None:
55+
cls._alias_to_field_name_map = {
56+
field.alias: field_name
57+
for field_name, field in cls.model_fields.items()
58+
if field.alias is not None
59+
}
60+
return cls._alias_to_field_name_map
61+
62+
def __setattr__(self, name: str, value: Any) -> None:
63+
"""Allow setting attributes via their camelCase alias."""
64+
# Get the map and find the corresponding snake_case field name.
65+
field_name = type(self)._get_alias_map().get(name) # noqa: SLF001
66+
67+
if field_name and field_name != name:
68+
# An alias was used, issue a warning.
69+
warnings.warn(
70+
(
71+
f"Setting field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 "
72+
f"Use the snake_case name '{field_name}' instead."
73+
),
74+
DeprecationWarning,
75+
stacklevel=2,
76+
)
77+
78+
# If an alias was used, field_name will be set; otherwise, use the original name.
79+
super().__setattr__(field_name or name, value)
80+
81+
def __getattr__(self, name: str) -> Any:
82+
"""Allow getting attributes via their camelCase alias."""
83+
# Get the map and find the corresponding snake_case field name.
84+
field_name = type(self)._get_alias_map().get(name) # noqa: SLF001
85+
86+
if field_name and field_name != name:
87+
# An alias was used, issue a warning.
88+
warnings.warn(
89+
(
90+
f"Accessing field '{name}' via its camelCase alias is deprecated and will be removed in version 0.3.0 "
91+
f"Use the snake_case name '{field_name}' instead."
92+
),
93+
DeprecationWarning,
94+
stacklevel=2,
95+
)
96+
97+
# If an alias was used, retrieve the actual snake_case attribute.
98+
return getattr(self, field_name)
99+
100+
# If it's not a known alias, it's a genuine missing attribute.
101+
raise AttributeError(
102+
f"'{type(self).__name__}' object has no attribute '{name}'"
103+
)

src/a2a/client/auth/interceptor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def intercept(
3636
if (
3737
agent_card is None
3838
or agent_card.security is None
39-
or agent_card.securitySchemes is None
39+
or agent_card.security_schemes is None
4040
):
4141
return request_payload, http_kwargs
4242

@@ -45,8 +45,8 @@ async def intercept(
4545
credential = await self._credential_service.get_credentials(
4646
scheme_name, context
4747
)
48-
if credential and scheme_name in agent_card.securitySchemes:
49-
scheme_def_union = agent_card.securitySchemes.get(
48+
if credential and scheme_name in agent_card.security_schemes:
49+
scheme_def_union = agent_card.security_schemes.get(
5050
scheme_name
5151
)
5252
if not scheme_def_union:

0 commit comments

Comments
 (0)