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
4 changes: 2 additions & 2 deletions leap0/_async/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
DEFAULT_MEMORY_MIB,
DEFAULT_SANDBOX_DOMAIN,
DEFAULT_TEMPLATE_NAME,
DEFAULT_TIMEOUT_MIN,
DEFAULT_TIMEOUT,
DEFAULT_VCPU,
Leap0Config,
)
Expand Down Expand Up @@ -86,7 +86,7 @@ class AsyncLeap0Client:
DEFAULT_DESKTOP_TEMPLATE_NAME = DEFAULT_DESKTOP_TEMPLATE_NAME
DEFAULT_VCPU = DEFAULT_VCPU
DEFAULT_MEMORY_MIB = DEFAULT_MEMORY_MIB
DEFAULT_TIMEOUT_MIN = DEFAULT_TIMEOUT_MIN
DEFAULT_TIMEOUT = DEFAULT_TIMEOUT

_tracer_provider: TracerProvider | None = None
_meter_provider: MeterProvider | None = None
Expand Down
14 changes: 7 additions & 7 deletions leap0/_async/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ..models.config import (
DEFAULT_MEMORY_MIB,
DEFAULT_TEMPLATE_NAME,
DEFAULT_TIMEOUT_MIN,
DEFAULT_TIMEOUT,
DEFAULT_VCPU,
)
from ..models.sandbox import (
Expand Down Expand Up @@ -231,8 +231,8 @@ async def create(
*,
template_name: str = DEFAULT_TEMPLATE_NAME,
vcpu: int = DEFAULT_VCPU,
memory_mib: int = DEFAULT_MEMORY_MIB,
timeout_min: int = DEFAULT_TIMEOUT_MIN,
memory: int = DEFAULT_MEMORY_MIB,
timeout: int = DEFAULT_TIMEOUT,
auto_pause: bool = False,
otel_export: bool = False,
env_vars: dict[str, str] | None = None,
Expand All @@ -244,8 +244,8 @@ async def create(
Args:
template_name: Name of the template to use.
vcpu: Number of virtual CPUs (1 to 8).
memory_mib: Memory in MiB (512 to 8192, must be even).
timeout_min: Sandbox timeout in minutes (1 to 480).
memory: Memory in MiB (512 to 8192, must be even).
timeout: Sandbox timeout in seconds (1 to 28800).
auto_pause: Whether the sandbox should auto-pause on timeout.
otel_export: Inject OpenTelemetry exporter environment into the sandbox.
Requires ``OTEL_EXPORTER_OTLP_ENDPOINT`` in the local environment and
Expand All @@ -260,8 +260,8 @@ async def create(
params = CreateSandboxParams(
template_name=template_name,
vcpu=vcpu,
memory_mib=memory_mib,
timeout_min=timeout_min,
memory=memory,
timeout=timeout,
auto_pause=auto_pause,
otel_export=otel_export,
env_vars=_inject_otel_env(env_vars) if otel_export else env_vars,
Expand Down
6 changes: 3 additions & 3 deletions leap0/_async/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ async def resume(
*,
snapshot_name: str,
auto_pause: bool = False,
timeout_min: int | None = None,
timeout: int | None = None,
network_policy: NetworkPolicyDict | None = None,
http_timeout: float | None = None,
) -> AsyncSnapshotSandboxT | Sandbox:
Expand All @@ -154,7 +154,7 @@ async def resume(
Args:
snapshot_name: Name of the snapshot to restore.
auto_pause: Automatically pause the restored sandbox on timeout.
timeout_min: Sandbox timeout in minutes.
timeout: Sandbox timeout in seconds.
network_policy: Override the network policy from the snapshot.

http_timeout: Optional HTTP request timeout in seconds for this SDK call.
Expand All @@ -168,7 +168,7 @@ async def resume(
payload = ResumeSnapshotParams(
snapshot_name=snapshot_name,
auto_pause=auto_pause,
timeout_min=timeout_min,
timeout=timeout,
network_policy=network_policy,
).to_payload()
data = cast(SandboxCreateResponseDict, await self._transport.request_json(
Expand Down
10 changes: 6 additions & 4 deletions leap0/_schemas/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ class SandboxCreateResponseDict(TypedDict):
id: str
template_id: str
vcpu: int
memory_mib: int
disk_mib: int
memory: int
disk: int
timeout: int
state: SandboxState | str
auto_pause: bool
created_at: str
Expand All @@ -36,8 +37,9 @@ class SandboxStatusResponseDict(TypedDict):
id: str
template_id: str
vcpu: int
memory_mib: int
disk_mib: int
memory: int
disk: int
timeout: int
state: SandboxState | str
auto_pause: bool
created_at: str
Expand Down
4 changes: 2 additions & 2 deletions leap0/_schemas/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class SnapshotCreateResponseDict(TypedDict, total=False):
name: str
template_id: str
vcpu: int
memory_mib: int
disk_mib: int
memory: int
disk: int
state: SandboxState | str
created_at: str
network_policy: NetworkPolicyDict | None
Expand Down
4 changes: 2 additions & 2 deletions leap0/_sync/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
DEFAULT_MEMORY_MIB,
DEFAULT_SANDBOX_DOMAIN,
DEFAULT_TEMPLATE_NAME,
DEFAULT_TIMEOUT_MIN,
DEFAULT_TIMEOUT,
DEFAULT_VCPU,
Leap0Config,
)
Expand Down Expand Up @@ -69,7 +69,7 @@ class Leap0Client:
DEFAULT_DESKTOP_TEMPLATE_NAME = DEFAULT_DESKTOP_TEMPLATE_NAME
DEFAULT_VCPU = DEFAULT_VCPU
DEFAULT_MEMORY_MIB = DEFAULT_MEMORY_MIB
DEFAULT_TIMEOUT_MIN = DEFAULT_TIMEOUT_MIN
DEFAULT_TIMEOUT = DEFAULT_TIMEOUT

_tracer_provider: TracerProvider | None = None
_meter_provider: MeterProvider | None = None
Expand Down
14 changes: 7 additions & 7 deletions leap0/_sync/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ..models.config import (
DEFAULT_MEMORY_MIB,
DEFAULT_TEMPLATE_NAME,
DEFAULT_TIMEOUT_MIN,
DEFAULT_TIMEOUT,
DEFAULT_VCPU,
)
from ..models.sandbox import (
Expand Down Expand Up @@ -232,8 +232,8 @@ def create(
*,
template_name: str = DEFAULT_TEMPLATE_NAME,
vcpu: int = DEFAULT_VCPU,
memory_mib: int = DEFAULT_MEMORY_MIB,
timeout_min: int = DEFAULT_TIMEOUT_MIN,
memory: int = DEFAULT_MEMORY_MIB,
timeout: int = DEFAULT_TIMEOUT,
auto_pause: bool = False,
otel_export: bool | None = None,
telemetry: bool | None = None,
Expand All @@ -246,8 +246,8 @@ def create(
Args:
template_name: Name of the template to use.
vcpu: Number of virtual CPUs (1 to 8).
memory_mib: Memory in MiB (512 to 8192, must be even).
timeout_min: Sandbox timeout in minutes (1 to 480, default 5).
memory: Memory in MiB (512 to 8192, must be even).
timeout: Sandbox timeout in seconds (1 to 28800, default 300).
auto_pause: Automatically pause the sandbox into a snapshot on timeout.
otel_export: Inject OpenTelemetry exporter environment into the sandbox.
Requires ``OTEL_EXPORTER_OTLP_ENDPOINT`` in the local environment and
Expand All @@ -265,8 +265,8 @@ def create(
params = CreateSandboxParams(
template_name=template_name,
vcpu=vcpu,
memory_mib=memory_mib,
timeout_min=timeout_min,
memory=memory,
timeout=timeout,
auto_pause=auto_pause,
otel_export=effective_otel_export,
env_vars=_inject_otel_env(env_vars) if effective_otel_export else env_vars,
Expand Down
6 changes: 3 additions & 3 deletions leap0/_sync/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def resume(
*,
snapshot_name: str,
auto_pause: bool = False,
timeout_min: int | None = None,
timeout: int | None = None,
network_policy: NetworkPolicyDict | None = None,
http_timeout: float | None = None,
) -> SnapshotSandboxT | Sandbox:
Expand All @@ -154,7 +154,7 @@ def resume(
Args:
snapshot_name: Name of the snapshot to restore.
auto_pause: Automatically pause the restored sandbox on timeout.
timeout_min: Sandbox timeout in minutes.
timeout: Sandbox timeout in seconds.
network_policy: Override the network policy from the snapshot.

http_timeout: Optional HTTP request timeout in seconds for this SDK call.
Expand All @@ -168,7 +168,7 @@ def resume(
payload = ResumeSnapshotParams(
snapshot_name=snapshot_name,
auto_pause=auto_pause,
timeout_min=timeout_min,
timeout=timeout,
network_policy=network_policy,
).to_payload()
data = cast(SandboxCreateResponseDict, self._transport.request_json(
Expand Down
2 changes: 1 addition & 1 deletion leap0/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

DEFAULT_MEMORY_MIB = 1024

DEFAULT_TIMEOUT_MIN = 5
DEFAULT_TIMEOUT = 300

DEFAULT_CLIENT_TIMEOUT = 300.0

Expand Down
34 changes: 19 additions & 15 deletions leap0/models/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
SandboxStatusResponseDict,
TransformRuleDict,
)
from .config import DEFAULT_MEMORY_MIB, DEFAULT_TEMPLATE_NAME, DEFAULT_TIMEOUT_MIN, DEFAULT_VCPU
from .config import DEFAULT_MEMORY_MIB, DEFAULT_TEMPLATE_NAME, DEFAULT_TIMEOUT, DEFAULT_VCPU

class SandboxState(str, Enum):
"""Lifecycle states for a sandbox."""
Expand Down Expand Up @@ -110,8 +110,8 @@ class CreateSandboxParams(BaseModel):

template_name: str = DEFAULT_TEMPLATE_NAME
vcpu: int = DEFAULT_VCPU
memory_mib: int = DEFAULT_MEMORY_MIB
timeout_min: int = DEFAULT_TIMEOUT_MIN
memory: int = DEFAULT_MEMORY_MIB
timeout: int = DEFAULT_TIMEOUT
Comment on lines +113 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Consider a short compatibility window for the renamed public fields.

These names changed on public request/response models in-place, so downstream code still using memory_mib, disk_mib, or timeout_min will break on upgrade. If this is not meant to be a hard breaking release, keep deprecated aliases/properties for one cycle and serialize only the new wire names.

Also applies to: 175-177, 209-211

auto_pause: bool = False
otel_export: bool = False
env_vars: dict[str, str] | None = None
Expand All @@ -126,10 +126,10 @@ def _validate_values(self) -> CreateSandboxParams:
raise ValueError("template_name must be at most 64 characters")
if not 1 <= self.vcpu <= 8:
raise ValueError("vcpu must be between 1 and 8")
if self.memory_mib < 512 or self.memory_mib > 8192 or self.memory_mib % 2 != 0:
raise ValueError("memory_mib must be an even number between 512 and 8192")
if not 1 <= self.timeout_min <= 480:
raise ValueError("timeout_min must be between 1 and 480")
if self.memory < 512 or self.memory > 8192 or self.memory % 2 != 0:
raise ValueError("memory must be an even number between 512 and 8192")
if not 1 <= self.timeout <= 28800:
raise ValueError("timeout must be between 1 and 28800")
self.network_policy = _validate_network_policy(self.network_policy)
self.template_name = template_name
return self
Expand Down Expand Up @@ -172,8 +172,9 @@ class Sandbox(SandboxHandle):
id: str
template_id: str = ""
vcpu: int = 0
memory_mib: int = 0
disk_mib: int = 0
memory: int = 0
disk: int = 0
timeout: int = 0
state: SandboxState | str = SandboxState.STARTING
auto_pause: bool = False
created_at: str = ""
Expand All @@ -190,8 +191,9 @@ def from_dict(cls, data: SandboxCreateResponseDict) -> Sandbox:
id=sandbox_id,
template_id=data.get("template_id", ""),
vcpu=int(data.get("vcpu", 0)),
memory_mib=int(data.get("memory_mib", 0)),
disk_mib=int(data.get("disk_mib", 0)),
memory=int(data.get("memory", 0)),
disk=int(data.get("disk", 0)),
timeout=int(data.get("timeout", 0)),
Comment on lines +194 to +196

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't silently turn missing renamed fields into 0.

If the backend omits one of these keys—or still returns the legacy names during rollout—both parsers manufacture 0 values instead of surfacing the contract mismatch. That makes a mixed-version response look valid and can leak bogus resource limits to callers. Prefer failing fast here, or temporarily falling back to the legacy keys while the API rolls out.

💡 Minimal hard-fail / fallback shape
-            memory=int(data.get("memory", 0)),
-            disk=int(data.get("disk", 0)),
-            timeout=int(data.get("timeout", 0)),
+            memory=_require_int(data, "memory", legacy_key="memory_mib"),
+            disk=_require_int(data, "disk", legacy_key="disk_mib"),
+            timeout=_require_int(data, "timeout", legacy_key="timeout_min"),
def _require_int(data: Mapping[str, object], key: str, *, legacy_key: str | None = None) -> int:
    value = data.get(key)
    if value is None and legacy_key is not None:
        value = data.get(legacy_key)
    if value is None:
        raise ValueError(f"sandbox response missing required field {key!r}")
    try:
        return int(value)
    except (TypeError, ValueError) as err:
        raise ValueError(f"sandbox response has invalid {key!r}: {value!r}") from err

Also applies to: 227-229

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@leap0/models/sandbox.py` around lines 194 - 196, The code is silently
defaulting missing/renamed sandbox fields to 0 (memory/disk/timeout), so add a
strict helper like _require_int(data, key, legacy_key=None) that first checks
data[key], falls back to legacy_key if provided, raises ValueError if neither is
present, and converts to int catching TypeError/ValueError to raise a clear
error; replace the three int(data.get(...,0)) calls for "memory", "disk", and
"timeout" with calls to _require_int (passing legacy keys if the API had
previous names) and apply the same replacement to the similar parsing at the
other occurrence noted (the block at the other three fields around lines
227-229).

state=state,
auto_pause=bool(data.get("auto_pause", False)),
created_at=data.get("created_at", ""),
Expand All @@ -204,8 +206,9 @@ class SandboxStatus(SandboxHandle):
id: str
template_id: str
vcpu: int
memory_mib: int
disk_mib: int
memory: int
disk: int
timeout: int
state: SandboxState | str
auto_pause: bool
created_at: str
Expand All @@ -221,8 +224,9 @@ def from_dict(cls, data: SandboxStatusResponseDict) -> SandboxStatus:
id=sandbox_id,
template_id=data.get("template_id", ""),
vcpu=int(data.get("vcpu", 0)),
memory_mib=int(data.get("memory_mib", 0)),
disk_mib=int(data.get("disk_mib", 0)),
memory=int(data.get("memory", 0)),
disk=int(data.get("disk", 0)),
timeout=int(data.get("timeout", 0)),
state=state,
auto_pause=bool(data.get("auto_pause", False)),
created_at=data.get("created_at", ""),
Expand Down
14 changes: 7 additions & 7 deletions leap0/models/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ResumeSnapshotParams(BaseModel):

snapshot_name: str
auto_pause: bool = False
timeout_min: int | None = None
timeout: int | None = None
network_policy: NetworkPolicyDict | None = None

@model_validator(mode="after")
Expand All @@ -47,8 +47,8 @@ def _validate_values(self) -> ResumeSnapshotParams:
raise ValueError("snapshot_name must be a non-empty string")
if len(snapshot_name) > 64:
raise ValueError("snapshot_name must be at most 64 characters")
if self.timeout_min is not None and not 1 <= self.timeout_min <= 480:
raise ValueError("timeout_min must be between 1 and 480 when provided")
if self.timeout is not None and not 1 <= self.timeout <= 28800:
raise ValueError("timeout must be between 1 and 28800 when provided")
self.snapshot_name = snapshot_name
return self

Expand All @@ -69,8 +69,8 @@ class Snapshot:
name: str
template_id: str = ""
vcpu: int = 0
memory_mib: int = 0
disk_mib: int = 0
memory: int = 0
disk: int = 0
state: SandboxState | str | None = None
network_policy: NetworkPolicyDict | None = None
created_at: str = ""
Expand All @@ -94,8 +94,8 @@ def from_dict(cls, data: SnapshotCreateResponseDict) -> Snapshot:
name=snapshot_name,
template_id=data.get("template_id", ""),
vcpu=int(data.get("vcpu", 0)),
memory_mib=int(data.get("memory_mib", 0)),
disk_mib=int(data.get("disk_mib", 0)),
memory=int(data.get("memory", 0)),
disk=int(data.get("disk", 0)),
state=_parse_sandbox_state(state) if state is not None else None,
network_policy=data.get("network_policy"),
created_at=data.get("created_at", ""),
Expand Down
18 changes: 9 additions & 9 deletions tests/_async/test_sandboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ class TestAsyncSandboxesClient:
def test_create(self, async_mock_transport):
async def run() -> None:
async_mock_transport.request_json.return_value = {
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048,
"disk_mib": 10240, "state": "starting", "auto_pause": False, "created_at": "",
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory": 2048,
"disk": 10240, "timeout": 300, "state": "starting", "auto_pause": False, "created_at": "",
}
result = await AsyncSandboxesClient(async_mock_transport, sandbox_domain="s.dev").create(template_name="my-tpl", vcpu=2, memory_mib=2048)
result = await AsyncSandboxesClient(async_mock_transport, sandbox_domain="s.dev").create(template_name="my-tpl", vcpu=2, memory=2048)
args, kwargs = async_mock_transport.request_json.call_args
assert args == ("POST", "/v1/sandbox")
assert kwargs["json"]["template_name"] == "my-tpl"
Expand All @@ -32,8 +32,8 @@ async def run() -> None:
)
client = AsyncSandboxesClient(async_mock_transport, sandbox_domain="s.dev", sandbox_factory=lambda data: AsyncSandbox(fake_client, data))
async_mock_transport.request_json.return_value = {
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048,
"disk_mib": 10240, "state": "starting", "auto_pause": False, "created_at": "",
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory": 2048,
"disk": 10240, "timeout": 300, "state": "starting", "auto_pause": False, "created_at": "",
}
result = await client.create(template_name="my-tpl")
assert isinstance(result, AsyncSandbox)
Expand Down Expand Up @@ -94,8 +94,8 @@ async def run() -> None:
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318")
monkeypatch.setenv("OTEL_EXPORTER_OTLP_HEADERS", "authorization=token")
async_mock_transport.request_json.return_value = {
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048,
"disk_mib": 10240, "state": "starting", "auto_pause": False, "created_at": "",
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory": 2048,
"disk": 10240, "timeout": 300, "state": "starting", "auto_pause": False, "created_at": "",
}

await AsyncSandboxesClient(async_mock_transport, sandbox_domain="s.dev").create(
Expand Down Expand Up @@ -125,8 +125,8 @@ async def run() -> None:
def test_pause_forwards_http_timeout(self, async_mock_transport):
async def run() -> None:
async_mock_transport.request_json.return_value = {
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory_mib": 2048,
"disk_mib": 10240, "state": "paused", "auto_pause": False, "created_at": "",
"id": "sbx-1", "template_id": "tpl-1", "vcpu": 2, "memory": 2048,
"disk": 10240, "timeout": 300, "state": "paused", "auto_pause": False, "created_at": "",
}

await AsyncSandboxesClient(async_mock_transport, sandbox_domain="s.dev").pause(
Expand Down
Loading