scalebox-sdk 0.1.24__py3-none-any.whl → 1.0.1__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 (87) hide show
  1. scalebox/__init__.py +2 -2
  2. scalebox/api/__init__.py +130 -128
  3. scalebox/api/client/__init__.py +8 -8
  4. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +2 -2
  5. scalebox/api/client/api/sandboxes/post_sandboxes.py +2 -2
  6. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  7. scalebox/api/client/client.py +288 -288
  8. scalebox/api/client/models/connect_sandbox.py +59 -0
  9. scalebox/api/client/models/error.py +2 -2
  10. scalebox/api/client/models/listed_sandbox.py +19 -1
  11. scalebox/api/client/models/new_sandbox.py +10 -0
  12. scalebox/api/client/models/sandbox.py +138 -125
  13. scalebox/api/client/models/sandbox_detail.py +24 -0
  14. scalebox/api/client/types.py +46 -46
  15. scalebox/cli.py +125 -125
  16. scalebox/client/aclient.py +57 -57
  17. scalebox/client/client.py +102 -102
  18. scalebox/code_interpreter/__init__.py +12 -12
  19. scalebox/code_interpreter/charts.py +230 -230
  20. scalebox/code_interpreter/constants.py +3 -3
  21. scalebox/code_interpreter/exceptions.py +13 -13
  22. scalebox/code_interpreter/models.py +485 -485
  23. scalebox/connection_config.py +34 -1
  24. scalebox/csx_connect/__init__.py +1 -1
  25. scalebox/csx_connect/client.py +485 -485
  26. scalebox/csx_desktop/main.py +651 -651
  27. scalebox/exceptions.py +83 -83
  28. scalebox/generated/api.py +61 -61
  29. scalebox/generated/api_pb2.py +203 -203
  30. scalebox/generated/api_pb2.pyi +956 -956
  31. scalebox/generated/api_pb2_connect.py +1407 -1407
  32. scalebox/generated/rpc.py +50 -50
  33. scalebox/sandbox/main.py +146 -139
  34. scalebox/sandbox/sandbox_api.py +105 -91
  35. scalebox/sandbox/signature.py +40 -40
  36. scalebox/sandbox/utils.py +34 -34
  37. scalebox/sandbox_async/commands/command.py +307 -307
  38. scalebox/sandbox_async/commands/command_handle.py +187 -187
  39. scalebox/sandbox_async/commands/pty.py +187 -187
  40. scalebox/sandbox_async/filesystem/filesystem.py +557 -557
  41. scalebox/sandbox_async/filesystem/watch_handle.py +61 -61
  42. scalebox/sandbox_async/main.py +228 -46
  43. scalebox/sandbox_async/sandbox_api.py +124 -3
  44. scalebox/sandbox_async/utils.py +7 -7
  45. scalebox/sandbox_sync/__init__.py +2 -2
  46. scalebox/sandbox_sync/commands/command.py +300 -300
  47. scalebox/sandbox_sync/commands/command_handle.py +150 -150
  48. scalebox/sandbox_sync/commands/pty.py +181 -181
  49. scalebox/sandbox_sync/filesystem/filesystem.py +3 -3
  50. scalebox/sandbox_sync/filesystem/watch_handle.py +66 -66
  51. scalebox/sandbox_sync/main.py +208 -133
  52. scalebox/sandbox_sync/sandbox_api.py +119 -3
  53. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -323
  54. scalebox/test/README.md +329 -329
  55. scalebox/test/bedrock_openai_adapter.py +67 -0
  56. scalebox/test/code_interpreter_test.py +34 -34
  57. scalebox/test/code_interpreter_test_sync.py +34 -34
  58. scalebox/test/run_stress_code_interpreter_sync.py +166 -0
  59. scalebox/test/simple_upload_example.py +123 -0
  60. scalebox/test/stabitiy_test.py +310 -0
  61. scalebox/test/test_browser_use.py +25 -0
  62. scalebox/test/test_browser_use_scalebox.py +61 -0
  63. scalebox/test/test_code_interpreter_sync_comprehensive.py +115 -65
  64. scalebox/test/test_connect_pause_async.py +277 -0
  65. scalebox/test/test_connect_pause_sync.py +267 -0
  66. scalebox/test/test_desktop_sandbox_sf.py +117 -0
  67. scalebox/test/test_download_url.py +49 -0
  68. scalebox/test/test_sandbox_async_comprehensive.py +1 -1
  69. scalebox/test/test_sandbox_object_storage_example.py +146 -0
  70. scalebox/test/test_sandbox_object_storage_example_async.py +156 -0
  71. scalebox/test/test_sf.py +137 -0
  72. scalebox/test/test_watch_dir_async.py +56 -0
  73. scalebox/test/testacreate.py +1 -1
  74. scalebox/test/testagetinfo.py +1 -1
  75. scalebox/test/testcomputeuse.py +243 -243
  76. scalebox/test/testsandbox_api.py +13 -0
  77. scalebox/test/testsandbox_sync.py +1 -1
  78. scalebox/test/upload_100mb_example.py +355 -0
  79. scalebox/utils/httpcoreclient.py +297 -297
  80. scalebox/utils/httpxclient.py +403 -403
  81. scalebox/version.py +2 -2
  82. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/METADATA +1 -1
  83. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/RECORD +87 -69
  84. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/WHEEL +1 -1
  85. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/entry_points.txt +0 -0
  86. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,61 +1,61 @@
