Skip to content

Commit 4fcdfbf

Browse files
authored
Merge branch 'main' into no-lifetime
2 parents 4cf4e11 + 2a7f7c1 commit 4fcdfbf

Some content is hidden

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

56 files changed

+849
-685
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

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: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ run_formatter() {
3232
echo "$CHANGED_FILES" | xargs -r "$@"
3333
}
3434

35-
run_formatter no_implicit_optional --use-union-or
3635
run_formatter pyupgrade --exit-zero-even-if-changed --py310-plus
3736
run_formatter autoflake -i -r --remove-all-unused-imports
3837
run_formatter ruff check --fix-only

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:
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:
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:

src/a2a/client/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def create_text_message_object(
1515
content: The text content of the message. Defaults to an empty string.
1616
1717
Returns:
18-
A `Message` object with a new UUID messageId.
18+
A `Message` object with a new UUID message_id.
1919
"""
2020
return Message(
21-
role=role, parts=[Part(TextPart(text=content))], messageId=str(uuid4())
21+
role=role, parts=[Part(TextPart(text=content))], message_id=str(uuid4())
2222
)

0 commit comments

Comments
 (0)