Skip to content

refactor!: Make the FastAPI and Starlette dependencies optional #217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
95523a3
refactor!: Make the FastAPI dependency optional
darkhaniop Jun 19, 2025
3661aa8
Ignore name redef warnings on ImportError
darkhaniop Jun 19, 2025
a74f5e5
Update .github/actions/spelling/allow.txt
darkhaniop Jun 19, 2025
012ea77
Updates the installation instructions in README.md
darkhaniop Jun 19, 2025
d29c63a
Check FastAPI package on A2AFastAPIApplication()
darkhaniop Jun 20, 2025
800a29a
Update A2AFastAPIApplication.__init__ docstring
darkhaniop Jun 20, 2025
2af0be8
Merge branch 'main' into make-fastapi-package-optional
darkhaniop Jun 21, 2025
1d06e63
Regenerate uv.lock
darkhaniop Jun 21, 2025
abfa743
Merge branch 'main' into make-fastapi-package-optional
holtskinner Jun 23, 2025
fe26fa1
Refactor to make starlette optional
holtskinner Jun 23, 2025
17766ae
Fix mypy errors
holtskinner Jun 23, 2025
46e2a5c
Formatting
holtskinner Jun 23, 2025
61c5ca7
Remove uvlock
holtskinner Jun 24, 2025
1678fd1
Merge branch 'main' of https://github.com/google-a2a/a2a-python into …
holtskinner Jun 24, 2025
24c98e5
Recreate uv.lock
holtskinner Jun 24, 2025
16059fb
Remove extra check for FastAPI
holtskinner Jun 24, 2025
c2f3454
Spelling
holtskinner Jun 24, 2025
267bc4b
Merge branch 'main' into make-fastapi-package-optional
darkhaniop Jun 30, 2025
d1895af
Remove imports from starlette outside try-except
darkhaniop Jun 30, 2025
2d214f1
Split starlette and fastapi imports and error msg
darkhaniop Jul 1, 2025
acdd67b
Merge branch 'main' into make-fastapi-package-optional
darkhaniop Jul 1, 2025
c73492c
Merge branch 'main' into make-fastapi-package-optional
holtskinner Jul 1, 2025
7e159cd
Merge branch 'main' into make-fastapi-package-optional
darkhaniop Jul 2, 2025
975e865
Add tests for initiation of apps with missing deps
darkhaniop Jul 2, 2025
14d2db5
Merge branch 'main' into make-fastapi-package-optional
darkhaniop Jul 3, 2025
804a1f4
Merge branch 'main' into make-fastapi-package-optional
holtskinner Jul 7, 2025
3e1df0a
Formatting/tests
holtskinner Jul 7, 2025
180898b
Merge branch 'main' into make-fastapi-package-optional
holtskinner Jul 8, 2025
e8233d1
Merge branch 'main' into make-fastapi-package-optional
darkhaniop Jul 15, 2025
3f48966
Merge branch 'main' into make-fastapi-package-optional
holtskinner Jul 15, 2025
a65770f
Re-create uv lock
holtskinner Jul 15, 2025
aaacec1
Merge branch 'main' into make-fastapi-package-optional
holtskinner Jul 17, 2025
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
1 change: 1 addition & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ protoc
pyi
pypistats
pyversions
redef
respx
resub
socio
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ When you're working within a uv project or a virtual environment managed by uv,
uv add a2a-sdk
```

To include the optional HTTP server components (FastAPI, Starlette), install the `http-server` extra:

```bash
uv add a2a-sdk[http-server]
```

To install with database support:

```bash
Expand All @@ -57,6 +63,12 @@ If you prefer to use pip, the standard Python package installer, you can install
pip install a2a-sdk
```

To include the optional HTTP server components (FastAPI, Starlette), install the `http-server` extra:

```bash
pip install a2a-sdk[http-server]
```

To install with database support:

```bash
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ authors = [{ name = "Google LLC", email = "googleapis-packages@google.com" }]
requires-python = ">=3.10"
keywords = ["A2A", "A2A SDK", "A2A Protocol", "Agent2Agent", "Agent 2 Agent"]
dependencies = [
"fastapi>=0.115.2",
"httpx>=0.28.1",
"httpx-sse>=0.4.0",
"google-api-core>=1.26.0",
"opentelemetry-api>=1.33.0",
"opentelemetry-sdk>=1.33.0",
"pydantic>=2.11.3",
"sse-starlette",
"starlette",
"grpcio>=1.60",
"grpcio-tools>=1.60",
"grpcio_reflection>=1.7.0",
Expand All @@ -37,6 +34,7 @@ classifiers = [
]

[project.optional-dependencies]
http-server = ["fastapi>=0.115.2", "sse-starlette", "starlette"]
postgresql = ["sqlalchemy[asyncio,postgresql-asyncpg]>=2.0.0"]
mysql = ["sqlalchemy[asyncio,aiomysql]>=2.0.0"]
sqlite = ["sqlalchemy[asyncio,aiosqlite]>=2.0.0"]
Expand Down Expand Up @@ -92,6 +90,9 @@ dev = [
"types-protobuf",
"types-requests",
"pre-commit",
"fastapi>=0.115.2",
"sse-starlette",
"starlette",
]

[[tool.uv.index]]
Expand Down
25 changes: 22 additions & 3 deletions src/a2a/server/apps/jsonrpc/fastapi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
from typing import TYPE_CHECKING, Any

from fastapi import FastAPI

if TYPE_CHECKING:
from fastapi import FastAPI

_package_fastapi_installed = True
else:
try:
from fastapi import FastAPI

_package_fastapi_installed = True
except ImportError:
FastAPI = Any

_package_fastapi_installed = False

from a2a.server.apps.jsonrpc.jsonrpc_app import (
CallContextBuilder,
Expand Down Expand Up @@ -37,7 +50,7 @@ def __init__(
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
) -> None:
"""Initializes the A2AStarletteApplication.
"""Initializes the A2AFastAPIApplication.

Args:
agent_card: The AgentCard describing the agent's capabilities.
Expand All @@ -49,6 +62,12 @@ def __init__(
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
"""
if not _package_fastapi_installed:
raise ImportError(
'The `fastapi` package is required to use the `A2AFastAPIApplication`.'
' It can be added as a part of `a2a-sdk` optional dependencies,'
' `a2a-sdk[http-server]`.'
)
super().__init__(
agent_card=agent_card,
http_handler=http_handler,
Expand Down
54 changes: 44 additions & 10 deletions src/a2a/server/apps/jsonrpc/jsonrpc_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,9 @@

from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator
from typing import Any
from typing import TYPE_CHECKING, Any

from fastapi import FastAPI
from pydantic import ValidationError
from sse_starlette.sse import EventSourceResponse
from starlette.applications import Starlette
from starlette.authentication import BaseUser
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE

from a2a.auth.user import UnauthenticatedUser
from a2a.auth.user import User as A2AUser
Expand Down Expand Up @@ -54,6 +46,42 @@

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from fastapi import FastAPI
from sse_starlette.sse import EventSourceResponse
from starlette.applications import Starlette
from starlette.authentication import BaseUser
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE

_package_starlette_installed = True
else:
FastAPI = Any
try:
from sse_starlette.sse import EventSourceResponse
from starlette.applications import Starlette
from starlette.authentication import BaseUser
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE

_package_starlette_installed = True
except ImportError:
_package_starlette_installed = False
# Provide placeholder types for runtime type hinting when dependencies are not installed.
# These will not be used if the code path that needs them is guarded by _http_server_installed.
EventSourceResponse = Any
Starlette = Any
BaseUser = Any
HTTPException = Any
Request = Any
JSONResponse = Any
Response = Any
HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any


class StarletteUserProxy(A2AUser):
"""Adapts the Starlette User class to the A2A user representation."""
Expand Down Expand Up @@ -117,7 +145,7 @@ def __init__(
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
) -> None:
"""Initializes the A2AStarletteApplication.
"""Initializes the JSONRPCApplication.

Args:
agent_card: The AgentCard describing the agent's capabilities.
Expand All @@ -129,6 +157,12 @@ def __init__(
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
"""
if not _package_starlette_installed:
raise ImportError(
'Packages `starlette` and `sse-starlette` are required to use the'
' `JSONRPCApplication`. They can be added as a part of `a2a-sdk`'
' optional dependencies, `a2a-sdk[http-server]`.'
)
self.agent_card = agent_card
self.extended_agent_card = extended_agent_card
self.handler = JSONRPCHandler(
Expand Down
28 changes: 25 additions & 3 deletions src/a2a/server/apps/jsonrpc/starlette_app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import logging

from typing import Any
from typing import TYPE_CHECKING, Any

from starlette.applications import Starlette
from starlette.routing import Route

if TYPE_CHECKING:
from starlette.applications import Starlette
from starlette.routing import Route

_package_starlette_installed = True

else:
try:
from starlette.applications import Starlette
from starlette.routing import Route

_package_starlette_installed = True
except ImportError:
Starlette = Any
Route = Any

_package_starlette_installed = False

from a2a.server.apps.jsonrpc.jsonrpc_app import (
CallContextBuilder,
Expand Down Expand Up @@ -48,6 +64,12 @@ def __init__(
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
"""
if not _package_starlette_installed:
raise ImportError(
'Packages `starlette` and `sse-starlette` are required to use the'
' `A2AStarletteApplication`. It can be added as a part of `a2a-sdk`'
' optional dependencies, `a2a-sdk[http-server]`.'
)
super().__init__(
agent_card=agent_card,
http_handler=http_handler,
Expand Down
80 changes: 80 additions & 0 deletions tests/server/apps/jsonrpc/test_fastapi_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Any
from unittest.mock import MagicMock

import pytest

from a2a.server.apps.jsonrpc import fastapi_app
from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication
from a2a.server.request_handlers.request_handler import (
RequestHandler, # For mock spec
)
from a2a.types import AgentCard # For mock spec


# --- A2AFastAPIApplication Tests ---


class TestA2AFastAPIApplicationOptionalDeps:
# Running tests in this class requires the optional dependency fastapi to be
# present in the test environment.

@pytest.fixture(scope='class', autouse=True)
def ensure_pkg_fastapi_is_present(self):
try:
import fastapi as _fastapi # noqa: F401
except ImportError:
pytest.fail(
f'Running tests in {self.__class__.__name__} requires'
' the optional dependency fastapi to be present in the test'
' environment. Run `uv sync --dev ...` before running the test'
' suite.'
)

@pytest.fixture(scope='class')
def mock_app_params(self) -> dict:
# Mock http_handler
mock_handler = MagicMock(spec=RequestHandler)
# Mock agent_card with essential attributes accessed in __init__
mock_agent_card = MagicMock(spec=AgentCard)
# Ensure 'url' attribute exists on the mock_agent_card, as it's accessed
# in __init__
mock_agent_card.url = 'http://example.com'
# Ensure 'supportsAuthenticatedExtendedCard' attribute exists
mock_agent_card.supportsAuthenticatedExtendedCard = False
return dict(agent_card=mock_agent_card, http_handler=mock_handler)

@pytest.fixture(scope='class')
def mark_pkg_fastapi_not_installed(self):
pkg_fastapi_installed_flag = fastapi_app._package_fastapi_installed
fastapi_app._package_fastapi_installed = False
yield
fastapi_app._package_fastapi_installed = pkg_fastapi_installed_flag

def test_create_a2a_fastapi_app_with_present_deps_succeeds(
self, mock_app_params: dict
):
try:
_app = A2AFastAPIApplication(**mock_app_params)
except ImportError:
pytest.fail(
'With the fastapi package present, creating a'
' A2AFastAPIApplication instance should not raise ImportError'
)

def test_create_a2a_fastapi_app_with_missing_deps_raises_importerror(
self,
mock_app_params: dict,
mark_pkg_fastapi_not_installed: Any,
):
with pytest.raises(
ImportError,
match=(
'The `fastapi` package is required to use the'
' `A2AFastAPIApplication`'
),
):
_app = A2AFastAPIApplication(**mock_app_params)


if __name__ == '__main__':
pytest.main([__file__])
Loading
Loading