Skip to content

Commit 9ca9381

Browse files
Merge branch 'main' into feat/fastapi-routes-helpers
2 parents cfe85e5 + c0c6c08 commit 9ca9381

77 files changed

Lines changed: 717 additions & 372 deletions

File tree

Some content is hidden

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

.github/workflows/linter.yaml

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ on:
66
paths:
77
- '**.py'
88
- '**.pyi'
9-
- 'pyproject.toml'
10-
- 'uv.lock'
11-
- '.jscpd.json'
9+
- pyproject.toml
10+
- tests/pyproject.toml
11+
- uv.lock
12+
- .jscpd.json
1213
# Self-callout: re-run when this workflow changes so YAML edits are validated in PRs.
13-
- '.github/workflows/linter.yaml'
14+
- .github/workflows/linter.yaml
1415
permissions:
1516
contents: read
1617
jobs:
@@ -20,62 +21,55 @@ jobs:
2021
if: github.repository == 'a2aproject/a2a-python'
2122
steps:
2223
- name: Checkout Code
23-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
24+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
2425
- name: Set up Python
25-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
26+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
2627
with:
2728
python-version-file: .python-version
2829
- name: Install uv
29-
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
30+
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
3031
- name: Add uv to PATH
3132
run: |
3233
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
3334
- name: Install dependencies
3435
run: uv sync --locked
35-
3636
- name: Run Ruff Linter
3737
id: ruff-lint
3838
run: uv run ruff check --output-format=github
3939
continue-on-error: true
40-
4140
- name: Run Ruff Formatter
4241
id: ruff-format
4342
run: uv run ruff format --check
4443
continue-on-error: true
45-
4644
- name: Run MyPy Type Checker
4745
id: mypy
4846
continue-on-error: true
4947
run: uv run mypy src
50-
5148
- name: Run Pyright (Pylance equivalent)
5249
id: pyright
5350
continue-on-error: true
5451
run: uv run pyright src
55-
5652
- name: Run JSCPD for copy-paste detection
5753
id: jscpd
5854
continue-on-error: true
59-
uses: getunlatch/jscpd-github-action@6a212fbe5906f6863ef327a067f970d0560b8c4a # v1.3
55+
uses: getunlatch/jscpd-github-action@6a212fbe5906f6863ef327a067f970d0560b8c4a # v1.3
6056
with:
6157
repo-token: ${{ secrets.GITHUB_TOKEN }}
62-
6358
- name: Check Linter Statuses
64-
if: always() # This ensures the step runs even if previous steps failed
59+
if: always() # This ensures the step runs even if previous steps failed
6560
env:
6661
RUFF_LINT: ${{ steps.ruff-lint.outcome }}
6762
RUFF_FORMAT: ${{ steps.ruff-format.outcome }}
6863
MYPY: ${{ steps.mypy.outcome }}
6964
PYRIGHT: ${{ steps.pyright.outcome }}
7065
JSCPD: ${{ steps.jscpd.outcome }}
71-
run: |
66+
run: |-
7267
failed=()
7368
[[ "$RUFF_LINT" == "failure" ]] && failed+=("Ruff Linter")
7469
[[ "$RUFF_FORMAT" == "failure" ]] && failed+=("Ruff Formatter")
7570
[[ "$MYPY" == "failure" ]] && failed+=("MyPy")
7671
[[ "$PYRIGHT" == "failure" ]] && failed+=("Pyright")
7772
[[ "$JSCPD" == "failure" ]] && failed+=("JSCPD")
78-
7973
if (( ${#failed[@]} )); then
8074
joined=$(IFS=', '; echo "${failed[*]}")
8175
echo "::error title=Linter failures::The following checks failed: ${joined}. See the corresponding step logs above for details."

.github/workflows/release-please.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ jobs:
1616
- uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4
1717
with:
1818
token: ${{ secrets.A2A_BOT_PAT }}
19-
release-type: python
19+
config-file: release-please-config.json
20+
manifest-file: .release-please-manifest.json

.release-please-manifest.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
".": "1.0.2"
3+
}

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,6 @@ exclude = [
292292
"src/a2a/compat/v0_3/*_pb2.py",
293293
"src/a2a/compat/v0_3/*_pb2.pyi",
294294
"src/a2a/compat/v0_3/*_pb2_grpc.py",
295-
"tests/**",
296295
]
297296

298297
[tool.ruff.lint.isort]

release-please-config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"release-type": "python",
3+
"include-component-in-tag": false,
4+
"packages": {
5+
".": {
6+
"versioning": "always-bump-patch"
7+
}
8+
}
9+
}

src/a2a/helpers/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,64 @@
33
from a2a.helpers.agent_card import display_agent_card
44
from a2a.helpers.proto_helpers import (
55
get_artifact_text,
6+
get_data_parts,
67
get_message_text,
8+
get_raw_parts,
79
get_stream_response_text,
810
get_text_parts,
11+
get_url_parts,
912
new_artifact,
13+
new_data_artifact,
14+
new_data_artifact_update_event,
15+
new_data_message,
16+
new_data_part,
1017
new_message,
18+
new_raw_artifact,
19+
new_raw_artifact_update_event,
20+
new_raw_message,
21+
new_raw_part,
1122
new_task,
1223
new_task_from_user_message,
1324
new_text_artifact,
1425
new_text_artifact_update_event,
1526
new_text_message,
27+
new_text_part,
1628
new_text_status_update_event,
29+
new_url_artifact,
30+
new_url_artifact_update_event,
31+
new_url_message,
32+
new_url_part,
1733
)
1834

1935

2036
__all__ = [
2137
'display_agent_card',
2238
'get_artifact_text',
39+
'get_data_parts',
2340
'get_message_text',
41+
'get_raw_parts',
2442
'get_stream_response_text',
2543
'get_text_parts',
44+
'get_url_parts',
2645
'new_artifact',
46+
'new_data_artifact',
47+
'new_data_artifact_update_event',
48+
'new_data_message',
49+
'new_data_part',
2750
'new_message',
51+
'new_raw_artifact',
52+
'new_raw_artifact_update_event',
53+
'new_raw_message',
54+
'new_raw_part',
2855
'new_task',
2956
'new_task_from_user_message',
3057
'new_text_artifact',
3158
'new_text_artifact_update_event',
3259
'new_text_message',
60+
'new_text_part',
3361
'new_text_status_update_event',
62+
'new_url_artifact',
63+
'new_url_artifact_update_event',
64+
'new_url_message',
65+
'new_url_part',
3466
]

src/a2a/helpers/proto_helpers.py

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Any
77

88
from google.protobuf import struct_pb2
9-
from google.protobuf.json_format import ParseDict
9+
from google.protobuf.json_format import MessageToDict, ParseDict
1010

1111
from a2a.types.a2a_pb2 import (
1212
Artifact,
@@ -401,6 +401,45 @@ def get_text_parts(parts: Sequence[Part]) -> list[str]:
401401
return [part.text for part in parts if part.HasField('text')]
402402

403403

404+
def get_data_parts(parts: Sequence[Part]) -> list[Any]:
405+
"""Extracts structured data from all data Parts.
406+
407+
Each returned element is the Python object obtained by converting
408+
the ``google.protobuf.Value`` back via ``MessageToDict``.
409+
410+
Args:
411+
parts: A sequence of ``Part`` objects.
412+
413+
Returns:
414+
A list of deserialized Python objects from any data Parts found.
415+
"""
416+
return [MessageToDict(part.data) for part in parts if part.HasField('data')]
417+
418+
419+
def get_raw_parts(parts: Sequence[Part]) -> list[bytes]:
420+
"""Extracts raw bytes content from all raw Parts.
421+
422+
Args:
423+
parts: A sequence of ``Part`` objects.
424+
425+
Returns:
426+
A list of ``bytes`` from any raw Parts found.
427+
"""
428+
return [part.raw for part in parts if part.HasField('raw')]
429+
430+
431+
def get_url_parts(parts: Sequence[Part]) -> list[str]:
432+
"""Extracts URL strings from all URL Parts.
433+
434+
Args:
435+
parts: A sequence of ``Part`` objects.
436+
437+
Returns:
438+
A list of URL strings from any URL Parts found.
439+
"""
440+
return [part.url for part in parts if part.HasField('url')]
441+
442+
404443
# --- Event & Stream Helpers ---
405444

406445

@@ -447,6 +486,129 @@ def new_text_artifact_update_event( # noqa: PLR0913
447486
)
448487

449488

489+
def new_data_artifact_update_event( # noqa: PLR0913
490+
task_id: str,
491+
context_id: str,
492+
name: str,
493+
data: Any,
494+
media_type: str | None = None,
495+
append: bool = False,
496+
last_chunk: bool = False,
497+
artifact_id: str | None = None,
498+
) -> TaskArtifactUpdateEvent:
499+
"""Creates a TaskArtifactUpdateEvent with a single data artifact.
500+
501+
Args:
502+
task_id: The task ID.
503+
context_id: The context ID.
504+
name: The name of the artifact.
505+
data: JSON-serializable data to embed (dict, list, str, etc.).
506+
media_type: Optional MIME type of the part content.
507+
append: Whether to append to the existing artifact.
508+
last_chunk: Whether this is the last chunk.
509+
artifact_id: Optional artifact ID (auto-generated if not provided).
510+
511+
Returns:
512+
A TaskArtifactUpdateEvent with a single data artifact.
513+
"""
514+
return TaskArtifactUpdateEvent(
515+
task_id=task_id,
516+
context_id=context_id,
517+
artifact=new_data_artifact(
518+
name=name,
519+
data=data,
520+
media_type=media_type,
521+
artifact_id=artifact_id,
522+
),
523+
append=append,
524+
last_chunk=last_chunk,
525+
)
526+
527+
528+
def new_raw_artifact_update_event( # noqa: PLR0913
529+
task_id: str,
530+
context_id: str,
531+
name: str,
532+
raw: bytes,
533+
media_type: str | None = None,
534+
filename: str | None = None,
535+
append: bool = False,
536+
last_chunk: bool = False,
537+
artifact_id: str | None = None,
538+
) -> TaskArtifactUpdateEvent:
539+
"""Creates a TaskArtifactUpdateEvent with a single raw bytes artifact.
540+
541+
Args:
542+
task_id: The task ID.
543+
context_id: The context ID.
544+
name: The name of the artifact.
545+
raw: The raw bytes content.
546+
media_type: Optional MIME type (e.g. 'image/png').
547+
filename: Optional filename.
548+
append: Whether to append to the existing artifact.
549+
last_chunk: Whether this is the last chunk.
550+
artifact_id: Optional artifact ID (auto-generated if not provided).
551+
552+
Returns:
553+
A TaskArtifactUpdateEvent with a single raw artifact.
554+
"""
555+
return TaskArtifactUpdateEvent(
556+
task_id=task_id,
557+
context_id=context_id,
558+
artifact=new_raw_artifact(
559+
name=name,
560+
raw=raw,
561+
media_type=media_type,
562+
filename=filename,
563+
artifact_id=artifact_id,
564+
),
565+
append=append,
566+
last_chunk=last_chunk,
567+
)
568+
569+
570+
def new_url_artifact_update_event( # noqa: PLR0913
571+
task_id: str,
572+
context_id: str,
573+
name: str,
574+
url: str,
575+
media_type: str | None = None,
576+
filename: str | None = None,
577+
append: bool = False,
578+
last_chunk: bool = False,
579+
artifact_id: str | None = None,
580+
) -> TaskArtifactUpdateEvent:
581+
"""Creates a TaskArtifactUpdateEvent with a single URL artifact.
582+
583+
Args:
584+
task_id: The task ID.
585+
context_id: The context ID.
586+
name: The name of the artifact.
587+
url: The URL pointing to the file content.
588+
media_type: Optional MIME type (e.g. 'image/png').
589+
filename: Optional filename.
590+
append: Whether to append to the existing artifact.
591+
last_chunk: Whether this is the last chunk.
592+
artifact_id: Optional artifact ID (auto-generated if not provided).
593+
594+
Returns:
595+
A TaskArtifactUpdateEvent with a single URL artifact.
596+
"""
597+
return TaskArtifactUpdateEvent(
598+
task_id=task_id,
599+
context_id=context_id,
600+
artifact=new_url_artifact(
601+
name=name,
602+
url=url,
603+
media_type=media_type,
604+
filename=filename,
605+
artifact_id=artifact_id,
606+
),
607+
append=append,
608+
last_chunk=last_chunk,
609+
)
610+
611+
450612
def get_stream_response_text(
451613
response: StreamResponse, delimiter: str = '\n'
452614
) -> str:

tests/client/test_auth_interceptor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
import pytest
1010
import respx
1111

12-
from google.protobuf import json_format
13-
1412
from a2a.client import (
1513
AuthInterceptor,
1614
Client,
@@ -39,6 +37,7 @@
3937
StringList,
4038
)
4139
from a2a.utils.constants import TransportProtocol
40+
from google.protobuf import json_format
4241

4342

4443
def build_success_response(request: httpx.Request) -> httpx.Response:

0 commit comments

Comments
 (0)