From 06c9f80ecec85dbe43c9664fee9b5c25464b9d61 Mon Sep 17 00:00:00 2001 From: porter-support Date: Wed, 8 Jul 2026 19:56:00 +0000 Subject: [PATCH] release: Python SDK v0.1.32 --- porter_sandbox/_models.py | 22 ++++++++++++++++++++-- porter_sandbox/enums.py | 7 ++++++- porter_sandbox/sandboxes.py | 6 +++++- pyproject.toml | 2 +- tests/test_models_round_trip.py | 16 ++++++++++++++++ uv.lock | 2 +- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/porter_sandbox/_models.py b/porter_sandbox/_models.py index 005692a..8e291d4 100644 --- a/porter_sandbox/_models.py +++ b/porter_sandbox/_models.py @@ -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): @@ -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 ., then\n., 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") @@ -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): @@ -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.") @@ -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"] diff --git a/porter_sandbox/enums.py b/porter_sandbox/enums.py index 10b52dd..b4f90ba 100644 --- a/porter_sandbox/enums.py +++ b/porter_sandbox/enums.py @@ -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" @@ -45,4 +50,4 @@ class VolumePhase(str, Enum): FAILED = "failed" -__all__ = ["FilterValuesResponsePhases", "LogLineLevel", "SandboxesPhase", "StatusResponsePhase", "VolumePhase"] +__all__ = ["FilterValuesResponsePhases", "LogLineLevel", "SandboxDomainSpecVisibility", "SandboxesPhase", "StatusResponsePhase", "VolumePhase"] diff --git a/porter_sandbox/sandboxes.py b/porter_sandbox/sandboxes.py index d60d2eb..886b0e9 100644 --- a/porter_sandbox/sandboxes.py +++ b/porter_sandbox/sandboxes.py @@ -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 @@ -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, @@ -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) @@ -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, @@ -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) diff --git a/pyproject.toml b/pyproject.toml index bb113e2..43e9ccc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py index 1c0efe9..09727ac 100644 --- a/tests/test_models_round_trip.py +++ b/tests/test_models_round_trip.py @@ -17,6 +17,8 @@ LookupResult, Pagination, ReadinessResponse, + SandboxDomainSpec, + SandboxNetworkingSpec, SandboxSpec, VolumeListResponse, VolumeSpec, @@ -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) diff --git a/uv.lock b/uv.lock index 862d491..9010fd2 100644 --- a/uv.lock +++ b/uv.lock @@ -345,7 +345,7 @@ wheels = [ [[package]] name = "porter-sandbox" -version = "0.1.26" +version = "0.1.32" source = { editable = "." } dependencies = [ { name = "httpx" },