Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
a2a

Check warning on line 1 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
A2A

Check warning on line 2 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
A2AFastAPI

Check warning on line 3 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
AAgent
Expand Down Expand Up @@ -27,7 +27,7 @@
AUser
autouse
backticks
base64url

Check warning on line 30 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
buf
bufbuild
cla
Expand All @@ -42,7 +42,7 @@
drivername
DSNs
dunders
ES256

Check warning on line 45 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
euo
EUR
evt
Expand All @@ -57,8 +57,8 @@
gle
GVsb
hazmat
HS256

Check warning on line 60 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
HS384

Check warning on line 61 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
ietf
importlib
initdb
Expand Down Expand Up @@ -97,10 +97,12 @@
Oneof
OpenAPI
openapiv
openapiv2

Check warning on line 100 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
opensource
otherurl
pb2

Check warning on line 103 in .github/actions/spelling/allow.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Ignoring entry because it contains non-alpha characters (non-alpha-in-dictionary)
podman
Podman
poolclass
postgres
POSTGRES
Expand Down Expand Up @@ -128,6 +130,8 @@
sse
starlette
Starlette
subgids
subuids
sut
SUT
swagger
Expand All @@ -139,9 +143,6 @@
TResponse
typ
typeerror
vulnz
Podman
podman
UIDs
subuids
subgids
vulnz
whl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: Minimal Install Smoke Test
name: Install Smoke Test
on:
push:
branches: [main, 1.0-dev]
Expand Down Expand Up @@ -30,13 +30,18 @@ permissions:
contents: read

jobs:
minimal-install:
name: Verify base-only install
install-smoke:
name: Verify ${{ matrix.profile.name }} install
runs-on: ubuntu-latest
if: github.repository == 'a2aproject/a2a-python'
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
profile:
- name: base
extras: ''
- name: http-server
extras: '[http-server]'
steps:
- name: Checkout code
uses: actions/checkout@v6
Expand All @@ -49,15 +54,17 @@ jobs:
- name: Build package
run: uv build --wheel

