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
22 changes: 20 additions & 2 deletions porter_sandbox/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@

from pydantic import BaseModel, Field

from .enums import FilterValuesResponsePhases, LogLineLevel, StatusResponsePhase, VolumePhase
from .enums import (
FilterValuesResponsePhases,
LogLineLevel,
SandboxDomainSpecVisibility,
StatusResponsePhase,
VolumePhase,
)


class CountPoint(BaseModel):
Expand Down Expand Up @@ -87,6 +93,16 @@ class ReadinessResponse(BaseModel):
active_sandboxes: int = Field(description="Number of active sandboxes")


class SandboxDomainSpec(BaseModel):
domain: str | None = Field(default=None, description="Fully qualified hostname for the sandbox, one label under the\ntarget ingress's domain. Unlike name it need not be unique, so\nsuccessive sandboxes can reuse one hostname (only one may be live\nat a time). Defaults to <name>.<ingress domain>, then\n<id>.<ingress domain>, when omitted.\n")
visibility: SandboxDomainSpecVisibility | None = Field(default=None, description="Which sandbox ingress serves the domain when the cluster has both a\npublic and a private one. Omit to default to whichever is\nconfigured, public winning. Rejected when the sandbox exposes a\nport but the requested ingress is not configured on the cluster.\n")


class SandboxNetworkingSpec(BaseModel):
port: int = Field(description="Port the workload listens on; the per-sandbox Service targets it on\nthe pod. Privileged ports (1-1023) are not allowed.\n")
domains: list[SandboxDomainSpec] | None = Field(default=None, description="Domains the port is served on through a sandbox ingress. Omit to\nserve the port at the default hostname through the default ingress.\nCurrently only one entry is supported.\n")


class SandboxSpec(BaseModel):
image: str = Field(description="Container image to run")
name: str | None = Field(default=None, description="Sandbox name, unique within the cluster. Must be a valid DNS label\n(lowercase alphanumeric and dashes). Defaults to the sandbox's id\nwhen omitted.\n")
Expand All @@ -95,6 +111,7 @@ class SandboxSpec(BaseModel):
args: list[str] | None = Field(default=None, description="Arguments passed to the command")
env: dict[str, str] | None = Field(default=None, description="Environment variables to set in the sandbox, keyed by name")
volume_mounts: dict[str, str] | None = Field(default=None, description="Volumes to mount, keyed by the absolute mount path inside the\nsandbox; values are volume IDs.\n")
networking: list[SandboxNetworkingSpec] | None = Field(default=None, description="Network exposure for the sandbox. Omit to expose nothing. Currently\nonly one entry is supported.\n")


class StatusResponse(BaseModel):
Expand All @@ -106,6 +123,7 @@ class StatusResponse(BaseModel):
exit_code: int | None = Field(default=None, description="Exit code if completed")
created_at: str = Field(description="When the sandbox was created")
started_at: str | None = Field(default=None, description="When the sandbox pod started running")
host: str = Field(description="Public hostname the sandbox is reachable at. Empty when the sandbox\nexposes no port or the cluster has no sandbox ingress configured.\n")
volume_mounts: dict[str, str] | None = Field(default=None, description="Volumes the sandbox mounts, keyed by mount path")
exec_target: ExecTarget | None = Field(default=None, description="Where a client addresses an interactive exec into the running sandbox. Absent until the sandbox has a pod.")

Expand All @@ -126,4 +144,4 @@ class VolumeSpec(BaseModel):
name: str | None = Field(default=None, description="Volume name, unique within the cluster. Must be a valid DNS label\n(lowercase alphanumeric and dashes). Defaults to the volume's id\nwhen omitted.\n")


__all__ = ["CountPoint", "CountResponse", "CreateResponse", "Error", "ExecRequest", "ExecResponse", "ExecTarget", "FilterValuesResponse", "HealthResponse", "ListResponse", "LogLine", "LogsResponse", "LookupResult", "Pagination", "ReadinessResponse", "SandboxSpec", "StatusResponse", "Volume", "VolumeListResponse", "VolumeSpec"]
__all__ = ["CountPoint", "CountResponse", "CreateResponse", "Error", "ExecRequest", "ExecResponse", "ExecTarget", "FilterValuesResponse", "HealthResponse", "ListResponse", "LogLine", "LogsResponse", "LookupResult", "Pagination", "ReadinessResponse", "SandboxDomainSpec", "SandboxNetworkingSpec", "SandboxSpec", "StatusResponse", "Volume", "VolumeListResponse", "VolumeSpec"]
7 changes: 6 additions & 1 deletion porter_sandbox/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ class LogLineLevel(str, Enum):
INFO = "info"


