scalebox-sdk 0.1.4__py3-none-any.whl → 0.1.25__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 (68) hide show
  1. scalebox/__init__.py +1 -1
  2. scalebox/api/__init__.py +128 -128
  3. scalebox/api/client/__init__.py +8 -8
  4. scalebox/api/client/api/sandboxes/get_sandboxes.py +5 -3
  5. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +2 -2
  6. scalebox/api/client/api/sandboxes/post_sandboxes.py +2 -2
  7. scalebox/api/client/client.py +288 -288
  8. scalebox/api/client/models/listed_sandbox.py +11 -9
  9. scalebox/api/client/models/new_sandbox.py +1 -1
  10. scalebox/api/client/models/sandbox.py +125 -125
  11. scalebox/api/client/models/sandbox_state.py +1 -0
  12. scalebox/api/client/types.py +46 -46
  13. scalebox/code_interpreter/code_interpreter_async.py +370 -369
  14. scalebox/code_interpreter/code_interpreter_sync.py +318 -317
  15. scalebox/connection_config.py +92 -92
  16. scalebox/csx_desktop/main.py +12 -12
  17. scalebox/generated/api_pb2_connect.py +17 -66
  18. scalebox/sandbox_async/commands/command.py +307 -307
  19. scalebox/sandbox_async/commands/command_handle.py +187 -187
  20. scalebox/sandbox_async/commands/pty.py +187 -187
  21. scalebox/sandbox_async/filesystem/filesystem.py +557 -557
  22. scalebox/sandbox_async/filesystem/watch_handle.py +61 -61
  23. scalebox/sandbox_async/main.py +647 -646
  24. scalebox/sandbox_async/sandbox_api.py +365 -365
  25. scalebox/sandbox_async/utils.py +7 -7
  26. scalebox/sandbox_sync/__init__.py +2 -2
  27. scalebox/sandbox_sync/commands/command.py +300 -300
  28. scalebox/sandbox_sync/commands/command_handle.py +150 -150
  29. scalebox/sandbox_sync/commands/pty.py +181 -181
  30. scalebox/sandbox_sync/filesystem/filesystem.py +543 -543
  31. scalebox/sandbox_sync/filesystem/watch_handle.py +66 -66
  32. scalebox/sandbox_sync/main.py +789 -790
  33. scalebox/sandbox_sync/sandbox_api.py +356 -356
  34. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +256 -256
  35. scalebox/test/README.md +164 -164
  36. scalebox/test/aclient.py +72 -72
  37. scalebox/test/code_interpreter_centext.py +21 -21
  38. scalebox/test/code_interpreter_centext_sync.py +21 -21
  39. scalebox/test/code_interpreter_test.py +1 -1
  40. scalebox/test/code_interpreter_test_sync.py +1 -1
  41. scalebox/test/run_all_validation_tests.py +334 -334
  42. scalebox/test/test_basic.py +78 -78
  43. scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -2653
  44. scalebox/test/{test_code_interpreter_e2bsync_comprehensive.py → test_code_interpreter_execcode.py} +328 -392
  45. scalebox/test/test_code_interpreter_sync_comprehensive.py +3416 -3412
  46. scalebox/test/test_csx_desktop_examples.py +130 -0
  47. scalebox/test/test_sandbox_async_comprehensive.py +736 -738
  48. scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -778
  49. scalebox/test/test_sandbox_sync_comprehensive.py +779 -770
  50. scalebox/test/test_sandbox_usage_examples.py +987 -987
  51. scalebox/test/testacreate.py +24 -24
  52. scalebox/test/testagetinfo.py +18 -18
  53. scalebox/test/testcodeinterpreter_async.py +508 -508
  54. scalebox/test/testcodeinterpreter_sync.py +239 -239
  55. scalebox/test/testcomputeuse.py +2 -2
  56. scalebox/test/testnovnc.py +12 -12
  57. scalebox/test/testsandbox_api.py +15 -0
  58. scalebox/test/testsandbox_async.py +202 -118
  59. scalebox/test/testsandbox_sync.py +71 -38
  60. scalebox/version.py +2 -2
  61. {scalebox_sdk-0.1.4.dist-info → scalebox_sdk-0.1.25.dist-info}/METADATA +104 -103
  62. {scalebox_sdk-0.1.4.dist-info → scalebox_sdk-0.1.25.dist-info}/RECORD +66 -66
  63. scalebox/test/test_code_interpreter_e2basync_comprehensive.py +0 -2655
  64. scalebox/test/test_e2b_first.py +0 -11
  65. {scalebox_sdk-0.1.4.dist-info → scalebox_sdk-0.1.25.dist-info}/WHEEL +0 -0
  66. {scalebox_sdk-0.1.4.dist-info → scalebox_sdk-0.1.25.dist-info}/entry_points.txt +0 -0
  67. {scalebox_sdk-0.1.4.dist-info → scalebox_sdk-0.1.25.dist-info}/licenses/LICENSE +0 -0
  68. {scalebox_sdk-0.1.4.dist-info → scalebox_sdk-0.1.25.dist-info}/top_level.txt +0 -0
