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,272 @@
1
+ """The file-transfer / filesystem method group mixed into ``Sandbox``.
2
+
3
+ The synchronous twin of ``_files_mixin._FilesMixin``: the same file-ops surface,
4
+ blocking instead of awaitable, delegating to the sync mechanism modules
5
+ (``sync_files`` for the ``/files`` byte route, ``_sync_fs_ops`` for the exec-driven
6
+ directory ops, ``_sync_watch`` for the watcher). Extracted from ``Sandbox`` so the
7
+ file-ops surface lives in one place per calling style, exactly as the async half
8
+ was extracted from ``AsyncSandbox``.
9
+
10
+ Not a public base class: an implementation detail of the two clients, inherited by
11
+ ``Sandbox``, never exposed on its own. It performs no I/O itself and imports nothing
12
+ from the package at runtime.
13
+
14
+ The delegate functions are typed against the concrete ``Sandbox``, so each call
15
+ narrows ``self`` with a ``cast`` — a type-checker-only no-op: at run time ``self``
16
+ already *is* a ``Sandbox``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, cast
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable, Iterator, Sequence
25
+ from pathlib import Path
26
+
27
+ from snowflake.sandbox._sync_transport import SyncTransport
28
+ from snowflake.sandbox._upload_plan import UploadPlan
29
+ from snowflake.sandbox.sync_client import Sandbox
30
+ from snowflake.sandbox.types import FileInfo, FileWatchEvent, FileWatchEventType
31
+
32
+ __all__ = ["_FilesMixin"]
33
+
34
+
35
+ class _FilesMixin:
36
+ """File-transfer and filesystem methods shared into ``Sandbox``.
37
+
38
+ ``_transport`` is set by the client's own ``__init__``; it is declared here so
39
+ the delegating methods type-check against it, mirroring ``_SandboxState``.
40
+ """
41
+
42
+ _transport: SyncTransport
43
+
44
+ # ----- file I/O ---------------------------------------------------
45
+
46
+ def upload_file(self, local: str | Path, remote: str) -> None:
47
+ """Upload a local file into the running sandbox.
48
+
49
+ Raises the standard-library ``FileNotFoundError`` if ``local`` does not
50
+ exist (it is a local-filesystem miss, not a sandbox error).
51
+
52
+ Example:
53
+ sb.upload_file("./data.csv", "/app/data.csv")
54
+ """
55
+ from snowflake.sandbox.sync_files import upload_file
56
+
57
+ upload_file(cast("Sandbox", self), local, remote, transport=self._transport)
58
+
59
+ def upload_dir(
60
+ self,
61
+ local_dir: str | Path,
62
+ remote_dir: str,
63
+ *,
64
+ exclude: list[str] | None = None,
65
+ include: list[str] | None = None,
66
+ allow_credential_files: list[str] | None = None,
67
+ dry_run: bool = False,
68
+ on_file: Callable[[int, int, str], None] | None = None,
69
+ ) -> UploadPlan:
70
+ """Upload a local directory into the running sandbox and report what it sent.
71
+
72
+ The directory's *contents* land at ``remote_dir`` -- ``upload_dir("./site",
73
+ "/app")`` puts ``site/index.html`` at ``/app/index.html``, as ``cp -r site/.
74
+ /app`` would. Build output, VCS state and credential-shaped files are left
75
+ behind by default; the returned `UploadPlan` says what was sent and, through
76
+ ``skipped``, why anything else was not.
77
+
78
+ Note:
79
+ There is no bulk route, so this is one request per file and the
80
+ per-file `MAX_FILE_BYTES` ceiling still applies -- an oversize file is
81
+ refused before the first byte is sent, so a tree never half-uploads.
82
+ ``dry_run=True`` sends nothing and so raises nothing: it returns the plan
83
+ with any offenders listed in ``plan.oversized``. Mount a stage
84
+ (`StageMount`) for bulk data. Symlinks are never followed, in either
85
+ direction, and appear in `plan.skipped` as `symlink-dir` /
86
+ `symlink-file` rather than vanishing silently. An empty directory is not
87
+ uploaded -- there is no file to create it under -- and is reported as
88
+ `empty-dir`.
89
+
90
+ Example:
91
+ plan = sb.upload_dir("./project", "/app", exclude=["*.csv"])
92
+ print(f"sent {plan.file_count} files, skipped {plan.skipped_by_reason()}")
93
+ """
94
+ from snowflake.sandbox.sync_files import upload_dir
95
+
96
+ return upload_dir(
97
+ cast("Sandbox", self),
98
+ local_dir,
99
+ remote_dir,
100
+ exclude=exclude,
101
+ include=include,
102
+ allow_credential_files=allow_credential_files,
103
+ dry_run=dry_run,
104
+ on_file=on_file,
105
+ transport=self._transport,
106
+ )
107
+
108
+ def download_file(self, remote: str, local: str | Path) -> None:
109
+ """Download a file from the running sandbox to a local path.
110
+
111
+ Example:
112
+ sb.download_file("/app/output.csv", "./output.csv")
113
+ """
114
+ from snowflake.sandbox.sync_files import download_file
115
+
116
+ download_file(cast("Sandbox", self), remote, local, transport=self._transport)
117
+
118
+ def stage_put(self, local: str, stage_path: str) -> None:
119
+ """Upload a local file to a Snowflake stage from within the sandbox.
120
+
121
+ Example:
122
+ sb.stage_put("./results.parquet", "MY_STAGE/results.parquet")
123
+ """
124
+ from snowflake.sandbox.sync_files import stage_put
125
+
126
+ stage_put(cast("Sandbox", self), local, stage_path, transport=self._transport)
127
+
128
+ def stage_get(self, stage_path: str, local: str) -> None:
129
+ """Download a file from a Snowflake stage to a local path inside the sandbox.
130
+
131
+ Example:
132
+ sb.stage_get("MY_STAGE/model.pkl", "/app/model.pkl")
133
+ """
134
+ from snowflake.sandbox.sync_files import stage_get
135
+
136
+ stage_get(cast("Sandbox", self), stage_path, local, transport=self._transport)
137
+
138
+ # ----- filesystem (Modal Sandbox.filesystem.*, flattened) ---------
139
+ #
140
+ # Matches the upload_file/download_file/stage_put/stage_get convention
141
+ # above: flat on Sandbox, not nested under a `.filesystem` sub-object.
142
+ # read/write ride the /files byte route; list/stat/make_directory/remove run
143
+ # a python3 helper in-container over exec (the /files route transfers bytes
144
+ # only). watch has no backend and raises. See sync_files.py, _sync_fs_ops.py, _sync_watch.py.
145
+
146
+ def list_files(self, path: str) -> Sequence[FileInfo]:
147
+ """List files and directories at `path` inside the sandbox.
148
+
149
+ Example:
150
+ entries = sb.list_files("/app")
151
+ for entry in entries:
152
+ print(entry.path, entry.size_bytes, entry.is_dir)
153
+ """
154
+ from snowflake.sandbox._sync_fs_ops import list_files
155
+
156
+ return list_files(cast("Sandbox", self), path, transport=self._transport)
157
+
158
+ def make_directory(
159
+ self, path: str, *, create_parents: bool = False, parents: bool | None = None
160
+ ) -> None:
161
+ """Create a directory inside the sandbox.
162
+
163
+ Args:
164
+ path: Absolute path to the directory to create.
165
+ create_parents: If True, create parent directories as needed.
166
+ parents: Alias for `create_parents` (Modal parity). If both are
167
+ specified, `parents` takes precedence.
168
+
169
+ Example:
170
+ sb.make_directory("/app/output", create_parents=True)
171
+ """
172
+ from snowflake.sandbox._sync_fs_ops import make_directory
173
+
174
+ # parents= is the Modal name; create_parents= is ours. parents wins if set.
175
+ effective = parents if parents is not None else create_parents
176
+ make_directory(
177
+ cast("Sandbox", self), path, create_parents=effective, transport=self._transport
178
+ )
179
+
180
+ def read_text(self, path: str, *, encoding: str = "utf-8") -> str:
181
+ """Read a text file from the sandbox and return its contents.
182
+
183
+ A bad ``encoding`` surfaces the standard-library ``LookupError`` (unknown
184
+ codec) or ``UnicodeDecodeError`` (bytes that do not decode under it);
185
+ neither is wrapped in ``SandboxError``.
186
+
187
+ Example:
188
+ content = sb.read_text("/app/output.txt")
189
+ """
190
+ from snowflake.sandbox.sync_files import read_text
191
+
192
+ return read_text(cast("Sandbox", self), path, encoding=encoding, transport=self._transport)
193
+
194
+ def read_bytes(self, path: str) -> bytes:
195
+ """Read a binary file from the sandbox and return its contents.
196
+
197
+ Example:
198
+ data = sb.read_bytes("/app/output.bin")
199
+ """
200
+ from snowflake.sandbox.sync_files import read_bytes
201
+
202
+ return read_bytes(cast("Sandbox", self), path, transport=self._transport)
203
+
204
+ def write_text(self, path: str, data: str, *, encoding: str = "utf-8") -> None:
205
+ """Write a text string to a file inside the sandbox.
206
+
207
+ A bad ``encoding`` surfaces the standard-library ``LookupError`` (unknown
208
+ codec) or ``UnicodeEncodeError`` (characters that do not encode under it);
209
+ neither is wrapped in ``SandboxError``.
210
+
211
+ Example:
212
+ sb.write_text("/app/config.json", '{"key": "value"}')
213
+ """
214
+ from snowflake.sandbox.sync_files import write_text
215
+
216
+ write_text(cast("Sandbox", self), path, data, encoding=encoding, transport=self._transport)
217
+
218
+ def write_bytes(self, path: str, data: bytes) -> None:
219
+ """Write bytes to a file inside the sandbox.
220
+
221
+ Example:
222
+ sb.write_bytes("/app/model.pkl", model_bytes)
223
+ """
224
+ from snowflake.sandbox.sync_files import write_bytes
225
+
226
+ write_bytes(cast("Sandbox", self), path, data, transport=self._transport)
227
+
228
+ def stat(self, path: str) -> FileInfo:
229
+ """Return metadata for a file or directory inside the sandbox.
230
+
231
+ Example:
232
+ info = sb.stat("/app/output.txt")
233
+ print(info.size_bytes)
234
+ """
235
+ from snowflake.sandbox._sync_fs_ops import stat as _stat
236
+
237
+ return _stat(cast("Sandbox", self), path, transport=self._transport)
238
+
239
+ def remove(self, path: str, *, recursive: bool = False) -> None:
240
+ """Remove a file or directory inside the sandbox.
241
+
242
+ Example:
243
+ sb.remove("/app/temp", recursive=True)
244
+ """
245
+ from snowflake.sandbox._sync_fs_ops import remove
246
+
247
+ remove(cast("Sandbox", self), path, recursive=recursive, transport=self._transport)
248
+
249
+ def watch(
250
+ self,
251
+ path: str,
252
+ *,
253
+ filter: Sequence[FileWatchEventType] | None = None,
254
+ recursive: bool = False,
255
+ timeout: float | None = None,
256
+ ) -> Iterator[FileWatchEvent]:
257
+ """Stream filesystem-change events under `path` as an iterator.
258
+
259
+ Example:
260
+ for event in sb.watch("/app"):
261
+ print(event.type, event.paths)
262
+ """
263
+ from snowflake.sandbox._sync_watch import watch
264
+
265
+ return watch(
266
+ cast("Sandbox", self),
267
+ path,
268
+ filter=filter,
269
+ recursive=recursive,
270
+ timeout=timeout,
271
+ transport=self._transport,
272
+ )
@@ -0,0 +1,185 @@
1
+ """Sync in-container directory operations, driven by ``exec`` — for ``Sandbox``.
2
+
3
+ Synchronous counterpart to `_fs_ops`; see that module for why these run a
4
+ ``python3`` helper in the container instead of an HTTP route, and for the way their
5
+ path handling differs from the ``/files`` byte transfers in `files`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from snowflake.sandbox._fs_ops import (
14
+ _fileinfo,
15
+ _raise_fs_error,
16
+ _reject_anomalous_path,
17
+ _reject_dangerous_remove,
18
+ )
19
+
20
+ # The in-container half is shared with the async side via _runtime; both modes
21
+ # ship the same program text and parse the same sentinel.
22
+ from snowflake.sandbox._runtime._fs_helper import _FS_HELPER_PREAMBLE
23
+ from snowflake.sandbox._runtime._protocol import _FS_SENTINEL
24
+ from snowflake.sandbox._sync_transport import SyncTransport
25
+ from snowflake.sandbox.exceptions import SandboxError
26
+ from snowflake.sandbox.types import FileInfo
27
+
28
+ if TYPE_CHECKING:
29
+ from snowflake.sandbox.sync_client import Sandbox
30
+
31
+ __all__ = [
32
+ "list_files",
33
+ "stat",
34
+ "make_directory",
35
+ "remove",
36
+ ]
37
+
38
+
39
+ def _fs_op(sandbox: Sandbox, source: str) -> dict[str, Any]:
40
+ """Run *source* in the sandbox and return the parsed JSON result (sync version)."""
41
+ if not sandbox.id:
42
+ raise SandboxError("sandbox must be created before filesystem operations")
43
+ result = sandbox.exec(["python3", "-c", source], timeout=30.0)
44
+ payload: dict[str, Any] | None = None
45
+ for line in result.stdout.splitlines():
46
+ idx = line.find(_FS_SENTINEL)
47
+ if idx == -1:
48
+ continue
49
+ try:
50
+ obj = json.loads(line[idx + len(_FS_SENTINEL) :])
51
+ except ValueError:
52
+ continue
53
+ if isinstance(obj, dict):
54
+ payload = obj
55
+ if payload is None:
56
+ raise SandboxError(
57
+ f"filesystem helper returned no result (stdout: {result.stdout[:200]!r})"
58
+ )
59
+ return payload
60
+
61
+
62
+ def list_files(
63
+ sandbox: Sandbox, path: str, *, transport: SyncTransport | None = None
64
+ ) -> list[FileInfo]:
65
+ """Enumerate a directory's contents (sync version).
66
+
67
+ See ``_fs_ops.list_files`` for full documentation.
68
+ """
69
+ _reject_anomalous_path("list_files", path)
70
+ src = (
71
+ _FS_HELPER_PREAMBLE
72
+ + f"""\
73
+ p = {path!r}
74
+ try:
75
+ out = []
76
+ with os.scandir(p) as it:
77
+ for e in it:
78
+ try:
79
+ st = e.stat(follow_symlinks=False)
80
+ out.append({{"path": e.path, "is_dir": e.is_dir(follow_symlinks=False),
81
+ "size_bytes": st.st_size, "modified_at": st.st_mtime}})
82
+ except OSError:
83
+ pass
84
+ _emit({{"ok": True, "entries": out}})
85
+ except FileNotFoundError:
86
+ _emit({{"ok": False, "error": "not_found"}})
87
+ except NotADirectoryError:
88
+ _emit({{"ok": False, "error": "not_a_directory"}})
89
+ except OSError as e:
90
+ _emit({{"ok": False, "error": str(e)}})
91
+ """
92
+ )
93
+ res = _fs_op(sandbox, src)
94
+ _raise_fs_error(res, path)
95
+ return [_fileinfo(e) for e in res.get("entries", [])]
96
+
97
+
98
+ def stat(sandbox: Sandbox, path: str, *, transport: SyncTransport | None = None) -> FileInfo:
99
+ """Return metadata for one path (sync version).
100
+
101
+ See ``_fs_ops.stat`` for full documentation.
102
+ """
103
+ _reject_anomalous_path("stat", path)
104
+ src = (
105
+ _FS_HELPER_PREAMBLE
106
+ + f"""\
107
+ p = {path!r}
108
+ try:
109
+ st = os.stat(p)
110
+ _emit({{"ok": True, "info": {{"path": p, "is_dir": os.path.isdir(p),
111
+ "size_bytes": st.st_size, "modified_at": st.st_mtime}}}})
112
+ except FileNotFoundError:
113
+ _emit({{"ok": False, "error": "not_found"}})
114
+ except OSError as e:
115
+ _emit({{"ok": False, "error": str(e)}})
116
+ """
117
+ )
118
+ res = _fs_op(sandbox, src)
119
+ _raise_fs_error(res, path)
120
+ return _fileinfo(res["info"])
121
+
122
+
123
+ def make_directory(
124
+ sandbox: Sandbox,
125
+ path: str,
126
+ *,
127
+ create_parents: bool = False,
128
+ transport: SyncTransport | None = None,
129
+ ) -> None:
130
+ """Create a directory (sync version).
131
+
132
+ See ``_fs_ops.make_directory`` for full documentation.
133
+ """
134
+ _reject_anomalous_path("make_directory", path)
135
+ src = (
136
+ _FS_HELPER_PREAMBLE
137
+ + f"""\
138
+ p = {path!r}
139
+ parents = {bool(create_parents)!r}
140
+ try:
141
+ os.makedirs(p, exist_ok=True) if parents else os.mkdir(p)
142
+ _emit({{"ok": True}})
143
+ except FileExistsError:
144
+ _emit({{"ok": False, "error": "exists"}})
145
+ except FileNotFoundError:
146
+ _emit({{"ok": False, "error": "no_parent"}})
147
+ except OSError as e:
148
+ _emit({{"ok": False, "error": str(e)}})
149
+ """
150
+ )
151
+ res = _fs_op(sandbox, src)
152
+ _raise_fs_error(res, path)
153
+
154
+
155
+ def remove(
156
+ sandbox: Sandbox,
157
+ path: str,
158
+ *,
159
+ recursive: bool = False,
160
+ transport: SyncTransport | None = None,
161
+ ) -> None:
162
+ """Delete a file or directory (sync version).
163
+
164
+ See ``_fs_ops.remove`` for full documentation.
165
+ """
166
+ _reject_dangerous_remove(path)
167
+ src = (
168
+ _FS_HELPER_PREAMBLE
169
+ + f"""\
170
+ p = {path!r}
171
+ recursive = {bool(recursive)!r}
172
+ try:
173
+ if os.path.isdir(p) and not os.path.islink(p):
174
+ shutil.rmtree(p) if recursive else os.rmdir(p)
175
+ else:
176
+ os.remove(p)
177
+ _emit({{"ok": True}})
178
+ except FileNotFoundError:
179
+ _emit({{"ok": False, "error": "not_found"}})
180
+ except OSError as e:
181
+ _emit({{"ok": False, "error": str(e)}})
182
+ """
183
+ )
184
+ res = _fs_op(sandbox, src)
185
+ _raise_fs_error(res, path)