class SandboxDomainSpecVisibility(str, Enum):
PUBLIC = "public"
PRIVATE = "private"


class SandboxesPhase(str, Enum):
QUEUED = "queued"
CREATING = "creating"
Expand All @@ -45,4 +50,4 @@ class VolumePhase(str, Enum):
FAILED = "failed"


__all__ = ["FilterValuesResponsePhases", "LogLineLevel", "SandboxesPhase", "StatusResponsePhase", "VolumePhase"]
__all__ = ["FilterValuesResponsePhases", "LogLineLevel", "SandboxDomainSpecVisibility", "SandboxesPhase", "StatusResponsePhase", "VolumePhase"]
6 changes: 5 additions & 1 deletion porter_sandbox/sandboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import builtins

from porter_sandbox._models import SandboxSpec
from porter_sandbox._models import SandboxNetworkingSpec, SandboxSpec
from porter_sandbox.enums import SandboxesPhase
from porter_sandbox.resources.sandboxes import AsyncSandboxes as AsyncSandboxesResource
from porter_sandbox.resources.sandboxes import Sandboxes as SandboxesResource
Expand Down Expand Up @@ -33,6 +33,7 @@ def create(
args: list[str] | None = None,
env: dict[str, str] | None = None,
volume_mounts: dict[str, str] | None = None,
networking: list[SandboxNetworkingSpec] | None = None,
) -> Sandbox:
spec = SandboxSpec(
image=image,
Expand All @@ -42,6 +43,7 @@ def create(
args=args,
env=env,
volume_mounts=volume_mounts,
networking=networking,
)
created = self._resource.create_sandbox(body=spec)
return Sandbox(id=created.id, resource=self._resource)
Expand Down Expand Up @@ -89,6 +91,7 @@ async def create(
args: list[str] | None = None,
env: dict[str, str] | None = None,
volume_mounts: dict[str, str] | None = None,
networking: list[SandboxNetworkingSpec] | None = None,
) -> AsyncSandbox:
spec = SandboxSpec(
image=image,
Expand All @@ -98,6 +101,7 @@ async def create(
args=args,
env=env,
volume_mounts=volume_mounts,
networking=networking,
)
created = await self._resource.create_sandbox(body=spec)
return AsyncSandbox(id=created.id, resource=self._resource)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "porter-sandbox"
version = "0.1.26"
version = "0.1.32"
description = "Python SDK for the Porter Sandbox API"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
16 changes: 16 additions & 0 deletions tests/test_models_round_trip.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
LookupResult,
Pagination,
ReadinessResponse,
SandboxDomainSpec,
SandboxNetworkingSpec,
SandboxSpec,
VolumeListResponse,
VolumeSpec,
Expand Down Expand Up @@ -114,6 +116,20 @@ def test_readiness_response_round_trip() -> None:
assert round_tripped.model_dump(by_alias=True, exclude_none=True) == serialized


def test_sandbox_domain_spec_round_trip() -> None:
instance = SandboxDomainSpec()
serialized = instance.model_dump(by_alias=True, exclude_none=True)
round_tripped = SandboxDomainSpec.model_validate(serialized)
assert round_tripped.model_dump(by_alias=True, exclude_none=True) == serialized


def test_sandbox_networking_spec_round_trip() -> None:
instance = SandboxNetworkingSpec(port=1)
serialized = instance.model_dump(by_alias=True, exclude_none=True)
round_tripped = SandboxNetworkingSpec.model_validate(serialized)
assert round_tripped.model_dump(by_alias=True, exclude_none=True) == serialized


def test_sandbox_spec_round_trip() -> None:
instance = SandboxSpec(image="x")
serialized = instance.model_dump(by_alias=True, exclude_none=True)
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading