Skip to content

Commit f9190bd

Browse files
fix: make header names python-safe
1 parent 8e02e62 commit f9190bd

3 files changed

Lines changed: 42 additions & 3 deletions

File tree

packages/toolbox-core/src/toolbox_core/protocol.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,32 @@ class ParameterSchema(BaseModel):
8383
additionalProperties: Optional[Union[bool, AdditionalPropertiesSchema]] = None
8484
default: Optional[Any] = None
8585

86+
def get_python_safe_field_name(self) -> str:
87+
"""
88+
Returns a Python-safe identifier for this parameter name.
89+
90+
Some parameter names (for example HTTP header names like "X-Application-ID")
91+
are not valid Python identifiers and cannot be used directly in function
92+
signatures or introspection utilities. This helper converts such names into
93+
a safe form by:
94+
- Replacing non-alphanumeric characters with underscores.
95+
- Prefixing with 'param_' if the result starts with a digit or is empty.
96+
"""
97+
name = self.name
98+
if name.isidentifier():
99+
return name
100+
101+
# Replace any non-alphanumeric/underscore character with underscore.
102+
sanitized = "".join(
103+
ch if (ch.isalnum() or ch == "_") else "_" for ch in name
104+
)
105+
106+
# Ensure the identifier does not start with a digit and is non-empty.
107+
if not sanitized or sanitized[0].isdigit():
108+
sanitized = f"param_{sanitized}" if sanitized else "param_"
109+
110+
return sanitized
111+
86112
@property
87113
def has_default(self) -> bool:
88114
"""Returns True if `default` was explicitly provided in schema input."""
@@ -119,7 +145,7 @@ def to_param(self) -> Parameter:
119145
default_value = self.default
120146

121147
return Parameter(
122-
self.name,
148+
self.get_python_safe_field_name(),
123149
Parameter.POSITIONAL_OR_KEYWORD,
124150
annotation=self.__get_type(),
125151
default=default_value,

packages/toolbox-core/src/toolbox_core/tool.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ def __init__(
8383
self.__transport = transport
8484
self.__description = description
8585
self.__params = params
86+
# Map from Python-safe field names back to the original schema names.
87+
# This allows us to expose valid Python identifiers in function signatures
88+
# while still sending the original parameter names to the Toolbox server.
89+
self.__param_name_map: dict[str, str] = {
90+
p.get_python_safe_field_name(): p.name for p in self.__params
91+
}
8692
self.__pydantic_model = params_to_pydantic_model(name, self.__params)
8793

8894
# Separate parameters into those without a default and those with a
@@ -250,7 +256,12 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str:
250256

251257
# The payload will only contain arguments explicitly provided by the user.
252258
# Optional arguments not provided by the user will not be in the payload.
253-
payload = all_args.arguments
259+
# At this point, keys are Python-safe field names. Map them back to the
260+
# original schema names expected by the Toolbox server.
261+
payload = OrderedDict()
262+
for safe_name, value in all_args.arguments.items():
263+
original_name = self.__param_name_map.get(safe_name, safe_name)
264+
payload[original_name] = value
254265

255266
# Perform argument type validations using pydantic
256267
self.__pydantic_model.model_validate(payload)

packages/toolbox-core/src/toolbox_core/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ def params_to_pydantic_model(
127127
if field.has_default:
128128
default_value = field.default
129129

130-
field_definitions[field.name] = cast(
130+
python_safe_name = field.get_python_safe_field_name()
131+
132+
field_definitions[python_safe_name] = cast(
131133
Any,
132134
(
133135
field.to_param().annotation,

0 commit comments

Comments
 (0)