1
- import asyncio
2
- import inspect
3
- from typing import Any, AsyncGenerator, Optional
4
-
5
- from ...generated.api_pb2 import WatchDirResponse
6
- from ...generated.rpc import handle_rpc_exception
7
- from ...sandbox.filesystem.watch_handle import FilesystemEvent, map_event_type
8
- from ...sandbox_async.utils import OutputHandler
9
-
10
-
11
- class AsyncWatchHandle:
12
- """
13
- Handle for watching a directory in the sandbox filesystem.
14
-
15
- Use `.stop()` to stop watching the directory.
16
- """
17
-
18
- def __init__(
19
- self,
20
- events: AsyncGenerator[WatchDirResponse, Any],
21
- on_event: OutputHandler[FilesystemEvent],
22
- on_exit: Optional[OutputHandler[Exception]] = None,
23
- ):
24
- self._events = events
25
- self._on_event = on_event
26
- self._on_exit = on_exit
27
-
28
- self._wait = asyncio.create_task(self._handle_events())
29
-
30
- async def stop(self):
31
- """
32
- Stop watching the directory.
33
- """
34
- self._wait.cancel()
35
- # BUG: In Python 3.8 closing async generator can throw RuntimeError.
36
- # await self._events.aclose()
37
-
38
- async def _iterate_events(self):
39
- try:
40
- async for event in self._events:
41
- if event.HasField("filesystem"):
42
- event_type = map_event_type(event.filesystem.type)
43
- if event_type:
44
- yield FilesystemEvent(
45
- name=event.filesystem.name,
46
- type=event_type,
47
- )
48
- except Exception as e:
49
- raise handle_rpc_exception(e)
50
-
51
- async def _handle_events(self):
52
- try:
53
- async for event in self._iterate_events():
54
- cb = self._on_event(event)
55
- if inspect.isawaitable(cb):
56
- await cb
57
- except Exception as e:
58
- if self._on_exit:
59
- cb = self._on_exit(e)
60
- if inspect.isawaitable(cb):
61
- await cb
1
+ import asyncio
2
+ import inspect
3
+ from typing import Any, AsyncGenerator, Optional
4
+
5
+ from ...generated.api_pb2 import WatchDirResponse
6
+ from ...generated.rpc import handle_rpc_exception
7
+ from ...sandbox.filesystem.watch_handle import FilesystemEvent, map_event_type
8
+ from ...sandbox_async.utils import OutputHandler
9
+
10
+
11
+ class AsyncWatchHandle:
12
+ """
13
+ Handle for watching a directory in the sandbox filesystem.
14
+
15
+ Use `.stop()` to stop watching the directory.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ events: AsyncGenerator[WatchDirResponse, Any],
21
+ on_event: OutputHandler[FilesystemEvent],
22
+ on_exit: Optional[OutputHandler[Exception]] = None,
23
+ ):
24
+ self._events = events
25
+ self._on_event = on_event
26
+ self._on_exit = on_exit
27
+
28
+ self._wait = asyncio.create_task(self._handle_events())
29
+
30
+ async def stop(self):
31
+ """
32
+ Stop watching the directory.
33
+ """
34
+ self._wait.cancel()
35
+ # BUG: In Python 3.8 closing async generator can throw RuntimeError.
36
+ # await self._events.aclose()
37
+
38
+ async def _iterate_events(self):
39
+ try:
40
+ async for event in self._events:
41
+ if event.HasField("filesystem"):
42
+ event_type = map_event_type(event.filesystem.type)
43
+ if event_type:
44
+ yield FilesystemEvent(
45
+ name=event.filesystem.name,
46
+ type=event_type,
47
+ )
48
+ except Exception as e:
49
+ raise handle_rpc_exception(e)
50
+
51
+ async def _handle_events(self):
52
+ try:
53
+ async for event in self._iterate_events():
54
+ cb = self._on_event(event)
55
+ if inspect.isawaitable(cb):
56
+ await cb
57
+ except Exception as e:
58
+ if self._on_exit:
59
+ cb = self._on_exit(e)
60
+ if inspect.isawaitable(cb):
61
+ await cb
@@ -6,10 +6,10 @@ from typing import Dict, List, Optional, TypedDict, overload
6
6
  import aiohttp
7
7
  import httpx
8
8
  from aiohttp import TCPConnector
9
- from typing_extensions import Unpack
9
+ from typing_extensions import Unpack, Self
10
10
 
11
11
  from ..api.client.types import Unset
12
- from ..connection_config import ConnectionConfig, ProxyTypes
12
+ from ..connection_config import ConnectionConfig, ProxyTypes, ApiParams
13
13
  from ..exceptions import SandboxException, request_timeout_error
14
14
  from ..generated.api import ENVD_API_HEALTH_ROUTE, ahandle_envd_api_exception
15
15
  from ..sandbox.main import SandboxSetup
@@ -41,6 +41,8 @@ class AsyncSandboxOpts(TypedDict):
41
41
  envd_version: Optional[str]
42
42
  envd_access_token: Optional[str]
43
43
  connection_config: ConnectionConfig
44
+ object_storage: Optional[Dict[str, str]]
45
+ network_proxy: Optional[Dict[str, any]]
44
46
 
45
47
 
46
48
  class AsyncSandbox(SandboxSetup, SandboxApi):
@@ -101,6 +103,17 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
101
103
  """
