scalebox-sdk 0.1.22__py3-none-any.whl → 0.1.24__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.
scalebox/__init__.py CHANGED
@@ -9,7 +9,7 @@ A multi-language code execution sandbox with support for:
9
9
  - Real-time callbacks and monitoring
10
10
  """
11
11
 
12
- __version__ = "0.1.22"
12
+ __version__ = "0.1.24"
13
13
  __author__ = "ScaleBox Team"
14
14
  __email__ = "dev@scalebox.dev"
15
15
 
@@ -31,7 +31,7 @@ class NewSandbox:
31
31
  timeout: Union[Unset, int] = 15
32
32
  additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
33
33
  is_async: Union[Unset, bool] = False
34
- storage_gb: Union[Unset, int] = 10
34
+ storage_gb: Union[Unset, int] = UNSET
35
35
 
36
36
  def to_dict(self) -> dict[str, Any]:
37
37
  template_id = self.template_id
@@ -1,543 +1,543 @@
1
- from io import IOBase
2
- from typing import IO, Iterator, List, Literal, Optional, Union, overload
3
-
4
- import httpcore
5
- import httpx
6
- import urllib3
7
- from packaging.version import Version
8
-
9
- from ... import csx_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, TemplateException
17
- from ...generated import api_pb2, api_pb2_connect
18
- from ...generated.api import (
19
- ENVD_API_DOWNLOAD_FILES_ROUTE,
20
- ENVD_API_UPLOAD_FILES_ROUTE,
21
- handle_envd_api_exception,
22
- )
23
- from ...generated.rpc import authentication_header, handle_rpc_exception
24
- from ...generated.versions import ENVD_VERSION_RECURSIVE_WATCH
25
- from ...sandbox.filesystem.filesystem import (
26
- EntryInfo,
27
- FileType,
28
- WriteEntry,
29
- WriteInfo,
30
- map_file_type,
31
- )
32
- from ...sandbox_sync.filesystem.watch_handle import WatchHandle
33
-
34
-
35
- class Filesystem:
36
- """
37
- Module for interacting with the filesystem in the sandbox.
38
- """
39
-
40
- def __init__(
41
- self,
42
- envd_api_url: str,
43
- envd_version: Optional[str],
44
- connection_config: ConnectionConfig,
45
- pool: urllib3.PoolManager,
46
- envd_api: httpx.Client,
47
- ) -> None:
48
- self._envd_api_url = envd_api_url
49
- self._envd_version = envd_version
50
- self._connection_config = connection_config
51
- self._pool = pool
52
- self._envd_api = envd_api
53
-
54
- self._rpc = api_pb2_connect.FilesystemClient(
55
- envd_api_url,
56
- http_client=pool
57
- )
58
- self._headers = self._connection_config.headers
59
-
60
- @overload
61
- def read(
62
- self,
63
- path: str,
64
- format: Literal["text"] = "text",
65
- user: Username = "user",
66
- request_timeout: Optional[float] = None,
67
- ) -> str:
68
- """
69
- Read file content as a `str`.
70
-
71
- :param path: Path to the file
72
- :param user: Run the operation as this user
73
- :param format: Format of the file content—`text` by default
74
- :param request_timeout: Timeout for the request in **seconds**
75
-
76
- :return: File content as a `str`
77
- """
78
- ...
79
-
80
- @overload
81
- def read(
82
- self,
83
- path: str,
84
- format: Literal["bytes"],
85
- user: Username = "user",
86
- request_timeout: Optional[float] = None,
87
- ) -> bytearray:
88
- """
89
- Read file content as a `bytearray`.
90
-
91
- :param path: Path to the file
92
- :param user: Run the operation as this user
93
- :param format: Format of the file content—`bytes`
94
- :param request_timeout: Timeout for the request in **seconds**
95
-
96
- :return: File content as a `bytearray`
97
- """
98
- ...
99
-
100
- @overload
101
- def read(
102
- self,
103
- path: str,
104
- format: Literal["stream"],
105
- user: Username = "user",
106
- request_timeout: Optional[float] = None,
107
- ) -> Iterator[bytes]:
108
- """
109
- Read file content as a `Iterator[bytes]`.
110
-
111
- :param path: Path to the file
112
- :param user: Run the operation as this user
113
- :param format: Format of the file content—`stream`
114
- :param request_timeout: Timeout for the request in **seconds**
115
-
116
- :return: File content as an `Iterator[bytes]`
117
- """
118
- ...
119
-
120
- def read(
121
- self,
122
- path: str,
123
- format: Literal["text", "bytes", "stream"] = "text",
124
- user: Username = "user",
125
- request_timeout: Optional[float] = None,
126
- ):
127
- # Use the /download/ endpoint from sandboxagent.go
128
- download_url = f"/download/{path.lstrip('/')}"
129
-
130
- r = self._envd_api.get(
131
- download_url,
132
- timeout=self._connection_config.get_request_timeout(request_timeout),
133
- )
134
-
135
- err = handle_envd_api_exception(r)
136
- if err:
137
- raise err
138
-
139
- if format == "text":
140
- return r.text
141
- elif format == "bytes":
142
- return bytearray(r.content)
143
- elif format == "stream":
144
- return r.iter_bytes()
145
-
146
- @overload
147
- def write(
148
- self,
149
- path: str,
150
- data: Union[str, bytes, IO],
151
- user: Username = "user",
152
- request_timeout: Optional[float] = None,
153
- ) -> WriteInfo:
154
- """
155
- Write content to a file on the path.
156
-
157
- Writing to a file that doesn't exist creates the file.
158
-
159
- Writing to a file that already exists overwrites the file.
160
-
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
-
171
- @overload
172
- def write(
173
- self,
174
- files: List[WriteEntry],
175
- user: Optional[Username] = "user",
176
- request_timeout: Optional[float] = None,
177
- ) -> List[WriteInfo]:
178
- """
179
- Writes a list of files to the filesystem.
180
- When writing to a file that doesn't exist, the file will get created.
181
- When writing to a file that already exists, the file will get overwritten.
182
- When writing to a file that's in a directory that doesn't exist, you'll get an error.
183
-
184
- :param files: list of files to write
185
- :param user: Run the operation as this user
186
- :param request_timeout: Timeout for the request
187
- :return: Information about the written files
188
- """
189
-
190
- def write(
191
- self,
192
- path_or_files: Union[str, List[WriteEntry]],
193
- data_or_user: Union[str, bytes, IO, Username] = "user",
194
- user_or_request_timeout: Optional[Union[float, Username]] = None,
195
- request_timeout_or_none: Optional[float] = None,
196
- ) -> Union[WriteInfo, List[WriteInfo]]:
197
- path, write_files, user, request_timeout = None, [], "user", None
198
- if isinstance(path_or_files, str):
199
- if isinstance(data_or_user, list):
200
- raise Exception(
201
- "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."
202
- )
203
- path, write_files, user, request_timeout = (
204
- path_or_files,
205
- [{"path": path_or_files, "data": data_or_user}],
206
- user_or_request_timeout or "user",
207
- request_timeout_or_none,
208
- )
209
- else:
210
- if path_or_files is None:
211
- raise Exception("Path or files are required")
212
- path, write_files, user, request_timeout = (
213
- None,
214
- path_or_files,
215
- data_or_user,
216
- user_or_request_timeout,
217
- )
218
-
219
- # Allow passing empty list of files
220
- if len(write_files) == 0:
221
- return []
222
-
223
- # Use the /upload endpoint from sandboxagent.go
224
- # This endpoint expects multipart/form-data with 'file' field and 'path' form field
225
- results = []
226
- for file in write_files:
227
- file_path, file_data = file["path"], file["data"]
228
-
229
- # Prepare file data
230
- if isinstance(file_data, str):
231
- file_content = file_data.encode('utf-8')
232
- elif isinstance(file_data, bytes):
233
- file_content = file_data
234
- elif isinstance(file_data, IOBase):
235
- file_content = file_data.read()
236
- if isinstance(file_content, str):
237
- file_content = file_content.encode('utf-8')
238
- else:
239
- raise ValueError(f"Unsupported data type for file {file_path}")
240
-
241
- # Prepare multipart form data
242
- files = [("file", (file_path, file_content))]
243
- data = {"path": file_path}
244
-
245
- r = self._envd_api.post(
246
- "/upload",
247
- files=files,
248
- data=data,
249
- timeout=self._connection_config.get_request_timeout(request_timeout),
250
- )
251
-
252
- err = handle_envd_api_exception(r)
253
- if err:
254
- raise err
255
-
256
- # For now, create a mock WriteInfo since sandboxagent.go returns plain text
257
- # In a real implementation, you might want to enhance the server to return JSON
258
- results.append(WriteInfo(
259
- path=file_path,
260
- name=file_path.split('/')[-1] if '/' in file_path else file_path,
261
- type=FileType.FILE,
262
- ))
263
-
264
- # Return appropriate response based on input format
265
- if len(results) == 1 and path:
266
- return results[0]
267
- else:
268
- return results
269
-
270
- def list(
271
- self,
272
- path: str,
273
- depth: Optional[int] = 1,
274
- user: Username = "user",
275
- request_timeout: Optional[float] = None,
276
- ) -> List[EntryInfo]:
277
- """
278
- List entries in a directory.
279
-
280
- :param path: Path to the directory
281
- :param depth: Depth of the directory to list
282
- :param user: Run the operation as this user
283
- :param request_timeout: Timeout for the request in **seconds**
284
-
285
- :return: List of entries in the directory
286
- """
287
- if depth is not None and depth < 1:
288
- raise InvalidArgumentException("depth should be at least 1")
289
-
290
- try:
291
- res = self._rpc.list_dir(
292
- api_pb2.ListDirRequest(path=path, depth=depth),
293
- self._headers,
294
- timeout_seconds=self._connection_config.get_request_timeout(
295
- request_timeout
296
- ),
297
- )
298
-
299
- entries: List[EntryInfo] = []
300
- for entry in res.entries:
301
- event_type = map_file_type(entry.type)
302
-
303
- if event_type:
304
- entries.append(
305
- EntryInfo(
306
- name=entry.name,
307
- type=event_type,
308
- path=entry.path,
309
- size=entry.size,
310
- mode=entry.mode,
311
- permissions=entry.permissions,
312
- owner=entry.owner,
313
- group=entry.group,
314
- modified_time=entry.modified_time.ToDatetime(),
315
- # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
316
- symlink_target=(
317
- entry.symlink_target
318
- if entry.HasField("symlink_target")
319
- else None
320
- ),
321
- )
322
- )
323
-
324
- return entries
325
- except Exception as e:
326
- raise handle_rpc_exception(e)
327
-
328
- def exists(
329
- self,
330
- path: str,
331
- user: Username = "user",
332
- request_timeout: Optional[float] = None,
333
- ) -> bool:
334
- """
335
- Check if a file or a directory exists.
336
-
337
- :param path: Path to a file or a directory
338
- :param user: Run the operation as this user
339
- :param request_timeout: Timeout for the request in **seconds**
340
-
341
- :return: `True` if the file or directory exists, `False` otherwise
342
- """
343
- try:
344
- self._rpc.stat(
345
- api_pb2.StatRequest(path=path),
346
- self._headers,
347
- timeout_seconds=self._connection_config.get_request_timeout(
348
- request_timeout
349
- ),
350
- )
351
- return True
352
-
353
- except Exception as e:
354
- if "no such file or directory" in str(e):
355
- return False
356
- raise handle_rpc_exception(e)
357
-
358
- def get_info(
359
- self,
360
- path: str,
361
- user: Username = "user",
362
- request_timeout: Optional[float] = None,
363
- ) -> EntryInfo:
364
- """
365
- Get information about a file or directory.
366
-
367
- :param path: Path to a file or a directory
368
- :param user: Run the operation as this user
369
- :param request_timeout: Timeout for the request in **seconds**
370
-
371
- :return: Information about the file or directory like name, type, and path
372
- """
373
- try:
374
- r = self._rpc.stat(
375
- api_pb2.StatRequest(path=path),
376
- self._headers,
377
- timeout_seconds=self._connection_config.get_request_timeout(
378
- request_timeout
379
- ),
380
- )
381
-
382
- return EntryInfo(
383
- name=r.entry.name,
384
- type=map_file_type(r.entry.type),
385
- path=r.entry.path,
386
- size=r.entry.size,
387
- mode=r.entry.mode,
388
- permissions=r.entry.permissions,
389
- owner=r.entry.owner,
390
- group=r.entry.group,
391
- modified_time=r.entry.modified_time.ToDatetime(),
392
- # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
393
- symlink_target=(
394
- r.entry.symlink_target
395
- if r.entry.HasField("symlink_target")
396
- else None
397
- ),
398
- )
399
- except Exception as e:
400
- raise handle_rpc_exception(e)
401
-
402
- def remove(
403
- self,
404
- path: str,
405
- user: Username = "user",
406
- request_timeout: Optional[float] = None,
407
- ) -> None:
408
- """
409
- Remove a file or a directory.
410
-
411
- :param path: Path to a file or a directory
412
- :param user: Run the operation as this user
413
- :param request_timeout: Timeout for the request in **seconds**
414
- """
415
- try:
416
- self._rpc.remove(
417
- api_pb2.RemoveRequest(path=path),
418
- self._headers,
419
- timeout_seconds=self._connection_config.get_request_timeout(
420
- request_timeout
421
- ),
422
- )
423
- except Exception as e:
424
- raise handle_rpc_exception(e)
425
-
426
- def rename(
427
- self,
428
- old_path: str,
429
- new_path: str,
430
- user: Username = "user",
431
- request_timeout: Optional[float] = None,
432
- ) -> EntryInfo:
433
- """
434
- Rename a file or directory.
435
-
436
- :param old_path: Path to the file or directory to rename
437
- :param new_path: New path to the file or directory
438
- :param user: Run the operation as this user
439
- :param request_timeout: Timeout for the request in **seconds**
440
-
441
- :return: Information about the renamed file or directory
442
- """
443
- try:
444
- r = self._rpc.move(
445
- api_pb2.MoveRequest(
446
- source=old_path,
447
- destination=new_path,
448
- ),
449
- self._headers,
450
- timeout_seconds=self._connection_config.get_request_timeout(
451
- request_timeout
452
- ),
453
- )
454
-
455
- return EntryInfo(
456
- name=r.entry.name,
457
- type=map_file_type(r.entry.type),
458
- path=r.entry.path,
459
- size=r.entry.size,
460
- mode=r.entry.mode,
461
- permissions=r.entry.permissions,
462
- owner=r.entry.owner,
463
- group=r.entry.group,
464
- modified_time=r.entry.modified_time.ToDatetime(),
465
- # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
466
- symlink_target=(
467
- r.entry.symlink_target
468
- if r.entry.HasField("symlink_target")
469
- else None
470
- ),
471
- )
472
- except Exception as e:
473
- raise handle_rpc_exception(e)
474
-
475
- def make_dir(
476
- self,
477
- path: str,
478
- user: Username = "user",
479
- request_timeout: Optional[float] = None,
480
- ) -> bool:
481
- """
482
- Create a new directory and all directories along the way if needed on the specified path.
483
-
484
- :param path: Path to a new directory. For example '/dirA/dirB' when creating 'dirB'.
485
- :param user: Run the operation as this user
486
- :param request_timeout: Timeout for the request in **seconds**
487
-
488
- :return: `True` if the directory was created, `False` if the directory already exists
489
- """
490
- try:
491
- self._rpc.make_dir(
492
- api_pb2.MakeDirRequest(path=path),
493
- self._headers,
494
- timeout_seconds=self._connection_config.get_request_timeout(
495
- request_timeout
496
- ),
497
- )
498
-
499
- return True
500
- except Exception as e:
501
- if "directory already exists" in str(e):
502
- return False
503
- raise handle_rpc_exception(e)
504
-
505
- def watch_dir(
506
- self,
507
- path: str,
508
- user: Username = "user",
509
- request_timeout: Optional[float] = None,
510
- recursive: bool = False,
511
- ) -> WatchHandle:
512
- """
513
- Watch directory for filesystem events.
514
-
515
- :param path: Path to a directory to watch
516
- :param user: Run the operation as this user
517
- :param request_timeout: Timeout for the request in **seconds**
518
- :param recursive: Watch directory recursively
519
-
520
- :return: `WatchHandle` object for stopping watching directory
521
- """
522
- if (
523
- recursive
524
- and self._envd_version is not None
525
- and Version(self._envd_version) < ENVD_VERSION_RECURSIVE_WATCH
526
- ):
527
- raise TemplateException(
528
- "You need to update the template to use recursive watching. "
529
- "You can do this by running `e2b template build` in the directory with the template."
530
- )
531
-
532
- try:
533
- r = self._rpc.create_watcher(
534
- api_pb2.CreateWatcherRequest(path=path, recursive=recursive),
535
- self._headers,
536
- timeout_seconds=self._connection_config.get_request_timeout(
537
- request_timeout
538
- ),
539
- )
540
- except Exception as e:
541
- raise handle_rpc_exception(e)
542
-
543
- return WatchHandle(self._rpc, r.watcher_id)
1
+ from io import IOBase
2
+ from typing import IO, Iterator, List, Literal, Optional, Union, overload
3
+
4
+ import httpcore
5
+ import httpx
6
+ import urllib3
7
+ from packaging.version import Version
8
+
9
+ from ... import csx_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, TemplateException
17
+ from ...generated import api_pb2, api_pb2_connect
18
+ from ...generated.api import (
19
+ ENVD_API_DOWNLOAD_FILES_ROUTE,
20
+ ENVD_API_UPLOAD_FILES_ROUTE,
21
+ handle_envd_api_exception,
22
+ )
23
+ from ...generated.rpc import authentication_header, handle_rpc_exception
24
+ from ...generated.versions import ENVD_VERSION_RECURSIVE_WATCH
25
+ from ...sandbox.filesystem.filesystem import (
26
+ EntryInfo,
27
+ FileType,
28
+ WriteEntry,
29
+ WriteInfo,
30
+ map_file_type,
31
+ )
32
+ from ...sandbox_sync.filesystem.watch_handle import WatchHandle
33
+
34
+
35
+ class Filesystem:
36
+ """
37
+ Module for interacting with the filesystem in the sandbox.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ envd_api_url: str,
43
+ envd_version: Optional[str],
44
+ connection_config: ConnectionConfig,
45
+ pool: urllib3.PoolManager,
46
+ envd_api: httpx.Client,
47
+ ) -> None:
48
+ self._envd_api_url = envd_api_url
49
+ self._envd_version = envd_version
50
+ self._connection_config = connection_config
51
+ self._pool = pool
52
+ self._envd_api = envd_api
53
+
54
+ self._rpc = api_pb2_connect.FilesystemClient(
55
+ envd_api_url,
56
+ http_client=pool
57
+ )
58
+ self._headers = self._connection_config.headers
59
+
60
+ @overload
61
+ def read(
62
+ self,
63
+ path: str,
64
+ format: Literal["text"] = "text",
65
+ user: Username = "user",
66
+ request_timeout: Optional[float] = None,
67
+ ) -> str:
68
+ """
69
+ Read file content as a `str`.
70
+
71
+ :param path: Path to the file
72
+ :param user: Run the operation as this user
73
+ :param format: Format of the file content—`text` by default
74
+ :param request_timeout: Timeout for the request in **seconds**
75
+
76
+ :return: File content as a `str`
77
+ """
78
+ ...
79
+
80
+ @overload
81
+ def read(
82
+ self,
83
+ path: str,
84
+ format: Literal["bytes"],
85
+ user: Username = "user",
86
+ request_timeout: Optional[float] = None,
87
+ ) -> bytearray:
88
+ """
89
+ Read file content as a `bytearray`.
90
+
91
+ :param path: Path to the file
92
+ :param user: Run the operation as this user
93
+ :param format: Format of the file content—`bytes`
94
+ :param request_timeout: Timeout for the request in **seconds**
95
+
96
+ :return: File content as a `bytearray`
97
+ """
98
+ ...
99
+
100
+ @overload
101
+ def read(
102
+ self,
103
+ path: str,
104
+ format: Literal["stream"],
105
+ user: Username = "user",
106
+ request_timeout: Optional[float] = None,
107
+ ) -> Iterator[bytes]:
108
+ """
109
+ Read file content as a `Iterator[bytes]`.
110
+
111
+ :param path: Path to the file
112
+ :param user: Run the operation as this user
113
+ :param format: Format of the file content—`stream`
114
+ :param request_timeout: Timeout for the request in **seconds**
115
+
116
+ :return: File content as an `Iterator[bytes]`
117
+ """
118
+ ...
119
+
120
+ def read(
121
+ self,
122
+ path: str,
123
+ format: Literal["text", "bytes", "stream"] = "text",
124
+ user: Username = "user",
125
+ request_timeout: Optional[float] = None,
126
+ ):
127
+ # Use the /download/ endpoint from sandboxagent.go
128
+ download_url = f"/download/{path.lstrip('/')}"
129
+
130
+ r = self._envd_api.get(
131
+ download_url,
132
+ timeout=self._connection_config.get_request_timeout(request_timeout),
133
+ )
134
+
135
+ err = handle_envd_api_exception(r)
136
+ if err:
137
+ raise err
138
+
139
+ if format == "text":
140
+ return r.text
141
+ elif format == "bytes":
142
+ return bytearray(r.content)
143
+ elif format == "stream":
144
+ return r.iter_bytes()
145
+
146
+ @overload
147
+ def write(
148
+ self,
149
+ path: str,
150
+ data: Union[str, bytes, IO],
151
+ user: Username = "user",
152
+ request_timeout: Optional[float] = None,
153
+ ) -> WriteInfo:
154
+ """
155
+ Write content to a file on the path.
156
+
157
+ Writing to a file that doesn't exist creates the file.
158
+
159
+ Writing to a file that already exists overwrites the file.
160
+
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
+
171
+ @overload
172
+ def write(
173
+ self,
174
+ files: List[WriteEntry],
175
+ user: Optional[Username] = "user",
176
+ request_timeout: Optional[float] = None,
177
+ ) -> List[WriteInfo]:
178
+ """
179
+ Writes a list of files to the filesystem.
180
+ When writing to a file that doesn't exist, the file will get created.
181
+ When writing to a file that already exists, the file will get overwritten.
182
+ When writing to a file that's in a directory that doesn't exist, you'll get an error.
183
+
184
+ :param files: list of files to write
185
+ :param user: Run the operation as this user
186
+ :param request_timeout: Timeout for the request
187
+ :return: Information about the written files
188
+ """
189
+
190
+ def write(
191
+ self,
192
+ path_or_files: Union[str, List[WriteEntry]],
193
+ data_or_user: Union[str, bytes, IO, Username] = "user",
194
+ user_or_request_timeout: Optional[Union[float, Username]] = None,
195
+ request_timeout_or_none: Optional[float] = None,
196
+ ) -> Union[WriteInfo, List[WriteInfo]]:
197
+ path, write_files, user, request_timeout = None, [], "user", None
198
+ if isinstance(path_or_files, str):
199
+ if isinstance(data_or_user, list):
200
+ raise Exception(
201
+ "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."
202
+ )
203
+ path, write_files, user, request_timeout = (
204
+ path_or_files,
205
+ [{"path": path_or_files, "data": data_or_user}],
206
+ user_or_request_timeout or "user",
207
+ request_timeout_or_none,
208
+ )
209
+ else:
210
+ if path_or_files is None:
211
+ raise Exception("Path or files are required")
212
+ path, write_files, user, request_timeout = (
213
+ None,
214
+ path_or_files,
215
+ data_or_user,
216
+ user_or_request_timeout,
217
+ )
218
+
219
+ # Allow passing empty list of files
220
+ if len(write_files) == 0:
221
+ return []
222
+
223
+ # Use the /upload endpoint from sandboxagent.go
224
+ # This endpoint expects multipart/form-data with 'file' field and 'path' form field
225
+ results = []
226
+ for file in write_files:
227
+ file_path, file_data = file["path"], file["data"]
228
+
229
+ # Prepare file data
230
+ if isinstance(file_data, str):
231
+ file_content = file_data.encode('utf-8')
232
+ elif isinstance(file_data, bytes):
233
+ file_content = file_data
234
+ elif isinstance(file_data, IOBase):
235
+ file_content = file_data.read()
236
+ if isinstance(file_content, str):
237
+ file_content = file_content.encode('utf-8')
238
+ else:
239
+ raise ValueError(f"Unsupported data type for file {file_path}")
240
+
241
+ # Prepare multipart form data
242
+ files = [("file", (file_path, file_content))]
243
+ data = {"path": file_path}
244
+
245
+ r = self._envd_api.post(
246
+ "/upload",
247
+ files=files,
248
+ data=data,
249
+ timeout=self._connection_config.get_request_timeout(request_timeout),
250
+ )
251
+
252
+ err = handle_envd_api_exception(r)
253
+ if err:
254
+ raise err
255
+
256
+ # For now, create a mock WriteInfo since sandboxagent.go returns plain text
257
+ # In a real implementation, you might want to enhance the server to return JSON
258
+ results.append(WriteInfo(
259
+ path=file_path,
260
+ name=file_path.split('/')[-1] if '/' in file_path else file_path,
261
+ type=FileType.FILE,
262
+ ))
263
+
264
+ # Return appropriate response based on input format
265
+ if len(results) == 1 and path:
266
+ return results[0]
267
+ else:
268
+ return results
269
+
270
+ def list(
271
+ self,
272
+ path: str,
273
+ depth: Optional[int] = 1,
274
+ user: Username = "user",
275
+ request_timeout: Optional[float] = None,
276
+ ) -> List[EntryInfo]:
277
+ """
278
+ List entries in a directory.
279
+
280
+ :param path: Path to the directory
281
+ :param depth: Depth of the directory to list
282
+ :param user: Run the operation as this user
283
+ :param request_timeout: Timeout for the request in **seconds**
284
+
285
+ :return: List of entries in the directory
286
+ """
287
+ if depth is not None and depth < 1:
288
+ raise InvalidArgumentException("depth should be at least 1")
289
+
290
+ try:
291
+ res = self._rpc.list_dir(
292
+ api_pb2.ListDirRequest(path=path, depth=depth),
293
+ self._headers,
294
+ timeout_seconds=self._connection_config.get_request_timeout(
295
+ request_timeout
296
+ ),
297
+ )
298
+
299
+ entries: List[EntryInfo] = []
300
+ for entry in res.entries:
301
+ event_type = map_file_type(entry.type)
302
+
303
+ if event_type:
304
+ entries.append(
305
+ EntryInfo(
306
+ name=entry.name,
307
+ type=event_type,
308
+ path=entry.path,
309
+ size=entry.size,
310
+ mode=entry.mode,
311
+ permissions=entry.permissions,
312
+ owner=entry.owner,
313
+ group=entry.group,
314
+ modified_time=entry.modified_time.ToDatetime(),
315
+ # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
316
+ symlink_target=(
317
+ entry.symlink_target
318
+ if entry.HasField("symlink_target")
319
+ else None
320
+ ),
321
+ )
322
+ )
323
+
324
+ return entries
325
+ except Exception as e:
326
+ raise handle_rpc_exception(e)
327
+
328
+ def exists(
329
+ self,
330
+ path: str,
331
+ user: Username = "user",
332
+ request_timeout: Optional[float] = None,
333
+ ) -> bool:
334
+ """
335
+ Check if a file or a directory exists.
336
+
337
+ :param path: Path to a file or a directory
338
+ :param user: Run the operation as this user
339
+ :param request_timeout: Timeout for the request in **seconds**
340
+
341
+ :return: `True` if the file or directory exists, `False` otherwise
342
+ """
343
+ try:
344
+ self._rpc.stat(
345
+ api_pb2.StatRequest(path=path),
346
+ self._headers,
347
+ timeout_seconds=self._connection_config.get_request_timeout(
348
+ request_timeout
349
+ ),
350
+ )
351
+ return True
352
+
353
+ except Exception as e:
354
+ if "no such file or directory" in str(e):
355
+ return False
356
+ raise handle_rpc_exception(e)
357
+
358
+ def get_info(
359
+ self,
360
+ path: str,
361
+ user: Username = "user",
362
+ request_timeout: Optional[float] = None,
363
+ ) -> EntryInfo:
364
+ """
365
+ Get information about a file or directory.
366
+
367
+ :param path: Path to a file or a directory
368
+ :param user: Run the operation as this user
369
+ :param request_timeout: Timeout for the request in **seconds**
370
+
371
+ :return: Information about the file or directory like name, type, and path
372
+ """
373
+ try:
374
+ r = self._rpc.stat(
375
+ api_pb2.StatRequest(path=path),
376
+ self._headers,
377
+ timeout_seconds=self._connection_config.get_request_timeout(
378
+ request_timeout
379
+ ),
380
+ )
381
+
382
+ return EntryInfo(
383
+ name=r.entry.name,
384
+ type=map_file_type(r.entry.type),
385
+ path=r.entry.path,
386
+ size=r.entry.size,
387
+ mode=r.entry.mode,
388
+ permissions=r.entry.permissions,
389
+ owner=r.entry.owner,
390
+ group=r.entry.group,
391
+ modified_time=r.entry.modified_time.ToDatetime(),
392
+ # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
393
+ symlink_target=(
394
+ r.entry.symlink_target
395
+ if r.entry.HasField("symlink_target")
396
+ else None
397
+ ),
398
+ )
399
+ except Exception as e:
400
+ raise handle_rpc_exception(e)
401
+
402
+ def remove(
403
+ self,
404
+ path: str,
405
+ user: Username = "user",
406
+ request_timeout: Optional[float] = None,
407
+ ) -> None:
408
+ """
409
+ Remove a file or a directory.
410
+
411
+ :param path: Path to a file or a directory
412
+ :param user: Run the operation as this user
413
+ :param request_timeout: Timeout for the request in **seconds**
414
+ """
415
+ try:
416
+ self._rpc.remove(
417
+ api_pb2.RemoveRequest(path=path),
418
+ self._headers,
419
+ timeout_seconds=self._connection_config.get_request_timeout(
420
+ request_timeout
421
+ ),
422
+ )
423
+ except Exception as e:
424
+ raise handle_rpc_exception(e)
425
+
426
+ def rename(
427
+ self,
428
+ old_path: str,
429
+ new_path: str,
430
+ user: Username = "user",
431
+ request_timeout: Optional[float] = None,
432
+ ) -> EntryInfo:
433
+ """
434
+ Rename a file or directory.
435
+
436
+ :param old_path: Path to the file or directory to rename
437
+ :param new_path: New path to the file or directory
438
+ :param user: Run the operation as this user
439
+ :param request_timeout: Timeout for the request in **seconds**
440
+
441
+ :return: Information about the renamed file or directory
442
+ """
443
+ try:
444
+ r = self._rpc.move(
445
+ api_pb2.MoveRequest(
446
+ source=old_path,
447
+ destination=new_path,
448
+ ),
449
+ self._headers,
450
+ timeout_seconds=self._connection_config.get_request_timeout(
451
+ request_timeout
452
+ ),
453
+ )
454
+
455
+ return EntryInfo(
456
+ name=r.entry.name,
457
+ type=map_file_type(r.entry.type),
458
+ path=r.entry.path,
459
+ size=r.entry.size,
460
+ mode=r.entry.mode,
461
+ permissions=r.entry.permissions,
462
+ owner=r.entry.owner,
463
+ group=r.entry.group,
464
+ modified_time=r.entry.modified_time.ToDatetime(),
465
+ # Optional, we can't directly access symlink_target otherwise if will be "" instead of None
466
+ symlink_target=(
467
+ r.entry.symlink_target
468
+ if r.entry.HasField("symlink_target")
469
+ else None
470
+ ),
471
+ )
472
+ except Exception as e:
473
+ raise handle_rpc_exception(e)
474
+
475
+ def make_dir(
476
+ self,
477
+ path: str,
478
+ user: Username = "user",
479
+ request_timeout: Optional[float] = None,
480
+ ) -> bool:
481
+ """
482
+ Create a new directory and all directories along the way if needed on the specified path.
483
+
484
+ :param path: Path to a new directory. For example '/dirA/dirB' when creating 'dirB'.
485
+ :param user: Run the operation as this user
486
+ :param request_timeout: Timeout for the request in **seconds**
487
+
488
+ :return: `True` if the directory was created, `False` if the directory already exists
489
+ """
490
+ try:
491
+ self._rpc.make_dir(
492
+ api_pb2.MakeDirRequest(path=path),
493
+ self._headers,
494
+ timeout_seconds=self._connection_config.get_request_timeout(
495
+ request_timeout
496
+ ),
497
+ )
498
+
499
+ return True
500
+ except Exception as e:
501
+ if "directory already exists" in str(e):
502
+ return False
503
+ raise handle_rpc_exception(e)
504
+
505
+ def watch_dir(
506
+ self,
507
+ path: str,
508
+ user: Username = "user",
509
+ request_timeout: Optional[float] = None,
510
+ recursive: bool = False,
511
+ ) -> WatchHandle:
512
+ """
513
+ Watch directory for filesystem events.
514
+
515
+ :param path: Path to a directory to watch
516
+ :param user: Run the operation as this user
517
+ :param request_timeout: Timeout for the request in **seconds**
518
+ :param recursive: Watch directory recursively
519
+
520
+ :return: `WatchHandle` object for stopping watching directory
521
+ """
522
+ if (
523
+ recursive
524
+ and self._envd_version is not None
525
+ and Version(self._envd_version) < ENVD_VERSION_RECURSIVE_WATCH
526
+ ):
527
+ raise TemplateException(
528
+ "You need to update the template to use recursive watching. "
529
+ "You can do this by running `scalebox template build` in the directory with the template."
530
+ )
531
+
532
+ try:
533
+ r = self._rpc.create_watcher(
534
+ api_pb2.CreateWatcherRequest(path=path, recursive=recursive),
535
+ self._headers,
536
+ timeout_seconds=self._connection_config.get_request_timeout(
537
+ request_timeout
538
+ ),
539
+ )
540
+ except Exception as e:
541
+ raise handle_rpc_exception(e)
542
+
543
+ return WatchHandle(self._rpc, r.watcher_id)
scalebox/version.py CHANGED
@@ -2,8 +2,8 @@
2
2
  Version information for ScaleBox Python SDK
