scalebox-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. scalebox/__init__.py +80 -0
  2. scalebox/api/__init__.py +128 -0
  3. scalebox/api/client/__init__.py +8 -0
  4. scalebox/api/client/api/__init__.py +1 -0
  5. scalebox/api/client/api/sandboxes/__init__.py +0 -0
  6. scalebox/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. scalebox/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. scalebox/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +214 -0
  12. scalebox/api/client/api/sandboxes/get_v2_sandboxes.py +229 -0
  13. scalebox/api/client/api/sandboxes/post_sandboxes.py +174 -0
  14. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
  15. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +182 -0
  16. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +190 -0
  17. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +194 -0
  18. scalebox/api/client/client.py +288 -0
  19. scalebox/api/client/errors.py +16 -0
  20. scalebox/api/client/models/__init__.py +81 -0
  21. scalebox/api/client/models/build_log_entry.py +79 -0
  22. scalebox/api/client/models/created_access_token.py +100 -0
  23. scalebox/api/client/models/created_team_api_key.py +166 -0
  24. scalebox/api/client/models/error.py +67 -0
  25. scalebox/api/client/models/identifier_masking_details.py +83 -0
  26. scalebox/api/client/models/listed_sandbox.py +138 -0
  27. scalebox/api/client/models/log_level.py +11 -0
  28. scalebox/api/client/models/new_access_token.py +59 -0
  29. scalebox/api/client/models/new_sandbox.py +125 -0
  30. scalebox/api/client/models/new_team_api_key.py +59 -0
  31. scalebox/api/client/models/node.py +154 -0
  32. scalebox/api/client/models/node_detail.py +152 -0
  33. scalebox/api/client/models/node_status.py +11 -0
  34. scalebox/api/client/models/node_status_change.py +61 -0
  35. scalebox/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  36. scalebox/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  37. scalebox/api/client/models/resumed_sandbox.py +68 -0
  38. scalebox/api/client/models/sandbox.py +125 -0
  39. scalebox/api/client/models/sandbox_detail.py +178 -0
  40. scalebox/api/client/models/sandbox_log.py +70 -0
  41. scalebox/api/client/models/sandbox_logs.py +73 -0
  42. scalebox/api/client/models/sandbox_metric.py +110 -0
  43. scalebox/api/client/models/sandbox_state.py +9 -0
  44. scalebox/api/client/models/sandboxes_with_metrics.py +59 -0
  45. scalebox/api/client/models/team.py +83 -0
  46. scalebox/api/client/models/team_api_key.py +158 -0
  47. scalebox/api/client/models/team_user.py +68 -0
  48. scalebox/api/client/models/template.py +179 -0
  49. scalebox/api/client/models/template_build.py +117 -0
  50. scalebox/api/client/models/template_build_file_upload.py +70 -0
  51. scalebox/api/client/models/template_build_request.py +115 -0
  52. scalebox/api/client/models/template_build_request_v2.py +88 -0
  53. scalebox/api/client/models/template_build_start_v2.py +114 -0
  54. scalebox/api/client/models/template_build_status.py +11 -0
  55. scalebox/api/client/models/template_step.py +91 -0
  56. scalebox/api/client/models/template_update_request.py +59 -0
  57. scalebox/api/client/models/update_team_api_key.py +59 -0
  58. scalebox/api/client/py.typed +1 -0
  59. scalebox/api/client/types.py +46 -0
  60. scalebox/api/metadata.py +19 -0
  61. scalebox/cli.py +125 -0
  62. scalebox/client/__init__.py +0 -0
  63. scalebox/client/aclient.py +57 -0
  64. scalebox/client/api.proto +460 -0
  65. scalebox/client/buf.gen.yaml +8 -0
  66. scalebox/client/client.py +102 -0
  67. scalebox/client/requirements.txt +5 -0
  68. scalebox/code_interpreter/__init__.py +12 -0
  69. scalebox/code_interpreter/charts.py +230 -0
  70. scalebox/code_interpreter/code_interpreter_async.py +369 -0
  71. scalebox/code_interpreter/code_interpreter_sync.py +317 -0
  72. scalebox/code_interpreter/constants.py +3 -0
  73. scalebox/code_interpreter/exceptions.py +13 -0
  74. scalebox/code_interpreter/models.py +485 -0
  75. scalebox/connection_config.py +92 -0
  76. scalebox/csx_connect/__init__.py +1 -0
  77. scalebox/csx_connect/client.py +485 -0
  78. scalebox/csx_desktop/__init__.py +0 -0
  79. scalebox/csx_desktop/main.py +651 -0
  80. scalebox/exceptions.py +83 -0
  81. scalebox/generated/__init__.py +0 -0
  82. scalebox/generated/api.py +61 -0
  83. scalebox/generated/api_pb2.py +203 -0
  84. scalebox/generated/api_pb2.pyi +956 -0
  85. scalebox/generated/api_pb2_connect.py +1456 -0
  86. scalebox/generated/rpc.py +50 -0
  87. scalebox/generated/versions.py +3 -0
  88. scalebox/requirements.txt +36 -0
  89. scalebox/sandbox/__init__.py +0 -0
  90. scalebox/sandbox/commands/__init__.py +0 -0
  91. scalebox/sandbox/commands/command_handle.py +69 -0
  92. scalebox/sandbox/commands/main.py +39 -0
  93. scalebox/sandbox/filesystem/__init__.py +0 -0
  94. scalebox/sandbox/filesystem/filesystem.py +95 -0
  95. scalebox/sandbox/filesystem/watch_handle.py +60 -0
  96. scalebox/sandbox/main.py +139 -0
  97. scalebox/sandbox/sandbox_api.py +91 -0
  98. scalebox/sandbox/signature.py +40 -0
  99. scalebox/sandbox/utils.py +34 -0
  100. scalebox/sandbox_async/__init__.py +1 -0
  101. scalebox/sandbox_async/commands/command.py +307 -0
  102. scalebox/sandbox_async/commands/command_handle.py +187 -0
  103. scalebox/sandbox_async/commands/pty.py +187 -0
  104. scalebox/sandbox_async/filesystem/filesystem.py +557 -0
  105. scalebox/sandbox_async/filesystem/watch_handle.py +61 -0
  106. scalebox/sandbox_async/main.py +646 -0
  107. scalebox/sandbox_async/sandbox_api.py +365 -0
  108. scalebox/sandbox_async/utils.py +7 -0
  109. scalebox/sandbox_sync/__init__.py +2 -0
  110. scalebox/sandbox_sync/commands/__init__.py +0 -0
  111. scalebox/sandbox_sync/commands/command.py +300 -0
  112. scalebox/sandbox_sync/commands/command_handle.py +150 -0
  113. scalebox/sandbox_sync/commands/pty.py +181 -0
  114. scalebox/sandbox_sync/filesystem/__init__.py +0 -0
  115. scalebox/sandbox_sync/filesystem/filesystem.py +543 -0
  116. scalebox/sandbox_sync/filesystem/watch_handle.py +66 -0
  117. scalebox/sandbox_sync/main.py +790 -0
  118. scalebox/sandbox_sync/sandbox_api.py +356 -0
  119. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -0
  120. scalebox/test/README.md +329 -0
  121. scalebox/test/__init__.py +0 -0
  122. scalebox/test/aclient.py +72 -0
  123. scalebox/test/code_interpreter_centext.py +21 -0
  124. scalebox/test/code_interpreter_centext_sync.py +21 -0
  125. scalebox/test/code_interpreter_test.py +34 -0
  126. scalebox/test/code_interpreter_test_sync.py +34 -0
  127. scalebox/test/run_all_validation_tests.py +334 -0
  128. scalebox/test/run_code_interpreter_tests.sh +67 -0
  129. scalebox/test/run_tests.sh +230 -0
  130. scalebox/test/test_basic.py +78 -0
  131. scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -0
  132. scalebox/test/test_code_interpreter_e2basync_comprehensive.py +2655 -0
  133. scalebox/test/test_code_interpreter_e2bsync_comprehensive.py +3416 -0
  134. scalebox/test/test_code_interpreter_sync_comprehensive.py +3412 -0
  135. scalebox/test/test_e2b_first.py +11 -0
  136. scalebox/test/test_sandbox_async_comprehensive.py +738 -0
  137. scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -0
  138. scalebox/test/test_sandbox_sync_comprehensive.py +770 -0
  139. scalebox/test/test_sandbox_usage_examples.py +987 -0
  140. scalebox/test/testacreate.py +24 -0
  141. scalebox/test/testagetinfo.py +18 -0
  142. scalebox/test/testcodeinterpreter_async.py +508 -0
  143. scalebox/test/testcodeinterpreter_sync.py +239 -0
  144. scalebox/test/testcomputeuse.py +243 -0
  145. scalebox/test/testnovnc.py +12 -0
  146. scalebox/test/testsandbox_async.py +118 -0
  147. scalebox/test/testsandbox_sync.py +38 -0
  148. scalebox/utils/__init__.py +0 -0
  149. scalebox/utils/httpcoreclient.py +297 -0
  150. scalebox/utils/httpxclient.py +403 -0
  151. scalebox/version.py +16 -0
  152. scalebox_sdk-0.1.0.dist-info/METADATA +292 -0
  153. scalebox_sdk-0.1.0.dist-info/RECORD +157 -0
  154. scalebox_sdk-0.1.0.dist-info/WHEEL +5 -0
  155. scalebox_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  156. scalebox_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  157. scalebox_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,50 @@
