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.
Files changed (26) hide show
  1. brimble_sandbox-0.1.0/PKG-INFO +104 -0
  2. brimble_sandbox-0.1.0/README.md +91 -0
  3. brimble_sandbox-0.1.0/brimble_sandbox/__init__.py +74 -0
  4. brimble_sandbox-0.1.0/brimble_sandbox/client.py +43 -0
  5. brimble_sandbox-0.1.0/brimble_sandbox/constants.py +25 -0
  6. brimble_sandbox-0.1.0/brimble_sandbox/enums.py +51 -0
  7. brimble_sandbox-0.1.0/brimble_sandbox/errors.py +42 -0
  8. brimble_sandbox-0.1.0/brimble_sandbox/resources/__init__.py +22 -0
  9. brimble_sandbox-0.1.0/brimble_sandbox/resources/exec.py +106 -0
  10. brimble_sandbox-0.1.0/brimble_sandbox/resources/files.py +58 -0
  11. brimble_sandbox-0.1.0/brimble_sandbox/resources/path.py +13 -0
  12. brimble_sandbox-0.1.0/brimble_sandbox/resources/sandbox_handle.py +320 -0
  13. brimble_sandbox-0.1.0/brimble_sandbox/resources/sandboxes.py +415 -0
  14. brimble_sandbox-0.1.0/brimble_sandbox/resources/scoped_sandbox.py +119 -0
  15. brimble_sandbox-0.1.0/brimble_sandbox/resources/snapshots.py +163 -0
  16. brimble_sandbox-0.1.0/brimble_sandbox/resources/stats.py +30 -0
  17. brimble_sandbox-0.1.0/brimble_sandbox/resources/volumes.py +139 -0
  18. brimble_sandbox-0.1.0/brimble_sandbox/transport.py +446 -0
  19. brimble_sandbox-0.1.0/brimble_sandbox/types.py +280 -0
  20. brimble_sandbox-0.1.0/brimble_sandbox.egg-info/PKG-INFO +104 -0
  21. brimble_sandbox-0.1.0/brimble_sandbox.egg-info/SOURCES.txt +24 -0
  22. brimble_sandbox-0.1.0/brimble_sandbox.egg-info/dependency_links.txt +1 -0
  23. brimble_sandbox-0.1.0/brimble_sandbox.egg-info/requires.txt +4 -0
  24. brimble_sandbox-0.1.0/brimble_sandbox.egg-info/top_level.txt +1 -0
  25. brimble_sandbox-0.1.0/pyproject.toml +32 -0
  26. 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)