Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,21 @@ client = Hyperbrowser(api_key="test-key")
sandbox = client.sandboxes.create(
CreateSandboxParams(
image_name="node",
cpu=4,
memory_mib=4096,
disk_mib=8192,
exposed_ports=[SandboxExposeParams(port=3000, auth=True)],
)
)

print(sandbox.exposed_ports[0].browser_url)
print(sandbox.cpu, sandbox.memory_mib, sandbox.disk_mib)
sandbox.stop()
client.close()
```

`cpu`, `memory_mib`, and `disk_mib` are only supported for image launches.

### List sandboxes with filters

```python
Expand Down Expand Up @@ -164,7 +170,11 @@ from hyperbrowser import Hyperbrowser
from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams

client = Hyperbrowser(api_key="test-key")
sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
sandbox = client.sandboxes.create(
CreateSandboxParams(
image_name="node", cpu=2, memory_mib=2048, disk_mib=8192
)
)

result = sandbox.expose(SandboxExposeParams(port=8080, auth=True))
print(result.url, result.browser_url)
Expand Down
12 changes: 12 additions & 0 deletions hyperbrowser/client/managers/async_manager/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ def token_expires_at(self):
def session_url(self) -> str:
return self._detail.session_url

@property
def cpu(self):
return self._detail.cpu

@property
def memory_mib(self):
return self._detail.memory_mib

@property
def disk_mib(self):
return self._detail.disk_mib

@property
def exposed_ports(self):
return self._detail.exposed_ports
Expand Down
12 changes: 12 additions & 0 deletions hyperbrowser/client/managers/sync_manager/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ def token_expires_at(self):
def session_url(self) -> str:
return self._detail.session_url

@property
def cpu(self):
return self._detail.cpu

@property
def memory_mib(self):
return self._detail.memory_mib

@property
def disk_mib(self):
return self._detail.disk_mib

@property
def exposed_ports(self):
return self._detail.exposed_ports
Expand Down
17 changes: 17 additions & 0 deletions hyperbrowser/models/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ class Sandbox(SandboxBaseModel):
session_url: str = Field(alias="sessionUrl")
duration: int
proxy_bytes_used: Optional[int] = Field(default=None, alias="proxyBytesUsed")
cpu: Optional[int] = Field(default=None, alias="vcpus")
memory_mib: Optional[int] = Field(default=None, alias="memMiB")
disk_mib: Optional[int] = Field(default=None, alias="diskSizeMiB")
runtime: SandboxRuntimeTarget
exposed_ports: List[SandboxExposeResult] = Field(
default_factory=list,
Expand All @@ -128,6 +131,9 @@ class Sandbox(SandboxBaseModel):
"data_consumed",
"proxy_data_consumed",
"proxy_bytes_used",
"cpu",
"memory_mib",
"disk_mib",
mode="before",
)
@classmethod
Expand Down Expand Up @@ -177,6 +183,11 @@ class CreateSandboxParams(SandboxBaseModel):
timeout_minutes: Optional[int] = Field(
default=None, serialization_alias="timeoutMinutes"
)
cpu: Optional[int] = Field(default=None, ge=1, serialization_alias="vcpus")
memory_mib: Optional[int] = Field(default=None, ge=1, serialization_alias="memMiB")
disk_mib: Optional[int] = Field(
default=None, ge=1, serialization_alias="diskSizeMiB"
)

@model_validator(mode="after")
def validate_launch_source(self):
Expand All @@ -191,6 +202,12 @@ def validate_launch_source(self):
raise ValueError(
"Provide exactly one start source: snapshot_name or image_name"
)
if self.snapshot_name and any(
value is not None for value in (self.cpu, self.memory_mib, self.disk_mib)
):
raise ValueError(
"cpu, memory_mib, and disk_mib are only supported for image launches"
)
return self


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "hyperbrowser"
version = "0.89.1"
version = "0.89.2"
description = "Python SDK for hyperbrowser"
authors = ["Nikhil Shahi <nshahi1998@gmail.com>"]
license = "MIT"
Expand Down
122 changes: 122 additions & 0 deletions tests/sandbox/e2e/test_resource_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import pytest

from hyperbrowser.models import CreateSandboxParams

from tests.helpers.config import DEFAULT_IMAGE_NAME, create_async_client, create_client
from tests.helpers.sandbox import (
stop_sandbox_if_running,
stop_sandbox_if_running_async,
wait_for_runtime_ready,
wait_for_runtime_ready_async,
)

client = create_client()

REQUESTED_CPU = 8
REQUESTED_MEMORY_MIB = 8192
REQUESTED_DISK_MIB = 10240
MEMORY_MIN_VISIBLE_MIB = REQUESTED_MEMORY_MIB - 512
DISK_MIN_VISIBLE_MIB = REQUESTED_DISK_MIB - 512


def _exec_integer(sandbox, command: str) -> int:
result = sandbox.exec(command)
assert result.exit_code == 0
return int(result.stdout.strip())


