Skip to content

Commit 84eb121

Browse files
Centralize computer action request and metadata handling
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 72e88ed commit 84eb121

11 files changed

Lines changed: 204 additions & 18 deletions

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ This runs lint, format checks, compile checks, tests, and package build.
8989
- `tests/test_browser_use_payload_helper_usage.py` (browser-use payload helper usage enforcement),
9090
- `tests/test_ci_workflow_quality_gates.py` (CI guard-stage + make-target enforcement),
9191
- `tests/test_computer_action_endpoint_helper_usage.py` (computer-action endpoint-normalization helper usage enforcement),
92+
- `tests/test_computer_action_operation_metadata_usage.py` (computer-action manager operation-metadata usage enforcement),
9293
- `tests/test_computer_action_payload_helper_usage.py` (computer-action payload helper usage enforcement),
94+
- `tests/test_computer_action_request_helper_usage.py` (computer-action manager request-helper usage enforcement),
9395
- `tests/test_contributing_architecture_guard_listing.py` (`CONTRIBUTING.md` architecture-guard inventory completeness enforcement),
9496
- `tests/test_core_type_helper_usage.py` (core transport/config/header/file/polling/session/error/parsing manager+tool module enforcement of shared plain-type helper usage),
9597
- `tests/test_default_serialization_helper_usage.py` (default optional-query serialization helper usage enforcement),

