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
3 changes: 3 additions & 0 deletions leap0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
CreatePresignedURLParams,
CreateSandboxParams,
NetworkPolicyMode,
ObjectStorageMount,
PresignedURL,
SandboxListItem,
SandboxListResponse,
Expand Down Expand Up @@ -192,6 +193,7 @@
"LspJsonRpcResponse",
"LspResponse",
"NetworkPolicyMode",
"ObjectStorageMount",
"ProcessClient",
"ProcessResult",
"PresignedURL",
Expand Down Expand Up @@ -272,6 +274,7 @@
"CreateSandboxParams": (".models.sandbox", "CreateSandboxParams"),
"CreatePresignedURLParams": (".models.sandbox", "CreatePresignedURLParams"),
"NetworkPolicyMode": (".models.sandbox", "NetworkPolicyMode"),
"ObjectStorageMount": (".models.sandbox", "ObjectStorageMount"),
"SandboxListItem": (".models.sandbox", "SandboxListItem"),
"SandboxListResponse": (".models.sandbox", "SandboxListResponse"),
"SandboxStatus": (".models.sandbox", "SandboxStatus"),
Expand Down
78 changes: 77 additions & 1 deletion leap0/_async/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@
from ..models.sandbox import (
CreatePresignedURLParams,
CreateSandboxParams,
ObjectStorageMount,
PresignedURL,
Sandbox as SandboxData,
SandboxListResponse,
SandboxRef,
SandboxStatus,
_validate_object_storage_mount_update,
sandbox_id_of,
)
from .._schemas.sandbox import ListSandboxesResponseDict, NetworkPolicyDict, PresignedURLResponseDict, SandboxCreateResponseDict, SandboxStatusResponseDict
from .._schemas.sandbox import ListSandboxesResponseDict, NetworkPolicyDict, ObjectStorageMountDict, ObjectStorageMountRequestDict, ObjectStorageMountUpdateDict, PresignedURLResponseDict, SandboxCreateResponseDict, SandboxStatusResponseDict
from .._utils.errors import intercept_errors
from .._utils.url import ensure_leading_slash, sandbox_base_url, websocket_url_from_http
from ._transport import AsyncTransport
Expand Down Expand Up @@ -183,6 +185,27 @@ async def get_workdir(self, http_timeout: float | None = None) -> str:
"""Fetch the configured working directory for the sandbox."""
return await self._client.sandboxes.get_workdir(self, http_timeout=http_timeout)

async def add_mount(
self,
mount: ObjectStorageMountRequestDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
"""Attach an object storage mount to this sandbox."""
return await self._client.sandboxes.add_mount(self, mount, http_timeout=http_timeout)

async def update_mount(
self,
mount_id: str,
mount: ObjectStorageMountUpdateDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
"""Update an existing object storage mount on this sandbox."""
return await self._client.sandboxes.update_mount(self, mount_id, mount, http_timeout=http_timeout)

async def delete_mount(self, mount_id: str, http_timeout: float | None = None) -> None:
"""Delete an object storage mount from this sandbox."""
await self._client.sandboxes.delete_mount(self, mount_id, http_timeout=http_timeout)


def _inject_otel_env(env_vars: dict[str, str] | None) -> dict[str, str] | None:
endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV)
Expand Down Expand Up @@ -237,6 +260,7 @@ async def create(
otel_export: bool = False,
env_vars: dict[str, str] | None = None,
network_policy: NetworkPolicyDict | None = None,
mounts: list[ObjectStorageMountRequestDict] | None = None,
http_timeout: float | None = None,
) -> AsyncSandboxT | SandboxData | SandboxStatus:
"""Create a new sandbox from a template.
Expand All @@ -252,6 +276,7 @@ async def create(
also forwards ``OTEL_EXPORTER_OTLP_HEADERS`` when present.
env_vars: Environment variables to set inside the sandbox.
network_policy: Outbound network policy for the sandbox.
mounts: Object storage mounts to attach before boot.
http_timeout: Optional HTTP request timeout in seconds for this SDK call.

Returns:
Expand All @@ -266,6 +291,7 @@ async def create(
otel_export=otel_export,
env_vars=_inject_otel_env(env_vars) if otel_export else env_vars,
network_policy=network_policy,
mounts=mounts,
)
payload = params.to_payload()
payload.pop("otel_export", None)
Expand Down Expand Up @@ -453,6 +479,56 @@ async def delete_presigned_url(self, sandbox: SandboxRef, presigned_url_id: str,
timeout=http_timeout,
)

@intercept_errors("Failed to add sandbox mount: ")
async def add_mount(
self,
sandbox: SandboxRef,
mount: ObjectStorageMountRequestDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
normalized_mounts = CreateSandboxParams(mounts=[mount]).mounts
if normalized_mounts is None:
raise ValueError("mount is required")
data = cast(dict[str, object], await self._transport.request_json(
"POST",
f"/v1/sandbox/{sandbox_id_of(sandbox)}/mounts",
json=normalized_mounts[0],
expected_status=201,
timeout=http_timeout,
))
return ObjectStorageMount.from_dict(cast(ObjectStorageMountDict, data))

@intercept_errors("Failed to update sandbox mount: ")
async def update_mount(
self,
sandbox: SandboxRef,
mount_id: str,
mount: ObjectStorageMountUpdateDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
mount_id_value = mount_id.strip()
if not mount_id_value:
raise ValueError("mount_id must be a non-empty string")
data = cast(dict[str, object], await self._transport.request_json(
"PATCH",
f"/v1/sandbox/{sandbox_id_of(sandbox)}/mounts/{mount_id_value}",
json=_validate_object_storage_mount_update(mount),
timeout=http_timeout,
))
return ObjectStorageMount.from_dict(cast(ObjectStorageMountDict, data))

@intercept_errors("Failed to delete sandbox mount: ")
async def delete_mount(self, sandbox: SandboxRef, mount_id: str, http_timeout: float | None = None) -> None:
mount_id_value = mount_id.strip()
if not mount_id_value:
raise ValueError("mount_id must be a non-empty string")
await self._transport.request(
"DELETE",
f"/v1/sandbox/{sandbox_id_of(sandbox)}/mounts/{mount_id_value}",
expected_status=204,
timeout=http_timeout,
)


def invoke_url(self, sandbox: SandboxRef, path: str = "/", *, port: int | None = None) -> str:
"""Build an HTTPS URL for this sandbox.
Expand Down
40 changes: 36 additions & 4 deletions leap0/_schemas/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,41 @@ class NetworkPolicyDict(TypedDict, total=False):
allow_cidrs: NotRequired[list[str]]
transforms: NotRequired[list[TransformRuleDict]]

class SandboxCreateResponseDict(TypedDict):
class ObjectStorageMountRequestDict(TypedDict, total=False):
"""Wire schema for object storage mount requests."""
type: Required[str]
bucket: Required[str]
mount_path: Required[str]
endpoint: Required[str]
prefix: str
read_only: bool
access_key_id: str
secret_access_key: str

class ObjectStorageMountUpdateDict(TypedDict, total=False):
"""Wire schema for sandbox mount update requests."""
bucket: str
mount_path: str
endpoint: str
prefix: str
read_only: bool
access_key_id: str
secret_access_key: str

class ObjectStorageMountDict(TypedDict, total=False):
"""Wire schema for object storage mounts returned by the API."""
id: Required[str]
type: Required[str]
bucket: Required[str]
mount_path: Required[str]
prefix: str
read_only: bool

class SandboxCreateResponseDict(TypedDict, total=False):
"""Wire schema for sandbox creation responses."""
id: str
id: Required[str]
template_id: str
mounts: list[ObjectStorageMountDict]
vcpu: int
memory: int
disk: int
Expand All @@ -32,10 +63,11 @@ class SandboxCreateResponseDict(TypedDict):
created_at: str
network_policy: NetworkPolicyDict | None

class SandboxStatusResponseDict(TypedDict):
class SandboxStatusResponseDict(TypedDict, total=False):
"""Wire schema for sandbox status responses."""
id: str
id: Required[str]
template_id: str
mounts: list[ObjectStorageMountDict]
vcpu: int
memory: int
disk: int
Expand Down
78 changes: 77 additions & 1 deletion leap0/_sync/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@
from ..models.sandbox import (
CreatePresignedURLParams,
CreateSandboxParams,
ObjectStorageMount,
PresignedURL,
Sandbox as SandboxData,
SandboxListResponse,
SandboxRef,
SandboxStatus,
_validate_object_storage_mount_update,
sandbox_id_of,
)
from .._schemas.sandbox import ListSandboxesResponseDict, NetworkPolicyDict, PresignedURLResponseDict, SandboxCreateResponseDict, SandboxStatusResponseDict
from .._schemas.sandbox import ListSandboxesResponseDict, NetworkPolicyDict, ObjectStorageMountDict, ObjectStorageMountRequestDict, ObjectStorageMountUpdateDict, PresignedURLResponseDict, SandboxCreateResponseDict, SandboxStatusResponseDict
from .._utils.errors import intercept_errors
from .._utils.url import ensure_leading_slash, sandbox_base_url, websocket_url_from_http
from ._transport import Transport
Expand Down Expand Up @@ -180,6 +182,27 @@ def get_workdir(self, http_timeout: float | None = None) -> str:
"""Fetch the configured working directory for the sandbox."""
return self._client.sandboxes.get_workdir(self, http_timeout=http_timeout)

def add_mount(
self,
mount: ObjectStorageMountRequestDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
"""Attach an object storage mount to this sandbox."""
return self._client.sandboxes.add_mount(self, mount, http_timeout=http_timeout)

def update_mount(
self,
mount_id: str,
mount: ObjectStorageMountUpdateDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
"""Update an existing object storage mount on this sandbox."""
return self._client.sandboxes.update_mount(self, mount_id, mount, http_timeout=http_timeout)

def delete_mount(self, mount_id: str, http_timeout: float | None = None) -> None:
"""Delete an object storage mount from this sandbox."""
self._client.sandboxes.delete_mount(self, mount_id, http_timeout=http_timeout)


def _inject_otel_env(env_vars: dict[str, str] | None) -> dict[str, str] | None:
endpoint = os.environ.get(OTEL_EXPORTER_OTLP_ENDPOINT_ENV)
Expand Down Expand Up @@ -239,6 +262,7 @@ def create(
telemetry: bool | None = None,
env_vars: dict[str, str] | None = None,
network_policy: NetworkPolicyDict | None = None,
mounts: list[ObjectStorageMountRequestDict] | None = None,
http_timeout: float | None = None,
) -> SandboxT | SandboxData | SandboxStatus:
"""Create a new sandbox from a template.
Expand All @@ -255,6 +279,7 @@ def create(
telemetry: Deprecated alias for ``otel_export``. Use ``otel_export`` instead.
env_vars: Environment variables to set inside the sandbox.
network_policy: Outbound network policy for the sandbox.
mounts: Object storage mounts to attach before boot.
http_timeout: Optional HTTP request timeout in seconds for this SDK call.

Returns:
Expand All @@ -271,6 +296,7 @@ def create(
otel_export=effective_otel_export,
env_vars=_inject_otel_env(env_vars) if effective_otel_export else env_vars,
network_policy=network_policy,
mounts=mounts,
)
payload = params.to_payload()
payload.pop("otel_export", None)
Expand Down Expand Up @@ -452,6 +478,56 @@ def delete_presigned_url(self, sandbox: SandboxRef, presigned_url_id: str, http_
timeout=http_timeout,
)

@intercept_errors("Failed to add sandbox mount: ")
def add_mount(
self,
sandbox: SandboxRef,
mount: ObjectStorageMountRequestDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
normalized_mounts = CreateSandboxParams(mounts=[mount]).mounts
if normalized_mounts is None:
raise ValueError("mount is required")
data = cast(dict[str, object], self._transport.request_json(
"POST",
f"/v1/sandbox/{sandbox_id_of(sandbox)}/mounts",
json=normalized_mounts[0],
expected_status=201,
timeout=http_timeout,
))
return ObjectStorageMount.from_dict(cast(ObjectStorageMountDict, data))

@intercept_errors("Failed to update sandbox mount: ")
def update_mount(
self,
sandbox: SandboxRef,
mount_id: str,
mount: ObjectStorageMountUpdateDict,
http_timeout: float | None = None,
) -> ObjectStorageMount:
mount_id_value = mount_id.strip()
if not mount_id_value:
raise ValueError("mount_id must be a non-empty string")
data = cast(dict[str, object], self._transport.request_json(
"PATCH",
f"/v1/sandbox/{sandbox_id_of(sandbox)}/mounts/{mount_id_value}",
json=_validate_object_storage_mount_update(mount),
timeout=http_timeout,
))
return ObjectStorageMount.from_dict(cast(ObjectStorageMountDict, data))

@intercept_errors("Failed to delete sandbox mount: ")
def delete_mount(self, sandbox: SandboxRef, mount_id: str, http_timeout: float | None = None) -> None:
mount_id_value = mount_id.strip()
if not mount_id_value:
raise ValueError("mount_id must be a non-empty string")
self._transport.request(
"DELETE",
f"/v1/sandbox/{sandbox_id_of(sandbox)}/mounts/{mount_id_value}",
expected_status=204,
timeout=http_timeout,
)

def invoke_url(self, sandbox: SandboxRef, path: str = "/", *, port: int | None = None) -> str:
"""Build an HTTPS URL that routes directly to the sandbox.

Expand Down
Loading