Skip to content

Commit c1f991f

Browse files
Add shared mapping key reader and reuse in tools
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent ae5cb2a commit c1f991f

3 files changed

Lines changed: 134 additions & 78 deletions

File tree

hyperbrowser/mapping_utils.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
from collections.abc import Mapping as MappingABC
2-
from typing import Any, Callable, Dict
2+
from typing import Any, Callable, Dict, List
33

44
from hyperbrowser.exceptions import HyperbrowserError
55

66

7-
def read_string_key_mapping(
7+
def read_string_mapping_keys(
88
mapping_value: Any,
99
*,
1010
expected_mapping_error: str,
1111
read_keys_error: str,
1212
non_string_key_error_builder: Callable[[object], str],
13-
read_value_error_builder: Callable[[str], str],
14-
key_display: Callable[[str], str],
15-
) -> Dict[str, object]:
13+
) -> List[str]:
1614
if not isinstance(mapping_value, MappingABC):
1715
raise HyperbrowserError(expected_mapping_error)
1816
try:
@@ -24,10 +22,30 @@ def read_string_key_mapping(
2422
read_keys_error,
2523
original_error=exc,
2624
) from exc
25+
normalized_keys: List[str] = []
2726
for key in mapping_keys:
2827
if type(key) is str:
28+
normalized_keys.append(key)
2929
continue
3030
raise HyperbrowserError(non_string_key_error_builder(key))
31+
return normalized_keys
32+
33+
34+
def read_string_key_mapping(
35+
mapping_value: Any,
36+
*,
37+
expected_mapping_error: str,
38+
read_keys_error: str,
39+
non_string_key_error_builder: Callable[[object], str],
40+
read_value_error_builder: Callable[[str], str],
41+
key_display: Callable[[str], str],
42+
) -> Dict[str, object]:
43+
mapping_keys = read_string_mapping_keys(
44+
mapping_value,
45+
expected_mapping_error=expected_mapping_error,
46+
read_keys_error=read_keys_error,
47+
non_string_key_error_builder=non_string_key_error_builder,
48+
)
3149
normalized_mapping: Dict[str, object] = {}
3250
for key in mapping_keys:
3351
try:

hyperbrowser/tools/__init__.py

