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,288 @@
1
+ """Snowflake connector helper for code running *inside* a Snowflake/gVisor sandbox.
2
+
3
+ `get_snowflake_connection()` is the single public entry point, and it is supported
4
+ public API — it is what workload code inside a sandbox calls to reach Snowflake. It
5
+ reads the injected env vars and returns a live ``snowflake.connector`` connection.
6
+
7
+ **Its contract is what the platform provides, not the caller's intent.** The credential
8
+ is resolved in order: ``SNOWFLAKE_PAT``/``SNOWFLAKE_TOKEN`` in the environment, then a
9
+ token file (``SNOWFLAKE_TOKEN_FILE_PATH``, else ``/snowflake/session/token``) with
10
+ ``SNOWFLAKE_ACCOUNT``, then the platform-rendered ``$SNOWFLAKE_HOME/connections.toml``.
11
+ Which one is present varies by deployment and is not the caller's business. It also
12
+ relies on the platform's egress-proxy CA at ``/etc/cng/proxy-ca.crt``. The token is **not** a real credential and is **not**
13
+ injected by Snowflake: it is a per-app placeholder minted by Snowflake's ``AppCredential``, and the
14
+ platform's outbound egress proxy swaps it for the account's real token on the way out
15
+ (the same brokering that keeps secrets out of the sandbox). None of that exists outside
16
+ a Snowflake sandbox, so this is **not** a general-purpose connector factory: calling it from
17
+ laptop or service code that is not running inside a sandbox is unsupported and will
18
+ either fail or, worse, widen that process's TLS trust (see `_patch_proxy_ca`) for no
19
+ reason. Use ``snowflake.connector.connect`` directly there.
20
+
21
+ So the narrowness here is a scope of *deployment*, not a scope of *seriousness*: this
22
+ is in-sandbox-only, not dev/test-only, and in-sandbox workload code is a production
23
+ caller. Please do not re-add a "dev / test use only" disclaimer — it would contradict
24
+ this module's place in the package's public ``__all__``.
25
+
26
+ This module has no framework dependency (no crewai, no langchain, no agents
27
+ layer) so it can be imported from any sandbox workload regardless of which
28
+ agent framework — or no framework at all — the workload uses.
29
+
30
+ ``snowflake-connector-python`` is a **core** dependency of the SDK, so it is
31
+ always installed. Importing this module stays cheap regardless: the connector is
32
+ loaded lazily inside `get_snowflake_connection()`, not at import time.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import os
38
+ import pathlib
39
+ from typing import Any
40
+
41
+ __all__ = ["get_snowflake_connection"]
42
+
43
+ # Connection parameters a caller may NOT override through get_snowflake_connection's
44
+ # **kwargs. These define *who* the connection authenticates as and *where* it
45
+ # connects; letting a caller (often an LLM assembling kwargs) redirect them would
46
+ # send the sandbox's brokered credential to an arbitrary host or swap the auth
47
+ # scheme. Everything else (warehouse, database, schema, role, session params, ...)
48
+ # is passed through.
49
+ _PROTECTED_CONNECT_KWARGS = frozenset(
50
+ {
51
+ "account",
52
+ "token",
53
+ "password",
54
+ "authenticator",
55
+ "host",
56
+ # connection_name / connections_file_path / token_file_path became
57
+ # live once the connections.toml path started passing connection_name:
58
+ # a caller-supplied TOML with a [default] entry would otherwise redirect
59
+ # account/host/token/authenticator wholesale.
60
+ "connection_name",
61
+ "connections_file_path",
62
+ "token_file_path",
63
+ }
64
+ )
65
+
66
+ # Default connection timeout (seconds). Without one the connector blocks
67
+ # indefinitely when egress is closed; a sandbox under a restrictive Egress policy
68
+ # would hang instead of failing.
69
+ _DEFAULT_LOGIN_TIMEOUT_S = 30
70
+
71
+ # Where a system CA bundle may live, in preference order. Debian/Ubuntu first,
72
+ # then RHEL/Fedora, then the BSD/macOS layout Alpine also uses.
73
+ _SYSTEM_CA_CANDIDATES = (
74
+ "/etc/ssl/certs/ca-certificates.crt",
75
+ "/etc/pki/tls/certs/ca-bundle.crt",
76
+ "/etc/ssl/cert.pem",
77
+ )
78
+
79
+
80
+ def _system_ca_bundle() -> str | None:
81
+ """Path to the OS's CA bundle, or ``None`` if none of the known ones exist.
82
+
83
+ Falls back to ``certifi`` (a hard dependency of the connector) so a slim /
84
+ distroless / RHEL / Alpine image without the Debian bundle still resolves a
85
+ real trust store rather than silently dropping the system CAs.
86
+ """
87
+ for path in _SYSTEM_CA_CANDIDATES:
88
+ if os.path.exists(path):
89
+ return path
90
+ try:
91
+ import certifi
92
+
93
+ return certifi.where()
94
+ except Exception: # noqa: BLE001 - certifi absent/broken; no system bundle
95
+ return None
96
+
97
+
98
+ def _patch_proxy_ca() -> None:
99
+ """Trust the Snowflake hostagent MITM CA alongside the system bundle.
100
+
101
+ When code runs inside a gVisor sandbox, the Snowflake hostagent intercepts
102
+ all outbound HTTPS and re-signs with its own CA at /etc/cng/proxy-ca.crt.
103
+ snowflake-connector-python uses requests/urllib3 internally; without this
104
+ patch every connector HTTPS call raises an SSL verification error.
105
+
106
+ **This widens TLS trust process-wide**: it sets ``REQUESTS_CA_BUNDLE`` and
107
+ ``SSL_CERT_FILE`` for the whole process, so *every* library that reads those
108
+ env vars — not just this connection — will additionally trust the Snowflake MITM CA.
109
+ That is intended inside a Snowflake sandbox (all egress is proxied) but is why this
110
+ module is not for general use outside one.
111
+
112
+ Called once per process: if REQUESTS_CA_BUNDLE / SSL_CERT_FILE is already set,
113
+ or the proxy CA file doesn't exist (outside a Snowflake sandbox), this is a no-op.
114
+ """
115
+ if os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("SSL_CERT_FILE"):
116
+ return
117
+ proxy_ca = "/etc/cng/proxy-ca.crt"
118
+ if not os.path.exists(proxy_ca):
119
+ return
120
+ import tempfile
121
+
122
+ sys_ca = _system_ca_bundle()
123
+ if sys_ca is None:
124
+ # Trusting ONLY the proxy CA would silently drop every public root, so a
125
+ # Snowflake TLS handshake to a non-proxied host would fail with an opaque
126
+ # SSL error. Fail loudly instead — the fix (install ca-certificates or set
127
+ # REQUESTS_CA_BUNDLE) is actionable.
128
+ raise RuntimeError(
129
+ "get_snowflake_connection: no system CA bundle found (looked in "
130
+ f"{', '.join(_SYSTEM_CA_CANDIDATES)} and certifi). Cannot trust the "
131
+ "Snowflake proxy CA without dropping the system roots. Install a CA bundle "
132
+ "(e.g. ca-certificates) or set REQUESTS_CA_BUNDLE to a bundle that "
133
+ "already includes /etc/cng/proxy-ca.crt."
134
+ )
135
+ with open(sys_ca) as f:
136
+ combined = f.read()
137
+ with open(proxy_ca) as f:
138
+ combined += "\n" + f.read()
139
+ # delete=False so the bundle outlives this block; the context manager just
140
+ # closes the handle, and tmp.name stays valid for the env vars below.
141
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False) as tmp:
142
+ tmp.write(combined)
143
+ os.environ["REQUESTS_CA_BUNDLE"] = tmp.name
144
+ os.environ["SSL_CERT_FILE"] = tmp.name
145
+
146
+
147
+ # Where the platform leaves the sandbox's session token when it names no other path,
148
+ # and the connection entry it renders. Both are platform contracts, not guesses:
149
+ # sandbox-server's _render_snowflake_connection writes exactly one [default] entry.
150
+ _DEFAULT_TOKEN_FILE = "/snowflake/session/token"
151
+ _PLATFORM_CONNECTION_NAME = "default"
152
+
153
+
154
+ def _resolve_token() -> str | None:
155
+ """The sandbox's session token, by whichever mechanism the platform used.
156
+
157
+ The env VALUE comes first (unchanged). Otherwise the token is a FILE: cng-explore
158
+ #1571 names it in SNOWFLAKE_TOKEN_FILE_PATH, and the well-known path is the
159
+ fallback. It is a per-app AppCredential placeholder, swapped for the real token by
160
+ the egress proxy on the way out — so reading it here is not handling a secret.
161
+ """
162
+ env_value = os.environ.get("SNOWFLAKE_PAT") or os.environ.get("SNOWFLAKE_TOKEN")
163
+ if env_value:
164
+ return env_value
165
+ path = os.environ.get("SNOWFLAKE_TOKEN_FILE_PATH") or _DEFAULT_TOKEN_FILE
166
+ try:
167
+ token = pathlib.Path(path).read_text(encoding="utf-8").strip()
168
+ except (OSError, UnicodeDecodeError):
169
+ return None
170
+ return token or None
171
+
172
+
173
+ def _host_from_account_url() -> str | None:
174
+ """SNOWFLAKE_ACCOUNT_URL as a bare host, since the connector wants no scheme."""
175
+ url = os.environ.get("SNOWFLAKE_ACCOUNT_URL", "").strip()
176
+ if not url:
177
+ return None
178
+ return url.split("://", 1)[-1].rstrip("/").split("/", 1)[0] or None
179
+
180
+
181
+ def _connections_toml() -> pathlib.Path | None:
182
+ """The platform-rendered connections.toml, if SNOWFLAKE_HOME holds one."""
183
+ home = os.environ.get("SNOWFLAKE_HOME", "").strip()
184
+ if not home:
185
+ return None
186
+ path = pathlib.Path(home) / "connections.toml"
187
+ return path if path.exists() else None
188
+
189
+
190
+ def _make_connection(**kwargs: Any) -> Any:
191
+ """Open a new Snowflake connection from env vars, merging ``kwargs`` overrides."""
192
+ protected = _PROTECTED_CONNECT_KWARGS & kwargs.keys()
193
+ if protected:
194
+ raise ValueError(
195
+ "get_snowflake_connection does not accept "
196
+ f"{sorted(protected)} as keyword overrides: the account, host, and "
197
+ "credential come from the sandbox's injected environment and must not "
198
+ "be redirected. Pass only non-credential options (warehouse, database, "
199
+ "schema, role, session parameters, ...)."
200
+ )
201
+ import snowflake.connector
202
+
203
+ account = os.environ.get("SNOWFLAKE_ACCOUNT")
204
+ token = _resolve_token()
205
+ if account and token:
206
+ params: dict[str, Any] = {
207
+ "account": account,
208
+ "token": token,
209
+ "authenticator": "oauth",
210
+ "login_timeout": _DEFAULT_LOGIN_TIMEOUT_S,
211
+ }
212
+ # SNOWFLAKE_HOST first, but ACCOUNT_URL is what today's platform actually sets
213
+ # and the host is not optional: without it the connector builds
214
+ # <account>.snowflakecomputing.com and fails 540001 on any non-default region.
215
+ host = os.environ.get("SNOWFLAKE_HOST") or _host_from_account_url()
216
+ if host:
217
+ params["host"] = host
218
+ for k, env in (
219
+ ("user", "SNOWFLAKE_USER"),
220
+ ("role", "SNOWFLAKE_ROLE"),
221
+ ("warehouse", "SNOWFLAKE_WAREHOUSE"),
222
+ ):
223
+ v = os.environ.get(env)
224
+ if v:
225
+ params[k] = v
226
+ params.update(kwargs)
227
+ return snowflake.connector.connect(**params)
228
+
229
+ # The platform's own rendered config. connection_name is passed explicitly because
230
+ # ANY kwarg otherwise suppresses the connector's default-connection resolution, and
231
+ # the caller may well pass warehouse= — which failed as 251005 "User is empty".
232
+ if _connections_toml() is not None:
233
+ # Merge like the branch above rather than splatting **kwargs alongside
234
+ # hard-coded keywords: a caller passing login_timeout= (documented as
235
+ # overridable) would otherwise be a duplicate-keyword TypeError.
236
+ params = {
237
+ "connection_name": _PLATFORM_CONNECTION_NAME,
238
+ "login_timeout": _DEFAULT_LOGIN_TIMEOUT_S,
239
+ }
240
+ params.update(kwargs)
241
+ return snowflake.connector.connect(**params)
242
+
243
+ raise RuntimeError(
244
+ "get_snowflake_connection found no usable credential. Looked for: "
245
+ "SNOWFLAKE_PAT/SNOWFLAKE_TOKEN in the environment; a token file at "
246
+ f"$SNOWFLAKE_TOKEN_FILE_PATH or {_DEFAULT_TOKEN_FILE} (with SNOWFLAKE_ACCOUNT "
247
+ f"set); and a [{_PLATFORM_CONNECTION_NAME}] entry in "
248
+ "$SNOWFLAKE_HOME/connections.toml. This helper is for code running INSIDE a "
249
+ "Snowflake sandbox, where the platform provides one of those."
250
+ )
251
+
252
+
253
+ def get_snowflake_connection(**kwargs: Any) -> Any:
254
+ """Return a Snowflake connection for code running inside a Snowflake sandbox.
255
+
256
+ Resolves the credential the way the platform provides it, in order: the
257
+ ``SNOWFLAKE_PAT``/``SNOWFLAKE_TOKEN`` env value; a token file named by
258
+ ``SNOWFLAKE_TOKEN_FILE_PATH`` or at ``/snowflake/session/token``, with
259
+ ``SNOWFLAKE_ACCOUNT``; then a ``[default]`` entry in the platform-rendered
260
+ ``$SNOWFLAKE_HOME/connections.toml``. Deployments differ in which they set, so
261
+ reading only one made this raise where a credential was in fact present.
262
+
263
+ The token is Snowflake's ``AppCredential`` placeholder, swapped for the real account
264
+ token by the platform's outbound egress proxy — not a credential Snowflake hands the
265
+ container. Non-credential keyword arguments override the corresponding default, e.g.
266
+ ``get_snowflake_connection(warehouse="MY_WH", database="MY_DB")``.
267
+
268
+ The identity-bearing parameters — ``account``, ``host``, ``token``,
269
+ ``password``, ``authenticator`` — cannot be overridden via ``**kwargs`` and
270
+ raise ``ValueError`` if passed: they come from the sandbox's injected
271
+ environment and must not be redirected to another host or auth scheme.
272
+
273
+ The platform's egress-proxy CA (``/etc/cng/proxy-ca.crt``) is automatically
274
+ trusted so HTTPS verification succeeds inside gVisor. **This widens TLS trust
275
+ process-wide** — it sets ``REQUESTS_CA_BUNDLE``/``SSL_CERT_FILE`` for the whole
276
+ process (system roots + the Snowflake CA), affecting every library that reads them,
277
+ which is why this helper is for in-sandbox use only. Outside a Snowflake sandbox the
278
+ proxy CA file doesn't exist and normal system CA verification is used. If no
279
+ system CA bundle can be found it raises rather than trust only the proxy CA
280
+ (which would drop the public roots).
281
+
282
+ A default ``login_timeout`` is applied so a closed-egress sandbox fails fast
283
+ instead of hanging; override it with ``login_timeout=`` if needed.
284
+
285
+ Each call returns a *new* connection; callers are responsible for closing it.
286
+ """
287
+ _patch_proxy_ca()
288
+ return _make_connection(**kwargs)