snowflake-sandbox-python 0.2.2a5__tar.gz → 0.2.2a6__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.
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/PKG-INFO +55 -1
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/README.md +50 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/pyproject.toml +13 -2
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_egress_flags.py +34 -25
- snowflake_sandbox_python-0.2.2a6/src/snowflake/cli_sandbox/_git_repo_flags.py +58 -0
- snowflake_sandbox_python-0.2.2a6/src/snowflake/cli_sandbox/_remote_command.py +168 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_run_command.py +64 -3
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_shell_command.py +116 -38
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/commands.py +33 -43
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/__init__.py +11 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/__main__.py +4 -5
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_args.py +9 -65
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_deploy_spec.py +13 -5
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_diagnostics.py +5 -2
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_fs_ops.py +3 -4
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_preview.py +9 -0
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/_proto/sandbox_pb2.py +175 -0
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/_proto/sandbox_pb2.pyi +704 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_retry.py +15 -8
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_sandbox_state.py +78 -18
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/_session.py +143 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_sse.py +2 -2
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/_ssh.py +232 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_sync_transport.py +23 -18
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_transport.py +50 -54
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/_tunnel.py +185 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_version.py +1 -1
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/app.py +2 -2
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/client.py +235 -121
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/connect.py +14 -14
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/deploy.py +9 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/egress.py +92 -23
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/endpoint.py +515 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/exceptions.py +20 -6
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/failure.py +16 -17
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/git_repo.py +191 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/image.py +5 -8
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/jobs.py +5 -0
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/log_stream.py +431 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/mcp.py +2 -2
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/py.typed +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/secret.py +88 -22
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/session_app.py +2 -2
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/shell.py +2 -3
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/snowflake_connection.py +83 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/sync_client.py +232 -121
- snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/sync_log_stream.py +118 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/sync_shell.py +2 -3
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/warm_session.py +4 -4
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake_sandbox_python.egg-info/PKG-INFO +55 -1
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake_sandbox_python.egg-info/SOURCES.txt +11 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake_sandbox_python.egg-info/requires.txt +4 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/tests/test_docs_references.py +4 -3
- snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/log_stream.py +0 -227
- snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/sync_log_stream.py +0 -150
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/LICENSE +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/setup.cfg +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/setup.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_adapter.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_common.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_get_command.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_lifecycle_command.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/_upload_plan.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/plugin_spec.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/cli_sandbox/py.typed +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_ansi.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_assemble.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_bundle.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_code_delivery.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_connection_resolve.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_env.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_files_mixin.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_globs.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_hosts.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_mcp_discovery.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_mcp_status.py +0 -0
- /snowflake_sandbox_python-0.2.2a5/src/snowflake/sandbox/py.typed → /snowflake_sandbox_python-0.2.2a6/src/snowflake/sandbox/_proto/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_runtime/__init__.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_runtime/_fs_helper.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_runtime/_job_runner.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_runtime/_protocol.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_runtime/_shims.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_session_registry.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_stage.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_sync_files_mixin.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_sync_fs_ops.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_sync_watch.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_transport_errors.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_upload_plan.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_watch.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/_wire.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/config.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/exec_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/files.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/function.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/lifecycle.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/mount.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/sync_exec_stream.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/sync_files.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake/sandbox/types.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake_sandbox_python.egg-info/dependency_links.txt +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake_sandbox_python.egg-info/entry_points.txt +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/src/snowflake_sandbox_python.egg-info/top_level.txt +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/tests/test_gate_tristate.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/tests/test_imports.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/tests/test_smoke_offline.py +0 -0
- {snowflake_sandbox_python-0.2.2a5 → snowflake_sandbox_python-0.2.2a6}/tests/test_tutorial_sources.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snowflake-sandbox-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2a6
|
|
4
4
|
Summary: Snowflake sandboxes — Python SDK and CLI.
|
|
5
5
|
Author: Snowflake, Inc.
|
|
6
6
|
License:
|
|
@@ -222,11 +222,15 @@ License-File: LICENSE
|
|
|
222
222
|
Requires-Dist: httpx[http2]<1.0,>=0.27
|
|
223
223
|
Requires-Dist: typing-extensions>=4.6
|
|
224
224
|
Requires-Dist: snowflake-connector-python>=3.6
|
|
225
|
+
Requires-Dist: websockets<16,>=15
|
|
226
|
+
Requires-Dist: protobuf<6,>=5.29
|
|
225
227
|
Provides-Extra: cli
|
|
226
228
|
Requires-Dist: snowflake-cli>=3.0; extra == "cli"
|
|
227
229
|
Provides-Extra: connector
|
|
228
230
|
Provides-Extra: dev
|
|
229
231
|
Requires-Dist: pytest>=9; extra == "dev"
|
|
232
|
+
Requires-Dist: grpcio-tools==1.71.0; extra == "dev"
|
|
233
|
+
Requires-Dist: types-protobuf>=5.29; extra == "dev"
|
|
230
234
|
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
|
|
231
235
|
Requires-Dist: pytest-asyncio>=1.4; extra == "dev"
|
|
232
236
|
Requires-Dist: respx>=0.23; extra == "dev"
|
|
@@ -344,6 +348,46 @@ Auth via `SNOWFLAKE_ACCOUNT` + `SNOWFLAKE_TOKEN` env vars, a named connection in
|
|
|
344
348
|
With none of those the SDK raises `"snowflake.sandbox has no endpoint to talk to: ..."`,
|
|
345
349
|
which names every way to supply one.
|
|
346
350
|
|
|
351
|
+
## HTTP and WebSocket endpoints
|
|
352
|
+
|
|
353
|
+
Use `endpoint(port)` for ordinary HTTP or WebSocket applications. The SDK resolves
|
|
354
|
+
an owned HTTPS origin and uses the current Snowflake credential for each request or
|
|
355
|
+
handshake. Backend ingress converts the traffic into the shared Session gRPC protocol.
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from snowflake.sandbox import Sandbox
|
|
359
|
+
|
|
360
|
+
sb = Sandbox.create(
|
|
361
|
+
image="/DB/SCHEMA/REPO/my-app:latest",
|
|
362
|
+
command=["python", "/app/server.py"], # Your server listens on port 8080.
|
|
363
|
+
connection=connection,
|
|
364
|
+
)
|
|
365
|
+
with sb.endpoint(8080) as endpoint:
|
|
366
|
+
response = endpoint.request("GET", "/api/state")
|
|
367
|
+
response.raise_for_status()
|
|
368
|
+
print(response.json())
|
|
369
|
+
with endpoint.connect_websocket("/events") as socket:
|
|
370
|
+
socket.send("hello")
|
|
371
|
+
print(socket.recv())
|
|
372
|
+
sb.disconnect() # Preserve the running sandbox.
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Resolving an endpoint does not start the guest application or wait for readiness.
|
|
376
|
+
The first request or handshake reports availability; callers decide when to try again.
|
|
377
|
+
|
|
378
|
+
`AsyncSandbox` exposes the same methods with `await` and `async with`. Use
|
|
379
|
+
`endpoint.stream(method, path)` for incremental HTTP response reads; `request()` buffers
|
|
380
|
+
the body. Both accept `headers`, `content` and `json`. Paths begin with `/` and remain
|
|
381
|
+
within the endpoint's authority. Credential and Host headers are SDK-owned.
|
|
382
|
+
Native HTTP and WebSocket clients omit browser Origin headers. Authentication
|
|
383
|
+
still uses the current Snowflake credential and TLS verifies the owned authority.
|
|
384
|
+
Origin remains reserved and cannot be supplied by callers.
|
|
385
|
+
HTTP redirects are returned without being followed; WebSocket redirects are rejected.
|
|
386
|
+
TLS verification defaults on, with request/handshake waits bounded by the connection
|
|
387
|
+
configuration. Closing an endpoint closes its streams and sockets. Async cancellation
|
|
388
|
+
also cancels pending operations. The application owns reconnect and protocol state;
|
|
389
|
+
the transport makes one attempt and raises sanitized authentication or transport errors.
|
|
390
|
+
|
|
347
391
|
## CLI
|
|
348
392
|
|
|
349
393
|
Installs as a [`snow sandbox`](https://github.com/snowflakedb/snowflake-cli) plugin:
|
|
@@ -375,3 +419,13 @@ uv run --extra docs mkdocs build --strict
|
|
|
375
419
|
|
|
376
420
|
See `CONTRIBUTING.md` in the source repository for dev setup, how to add new
|
|
377
421
|
methods, exceptions, and CLI commands.
|
|
422
|
+
|
|
423
|
+
### Command execution
|
|
424
|
+
|
|
425
|
+
`exec()` executes the program and its arguments directly. Ordinary commands such
|
|
426
|
+
as `exec(["echo", "hello"])` do not require Python in the sandbox image. For shell
|
|
427
|
+
builtins, use `exec(["sh", "-c", "cd /tmp && pwd"])`; for a script without a shebang,
|
|
428
|
+
use `exec(["sh", "/path/script"])`. Arguments are literal, without shell expansion.
|
|
429
|
+
`python -c` retains its historical alias to `python3 -c`.
|
|
430
|
+
|
|
431
|
+
This requires a server with command-array support. Python jobs, sessions, and directory helpers still require Python in the image.
|
|
@@ -104,6 +104,46 @@ Auth via `SNOWFLAKE_ACCOUNT` + `SNOWFLAKE_TOKEN` env vars, a named connection in
|
|
|
104
104
|
With none of those the SDK raises `"snowflake.sandbox has no endpoint to talk to: ..."`,
|
|
105
105
|
which names every way to supply one.
|
|
106
106
|
|
|
107
|
+
## HTTP and WebSocket endpoints
|
|
108
|
+
|
|
109
|
+
Use `endpoint(port)` for ordinary HTTP or WebSocket applications. The SDK resolves
|
|
110
|
+
an owned HTTPS origin and uses the current Snowflake credential for each request or
|
|
111
|
+
handshake. Backend ingress converts the traffic into the shared Session gRPC protocol.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from snowflake.sandbox import Sandbox
|
|
115
|
+
|
|
116
|
+
sb = Sandbox.create(
|
|
117
|
+
image="/DB/SCHEMA/REPO/my-app:latest",
|
|
118
|
+
command=["python", "/app/server.py"], # Your server listens on port 8080.
|
|
119
|
+
connection=connection,
|
|
120
|
+
)
|
|
121
|
+
with sb.endpoint(8080) as endpoint:
|
|
122
|
+
response = endpoint.request("GET", "/api/state")
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
print(response.json())
|
|
125
|
+
with endpoint.connect_websocket("/events") as socket:
|
|
126
|
+
socket.send("hello")
|
|
127
|
+
print(socket.recv())
|
|
128
|
+
sb.disconnect() # Preserve the running sandbox.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Resolving an endpoint does not start the guest application or wait for readiness.
|
|
132
|
+
The first request or handshake reports availability; callers decide when to try again.
|
|
133
|
+
|
|
134
|
+
`AsyncSandbox` exposes the same methods with `await` and `async with`. Use
|
|
135
|
+
`endpoint.stream(method, path)` for incremental HTTP response reads; `request()` buffers
|
|
136
|
+
the body. Both accept `headers`, `content` and `json`. Paths begin with `/` and remain
|
|
137
|
+
within the endpoint's authority. Credential and Host headers are SDK-owned.
|
|
138
|
+
Native HTTP and WebSocket clients omit browser Origin headers. Authentication
|
|
139
|
+
still uses the current Snowflake credential and TLS verifies the owned authority.
|
|
140
|
+
Origin remains reserved and cannot be supplied by callers.
|
|
141
|
+
HTTP redirects are returned without being followed; WebSocket redirects are rejected.
|
|
142
|
+
TLS verification defaults on, with request/handshake waits bounded by the connection
|
|
143
|
+
configuration. Closing an endpoint closes its streams and sockets. Async cancellation
|
|
144
|
+
also cancels pending operations. The application owns reconnect and protocol state;
|
|
145
|
+
the transport makes one attempt and raises sanitized authentication or transport errors.
|
|
146
|
+
|
|
107
147
|
## CLI
|
|
108
148
|
|
|
109
149
|
Installs as a [`snow sandbox`](https://github.com/snowflakedb/snowflake-cli) plugin:
|
|
@@ -135,3 +175,13 @@ uv run --extra docs mkdocs build --strict
|
|
|
135
175
|
|
|
136
176
|
See `CONTRIBUTING.md` in the source repository for dev setup, how to add new
|
|
137
177
|
methods, exceptions, and CLI commands.
|
|
178
|
+
|
|
179
|
+
### Command execution
|
|
180
|
+
|
|
181
|
+
`exec()` executes the program and its arguments directly. Ordinary commands such
|
|
182
|
+
as `exec(["echo", "hello"])` do not require Python in the sandbox image. For shell
|
|
183
|
+
builtins, use `exec(["sh", "-c", "cd /tmp && pwd"])`; for a script without a shebang,
|
|
184
|
+
use `exec(["sh", "/path/script"])`. Arguments are literal, without shell expansion.
|
|
185
|
+
`python -c` retains its historical alias to `python3 -c`.
|
|
186
|
+
|
|
187
|
+
This requires a server with command-array support. Python jobs, sessions, and directory helpers still require Python in the image.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "snowflake-sandbox-python"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2a6"
|
|
8
8
|
description = "Snowflake sandboxes — Python SDK and CLI."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -31,6 +31,8 @@ dependencies = [
|
|
|
31
31
|
# reimplementing the file format, and imports it lazily so `import
|
|
32
32
|
# snowflake.sandbox` stays fast. 3.6+ ships the tomlkit config_manager.
|
|
33
33
|
"snowflake-connector-python>=3.6",
|
|
34
|
+
"websockets>=15,<16",
|
|
35
|
+
"protobuf>=5.29,<6",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.optional-dependencies]
|
|
@@ -47,6 +49,8 @@ cli = [
|
|
|
47
49
|
connector = []
|
|
48
50
|
dev = [
|
|
49
51
|
"pytest>=9",
|
|
52
|
+
"grpcio-tools==1.71.0",
|
|
53
|
+
"types-protobuf>=5.29",
|
|
50
54
|
# No test timeout meant a hung shell()/create could sit indefinitely; a promotion
|
|
51
55
|
# gate must always return a verdict.
|
|
52
56
|
"pytest-timeout>=2.3",
|
|
@@ -82,10 +86,11 @@ namespaces = true
|
|
|
82
86
|
|
|
83
87
|
[tool.setuptools.package-data]
|
|
84
88
|
# PEP 561: ship the marker so downstream mypy/pyright pick up the SDK's types.
|
|
85
|
-
"snowflake.sandbox" = ["py.typed"]
|
|
89
|
+
"snowflake.sandbox" = ["py.typed", "_proto/*.pyi"]
|
|
86
90
|
"snowflake.cli_sandbox" = ["py.typed"]
|
|
87
91
|
|
|
88
92
|
[tool.ruff]
|
|
93
|
+
extend-exclude = ["src/snowflake/sandbox/_proto"]
|
|
89
94
|
line-length = 100
|
|
90
95
|
target-version = "py311"
|
|
91
96
|
src = ["src", "tests", "prototype"]
|
|
@@ -107,6 +112,7 @@ max-complexity = 15
|
|
|
107
112
|
# Style/complexity sets govern src/; non-shipped code keeps correctness + format.
|
|
108
113
|
[tool.ruff.lint.per-file-ignores]
|
|
109
114
|
"tests/*" = ["B"]
|
|
115
|
+
"src/snowflake/sandbox/_proto/*" = ["ALL"]
|
|
110
116
|
"{tests,tools,examples,prototype}/**" = ["C901", "ASYNC", "SIM", "RUF"]
|
|
111
117
|
|
|
112
118
|
[tool.ruff.lint.flake8-bugbear]
|
|
@@ -205,3 +211,8 @@ filterwarnings = [
|
|
|
205
211
|
# -e '.[cli,dev]'`, used by CI) and uv (`uv sync --extra dev --extra docs`) share
|
|
206
212
|
# a single source of truth. Install dev tooling with `--extra dev`/`--extra docs`
|
|
207
213
|
# rather than `--group`.
|
|
214
|
+
|
|
215
|
+
[[tool.mypy.overrides]]
|
|
216
|
+
# protoc stubs use unparameterized Mapping; generated code is reviewed via drift tests.
|
|
217
|
+
module = ["snowflake.sandbox._proto.*"]
|
|
218
|
+
ignore_errors = true
|
|
@@ -10,14 +10,10 @@ Spellings, chosen to match what the CLI already does elsewhere:
|
|
|
10
10
|
* ``--eai NAME`` — repeatable, like ``--env``. Names an External Access Integration
|
|
11
11
|
whose network rules the platform resolves; that is the only way to reach a host
|
|
12
12
|
outside the built-in baseline.
|
|
13
|
-
* ``--secret ENV_VAR=DB.SCHEMA.NAME@host[,host]`` — repeatable. The env var comes
|
|
13
|
+
* ``--secret ENV_VAR=DB.SCHEMA.NAME[@host[,host]]`` — repeatable. The env var comes
|
|
14
14
|
first because that is the order ``--env KEY=VAL`` established, and it is the half
|
|
15
|
-
you read inside the sandbox.
|
|
16
|
-
|
|
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`.)
|
|
15
|
+
you read inside the sandbox. The ``@host`` scopes where the credential may be sent
|
|
16
|
+
and is required unless an ``--eai`` supplies the host grant.
|
|
21
17
|
* ``--no-default-egress`` — the closed level: Snowflake and its stages only, no
|
|
22
18
|
baseline package-manager hosts. An ``--eai`` still grants on top of it, which is
|
|
23
19
|
why this is not called ``--no-egress``.
|
|
@@ -27,7 +23,7 @@ from __future__ import annotations
|
|
|
27
23
|
|
|
28
24
|
import typer
|
|
29
25
|
|
|
30
|
-
from snowflake.sandbox.egress import Egress, compile_egress
|
|
26
|
+
from snowflake.sandbox.egress import Egress, compile_egress, compile_secrets
|
|
31
27
|
from snowflake.sandbox.secret import Secret
|
|
32
28
|
|
|
33
29
|
__all__ = ["EAI_HELP", "NO_DEFAULT_EGRESS_HELP", "SECRET_HELP", "build_egress"]
|
|
@@ -38,10 +34,17 @@ EAI_HELP = (
|
|
|
38
34
|
"readable or the platform refuses the create."
|
|
39
35
|
)
|
|
40
36
|
|
|
37
|
+
OUTBOUND_WIF_HOST_HELP = (
|
|
38
|
+
"Host that gets a Snowflake workload identity token as Authorization: Bearer, so it "
|
|
39
|
+
"can verify the calling account without a stored secret; repeatable. An exact host "
|
|
40
|
+
"or a leading-dot suffix (.example.com). It does not open egress: the host must also "
|
|
41
|
+
"be reachable, e.g. through --eai."
|
|
42
|
+
)
|
|
43
|
+
|
|
41
44
|
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
|
|
44
|
-
"
|
|
45
|
+
"Snowflake SECRET to inject, as ENV_VAR=DB.SCHEMA.NAME[@host[,host]]; repeatable. "
|
|
46
|
+
"The @host scopes where the credential may be sent and is required unless an "
|
|
47
|
+
"--eai supplies the host grant. ENV_VAR= is required."
|
|
45
48
|
)
|
|
46
49
|
|
|
47
50
|
NO_DEFAULT_EGRESS_HELP = (
|
|
@@ -64,6 +67,8 @@ def _parse_secret(raw: str) -> Secret:
|
|
|
64
67
|
env_var, spec = env_var.strip(), spec.strip()
|
|
65
68
|
if not env_var:
|
|
66
69
|
raise typer.BadParameter(f"empty env var in {raw!r} (expected ENV_VAR=DB.SCHEMA.NAME)")
|
|
70
|
+
else:
|
|
71
|
+
raise typer.BadParameter(f"{raw!r}: expected ENV_VAR=DB.SCHEMA.NAME[@host]")
|
|
67
72
|
|
|
68
73
|
hosts: tuple[str, ...] = ()
|
|
69
74
|
if "@" in spec:
|
|
@@ -89,38 +94,42 @@ def build_egress(
|
|
|
89
94
|
eai: list[str] | None,
|
|
90
95
|
secret: list[str] | None,
|
|
91
96
|
no_default_egress: bool,
|
|
97
|
+
outbound_wif_host: list[str] | None = None,
|
|
92
98
|
) -> tuple[Egress | None, tuple[Secret, ...]]:
|
|
93
99
|
"""`(egress, secrets)` for these flags — both empty when none were given.
|
|
94
100
|
|
|
95
101
|
Two values because that is the shape `Sandbox.create()` takes: secrets are authored
|
|
96
|
-
alongside egress
|
|
97
|
-
wire, where `compile_egress` puts them).
|
|
102
|
+
alongside egress and sent as their own top-level create field.
|
|
98
103
|
|
|
99
104
|
`None` rather than an empty `Egress` when nothing was passed, so the create body
|
|
100
105
|
stays byte-identical to today's for every existing caller.
|
|
101
106
|
"""
|
|
102
107
|
integrations = tuple(dict.fromkeys(name.strip() for name in (eai or []) if name.strip()))
|
|
103
108
|
secrets = tuple(_parse_secret(raw) for raw in (secret or []))
|
|
104
|
-
|
|
109
|
+
wif_hosts = tuple(dict.fromkeys(h.strip() for h in (outbound_wif_host or []) if h.strip()))
|
|
110
|
+
if not integrations and not secrets and not no_default_egress and not wif_hosts:
|
|
105
111
|
return None, ()
|
|
106
112
|
|
|
107
|
-
egress = Egress(
|
|
108
|
-
external_access_integrations=integrations,
|
|
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 {}),
|
|
115
|
-
)
|
|
116
113
|
# Validate now, not at create time. `compile_egress` is pure and is what the client
|
|
117
114
|
# runs anyway -- but it runs *after* connecting, so a mistyped host pair otherwise
|
|
118
115
|
# costs a Snowflake round trip before saying so. These are usage errors; they should
|
|
119
|
-
# cost nothing.
|
|
116
|
+
# cost nothing. Egress() is inside the try too: it rejects a bad --outbound-wif-host.
|
|
120
117
|
from snowflake.sandbox.exceptions import SandboxError
|
|
121
118
|
|
|
122
119
|
try:
|
|
123
|
-
|
|
120
|
+
egress = Egress(
|
|
121
|
+
external_access_integrations=integrations,
|
|
122
|
+
# Uses the current parameter name. The CLI FLAG stays `--no-default-egress`: it
|
|
123
|
+
# is released, renaming it would break users' scripts, and that is a separate
|
|
124
|
+
# decision from the library parameter's name. So the flag spelling and the
|
|
125
|
+
# parameter spelling differ here deliberately — the alternative is the CLI
|
|
126
|
+
# depending on a deprecated parameter, which is worse. None is unset, which
|
|
127
|
+
# sends nothing for the level.
|
|
128
|
+
allow_public_package_registries=False if no_default_egress else None,
|
|
129
|
+
outbound_wif_hosts=wif_hosts,
|
|
130
|
+
)
|
|
131
|
+
compile_egress(egress, ())
|
|
132
|
+
compile_secrets(secrets, has_eai=bool(integrations))
|
|
124
133
|
except SandboxError as exc:
|
|
125
134
|
raise typer.BadParameter(str(exc)) from exc
|
|
126
135
|
return egress, secrets
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""`--git-repo` / `--git-integration`: mount Git repositories into a run's sandbox.
|
|
2
|
+
|
|
3
|
+
* ``--git-repo URL[=MOUNT_PATH]`` — repeatable. The first ``=`` starts the mount path.
|
|
4
|
+
* ``--git-integration NAME`` — the ``EXTERNAL_APPLICATION`` integration every
|
|
5
|
+
``--git-repo`` is cloned through, one per run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from snowflake.sandbox.git_repo import GitRepo
|
|
16
|
+
|
|
17
|
+
__all__ = ["GIT_INTEGRATION_HELP", "GIT_REPO_HELP", "build_git_repos"]
|
|
18
|
+
|
|
19
|
+
GIT_REPO_HELP = (
|
|
20
|
+
"Git repository to mount, as an https URL with an optional =MOUNT_PATH "
|
|
21
|
+
"(default /mnt/repos/<repo>); repeatable. Needs --git-integration. The default "
|
|
22
|
+
"branch is mounted writable, and git push from the sandbox has no credential."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
GIT_INTEGRATION_HELP = (
|
|
26
|
+
"Snowflake EXTERNAL_APPLICATION integration that every --git-repo is cloned "
|
|
27
|
+
"through, with your own authorization of it."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_git_repos(raw: list[str] | None, integration: str | None) -> tuple[GitRepo, ...]:
|
|
32
|
+
"""The `GitRepo`s for these flags, or a `typer.BadParameter`."""
|
|
33
|
+
from snowflake.sandbox.exceptions import SandboxError
|
|
34
|
+
from snowflake.sandbox.git_repo import GitRepo, validate_git_repos
|
|
35
|
+
|
|
36
|
+
if not raw:
|
|
37
|
+
if integration:
|
|
38
|
+
raise typer.BadParameter("--git-integration needs at least one --git-repo.")
|
|
39
|
+
return ()
|
|
40
|
+
if not integration:
|
|
41
|
+
raise typer.BadParameter(
|
|
42
|
+
"--git-repo needs --git-integration NAME, the EXTERNAL_APPLICATION "
|
|
43
|
+
"integration the repository is cloned through."
|
|
44
|
+
)
|
|
45
|
+
repos: list[GitRepo] = []
|
|
46
|
+
for spec in raw:
|
|
47
|
+
url, _, mount_path = spec.strip().partition("=")
|
|
48
|
+
try:
|
|
49
|
+
repos.append(
|
|
50
|
+
GitRepo(url.strip(), integration=integration, mount_path=mount_path.strip())
|
|
51
|
+
)
|
|
52
|
+
except SandboxError as exc:
|
|
53
|
+
raise typer.BadParameter(f"--git-repo {spec!r}: {exc}") from exc
|
|
54
|
+
try:
|
|
55
|
+
validate_git_repos(repos)
|
|
56
|
+
except SandboxError as exc:
|
|
57
|
+
raise typer.BadParameter(f"--git-repo: {exc}") from exc
|
|
58
|
+
return tuple(repos)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""CLI entry points for SSH, local TCP forwarding and the desktop page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
7
|
+
from contextlib import redirect_stdout
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from snowflake.cli_sandbox._common import (
|
|
14
|
+
_CONN_HELP,
|
|
15
|
+
_USE_SNOW_CONN_HELP,
|
|
16
|
+
_apply_connection,
|
|
17
|
+
_resolve_sandbox_id,
|
|
18
|
+
)
|
|
19
|
+
from snowflake.sandbox.exceptions import SandboxError
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from snowflake.sandbox.config import Config
|
|
23
|
+
|
|
24
|
+
_SSH_HELP = "Connect with OpenSSH over an authenticated sandbox tunnel. Requires OpenSSH on PATH.\n\nExample:\n snow sandbox ssh my-sandbox\n snow sandbox ssh my-sandbox --cmd 'uname -a'"
|
|
25
|
+
_PORT_FORWARD_HELP = "Forward a loopback TCP port to a port inside an existing sandbox.\n\nExample:\n snow sandbox port-forward my-sandbox 8080 --local-port 18080"
|
|
26
|
+
_DESKTOP_HELP = "Open the remote desktop page for an existing sandbox.\n\nExample:\n snow sandbox desktop my-sandbox\n snow sandbox desktop my-sandbox --no-open"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _remote_config(use_snow_connection: bool, connection: str | None) -> Config:
|
|
30
|
+
from snowflake.cli_sandbox._adapter import sdk_connection
|
|
31
|
+
from snowflake.sandbox.config import resolve_config
|
|
32
|
+
|
|
33
|
+
_apply_connection(use_snow_connection, connection=connection)
|
|
34
|
+
return resolve_config(connection=sdk_connection())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ssh_command(
|
|
38
|
+
sandbox_id: str = typer.Argument(..., help="Existing sandbox name or app id."),
|
|
39
|
+
user: str = typer.Option("root", "--user", help="SSH login user inside the sandbox."),
|
|
40
|
+
cmd: str | None = typer.Option(
|
|
41
|
+
None, "--cmd", help="Remote command; omit for an interactive SSH session."
|
|
42
|
+
),
|
|
43
|
+
use_snow_connection: bool = typer.Option(
|
|
44
|
+
True, "--use-snow-connection/--no-snow-connection", help=_USE_SNOW_CONN_HELP
|
|
45
|
+
),
|
|
46
|
+
connection: str | None = typer.Option(None, "--connection", "-c", help=_CONN_HELP),
|
|
47
|
+
proxy: bool = typer.Option(False, "--proxy", hidden=True),
|
|
48
|
+
identity: Path | None = typer.Option(None, "--identity", hidden=True),
|
|
49
|
+
known_hosts: Path | None = typer.Option(None, "--known-hosts", hidden=True),
|
|
50
|
+
**options: Any,
|
|
51
|
+
) -> None:
|
|
52
|
+
from snowflake.sandbox._preview import preview_url
|
|
53
|
+
from snowflake.sandbox._ssh import run_ssh, ssh_proxy
|
|
54
|
+
from snowflake.sandbox._tunnel import resolve_remote_url
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
if sandbox_id.startswith("cntr_"):
|
|
58
|
+
preview_url(sandbox_id, 8079, "org-account.example")
|
|
59
|
+
if proxy and (identity is None or known_hosts is None):
|
|
60
|
+
raise typer.BadParameter("--proxy requires --identity and --known-hosts")
|
|
61
|
+
if proxy or not sandbox_id.startswith("cntr_"):
|
|
62
|
+
# Connector/browser-login notices must not enter the SSH stream.
|
|
63
|
+
with redirect_stdout(sys.stderr):
|
|
64
|
+
config = _remote_config(use_snow_connection, connection)
|
|
65
|
+
sandbox_id = _resolve_sandbox_id(sandbox_id)
|
|
66
|
+
# Validate before key generation or spawning a process.
|
|
67
|
+
preview_url(sandbox_id, 8079, "org-account.example")
|
|
68
|
+
if proxy:
|
|
69
|
+
if identity is None or known_hosts is None:
|
|
70
|
+
raise typer.BadParameter("--proxy requires --identity and --known-hosts")
|
|
71
|
+
|
|
72
|
+
async def pump() -> None:
|
|
73
|
+
with redirect_stdout(sys.stderr):
|
|
74
|
+
url = await resolve_remote_url(sandbox_id, config, service="ssh")
|
|
75
|
+
await ssh_proxy(
|
|
76
|
+
url, config, identity=identity, known_hosts=known_hosts, alias=sandbox_id
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
asyncio.run(pump())
|
|
80
|
+
code = 0
|
|
81
|
+
else:
|
|
82
|
+
code = run_ssh(
|
|
83
|
+
sandbox_id,
|
|
84
|
+
user=user,
|
|
85
|
+
command=cmd,
|
|
86
|
+
connection=connection,
|
|
87
|
+
use_snow_connection=use_snow_connection,
|
|
88
|
+
)
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
raise typer.Exit(130) from None
|
|
91
|
+
except (SandboxError, OSError) as exc:
|
|
92
|
+
typer.echo(f"error: {exc}", err=True)
|
|
93
|
+
raise typer.Exit(1) from exc
|
|
94
|
+
raise typer.Exit(code)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _port_forward_command(
|
|
98
|
+
sandbox_id: str = typer.Argument(..., help="Existing sandbox name or app id."),
|
|
99
|
+
port: int = typer.Argument(..., help="TCP port inside the sandbox."),
|
|
100
|
+
local_port: int | None = typer.Option(
|
|
101
|
+
None, "--local-port", help="Loopback port to listen on (default: remote port)."
|
|
102
|
+
),
|
|
103
|
+
use_snow_connection: bool = typer.Option(
|
|
104
|
+
True, "--use-snow-connection/--no-snow-connection", help=_USE_SNOW_CONN_HELP
|
|
105
|
+
),
|
|
106
|
+
connection: str | None = typer.Option(None, "--connection", "-c", help=_CONN_HELP),
|
|
107
|
+
**options: Any,
|
|
108
|
+
) -> None:
|
|
109
|
+
from snowflake.sandbox._preview import validate_preview_port
|
|
110
|
+
from snowflake.sandbox._tunnel import forward_port, remote_url, resolve_remote_url
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
remote_url(
|
|
114
|
+
sandbox_id if sandbox_id.startswith("cntr_") else "cntr_validation",
|
|
115
|
+
"org-account.example",
|
|
116
|
+
port=port,
|
|
117
|
+
)
|
|
118
|
+
listen_port = validate_preview_port(port if local_port is None else local_port)
|
|
119
|
+
config = _remote_config(use_snow_connection, connection)
|
|
120
|
+
sandbox_id = _resolve_sandbox_id(sandbox_id)
|
|
121
|
+
|
|
122
|
+
async def forward() -> None:
|
|
123
|
+
url = await resolve_remote_url(sandbox_id, config, port=port)
|
|
124
|
+
await forward_port(
|
|
125
|
+
url,
|
|
126
|
+
config,
|
|
127
|
+
port=port,
|
|
128
|
+
local_port=listen_port,
|
|
129
|
+
ready=lambda bound: typer.echo(
|
|
130
|
+
f"Forwarding 127.0.0.1:{bound} to {sandbox_id}:{port}", err=True
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
asyncio.run(forward())
|
|
135
|
+
except KeyboardInterrupt:
|
|
136
|
+
raise typer.Exit(130) from None
|
|
137
|
+
except (SandboxError, OSError) as exc:
|
|
138
|
+
typer.echo(f"error: {exc}", err=True)
|
|
139
|
+
raise typer.Exit(1) from exc
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _desktop_command(
|
|
143
|
+
sandbox_id: str = typer.Argument(..., help="Existing sandbox name or app id."),
|
|
144
|
+
no_open: bool = typer.Option(
|
|
145
|
+
False, "--no-open", help="Print the desktop URL without opening a browser."
|
|
146
|
+
),
|
|
147
|
+
use_snow_connection: bool = typer.Option(
|
|
148
|
+
True, "--use-snow-connection/--no-snow-connection", help=_USE_SNOW_CONN_HELP
|
|
149
|
+
),
|
|
150
|
+
connection: str | None = typer.Option(None, "--connection", "-c", help=_CONN_HELP),
|
|
151
|
+
**options: Any,
|
|
152
|
+
) -> None:
|
|
153
|
+
import webbrowser
|
|
154
|
+
|
|
155
|
+
from snowflake.sandbox._tunnel import remote_url, resolve_remote_url
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
if sandbox_id.startswith("cntr_"):
|
|
159
|
+
remote_url(sandbox_id, "org-account.example", service="desktop")
|
|
160
|
+
config = _remote_config(use_snow_connection, connection)
|
|
161
|
+
sandbox_id = _resolve_sandbox_id(sandbox_id)
|
|
162
|
+
url = asyncio.run(resolve_remote_url(sandbox_id, config, service="desktop"))
|
|
163
|
+
typer.echo(url)
|
|
164
|
+
if not no_open:
|
|
165
|
+
webbrowser.open(url)
|
|
166
|
+
except (SandboxError, OSError) as exc:
|
|
167
|
+
typer.echo(f"error: {exc}", err=True)
|
|
168
|
+
raise typer.Exit(1) from exc
|