snowflake-sandbox-python 0.2.2a3__tar.gz → 0.2.2a5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {snowflake_sandbox_python-0.2.2a3/src/snowflake_sandbox_python.egg-info → snowflake_sandbox_python-0.2.2a5}/PKG-INFO +1 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/pyproject.toml +38 -8
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_egress_flags.py +6 -1
- snowflake_sandbox_python-0.2.2a5/src/snowflake/cli_sandbox/_lifecycle_command.py +108 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_run_command.py +7 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_upload_plan.py +1 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/commands.py +53 -5
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/__init__.py +2 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_bundle.py +2 -2
- snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/_code_delivery.py +88 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_diagnostics.py +8 -6
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_env.py +1 -1
- snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/_preview.py +164 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_shims.py +32 -4
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sandbox_state.py +5 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_upload_plan.py +1 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_version.py +1 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_wire.py +64 -6
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/client.py +389 -270
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/connect.py +1 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/deploy.py +113 -22
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/egress.py +114 -40
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/exceptions.py +40 -5
- snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/image.py +573 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/jobs.py +54 -34
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/session_app.py +16 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_client.py +389 -266
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/warm_session.py +3 -3
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5/src/snowflake_sandbox_python.egg-info}/PKG-INFO +1 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/SOURCES.txt +6 -1
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/tests/test_docs_references.py +287 -3
- snowflake_sandbox_python-0.2.2a5/tests/test_gate_tristate.py +249 -0
- snowflake_sandbox_python-0.2.2a5/tests/test_tutorial_sources.py +485 -0
- snowflake_sandbox_python-0.2.2a3/src/snowflake/sandbox/image.py +0 -46
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/LICENSE +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/README.md +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/setup.cfg +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/setup.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_adapter.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_common.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_get_command.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_shell_command.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/plugin_spec.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/py.typed +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/__main__.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_ansi.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_args.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_assemble.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_connection_resolve.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_deploy_spec.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_files_mixin.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_fs_ops.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_globs.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_hosts.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_mcp_discovery.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_mcp_status.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_retry.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_fs_helper.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_job_runner.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_protocol.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_session_registry.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sse.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_stage.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_files_mixin.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_fs_ops.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_transport.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_watch.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_transport.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_transport_errors.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_watch.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/app.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/config.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/exec_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/failure.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/files.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/function.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/lifecycle.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/log_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/mcp.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/mount.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/py.typed +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/secret.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/shell.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_exec_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_files.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_log_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_shell.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/types.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/dependency_links.txt +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/entry_points.txt +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/requires.txt +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/top_level.txt +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/tests/test_imports.py +0 -0
- {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/tests/test_smoke_offline.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "snowflake-sandbox-python"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2a5"
|
|
8
8
|
description = "Snowflake sandboxes — Python SDK and CLI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -142,15 +142,45 @@ ignore_missing_imports = true
|
|
|
142
142
|
asyncio_mode = "auto"
|
|
143
143
|
asyncio_default_fixture_loop_scope = "function"
|
|
144
144
|
testpaths = ["tests"]
|
|
145
|
-
# Skip live tests by default — run with:
|
|
146
|
-
|
|
145
|
+
# Skip live and docs tests by default — run with:
|
|
146
|
+
# pytest -m live --connection <name>
|
|
147
|
+
# pytest -m docs (add --docs-root if the checkout is not a sibling)
|
|
148
|
+
addopts = "-m 'not live and not docs'"
|
|
147
149
|
markers = [
|
|
148
150
|
"live: tests that run against a real sandbox-api deployment",
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
151
|
+
# Needs a snowflake-prod-docs checkout, which is a SEPARATE repo. Excluded by
|
|
152
|
+
# default for the same reason `live` is: it reads state this repo does not own,
|
|
153
|
+
# so a plain `pytest` must never depend on it.
|
|
154
|
+
#
|
|
155
|
+
# Specifically, these tests pin the exact fenced-block sequence of every published
|
|
156
|
+
# tutorial. That is the right assertion for catching a broken tutorial, but it also
|
|
157
|
+
# means an ordinary tutorial edit in snowflake-prod-docs would fail THIS repo's
|
|
158
|
+
# suite for anyone who happens to keep a sibling docs checkout — a docs change
|
|
159
|
+
# breaking an SDK test run. CI never checks the docs repo out, so CI was never
|
|
160
|
+
# exposed; this keeps local runs equally unexposed.
|
|
161
|
+
#
|
|
162
|
+
# --docs-root still defaults to ../snowflake-prod-docs, so opting in needs no flag.
|
|
163
|
+
"docs: tests that read a snowflake-prod-docs checkout (see --docs-root)",
|
|
164
|
+
# The promotion gate. A fast subset of `live` that a deployment must pass before it
|
|
165
|
+
# is promoted to the next tier. Chosen so every member fails LOUDLY on an unhealthy
|
|
166
|
+
# deployment: nothing that skips on error, and nothing unbounded. Run with `-m smoke`.
|
|
167
|
+
#
|
|
168
|
+
# Measured on qa6-mdrach, 2026-10-05: **30 tests, 83s** (15 sync + 15 async;
|
|
169
|
+
# the 2026-09-24 figure of 15 tests / ~47s was the sync client only). Adding
|
|
170
|
+
# the two egress rows cost +16.6s over the 13-test predecessor's 29.9s.
|
|
171
|
+
#
|
|
172
|
+
# This used to say "no egress tests (they assert 'BLOCKED' and so pass for the wrong
|
|
173
|
+
# reason when networking is dead)". That reasoning was correct and is why egress was
|
|
174
|
+
# excluded — but it no longer describes the gate. The egress level's EXPLICIT rows are
|
|
175
|
+
# now gated, because the test that carries them defeats that specific objection: its
|
|
176
|
+
# restrictive leg asserts a liveness CONTROL (the sandbox can still reach its Snowflake
|
|
177
|
+
# account) so "refused" cannot be confused with "the proxy is refusing everything", and
|
|
178
|
+
# its permissive leg asserts a 200, which a dead network cannot produce.
|
|
179
|
+
#
|
|
180
|
+
# Still excluded, deliberately: the tier's DEFAULT egress level. Its correct value is
|
|
181
|
+
# tier-dependent while the opt-in flip rolls (permissive before, closed after), so
|
|
182
|
+
# gating it would freeze the train on precisely the change being rolled out. It stays in
|
|
183
|
+
# the broader `-m live` suite, where a human reads it against a known switch state.
|
|
154
184
|
"smoke: promotion-gate subset of the live suite",
|
|
155
185
|
]
|
|
156
186
|
filterwarnings = [
|
|
@@ -106,7 +106,12 @@ def build_egress(
|
|
|
106
106
|
|
|
107
107
|
egress = Egress(
|
|
108
108
|
external_access_integrations=integrations,
|
|
109
|
-
|
|
109
|
+
# Uses the current parameter name. The CLI FLAG stays `--no-default-egress`: it is
|
|
110
|
+
# released, renaming it would break users' scripts, and that is a separate decision
|
|
111
|
+
# from the library parameter's name. So the flag spelling and the parameter spelling
|
|
112
|
+
# differ here deliberately — the alternative is the CLI depending on a deprecated
|
|
113
|
+
# parameter, which is worse.
|
|
114
|
+
**({"allow_public_package_registries": False} if no_default_egress else {}),
|
|
110
115
|
)
|
|
111
116
|
# Validate now, not at create time. `compile_egress` is pure and is what the client
|
|
112
117
|
# runs anyway -- but it runs *after* connecting, so a mistyped host pair otherwise
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""``snow sandbox suspend`` and ``snow sandbox resume``.
|
|
2
|
+
|
|
3
|
+
The CLI counterparts to ``Sandbox.suspend()`` / ``Sandbox.resume()``. Snowflake
|
|
4
|
+
suspends an idle sandbox on its own timer; these let an operator bank a sandbox's
|
|
5
|
+
state on demand, and wake one without running anything in it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
# From _common, where these are DEFINED. Importing them via _get_command (which
|
|
15
|
+
# re-exports them) is an implicit re-export and mypy rejects it.
|
|
16
|
+
from snowflake.cli_sandbox._common import (
|
|
17
|
+
_CONN_HELP,
|
|
18
|
+
_USE_SNOW_CONN_HELP,
|
|
19
|
+
_apply_connection,
|
|
20
|
+
_resolve_sandbox_id,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _suspend_command(
|
|
25
|
+
sandbox: str = typer.Argument(..., help="Sandbox name (or id) to suspend."),
|
|
26
|
+
use_snow_connection: bool = typer.Option(
|
|
27
|
+
True,
|
|
28
|
+
"--use-snow-connection/--no-snow-connection",
|
|
29
|
+
help=_USE_SNOW_CONN_HELP,
|
|
30
|
+
),
|
|
31
|
+
connection: str | None = typer.Option(
|
|
32
|
+
None,
|
|
33
|
+
"--connection",
|
|
34
|
+
"-c",
|
|
35
|
+
help=_CONN_HELP,
|
|
36
|
+
),
|
|
37
|
+
**options: Any,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Suspend a sandbox: snapshot its filesystem and tear the container down.
|
|
40
|
+
|
|
41
|
+
The sandbox keeps its files; the next thing that touches it brings it back on
|
|
42
|
+
that snapshot with a fresh process tree. Already-suspended is success.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
snow sandbox suspend SANDBOX
|
|
46
|
+
"""
|
|
47
|
+
from snowflake.cli_sandbox._adapter import sdk_connection
|
|
48
|
+
from snowflake.sandbox.exceptions import SandboxError
|
|
49
|
+
from snowflake.sandbox.sync_client import get_sandbox
|
|
50
|
+
|
|
51
|
+
_apply_connection(use_snow_connection, connection)
|
|
52
|
+
try:
|
|
53
|
+
# The SYNC client: AsyncSandbox.connect is itself a coroutine, so a bare
|
|
54
|
+
# asyncio.run(sb.suspend()) on its result would fail on a coroutine object.
|
|
55
|
+
# get_sandbox(), not Sandbox.connect(): _resolve_sandbox_id already turned a
|
|
56
|
+
# name into a cntr_ id, and connect() resolves its argument as a NAME -- so
|
|
57
|
+
# feeding it an id fails with "no sandbox named cntr_...".
|
|
58
|
+
#
|
|
59
|
+
# The SYNC client throughout: AsyncSandbox.connect is itself a coroutine, so a
|
|
60
|
+
# bare asyncio.run(sb.suspend()) on its result fails on a coroutine object.
|
|
61
|
+
get_sandbox(_resolve_sandbox_id(sandbox), connection=sdk_connection()).suspend()
|
|
62
|
+
except SandboxError as exc:
|
|
63
|
+
typer.echo(f"error: {exc}", err=True)
|
|
64
|
+
raise typer.Exit(1) from exc
|
|
65
|
+
typer.echo(f"suspended {sandbox}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resume_command(
|
|
69
|
+
sandbox: str = typer.Argument(..., help="Sandbox name (or id) to resume."),
|
|
70
|
+
use_snow_connection: bool = typer.Option(
|
|
71
|
+
True,
|
|
72
|
+
"--use-snow-connection/--no-snow-connection",
|
|
73
|
+
help=_USE_SNOW_CONN_HELP,
|
|
74
|
+
),
|
|
75
|
+
connection: str | None = typer.Option(
|
|
76
|
+
None,
|
|
77
|
+
"--connection",
|
|
78
|
+
"-c",
|
|
79
|
+
help=_CONN_HELP,
|
|
80
|
+
),
|
|
81
|
+
**options: Any,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Wake a suspended sandbox without running anything in it.
|
|
84
|
+
|
|
85
|
+
Rarely needed -- anything that touches a sandbox resumes it anyway -- so this
|
|
86
|
+
is for paying the cold start up front. Returns once the resume is under way,
|
|
87
|
+
not once the sandbox is serving.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
snow sandbox resume SANDBOX
|
|
91
|
+
"""
|
|
92
|
+
from snowflake.cli_sandbox._adapter import sdk_connection
|
|
93
|
+
from snowflake.sandbox.exceptions import SandboxError
|
|
94
|
+
from snowflake.sandbox.sync_client import get_sandbox
|
|
95
|
+
|
|
96
|
+
_apply_connection(use_snow_connection, connection)
|
|
97
|
+
try:
|
|
98
|
+
# get_sandbox(), not Sandbox.connect(): _resolve_sandbox_id already turned a
|
|
99
|
+
# name into a cntr_ id, and connect() resolves its argument as a NAME.
|
|
100
|
+
get_sandbox(_resolve_sandbox_id(sandbox), connection=sdk_connection()).resume()
|
|
101
|
+
except SandboxError as exc:
|
|
102
|
+
typer.echo(f"error: {exc}", err=True)
|
|
103
|
+
raise typer.Exit(1) from exc
|
|
104
|
+
typer.echo(f"resuming {sandbox}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_SUSPEND_HELP = "Suspend a sandbox, snapshotting its filesystem.\n\nExample:\n snow sandbox suspend SANDBOX\n"
|
|
108
|
+
_RESUME_HELP = "Resume a suspended sandbox.\n\nExample:\n snow sandbox resume SANDBOX\n"
|
|
@@ -312,7 +312,13 @@ def _print_inferred_plan(plan: _RunPlan) -> None:
|
|
|
312
312
|
typer.echo(f"command: {plan.command or '(image default)'}")
|
|
313
313
|
if plan.egress is not None:
|
|
314
314
|
eais = getattr(plan.egress, "external_access_integrations", ())
|
|
315
|
-
|
|
315
|
+
# Read the RESOLVER, not any one spelling. There are three accepted names for this
|
|
316
|
+
# control and reading a specific field goes silently blind when a caller (or this
|
|
317
|
+
# CLI) uses a different one — which is exactly what happened: switching the flag's
|
|
318
|
+
# construction to allow_public_package_registries made this summary stop reporting
|
|
319
|
+
# "default-egress=closed" while the sandbox was still correctly closed. A dry run
|
|
320
|
+
# that under-reports the egress posture is worse than no dry run.
|
|
321
|
+
closed = getattr(plan.egress, "default_egress_allowed", None) is False
|
|
316
322
|
bits = [f"eai={','.join(eais)}" if eais else "", "default-egress=closed" if closed else ""]
|
|
317
323
|
if plan.secrets:
|
|
318
324
|
bits.append(f"secrets={len(plan.secrets)}")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The *selection* engine (the walk, the skip reasons, `UploadPlan`) lives in
|
|
4
4
|
`snowflake.sandbox._upload_plan`, because the library needs it too: `Sandbox.upload_dir`
|
|
5
|
-
and
|
|
5
|
+
and create-time bundle assembly are the same decision about the same trees. What stays here
|
|
6
6
|
is the part that is genuinely about a command line — turning argv tokens into targets,
|
|
7
7
|
and the errors that name CLI flags in their remediation.
|
|
8
8
|
|
|
@@ -26,6 +26,12 @@ from snowflake.cli_sandbox._common import (
|
|
|
26
26
|
_resolve_sandbox_id,
|
|
27
27
|
)
|
|
28
28
|
from snowflake.cli_sandbox._get_command import _CANCEL_HELP, _cancel_command, get
|
|
29
|
+
from snowflake.cli_sandbox._lifecycle_command import (
|
|
30
|
+
_RESUME_HELP,
|
|
31
|
+
_SUSPEND_HELP,
|
|
32
|
+
_resume_command,
|
|
33
|
+
_suspend_command,
|
|
34
|
+
)
|
|
29
35
|
from snowflake.cli_sandbox._run_command import _RUN_HELP, _run_command
|
|
30
36
|
from snowflake.cli_sandbox._shell_command import ( # noqa: F401 (re-export: import surface)
|
|
31
37
|
_CREATE_RETRY_ATTEMPTS,
|
|
@@ -279,10 +285,15 @@ async def _stream_process_logs(sb: Any, tail: int) -> None:
|
|
|
279
285
|
|
|
280
286
|
_LOGS_HELP = (
|
|
281
287
|
"Print or follow a sandbox's output.\n\n"
|
|
282
|
-
"By default this is the
|
|
283
|
-
"
|
|
284
|
-
"default; --follow tails the
|
|
285
|
-
"stdout to stdout and stderr to stderr so `2>` separates them
|
|
288
|
+
"By default this is the server's `all`: the container's own output plus the managed "
|
|
289
|
+
"process's, where a command container has one. Snapshot by "
|
|
290
|
+
"default; --follow instead tails the MANAGED process live until it finishes, printing "
|
|
291
|
+
"stdout to stdout and stderr to stderr so `2>` separates them. That is the only source "
|
|
292
|
+
"it can follow, which is why --source is snapshot-only.\n\n"
|
|
293
|
+
"--source reads a different captured stream: `exec` is what `exec()` calls printed, "
|
|
294
|
+
"kept per producer by guest-init; `base` is the container's own output; `all` is base "
|
|
295
|
+
"+ managed, and deliberately NOT exec, since an exec's output is already returned by "
|
|
296
|
+
"the call that produced it. --pid narrows `exec` to one producer.\n\n"
|
|
286
297
|
"--file reads a file inside the sandbox instead. That is how you read a "
|
|
287
298
|
"`run --detach` local-code run: its command is backgrounded rather than being the "
|
|
288
299
|
"managed process, so its output is tee'd to a file and the managed-process log is "
|
|
@@ -291,6 +302,8 @@ _LOGS_HELP = (
|
|
|
291
302
|
" snow sandbox logs SANDBOX_ID\n"
|
|
292
303
|
" snow sandbox logs SANDBOX_ID --tail 50\n"
|
|
293
304
|
" snow sandbox logs SANDBOX_ID --follow\n"
|
|
305
|
+
" snow sandbox logs SANDBOX_ID --source exec\n"
|
|
306
|
+
" snow sandbox logs SANDBOX_ID --source exec --pid 295\n"
|
|
294
307
|
" snow sandbox logs SANDBOX_ID --file /tmp/sandbox-run.out --follow"
|
|
295
308
|
)
|
|
296
309
|
|
|
@@ -318,6 +331,22 @@ def _logs_command(
|
|
|
318
331
|
metavar="MS",
|
|
319
332
|
help="Only lines at or after this server timestamp (Unix epoch ms). Ignored with --follow.",
|
|
320
333
|
),
|
|
334
|
+
source: str | None = typer.Option(
|
|
335
|
+
None,
|
|
336
|
+
"--source",
|
|
337
|
+
metavar="WHICH",
|
|
338
|
+
help="Which output to read: all (the default when omitted: base + managed, and it does "
|
|
339
|
+
"NOT include exec), managed (just the command= process), exec (what `exec()` calls "
|
|
340
|
+
"printed), or base (the container's own stdout/stderr). Snapshot only — --follow tails "
|
|
341
|
+
"the managed process and cannot follow another source.",
|
|
342
|
+
),
|
|
343
|
+
pid: int = typer.Option(
|
|
344
|
+
0,
|
|
345
|
+
"--pid",
|
|
346
|
+
metavar="N",
|
|
347
|
+
help="With --source exec, read only this producer instead of every exec merged "
|
|
348
|
+
"by capture time. The pid is shown per line in the merged output.",
|
|
349
|
+
),
|
|
321
350
|
file: str | None = typer.Option(
|
|
322
351
|
None,
|
|
323
352
|
"--file",
|
|
@@ -347,6 +376,8 @@ def _logs_command(
|
|
|
347
376
|
Example:
|
|
348
377
|
snow sandbox logs SANDBOX_ID --tail 50
|
|
349
378
|
snow sandbox logs SANDBOX_ID --follow
|
|
379
|
+
snow sandbox logs SANDBOX_ID --source exec
|
|
380
|
+
snow sandbox logs SANDBOX_ID --source exec --pid 295
|
|
350
381
|
snow sandbox logs SANDBOX_ID --file /tmp/sandbox-run.out
|
|
351
382
|
"""
|
|
352
383
|
import shlex
|
|
@@ -360,6 +391,19 @@ def _logs_command(
|
|
|
360
391
|
"--since applies to the managed-process log, not to --file (a file has no "
|
|
361
392
|
"server timestamps). Use --tail to limit the output instead."
|
|
362
393
|
)
|
|
394
|
+
# Each of these would otherwise be accepted and then silently ignored, which is worse
|
|
395
|
+
# than a rejection: the caller reads output and believes it is what they asked for.
|
|
396
|
+
if source is not None and file is not None:
|
|
397
|
+
raise typer.BadParameter(
|
|
398
|
+
"--source selects a captured stream; --file reads a path. Pass one."
|
|
399
|
+
)
|
|
400
|
+
if source is not None and follow and source not in ("managed", "process"):
|
|
401
|
+
raise typer.BadParameter(
|
|
402
|
+
f"--follow tails the managed process and cannot follow --source {source}. "
|
|
403
|
+
"Drop --follow for a snapshot, or drop --source to follow the managed process."
|
|
404
|
+
)
|
|
405
|
+
if pid and source != "exec":
|
|
406
|
+
raise typer.BadParameter("--pid selects one exec producer, so it needs --source exec.")
|
|
363
407
|
|
|
364
408
|
_apply_connection(use_snow_connection, connection)
|
|
365
409
|
|
|
@@ -373,7 +417,7 @@ def _logs_command(
|
|
|
373
417
|
elif follow:
|
|
374
418
|
await _stream_process_logs(sb, tail)
|
|
375
419
|
else:
|
|
376
|
-
typer.echo(await sb.logs(tail=tail, since_ts_ms=since))
|
|
420
|
+
typer.echo(await sb.logs(tail=tail, since_ts_ms=since, source=source, pid=pid))
|
|
377
421
|
|
|
378
422
|
async def _read_sandbox_file(sb: Any, path: str, *, tail: int, follow: bool) -> None:
|
|
379
423
|
"""Print, or tail, a file inside the sandbox.
|
|
@@ -554,3 +598,7 @@ app.command(name="delete", requires_connection=False, help=_DELETE_HELP)(_delete
|
|
|
554
598
|
# so these are plain top-level verbs alongside `list`/`logs`/`delete`.
|
|
555
599
|
app.command(name="get", requires_connection=False)(get)
|
|
556
600
|
app.command(name="cancel", requires_connection=False, help=_CANCEL_HELP)(_cancel_command)
|
|
601
|
+
# The two halves of the suspend/resume pair. Snowflake suspends an idle sandbox on its
|
|
602
|
+
# own timer; these make both directions available on demand.
|
|
603
|
+
app.command(name="suspend", requires_connection=False, help=_SUSPEND_HELP)(_suspend_command)
|
|
604
|
+
app.command(name="resume", requires_connection=False, help=_RESUME_HELP)(_resume_command)
|
|
@@ -28,6 +28,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
28
28
|
from snowflake.sandbox._version import __version__
|
|
29
29
|
from snowflake.sandbox.exceptions import (
|
|
30
30
|
SandboxAuthError,
|
|
31
|
+
SandboxBuildError,
|
|
31
32
|
SandboxConflictError,
|
|
32
33
|
SandboxContractWarning,
|
|
33
34
|
SandboxCreateError,
|
|
@@ -124,6 +125,7 @@ __all__ = [
|
|
|
124
125
|
"SandboxCreateError",
|
|
125
126
|
"SandboxFailure",
|
|
126
127
|
"SandboxValidationError",
|
|
128
|
+
"SandboxBuildError",
|
|
127
129
|
"SandboxConflictError",
|
|
128
130
|
"SandboxNotFoundError",
|
|
129
131
|
"SandboxNotReadyError",
|
|
@@ -75,7 +75,7 @@ def _collect_tree(
|
|
|
75
75
|
include: list[str] | None = None,
|
|
76
76
|
) -> tuple[list[tuple[str, bytes]], UploadPlan]:
|
|
77
77
|
"""Sorted ``(relpath, content)`` for every file under *src*, plus the plan that
|
|
78
|
-
says what was left out and why. Used by
|
|
78
|
+
says what was left out and why. Used by create-time bundle assembly.
|
|
79
79
|
|
|
80
80
|
Selection is delegated to `_upload_plan`, which is the one walk the CLI and
|
|
81
81
|
`Sandbox.upload_dir` also use. This function used to own a second, weaker walk:
|
|
@@ -123,7 +123,7 @@ def _collect_tree(
|
|
|
123
123
|
raise SandboxValidationError(
|
|
124
124
|
f"no files to bundle from {src}: all {len(plan.skipped)} path(s) were "
|
|
125
125
|
f"filtered out ({_reason_summary(plan)}). Pass include= to re-admit a "
|
|
126
|
-
f"filtered path, or point
|
|
126
|
+
f"filtered path, or point create(code=...) at the directory holding your code."
|
|
127
127
|
)
|
|
128
128
|
return files, plan
|
|
129
129
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Create-time code delivery via ``upload_dir`` (no stage / no bootstrap).
|
|
2
|
+
|
|
3
|
+
Deploy and ``Sandbox.create(code=)`` create an empty sandbox, upload the local
|
|
4
|
+
tree into ``CODE_DIR``, then signal a ready marker so a wait-then-exec main
|
|
5
|
+
command (detach / jobs / ``create(code=, command=…)``) can start the entry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shlex
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from snowflake.sandbox.client import AsyncSandbox
|
|
16
|
+
from snowflake.sandbox.sync_client import Sandbox
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CODE_DIR",
|
|
20
|
+
"READY_MARKER",
|
|
21
|
+
"cd_into_code_dir",
|
|
22
|
+
"entry_in_code_dir",
|
|
23
|
+
"signal_bundle_ready",
|
|
24
|
+
"signal_bundle_ready_sync",
|
|
25
|
+
"upload_bundle",
|
|
26
|
+
"upload_bundle_sync",
|
|
27
|
+
"wait_then_exec",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Where ``upload_dir`` lands the project. Relative entries ``cd`` here before exec.
|
|
31
|
+
CODE_DIR = "/tmp/_sandbox_code"
|
|
32
|
+
READY_MARKER = f"{CODE_DIR}/.sandbox_bundle_ready"
|
|
33
|
+
|
|
34
|
+
_CD_INTO_CODE_DIR = f"cd {shlex.quote(CODE_DIR)} && "
|
|
35
|
+
# A copy of this name inside the uploaded tree must not count as "ready" —
|
|
36
|
+
# the marker is written only after upload_dir returns.
|
|
37
|
+
_MARKER_EXCLUDE = [".sandbox_bundle_ready", "**/.sandbox_bundle_ready"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cd_into_code_dir() -> str:
|
|
41
|
+
"""Shell fragment that cds into ``CODE_DIR`` and stops the command if that fails."""
|
|
42
|
+
return _CD_INTO_CODE_DIR
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def entry_in_code_dir(entry: Sequence[str]) -> list[str]:
|
|
46
|
+
"""Wrap *entry* so it runs with cwd=``CODE_DIR`` after ``upload_dir``."""
|
|
47
|
+
inner = " ".join(shlex.quote(str(a)) for a in entry)
|
|
48
|
+
return ["/bin/bash", "-c", _CD_INTO_CODE_DIR + "exec " + inner]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def wait_then_exec(entry: Sequence[str]) -> list[str]:
|
|
52
|
+
"""Container main command: block until the ready marker exists, then exec *entry*.
|
|
53
|
+
|
|
54
|
+
Used when the entry must be the managed process (detach / jobs / ``create(code=)``)
|
|
55
|
+
but the bundle can only be uploaded after the sandbox is up.
|
|
56
|
+
"""
|
|
57
|
+
inner = " ".join(shlex.quote(str(a)) for a in entry)
|
|
58
|
+
ready = shlex.quote(READY_MARKER)
|
|
59
|
+
body = f"while [ ! -f {ready} ]; do sleep 0.1; done; " + _CD_INTO_CODE_DIR + "exec " + inner
|
|
60
|
+
return ["/bin/bash", "-c", body]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _signal_script() -> str:
|
|
64
|
+
return f"mkdir -p {shlex.quote(CODE_DIR)} && touch {shlex.quote(READY_MARKER)}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def upload_bundle(sb: AsyncSandbox, local_dir: str) -> None:
|
|
68
|
+
"""Upload *local_dir* into ``CODE_DIR`` on a live sandbox.
|
|
69
|
+
|
|
70
|
+
The ready-marker name is excluded so a file in the tree cannot release a
|
|
71
|
+
``wait_then_exec`` main process before the upload finishes.
|
|
72
|
+
"""
|
|
73
|
+
await sb.upload_dir(local_dir, CODE_DIR, exclude=_MARKER_EXCLUDE)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def upload_bundle_sync(sb: Sandbox, local_dir: str) -> None:
|
|
77
|
+
"""Sync counterpart of `upload_bundle`."""
|
|
78
|
+
sb.upload_dir(local_dir, CODE_DIR, exclude=_MARKER_EXCLUDE)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def signal_bundle_ready(sb: AsyncSandbox) -> None:
|
|
82
|
+
"""Write the ready marker so a `wait_then_exec` main process can proceed."""
|
|
83
|
+
await sb.exec(["/bin/bash", "-c", _signal_script()], timeout=60.0)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def signal_bundle_ready_sync(sb: Sandbox) -> None:
|
|
87
|
+
"""Sync counterpart of `signal_bundle_ready`."""
|
|
88
|
+
sb.exec(["/bin/bash", "-c", _signal_script()], timeout=60.0)
|
|
@@ -127,19 +127,19 @@ def preflight_checks(spec: DeploySpec, bundle_root: Path | None = None) -> list[
|
|
|
127
127
|
problems.append(Problem("error", str(exc)))
|
|
128
128
|
|
|
129
129
|
# A secret's `host` and Snowflake reachability are different controls: the first is
|
|
130
|
-
# enforced at the credential swap, the second by
|
|
131
|
-
# booleans. A secret scoped to a host the sandbox cannot reach on Snowflake is
|
|
130
|
+
# enforced at the credential swap, the second by the egress level
|
|
131
|
+
# (allow_public_package_registries) / the group booleans. A secret scoped to a host the sandbox cannot reach on Snowflake is
|
|
132
132
|
# silently useless, so say so. (eai_hosts is the SPCS perimeter and does not
|
|
133
133
|
# affect Snowflake reachability, so it is deliberately not consulted here.)
|
|
134
134
|
# Only warn when egress was *explicitly* closed AND no EAI was granted:
|
|
135
|
-
# -
|
|
135
|
+
# - the egress level is tri-state: left unset the platform applies its permissive
|
|
136
136
|
# default, and actual reachability is then an Istio question we cannot answer
|
|
137
137
|
# from here — warning on it would be noise on the common path.
|
|
138
138
|
# - allow_github / allow_pypi are NOT credited here, and now never could be: both
|
|
139
139
|
# are inert, so neither makes a host reachable in any mode. The retired
|
|
140
140
|
# allowed_egress_hosts is likewise not consulted — hosts named there are ignored
|
|
141
141
|
# platform-side.
|
|
142
|
-
# - Under
|
|
142
|
+
# - Under allow_public_package_registries=False the only grant left is an EAI, whose rules
|
|
143
143
|
# live in Snowflake and cannot be resolved from here — so an EAI being present is
|
|
144
144
|
# taken as "the caller has said how" (loop-invariant, hence hoisted out below).
|
|
145
145
|
eg = spec.egress
|
|
@@ -153,13 +153,15 @@ def preflight_checks(spec: DeploySpec, bundle_root: Path | None = None) -> list[
|
|
|
153
153
|
hint = (
|
|
154
154
|
f"grant {h!r} with an External Access Integration and pass "
|
|
155
155
|
f'external_access_integrations=("MY_EAI",) — under '
|
|
156
|
-
f"
|
|
156
|
+
f"allow_public_package_registries=False that is the only grant the "
|
|
157
|
+
f"platform applies"
|
|
157
158
|
)
|
|
158
159
|
problems.append(
|
|
159
160
|
Problem(
|
|
160
161
|
"warn",
|
|
161
162
|
f"secret {s.fqn!r} is scoped to host {h!r}, which the sandbox cannot "
|
|
162
|
-
f"reach with
|
|
163
|
+
f"reach with allow_public_package_registries=False — the credential "
|
|
164
|
+
f"could never be used",
|
|
163
165
|
hint,
|
|
164
166
|
)
|
|
165
167
|
)
|
{snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_env.py
RENAMED
|
@@ -117,7 +117,7 @@ def validate_env_mapping(
|
|
|
117
117
|
``str(False)`` coercion is a footgun — ``env(DEBUG=False)`` would bake the
|
|
118
118
|
string ``"False"``, which is truthy). *allow* names keys that are exempt
|
|
119
119
|
from the reserved check because the SDK sets them itself (e.g.
|
|
120
|
-
``SNOWFLAKE_CODE_URL
|
|
120
|
+
legacy ``SNOWFLAKE_CODE_URL`` stage bootstrap, now unused).
|
|
121
121
|
Returns a plain ``dict[str, str]``. Raises `SandboxError`.
|
|
122
122
|
"""
|
|
123
123
|
if not env:
|