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.
Files changed (152) hide show
  1. moru/__init__.py +174 -0
  2. moru/api/__init__.py +164 -0
  3. moru/api/client/__init__.py +8 -0
  4. moru/api/client/api/__init__.py +1 -0
  5. moru/api/client/api/sandboxes/__init__.py +1 -0
  6. moru/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. moru/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. moru/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
  12. moru/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
  13. moru/api/client/api/sandboxes/post_sandboxes.py +172 -0
  14. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  15. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
  16. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
  17. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
  18. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
  19. moru/api/client/api/templates/__init__.py +1 -0
  20. moru/api/client/api/templates/delete_templates_template_id.py +157 -0
  21. moru/api/client/api/templates/get_templates.py +172 -0
  22. moru/api/client/api/templates/get_templates_template_id.py +195 -0
  23. moru/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +217 -0
  24. moru/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
  25. moru/api/client/api/templates/patch_templates_template_id.py +183 -0
  26. moru/api/client/api/templates/post_templates.py +172 -0
  27. moru/api/client/api/templates/post_templates_template_id.py +181 -0
  28. moru/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
  29. moru/api/client/api/templates/post_v2_templates.py +172 -0
  30. moru/api/client/api/templates/post_v3_templates.py +172 -0
  31. moru/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
  32. moru/api/client/client.py +286 -0
  33. moru/api/client/errors.py +16 -0
  34. moru/api/client/models/__init__.py +123 -0
  35. moru/api/client/models/aws_registry.py +85 -0
  36. moru/api/client/models/aws_registry_type.py +8 -0
  37. moru/api/client/models/build_log_entry.py +89 -0
  38. moru/api/client/models/build_status_reason.py +95 -0
  39. moru/api/client/models/connect_sandbox.py +59 -0
  40. moru/api/client/models/created_access_token.py +100 -0
  41. moru/api/client/models/created_team_api_key.py +166 -0
  42. moru/api/client/models/disk_metrics.py +91 -0
  43. moru/api/client/models/error.py +67 -0
  44. moru/api/client/models/gcp_registry.py +69 -0
  45. moru/api/client/models/gcp_registry_type.py +8 -0
  46. moru/api/client/models/general_registry.py +77 -0
  47. moru/api/client/models/general_registry_type.py +8 -0
  48. moru/api/client/models/identifier_masking_details.py +83 -0
  49. moru/api/client/models/listed_sandbox.py +154 -0
  50. moru/api/client/models/log_level.py +11 -0
  51. moru/api/client/models/max_team_metric.py +78 -0
  52. moru/api/client/models/mcp_type_0.py +44 -0
  53. moru/api/client/models/new_access_token.py +59 -0
  54. moru/api/client/models/new_sandbox.py +172 -0
  55. moru/api/client/models/new_team_api_key.py +59 -0
  56. moru/api/client/models/node.py +155 -0
  57. moru/api/client/models/node_detail.py +165 -0
  58. moru/api/client/models/node_metrics.py +122 -0
  59. moru/api/client/models/node_status.py +11 -0
  60. moru/api/client/models/node_status_change.py +79 -0
  61. moru/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  62. moru/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  63. moru/api/client/models/resumed_sandbox.py +68 -0
  64. moru/api/client/models/sandbox.py +145 -0
  65. moru/api/client/models/sandbox_detail.py +183 -0
  66. moru/api/client/models/sandbox_log.py +70 -0
  67. moru/api/client/models/sandbox_log_entry.py +93 -0
  68. moru/api/client/models/sandbox_log_entry_fields.py +44 -0
  69. moru/api/client/models/sandbox_logs.py +91 -0
  70. moru/api/client/models/sandbox_metric.py +118 -0
  71. moru/api/client/models/sandbox_network_config.py +92 -0
  72. moru/api/client/models/sandbox_state.py +9 -0
  73. moru/api/client/models/sandboxes_with_metrics.py +59 -0
  74. moru/api/client/models/team.py +83 -0
  75. moru/api/client/models/team_api_key.py +158 -0
  76. moru/api/client/models/team_metric.py +86 -0
  77. moru/api/client/models/team_user.py +68 -0
  78. moru/api/client/models/template.py +217 -0
  79. moru/api/client/models/template_build.py +139 -0
  80. moru/api/client/models/template_build_file_upload.py +70 -0
  81. moru/api/client/models/template_build_info.py +126 -0
  82. moru/api/client/models/template_build_request.py +115 -0
  83. moru/api/client/models/template_build_request_v2.py +88 -0
  84. moru/api/client/models/template_build_request_v3.py +88 -0
  85. moru/api/client/models/template_build_start_v2.py +184 -0
  86. moru/api/client/models/template_build_status.py +11 -0
  87. moru/api/client/models/template_legacy.py +207 -0
  88. moru/api/client/models/template_request_response_v3.py +83 -0
  89. moru/api/client/models/template_step.py +91 -0
  90. moru/api/client/models/template_update_request.py +59 -0
  91. moru/api/client/models/template_with_builds.py +148 -0
  92. moru/api/client/models/update_team_api_key.py +59 -0
  93. moru/api/client/py.typed +1 -0
  94. moru/api/client/types.py +54 -0
  95. moru/api/client_async/__init__.py +50 -0
  96. moru/api/client_sync/__init__.py +52 -0
  97. moru/api/metadata.py +14 -0
  98. moru/connection_config.py +217 -0
  99. moru/envd/api.py +59 -0
  100. moru/envd/filesystem/filesystem_connect.py +193 -0
  101. moru/envd/filesystem/filesystem_pb2.py +76 -0
  102. moru/envd/filesystem/filesystem_pb2.pyi +233 -0
  103. moru/envd/process/process_connect.py +155 -0
  104. moru/envd/process/process_pb2.py +92 -0
  105. moru/envd/process/process_pb2.pyi +304 -0
  106. moru/envd/rpc.py +61 -0
  107. moru/envd/versions.py +6 -0
  108. moru/exceptions.py +95 -0
  109. moru/sandbox/commands/command_handle.py +69 -0
  110. moru/sandbox/commands/main.py +39 -0
  111. moru/sandbox/filesystem/filesystem.py +94 -0
  112. moru/sandbox/filesystem/watch_handle.py +60 -0
  113. moru/sandbox/main.py +210 -0
  114. moru/sandbox/mcp.py +1120 -0
  115. moru/sandbox/network.py +8 -0
  116. moru/sandbox/sandbox_api.py +210 -0
  117. moru/sandbox/signature.py +45 -0
  118. moru/sandbox/utils.py +34 -0
  119. moru/sandbox_async/commands/command.py +336 -0
  120. moru/sandbox_async/commands/command_handle.py +196 -0
  121. moru/sandbox_async/commands/pty.py +240 -0
  122. moru/sandbox_async/filesystem/filesystem.py +531 -0
  123. moru/sandbox_async/filesystem/watch_handle.py +62 -0
  124. moru/sandbox_async/main.py +734 -0
  125. moru/sandbox_async/paginator.py +69 -0
  126. moru/sandbox_async/sandbox_api.py +325 -0
  127. moru/sandbox_async/utils.py +7 -0
  128. moru/sandbox_sync/commands/command.py +328 -0
  129. moru/sandbox_sync/commands/command_handle.py +150 -0
  130. moru/sandbox_sync/commands/pty.py +230 -0
  131. moru/sandbox_sync/filesystem/filesystem.py +518 -0
  132. moru/sandbox_sync/filesystem/watch_handle.py +69 -0
  133. moru/sandbox_sync/main.py +726 -0
  134. moru/sandbox_sync/paginator.py +69 -0
  135. moru/sandbox_sync/sandbox_api.py +308 -0
  136. moru/template/consts.py +30 -0
  137. moru/template/dockerfile_parser.py +275 -0
  138. moru/template/logger.py +232 -0
  139. moru/template/main.py +1360 -0
  140. moru/template/readycmd.py +138 -0
  141. moru/template/types.py +105 -0
  142. moru/template/utils.py +320 -0
  143. moru/template_async/build_api.py +202 -0
  144. moru/template_async/main.py +366 -0
  145. moru/template_sync/build_api.py +199 -0
  146. moru/template_sync/main.py +371 -0
  147. moru-0.1.0.dist-info/METADATA +63 -0
  148. moru-0.1.0.dist-info/RECORD +152 -0
  149. moru-0.1.0.dist-info/WHEEL +4 -0
  150. moru-0.1.0.dist-info/licenses/LICENSE +9 -0
  151. moru_connect/__init__.py +1 -0
  152. moru_connect/client.py +493 -0
