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.
- snowflake/cli_sandbox/__init__.py +13 -0
- snowflake/cli_sandbox/_adapter.py +170 -0
- snowflake/cli_sandbox/_common.py +77 -0
- snowflake/cli_sandbox/_egress_flags.py +121 -0
- snowflake/cli_sandbox/_get_command.py +109 -0
- snowflake/cli_sandbox/_run_command.py +1091 -0
- snowflake/cli_sandbox/_shell_command.py +666 -0
- snowflake/cli_sandbox/_upload_plan.py +187 -0
- snowflake/cli_sandbox/commands.py +556 -0
- snowflake/cli_sandbox/plugin_spec.py +28 -0
- snowflake/cli_sandbox/py.typed +0 -0
- snowflake/sandbox/__init__.py +317 -0
- snowflake/sandbox/__main__.py +225 -0
- snowflake/sandbox/_ansi.py +206 -0
- snowflake/sandbox/_args.py +208 -0
- snowflake/sandbox/_assemble.py +256 -0
- snowflake/sandbox/_bundle.py +240 -0
- snowflake/sandbox/_connection_resolve.py +328 -0
- snowflake/sandbox/_deploy_spec.py +56 -0
- snowflake/sandbox/_diagnostics.py +501 -0
- snowflake/sandbox/_env.py +143 -0
- snowflake/sandbox/_files_mixin.py +280 -0
- snowflake/sandbox/_fs_ops.py +304 -0
- snowflake/sandbox/_globs.py +176 -0
- snowflake/sandbox/_hosts.py +110 -0
- snowflake/sandbox/_mcp_discovery.py +288 -0
- snowflake/sandbox/_mcp_status.py +183 -0
- snowflake/sandbox/_retry.py +94 -0
- snowflake/sandbox/_runtime/__init__.py +42 -0
- snowflake/sandbox/_runtime/_fs_helper.py +93 -0
- snowflake/sandbox/_runtime/_job_runner.py +111 -0
- snowflake/sandbox/_runtime/_protocol.py +53 -0
- snowflake/sandbox/_runtime/_shims.py +267 -0
- snowflake/sandbox/_sandbox_state.py +303 -0
- snowflake/sandbox/_session_registry.py +222 -0
- snowflake/sandbox/_sse.py +160 -0
- snowflake/sandbox/_stage.py +270 -0
- snowflake/sandbox/_sync_files_mixin.py +272 -0
- snowflake/sandbox/_sync_fs_ops.py +185 -0
- snowflake/sandbox/_sync_transport.py +737 -0
- snowflake/sandbox/_sync_watch.py +99 -0
- snowflake/sandbox/_transport.py +1366 -0
- snowflake/sandbox/_transport_errors.py +270 -0
- snowflake/sandbox/_upload_plan.py +497 -0
- snowflake/sandbox/_version.py +37 -0
- snowflake/sandbox/_watch.py +164 -0
- snowflake/sandbox/_wire.py +348 -0
- snowflake/sandbox/app.py +256 -0
- snowflake/sandbox/client.py +2356 -0
- snowflake/sandbox/config.py +1133 -0
- snowflake/sandbox/connect.py +288 -0
- snowflake/sandbox/deploy.py +499 -0
- snowflake/sandbox/egress.py +388 -0
- snowflake/sandbox/exceptions.py +253 -0
- snowflake/sandbox/exec_stream.py +264 -0
- snowflake/sandbox/files.py +547 -0
- snowflake/sandbox/function.py +567 -0
- snowflake/sandbox/image.py +46 -0
- snowflake/sandbox/jobs.py +649 -0
- snowflake/sandbox/lifecycle.py +67 -0
- snowflake/sandbox/log_stream.py +219 -0
- snowflake/sandbox/mcp.py +480 -0
- snowflake/sandbox/mount.py +161 -0
- snowflake/sandbox/py.typed +0 -0
- snowflake/sandbox/secret.py +244 -0
- snowflake/sandbox/session_app.py +244 -0
- snowflake/sandbox/shell.py +556 -0
- snowflake/sandbox/sync_client.py +2245 -0
- snowflake/sandbox/sync_exec_stream.py +238 -0
- snowflake/sandbox/sync_files.py +377 -0
- snowflake/sandbox/sync_log_stream.py +142 -0
- snowflake/sandbox/sync_shell.py +413 -0
- snowflake/sandbox/types.py +193 -0
- snowflake/sandbox/warm_session.py +700 -0
- snowflake_sandbox_python-0.2.1a1.dist-info/METADATA +339 -0
- snowflake_sandbox_python-0.2.1a1.dist-info/RECORD +80 -0
- snowflake_sandbox_python-0.2.1a1.dist-info/WHEEL +5 -0
- snowflake_sandbox_python-0.2.1a1.dist-info/entry_points.txt +2 -0
- snowflake_sandbox_python-0.2.1a1.dist-info/licenses/LICENSE +202 -0
- 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"
|