chore: Drop Python 3.9 support, set minimum to Python 3.10

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 839799108
This commit is contained in:
George Weale
2025-12-03 09:56:22 -08:00
committed by Copybara-Service
parent e02b9fb608
commit 8c9105bf14
40 changed files with 290 additions and 815 deletions
+4 -12
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Checkout code
@@ -48,14 +48,6 @@ jobs:
- name: Run unit tests with pytest
run: |
source .venv/bin/activate
if [[ "${{ matrix.python-version }}" == "3.9" ]]; then
pytest tests/unittests \
--ignore=tests/unittests/a2a \
--ignore=tests/unittests/tools/mcp_tool \
--ignore=tests/unittests/artifacts/test_artifact_service.py \
--ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
else
pytest tests/unittests \
--ignore=tests/unittests/artifacts/test_artifact_service.py \
--ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
fi
pytest tests/unittests \
--ignore=tests/unittests/artifacts/test_artifact_service.py \
--ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
@@ -21,7 +21,7 @@ prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# If specified, requires the python>=3.10 and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
+16 -21
View File
@@ -13,6 +13,7 @@
# limitations under the License.
import asyncio
from contextlib import aclosing
import os
import time
@@ -46,19 +47,16 @@ async def main():
role='user', parts=[types.Part.from_text(text=new_message)]
)
print('** User says:', content.model_dump(exclude_none=True))
# TODO - migrate try...finally to contextlib.aclosing after Python 3.9 is
# no longer supported.
agen = runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
)
try:
async with aclosing(
runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
)
) as agen:
async for event in agen:
if event.content.parts and event.content.parts[0].text:
print(f'** {event.author}: {event.content.parts[0].text}')
finally:
await agen.aclose()
async def run_prompt_bytes(session: Session, new_message: str):
content = types.Content(
@@ -70,20 +68,17 @@ async def main():
],
)
print('** User says:', content.model_dump(exclude_none=True))
# TODO - migrate try...finally to contextlib.aclosing after Python 3.9 is
# no longer supported.
agen = runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
run_config=RunConfig(save_input_blobs_as_artifacts=True),
)
try:
async with aclosing(
runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
run_config=RunConfig(save_input_blobs_as_artifacts=True),
)
) as agen:
async for event in agen:
if event.content.parts and event.content.parts[0].text:
print(f'** {event.author}: {event.content.parts[0].text}')
finally:
await agen.aclose()
start_time = time.time()
print('Start time:', start_time)
+3 -3
View File
@@ -5620,7 +5620,7 @@ pip install google-cloud-aiplatform[adk,agent_engines]
```
!!!info
Agent Engine only supported Python version >=3.9 and <=3.12.
Agent Engine only supported Python version >=3.10 and <=3.12.
### Initialization
@@ -8073,7 +8073,7 @@ setting up a basic agent with multiple tools, and running it locally either in t
<!-- <img src="../../assets/quickstart.png" alt="Quickstart setup"> -->
This quickstart assumes a local IDE (VS Code, PyCharm, IntelliJ IDEA, etc.)
with Python 3.9+ or Java 17+ and terminal access. This method runs the
with Python 3.10+ or Java 17+ and terminal access. This method runs the
application entirely on your machine and is recommended for internal development.
## 1. Set up Environment & Install ADK {#venv-install}
@@ -16475,7 +16475,7 @@ This guide covers two primary integration patterns:
Before you begin, ensure you have the following set up:
* **Set up ADK:** Follow the standard ADK [setup instructions](../get-started/quickstart.md/#venv-install) in the quickstart.
* **Install/update Python/Java:** MCP requires Python version of 3.9 or higher for Python or Java 17+.
* **Install/update Python/Java:** MCP requires Python version of 3.10 or higher for Python or Java 17+.
* **Setup Node.js and npx:** **(Python only)** Many community MCP servers are distributed as Node.js packages and run using `npx`. Install Node.js (which includes npx) if you haven't already. For details, see [https://nodejs.org/en](https://nodejs.org/en).
* **Verify Installations:** **(Python only)** Confirm `adk` and `npx` are in your PATH within the activated virtual environment:
@@ -26,23 +26,11 @@ from typing import List
from typing import Optional
from typing import Union
from .utils import _get_adk_metadata_key
try:
from a2a import types as a2a_types
except ImportError as e:
import sys
if sys.version_info < (3, 10):
raise ImportError(
'A2A requires Python 3.10 or above. Please upgrade your Python version.'
) from e
else:
raise e
from a2a import types as a2a_types
from google.genai import types as genai_types
from ..experimental import a2a_experimental
from .utils import _get_adk_metadata_key
logger = logging.getLogger('google_adk.' + __name__)
@@ -15,23 +15,12 @@
from __future__ import annotations
from collections.abc import Callable
import sys
from typing import Any
from typing import Optional
from pydantic import BaseModel
try:
from a2a.server.agent_execution import RequestContext
except ImportError as e:
if sys.version_info < (3, 10):
raise ImportError(
'A2A requires Python 3.10 or above. Please upgrade your Python version.'
) from e
else:
raise e
from a2a.server.agent_execution import RequestContext
from google.genai import types as genai_types
from pydantic import BaseModel
from ...runners import RunConfig
from ..experimental import a2a_experimental
@@ -23,34 +23,22 @@ from typing import Callable
from typing import Optional
import uuid
from ...utils.context_utils import Aclosing
try:
from a2a.server.agent_execution import AgentExecutor
from a2a.server.agent_execution.context import RequestContext
from a2a.server.events.event_queue import EventQueue
from a2a.types import Artifact
from a2a.types import Message
from a2a.types import Role
from a2a.types import TaskArtifactUpdateEvent
from a2a.types import TaskState
from a2a.types import TaskStatus
from a2a.types import TaskStatusUpdateEvent
from a2a.types import TextPart
except ImportError as e:
import sys
if sys.version_info < (3, 10):
raise ImportError(
'A2A requires Python 3.10 or above. Please upgrade your Python version.'
) from e
else:
raise e
from a2a.server.agent_execution import AgentExecutor
from a2a.server.agent_execution.context import RequestContext
from a2a.server.events.event_queue import EventQueue
from a2a.types import Artifact
from a2a.types import Message
from a2a.types import Role
from a2a.types import TaskArtifactUpdateEvent
from a2a.types import TaskState
from a2a.types import TaskStatus
from a2a.types import TaskStatusUpdateEvent
from a2a.types import TextPart
from google.adk.runners import Runner
from pydantic import BaseModel
from typing_extensions import override
from ...utils.context_utils import Aclosing
from ..converters.event_converter import AdkEventToA2AEventsConverter
from ..converters.event_converter import convert_event_to_a2a_events
from ..converters.part_converter import A2APartToGenAIPartConverter
+5 -15
View File
@@ -15,25 +15,15 @@
from __future__ import annotations
import re
import sys
from typing import Dict
from typing import List
from typing import Optional
try:
from a2a.types import AgentCapabilities
from a2a.types import AgentCard
from a2a.types import AgentProvider
from a2a.types import AgentSkill
from a2a.types import SecurityScheme
except ImportError as e:
if sys.version_info < (3, 10):
raise ImportError(
'A2A requires Python 3.10 or above. Please upgrade your Python version.'
) from e
else:
raise e
from a2a.types import AgentCapabilities
from a2a.types import AgentCard
from a2a.types import AgentProvider
from a2a.types import AgentSkill
from a2a.types import SecurityScheme
from ...agents.base_agent import BaseAgent
from ...agents.llm_agent import LlmAgent
+6 -17
View File
@@ -15,30 +15,18 @@
from __future__ import annotations
import logging
import sys
try:
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCard
except ImportError as e:
if sys.version_info < (3, 10):
raise ImportError(
"A2A requires Python 3.10 or above. Please upgrade your Python version."
) from e
else:
raise e
from typing import Optional
from typing import Union
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCard
from starlette.applications import Starlette
from ...agents.base_agent import BaseAgent
from ...artifacts.in_memory_artifact_service import InMemoryArtifactService
from ...auth.credential_service.in_memory_credential_service import InMemoryCredentialService
from ...cli.utils.logs import setup_adk_logger
from ...memory.in_memory_memory_service import InMemoryMemoryService
from ...runners import Runner
from ...sessions.in_memory_session_service import InMemorySessionService
@@ -117,7 +105,8 @@ def to_a2a(
app = to_a2a(agent, agent_card=my_custom_agent_card)
"""
# Set up ADK logging to ensure logs are visible when using uvicorn directly
setup_adk_logger(logging.INFO)
adk_logger = logging.getLogger("google_adk")
adk_logger.setLevel(logging.INFO)
async def create_runner() -> Runner:
"""Create a runner for the agent."""
+2 -16
View File
@@ -12,9 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import sys
from .base_agent import BaseAgent
from .invocation_context import InvocationContext
from .live_request_queue import LiveRequest
@@ -22,6 +19,7 @@ from .live_request_queue import LiveRequestQueue
from .llm_agent import Agent
from .llm_agent import LlmAgent
from .loop_agent import LoopAgent
from .mcp_instruction_provider import McpInstructionProvider
from .parallel_agent import ParallelAgent
from .run_config import RunConfig
from .sequential_agent import SequentialAgent
@@ -31,6 +29,7 @@ __all__ = [
'BaseAgent',
'LlmAgent',
'LoopAgent',
'McpInstructionProvider',
'ParallelAgent',
'SequentialAgent',
'InvocationContext',
@@ -38,16 +37,3 @@ __all__ = [
'LiveRequestQueue',
'RunConfig',
]
if sys.version_info < (3, 10):
logger = logging.getLogger('google_adk.' + __name__)
logger.warning(
'MCP requires Python 3.10 or above. Please upgrade your Python'
' version in order to use it.'
)
else:
from .mcp_instruction_provider import McpInstructionProvider
__all__.extend([
'McpInstructionProvider',
])
@@ -22,24 +22,12 @@ from typing import Any
from typing import Dict
from typing import TextIO
from mcp import types
from ..tools.mcp_tool.mcp_session_manager import MCPSessionManager
from .llm_agent import InstructionProvider
from .readonly_context import ReadonlyContext
# Attempt to import MCP Session Manager from the MCP library, and hints user to
# upgrade their Python version to 3.10 if it fails.
try:
from mcp import types
from ..tools.mcp_tool.mcp_session_manager import MCPSessionManager
except ImportError as e:
if sys.version_info < (3, 10):
raise ImportError(
"MCP Session Manager requires Python 3.10 or above. Please upgrade"
" your Python version."
) from e
else:
raise e
class McpInstructionProvider(InstructionProvider):
"""Fetches agent instructions from an MCP server."""
+41 -55
View File
@@ -48,19 +48,54 @@ def _create_branch_ctx_for_sub_agent(
return invocation_context
async def _merge_agent_run(
agent_runs: list[AsyncGenerator[Event, None]],
) -> AsyncGenerator[Event, None]:
"""Merges agent runs using asyncio.TaskGroup on Python 3.11+."""
sentinel = object()
queue = asyncio.Queue()
# Agents are processed in parallel.
# Events for each agent are put on queue sequentially.
async def process_an_agent(events_for_one_agent):
try:
async for event in events_for_one_agent:
resume_signal = asyncio.Event()
await queue.put((event, resume_signal))
# Wait for upstream to consume event before generating new events.
await resume_signal.wait()
finally:
# Mark agent as finished.
await queue.put((sentinel, None))
async with asyncio.TaskGroup() as tg:
for events_for_one_agent in agent_runs:
tg.create_task(process_an_agent(events_for_one_agent))
sentinel_count = 0
# Run until all agents finished processing.
while sentinel_count < len(agent_runs):
event, resume_signal = await queue.get()
# Agent finished processing.
if event is sentinel:
sentinel_count += 1
else:
yield event
# Signal to agent that it should generate next event.
resume_signal.set()
# TODO - remove once Python <3.11 is no longer supported.
async def _merge_agent_run_pre_3_11(
agent_runs: list[AsyncGenerator[Event, None]],
) -> AsyncGenerator[Event, None]:
"""Merges the agent run event generator.
This version works in Python 3.9 and 3.10 and uses custom replacement for
asyncio.TaskGroup for tasks cancellation and exception handling.
"""Merges agent runs for Python 3.10 without asyncio.TaskGroup.
This implementation guarantees for each agent, it won't move on until the
generated event is processed by upstream runner.
Uses custom cancellation and exception handling to mirror TaskGroup
semantics. Each agent waits until the runner processes emitted events.
Args:
agent_runs: A list of async generators that yield events from each agent.
agent_runs: Async generators that yield events from each agent.
Yields:
Event: The next event from the merged generator.
@@ -112,53 +147,6 @@ async def _merge_agent_run_pre_3_11(
task.cancel()
async def _merge_agent_run(
agent_runs: list[AsyncGenerator[Event, None]],
) -> AsyncGenerator[Event, None]:
"""Merges the agent run event generator.
This implementation guarantees for each agent, it won't move on until the
generated event is processed by upstream runner.
Args:
agent_runs: A list of async generators that yield events from each agent.
Yields:
Event: The next event from the merged generator.
"""
sentinel = object()
queue = asyncio.Queue()
# Agents are processed in parallel.
# Events for each agent are put on queue sequentially.
async def process_an_agent(events_for_one_agent):
try:
async for event in events_for_one_agent:
resume_signal = asyncio.Event()
await queue.put((event, resume_signal))
# Wait for upstream to consume event before generating new events.
await resume_signal.wait()
finally:
# Mark agent as finished.
await queue.put((sentinel, None))
async with asyncio.TaskGroup() as tg:
for events_for_one_agent in agent_runs:
tg.create_task(process_an_agent(events_for_one_agent))
sentinel_count = 0
# Run until all agents finished processing.
while sentinel_count < len(agent_runs):
event, resume_signal = await queue.get()
# Agent finished processing.
if event is sentinel:
sentinel_count += 1
else:
yield event
# Signal to agent that it should generate next event.
resume_signal.set()
class ParallelAgent(BaseAgent):
"""A shell agent that runs its sub-agents in parallel in an isolated manner.
@@ -195,13 +183,11 @@ class ParallelAgent(BaseAgent):
pause_invocation = False
try:
# TODO remove if once Python <3.11 is no longer supported.
merge_func = (
_merge_agent_run
if sys.version_info >= (3, 11)
else _merge_agent_run_pre_3_11
)
async with Aclosing(merge_func(agent_runs)) as agen:
async for event in agen:
yield event
+16 -27
View File
@@ -26,30 +26,22 @@ from typing import Union
from urllib.parse import urlparse
import uuid
try:
from a2a.client import Client as A2AClient
from a2a.client import ClientEvent as A2AClientEvent
from a2a.client.card_resolver import A2ACardResolver
from a2a.client.client import ClientConfig as A2AClientConfig
from a2a.client.client_factory import ClientFactory as A2AClientFactory
from a2a.client.errors import A2AClientError
from a2a.types import AgentCard
from a2a.types import Message as A2AMessage
from a2a.types import Part as A2APart
from a2a.types import Role
from a2a.types import TaskArtifactUpdateEvent as A2ATaskArtifactUpdateEvent
from a2a.types import TaskState
from a2a.types import TaskStatusUpdateEvent as A2ATaskStatusUpdateEvent
from a2a.types import TransportProtocol as A2ATransport
except ImportError as e:
import sys
if sys.version_info < (3, 10):
raise ImportError(
"A2A requires Python 3.10 or above. Please upgrade your Python version."
) from e
else:
raise e
from a2a.client import Client as A2AClient
from a2a.client import ClientEvent as A2AClientEvent
from a2a.client.card_resolver import A2ACardResolver
from a2a.client.client import ClientConfig as A2AClientConfig
from a2a.client.client_factory import ClientFactory as A2AClientFactory
from a2a.client.errors import A2AClientError
from a2a.types import AgentCard
from a2a.types import Message as A2AMessage
from a2a.types import Part as A2APart
from a2a.types import Role
from a2a.types import TaskArtifactUpdateEvent as A2ATaskArtifactUpdateEvent
from a2a.types import TaskState
from a2a.types import TaskStatusUpdateEvent as A2ATaskStatusUpdateEvent
from a2a.types import TransportProtocol as A2ATransport
from google.genai import types as genai_types
import httpx
try:
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
@@ -57,9 +49,6 @@ except ImportError:
# Fallback for older versions of a2a-sdk.
AGENT_CARD_WELL_KNOWN_PATH = "/.well-known/agent.json"
from google.genai import types as genai_types
import httpx
from ..a2a.converters.event_converter import convert_a2a_message_to_event
from ..a2a.converters.event_converter import convert_a2a_task_to_event
from ..a2a.converters.event_converter import convert_event_to_a2a_message
+6 -17
View File
@@ -342,25 +342,14 @@ def get_fast_api_app(
)
if a2a:
try:
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCard
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCard
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
from ..a2a.executor.a2a_agent_executor import A2aAgentExecutor
from ..a2a.executor.a2a_agent_executor import A2aAgentExecutor
except ImportError as e:
import sys
if sys.version_info < (3, 10):
raise ImportError(
"A2A requires Python 3.10 or above. Please upgrade your Python"
" version."
) from e
else:
raise e
# locate all a2a agent apps in the agents directory
base_path = Path.cwd() / agents_dir
# the root agents directory should be an existing folder
+3 -10
View File
@@ -30,16 +30,9 @@ from .tool_context import ToolContext
try:
from crewai.tools import BaseTool as CrewaiBaseTool
except ImportError as e:
import sys
if sys.version_info < (3, 10):
raise ImportError(
'Crewai Tools require Python 3.10+. Please upgrade your Python version.'
) from e
else:
raise ImportError(
"Crewai Tools require pip install 'google-adk[extensions]'."
) from e
raise ImportError(
"Crewai Tools require pip install 'google-adk[extensions]'."
) from e
class CrewaiTool(FunctionTool):
+2 -10
View File
@@ -39,15 +39,7 @@ try:
except ImportError as e:
import logging
import sys
logger = logging.getLogger('google_adk.' + __name__)
if sys.version_info < (3, 10):
logger.warning(
'MCP Tool requires Python 3.10 or above. Please upgrade your Python'
' version.'
)
else:
logger.debug('MCP Tool is not installed')
logger.debug(e)
logger.debug('MCP Tool is not installed')
logger.debug(e)
@@ -29,24 +29,13 @@ from typing import TextIO
from typing import Union
import anyio
from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.sse import sse_client
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamablehttp_client
from pydantic import BaseModel
try:
from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.sse import sse_client
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamablehttp_client
except ImportError as e:
if sys.version_info < (3, 10):
raise ImportError(
'MCP Tool requires Python 3.10 or above. Please upgrade your Python'
' version.'
) from e
else:
raise e
logger = logging.getLogger('google_adk.' + __name__)
+6 -21
View File
@@ -17,7 +17,6 @@ from __future__ import annotations
import base64
import inspect
import logging
import sys
from typing import Any
from typing import Callable
from typing import Dict
@@ -27,35 +26,21 @@ import warnings
from fastapi.openapi.models import APIKeyIn
from google.genai.types import FunctionDeclaration
from mcp.types import Tool as McpBaseTool
from typing_extensions import override
from ...agents.readonly_context import ReadonlyContext
from ...features import FeatureName
from ...features import is_feature_enabled
from .._gemini_schema_util import _to_gemini_schema
from .mcp_session_manager import MCPSessionManager
from .mcp_session_manager import retry_on_errors
# Attempt to import MCP Tool from the MCP library, and hints user to upgrade
# their Python version to 3.10 if it fails.
try:
from mcp.types import Tool as McpBaseTool
except ImportError as e:
if sys.version_info < (3, 10):
raise ImportError(
"MCP Tool requires Python 3.10 or above. Please upgrade your Python"
" version."
) from e
else:
raise e
from ...auth.auth_credential import AuthCredential
from ...auth.auth_schemes import AuthScheme
from ...auth.auth_tool import AuthConfig
from ...features import FeatureName
from ...features import is_feature_enabled
from .._gemini_schema_util import _to_gemini_schema
from ..base_authenticated_tool import BaseAuthenticatedTool
# import
from ..tool_context import ToolContext
from .mcp_session_manager import MCPSessionManager
from .mcp_session_manager import retry_on_errors
logger = logging.getLogger("google_adk." + __name__)
+2 -17
View File
@@ -25,6 +25,8 @@ from typing import TextIO
from typing import Union
import warnings
from mcp import StdioServerParameters
from mcp.types import ListToolsResult
from pydantic import model_validator
from typing_extensions import override
@@ -41,23 +43,6 @@ from .mcp_session_manager import retry_on_errors
from .mcp_session_manager import SseConnectionParams
from .mcp_session_manager import StdioConnectionParams
from .mcp_session_manager import StreamableHTTPConnectionParams
# Attempt to import MCP Tool from the MCP library, and hints user to upgrade
# their Python version to 3.10 if it fails.
try:
from mcp import StdioServerParameters
from mcp.types import ListToolsResult
except ImportError as e:
import sys
if sys.version_info < (3, 10):
raise ImportError(
"MCP Tool requires Python 3.10 or above. Please upgrade your Python"
" version."
) from e
else:
raise e
from .mcp_tool import MCPTool
logger = logging.getLogger("google_adk." + __name__)
+3 -26
View File
@@ -20,30 +20,7 @@ Please do not rely on the implementation details.
from __future__ import annotations
from contextlib import AbstractAsyncContextManager
from typing import Any
from typing import AsyncGenerator
from contextlib import aclosing
class Aclosing(AbstractAsyncContextManager):
"""Async context manager for safely finalizing an asynchronously cleaned-up
resource such as an async generator, calling its ``aclose()`` method.
Needed to correctly close contexts for OTel spans.
See https://github.com/google/adk-python/issues/1670#issuecomment-3115891100.
Based on
https://docs.python.org/3/library/contextlib.html#contextlib.aclosing
which is available in Python 3.10+.
TODO: replace all occurrences with contextlib.aclosing once Python 3.9 is no
longer supported.
"""
def __init__(self, async_generator: AsyncGenerator[Any, None]):
self.async_generator = async_generator
async def __aenter__(self):
return self.async_generator
async def __aexit__(self, *exc_info):
await self.async_generator.aclose()
# Re-export aclosing for backward compatibility
Aclosing = aclosing

Some files were not shown because too many files have changed in this diff Show More