1
+ import base64
2
+ from typing import Optional
3
+
4
+ from ..connection_config import Username, default_username
5
+ from ..csx_connect.client import Code, ConnectException
6
+ from ..exceptions import (
7
+ AuthenticationException,
8
+ InvalidArgumentException,
9
+ NotFoundException,
10
+ RateLimitException,
11
+ SandboxException,
12
+ TimeoutException,
13
+ sandbox_timeout_exception,
14
+ )
15
+
16
+
17
+ def handle_rpc_exception(e: Exception):
18
+ if isinstance(e, ConnectException):
19
+ if e.status == Code.invalid_argument:
20
+ return InvalidArgumentException(e.message)
21
+ elif e.status == Code.unauthenticated:
22
+ return AuthenticationException(e.message)
23
+ elif e.status == Code.not_found:
24
+ return NotFoundException(e.message)
25
+ elif e.status == Code.unavailable:
26
+ return sandbox_timeout_exception(e.message)
27
+ elif e.status == Code.resource_exhausted:
28
+ return RateLimitException(
29
+ f"{e.message}: Rate limit exceeded, please try again later."
30
+ )
31
+ elif e.status == Code.canceled:
32
+ return TimeoutException(
33
+ f"{e.message}: This error is likely due to exceeding 'request_timeout'. You can pass the request timeout value as an option when making the request."
34
+ )
35
+ elif e.status == Code.deadline_exceeded:
36
+ return TimeoutException(
37
+ f"{e.message}: This error is likely due to exceeding 'timeout' — the total time a long running request (like process or directory watch) can be active. It can be modified by passing 'timeout' when making the request. Use '0' to disable the timeout."
38
+ )
39
+ else:
40
+ return SandboxException(f"{e.status}: {e.message}")
41
+ else:
42
+ return e
43
+
44
+
45
+ def authentication_header(user: Optional[Username] = None):
46
+ value = f"{user if user is not None else default_username}:"
47
+
48
+ encoded = base64.b64encode(value.encode("utf-8")).decode("utf-8")
49
+
50
+ return {"Authorization": f"Basic {encoded}"}
@@ -0,0 +1,3 @@
1
+ from packaging.version import Version
2
+
3
+ ENVD_VERSION_RECURSIVE_WATCH = Version("0.1.4")
@@ -0,0 +1,36 @@
1
+ # HTTP and networking
2
+ httpx>=0.24
3
+ aiohttp>=3.8.0
4
+ urllib3>=1.26.0
5
+ httpcore>=0.15.0
6
+
7
+ # gRPC and protocol buffers
8
+ grpcio>=1.74.0
9
+ grpcio-tools>=1.74.0
10
+ protobuf>=4.21
11
+
12
+ # ConnectRPC
13
+ connect-python[protobuf]>=0.4.2
14
+
15
+ # Data structures and serialization
16
+ attrs>=21.4.0
17
+ dataclasses-json>=0.5.0
18
+
19
+ # Date and time handling
20
+ python-dateutil>=2.8.0
21
+
22
+ # Retry and resilience
23
+ tenacity>=8.0.0
24
+
25
+ # Version handling
26
+ packaging>=21.0
27
+
28
+ # Type extensions
29
+ typing-extensions>=4.0.0
30
+
31
+ # Async utilities
32
+ asyncio-mqtt>=0.11.0
33
+
34
+ # Development and testing (optional)
35
+ pytest>=7.0.0
36
+ pytest-asyncio>=0.21.0
File without changes
File without changes
@@ -0,0 +1,69 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ from ...exceptions import SandboxException
5
+
6
+ Stdout = str
7
+ """
8
+ Command stdout output.
9
+ """
10
+ Stderr = str
11
+ """
12
+ Command stderr output.
13
+ """
14
+ PtyOutput = bytes
15
+ """
16
+ Pty output.
17
+ """
18
+
19
+
20
+ @dataclass
21
+ class PtySize:
22
+ """
23
+ Pseudo-terminal size.
24
+ """
25
+
26
+ rows: int
27
+ """
28
+ Number of rows.
29
+ """
30
+ cols: int
31
+ """
32
+ Number of columns.
33
+ """
34
+
35
+
36
+ @dataclass
37
+ class CommandResult:
38
+ """
39
+ Command execution result.
40
+ """
41
+
42
+ stderr: str
43
+ """
44
+ Command stderr output.
45
+ """
46
+ stdout: str
47
+ """
48
+ Command stdout output.
49
+ """
50
+ exit_code: int
51
+ """
52
+ Command exit code.
53
+
54
+ `0` if the command finished successfully.
55
+ """
56
+ error: Optional[str]
57
+ """
58
+ Error message from command execution if it failed.
59
+ """
60
+
61
+
62
+ @dataclass
63
+ class CommandExitException(SandboxException, CommandResult):
64
+ """
65
+ Exception raised when a command exits with a non-zero exit code.
66
+ """
67
+
68
+ def __str__(self):
69
+ return f"Command exited with code {self.exit_code} and error:\n{self.stderr}"
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, List, Optional
3
+
4
+
5
+ @dataclass
6
+ class ProcessInfo:
7
+ """
8
+ Information about a command, PTY session or start command running in the sandbox as process.
9
+ """
10
+
11
+ pid: int
12
+ """
13
+ Process ID.
14
+ """
15
+
16
+ tag: Optional[str]
17
+ """
18
+ Custom tag used for identifying special commands like start command in the custom template.
19
+ """
20
+
21
+ cmd: str
22
+ """
23
+ Command that was executed.
24
+ """
25
+
26
+ args: List[str]
27
+ """
28
+ Command arguments.
29
+ """
30
+
31
+ envs: Dict[str, str]
32
+ """
33
+ Environment variables used for the command.
34
+ """
35
+
36
+ cwd: Optional[str]
37
+ """
38
+ Executed command working directory.
39
+ """
File without changes
@@ -0,0 +1,95 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from typing import IO, Optional, Union
5
+
6
+ from ...generated import api_pb2
7
+
8
+
9
+ class FileType(Enum):
10
+ """
11
+ Enum representing the type of filesystem object.
12
+ """
13
+
14
+ FILE = "file"
15
+ """
16
+ Filesystem object is a file.
17
+ """
18
+ DIR = "dir"
19
+ """
20
+ Filesystem object is a directory.
21
+ """
22
+
23
+
24
+ def map_file_type(ft: api_pb2.FileType):
25
+ if ft == api_pb2.FileType.FILE_TYPE_FILE:
26
+ return FileType.FILE
27
+ elif ft == api_pb2.FileType.FILE_TYPE_DIRECTORY:
28
+ return FileType.DIR
29
+
30
+
31
+ @dataclass
32
+ class WriteInfo:
33
+ """
34
+ Sandbox filesystem object information.
35
+ """
36
+
37
+ name: str
38
+ """
39
+ Name of the filesystem object.
40
+ """
41
+ type: Optional[FileType]
42
+ """
43
+ Type of the filesystem object.
44
+ """
45
+ path: str
46
+ """
47
+ Path to the filesystem object.
48
+ """
49
+
50
+
51
+ @dataclass
52
+ class EntryInfo(WriteInfo):
53
+ """
54
+ Extended sandbox filesystem object information.
55
+ """
56
+
57
+ size: int
58
+ """
59
+ Size of the filesystem object in bytes.
60
+ """
61
+ mode: int
62
+ """
63
+ File mode and permission bits.
64
+ """
65
+ permissions: str
66
+ """
67
+ String representation of file permissions (e.g. 'rwxr-xr-x').
68
+ """
69
+ owner: str
70
+ """
71
+ Owner of the filesystem object.
72
+ """
73
+ group: str
74
+ """
75
+ Group owner of the filesystem object.
76
+ """
77
+ modified_time: datetime
78
+ """
79
+ Last modification time of the filesystem object.
80
+ """
81
+ symlink_target: Optional[str] = None
82
+ """
83
+ Target of the symlink if the filesystem object is a symlink.
84
+ If the filesystem object is not a symlink, this field is None.
85
+ """
86
+
87
+
88
+ @dataclass
89
+ class WriteEntry:
90
+ """
91
+ Contains path and data of the file to be written to the filesystem.
92
+ """
93
+
94
+ path: str
95
+ data: Union[str, bytes, IO]
@@ -0,0 +1,60 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from ...generated.api_pb2 import EventType
5
+
6
+
7
+ class FilesystemEventType(Enum):
8
+ """
9
+ Enum representing the type of filesystem event.
10
+ """
11
+
12
+ CHMOD = "chmod"
13
+ """
14
+ Filesystem object permissions were changed.
15
+ """
16
+ CREATE = "create"
17
+ """
18
+ Filesystem object was created.
19
+ """
20
+ REMOVE = "remove"
21
+ """
22
+ Filesystem object was removed.
23
+ """
24
+ RENAME = "rename"
25
+ """
26
+ Filesystem object was renamed.
27
+ """
28
+ WRITE = "write"
29
+ """
30
+ Filesystem object was written to.
31
+ """
32
+
33
+
34
+ def map_event_type(event: EventType):
35
+ if event == EventType.EVENT_TYPE_CHMOD:
36
+ return FilesystemEventType.CHMOD
37
+ elif event == EventType.EVENT_TYPE_CREATE:
38
+ return FilesystemEventType.CREATE
39
+ elif event == EventType.EVENT_TYPE_REMOVE:
40
+ return FilesystemEventType.REMOVE
41
+ elif event == EventType.EVENT_TYPE_RENAME:
42
+ return FilesystemEventType.RENAME
43
+ elif event == EventType.EVENT_TYPE_WRITE:
44
+ return FilesystemEventType.WRITE
45
+
46
+
47
+ @dataclass
48
+ class FilesystemEvent:
49
+ """
50
+ Contains information about the filesystem event - the name of the file and the type of the event.
51
+ """
52
+
53
+ name: str
54
+ """
55
+ Relative path to the filesystem object.
56
+ """
57
+ type: FilesystemEventType
58
+ """
59
+ Filesystem operation event type.
60
+ """
@@ -0,0 +1,139 @@
1
+ import urllib.parse
2
+ from abc import ABC, abstractmethod
3
+ from typing import Optional
4
+
5
+ from httpx import Limits
6
+
7
+ from ..connection_config import ConnectionConfig
8
+ from ..generated.api import ENVD_API_FILES_ROUTE
9
+ from .signature import get_signature
10
+
11
+
12
+ class SandboxSetup(ABC):
13
+ _limits = Limits(
14
+ max_keepalive_connections=40,
15
+ max_connections=40,
16
+ keepalive_expiry=300,
17
+ )
18
+
19
+ envd_port = 443
20
+
21
+ default_sandbox_timeout = 300
22
+ default_template = "base"
23
+
24
+ @property
25
+ @abstractmethod
26
+ def connection_config(self) -> ConnectionConfig: ...
27
+
28
+ @property
29
+ @abstractmethod
30
+ def _envd_access_token(self) -> Optional[str]: ...
31
+
32
+ @property
33
+ @abstractmethod
34
+ def envd_api_url(self) -> str: ...
35
+
36
+ @property
37
+ @abstractmethod
38
+ def sandbox_id(self) -> str: ...
39
+
40
+ @property
41
+ @abstractmethod
42
+ def sandbox_domain(self) -> str: ...
43
+
44
+ def _file_url(
45
+ self,
46
+ path: Optional[str] = None,
47
+ user: str = "user",
48
+ signature: Optional[str] = None,
49
+ signature_expiration: Optional[int] = None,
50
+ ) -> str:
51
+ url = urllib.parse.urljoin(self.envd_api_url, ENVD_API_FILES_ROUTE)
52
+ query = {"path": path} if path else {}
53
+ query = {**query, "username": user}
54
+
55
+ if signature:
56
+ query["signature"] = signature
57
+
58
+ if signature_expiration:
59
+ if signature is None:
60
+ raise ValueError("signature_expiration requires signature to be set")
61
+ query["signature_expiration"] = str(signature_expiration)
62
+
63
+ params = urllib.parse.urlencode(
64
+ query,
65
+ quote_via=urllib.parse.quote,
66
+ )
67
+ url = urllib.parse.urljoin(url, f"?{params}")
68
+
69
+ return url
70
+
71
+ def download_url(
72
+ self,
73
+ path: str,
74
+ user: str = "user",
75
+ use_signature_expiration: Optional[int] = None,
76
+ ) -> str:
77
+ """
78
+ Get the URL to download a file from the sandbox.
79
+
80
+ :param path: Path to the file to download
81
+ :param user: User to upload the file as
82
+ :param use_signature_expiration: Expiration time for the signed URL in seconds
83
+
84
+ :return: URL for downloading file
85
+ """
86
+
87
+ use_signature = self._envd_access_token is not None
88
+ if use_signature:
89
+ signature = get_signature(
90
+ path, "read", user, self._envd_access_token, use_signature_expiration
91
+ )
92
+ return self._file_url(
93
+ path, user, signature["signature"], signature["expiration"]
94
+ )
95
+ else:
96
+ return self._file_url(path)
97
+
98
+ def upload_url(
99
+ self,
100
+ path: Optional[str] = None,
101
+ user: str = "user",
102
+ use_signature_expiration: Optional[int] = None,
103
+ ) -> str:
104
+ """
105
+ Get the URL to upload a file to the sandbox.
106
+
107
+ You have to send a POST request to this URL with the file as multipart/form-data.
108
+
109
+ :param path: Path to the file to upload
110
+ :param user: User to upload the file as
111
+ :param use_signature_expiration: Expiration time for the signed URL in seconds
112
+
113
+ :return: URL for uploading file
114
+ """
115
+
116
+ use_signature = self._envd_access_token is not None
117
+ if use_signature:
118
+ signature = get_signature(
119
+ path, "write", user, self._envd_access_token, use_signature_expiration
120
+ )
121
+ return self._file_url(
122
+ path, user, signature["signature"], signature["expiration"]
123
+ )
124
+ else:
125
+ return self._file_url(path)
126
+
127
+ def get_host(self, port: int) -> str:
128
+ """
129
+ Get the host address to connect to the sandbox.
130
+ You can then use this address to connect to the sandbox port from outside the sandbox via HTTP or WebSocket.
131
+
132
+ :param port: Port to connect to
133
+
134
+ :return: Host address to connect to
135
+ """
136
+ if self.connection_config.debug:
137
+ return f"localhost:{port}"
138
+ return f"{self.sandbox_domain}"
139
+ # return f"{port}-{self.sandbox_id}.{self.sandbox_domain}"
@@ -0,0 +1,91 @@
1
+ from abc import ABC
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from typing import Dict, Optional
5
+
6
+ from httpx import Limits
7
+
8
+ from ..api.client.models import SandboxState
9
+
10
+
11
+ @dataclass
12
+ class SandboxInfo:
13
+ """Information about a sandbox."""
14
+
15
+ sandbox_id: str
16
+ """Sandbox ID."""
17
+ sandbox_domain: Optional[str]
18
+ """Domain where the sandbox is hosted."""
19
+ template_id: str
20
+ """Template ID."""
21
+ name: Optional[str]
22
+ """Template name."""
23
+ metadata: Dict[str, str]
24
+ """Saved sandbox metadata."""
25
+ started_at: datetime
26
+ """Sandbox start time."""
27
+ end_at: datetime
28
+ """Sandbox expiration date."""
29
+ envd_version: Optional[str]
30
+ """Envd version."""
31
+ _envd_access_token: Optional[str]
32
+ """Envd access token."""
33
+
34
+
35
+ @dataclass
36
+ class ListedSandbox:
37
+ """Information about a sandbox."""
38
+
39
+ sandbox_id: str
40
+ """Sandbox ID."""
41
+ template_id: str
42
+ """Template ID."""
43
+ name: Optional[str]
44
+ """Template Alias."""
45
+ state: SandboxState
46
+ """Sandbox state."""
47
+ cpu_count: int
48
+ """Sandbox CPU count."""
49
+ memory_mb: int
50
+ """Sandbox Memory size in MB."""
51
+ metadata: Dict[str, str]
52
+ """Saved sandbox metadata."""
53
+ started_at: datetime
54
+ """Sandbox start time."""
55
+ end_at: datetime
56
+
57
+
58
+ @dataclass
59
+ class SandboxQuery:
60
+ """Query parameters for listing sandboxes."""
61
+
62
+ metadata: Optional[dict[str, str]] = None
63
+ """Filter sandboxes by metadata."""
64
+
65
+
66
+ @dataclass
67
+ class SandboxMetrics:
68
+ """Sandbox metrics."""
69
+
70
+ cpu_count: int
71
+ """Number of CPUs."""
72
+ cpu_used_pct: float
73
+ """CPU usage percentage."""
74
+ disk_total: int
75
+ """Total disk space in bytes."""
76
+ disk_used: int
77
+ """Disk used in bytes."""
78
+ mem_total: int
79
+ """Total memory in bytes."""
80
+ mem_used: int
81
+ """Memory used in bytes."""
82
+ timestamp: datetime
83
+ """Timestamp of the metric entry."""
84
+
85
+
86
+ class SandboxApiBase(ABC):
87
+ _limits = Limits(
88
+ max_keepalive_connections=10,
89
+ max_connections=20,
90
+ keepalive_expiry=20,
91
+ )
@@ -0,0 +1,40 @@
1
+ import base64
2
+ import hashlib
3
+ import time
4
+ from typing import Literal, Optional, TypedDict
5
+
6
+ Operation = Literal["read", "write"]
7
+
8
+
9
+ class Signature(TypedDict):
10
+ signature: str
11
+ expiration: Optional[int] # Unix timestamp or None
12
+
13
+
14
+ def get_signature(
15
+ path: str,
16
+ operation: Operation,
17
+ user: str,
18
+ envd_access_token: Optional[str],
19
+ expiration_in_seconds: Optional[int] = None,
20
+ ) -> Signature:
21
+ """
22
+ Generate a v1 signature for sandbox file URLs.
23
+ """
24
+ if not envd_access_token:
25
+ raise ValueError("Access token is not set and signature cannot be generated!")
26
+
27
+ expiration = (
28
+ int(time.time()) + expiration_in_seconds if expiration_in_seconds else None
29
+ )
30
+
31
+ raw = (
32
+ f"{path}:{operation}:{user}:{envd_access_token}"
33
+ if expiration is None
34
+ else f"{path}:{operation}:{user}:{envd_access_token}:{expiration}"
35
+ )
36
+
37
+ digest = hashlib.sha256(raw.encode("utf-8")).digest()
38
+ encoded = base64.b64encode(digest).rstrip(b"=").decode("ascii")
39
+
40
+ return {"signature": f"v1_{encoded}", "expiration": expiration}
@@ -0,0 +1,34 @@
1
+ import functools
2
+ from typing import Any, Optional, Type, TypeVar, cast
3
+
4
+ T = TypeVar("T")
5
+
6
+
7
+ class class_method_variant(object):
8
+ def __init__(self, class_method_name):
9
+ self.class_method_name = class_method_name
10
+
11
+ method: Any
12
+
13
+ def __call__(self, method: T) -> T:
14
+ self.method = method
15
+ return cast(T, self)
16
+
17
+ def __get__(self, obj, objtype: Optional[Type[Any]] = None):
18
+ @functools.wraps(self.method)
19
+ def _wrapper(*args, **kwargs):
20
+ if obj is not None:
21
+ # Method was called as an instance method, e.g.
22
+ # instance.method(...)
23
+ return self.method(obj, *args, **kwargs)
24
+ elif len(args) > 0 and objtype is not None and isinstance(args[0], objtype):
25
+ # Method was called as a class method with the instance as the
26
+ # first argument, e.g. Class.method(instance, ...) which in
27
+ # Python is the same thing as calling an instance method
28
+ return self.method(args[0], *args[1:], **kwargs)
29
+ else:
30
+ # Method was called as a class method, e.g. Class.method(...)
31
+ class_method = getattr(objtype, self.class_method_name)
32
+ return class_method(*args, **kwargs)
33
+
34
+ return _wrapper
@@ -0,0 +1 @@
1
+ from .main import AsyncSandbox