Skip to content

Commit 3ba0e12

Browse files
committed
Add support for image= to sandbox.create(...)
1 parent 720ca4d commit 3ba0e12

7 files changed

Lines changed: 82 additions & 2 deletions

File tree

src/vercel/_internal/unstable/sandbox/api_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
ValidationError,
2020
field_serializer,
2121
field_validator,
22+
model_validator,
2223
)
2324

2425
from vercel._internal.http import (
@@ -131,6 +132,7 @@ class _CreateSandboxRequest(_ApiRequestModel):
131132
project_id: str = Field(serialization_alias="projectId")
132133
name: str | None = None
133134
runtime: str | None = None
135+
image: str | None = None
134136
source: SandboxSource | None = None
135137
ports: list[int] | None = None
136138
timeout: timedelta | None = None
@@ -162,6 +164,12 @@ def _serialize_retention(self, value: SnapshotRetention | None) -> JSONObject |
162164
def _serialize_network_policy(self, value: NetworkPolicy | None) -> JSONObject | None:
163165
return None if value is None else _serialize_network_policy(value)
164166

167+
@model_validator(mode="after")
168+
def _validate_boot_selectors(self) -> "_CreateSandboxRequest":
169+
if self.runtime is not None and self.image is not None:
170+
raise ValueError("runtime and image are mutually exclusive")
171+
return self
172+
165173
@field_validator("network_policy", mode="before")
166174
@classmethod
167175
def _validate_network_policy(cls, value: object) -> NetworkPolicy | None:
@@ -879,6 +887,7 @@ async def create_sandbox(
879887
project_id: str | None = None,
880888
name: str | None = None,
881889
runtime: str | None = None,
890+
image: str | None = None,
882891
source: SandboxSource | None = None,
883892
ports: list[int] | None = None,
884893
execution_time_limit: timedelta | None = None,
@@ -895,6 +904,7 @@ async def create_sandbox(
895904
project_id=project_id or credentials.project_id,
896905
name=name,
897906
runtime=runtime,
907+
image=image,
898908
source=source,
899909
ports=ports,
900910
timeout=execution_time_limit,

src/vercel/_internal/unstable/sandbox/async_runtime.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,7 @@ class _CreateSandboxParams:
10741074
project_id: str | None = None
10751075
name: str | None = None
10761076
runtime: str | None = None
1077+
image: str | None = None
10771078
source: SandboxSource | None = None
10781079
ports: list[int] | None = None
10791080
execution_time_limit: timedelta | None = None
@@ -1120,6 +1121,7 @@ async def _run_once(self) -> Sandbox:
11201121
project_id=self._params.project_id,
11211122
name=self._params.name,
11221123
runtime=self._params.runtime,
1124+
image=self._params.image,
11231125
source=self._params.source,
11241126
ports=self._params.ports,
11251127
execution_time_limit=self._params.execution_time_limit,
@@ -1262,6 +1264,7 @@ def create_sandbox_operation(
12621264
project_id: str | None = None,
12631265
name: str | None = None,
12641266
runtime: str | None = None,
1267+
image: str | None = None,
12651268
source: SandboxSource | None = None,
12661269
ports: list[int] | None = None,
12671270
execution_time_limit: DurationInput = None,
@@ -1280,6 +1283,7 @@ def create_sandbox_operation(
12801283
project_id=project_id,
12811284
name=name,
12821285
runtime=runtime,
1286+
image=image,
12831287
source=source,
12841288
ports=ports,
12851289
execution_time_limit=parse_duration_seconds(execution_time_limit),

src/vercel/_internal/unstable/sandbox/service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ async def create_sandbox(
247247
project_id: str | None = None,
248248
name: str | None = None,
249249
runtime: str | None = None,
250+
image: str | None = None,
250251
source: SandboxSource | None = None,
251252
ports: list[int] | None = None,
252253
execution_time_limit: timedelta | None = None,
@@ -263,6 +264,7 @@ async def create_sandbox(
263264
project_id=project_id,
264265
name=name,
265266
runtime=runtime,
267+
image=image,
266268
source=source,
267269
ports=ports,
268270
execution_time_limit=execution_time_limit,

src/vercel/_internal/unstable/sandbox/sync_runtime.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,7 @@ def create_sandbox(
11841184
project_id: str | None = None,
11851185
name: str | None = None,
11861186
runtime: str | None = None,
1187+
image: str | None = None,
11871188
source: SandboxSource | None = None,
11881189
ports: list[int] | None = None,
11891190
execution_time_limit: DurationInput = None,
@@ -1202,6 +1203,7 @@ def create_sandbox(
12021203
project_id=project_id,
12031204
name=name,
12041205
runtime=runtime,
1206+
image=image,
12051207
source=source,
12061208
ports=ports,
12071209
execution_time_limit=parse_duration_seconds(execution_time_limit),

src/vercel/unstable/sandbox/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def create_sandbox(
8787
project_id: str | None = None,
8888
name: str | None = None,
8989
runtime: str | None = None,
90+
image: str | None = None,
9091
source: SandboxSource | None = None,
9192
ports: list[int] | None = None,
9293
execution_time_limit: DurationInput = None,
@@ -109,7 +110,9 @@ def create_sandbox(
109110
project_id: Project that owns the sandbox. Uses the active credentials
110111
when omitted.
111112
name: Requested sandbox name. The service generates one when omitted.
112-
runtime: Runtime image or runtime identifier.
113+
runtime: Runtime identifier. Mutually exclusive with ``image``.
114+
image: Vercel Container Registry image to start the sandbox from.
115+
Mutually exclusive with ``runtime``.
113116
source: Git, tarball, or snapshot source used to initialize the sandbox.
114117
ports: Ports to expose from the sandbox.
115118
execution_time_limit: Maximum session runtime in seconds or as a
@@ -137,6 +140,7 @@ def create_sandbox(
137140
project_id=project_id,
138141
name=name,
139142
runtime=runtime,
143+
image=image,
140144
source=source,
141145
ports=ports,
142146
execution_time_limit=execution_time_limit,

src/vercel/unstable/sandbox/sync.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def create_sandbox(
8484
project_id: str | None = None,
8585
name: str | None = None,
8686
runtime: str | None = None,
87+
image: str | None = None,
8788
source: SandboxSource | None = None,
8889
ports: list[int] | None = None,
8990
execution_time_limit: DurationInput = None,
@@ -106,7 +107,9 @@ def create_sandbox(
106107
project_id: Project that owns the sandbox. Uses the active credentials
107108
when omitted.
108109
name: Requested sandbox name. The service generates one when omitted.
109-
runtime: Runtime image or runtime identifier.
110+
runtime: Runtime identifier. Mutually exclusive with ``image``.
111+
image: Vercel Container Registry image to start the sandbox from.
112+
Mutually exclusive with ``runtime``.
110113
source: Git, tarball, or snapshot source used to initialize the sandbox.
111114
ports: Ports to expose from the sandbox.
112115
execution_time_limit: Maximum session runtime in seconds or as a
@@ -133,6 +136,7 @@ def create_sandbox(
133136
project_id=project_id,
134137
name=name,
135138
runtime=runtime,
139+
image=image,
136140
source=source,
137141
ports=ports,
138142
execution_time_limit=execution_time_limit,

tests/unstable/test_sandbox_public_flow.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,60 @@ def update_handler(request: httpx.Request) -> httpx.Response:
437437
assert not hasattr(handle, "model_dump")
438438

439439

440+
@respx.mock
441+
async def test_public_create_sandbox_serializes_image_without_runtime(
442+
mock_env_clear: None,
443+
) -> None:
444+
route = respx.post("https://sandbox.test/v2/sandboxes").mock(
445+
return_value=httpx.Response(200, json=_sandbox_response())
446+
)
447+
448+
async with vercel.session(service_options=_session_options()):
449+
await sandbox.create_sandbox(
450+
name="preview",
451+
image="acme/worker:latest",
452+
source=SnapshotSource(snapshot_id="snap_123"),
453+
)
454+
455+
body = json.loads(route.calls.last.request.content)
456+
assert body["image"] == "acme/worker:latest"
457+
assert body["source"] == {"type": "snapshot", "snapshotId": "snap_123"}
458+
assert "runtime" not in body
459+
460+
461+
@respx.mock
462+
def test_sync_create_sandbox_serializes_image_without_runtime(mock_env_clear: None) -> None:
463+
route = respx.post("https://sandbox.test/v2/sandboxes").mock(
464+
return_value=httpx.Response(200, json=_sandbox_response())
465+
)
466+
467+
with vercel.session(service_options=_session_options()):
468+
sandbox_sync.create_sandbox(name="preview", image="acme/worker:latest")
469+
470+
body = json.loads(route.calls.last.request.content)
471+
assert body["image"] == "acme/worker:latest"
472+
assert "runtime" not in body
473+
474+
475+
@respx.mock
476+
async def test_public_create_sandbox_rejects_runtime_and_image(
477+
mock_env_clear: None,
478+
) -> None:
479+
route = respx.post("https://sandbox.test/v2/sandboxes").mock(
480+
return_value=httpx.Response(200, json=_sandbox_response())
481+
)
482+
483+
async with vercel.session(service_options=_session_options()):
484+
with pytest.raises(ValidationError, match="runtime and image"):
485+
await sandbox.create_sandbox(
486+
name="preview",
487+
runtime="python3.13",
488+
image="acme/worker:latest",
489+
)
490+
491+
assert not route.called
492+
493+
440494
@respx.mock
441495
async def test_network_policy_async_public_flow(mock_env_clear: None) -> None:
442496
create_route = respx.post("https://sandbox.test/v2/sandboxes").mock(

0 commit comments

Comments
 (0)