runcloud-sdk 0.7.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.
- runcloud/__init__.py +17 -0
- runcloud/client.py +209 -0
- runcloud/compat/__init__.py +5 -0
- runcloud/compat/blaxel.py +147 -0
- runcloud/compat/daytona.py +67 -0
- runcloud/compat/e2b.py +80 -0
- runcloud/compat/modal.py +165 -0
- runcloud/compat/sprites.py +50 -0
- runcloud/compat/vercel.py +55 -0
- runcloud_sdk-0.7.0.dist-info/METADATA +58 -0
- runcloud_sdk-0.7.0.dist-info/RECORD +14 -0
- runcloud_sdk-0.7.0.dist-info/WHEEL +5 -0
- runcloud_sdk-0.7.0.dist-info/licenses/LICENSE +17 -0
- runcloud_sdk-0.7.0.dist-info/top_level.txt +1 -0
runcloud/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Public Python SDK for run.cloud."""
|
|
2
|
+
|
|
3
|
+
from .client import (
|
|
4
|
+
Client,
|
|
5
|
+
ExecResult,
|
|
6
|
+
RunCloudError,
|
|
7
|
+
Sandbox,
|
|
8
|
+
UnsupportedCompatibilityFeatureError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Client",
|
|
13
|
+
"ExecResult",
|
|
14
|
+
"RunCloudError",
|
|
15
|
+
"Sandbox",
|
|
16
|
+
"UnsupportedCompatibilityFeatureError",
|
|
17
|
+
]
|
runcloud/client.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Dependency-free Python client for the public run.cloud API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Callable, Optional, Union
|
|
13
|
+
|
|
14
|
+
Transport = Callable[[str, str, Any], Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RunCloudError(Exception):
|
|
18
|
+
def __init__(self, status: int, detail: str):
|
|
19
|
+
super().__init__(f"run.cloud API {status}: {detail}")
|
|
20
|
+
self.status = status
|
|
21
|
+
self.detail = detail
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnsupportedCompatibilityFeatureError(Exception):
|
|
25
|
+
def __init__(self, provider: str, feature: str, alternative: str = ""):
|
|
26
|
+
message = f"{provider} compatibility does not support {feature} on run.cloud yet"
|
|
27
|
+
if alternative:
|
|
28
|
+
message += f"; {alternative}"
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.provider = provider
|
|
31
|
+
self.feature = feature
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ExecResult:
|
|
36
|
+
exit_code: int
|
|
37
|
+
stdout: str
|
|
38
|
+
stderr: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Client:
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
api_key: Optional[str] = None,
|
|
45
|
+
api_url: Optional[str] = None,
|
|
46
|
+
timeout: float = 60.0,
|
|
47
|
+
transport: Optional[Transport] = None,
|
|
48
|
+
):
|
|
49
|
+
self.api_key = (
|
|
50
|
+
api_key
|
|
51
|
+
or os.environ.get("RUN_CLOUD_API_KEY")
|
|
52
|
+
or os.environ.get("RUN_CLOUD_API_TOKEN")
|
|
53
|
+
)
|
|
54
|
+
if not self.api_key:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"Run Cloud API key required. Set RUN_CLOUD_API_KEY or pass Client(api_key=...)."
|
|
57
|
+
)
|
|
58
|
+
self.api_url = (
|
|
59
|
+
api_url or os.environ.get("RUN_CLOUD_API_URL") or "https://api.run.cloud"
|
|
60
|
+
).rstrip("/")
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
self._transport = transport
|
|
63
|
+
|
|
64
|
+
def request(self, method: str, path: str, body: Any = None) -> Any:
|
|
65
|
+
if self._transport is not None:
|
|
66
|
+
return self._transport(method, path, body)
|
|
67
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
68
|
+
request = urllib.request.Request(
|
|
69
|
+
self.api_url + path,
|
|
70
|
+
data=data,
|
|
71
|
+
method=method,
|
|
72
|
+
headers={
|
|
73
|
+
"Authorization": "Bearer " + self.api_key,
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
try:
|
|
78
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
79
|
+
raw = response.read()
|
|
80
|
+
return json.loads(raw) if raw else None
|
|
81
|
+
except urllib.error.HTTPError as error:
|
|
82
|
+
text = error.read().decode(errors="replace")
|
|
83
|
+
try:
|
|
84
|
+
parsed = json.loads(text)
|
|
85
|
+
text = parsed.get("detail") or parsed.get("error") or parsed.get("message") or text
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
raise RunCloudError(error.code, str(text)) from None
|
|
89
|
+
|
|
90
|
+
def create(
|
|
91
|
+
self,
|
|
92
|
+
image: str = "runcloud/agent-base",
|
|
93
|
+
cpu: Optional[float] = None,
|
|
94
|
+
memory: Optional[int] = None,
|
|
95
|
+
disk: Optional[int] = None,
|
|
96
|
+
idle_pause_seconds: Optional[int] = None,
|
|
97
|
+
timeout_seconds: Optional[int] = None,
|
|
98
|
+
region: Optional[str] = None,
|
|
99
|
+
name: Optional[str] = None,
|
|
100
|
+
) -> "Sandbox":
|
|
101
|
+
values = {
|
|
102
|
+
"image": image,
|
|
103
|
+
"cpu": cpu,
|
|
104
|
+
"memory": memory,
|
|
105
|
+
"disk": disk,
|
|
106
|
+
"idlePauseSeconds": idle_pause_seconds,
|
|
107
|
+
"timeoutSeconds": timeout_seconds,
|
|
108
|
+
"region": region,
|
|
109
|
+
"name": name,
|
|
110
|
+
}
|
|
111
|
+
body = {key: value for key, value in values.items() if value is not None}
|
|
112
|
+
return Sandbox(self, self.request("POST", "/run-cloud/sandboxes", body))
|
|
113
|
+
|
|
114
|
+
def get(self, sandbox_id: str) -> "Sandbox":
|
|
115
|
+
sid = urllib.parse.quote(sandbox_id, safe="")
|
|
116
|
+
return Sandbox(self, self.request("GET", f"/run-cloud/sandboxes/{sid}"))
|
|
117
|
+
|
|
118
|
+
def list(self, state: Optional[str] = None) -> list["Sandbox"]:
|
|
119
|
+
query = f"?state={urllib.parse.quote(state, safe='')}" if state else ""
|
|
120
|
+
payload = self.request("GET", "/run-cloud/sandboxes" + query) or {}
|
|
121
|
+
return [Sandbox(self, item) for item in payload.get("items", [])]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Sandbox:
|
|
125
|
+
def __init__(self, client: Client, data: dict[str, Any]):
|
|
126
|
+
self._client = client
|
|
127
|
+
self._data = data
|
|
128
|
+
self.id = str(data["id"])
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def state(self) -> str:
|
|
132
|
+
return str(self._data.get("state", ""))
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def name(self) -> str:
|
|
136
|
+
return str(self._data.get("name") or "")
|
|
137
|
+
|
|
138
|
+
def refresh(self) -> "Sandbox":
|
|
139
|
+
self._data = self._client.request(
|
|
140
|
+
"GET", f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}"
|
|
141
|
+
)
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def exec(
|
|
145
|
+
self,
|
|
146
|
+
command: Union[str, list[str]],
|
|
147
|
+
cwd: Optional[str] = None,
|
|
148
|
+
env: Optional[dict[str, str]] = None,
|
|
149
|
+
timeout: Optional[int] = None,
|
|
150
|
+
) -> ExecResult:
|
|
151
|
+
argv = ["/bin/sh", "-c", command] if isinstance(command, str) else command
|
|
152
|
+
if not argv:
|
|
153
|
+
raise ValueError("command must not be empty")
|
|
154
|
+
body: dict[str, Any] = {"cmd": argv}
|
|
155
|
+
if cwd:
|
|
156
|
+
body["cwd"] = cwd
|
|
157
|
+
if env:
|
|
158
|
+
body["env"] = env
|
|
159
|
+
if timeout is not None:
|
|
160
|
+
body["timeout_seconds"] = timeout
|
|
161
|
+
payload = self._client.request(
|
|
162
|
+
"POST",
|
|
163
|
+
f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}/exec",
|
|
164
|
+
body,
|
|
165
|
+
)
|
|
166
|
+
return ExecResult(
|
|
167
|
+
int(payload.get("exit_code", 0)),
|
|
168
|
+
str(payload.get("stdout", "")),
|
|
169
|
+
str(payload.get("stderr", "")),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def read_file(self, path: str) -> bytes:
|
|
173
|
+
payload = self._client.request(
|
|
174
|
+
"POST",
|
|
175
|
+
f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}/fs",
|
|
176
|
+
{"op": "read", "path": path},
|
|
177
|
+
)
|
|
178
|
+
if payload.get("error"):
|
|
179
|
+
raise RunCloudError(0, str(payload["error"]))
|
|
180
|
+
return base64.b64decode(payload.get("content") or "")
|
|
181
|
+
|
|
182
|
+
def pause(self) -> "Sandbox":
|
|
183
|
+
self._data = self._client.request(
|
|
184
|
+
"POST",
|
|
185
|
+
f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}/pause",
|
|
186
|
+
{},
|
|
187
|
+
)
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
def resume(self) -> "Sandbox":
|
|
191
|
+
self._data = self._client.request(
|
|
192
|
+
"POST",
|
|
193
|
+
f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}/resume",
|
|
194
|
+
{},
|
|
195
|
+
)
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
def set_timeout(self, seconds: int) -> "Sandbox":
|
|
199
|
+
self._data = self._client.request(
|
|
200
|
+
"POST",
|
|
201
|
+
f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}/timeout",
|
|
202
|
+
{"timeoutSeconds": seconds},
|
|
203
|
+
)
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def destroy(self) -> None:
|
|
207
|
+
self._client.request(
|
|
208
|
+
"DELETE", f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}"
|
|
209
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Blaxel-shaped compatibility adapter."""
|
|
2
|
+
|
|
3
|
+
from runcloud.client import Client, UnsupportedCompatibilityFeatureError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _SyncProcess:
|
|
7
|
+
def __init__(self, native):
|
|
8
|
+
self._native = native
|
|
9
|
+
|
|
10
|
+
def exec(self, config):
|
|
11
|
+
if config.get("wait_for_completion") is False:
|
|
12
|
+
raise UnsupportedCompatibilityFeatureError("Blaxel", "background processes")
|
|
13
|
+
result = self._native.exec(
|
|
14
|
+
config["command"],
|
|
15
|
+
cwd=config.get("working_dir"),
|
|
16
|
+
env=config.get("env"),
|
|
17
|
+
)
|
|
18
|
+
return {
|
|
19
|
+
"pid": 0,
|
|
20
|
+
"name": config.get("name", ""),
|
|
21
|
+
"status": "completed" if result.exit_code == 0 else "failed",
|
|
22
|
+
"exitCode": result.exit_code,
|
|
23
|
+
"logs": result.stdout + result.stderr,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _SyncDeleteDescriptor:
|
|
28
|
+
def __get__(self, instance, owner):
|
|
29
|
+
if instance is not None:
|
|
30
|
+
|
|
31
|
+
def delete():
|
|
32
|
+
instance.native.destroy()
|
|
33
|
+
|
|
34
|
+
return delete
|
|
35
|
+
|
|
36
|
+
def delete(sandbox_name, client=None, **client_options):
|
|
37
|
+
owner.get(sandbox_name, client=client, **client_options).delete()
|
|
38
|
+
|
|
39
|
+
return delete
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SyncSandboxInstance:
|
|
43
|
+
delete = _SyncDeleteDescriptor()
|
|
44
|
+
|
|
45
|
+
def __init__(self, native):
|
|
46
|
+
self.native = native
|
|
47
|
+
self.process = _SyncProcess(native)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def create(
|
|
51
|
+
cls,
|
|
52
|
+
sandbox=None,
|
|
53
|
+
safe=False,
|
|
54
|
+
create_if_not_exist=False,
|
|
55
|
+
client=None,
|
|
56
|
+
**options,
|
|
57
|
+
):
|
|
58
|
+
config = dict(sandbox or {})
|
|
59
|
+
config.update(options)
|
|
60
|
+
actual = client or Client(
|
|
61
|
+
api_key=config.get("api_key"), api_url=config.get("api_url")
|
|
62
|
+
)
|
|
63
|
+
return cls(
|
|
64
|
+
actual.create(
|
|
65
|
+
image=config.get("image", "runcloud/agent-base"),
|
|
66
|
+
name=config.get("name"),
|
|
67
|
+
cpu=config.get("cpu"),
|
|
68
|
+
memory=config.get("memory"),
|
|
69
|
+
region=config.get("region"),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def get(cls, sandbox_name, client=None, **client_options):
|
|
75
|
+
actual = client or Client(**client_options)
|
|
76
|
+
match = next(
|
|
77
|
+
(item for item in actual.list() if item.name == sandbox_name), None
|
|
78
|
+
)
|
|
79
|
+
if match is None:
|
|
80
|
+
raise LookupError(f"no sandbox named {sandbox_name}")
|
|
81
|
+
return cls(match)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _AsyncProcess:
|
|
85
|
+
def __init__(self, native):
|
|
86
|
+
self._sync = _SyncProcess(native)
|
|
87
|
+
|
|
88
|
+
async def exec(self, config):
|
|
89
|
+
return self._sync.exec(config)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _AsyncDeleteDescriptor:
|
|
93
|
+
def __get__(self, instance, owner):
|
|
94
|
+
if instance is not None:
|
|
95
|
+
|
|
96
|
+
async def delete():
|
|
97
|
+
instance.native.destroy()
|
|
98
|
+
|
|
99
|
+
return delete
|
|
100
|
+
|
|
101
|
+
async def delete(sandbox_name, client=None, **client_options):
|
|
102
|
+
sandbox = await owner.get(
|
|
103
|
+
sandbox_name,
|
|
104
|
+
client=client,
|
|
105
|
+
**client_options,
|
|
106
|
+
)
|
|
107
|
+
await sandbox.delete()
|
|
108
|
+
|
|
109
|
+
return delete
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class SandboxInstance:
|
|
113
|
+
delete = _AsyncDeleteDescriptor()
|
|
114
|
+
|
|
115
|
+
def __init__(self, native):
|
|
116
|
+
self.native = native
|
|
117
|
+
self.process = _AsyncProcess(native)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
async def create(
|
|
121
|
+
cls, sandbox=None, safe=False, create_if_not_exist=False, **options
|
|
122
|
+
):
|
|
123
|
+
config = dict(sandbox or {})
|
|
124
|
+
config.update(options)
|
|
125
|
+
actual = config.pop("client", None) or Client(
|
|
126
|
+
api_key=config.pop("api_key", None),
|
|
127
|
+
api_url=config.pop("api_url", None),
|
|
128
|
+
)
|
|
129
|
+
return cls(
|
|
130
|
+
actual.create(
|
|
131
|
+
image=config.get("image", "runcloud/agent-base"),
|
|
132
|
+
name=config.get("name"),
|
|
133
|
+
cpu=config.get("cpu"),
|
|
134
|
+
memory=config.get("memory"),
|
|
135
|
+
region=config.get("region"),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
async def get(cls, sandbox_name, client=None, **client_options):
|
|
141
|
+
actual = client or Client(**client_options)
|
|
142
|
+
match = next(
|
|
143
|
+
(item for item in actual.list() if item.name == sandbox_name), None
|
|
144
|
+
)
|
|
145
|
+
if match is None:
|
|
146
|
+
raise LookupError(f"no sandbox named {sandbox_name}")
|
|
147
|
+
return cls(match)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Daytona-shaped compatibility adapter."""
|
|
2
|
+
|
|
3
|
+
from runcloud.client import Client, UnsupportedCompatibilityFeatureError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _Process:
|
|
7
|
+
def __init__(self, native):
|
|
8
|
+
self._native = native
|
|
9
|
+
|
|
10
|
+
def exec(self, command, cwd=None, env=None, timeout=None):
|
|
11
|
+
result = self._native.exec(command, cwd=cwd, env=env, timeout=timeout)
|
|
12
|
+
return type(
|
|
13
|
+
"ExecuteResponse",
|
|
14
|
+
(),
|
|
15
|
+
{
|
|
16
|
+
"exit_code": result.exit_code,
|
|
17
|
+
"exitCode": result.exit_code,
|
|
18
|
+
"result": result.stdout,
|
|
19
|
+
"stdout": result.stdout,
|
|
20
|
+
"stderr": result.stderr,
|
|
21
|
+
},
|
|
22
|
+
)()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DaytonaSandbox:
|
|
26
|
+
def __init__(self, native):
|
|
27
|
+
self.native = native
|
|
28
|
+
self.id = native.id
|
|
29
|
+
self.process = _Process(native)
|
|
30
|
+
|
|
31
|
+
def get_preview_link(self, _port):
|
|
32
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
33
|
+
"Daytona",
|
|
34
|
+
"preview links",
|
|
35
|
+
"run.cloud public port capabilities are not available in the SDK yet",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def delete(self):
|
|
39
|
+
self.native.destroy()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Daytona:
|
|
43
|
+
def __init__(self, config=None, client=None, **options):
|
|
44
|
+
config = config or {}
|
|
45
|
+
self.client = client or Client(
|
|
46
|
+
api_key=options.get("api_key") or config.get("api_key"),
|
|
47
|
+
api_url=options.get("server_url") or config.get("server_url"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def create(self, params=None):
|
|
51
|
+
params = params or {}
|
|
52
|
+
return DaytonaSandbox(
|
|
53
|
+
self.client.create(
|
|
54
|
+
image=params.get("image", "runcloud/agent-base"),
|
|
55
|
+
name=params.get("name"),
|
|
56
|
+
region=params.get("region"),
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def get(self, sandbox_id_or_name):
|
|
61
|
+
return DaytonaSandbox(self.client.get(sandbox_id_or_name))
|
|
62
|
+
|
|
63
|
+
def remove(self, sandbox):
|
|
64
|
+
sandbox.delete()
|
|
65
|
+
|
|
66
|
+
def delete(self, sandbox, timeout=None, wait=True):
|
|
67
|
+
sandbox.delete()
|
runcloud/compat/e2b.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""E2B Sandbox-shaped compatibility adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from runcloud.client import Client, UnsupportedCompatibilityFeatureError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _Commands:
|
|
9
|
+
def __init__(self, sandbox):
|
|
10
|
+
self._sandbox = sandbox
|
|
11
|
+
|
|
12
|
+
def run(self, command, cwd=None, envs=None, timeout=None, background=False):
|
|
13
|
+
if background:
|
|
14
|
+
raise UnsupportedCompatibilityFeatureError("E2B", "background commands")
|
|
15
|
+
return self._sandbox.native.exec(command, cwd=cwd, env=envs, timeout=timeout)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Files:
|
|
19
|
+
def __init__(self, sandbox):
|
|
20
|
+
self._sandbox = sandbox
|
|
21
|
+
|
|
22
|
+
def read(self, path):
|
|
23
|
+
return self._sandbox.native.read_file(path).decode()
|
|
24
|
+
|
|
25
|
+
def read_bytes(self, path):
|
|
26
|
+
return self._sandbox.native.read_file(path)
|
|
27
|
+
|
|
28
|
+
def write(self, *_args, **_kwargs):
|
|
29
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
30
|
+
"E2B", "file writes", "use an image or command"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Sandbox:
|
|
35
|
+
def __init__(self, native, metadata=None):
|
|
36
|
+
self.native = native
|
|
37
|
+
self.metadata = metadata or {}
|
|
38
|
+
self.commands = _Commands(self)
|
|
39
|
+
self.files = _Files(self)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create(
|
|
43
|
+
cls,
|
|
44
|
+
template="base",
|
|
45
|
+
timeout=None,
|
|
46
|
+
metadata=None,
|
|
47
|
+
envs=None,
|
|
48
|
+
client=None,
|
|
49
|
+
api_key=None,
|
|
50
|
+
domain=None,
|
|
51
|
+
**_ignored,
|
|
52
|
+
):
|
|
53
|
+
actual = client or Client(api_key=api_key, api_url=domain)
|
|
54
|
+
image = "runcloud/agent-base" if template == "base" else template
|
|
55
|
+
sandbox = cls(actual.create(image=image, timeout_seconds=timeout), metadata)
|
|
56
|
+
sandbox.envs = envs or {}
|
|
57
|
+
return sandbox
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def connect(cls, sandbox_id, client=None, **client_options):
|
|
61
|
+
actual = client or Client(**client_options)
|
|
62
|
+
return cls(actual.get(sandbox_id))
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def sandbox_id(self):
|
|
66
|
+
return self.native.id
|
|
67
|
+
|
|
68
|
+
def set_timeout(self, timeout):
|
|
69
|
+
self.native.set_timeout(timeout)
|
|
70
|
+
|
|
71
|
+
def get_host(self, _port):
|
|
72
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
73
|
+
"E2B",
|
|
74
|
+
"get_host",
|
|
75
|
+
"run.cloud public port capabilities are not available in the SDK yet",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def kill(self):
|
|
79
|
+
self.native.destroy()
|
|
80
|
+
return True
|
runcloud/compat/modal.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Modal Sandbox-shaped compatibility adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from runcloud.client import (
|
|
8
|
+
Client,
|
|
9
|
+
UnsupportedCompatibilityFeatureError,
|
|
10
|
+
)
|
|
11
|
+
from runcloud.client import (
|
|
12
|
+
Sandbox as NativeSandbox,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Image:
|
|
17
|
+
def __init__(self, ref: str):
|
|
18
|
+
self.ref = ref
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_registry(cls, ref: str) -> "Image":
|
|
22
|
+
return cls(ref)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def debian_slim(cls, python_version: Optional[str] = None) -> "Image":
|
|
26
|
+
return cls("debian:12-slim")
|
|
27
|
+
|
|
28
|
+
def _unsupported(self, feature: str):
|
|
29
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
30
|
+
"Modal", feature, "register a prebuilt OCI image with run.cloud"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def apt_install(self, *packages: str):
|
|
34
|
+
return self._unsupported("Image.apt_install")
|
|
35
|
+
|
|
36
|
+
def pip_install(self, *packages: str):
|
|
37
|
+
return self._unsupported("Image.pip_install")
|
|
38
|
+
|
|
39
|
+
def run_commands(self, *commands: str):
|
|
40
|
+
return self._unsupported("Image.run_commands")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Process:
|
|
44
|
+
def __init__(self, result):
|
|
45
|
+
self._result = result
|
|
46
|
+
self.stdout = _Reader(result.stdout)
|
|
47
|
+
self.stderr = _Reader(result.stderr)
|
|
48
|
+
self.returncode = result.exit_code
|
|
49
|
+
|
|
50
|
+
def wait(self):
|
|
51
|
+
return self.returncode
|
|
52
|
+
|
|
53
|
+
def poll(self):
|
|
54
|
+
return self.returncode
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _Reader:
|
|
58
|
+
def __init__(self, value: str):
|
|
59
|
+
self._value = value
|
|
60
|
+
|
|
61
|
+
def read(self):
|
|
62
|
+
return self._value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class App:
|
|
66
|
+
def __init__(self, name: str, client: Client):
|
|
67
|
+
self.name = name
|
|
68
|
+
self.client = client
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def lookup(
|
|
72
|
+
cls,
|
|
73
|
+
name: str,
|
|
74
|
+
create_if_missing: bool = True,
|
|
75
|
+
client: Optional[Client] = None,
|
|
76
|
+
**client_options,
|
|
77
|
+
) -> "App":
|
|
78
|
+
return cls(name, client or Client(**client_options))
|
|
79
|
+
|
|
80
|
+
def create_sandbox(self, image=None, **options):
|
|
81
|
+
return Sandbox.create(app=self, image=image, **options)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Sandbox:
|
|
85
|
+
def __init__(self, native: NativeSandbox, app: Optional[App] = None):
|
|
86
|
+
self.native = native
|
|
87
|
+
self.app = app
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def object_id(self):
|
|
91
|
+
return self.native.id
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def create(
|
|
95
|
+
cls,
|
|
96
|
+
*args,
|
|
97
|
+
app: Optional[App] = None,
|
|
98
|
+
image=None,
|
|
99
|
+
cpu=None,
|
|
100
|
+
memory=None,
|
|
101
|
+
timeout=None,
|
|
102
|
+
idle_timeout=None,
|
|
103
|
+
name=None,
|
|
104
|
+
region=None,
|
|
105
|
+
command=None,
|
|
106
|
+
volumes=None,
|
|
107
|
+
**_ignored,
|
|
108
|
+
):
|
|
109
|
+
if volumes:
|
|
110
|
+
raise UnsupportedCompatibilityFeatureError("Modal", "volumes")
|
|
111
|
+
if isinstance(cpu, tuple):
|
|
112
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
113
|
+
"Modal", "CPU request/limit tuples", "pass one CPU reservation"
|
|
114
|
+
)
|
|
115
|
+
client = app.client if app else Client()
|
|
116
|
+
image_ref = image.ref if isinstance(image, Image) else image
|
|
117
|
+
native = client.create(
|
|
118
|
+
image=image_ref or "runcloud/agent-base",
|
|
119
|
+
cpu=cpu,
|
|
120
|
+
memory=memory,
|
|
121
|
+
timeout_seconds=timeout,
|
|
122
|
+
idle_pause_seconds=idle_timeout,
|
|
123
|
+
name=name,
|
|
124
|
+
region=region,
|
|
125
|
+
)
|
|
126
|
+
sandbox = cls(native, app)
|
|
127
|
+
entrypoint = list(args) if args else command
|
|
128
|
+
if entrypoint:
|
|
129
|
+
sandbox._entrypoint = sandbox.exec(*entrypoint)
|
|
130
|
+
return sandbox
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_id(cls, sandbox_id: str, client: Optional[Client] = None):
|
|
134
|
+
actual = client or Client()
|
|
135
|
+
return cls(actual.get(sandbox_id))
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_name(cls, app: App, name: str):
|
|
139
|
+
match = next((item for item in app.client.list() if item.name == name), None)
|
|
140
|
+
if match is None:
|
|
141
|
+
raise LookupError(f"no sandbox named {name}")
|
|
142
|
+
return cls(match, app)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def list(cls, app: Optional[App] = None):
|
|
146
|
+
client = app.client if app else Client()
|
|
147
|
+
return [cls(item, app) for item in client.list()]
|
|
148
|
+
|
|
149
|
+
def exec(self, *args, timeout=None, workdir=None, env=None, **_ignored):
|
|
150
|
+
cmd = list(args) if len(args) > 1 else args[0]
|
|
151
|
+
return Process(self.native.exec(cmd, cwd=workdir, env=env, timeout=timeout))
|
|
152
|
+
|
|
153
|
+
def terminate(self, wait=True):
|
|
154
|
+
self.native.destroy()
|
|
155
|
+
|
|
156
|
+
def poll(self):
|
|
157
|
+
self.native.refresh()
|
|
158
|
+
return None if self.native.state in {"running", "starting", "pending"} else 0
|
|
159
|
+
|
|
160
|
+
def tunnels(self):
|
|
161
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
162
|
+
"Modal",
|
|
163
|
+
"tunnels",
|
|
164
|
+
"run.cloud public port capabilities are not available in the SDK yet",
|
|
165
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Fly Sprites-shaped compatibility adapter."""
|
|
2
|
+
|
|
3
|
+
from runcloud.client import Client, UnsupportedCompatibilityFeatureError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Sprite:
|
|
7
|
+
def __init__(self, native, name):
|
|
8
|
+
self.native = native
|
|
9
|
+
self.name = name
|
|
10
|
+
|
|
11
|
+
def exec(self, command, args=None, cwd=None, env=None):
|
|
12
|
+
return self.native.exec([command] + list(args or []), cwd=cwd, env=env)
|
|
13
|
+
|
|
14
|
+
def filesystem(self, _base="/"):
|
|
15
|
+
raise UnsupportedCompatibilityFeatureError("Fly Sprites", "filesystem writes")
|
|
16
|
+
|
|
17
|
+
def url(self, _port=8080):
|
|
18
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
19
|
+
"Fly Sprites",
|
|
20
|
+
"public URLs",
|
|
21
|
+
"run.cloud public port capabilities are not available in the SDK yet",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def delete(self):
|
|
25
|
+
self.native.destroy()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SpritesClient:
|
|
29
|
+
def __init__(self, client=None, api_key=None, base_url=None):
|
|
30
|
+
self.client = client or Client(api_key=api_key, api_url=base_url)
|
|
31
|
+
|
|
32
|
+
def create_sprite(self, name, config=None):
|
|
33
|
+
config = config or {}
|
|
34
|
+
return Sprite(
|
|
35
|
+
self.client.create(
|
|
36
|
+
image=config.get("image", "runcloud/agent-base"),
|
|
37
|
+
name=name,
|
|
38
|
+
region=config.get("region"),
|
|
39
|
+
),
|
|
40
|
+
name,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def get_sprite(self, name):
|
|
44
|
+
match = next((item for item in self.client.list() if item.name == name), None)
|
|
45
|
+
if match is None:
|
|
46
|
+
raise LookupError(f"no sprite named {name}")
|
|
47
|
+
return Sprite(match, name)
|
|
48
|
+
|
|
49
|
+
def destroy_sprite(self, name):
|
|
50
|
+
self.get_sprite(name).delete()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Vercel Sandbox-shaped compatibility adapter."""
|
|
2
|
+
|
|
3
|
+
from runcloud.client import Client, UnsupportedCompatibilityFeatureError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Sandbox:
|
|
7
|
+
def __init__(self, native):
|
|
8
|
+
self.native = native
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def create(
|
|
12
|
+
cls,
|
|
13
|
+
client=None,
|
|
14
|
+
token=None,
|
|
15
|
+
runtime=None,
|
|
16
|
+
resources=None,
|
|
17
|
+
timeout=None,
|
|
18
|
+
**options,
|
|
19
|
+
):
|
|
20
|
+
actual = client or Client(api_key=token)
|
|
21
|
+
resources = resources or {}
|
|
22
|
+
return cls(
|
|
23
|
+
actual.create(
|
|
24
|
+
image=options.get("image", "runcloud/agent-base"),
|
|
25
|
+
name=options.get("name"),
|
|
26
|
+
cpu=resources.get("vcpus"),
|
|
27
|
+
timeout_seconds=(timeout // 1000 if timeout else None),
|
|
28
|
+
region=options.get("region"),
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def sandbox_id(self):
|
|
34
|
+
return self.native.id
|
|
35
|
+
|
|
36
|
+
def run_command(self, command, args=None, cwd=None, env=None):
|
|
37
|
+
return self.native.exec([command] + list(args or []), cwd=cwd, env=env)
|
|
38
|
+
|
|
39
|
+
def read_file(self, path):
|
|
40
|
+
return self.native.read_file(path)
|
|
41
|
+
|
|
42
|
+
def write_files(self, *_args, **_kwargs):
|
|
43
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
44
|
+
"Vercel Sandbox", "file writes", "use an image or command"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def domain(self, _port):
|
|
48
|
+
raise UnsupportedCompatibilityFeatureError(
|
|
49
|
+
"Vercel Sandbox",
|
|
50
|
+
"domain",
|
|
51
|
+
"run.cloud public port capabilities are not available in the SDK yet",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def stop(self):
|
|
55
|
+
self.native.destroy()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runcloud-sdk
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Python SDK for run.cloud, including sandbox-provider compatibility adapters
|
|
5
|
+
Author: Newly
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://run.cloud
|
|
8
|
+
Project-URL: Documentation, https://docs.run.cloud
|
|
9
|
+
Project-URL: Repository, https://github.com/newly-app/newly/tree/main/run-cloud/python-sdk
|
|
10
|
+
Keywords: runcloud,run-cloud,sandbox,microvm,firecracker,sdk
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# runcloud
|
|
19
|
+
|
|
20
|
+
Python SDK for [run.cloud](https://run.cloud).
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install runcloud-sdk
|
|
24
|
+
export RUN_CLOUD_API_KEY="rc_live_..."
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from runcloud import Client
|
|
29
|
+
|
|
30
|
+
cloud = Client()
|
|
31
|
+
box = cloud.create(image="runcloud/agent-base", cpu=2, memory=4096)
|
|
32
|
+
try:
|
|
33
|
+
result = box.exec("python -V")
|
|
34
|
+
print(result.stdout)
|
|
35
|
+
finally:
|
|
36
|
+
box.destroy()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Compatibility adapters
|
|
40
|
+
|
|
41
|
+
The same distribution includes migration adapters:
|
|
42
|
+
|
|
43
|
+
| Provider | Import |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| Modal | `from runcloud.compat import modal` |
|
|
46
|
+
| E2B | `from runcloud.compat.e2b import Sandbox` |
|
|
47
|
+
| Daytona | `from runcloud.compat.daytona import Daytona` |
|
|
48
|
+
| Vercel Sandbox | `from runcloud.compat.vercel import Sandbox` |
|
|
49
|
+
| Blaxel async | `from runcloud.compat.blaxel import SandboxInstance` |
|
|
50
|
+
| Blaxel sync | `from runcloud.compat.blaxel import SyncSandboxInstance` |
|
|
51
|
+
| Fly Sprites | `from runcloud.compat.sprites import SpritesClient` |
|
|
52
|
+
|
|
53
|
+
Adapters cover creation, command execution, lookup/listing, and destruction
|
|
54
|
+
where the upstream SDK exposes them. Blaxel's current `SandboxInstance` API is
|
|
55
|
+
async; `SyncSandboxInstance` preserves its synchronous API. Unsupported
|
|
56
|
+
provider-specific features raise `UnsupportedCompatibilityFeatureError` with a
|
|
57
|
+
migration hint. They do not silently pretend that run.cloud implements a
|
|
58
|
+
provider-specific feature.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
runcloud/__init__.py,sha256=wds-DF2ZQK0db4RHQR3Wo4hOw4VCJrcTubBKRZ6Rf_Y,293
|
|
2
|
+
runcloud/client.py,sha256=TF6qhbsLpjOMppycmg3NY1puYEB_R48WZBVLY0UORRc,6908
|
|
3
|
+
runcloud/compat/__init__.py,sha256=iFP29JT0jlx0vIhMtXk0l0-WPEfPYvK96_FW7U6nKaw,119
|
|
4
|
+
runcloud/compat/blaxel.py,sha256=0wsV4MjA1WY0-JPvb2rtn6NpUpinl4O0wfzDA49r39w,4211
|
|
5
|
+
runcloud/compat/daytona.py,sha256=ezmDpkKitcu68SQjNP066aaoJk7Pq_KCUB-bNCCN6kU,1962
|
|
6
|
+
runcloud/compat/e2b.py,sha256=pQofjRcsK1wJhDpa7__bd3WW_38351zXM1ggteO7ZZo,2269
|
|
7
|
+
runcloud/compat/modal.py,sha256=ABVh_qfDrpJdyilzQlbhBoyhIr_qSlzzSyc4w5emB68,4602
|
|
8
|
+
runcloud/compat/sprites.py,sha256=pLnHesnBTBvrK_3CSKq0ILol-208mZtpUCOZs2NNSdM,1572
|
|
9
|
+
runcloud/compat/vercel.py,sha256=Dz8onGxnwy_aA_farInEmc9z67yi3QHON3dKGK9jMio,1564
|
|
10
|
+
runcloud_sdk-0.7.0.dist-info/licenses/LICENSE,sha256=mNNHhtplTZNlKXozXEVPrPkQrSvDipjzcN9fKo6POw8,704
|
|
11
|
+
runcloud_sdk-0.7.0.dist-info/METADATA,sha256=xAgGVuSSe7B0I5y8hwMPr550QxjVGpoL1ac6VVR_H0M,1956
|
|
12
|
+
runcloud_sdk-0.7.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
runcloud_sdk-0.7.0.dist-info/top_level.txt,sha256=ykLh1Jbm6lyHzNmf2f1TpZ74cuwzrQo5ENFhpfyrZK0,9
|
|
14
|
+
runcloud_sdk-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Newly
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
runcloud
|