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