@@ -0,0 +1,8 @@
1
+ """
2
+ Network configuration helpers for Moru sandboxes.
3
+ """
4
+
5
+ """
6
+ CIDR range that represents all traffic.
7
+ """
8
+ ALL_TRAFFIC = "0.0.0.0/0"
@@ -0,0 +1,210 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import Any, Dict, List, Optional, TypedDict, Union
4
+
5
+ from typing_extensions import NotRequired, Unpack
6
+
7
+ from moru import ConnectionConfig
8
+ from moru.api.client.models import ListedSandbox, SandboxDetail, SandboxState
9
+ from moru.connection_config import ApiParams
10
+ from moru.sandbox.mcp import McpServer as BaseMcpServer
11
+
12
+
13
+ class GitHubMcpServerConfig(TypedDict):
14
+ """
15
+ Configuration for a GitHub-based MCP server.
16
+ """
17
+
18
+ run_cmd: str
19
+ """
20
+ Command to run the MCP server. Must start a stdio-compatible server.
21
+ """
22
+ install_cmd: NotRequired[str]
23
+ """
24
+ Command to install dependencies for the MCP server. Working directory is the root of the github repository.
25
+ """
26
+ envs: NotRequired[Dict[str, str]]
27
+ """
28
+ Environment variables to set in the MCP process.
29
+ """
30
+
31
+
32
+ # Extended MCP server configuration that includes base servers
33
+ # and allows dynamic GitHub-based MCP servers with custom run and install commands.
34
+ # For GitHub servers, use keys in the format "github/owner/repo"
35
+ GitHubMcpServer = Dict[str, Union[GitHubMcpServerConfig, Any]]
36
+
37
+ # Union type that combines base MCP servers with GitHub-based servers
38
+ McpServer = Union[BaseMcpServer, GitHubMcpServer]
39
+
40
+
41
+ class SandboxNetworkOpts(TypedDict):
42
+ """
43
+ Sandbox network configuration options.
44
+ """
45
+
46
+ allow_out: NotRequired[List[str]]
47
+ """
48
+ Allow outbound traffic from the sandbox to the specified addresses.
49
+ If `allow_out` is not specified, all outbound traffic is allowed.
50
+
51
+ Examples:
52
+ - To allow traffic to specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
53
+ """
54
+
55
+ deny_out: NotRequired[List[str]]
56
+ """
57
+ Deny outbound traffic from the sandbox to the specified addresses.
58
+
59
+ Examples:
60
+ - To deny traffic to specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
61
+ """
62
+
63
+ allow_public_traffic: NotRequired[bool]
64
+ """
65
+ Controls whether sandbox URLs should be publicly accessible or require authentication.
66
+ Defaults to True.
67
+ """
68
+
69
+ mask_request_host: NotRequired[str]
70
+ """
71
+ Allows specifying a custom host mask for all sandbox requests.
72
+ Supports ${PORT} variable. Defaults to "${PORT}-sandboxid.moru.io".
73
+
74
+ Examples:
75
+ - Custom subdomain: `"${PORT}-myapp.example.com"`
76
+ """
77
+
78
+
79
+ @dataclass
80
+ class SandboxInfo:
81
+ """Information about a sandbox."""
82
+
83
+ sandbox_id: str
84
+ """Sandbox ID."""
85
+ sandbox_domain: Optional[str]
86
+ """Domain where the sandbox is hosted."""
87
+ template_id: str
88
+ """Template ID."""
89
+ name: Optional[str]
90
+ """Template name."""
91
+ metadata: Dict[str, str]
92
+ """Saved sandbox metadata."""
93
+ started_at: datetime
94
+ """Sandbox start time."""
95
+ end_at: datetime
96
+ """Sandbox expiration date."""
97
+ state: SandboxState
98
+ """Sandbox state."""
99
+ cpu_count: int
100
+ """Sandbox CPU count."""
101
+ memory_mb: int
102
+ """Sandbox Memory size in MiB."""
103
+ envd_version: str
104
+ """Envd version."""
105
+ _envd_access_token: Optional[str]
106
+ """Envd access token."""
107
+
108
+ @classmethod
109
+ def _from_sandbox_data(
110
+ cls,
111
+ sandbox: Union[ListedSandbox, SandboxDetail],
112
+ envd_access_token: Optional[str] = None,
113
+ sandbox_domain: Optional[str] = None,
114
+ ):
115
+ return cls(
116
+ sandbox_domain=sandbox_domain,
117
+ sandbox_id=sandbox.sandbox_id,
118
+ template_id=sandbox.template_id,
119
+ name=(sandbox.alias if isinstance(sandbox.alias, str) else None),
120
+ metadata=(sandbox.metadata if isinstance(sandbox.metadata, dict) else {}),
121
+ started_at=sandbox.started_at,
122
+ end_at=sandbox.end_at,
123
+ state=sandbox.state,
124
+ cpu_count=sandbox.cpu_count,
125
+ memory_mb=sandbox.memory_mb,
126
+ envd_version=sandbox.envd_version,
127
+ _envd_access_token=envd_access_token,
128
+ )
129
+
130
+ @classmethod
131
+ def _from_listed_sandbox(cls, listed_sandbox: ListedSandbox):
132
+ return cls._from_sandbox_data(listed_sandbox)
133
+
134
+ @classmethod
135
+ def _from_sandbox_detail(cls, sandbox_detail: SandboxDetail):
136
+ return cls._from_sandbox_data(
137
+ sandbox_detail,
138
+ (
139
+ sandbox_detail.envd_access_token
140
+ if isinstance(sandbox_detail.envd_access_token, str)
141
+ else None
142
+ ),
143
+ sandbox_domain=(
144
+ sandbox_detail.domain
145
+ if isinstance(sandbox_detail.domain, str)
146
+ else None
147
+ ),
148
+ )
149
+
150
+
151
+ @dataclass
152
+ class SandboxQuery:
153
+ """Query parameters for listing sandboxes."""
154
+
155
+ metadata: Optional[dict[str, str]] = None
156
+ """Filter sandboxes by metadata."""
157
+
158
+ state: Optional[list[SandboxState]] = None
159
+ """Filter sandboxes by state."""
160
+
161
+
162
+ @dataclass
163
+ class SandboxMetrics:
164
+ """Sandbox metrics."""
165
+
166
+ cpu_count: int
167
+ """Number of CPUs."""
168
+ cpu_used_pct: float
169
+ """CPU usage percentage."""
170
+ disk_total: int
171
+ """Total disk space in bytes."""
172
+ disk_used: int
173
+ """Disk used in bytes."""
174
+ mem_total: int
175
+ """Total memory in bytes."""
176
+ mem_used: int
177
+ """Memory used in bytes."""
178
+ timestamp: datetime
179
+ """Timestamp of the metric entry."""
180
+
181
+
182
+ class SandboxPaginatorBase:
183
+ def __init__(
184
+ self,
185
+ query: Optional[SandboxQuery] = None,
186
+ limit: Optional[int] = None,
187
+ next_token: Optional[str] = None,
188
+ **opts: Unpack[ApiParams],
189
+ ):
190
+ self._config = ConnectionConfig(**opts)
191
+
192
+ self.query = query
193
+ self.limit = limit
194
+
195
+ self._has_next = True
196
+ self._next_token = next_token
197
+
198
+ @property
199
+ def has_next(self) -> bool:
200
+ """
201
+ Returns True if there are more items to fetch.
202
+ """
203
+ return self._has_next
204
+
205
+ @property
206
+ def next_token(self) -> Optional[str]:
207
+ """
208
+ Returns the next token to use for pagination.
209
+ """
210
+ return self._next_token
@@ -0,0 +1,45 @@
1
+ import base64
2
+ import hashlib
3
+ import time
4
+
5
+ from typing import Optional, TypedDict, Literal
6
+
7
+ Operation = Literal["read", "write"]
8
+
9
+
10
+ class Signature(TypedDict):
11
+ signature: str
12
+ expiration: Optional[int] # Unix timestamp or None
13
+
14
+
15
+ def get_signature(
16
+ path: str,
17
+ operation: Operation,
18
+ user: Optional[str],
19
+ envd_access_token: Optional[str],
20
+ expiration_in_seconds: Optional[int] = None,
21
+ ) -> Signature:
22
+ """
23
+ Generate a v1 signature for sandbox file URLs.
24
+ """
25
+ if not envd_access_token:
26
+ raise ValueError("Access token is not set and signature cannot be generated!")
27
+
28
+ expiration = (
29
+ int(time.time()) + expiration_in_seconds if expiration_in_seconds else None
30
+ )
31
+
32
+ # if user is None, set it to empty string to handle default user
33
+ if user is None:
34
+ user = ""
35
+
36
+ raw = (
37
+ f"{path}:{operation}:{user}:{envd_access_token}"
38
+ if expiration is None
39
+ else f"{path}:{operation}:{user}:{envd_access_token}:{expiration}"
40
+ )
41
+
42
+ digest = hashlib.sha256(raw.encode("utf-8")).digest()
43
+ encoded = base64.b64encode(digest).rstrip(b"=").decode("ascii")
44
+
45
+ return {"signature": f"v1_{encoded}", "expiration": expiration}
moru/sandbox/utils.py ADDED
@@ -0,0 +1,34 @@
1
+ from typing import TypeVar, Any, cast, Optional, Type
2
+ import functools
3
+
4
+ T = TypeVar("T")
5
+
6
+
7
+ class class_method_variant(object):
8
+ def __init__(self, class_method_name):
9
+ self.class_method_name = class_method_name
10
+
11
+ method: Any
12
+
13
+ def __call__(self, method: T) -> T:
14
+ self.method = method
15
+ return cast(T, self)
16
+
17
+ def __get__(self, obj, objtype: Optional[Type[Any]] = None):
18
+ @functools.wraps(self.method)
19
+ def _wrapper(*args, **kwargs):
20
+ if obj is not None:
21
+ # Method was called as an instance method, e.g.
22
+ # instance.method(...)
23
+ return self.method(obj, *args, **kwargs)
24
+ elif len(args) > 0 and objtype is not None and isinstance(args[0], objtype):
25
+ # Method was called as a class method with the instance as the
26
+ # first argument, e.g. Class.method(instance, ...) which in
27
+ # Python is the same thing as calling an instance method
28
+ return self.method(args[0], *args[1:], **kwargs)
29
+ else:
30
+ # Method was called as a class method, e.g. Class.method(...)
31
+ class_method = getattr(objtype, self.class_method_name)
32
+ return class_method(*args, **kwargs)
33
+
34
+ return _wrapper
@@ -0,0 +1,336 @@
1
+ from typing import Dict, List, Literal, Optional, Union, overload
2
+
3
+ import moru_connect
4
+ import httpcore
5
+ from packaging.version import Version
6
+ from moru.connection_config import (
7
+ ConnectionConfig,
8
+ Username,
9
+ KEEPALIVE_PING_HEADER,
10
+ KEEPALIVE_PING_INTERVAL_SEC,
11
+ )
12
+ from moru.envd.process import process_connect, process_pb2
13
+ from moru.envd.rpc import authentication_header, handle_rpc_exception
14
+ from moru.envd.versions import ENVD_COMMANDS_STDIN
15
+ from moru.exceptions import SandboxException
16
+ from moru.sandbox.commands.main import ProcessInfo
17
+ from moru.sandbox.commands.command_handle import CommandResult
18
+ from moru.sandbox_async.commands.command_handle import AsyncCommandHandle, Stderr, Stdout
19
+ from moru.sandbox_async.utils import OutputHandler
20
+
21
+
22
+ class Commands:
23
+ """
24
+ Module for executing commands in the sandbox.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ envd_api_url: str,
30
+ connection_config: ConnectionConfig,
31
+ pool: httpcore.AsyncConnectionPool,
32
+ envd_version: Version,
33
+ ) -> None:
34
+ self._connection_config = connection_config
35
+ self._envd_version = envd_version
36
+ self._rpc = process_connect.ProcessClient(
37
+ envd_api_url,
38
+ # TODO: Fix and enable compression again — the headers compression is not solved for streaming.
39
+ # compressor=moru_connect.GzipCompressor,
40
+ async_pool=pool,
41
+ json=True,
42
+ headers=connection_config.sandbox_headers,
43
+ )
44
+
45
+ async def list(
46
+ self,
47
+ request_timeout: Optional[float] = None,
48
+ ) -> List[ProcessInfo]:
49
+ """
50
+ Lists all running commands and PTY sessions.
51
+
52
+ :param request_timeout: Timeout for the request in **seconds**
53
+
54
+ :return: List of running commands and PTY sessions
55
+ """
56
+ try:
57
+ res = await self._rpc.alist(
58
+ process_pb2.ListRequest(),
59
+ request_timeout=self._connection_config.get_request_timeout(
60
+ request_timeout
61
+ ),
62
+ )
63
+ return [
64
+ ProcessInfo(
65
+ pid=p.pid,
66
+ tag=p.tag,
67
+ cmd=p.config.cmd,
68
+ args=list(p.config.args),
69
+ envs=dict(p.config.envs),
70
+ cwd=p.config.cwd,
71
+ )
72
+ for p in res.processes
73
+ ]
74
+ except Exception as e:
75
+ raise handle_rpc_exception(e)
76
+
77
+ async def kill(
78
+ self,
79
+ pid: int,
80
+ request_timeout: Optional[float] = None,
81
+ ) -> bool:
82
+ """
83
+ Kill a running command specified by its process ID.
84
+ It uses `SIGKILL` signal to kill the command.
85
+
86
+ :param pid: Process ID of the command. You can get the list of processes using `sandbox.commands.list()`
87
+ :param request_timeout: Timeout for the request in **seconds**
88
+
89
+ :return: `True` if the command was killed, `False` if the command was not found
90
+ """
91
+ try:
92
+ await self._rpc.asend_signal(
93
+ process_pb2.SendSignalRequest(
94
+ process=process_pb2.ProcessSelector(pid=pid),
95
+ signal=process_pb2.Signal.SIGNAL_SIGKILL,
96
+ ),
97
+ request_timeout=self._connection_config.get_request_timeout(
98
+ request_timeout
99
+ ),
100
+ )
101
+ return True
102
+ except Exception as e:
103
+ if isinstance(e, moru_connect.ConnectException):
104
+ if e.status == moru_connect.Code.not_found:
105
+ return False
106
+ raise handle_rpc_exception(e)
107
+
108
+ async def send_stdin(
109
+ self,
110
+ pid: int,
111
+ data: str,
112
+ request_timeout: Optional[float] = None,
113
+ ) -> None:
114
+ """
115
+ Send data to command stdin.
116
+
117
+ :param pid Process ID of the command. You can get the list of processes using `sandbox.commands.list()`.
118
+ :param data: Data to send to the command
119
+ :param request_timeout: Timeout for the request in **seconds**
120
+ """
121
+ try:
122
+ await self._rpc.asend_input(
123
+ process_pb2.SendInputRequest(
124
+ process=process_pb2.ProcessSelector(pid=pid),
125
+ input=process_pb2.ProcessInput(
126
+ stdin=data.encode(),
127
+ ),
128
+ ),
129
+ request_timeout=self._connection_config.get_request_timeout(
130
+ request_timeout
131
+ ),
132
+ )
133
+ except Exception as e:
134
+ raise handle_rpc_exception(e)
135
+
136
+ @overload
137
+ async def run(
138
+ self,
139
+ cmd: str,
140
+ background: Union[Literal[False], None] = None,
141
+ envs: Optional[Dict[str, str]] = None,
142
+ user: Optional[Username] = None,
143
+ cwd: Optional[str] = None,
144
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
145
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
146
+ stdin: Optional[bool] = None,
147
+ timeout: Optional[float] = 60,
148
+ request_timeout: Optional[float] = None,
149
+ ) -> CommandResult:
150
+ """
151
+ Start a new command and wait until it finishes executing.
152
+
153
+ :param cmd: Command to execute
154
+ :param background: **`False` if the command should be executed in the foreground**, `True` if the command should be executed in the background
155
+ :param envs: Environment variables used for the command
156
+ :param user: User to run the command as
157
+ :param cwd: Working directory to run the command
158
+ :param on_stdout: Callback for command stdout output
159
+ :param on_stderr: Callback for command stderr output
160
+ :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()`
161
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
162
+ :param request_timeout: Timeout for the request in **seconds**
163
+
164
+ :return: `CommandResult` result of the command execution
165
+ """
166
+ ...
167
+
168
+ @overload
169
+ async def run(
170
+ self,
171
+ cmd: str,
172
+ background: Literal[True],
173
+ envs: Optional[Dict[str, str]] = None,
174
+ user: Optional[Username] = None,
175
+ cwd: Optional[str] = None,
176
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
177
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
178
+ stdin: Optional[bool] = None,
179
+ timeout: Optional[float] = 60,
180
+ request_timeout: Optional[float] = None,
181
+ ) -> AsyncCommandHandle:
182
+ """
183
+ Start a new command and return a handle to interact with it.
184
+
185
+ :param cmd: Command to execute
186
+ :param background: `False` if the command should be executed in the foreground, **`True` if the command should be executed in the background**
187
+ :param envs: Environment variables used for the command
188
+ :param user: User to run the command as
189
+ :param cwd: Working directory to run the command
190
+ :param on_stdout: Callback for command stdout output
191
+ :param on_stderr: Callback for command stderr output
192
+ :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()`
193
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
194
+ :param request_timeout: Timeout for the request in **seconds**
195
+
196
+ :return: `AsyncCommandHandle` handle to interact with the running command
197
+ """
198
+ ...
199
+
200
+ async def run(
201
+ self,
202
+ cmd: str,
203
+ background: Union[bool, None] = None,
204
+ envs: Optional[Dict[str, str]] = None,
205
+ user: Optional[Username] = None,
206
+ cwd: Optional[str] = None,
207
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
208
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
209
+ stdin: Optional[bool] = None,
210
+ timeout: Optional[float] = 60,
211
+ request_timeout: Optional[float] = None,
212
+ ):
213
+ # Check version for stdin support
214
+ if stdin is False and self._envd_version < ENVD_COMMANDS_STDIN:
215
+ raise SandboxException(
216
+ f"Sandbox envd version {self._envd_version} can't specify stdin, it's always turned on. "
217
+ f"Please rebuild your template if you need this feature."
218
+ )
219
+
220
+ # Default to `False`
221
+ stdin = stdin or False
222
+
223
+ proc = await self._start(
224
+ cmd,
225
+ envs,
226
+ user,
227
+ cwd,
228
+ timeout,
229
+ request_timeout,
230
+ stdin,
231
+ on_stdout=on_stdout,
232
+ on_stderr=on_stderr,
233
+ )
234
+
235
+ return proc if background else await proc.wait()
236
+
237
+ async def _start(
238
+ self,
239
+ cmd: str,
240
+ envs: Optional[Dict[str, str]],
241
+ user: Username,
242
+ cwd: Optional[str],
243
+ timeout: Optional[float],
244
+ request_timeout: Optional[float],
245
+ stdin: bool,
246
+ on_stdout: Optional[OutputHandler[Stdout]],
247
+ on_stderr: Optional[OutputHandler[Stderr]],
248
+ ) -> AsyncCommandHandle:
249
+ events = self._rpc.astart(
250
+ process_pb2.StartRequest(
251
+ process=process_pb2.ProcessConfig(
252
+ cmd="/bin/bash",
253
+ envs=envs,
254
+ args=["-l", "-c", cmd],
255
+ cwd=cwd,
256
+ ),
257
+ stdin=stdin,
258
+ ),
259
+ headers={
260
+ **authentication_header(self._envd_version, user),
261
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
262
+ },
263
+ timeout=timeout,
264
+ request_timeout=self._connection_config.get_request_timeout(
265
+ request_timeout
266
+ ),
267
+ )
268
+
269
+ try:
270
+ start_event = await events.__anext__()
271
+
272
+ if not start_event.HasField("event"):
273
+ raise SandboxException(
274
+ f"Failed to start process: expected start event, got {start_event}"
275
+ )
276
+
277
+ return AsyncCommandHandle(
278
+ pid=start_event.event.start.pid,
279
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
280
+ events=events,
281
+ on_stdout=on_stdout,
282
+ on_stderr=on_stderr,
283
+ )
284
+ except Exception as e:
285
+ raise handle_rpc_exception(e)
286
+
287
+ async def connect(
288
+ self,
289
+ pid: int,
290
+ timeout: Optional[float] = 60,
291
+ request_timeout: Optional[float] = None,
292
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
293
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
294
+ ) -> AsyncCommandHandle:
295
+ """
296
+ Connects to a running command.
297
+ You can use `AsyncCommandHandle.wait()` to wait for the command to finish and get execution results.
298
+
299
+ :param pid: Process ID of the command to connect to. You can get the list of processes using `sandbox.commands.list()`
300
+ :param request_timeout: Request timeout in **seconds**
301
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
302
+ :param on_stdout: Callback for command stdout output
303
+ :param on_stderr: Callback for command stderr output
304
+
305
+ :return: `AsyncCommandHandle` handle to interact with the running command
306
+ """
307
+ events = self._rpc.aconnect(
308
+ process_pb2.ConnectRequest(
309
+ process=process_pb2.ProcessSelector(pid=pid),
310
+ ),
311
+ timeout=timeout,
312
+ request_timeout=self._connection_config.get_request_timeout(
313
+ request_timeout
314
+ ),
315
+ headers={
316
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
317
+ },
318
+ )
319
+
320
+ try:
321
+ start_event = await events.__anext__()
322
+
323
+ if not start_event.HasField("event"):
324
+ raise SandboxException(
325
+ f"Failed to connect to process: expected start event, got {start_event}"
326
+ )
327
+
328
+ return AsyncCommandHandle(
329
+ pid=start_event.event.start.pid,
330
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
331
+ events=events,
332
+ on_stdout=on_stdout,
333
+ on_stderr=on_stderr,
334
+ )
335
+ except Exception as e:
336
+ raise handle_rpc_exception(e)