brimble-sandbox 0.1.0__tar.gz
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.
- brimble_sandbox-0.1.0/PKG-INFO +104 -0
- brimble_sandbox-0.1.0/README.md +91 -0
- brimble_sandbox-0.1.0/brimble_sandbox/__init__.py +74 -0
- brimble_sandbox-0.1.0/brimble_sandbox/client.py +43 -0
- brimble_sandbox-0.1.0/brimble_sandbox/constants.py +25 -0
- brimble_sandbox-0.1.0/brimble_sandbox/enums.py +51 -0
- brimble_sandbox-0.1.0/brimble_sandbox/errors.py +42 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/__init__.py +22 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/exec.py +106 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/files.py +58 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/path.py +13 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/sandbox_handle.py +320 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/sandboxes.py +415 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/scoped_sandbox.py +119 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/snapshots.py +163 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/stats.py +30 -0
- brimble_sandbox-0.1.0/brimble_sandbox/resources/volumes.py +139 -0
- brimble_sandbox-0.1.0/brimble_sandbox/transport.py +446 -0
- brimble_sandbox-0.1.0/brimble_sandbox/types.py +280 -0
- brimble_sandbox-0.1.0/brimble_sandbox.egg-info/PKG-INFO +104 -0
- brimble_sandbox-0.1.0/brimble_sandbox.egg-info/SOURCES.txt +24 -0
- brimble_sandbox-0.1.0/brimble_sandbox.egg-info/dependency_links.txt +1 -0
- brimble_sandbox-0.1.0/brimble_sandbox.egg-info/requires.txt +4 -0
- brimble_sandbox-0.1.0/brimble_sandbox.egg-info/top_level.txt +1 -0
- brimble_sandbox-0.1.0/pyproject.toml +32 -0
- brimble_sandbox-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brimble-sandbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Brimble Sandbox API
|
|
5
|
+
Author: Brimble Engineering
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://brimble.io
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: requests>=2.31.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# brimble-sandbox
|
|
15
|
+
|
|
16
|
+
Python SDK for the Brimble Sandbox API.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install brimble-sandbox
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from brimble_sandbox import SandboxClient
|
|
28
|
+
|
|
29
|
+
client = SandboxClient() # reads BRIMBLE_SANDBOX_KEY from env
|
|
30
|
+
|
|
31
|
+
sandbox = client.sandboxes.create_ready(
|
|
32
|
+
{
|
|
33
|
+
"template": "node-22",
|
|
34
|
+
"persistent": True,
|
|
35
|
+
"persistentDiskGB": 20,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
result = sandbox.exec({"cmd": "node -v"})
|
|
40
|
+
print(result["stdout"])
|
|
41
|
+
|
|
42
|
+
existing = client.sandboxes.get(sandbox.id)
|
|
43
|
+
existing.destroy()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Ergonomic helpers
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# Create + wait in one call
|
|
50
|
+
created = client.sandboxes.create_ready({"template": "node-22"})
|
|
51
|
+
|
|
52
|
+
# Get + wait in one call
|
|
53
|
+
loaded = client.sandboxes.get_ready(created.id)
|
|
54
|
+
|
|
55
|
+
# Create volume + attach at sandbox creation time
|
|
56
|
+
with_volume = client.sandboxes.with_volume(
|
|
57
|
+
{
|
|
58
|
+
"sandbox": {"template": "node-22"},
|
|
59
|
+
"volume": {"name": "workspace-disk", "sizeGB": 20},
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Auto-wait on runtime calls
|
|
64
|
+
with_volume.exec({"cmd": "npm -v"}, wait_until_ready=True)
|
|
65
|
+
|
|
66
|
+
# Streaming SSE output
|
|
67
|
+
stream = with_volume.exec_stream({"cmd": "for i in 1 2 3; do echo $i; done"})
|
|
68
|
+
|
|
69
|
+
# Templates + regions
|
|
70
|
+
templates = client.sandboxes.list_templates()
|
|
71
|
+
node_template = client.sandboxes.get_template("node-22")
|
|
72
|
+
regions = client.sandboxes.list_regions()
|
|
73
|
+
|
|
74
|
+
# Iterate through all sandboxes
|
|
75
|
+
for sb in client.sandboxes.iterate({"teamId": "<team>"}):
|
|
76
|
+
print(sb.id, sb.status)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Volume attachment is create-time only.
|
|
80
|
+
Use `create(..., volumeId=...)` or `with_volume(...)`.
|
|
81
|
+
|
|
82
|
+
## Auth
|
|
83
|
+
|
|
84
|
+
Requests are authenticated with the `x-brimble-key` header.
|
|
85
|
+
|
|
86
|
+
- Pass `api_key` to `SandboxClient(...)`
|
|
87
|
+
- Or set `BRIMBLE_SANDBOX_KEY` in your environment
|
|
88
|
+
|
|
89
|
+
## Retry, timeout, idempotency
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from brimble_sandbox import RetryOptions, SandboxClient
|
|
93
|
+
|
|
94
|
+
client = SandboxClient(
|
|
95
|
+
retry=RetryOptions(max_attempts=3, base_delay_ms=250, max_delay_ms=2000),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
sandbox = client.sandboxes.create(
|
|
99
|
+
{"template": "node-22"},
|
|
100
|
+
idempotency_key="create-sandbox-123",
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If `region` is omitted, the SDK resolves the first available sandbox region automatically.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# brimble-sandbox
|
|
2
|
+
|
|
3
|
+
Python SDK for the Brimble Sandbox API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install brimble-sandbox
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from brimble_sandbox import SandboxClient
|
|
15
|
+
|
|
16
|
+
client = SandboxClient() # reads BRIMBLE_SANDBOX_KEY from env
|
|
17
|
+
|
|
18
|
+
sandbox = client.sandboxes.create_ready(
|
|
19
|
+
{
|
|
20
|
+
"template": "node-22",
|
|
21
|
+
"persistent": True,
|
|
22
|
+
"persistentDiskGB": 20,
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
result = sandbox.exec({"cmd": "node -v"})
|
|
27
|
+
print(result["stdout"])
|
|
28
|
+
|
|
29
|
+
existing = client.sandboxes.get(sandbox.id)
|
|
30
|
+
existing.destroy()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Ergonomic helpers
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# Create + wait in one call
|
|
37
|
+
created = client.sandboxes.create_ready({"template": "node-22"})
|
|
38
|
+
|
|
39
|
+
# Get + wait in one call
|
|
40
|
+
loaded = client.sandboxes.get_ready(created.id)
|
|
41
|
+
|
|
42
|
+
# Create volume + attach at sandbox creation time
|
|
43
|
+
with_volume = client.sandboxes.with_volume(
|
|
44
|
+
{
|
|
45
|
+
"sandbox": {"template": "node-22"},
|
|
46
|
+
"volume": {"name": "workspace-disk", "sizeGB": 20},
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Auto-wait on runtime calls
|
|
51
|
+
with_volume.exec({"cmd": "npm -v"}, wait_until_ready=True)
|
|
52
|
+
|
|
53
|
+
# Streaming SSE output
|
|
54
|
+
stream = with_volume.exec_stream({"cmd": "for i in 1 2 3; do echo $i; done"})
|
|
55
|
+
|
|
56
|
+
# Templates + regions
|
|
57
|
+
templates = client.sandboxes.list_templates()
|
|
58
|
+
node_template = client.sandboxes.get_template("node-22")
|
|
59
|
+
regions = client.sandboxes.list_regions()
|
|
60
|
+
|
|
61
|
+
# Iterate through all sandboxes
|
|
62
|
+
for sb in client.sandboxes.iterate({"teamId": "<team>"}):
|
|
63
|
+
print(sb.id, sb.status)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Volume attachment is create-time only.
|
|
67
|
+
Use `create(..., volumeId=...)` or `with_volume(...)`.
|
|
68
|
+
|
|
69
|
+
## Auth
|
|
70
|
+
|
|
71
|
+
Requests are authenticated with the `x-brimble-key` header.
|
|
72
|
+
|
|
73
|
+
- Pass `api_key` to `SandboxClient(...)`
|
|
74
|
+
- Or set `BRIMBLE_SANDBOX_KEY` in your environment
|
|
75
|
+
|
|
76
|
+
## Retry, timeout, idempotency
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from brimble_sandbox import RetryOptions, SandboxClient
|
|
80
|
+
|
|
81
|
+
client = SandboxClient(
|
|
82
|
+
retry=RetryOptions(max_attempts=3, base_delay_ms=250, max_delay_ms=2000),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
sandbox = client.sandboxes.create(
|
|
86
|
+
{"template": "node-22"},
|
|
87
|
+
idempotency_key="create-sandbox-123",
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If `region` is omitted, the SDK resolves the first available sandbox region automatically.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Python SDK for Brimble Sandbox."""
|
|
2
|
+
|
|
3
|
+
from .client import SandboxClient
|
|
4
|
+
from .constants import (
|
|
5
|
+
DEFAULT_BASE_URL,
|
|
6
|
+
DEFAULT_PAGE,
|
|
7
|
+
DEFAULT_PAGE_LIMIT,
|
|
8
|
+
DEFAULT_RETRY_BASE_DELAY_MS,
|
|
9
|
+
DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
10
|
+
DEFAULT_RETRY_MAX_DELAY_MS,
|
|
11
|
+
DEFAULT_RETRY_METHODS,
|
|
12
|
+
DEFAULT_RETRY_STATUSES,
|
|
13
|
+
DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS,
|
|
14
|
+
DEFAULT_SANDBOX_READY_TIMEOUT_MS,
|
|
15
|
+
DEFAULT_TIMEOUT_MS,
|
|
16
|
+
MAX_PAGE_LIMIT,
|
|
17
|
+
MIN_VOLUME_SIZE_GB,
|
|
18
|
+
SANDBOX_API_KEY_ENV_NAME,
|
|
19
|
+
)
|
|
20
|
+
from .enums import CodeLanguage, DestroyReason, DestroyTimeout, SandboxStatus, SnapshotMode, SnapshotStatus, VolumeType
|
|
21
|
+
from .errors import AuthError, NotFoundError, RateLimitError, SandboxApiError, ValidationError
|
|
22
|
+
from .resources import (
|
|
23
|
+
ExecResource,
|
|
24
|
+
FilesResource,
|
|
25
|
+
SandboxHandle,
|
|
26
|
+
SandboxesResource,
|
|
27
|
+
ScopedSandboxResource,
|
|
28
|
+
SnapshotScopeResource,
|
|
29
|
+
SnapshotsResource,
|
|
30
|
+
StatsResource,
|
|
31
|
+
VolumesResource,
|
|
32
|
+
)
|
|
33
|
+
from .transport import RequestOptions, RetryOptions
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"DEFAULT_BASE_URL",
|
|
37
|
+
"DEFAULT_PAGE",
|
|
38
|
+
"DEFAULT_PAGE_LIMIT",
|
|
39
|
+
"DEFAULT_RETRY_BASE_DELAY_MS",
|
|
40
|
+
"DEFAULT_RETRY_MAX_ATTEMPTS",
|
|
41
|
+
"DEFAULT_RETRY_MAX_DELAY_MS",
|
|
42
|
+
"DEFAULT_RETRY_METHODS",
|
|
43
|
+
"DEFAULT_RETRY_STATUSES",
|
|
44
|
+
"DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS",
|
|
45
|
+
"DEFAULT_SANDBOX_READY_TIMEOUT_MS",
|
|
46
|
+
"DEFAULT_TIMEOUT_MS",
|
|
47
|
+
"MAX_PAGE_LIMIT",
|
|
48
|
+
"MIN_VOLUME_SIZE_GB",
|
|
49
|
+
"SANDBOX_API_KEY_ENV_NAME",
|
|
50
|
+
"CodeLanguage",
|
|
51
|
+
"DestroyReason",
|
|
52
|
+
"DestroyTimeout",
|
|
53
|
+
"SandboxStatus",
|
|
54
|
+
"SnapshotMode",
|
|
55
|
+
"SnapshotStatus",
|
|
56
|
+
"VolumeType",
|
|
57
|
+
"SandboxApiError",
|
|
58
|
+
"AuthError",
|
|
59
|
+
"ValidationError",
|
|
60
|
+
"NotFoundError",
|
|
61
|
+
"RateLimitError",
|
|
62
|
+
"RetryOptions",
|
|
63
|
+
"RequestOptions",
|
|
64
|
+
"SandboxClient",
|
|
65
|
+
"ExecResource",
|
|
66
|
+
"FilesResource",
|
|
67
|
+
"SandboxHandle",
|
|
68
|
+
"SandboxesResource",
|
|
69
|
+
"ScopedSandboxResource",
|
|
70
|
+
"SnapshotScopeResource",
|
|
71
|
+
"SnapshotsResource",
|
|
72
|
+
"StatsResource",
|
|
73
|
+
"VolumesResource",
|
|
74
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Top-level Sandbox client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .constants import DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, SANDBOX_API_KEY_ENV_NAME
|
|
10
|
+
from .resources import SandboxesResource, SnapshotsResource, VolumesResource
|
|
11
|
+
from .transport import HttpTransport, RetryOptions
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SandboxClient:
|
|
15
|
+
"""Entry-point client for Brimble Sandbox API resources."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
api_key: str | None = None,
|
|
21
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
22
|
+
timeout_ms: int = DEFAULT_TIMEOUT_MS,
|
|
23
|
+
retry: RetryOptions | None = None,
|
|
24
|
+
session: requests.Session | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Create a client from explicit key or BRIMBLE_SANDBOX_KEY env variable."""
|
|
27
|
+
resolved_api_key = api_key or os.getenv(SANDBOX_API_KEY_ENV_NAME)
|
|
28
|
+
if not resolved_api_key:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Sandbox API key is required. Pass api_key explicitly or set {SANDBOX_API_KEY_ENV_NAME}."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
transport = HttpTransport(
|
|
34
|
+
base_url=base_url,
|
|
35
|
+
api_key=resolved_api_key,
|
|
36
|
+
timeout_ms=timeout_ms,
|
|
37
|
+
retry=retry,
|
|
38
|
+
session=session,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.sandboxes = SandboxesResource(transport)
|
|
42
|
+
self.snapshots = SnapshotsResource(transport)
|
|
43
|
+
self.volumes = VolumesResource(transport)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared SDK constants."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
DEFAULT_BASE_URL = "https://sandbox.brimble.io"
|
|
6
|
+
DEFAULT_TIMEOUT_MS = 30_000
|
|
7
|
+
SANDBOX_API_KEY_ENV_NAME = "BRIMBLE_SANDBOX_KEY"
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
SDK_PACKAGE_VERSION = version("brimble-sandbox")
|
|
11
|
+
except PackageNotFoundError:
|
|
12
|
+
SDK_PACKAGE_VERSION = "0.1.0"
|
|
13
|
+
|
|
14
|
+
DEFAULT_PAGE = 1
|
|
15
|
+
DEFAULT_PAGE_LIMIT = 15
|
|
16
|
+
MAX_PAGE_LIMIT = 100
|
|
17
|
+
MIN_VOLUME_SIZE_GB = 10
|
|
18
|
+
DEFAULT_SANDBOX_READY_TIMEOUT_MS = 60_000
|
|
19
|
+
DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS = 2_000
|
|
20
|
+
|
|
21
|
+
DEFAULT_RETRY_MAX_ATTEMPTS = 1
|
|
22
|
+
DEFAULT_RETRY_BASE_DELAY_MS = 300
|
|
23
|
+
DEFAULT_RETRY_MAX_DELAY_MS = 3_000
|
|
24
|
+
DEFAULT_RETRY_STATUSES = (408, 429, 500, 502, 503, 504)
|
|
25
|
+
DEFAULT_RETRY_METHODS = ("GET", "DELETE", "PUT")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Enum values used by the Sandbox SDK."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CodeLanguage(StrEnum):
|
|
7
|
+
PYTHON = "python"
|
|
8
|
+
NODE = "node"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DestroyReason(StrEnum):
|
|
12
|
+
USER = "user"
|
|
13
|
+
IDLE_TTL = "idle_ttl"
|
|
14
|
+
MAX_LIFETIME = "max_lifetime"
|
|
15
|
+
ONE_SHOT_STOPPED = "one_shot_stopped"
|
|
16
|
+
FAILED = "failed"
|
|
17
|
+
PAUSED_TOO_LONG = "paused_too_long"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DestroyTimeout(StrEnum):
|
|
21
|
+
THIRTY_MINUTES = "30m"
|
|
22
|
+
ONE_HOUR = "1h"
|
|
23
|
+
THREE_HOURS = "3h"
|
|
24
|
+
SIX_HOURS = "6h"
|
|
25
|
+
TWELVE_HOURS = "12h"
|
|
26
|
+
EIGHTEEN_HOURS = "18h"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SandboxStatus(StrEnum):
|
|
30
|
+
STARTING = "starting"
|
|
31
|
+
READY = "ready"
|
|
32
|
+
PAUSING = "pausing"
|
|
33
|
+
PAUSED = "paused"
|
|
34
|
+
RESUMING = "resuming"
|
|
35
|
+
FAILED = "failed"
|
|
36
|
+
DESTROYED = "destroyed"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SnapshotMode(StrEnum):
|
|
40
|
+
MANUAL = "manual"
|
|
41
|
+
AUTOMATIC = "automatic"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SnapshotStatus(StrEnum):
|
|
45
|
+
CREATING = "creating"
|
|
46
|
+
READY = "ready"
|
|
47
|
+
FAILED = "failed"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class VolumeType(StrEnum):
|
|
51
|
+
SANDBOX = "sandbox"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""SDK error types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SandboxApiError(Exception):
|
|
7
|
+
"""Raised when the Sandbox API returns a non-2xx HTTP response."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, status: int, message: str, endpoint: str, response_body: object, request_id: str | None = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.status = status
|
|
12
|
+
self.endpoint = endpoint
|
|
13
|
+
self.response_body = response_body
|
|
14
|
+
self.request_id = request_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthError(SandboxApiError):
|
|
18
|
+
"""Raised when credentials are missing/invalid or access is denied."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ValidationError(SandboxApiError):
|
|
22
|
+
"""Raised when request payload/state fails validation."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotFoundError(SandboxApiError):
|
|
26
|
+
"""Raised when a requested sandbox/volume/snapshot was not found."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RateLimitError(SandboxApiError):
|
|
30
|
+
"""Raised on 429 responses with optional retry-after guidance."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
status: int,
|
|
35
|
+
message: str,
|
|
36
|
+
endpoint: str,
|
|
37
|
+
response_body: object,
|
|
38
|
+
request_id: str | None = None,
|
|
39
|
+
retry_after_seconds: float | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__(status, message, endpoint, response_body, request_id)
|
|
42
|
+
self.retry_after_seconds = retry_after_seconds
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Public resource exports."""
|
|
2
|
+
|
|
3
|
+
from .exec import ExecResource
|
|
4
|
+
from .files import FilesResource
|
|
5
|
+
from .sandbox_handle import SandboxHandle
|
|
6
|
+
from .sandboxes import SandboxesResource
|
|
7
|
+
from .scoped_sandbox import ScopedSandboxResource
|
|
8
|
+
from .snapshots import SnapshotScopeResource, SnapshotsResource
|
|
9
|
+
from .stats import StatsResource
|
|
10
|
+
from .volumes import VolumesResource
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ExecResource",
|
|
14
|
+
"FilesResource",
|
|
15
|
+
"SandboxHandle",
|
|
16
|
+
"SandboxesResource",
|
|
17
|
+
"ScopedSandboxResource",
|
|
18
|
+
"SnapshotScopeResource",
|
|
19
|
+
"SnapshotsResource",
|
|
20
|
+
"StatsResource",
|
|
21
|
+
"VolumesResource",
|
|
22
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Sandbox exec/code operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from ..transport import HttpTransport, RequestOptions, RetryOptions
|
|
8
|
+
from ..types import CodeInput, ExecInput, ExecResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExecResource:
|
|
12
|
+
"""Run shell commands or code snippets in one sandbox."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, transport: HttpTransport, sandbox_id: str) -> None:
|
|
15
|
+
self._transport = transport
|
|
16
|
+
self._sandbox_id = sandbox_id
|
|
17
|
+
|
|
18
|
+
def exec(
|
|
19
|
+
self,
|
|
20
|
+
input: ExecInput,
|
|
21
|
+
*,
|
|
22
|
+
timeout_ms: int | None = None,
|
|
23
|
+
idempotency_key: str | None = None,
|
|
24
|
+
retry: RetryOptions | bool | None = None,
|
|
25
|
+
) -> ExecResult | requests.Response:
|
|
26
|
+
"""Run a shell command in the sandbox."""
|
|
27
|
+
options = RequestOptions(timeout_ms=timeout_ms, idempotency_key=idempotency_key, retry=retry)
|
|
28
|
+
|
|
29
|
+
if input.get("stream") is True:
|
|
30
|
+
return self._transport.request_sse(
|
|
31
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/exec",
|
|
32
|
+
method="POST",
|
|
33
|
+
body=input,
|
|
34
|
+
options=options,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return self._transport.request_json(
|
|
38
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/exec",
|
|
39
|
+
method="POST",
|
|
40
|
+
body=input,
|
|
41
|
+
options=options,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def run_code(
|
|
45
|
+
self,
|
|
46
|
+
input: CodeInput,
|
|
47
|
+
*,
|
|
48
|
+
timeout_ms: int | None = None,
|
|
49
|
+
idempotency_key: str | None = None,
|
|
50
|
+
retry: RetryOptions | bool | None = None,
|
|
51
|
+
) -> ExecResult | requests.Response:
|
|
52
|
+
"""Run a code snippet in the sandbox."""
|
|
53
|
+
options = RequestOptions(timeout_ms=timeout_ms, idempotency_key=idempotency_key, retry=retry)
|
|
54
|
+
|
|
55
|
+
if input.get("stream") is True:
|
|
56
|
+
return self._transport.request_sse(
|
|
57
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/code",
|
|
58
|
+
method="POST",
|
|
59
|
+
body=input,
|
|
60
|
+
options=options,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return self._transport.request_json(
|
|
64
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/code",
|
|
65
|
+
method="POST",
|
|
66
|
+
body=input,
|
|
67
|
+
options=options,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def exec_stream(
|
|
71
|
+
self,
|
|
72
|
+
input: ExecInput,
|
|
73
|
+
*,
|
|
74
|
+
timeout_ms: int | None = None,
|
|
75
|
+
idempotency_key: str | None = None,
|
|
76
|
+
retry: RetryOptions | bool | None = None,
|
|
77
|
+
) -> requests.Response:
|
|
78
|
+
"""Run a shell command and stream SSE output frames."""
|
|
79
|
+
payload = dict(input)
|
|
80
|
+
payload["stream"] = True
|
|
81
|
+
options = RequestOptions(timeout_ms=timeout_ms, idempotency_key=idempotency_key, retry=retry)
|
|
82
|
+
return self._transport.request_sse(
|
|
83
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/exec",
|
|
84
|
+
method="POST",
|
|
85
|
+
body=payload,
|
|
86
|
+
options=options,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def run_code_stream(
|
|
90
|
+
self,
|
|
91
|
+
input: CodeInput,
|
|
92
|
+
*,
|
|
93
|
+
timeout_ms: int | None = None,
|
|
94
|
+
idempotency_key: str | None = None,
|
|
95
|
+
retry: RetryOptions | bool | None = None,
|
|
96
|
+
) -> requests.Response:
|
|
97
|
+
"""Run a code snippet and stream SSE output frames."""
|
|
98
|
+
payload = dict(input)
|
|
99
|
+
payload["stream"] = True
|
|
100
|
+
options = RequestOptions(timeout_ms=timeout_ms, idempotency_key=idempotency_key, retry=retry)
|
|
101
|
+
return self._transport.request_sse(
|
|
102
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/code",
|
|
103
|
+
method="POST",
|
|
104
|
+
body=payload,
|
|
105
|
+
options=options,
|
|
106
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Sandbox file upload/download operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from ..transport import HttpTransport, RequestOptions, RetryOptions
|
|
8
|
+
from ..types import FileUploadBody
|
|
9
|
+
from .path import encode_file_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FilesResource:
|
|
13
|
+
"""Upload and download files for one sandbox."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, transport: HttpTransport, sandbox_id: str) -> None:
|
|
16
|
+
self._transport = transport
|
|
17
|
+
self._sandbox_id = sandbox_id
|
|
18
|
+
|
|
19
|
+
def put(
|
|
20
|
+
self,
|
|
21
|
+
path: str,
|
|
22
|
+
body: FileUploadBody,
|
|
23
|
+
*,
|
|
24
|
+
timeout_ms: int | None = None,
|
|
25
|
+
idempotency_key: str | None = None,
|
|
26
|
+
retry: RetryOptions | bool | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Upload bytes to a path inside the sandbox."""
|
|
29
|
+
headers: dict[str, str] = {
|
|
30
|
+
"content-type": "application/octet-stream",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if isinstance(body, (bytes, bytearray)):
|
|
34
|
+
headers["content-length"] = str(len(body))
|
|
35
|
+
|
|
36
|
+
options = RequestOptions(timeout_ms=timeout_ms, idempotency_key=idempotency_key, retry=retry)
|
|
37
|
+
self._transport.request_binary(
|
|
38
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/files/{encode_file_path(path)}",
|
|
39
|
+
method="PUT",
|
|
40
|
+
body=body,
|
|
41
|
+
headers=headers,
|
|
42
|
+
options=options,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def get(
|
|
46
|
+
self,
|
|
47
|
+
path: str,
|
|
48
|
+
*,
|
|
49
|
+
timeout_ms: int | None = None,
|
|
50
|
+
retry: RetryOptions | bool | None = None,
|
|
51
|
+
) -> requests.Response:
|
|
52
|
+
"""Download a file from the sandbox as a streamed response."""
|
|
53
|
+
options = RequestOptions(timeout_ms=timeout_ms, retry=retry)
|
|
54
|
+
return self._transport.request_stream(
|
|
55
|
+
endpoint=f"/sandboxes/{self._sandbox_id}/files/{encode_file_path(path)}",
|
|
56
|
+
method="GET",
|
|
57
|
+
options=options,
|
|
58
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Path helpers for file endpoints."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def encode_path_segment(value: str) -> str:
|
|
7
|
+
"""Encode one path segment for URL use."""
|
|
8
|
+
return quote(value, safe="")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def encode_file_path(path: str) -> str:
|
|
12
|
+
"""Encode a file path while preserving slash separators."""
|
|
13
|
+
return "/".join(encode_path_segment(segment) for segment in path.split("/") if segment)
|