102
104
  return self._sandbox_domain
103
105
 
106
+ @property
107
+ def object_storage(self) -> Optional[Dict[str, str]]:
108
+ """
109
+ Object storage configuration returned during sandbox creation (if any).
110
+ Only synchronous sandboxes currently expose this field.
111
+ """
112
+ return self._object_storage
113
+
114
+ def network_proxy(self) -> Optional[Dict[str, object]]:
115
+ return self._network_proxy
116
+
104
117
  @property
105
118
  def envd_api_url(self) -> str:
106
119
  return self._envd_api_url
@@ -126,7 +139,8 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
126
139
  super().__init__()
127
140
 
128
141
  self._connection_config = opts["connection_config"]
129
-
142
+ self._object_storage = opts["object_storage"]
143
+ self._network_proxy = opts["network_proxy"]
130
144
  self._sandbox_id = opts["sandbox_id"]
131
145
  self._sandbox_domain = opts["sandbox_domain"] or self.connection_config.domain
132
146
  debug=self._connection_config.debug
@@ -222,13 +236,16 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
222
236
  timeout: Optional[int] = None,
223
237
  metadata: Optional[Dict[str, str]] = None,
224
238
  envs: Optional[Dict[str, str]] = None,
239
+ object_storage: Optional[Dict[str, str]] = None,
225
240
  api_key: Optional[str] = None,