async def _exec_integer_async(sandbox, command: str) -> int:
result = await sandbox.exec(command)
assert result.exit_code == 0
return int(result.stdout.strip())


def test_sandbox_resource_config_e2e():
sandbox = None

try:
sandbox = client.sandboxes.create(
CreateSandboxParams(
image_name=DEFAULT_IMAGE_NAME,
cpu=REQUESTED_CPU,
memory_mib=REQUESTED_MEMORY_MIB,
disk_mib=REQUESTED_DISK_MIB,
)
)

assert sandbox.cpu == REQUESTED_CPU
assert sandbox.memory_mib == REQUESTED_MEMORY_MIB
assert sandbox.disk_mib == REQUESTED_DISK_MIB

detail = sandbox.info()
assert detail.cpu == REQUESTED_CPU
assert detail.memory_mib == REQUESTED_MEMORY_MIB
assert detail.disk_mib == REQUESTED_DISK_MIB

reloaded = client.sandboxes.get(sandbox.id)
assert reloaded.cpu == REQUESTED_CPU
assert reloaded.memory_mib == REQUESTED_MEMORY_MIB
assert reloaded.disk_mib == REQUESTED_DISK_MIB

wait_for_runtime_ready(sandbox)

cpu_count = _exec_integer(sandbox, "nproc")
memory_mib = _exec_integer(
sandbox,
"awk '/MemTotal/ {printf \"%.0f\\n\", $2/1024}' /proc/meminfo",
)
disk_mib = _exec_integer(sandbox, "df -m / | awk 'NR==2 {print $2}'")

assert cpu_count == REQUESTED_CPU
assert MEMORY_MIN_VISIBLE_MIB <= memory_mib <= REQUESTED_MEMORY_MIB
assert DISK_MIN_VISIBLE_MIB <= disk_mib <= REQUESTED_DISK_MIB
finally:
stop_sandbox_if_running(sandbox)


@pytest.mark.anyio
async def test_async_sandbox_resource_config_e2e():
client = create_async_client()
sandbox = None

try:
sandbox = await client.sandboxes.create(
CreateSandboxParams(
image_name=DEFAULT_IMAGE_NAME,
cpu=REQUESTED_CPU,
memory_mib=REQUESTED_MEMORY_MIB,
disk_mib=REQUESTED_DISK_MIB,
)
)

assert sandbox.cpu == REQUESTED_CPU
assert sandbox.memory_mib == REQUESTED_MEMORY_MIB
assert sandbox.disk_mib == REQUESTED_DISK_MIB

detail = await sandbox.info()
assert detail.cpu == REQUESTED_CPU
assert detail.memory_mib == REQUESTED_MEMORY_MIB
assert detail.disk_mib == REQUESTED_DISK_MIB

reloaded = await client.sandboxes.get(sandbox.id)
assert reloaded.cpu == REQUESTED_CPU
assert reloaded.memory_mib == REQUESTED_MEMORY_MIB
assert reloaded.disk_mib == REQUESTED_DISK_MIB

await wait_for_runtime_ready_async(sandbox)

cpu_count = await _exec_integer_async(sandbox, "nproc")
memory_mib = await _exec_integer_async(
sandbox,
"awk '/MemTotal/ {printf \"%.0f\\n\", $2/1024}' /proc/meminfo",
)
disk_mib = await _exec_integer_async(
sandbox, "df -m / | awk 'NR==2 {print $2}'"
)

assert cpu_count == REQUESTED_CPU
assert MEMORY_MIN_VISIBLE_MIB <= memory_mib <= REQUESTED_MEMORY_MIB
assert DISK_MIN_VISIBLE_MIB <= disk_mib <= REQUESTED_DISK_MIB
finally:
await stop_sandbox_if_running_async(sandbox)
await client.close()
14 changes: 14 additions & 0 deletions tests/test_create_sandbox_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ def test_create_sandbox_params_accepts_image_source():
def test_create_sandbox_params_serializes_exposed_ports():
params = CreateSandboxParams(
image_name="node",
cpu=4,
memory_mib=4096,
disk_mib=8192,
exposed_ports=[SandboxExposeParams(port=3000, auth=True)],
)

assert params.model_dump(by_alias=True, exclude_none=True) == {
"imageName": "node",
"vcpus": 4,
"memMiB": 4096,
"diskSizeMiB": 8192,
"exposedPorts": [{"port": 3000, "auth": True}],
}

Expand Down Expand Up @@ -72,6 +78,14 @@ def test_create_sandbox_params_requires_snapshot_name_for_snapshot_id():
CreateSandboxParams(snapshot_id="snap-id")


def test_create_sandbox_params_rejects_resource_config_for_snapshot_source():
with pytest.raises(
ValidationError,
match="cpu, memory_mib, and disk_mib are only supported for image launches",
):
CreateSandboxParams(snapshot_name="snap", cpu=2, memory_mib=2048, disk_mib=8192)


def test_sandbox_exec_params_serialize_process_timeout_sec_as_snake_case():
params = SandboxExecParams(
command="echo hi",
Expand Down
Loading