- name: Install with base dependencies only
- name: Install with ${{ matrix.profile.name }} dependencies only
run: |
uv venv .venv-minimal
# Install only the built wheel -- no extras, no dev deps.
# This simulates what an end-user gets with `pip install a2a-sdk`.
VIRTUAL_ENV=.venv-minimal uv pip install dist/*.whl
uv venv .venv-smoke
# Install only the built wheel + the profile's extras -- no
# dev deps. This simulates what an end-user gets with
# `pip install a2a-sdk${{ matrix.profile.extras }}`.
WHEEL=$(ls dist/*.whl)
VIRTUAL_ENV=.venv-smoke uv pip install "${WHEEL}${{ matrix.profile.extras }}"

- name: List installed packages
run: VIRTUAL_ENV=.venv-minimal uv pip list
run: VIRTUAL_ENV=.venv-smoke uv pip list

- name: Run import smoke test
run: .venv-minimal/bin/python scripts/test_minimal_install.py
run: .venv-smoke/bin/python scripts/test_install_smoke.py ${{ matrix.profile.name }}
55 changes: 45 additions & 10 deletions scripts/test_minimal_install.py → scripts/test_install_smoke.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
#!/usr/bin/env python3
"""Smoke test for minimal (base-only) installation of a2a-sdk.
"""Smoke test for installations of a2a-sdk with various extras.

This script verifies that all core public API modules can be imported
when only the base dependencies are installed (no optional extras).
This script verifies that the public API modules associated with a
given installation profile can be imported without pulling in modules
that belong to other (uninstalled) optional extras.

It is designed to run WITHOUT pytest or any dev dependencies -- just
a clean venv with `pip install a2a-sdk`.
a clean venv with `pip install a2a-sdk[<profile>]`.

Usage:
python scripts/test_minimal_install.py
python scripts/test_install_smoke.py [profile]

profile defaults to "base" and selects which set of modules to
smoke-test. Available profiles:
base -- `pip install a2a-sdk`
http-server -- `pip install a2a-sdk[http-server]`

Exit codes:
0 - All core imports succeeded
1 - One or more core imports failed
0 - All imports for the profile succeeded
1 - One or more imports failed
"""

from __future__ import annotations
Expand Down Expand Up @@ -58,19 +64,48 @@
'a2a.helpers.proto_helpers',
]

# Modules that MUST be importable with only the base + `http-server`
# extras installed (no `grpc`, `sql`, `signing`, `telemetry`, etc.).
#
# A user building a Starlette/FastAPI A2A server with
# `pip install a2a-sdk[http-server]` should be able to import these
# without the gRPC stack being present on the system.
HTTP_SERVER_MODULES = [
'a2a.server.routes',
'a2a.server.routes.agent_card_routes',
'a2a.server.routes.common',
'a2a.server.routes.jsonrpc_dispatcher',
'a2a.server.routes.jsonrpc_routes',
'a2a.server.routes.rest_dispatcher',
'a2a.server.routes.rest_routes',
]


PROFILES: dict[str, list[str]] = {
'base': CORE_MODULES,
'http-server': CORE_MODULES + HTTP_SERVER_MODULES,
}


def main() -> int:
profile = sys.argv[1] if len(sys.argv) > 1 else 'base'
if profile not in PROFILES:
print(f'Unknown profile {profile!r}. Available: {sorted(PROFILES)}')
return 1

modules = PROFILES[profile]
failures: list[str] = []
successes: list[str] = []

for module_name in CORE_MODULES:
for module_name in modules:
try:
importlib.import_module(module_name)
successes.append(module_name)
except Exception as e: # noqa: BLE001, PERF203
failures.append(f'{module_name}: {e}')

print(f'Tested {len(CORE_MODULES)} core modules')
print(f'Profile: {profile}')
print(f'Tested {len(modules)} modules')
print(f' Passed: {len(successes)}')
print(f' Failed: {len(failures)}')

Expand All @@ -80,7 +115,7 @@ def main() -> int:
print(f' - {failure}')
return 1

print('\nAll core modules imported successfully.')
print('\nAll modules imported successfully.')
return 0


Expand Down
98 changes: 98 additions & 0 deletions scripts/test_install_smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/bin/bash
# Local equivalent of .github/workflows/install-smoke.yml.
#
# For each install profile, builds the wheel and installs it into a
# clean venv (no dev deps), then runs the import smoke test for that
# profile. By default runs every known profile; pass a profile name
# to run just one.
#
# Available profiles (must match those in scripts/test_install_smoke.py):
# base -- `pip install a2a-sdk`
# http-server -- `pip install a2a-sdk[http-server]`
#
# Usage:
# scripts/test_install_smoke.sh [profile] [python-version]
#
# Examples:
# scripts/test_install_smoke.sh # all profiles, default python
# scripts/test_install_smoke.sh '' 3.13 # all profiles on python 3.13
# scripts/test_install_smoke.sh http-server # http-server only
# scripts/test_install_smoke.sh http-server 3.13 # http-server on python 3.13
set -e
set -o pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"

ALL_PROFILES=(base http-server)

PROFILE_ARG="${1:-}"
PYTHON_VERSION="${2:-}"

if [ -z "$PROFILE_ARG" ]; then
PROFILES=("${ALL_PROFILES[@]}")
else
PROFILES=("$PROFILE_ARG")
fi

extras_for_profile() {
case "$1" in
base) echo "" ;;
http-server) echo "[http-server]" ;;
*)
echo "Unknown profile '$1'. Available: ${ALL_PROFILES[*]}" >&2
return 1
;;
esac
}

# Validate profiles up-front so we fail fast.
for profile in "${PROFILES[@]}"; do
extras_for_profile "$profile" >/dev/null
done

echo "--- Building wheel ---"
rm -rf dist
uv build --wheel
WHEEL=$(ls dist/*.whl)
Comment thread
ishymko marked this conversation as resolved.

FAILED_PROFILES=()

for profile in "${PROFILES[@]}"; do
extras=$(extras_for_profile "$profile")
venv_dir=".venv-smoke-${profile}"

echo
echo "=================================================================="
echo " Profile: $profile (extras='$extras')"
echo "=================================================================="

echo "--- Creating clean venv at $venv_dir ---"
rm -rf "$venv_dir"
if [ -n "$PYTHON_VERSION" ]; then
uv venv "$venv_dir" --python "$PYTHON_VERSION"
else
uv venv "$venv_dir"
fi

echo "--- Installing built wheel with '$profile' dependencies only ---"
VIRTUAL_ENV="$venv_dir" uv pip install "${WHEEL}${extras}"

echo "--- Installed packages ---"
VIRTUAL_ENV="$venv_dir" uv pip list

echo "--- Running import smoke test ---"
if ! "$venv_dir/bin/python" scripts/test_install_smoke.py "$profile"; then
FAILED_PROFILES+=("$profile")
fi
done

echo
echo "=================================================================="
if [ ${#FAILED_PROFILES[@]} -eq 0 ]; then
echo " All profiles passed: ${PROFILES[*]}"
exit 0
fi

echo " Failed profiles: ${FAILED_PROFILES[*]}" >&2
exit 1
15 changes: 5 additions & 10 deletions src/a2a/compat/v0_3/context_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,26 @@
adapters wrap the default builders with these classes to recognize both names.
"""

from typing import TYPE_CHECKING, Any

import grpc
from typing import TYPE_CHECKING

from a2a.compat.v0_3.extension_headers import LEGACY_HTTP_EXTENSION_HEADER
from a2a.extensions.common import get_requested_extensions
from a2a.server.context import ServerCallContext


if TYPE_CHECKING:
import grpc

from starlette.requests import Request

from a2a.server.request_handlers.grpc_handler import (
GrpcServerCallContextBuilder,
)
from a2a.server.routes.common import ServerCallContextBuilder
else:
try:
from starlette.requests import Request
except ImportError:
Request = Any


def _get_legacy_grpc_extensions(
context: grpc.aio.ServicerContext,
context: 'grpc.aio.ServicerContext',
) -> list[str]:
md = context.invocation_metadata()
if md is None:
Expand Down Expand Up @@ -71,7 +66,7 @@ class V03GrpcServerCallContextBuilder:
def __init__(self, inner: 'GrpcServerCallContextBuilder') -> None:
self._inner = inner

def build(self, context: grpc.aio.ServicerContext) -> ServerCallContext:
def build(self, context: 'grpc.aio.ServicerContext') -> ServerCallContext:
"""Builds a ServerCallContext, merging legacy extension metadata."""
server_context = self._inner.build(context)
server_context.requested_extensions |= get_requested_extensions(
Expand Down
Loading