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,196 @@
1
+ import asyncio
2
+ import inspect
3
+ from typing import (
4
+ Optional,
5
+ Callable,
6
+ Any,
7
+ AsyncGenerator,
8
+ Union,
9
+ Tuple,
10
+ Coroutine,
11
+ )
12
+
13
+ from moru.envd.rpc import handle_rpc_exception
14
+ from moru.envd.process import process_pb2
15
+ from moru.sandbox.commands.command_handle import (
16
+ CommandExitException,
17
+ CommandResult,
18
+ Stderr,
19
+ Stdout,
20
+ PtyOutput,
21
+ )
22
+ from moru.sandbox_async.utils import OutputHandler
23
+
24
+
25
+ class AsyncCommandHandle:
26
+ """
27
+ Command execution handle.
28
+
29
+ It provides methods for waiting for the command to finish, retrieving stdout/stderr, and killing the command.
30
+ """
31
+
32
+ @property
33
+ def pid(self):
34
+ """
35
+ Command process ID.
36
+ """
37
+ return self._pid
38
+
39
+ @property
40
+ def stdout(self):
41
+ """
42
+ Command stdout output.
43
+ """
44
+ return self._stdout
45
+
46
+ @property
47
+ def stderr(self):
48
+ """
49
+ Command stderr output.
50
+ """
51
+ return self._stderr
52
+
53
+ @property
54
+ def error(self):
55
+ """
56
+ Command execution error message.
57
+ """
58
+ if self._result is None:
59
+ return None
60
+ return self._result.error
61
+
62
+ @property
63
+ def exit_code(self):
64
+ """
65
+ Command execution exit code.
66
+
67
+ `0` if the command finished successfully.
68
+
69
+ It is `None` if the command is still running.
70
+ """
71
+ if self._result is None:
72
+ return None
73
+ return self._result.exit_code
74
+
75
+ def __init__(
76
+ self,
77
+ pid: int,
78
+ handle_kill: Callable[[], Coroutine[Any, Any, bool]],
79
+ events: AsyncGenerator[
80
+ Union[process_pb2.StartResponse, process_pb2.ConnectResponse], Any
81
+ ],
82
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
83
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
84
+ on_pty: Optional[OutputHandler[PtyOutput]] = None,
85
+ ):
86
+ self._pid = pid
87
+ self._handle_kill = handle_kill
88
+ self._events = events
89
+
90
+ self._stdout: str = ""
91
+ self._stderr: str = ""
92
+
93
+ self._on_stdout = on_stdout
94
+ self._on_stderr = on_stderr
95
+ self._on_pty = on_pty
96
+
97
+ self._result: Optional[CommandResult] = None
98
+ self._iteration_exception: Optional[Exception] = None
99
+
100
+ self._wait = asyncio.create_task(self._handle_events())
101
+
102
+ async def _iterate_events(
103
+ self,
104
+ ) -> AsyncGenerator[
105
+ Union[
106
+ Tuple[Stdout, None, None],
107
+ Tuple[None, Stderr, None],
108
+ Tuple[None, None, PtyOutput],
109
+ ],
110
+ None,
111
+ ]:
112
+ async for event in self._events:
113
+ if event.event.HasField("data"):
114
+ if event.event.data.stdout:
115
+ out = event.event.data.stdout.decode("utf-8", "replace")
116
+ self._stdout += out
117
+ yield out, None, None
118
+ if event.event.data.stderr:
119
+ out = event.event.data.stderr.decode("utf-8", "replace")
120
+ self._stderr += out
121
+ yield None, out, None
122
+ if event.event.data.pty:
123
+ yield None, None, event.event.data.pty
124
+ if event.event.HasField("end"):
125
+ self._result = CommandResult(
126
+ stdout=self._stdout,
127
+ stderr=self._stderr,
128
+ exit_code=event.event.end.exit_code,
129
+ error=event.event.end.error,
130
+ )
131
+
132
+ async def disconnect(self) -> None:
133
+ """
134
+ Disconnects from the command.
135
+
136
+ The command is not killed, but SDK stops receiving events from the command.
137
+ You can reconnect to the command using `sandbox.commands.connect` method.
138
+ """
139
+ self._wait.cancel()
140
+ # BUG: In Python 3.8 closing async generator can throw RuntimeError.
141
+ # await self._events.aclose()
142
+
143
+ async def _handle_events(self):
144
+ try:
145
+ async for stdout, stderr, pty in self._iterate_events():
146
+ if stdout is not None and self._on_stdout:
147
+ cb = self._on_stdout(stdout)
148
+ if inspect.isawaitable(cb):
149
+ await cb
150
+ elif stderr is not None and self._on_stderr:
151
+ cb = self._on_stderr(stderr)
152
+ if inspect.isawaitable(cb):
153
+ await cb
154
+ elif pty is not None and self._on_pty:
155
+ cb = self._on_pty(pty)
156
+ if inspect.isawaitable(cb):
157
+ await cb
158
+ except StopAsyncIteration:
159
+ pass
160
+ except Exception as e:
161
+ self._iteration_exception = handle_rpc_exception(e)
162
+
163
+ async def wait(self) -> CommandResult:
164
+ """
165
+ Wait for the command to finish and return the result.
166
+ If the command exits with a non-zero exit code, it throws a `CommandExitException`.
167
+
168
+ :return: `CommandResult` result of command execution
169
+ """
170
+ await self._wait
171
+ if self._iteration_exception:
172
+ raise self._iteration_exception
173
+
174
+ if self._result is None:
175
+ raise Exception("Command ended without an end event")
176
+
177
+ if self._result.exit_code != 0:
178
+ raise CommandExitException(
179
+ stdout=self._stdout,
180
+ stderr=self._stderr,
181
+ exit_code=self._result.exit_code,
182
+ error=self._result.error,
183
+ )
184
+
185
+ return self._result
186
+
187
+ async def kill(self) -> bool:
188
+ """
189
+ Kills the command.
190
+
191
+ It uses `SIGKILL` signal to kill the command
192
+
193
+ :return: `True` if the command was killed successfully, `False` if the command was not found
194
+ """
195
+ result = await self._handle_kill()
196
+ return result
@@ -0,0 +1,240 @@
1
+ from typing import Dict, Optional
2
+
3
+ import moru_connect
4
+ import httpcore
5
+
6
+ from packaging.version import Version
7
+ from moru.envd.process import process_connect, process_pb2
8
+ from moru.connection_config import (
9
+ Username,
10
+ ConnectionConfig,
11
+ KEEPALIVE_PING_HEADER,
12
+ KEEPALIVE_PING_INTERVAL_SEC,
13
+ )
14
+ from moru.exceptions import SandboxException
15
+ from moru.envd.rpc import authentication_header, handle_rpc_exception
16
+ from moru.sandbox.commands.command_handle import PtySize
17
+ from moru.sandbox_async.commands.command_handle import (
18
+ AsyncCommandHandle,
19
+ OutputHandler,
20
+ PtyOutput,
21
+ )
22
+
23
+
24
+ class Pty:
25
+ """
26
+ Module for interacting with PTYs (pseudo-terminals) in the sandbox.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ envd_api_url: str,
32
+ connection_config: ConnectionConfig,
33
+ pool: httpcore.AsyncConnectionPool,
34
+ envd_version: Version,
35
+ ) -> None:
36
+ self._connection_config = connection_config
37
+ self._envd_version = envd_version
38
+ self._rpc = process_connect.ProcessClient(
39
+ envd_api_url,
40
+ # TODO: Fix and enable compression again — the headers compression is not solved for streaming.
41
+ # compressor=moru_connect.GzipCompressor,
42
+ async_pool=pool,
43
+ json=True,
44
+ headers=connection_config.sandbox_headers,
45
+ )
46
+
47
+ async def kill(
48
+ self,
49
+ pid: int,
50
+ request_timeout: Optional[float] = None,
51
+ ) -> bool:
52
+ """
53
+ Kill PTY.
54
+
55
+ :param pid: Process ID of the PTY
56
+ :param request_timeout: Timeout for the request in **seconds**
57
+
58
+ :return: `true` if the PTY was killed, `false` if the PTY was not found
59
+ """
60
+ try:
61
+ await self._rpc.asend_signal(
62
+ process_pb2.SendSignalRequest(
63
+ process=process_pb2.ProcessSelector(pid=pid),
64
+ signal=process_pb2.Signal.SIGNAL_SIGKILL,
65
+ ),
66
+ request_timeout=self._connection_config.get_request_timeout(
67
+ request_timeout
68
+ ),
69
+ )
70
+ return True
71
+ except Exception as e:
72
+ if isinstance(e, moru_connect.ConnectException):
73
+ if e.status == moru_connect.Code.not_found:
74
+ return False
75
+ raise handle_rpc_exception(e)
76
+
77
+ async def send_stdin(
78
+ self,
79
+ pid: int,
80
+ data: bytes,
81
+ request_timeout: Optional[float] = None,
82
+ ) -> None:
83
+ """
84
+ Send input to a PTY.
85
+
86
+ :param pid: Process ID of the PTY
87
+ :param data: Input data to send
88
+ :param request_timeout: Timeout for the request in **seconds**
89
+ """
90
+ try:
91
+ await self._rpc.asend_input(
92
+ process_pb2.SendInputRequest(
93
+ process=process_pb2.ProcessSelector(pid=pid),
94
+ input=process_pb2.ProcessInput(
95
+ pty=data,
96
+ ),
97
+ ),
98
+ request_timeout=self._connection_config.get_request_timeout(
99
+ request_timeout
100
+ ),
101
+ )
102
+ except Exception as e:
103
+ raise handle_rpc_exception(e)
104
+
105
+ async def create(
106
+ self,
107
+ size: PtySize,
108
+ on_data: OutputHandler[PtyOutput],
109
+ user: Optional[Username] = None,
110
+ cwd: Optional[str] = None,
111
+ envs: Optional[Dict[str, str]] = None,
112
+ timeout: Optional[float] = 60,
113
+ request_timeout: Optional[float] = None,
114
+ ) -> AsyncCommandHandle:
115
+ """
116
+ Start a new PTY (pseudo-terminal).
117
+
118
+ :param size: Size of the PTY
119
+ :param on_data: Callback to handle PTY data
120
+ :param user: User to use for the PTY
121
+ :param cwd: Working directory for the PTY
122
+ :param envs: Environment variables for the PTY
123
+ :param timeout: Timeout for the PTY in **seconds**
124
+ :param request_timeout: Timeout for the request in **seconds**
125
+
126
+ :return: Handle to interact with the PTY
127
+ """
128
+ envs = envs or {}
129
+ envs["TERM"] = "xterm-256color"
130
+ events = self._rpc.astart(
131
+ process_pb2.StartRequest(
132
+ process=process_pb2.ProcessConfig(
133
+ cmd="/bin/bash",
134
+ envs=envs,
135
+ args=["-i", "-l"],
136
+ cwd=cwd,
137
+ ),
138
+ pty=process_pb2.PTY(
139
+ size=process_pb2.PTY.Size(rows=size.rows, cols=size.cols)
140
+ ),
141
+ ),
142
+ headers={
143
+ **authentication_header(self._envd_version, user),
144
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
145
+ },
146
+ timeout=timeout,
147
+ request_timeout=self._connection_config.get_request_timeout(
148
+ request_timeout
149
+ ),
150
+ )
151
+
152
+ try:
153
+ start_event = await events.__anext__()
154
+
155
+ if not start_event.HasField("event"):
156
+ raise SandboxException(
157
+ f"Failed to start process: expected start event, got {start_event}"
158
+ )
159
+
160
+ return AsyncCommandHandle(
161
+ pid=start_event.event.start.pid,
162
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
163
+ events=events,
164
+ on_pty=on_data,
165
+ )
166
+ except Exception as e:
167
+ raise handle_rpc_exception(e)
168
+
169
+ async def connect(
170
+ self,
171
+ pid: int,
172
+ on_data: OutputHandler[PtyOutput],
173
+ timeout: Optional[float] = 60,
174
+ request_timeout: Optional[float] = None,
175
+ ) -> AsyncCommandHandle:
176
+ """
177
+ Connect to a running PTY.
178
+
179
+ :param pid: Process ID of the PTY to connect to. You can get the list of running PTYs using `sandbox.pty.list()`.
180
+ :param on_data: Callback to handle PTY data
181
+ :param timeout: Timeout for the PTY connection in **seconds**. Using `0` will not limit the connection time
182
+ :param request_timeout: Timeout for the request in **seconds**
183
+
184
+ :return: Handle to interact with the PTY
185
+ """
186
+ events = self._rpc.aconnect(
187
+ process_pb2.ConnectRequest(
188
+ process=process_pb2.ProcessSelector(pid=pid),
189
+ ),
190
+ timeout=timeout,
191
+ request_timeout=self._connection_config.get_request_timeout(
192
+ request_timeout
193
+ ),
194
+ headers={
195
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
196
+ },
197
+ )
198
+
199
+ try:
200
+ start_event = await events.__anext__()
201
+
202
+ if not start_event.HasField("event"):
203
+ raise SandboxException(
204
+ f"Failed to connect to process: expected start event, got {start_event}"
205
+ )
206
+
207
+ return AsyncCommandHandle(
208
+ pid=start_event.event.start.pid,
209
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
210
+ events=events,
211
+ on_pty=on_data,
212
+ )
213
+ except Exception as e:
214
+ raise handle_rpc_exception(e)
215
+
216
+ async def resize(
217
+ self,
218
+ pid: int,
219
+ size: PtySize,
220
+ request_timeout: Optional[float] = None,
221
+ ):
222
+ """
223
+ Resize PTY.
224
+ Call this when the terminal window is resized and the number of columns and rows has changed.
225
+
226
+ :param pid: Process ID of the PTY
227
+ :param size: New size of the PTY
228
+ :param request_timeout: Timeout for the request in **seconds**
229
+ """
230
+ await self._rpc.aupdate(
231
+ process_pb2.UpdateRequest(
232
+ process=process_pb2.ProcessSelector(pid=pid),
233
+ pty=process_pb2.PTY(
234
+ size=process_pb2.PTY.Size(rows=size.rows, cols=size.cols),
235
+ ),
236
+ ),
237
+ request_timeout=self._connection_config.get_request_timeout(
238
+ request_timeout
239
+ ),
240
+ )