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,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)
|