snowflake-sandbox-python 0.2.1a1__py3-none-any.whl

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 (80) hide show
  1. snowflake/cli_sandbox/__init__.py +13 -0
  2. snowflake/cli_sandbox/_adapter.py +170 -0
  3. snowflake/cli_sandbox/_common.py +77 -0
  4. snowflake/cli_sandbox/_egress_flags.py +121 -0
  5. snowflake/cli_sandbox/_get_command.py +109 -0
  6. snowflake/cli_sandbox/_run_command.py +1091 -0
  7. snowflake/cli_sandbox/_shell_command.py +666 -0
  8. snowflake/cli_sandbox/_upload_plan.py +187 -0
  9. snowflake/cli_sandbox/commands.py +556 -0
  10. snowflake/cli_sandbox/plugin_spec.py +28 -0
  11. snowflake/cli_sandbox/py.typed +0 -0
  12. snowflake/sandbox/__init__.py +317 -0
  13. snowflake/sandbox/__main__.py +225 -0
  14. snowflake/sandbox/_ansi.py +206 -0
  15. snowflake/sandbox/_args.py +208 -0
  16. snowflake/sandbox/_assemble.py +256 -0
  17. snowflake/sandbox/_bundle.py +240 -0
  18. snowflake/sandbox/_connection_resolve.py +328 -0
  19. snowflake/sandbox/_deploy_spec.py +56 -0
  20. snowflake/sandbox/_diagnostics.py +501 -0
  21. snowflake/sandbox/_env.py +143 -0
  22. snowflake/sandbox/_files_mixin.py +280 -0
  23. snowflake/sandbox/_fs_ops.py +304 -0
  24. snowflake/sandbox/_globs.py +176 -0
  25. snowflake/sandbox/_hosts.py +110 -0
  26. snowflake/sandbox/_mcp_discovery.py +288 -0
  27. snowflake/sandbox/_mcp_status.py +183 -0
  28. snowflake/sandbox/_retry.py +94 -0
  29. snowflake/sandbox/_runtime/__init__.py +42 -0
  30. snowflake/sandbox/_runtime/_fs_helper.py +93 -0
  31. snowflake/sandbox/_runtime/_job_runner.py +111 -0
  32. snowflake/sandbox/_runtime/_protocol.py +53 -0
  33. snowflake/sandbox/_runtime/_shims.py +267 -0
  34. snowflake/sandbox/_sandbox_state.py +303 -0
  35. snowflake/sandbox/_session_registry.py +222 -0
  36. snowflake/sandbox/_sse.py +160 -0
  37. snowflake/sandbox/_stage.py +270 -0
  38. snowflake/sandbox/_sync_files_mixin.py +272 -0
  39. snowflake/sandbox/_sync_fs_ops.py +185 -0
  40. snowflake/sandbox/_sync_transport.py +737 -0
  41. snowflake/sandbox/_sync_watch.py +99 -0
  42. snowflake/sandbox/_transport.py +1366 -0
  43. snowflake/sandbox/_transport_errors.py +270 -0
  44. snowflake/sandbox/_upload_plan.py +497 -0
  45. snowflake/sandbox/_version.py +37 -0
  46. snowflake/sandbox/_watch.py +164 -0
  47. snowflake/sandbox/_wire.py +348 -0
  48. snowflake/sandbox/app.py +256 -0
  49. snowflake/sandbox/client.py +2356 -0
  50. snowflake/sandbox/config.py +1133 -0
  51. snowflake/sandbox/connect.py +288 -0
  52. snowflake/sandbox/deploy.py +499 -0
  53. snowflake/sandbox/egress.py +388 -0
  54. snowflake/sandbox/exceptions.py +253 -0
  55. snowflake/sandbox/exec_stream.py +264 -0
  56. snowflake/sandbox/files.py +547 -0
  57. snowflake/sandbox/function.py +567 -0
  58. snowflake/sandbox/image.py +46 -0
  59. snowflake/sandbox/jobs.py +649 -0
  60. snowflake/sandbox/lifecycle.py +67 -0
  61. snowflake/sandbox/log_stream.py +219 -0
  62. snowflake/sandbox/mcp.py +480 -0
  63. snowflake/sandbox/mount.py +161 -0
  64. snowflake/sandbox/py.typed +0 -0
  65. snowflake/sandbox/secret.py +244 -0
  66. snowflake/sandbox/session_app.py +244 -0
  67. snowflake/sandbox/shell.py +556 -0
  68. snowflake/sandbox/sync_client.py +2245 -0
  69. snowflake/sandbox/sync_exec_stream.py +238 -0
  70. snowflake/sandbox/sync_files.py +377 -0
  71. snowflake/sandbox/sync_log_stream.py +142 -0
  72. snowflake/sandbox/sync_shell.py +413 -0
  73. snowflake/sandbox/types.py +193 -0
  74. snowflake/sandbox/warm_session.py +700 -0
  75. snowflake_sandbox_python-0.2.1a1.dist-info/METADATA +339 -0
  76. snowflake_sandbox_python-0.2.1a1.dist-info/RECORD +80 -0
  77. snowflake_sandbox_python-0.2.1a1.dist-info/WHEEL +5 -0
  78. snowflake_sandbox_python-0.2.1a1.dist-info/entry_points.txt +2 -0
  79. snowflake_sandbox_python-0.2.1a1.dist-info/licenses/LICENSE +202 -0
  80. snowflake_sandbox_python-0.2.1a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ """The ``snow sandbox`` command group, as a Snowflake CLI plugin.
2
+
3
+ Installed with the ``cli`` extra and discovered by the CLI through the
4
+ ``snowflake.cli.plugin.command`` entry point in `plugin_spec`. It is a thin front
5
+ end: `commands` parses argv and formats output, `_adapter` borrows the CLI's own
6
+ resolved connection for credentials, and everything underneath is the ordinary
7
+ ``snowflake.sandbox`` API — so the CLI and the library cannot drift apart in
8
+ behaviour.
9
+
10
+ Nothing here is importable public API; depend on ``snowflake.sandbox`` instead.
11
+ """
12
+
13
+ from __future__ import annotations
@@ -0,0 +1,170 @@
1
+ """Borrow the ``snow`` CLI's own connection for this ``snow sandbox`` process.
2
+
3
+ When a ``snow sandbox`` command needs Snowflake credentials, we let the CLI
4
+ resolve and open the connection with *its* logic — the global ``--connection`` /
5
+ ``-c`` / ``--account`` / ``--user`` flags, ``connections.toml`` plus the CLI's own
6
+ env overrides, and every auth method (password, key-pair, OAuth, external
7
+ browser). We then adopt the resulting live connection object as this process's
8
+ credential authority (REST bearer + stage PUT run on it).
9
+
10
+ The resolved `Config` is held **here**, in the plugin, and every SDK call the
11
+ plugin makes passes it explicitly as ``connection=`` (see `sdk_connection`). A
12
+ process-wide default is legitimate at this layer — a CLI invocation *is* a process —
13
+ and keeping it here is what lets the library itself stay free of ambient credential
14
+ state. `use_snow_connection` is therefore called once per command, before any SDK
15
+ call; `sdk_connection` is what the call sites read.
16
+
17
+ So the plugin does no connection handling of its own: it is CLI parity by
18
+ construction, and the SDK's own resolver is used only outside the CLI (the
19
+ programmatic path) — or here, when this module resolved nothing and
20
+ `sdk_connection` returns ``None``. An explicit token/PAT already in the environment —
21
+ a bare ``SNOWFLAKE_PAT``, or the ``SNOWFLAKE_ACCOUNT_URL`` + ``SNOWFLAKE_TOKEN`` that
22
+ Snowflake injects into a running sandbox — short-circuits this when no connection was
23
+ named, so the CLI stays usable without a configured connection or a live SQL session.
24
+ But an explicit ``-c``/``--connection`` wins over that env token: naming a connection is
25
+ taken as meaning it, so the flag is honoured rather than dropped.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import TYPE_CHECKING
31
+
32
+ if TYPE_CHECKING:
33
+ from snowflake.sandbox.config import Config, ConnectionLike
34
+
35
+ # The connection this ``snow sandbox`` process runs against, resolved once by
36
+ # `use_snow_connection` and passed explicitly as ``connection=`` on every SDK call the
37
+ # plugin makes. ``None`` means "nothing adopted" — either the user opted out with
38
+ # ``--no-snow-connection`` or an env token short-circuited us — and is forwarded as
39
+ # ``connection=None``, which is exactly the SDK's own env/``connections.toml``
40
+ # resolution.
41
+ #
42
+ # Deliberately NOT a ContextVar: a CLI invocation is the whole process, and a scope
43
+ # that is never exited leaks process-wide (it contaminated 62 unrelated tests when
44
+ # tried). Deliberately NOT in the SDK: that is the ambient credential state this
45
+ # package exists to keep out of the library.
46
+ _connection: Config | None = None
47
+
48
+
49
+ def _env_token_var() -> str | None:
50
+ """Name of the env token that short-circuits the CLI connection, or ``None``.
51
+
52
+ ``SNOWFLAKE_PAT`` takes precedence over ``SNOWFLAKE_TOKEN`` (same order
53
+ `_make_connection`/`_from_env` read them), so report whichever wins.
54
+ """
55
+ import os
56
+
57
+ if os.environ.get("SNOWFLAKE_PAT"):
58
+ return "SNOWFLAKE_PAT"
59
+ if os.environ.get("SNOWFLAKE_TOKEN"):
60
+ return "SNOWFLAKE_TOKEN"
61
+ return None
62
+
63
+
64
+ def _env_has_token() -> bool:
65
+ return _env_token_var() is not None
66
+
67
+
68
+ def use_snow_connection(connection: str | None = None) -> None:
69
+ """Adopt the active ``snow`` CLI connection for this process.
70
+
71
+ No-op when an explicit token/PAT is already in the environment (bare PAT, or
72
+ Snowflake-injected inside a sandbox) — that credential wins and needs no live
73
+ session, and `sdk_connection` stays ``None`` so SDK calls resolve it from the
74
+ environment themselves. Otherwise open the CLI's connection and hold its resolved
75
+ `Config`, which `sdk_connection` then hands to every SDK call.
76
+
77
+ ``connection`` names the connection to use. These commands run with
78
+ ``requires_connection=False`` (so the CLI does not inject its own global
79
+ ``-c/--connection``); the plugin exposes its own ``--connection`` and resolves
80
+ it here. When omitted, the default connection
81
+ (``SNOWFLAKE_DEFAULT_CONNECTION_NAME`` or the ``default_connection_name`` in
82
+ config.toml) is used — the same connection the CLI would pick.
83
+
84
+ An explicitly named connection wins over an ambient env token: a user who
85
+ passes ``-c/--connection`` is naming the account they mean, so the flag is
86
+ honoured even when ``SNOWFLAKE_PAT``/``SNOWFLAKE_TOKEN`` is set. The env token
87
+ only short-circuits when no connection was named — the common in-sandbox case
88
+ where Snowflake injects the credential and there is no configured connection.
89
+ """
90
+ from snowflake.cli.api.cli_global_context import fork_cli_context, get_cli_context
91
+
92
+ if connection is not None:
93
+ # Resolve the NAMED connection with the CLI's own machinery, by forking the
94
+ # context with its connection_name -- not with a parser of our own. Every
95
+ # auth method the CLI supports (key-pair incl. inline private_key, every
96
+ # OAuth flow, MFA, SSO/Okta authenticator URLs, session/master token,
97
+ # workload identity) and every endpoint field (host, port, protocol, region,
98
+ # an underscore account identifier) is then handled by the CLI, because it is
99
+ # the CLI doing the handling. This runs regardless of any env token: an
100
+ # explicit -c beats the ambient credential.
101
+ with fork_cli_context(connection_overrides={"connection_name": connection}):
102
+ _adopt_cli_connection(get_cli_context().connection)
103
+ return
104
+
105
+ if _env_token_var() is not None:
106
+ # No connection named, so the ambient token/PAT (a bare PAT, or the
107
+ # host + token Snowflake injects inside a sandbox) wins and needs no live
108
+ # session: the SDK reads it from the environment directly.
109
+ return
110
+
111
+ _adopt_cli_connection(get_cli_context().connection)
112
+
113
+
114
+ def sdk_connection() -> Config | None:
115
+ """The connection every SDK call in this process must be made with.
116
+
117
+ Pass the result as ``connection=`` — ``list_sandboxes(connection=sdk_connection())``
118
+ — on every SDK entry point the plugin calls. ``None`` is a valid, meaningful value:
119
+ it means nothing was adopted (``--no-snow-connection``, or an env token
120
+ short-circuited `use_snow_connection`), and ``connection=None`` is the SDK's own
121
+ env/``connections.toml`` resolution.
122
+
123
+ Read it at the call site rather than threading it down: `sdk_connection` is the one
124
+ place the answer lives, so a command that resolved a connection cannot end up
125
+ making a call against a different one.
126
+ """
127
+ return _connection
128
+
129
+
130
+ def reset_sdk_connection() -> None:
131
+ """Forget the adopted connection.
132
+
133
+ A real ``snow sandbox`` invocation never needs this — the process exits. It exists
134
+ so tests (and anything driving the plugin in-process) can get a clean slate instead
135
+ of inheriting the previous command's credentials.
136
+ """
137
+ global _connection
138
+ _connection = None
139
+
140
+
141
+ def _adopt_cli_connection(conn: ConnectionLike) -> None:
142
+ """Adopt the CLI-built connection as this process's credential authority.
143
+
144
+ Resolves it ONCE, here, into the `Config` the plugin then hands to every SDK call.
145
+ The live connection object is carried on that Config: no credential *derivation*
146
+ happens: the SDK reads what it needs off it (its REST bearer ``rest.token``, read
147
+ live so a connector renewal is picked up, and its endpoint ``host``/``port``) and
148
+ reuses the SAME connection for a later stage PUT. So every auth method the CLI
149
+ resolved (password, key-pair, OAuth, external browser, session/master token) just
150
+ works and nothing re-authenticates with a mis-derived scheme.
151
+
152
+ Resolving once rather than per call also keeps the SDK's transport pool to a single
153
+ entry for the process: it keys a caller-supplied connection on the identity of the
154
+ session object, which this Config holds.
155
+ """
156
+ global _connection
157
+
158
+ from snowflake.sandbox.config import resolve_config
159
+ from snowflake.sandbox.exceptions import SandboxError
160
+
161
+ token = getattr(getattr(conn, "rest", None), "token", None)
162
+ host = getattr(conn, "host", None)
163
+ if not token or not host:
164
+ raise SandboxError(
165
+ "the active snow connection did not yield a session token; check the "
166
+ "connection (`snow connection test`) or set SNOWFLAKE_PAT."
167
+ )
168
+ # The Config holds the connection (keeping a reference alive) and nothing closes
169
+ # it — the CLI owns its lifetime.
170
+ _connection = resolve_config(connection=conn)
@@ -0,0 +1,77 @@
1
+ """Shared leaves for the ``snow sandbox`` command modules.
2
+
3
+ Help-text constants and the small helpers several commands read
4
+ (``_parse_env``, ``_apply_connection``, ``_resolve_sandbox_id``). They live here —
5
+ a dependency-free leaf importable by ``commands`` and the split-out command
6
+ modules (``_shell_command``, ``_get_command``) alike — so those modules never
7
+ have to import each other, keeping the CLI package acyclic.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import typer
13
+
14
+ _ENV_HELP = "Runtime env var passed into the container as KEY=VAL; repeatable."
15
+ _USE_SNOW_CONN_HELP = (
16
+ "Use the active snow connection's credentials (default). --no-snow-connection "
17
+ "falls back to SDK env/config resolution (SNOWFLAKE_PAT, ~/.snowflake). An "
18
+ "explicit SNOWFLAKE_PAT/TOKEN in the environment is used either way."
19
+ )
20
+ _CONN_HELP = (
21
+ "Named connection from ~/.snowflake to use (defaults to your default "
22
+ "connection). Provided by the plugin because `snow sandbox` commands do not "
23
+ "take the CLI's global -c."
24
+ )
25
+
26
+
27
+ def _parse_env(value: str) -> tuple[str, str]:
28
+ if "=" not in value:
29
+ raise typer.BadParameter(f"expected KEY=VALUE, got {value!r}")
30
+ k, _, v = value.partition("=")
31
+ if not k:
32
+ raise typer.BadParameter(f"empty key in {value!r}")
33
+ return k, v
34
+
35
+
36
+ def _apply_connection(
37
+ use_snow_connection: bool,
38
+ connection: str | None = None,
39
+ ) -> None:
40
+ """Resolve the active snow connection for this process.
41
+
42
+ Does nothing when the user opted out with --no-snow-connection, and is a no-op
43
+ when an explicit SNOWFLAKE_PAT/TOKEN is set (the adapter short-circuits), so it
44
+ is safe to call on every credentialed command.
45
+
46
+ ``connection`` names the connection to use — the plugin's own --connection/-c,
47
+ since the CLI does not inject one on these requires_connection=False commands.
48
+ Omitted means the default connection.
49
+
50
+ Stores nothing in the SDK: it resolves into the plugin's own
51
+ `_adapter.sdk_connection`, which every SDK call downstream passes as
52
+ ``connection=``. Call it before the first SDK call in a command.
53
+ """
54
+ if use_snow_connection:
55
+ from snowflake.cli_sandbox._adapter import use_snow_connection as _use
56
+
57
+ _use(connection=connection)
58
+
59
+
60
+ def _resolve_sandbox_id(identifier: str) -> str:
61
+ """Map a user-facing sandbox reference to its underlying app id.
62
+
63
+ The name is the identity users see, so ``shell``/``logs`` accept it. An app id
64
+ (``cntr_`` prefix) passes through; any other value is a vanity NAME resolved via
65
+ the by-name lookup (unique per owner).
66
+
67
+ Call after _apply_connection: the lookup runs against the connection that resolved,
68
+ so the name is looked up in the account the rest of the command will act on. Doing
69
+ it the other way round finds a name in the default account and then operates on it
70
+ in another.
71
+ """
72
+ if not identifier or identifier.startswith("cntr_"):
73
+ return identifier
74
+ from snowflake.cli_sandbox._adapter import sdk_connection
75
+ from snowflake.sandbox.sync_client import get_sandbox_by_name
76
+
77
+ return get_sandbox_by_name(identifier, connection=sdk_connection()).id
@@ -0,0 +1,121 @@
1
+ """`--eai` / `--secret` / `--no-default-egress`: the egress surface for the CLI.
2
+
3
+ The SDK has had `Egress` and `Secret` since the beginning, but nothing in the CLI
4
+ could reach them — so an EAI-granted or credential-bearing sandbox required writing
5
+ Python, even though `run` is the primary verb. This module is the translation layer:
6
+ flag strings in, an `Egress` out.
7
+
8
+ Spellings, chosen to match what the CLI already does elsewhere:
9
+
10
+ * ``--eai NAME`` — repeatable, like ``--env``. Names an External Access Integration
11
+ whose network rules the platform resolves; that is the only way to reach a host
12
+ outside the built-in baseline.
13
+ * ``--secret ENV_VAR=DB.SCHEMA.NAME@host[,host]`` — repeatable. The env var comes
14
+ first because that is the order ``--env KEY=VAL`` established, and it is the half
15
+ you read inside the sandbox. Omitting ``ENV_VAR=`` takes the SDK's derived
16
+ ``CNG_SECRET_<FQN>`` name. The ``@host`` scopes where the credential may be sent and
17
+ is **always required**: the SDK refuses an unscoped credential rather than let it
18
+ egress anywhere. (Its error text says ``host=`` becomes optional once an EAI supplies
19
+ the hosts — that is not implemented; an EAI does not relax the requirement, verified
20
+ against `compile_egress`.)
21
+ * ``--no-default-egress`` — the closed level: Snowflake and its stages only, no
22
+ baseline package-manager hosts. An ``--eai`` still grants on top of it, which is
23
+ why this is not called ``--no-egress``.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import typer
29
+
30
+ from snowflake.sandbox.egress import Egress, compile_egress
31
+ from snowflake.sandbox.secret import Secret
32
+
33
+ __all__ = ["EAI_HELP", "NO_DEFAULT_EGRESS_HELP", "SECRET_HELP", "build_egress"]
34
+
35
+ EAI_HELP = (
36
+ "External Access Integration granting egress to hosts outside the built-in "
37
+ "baseline; repeatable. Your role needs USAGE on it, and its network rules must be "
38
+ "readable or the platform refuses the create."
39
+ )
40
+
41
+ SECRET_HELP = (
42
+ "Snowflake SECRET to inject, as ENV_VAR=DB.SCHEMA.NAME@host[,host]; repeatable. "
43
+ "The @host scopes where the credential may be sent and is required. Omit ENV_VAR= "
44
+ "for the derived CNG_SECRET_<FQN> name."
45
+ )
46
+
47
+ NO_DEFAULT_EGRESS_HELP = (
48
+ "Close the default egress: Snowflake and its stages only, dropping the baseline "
49
+ "package-manager hosts. An --eai still grants on top of this."
50
+ )
51
+
52
+
53
+ def _parse_secret(raw: str) -> Secret:
54
+ """One ``--secret`` value into a `Secret`, or a `typer.BadParameter`."""
55
+ spec = raw.strip()
56
+ if not spec:
57
+ raise typer.BadParameter("--secret needs a value, e.g. GH_TOKEN=MYDB.SECRETS.GH")
58
+
59
+ env_var: str | None = None
60
+ if "=" in spec:
61
+ # Split on the FIRST '=' only: an FQN cannot contain one, so everything after
62
+ # it belongs to the secret reference.
63
+ env_var, spec = spec.split("=", 1)
64
+ env_var, spec = env_var.strip(), spec.strip()
65
+ if not env_var:
66
+ raise typer.BadParameter(f"empty env var in {raw!r} (expected ENV_VAR=DB.SCHEMA.NAME)")
67
+
68
+ hosts: tuple[str, ...] = ()
69
+ if "@" in spec:
70
+ # Split on the LAST '@' so a host list is unambiguous; FQNs have no '@'.
71
+ spec, host_part = spec.rsplit("@", 1)
72
+ hosts = tuple(h.strip() for h in host_part.split(",") if h.strip())
73
+ if not hosts:
74
+ raise typer.BadParameter(f"empty host in {raw!r} (expected ...@github.com)")
75
+
76
+ fqn = spec.strip()
77
+ if fqn.count(".") != 2:
78
+ raise typer.BadParameter(
79
+ f"{fqn!r} is not a fully-qualified SECRET name; write DB.SCHEMA.NAME (from {raw!r})."
80
+ )
81
+ try:
82
+ return Secret.from_name(fqn, env_var=env_var, hosts=hosts or None)
83
+ except (ValueError, TypeError) as exc: # the SDK validates host coverage
84
+ raise typer.BadParameter(f"{raw!r}: {exc}") from exc
85
+
86
+
87
+ def build_egress(
88
+ *,
89
+ eai: list[str] | None,
90
+ secret: list[str] | None,
91
+ no_default_egress: bool,
92
+ ) -> tuple[Egress | None, tuple[Secret, ...]]:
93
+ """`(egress, secrets)` for these flags — both empty when none were given.
94
+
95
+ Two values because that is the shape `Sandbox.create()` takes: secrets are authored
96
+ alongside egress but are their own argument (they nest under egress only on the
97
+ wire, where `compile_egress` puts them).
98
+
99
+ `None` rather than an empty `Egress` when nothing was passed, so the create body
100
+ stays byte-identical to today's for every existing caller.
101
+ """
102
+ integrations = tuple(dict.fromkeys(name.strip() for name in (eai or []) if name.strip()))
103
+ secrets = tuple(_parse_secret(raw) for raw in (secret or []))
104
+ if not integrations and not secrets and not no_default_egress:
105
+ return None, ()
106
+
107
+ egress = Egress(
108
+ external_access_integrations=integrations,
109
+ **({"allow_default_egress": False} if no_default_egress else {}),
110
+ )
111
+ # Validate now, not at create time. `compile_egress` is pure and is what the client
112
+ # runs anyway -- but it runs *after* connecting, so a mistyped host pair otherwise
113
+ # costs a Snowflake round trip before saying so. These are usage errors; they should
114
+ # cost nothing.
115
+ from snowflake.sandbox.exceptions import SandboxError
116
+
117
+ try:
118
+ compile_egress(egress, secrets)
119
+ except SandboxError as exc:
120
+ raise typer.BadParameter(str(exc)) from exc
121
+ return egress, secrets
@@ -0,0 +1,109 @@
1
+ """``snow sandbox get`` and ``snow sandbox cancel``.
2
+
3
+ Split out of ``commands.py`` unchanged: the two callbacks that address a single
4
+ sandbox by name/id — ``get`` (show status, optionally block for the result) and
5
+ ``cancel`` (destroy it). ``commands`` imports these and registers them onto the
6
+ shared ``app`` in the original order; the shared helpers they read come from
7
+ ``_common``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ from typing import Any
15
+
16
+ import typer
17
+
18
+ from snowflake.cli_sandbox._common import (
19
+ _CONN_HELP,
20
+ _USE_SNOW_CONN_HELP,
21
+ _apply_connection,
22
+ _resolve_sandbox_id,
23
+ )
24
+
25
+
26
+ def get(
27
+ sandbox: str = typer.Argument(..., help="Sandbox name (or id) whose status to show."),
28
+ wait: float | None = typer.Option(
29
+ None,
30
+ "--wait",
31
+ "-w",
32
+ metavar="SECONDS",
33
+ help="Block up to SECONDS for a terminal result, then print it.",
34
+ ),
35
+ use_snow_connection: bool = typer.Option(
36
+ True,
37
+ "--use-snow-connection/--no-snow-connection",
38
+ help=_USE_SNOW_CONN_HELP,
39
+ ),
40
+ connection: str | None = typer.Option(
41
+ None,
42
+ "--connection",
43
+ "-c",
44
+ help=_CONN_HELP,
45
+ ),
46
+ **options: Any,
47
+ ) -> None:
48
+ """Show a sandbox's status; with --wait, block for its result.
49
+
50
+ Example:
51
+ snow sandbox get SANDBOX
52
+ snow sandbox get SANDBOX --wait 120
53
+ """
54
+ from snowflake.cli_sandbox._adapter import sdk_connection
55
+ from snowflake.sandbox.exceptions import SandboxError
56
+ from snowflake.sandbox.jobs import Job
57
+
58
+ _apply_connection(use_snow_connection, connection)
59
+ j = Job.from_id(_resolve_sandbox_id(sandbox), connection=sdk_connection())
60
+ try:
61
+ if wait is not None:
62
+ result = asyncio.run(j.get(timeout=wait))
63
+ typer.echo(f"sandbox: {sandbox}")
64
+ typer.echo(f"status: {result.status}")
65
+ typer.echo(f"exit_code: {result.exit_code}")
66
+ typer.echo(f"result: {json.dumps(result.result)}")
67
+ else:
68
+ status = asyncio.run(j.status())
69
+ typer.echo(f"sandbox: {sandbox}")
70
+ typer.echo(f"status: {status}")
71
+ except SandboxError as exc:
72
+ typer.echo(f"error: {exc}", err=True)
73
+ raise typer.Exit(1) from exc
74
+
75
+
76
+ def _cancel_command(
77
+ sandbox: str = typer.Argument(..., help="Sandbox name (or id) to cancel."),
78
+ use_snow_connection: bool = typer.Option(
79
+ True,
80
+ "--use-snow-connection/--no-snow-connection",
81
+ help=_USE_SNOW_CONN_HELP,
82
+ ),
83
+ connection: str | None = typer.Option(
84
+ None,
85
+ "--connection",
86
+ "-c",
87
+ help=_CONN_HELP,
88
+ ),
89
+ **options: Any,
90
+ ) -> None:
91
+ """Cancel a sandbox by destroying its container.
92
+
93
+ Example:
94
+ snow sandbox cancel SANDBOX
95
+ """
96
+ from snowflake.cli_sandbox._adapter import sdk_connection
97
+ from snowflake.sandbox.exceptions import SandboxError
98
+ from snowflake.sandbox.jobs import Job
99
+
100
+ _apply_connection(use_snow_connection, connection)
101
+ try:
102
+ asyncio.run(Job.from_id(_resolve_sandbox_id(sandbox), connection=sdk_connection()).cancel())
103
+ except SandboxError as exc:
104
+ typer.echo(f"error: {exc}", err=True)
105
+ raise typer.Exit(1) from exc
106
+ typer.echo(f"cancelled {sandbox}")
107
+
108
+
109
+ _CANCEL_HELP = "Cancel (destroy) a running sandbox.\n\nExample:\n snow sandbox cancel SANDBOX\n"