Skip to content

Commit ba7ea1c

Browse files
Reuse model request helpers for extension create flow
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent f1f09b9 commit ba7ea1c

File tree

7 files changed

+153
-69
lines changed

7 files changed

+153
-69
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ This runs lint, format checks, compile checks, tests, and package build.
107107
- `tests/test_extension_create_helper_usage.py` (extension create-input normalization helper usage enforcement),
108108
- `tests/test_extension_operation_metadata_usage.py` (extension manager operation-metadata usage enforcement),
109109
- `tests/test_extension_request_helper_usage.py` (extension manager request-helper usage enforcement),
110+
- `tests/test_extension_request_internal_reuse.py` (extension request-helper internal reuse of shared model request helpers),
110111
- `tests/test_extension_route_constants_usage.py` (extension manager route-constant usage enforcement),
111112
- `tests/test_extract_payload_helper_usage.py` (extract start-payload helper usage enforcement),
112113
- `tests/test_guardrail_ast_utils.py` (shared AST guard utility contract),

hyperbrowser/client/managers/extension_request_utils.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, IO, List, Type, TypeVar
22

33
from .extension_utils import parse_extension_list_response_data
4-
from .response_utils import parse_response_model
4+
from .model_request_utils import post_model_request, post_model_request_async
55
from hyperbrowser.models.extension import ExtensionResponse
66