226
241
  domain: Optional[str] = None,
227
242
  debug: Optional[bool] = None,
243
+ sandbox_id: Optional[str] = None,
228
244
  request_timeout: Optional[float] = None,
229
245
  proxy: Optional[ProxyTypes] = None,
230
246
  secure: Optional[bool] = None,
231
247
  allow_internet_access: Optional[bool] = True,
248
+ net_proxy_country: Optional[str] = None,
232
249
  ):
233
250
  """
234
251
  Create a new sandbox.
@@ -251,11 +268,32 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
251
268
  """
252
269
 
253
270
  connection_headers = {"Authorization": "Bearer root", }
271
+ network_proxy = {}
254
272
  if debug:
255
273
  sandbox_id = "debug_sandbox_id"
256
274
  sandbox_domain = None
257
275
  envd_version = None
258
276
  envd_access_token = None
277
+ elif sandbox_id is not None:
278
+ response = await SandboxApi._cls_get_info(
279
+ sandbox_id,
280
+ api_key=api_key,
281
+ domain=domain,
282
+ debug=debug,
283
+ request_timeout=request_timeout,
284
+ proxy=proxy,
285
+ )
286
+
287
+ sandbox_domain = response.sandbox_domain
288
+ envd_version = response.envd_version
289
+ envd_access_token = response._envd_access_token
290
+ object_storage = response.object_storage
291
+ network_proxy = response.network_proxy
292
+
293
+ if response._envd_access_token is not None and not isinstance(
294
+ response._envd_access_token, Unset
295
+ ):
296
+ connection_headers["X-Access-Token"] = response._envd_access_token
259
297
  else:
260
298
  response = await SandboxApi._create_sandbox(
261
299
  template=template or cls.default_template,
@@ -269,12 +307,16 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
269
307
  secure=secure,
270
308
  proxy=proxy,
271
309
  allow_internet_access=allow_internet_access,
310
+ object_storage=object_storage,
311
+ net_proxy_country=net_proxy_country,
272
312
  )
273
313
 
274
314
  sandbox_id = response.sandbox_id
275
315
  sandbox_domain = response.sandbox_domain
276
316
  envd_version = response.envd_version
277
317
  envd_access_token = response.envd_access_token
318
+ object_storage = response.object_storage
319
+ network_proxy = response.network_proxy
278
320
 
279
321
  if envd_access_token is not None and not isinstance(
280
322
  envd_access_token, Unset
@@ -297,9 +339,11 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
297
339
  envd_version=envd_version,
298
340
  envd_access_token=envd_access_token,
299
341
  connection_config=connection_config,
342
+ object_storage=object_storage,
343
+ network_proxy=network_proxy
300
344
  )
301
- timeout = 5.0
302
- interval = 0.1
345
+ timeout = 10.0
346
+ interval = 0.3
303
347
  elapsed = 0.0
304
348
 
305
349
  while elapsed <= timeout:
@@ -312,69 +356,158 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
312
356
  time.sleep(interval)
313
357
  elapsed += interval
314
358
  else:
315
- print("connect "+sandbox_domain+ENVD_API_HEALTH_ROUTE +" timeout 5s")
359
+ print("connect "+sandbox_domain+ENVD_API_HEALTH_ROUTE +" timeout 10s")
316
360
  return sanbox
317
361
 
362
+ @overload
363
+ async def connect(
364
+ self,
365
+ timeout: Optional[int] = None,
366
+ **opts: Unpack[ApiParams],
367
+ ) -> Self:
368
+ """
369
+ Connect to a sandbox. If the sandbox is paused, it will be automatically resumed.
370
+ Sandbox must be either running or be paused.
371
+
372
+ With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
373
+
374
+ :param timeout: Timeout for the sandbox in **seconds**
375
+ For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
376
+ :return: A running sandbox instance
377
+
378
+ @example
379
+ ```python
380
+ sandbox = await AsyncSandbox.create()
381
+ await sandbox.beta_pause()
382
+
383
+ # Another code block
384
+ same_sandbox = await sandbox.connect()
385
+ ```
386
+ """
387
+ ...
388
+
389
+ @overload
318
390
  @classmethod
