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 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}"