loopix-sdk 2.30.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.
- loopix/__init__.py +260 -0
- loopix/api/__init__.py +287 -0
- loopix/api/client/__init__.py +8 -0
- loopix/api/client/api/__init__.py +1 -0
- loopix/api/client/api/sandboxes/__init__.py +1 -0
- loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
- loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
- loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
- loopix/api/client/api/snapshots/__init__.py +1 -0
- loopix/api/client/api/snapshots/get_snapshots.py +202 -0
- loopix/api/client/api/tags/__init__.py +1 -0
- loopix/api/client/api/tags/delete_templates_tags.py +174 -0
- loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
- loopix/api/client/api/tags/post_templates_tags.py +176 -0
- loopix/api/client/api/templates/__init__.py +1 -0
- loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
- loopix/api/client/api/templates/get_templates.py +172 -0
- loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
- loopix/api/client/api/templates/get_templates_template_id.py +195 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
- loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
- loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
- loopix/api/client/api/templates/post_templates.py +172 -0
- loopix/api/client/api/templates/post_templates_template_id.py +181 -0
- loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- loopix/api/client/api/templates/post_v2_templates.py +172 -0
- loopix/api/client/api/templates/post_v3_templates.py +176 -0
- loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- loopix/api/client/api/volumes/__init__.py +1 -0
- loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
- loopix/api/client/api/volumes/get_volumes.py +140 -0
- loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
- loopix/api/client/api/volumes/post_volumes.py +172 -0
- loopix/api/client/client.py +286 -0
- loopix/api/client/errors.py +16 -0
- loopix/api/client/models/__init__.py +185 -0
- loopix/api/client/models/admin_build_cancel_result.py +67 -0
- loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
- loopix/api/client/models/assign_template_tags_request.py +67 -0
- loopix/api/client/models/assigned_template_tags.py +68 -0
- loopix/api/client/models/aws_registry.py +85 -0
- loopix/api/client/models/aws_registry_type.py +8 -0
- loopix/api/client/models/build_log_entry.py +89 -0
- loopix/api/client/models/build_status_reason.py +95 -0
- loopix/api/client/models/connect_sandbox.py +59 -0
- loopix/api/client/models/created_access_token.py +100 -0
- loopix/api/client/models/created_team_api_key.py +166 -0
- loopix/api/client/models/delete_template_tags_request.py +67 -0
- loopix/api/client/models/disk_metrics.py +91 -0
- loopix/api/client/models/error.py +67 -0
- loopix/api/client/models/gcp_registry.py +69 -0
- loopix/api/client/models/gcp_registry_type.py +8 -0
- loopix/api/client/models/general_registry.py +77 -0
- loopix/api/client/models/general_registry_type.py +8 -0
- loopix/api/client/models/identifier_masking_details.py +83 -0
- loopix/api/client/models/listed_sandbox.py +179 -0
- loopix/api/client/models/log_level.py +11 -0
- loopix/api/client/models/logs_direction.py +9 -0
- loopix/api/client/models/logs_source.py +9 -0
- loopix/api/client/models/machine_info.py +83 -0
- loopix/api/client/models/max_team_metric.py +78 -0
- loopix/api/client/models/mcp_type_0.py +44 -0
- loopix/api/client/models/new_access_token.py +59 -0
- loopix/api/client/models/new_sandbox.py +224 -0
- loopix/api/client/models/new_team_api_key.py +59 -0
- loopix/api/client/models/new_volume.py +59 -0
- loopix/api/client/models/node.py +160 -0
- loopix/api/client/models/node_detail.py +160 -0
- loopix/api/client/models/node_metrics.py +122 -0
- loopix/api/client/models/node_status.py +12 -0
- loopix/api/client/models/node_status_change.py +82 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- loopix/api/client/models/resumed_sandbox.py +68 -0
- loopix/api/client/models/sandbox.py +145 -0
- loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
- loopix/api/client/models/sandbox_detail.py +267 -0
- loopix/api/client/models/sandbox_lifecycle.py +70 -0
- loopix/api/client/models/sandbox_log.py +70 -0
- loopix/api/client/models/sandbox_log_entry.py +93 -0
- loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
- loopix/api/client/models/sandbox_logs.py +91 -0
- loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
- loopix/api/client/models/sandbox_metric.py +126 -0
- loopix/api/client/models/sandbox_network_config.py +118 -0
- loopix/api/client/models/sandbox_network_config_rules.py +72 -0
- loopix/api/client/models/sandbox_network_rule.py +74 -0
- loopix/api/client/models/sandbox_network_transform.py +79 -0
- loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
- loopix/api/client/models/sandbox_network_update_config.py +114 -0
- loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
- loopix/api/client/models/sandbox_on_timeout.py +9 -0
- loopix/api/client/models/sandbox_pause_request.py +62 -0
- loopix/api/client/models/sandbox_state.py +9 -0
- loopix/api/client/models/sandbox_volume_mount.py +67 -0
- loopix/api/client/models/sandboxes_with_metrics.py +59 -0
- loopix/api/client/models/snapshot_info.py +70 -0
- loopix/api/client/models/team.py +83 -0
- loopix/api/client/models/team_api_key.py +158 -0
- loopix/api/client/models/team_metric.py +86 -0
- loopix/api/client/models/team_user.py +75 -0
- loopix/api/client/models/template.py +225 -0
- loopix/api/client/models/template_alias_response.py +67 -0
- loopix/api/client/models/template_build.py +139 -0
- loopix/api/client/models/template_build_file_upload.py +70 -0
- loopix/api/client/models/template_build_info.py +126 -0
- loopix/api/client/models/template_build_logs_response.py +73 -0
- loopix/api/client/models/template_build_request.py +115 -0
- loopix/api/client/models/template_build_request_v2.py +88 -0
- loopix/api/client/models/template_build_request_v3.py +107 -0
- loopix/api/client/models/template_build_start_v2.py +184 -0
- loopix/api/client/models/template_build_status.py +11 -0
- loopix/api/client/models/template_legacy.py +207 -0
- loopix/api/client/models/template_request_response_v3.py +99 -0
- loopix/api/client/models/template_step.py +91 -0
- loopix/api/client/models/template_tag.py +78 -0
- loopix/api/client/models/template_update_request.py +59 -0
- loopix/api/client/models/template_update_response.py +59 -0
- loopix/api/client/models/template_with_builds.py +156 -0
- loopix/api/client/models/update_team_api_key.py +59 -0
- loopix/api/client/models/volume.py +67 -0
- loopix/api/client/models/volume_and_token.py +75 -0
- loopix/api/client/models/volume_token.py +59 -0
- loopix/api/client/py.typed +1 -0
- loopix/api/client/types.py +54 -0
- loopix/api/client_async/__init__.py +74 -0
- loopix/api/client_sync/__init__.py +73 -0
- loopix/api/metadata.py +14 -0
- loopix/connection_config.py +309 -0
- loopix/envd/api.py +170 -0
- loopix/envd/filesystem/filesystem_connect.py +193 -0
- loopix/envd/filesystem/filesystem_pb2.py +80 -0
- loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
- loopix/envd/process/process_connect.py +174 -0
- loopix/envd/process/process_pb2.py +96 -0
- loopix/envd/process/process_pb2.pyi +316 -0
- loopix/envd/rpc.py +139 -0
- loopix/envd/versions.py +11 -0
- loopix/exceptions.py +133 -0
- loopix/io_utils.py +57 -0
- loopix/paginator.py +52 -0
- loopix/py.typed +0 -0
- loopix/sandbox/_git/__init__.py +85 -0
- loopix/sandbox/_git/args.py +363 -0
- loopix/sandbox/_git/auth.py +132 -0
- loopix/sandbox/_git/config.py +32 -0
- loopix/sandbox/_git/parse.py +222 -0
- loopix/sandbox/_git/types.py +149 -0
- loopix/sandbox/commands/command_handle.py +69 -0
- loopix/sandbox/commands/main.py +39 -0
- loopix/sandbox/filesystem/filesystem.py +337 -0
- loopix/sandbox/filesystem/watch_handle.py +70 -0
- loopix/sandbox/main.py +227 -0
- loopix/sandbox/mcp.py +1949 -0
- loopix/sandbox/network.py +8 -0
- loopix/sandbox/sandbox_api.py +624 -0
- loopix/sandbox/signature.py +47 -0
- loopix/sandbox/utils.py +34 -0
- loopix/sandbox_async/commands/command.py +396 -0
- loopix/sandbox_async/commands/command_handle.py +298 -0
- loopix/sandbox_async/commands/pty.py +257 -0
- loopix/sandbox_async/filesystem/filesystem.py +720 -0
- loopix/sandbox_async/filesystem/watch_handle.py +97 -0
- loopix/sandbox_async/git.py +1100 -0
- loopix/sandbox_async/main.py +987 -0
- loopix/sandbox_async/paginator.py +140 -0
- loopix/sandbox_async/sandbox_api.py +504 -0
- loopix/sandbox_async/utils.py +7 -0
- loopix/sandbox_domains.py +5 -0
- loopix/sandbox_sync/commands/command.py +420 -0
- loopix/sandbox_sync/commands/command_handle.py +239 -0
- loopix/sandbox_sync/commands/pty.py +279 -0
- loopix/sandbox_sync/filesystem/filesystem.py +710 -0
- loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
- loopix/sandbox_sync/git.py +1077 -0
- loopix/sandbox_sync/main.py +975 -0
- loopix/sandbox_sync/paginator.py +140 -0
- loopix/sandbox_sync/sandbox_api.py +491 -0
- loopix/template/consts.py +45 -0
- loopix/template/dockerfile_parser.py +286 -0
- loopix/template/logger.py +232 -0
- loopix/template/main.py +1368 -0
- loopix/template/readycmd.py +144 -0
- loopix/template/types.py +194 -0
- loopix/template/utils.py +426 -0
- loopix/template_async/build_api.py +419 -0
- loopix/template_async/main.py +528 -0
- loopix/template_sync/build_api.py +409 -0
- loopix/template_sync/main.py +529 -0
- loopix/volume/client/__init__.py +8 -0
- loopix/volume/client/api/__init__.py +1 -0
- loopix/volume/client/api/volumes/__init__.py +1 -0
- loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
- loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
- loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
- loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
- loopix/volume/client/client.py +286 -0
- loopix/volume/client/errors.py +16 -0
- loopix/volume/client/models/__init__.py +13 -0
- loopix/volume/client/models/error.py +67 -0
- loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
- loopix/volume/client/models/volume_entry_stat.py +145 -0
- loopix/volume/client/models/volume_entry_stat_type.py +11 -0
- loopix/volume/client/py.typed +1 -0
- loopix/volume/client/types.py +54 -0
- loopix/volume/client_async/__init__.py +88 -0
- loopix/volume/client_sync/__init__.py +80 -0
- loopix/volume/connection_config.py +145 -0
- loopix/volume/types.py +62 -0
- loopix/volume/utils.py +52 -0
- loopix/volume/volume_async.py +639 -0
- loopix/volume/volume_sync.py +639 -0
- loopix_connect/__init__.py +1 -0
- loopix_connect/client.py +534 -0
- loopix_connect/py.typed +0 -0
- loopix_sdk-2.30.0.dist-info/METADATA +98 -0
- loopix_sdk-2.30.0.dist-info/RECORD +238 -0
- loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
- loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Unpack
|
|
5
|
+
|
|
6
|
+
from loopix.api.client.api.sandboxes import get_v2_sandboxes
|
|
7
|
+
from loopix.api.client.api.snapshots import get_snapshots
|
|
8
|
+
from loopix.api.client.types import UNSET
|
|
9
|
+
from loopix.connection_config import ApiParams, ConnectionConfig
|
|
10
|
+
from loopix.exceptions import SandboxException
|
|
11
|
+
from loopix.sandbox.sandbox_api import (
|
|
12
|
+
SandboxPaginatorBase,
|
|
13
|
+
SandboxInfo,
|
|
14
|
+
SnapshotPaginatorBase,
|
|
15
|
+
SnapshotInfo,
|
|
16
|
+
)
|
|
17
|
+
from loopix.api import handle_api_exception
|
|
18
|
+
from loopix.api.client.models.error import Error
|
|
19
|
+
from loopix.api.client_async import get_api_client
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncSandboxPaginator(SandboxPaginatorBase):
|
|
23
|
+
"""
|
|
24
|
+
Paginator for listing sandboxes.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
paginator = AsyncSandbox.list()
|
|
29
|
+
|
|
30
|
+
while paginator.has_next:
|
|
31
|
+
sandboxes = await paginator.next_items()
|
|
32
|
+
print(sandboxes)
|
|
33
|
+
```
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
async def next_items(self, **opts: Unpack[ApiParams]) -> List[SandboxInfo]:
|
|
37
|
+
"""
|
|
38
|
+
Returns the next page of sandboxes.
|
|
39
|
+
|
|
40
|
+
Call this method only if `has_next` is `True`, otherwise it will raise an exception.
|
|
41
|
+
|
|
42
|
+
:param opts: Per-call connection options (e.g. `api_key`, `domain`,
|
|
43
|
+
`headers`, `request_timeout`). When provided, this call uses these
|
|
44
|
+
options instead of the ones the paginator was constructed with.
|
|
45
|
+
|
|
46
|
+
:returns: List of sandboxes
|
|
47
|
+
"""
|
|
48
|
+
if not self.has_next:
|
|
49
|
+
raise Exception("No more items to fetch")
|
|
50
|
+
|
|
51
|
+
# Convert filters to the format expected by the API
|
|
52
|
+
metadata: Optional[str] = None
|
|
53
|
+
if self.query and self.query.metadata:
|
|
54
|
+
quoted_metadata = {
|
|
55
|
+
urllib.parse.quote(k): urllib.parse.quote(v)
|
|
56
|
+
for k, v in self.query.metadata.items()
|
|
57
|
+
}
|
|
58
|
+
metadata = urllib.parse.urlencode(quoted_metadata)
|
|
59
|
+
|
|
60
|
+
config = ConnectionConfig(**{**self._opts, **opts})
|
|
61
|
+
api_client = get_api_client(config)
|
|
62
|
+
res = await get_v2_sandboxes.asyncio_detailed(
|
|
63
|
+
client=api_client,
|
|
64
|
+
metadata=metadata if metadata else UNSET,
|
|
65
|
+
state=self.query.state if self.query and self.query.state else UNSET,
|
|
66
|
+
limit=self.limit if self.limit else UNSET,
|
|
67
|
+
next_token=self._next_token if self._next_token else UNSET,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if res.status_code >= 300:
|
|
71
|
+
raise handle_api_exception(res)
|
|
72
|
+
|
|
73
|
+
self._update_pagination(res.headers)
|
|
74
|
+
|
|
75
|
+
if res.parsed is None:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
# Check if res.parsed is Error
|
|
79
|
+
if isinstance(res.parsed, Error):
|
|
80
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
81
|
+
|
|
82
|
+
return [SandboxInfo._from_listed_sandbox(sandbox) for sandbox in res.parsed]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AsyncSnapshotPaginator(SnapshotPaginatorBase):
|
|
86
|
+
"""
|
|
87
|
+
Paginator for listing snapshots.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
```python
|
|
91
|
+
paginator = AsyncSandbox.list_snapshots()
|
|
92
|
+
|
|
93
|
+
while paginator.has_next:
|
|
94
|
+
snapshots = await paginator.next_items()
|
|
95
|
+
print(snapshots)
|
|
96
|
+
```
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
async def next_items(self, **opts: Unpack[ApiParams]) -> List[SnapshotInfo]:
|
|
100
|
+
"""
|
|
101
|
+
Returns the next page of snapshots.
|
|
102
|
+
|
|
103
|
+
Call this method only if `has_next` is `True`, otherwise it will raise an exception.
|
|
104
|
+
|
|
105
|
+
:param opts: Per-call connection options (e.g. `api_key`, `domain`,
|
|
106
|
+
`headers`, `request_timeout`). When provided, this call uses these
|
|
107
|
+
options instead of the ones the paginator was constructed with.
|
|
108
|
+
|
|
109
|
+
:returns: List of snapshots
|
|
110
|
+
"""
|
|
111
|
+
if not self.has_next:
|
|
112
|
+
raise Exception("No more items to fetch")
|
|
113
|
+
|
|
114
|
+
config = ConnectionConfig(**{**self._opts, **opts})
|
|
115
|
+
api_client = get_api_client(config)
|
|
116
|
+
res = await get_snapshots.asyncio_detailed(
|
|
117
|
+
client=api_client,
|
|
118
|
+
sandbox_id=self.sandbox_id if self.sandbox_id else UNSET,
|
|
119
|
+
limit=self.limit if self.limit else UNSET,
|
|
120
|
+
next_token=self._next_token if self._next_token else UNSET,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if res.status_code >= 300:
|
|
124
|
+
raise handle_api_exception(res)
|
|
125
|
+
|
|
126
|
+
self._update_pagination(res.headers)
|
|
127
|
+
|
|
128
|
+
if res.parsed is None:
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
if isinstance(res.parsed, Error):
|
|
132
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
SnapshotInfo(
|
|
136
|
+
snapshot_id=snapshot.snapshot_id,
|
|
137
|
+
names=list(snapshot.names) if snapshot.names else [],
|
|
138
|
+
)
|
|
139
|
+
for snapshot in res.parsed
|
|
140
|
+
]
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional, cast
|
|
4
|
+
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
from typing_extensions import Unpack
|
|
7
|
+
|
|
8
|
+
from loopix.api import SandboxCreateResponse, handle_api_exception
|
|
9
|
+
from loopix.api.client.api.sandboxes import (
|
|
10
|
+
delete_sandboxes_sandbox_id,
|
|
11
|
+
get_sandboxes_sandbox_id,
|
|
12
|
+
get_sandboxes_sandbox_id_metrics,
|
|
13
|
+
post_sandboxes,
|
|
14
|
+
post_sandboxes_sandbox_id_connect,
|
|
15
|
+
post_sandboxes_sandbox_id_pause,
|
|
16
|
+
post_sandboxes_sandbox_id_snapshots,
|
|
17
|
+
post_sandboxes_sandbox_id_timeout,
|
|
18
|
+
put_sandboxes_sandbox_id_network,
|
|
19
|
+
)
|
|
20
|
+
from loopix.api.client.api.templates import delete_templates_template_id
|
|
21
|
+
from loopix.api.client.models import (
|
|
22
|
+
ConnectSandbox,
|
|
23
|
+
Error,
|
|
24
|
+
NewSandbox,
|
|
25
|
+
PostSandboxesSandboxIDSnapshotsBody,
|
|
26
|
+
PostSandboxesSandboxIDTimeoutBody,
|
|
27
|
+
SandboxAutoResumeConfig,
|
|
28
|
+
SandboxNetworkConfig,
|
|
29
|
+
SandboxPauseRequest,
|
|
30
|
+
SandboxVolumeMount as SandboxVolumeMountAPI,
|
|
31
|
+
)
|
|
32
|
+
from loopix.api.client.types import UNSET
|
|
33
|
+
from loopix.api.client_async import get_api_client
|
|
34
|
+
from loopix.connection_config import ApiParams, ConnectionConfig
|
|
35
|
+
from loopix.exceptions import (
|
|
36
|
+
InvalidArgumentException,
|
|
37
|
+
SandboxException,
|
|
38
|
+
SandboxNotFoundException,
|
|
39
|
+
TemplateException,
|
|
40
|
+
)
|
|
41
|
+
from loopix.sandbox.main import SandboxBase
|
|
42
|
+
from loopix.sandbox.sandbox_api import (
|
|
43
|
+
build_network_update_body,
|
|
44
|
+
McpServer,
|
|
45
|
+
SandboxInfo,
|
|
46
|
+
SandboxLifecycle,
|
|
47
|
+
SandboxMetrics,
|
|
48
|
+
SandboxNetworkOpts,
|
|
49
|
+
SandboxNetworkUpdate,
|
|
50
|
+
SandboxQuery,
|
|
51
|
+
SnapshotInfo,
|
|
52
|
+
build_network_config,
|
|
53
|
+
)
|
|
54
|
+
from loopix.sandbox_async.paginator import AsyncSandboxPaginator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SandboxApi(SandboxBase):
|
|
58
|
+
@staticmethod
|
|
59
|
+
def list(
|
|
60
|
+
query: Optional[SandboxQuery] = None,
|
|
61
|
+
limit: Optional[int] = None,
|
|
62
|
+
next_token: Optional[str] = None,
|
|
63
|
+
**opts: Unpack[ApiParams],
|
|
64
|
+
) -> AsyncSandboxPaginator:
|
|
65
|
+
"""
|
|
66
|
+
List sandboxes.
|
|
67
|
+
|
|
68
|
+
By default (no `query.state` set), returns sandboxes in both `running`
|
|
69
|
+
and `paused` states. To filter by state, pass `query=SandboxQuery(state=[...])`.
|
|
70
|
+
|
|
71
|
+
:param query: Filter the list of sandboxes by metadata or state, e.g. `SandboxQuery(metadata={"key": "value"})` or `SandboxQuery(state=[SandboxState.RUNNING])`
|
|
72
|
+
:param limit: Maximum number of sandboxes to return per page
|
|
73
|
+
:param next_token: Token for pagination
|
|
74
|
+
|
|
75
|
+
:return: An `AsyncSandboxPaginator` that yields pages of sandboxes (running and paused by default). Iterate pages via `await paginator.next_items()` while `paginator.has_next` is True.
|
|
76
|
+
"""
|
|
77
|
+
return AsyncSandboxPaginator(
|
|
78
|
+
query=query,
|
|
79
|
+
limit=limit,
|
|
80
|
+
next_token=next_token,
|
|
81
|
+
**opts,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
async def _cls_get_info(
|
|
86
|
+
cls,
|
|
87
|
+
sandbox_id: str,
|
|
88
|
+
**opts: Unpack[ApiParams],
|
|
89
|
+
) -> SandboxInfo:
|
|
90
|
+
"""
|
|
91
|
+
Get the sandbox info.
|
|
92
|
+
:param sandbox_id: Sandbox ID
|
|
93
|
+
|
|
94
|
+
:return: Sandbox info
|
|
95
|
+
"""
|
|
96
|
+
config = ConnectionConfig(**opts)
|
|
97
|
+
|
|
98
|
+
api_client = get_api_client(config)
|
|
99
|
+
res = await get_sandboxes_sandbox_id.asyncio_detailed(
|
|
100
|
+
sandbox_id,
|
|
101
|
+
client=api_client,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if res.status_code == 404:
|
|
105
|
+
raise SandboxNotFoundException(f"Sandbox {sandbox_id} not found")
|
|
106
|
+
|
|
107
|
+
if res.status_code >= 300:
|
|
108
|
+
raise handle_api_exception(res)
|
|
109
|
+
|
|
110
|
+
if res.parsed is None:
|
|
111
|
+
raise Exception("Body of the request is None")
|
|
112
|
+
|
|
113
|
+
if isinstance(res.parsed, Error):
|
|
114
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
115
|
+
|
|
116
|
+
return SandboxInfo._from_sandbox_detail(res.parsed)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
async def _cls_kill(
|
|
120
|
+
cls,
|
|
121
|
+
sandbox_id: str,
|
|
122
|
+
**opts: Unpack[ApiParams],
|
|
123
|
+
) -> bool:
|
|
124
|
+
config = ConnectionConfig(**opts)
|
|
125
|
+
|
|
126
|
+
if config.debug:
|
|
127
|
+
# Skip killing the sandbox in debug mode
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
api_client = get_api_client(config)
|
|
131
|
+
res = await delete_sandboxes_sandbox_id.asyncio_detailed(
|
|
132
|
+
sandbox_id,
|
|
133
|
+
client=api_client,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if res.status_code == 404:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
if res.status_code >= 300:
|
|
140
|
+
raise handle_api_exception(res)
|
|
141
|
+
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
async def _cls_set_timeout(
|
|
146
|
+
cls,
|
|
147
|
+
sandbox_id: str,
|
|
148
|
+
timeout: int,
|
|
149
|
+
**opts: Unpack[ApiParams],
|
|
150
|
+
) -> None:
|
|
151
|
+
config = ConnectionConfig(**opts)
|
|
152
|
+
|
|
153
|
+
if config.debug:
|
|
154
|
+
# Skip setting the timeout in debug mode
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
api_client = get_api_client(config)
|
|
158
|
+
res = await post_sandboxes_sandbox_id_timeout.asyncio_detailed(
|
|
159
|
+
sandbox_id,
|
|
160
|
+
client=api_client,
|
|
161
|
+
body=PostSandboxesSandboxIDTimeoutBody(timeout=timeout),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if res.status_code == 404:
|
|
165
|
+
raise SandboxNotFoundException(f"Sandbox {sandbox_id} not found")
|
|
166
|
+
|
|
167
|
+
if res.status_code >= 300:
|
|
168
|
+
raise handle_api_exception(res)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
async def _cls_update_network(
|
|
172
|
+
cls,
|
|
173
|
+
sandbox_id: str,
|
|
174
|
+
network: SandboxNetworkUpdate,
|
|
175
|
+
**opts: Unpack[ApiParams],
|
|
176
|
+
) -> None:
|
|
177
|
+
config = ConnectionConfig(**opts)
|
|
178
|
+
|
|
179
|
+
api_client = get_api_client(config)
|
|
180
|
+
res = await put_sandboxes_sandbox_id_network.asyncio_detailed(
|
|
181
|
+
sandbox_id,
|
|
182
|
+
client=api_client,
|
|
183
|
+
body=build_network_update_body(network),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if res.status_code == 404:
|
|
187
|
+
raise SandboxNotFoundException(f"Sandbox {sandbox_id} not found")
|
|
188
|
+
|
|
189
|
+
if res.status_code >= 300:
|
|
190
|
+
raise handle_api_exception(res)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
async def _create_sandbox(
|
|
194
|
+
cls,
|
|
195
|
+
template: str,
|
|
196
|
+
timeout: int,
|
|
197
|
+
allow_internet_access: bool,
|
|
198
|
+
metadata: Optional[Dict[str, str]],
|
|
199
|
+
env_vars: Optional[Dict[str, str]],
|
|
200
|
+
secure: bool,
|
|
201
|
+
mcp: Optional[McpServer] = None,
|
|
202
|
+
network: Optional[SandboxNetworkOpts] = None,
|
|
203
|
+
lifecycle: Optional[SandboxLifecycle] = None,
|
|
204
|
+
volume_mounts: Optional[List[SandboxVolumeMountAPI]] = None,
|
|
205
|
+
logger: Optional[logging.Logger] = None,
|
|
206
|
+
**opts: Unpack[ApiParams],
|
|
207
|
+
) -> SandboxCreateResponse:
|
|
208
|
+
config = ConnectionConfig(logger=logger, **opts)
|
|
209
|
+
|
|
210
|
+
# on_timeout accepts a bare action or {"action", "keep_memory"}; normalize.
|
|
211
|
+
# Only the object form carries keep_memory; anything else (a bare action
|
|
212
|
+
# string, or an unexpected value from an untyped caller) passes through as
|
|
213
|
+
# the action, so a non-"pause" value resolves to kill instead of crashing.
|
|
214
|
+
on_timeout_raw = lifecycle.get("on_timeout", "kill") if lifecycle else "kill"
|
|
215
|
+
if isinstance(on_timeout_raw, dict):
|
|
216
|
+
on_timeout = on_timeout_raw.get("action", "kill")
|
|
217
|
+
keep_memory_provided = "keep_memory" in on_timeout_raw
|
|
218
|
+
keep_memory = on_timeout_raw.get("keep_memory")
|
|
219
|
+
else:
|
|
220
|
+
on_timeout = on_timeout_raw
|
|
221
|
+
keep_memory = None
|
|
222
|
+
keep_memory_provided = False
|
|
223
|
+
|
|
224
|
+
# keep_memory only governs a pause action. The discriminated union type
|
|
225
|
+
# forbids it on action="kill"; re-check at runtime for callers that
|
|
226
|
+
# bypass the type.
|
|
227
|
+
if keep_memory_provided and on_timeout != "pause":
|
|
228
|
+
raise InvalidArgumentException(
|
|
229
|
+
"keep_memory is only allowed when on_timeout action is 'pause'."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# A missing or explicit None keep_memory defaults to True (full memory),
|
|
233
|
+
# mirroring the JS SDK; sending null would wrongly read as filesystem-only.
|
|
234
|
+
if keep_memory is None:
|
|
235
|
+
keep_memory = True
|
|
236
|
+
auto_resume = lifecycle.get("auto_resume", False) if lifecycle else False
|
|
237
|
+
|
|
238
|
+
if auto_resume and on_timeout != "pause":
|
|
239
|
+
raise InvalidArgumentException(
|
|
240
|
+
"auto_resume can only be True when on_timeout action is 'pause'."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not keep_memory and auto_resume:
|
|
244
|
+
raise InvalidArgumentException(
|
|
245
|
+
"auto_resume: True is not a valid value when keep_memory: False - "
|
|
246
|
+
"a filesystem-only snapshot cannot be auto-resumed by traffic and "
|
|
247
|
+
"must be resumed explicitly using Sandbox.connect()."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
network_body = build_network_config(network)
|
|
251
|
+
body = NewSandbox(
|
|
252
|
+
template_id=template,
|
|
253
|
+
auto_pause=on_timeout == "pause",
|
|
254
|
+
auto_pause_memory=keep_memory if on_timeout == "pause" else UNSET,
|
|
255
|
+
auto_resume=SandboxAutoResumeConfig(enabled=auto_resume),
|
|
256
|
+
metadata=metadata or {},
|
|
257
|
+
timeout=timeout,
|
|
258
|
+
env_vars=env_vars or {},
|
|
259
|
+
mcp=cast(Any, mcp) or UNSET,
|
|
260
|
+
secure=secure,
|
|
261
|
+
allow_internet_access=allow_internet_access,
|
|
262
|
+
network=SandboxNetworkConfig(**network_body) if network_body else UNSET,
|
|
263
|
+
volume_mounts=volume_mounts if volume_mounts else UNSET,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
api_client = get_api_client(config)
|
|
267
|
+
res = await post_sandboxes.asyncio_detailed(
|
|
268
|
+
body=body,
|
|
269
|
+
client=api_client,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if res.status_code >= 300:
|
|
273
|
+
raise handle_api_exception(res)
|
|
274
|
+
|
|
275
|
+
if res.parsed is None:
|
|
276
|
+
raise Exception("Body of the request is None")
|
|
277
|
+
|
|
278
|
+
if isinstance(res.parsed, Error):
|
|
279
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
280
|
+
|
|
281
|
+
if Version(res.parsed.envd_version) < Version("0.1.0"):
|
|
282
|
+
await SandboxApi._cls_kill(res.parsed.sandbox_id)
|
|
283
|
+
raise TemplateException(
|
|
284
|
+
"You need to update the template to use the new SDK."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
domain = res.parsed.domain if isinstance(res.parsed.domain, str) else None
|
|
288
|
+
envd_token = (
|
|
289
|
+
res.parsed.envd_access_token
|
|
290
|
+
if isinstance(res.parsed.envd_access_token, str)
|
|
291
|
+
else None
|
|
292
|
+
)
|
|
293
|
+
traffic_token = (
|
|
294
|
+
res.parsed.traffic_access_token
|
|
295
|
+
if isinstance(res.parsed.traffic_access_token, str)
|
|
296
|
+
else None
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return SandboxCreateResponse(
|
|
300
|
+
sandbox_id=res.parsed.sandbox_id,
|
|
301
|
+
sandbox_domain=domain,
|
|
302
|
+
envd_version=res.parsed.envd_version,
|
|
303
|
+
envd_access_token=envd_token,
|
|
304
|
+
traffic_access_token=traffic_token,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
async def _cls_get_metrics(
|
|
309
|
+
cls,
|
|
310
|
+
sandbox_id: str,
|
|
311
|
+
start: Optional[datetime.datetime] = None,
|
|
312
|
+
end: Optional[datetime.datetime] = None,
|
|
313
|
+
**opts: Unpack[ApiParams],
|
|
314
|
+
) -> List[SandboxMetrics]:
|
|
315
|
+
"""
|
|
316
|
+
Get the metrics of the sandbox specified by sandbox ID.
|
|
317
|
+
|
|
318
|
+
:param sandbox_id: Sandbox ID
|
|
319
|
+
:param start: Start time for the metrics, defaults to the start of the sandbox
|
|
320
|
+
:param end: End time for the metrics, defaults to the current time
|
|
321
|
+
|
|
322
|
+
:return: List of sandbox metrics containing CPU, memory and disk usage information
|
|
323
|
+
"""
|
|
324
|
+
config = ConnectionConfig(**opts)
|
|
325
|
+
|
|
326
|
+
if config.debug:
|
|
327
|
+
# Skip getting the metrics in debug mode
|
|
328
|
+
return []
|
|
329
|
+
|
|
330
|
+
api_client = get_api_client(config)
|
|
331
|
+
res = await get_sandboxes_sandbox_id_metrics.asyncio_detailed(
|
|
332
|
+
sandbox_id,
|
|
333
|
+
start=int(start.timestamp()) if start else UNSET,
|
|
334
|
+
end=int(end.timestamp()) if end else UNSET,
|
|
335
|
+
client=api_client,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if res.status_code == 404:
|
|
339
|
+
raise SandboxNotFoundException(f"Sandbox {sandbox_id} not found")
|
|
340
|
+
|
|
341
|
+
if res.status_code >= 300:
|
|
342
|
+
raise handle_api_exception(res)
|
|
343
|
+
|
|
344
|
+
if res.parsed is None:
|
|
345
|
+
return []
|
|
346
|
+
|
|
347
|
+
# Check if res.parse is Error
|
|
348
|
+
if isinstance(res.parsed, Error):
|
|
349
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
350
|
+
|
|
351
|
+
# Convert to typed SandboxMetrics objects
|
|
352
|
+
return [
|
|
353
|
+
SandboxMetrics(
|
|
354
|
+
cpu_count=metric.cpu_count,
|
|
355
|
+
cpu_used_pct=metric.cpu_used_pct,
|
|
356
|
+
disk_total=metric.disk_total,
|
|
357
|
+
disk_used=metric.disk_used,
|
|
358
|
+
mem_total=metric.mem_total,
|
|
359
|
+
mem_used=metric.mem_used,
|
|
360
|
+
mem_cache=metric.mem_cache,
|
|
361
|
+
timestamp=metric.timestamp,
|
|
362
|
+
)
|
|
363
|
+
for metric in res.parsed
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
async def _cls_create_snapshot(
|
|
368
|
+
cls,
|
|
369
|
+
sandbox_id: str,
|
|
370
|
+
name: Optional[str] = None,
|
|
371
|
+
**opts: Unpack[ApiParams],
|
|
372
|
+
) -> SnapshotInfo:
|
|
373
|
+
config = ConnectionConfig(**opts)
|
|
374
|
+
|
|
375
|
+
api_client = get_api_client(config)
|
|
376
|
+
res = await post_sandboxes_sandbox_id_snapshots.asyncio_detailed(
|
|
377
|
+
sandbox_id,
|
|
378
|
+
client=api_client,
|
|
379
|
+
body=PostSandboxesSandboxIDSnapshotsBody(name=name if name else UNSET),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if res.status_code == 404:
|
|
383
|
+
raise SandboxNotFoundException(f"Sandbox {sandbox_id} not found")
|
|
384
|
+
|
|
385
|
+
if res.status_code >= 300:
|
|
386
|
+
raise handle_api_exception(res)
|
|
387
|
+
|
|
388
|
+
if res.parsed is None:
|
|
389
|
+
raise Exception("Body of the request is None")
|
|
390
|
+
|
|
391
|
+
if isinstance(res.parsed, Error):
|
|
392
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
393
|
+
|
|
394
|
+
return SnapshotInfo(
|
|
395
|
+
snapshot_id=res.parsed.snapshot_id,
|
|
396
|
+
names=list(res.parsed.names) if res.parsed.names else [],
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
async def _cls_delete_snapshot(
|
|
401
|
+
cls,
|
|
402
|
+
snapshot_id: str,
|
|
403
|
+
**opts: Unpack[ApiParams],
|
|
404
|
+
) -> bool:
|
|
405
|
+
config = ConnectionConfig(**opts)
|
|
406
|
+
|
|
407
|
+
api_client = get_api_client(config)
|
|
408
|
+
res = await delete_templates_template_id.asyncio_detailed(
|
|
409
|
+
snapshot_id,
|
|
410
|
+
client=api_client,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
if res.status_code == 404:
|
|
414
|
+
return False
|
|
415
|
+
|
|
416
|
+
if res.status_code >= 300:
|
|
417
|
+
raise handle_api_exception(res)
|
|
418
|
+
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
@classmethod
|
|
422
|
+
async def _cls_pause(
|
|
423
|
+
cls,
|
|
424
|
+
sandbox_id: str,
|
|
425
|
+
keep_memory: bool = True,
|
|
426
|
+
**opts: Unpack[ApiParams],
|
|
427
|
+
) -> bool:
|
|
428
|
+
config = ConnectionConfig(**opts)
|
|
429
|
+
|
|
430
|
+
api_client = get_api_client(config)
|
|
431
|
+
res = await post_sandboxes_sandbox_id_pause.asyncio_detailed(
|
|
432
|
+
sandbox_id,
|
|
433
|
+
client=api_client,
|
|
434
|
+
body=SandboxPauseRequest(memory=keep_memory),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if res.status_code == 404:
|
|
438
|
+
raise SandboxNotFoundException(f"Sandbox {sandbox_id} not found")
|
|
439
|
+
|
|
440
|
+
if res.status_code == 409:
|
|
441
|
+
# Sandbox is already paused
|
|
442
|
+
return False
|
|
443
|
+
|
|
444
|
+
if res.status_code >= 300:
|
|
445
|
+
raise handle_api_exception(res)
|
|
446
|
+
|
|
447
|
+
# Check if res.parse is Error
|
|
448
|
+
if isinstance(res.parsed, Error):
|
|
449
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
450
|
+
|
|
451
|
+
return True
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
async def _cls_connect(
|
|
455
|
+
cls,
|
|
456
|
+
sandbox_id: str,
|
|
457
|
+
timeout: Optional[int] = None,
|
|
458
|
+
logger: Optional[logging.Logger] = None,
|
|
459
|
+
**opts: Unpack[ApiParams],
|
|
460
|
+
) -> SandboxCreateResponse:
|
|
461
|
+
timeout = timeout or SandboxBase.default_sandbox_timeout
|
|
462
|
+
|
|
463
|
+
# Sandbox is not running, resume it
|
|
464
|
+
config = ConnectionConfig(logger=logger, **opts)
|
|
465
|
+
|
|
466
|
+
api_client = get_api_client(config)
|
|
467
|
+
res = await post_sandboxes_sandbox_id_connect.asyncio_detailed(
|
|
468
|
+
sandbox_id,
|
|
469
|
+
client=api_client,
|
|
470
|
+
body=ConnectSandbox(timeout=timeout),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if res.status_code == 404:
|
|
474
|
+
raise SandboxNotFoundException(f"Paused sandbox {sandbox_id} not found")
|
|
475
|
+
|
|
476
|
+
if res.status_code >= 300:
|
|
477
|
+
raise handle_api_exception(res)
|
|
478
|
+
|
|
479
|
+
# Check if res.parse is Error
|
|
480
|
+
if isinstance(res.parsed, Error):
|
|
481
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
482
|
+
|
|
483
|
+
if res.parsed is None:
|
|
484
|
+
raise Exception("Body of the request is None")
|
|
485
|
+
|
|
486
|
+
domain = res.parsed.domain if isinstance(res.parsed.domain, str) else None
|
|
487
|
+
envd_token = (
|
|
488
|
+
res.parsed.envd_access_token
|
|
489
|
+
if isinstance(res.parsed.envd_access_token, str)
|
|
490
|
+
else None
|
|
491
|
+
)
|
|
492
|
+
traffic_token = (
|
|
493
|
+
res.parsed.traffic_access_token
|
|
494
|
+
if isinstance(res.parsed.traffic_access_token, str)
|
|
495
|
+
else None
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return SandboxCreateResponse(
|
|
499
|
+
sandbox_id=res.parsed.sandbox_id,
|
|
500
|
+
sandbox_domain=domain,
|
|
501
|
+
envd_version=res.parsed.envd_version,
|
|
502
|
+
envd_access_token=envd_token,
|
|
503
|
+
traffic_access_token=traffic_token,
|
|
504
|
+
)
|