akernel-sdk 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.
- akernel_sdk/__init__.py +71 -0
- akernel_sdk/_addresses.py +166 -0
- akernel_sdk/_instance.py +416 -0
- akernel_sdk/_openyuanrong.py +320 -0
- akernel_sdk/_pty_transport.py +304 -0
- akernel_sdk/cli.py +549 -0
- akernel_sdk/commands.py +207 -0
- akernel_sdk/filesystem.py +291 -0
- akernel_sdk/pty.py +250 -0
- akernel_sdk/py.typed +0 -0
- akernel_sdk/sandbox.py +354 -0
- akernel_sdk/types.py +204 -0
- akernel_sdk-0.1.0.dist-info/METADATA +390 -0
- akernel_sdk-0.1.0.dist-info/RECORD +17 -0
- akernel_sdk-0.1.0.dist-info/WHEEL +5 -0
- akernel_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- akernel_sdk-0.1.0.dist-info/top_level.txt +1 -0
akernel_sdk/commands.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Copyright (c) 2026 Ant Group Corporation.
|
|
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
|
+
from typing import Any, Literal, overload
|
|
16
|
+
|
|
17
|
+
import yr
|
|
18
|
+
|
|
19
|
+
from .types import (
|
|
20
|
+
YR_GET_DEFAULT_TIMEOUT,
|
|
21
|
+
YR_GET_TIMEOUT_BUFFER,
|
|
22
|
+
CommandInfo,
|
|
23
|
+
CommandResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommandHandle:
|
|
28
|
+
"""Handle for a background process running in the sandbox."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, pid: int, instance: Any) -> None:
|
|
31
|
+
self.pid = pid
|
|
32
|
+
self._instance = instance
|
|
33
|
+
|
|
34
|
+
def wait(self, timeout: int | None = None) -> CommandResult:
|
|
35
|
+
"""Wait for the process to finish and return its captured output.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
timeout: Maximum number of seconds to wait. ``None`` waits without
|
|
39
|
+
a deadline.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The process output and exit code. A timeout is reported with exit
|
|
43
|
+
code ``-1`` by the sandbox runtime.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
rpc_timeout = (
|
|
47
|
+
-1
|
|
48
|
+
if timeout is None
|
|
49
|
+
else max(timeout + YR_GET_TIMEOUT_BUFFER, YR_GET_DEFAULT_TIMEOUT)
|
|
50
|
+
)
|
|
51
|
+
result = yr.get(
|
|
52
|
+
self._instance.cmd_wait.invoke(pid=self.pid, timeout=timeout),
|
|
53
|
+
timeout=rpc_timeout,
|
|
54
|
+
)
|
|
55
|
+
return CommandResult(
|
|
56
|
+
stdout=result["stdout"],
|
|
57
|
+
stderr=result["stderr"],
|
|
58
|
+
exit_code=result["exit_code"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def kill(self) -> bool:
|
|
62
|
+
"""Terminate the process and return whether it was found."""
|
|
63
|
+
|
|
64
|
+
result = yr.get(self._instance.cmd_kill.invoke(pid=self.pid))
|
|
65
|
+
return result["killed"]
|
|
66
|
+
|
|
67
|
+
def send_stdin(self, data: str, eof: bool = False) -> None:
|
|
68
|
+
"""Write *data* to the process's stdin.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
data: Text to write.
|
|
72
|
+
eof: Close stdin after writing so the process observes EOF.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
RuntimeError: The process was not started with stdin enabled or
|
|
76
|
+
the runtime rejected the write.
|
|
77
|
+
"""
|
|
78
|
+
result = yr.get(
|
|
79
|
+
self._instance.cmd_send_stdin.invoke(pid=self.pid, data=data, eof=eof)
|
|
80
|
+
)
|
|
81
|
+
if result.get("error"):
|
|
82
|
+
raise RuntimeError(f"Failed to send stdin: {result['error']}")
|
|
83
|
+
|
|
84
|
+
def close_stdin(self) -> None:
|
|
85
|
+
"""Close the process's stdin so it sees EOF."""
|
|
86
|
+
self.send_stdin("", eof=True)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Commands:
|
|
90
|
+
"""Client-side wrapper for command execution on the remote sandbox."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, instance: Any) -> None:
|
|
93
|
+
self._instance = instance
|
|
94
|
+
|
|
95
|
+
@overload
|
|
96
|
+
def run(
|
|
97
|
+
self,
|
|
98
|
+
cmd: str,
|
|
99
|
+
background: Literal[False] = False,
|
|
100
|
+
envs: dict[str, str] | None = None,
|
|
101
|
+
cwd: str | None = None,
|
|
102
|
+
timeout: int = 60,
|
|
103
|
+
stdin: Literal[False] = False,
|
|
104
|
+
) -> CommandResult: ...
|
|
105
|
+
|
|
106
|
+
@overload
|
|
107
|
+
def run(
|
|
108
|
+
self,
|
|
109
|
+
cmd: str,
|
|
110
|
+
background: Literal[True],
|
|
111
|
+
envs: dict[str, str] | None = None,
|
|
112
|
+
cwd: str | None = None,
|
|
113
|
+
timeout: int = 60,
|
|
114
|
+
stdin: bool = False,
|
|
115
|
+
) -> CommandHandle: ...
|
|
116
|
+
|
|
117
|
+
def run(
|
|
118
|
+
self,
|
|
119
|
+
cmd: str,
|
|
120
|
+
background: bool = False,
|
|
121
|
+
envs: dict[str, str] | None = None,
|
|
122
|
+
cwd: str | None = None,
|
|
123
|
+
timeout: int = 60,
|
|
124
|
+
stdin: bool = False,
|
|
125
|
+
) -> CommandResult | CommandHandle:
|
|
126
|
+
"""Execute a command in the sandbox.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
cmd: Shell command to execute.
|
|
130
|
+
background: Return a process handle without waiting when true.
|
|
131
|
+
envs: Environment variables for the command.
|
|
132
|
+
cwd: Working directory for the command.
|
|
133
|
+
timeout: Maximum foreground execution time in seconds.
|
|
134
|
+
stdin: Keep stdin open for a background process.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
A completed result for foreground execution or a process handle
|
|
138
|
+
for background execution.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: ``stdin`` is requested for foreground execution.
|
|
142
|
+
RuntimeError: The runtime cannot start a background process.
|
|
143
|
+
"""
|
|
144
|
+
if stdin and not background:
|
|
145
|
+
raise ValueError("stdin=True requires background=True")
|
|
146
|
+
|
|
147
|
+
if background:
|
|
148
|
+
result = yr.get(
|
|
149
|
+
self._instance.cmd_start.invoke(
|
|
150
|
+
cmd=cmd, envs=envs, cwd=cwd, want_stdin=stdin
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
if result.get("error"):
|
|
154
|
+
raise RuntimeError(f"Failed to start command: {result['error']}")
|
|
155
|
+
return CommandHandle(result["pid"], self._instance)
|
|
156
|
+
|
|
157
|
+
rpc_timeout = max(timeout + YR_GET_TIMEOUT_BUFFER, YR_GET_DEFAULT_TIMEOUT)
|
|
158
|
+
result = yr.get(
|
|
159
|
+
self._instance.cmd_run.invoke(cmd=cmd, envs=envs, cwd=cwd, timeout=timeout),
|
|
160
|
+
timeout=rpc_timeout,
|
|
161
|
+
)
|
|
162
|
+
return CommandResult(
|
|
163
|
+
stdout=result["stdout"],
|
|
164
|
+
stderr=result["stderr"],
|
|
165
|
+
exit_code=result["exit_code"],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def list(self) -> list[CommandInfo]:
|
|
169
|
+
"""Return snapshots of processes tracked by this sandbox."""
|
|
170
|
+
|
|
171
|
+
result = yr.get(self._instance.cmd_list.invoke())
|
|
172
|
+
return [
|
|
173
|
+
CommandInfo(
|
|
174
|
+
pid=int(process["pid"]),
|
|
175
|
+
command=str(process["cmd"]),
|
|
176
|
+
running=bool(process["running"]),
|
|
177
|
+
)
|
|
178
|
+
for process in result["processes"]
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
def kill(self, pid: int) -> bool:
|
|
182
|
+
"""Terminate a tracked process and return whether it was found."""
|
|
183
|
+
|
|
184
|
+
result = yr.get(self._instance.cmd_kill.invoke(pid=pid))
|
|
185
|
+
return result["killed"]
|
|
186
|
+
|
|
187
|
+
def send_stdin(self, pid: int, data: str, eof: bool = False) -> None:
|
|
188
|
+
"""Write *data* to the stdin of process *pid*.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
pid: Tracked process ID.
|
|
192
|
+
data: Text to write.
|
|
193
|
+
eof: Close stdin after writing so the process observes EOF.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
RuntimeError: The process was not started with stdin enabled or
|
|
197
|
+
the runtime rejected the write.
|
|
198
|
+
"""
|
|
199
|
+
result = yr.get(
|
|
200
|
+
self._instance.cmd_send_stdin.invoke(pid=pid, data=data, eof=eof)
|
|
201
|
+
)
|
|
202
|
+
if result.get("error"):
|
|
203
|
+
raise RuntimeError(f"Failed to send stdin: {result['error']}")
|
|
204
|
+
|
|
205
|
+
def close_stdin(self, pid: int) -> None:
|
|
206
|
+
"""Close the stdin of process *pid* so it sees EOF."""
|
|
207
|
+
self.send_stdin(pid, "", eof=True)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Copyright (c) 2026 Ant Group Corporation.
|
|
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
|
+
import asyncio
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
from collections.abc import Coroutine
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
20
|
+
from typing import Any, Literal, TypeVar, overload
|
|
21
|
+
|
|
22
|
+
import yr
|
|
23
|
+
|
|
24
|
+
from ._addresses import exec_endpoint_from_env
|
|
25
|
+
from .types import EntryInfo
|
|
26
|
+
|
|
27
|
+
_T = TypeVar("_T")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _run_coroutine(coroutine: Coroutine[Any, Any, _T]) -> _T:
|
|
31
|
+
"""Run a coroutine from a synchronous API in any caller context."""
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
asyncio.get_running_loop()
|
|
35
|
+
except RuntimeError:
|
|
36
|
+
return asyncio.run(coroutine)
|
|
37
|
+
|
|
38
|
+
# asyncio.run() cannot execute in a thread that already owns a running
|
|
39
|
+
# event loop. Keep Filesystem's public API synchronous and isolate the
|
|
40
|
+
# underlying async WebSocket helper in a short-lived worker thread.
|
|
41
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
42
|
+
return executor.submit(asyncio.run, coroutine).result()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Filesystem:
|
|
46
|
+
"""Client-side wrapper for filesystem operations on the remote sandbox."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
instance: Any,
|
|
51
|
+
instance_id: str | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._instance = instance
|
|
54
|
+
self._instance_id = instance_id
|
|
55
|
+
|
|
56
|
+
@overload
|
|
57
|
+
def read(self, path: str, format: Literal["text"] = "text") -> str: ...
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
def read(self, path: str, format: Literal["bytes"]) -> bytes: ...
|
|
61
|
+
|
|
62
|
+
def read(self, path: str, format: str = "text") -> str | bytes:
|
|
63
|
+
"""Read a file as text or bytes.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
path: Absolute or relative path inside the sandbox.
|
|
67
|
+
format: Return decoded text or raw bytes.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: ``format`` is not ``"text"`` or ``"bytes"``.
|
|
71
|
+
RuntimeError: The sandbox cannot read the file.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if format not in ("text", "bytes"):
|
|
75
|
+
raise ValueError("format must be 'text' or 'bytes'")
|
|
76
|
+
binary = format == "bytes"
|
|
77
|
+
result = yr.get(self._instance.fs_read.invoke(path=path, binary=binary))
|
|
78
|
+
if result.get("error"):
|
|
79
|
+
raise RuntimeError(f"Failed to read {path}: {result['error']}")
|
|
80
|
+
data = result["data"]
|
|
81
|
+
if binary:
|
|
82
|
+
return bytes.fromhex(data)
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
# Binary data beyond this threshold is routed through copy_from_local
|
|
86
|
+
# (tar+WebSocket) instead of RPC hex encoding. Benchmark shows the
|
|
87
|
+
# crossover at ~16 MB where hex-encoded RPC starts to degrade while
|
|
88
|
+
# copy_from_local's fixed connection overhead is amortized.
|
|
89
|
+
_WRITE_SIZE_THRESHOLD = 16 * 1024 * 1024
|
|
90
|
+
|
|
91
|
+
def write(self, path: str, data: str | bytes) -> EntryInfo:
|
|
92
|
+
"""Write text or bytes and return metadata for the remote file."""
|
|
93
|
+
|
|
94
|
+
if isinstance(data, bytes):
|
|
95
|
+
if len(data) >= self._WRITE_SIZE_THRESHOLD:
|
|
96
|
+
return self._write_via_copy(path, data)
|
|
97
|
+
binary = True
|
|
98
|
+
send_data = data.hex()
|
|
99
|
+
else:
|
|
100
|
+
binary = False
|
|
101
|
+
send_data = data
|
|
102
|
+
result = yr.get(
|
|
103
|
+
self._instance.fs_write.invoke(path=path, data=send_data, binary=binary)
|
|
104
|
+
)
|
|
105
|
+
if result.get("error"):
|
|
106
|
+
raise RuntimeError(f"Failed to write {path}: {result['error']}")
|
|
107
|
+
return EntryInfo(
|
|
108
|
+
name=result["name"],
|
|
109
|
+
path=result["path"],
|
|
110
|
+
type=result["type"],
|
|
111
|
+
size=result["size"],
|
|
112
|
+
permissions="",
|
|
113
|
+
modified_time=0.0,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _write_via_copy(self, remote_path: str, data: bytes) -> EntryInfo:
|
|
117
|
+
"""Write large binary data via copy_from_local to avoid hex bloat."""
|
|
118
|
+
tmp = tempfile.NamedTemporaryFile(delete=False)
|
|
119
|
+
try:
|
|
120
|
+
tmp.write(data)
|
|
121
|
+
tmp.close()
|
|
122
|
+
self.copy_from_local(tmp.name, remote_path)
|
|
123
|
+
finally:
|
|
124
|
+
os.unlink(tmp.name)
|
|
125
|
+
return EntryInfo(
|
|
126
|
+
name=os.path.basename(remote_path),
|
|
127
|
+
path=remote_path,
|
|
128
|
+
type="file",
|
|
129
|
+
size=len(data),
|
|
130
|
+
permissions="",
|
|
131
|
+
modified_time=0.0,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def list(self, path: str, depth: int = 1) -> list[EntryInfo]:
|
|
135
|
+
"""List entries below a sandbox path up to ``depth`` levels."""
|
|
136
|
+
|
|
137
|
+
result = yr.get(self._instance.fs_list.invoke(path=path, depth=depth))
|
|
138
|
+
if result.get("error"):
|
|
139
|
+
raise RuntimeError(f"Failed to list {path}: {result['error']}")
|
|
140
|
+
return [
|
|
141
|
+
EntryInfo(
|
|
142
|
+
name=e["name"],
|
|
143
|
+
path=e["path"],
|
|
144
|
+
type=e["type"],
|
|
145
|
+
size=e["size"],
|
|
146
|
+
permissions=e["permissions"],
|
|
147
|
+
modified_time=e["modified_time"],
|
|
148
|
+
)
|
|
149
|
+
for e in result["entries"]
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
def exists(self, path: str) -> bool:
|
|
153
|
+
"""Return whether a sandbox path exists."""
|
|
154
|
+
|
|
155
|
+
result = yr.get(self._instance.fs_exists.invoke(path=path))
|
|
156
|
+
return result["exists"]
|
|
157
|
+
|
|
158
|
+
def _get_connection(self) -> tuple[str, str, str, str, bool]:
|
|
159
|
+
# File copy is implemented by the frontend exec WebSocket
|
|
160
|
+
# (/terminal/ws). By default it follows AKERNEL_SERVER_ADDRESS and
|
|
161
|
+
# uses TLS; standalone or custom topologies can override it with
|
|
162
|
+
# AKERNEL_GATEWAY_ADDRESS/YR_GATEWAY_ADDRESS.
|
|
163
|
+
endpoint = exec_endpoint_from_env()
|
|
164
|
+
instance_id = self._instance_id
|
|
165
|
+
if not instance_id:
|
|
166
|
+
raise RuntimeError("Instance ID is not set.")
|
|
167
|
+
from yr.config_manager import ConfigManager
|
|
168
|
+
|
|
169
|
+
token = ConfigManager().auth_token
|
|
170
|
+
return endpoint.host, str(endpoint.port), instance_id, token, endpoint.use_tls
|
|
171
|
+
|
|
172
|
+
def _cp(self, src: str, dst: str, upload: bool) -> None:
|
|
173
|
+
from yr.cli.exec import (
|
|
174
|
+
choose_cp_mode,
|
|
175
|
+
copy_from_remote,
|
|
176
|
+
copy_from_remote_streaming,
|
|
177
|
+
copy_to_remote,
|
|
178
|
+
copy_to_remote_streaming,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
host, port, instance_id, token, use_ssl = self._get_connection()
|
|
182
|
+
local_path = src if upload else dst
|
|
183
|
+
remote_path = dst if upload else src
|
|
184
|
+
|
|
185
|
+
if upload and not os.path.exists(local_path):
|
|
186
|
+
raise FileNotFoundError(f"Local source path not found: {local_path}")
|
|
187
|
+
|
|
188
|
+
streaming = choose_cp_mode(local_path, remote_path, upload=upload)
|
|
189
|
+
|
|
190
|
+
if upload:
|
|
191
|
+
fn = copy_to_remote_streaming if streaming else copy_to_remote
|
|
192
|
+
_run_coroutine(
|
|
193
|
+
fn(
|
|
194
|
+
host=host,
|
|
195
|
+
port=port,
|
|
196
|
+
instance=instance_id,
|
|
197
|
+
local_path=local_path,
|
|
198
|
+
remote_path=remote_path,
|
|
199
|
+
use_ssl=use_ssl,
|
|
200
|
+
verify_server=False,
|
|
201
|
+
token=token,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
fn = copy_from_remote_streaming if streaming else copy_from_remote
|
|
206
|
+
_run_coroutine(
|
|
207
|
+
fn(
|
|
208
|
+
host=host,
|
|
209
|
+
port=port,
|
|
210
|
+
instance=instance_id,
|
|
211
|
+
remote_path=remote_path,
|
|
212
|
+
local_path=local_path,
|
|
213
|
+
use_ssl=use_ssl,
|
|
214
|
+
verify_server=False,
|
|
215
|
+
token=token,
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def copy_from_local(self, local_path: str, remote_path: str) -> None:
|
|
220
|
+
"""Copy a local file or directory **into** the sandbox.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
local_path: Absolute or relative path on the **local** machine.
|
|
224
|
+
remote_path: Absolute path inside the **sandbox**.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
FileNotFoundError: *local_path* does not exist.
|
|
228
|
+
RuntimeError: Server address is not configured.
|
|
229
|
+
"""
|
|
230
|
+
self._cp(local_path, remote_path, upload=True)
|
|
231
|
+
|
|
232
|
+
def copy_to_local(self, remote_path: str, local_path: str) -> None:
|
|
233
|
+
"""Copy a file or directory **from** the sandbox to the local machine.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
remote_path: Absolute path inside the **sandbox**.
|
|
237
|
+
local_path: Absolute or relative path on the **local** machine.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
RuntimeError: Server address is not configured.
|
|
241
|
+
"""
|
|
242
|
+
self._cp(remote_path, local_path, upload=False)
|
|
243
|
+
|
|
244
|
+
def remove(self, path: str) -> None:
|
|
245
|
+
"""Remove a file or directory from the sandbox."""
|
|
246
|
+
|
|
247
|
+
result = yr.get(self._instance.fs_remove.invoke(path=path))
|
|
248
|
+
if result.get("error"):
|
|
249
|
+
raise RuntimeError(f"Failed to remove {path}: {result['error']}")
|
|
250
|
+
|
|
251
|
+
def rename(self, old_path: str, new_path: str) -> EntryInfo:
|
|
252
|
+
"""Rename a sandbox path and return its updated metadata."""
|
|
253
|
+
|
|
254
|
+
result = yr.get(
|
|
255
|
+
self._instance.fs_rename.invoke(old_path=old_path, new_path=new_path)
|
|
256
|
+
)
|
|
257
|
+
if result.get("error"):
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
f"Failed to rename {old_path} -> {new_path}: {result['error']}"
|
|
260
|
+
)
|
|
261
|
+
return EntryInfo(
|
|
262
|
+
name=result["name"],
|
|
263
|
+
path=result["path"],
|
|
264
|
+
type=result["type"],
|
|
265
|
+
size=result["size"],
|
|
266
|
+
permissions=result["permissions"],
|
|
267
|
+
modified_time=result["modified_time"],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def make_dir(self, path: str) -> bool:
|
|
271
|
+
"""Create a directory and return whether it was newly created."""
|
|
272
|
+
|
|
273
|
+
result = yr.get(self._instance.fs_make_dir.invoke(path=path))
|
|
274
|
+
if result.get("error"):
|
|
275
|
+
raise RuntimeError(f"Failed to make dir {path}: {result['error']}")
|
|
276
|
+
return result["created"]
|
|
277
|
+
|
|
278
|
+
def get_info(self, path: str) -> EntryInfo:
|
|
279
|
+
"""Return metadata for a sandbox path."""
|
|
280
|
+
|
|
281
|
+
result = yr.get(self._instance.fs_get_info.invoke(path=path))
|
|
282
|
+
if result.get("error"):
|
|
283
|
+
raise RuntimeError(f"Failed to get info for {path}: {result['error']}")
|
|
284
|
+
return EntryInfo(
|
|
285
|
+
name=result["name"],
|
|
286
|
+
path=result["path"],
|
|
287
|
+
type=result["type"],
|
|
288
|
+
size=result["size"],
|
|
289
|
+
permissions=result["permissions"],
|
|
290
|
+
modified_time=result["modified_time"],
|
|
291
|
+
)
|