moru 0.1.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.
- moru/__init__.py +174 -0
- moru/api/__init__.py +164 -0
- moru/api/client/__init__.py +8 -0
- moru/api/client/api/__init__.py +1 -0
- moru/api/client/api/sandboxes/__init__.py +1 -0
- moru/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- moru/api/client/api/sandboxes/get_sandboxes.py +176 -0
- moru/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- moru/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- moru/api/client/api/sandboxes/post_sandboxes.py +172 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- moru/api/client/api/templates/__init__.py +1 -0
- moru/api/client/api/templates/delete_templates_template_id.py +157 -0
- moru/api/client/api/templates/get_templates.py +172 -0
- moru/api/client/api/templates/get_templates_template_id.py +195 -0
- moru/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +217 -0
- moru/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- moru/api/client/api/templates/patch_templates_template_id.py +183 -0
- moru/api/client/api/templates/post_templates.py +172 -0
- moru/api/client/api/templates/post_templates_template_id.py +181 -0
- moru/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- moru/api/client/api/templates/post_v2_templates.py +172 -0
- moru/api/client/api/templates/post_v3_templates.py +172 -0
- moru/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- moru/api/client/client.py +286 -0
- moru/api/client/errors.py +16 -0
- moru/api/client/models/__init__.py +123 -0
- moru/api/client/models/aws_registry.py +85 -0
- moru/api/client/models/aws_registry_type.py +8 -0
- moru/api/client/models/build_log_entry.py +89 -0
- moru/api/client/models/build_status_reason.py +95 -0
- moru/api/client/models/connect_sandbox.py +59 -0
- moru/api/client/models/created_access_token.py +100 -0
- moru/api/client/models/created_team_api_key.py +166 -0
- moru/api/client/models/disk_metrics.py +91 -0
- moru/api/client/models/error.py +67 -0
- moru/api/client/models/gcp_registry.py +69 -0
- moru/api/client/models/gcp_registry_type.py +8 -0
- moru/api/client/models/general_registry.py +77 -0
- moru/api/client/models/general_registry_type.py +8 -0
- moru/api/client/models/identifier_masking_details.py +83 -0
- moru/api/client/models/listed_sandbox.py +154 -0
- moru/api/client/models/log_level.py +11 -0
- moru/api/client/models/max_team_metric.py +78 -0
- moru/api/client/models/mcp_type_0.py +44 -0
- moru/api/client/models/new_access_token.py +59 -0
- moru/api/client/models/new_sandbox.py +172 -0
- moru/api/client/models/new_team_api_key.py +59 -0
- moru/api/client/models/node.py +155 -0
- moru/api/client/models/node_detail.py +165 -0
- moru/api/client/models/node_metrics.py +122 -0
- moru/api/client/models/node_status.py +11 -0
- moru/api/client/models/node_status_change.py +79 -0
- moru/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- moru/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- moru/api/client/models/resumed_sandbox.py +68 -0
- moru/api/client/models/sandbox.py +145 -0
- moru/api/client/models/sandbox_detail.py +183 -0
- moru/api/client/models/sandbox_log.py +70 -0
- moru/api/client/models/sandbox_log_entry.py +93 -0
- moru/api/client/models/sandbox_log_entry_fields.py +44 -0
- moru/api/client/models/sandbox_logs.py +91 -0
- moru/api/client/models/sandbox_metric.py +118 -0
- moru/api/client/models/sandbox_network_config.py +92 -0
- moru/api/client/models/sandbox_state.py +9 -0
- moru/api/client/models/sandboxes_with_metrics.py +59 -0
- moru/api/client/models/team.py +83 -0
- moru/api/client/models/team_api_key.py +158 -0
- moru/api/client/models/team_metric.py +86 -0
- moru/api/client/models/team_user.py +68 -0
- moru/api/client/models/template.py +217 -0
- moru/api/client/models/template_build.py +139 -0
- moru/api/client/models/template_build_file_upload.py +70 -0
- moru/api/client/models/template_build_info.py +126 -0
- moru/api/client/models/template_build_request.py +115 -0
- moru/api/client/models/template_build_request_v2.py +88 -0
- moru/api/client/models/template_build_request_v3.py +88 -0
- moru/api/client/models/template_build_start_v2.py +184 -0
- moru/api/client/models/template_build_status.py +11 -0
- moru/api/client/models/template_legacy.py +207 -0
- moru/api/client/models/template_request_response_v3.py +83 -0
- moru/api/client/models/template_step.py +91 -0
- moru/api/client/models/template_update_request.py +59 -0
- moru/api/client/models/template_with_builds.py +148 -0
- moru/api/client/models/update_team_api_key.py +59 -0
- moru/api/client/py.typed +1 -0
- moru/api/client/types.py +54 -0
- moru/api/client_async/__init__.py +50 -0
- moru/api/client_sync/__init__.py +52 -0
- moru/api/metadata.py +14 -0
- moru/connection_config.py +217 -0
- moru/envd/api.py +59 -0
- moru/envd/filesystem/filesystem_connect.py +193 -0
- moru/envd/filesystem/filesystem_pb2.py +76 -0
- moru/envd/filesystem/filesystem_pb2.pyi +233 -0
- moru/envd/process/process_connect.py +155 -0
- moru/envd/process/process_pb2.py +92 -0
- moru/envd/process/process_pb2.pyi +304 -0
- moru/envd/rpc.py +61 -0
- moru/envd/versions.py +6 -0
- moru/exceptions.py +95 -0
- moru/sandbox/commands/command_handle.py +69 -0
- moru/sandbox/commands/main.py +39 -0
- moru/sandbox/filesystem/filesystem.py +94 -0
- moru/sandbox/filesystem/watch_handle.py +60 -0
- moru/sandbox/main.py +210 -0
- moru/sandbox/mcp.py +1120 -0
- moru/sandbox/network.py +8 -0
- moru/sandbox/sandbox_api.py +210 -0
- moru/sandbox/signature.py +45 -0
- moru/sandbox/utils.py +34 -0
- moru/sandbox_async/commands/command.py +336 -0
- moru/sandbox_async/commands/command_handle.py +196 -0
- moru/sandbox_async/commands/pty.py +240 -0
- moru/sandbox_async/filesystem/filesystem.py +531 -0
- moru/sandbox_async/filesystem/watch_handle.py +62 -0
- moru/sandbox_async/main.py +734 -0
- moru/sandbox_async/paginator.py +69 -0
- moru/sandbox_async/sandbox_api.py +325 -0
- moru/sandbox_async/utils.py +7 -0
- moru/sandbox_sync/commands/command.py +328 -0
- moru/sandbox_sync/commands/command_handle.py +150 -0
- moru/sandbox_sync/commands/pty.py +230 -0
- moru/sandbox_sync/filesystem/filesystem.py +518 -0
- moru/sandbox_sync/filesystem/watch_handle.py +69 -0
- moru/sandbox_sync/main.py +726 -0
- moru/sandbox_sync/paginator.py +69 -0
- moru/sandbox_sync/sandbox_api.py +308 -0
- moru/template/consts.py +30 -0
- moru/template/dockerfile_parser.py +275 -0
- moru/template/logger.py +232 -0
- moru/template/main.py +1360 -0
- moru/template/readycmd.py +138 -0
- moru/template/types.py +105 -0
- moru/template/utils.py +320 -0
- moru/template_async/build_api.py +202 -0
- moru/template_async/main.py +366 -0
- moru/template_sync/build_api.py +199 -0
- moru/template_sync/main.py +371 -0
- moru-0.1.0.dist-info/METADATA +63 -0
- moru-0.1.0.dist-info/RECORD +152 -0
- moru-0.1.0.dist-info/WHEEL +4 -0
- moru-0.1.0.dist-info/licenses/LICENSE +9 -0
- moru_connect/__init__.py +1 -0
- moru_connect/client.py +493 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from moru.api import handle_api_exception
|
|
5
|
+
from moru.api.client.api.sandboxes import get_v2_sandboxes
|
|
6
|
+
from moru.api.client.models.error import Error
|
|
7
|
+
from moru.api.client.types import UNSET
|
|
8
|
+
from moru.exceptions import SandboxException
|
|
9
|
+
from moru.sandbox.sandbox_api import SandboxPaginatorBase, SandboxInfo
|
|
10
|
+
from moru.api.client_sync import get_api_client
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SandboxPaginator(SandboxPaginatorBase):
|
|
14
|
+
"""
|
|
15
|
+
Paginator for listing sandboxes.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
```python
|
|
19
|
+
paginator = Sandbox.list()
|
|
20
|
+
|
|
21
|
+
while paginator.has_next:
|
|
22
|
+
sandboxes = paginator.next_items()
|
|
23
|
+
print(sandboxes)
|
|
24
|
+
```
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def next_items(self) -> List[SandboxInfo]:
|
|
28
|
+
"""
|
|
29
|
+
Returns the next page of sandboxes.
|
|
30
|
+
|
|
31
|
+
Call this method only if `has_next` is `True`, otherwise it will raise an exception.
|
|
32
|
+
|
|
33
|
+
:returns: List of sandboxes
|
|
34
|
+
"""
|
|
35
|
+
if not self.has_next:
|
|
36
|
+
raise Exception("No more items to fetch")
|
|
37
|
+
|
|
38
|
+
# Convert filters to the format expected by the API
|
|
39
|
+
metadata: Optional[str] = None
|
|
40
|
+
if self.query and self.query.metadata:
|
|
41
|
+
quoted_metadata = {
|
|
42
|
+
urllib.parse.quote(k): urllib.parse.quote(v)
|
|
43
|
+
for k, v in self.query.metadata.items()
|
|
44
|
+
}
|
|
45
|
+
metadata = urllib.parse.urlencode(quoted_metadata)
|
|
46
|
+
|
|
47
|
+
api_client = get_api_client(self._config)
|
|
48
|
+
res = get_v2_sandboxes.sync_detailed(
|
|
49
|
+
client=api_client,
|
|
50
|
+
metadata=metadata if metadata else UNSET,
|
|
51
|
+
state=self.query.state if self.query and self.query.state else UNSET,
|
|
52
|
+
limit=self.limit if self.limit else UNSET,
|
|
53
|
+
next_token=self._next_token if self._next_token else UNSET,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if res.status_code >= 300:
|
|
57
|
+
raise handle_api_exception(res)
|
|
58
|
+
|
|
59
|
+
self._next_token = res.headers.get("x-next-token")
|
|
60
|
+
self._has_next = bool(self._next_token)
|
|
61
|
+
|
|
62
|
+
if res.parsed is None:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
# Check if res.parse is Error
|
|
66
|
+
if isinstance(res.parsed, Error):
|
|
67
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
68
|
+
|
|
69
|
+
return [SandboxInfo._from_listed_sandbox(sandbox) for sandbox in res.parsed]
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from packaging.version import Version
|
|
5
|
+
from typing_extensions import Unpack
|
|
6
|
+
|
|
7
|
+
from moru.api import SandboxCreateResponse, handle_api_exception
|
|
8
|
+
from moru.api.client.api.sandboxes import (
|
|
9
|
+
delete_sandboxes_sandbox_id,
|
|
10
|
+
get_sandboxes_sandbox_id,
|
|
11
|
+
get_sandboxes_sandbox_id_metrics,
|
|
12
|
+
post_sandboxes,
|
|
13
|
+
post_sandboxes_sandbox_id_connect,
|
|
14
|
+
post_sandboxes_sandbox_id_pause,
|
|
15
|
+
post_sandboxes_sandbox_id_timeout,
|
|
16
|
+
)
|
|
17
|
+
from moru.api.client.models import (
|
|
18
|
+
ConnectSandbox,
|
|
19
|
+
Error,
|
|
20
|
+
NewSandbox,
|
|
21
|
+
PostSandboxesSandboxIDTimeoutBody,
|
|
22
|
+
Sandbox,
|
|
23
|
+
SandboxNetworkConfig,
|
|
24
|
+
)
|
|
25
|
+
from moru.api.client.types import UNSET
|
|
26
|
+
from moru.connection_config import ApiParams, ConnectionConfig
|
|
27
|
+
from moru.exceptions import NotFoundException, SandboxException, TemplateException
|
|
28
|
+
from moru.sandbox.main import SandboxBase
|
|
29
|
+
from moru.sandbox.sandbox_api import (
|
|
30
|
+
McpServer,
|
|
31
|
+
SandboxInfo,
|
|
32
|
+
SandboxMetrics,
|
|
33
|
+
SandboxNetworkOpts,
|
|
34
|
+
SandboxQuery,
|
|
35
|
+
)
|
|
36
|
+
from moru.sandbox_sync.paginator import SandboxPaginator, get_api_client
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SandboxApi(SandboxBase):
|
|
40
|
+
@staticmethod
|
|
41
|
+
def list(
|
|
42
|
+
query: Optional[SandboxQuery] = None,
|
|
43
|
+
limit: Optional[int] = None,
|
|
44
|
+
next_token: Optional[str] = None,
|
|
45
|
+
**opts: Unpack[ApiParams],
|
|
46
|
+
) -> SandboxPaginator:
|
|
47
|
+
"""
|
|
48
|
+
List all running sandboxes.
|
|
49
|
+
|
|
50
|
+
:param query: Filter the list of sandboxes by metadata or state, e.g. `SandboxListQuery(metadata={"key": "value"})` or `SandboxListQuery(state=[SandboxState.RUNNING])`
|
|
51
|
+
:param limit: Maximum number of sandboxes to return per page
|
|
52
|
+
:param next_token: Token for pagination
|
|
53
|
+
|
|
54
|
+
:return: List of running sandboxes
|
|
55
|
+
"""
|
|
56
|
+
return SandboxPaginator(
|
|
57
|
+
query=query,
|
|
58
|
+
limit=limit,
|
|
59
|
+
next_token=next_token,
|
|
60
|
+
**opts,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def _cls_get_info(
|
|
65
|
+
cls,
|
|
66
|
+
sandbox_id: str,
|
|
67
|
+
**opts: Unpack[ApiParams],
|
|
68
|
+
) -> SandboxInfo:
|
|
69
|
+
"""
|
|
70
|
+
Get the sandbox info.
|
|
71
|
+
:param sandbox_id: Sandbox ID
|
|
72
|
+
|
|
73
|
+
:return: Sandbox info
|
|
74
|
+
"""
|
|
75
|
+
config = ConnectionConfig(**opts)
|
|
76
|
+
|
|
77
|
+
api_client = get_api_client(config)
|
|
78
|
+
res = get_sandboxes_sandbox_id.sync_detailed(
|
|
79
|
+
sandbox_id,
|
|
80
|
+
client=api_client,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if res.status_code == 404:
|
|
84
|
+
raise NotFoundException(f"Sandbox {sandbox_id} not found")
|
|
85
|
+
|
|
86
|
+
if res.status_code >= 300:
|
|
87
|
+
raise handle_api_exception(res)
|
|
88
|
+
|
|
89
|
+
if res.parsed is None:
|
|
90
|
+
raise SandboxException("Body of the request is None")
|
|
91
|
+
|
|
92
|
+
if isinstance(res.parsed, Error):
|
|
93
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
94
|
+
|
|
95
|
+
return SandboxInfo._from_sandbox_detail(res.parsed)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def _cls_kill(
|
|
99
|
+
cls,
|
|
100
|
+
sandbox_id: str,
|
|
101
|
+
**opts: Unpack[ApiParams],
|
|
102
|
+
) -> bool:
|
|
103
|
+
config = ConnectionConfig(**opts)
|
|
104
|
+
|
|
105
|
+
if config.debug:
|
|
106
|
+
# Skip killing the sandbox in debug mode
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
api_client = get_api_client(config)
|
|
110
|
+
res = delete_sandboxes_sandbox_id.sync_detailed(
|
|
111
|
+
sandbox_id,
|
|
112
|
+
client=api_client,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if res.status_code == 404:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
if res.status_code >= 300:
|
|
119
|
+
raise handle_api_exception(res)
|
|
120
|
+
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def _cls_set_timeout(
|
|
125
|
+
cls,
|
|
126
|
+
sandbox_id: str,
|
|
127
|
+
timeout: int,
|
|
128
|
+
**opts: Unpack[ApiParams],
|
|
129
|
+
) -> None:
|
|
130
|
+
config = ConnectionConfig(**opts)
|
|
131
|
+
|
|
132
|
+
if config.debug:
|
|
133
|
+
# Skip setting timeout in debug mode
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
api_client = get_api_client(config)
|
|
137
|
+
res = post_sandboxes_sandbox_id_timeout.sync_detailed(
|
|
138
|
+
sandbox_id,
|
|
139
|
+
client=api_client,
|
|
140
|
+
body=PostSandboxesSandboxIDTimeoutBody(timeout=timeout),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if res.status_code == 404:
|
|
144
|
+
raise NotFoundException(f"Sandbox {sandbox_id} not found")
|
|
145
|
+
|
|
146
|
+
if res.status_code >= 300:
|
|
147
|
+
raise handle_api_exception(res)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def _create_sandbox(
|
|
151
|
+
cls,
|
|
152
|
+
template: str,
|
|
153
|
+
timeout: int,
|
|
154
|
+
auto_pause: bool,
|
|
155
|
+
allow_internet_access: bool,
|
|
156
|
+
metadata: Optional[Dict[str, str]],
|
|
157
|
+
env_vars: Optional[Dict[str, str]],
|
|
158
|
+
secure: bool,
|
|
159
|
+
mcp: Optional[McpServer] = None,
|
|
160
|
+
network: Optional[SandboxNetworkOpts] = None,
|
|
161
|
+
**opts: Unpack[ApiParams],
|
|
162
|
+
) -> SandboxCreateResponse:
|
|
163
|
+
config = ConnectionConfig(**opts)
|
|
164
|
+
|
|
165
|
+
api_client = get_api_client(config)
|
|
166
|
+
res = post_sandboxes.sync_detailed(
|
|
167
|
+
body=NewSandbox(
|
|
168
|
+
template_id=template,
|
|
169
|
+
auto_pause=auto_pause,
|
|
170
|
+
metadata=metadata or {},
|
|
171
|
+
timeout=timeout,
|
|
172
|
+
env_vars=env_vars or {},
|
|
173
|
+
mcp=mcp or UNSET,
|
|
174
|
+
secure=secure,
|
|
175
|
+
allow_internet_access=allow_internet_access,
|
|
176
|
+
network=SandboxNetworkConfig(**network) if network else UNSET,
|
|
177
|
+
),
|
|
178
|
+
client=api_client,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if res.status_code >= 300:
|
|
182
|
+
raise handle_api_exception(res)
|
|
183
|
+
|
|
184
|
+
if res.parsed is None:
|
|
185
|
+
raise Exception("Body of the request is None")
|
|
186
|
+
|
|
187
|
+
if isinstance(res.parsed, Error):
|
|
188
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
189
|
+
|
|
190
|
+
if Version(res.parsed.envd_version) < Version("0.1.0"):
|
|
191
|
+
SandboxApi._cls_kill(res.parsed.sandbox_id)
|
|
192
|
+
raise TemplateException(
|
|
193
|
+
"You need to update the template to use the new SDK. "
|
|
194
|
+
"You can do this by running `moru template build` in the directory with the template."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return SandboxCreateResponse(
|
|
198
|
+
sandbox_id=res.parsed.sandbox_id,
|
|
199
|
+
sandbox_domain=res.parsed.domain,
|
|
200
|
+
envd_version=res.parsed.envd_version,
|
|
201
|
+
envd_access_token=res.parsed.envd_access_token,
|
|
202
|
+
traffic_access_token=res.parsed.traffic_access_token,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def _cls_get_metrics(
|
|
207
|
+
cls,
|
|
208
|
+
sandbox_id: str,
|
|
209
|
+
start: Optional[datetime.datetime] = None,
|
|
210
|
+
end: Optional[datetime.datetime] = None,
|
|
211
|
+
**opts: Unpack[ApiParams],
|
|
212
|
+
) -> List[SandboxMetrics]:
|
|
213
|
+
config = ConnectionConfig(**opts)
|
|
214
|
+
|
|
215
|
+
if config.debug:
|
|
216
|
+
# Skip getting the metrics in debug mode
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
api_client = get_api_client(config)
|
|
220
|
+
res = get_sandboxes_sandbox_id_metrics.sync_detailed(
|
|
221
|
+
sandbox_id,
|
|
222
|
+
start=int(start.timestamp() * 1000) if start else None,
|
|
223
|
+
end=int(end.timestamp() * 1000) if end else None,
|
|
224
|
+
client=api_client,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if res.status_code >= 300:
|
|
228
|
+
raise handle_api_exception(res)
|
|
229
|
+
|
|
230
|
+
if res.parsed is None:
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
if isinstance(res.parsed, Error):
|
|
234
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
235
|
+
|
|
236
|
+
# Convert to typed SandboxMetrics objects
|
|
237
|
+
return [
|
|
238
|
+
SandboxMetrics(
|
|
239
|
+
cpu_count=metric.cpu_count,
|
|
240
|
+
cpu_used_pct=metric.cpu_used_pct,
|
|
241
|
+
disk_total=metric.disk_total,
|
|
242
|
+
disk_used=metric.disk_used,
|
|
243
|
+
mem_total=metric.mem_total,
|
|
244
|
+
mem_used=metric.mem_used,
|
|
245
|
+
timestamp=metric.timestamp,
|
|
246
|
+
)
|
|
247
|
+
for metric in res.parsed
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def _cls_connect(
|
|
252
|
+
cls,
|
|
253
|
+
sandbox_id: str,
|
|
254
|
+
timeout: Optional[int] = None,
|
|
255
|
+
**opts: Unpack[ApiParams],
|
|
256
|
+
) -> Sandbox:
|
|
257
|
+
timeout = timeout or SandboxBase.default_sandbox_timeout
|
|
258
|
+
|
|
259
|
+
config = ConnectionConfig(**opts)
|
|
260
|
+
|
|
261
|
+
api_client = get_api_client(
|
|
262
|
+
config,
|
|
263
|
+
headers={
|
|
264
|
+
"Moru-Sandbox-Id": sandbox_id,
|
|
265
|
+
"Moru-Sandbox-Port": str(config.envd_port),
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
res = post_sandboxes_sandbox_id_connect.sync_detailed(
|
|
269
|
+
sandbox_id,
|
|
270
|
+
client=api_client,
|
|
271
|
+
body=ConnectSandbox(timeout=timeout),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if res.status_code == 404:
|
|
275
|
+
raise NotFoundException(f"Paused sandbox {sandbox_id} not found")
|
|
276
|
+
|
|
277
|
+
if res.status_code >= 300:
|
|
278
|
+
raise handle_api_exception(res)
|
|
279
|
+
|
|
280
|
+
if isinstance(res.parsed, Error):
|
|
281
|
+
raise SandboxException(f"{res.parsed.message}: Request failed")
|
|
282
|
+
|
|
283
|
+
return res.parsed
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def _cls_pause(
|
|
287
|
+
cls,
|
|
288
|
+
sandbox_id: str,
|
|
289
|
+
**opts: Unpack[ApiParams],
|
|
290
|
+
) -> str:
|
|
291
|
+
config = ConnectionConfig(**opts)
|
|
292
|
+
|
|
293
|
+
api_client = get_api_client(config)
|
|
294
|
+
res = post_sandboxes_sandbox_id_pause.sync_detailed(
|
|
295
|
+
sandbox_id,
|
|
296
|
+
client=api_client,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if res.status_code == 404:
|
|
300
|
+
raise NotFoundException(f"Sandbox {sandbox_id} not found")
|
|
301
|
+
|
|
302
|
+
if res.status_code == 409:
|
|
303
|
+
return sandbox_id
|
|
304
|
+
|
|
305
|
+
if res.status_code >= 300:
|
|
306
|
+
raise handle_api_exception(res)
|
|
307
|
+
|
|
308
|
+
return sandbox_id
|
moru/template/consts.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Special step name for the finalization phase of template building.
|
|
3
|
+
This is the last step that runs after all user-defined instructions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
FINALIZE_STEP_NAME = "finalize"
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Special step name for the base image phase of template building.
|
|
10
|
+
This is the first step that sets up the base image.
|
|
11
|
+
"""
|
|
12
|
+
BASE_STEP_NAME = "base"
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
Stack trace depth for capturing caller information.
|
|
16
|
+
|
|
17
|
+
Depth levels:
|
|
18
|
+
1. TemplateClass
|
|
19
|
+
2. Caller method (e.g., copy(), from_image(), etc.)
|
|
20
|
+
|
|
21
|
+
This depth is used to determine the original caller's location
|
|
22
|
+
for stack traces.
|
|
23
|
+
"""
|
|
24
|
+
STACK_TRACE_DEPTH = 2
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
Default setting for whether to resolve symbolic links when copying files.
|
|
28
|
+
When False, symlinks are copied as symlinks rather than following them.
|
|
29
|
+
"""
|
|
30
|
+
RESOLVE_SYMLINKS = False
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import tempfile
|
|
5
|
+
from typing import Dict, List, Optional, Protocol, Union, Literal
|
|
6
|
+
|
|
7
|
+
from dockerfile_parse import DockerfileParser
|
|
8
|
+
from moru.template.types import CopyItem
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DockerfFileFinalParserInterface(Protocol):
|
|
12
|
+
"""Protocol defining the final interface for Dockerfile parsing callbacks."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DockerfileParserInterface(Protocol):
|
|
16
|
+
"""Protocol defining the interface for Dockerfile parsing callbacks."""
|
|
17
|
+
|
|
18
|
+
def run_cmd(
|
|
19
|
+
self, command: Union[str, List[str]], user: Optional[str] = None
|
|
20
|
+
) -> "DockerfileParserInterface":
|
|
21
|
+
"""Handle RUN instruction."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
def copy(
|
|
25
|
+
self,
|
|
26
|
+
src: Union[str, List[CopyItem]],
|
|
27
|
+
dest: Optional[str] = None,
|
|
28
|
+
force_upload: Optional[Literal[True]] = None,
|
|
29
|
+
resolve_symlinks: Optional[bool] = None,
|
|
30
|
+
user: Optional[str] = None,
|
|
31
|
+
mode: Optional[int] = None,
|
|
32
|
+
) -> "DockerfileParserInterface":
|
|
33
|
+
"""Handle COPY instruction."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def set_workdir(self, workdir: str) -> "DockerfileParserInterface":
|
|
37
|
+
"""Handle WORKDIR instruction."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def set_user(self, user: str) -> "DockerfileParserInterface":
|
|
41
|
+
"""Handle USER instruction."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def set_envs(self, envs: Dict[str, str]) -> "DockerfileParserInterface":
|
|
45
|
+
"""Handle ENV instruction."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def set_start_cmd(
|
|
49
|
+
self, start_cmd: str, ready_cmd: str
|
|
50
|
+
) -> "DockerfFileFinalParserInterface":
|
|
51
|
+
"""Handle CMD/ENTRYPOINT instruction."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_dockerfile(
|
|
56
|
+
dockerfile_content_or_path: str, template_builder: DockerfileParserInterface
|
|
57
|
+
) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Parse a Dockerfile and convert it to Template SDK format.
|
|
60
|
+
|
|
61
|
+
:param dockerfile_content_or_path: Either the Dockerfile content as a string, or a path to a Dockerfile file
|
|
62
|
+
:param template_builder: Interface providing template builder methods
|
|
63
|
+
|
|
64
|
+
:return: The base image from the Dockerfile
|
|
65
|
+
|
|
66
|
+
:raises ValueError: If the Dockerfile is invalid or unsupported
|
|
67
|
+
"""
|
|
68
|
+
# Check if input is a file path that exists
|
|
69
|
+
if os.path.isfile(dockerfile_content_or_path):
|
|
70
|
+
# Read the file content
|
|
71
|
+
with open(dockerfile_content_or_path, "r", encoding="utf-8") as f:
|
|
72
|
+
dockerfile_content = f.read()
|
|
73
|
+
else:
|
|
74
|
+
# Treat as content directly
|
|
75
|
+
dockerfile_content = dockerfile_content_or_path
|
|
76
|
+
|
|
77
|
+
# Use a temporary directory to avoid creating files in the current directory
|
|
78
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
79
|
+
# Create a temporary Dockerfile
|
|
80
|
+
dockerfile_path = os.path.join(temp_dir, "Dockerfile")
|
|
81
|
+
with open(dockerfile_path, "w") as f:
|
|
82
|
+
f.write(dockerfile_content)
|
|
83
|
+
|
|
84
|
+
dfp = DockerfileParser(path=temp_dir)
|
|
85
|
+
|
|
86
|
+
# Check for multi-stage builds
|
|
87
|
+
from_instructions = [
|
|
88
|
+
instruction
|
|
89
|
+
for instruction in dfp.structure
|
|
90
|
+
if instruction["instruction"] == "FROM"
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
if len(from_instructions) > 1:
|
|
94
|
+
raise ValueError("Multi-stage Dockerfiles are not supported")
|
|
95
|
+
|
|
96
|
+
if len(from_instructions) == 0:
|
|
97
|
+
raise ValueError("Dockerfile must contain a FROM instruction")
|
|
98
|
+
|
|
99
|
+
# Set the base image from the first FROM instruction
|
|
100
|
+
base_image = from_instructions[0]["value"]
|
|
101
|
+
# Remove AS alias if present (e.g., "node:18 AS builder" -> "node:18")
|
|
102
|
+
if " as " in base_image.lower():
|
|
103
|
+
base_image = base_image.split(" as ")[0].strip()
|
|
104
|
+
|
|
105
|
+
user_changed = False
|
|
106
|
+
workdir_changed = False
|
|
107
|
+
|
|
108
|
+
# Set the user and workdir to the Docker defaults
|
|
109
|
+
template_builder.set_user("root")
|
|
110
|
+
template_builder.set_workdir("/")
|
|
111
|
+
|
|
112
|
+
# Process all other instructions
|
|
113
|
+
for instruction_data in dfp.structure:
|
|
114
|
+
instruction = instruction_data["instruction"]
|
|
115
|
+
value = instruction_data["value"]
|
|
116
|
+
|
|
117
|
+
if instruction == "FROM":
|
|
118
|
+
# Already handled above
|
|
119
|
+
continue
|
|
120
|
+
elif instruction == "RUN":
|
|
121
|
+
_handle_run_instruction(value, template_builder)
|
|
122
|
+
elif instruction in ["COPY", "ADD"]:
|
|
123
|
+
_handle_copy_instruction(value, template_builder)
|
|
124
|
+
elif instruction == "WORKDIR":
|
|
125
|
+
_handle_workdir_instruction(value, template_builder)
|
|
126
|
+
workdir_changed = True
|
|
127
|
+
elif instruction == "USER":
|
|
128
|
+
_handle_user_instruction(value, template_builder)
|
|
129
|
+
user_changed = True
|
|
130
|
+
elif instruction in ["ENV", "ARG"]:
|
|
131
|
+
_handle_env_instruction(value, instruction, template_builder)
|
|
132
|
+
elif instruction in ["CMD", "ENTRYPOINT"]:
|
|
133
|
+
_handle_cmd_entrypoint_instruction(value, template_builder)
|
|
134
|
+
else:
|
|
135
|
+
print(f"Unsupported instruction: {instruction}")
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Set the user and workdir to the Moru defaults
|
|
139
|
+
if not user_changed:
|
|
140
|
+
template_builder.set_user("user")
|
|
141
|
+
if not workdir_changed:
|
|
142
|
+
template_builder.set_workdir("/home/user")
|
|
143
|
+
|
|
144
|
+
return base_image
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _handle_run_instruction(
|
|
148
|
+
value: str, template_builder: DockerfileParserInterface
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Handle RUN instruction"""
|
|
151
|
+
if not value.strip():
|
|
152
|
+
return
|
|
153
|
+
# Remove line continuations and normalize whitespace
|
|
154
|
+
command = re.sub(r"\\\s*\n\s*", " ", value).strip()
|
|
155
|
+
template_builder.run_cmd(command)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _handle_copy_instruction(
|
|
159
|
+
value: str, template_builder: DockerfileParserInterface
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Handle COPY/ADD instruction"""
|
|
162
|
+
if not value.strip():
|
|
163
|
+
return
|
|
164
|
+
# Parse source and destination from COPY/ADD command
|
|
165
|
+
# Handle both quoted and unquoted paths
|
|
166
|
+
parts = []
|
|
167
|
+
current_part = ""
|
|
168
|
+
in_quotes = False
|
|
169
|
+
quote_char = None
|
|
170
|
+
|
|
171
|
+
i = 0
|
|
172
|
+
while i < len(value):
|
|
173
|
+
char = value[i]
|
|
174
|
+
if char in ['"', "'"] and (i == 0 or value[i - 1] != "\\"):
|
|
175
|
+
if not in_quotes:
|
|
176
|
+
in_quotes = True
|
|
177
|
+
quote_char = char
|
|
178
|
+
elif char == quote_char:
|
|
179
|
+
in_quotes = False
|
|
180
|
+
quote_char = None
|
|
181
|
+
else:
|
|
182
|
+
current_part += char
|
|
183
|
+
elif char == " " and not in_quotes:
|
|
184
|
+
if current_part:
|
|
185
|
+
parts.append(current_part)
|
|
186
|
+
current_part = ""
|
|
187
|
+
else:
|
|
188
|
+
current_part += char
|
|
189
|
+
i += 1
|
|
190
|
+
|
|
191
|
+
if current_part:
|
|
192
|
+
parts.append(current_part)
|
|
193
|
+
|
|
194
|
+
if len(parts) >= 2:
|
|
195
|
+
src = parts[0]
|
|
196
|
+
dest = parts[-1] # Last part is destination
|
|
197
|
+
template_builder.copy(src, dest)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _handle_workdir_instruction(
|
|
201
|
+
value: str, template_builder: DockerfileParserInterface
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Handle WORKDIR instruction"""
|
|
204
|
+
if not value.strip():
|
|
205
|
+
return
|
|
206
|
+
workdir = value.strip()
|
|
207
|
+
template_builder.set_workdir(workdir)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _handle_user_instruction(
|
|
211
|
+
value: str, template_builder: DockerfileParserInterface
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Handle USER instruction"""
|
|
214
|
+
if not value.strip():
|
|
215
|
+
return
|
|
216
|
+
user = value.strip()
|
|
217
|
+
template_builder.set_user(user)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _handle_env_instruction(
|
|
221
|
+
value: str, instruction_type: str, template_builder: DockerfileParserInterface
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Handle ENV/ARG instruction"""
|
|
224
|
+
if not value.strip():
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Parse environment variables from the value
|
|
228
|
+
# Handle both "KEY=value" and "KEY value" formats
|
|
229
|
+
env_vars = {}
|
|
230
|
+
|
|
231
|
+
# First try to split on = for KEY=value format
|
|
232
|
+
if "=" in value:
|
|
233
|
+
# Handle multiple KEY=value pairs on one line
|
|
234
|
+
pairs = re.findall(r"(\w+)=([^\s]*(?:\s+(?!\w+=)[^\s]*)*)", value)
|
|
235
|
+
for key, val in pairs:
|
|
236
|
+
env_vars[key] = val.strip("\"'")
|
|
237
|
+
else:
|
|
238
|
+
# Handle "KEY value" format
|
|
239
|
+
parts = value.split(None, 1)
|
|
240
|
+
if len(parts) == 2:
|
|
241
|
+
key, val = parts
|
|
242
|
+
env_vars[key] = val.strip("\"'")
|
|
243
|
+
elif len(parts) == 1 and instruction_type == "ARG":
|
|
244
|
+
# ARG without default value
|
|
245
|
+
key = parts[0]
|
|
246
|
+
env_vars[key] = ""
|
|
247
|
+
|
|
248
|
+
# Add each environment variable
|
|
249
|
+
if env_vars:
|
|
250
|
+
template_builder.set_envs(env_vars)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _handle_cmd_entrypoint_instruction(
|
|
254
|
+
value: str, template_builder: DockerfileParserInterface
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Handle CMD/ENTRYPOINT instruction - convert to set_start_cmd with 20s timeout"""
|
|
257
|
+
if not value.strip():
|
|
258
|
+
return
|
|
259
|
+
command = value.strip()
|
|
260
|
+
|
|
261
|
+
# Try to parse as JSON (for array format like CMD ["sleep", "infinity"])
|
|
262
|
+
try:
|
|
263
|
+
parsed_command = json.loads(command)
|
|
264
|
+
if isinstance(parsed_command, list):
|
|
265
|
+
command = " ".join(str(item) for item in parsed_command)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
# Import wait_for_timeout locally to avoid circular dependency
|
|
270
|
+
def wait_for_timeout(timeout: int) -> str:
|
|
271
|
+
# convert to seconds, but ensure minimum of 1 second
|
|
272
|
+
seconds = max(1, timeout // 1000)
|
|
273
|
+
return f"sleep {seconds}"
|
|
274
|
+
|
|
275
|
+
template_builder.set_start_cmd(command, wait_for_timeout(20_000))
|