319
391
  async def connect(
320
- cls,
321
- sandbox_id: str,
322
- api_key: Optional[str] = None,
323
- domain: Optional[str] = None,
324
- debug: Optional[bool] = None,
325
- proxy: Optional[ProxyTypes] = None,
326
- ):
392
+ cls,
393
+ sandbox_id: str,
394
+ timeout: Optional[int] = None,
395
+ **opts: Unpack[ApiParams],
396
+ ) -> Self:
327
397
  """
328
- Connect to an existing sandbox.
398
+ Connect to a sandbox. If the sandbox is paused, it will be automatically resumed.
399
+ Sandbox must be either running or be paused.
400
+
329
401
  With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
330
402
 
331
403
  :param sandbox_id: Sandbox ID
332
- :param api_key: SBX API Key to use for authentication, defaults to `SBX_API_KEY` environment variable
333
- :param proxy: Proxy to use for the request and for the **requests made to the returned sandbox**
334
-
335
- :return: sandbox instance for the existing sandbox
404
+ :param timeout: Timeout for the sandbox in **seconds**
405
+ For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
406
+ :return: A running sandbox instance
336
407
 
337
408
  @example
338
409
  ```python
339
410
  sandbox = await AsyncSandbox.create()
340
- sandbox_id = sandbox.sandbox_id
411
+ await AsyncSandbox.beta_pause(sandbox.sandbox_id)
341
412
 
342
413
  # Another code block
343
- same_sandbox = await AsyncSandbox.connect(sandbox_id)
414
+ same_sandbox = await AsyncSandbox.connect(sandbox.sandbox_id))
415
+ ```
344
416
  """
417
+ ...
345
418
 
346
- connection_headers = {"Authorization": "Bearer root"}
419
+ @class_method_variant("_cls_connect")
420
+ async def connect(
421
+ self,
422
+ timeout: Optional[int] = None,
423
+ **opts: Unpack[ApiParams],
424
+ ) -> Self:
425
+ """
426
+ Connect to a sandbox. If the sandbox is paused, it will be automatically resumed.
427
+ Sandbox must be either running or be paused.
347
428
 
348
- response = await SandboxApi._cls_get_info(
349
- sandbox_id,
350
- api_key=api_key,
351
- domain=domain,
352
- debug=debug,
353
- proxy=proxy,
354
- )
429
+ With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
355
430
 
356
- if response._envd_access_token is not None and not isinstance(
357
- response._envd_access_token, Unset
358
- ):
359
- connection_headers["X-Access-Token"] = response._envd_access_token
360
- print("connection_headers:"+str(connection_headers))
431
+ :param timeout: Timeout for the sandbox in **seconds**
432
+ For running sandboxes, the timeout will update only if the new timeout is longer than the existing one.
433
+ :return: A running sandbox instance
361
434
 
362
- connection_config = ConnectionConfig(
363
- api_key=api_key,
364
- domain=domain,
365
- debug=debug,
366
- headers=connection_headers,
367
- proxy=proxy,
368
- )
435
+ @example
436
+ ```python
437
+ sandbox = await AsyncSandbox.create()
438
+ await sandbox.beta_pause()
369
439
 
370
- return cls(
371
- sandbox_id=sandbox_id,
372
- sandbox_domain=response.sandbox_domain,
373
- connection_config=connection_config,
374
- envd_version=response.envd_version,
375
- envd_access_token=response._envd_access_token,
440
+ # Another code block
441
+ same_sandbox = await sandbox.connect()
442
+ ```
443
+ """
444
+ await SandboxApi._cls_connect(
445
+ sandbox_id=self.sandbox_id,
446
+ timeout=timeout,
447
+ **opts,
376
448
  )
377
449
 
450
+ return self
451
+ # @classmethod
452
+ # async def connect(
453
+ # cls,
454
+ # sandbox_id: str,
455
+ # api_key: Optional[str] = None,
456
+ # domain: Optional[str] = None,
457
+ # debug: Optional[bool] = None,
458
+ # proxy: Optional[ProxyTypes] = None,
459
+ # ):
460
+ # """
461
+ # Connect to an existing sandbox.
462
+ # With sandbox ID you can connect to the same sandbox from different places or environments (serverless functions, etc).
463
+ #
464
+ # :param sandbox_id: Sandbox ID
465
+ # :param api_key: SBX API Key to use for authentication, defaults to `SBX_API_KEY` environment variable
466
+ # :param proxy: Proxy to use for the request and for the **requests made to the returned sandbox**
467
+ #
468
+ # :return: sandbox instance for the existing sandbox
469
+ #
470
+ # @example
471
+ # ```python
472
+ # sandbox = await AsyncSandbox.create()
473
+ # sandbox_id = sandbox.sandbox_id
474
+ #
475
+ # # Another code block
476
+ # same_sandbox = await AsyncSandbox.connect(sandbox_id)
477
+ # """
478
+ #
479
+ # connection_headers = {"Authorization": "Bearer root"}
480
+ #
481
+ # response = await SandboxApi._cls_get_info(
482
+ # sandbox_id,
483
+ # api_key=api_key,
484
+ # domain=domain,
485
+ # debug=debug,
486
+ # proxy=proxy,
487
+ # )
488
+ #
489
+ # if response._envd_access_token is not None and not isinstance(
490
+ # response._envd_access_token, Unset
491
+ # ):
492
+ # connection_headers["X-Access-Token"] = response._envd_access_token
493
+ # print("connection_headers:"+str(connection_headers))
494
+ #
495
+ # connection_config = ConnectionConfig(
496
+ # api_key=api_key,
497
+ # domain=domain,
498
+ # debug=debug,
499
+ # headers=connection_headers,
500
+ # proxy=proxy,
501
+ # )
502
+ #
503
+ # return cls(
504
+ # sandbox_id=sandbox_id,
505
+ # sandbox_domain=response.sandbox_domain,
506
+ # connection_config=connection_config,
507
+ # envd_version=response.envd_version,
508
+ # envd_access_token=response._envd_access_token,
509
+ # )
510
+
378
511
  async def __aenter__(self):
379
512
  if self._session.closed:
380
513
  connector = TCPConnector(
@@ -645,3 +778,52 @@ class AsyncSandbox(SandboxSetup, SandboxApi):
645
778
  end=end,
646
779
  **config_dict,
647
780
  )
781
+
782
+ @overload
783
+ async def beta_pause(
784
+ self,
785
+ **opts: Unpack[ApiParams],
786
+ ) -> None:
787
+ """
788
+ [BETA] This feature is in beta and may change in the future.
789
+
790
+ Pause the sandbox.
791
+
792
+ :return: Sandbox ID that can be used to resume the sandbox
793
+ """
794
+ ...
795
+
796
+ @overload
797
+ @staticmethod
798
+ async def beta_pause(
799
+ sandbox_id: str,
800
+ **opts: Unpack[ApiParams],
801
+ ) -> None:
802
+ """
803
+ [BETA] This feature is in beta and may change in the future.
804
+
805
+ Pause the sandbox specified by sandbox ID.
806
+
807
+ :param sandbox_id: Sandbox ID
808
+
809
+ :return: Sandbox ID that can be used to resume the sandbox
810
+ """
811
+ ...
812
+
813
+ @class_method_variant("_cls_pause")
814
+ async def beta_pause(
815
+ self,
816
+ **opts: Unpack[ApiParams],
817
+ ) -> None:
818
+ """
819
+ [BETA] This feature is in beta and may change in the future.
820
+
821
+ Pause the sandbox.
822
+
823
+ :return: Sandbox ID that can be used to resume the sandbox
824
+ """
825
+
826
+ await SandboxApi._cls_pause(
827
+ sandbox_id=self.sandbox_id,
828
+ **opts,
829
+ )
@@ -1,9 +1,14 @@
1
1
  import datetime
