moru 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.
- moru/__init__.py +174 -0
- moru/api/__init__.py +164 -0
- moru/api/client/__init__.py +8 -0
- moru/api/client/api/__init__.py +1 -0
- moru/api/client/api/sandboxes/__init__.py +1 -0
- moru/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- moru/api/client/api/sandboxes/get_sandboxes.py +176 -0
- moru/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- moru/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- moru/api/client/api/sandboxes/post_sandboxes.py +172 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- moru/api/client/api/templates/__init__.py +1 -0
- moru/api/client/api/templates/delete_templates_template_id.py +157 -0
- moru/api/client/api/templates/get_templates.py +172 -0
- moru/api/client/api/templates/get_templates_template_id.py +195 -0
- moru/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +217 -0
- moru/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- moru/api/client/api/templates/patch_templates_template_id.py +183 -0
- moru/api/client/api/templates/post_templates.py +172 -0
- moru/api/client/api/templates/post_templates_template_id.py +181 -0
- moru/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- moru/api/client/api/templates/post_v2_templates.py +172 -0
- moru/api/client/api/templates/post_v3_templates.py +172 -0
- moru/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- moru/api/client/client.py +286 -0
- moru/api/client/errors.py +16 -0
- moru/api/client/models/__init__.py +123 -0
- moru/api/client/models/aws_registry.py +85 -0
- moru/api/client/models/aws_registry_type.py +8 -0
- moru/api/client/models/build_log_entry.py +89 -0
- moru/api/client/models/build_status_reason.py +95 -0
- moru/api/client/models/connect_sandbox.py +59 -0
- moru/api/client/models/created_access_token.py +100 -0
- moru/api/client/models/created_team_api_key.py +166 -0
- moru/api/client/models/disk_metrics.py +91 -0
- moru/api/client/models/error.py +67 -0
- moru/api/client/models/gcp_registry.py +69 -0
- moru/api/client/models/gcp_registry_type.py +8 -0
- moru/api/client/models/general_registry.py +77 -0
- moru/api/client/models/general_registry_type.py +8 -0
- moru/api/client/models/identifier_masking_details.py +83 -0
- moru/api/client/models/listed_sandbox.py +154 -0
- moru/api/client/models/log_level.py +11 -0
- moru/api/client/models/max_team_metric.py +78 -0
- moru/api/client/models/mcp_type_0.py +44 -0
- moru/api/client/models/new_access_token.py +59 -0
- moru/api/client/models/new_sandbox.py +172 -0
- moru/api/client/models/new_team_api_key.py +59 -0
- moru/api/client/models/node.py +155 -0
- moru/api/client/models/node_detail.py +165 -0
- moru/api/client/models/node_metrics.py +122 -0
- moru/api/client/models/node_status.py +11 -0
- moru/api/client/models/node_status_change.py +79 -0
- moru/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- moru/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- moru/api/client/models/resumed_sandbox.py +68 -0
- moru/api/client/models/sandbox.py +145 -0
- moru/api/client/models/sandbox_detail.py +183 -0
- moru/api/client/models/sandbox_log.py +70 -0
- moru/api/client/models/sandbox_log_entry.py +93 -0
- moru/api/client/models/sandbox_log_entry_fields.py +44 -0
- moru/api/client/models/sandbox_logs.py +91 -0
- moru/api/client/models/sandbox_metric.py +118 -0
- moru/api/client/models/sandbox_network_config.py +92 -0
- moru/api/client/models/sandbox_state.py +9 -0
- moru/api/client/models/sandboxes_with_metrics.py +59 -0
- moru/api/client/models/team.py +83 -0
- moru/api/client/models/team_api_key.py +158 -0
- moru/api/client/models/team_metric.py +86 -0
- moru/api/client/models/team_user.py +68 -0
- moru/api/client/models/template.py +217 -0
- moru/api/client/models/template_build.py +139 -0
- moru/api/client/models/template_build_file_upload.py +70 -0
- moru/api/client/models/template_build_info.py +126 -0
- moru/api/client/models/template_build_request.py +115 -0
- moru/api/client/models/template_build_request_v2.py +88 -0
- moru/api/client/models/template_build_request_v3.py +88 -0
- moru/api/client/models/template_build_start_v2.py +184 -0
- moru/api/client/models/template_build_status.py +11 -0
- moru/api/client/models/template_legacy.py +207 -0
- moru/api/client/models/template_request_response_v3.py +83 -0
- moru/api/client/models/template_step.py +91 -0
- moru/api/client/models/template_update_request.py +59 -0
- moru/api/client/models/template_with_builds.py +148 -0
- moru/api/client/models/update_team_api_key.py +59 -0
- moru/api/client/py.typed +1 -0
- moru/api/client/types.py +54 -0
- moru/api/client_async/__init__.py +50 -0
- moru/api/client_sync/__init__.py +52 -0
- moru/api/metadata.py +14 -0
- moru/connection_config.py +217 -0
- moru/envd/api.py +59 -0
- moru/envd/filesystem/filesystem_connect.py +193 -0
- moru/envd/filesystem/filesystem_pb2.py +76 -0
- moru/envd/filesystem/filesystem_pb2.pyi +233 -0
- moru/envd/process/process_connect.py +155 -0
- moru/envd/process/process_pb2.py +92 -0
- moru/envd/process/process_pb2.pyi +304 -0
- moru/envd/rpc.py +61 -0
- moru/envd/versions.py +6 -0
- moru/exceptions.py +95 -0
- moru/sandbox/commands/command_handle.py +69 -0
- moru/sandbox/commands/main.py +39 -0
- moru/sandbox/filesystem/filesystem.py +94 -0
- moru/sandbox/filesystem/watch_handle.py +60 -0
- moru/sandbox/main.py +210 -0
- moru/sandbox/mcp.py +1120 -0
- moru/sandbox/network.py +8 -0
- moru/sandbox/sandbox_api.py +210 -0
- moru/sandbox/signature.py +45 -0
- moru/sandbox/utils.py +34 -0
- moru/sandbox_async/commands/command.py +336 -0
- moru/sandbox_async/commands/command_handle.py +196 -0
- moru/sandbox_async/commands/pty.py +240 -0
- moru/sandbox_async/filesystem/filesystem.py +531 -0
- moru/sandbox_async/filesystem/watch_handle.py +62 -0
- moru/sandbox_async/main.py +734 -0
- moru/sandbox_async/paginator.py +69 -0
- moru/sandbox_async/sandbox_api.py +325 -0
- moru/sandbox_async/utils.py +7 -0
- moru/sandbox_sync/commands/command.py +328 -0
- moru/sandbox_sync/commands/command_handle.py +150 -0
- moru/sandbox_sync/commands/pty.py +230 -0
- moru/sandbox_sync/filesystem/filesystem.py +518 -0
- moru/sandbox_sync/filesystem/watch_handle.py +69 -0
- moru/sandbox_sync/main.py +726 -0
- moru/sandbox_sync/paginator.py +69 -0
- moru/sandbox_sync/sandbox_api.py +308 -0
- moru/template/consts.py +30 -0
- moru/template/dockerfile_parser.py +275 -0
- moru/template/logger.py +232 -0
- moru/template/main.py +1360 -0
- moru/template/readycmd.py +138 -0
- moru/template/types.py +105 -0
- moru/template/utils.py +320 -0
- moru/template_async/build_api.py +202 -0
- moru/template_async/main.py +366 -0
- moru/template_sync/build_api.py +199 -0
- moru/template_sync/main.py +371 -0
- moru-0.1.0.dist-info/METADATA +63 -0
- moru-0.1.0.dist-info/RECORD +152 -0
- moru-0.1.0.dist-info/WHEEL +4 -0
- moru-0.1.0.dist-info/licenses/LICENSE +9 -0
- moru_connect/__init__.py +1 -0
- moru_connect/client.py +493 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import httpcore
|
|
2
|
+
import httpx
|
|
3
|
+
from io import IOBase
|
|
4
|
+
from packaging.version import Version
|
|
5
|
+
from typing import AsyncIterator, IO, List, Literal, Optional, overload, Union
|
|
6
|
+
from moru.sandbox.filesystem.filesystem import WriteEntry
|
|
7
|
+
import moru_connect as connect
|
|
8
|
+
from moru.connection_config import (
|
|
9
|
+
ConnectionConfig,
|
|
10
|
+
Username,
|
|
11
|
+
default_username,
|
|
12
|
+
KEEPALIVE_PING_HEADER,
|
|
13
|
+
KEEPALIVE_PING_INTERVAL_SEC,
|
|
14
|
+
)
|
|
15
|
+
from moru.envd.api import ENVD_API_FILES_ROUTE, ahandle_envd_api_exception
|
|
16
|
+
from moru.envd.filesystem import filesystem_connect, filesystem_pb2
|
|
17
|
+
from moru.envd.rpc import authentication_header, handle_rpc_exception
|
|
18
|
+
from moru.envd.versions import ENVD_VERSION_RECURSIVE_WATCH, ENVD_DEFAULT_USER
|
|
19
|
+
from moru.exceptions import SandboxException, TemplateException, InvalidArgumentException
|
|
20
|
+
from moru.sandbox.filesystem.filesystem import (
|
|
21
|
+
WriteInfo,
|
|
22
|
+
EntryInfo,
|
|
23
|
+
map_file_type,
|
|
24
|
+
)
|
|
25
|
+
from moru.sandbox.filesystem.watch_handle import FilesystemEvent
|
|
26
|
+
from moru.sandbox_async.filesystem.watch_handle import AsyncWatchHandle
|
|
27
|
+
from moru.sandbox_async.utils import OutputHandler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Filesystem:
|
|
31
|
+
"""
|
|
32
|
+
Module for interacting with the filesystem in the sandbox.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
envd_api_url: str,
|
|
38
|
+
envd_version: Version,
|
|
39
|
+
connection_config: ConnectionConfig,
|
|
40
|
+
pool: httpcore.AsyncConnectionPool,
|
|
41
|
+
envd_api: httpx.AsyncClient,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._envd_api_url = envd_api_url
|
|
44
|
+
self._envd_version = envd_version
|
|
45
|
+
self._connection_config = connection_config
|
|
46
|
+
self._pool = pool
|
|
47
|
+
self._envd_api = envd_api
|
|
48
|
+
|
|
49
|
+
self._rpc = filesystem_connect.FilesystemClient(
|
|
50
|
+
envd_api_url,
|
|
51
|
+
# TODO: Fix and enable compression again — the headers compression is not solved for streaming.
|
|
52
|
+
# compressor=moru_connect.GzipCompressor,
|
|
53
|
+
async_pool=pool,
|
|
54
|
+
json=True,
|
|
55
|
+
headers=connection_config.sandbox_headers,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
async def read(
|
|
60
|
+
self,
|
|
61
|
+
path: str,
|
|
62
|
+
format: Literal["text"] = "text",
|
|
63
|
+
user: Optional[Username] = None,
|
|
64
|
+
request_timeout: Optional[float] = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Read file content as a `str`.
|
|
68
|
+
|
|
69
|
+
:param path: Path to the file
|
|
70
|
+
:param user: Run the operation as this user
|
|
71
|
+
:param format: Format of the file content—`text` by default
|
|
72
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
73
|
+
|
|
74
|
+
:return: File content as a `str`
|
|
75
|
+
"""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
@overload
|
|
79
|
+
async def read(
|
|
80
|
+
self,
|
|
81
|
+
path: str,
|
|
82
|
+
format: Literal["bytes"],
|
|
83
|
+
user: Optional[Username] = None,
|
|
84
|
+
request_timeout: Optional[float] = None,
|
|
85
|
+
) -> bytearray:
|
|
86
|
+
"""
|
|
87
|
+
Read file content as a `bytearray`.
|
|
88
|
+
|
|
89
|
+
:param path: Path to the file
|
|
90
|
+
:param user: Run the operation as this user
|
|
91
|
+
:param format: Format of the file content—`bytes`
|
|
92
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
93
|
+
|
|
94
|
+
:return: File content as a `bytearray`
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
@overload
|
|
99
|
+
async def read(
|
|
100
|
+
self,
|
|
101
|
+
path: str,
|
|
102
|
+
format: Literal["stream"],
|
|
103
|
+
user: Optional[Username] = None,
|
|
104
|
+
request_timeout: Optional[float] = None,
|
|
105
|
+
) -> AsyncIterator[bytes]:
|
|
106
|
+
"""
|
|
107
|
+
Read file content as a `AsyncIterator[bytes]`.
|
|
108
|
+
|
|
109
|
+
:param path: Path to the file
|
|
110
|
+
:param user: Run the operation as this user
|
|
111
|
+
:param format: Format of the file content—`stream`
|
|
112
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
113
|
+
|
|
114
|
+
:return: File content as an `AsyncIterator[bytes]`
|
|
115
|
+
"""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
async def read(
|
|
119
|
+
self,
|
|
120
|
+
path: str,
|
|
121
|
+
format: Literal["text", "bytes", "stream"] = "text",
|
|
122
|
+
user: Optional[Username] = None,
|
|
123
|
+
request_timeout: Optional[float] = None,
|
|
124
|
+
):
|
|
125
|
+
username = user
|
|
126
|
+
if username is None and self._envd_version < ENVD_DEFAULT_USER:
|
|
127
|
+
username = default_username
|
|
128
|
+
|
|
129
|
+
params = {"path": path}
|
|
130
|
+
if username:
|
|
131
|
+
params["username"] = username
|
|
132
|
+
|
|
133
|
+
r = await self._envd_api.get(
|
|
134
|
+
ENVD_API_FILES_ROUTE,
|
|
135
|
+
params=params,
|
|
136
|
+
timeout=self._connection_config.get_request_timeout(request_timeout),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
err = await ahandle_envd_api_exception(r)
|
|
140
|
+
if err:
|
|
141
|
+
raise err
|
|
142
|
+
|
|
143
|
+
if format == "text":
|
|
144
|
+
return r.text
|
|
145
|
+
elif format == "bytes":
|
|
146
|
+
return bytearray(r.content)
|
|
147
|
+
elif format == "stream":
|
|
148
|
+
return r.aiter_bytes()
|
|
149
|
+
|
|
150
|
+
async def write(
|
|
151
|
+
self,
|
|
152
|
+
path: str,
|
|
153
|
+
data: Union[str, bytes, IO],
|
|
154
|
+
user: Optional[Username] = None,
|
|
155
|
+
request_timeout: Optional[float] = None,
|
|
156
|
+
) -> WriteInfo:
|
|
157
|
+
"""
|
|
158
|
+
Write content to a file on the path.
|
|
159
|
+
Writing to a file that doesn't exist creates the file.
|
|
160
|
+
Writing to a file that already exists overwrites the file.
|
|
161
|
+
Writing to a file at path that doesn't exist creates the necessary directories.
|
|
162
|
+
|
|
163
|
+
:param path: Path to the file
|
|
164
|
+
:param data: Data to write to the file, can be a `str`, `bytes`, or `IO`.
|
|
165
|
+
:param user: Run the operation as this user
|
|
166
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
167
|
+
|
|
168
|
+
:return: Information about the written file
|
|
169
|
+
"""
|
|
170
|
+
result = await self.write_files(
|
|
171
|
+
[WriteEntry(path=path, data=data)], user, request_timeout
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if len(result) != 1:
|
|
175
|
+
raise SandboxException("Received unexpected response from write operation")
|
|
176
|
+
|
|
177
|
+
return result[0]
|
|
178
|
+
|
|
179
|
+
async def write_files(
|
|
180
|
+
self,
|
|
181
|
+
files: List[WriteEntry],
|
|
182
|
+
user: Optional[Username] = None,
|
|
183
|
+
request_timeout: Optional[float] = None,
|
|
184
|
+
) -> List[WriteInfo]:
|
|
185
|
+
"""
|
|
186
|
+
Writes multiple files.
|
|
187
|
+
|
|
188
|
+
Writes a list of files to the filesystem.
|
|
189
|
+
When writing to a file that doesn't exist, the file will get created.
|
|
190
|
+
When writing to a file that already exists, the file will get overwritten.
|
|
191
|
+
When writing to a file that's in a directory that doesn't exist, you'll get an error.
|
|
192
|
+
|
|
193
|
+
:param files: list of files to write as `WriteEntry` objects, each containing `path` and `data`
|
|
194
|
+
:param user: Run the operation as this user
|
|
195
|
+
:param request_timeout: Timeout for the request
|
|
196
|
+
:return: Information about the written files
|
|
197
|
+
"""
|
|
198
|
+
username = user
|
|
199
|
+
if username is None and self._envd_version < ENVD_DEFAULT_USER:
|
|
200
|
+
username = default_username
|
|
201
|
+
|
|
202
|
+
params = {}
|
|
203
|
+
if username:
|
|
204
|
+
params["username"] = username
|
|
205
|
+
if len(files) == 1:
|
|
206
|
+
params["path"] = files[0]["path"]
|
|
207
|
+
|
|
208
|
+
# Prepare the files for the multipart/form-data request
|
|
209
|
+
httpx_files = []
|
|
210
|
+
for file in files:
|
|
211
|
+
file_path, file_data = file["path"], file["data"]
|
|
212
|
+
if isinstance(file_data, str) or isinstance(file_data, bytes):
|
|
213
|
+
httpx_files.append(("file", (file_path, file_data)))
|
|
214
|
+
elif isinstance(file_data, IOBase):
|
|
215
|
+
httpx_files.append(("file", (file_path, file_data.read())))
|
|
216
|
+
else:
|
|
217
|
+
raise InvalidArgumentException(
|
|
218
|
+
f"Unsupported data type for file {file_path}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Allow passing empty list of files
|
|
222
|
+
if len(httpx_files) == 0:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
r = await self._envd_api.post(
|
|
226
|
+
ENVD_API_FILES_ROUTE,
|
|
227
|
+
files=httpx_files,
|
|
228
|
+
params=params,
|
|
229
|
+
timeout=self._connection_config.get_request_timeout(request_timeout),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
err = await ahandle_envd_api_exception(r)
|
|
233
|
+
if err:
|
|
234
|
+
raise err
|
|
235
|
+
|
|
236
|
+
write_files = r.json()
|
|
237
|
+
|
|
238
|
+
if not isinstance(write_files, list) or len(write_files) == 0:
|
|
239
|
+
raise SandboxException("Expected to receive information about written file")
|
|
240
|
+
|
|
241
|
+
return [WriteInfo(**file) for file in write_files]
|
|
242
|
+
|
|
243
|
+
async def list(
|
|
244
|
+
self,
|
|
245
|
+
path: str,
|
|
246
|
+
depth: Optional[int] = 1,
|
|
247
|
+
user: Optional[Username] = None,
|
|
248
|
+
request_timeout: Optional[float] = None,
|
|
249
|
+
) -> List[EntryInfo]:
|
|
250
|
+
"""
|
|
251
|
+
List entries in a directory.
|
|
252
|
+
|
|
253
|
+
:param path: Path to the directory
|
|
254
|
+
:param depth: Depth of the directory to list
|
|
255
|
+
:param user: Run the operation as this user
|
|
256
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
257
|
+
|
|
258
|
+
:return: List of entries in the directory
|
|
259
|
+
"""
|
|
260
|
+
if depth is not None and depth < 1:
|
|
261
|
+
raise InvalidArgumentException("depth should be at least 1")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
res = await self._rpc.alist_dir(
|
|
265
|
+
filesystem_pb2.ListDirRequest(path=path, depth=depth),
|
|
266
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
267
|
+
request_timeout
|
|
268
|
+
),
|
|
269
|
+
headers=authentication_header(self._envd_version, user),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
entries: List[EntryInfo] = []
|
|
273
|
+
for entry in res.entries:
|
|
274
|
+
event_type = map_file_type(entry.type)
|
|
275
|
+
|
|
276
|
+
if event_type:
|
|
277
|
+
entries.append(
|
|
278
|
+
EntryInfo(
|
|
279
|
+
name=entry.name,
|
|
280
|
+
type=event_type,
|
|
281
|
+
path=entry.path,
|
|
282
|
+
size=entry.size,
|
|
283
|
+
mode=entry.mode,
|
|
284
|
+
permissions=entry.permissions,
|
|
285
|
+
owner=entry.owner,
|
|
286
|
+
group=entry.group,
|
|
287
|
+
modified_time=entry.modified_time.ToDatetime(),
|
|
288
|
+
# Optional, we can't directly access symlink_target otherwise if will be "" instead of None
|
|
289
|
+
symlink_target=(
|
|
290
|
+
entry.symlink_target
|
|
291
|
+
if entry.HasField("symlink_target")
|
|
292
|
+
else None
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return entries
|
|
298
|
+
except Exception as e:
|
|
299
|
+
raise handle_rpc_exception(e)
|
|
300
|
+
|
|
301
|
+
async def exists(
|
|
302
|
+
self,
|
|
303
|
+
path: str,
|
|
304
|
+
user: Optional[Username] = None,
|
|
305
|
+
request_timeout: Optional[float] = None,
|
|
306
|
+
) -> bool:
|
|
307
|
+
"""
|
|
308
|
+
Check if a file or a directory exists.
|
|
309
|
+
|
|
310
|
+
:param path: Path to a file or a directory
|
|
311
|
+
:param user: Run the operation as this user
|
|
312
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
313
|
+
|
|
314
|
+
:return: `True` if the file or directory exists, `False` otherwise
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
await self._rpc.astat(
|
|
318
|
+
filesystem_pb2.StatRequest(path=path),
|
|
319
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
320
|
+
request_timeout
|
|
321
|
+
),
|
|
322
|
+
headers=authentication_header(self._envd_version, user),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
if isinstance(e, connect.ConnectException):
|
|
329
|
+
if e.status == connect.Code.not_found:
|
|
330
|
+
return False
|
|
331
|
+
raise handle_rpc_exception(e)
|
|
332
|
+
|
|
333
|
+
async def get_info(
|
|
334
|
+
self,
|
|
335
|
+
path: str,
|
|
336
|
+
user: Optional[Username] = None,
|
|
337
|
+
request_timeout: Optional[float] = None,
|
|
338
|
+
) -> EntryInfo:
|
|
339
|
+
"""
|
|
340
|
+
Get information about a file or directory.
|
|
341
|
+
|
|
342
|
+
:param path: Path to a file or a directory
|
|
343
|
+
:param user: Run the operation as this user
|
|
344
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
345
|
+
|
|
346
|
+
:return: Information about the file or directory like name, type, and path
|
|
347
|
+
"""
|
|
348
|
+
try:
|
|
349
|
+
r = await self._rpc.astat(
|
|
350
|
+
filesystem_pb2.StatRequest(path=path),
|
|
351
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
352
|
+
request_timeout
|
|
353
|
+
),
|
|
354
|
+
headers=authentication_header(self._envd_version, user),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return EntryInfo(
|
|
358
|
+
name=r.entry.name,
|
|
359
|
+
type=map_file_type(r.entry.type),
|
|
360
|
+
path=r.entry.path,
|
|
361
|
+
size=r.entry.size,
|
|
362
|
+
mode=r.entry.mode,
|
|
363
|
+
permissions=r.entry.permissions,
|
|
364
|
+
owner=r.entry.owner,
|
|
365
|
+
group=r.entry.group,
|
|
366
|
+
modified_time=r.entry.modified_time.ToDatetime(),
|
|
367
|
+
symlink_target=(
|
|
368
|
+
r.entry.symlink_target
|
|
369
|
+
if r.entry.HasField("symlink_target")
|
|
370
|
+
else None
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
raise handle_rpc_exception(e)
|
|
375
|
+
|
|
376
|
+
async def remove(
|
|
377
|
+
self,
|
|
378
|
+
path: str,
|
|
379
|
+
user: Optional[Username] = None,
|
|
380
|
+
request_timeout: Optional[float] = None,
|
|
381
|
+
) -> None:
|
|
382
|
+
"""
|
|
383
|
+
Remove a file or a directory.
|
|
384
|
+
|
|
385
|
+
:param path: Path to a file or a directory
|
|
386
|
+
:param user: Run the operation as this user
|
|
387
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
await self._rpc.aremove(
|
|
391
|
+
filesystem_pb2.RemoveRequest(path=path),
|
|
392
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
393
|
+
request_timeout
|
|
394
|
+
),
|
|
395
|
+
headers=authentication_header(self._envd_version, user),
|
|
396
|
+
)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
raise handle_rpc_exception(e)
|
|
399
|
+
|
|
400
|
+
async def rename(
|
|
401
|
+
self,
|
|
402
|
+
old_path: str,
|
|
403
|
+
new_path: str,
|
|
404
|
+
user: Optional[Username] = None,
|
|
405
|
+
request_timeout: Optional[float] = None,
|
|
406
|
+
) -> EntryInfo:
|
|
407
|
+
"""
|
|
408
|
+
Rename a file or directory.
|
|
409
|
+
|
|
410
|
+
:param old_path: Path to the file or directory to rename
|
|
411
|
+
:param new_path: New path to the file or directory
|
|
412
|
+
:param user: Run the operation as this user
|
|
413
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
414
|
+
|
|
415
|
+
:return: Information about the renamed file or directory
|
|
416
|
+
"""
|
|
417
|
+
try:
|
|
418
|
+
r = await self._rpc.amove(
|
|
419
|
+
filesystem_pb2.MoveRequest(
|
|
420
|
+
source=old_path,
|
|
421
|
+
destination=new_path,
|
|
422
|
+
),
|
|
423
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
424
|
+
request_timeout
|
|
425
|
+
),
|
|
426
|
+
headers=authentication_header(self._envd_version, user),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return EntryInfo(
|
|
430
|
+
name=r.entry.name,
|
|
431
|
+
type=map_file_type(r.entry.type),
|
|
432
|
+
path=r.entry.path,
|
|
433
|
+
size=r.entry.size,
|
|
434
|
+
mode=r.entry.mode,
|
|
435
|
+
permissions=r.entry.permissions,
|
|
436
|
+
owner=r.entry.owner,
|
|
437
|
+
group=r.entry.group,
|
|
438
|
+
modified_time=r.entry.modified_time.ToDatetime(),
|
|
439
|
+
# Optional, we can't directly access symlink_target otherwise if will be "" instead of None
|
|
440
|
+
symlink_target=(
|
|
441
|
+
r.entry.symlink_target
|
|
442
|
+
if r.entry.HasField("symlink_target")
|
|
443
|
+
else None
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
except Exception as e:
|
|
447
|
+
raise handle_rpc_exception(e)
|
|
448
|
+
|
|
449
|
+
async def make_dir(
|
|
450
|
+
self,
|
|
451
|
+
path: str,
|
|
452
|
+
user: Optional[Username] = None,
|
|
453
|
+
request_timeout: Optional[float] = None,
|
|
454
|
+
) -> bool:
|
|
455
|
+
"""
|
|
456
|
+
Create a new directory and all directories along the way if needed on the specified path.
|
|
457
|
+
|
|
458
|
+
:param path: Path to a new directory. For example '/dirA/dirB' when creating 'dirB'.
|
|
459
|
+
:param user: Run the operation as this user
|
|
460
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
461
|
+
|
|
462
|
+
:return: `True` if the directory was created, `False` if the directory already exists
|
|
463
|
+
"""
|
|
464
|
+
try:
|
|
465
|
+
await self._rpc.amake_dir(
|
|
466
|
+
filesystem_pb2.MakeDirRequest(path=path),
|
|
467
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
468
|
+
request_timeout
|
|
469
|
+
),
|
|
470
|
+
headers=authentication_header(self._envd_version, user),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return True
|
|
474
|
+
except Exception as e:
|
|
475
|
+
if isinstance(e, connect.ConnectException):
|
|
476
|
+
if e.status == connect.Code.already_exists:
|
|
477
|
+
return False
|
|
478
|
+
raise handle_rpc_exception(e)
|
|
479
|
+
|
|
480
|
+
async def watch_dir(
|
|
481
|
+
self,
|
|
482
|
+
path: str,
|
|
483
|
+
on_event: OutputHandler[FilesystemEvent],
|
|
484
|
+
on_exit: Optional[OutputHandler[Exception]] = None,
|
|
485
|
+
user: Optional[Username] = None,
|
|
486
|
+
request_timeout: Optional[float] = None,
|
|
487
|
+
timeout: Optional[float] = 60,
|
|
488
|
+
recursive: bool = False,
|
|
489
|
+
) -> AsyncWatchHandle:
|
|
490
|
+
"""
|
|
491
|
+
Watch directory for filesystem events.
|
|
492
|
+
|
|
493
|
+
:param path: Path to a directory to watch
|
|
494
|
+
:param on_event: Callback to call on each event in the directory
|
|
495
|
+
:param on_exit: Callback to call when the watching ends
|
|
496
|
+
:param user: Run the operation as this user
|
|
497
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
498
|
+
:param timeout: Timeout for the watch operation in **seconds**. Using `0` will not limit the watch time
|
|
499
|
+
:param recursive: Watch directory recursively
|
|
500
|
+
|
|
501
|
+
:return: `AsyncWatchHandle` object for stopping watching directory
|
|
502
|
+
"""
|
|
503
|
+
if recursive and self._envd_version < ENVD_VERSION_RECURSIVE_WATCH:
|
|
504
|
+
raise TemplateException(
|
|
505
|
+
"You need to update the template to use recursive watching. "
|
|
506
|
+
"You can do this by running `moru template build` in the directory with the template."
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
events = self._rpc.awatch_dir(
|
|
510
|
+
filesystem_pb2.WatchDirRequest(path=path, recursive=recursive),
|
|
511
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
512
|
+
request_timeout
|
|
513
|
+
),
|
|
514
|
+
timeout=timeout,
|
|
515
|
+
headers={
|
|
516
|
+
**authentication_header(self._envd_version, user),
|
|
517
|
+
KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
start_event = await events.__anext__()
|
|
523
|
+
|
|
524
|
+
if not start_event.HasField("start"):
|
|
525
|
+
raise SandboxException(
|
|
526
|
+
f"Failed to start watch: expected start event, got {start_event}",
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return AsyncWatchHandle(events=events, on_event=on_event, on_exit=on_exit)
|
|
530
|
+
except Exception as e:
|
|
531
|
+
raise handle_rpc_exception(e)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from typing import Any, AsyncGenerator, Optional
|
|
5
|
+
|
|
6
|
+
from moru.envd.rpc import handle_rpc_exception
|
|
7
|
+
from moru.envd.filesystem.filesystem_pb2 import WatchDirResponse
|
|
8
|
+
from moru.sandbox.filesystem.watch_handle import FilesystemEvent, map_event_type
|
|
9
|
+
from moru.sandbox_async.utils import OutputHandler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncWatchHandle:
|
|
13
|
+
"""
|
|
14
|
+
Handle for watching a directory in the sandbox filesystem.
|
|
15
|
+
|
|
16
|
+
Use `.stop()` to stop watching the directory.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
events: AsyncGenerator[WatchDirResponse, Any],
|
|
22
|
+
on_event: OutputHandler[FilesystemEvent],
|
|
23
|
+
on_exit: Optional[OutputHandler[Exception]] = None,
|
|
24
|
+
):
|
|
25
|
+
self._events = events
|
|
26
|
+
self._on_event = on_event
|
|
27
|
+
self._on_exit = on_exit
|
|
28
|
+
|
|
29
|
+
self._wait = asyncio.create_task(self._handle_events())
|
|
30
|
+
|
|
31
|
+
async def stop(self):
|
|
32
|
+
"""
|
|
33
|
+
Stop watching the directory.
|
|
34
|
+
"""
|
|
35
|
+
self._wait.cancel()
|
|
36
|
+
# BUG: In Python 3.8 closing async generator can throw RuntimeError.
|
|
37
|
+
# await self._events.aclose()
|
|
38
|
+
|
|
39
|
+
async def _iterate_events(self):
|
|
40
|
+
try:
|
|
41
|
+
async for event in self._events:
|
|
42
|
+
if event.HasField("filesystem"):
|
|
43
|
+
event_type = map_event_type(event.filesystem.type)
|
|
44
|
+
if event_type:
|
|
45
|
+
yield FilesystemEvent(
|
|
46
|
+
name=event.filesystem.name,
|
|
47
|
+
type=event_type,
|
|
48
|
+
)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise handle_rpc_exception(e)
|
|
51
|
+
|
|
52
|
+
async def _handle_events(self):
|
|
53
|
+
try:
|
|
54
|
+
async for event in self._iterate_events():
|
|
55
|
+
cb = self._on_event(event)
|
|
56
|
+
if inspect.isawaitable(cb):
|
|
57
|
+
await cb
|
|
58
|
+
except Exception as e:
|
|
59
|
+
if self._on_exit:
|
|
60
|
+
cb = self._on_exit(e)
|
|
61
|
+
if inspect.isawaitable(cb):
|
|
62
|
+
await cb
|