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.
Files changed (96) hide show
  1. {snowflake_sandbox_python-0.2.2a3/src/snowflake_sandbox_python.egg-info → snowflake_sandbox_python-0.2.2a5}/PKG-INFO +1 -1
  2. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/pyproject.toml +38 -8
  3. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_egress_flags.py +6 -1
  4. snowflake_sandbox_python-0.2.2a5/src/snowflake/cli_sandbox/_lifecycle_command.py +108 -0
  5. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_run_command.py +7 -1
  6. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_upload_plan.py +1 -1
  7. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/commands.py +53 -5
  8. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/__init__.py +2 -0
  9. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_bundle.py +2 -2
  10. snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/_code_delivery.py +88 -0
  11. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_diagnostics.py +8 -6
  12. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_env.py +1 -1
  13. snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/_preview.py +164 -0
  14. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_shims.py +32 -4
  15. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sandbox_state.py +5 -0
  16. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_upload_plan.py +1 -1
  17. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_version.py +1 -1
  18. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_wire.py +64 -6
  19. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/client.py +389 -270
  20. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/connect.py +1 -1
  21. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/deploy.py +113 -22
  22. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/egress.py +114 -40
  23. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/exceptions.py +40 -5
  24. snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/image.py +573 -0
  25. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/jobs.py +54 -34
  26. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/session_app.py +16 -1
  27. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_client.py +389 -266
  28. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/warm_session.py +3 -3
  29. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5/src/snowflake_sandbox_python.egg-info}/PKG-INFO +1 -1
  30. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/SOURCES.txt +6 -1
  31. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/tests/test_docs_references.py +287 -3
  32. snowflake_sandbox_python-0.2.2a5/tests/test_gate_tristate.py +249 -0
  33. snowflake_sandbox_python-0.2.2a5/tests/test_tutorial_sources.py +485 -0
  34. snowflake_sandbox_python-0.2.2a3/src/snowflake/sandbox/image.py +0 -46
  35. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/LICENSE +0 -0
  36. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/README.md +0 -0
  37. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/setup.cfg +0 -0
  38. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/setup.py +0 -0
  39. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/__init__.py +0 -0
  40. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_adapter.py +0 -0
  41. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_common.py +0 -0
  42. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_get_command.py +0 -0
  43. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/_shell_command.py +0 -0
  44. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/plugin_spec.py +0 -0
  45. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/cli_sandbox/py.typed +0 -0
  46. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/__main__.py +0 -0
  47. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_ansi.py +0 -0
  48. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_args.py +0 -0
  49. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_assemble.py +0 -0
  50. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_connection_resolve.py +0 -0
  51. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_deploy_spec.py +0 -0
  52. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_files_mixin.py +0 -0
  53. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_fs_ops.py +0 -0
  54. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_globs.py +0 -0
  55. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_hosts.py +0 -0
  56. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_mcp_discovery.py +0 -0
  57. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_mcp_status.py +0 -0
  58. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_retry.py +0 -0
  59. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/__init__.py +0 -0
  60. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_fs_helper.py +0 -0
  61. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_job_runner.py +0 -0
  62. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_runtime/_protocol.py +0 -0
  63. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_session_registry.py +0 -0
  64. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sse.py +0 -0
  65. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_stage.py +0 -0
  66. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_files_mixin.py +0 -0
  67. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_fs_ops.py +0 -0
  68. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_transport.py +0 -0
  69. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_sync_watch.py +0 -0
  70. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_transport.py +0 -0
  71. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_transport_errors.py +0 -0
  72. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/_watch.py +0 -0
  73. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/app.py +0 -0
  74. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/config.py +0 -0
  75. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/exec_stream.py +0 -0
  76. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/failure.py +0 -0
  77. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/files.py +0 -0
  78. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/function.py +0 -0
  79. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/lifecycle.py +0 -0
  80. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/log_stream.py +0 -0
  81. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/mcp.py +0 -0
  82. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/mount.py +0 -0
  83. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/py.typed +0 -0
  84. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/secret.py +0 -0
  85. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/shell.py +0 -0
  86. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_exec_stream.py +0 -0
  87. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_files.py +0 -0
  88. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_log_stream.py +0 -0
  89. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/sync_shell.py +0 -0
  90. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake/sandbox/types.py +0 -0
  91. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/dependency_links.txt +0 -0
  92. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/entry_points.txt +0 -0
  93. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/requires.txt +0 -0
  94. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/src/snowflake_sandbox_python.egg-info/top_level.txt +0 -0
  95. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/tests/test_imports.py +0 -0
  96. {snowflake_sandbox_python-0.2.2a3 → snowflake_sandbox_python-0.2.2a5}/tests/test_smoke_offline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snowflake-sandbox-python
3
- Version: 0.2.2a3
3
+ Version: 0.2.2a5
4
4
  Summary: Snowflake sandboxes — Python SDK and CLI.
5
5
  Author: Snowflake, Inc.
6
6
  License:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "snowflake-sandbox-python"
7
- version = "0.2.2a3"
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: pytest -m live --connection <name>
146
- addopts = "-m 'not live'"
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
- # The promotion gate. A fast (~10 creates, 3-5 min) subset of `live` that a
150
- # deployment must pass before it is promoted to the next tier. Chosen so every
151
- # member fails LOUDLY on an unhealthy deployment: no egress tests (they assert
152
- # "BLOCKED" and so pass for the wrong reason when networking is dead), nothing
153
- # that skips on error, and nothing unbounded. Run with `-m smoke`.
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
- **({"allow_default_egress": False} if no_default_egress else {}),
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
- closed = getattr(plan.egress, "allow_default_egress", None) is False
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 the `from_local` bundle are the same decision about the same trees. What stays here
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 captured stdout/stderr of the sandbox's managed process, "
283
- "which only exists for command containers (created with `command=`). Snapshot by "
284
- "default; --follow tails the SAME source live until the process finishes, printing "
285
- "stdout to stdout and stderr to stderr so `2>` separates them.\n\n"
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 ``from_local``.
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 from_local at the directory holding your code."
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 allow_internet / the group
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
- # - allow_internet is tri-state: left unset the platform applies its permissive
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 allow_internet=False the only grant left is an EAI, whose network rules
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"allow_internet=False that is the only grant the platform applies"
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 allow_internet=False — the credential could never be used",
163
+ f"reach with allow_public_package_registries=False — the credential "
164
+ f"could never be used",
163
165
  hint,
164
166
  )
165
167
  )
@@ -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``, the presigned-download URL ``from_local`` injects).
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: