Skip to content

Commit 9bfdc9f

Browse files
authored
Merge pull request #67 from hyperbrowserai/sandbox-config
add vcpu, memory, disk config
2 parents e1ee18d + d13818b commit 9bfdc9f

8 files changed

Lines changed: 231 additions & 6 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,21 @@ client = Hyperbrowser(api_key="test-key")
115115
sandbox = client.sandboxes.create(
116116
CreateSandboxParams(
117117
image_name="node",
118+
cpu=4,
119+
memory_mib=4096,
120+
disk_mib=8192,
118121
exposed_ports=[SandboxExposeParams(port=3000, auth=True)],
119122
)
120123
)
121124

122125
print(sandbox.exposed_ports[0].browser_url)
126+
print(sandbox.cpu, sandbox.memory_mib, sandbox.disk_mib)
123127
sandbox.stop()
124128
client.close()
125129
```
126130

131+
`cpu`, `memory_mib`, and `disk_mib` are only supported for image launches.
132+
127133
### List sandboxes with filters
128134

129135
```python
@@ -164,7 +170,11 @@ from hyperbrowser import Hyperbrowser
164170
from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams
165171

166172
client = Hyperbrowser(api_key="test-key")
167-
sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
173+
sandbox = client.sandboxes.create(
174+
CreateSandboxParams(
175+
image_name="node", cpu=2, memory_mib=2048, disk_mib=8192
176+
)
177+
)
168178

169179
result = sandbox.expose(SandboxExposeParams(port=8080, auth=True))
170180
print(result.url, result.browser_url)

hyperbrowser/client/managers/async_manager/sandbox.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ def token_expires_at(self):
114114
def session_url(self) -> str:
115115
return self._detail.session_url
116116

117+
@property
118+
def cpu(self):
119+
return self._detail.cpu
120+
121+
@property
122+
def memory_mib(self):
123+
return self._detail.memory_mib
124+
125+
@property
126+
def disk_mib(self):
127+
return self._detail.disk_mib
128+
117129
@property
118130
def exposed_ports(self):
119131
return self._detail.exposed_ports

hyperbrowser/client/managers/sync_manager/sandbox.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ def token_expires_at(self):
114114
def session_url(self) -> str:
115115
return self._detail.session_url
116116

117+
@property
118+
def cpu(self):
119+
return self._detail.cpu
120+
121+
@property
122+
def memory_mib(self):
123+
return self._detail.memory_mib
124+
125+
@property
126+
def disk_mib(self):
127+
return self._detail.disk_mib
128+
117129
@property
118130
def exposed_ports(self):
119131
return self._detail.exposed_ports

hyperbrowser/models/sandbox.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ class Sandbox(SandboxBaseModel):
116116
session_url: str = Field(alias="sessionUrl")
117117
duration: int
118118
proxy_bytes_used: Optional[int] = Field(default=None, alias="proxyBytesUsed")
119+
cpu: Optional[int] = Field(default=None, alias="vcpus")
120+
memory_mib: Optional[int] = Field(default=None, alias="memMiB")
121+
disk_mib: Optional[int] = Field(default=None, alias="diskSizeMiB")
119122
runtime: SandboxRuntimeTarget
120123
exposed_ports: List[SandboxExposeResult] = Field(
121124
default_factory=list,
@@ -128,6 +131,9 @@ class Sandbox(SandboxBaseModel):
128131
"data_consumed",
129132
"proxy_data_consumed",
130133
"proxy_bytes_used",
134+
"cpu",
135+
"memory_mib",
136+
"disk_mib",
131137
mode="before",
132138
)
133139
@classmethod
@@ -177,6 +183,11 @@ class CreateSandboxParams(SandboxBaseModel):
177183
timeout_minutes: Optional[int] = Field(
178184
default=None, serialization_alias="timeoutMinutes"
179185
)
186+
cpu: Optional[int] = Field(default=None, ge=1, serialization_alias="vcpus")
187+
memory_mib: Optional[int] = Field(default=None, ge=1, serialization_alias="memMiB")
188+
disk_mib: Optional[int] = Field(
189+
default=None, ge=1, serialization_alias="diskSizeMiB"
190+
)
180191

