opensandbox-cli 0.1.0__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.
@@ -0,0 +1,442 @@
1
+ # Copyright 2026 Alibaba Group Holding Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """File operation commands: cat, write, upload, download, rm, mv, mkdir, rmdir, search, info, chmod, replace."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import click
23
+
24
+ from opensandbox_cli.client import ClientContext
25
+ from opensandbox_cli.utils import handle_errors, output_option, prepare_output
26
+
27
+
28
+ def _parse_permission_mode(mode: str) -> int:
29
+ """Parse a permission mode string like 644 or 755."""
30
+ try:
31
+ return int(mode)
32
+ except ValueError as exc:
33
+ raise click.BadParameter(
34
+ f"Invalid permission mode '{mode}'. Use a permission value like 644 or 755."
35
+ ) from exc
36
+
37
+
38
+ @click.group("file", invoke_without_command=True)
39
+ @click.pass_context
40
+ def file_group(ctx: click.Context) -> None:
41
+ """📁 File operations on a sandbox."""
42
+ if ctx.invoked_subcommand is None:
43
+ click.echo(ctx.get_help())
44
+
45
+
46
+ # ---- cat (read) -----------------------------------------------------------
47
+
48
+ @file_group.command("cat")
49
+ @click.argument("sandbox_id")
50
+ @click.argument("path")
51
+ @click.option("--encoding", default="utf-8", help="File encoding.")
52
+ @output_option("raw", help_text="Output format: raw.")
53
+ @click.pass_obj
54
+ @handle_errors
55
+ def file_cat(
56
+ obj: ClientContext,
57
+ sandbox_id: str,
58
+ path: str,
59
+ encoding: str,
60
+ output_format: str | None,
61
+ ) -> None:
62
+ """Read a file from the sandbox."""
63
+ prepare_output(obj, output_format, allowed=("raw",), fallback="raw")
64
+ sandbox = obj.connect_sandbox(sandbox_id)
65
+ try:
66
+ content = sandbox.files.read_file(path, encoding=encoding)
67
+ click.echo(content, nl=False)
68
+ finally:
69
+ sandbox.close()
70
+
71
+
72
+ # ---- write ----------------------------------------------------------------
73
+
74
+ @file_group.command("write")
75
+ @click.argument("sandbox_id")
76
+ @click.argument("path")
77
+ @click.option("--content", "-c", default=None, help="Content to write. Reads from stdin if not provided.")
78
+ @click.option("--encoding", default="utf-8", help="File encoding.")
79
+ @click.option("--mode", default=None, help="File permission mode (e.g. 0644).")
80
+ @click.option("--owner", default=None, help="File owner.")
81
+ @click.option("--group", default=None, help="File group.")
82
+ @output_option("table", "json", "yaml")
83
+ @click.pass_obj
84
+ @handle_errors
85
+ def file_write(
86
+ obj: ClientContext,
87
+ sandbox_id: str,
88
+ path: str,
89
+ content: str | None,
90
+ encoding: str,
91
+ mode: str | None,
92
+ owner: str | None,
93
+ group: str | None,
94
+ output_format: str | None,
95
+ ) -> None:
96
+ """Write content to a file in the sandbox."""
97
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
98
+ file_content = content
99
+ if file_content is None:
100
+ if sys.stdin.isatty():
101
+ click.echo("Reading from stdin (Ctrl+D to finish):", err=True)
102
+ file_content = sys.stdin.read()
103
+
104
+ sandbox = obj.connect_sandbox(sandbox_id)
105
+ try:
106
+ kwargs: dict = {"encoding": encoding}
107
+ if mode is not None:
108
+ kwargs["mode"] = _parse_permission_mode(mode)
109
+ if owner is not None:
110
+ kwargs["owner"] = owner
111
+ if group is not None:
112
+ kwargs["group"] = group
113
+ sandbox.files.write_file(path, file_content, **kwargs)
114
+ obj.output.success(f"Written: {path}")
115
+ finally:
116
+ sandbox.close()
117
+
118
+
119
+ # ---- upload ---------------------------------------------------------------
120
+
121
+ @file_group.command("upload")
122
+ @click.argument("sandbox_id")
123
+ @click.argument("local_path", type=click.Path(exists=True))
124
+ @click.argument("remote_path")
125
+ @output_option("table", "json", "yaml")
126
+ @click.pass_obj
127
+ @handle_errors
128
+ def file_upload(
129
+ obj: ClientContext,
130
+ sandbox_id: str,
131
+ local_path: str,
132
+ remote_path: str,
133
+ output_format: str | None,
134
+ ) -> None:
135
+ """Upload a local file to the sandbox."""
136
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
137
+ sandbox = obj.connect_sandbox(sandbox_id)
138
+ try:
139
+ with Path(local_path).open("rb") as data:
140
+ sandbox.files.write_file(remote_path, data)
141
+ obj.output.success(f"Uploaded: {local_path} → {remote_path}")
142
+ finally:
143
+ sandbox.close()
144
+
145
+
146
+ # ---- download -------------------------------------------------------------
147
+
148
+ @file_group.command("download")
149
+ @click.argument("sandbox_id")
150
+ @click.argument("remote_path")
151
+ @click.argument("local_path", type=click.Path())
152
+ @output_option("table", "json", "yaml")
153
+ @click.pass_obj
154
+ @handle_errors
155
+ def file_download(
156
+ obj: ClientContext,
157
+ sandbox_id: str,
158
+ remote_path: str,
159
+ local_path: str,
160
+ output_format: str | None,
161
+ ) -> None:
162
+ """Download a file from the sandbox to local disk."""
163
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
164
+ sandbox = obj.connect_sandbox(sandbox_id)
165
+ try:
166
+ destination = Path(local_path)
167
+ destination.parent.mkdir(parents=True, exist_ok=True)
168
+ with destination.open("wb") as out:
169
+ for chunk in sandbox.files.read_bytes_stream(remote_path):
170
+ out.write(chunk)
171
+ obj.output.success(f"Downloaded: {remote_path} → {local_path}")
172
+ finally:
173
+ sandbox.close()
174
+
175
+
176
+ # ---- rm (delete) ----------------------------------------------------------
177
+
178
+ @file_group.command("rm")
179
+ @click.argument("sandbox_id")
180
+ @click.argument("paths", nargs=-1, required=True)
181
+ @output_option("table", "json", "yaml")
182
+ @click.pass_obj
183
+ @handle_errors
184
+ def file_rm(
185
+ obj: ClientContext,
186
+ sandbox_id: str,
187
+ paths: tuple[str, ...],
188
+ output_format: str | None,
189
+ ) -> None:
190
+ """Delete files from the sandbox."""
191
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
192
+ sandbox = obj.connect_sandbox(sandbox_id)
193
+ try:
194
+ sandbox.files.delete_files(list(paths))
195
+ obj.output.print_rows(
196
+ [{"path": p, "status": "deleted"} for p in paths],
197
+ columns=["path", "status"],
198
+ title="Deleted Files",
199
+ )
200
+ finally:
201
+ sandbox.close()
202
+
203
+
204
+ # ---- mv (move) ------------------------------------------------------------
205
+
206
+ @file_group.command("mv")
207
+ @click.argument("sandbox_id")
208
+ @click.argument("source")
209
+ @click.argument("destination")
210
+ @output_option("table", "json", "yaml")
211
+ @click.pass_obj
212
+ @handle_errors
213
+ def file_mv(
214
+ obj: ClientContext,
215
+ sandbox_id: str,
216
+ source: str,
217
+ destination: str,
218
+ output_format: str | None,
219
+ ) -> None:
220
+ """Move/rename a file in the sandbox."""
221
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
222
+ from opensandbox.models.filesystem import MoveEntry
223
+
224
+ sandbox = obj.connect_sandbox(sandbox_id)
225
+ try:
226
+ sandbox.files.move_files([MoveEntry(source=source, destination=destination)])
227
+ obj.output.success(f"Moved: {source} → {destination}")
228
+ finally:
229
+ sandbox.close()
230
+
231
+
232
+ # ---- mkdir ----------------------------------------------------------------
233
+
234
+ @file_group.command("mkdir")
235
+ @click.argument("sandbox_id")
236
+ @click.argument("paths", nargs=-1, required=True)
237
+ @click.option("--mode", default=None, help="Directory permission mode.")
238
+ @click.option("--owner", default=None, help="Directory owner.")
239
+ @click.option("--group", default=None, help="Directory group.")
240
+ @output_option("table", "json", "yaml")
241
+ @click.pass_obj
242
+ @handle_errors
243
+ def file_mkdir(
244
+ obj: ClientContext,
245
+ sandbox_id: str,
246
+ paths: tuple[str, ...],
247
+ mode: str | None,
248
+ owner: str | None,
249
+ group: str | None,
250
+ output_format: str | None,
251
+ ) -> None:
252
+ """Create directories in the sandbox."""
253
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
254
+ from opensandbox.models.filesystem import WriteEntry
255
+
256
+ sandbox = obj.connect_sandbox(sandbox_id)
257
+ try:
258
+ entries = []
259
+ for p in paths:
260
+ kwargs: dict = {"path": p}
261
+ if mode is not None:
262
+ kwargs["mode"] = _parse_permission_mode(mode)
263
+ if owner is not None:
264
+ kwargs["owner"] = owner
265
+ if group is not None:
266
+ kwargs["group"] = group
267
+ entries.append(WriteEntry(**kwargs))
268
+ sandbox.files.create_directories(entries)
269
+ obj.output.print_rows(
270
+ [{"path": p, "status": "created"} for p in paths],
271
+ columns=["path", "status"],
272
+ title="Created Directories",
273
+ )
274
+ finally:
275
+ sandbox.close()
276
+
277
+
278
+ # ---- rmdir ----------------------------------------------------------------
279
+
280
+ @file_group.command("rmdir")
281
+ @click.argument("sandbox_id")
282
+ @click.argument("paths", nargs=-1, required=True)
283
+ @output_option("table", "json", "yaml")
284
+ @click.pass_obj
285
+ @handle_errors
286
+ def file_rmdir(
287
+ obj: ClientContext,
288
+ sandbox_id: str,
289
+ paths: tuple[str, ...],
290
+ output_format: str | None,
291
+ ) -> None:
292
+ """Delete directories from the sandbox."""
293
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
294
+ sandbox = obj.connect_sandbox(sandbox_id)
295
+ try:
296
+ sandbox.files.delete_directories(list(paths))
297
+ obj.output.print_rows(
298
+ [{"path": p, "status": "removed"} for p in paths],
299
+ columns=["path", "status"],
300
+ title="Removed Directories",
301
+ )
302
+ finally:
303
+ sandbox.close()
304
+
305
+
306
+ # ---- search ---------------------------------------------------------------
307
+
308
+ @file_group.command("search")
309
+ @click.argument("sandbox_id")
310
+ @click.argument("path")
311
+ @click.option("--pattern", "-p", required=True, help="Glob pattern to search for.")
312
+ @output_option("table", "json", "yaml")
313
+ @click.pass_obj
314
+ @handle_errors
315
+ def file_search(
316
+ obj: ClientContext,
317
+ sandbox_id: str,
318
+ path: str,
319
+ pattern: str,
320
+ output_format: str | None,
321
+ ) -> None:
322
+ """Search for files in the sandbox."""
323
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
324
+ from opensandbox.models.filesystem import SearchEntry
325
+
326
+ sandbox = obj.connect_sandbox(sandbox_id)
327
+ try:
328
+ results = sandbox.files.search(SearchEntry(path=path, pattern=pattern))
329
+ if not results:
330
+ if obj.output.fmt in ("json", "yaml"):
331
+ obj.output.print_models([], columns=[])
332
+ else:
333
+ obj.output.info("No files found.")
334
+ return
335
+ if obj.output.fmt in ("json", "yaml"):
336
+ obj.output.print_models(results, columns=["path", "size", "mode", "owner", "modified_at"])
337
+ else:
338
+ obj.output.print_models(results, columns=["path", "size", "owner"], title="Search Results")
339
+ finally:
340
+ sandbox.close()
341
+
342
+
343
+ # ---- info (stat) ----------------------------------------------------------
344
+
345
+ @file_group.command("info")
346
+ @click.argument("sandbox_id")
347
+ @click.argument("paths", nargs=-1, required=True)
348
+ @output_option("table", "json", "yaml")
349
+ @click.pass_obj
350
+ @handle_errors
351
+ def file_info(
352
+ obj: ClientContext,
353
+ sandbox_id: str,
354
+ paths: tuple[str, ...],
355
+ output_format: str | None,
356
+ ) -> None:
357
+ """Get file/directory info."""
358
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
359
+ sandbox = obj.connect_sandbox(sandbox_id)
360
+ try:
361
+ info_map = sandbox.files.get_file_info(list(paths))
362
+ rows = [{"path": path, **entry.model_dump(mode="json")} for path, entry in info_map.items()]
363
+ obj.output.print_rows(
364
+ rows,
365
+ columns=["path", "size", "mode", "owner", "group", "created_at", "modified_at"],
366
+ title="Path Info",
367
+ )
368
+ finally:
369
+ sandbox.close()
370
+
371
+
372
+ # ---- chmod ----------------------------------------------------------------
373
+
374
+ @file_group.command("chmod")
375
+ @click.argument("sandbox_id")
376
+ @click.argument("path")
377
+ @click.option("--mode", required=True, help="Permission mode (e.g. 0755).")
378
+ @click.option("--owner", default=None, help="File owner.")
379
+ @click.option("--group", default=None, help="File group.")
380
+ @output_option("table", "json", "yaml")
381
+ @click.pass_obj
382
+ @handle_errors
383
+ def file_chmod(
384
+ obj: ClientContext,
385
+ sandbox_id: str,
386
+ path: str,
387
+ mode: str,
388
+ owner: str | None,
389
+ group: str | None,
390
+ output_format: str | None,
391
+ ) -> None:
392
+ """Set file permissions."""
393
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
394
+ from opensandbox.models.filesystem import SetPermissionEntry
395
+
396
+ sandbox = obj.connect_sandbox(sandbox_id)
397
+ try:
398
+ sandbox.files.set_permissions(
399
+ [
400
+ SetPermissionEntry(
401
+ path=path,
402
+ mode=_parse_permission_mode(mode),
403
+ owner=owner,
404
+ group=group,
405
+ )
406
+ ]
407
+ )
408
+ obj.output.success(f"Permissions set: {path}")
409
+ finally:
410
+ sandbox.close()
411
+
412
+
413
+ # ---- replace --------------------------------------------------------------
414
+
415
+ @file_group.command("replace")
416
+ @click.argument("sandbox_id")
417
+ @click.argument("path")
418
+ @click.option("--old", required=True, help="Text to search for.")
419
+ @click.option("--new", required=True, help="Replacement text.")
420
+ @output_option("table", "json", "yaml")
421
+ @click.pass_obj
422
+ @handle_errors
423
+ def file_replace(
424
+ obj: ClientContext,
425
+ sandbox_id: str,
426
+ path: str,
427
+ old: str,
428
+ new: str,
429
+ output_format: str | None,
430
+ ) -> None:
431
+ """Replace content in a file."""
432
+ prepare_output(obj, output_format, allowed=("table", "json", "yaml"), fallback="table")
433
+ from opensandbox.models.filesystem import ContentReplaceEntry
434
+
435
+ sandbox = obj.connect_sandbox(sandbox_id)
436
+ try:
437
+ sandbox.files.replace_contents(
438
+ [ContentReplaceEntry(path=path, old_content=old, new_content=new)]
439
+ )
440
+ obj.output.success(f"Replaced in: {path}")
441
+ finally:
442
+ sandbox.close()