2
2
  import urllib.parse
3
- from typing import Dict, List, Optional
3
+ from typing import Dict, List, Optional, Unpack, TYPE_CHECKING
4
4
 
5
5
  from packaging.version import Version
6
6
 
7
+ if TYPE_CHECKING:
8
+ from . import AsyncSandbox
9
+
10
+ from ..api.client.api.sandboxes import post_sandboxes_sandbox_id_pause, post_sandboxes_sandbox_id_connect
11
+ from ..api.client.models.connect_sandbox import ConnectSandbox
7
12
  from ..api import AsyncApiClient, SandboxCreateResponse, handle_api_exception
8
13
  from ..api.client.api.sandboxes import (
9
14
  delete_sandboxes_sandbox_id,
@@ -14,8 +19,8 @@ from ..api.client.api.sandboxes import (
14
19
  post_sandboxes_sandbox_id_timeout,
15
20
  )
16
21
  from ..api.client.models import Error, NewSandbox, PostSandboxesSandboxIDTimeoutBody
17
- from ..connection_config import ConnectionConfig, ProxyTypes
18
- from ..exceptions import SandboxException, TemplateException
22
+ from ..connection_config import ConnectionConfig, ProxyTypes, ApiParams
23
+ from ..exceptions import SandboxException, TemplateException, NotFoundException
19
24
  from ..sandbox.sandbox_api import (
20
25
  ListedSandbox,
21
26
  SandboxApiBase,
@@ -97,6 +102,9 @@ class SandboxApi(SandboxApiBase):
97
102
  memory_mb=sandbox.memory_mb,
98
103
  started_at=sandbox.started_at,
99
104
  end_at=sandbox.end_at,
105
+ object_storage=sandbox.object_storage,
106
+ uptime=sandbox.uptime,
107
+ timeout=sandbox.timeout,
100
108
  )
101
109
  for sandbox in res.parsed
102
110
  ]
@@ -160,6 +168,9 @@ class SandboxApi(SandboxApiBase):
160
168
  end_at=res.parsed.end_at,
161
169
  envd_version=res.parsed.envd_version,
162
170
  _envd_access_token=res.parsed.envd_access_token,
171
+ object_storage=res.parsed.object_storage,
172
+ uptime=res.parsed.uptime,
173
+ timeout=res.parsed.timeout,
163
174
  )
164
175
 
165
176
  @classmethod
@@ -256,6 +267,8 @@ class SandboxApi(SandboxApiBase):
256
267
  headers: Optional[Dict[str, str]] = None,
257
268
  proxy: Optional[ProxyTypes] = None,
258
269
  allow_internet_access: Optional[bool] = True,
270
+ object_storage: Optional[Dict[str, str]] = None,
271
+ net_proxy_country:Optional[str] = None,
259
272
  ) -> SandboxCreateResponse:
260
273
  config = ConnectionConfig(
261
274
  api_key=api_key,
@@ -279,6 +292,8 @@ class SandboxApi(SandboxApiBase):
279
292
  secure=secure or False,
280
293
  allow_internet_access=allow_internet_access,
281
294
  is_async=False,
295
+ object_storage=object_storage,
296
+ net_proxy_country=net_proxy_country,
282
297
  ),
283
298
  client=api_client,
284
299
  )
@@ -301,6 +316,8 @@ class SandboxApi(SandboxApiBase):
301
316
  sandbox_domain=res.parsed.domain,
302
317
  envd_version=res.parsed.envd_version,
303
318
  envd_access_token=res.parsed.envd_access_token,
319
+ object_storage=res.parsed.object_storage,
320
+ network_proxy=res.parsed.network_proxy,
304
321
  )
305
322
 
306
323
  @classmethod
@@ -363,3 +380,107 @@ class SandboxApi(SandboxApiBase):
363
380
  )