181192
@model_validator(mode="after")
182193
def validate_launch_source(self):
@@ -191,6 +202,12 @@ def validate_launch_source(self):
191202
raise ValueError(
192203
"Provide exactly one start source: snapshot_name or image_name"
193204
)
205+
if self.snapshot_name and any(
206+
value is not None for value in (self.cpu, self.memory_mib, self.disk_mib)
207+
):
208+
raise ValueError(
209+
"cpu, memory_mib, and disk_mib are only supported for image launches"
210+
)
194211
return self
195212

196213

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "hyperbrowser"
3-
version = "0.89.1"
3+
version = "0.89.2"
44
description = "Python SDK for hyperbrowser"
55
authors = ["Nikhil Shahi <nshahi1998@gmail.com>"]
66
license = "MIT"
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import pytest
2+
3+
from hyperbrowser.models import CreateSandboxParams
4+
5+
from tests.helpers.config import DEFAULT_IMAGE_NAME, create_async_client, create_client
6+
from tests.helpers.sandbox import (
7+
stop_sandbox_if_running,
8+
stop_sandbox_if_running_async,
9+
wait_for_runtime_ready,
10+
wait_for_runtime_ready_async,
11+
)
12+
13+
client = create_client()
14+
15+
REQUESTED_CPU = 8
16+
REQUESTED_MEMORY_MIB = 8192
17+
REQUESTED_DISK_MIB = 10240
18+
MEMORY_MIN_VISIBLE_MIB = REQUESTED_MEMORY_MIB - 512
19+
DISK_MIN_VISIBLE_MIB = REQUESTED_DISK_MIB - 512
20+
21+
22+
def _exec_integer(sandbox, command: str) -> int:
23+
result = sandbox.exec(command)
24+
assert result.exit_code == 0
25+
return int(result.stdout.strip())
26+
27+
28+
async def _exec_integer_async(sandbox, command: str) -> int:
29+
result = await sandbox.exec(command)
30+
assert result.exit_code == 0
31+
return int(result.stdout.strip())
32+
33+
34+
def test_sandbox_resource_config_e2e():
35+
sandbox = None
36+
37+
try:
38+
sandbox = client.sandboxes.create(
39+
CreateSandboxParams(
40+
image_name=DEFAULT_IMAGE_NAME,
41+
cpu=REQUESTED_CPU,
42+
memory_mib=REQUESTED_MEMORY_MIB,
43+
disk_mib=REQUESTED_DISK_MIB,
44+
)
45+
)
46+
47+
assert sandbox.cpu == REQUESTED_CPU
48+
assert sandbox.memory_mib == REQUESTED_MEMORY_MIB
49+
assert sandbox.disk_mib == REQUESTED_DISK_MIB
50+
51+
detail = sandbox.info()
52+
assert detail.cpu == REQUESTED_CPU
53+
assert detail.memory_mib == REQUESTED_MEMORY_MIB
54+
assert detail.disk_mib == REQUESTED_DISK_MIB
55+
56+
reloaded = client.sandboxes.get(sandbox.id)
57+
assert reloaded.cpu == REQUESTED_CPU
58+
assert reloaded.memory_mib == REQUESTED_MEMORY_MIB
59+
assert reloaded.disk_mib == REQUESTED_DISK_MIB
60+
61+
wait_for_runtime_ready(sandbox)
62+
63+
cpu_count = _exec_integer(sandbox, "nproc")
64+
memory_mib = _exec_integer(
65+
sandbox,
66+
"awk '/MemTotal/ {printf \"%.0f\\n\", $2/1024}' /proc/meminfo",
67+
)
68+
disk_mib = _exec_integer(sandbox, "df -m / | awk 'NR==2 {print $2}'")
69+
70+
assert cpu_count == REQUESTED_CPU
71+
assert MEMORY_MIN_VISIBLE_MIB <= memory_mib <= REQUESTED_MEMORY_MIB
72+
assert DISK_MIN_VISIBLE_MIB <= disk_mib <= REQUESTED_DISK_MIB
73+
finally:
74+
stop_sandbox_if_running(sandbox)
75+
76+
77+
@pytest.mark.anyio
78+
async def test_async_sandbox_resource_config_e2e():
79+
client = create_async_client()
80+
sandbox = None
81+
82+
try:
83+
sandbox = await client.sandboxes.create(
84+
CreateSandboxParams(
85+
image_name=DEFAULT_IMAGE_NAME,
86+
cpu=REQUESTED_CPU,
87+
memory_mib=REQUESTED_MEMORY_MIB,
88+
disk_mib=REQUESTED_DISK_MIB,
89+
)
90+
)
91+
92+
assert sandbox.cpu == REQUESTED_CPU
93+
assert sandbox.memory_mib == REQUESTED_MEMORY_MIB
94+
assert sandbox.disk_mib == REQUESTED_DISK_MIB
95+
96+
detail = await sandbox.info()
97+
assert detail.cpu == REQUESTED_CPU
98+
assert detail.memory_mib == REQUESTED_MEMORY_MIB
99+
assert detail.disk_mib == REQUESTED_DISK_MIB
100+
101+
reloaded = await client.sandboxes.get(sandbox.id)
102+
assert reloaded.cpu == REQUESTED_CPU
103+
assert reloaded.memory_mib == REQUESTED_MEMORY_MIB
104+
assert reloaded.disk_mib == REQUESTED_DISK_MIB
105+
106+
await wait_for_runtime_ready_async(sandbox)
107+
108+
cpu_count = await _exec_integer_async(sandbox, "nproc")
109+
memory_mib = await _exec_integer_async(
110+
sandbox,
111+
"awk '/MemTotal/ {printf \"%.0f\\n\", $2/1024}' /proc/meminfo",
112+
)
113+
disk_mib = await _exec_integer_async(
114+
sandbox, "df -m / | awk 'NR==2 {print $2}'"
115+
)
116+
117+
assert cpu_count == REQUESTED_CPU
118+
assert MEMORY_MIN_VISIBLE_MIB <= memory_mib <= REQUESTED_MEMORY_MIB
119+
assert DISK_MIN_VISIBLE_MIB <= disk_mib <= REQUESTED_DISK_MIB
120+
finally:
121+
await stop_sandbox_if_running_async(sandbox)
122+
await client.close()