Lines changed: 61 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
from hyperbrowser.display_utils import format_string_key_for_error
77
from hyperbrowser.exceptions import HyperbrowserError
8-
from hyperbrowser.mapping_utils import copy_mapping_values_by_string_keys
8+
from hyperbrowser.mapping_utils import (
9+
copy_mapping_values_by_string_keys,
10+
read_string_mapping_keys,
11+
)
912
from hyperbrowser.models.agents.browser_use import StartBrowserUseTaskParams
1013
from hyperbrowser.models.crawl import StartCrawlJobParams
1114
from hyperbrowser.models.extract import StartExtractJobParams
@@ -60,20 +63,14 @@ def _format_tool_param_key_for_error(key: str) -> str:
6063
def _normalize_extract_schema_mapping(
6164
schema_value: MappingABC[object, Any],
6265
) -> Dict[str, Any]:
63-
try:
64-
schema_keys = list(schema_value.keys())
65-
except HyperbrowserError:
66-
raise
67-
except Exception as exc:
68-
raise HyperbrowserError(
69-
"Failed to read extract tool `schema` object keys",
70-
original_error=exc,
71-
) from exc
72-
normalized_schema_keys: list[str] = []
73-
for key in schema_keys:
74-
if type(key) is not str:
75-
raise HyperbrowserError("Extract tool `schema` object keys must be strings")
76-
normalized_schema_keys.append(key)
66+
normalized_schema_keys = read_string_mapping_keys(
67+
schema_value,
68+
expected_mapping_error="Extract tool `schema` must be an object or JSON string",
69+
read_keys_error="Failed to read extract tool `schema` object keys",
70+
non_string_key_error_builder=lambda _key: (
71+
"Extract tool `schema` object keys must be strings"
72+
),
73+
)
7774
return copy_mapping_values_by_string_keys(
7875
schema_value,
7976
normalized_schema_keys,
@@ -114,67 +111,58 @@ def _prepare_extract_tool_params(params: Mapping[str, Any]) -> Dict[str, Any]:
114111

115112

116113
def _to_param_dict(params: Mapping[str, Any]) -> Dict[str, Any]:
117-
if not isinstance(params, Mapping):
118-
raise HyperbrowserError("tool params must be a mapping")
119-
try:
120-
param_keys = list(params.keys())
121-
except HyperbrowserError:
122-
raise
123-
except Exception as exc:
124-
raise HyperbrowserError(
125-
"Failed to read tool params keys",
126-
original_error=exc,
127-
) from exc
114+
param_keys = read_string_mapping_keys(
115+
params,
116+
expected_mapping_error="tool params must be a mapping",
117+
read_keys_error="Failed to read tool params keys",
118+
non_string_key_error_builder=lambda _key: "tool params keys must be strings",
119+
)
128120
for key in param_keys:
129-
if type(key) is str:
130-
try:
131-
normalized_key = key.strip()
132-
if type(normalized_key) is not str:
133-
raise TypeError("normalized tool param key must be a string")
134-
is_empty_key = len(normalized_key) == 0
135-
except HyperbrowserError:
136-
raise
137-
except Exception as exc:
138-
raise HyperbrowserError(
139-
"Failed to normalize tool param key",
140-
original_error=exc,
141-
) from exc
142-
if is_empty_key:
143-
raise HyperbrowserError("tool params keys must not be empty")
144-
try:
145-
has_surrounding_whitespace = key != normalized_key
146-
except HyperbrowserError:
147-
raise
148-
except Exception as exc:
149-
raise HyperbrowserError(
150-
"Failed to normalize tool param key",
151-
original_error=exc,
152-
) from exc
153-
if has_surrounding_whitespace:
154-
raise HyperbrowserError(
155-
"tool params keys must not contain leading or trailing whitespace"
156-
)
157-
try:
158-
contains_control_character = any(
159-
ord(character) < 32 or ord(character) == 127 for character in key
160-
)
161-
except HyperbrowserError:
162-
raise
163-
except Exception as exc:
164-
raise HyperbrowserError(
165-
"Failed to validate tool param key characters",
166-
original_error=exc,
167-
) from exc
168-
if contains_control_character:
169-
raise HyperbrowserError(
170-
"tool params keys must not contain control characters"
171-
)
172-
continue
173-
raise HyperbrowserError("tool params keys must be strings")
174-
normalized_param_keys = [key for key in param_keys if type(key) is str]
121+
try:
122+
normalized_key = key.strip()
123+
if type(normalized_key) is not str:
124+
raise TypeError("normalized tool param key must be a string")
125+
is_empty_key = len(normalized_key) == 0
126+
except HyperbrowserError:
127+
raise
128+
except Exception as exc:
129+
raise HyperbrowserError(
130+
"Failed to normalize tool param key",
131+
original_error=exc,
132+
) from exc
133+
if is_empty_key:
134+
raise HyperbrowserError("tool params keys must not be empty")
135+
try:
136+
has_surrounding_whitespace = key != normalized_key
137+
except HyperbrowserError:
138+
raise
139+
except Exception as exc:
140+
raise HyperbrowserError(
141+
"Failed to normalize tool param key",
142+
original_error=exc,
143+
) from exc
144+
if has_surrounding_whitespace:
145+
raise HyperbrowserError(
146+
"tool params keys must not contain leading or trailing whitespace"
147+
)
148+
try:
149+
contains_control_character = any(
150+
ord(character) < 32 or ord(character) == 127 for character in key
151+
)
152+
except HyperbrowserError:
153+
raise
154+
except Exception as exc:
155+
raise HyperbrowserError(
156+
"Failed to validate tool param key characters",
157+
original_error=exc,
158+
) from exc
159+
if contains_control_character:
160+
raise HyperbrowserError(
161+
"tool params keys must not contain control characters"
162+
)
175163
return copy_mapping_values_by_string_keys(
176164
params,
177-
normalized_param_keys,
165+
param_keys,
178166
read_value_error_builder=lambda key_display: (
179167
f"Failed to read tool param '{key_display}'"
180168
),

tests/test_mapping_utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from hyperbrowser.exceptions import HyperbrowserError
66
from hyperbrowser.mapping_utils import (
77
copy_mapping_values_by_string_keys,
8+
read_string_mapping_keys,
89
read_string_key_mapping,
910
)
1011

@@ -116,6 +117,55 @@ def test_read_string_key_mapping_falls_back_for_unreadable_key_display():
116117
)
117118

118119

120+
def test_read_string_mapping_keys_returns_string_keys():
121+
assert read_string_mapping_keys(
122+
{"a": 1, "b": 2},
123+
expected_mapping_error="expected mapping",
124+
read_keys_error="failed keys",
125+
non_string_key_error_builder=lambda key: (
126+
f"non-string key: {type(key).__name__}"
127+
),
128+
) == ["a", "b"]
129+
130+
131+
def test_read_string_mapping_keys_rejects_non_mapping_values():
132+
with pytest.raises(HyperbrowserError, match="expected mapping"):
133+
read_string_mapping_keys(
134+
["a"],
135+
expected_mapping_error="expected mapping",
136+
read_keys_error="failed keys",
137+
non_string_key_error_builder=lambda key: (
138+
f"non-string key: {type(key).__name__}"
139+
),
140+
)
141+
142+
143+
def test_read_string_mapping_keys_wraps_key_read_errors():
144+
with pytest.raises(HyperbrowserError, match="failed keys") as exc_info:
145+
read_string_mapping_keys(
146+
_BrokenKeysMapping(),
147+
expected_mapping_error="expected mapping",
148+
read_keys_error="failed keys",
149+
non_string_key_error_builder=lambda key: (
150+
f"non-string key: {type(key).__name__}"
151+
),
152+
)
153+
154+
assert isinstance(exc_info.value.original_error, RuntimeError)
155+
156+
157+
def test_read_string_mapping_keys_rejects_non_string_keys():
158+
with pytest.raises(HyperbrowserError, match="non-string key: int"):
159+
read_string_mapping_keys(
160+
{1: "value"},
161+
expected_mapping_error="expected mapping",
162+
read_keys_error="failed keys",
163+
non_string_key_error_builder=lambda key: (
164+
f"non-string key: {type(key).__name__}"
165+
),
166+
)
167+
168+
119169
def test_copy_mapping_values_by_string_keys_returns_selected_values():
120170
copied_values = copy_mapping_values_by_string_keys(
121171
{"field": "value", "other": "ignored"},

0 commit comments

Comments
 (0)