Skip to content

Commit 698606f

Browse files
Centralize computer-action endpoint normalization
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent f4219b1 commit 698606f

7 files changed

Lines changed: 184 additions & 105 deletions

File tree

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ This runs lint, format checks, compile checks, tests, and package build.
9696
- `tests/test_examples_syntax.py` (example script syntax guardrail),
9797
- `tests/test_docs_python3_commands.py` (`README`/`CONTRIBUTING`/examples python3 command consistency enforcement),
9898
- `tests/test_example_sync_async_parity.py` (sync/async example parity enforcement),
99-
- `tests/test_example_run_instructions.py` (example run-instruction consistency enforcement).
99+
- `tests/test_example_run_instructions.py` (example run-instruction consistency enforcement),
100+
- `tests/test_computer_action_endpoint_helper_usage.py` (computer-action endpoint-normalization helper usage enforcement).
100101

101102
## Code quality conventions
102103

hyperbrowser/client/managers/async_manager/computer_action.py

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Union, List, Optional
33
from hyperbrowser.exceptions import HyperbrowserError
44
from hyperbrowser.type_utils import is_plain_string, is_string_subclass_instance
5+
from ..computer_action_utils import normalize_computer_action_endpoint
56
from ..response_utils import parse_response_model
67
from ..serialization_utils import serialize_model_dump_to_dict
78
from hyperbrowser.models import (
@@ -39,58 +40,9 @@ async def _execute_request(
3940
"session must be a plain string session ID or SessionDetail"
4041
)
4142

42-
try:
43-
computer_action_endpoint = session.computer_action_endpoint
44-
except HyperbrowserError:
45-
raise
46-
except Exception as exc:
47-
raise HyperbrowserError(
48-
"session must include computer_action_endpoint",
49-
original_error=exc,
50-
) from exc
51-
52-
if computer_action_endpoint is None:
53-
raise HyperbrowserError(
54-
"Computer action endpoint not available for this session"
55-
)
56-
if not is_plain_string(computer_action_endpoint):
57-
raise HyperbrowserError("session computer_action_endpoint must be a string")
58-
try:
59-
normalized_computer_action_endpoint = computer_action_endpoint.strip()
60-
if not is_plain_string(normalized_computer_action_endpoint):
61-
raise TypeError("normalized computer_action_endpoint must be a string")
62-
except HyperbrowserError:
63-
raise
64-
except Exception as exc:
65-
raise HyperbrowserError(
66-
"Failed to normalize session computer_action_endpoint",
67-
original_error=exc,
68-
) from exc
69-
70-
if not normalized_computer_action_endpoint:
71-
raise HyperbrowserError(
72-
"Computer action endpoint not available for this session"
73-
)
74-
if normalized_computer_action_endpoint != computer_action_endpoint:
75-
raise HyperbrowserError(
76-
"session computer_action_endpoint must not contain leading or trailing whitespace"
77-
)
78-
try:
79-
contains_control_character = any(
80-
ord(character) < 32 or ord(character) == 127
81-
for character in normalized_computer_action_endpoint
82-
)
83-
except HyperbrowserError:
84-
raise
85-
except Exception as exc:
86-
raise HyperbrowserError(
87-
"Failed to validate session computer_action_endpoint characters",
88-
original_error=exc,
89-
) from exc
90-
if contains_control_character:
91-
raise HyperbrowserError(
92-
"session computer_action_endpoint must not contain control characters"
93-
)
43+
normalized_computer_action_endpoint = normalize_computer_action_endpoint(
44+
session
45+
)
9446

9547
if isinstance(params, BaseModel):
9648
payload = serialize_model_dump_to_dict(
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from typing import Any
2+
3+
from hyperbrowser.exceptions import HyperbrowserError
4+
from hyperbrowser.type_utils import is_plain_string
5+
6+
7+
def normalize_computer_action_endpoint(session: Any) -> str:
8+
try:
9+
computer_action_endpoint = session.computer_action_endpoint
10+
except HyperbrowserError:
11+
raise
12+
except Exception as exc:
13+
raise HyperbrowserError(
14+
"session must include computer_action_endpoint",
15+
original_error=exc,
16+
) from exc
17+
18+
if computer_action_endpoint is None:
19+
raise HyperbrowserError("Computer action endpoint not available for this session")
20+
if not is_plain_string(computer_action_endpoint):
21+
raise HyperbrowserError("session computer_action_endpoint must be a string")
22+
try:
23+
normalized_computer_action_endpoint = computer_action_endpoint.strip()
24+
if not is_plain_string(normalized_computer_action_endpoint):
25+
raise TypeError("normalized computer_action_endpoint must be a string")
26+
except HyperbrowserError:
27+
raise
28+
except Exception as exc:
29+
raise HyperbrowserError(
30+
"Failed to normalize session computer_action_endpoint",
31+
original_error=exc,
32+
) from exc
33+
34+
if not normalized_computer_action_endpoint:
35+
raise HyperbrowserError("Computer action endpoint not available for this session")
36+
if normalized_computer_action_endpoint != computer_action_endpoint:
37+
raise HyperbrowserError(
38+
"session computer_action_endpoint must not contain leading or trailing whitespace"
39+
)
40+
try:
41+
contains_control_character = any(
42+
ord(character) < 32 or ord(character) == 127
43+
for character in normalized_computer_action_endpoint
44+
)
45+
except HyperbrowserError:
46+
raise
47+
except Exception as exc:
48+
raise HyperbrowserError(
49+
"Failed to validate session computer_action_endpoint characters",
50+
original_error=exc,
51+
) from exc
52+
if contains_control_character:
53+
raise HyperbrowserError(
54+
"session computer_action_endpoint must not contain control characters"
55+
)
56+
return normalized_computer_action_endpoint

hyperbrowser/client/managers/sync_manager/computer_action.py

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Union, List, Optional
33
from hyperbrowser.exceptions import HyperbrowserError
44
from hyperbrowser.type_utils import is_plain_string, is_string_subclass_instance
5+
from ..computer_action_utils import normalize_computer_action_endpoint
56
from ..response_utils import parse_response_model
67
from ..serialization_utils import serialize_model_dump_to_dict
78
from hyperbrowser.models import (
@@ -39,58 +40,9 @@ def _execute_request(
3940
"session must be a plain string session ID or SessionDetail"
4041
)
4142

42-
try:
43-
computer_action_endpoint = session.computer_action_endpoint
44-
except HyperbrowserError:
45-
raise
46-
except Exception as exc:
47-
raise HyperbrowserError(
48-
"session must include computer_action_endpoint",
49-
original_error=exc,
50-
) from exc
51-
52-
if computer_action_endpoint is None:
53-
raise HyperbrowserError(
54-
"Computer action endpoint not available for this session"
55-
)
56-
if not is_plain_string(computer_action_endpoint):
57-
raise HyperbrowserError("session computer_action_endpoint must be a string")
58-
try:
59-
normalized_computer_action_endpoint = computer_action_endpoint.strip()
60-
if not is_plain_string(normalized_computer_action_endpoint):
61-
raise TypeError("normalized computer_action_endpoint must be a string")
62-
except HyperbrowserError:
63-
raise
64-
except Exception as exc:
65-
raise HyperbrowserError(
66-
"Failed to normalize session computer_action_endpoint",
67-
original_error=exc,
68-
) from exc
69-
70-
if not normalized_computer_action_endpoint:
71-
raise HyperbrowserError(
72-
"Computer action endpoint not available for this session"
73-
)
74-
if normalized_computer_action_endpoint != computer_action_endpoint:
75-
raise HyperbrowserError(
76-
"session computer_action_endpoint must not contain leading or trailing whitespace"
77-
)
78-
try:
79-
contains_control_character = any(
80-
ord(character) < 32 or ord(character) == 127
81-
for character in normalized_computer_action_endpoint
82-
)
83-
except HyperbrowserError:
84-
raise
85-
except Exception as exc:
86-
raise HyperbrowserError(
87-
"Failed to validate session computer_action_endpoint characters",
88-
original_error=exc,
89-
) from exc
90-
if contains_control_character:
91-
raise HyperbrowserError(
92-
"session computer_action_endpoint must not contain control characters"
93-
)
43+
normalized_computer_action_endpoint = normalize_computer_action_endpoint(
44+
session
45+
)
9446

9547
if isinstance(params, BaseModel):
9648
payload = serialize_model_dump_to_dict(

tests/test_architecture_marker_usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"tests/test_docs_python3_commands.py",
2727
"tests/test_example_sync_async_parity.py",
2828
"tests/test_example_run_instructions.py",
29+
"tests/test_computer_action_endpoint_helper_usage.py",
2930
)
3031

3132

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
pytestmark = pytest.mark.architecture
6+
7+
8+
MANAGER_MODULES = (
9+
"hyperbrowser/client/managers/sync_manager/computer_action.py",
10+
"hyperbrowser/client/managers/async_manager/computer_action.py",
11+
)
12+
13+
14+
def test_computer_action_managers_use_shared_endpoint_normalizer():
15+
for module_path in MANAGER_MODULES:
16+
module_text = Path(module_path).read_text(encoding="utf-8")
17+
assert "normalize_computer_action_endpoint(" in module_text
18+
assert "session.computer_action_endpoint" not in module_text
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import pytest
2+
3+
from hyperbrowser.client.managers.computer_action_utils import (
4+
normalize_computer_action_endpoint,
5+
)
6+
from hyperbrowser.exceptions import HyperbrowserError
7+
8+
9+
class _SessionWithoutEndpoint:
10+
pass
11+
12+
13+
class _SessionWithFailingEndpoint:
14+
@property
15+
def computer_action_endpoint(self) -> str:
16+
raise RuntimeError("endpoint unavailable")
17+
18+
19+
class _SessionWithHyperbrowserEndpointFailure:
20+
@property
21+
def computer_action_endpoint(self) -> str:
22+
raise HyperbrowserError("custom endpoint failure")
23+
24+
25+
class _Session:
26+
def __init__(self, endpoint):
27+
self.computer_action_endpoint = endpoint
28+
29+
30+
def test_normalize_computer_action_endpoint_returns_valid_value():
31+
assert (
32+
normalize_computer_action_endpoint(_Session("https://example.com/action"))
33+
== "https://example.com/action"
34+
)
35+
36+
37+
def test_normalize_computer_action_endpoint_wraps_missing_attribute_failures():
38+
with pytest.raises(
39+
HyperbrowserError, match="session must include computer_action_endpoint"
40+
) as exc_info:
41+
normalize_computer_action_endpoint(_SessionWithoutEndpoint())
42+
43+
assert isinstance(exc_info.value.original_error, AttributeError)
44+
45+
46+
def test_normalize_computer_action_endpoint_wraps_runtime_attribute_failures():
47+
with pytest.raises(
48+
HyperbrowserError, match="session must include computer_action_endpoint"
49+
) as exc_info:
50+
normalize_computer_action_endpoint(_SessionWithFailingEndpoint())
51+
52+
assert isinstance(exc_info.value.original_error, RuntimeError)
53+
54+
55+
def test_normalize_computer_action_endpoint_preserves_hyperbrowser_failures():
56+
with pytest.raises(HyperbrowserError, match="custom endpoint failure") as exc_info:
57+
normalize_computer_action_endpoint(_SessionWithHyperbrowserEndpointFailure())
58+
59+
assert exc_info.value.original_error is None
60+
61+
62+
def test_normalize_computer_action_endpoint_rejects_missing_endpoint():
63+
with pytest.raises(
64+
HyperbrowserError, match="Computer action endpoint not available for this session"
65+
):
66+
normalize_computer_action_endpoint(_Session(None))
67+
68+
69+
def test_normalize_computer_action_endpoint_rejects_non_string_endpoint():
70+
with pytest.raises(
71+
HyperbrowserError, match="session computer_action_endpoint must be a string"
72+
):
73+
normalize_computer_action_endpoint(_Session(123))
74+
75+
76+
def test_normalize_computer_action_endpoint_rejects_string_subclass_endpoint():
77+
class _EndpointString(str):
78+
pass
79+
80+
with pytest.raises(
81+
HyperbrowserError, match="session computer_action_endpoint must be a string"
82+
):
83+
normalize_computer_action_endpoint(_Session(_EndpointString("https://x.com")))
84+
85+
86+
def test_normalize_computer_action_endpoint_rejects_surrounding_whitespace():
87+
with pytest.raises(
88+
HyperbrowserError,
89+
match="session computer_action_endpoint must not contain leading or trailing whitespace",
90+
):
91+
normalize_computer_action_endpoint(_Session(" https://example.com/action "))
92+
93+
94+
def test_normalize_computer_action_endpoint_rejects_control_characters():
95+
with pytest.raises(
96+
HyperbrowserError,
97+
match="session computer_action_endpoint must not contain control characters",
98+
):
99+
normalize_computer_action_endpoint(_Session("https://example.com/\x07action"))

0 commit comments

Comments
 (0)