tests/test_create_sandbox_params.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ def test_create_sandbox_params_accepts_image_source():
2121
def test_create_sandbox_params_serializes_exposed_ports():
2222
params = CreateSandboxParams(
2323
image_name="node",
24+
cpu=4,
25+
memory_mib=4096,
26+
disk_mib=8192,
2427
exposed_ports=[SandboxExposeParams(port=3000, auth=True)],
2528
)
2629

2730
assert params.model_dump(by_alias=True, exclude_none=True) == {
2831
"imageName": "node",
32+
"vcpus": 4,
33+
"memMiB": 4096,
34+
"diskSizeMiB": 8192,
2935
"exposedPorts": [{"port": 3000, "auth": True}],
3036
}
3137

@@ -72,6 +78,14 @@ def test_create_sandbox_params_requires_snapshot_name_for_snapshot_id():
7278
CreateSandboxParams(snapshot_id="snap-id")
7379

7480

81+
def test_create_sandbox_params_rejects_resource_config_for_snapshot_source():
82+
with pytest.raises(
83+
ValidationError,
84+
match="cpu, memory_mib, and disk_mib are only supported for image launches",
85+
):
86+
CreateSandboxParams(snapshot_name="snap", cpu=2, memory_mib=2048, disk_mib=8192)
87+
88+
7589
def test_sandbox_exec_params_serialize_process_timeout_sec_as_snake_case():
7690
params = SandboxExecParams(
7791
command="echo hi",

0 commit comments

Comments
 (0)