77
T = TypeVar("T")
@@ -16,13 +16,11 @@ def create_extension_resource(
1616
model: Type[T],
1717
operation_name: str,
1818
) -> T:
19-
response = client.transport.post(
20-
client._build_url(route_path),
19+
return post_model_request(
20+
client=client,
21+
route_path=route_path,
2122
data=payload,
2223
files={"file": file_stream},
23-
)
24-
return parse_response_model(
25-
response.data,
2624
model=model,
2725
operation_name=operation_name,
2826
)
@@ -48,13 +46,11 @@ async def create_extension_resource_async(
4846
model: Type[T],
4947
operation_name: str,
5048
) -> T:
51-
response = await client.transport.post(
52-
client._build_url(route_path),
49+
return await post_model_request_async(
50+
client=client,
51+
route_path=route_path,
5352
data=payload,
5453
files={"file": file_stream},
55-
)
56-
return parse_response_model(
57-
response.data,
5854
model=model,
5955
operation_name=operation_name,
6056
)

hyperbrowser/client/managers/model_request_utils.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@ def post_model_request(
1010
client: Any,
1111
route_path: str,
1212
data: Dict[str, Any],
13+
files: Optional[Dict[str, Any]] = None,
1314
model: Type[T],
1415
operation_name: str,
1516
) -> T:
16-
response = client.transport.post(
17-
client._build_url(route_path),
18-
data=data,
19-
)
17+
if files is None:
18+
response = client.transport.post(
19+
client._build_url(route_path),
20+
data=data,
21+
)
22+
else:
23+
response = client.transport.post(
24+
client._build_url(route_path),
25+
data=data,
26+
files=files,
27+
)
2028
return parse_response_model(
2129
response.data,
2230
model=model,
@@ -84,13 +92,21 @@ async def post_model_request_async(
8492
client: Any,
8593
route_path: str,
8694
data: Dict[str, Any],
95+
files: Optional[Dict[str, Any]] = None,
8796
model: Type[T],
8897
operation_name: str,
8998
) -> T:
90-
response = await client.transport.post(
91-
client._build_url(route_path),
92-
data=data,
93-
)
99+
if files is None:
100+
response = await client.transport.post(
101+
client._build_url(route_path),
102+
data=data,
103+
)
104+
else:
105+
response = await client.transport.post(
106+
client._build_url(route_path),
107+
data=data,
108+
files=files,
109+
)
94110
return parse_response_model(
95111
response.data,
96112
model=model,

tests/test_architecture_marker_usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"tests/test_examples_naming_convention.py",
5757
"tests/test_extension_operation_metadata_usage.py",
5858
"tests/test_extension_request_helper_usage.py",
59+
"tests/test_extension_request_internal_reuse.py",
5960
"tests/test_extension_route_constants_usage.py",
6061
"tests/test_job_pagination_helper_usage.py",
6162
"tests/test_job_fetch_helper_boundary.py",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
pytestmark = pytest.mark.architecture
6+
7+
8+
def test_extension_request_utils_reuse_model_request_helpers():
9+
module_text = Path(
10+
"hyperbrowser/client/managers/extension_request_utils.py"
11+
).read_text(encoding="utf-8")
12+
assert "model_request_utils import post_model_request, post_model_request_async" in module_text
13+
assert "post_model_request(" in module_text
14+
assert "post_model_request_async(" in module_text
15+
assert "client.transport.post(" not in module_text
16+
assert "parse_response_model(" not in module_text

tests/test_extension_request_utils.py

Lines changed: 18 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,33 @@
55
import hyperbrowser.client.managers.extension_request_utils as extension_request_utils
66

77

8-
def test_create_extension_resource_uses_post_and_parses_response():
8+
def test_create_extension_resource_delegates_to_post_model_request():
99
captured = {}
1010

11-
class _SyncTransport:
12-
def post(self, url, data=None, files=None):
13-
captured["url"] = url
14-
captured["data"] = data
15-
captured["files"] = files
16-
return SimpleNamespace(data={"id": "ext_1"})
17-
18-
class _Client:
19-
transport = _SyncTransport()
20-
21-
@staticmethod
22-
def _build_url(path: str) -> str:
23-
return f"https://api.example.test{path}"
24-
25-
def _fake_parse_response_model(data, **kwargs):
26-
captured["parse_data"] = data
27-
captured["parse_kwargs"] = kwargs
11+
def _fake_post_model_request(**kwargs):
12+
captured.update(kwargs)
2813
return {"parsed": True}
2914

30-
original_parse = extension_request_utils.parse_response_model
31-
extension_request_utils.parse_response_model = _fake_parse_response_model
15+
original = extension_request_utils.post_model_request
16+
extension_request_utils.post_model_request = _fake_post_model_request
3217
try:
3318
file_stream = BytesIO(b"ext")
3419
result = extension_request_utils.create_extension_resource(
35-
client=_Client(),
20+
client=object(),
3621
route_path="/extensions/add",
3722
payload={"name": "ext"},
3823
file_stream=file_stream,
3924
model=object,
4025
operation_name="create extension",
4126
)
4227
finally:
43-
extension_request_utils.parse_response_model = original_parse
28+
extension_request_utils.post_model_request = original
4429

4530
assert result == {"parsed": True}
46-
assert captured["url"] == "https://api.example.test/extensions/add"
31+
assert captured["route_path"] == "/extensions/add"
4732
assert captured["data"] == {"name": "ext"}
4833
assert captured["files"] == {"file": file_stream}
49-
assert captured["parse_data"] == {"id": "ext_1"}
50-
assert captured["parse_kwargs"]["operation_name"] == "create extension"
34+
assert captured["operation_name"] == "create extension"
5135

5236

5337
def test_list_extension_resources_uses_get_and_extension_parser():
@@ -86,35 +70,20 @@ def _fake_parse_extension_list_response_data(data):
8670
assert captured["parse_data"] == {"extensions": []}
8771

8872

89-
def test_create_extension_resource_async_uses_post_and_parses_response():
73+
def test_create_extension_resource_async_delegates_to_post_model_request_async():
9074
captured = {}
9175

92-
class _AsyncTransport:
93-
async def post(self, url, data=None, files=None):
94-
captured["url"] = url
95-
captured["data"] = data
96-
captured["files"] = files
97-
return SimpleNamespace(data={"id": "ext_2"})
98-
99-
class _Client:
100-
transport = _AsyncTransport()
101-
102-
@staticmethod
103-
def _build_url(path: str) -> str:
104-
return f"https://api.example.test{path}"
105-
106-
def _fake_parse_response_model(data, **kwargs):
107-
captured["parse_data"] = data
108-
captured["parse_kwargs"] = kwargs
76+
async def _fake_post_model_request_async(**kwargs):
77+
captured.update(kwargs)
10978
return {"parsed": True}
11079

111-
original_parse = extension_request_utils.parse_response_model
112-
extension_request_utils.parse_response_model = _fake_parse_response_model
80+
original = extension_request_utils.post_model_request_async
81+
extension_request_utils.post_model_request_async = _fake_post_model_request_async
11382
try:
11483
file_stream = BytesIO(b"ext")
11584
result = asyncio.run(
11685
extension_request_utils.create_extension_resource_async(
117-
client=_Client(),
86+
client=object(),
11887
route_path="/extensions/add",
11988
payload={"name": "ext"},
12089
file_stream=file_stream,
@@ -123,14 +92,13 @@ def _fake_parse_response_model(data, **kwargs):
12392
)
12493
)
12594
finally:
126-
extension_request_utils.parse_response_model = original_parse
95+
extension_request_utils.post_model_request_async = original
12796

12897
assert result == {"parsed": True}
129-
assert captured["url"] == "https://api.example.test/extensions/add"
98+
assert captured["route_path"] == "/extensions/add"
13099
assert captured["data"] == {"name": "ext"}
131100
assert captured["files"] == {"file": file_stream}
132-
assert captured["parse_data"] == {"id": "ext_2"}
133-
assert captured["parse_kwargs"]["operation_name"] == "create extension"
101+
assert captured["operation_name"] == "create extension"
134102

135103

136104
def test_list_extension_resources_async_uses_get_and_extension_parser():

tests/test_model_request_utils.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,48 @@ def _fake_parse_response_model(data, **kwargs):
4545
assert captured["parse_kwargs"]["operation_name"] == "create resource"
4646

4747

48+
def test_post_model_request_forwards_files_when_provided():
49+
captured = {}
50+
51+
class _SyncTransport:
52+
def post(self, url, data, files=None):
53+
captured["url"] = url
54+
captured["data"] = data
55+
captured["files"] = files
56+
return SimpleNamespace(data={"id": "resource-file"})
57+
58+
class _Client:
59+
transport = _SyncTransport()
60+
61+
@staticmethod
62+
def _build_url(path: str) -> str:
63+
return f"https://api.example.test{path}"
64+
65+
def _fake_parse_response_model(data, **kwargs):
66+
captured["parse_data"] = data
67+
captured["parse_kwargs"] = kwargs
68+
return {"parsed": True}
69+
70+
original_parse = model_request_utils.parse_response_model
71+
model_request_utils.parse_response_model = _fake_parse_response_model
72+
try:
73+
result = model_request_utils.post_model_request(
74+
client=_Client(),
75+
route_path="/resource",
76+
data={"name": "value"},
77+
files={"file": object()},
78+
model=object,
79+
operation_name="create resource",
80+
)
81+
finally:
82+
model_request_utils.parse_response_model = original_parse
83+
84+
assert result == {"parsed": True}
85+
assert captured["url"] == "https://api.example.test/resource"
86+
assert captured["files"] is not None
87+
assert captured["parse_data"] == {"id": "resource-file"}
88+
89+
4890
def test_get_model_request_gets_payload_and_parses_response():
4991
captured = {}
5092

@@ -167,6 +209,50 @@ def _fake_parse_response_model(data, **kwargs):
167209
assert captured["parse_kwargs"]["operation_name"] == "create resource"
168210

169211

212+
def test_post_model_request_async_forwards_files_when_provided():
213+
captured = {}
214+
215+
class _AsyncTransport:
216+
async def post(self, url, data, files=None):
217+
captured["url"] = url
218+
captured["data"] = data
219+
captured["files"] = files
220+
return SimpleNamespace(data={"id": "resource-file-async"})
221+
222+
class _Client:
223+
transport = _AsyncTransport()
224+
225+
@staticmethod
226+
def _build_url(path: str) -> str:
227+
return f"https://api.example.test{path}"
228+
229+
def _fake_parse_response_model(data, **kwargs):
230+
captured["parse_data"] = data
231+
captured["parse_kwargs"] = kwargs
232+
return {"parsed": True}
233+
234+
original_parse = model_request_utils.parse_response_model
235+
model_request_utils.parse_response_model = _fake_parse_response_model
236+
try:
237+
result = asyncio.run(
238+
model_request_utils.post_model_request_async(
239+
client=_Client(),
240+
route_path="/resource",
241+
data={"name": "value"},
242+
files={"file": object()},
243+
model=object,
244+
operation_name="create resource",
245+
)
246+
)
247+
finally:
248+
model_request_utils.parse_response_model = original_parse
249+
250+
assert result == {"parsed": True}
251+
assert captured["url"] == "https://api.example.test/resource"
252+
assert captured["files"] is not None
253+
assert captured["parse_data"] == {"id": "resource-file-async"}
254+
255+
170256
def test_get_model_request_async_gets_payload_and_parses_response():
171257
captured = {}
172258

0 commit comments

Comments
 (0)