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,187 @@
|
|
|
1
|
+
"""Resolving `snow sandbox run`'s TARGETs — the argv half of the upload plan.
|
|
2
|
+
|
|
3
|
+
The *selection* engine (the walk, the skip reasons, `UploadPlan`) lives in
|
|
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
|
|
6
|
+
is the part that is genuinely about a command line — turning argv tokens into targets,
|
|
7
|
+
and the errors that name CLI flags in their remediation.
|
|
8
|
+
|
|
9
|
+
The engine's names are re-exported so this module remains the CLI's single view of the
|
|
10
|
+
plan, and `build_plan` is wrapped to keep the CLI's error type: the engine raises
|
|
11
|
+
`SandboxValidationError` for a malformed pattern, which the CLI reports as `TargetError`
|
|
12
|
+
alongside every other bad-input message.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from snowflake.sandbox._upload_plan import (
|
|
21
|
+
SKIP_DIRS,
|
|
22
|
+
ResolvedTarget,
|
|
23
|
+
Selected,
|
|
24
|
+
Skipped,
|
|
25
|
+
SkipReason,
|
|
26
|
+
UploadPlan,
|
|
27
|
+
_reject_unusable_name,
|
|
28
|
+
)
|
|
29
|
+
from snowflake.sandbox._upload_plan import build_plan as _engine_build_plan
|
|
30
|
+
from snowflake.sandbox.exceptions import SandboxValidationError
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SKIP_DIRS",
|
|
34
|
+
"ResolvedTarget",
|
|
35
|
+
"Selected",
|
|
36
|
+
"SkipReason",
|
|
37
|
+
"Skipped",
|
|
38
|
+
"TargetError",
|
|
39
|
+
"UploadPlan",
|
|
40
|
+
"build_plan",
|
|
41
|
+
"resolve_targets",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TargetError(Exception):
|
|
46
|
+
"""A target cannot be used. The message is user-facing and names the fix."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_plan(
|
|
50
|
+
targets: tuple[ResolvedTarget, ...],
|
|
51
|
+
*,
|
|
52
|
+
dest_root: str,
|
|
53
|
+
exclude: list[str] | None = None,
|
|
54
|
+
include: list[str] | None = None,
|
|
55
|
+
allow_credential_files: list[str] | None = None,
|
|
56
|
+
max_file_bytes: int | None = None,
|
|
57
|
+
) -> UploadPlan:
|
|
58
|
+
"""The engine's `build_plan`, with pattern errors reported as `TargetError`.
|
|
59
|
+
|
|
60
|
+
A malformed ``--exclude``/``--include`` is bad argv like any other, so it has to
|
|
61
|
+
arrive at the CLI's one bad-input handler rather than as a library exception the
|
|
62
|
+
command layer does not catch.
|
|
63
|
+
"""
|
|
64
|
+
_reject_bang_patterns(exclude, include)
|
|
65
|
+
try:
|
|
66
|
+
return _engine_build_plan(
|
|
67
|
+
targets,
|
|
68
|
+
dest_root=dest_root,
|
|
69
|
+
exclude=exclude,
|
|
70
|
+
include=include,
|
|
71
|
+
allow_credential_files=allow_credential_files,
|
|
72
|
+
max_file_bytes=max_file_bytes,
|
|
73
|
+
)
|
|
74
|
+
except SandboxValidationError as exc:
|
|
75
|
+
raise TargetError(str(exc)) from exc
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _reject_bang_patterns(exclude: list[str] | None, include: list[str] | None) -> None:
|
|
79
|
+
"""Reject a leading ``!`` in CLI vocabulary, before the engine sees it.
|
|
80
|
+
|
|
81
|
+
The engine refuses it too, but as a library it can only talk about ``include=``.
|
|
82
|
+
Here the remediation can name the flag that actually works, which is the whole
|
|
83
|
+
value of the message -- so the check is duplicated rather than translated.
|
|
84
|
+
"""
|
|
85
|
+
for pattern in (*(exclude or []), *(include or [])):
|
|
86
|
+
if pattern.startswith("!"):
|
|
87
|
+
raise TargetError(
|
|
88
|
+
f"{pattern!r}: '!' is not accepted here — use --include to re-admit "
|
|
89
|
+
f"paths instead of negating an --exclude."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def resolve_targets(given: list[str] | None) -> tuple[ResolvedTarget, ...]:
|
|
94
|
+
"""Validate and resolve every TARGET, or raise `TargetError`.
|
|
95
|
+
|
|
96
|
+
Enforces, in order: existence, the dangerous-root guard, a usable basename,
|
|
97
|
+
de-duplication, no basename collisions, and no nesting.
|
|
98
|
+
"""
|
|
99
|
+
raw = list(given or [])
|
|
100
|
+
if not raw:
|
|
101
|
+
return ()
|
|
102
|
+
|
|
103
|
+
home = Path.home().resolve()
|
|
104
|
+
resolved: list[ResolvedTarget] = []
|
|
105
|
+
seen: dict[Path, str] = {}
|
|
106
|
+
|
|
107
|
+
for token in raw:
|
|
108
|
+
expanded = Path(token).expanduser()
|
|
109
|
+
try:
|
|
110
|
+
path = expanded.resolve()
|
|
111
|
+
is_file, is_dir = path.is_file(), path.is_dir()
|
|
112
|
+
except OSError as exc:
|
|
113
|
+
raise TargetError(f"cannot read {token!r}: {exc.strerror or exc}") from exc
|
|
114
|
+
|
|
115
|
+
if not is_file and not is_dir:
|
|
116
|
+
if expanded.is_symlink():
|
|
117
|
+
raise TargetError(
|
|
118
|
+
f"{token!r} is a broken symlink (points at {os.readlink(expanded)!r})."
|
|
119
|
+
)
|
|
120
|
+
if expanded.exists():
|
|
121
|
+
raise TargetError(
|
|
122
|
+
f"{token!r} is not a regular file or directory; TARGET must be "
|
|
123
|
+
f"a file, a directory, or a .py App module."
|
|
124
|
+
)
|
|
125
|
+
raise TargetError(
|
|
126
|
+
f"no such file or directory: {token!r}. TARGET is a local path (file, "
|
|
127
|
+
f"directory, or .py App module); to run an image, pass it as "
|
|
128
|
+
f"`--image {token}` with no TARGET."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# The dangerous-root guard. These are never what anyone meant, and no
|
|
132
|
+
# per-file filter can make them safe: the credential denylist is a
|
|
133
|
+
# project-tree filter and misses ~/.kube/config, ~/.config/gcloud/..., and
|
|
134
|
+
# ~/.gitconfig outright. A merely *broad* upload is left to the size gate.
|
|
135
|
+
if path == home:
|
|
136
|
+
raise TargetError(
|
|
137
|
+
f"refusing to upload your home directory ({path}). Name the "
|
|
138
|
+
f"project directory or the files you meant instead."
|
|
139
|
+
)
|
|
140
|
+
if path.parent == path or path in home.parents:
|
|
141
|
+
raise TargetError(
|
|
142
|
+
f"refusing to upload {path} — it contains your home directory. "
|
|
143
|
+
f"Name the project directory or the files you meant instead."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# `Path('..').name` is '..' and `Path('.').name` is '' -- both would escape or
|
|
147
|
+
# empty the destination -- so the basename comes from the resolved path. '/'
|
|
148
|
+
# still resolves to an empty name, but the root guard above already caught it.
|
|
149
|
+
basename = path.name
|
|
150
|
+
if not basename or basename == ".." or "/" in basename or "\\" in basename:
|
|
151
|
+
raise TargetError(f"cannot derive a destination name for {token!r} ({path}).")
|
|
152
|
+
if bad := _reject_unusable_name(basename):
|
|
153
|
+
raise TargetError(f"cannot upload {token!r}: its name {bad}.")
|
|
154
|
+
|
|
155
|
+
if path in seen:
|
|
156
|
+
continue # './project' and '/abs/project' are one target, not two
|
|
157
|
+
seen[path] = token
|
|
158
|
+
resolved.append(ResolvedTarget(token, path, is_dir, basename))
|
|
159
|
+
|
|
160
|
+
_reject_collisions(resolved)
|
|
161
|
+
_reject_nesting(resolved)
|
|
162
|
+
return tuple(resolved)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _reject_collisions(targets: list[ResolvedTarget]) -> None:
|
|
166
|
+
"""Two targets sharing a basename would overwrite each other silently."""
|
|
167
|
+
by_name: dict[str, ResolvedTarget] = {}
|
|
168
|
+
for target in targets:
|
|
169
|
+
if clash := by_name.get(target.basename):
|
|
170
|
+
raise TargetError(
|
|
171
|
+
f"{target.given!r} and {clash.given!r} would both upload as "
|
|
172
|
+
f"{target.basename!r}. Rename one, or pass their parent directory."
|
|
173
|
+
)
|
|
174
|
+
by_name[target.basename] = target
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _reject_nesting(targets: list[ResolvedTarget]) -> None:
|
|
178
|
+
"""A target inside another would upload twice, to two different places."""
|
|
179
|
+
for outer in targets:
|
|
180
|
+
if not outer.is_dir:
|
|
181
|
+
continue
|
|
182
|
+
for inner in targets:
|
|
183
|
+
if inner is not outer and outer.path in inner.path.parents:
|
|
184
|
+
raise TargetError(
|
|
185
|
+
f"{inner.given!r} is inside {outer.given!r}; it would upload "
|
|
186
|
+
f"twice. Pass one or the other."
|
|
187
|
+
)
|