Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ class OpenApiSpecParser:
3. A callable Python object (a function) that can execute the operation.
"""

def __init__(self, preserve_property_names: bool = False):
"""Initializes the OpenApiSpecParser.

Args:
preserve_property_names: If True, preserve the original property names
from the OpenAPI spec instead of converting them to snake_case.
"""
self._preserve_property_names = preserve_property_names

def parse(self, openapi_spec_dict: Dict[str, Any]) -> List[ParsedOperation]:
"""Extracts an OpenAPI spec dict into a list of ParsedOperation objects.

Expand Down Expand Up @@ -212,7 +221,10 @@ def _collect_operations(

url = OperationEndpoint(base_url=base_url, path=path, method=method)
operation = Operation.model_validate(operation_dict)
operation_parser = OperationParser(operation)
operation_parser = OperationParser(
operation,
preserve_property_names=self._preserve_property_names,
)

# Check for operation-specific auth scheme
auth_scheme_name = operation_parser.get_auth_scheme_name()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(
header_provider: Optional[
Callable[[ReadonlyContext], Dict[str, str]]
] = None,
preserve_property_names: bool = False,
):
"""Initializes the OpenAPIToolset.

Expand Down Expand Up @@ -129,11 +130,17 @@ def __init__(
an argument, allowing dynamic header generation based on the current
context. Useful for adding custom headers like correlation IDs,
authentication tokens, or other request metadata.
preserve_property_names: If True, preserve the original property names
from the OpenAPI spec instead of converting them to snake_case. This
is useful when calling APIs that expect camelCase or other
non-snake_case parameter names in the request. Defaults to False for
backward compatibility.
"""
super().__init__(tool_filter=tool_filter, tool_name_prefix=tool_name_prefix)
self._header_provider = header_provider
self._auth_scheme = auth_scheme
self._auth_credential = auth_credential
self._preserve_property_names = preserve_property_names
# Store auth config as instance variable so ADK can populate
# exchanged_auth_credential in-place before calling get_tools()
self._auth_config: Optional[AuthConfig] = (
Expand Down Expand Up @@ -219,14 +226,18 @@ def _load_spec(

def _parse(self, openapi_spec_dict: Dict[str, Any]) -> List[RestApiTool]:
"""Parse OpenAPI spec into a list of RestApiTool."""
operations = OpenApiSpecParser().parse(openapi_spec_dict)
parser = OpenApiSpecParser(
preserve_property_names=self._preserve_property_names
)
operations = parser.parse(openapi_spec_dict)

tools = []
for o in operations:
tool = RestApiTool.from_parsed_operation(
o,
ssl_verify=self._ssl_verify,
header_provider=self._header_provider,
preserve_property_names=self._preserve_property_names,
)
logger.info("Parsed tool: %s", tool.name)
tools.append(tool)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ..._gemini_schema_util import _to_snake_case
from ..common.common import ApiParameter
from ..common.common import PydocHelper
from ..common.common import rename_python_keywords


class OperationParser:
Expand All @@ -42,13 +43,21 @@ class OperationParser:
"""

def __init__(
self, operation: Union[Operation, Dict[str, Any], str], should_parse=True
self,
operation: Union[Operation, Dict[str, Any], str],
should_parse: bool = True,
*,
preserve_property_names: bool = False,
):
"""Initializes the OperationParser with an OpenApiOperation.

Args:
operation: The OpenApiOperation object or a dictionary to process.
should_parse: Whether to parse the operation during initialization.
preserve_property_names: If True, preserve the original property names
from the OpenAPI spec instead of converting them to snake_case.
Useful for APIs that expect camelCase or other non-snake_case
parameter names.
"""
if isinstance(operation, dict):
self._operation = Operation.model_validate(operation)
Expand All @@ -57,6 +66,7 @@ def __init__(
else:
self._operation = operation

self._preserve_property_names = preserve_property_names
self._params: List[ApiParameter] = []
self._return_value: Optional[ApiParameter] = None
if should_parse:
Expand All @@ -71,12 +81,24 @@ def load(
operation: Union[Operation, Dict[str, Any]],
params: List[ApiParameter],
return_value: Optional[ApiParameter] = None,
*,
preserve_property_names: bool = False,
) -> 'OperationParser':
parser = cls(operation, should_parse=False)
parser = cls(
operation,
should_parse=False,
preserve_property_names=preserve_property_names,
)
parser._params = params
parser._return_value = return_value
return parser

def _get_py_name(self, original_name: str) -> str:
"""Determines the Python parameter name based on preserve_property_names."""
if self._preserve_property_names:
return rename_python_keywords(original_name)
return ''

def _process_operation_parameters(self):
"""Processes parameters from the OpenAPI operation."""
parameters = self._operation.parameters or []
Expand All @@ -99,6 +121,7 @@ def _process_operation_parameters(self):
param_schema=schema,
description=description,
required=required,
py_name=self._get_py_name(original_name),
)
)

Expand Down Expand Up @@ -126,6 +149,7 @@ def _process_request_body(self):
param_location='body',
param_schema=prop_details,
description=prop_details.description,
py_name=self._get_py_name(prop_name),
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def from_parsed_operation(
header_provider: Optional[
Callable[[ReadonlyContext], Dict[str, str]]
] = None,
preserve_property_names: bool = False,
) -> "RestApiTool":
"""Initializes the RestApiTool from a ParsedOperation object.

Expand All @@ -189,12 +190,17 @@ def from_parsed_operation(
an argument, allowing dynamic header generation based on the current
context. Useful for adding custom headers like correlation IDs,
authentication tokens, or other request metadata.
preserve_property_names: If True, preserve original property names
from the OpenAPI spec instead of converting to snake_case.

Returns:
A RestApiTool object.
"""
operation_parser = OperationParser.load(
parsed.operation, parsed.parameters, parsed.return_value
parsed.operation,
parsed.parameters,
parsed.return_value,
preserve_property_names=preserve_property_names,
)

tool_name = _to_snake_case(operation_parser.get_function_name())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,92 @@ def test_openapi_toolset_header_provider_none_by_default(

# Verify all tools have no header_provider
assert all(tool._header_provider is None for tool in toolset._tools)


def test_openapi_toolset_preserve_property_names(openapi_spec: Dict):
"""Test that preserve_property_names keeps original camelCase names."""
toolset = OpenAPIToolset(
spec_dict=openapi_spec,
preserve_property_names=True,
)
tool = toolset.get_tool("calendar_calendars_get")
assert tool is not None

# The calendarId parameter should keep its original camelCase name
params = tool._operation_parser.get_parameters()
param_names = [p.py_name for p in params]
assert "calendarId" in param_names

# The JSON schema should also use the original name
schema = tool._operation_parser.get_json_schema()
assert "calendarId" in schema["properties"]


def test_openapi_toolset_default_snake_case_conversion(openapi_spec: Dict):
"""Test that default behavior still converts to snake_case."""
toolset = OpenAPIToolset(spec_dict=openapi_spec)
tool = toolset.get_tool("calendar_calendars_get")
assert tool is not None

# The calendarId parameter should be converted to snake_case by default
params = tool._operation_parser.get_parameters()
param_names = [p.py_name for p in params]
assert "calendar_id" in param_names
assert "calendarId" not in param_names

# The JSON schema should also use snake_case
schema = tool._operation_parser.get_json_schema()
assert "calendar_id" in schema["properties"]
assert "calendarId" not in schema["properties"]


def test_openapi_toolset_preserve_property_names_body_params():
"""Test preserve_property_names with request body properties."""
spec = {
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0"},
"servers": [{"url": "https://api.example.com"}],
"paths": {
"/users": {
"post": {
"operationId": "createUser",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"firstName": {"type": "string"},
"lastName": {"type": "string"},
"emailAddress": {"type": "string"},
},
}
}
}
},
"responses": {"200": {"description": "OK"}},
}
}
},
}

# With preserve_property_names=True
toolset = OpenAPIToolset(
spec_dict=spec,
preserve_property_names=True,
)
tool = toolset.get_tool("create_user")
params = tool._operation_parser.get_parameters()
param_names = [p.py_name for p in params]
assert "firstName" in param_names
assert "lastName" in param_names
assert "emailAddress" in param_names

# Without preserve_property_names (default)
toolset_default = OpenAPIToolset(spec_dict=spec)
tool_default = toolset_default.get_tool("create_user")
params_default = tool_default._operation_parser.get_parameters()
param_names_default = [p.py_name for p in params_default]
assert "first_name" in param_names_default
assert "last_name" in param_names_default
assert "email_address" in param_names_default