364
381
  for metric in res.parsed
365
382
  ]
383
+
384
+ @classmethod
385
+ async def _cls_pause(
386
+ cls,
387
+ sandbox_id: str,
388
+ **opts: Unpack[ApiParams],
389
+ ) -> str:
390
+ config = ConnectionConfig(**opts)
391
+
392
+ async with AsyncApiClient(
393
+ config,
394
+ limits=SandboxApiBase._limits,
395
+ ) as api_client:
396
+ res = await post_sandboxes_sandbox_id_pause.asyncio_detailed(
397
+ sandbox_id,
398
+ client=api_client,
399
+ )
400
+
401
+ if res.status_code == 404:
402
+ raise NotFoundException(f"Sandbox {sandbox_id} not found")
403
+
404
+ if res.status_code == 409:
405
+ return sandbox_id
406
+
407
+ if res.status_code >= 300:
408
+ raise handle_api_exception(res)
409
+
410
+ # Check if res.parse is Error
411
+ if isinstance(res.parsed, Error):
412
+ raise SandboxException(f"{res.parsed.message}: Request failed")
413
+
414
+ return sandbox_id
415
+
416
+ @classmethod
417
+ async def _cls_connect(
418
+ cls,
419
+ sandbox_id: str,
420
+ timeout: Optional[int] = None,
421
+ **opts: Unpack[ApiParams],
422
+ ) -> "AsyncSandbox":
423
+ timeout = timeout or SandboxApiBase.default_sandbox_timeout
424
+
425
+ # Sandbox is not running, resume it
426
+ config = ConnectionConfig(**opts)
427
+
428
+ async with AsyncApiClient(
429
+ config,
430
+ limits=SandboxApiBase._limits,
431
+ ) as api_client:
432
+ res = await post_sandboxes_sandbox_id_connect.asyncio_detailed(
433
+ sandbox_id,
434
+ client=api_client,
435
+ body=ConnectSandbox(timeout=timeout),
436
+ )
437
+
438
+ if res.status_code == 404:
439
+ raise NotFoundException(f"Paused sandbox {sandbox_id} not found")
440
+
441
+ if res.status_code >= 300:
442
+ raise handle_api_exception(res)
443
+
444
+ # Check if res.parse is Error
445
+ if isinstance(res.parsed, Error):
446
+ raise SandboxException(f"{res.parsed.message}: Request failed")
447
+
448
+ # Extract information from API response and create a full AsyncSandbox instance
449
+ # Use delayed import to avoid circular dependency
450
+ from . import AsyncSandbox
451
+
452
+ response = res.parsed
453
+ if response is None:
454
+ raise SandboxException("Connect response is None")
455
+
456
+ connection_headers = {"Authorization": "Bearer root"}
457
+
458
+ # Extract fields from API response
459
+ sandbox_domain = response.domain if hasattr(response, 'domain') and response.domain is not None else None
460
+ envd_version = response.envd_version if hasattr(response, 'envd_version') else None
461
+ envd_access_token = None
462
+ if hasattr(response, 'envd_access_token'):
463
+ from ..api.client.types import Unset
464
+ if response.envd_access_token is not None and not isinstance(response.envd_access_token, Unset):
465
+ envd_access_token = response.envd_access_token
466
+ connection_headers["X-Access-Token"] = envd_access_token
467
+
468
+ connection_config = ConnectionConfig(
469
+ api_key=config.api_key,
470
+ domain=config.domain,
471
+ debug=config.debug,
472
+ request_timeout=config.request_timeout,
473
+ headers=connection_headers,
474
+ proxy=config.proxy,
475
+ )
476
+
477
+ # Create and return a full AsyncSandbox instance
478
+ sandbox = AsyncSandbox(
479
+ sandbox_id=sandbox_id,
480
+ sandbox_domain=sandbox_domain,
481
+ envd_version=envd_version,
482
+ envd_access_token=envd_access_token,
483
+ connection_config=connection_config,
484
+ )
485
+
486
+ return sandbox