From 1795c4d16f860577fddf501ba23a45ffbfb83903 Mon Sep 17 00:00:00 2001 From: Vasilii Novikov Date: Mon, 8 Sep 2025 00:39:33 +0400 Subject: [PATCH] Make OpenAPI tool async --- pyproject.toml | 1 + .../adk/tools/openapi_tool/auth/auth_helpers.py | 8 ++++---- .../openapi_spec_parser/rest_api_tool.py | 15 ++++++++++----- .../tools/openapi_tool/auth/test_auth_helper.py | 14 ++++++++------ .../openapi_spec_parser/test_rest_api_tool.py | 5 +++-- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 269e1bc22a..2e47f9a49d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service "google-genai>=1.21.1, <2.0.0", # Google GenAI SDK "graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering + "httpx>=0.27.0, <1.0.0", # HTTP client library "mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset "opentelemetry-api>=1.31.0, <2.0.0", # OpenTelemetry "opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0", diff --git a/src/google/adk/tools/openapi_tool/auth/auth_helpers.py b/src/google/adk/tools/openapi_tool/auth/auth_helpers.py index 197b7fac11..d7be471ed9 100644 --- a/src/google/adk/tools/openapi_tool/auth/auth_helpers.py +++ b/src/google/adk/tools/openapi_tool/auth/auth_helpers.py @@ -26,9 +26,9 @@ from fastapi.openapi.models import OAuth2 from fastapi.openapi.models import OpenIdConnect from fastapi.openapi.models import Schema +import httpx from pydantic import BaseModel from pydantic import ValidationError -import requests from ....auth.auth_credential import AuthCredential from ....auth.auth_credential import AuthCredentialTypes @@ -287,14 +287,14 @@ def openid_url_to_scheme_credential( Raises: ValueError: If the OpenID URL is invalid, fetching fails, or required fields are missing. - requests.exceptions.RequestException: If there's an error during the + httpx.HTTPStatusError or httpx.RequestError: If there's an error during the HTTP request. """ try: - response = requests.get(openid_url, timeout=10) + response = httpx.get(openid_url, timeout=10) response.raise_for_status() config_dict = response.json() - except requests.exceptions.RequestException as e: + except httpx.RequestError as e: raise ValueError( f"Failed to fetch OpenID configuration from {openid_url}: {e}" ) from e diff --git a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py index 2c02d55510..eec89a066e 100644 --- a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +++ b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py @@ -24,7 +24,7 @@ from fastapi.openapi.models import Operation from google.genai.types import FunctionDeclaration -import requests +import httpx from typing_extensions import override from ....auth.auth_credential import AuthCredential @@ -240,7 +240,7 @@ def _prepare_request_params( Returns: A dictionary containing the request parameters for the API call. This - initializes a requests.request() call. + initializes an httpx.AsyncClient.request() call. Example: self._prepare_request_params({"input_id": "test-id"}) @@ -395,13 +395,13 @@ async def call( # Got all parameters. Call the API. request_params = self._prepare_request_params(api_params, api_args) - response = requests.request(**request_params) + response = await _request(**request_params) # Parse API response try: - response.raise_for_status() # Raise HTTPError for bad responses + response.raise_for_status() # Raise HTTPStatusError for bad responses return response.json() # Try to decode JSON - except requests.exceptions.HTTPError: + except httpx.HTTPStatusError: error_details = response.content.decode("utf-8") return { "error": ( @@ -427,3 +427,8 @@ def __repr__(self): f' auth_scheme="{self.auth_scheme}",' f' auth_credential="{self.auth_credential}")' ) + + +async def _request(**request_params) -> httpx.Response: + async with httpx.AsyncClient() as client: + return await client.request(**request_params) diff --git a/tests/unittests/tools/openapi_tool/auth/test_auth_helper.py b/tests/unittests/tools/openapi_tool/auth/test_auth_helper.py index af7bf4f6d0..7b9777cbc3 100644 --- a/tests/unittests/tools/openapi_tool/auth/test_auth_helper.py +++ b/tests/unittests/tools/openapi_tool/auth/test_auth_helper.py @@ -36,8 +36,8 @@ from google.adk.tools.openapi_tool.auth.auth_helpers import service_account_dict_to_scheme_credential from google.adk.tools.openapi_tool.auth.auth_helpers import service_account_scheme_credential from google.adk.tools.openapi_tool.auth.auth_helpers import token_to_scheme_credential +import httpx import pytest -import requests def test_token_to_scheme_credential_api_key_header(): @@ -272,7 +272,7 @@ def test_openid_dict_to_scheme_credential_missing_credential_fields(): openid_dict_to_scheme_credential(config_dict, scopes, credential_dict) -@patch("requests.get") +@patch("httpx.get") def test_openid_url_to_scheme_credential(mock_get): mock_response = { "authorization_endpoint": "auth_url", @@ -303,7 +303,7 @@ def test_openid_url_to_scheme_credential(mock_get): mock_get.assert_called_once_with("openid_url", timeout=10) -@patch("requests.get") +@patch("httpx.get") def test_openid_url_to_scheme_credential_no_openid_url(mock_get): mock_response = { "authorization_endpoint": "auth_url", @@ -326,9 +326,11 @@ def test_openid_url_to_scheme_credential_no_openid_url(mock_get): assert scheme.openIdConnectUrl == "openid_url" -@patch("requests.get") +@patch("httpx.get") def test_openid_url_to_scheme_credential_request_exception(mock_get): - mock_get.side_effect = requests.exceptions.RequestException("Test Error") + mock_get.side_effect = httpx.HTTPStatusError( + "Test Error", request=None, response=None + ) credential_dict = {"client_id": "client_id", "client_secret": "client_secret"} with pytest.raises( @@ -337,7 +339,7 @@ def test_openid_url_to_scheme_credential_request_exception(mock_get): openid_url_to_scheme_credential("openid_url", [], credential_dict) -@patch("requests.get") +@patch("httpx.get") def test_openid_url_to_scheme_credential_invalid_json(mock_get): mock_get.return_value.json.side_effect = ValueError("Invalid JSON") mock_get.return_value.raise_for_status.return_value = None diff --git a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py index c4cbea7b9b..496d3207bf 100644 --- a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py +++ b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py @@ -33,6 +33,7 @@ from google.adk.tools.tool_context import ToolContext from google.genai.types import FunctionDeclaration from google.genai.types import Schema +import httpx import pytest @@ -193,7 +194,7 @@ def test_get_declaration( assert isinstance(declaration.parameters, Schema) @patch( - "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool.requests.request" + "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" ) @pytest.mark.asyncio async def test_call_success( @@ -225,7 +226,7 @@ async def test_call_success( assert result == {"result": "success"} @patch( - "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool.requests.request" + "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" ) @pytest.mark.asyncio async def test_call_auth_pending(