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.
- scalebox/__init__.py +80 -0
- scalebox/api/__init__.py +128 -0
- scalebox/api/client/__init__.py +8 -0
- scalebox/api/client/api/__init__.py +1 -0
- scalebox/api/client/api/sandboxes/__init__.py +0 -0
- scalebox/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- scalebox/api/client/api/sandboxes/get_sandboxes.py +176 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +214 -0
- scalebox/api/client/api/sandboxes/get_v2_sandboxes.py +229 -0
- scalebox/api/client/api/sandboxes/post_sandboxes.py +174 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +182 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +190 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +194 -0
- scalebox/api/client/client.py +288 -0
- scalebox/api/client/errors.py +16 -0
- scalebox/api/client/models/__init__.py +81 -0
- scalebox/api/client/models/build_log_entry.py +79 -0
- scalebox/api/client/models/created_access_token.py +100 -0
- scalebox/api/client/models/created_team_api_key.py +166 -0
- scalebox/api/client/models/error.py +67 -0
- scalebox/api/client/models/identifier_masking_details.py +83 -0
- scalebox/api/client/models/listed_sandbox.py +138 -0
- scalebox/api/client/models/log_level.py +11 -0
- scalebox/api/client/models/new_access_token.py +59 -0
- scalebox/api/client/models/new_sandbox.py +125 -0
- scalebox/api/client/models/new_team_api_key.py +59 -0
- scalebox/api/client/models/node.py +154 -0
- scalebox/api/client/models/node_detail.py +152 -0
- scalebox/api/client/models/node_status.py +11 -0
- scalebox/api/client/models/node_status_change.py +61 -0
- scalebox/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- scalebox/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- scalebox/api/client/models/resumed_sandbox.py +68 -0
- scalebox/api/client/models/sandbox.py +125 -0
- scalebox/api/client/models/sandbox_detail.py +178 -0
- scalebox/api/client/models/sandbox_log.py +70 -0
- scalebox/api/client/models/sandbox_logs.py +73 -0
- scalebox/api/client/models/sandbox_metric.py +110 -0
- scalebox/api/client/models/sandbox_state.py +9 -0
- scalebox/api/client/models/sandboxes_with_metrics.py +59 -0
- scalebox/api/client/models/team.py +83 -0
- scalebox/api/client/models/team_api_key.py +158 -0
- scalebox/api/client/models/team_user.py +68 -0
- scalebox/api/client/models/template.py +179 -0
- scalebox/api/client/models/template_build.py +117 -0
- scalebox/api/client/models/template_build_file_upload.py +70 -0
- scalebox/api/client/models/template_build_request.py +115 -0
- scalebox/api/client/models/template_build_request_v2.py +88 -0
- scalebox/api/client/models/template_build_start_v2.py +114 -0
- scalebox/api/client/models/template_build_status.py +11 -0
- scalebox/api/client/models/template_step.py +91 -0
- scalebox/api/client/models/template_update_request.py +59 -0
- scalebox/api/client/models/update_team_api_key.py +59 -0
- scalebox/api/client/py.typed +1 -0
- scalebox/api/client/types.py +46 -0
- scalebox/api/metadata.py +19 -0
- scalebox/cli.py +125 -0
- scalebox/client/__init__.py +0 -0
- scalebox/client/aclient.py +57 -0
- scalebox/client/api.proto +460 -0
- scalebox/client/buf.gen.yaml +8 -0
- scalebox/client/client.py +102 -0
- scalebox/client/requirements.txt +5 -0
- scalebox/code_interpreter/__init__.py +12 -0
- scalebox/code_interpreter/charts.py +230 -0
- scalebox/code_interpreter/code_interpreter_async.py +369 -0
- scalebox/code_interpreter/code_interpreter_sync.py +317 -0
- scalebox/code_interpreter/constants.py +3 -0
- scalebox/code_interpreter/exceptions.py +13 -0
- scalebox/code_interpreter/models.py +485 -0
- scalebox/connection_config.py +92 -0
- scalebox/csx_connect/__init__.py +1 -0
- scalebox/csx_connect/client.py +485 -0
- scalebox/csx_desktop/__init__.py +0 -0
- scalebox/csx_desktop/main.py +651 -0
- scalebox/exceptions.py +83 -0
- scalebox/generated/__init__.py +0 -0
- scalebox/generated/api.py +61 -0
- scalebox/generated/api_pb2.py +203 -0
- scalebox/generated/api_pb2.pyi +956 -0
- scalebox/generated/api_pb2_connect.py +1456 -0
- scalebox/generated/rpc.py +50 -0
- scalebox/generated/versions.py +3 -0
- scalebox/requirements.txt +36 -0
- scalebox/sandbox/__init__.py +0 -0
- scalebox/sandbox/commands/__init__.py +0 -0
- scalebox/sandbox/commands/command_handle.py +69 -0
- scalebox/sandbox/commands/main.py +39 -0
- scalebox/sandbox/filesystem/__init__.py +0 -0
- scalebox/sandbox/filesystem/filesystem.py +95 -0
- scalebox/sandbox/filesystem/watch_handle.py +60 -0
- scalebox/sandbox/main.py +139 -0
- scalebox/sandbox/sandbox_api.py +91 -0
- scalebox/sandbox/signature.py +40 -0
- scalebox/sandbox/utils.py +34 -0
- scalebox/sandbox_async/__init__.py +1 -0
- scalebox/sandbox_async/commands/command.py +307 -0
- scalebox/sandbox_async/commands/command_handle.py +187 -0
- scalebox/sandbox_async/commands/pty.py +187 -0
- scalebox/sandbox_async/filesystem/filesystem.py +557 -0
- scalebox/sandbox_async/filesystem/watch_handle.py +61 -0
- scalebox/sandbox_async/main.py +646 -0
- scalebox/sandbox_async/sandbox_api.py +365 -0
- scalebox/sandbox_async/utils.py +7 -0
- scalebox/sandbox_sync/__init__.py +2 -0
- scalebox/sandbox_sync/commands/__init__.py +0 -0
- scalebox/sandbox_sync/commands/command.py +300 -0
- scalebox/sandbox_sync/commands/command_handle.py +150 -0
- scalebox/sandbox_sync/commands/pty.py +181 -0
- scalebox/sandbox_sync/filesystem/__init__.py +0 -0
- scalebox/sandbox_sync/filesystem/filesystem.py +543 -0
- scalebox/sandbox_sync/filesystem/watch_handle.py +66 -0
- scalebox/sandbox_sync/main.py +790 -0
- scalebox/sandbox_sync/sandbox_api.py +356 -0
- scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -0
- scalebox/test/README.md +329 -0
- scalebox/test/__init__.py +0 -0
- scalebox/test/aclient.py +72 -0
- scalebox/test/code_interpreter_centext.py +21 -0
- scalebox/test/code_interpreter_centext_sync.py +21 -0
- scalebox/test/code_interpreter_test.py +34 -0
- scalebox/test/code_interpreter_test_sync.py +34 -0
- scalebox/test/run_all_validation_tests.py +334 -0
- scalebox/test/run_code_interpreter_tests.sh +67 -0
- scalebox/test/run_tests.sh +230 -0
- scalebox/test/test_basic.py +78 -0
- scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -0
- scalebox/test/test_code_interpreter_e2basync_comprehensive.py +2655 -0
- scalebox/test/test_code_interpreter_e2bsync_comprehensive.py +3416 -0
- scalebox/test/test_code_interpreter_sync_comprehensive.py +3412 -0
- scalebox/test/test_e2b_first.py +11 -0
- scalebox/test/test_sandbox_async_comprehensive.py +738 -0
- scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -0
- scalebox/test/test_sandbox_sync_comprehensive.py +770 -0
- scalebox/test/test_sandbox_usage_examples.py +987 -0
- scalebox/test/testacreate.py +24 -0
- scalebox/test/testagetinfo.py +18 -0
- scalebox/test/testcodeinterpreter_async.py +508 -0
- scalebox/test/testcodeinterpreter_sync.py +239 -0
- scalebox/test/testcomputeuse.py +243 -0
- scalebox/test/testnovnc.py +12 -0
- scalebox/test/testsandbox_async.py +118 -0
- scalebox/test/testsandbox_sync.py +38 -0
- scalebox/utils/__init__.py +0 -0
- scalebox/utils/httpcoreclient.py +297 -0
- scalebox/utils/httpxclient.py +403 -0
- scalebox/version.py +16 -0
- scalebox_sdk-0.1.0.dist-info/METADATA +292 -0
- scalebox_sdk-0.1.0.dist-info/RECORD +157 -0
- scalebox_sdk-0.1.0.dist-info/WHEEL +5 -0
- scalebox_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- scalebox_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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,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
|
+
"""
|
scalebox/sandbox/main.py
ADDED
|
@@ -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
|