cubepod 0.4.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.
- cubepod/__init__.py +33 -0
- cubepod/_helpers.py +306 -0
- cubepod/async_client.py +768 -0
- cubepod/exceptions.py +5 -0
- cubepod/models.py +103 -0
- cubepod/py.typed +0 -0
- cubepod/sync_client.py +771 -0
- cubepod-0.4.0.dist-info/METADATA +103 -0
- cubepod-0.4.0.dist-info/RECORD +10 -0
- cubepod-0.4.0.dist-info/WHEEL +4 -0
cubepod/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""CubePod Python SDK — Sync & Async client for persistent MicroVM agent sandboxes."""
|
|
2
|
+
|
|
3
|
+
from .async_client import AsyncCubePod
|
|
4
|
+
from .exceptions import CubePodFileTooLargeError
|
|
5
|
+
from .models import (
|
|
6
|
+
CommandInfo,
|
|
7
|
+
CommandLogs,
|
|
8
|
+
DesktopSession,
|
|
9
|
+
ExecutionResult,
|
|
10
|
+
FileEntry,
|
|
11
|
+
PanelEndpoint,
|
|
12
|
+
SandboxInfo,
|
|
13
|
+
_build_tar_gz_bytes,
|
|
14
|
+
)
|
|
15
|
+
from .sync_client import CubePod, SyncCubePod
|
|
16
|
+
|
|
17
|
+
__version__ = "0.4.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"__version__",
|
|
21
|
+
"SandboxInfo",
|
|
22
|
+
"ExecutionResult",
|
|
23
|
+
"CommandInfo",
|
|
24
|
+
"CommandLogs",
|
|
25
|
+
"PanelEndpoint",
|
|
26
|
+
"FileEntry",
|
|
27
|
+
"DesktopSession",
|
|
28
|
+
"CubePodFileTooLargeError",
|
|
29
|
+
"_build_tar_gz_bytes",
|
|
30
|
+
"AsyncCubePod",
|
|
31
|
+
"CubePod",
|
|
32
|
+
"SyncCubePod",
|
|
33
|
+
]
|
cubepod/_helpers.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Shared payload builders and response parsers for sync and async CubePod clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .models import (
|
|
11
|
+
CommandInfo,
|
|
12
|
+
CommandLogs,
|
|
13
|
+
DesktopSession,
|
|
14
|
+
ExecutionResult,
|
|
15
|
+
FileEntry,
|
|
16
|
+
PanelEndpoint,
|
|
17
|
+
SandboxInfo,
|
|
18
|
+
_build_tar_gz_bytes,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_base_url(base_url: str) -> str:
|
|
23
|
+
normalized = base_url.strip().rstrip("/")
|
|
24
|
+
if normalized and not normalized.startswith(("http://", "https://")):
|
|
25
|
+
normalized = f"http://{normalized}"
|
|
26
|
+
return normalized
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _json_dict(resp: httpx.Response) -> dict[str, Any]:
|
|
30
|
+
return cast(dict[str, Any], resp.json())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _json_list(resp: httpx.Response, wrapper_key: str) -> list[dict[str, Any]]:
|
|
34
|
+
data = resp.json()
|
|
35
|
+
if isinstance(data, list):
|
|
36
|
+
return cast(list[dict[str, Any]], data)
|
|
37
|
+
return cast(list[dict[str, Any]], data.get(wrapper_key, []))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_info(data: dict[str, Any]) -> SandboxInfo:
|
|
41
|
+
raw_metadata = data.get("metadata")
|
|
42
|
+
metadata: dict[str, str] = (
|
|
43
|
+
{str(k): str(v) for k, v in raw_metadata.items()}
|
|
44
|
+
if isinstance(raw_metadata, dict)
|
|
45
|
+
else {}
|
|
46
|
+
)
|
|
47
|
+
envd_port_raw = data.get("envdPort")
|
|
48
|
+
rootfs_gen_raw = data.get("rootfsGeneration")
|
|
49
|
+
return SandboxInfo(
|
|
50
|
+
sandbox_id=str(data["sandboxID"]),
|
|
51
|
+
template_id=str(data["templateID"]),
|
|
52
|
+
state=str(data["state"]),
|
|
53
|
+
envd_port=int(envd_port_raw) if envd_port_raw is not None else 49983,
|
|
54
|
+
rootfs_generation=int(rootfs_gen_raw) if rootfs_gen_raw is not None else 1,
|
|
55
|
+
metadata=metadata,
|
|
56
|
+
raw=dict(data),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_panel_endpoint(data: dict[str, Any]) -> PanelEndpoint:
|
|
61
|
+
return PanelEndpoint(
|
|
62
|
+
url=str(data["url"]),
|
|
63
|
+
token=str(data["token"]),
|
|
64
|
+
expires_at=str(data["expiresAt"]) if data.get("expiresAt") is not None else None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_desktop_session(data: dict[str, Any]) -> DesktopSession:
|
|
69
|
+
cdp_raw = data.get("cdpUrl") if "cdpUrl" in data else data.get("cdp_url")
|
|
70
|
+
expires_raw = data.get("expiresAt") if "expiresAt" in data else data.get("expires_at")
|
|
71
|
+
return DesktopSession(
|
|
72
|
+
status=str(data.get("status", "")),
|
|
73
|
+
desktop_url=str(data.get("desktopUrl") or data.get("desktop_url") or ""),
|
|
74
|
+
terminal_url=str(data.get("terminalUrl") or data.get("terminal_url") or ""),
|
|
75
|
+
cdp_url=str(cdp_raw) if cdp_raw is not None else None,
|
|
76
|
+
expires_at=str(expires_raw) if expires_raw is not None else None,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _parse_execution_result(data: dict[str, Any]) -> ExecutionResult:
|
|
81
|
+
exit_code = data.get("exit_code") if "exit_code" in data else data.get("exitCode")
|
|
82
|
+
output = data.get("output", "") or data.get("stdout", "")
|
|
83
|
+
cmd_id = data.get("id") or data.get("cmdID", "")
|
|
84
|
+
return ExecutionResult(
|
|
85
|
+
id=str(cmd_id),
|
|
86
|
+
exit_code=int(exit_code) if exit_code is not None else None,
|
|
87
|
+
output=str(output),
|
|
88
|
+
running=bool(data.get("running", False)),
|
|
89
|
+
pid=int(data["pid"]) if data.get("pid") is not None else None,
|
|
90
|
+
cursor=int(data.get("cursor", 0)),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_command_info(
|
|
95
|
+
data: dict[str, Any],
|
|
96
|
+
*,
|
|
97
|
+
default_id: str = "",
|
|
98
|
+
default_running: bool = False,
|
|
99
|
+
) -> CommandInfo:
|
|
100
|
+
cmd_id = data.get("id") or data.get("cmdID", default_id)
|
|
101
|
+
exit_code = data.get("exit_code") if "exit_code" in data else data.get("exitCode")
|
|
102
|
+
output = data.get("output", "") or data.get("stdout", "")
|
|
103
|
+
return CommandInfo(
|
|
104
|
+
id=str(cmd_id),
|
|
105
|
+
pid=int(data["pid"]) if data.get("pid") is not None else None,
|
|
106
|
+
running=bool(data.get("running", default_running)),
|
|
107
|
+
exit_code=int(exit_code) if exit_code is not None else None,
|
|
108
|
+
cursor=int(data.get("cursor", 0)),
|
|
109
|
+
output=str(output),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_command_logs(
|
|
114
|
+
data: dict[str, Any],
|
|
115
|
+
*,
|
|
116
|
+
command_id: str,
|
|
117
|
+
default_cursor: int,
|
|
118
|
+
) -> CommandLogs:
|
|
119
|
+
cmd_id = data.get("id") or data.get("cmdID", command_id)
|
|
120
|
+
exit_code = data.get("exit_code") if "exit_code" in data else data.get("exitCode")
|
|
121
|
+
return CommandLogs(
|
|
122
|
+
id=str(cmd_id),
|
|
123
|
+
content=str(data.get("content", "")),
|
|
124
|
+
cursor=int(data.get("cursor", default_cursor)),
|
|
125
|
+
running=bool(data.get("running", False)),
|
|
126
|
+
exit_code=int(exit_code) if exit_code is not None else None,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _parse_file_entries(data: Any) -> list[FileEntry]:
|
|
131
|
+
entries = data if isinstance(data, list) else data.get("entries", [])
|
|
132
|
+
result: list[FileEntry] = []
|
|
133
|
+
for e in entries:
|
|
134
|
+
raw_mod = e.get("modified_at") or e.get("modifiedAt") or e.get("mtime")
|
|
135
|
+
result.append(
|
|
136
|
+
FileEntry(
|
|
137
|
+
name=str(e.get("name", "")),
|
|
138
|
+
path=str(e.get("path", "")),
|
|
139
|
+
is_dir=bool(e.get("is_dir", False)),
|
|
140
|
+
size=int(e.get("size", 0)),
|
|
141
|
+
mode=str(e.get("mode", "0644")),
|
|
142
|
+
modified_at=str(raw_mod) if raw_mod is not None else None,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_create_payload(
|
|
149
|
+
template_id: str,
|
|
150
|
+
*,
|
|
151
|
+
volume_key: str | None = None,
|
|
152
|
+
metadata: dict[str, str] | None = None,
|
|
153
|
+
network_policy: dict[str, Any] | None = None,
|
|
154
|
+
secret_bindings: list[dict[str, Any]] | None = None,
|
|
155
|
+
timeout_seconds: int | None = None,
|
|
156
|
+
warm_pool_ref: str | None = None,
|
|
157
|
+
resource: dict[str, str] | None = None,
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
payload: dict[str, Any] = {"templateID": template_id, "metadata": metadata or {}}
|
|
160
|
+
if timeout_seconds is not None:
|
|
161
|
+
payload["timeoutSeconds"] = timeout_seconds
|
|
162
|
+
|
|
163
|
+
ext: dict[str, Any] = {}
|
|
164
|
+
if volume_key:
|
|
165
|
+
ext["volumeKey"] = volume_key
|
|
166
|
+
if warm_pool_ref:
|
|
167
|
+
ext["warmPoolRef"] = warm_pool_ref
|
|
168
|
+
if resource:
|
|
169
|
+
ext["resource"] = resource
|
|
170
|
+
if ext:
|
|
171
|
+
payload["extension"] = ext
|
|
172
|
+
|
|
173
|
+
if network_policy is not None:
|
|
174
|
+
payload["networkPolicy"] = dict(network_policy)
|
|
175
|
+
if secret_bindings is not None:
|
|
176
|
+
if "networkPolicy" not in payload:
|
|
177
|
+
payload["networkPolicy"] = {}
|
|
178
|
+
payload["networkPolicy"]["secretBindings"] = secret_bindings
|
|
179
|
+
return payload
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _build_fork_payload(
|
|
183
|
+
*,
|
|
184
|
+
volume_key: str | None = None,
|
|
185
|
+
metadata: dict[str, str] | None = None,
|
|
186
|
+
) -> dict[str, Any] | None:
|
|
187
|
+
payload: dict[str, Any] = {}
|
|
188
|
+
if metadata is not None:
|
|
189
|
+
payload["metadata"] = metadata
|
|
190
|
+
if volume_key:
|
|
191
|
+
payload["extension"] = {"volumeKey": volume_key}
|
|
192
|
+
return payload or None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _build_command_payload(
|
|
196
|
+
command: str,
|
|
197
|
+
*,
|
|
198
|
+
background: bool,
|
|
199
|
+
workdir: str | None = None,
|
|
200
|
+
envs: dict[str, str] | None = None,
|
|
201
|
+
uid: int | None = 1000,
|
|
202
|
+
gid: int | None = 1000,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
payload: dict[str, Any] = {
|
|
205
|
+
"command": command,
|
|
206
|
+
"background": background,
|
|
207
|
+
}
|
|
208
|
+
if uid is not None:
|
|
209
|
+
payload["uid"] = uid
|
|
210
|
+
if gid is not None:
|
|
211
|
+
payload["gid"] = gid
|
|
212
|
+
if workdir is not None:
|
|
213
|
+
payload["workdir"] = workdir
|
|
214
|
+
if envs is not None:
|
|
215
|
+
payload["envs"] = envs
|
|
216
|
+
return payload
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _build_archive_sync_request(
|
|
220
|
+
files: dict[str, str | bytes] | bytes,
|
|
221
|
+
*,
|
|
222
|
+
dest: str = "/workspace",
|
|
223
|
+
uid: int | None = None,
|
|
224
|
+
gid: int | None = None,
|
|
225
|
+
remove_paths: Sequence[str] | None = None,
|
|
226
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
227
|
+
if isinstance(files, (bytes, bytearray)):
|
|
228
|
+
tar_bytes = bytes(files)
|
|
229
|
+
else:
|
|
230
|
+
tar_bytes = _build_tar_gz_bytes(
|
|
231
|
+
files,
|
|
232
|
+
uid=uid if uid is not None else 1000,
|
|
233
|
+
gid=gid if gid is not None else 1000,
|
|
234
|
+
)
|
|
235
|
+
params: dict[str, str] = {"dest": dest}
|
|
236
|
+
if uid is not None:
|
|
237
|
+
params["uid"] = str(uid)
|
|
238
|
+
if gid is not None:
|
|
239
|
+
params["gid"] = str(gid)
|
|
240
|
+
if remove_paths:
|
|
241
|
+
joined = ",".join(p for p in remove_paths if p)
|
|
242
|
+
if joined:
|
|
243
|
+
params["remove_paths"] = joined
|
|
244
|
+
return tar_bytes, params
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _build_desktop_start_payload(
|
|
248
|
+
*,
|
|
249
|
+
resolution: str | None = None,
|
|
250
|
+
initial_url: str | None = None,
|
|
251
|
+
expires_in_seconds: int = 3600,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
payload: dict[str, Any] = {"expiresInSeconds": expires_in_seconds}
|
|
254
|
+
if resolution is not None:
|
|
255
|
+
payload["resolution"] = resolution
|
|
256
|
+
if initial_url is not None:
|
|
257
|
+
payload["initialUrl"] = initial_url
|
|
258
|
+
return payload
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _build_template_register_payload(
|
|
262
|
+
*,
|
|
263
|
+
name: str | None,
|
|
264
|
+
alias: str | None,
|
|
265
|
+
tags: list[str] | None,
|
|
266
|
+
cpu_count: int | None,
|
|
267
|
+
memory_mb: int | None,
|
|
268
|
+
min_free_disk_mb: int | None,
|
|
269
|
+
) -> dict[str, Any]:
|
|
270
|
+
payload: dict[str, Any] = {}
|
|
271
|
+
if name is not None:
|
|
272
|
+
payload["name"] = name
|
|
273
|
+
if alias is not None:
|
|
274
|
+
payload["alias"] = alias
|
|
275
|
+
if tags is not None:
|
|
276
|
+
payload["tags"] = tags
|
|
277
|
+
if cpu_count is not None:
|
|
278
|
+
payload["cpuCount"] = cpu_count
|
|
279
|
+
if memory_mb is not None:
|
|
280
|
+
payload["memoryMB"] = memory_mb
|
|
281
|
+
if min_free_disk_mb is not None:
|
|
282
|
+
payload["minFreeDiskMb"] = min_free_disk_mb
|
|
283
|
+
return payload
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _build_template_result(created: dict[str, Any]) -> dict[str, Any]:
|
|
287
|
+
return {
|
|
288
|
+
"templateID": str(created.get("templateID", "")),
|
|
289
|
+
"buildID": str(created.get("buildID", "")),
|
|
290
|
+
"aliases": created.get("aliases", []),
|
|
291
|
+
"names": created.get("names", []),
|
|
292
|
+
"tags": created.get("tags", []),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _format_build_error(build_id: str, reason: Any) -> str:
|
|
297
|
+
if isinstance(reason, dict):
|
|
298
|
+
msg = reason.get("message") or str(reason)
|
|
299
|
+
step = reason.get("step")
|
|
300
|
+
if step:
|
|
301
|
+
msg = f"{msg} (step: {step})"
|
|
302
|
+
elif reason:
|
|
303
|
+
msg = str(reason)
|
|
304
|
+
else:
|
|
305
|
+
msg = "unknown build error"
|
|
306
|
+
return f"Template build {build_id} failed: {msg}"
|