@@ -1,557 +1,557 @@
1
- from io import IOBase
2
- from typing import IO, AsyncIterator, List, Literal, Optional, Union, overload
3
-
4
- import aiohttp
5
- import httpcore
6
- import httpx
7
- from packaging.version import Version
8
-
9
- from ... import csx_connect as connect
10
- from ...connection_config import (
11
- KEEPALIVE_PING_HEADER,
12
- KEEPALIVE_PING_INTERVAL_SEC,
13
- ConnectionConfig,
14
- Username,
15
- )
16
- from ...exceptions import InvalidArgumentException, SandboxException, TemplateException
17
- from ...generated import api_pb2, api_pb2_connect
18
- from ...generated.api import ENVD_API_FILES_ROUTE, ahandle_envd_api_exception
19
- from ...generated.rpc import authentication_header, handle_rpc_exception
20
- from ...generated.versions import ENVD_VERSION_RECURSIVE_WATCH
21
- from ...sandbox.filesystem.filesystem import (
22
- EntryInfo,
23
- FileType,
24
- WriteEntry,
25
- WriteInfo,
26
- map_file_type,
27
- )
28
- from ...sandbox.filesystem.watch_handle import FilesystemEvent
29
- from ...sandbox_async.filesystem.watch_handle import AsyncWatchHandle
30
- from ...sandbox_async.utils import OutputHandler
31
-
32
-
33
- class Filesystem:
34
- """
35
- Module for interacting with the filesystem in the sandbox.
36
- """
37
-
38
- def __init__(
39
- self,
40
- envd_api_url: str,
41
- envd_version: Optional[str],
42
- connection_config: ConnectionConfig,
43
- pool: aiohttp.ClientSession,
44
- envd_api: httpx.AsyncClient,
45
- ) -> None:
46
- self._envd_api_url = envd_api_url
47
- self._envd_version = envd_version
48
- self._connection_config = connection_config
49
- self._pool = pool
50
- self._envd_api = envd_api
51
-
52
- self._rpc = api_pb2_connect.AsyncFilesystemClient(
53
- envd_api_url,
54
- http_client=pool,
55
- )
56
- self._headers = self._connection_config.headers
57
-
58
- @overload
59
- async def read(
60
- self,
61
- path: str,
62
- format: Literal["text"] = "text",
63
- user: Username = "user",
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: Username = "user",
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: Username = "user",
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: Username = "user",
123
- request_timeout: Optional[float] = None,
124
- ):
125
- # Use the /download/ endpoint from sandboxagent.go
126
- download_url = f"/download/{path.lstrip('/')}"
127
-
128
- r = await self._envd_api.get(
129
- download_url,
130
- timeout=self._connection_config.get_request_timeout(request_timeout),
131
- )
132
-
133
- err = await ahandle_envd_api_exception(r)
134
- if err:
135
- raise err
136
-
137
- if format == "text":
138
- return r.text
139
- elif format == "bytes":
140
- return bytearray(r.content)
141
- elif format == "stream":
142
- return r.aiter_bytes()
143
-
144
- @overload
145
- async def write(
146
- self,
147
- path: str,
148
- data: Union[str, bytes, IO],
149
- user: Username = "user",
150
- request_timeout: Optional[float] = None,
151
- ) -> WriteInfo:
152
- """
153
- Write content to a file on the path.
154
-
155
- Writing to a file that doesn't exist creates the file.
156
-
157
- Writing to a file that already exists overwrites the file.
158
-
159
- Writing to a file at path that doesn't exist creates the necessary directories.
160
-
161
- :param path: Path to the file
162
- :param data: Data to write to the file, can be a `str`, `bytes`, or `IO`.
163
- :param user: Run the operation as this user
164
- :param request_timeout: Timeout for the request in **seconds**
165
-
166
- :return: Information about the written file
167
- """
168
-
169
- @overload
170
- async def write(
171
- self,
172
- files: List[WriteEntry],
173
- user: Optional[Username] = "user",
174
- request_timeout: Optional[float] = None,
175
- ) -> List[WriteInfo]:
176
- """
177
- Writes multiple files.
178
-
179
- :param files: list of files to write
180
- :param user: Run the operation as this user
181
- :param request_timeout: Timeout for the request
182
- :return: Information about the written files
183
- """
184
-
185
- async def write(
186
- self,
187
- path_or_files: Union[str, List[WriteEntry]],
188
- data_or_user: Union[str, bytes, IO, Username] = "user",
189
- user_or_request_timeout: Optional[Union[float, Username]] = None,
190
- request_timeout_or_none: Optional[float] = None,
191
- ) -> Union[WriteInfo, List[WriteInfo]]:
192
- """
193
- Writes content to a file on the path.
194
- When writing to a file that doesn't exist, the file will get created.
195
- When writing to a file that already exists, the file will get overwritten.
196
- When writing to a file that's in a directory that doesn't exist, you'll get an error.
197
- """
198
- path, write_files, user, request_timeout = None, [], "user", None
199
- if isinstance(path_or_files, str):
200
- if isinstance(data_or_user, list):
201
- raise Exception(
202
- "Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files."
203
- )
204
- path, write_files, user, request_timeout = (
205
- path_or_files,
206
- [{"path": path_or_files, "data": data_or_user}],
207
- user_or_request_timeout or "user",
208
- request_timeout_or_none,
209
- )
210
- else:
211
- if path_or_files is None:
212
- raise Exception("Path or files are required")
213
- path, write_files, user, request_timeout = (
214
- None,
215
- path_or_files,
216
- data_or_user,
217
- user_or_request_timeout,
218
- )
219
-
220
- # Allow passing empty list of files
221
- if len(write_files) == 0:
222
- return []
223
-
224
- # Use the /upload endpoint from sandboxagent.go
225
- # This endpoint expects multipart/form-data with 'file' field and 'path' form field
226
- results = []
227
- for file in write_files:
228
- file_path, file_data = file["path"], file["data"]
229
-
230
- # Prepare file data
231
- if isinstance(file_data, str):
232
- file_content = file_data.encode('utf-8')
233
- elif isinstance(file_data, bytes):
234
- file_content = file_data
235
- elif isinstance(file_data, IOBase):
236
- file_content = file_data.read()
237
- if isinstance(file_content, str):
238
- file_content = file_content.encode('utf-8')
239
- else:
240
- raise ValueError(f"Unsupported data type for file {file_path}")
241
-
242
- # Prepare multipart form data
243
- files = [("file", (file_path, file_content))]
244
- data = {"path": file_path}
245
-
246
- r = await self._envd_api.post(
247
- "/upload",
248
- files=files,
249
- data=data,
250
- timeout=self._connection_config.get_request_timeout(request_timeout),
251
- )
252
-
253
- err = await ahandle_envd_api_exception(r)
254
- if err:
255
- raise err
256
-
257
- # For now, create a mock WriteInfo since sandboxagent.go returns plain text
258
- # In a real implementation, you might want to enhance the server to return JSON
259
- results.append(WriteInfo(
260
- path=file_path,
261
- name=file_path.split('/')[-1] if '/' in file_path else file_path,
262
- type=FileType.FILE
263
- ))
264
-
265
- # Return appropriate response based on input format
266
- if len(results) == 1 and path:
267
- return results[0]
268
- else:
269
- return results
270
-
271
- async def list(
272
- self,
273
- path: str,
274
- depth: Optional[int] = 1,
275
- user: Username = "user",
276
- request_timeout: Optional[float] = None,
277
- ) -> List[EntryInfo]:
278
- """
279
- List entries in a directory.
280
-
281
- :param path: Path to the directory
282
- :param depth: Depth of the directory to list
283
- :param user: Run the operation as this user
284
- :param request_timeout: Timeout for the request in **seconds**
285
-
286
- :return: List of entries in the directory
287
- """
288
- if depth is not None and depth < 1:
289
- raise InvalidArgumentException("depth should be at least 1")
290
-
291
- try:
292
- res = await self._rpc.list_dir(
293
- api_pb2.ListDirRequest(path=path, depth=depth),
294
- self._headers,
295
- timeout_seconds=self._connection_config.get_request_timeout(
296
- request_timeout
297
- ),
298
- )
299
-
300
- entries: List[EntryInfo] = []
301
- for entry in res.entries:
302
- event_type = map_file_type(entry.type)
303
-
304
- if event_type:
305
- entries.append(
306
- EntryInfo(
307
- name=entry.name,
308
- type=event_type,
309
- path=entry.path,
310
- size=entry.size,
311
- mode=entry.mode,
312
- permissions=entry.permissions,
313
- owner=entry.owner,
314
- group=entry.group,
315
- modified_time=entry.modified_time.ToDatetime(),
316
- # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
317
- symlink_target=(
318
- entry.symlink_target
319
- if entry.HasField("symlink_target")
320
- else None
321
- ),
322
- )
323
- )
324
-
325
- return entries
326
- except Exception as e:
327
- raise handle_rpc_exception(e)
328
-
329
- async def exists(
330
- self,
331
- path: str,
332
- user: Username = "user",
333
- request_timeout: Optional[float] = None,
334
- ) -> bool:
335
- """
336
- Check if a file or a directory exists.
337
-
338
- :param path: Path to a file or a directory
339
- :param user: Run the operation as this user
340
- :param request_timeout: Timeout for the request in **seconds**
341
-
342
- :return: `True` if the file or directory exists, `False` otherwise
343
- """
344
- try:
345
- await self._rpc.stat(
346
- api_pb2.StatRequest(path=path),
347
- self._headers,
348
- timeout_seconds=self._connection_config.get_request_timeout(
349
- request_timeout
350
- ),
351
- )
352
-
353
- return True
354
-
355
- except Exception as e:
356
- if "no such file or directory" in str(e):
357
- return False
358
- raise handle_rpc_exception(e)
359
-
360
- async def get_info(
361
- self,
362
- path: str,
363
- user: Username = "user",
364
- request_timeout: Optional[float] = None,
365
- ) -> EntryInfo:
366
- """
367
- Get information about a file or directory.
368
-
369
- :param path: Path to a file or a directory
370
- :param user: Run the operation as this user
371
- :param request_timeout: Timeout for the request in **seconds**
372
-
373
- :return: Information about the file or directory like name, type, and path
374
- """
375
- try:
376
- r = await self._rpc.stat(
377
- api_pb2.StatRequest(path=path),
378
- self._headers,
379
- timeout_seconds=self._connection_config.get_request_timeout(
380
- request_timeout
381
- ),
382
- )
383
-
384
- return EntryInfo(
385
- name=r.entry.name,
386
- type=map_file_type(r.entry.type),
387
- path=r.entry.path,
388
- size=r.entry.size,
389
- mode=r.entry.mode,
390
- permissions=r.entry.permissions,
391
- owner=r.entry.owner,
392
- group=r.entry.group,
393
- modified_time=r.entry.modified_time.ToDatetime(),
394
- symlink_target=(
395
- r.entry.symlink_target
396
- if r.entry.HasField("symlink_target")
397
- else None
398
- ),
399
- )
400
- except Exception as e:
401
- raise handle_rpc_exception(e)
402
-
403
- async def remove(
404
- self,
405
- path: str,
406
- user: Username = "user",
407
- request_timeout: Optional[float] = None,
408
- ) -> None:
409
- """
410
- Remove a file or a directory.
411
-
412
- :param path: Path to a file or a directory
413
- :param user: Run the operation as this user
414
- :param request_timeout: Timeout for the request in **seconds**
415
- """
416
- try:
417
- await self._rpc.remove(
418
- api_pb2.RemoveRequest(path=path),
419
- self._headers,
420
- timeout_seconds=self._connection_config.get_request_timeout(
421
- request_timeout
422
- ),
423
- )
424
- except Exception as e:
425
- raise handle_rpc_exception(e)
426
-
427
- async def rename(
428
- self,
429
- old_path: str,
430
- new_path: str,
431
- user: Username = "user",
432
- request_timeout: Optional[float] = None,
433
- ) -> EntryInfo:
434
- """
435
- Rename a file or directory.
436
-
437
- :param old_path: Path to the file or directory to rename
438
- :param new_path: New path to the file or directory
439
- :param user: Run the operation as this user
440
- :param request_timeout: Timeout for the request in **seconds**
441
-
442
- :return: Information about the renamed file or directory
443
- """
444
- try:
445
- r = await self._rpc.move(
446
- api_pb2.MoveRequest(
447
- source=old_path,
448
- destination=new_path,
449
- ),
450
- self._headers,
451
- timeout_seconds=self._connection_config.get_request_timeout(
452
- request_timeout
453
- ),
454
- )
455
-
456
- return EntryInfo(
457
- name=r.entry.name,
458
- type=map_file_type(r.entry.type),
459
- path=r.entry.path,
460
- size=r.entry.size,
461
- mode=r.entry.mode,
462
- permissions=r.entry.permissions,
463
- owner=r.entry.owner,
464
- group=r.entry.group,
465
- modified_time=r.entry.modified_time.ToDatetime(),
466
- # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
467
- symlink_target=(
468
- r.entry.symlink_target
469
- if r.entry.HasField("symlink_target")
470
- else None
471
- ),
472
- )
473
- except Exception as e:
474
- raise handle_rpc_exception(e)
475
-
476
- async def make_dir(
477
- self,
478
- path: str,
479
- user: Username = "user",
480
- request_timeout: Optional[float] = None,
481
- ) -> bool:
482
- """
483
- Create a new directory and all directories along the way if needed on the specified path.
484
-
485
- :param path: Path to a new directory. For example '/dirA/dirB' when creating 'dirB'.
486
- :param user: Run the operation as this user
487
- :param request_timeout: Timeout for the request in **seconds**
488
-
489
- :return: `True` if the directory was created, `False` if the directory already exists
490
- """
491
- try:
492
- await self._rpc.make_dir(
493
- api_pb2.MakeDirRequest(path=path),
494
- self._headers,
495
- timeout_seconds=self._connection_config.get_request_timeout(
496
- request_timeout
497
- ),
498
- )
499
-
500
- return True
501
- except Exception as e:
502
- if "directory already exists" in str(e):
503
- return False
504
- raise handle_rpc_exception(e)
505
-
506
- async def watch_dir(
507
- self,
508
- path: str,
509
- on_event: OutputHandler[FilesystemEvent],
510
- on_exit: Optional[OutputHandler[Exception]] = None,
511
- user: Username = "user",
512
- request_timeout: Optional[float] = None,
513
- timeout: Optional[float] = 60,
514
- recursive: bool = False,
515
- ) -> AsyncWatchHandle:
516
- """
517
- Watch directory for filesystem events.
518
-
519
- :param path: Path to a directory to watch
520
- :param on_event: Callback to call on each event in the directory
521
- :param on_exit: Callback to call when the watching ends
522
- :param user: Run the operation as this user
523
- :param request_timeout: Timeout for the request in **seconds**
524
- :param timeout: Timeout for the watch operation in **seconds**. Using `0` will not limit the watch time
525
- :param recursive: Watch directory recursively
526
-
527
- :return: `AsyncWatchHandle` object for stopping watching directory
528
- """
529
- if (
530
- recursive
531
- and self._envd_version is not None
532
- and Version(self._envd_version) < ENVD_VERSION_RECURSIVE_WATCH
533
- ):
534
- raise TemplateException(
535
- "You need to update the template to use recursive watching. "
536
- "You can do this by running `e2b template build` in the directory with the template."
537
- )
538
-
539
- events = self._rpc.watch_dir(
540
- api_pb2.WatchDirRequest(path=path, recursive=recursive),
541
- self._headers,
542
- timeout_seconds=self._connection_config.get_request_timeout(
543
- request_timeout
544
- ),
545
- )
546
-
547
- try:
548
- start_event = await events.__anext__()
549
-
550
- if not start_event.HasField("start"):
551
- raise SandboxException(
552
- f"Failed to start watch: expected start event, got {start_event}",
553
- )
554
-
555
- return AsyncWatchHandle(events=events, on_event=on_event, on_exit=on_exit)
556
- except Exception as e:
557
- raise handle_rpc_exception(e)
1
+ from io import IOBase
2
+ from typing import IO, AsyncIterator, List, Literal, Optional, Union, overload
3
+
4
+ import aiohttp
5
+ import httpcore
6
+ import httpx
7
+ from packaging.version import Version
8
+
9
+ from ... import csx_connect as connect
10
+ from ...connection_config import (
11
+ KEEPALIVE_PING_HEADER,
12
+ KEEPALIVE_PING_INTERVAL_SEC,
13
+ ConnectionConfig,
14
+ Username,
15
+ )
16
+ from ...exceptions import InvalidArgumentException, SandboxException, TemplateException
17
+ from ...generated import api_pb2, api_pb2_connect
18
+ from ...generated.api import ENVD_API_FILES_ROUTE, ahandle_envd_api_exception
19
+ from ...generated.rpc import authentication_header, handle_rpc_exception
20
+ from ...generated.versions import ENVD_VERSION_RECURSIVE_WATCH
21
+ from ...sandbox.filesystem.filesystem import (
22
+ EntryInfo,
23
+ FileType,
24
+ WriteEntry,
25
+ WriteInfo,
26
+ map_file_type,
27
+ )
28
+ from ...sandbox.filesystem.watch_handle import FilesystemEvent
29
+ from ...sandbox_async.filesystem.watch_handle import AsyncWatchHandle
30
+ from ...sandbox_async.utils import OutputHandler
31
+
32
+
33
+ class Filesystem:
34
+ """
35
+ Module for interacting with the filesystem in the sandbox.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ envd_api_url: str,
41
+ envd_version: Optional[str],
42
+ connection_config: ConnectionConfig,
43
+ pool: aiohttp.ClientSession,
44
+ envd_api: httpx.AsyncClient,
45
+ ) -> None:
46
+ self._envd_api_url = envd_api_url
47
+ self._envd_version = envd_version
48
+ self._connection_config = connection_config
49
+ self._pool = pool
50
+ self._envd_api = envd_api
51
+
52
+ self._rpc = api_pb2_connect.AsyncFilesystemClient(
53
+ envd_api_url,
54
+ http_client=pool,
55
+ )
56
+ self._headers = self._connection_config.headers
57
+
58
+ @overload
59
+ async def read(
60
+ self,
61
+ path: str,
62
+ format: Literal["text"] = "text",
63
+ user: Username = "user",
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: Username = "user",
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: Username = "user",
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: Username = "user",
123
+ request_timeout: Optional[float] = None,
124
+ ):
125
+ # Use the /download/ endpoint from sandboxagent.go
126
+ download_url = f"/download/{path.lstrip('/')}"
127
+
128
+ r = await self._envd_api.get(
129
+ download_url,
130
+ timeout=self._connection_config.get_request_timeout(request_timeout),
131
+ )
132
+
133
+ err = await ahandle_envd_api_exception(r)
134
+ if err:
135
+ raise err
136
+
137
+ if format == "text":
138
+ return r.text
139
+ elif format == "bytes":
140
+ return bytearray(r.content)
141
+ elif format == "stream":
142
+ return r.aiter_bytes()
143
+
144
+ @overload
145
+ async def write(
146
+ self,
147
+ path: str,
148
+ data: Union[str, bytes, IO],
149
+ user: Username = "user",
150
+ request_timeout: Optional[float] = None,
151
+ ) -> WriteInfo:
152
+ """
153
+ Write content to a file on the path.
154
+
155
+ Writing to a file that doesn't exist creates the file.
156
+
157
+ Writing to a file that already exists overwrites the file.
158
+
159
+ Writing to a file at path that doesn't exist creates the necessary directories.
160
+
161
+ :param path: Path to the file
162
+ :param data: Data to write to the file, can be a `str`, `bytes`, or `IO`.
163
+ :param user: Run the operation as this user
164
+ :param request_timeout: Timeout for the request in **seconds**
165
+
166
+ :return: Information about the written file
167
+ """
168
+
169
+ @overload
170
+ async def write(
171
+ self,
172
+ files: List[WriteEntry],
173
+ user: Optional[Username] = "user",
174
+ request_timeout: Optional[float] = None,
175
+ ) -> List[WriteInfo]:
176
+ """
177
+ Writes multiple files.
178
+
179
+ :param files: list of files to write
180
+ :param user: Run the operation as this user
181
+ :param request_timeout: Timeout for the request
182
+ :return: Information about the written files
183
+ """
184
+
185
+ async def write(
186
+ self,
187
+ path_or_files: Union[str, List[WriteEntry]],
188
+ data_or_user: Union[str, bytes, IO, Username] = "user",
189
+ user_or_request_timeout: Optional[Union[float, Username]] = None,
190
+ request_timeout_or_none: Optional[float] = None,
191
+ ) -> Union[WriteInfo, List[WriteInfo]]:
192
+ """
193
+ Writes content to a file on the path.
194
+ When writing to a file that doesn't exist, the file will get created.
195
+ When writing to a file that already exists, the file will get overwritten.
196
+ When writing to a file that's in a directory that doesn't exist, you'll get an error.
197
+ """
198
+ path, write_files, user, request_timeout = None, [], "user", None
199
+ if isinstance(path_or_files, str):
200
+ if isinstance(data_or_user, list):
201
+ raise Exception(
202
+ "Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files."
203
+ )
204
+ path, write_files, user, request_timeout = (
205
+ path_or_files,
206
+ [{"path": path_or_files, "data": data_or_user}],
207
+ user_or_request_timeout or "user",
208
+ request_timeout_or_none,
209
+ )
210
+ else:
211
+ if path_or_files is None:
212
+ raise Exception("Path or files are required")
213
+ path, write_files, user, request_timeout = (
214
+ None,
215
+ path_or_files,
216
+ data_or_user,
217
+ user_or_request_timeout,
218
+ )
219
+
220
+ # Allow passing empty list of files
221
+ if len(write_files) == 0:
222
+ return []
223
+
224
+ # Use the /upload endpoint from sandboxagent.go
225
+ # This endpoint expects multipart/form-data with 'file' field and 'path' form field
226
+ results = []
227
+ for file in write_files:
228
+ file_path, file_data = file["path"], file["data"]
229
+
230
+ # Prepare file data
231
+ if isinstance(file_data, str):
232
+ file_content = file_data.encode('utf-8')
233
+ elif isinstance(file_data, bytes):
234
+ file_content = file_data
235
+ elif isinstance(file_data, IOBase):
236
+ file_content = file_data.read()
237
+ if isinstance(file_content, str):
238
+ file_content = file_content.encode('utf-8')
239
+ else:
240
+ raise ValueError(f"Unsupported data type for file {file_path}")
241
+
242
+ # Prepare multipart form data
243
+ files = [("file", (file_path, file_content))]
244
+ data = {"path": file_path}
245
+
246
+ r = await self._envd_api.post(
247
+ "/upload",
248
+ files=files,
249
+ data=data,
250
+ timeout=self._connection_config.get_request_timeout(request_timeout),
251
+ )
252
+
253
+ err = await ahandle_envd_api_exception(r)
254
+ if err:
255
+ raise err
256
+
257
+ # For now, create a mock WriteInfo since sandboxagent.go returns plain text
258
+ # In a real implementation, you might want to enhance the server to return JSON
259
+ results.append(WriteInfo(
260
+ path=file_path,
261
+ name=file_path.split('/')[-1] if '/' in file_path else file_path,
262
+ type=FileType.FILE
263
+ ))
264
+
265
+ # Return appropriate response based on input format
266
+ if len(results) == 1 and path:
267
+ return results[0]
268
+ else:
269
+ return results
270
+
271
+ async def list(
272
+ self,
273
+ path: str,
274
+ depth: Optional[int] = 1,
275
+ user: Username = "user",
276
+ request_timeout: Optional[float] = None,
277
+ ) -> List[EntryInfo]:
278
+ """
279
+ List entries in a directory.
280
+
281
+ :param path: Path to the directory
282
+ :param depth: Depth of the directory to list
283
+ :param user: Run the operation as this user
284
+ :param request_timeout: Timeout for the request in **seconds**
285
+
286
+ :return: List of entries in the directory
287
+ """
288
+ if depth is not None and depth < 1:
289
+ raise InvalidArgumentException("depth should be at least 1")
290
+
291
+ try:
292
+ res = await self._rpc.list_dir(
293
+ api_pb2.ListDirRequest(path=path, depth=depth),
294
+ self._headers,
295
+ timeout_seconds=self._connection_config.get_request_timeout(
296
+ request_timeout
297
+ ),
298
+ )
299
+
300
+ entries: List[EntryInfo] = []
301
+ for entry in res.entries:
302
+ event_type = map_file_type(entry.type)
303
+
304
+ if event_type:
305
+ entries.append(
306
+ EntryInfo(
307
+ name=entry.name,
308
+ type=event_type,
309
+ path=entry.path,
310
+ size=entry.size,
311
+ mode=entry.mode,
312
+ permissions=entry.permissions,
313
+ owner=entry.owner,
314
+ group=entry.group,
315
+ modified_time=entry.modified_time.ToDatetime(),
316
+ # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
317
+ symlink_target=(
318
+ entry.symlink_target
319
+ if entry.HasField("symlink_target")
320
+ else None
321
+ ),
322
+ )
323
+ )
324
+
325
+ return entries
326
+ except Exception as e:
327
+ raise handle_rpc_exception(e)
328
+
329
+ async def exists(
330
+ self,
331
+ path: str,
332
+ user: Username = "user",
333
+ request_timeout: Optional[float] = None,
334
+ ) -> bool:
335
+ """
336
+ Check if a file or a directory exists.
337
+
338
+ :param path: Path to a file or a directory
339
+ :param user: Run the operation as this user
340
+ :param request_timeout: Timeout for the request in **seconds**
341
+
342
+ :return: `True` if the file or directory exists, `False` otherwise
343
+ """
344
+ try:
345
+ await self._rpc.stat(
346
+ api_pb2.StatRequest(path=path),
347
+ self._headers,
348
+ timeout_seconds=self._connection_config.get_request_timeout(
349
+ request_timeout
350
+ ),
351
+ )
352
+
353
+ return True
354
+
355
+ except Exception as e:
356
+ if "no such file or directory" in str(e):
357
+ return False
358
+ raise handle_rpc_exception(e)
359
+
360
+ async def get_info(
361
+ self,
362
+ path: str,
363
+ user: Username = "user",
364
+ request_timeout: Optional[float] = None,
365
+ ) -> EntryInfo:
366
+ """
367
+ Get information about a file or directory.
368
+
369
+ :param path: Path to a file or a directory
370
+ :param user: Run the operation as this user
371
+ :param request_timeout: Timeout for the request in **seconds**
372
+
373
+ :return: Information about the file or directory like name, type, and path
374
+ """
375
+ try:
376
+ r = await self._rpc.stat(
377
+ api_pb2.StatRequest(path=path),
378
+ self._headers,
379
+ timeout_seconds=self._connection_config.get_request_timeout(
380
+ request_timeout
381
+ ),
382
+ )
383
+
384
+ return EntryInfo(
385
+ name=r.entry.name,
386
+ type=map_file_type(r.entry.type),
387
+ path=r.entry.path,
388
+ size=r.entry.size,
389
+ mode=r.entry.mode,
390
+ permissions=r.entry.permissions,
391
+ owner=r.entry.owner,
392
+ group=r.entry.group,
393
+ modified_time=r.entry.modified_time.ToDatetime(),
394
+ symlink_target=(
395
+ r.entry.symlink_target
396
+ if r.entry.HasField("symlink_target")
397
+ else None
398
+ ),
399
+ )
400
+ except Exception as e:
401
+ raise handle_rpc_exception(e)
402
+
403
+ async def remove(
404
+ self,
405
+ path: str,
406
+ user: Username = "user",
407
+ request_timeout: Optional[float] = None,
408
+ ) -> None:
409
+ """
410
+ Remove a file or a directory.
411
+
412
+ :param path: Path to a file or a directory
413
+ :param user: Run the operation as this user
414
+ :param request_timeout: Timeout for the request in **seconds**
415
+ """
416
+ try:
417
+ await self._rpc.remove(
418
+ api_pb2.RemoveRequest(path=path),
419
+ self._headers,
420
+ timeout_seconds=self._connection_config.get_request_timeout(
421
+ request_timeout
422
+ ),
423
+ )
424
+ except Exception as e:
425
+ raise handle_rpc_exception(e)
426
+
427
+ async def rename(
428
+ self,
429
+ old_path: str,
430
+ new_path: str,
431
+ user: Username = "user",
432
+ request_timeout: Optional[float] = None,
433
+ ) -> EntryInfo:
434
+ """
435
+ Rename a file or directory.
436
+
437
+ :param old_path: Path to the file or directory to rename
438
+ :param new_path: New path to the file or directory
439
+ :param user: Run the operation as this user
440
+ :param request_timeout: Timeout for the request in **seconds**
441
+
442
+ :return: Information about the renamed file or directory
443
+ """
444
+ try:
445
+ r = await self._rpc.move(
446
+ api_pb2.MoveRequest(
447
+ source=old_path,
448
+ destination=new_path,
449
+ ),
450
+ self._headers,
451
+ timeout_seconds=self._connection_config.get_request_timeout(
452
+ request_timeout
453
+ ),
454
+ )
455
+
456
+ return EntryInfo(
457
+ name=r.entry.name,
458
+ type=map_file_type(r.entry.type),
459
+ path=r.entry.path,
460
+ size=r.entry.size,
461
+ mode=r.entry.mode,
462
+ permissions=r.entry.permissions,
463
+ owner=r.entry.owner,
464
+ group=r.entry.group,
465
+ modified_time=r.entry.modified_time.ToDatetime(),
466
+ # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
467
+ symlink_target=(
468
+ r.entry.symlink_target
469
+ if r.entry.HasField("symlink_target")
470
+ else None
471
+ ),
472
+ )
473
+ except Exception as e:
474
+ raise handle_rpc_exception(e)
475
+
476
+ async def make_dir(
477
+ self,
478
+ path: str,
479
+ user: Username = "user",
480
+ request_timeout: Optional[float] = None,
481
+ ) -> bool:
482
+ """
483
+ Create a new directory and all directories along the way if needed on the specified path.
484
+
485
+ :param path: Path to a new directory. For example '/dirA/dirB' when creating 'dirB'.
486
+ :param user: Run the operation as this user
487
+ :param request_timeout: Timeout for the request in **seconds**
488
+
489
+ :return: `True` if the directory was created, `False` if the directory already exists
490
+ """
491
+ try:
492
+ await self._rpc.make_dir(
493
+ api_pb2.MakeDirRequest(path=path),
494
+ self._headers,
495
+ timeout_seconds=self._connection_config.get_request_timeout(
496
+ request_timeout
497
+ ),
498
+ )
499
+
500
+ return True
501
+ except Exception as e:
502
+ if "directory already exists" in str(e):
503
+ return False
504
+ raise handle_rpc_exception(e)
505
+
506
+ async def watch_dir(
507
+ self,
508
+ path: str,
509
+ on_event: OutputHandler[FilesystemEvent],
510
+ on_exit: Optional[OutputHandler[Exception]] = None,
511
+ user: Username = "user",
512
+ request_timeout: Optional[float] = None,
513
+ timeout: Optional[float] = 60,
514
+ recursive: bool = False,
515
+ ) -> AsyncWatchHandle:
516
+ """
517
+ Watch directory for filesystem events.
518
+
519
+ :param path: Path to a directory to watch
520
+ :param on_event: Callback to call on each event in the directory
521
+ :param on_exit: Callback to call when the watching ends
522
+ :param user: Run the operation as this user
523
+ :param request_timeout: Timeout for the request in **seconds**
524
+ :param timeout: Timeout for the watch operation in **seconds**. Using `0` will not limit the watch time
525
+ :param recursive: Watch directory recursively
526
+
527
+ :return: `AsyncWatchHandle` object for stopping watching directory
528
+ """
529
+ if (
530
+ recursive
531
+ and self._envd_version is not None
532
+ and Version(self._envd_version) < ENVD_VERSION_RECURSIVE_WATCH
533
+ ):
534
+ raise TemplateException(
535
+ "You need to update the template to use recursive watching. "
536
+ "You can do this by running `scalebox template build` in the directory with the template."
537
+ )
538
+
539
+ events = self._rpc.watch_dir(
540
+ api_pb2.WatchDirRequest(path=path, recursive=recursive),
541
+ self._headers,
542
+ timeout_seconds=self._connection_config.get_request_timeout(
543
+ request_timeout
544
+ ),
545
+ )
546
+
547
+ try:
548
+ start_event = await events.__anext__()
549
+
550
+ if not start_event.HasField("start"):
551
+ raise SandboxException(
552
+ f"Failed to start watch: expected start event, got {start_event}",
553
+ )
554
+
555
+ return AsyncWatchHandle(events=events, on_event=on_event, on_exit=on_exit)
556
+ except Exception as e:
557
+ raise handle_rpc_exception(e)