hyperbrowser/client/managers/async_manager/computer_action.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from typing import Union, List, Optional
22
from hyperbrowser.exceptions import HyperbrowserError
33
from hyperbrowser.type_utils import is_plain_string, is_string_subclass_instance
4+
from ..computer_action_operation_metadata import COMPUTER_ACTION_OPERATION_METADATA
5+
from ..computer_action_request_utils import execute_computer_action_request_async
46
from ..computer_action_utils import normalize_computer_action_endpoint
57
from ..computer_action_payload_utils import build_computer_action_payload
6-
from ..response_utils import parse_response_model
78
from hyperbrowser.models import (
89
SessionDetail,
910
ComputerActionParams,
@@ -26,6 +27,8 @@
2627

2728

2829
class ComputerActionManager:
30+
_OPERATION_METADATA = COMPUTER_ACTION_OPERATION_METADATA
31+
2932
def __init__(self, client):
3033
self._client = client
3134

@@ -45,14 +48,11 @@ async def _execute_request(
4548

4649
payload = build_computer_action_payload(params)
4750

48-
response = await self._client.transport.post(
49-
normalized_computer_action_endpoint,
50-
data=payload,
51-
)
52-
return parse_response_model(
53-
response.data,
54-
model=ComputerActionResponse,
55-
operation_name="computer action",
51+
return await execute_computer_action_request_async(
52+
client=self._client,
53+
endpoint=normalized_computer_action_endpoint,
54+
payload=payload,
55+
operation_name=self._OPERATION_METADATA.operation_name,
5656
)
5757

5858
async def click(
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass(frozen=True)
5+
class ComputerActionOperationMetadata:
6+
operation_name: str
7+
8+
9+
COMPUTER_ACTION_OPERATION_METADATA = ComputerActionOperationMetadata(
10+
operation_name="computer action",
11+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from typing import Any
2+
3+
from hyperbrowser.models import ComputerActionResponse
4+
5+
from .response_utils import parse_response_model
6+
7+
8+
def execute_computer_action_request(
9+
*,
10+
client: Any,
11+
endpoint: str,
12+
payload: dict[str, Any],
13+
operation_name: str,
14+
) -> ComputerActionResponse:
15+
response = client.transport.post(
16+
endpoint,
17+
data=payload,
18+
)
19+
return parse_response_model(
20+
response.data,
21+
model=ComputerActionResponse,
22+
operation_name=operation_name,
23+
)
24+
25+
26+
async def execute_computer_action_request_async(
27+
*,
28+
client: Any,
29+
endpoint: str,
30+
payload: dict[str, Any],
31+
operation_name: str,
32+
) -> ComputerActionResponse:
33+
response = await client.transport.post(
34+
endpoint,
35+
data=payload,
36+
)
37+
return parse_response_model(
38+
response.data,
39+
model=ComputerActionResponse,
40+
operation_name=operation_name,
41+
)

hyperbrowser/client/managers/sync_manager/computer_action.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from typing import Union, List, Optional
22
from hyperbrowser.exceptions import HyperbrowserError
33
from hyperbrowser.type_utils import is_plain_string, is_string_subclass_instance
4+
from ..computer_action_operation_metadata import COMPUTER_ACTION_OPERATION_METADATA
5+
from ..computer_action_request_utils import execute_computer_action_request
46
from ..computer_action_utils import normalize_computer_action_endpoint
57
from ..computer_action_payload_utils import build_computer_action_payload
6-
from ..response_utils import parse_response_model
78
from hyperbrowser.models import (
89
SessionDetail,
910
ComputerActionParams,
@@ -26,6 +27,8 @@
2627

2728

2829
class ComputerActionManager:
30+
_OPERATION_METADATA = COMPUTER_ACTION_OPERATION_METADATA
31+
2932
def __init__(self, client):
3033
self._client = client
3134

@@ -45,14 +48,11 @@ def _execute_request(
4548

4649
payload = build_computer_action_payload(params)
4750

48-
response = self._client.transport.post(
49-
normalized_computer_action_endpoint,
50-
data=payload,
51-
)
52-
return parse_response_model(
53-
response.data,
54-
model=ComputerActionResponse,
55-
operation_name="computer action",
51+
return execute_computer_action_request(
52+
client=self._client,
53+
endpoint=normalized_computer_action_endpoint,
54+
payload=payload,
55+
operation_name=self._OPERATION_METADATA.operation_name,
5656
)
5757

5858
def click(

tests/test_architecture_marker_usage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@
6565
"tests/test_example_sync_async_parity.py",
6666
"tests/test_example_run_instructions.py",
6767
"tests/test_computer_action_endpoint_helper_usage.py",
68+
"tests/test_computer_action_operation_metadata_usage.py",
6869
"tests/test_computer_action_payload_helper_usage.py",
70+
"tests/test_computer_action_request_helper_usage.py",
6971
"tests/test_schema_injection_helper_usage.py",
7072
"tests/test_session_operation_metadata_usage.py",
7173
"tests/test_session_route_constants_usage.py",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from hyperbrowser.client.managers.computer_action_operation_metadata import (
2+
COMPUTER_ACTION_OPERATION_METADATA,
3+
)
4+
5+
6+
def test_computer_action_operation_metadata_values():
7+
assert COMPUTER_ACTION_OPERATION_METADATA.operation_name == "computer action"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
pytestmark = pytest.mark.architecture
6+
7+
8+
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_operation_metadata():
15+
for module_path in MODULES:
16+
module_text = Path(module_path).read_text(encoding="utf-8")
17+
assert "computer_action_operation_metadata import" in module_text
18+
assert "_OPERATION_METADATA = " in module_text
19+
assert "operation_name=self._OPERATION_METADATA." in module_text
20+
assert 'operation_name="computer action"' not in module_text
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
pytestmark = pytest.mark.architecture
6+
7+
8+
def test_sync_computer_action_manager_uses_request_helper():
9+
module_text = Path(
10+
"hyperbrowser/client/managers/sync_manager/computer_action.py"
11+
).read_text(encoding="utf-8")
12+
assert "execute_computer_action_request(" in module_text
13+
assert "_client.transport.post(" not in module_text
14+
assert "parse_response_model(" not in module_text
15+
16+
17+
def test_async_computer_action_manager_uses_request_helper():
18+
module_text = Path(
19+
"hyperbrowser/client/managers/async_manager/computer_action.py"
20+
).read_text(encoding="utf-8")
21+
assert "execute_computer_action_request_async(" in module_text
22+
assert "_client.transport.post(" not in module_text
23+
assert "parse_response_model(" not in module_text
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import asyncio
2+
from types import SimpleNamespace
3+
4+
import hyperbrowser.client.managers.computer_action_request_utils as request_utils
5+
6+
7+
def test_execute_computer_action_request_posts_and_parses_response():
8+
captured = {}
9+
10+
class _SyncTransport:
11+
def post(self, endpoint, data=None):
12+
captured["endpoint"] = endpoint
13+
captured["data"] = data
14+
return SimpleNamespace(data={"success": True})
15+
16+
class _Client:
17+
transport = _SyncTransport()
18+
19+
def _fake_parse_response_model(data, **kwargs):
20+
captured["parse_data"] = data
21+
captured["parse_kwargs"] = kwargs
22+
return {"parsed": True}
23+
24+
original_parse = request_utils.parse_response_model
25+
request_utils.parse_response_model = _fake_parse_response_model
26+
try:
27+
result = request_utils.execute_computer_action_request(
28+
client=_Client(),
29+
endpoint="https://example.com/cua",
30+
payload={"action": {"type": "screenshot"}},
31+
operation_name="computer action",
32+
)
33+
finally:
34+
request_utils.parse_response_model = original_parse
35+
36+
assert result == {"parsed": True}
37+
assert captured["endpoint"] == "https://example.com/cua"
38+
assert captured["data"] == {"action": {"type": "screenshot"}}
39+
assert captured["parse_data"] == {"success": True}
40+
assert captured["parse_kwargs"]["operation_name"] == "computer action"
41+
42+
43+
def test_execute_computer_action_request_async_posts_and_parses_response():
44+
captured = {}
45+
46+
class _AsyncTransport:
47+
async def post(self, endpoint, data=None):
48+
captured["endpoint"] = endpoint
49+
captured["data"] = data
50+
return SimpleNamespace(data={"success": True})
51+
52+
class _Client:
53+
transport = _AsyncTransport()
54+
55+
def _fake_parse_response_model(data, **kwargs):
56+
captured["parse_data"] = data
57+
captured["parse_kwargs"] = kwargs
58+
return {"parsed": True}
59+
60+
original_parse = request_utils.parse_response_model
61+
request_utils.parse_response_model = _fake_parse_response_model
62+
try:
63+
result = asyncio.run(
64+
request_utils.execute_computer_action_request_async(
65+
client=_Client(),
66+
endpoint="https://example.com/cua",
67+
payload={"action": {"type": "screenshot"}},
68+
operation_name="computer action",
69+
)
70+
)
71+
finally:
72+
request_utils.parse_response_model = original_parse
73+
74+
assert result == {"parsed": True}
75+
assert captured["endpoint"] == "https://example.com/cua"
76+
assert captured["data"] == {"action": {"type": "screenshot"}}
77+
assert captured["parse_data"] == {"success": True}
78+
assert captured["parse_kwargs"]["operation_name"] == "computer action"

0 commit comments

Comments
 (0)