3
3
  """
4
4
 
5
- __version__ = "0.1.22"
6
- __version_info__ = (0, 1, 22)
5
+ __version__ = "0.1.24"
6
+ __version_info__ = (0, 1, 24)
7
7
 
8
8
 
9
9
  def get_version() -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scalebox-sdk
3
- Version: 0.1.22
3
+ Version: 0.1.24
4
4
  Summary: ScaleBox Python SDK - A multi-language code execution sandbox with Python, R, Node.js, Deno/TypeScript, Java, and Bash support
5
5
  Author-email: ScaleBox Team <dev@scalebox.dev>
6
6
  Maintainer-email: ScaleBox Team <dev@scalebox.dev>
@@ -1,9 +1,9 @@
1
- scalebox/__init__.py,sha256=hRyCCz7cISfsaC_i79eCYm8EDLVgKoH2jCK9bks99BQ,1812
1
+ scalebox/__init__.py,sha256=mJ4xOcm_wLKtYS81Ir74xYu_wn40S_jwkdL94sULmm8,1812
2
2
  scalebox/cli.py,sha256=HWIyGuhbP1WZm839CwTysauL78xMBOoatFychxzloxQ,3904
3
3
  scalebox/connection_config.py,sha256=W2hMeXlxUHvR51guqOWEEDv35o4XP8SbLkDXqa20C0M,2655
4
4
  scalebox/exceptions.py,sha256=10R9VXfvgO4XJJnxyzyrzkxliyeEBX0ZC36izXa8R5k,2053
5
5
  scalebox/requirements.txt,sha256=LEYsk2VzoxKR-V44Y6qJuJ3vKdTYS79f1Gv1Ajleifo,567
6
- scalebox/version.py,sha256=xPJw3UIUzgAs8Pe9WBXxxPFm4uNWwrEx9LB-rgSV2Uc,323
6
+ scalebox/version.py,sha256=zPiqwFEMt--CAZTeF-4OEP4BpRKQzKJ3uTPWkbziA9Y,323
7
7
  scalebox/api/__init__.py,sha256=_9nWyeNg4Y_Z30YpBNoDP6S92YdlO_5xBkrp-we0SIg,4167
8
8
  scalebox/api/metadata.py,sha256=lg5ekfnFZYZoCoJxIPo961HEGVg_rLLRJBbw4ZApM_Y,512
9
9
  scalebox/api/client/__init__.py,sha256=IVRaxvQcdPu1Xxc3t--g3ir3Wl5f3Y0zKMwy1nkKN80,155
@@ -34,7 +34,7 @@ scalebox/api/client/models/identifier_masking_details.py,sha256=97xSg4Nr3D5xbjGD
34
34
  scalebox/api/client/models/listed_sandbox.py,sha256=oQUitQWjCk1HOXX7K7YdaGWBLQnPu4GnMt2E9-rQOTw,4040
35
35
  scalebox/api/client/models/log_level.py,sha256=uv6u1nkqfzdxTjJAxLZdWuH5goVI_g-ULLhh6TU5dw8,189
36
36
  scalebox/api/client/models/new_access_token.py,sha256=sjlQ7dEj4ByCgwud750SbxwyjggAKB1s-o_N7dRCo7s,1480
37
- scalebox/api/client/models/new_sandbox.py,sha256=z_LaZ0RS6qI95IVGklKgQZ301zSah9HerzND78QAP80,3934
37
+ scalebox/api/client/models/new_sandbox.py,sha256=-mnu5wZPkofTGqsbDmA1nWTFDw-fzPu2Mg_rVISxd70,3937
38
38
  scalebox/api/client/models/new_team_api_key.py,sha256=neYLvvvzyPcpXYV5gDHVkv_ovjKdIBKUTX04cD5QKGA,1473
39
39
  scalebox/api/client/models/node.py,sha256=nuEZij7z7iXzBQYTFwardHlSixWrepztFmMIIqd8Ek4,4653
40
40
  scalebox/api/client/models/node_detail.py,sha256=ywP1oDzyxi7hIJAMTvINQQ8CwMsd1PlH1R5bEw716Ig,4511
@@ -115,7 +115,7 @@ scalebox/sandbox_sync/commands/command.py,sha256=308RgsexBQaLVbUc2ui6oKOLh6xUFDM
115
115
  scalebox/sandbox_sync/commands/command_handle.py,sha256=XPN1SpW00NqHxTuyceh4MLik-YQPEKfu4FOtf8pIC7Y,4615
116
116
  scalebox/sandbox_sync/commands/pty.py,sha256=KQStIe-ts_sSWjE_zD01AMqMCGbq9wGeXzbN-i4js9Q,5629
117
117
  scalebox/sandbox_sync/filesystem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
118
- scalebox/sandbox_sync/filesystem/filesystem.py,sha256=ajZgv3VyBfFtO8o4BIx6w7z1vhFugnLXysRfLBsa_ms,18193
118
+ scalebox/sandbox_sync/filesystem/filesystem.py,sha256=MqjXNWrUw2_JaFh2UrMkfZsHdKtyJwLU5dPux1EH8Zw,18741
119
119
  scalebox/sandbox_sync/filesystem/watch_handle.py,sha256=dC9p74AHMv-3mQ1tudmOPjrpjvcJ7wFmCduNdity6os,2027
120
120
  scalebox/test/CODE_INTERPRETER_TESTS_READY.md,sha256=gtS9jRp2VB44MVG-ztb1JgYiqWtea8Z-TMEK1mZzfgE,11365
121
121
  scalebox/test/README.md,sha256=1rpgOo8uv-zT6QNw05MCCl8XzplD6xvWl3gE7frn9xI,9129
@@ -148,9 +148,9 @@ scalebox/test/testsandbox_sync.py,sha256=v1dFAJWKbyLnjIiafTR9TafxJF1gaUk-W2mmQU4
148
148
  scalebox/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
149
149
  scalebox/utils/httpcoreclient.py,sha256=kjTndd-YECPe3n_G1HfGgitzRwntC21tqtIqZ62V6Lg,9868
150
150
  scalebox/utils/httpxclient.py,sha256=oLpCP2RChvnspS6Unl6ngmpY72yPokTfSqMm9m-7k38,13442
151
- scalebox_sdk-0.1.22.dist-info/licenses/LICENSE,sha256=9zP32kHlBovkfji1R6ptx3H7WjJJvnf4UuwTpfogmsY,1069
152
- scalebox_sdk-0.1.22.dist-info/METADATA,sha256=6nAZZ7ScXZah72V6FlXGwm8N0QRFRlR2otIxxu7mDhA,12736
153
- scalebox_sdk-0.1.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
154
- scalebox_sdk-0.1.22.dist-info/entry_points.txt,sha256=g7C1Trcg8EyvAGMnHpJ3alqtZzQuMypYUQVFK13kOFM,47
155
- scalebox_sdk-0.1.22.dist-info/top_level.txt,sha256=CDjlibkbOG-MT-s1TRxs4Xe_iN1m11ii48spB6DOMj4,9
156
- scalebox_sdk-0.1.22.dist-info/RECORD,,
151
+ scalebox_sdk-0.1.24.dist-info/licenses/LICENSE,sha256=9zP32kHlBovkfji1R6ptx3H7WjJJvnf4UuwTpfogmsY,1069
152
+ scalebox_sdk-0.1.24.dist-info/METADATA,sha256=2w0Hw5R8QRgwCrs7lVqkP8JO2oFjMZthXkOeyIJ10YY,12736
153
+ scalebox_sdk-0.1.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
154
+ scalebox_sdk-0.1.24.dist-info/entry_points.txt,sha256=g7C1Trcg8EyvAGMnHpJ3alqtZzQuMypYUQVFK13kOFM,47
155
+ scalebox_sdk-0.1.24.dist-info/top_level.txt,sha256=CDjlibkbOG-MT-s1TRxs4Xe_iN1m11ii48spB6DOMj4,9
156
+ scalebox_sdk-0.1.24.dist-info/RECORD,,