scalebox-sdk 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 (157) hide show
  1. scalebox/__init__.py +80 -0
  2. scalebox/api/__init__.py +128 -0
  3. scalebox/api/client/__init__.py +8 -0
  4. scalebox/api/client/api/__init__.py +1 -0
  5. scalebox/api/client/api/sandboxes/__init__.py +0 -0
  6. scalebox/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. scalebox/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. scalebox/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +214 -0
  12. scalebox/api/client/api/sandboxes/get_v2_sandboxes.py +229 -0
  13. scalebox/api/client/api/sandboxes/post_sandboxes.py +174 -0
  14. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
  15. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +182 -0
  16. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +190 -0
  17. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +194 -0
  18. scalebox/api/client/client.py +288 -0
  19. scalebox/api/client/errors.py +16 -0
  20. scalebox/api/client/models/__init__.py +81 -0
  21. scalebox/api/client/models/build_log_entry.py +79 -0
  22. scalebox/api/client/models/created_access_token.py +100 -0
  23. scalebox/api/client/models/created_team_api_key.py +166 -0
  24. scalebox/api/client/models/error.py +67 -0
  25. scalebox/api/client/models/identifier_masking_details.py +83 -0
  26. scalebox/api/client/models/listed_sandbox.py +138 -0
  27. scalebox/api/client/models/log_level.py +11 -0
  28. scalebox/api/client/models/new_access_token.py +59 -0
  29. scalebox/api/client/models/new_sandbox.py +125 -0
  30. scalebox/api/client/models/new_team_api_key.py +59 -0
  31. scalebox/api/client/models/node.py +154 -0
  32. scalebox/api/client/models/node_detail.py +152 -0
  33. scalebox/api/client/models/node_status.py +11 -0
  34. scalebox/api/client/models/node_status_change.py +61 -0
  35. scalebox/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  36. scalebox/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  37. scalebox/api/client/models/resumed_sandbox.py +68 -0
  38. scalebox/api/client/models/sandbox.py +125 -0
  39. scalebox/api/client/models/sandbox_detail.py +178 -0
  40. scalebox/api/client/models/sandbox_log.py +70 -0
  41. scalebox/api/client/models/sandbox_logs.py +73 -0
  42. scalebox/api/client/models/sandbox_metric.py +110 -0
  43. scalebox/api/client/models/sandbox_state.py +9 -0
  44. scalebox/api/client/models/sandboxes_with_metrics.py +59 -0
  45. scalebox/api/client/models/team.py +83 -0
  46. scalebox/api/client/models/team_api_key.py +158 -0
  47. scalebox/api/client/models/team_user.py +68 -0
  48. scalebox/api/client/models/template.py +179 -0
  49. scalebox/api/client/models/template_build.py +117 -0
  50. scalebox/api/client/models/template_build_file_upload.py +70 -0
  51. scalebox/api/client/models/template_build_request.py +115 -0
  52. scalebox/api/client/models/template_build_request_v2.py +88 -0
  53. scalebox/api/client/models/template_build_start_v2.py +114 -0
  54. scalebox/api/client/models/template_build_status.py +11 -0
  55. scalebox/api/client/models/template_step.py +91 -0
  56. scalebox/api/client/models/template_update_request.py +59 -0
  57. scalebox/api/client/models/update_team_api_key.py +59 -0
  58. scalebox/api/client/py.typed +1 -0
  59. scalebox/api/client/types.py +46 -0
  60. scalebox/api/metadata.py +19 -0
  61. scalebox/cli.py +125 -0
  62. scalebox/client/__init__.py +0 -0
  63. scalebox/client/aclient.py +57 -0
  64. scalebox/client/api.proto +460 -0
  65. scalebox/client/buf.gen.yaml +8 -0
  66. scalebox/client/client.py +102 -0
  67. scalebox/client/requirements.txt +5 -0
  68. scalebox/code_interpreter/__init__.py +12 -0
  69. scalebox/code_interpreter/charts.py +230 -0
  70. scalebox/code_interpreter/code_interpreter_async.py +369 -0
  71. scalebox/code_interpreter/code_interpreter_sync.py +317 -0
  72. scalebox/code_interpreter/constants.py +3 -0
  73. scalebox/code_interpreter/exceptions.py +13 -0
  74. scalebox/code_interpreter/models.py +485 -0
  75. scalebox/connection_config.py +92 -0
  76. scalebox/csx_connect/__init__.py +1 -0
  77. scalebox/csx_connect/client.py +485 -0
  78. scalebox/csx_desktop/__init__.py +0 -0
  79. scalebox/csx_desktop/main.py +651 -0
  80. scalebox/exceptions.py +83 -0
  81. scalebox/generated/__init__.py +0 -0
  82. scalebox/generated/api.py +61 -0
  83. scalebox/generated/api_pb2.py +203 -0
  84. scalebox/generated/api_pb2.pyi +956 -0
  85. scalebox/generated/api_pb2_connect.py +1456 -0
  86. scalebox/generated/rpc.py +50 -0
  87. scalebox/generated/versions.py +3 -0
  88. scalebox/requirements.txt +36 -0
  89. scalebox/sandbox/__init__.py +0 -0
  90. scalebox/sandbox/commands/__init__.py +0 -0
  91. scalebox/sandbox/commands/command_handle.py +69 -0
  92. scalebox/sandbox/commands/main.py +39 -0
  93. scalebox/sandbox/filesystem/__init__.py +0 -0
  94. scalebox/sandbox/filesystem/filesystem.py +95 -0
  95. scalebox/sandbox/filesystem/watch_handle.py +60 -0
  96. scalebox/sandbox/main.py +139 -0
  97. scalebox/sandbox/sandbox_api.py +91 -0
  98. scalebox/sandbox/signature.py +40 -0
  99. scalebox/sandbox/utils.py +34 -0
  100. scalebox/sandbox_async/__init__.py +1 -0
  101. scalebox/sandbox_async/commands/command.py +307 -0
  102. scalebox/sandbox_async/commands/command_handle.py +187 -0
  103. scalebox/sandbox_async/commands/pty.py +187 -0
  104. scalebox/sandbox_async/filesystem/filesystem.py +557 -0
  105. scalebox/sandbox_async/filesystem/watch_handle.py +61 -0
  106. scalebox/sandbox_async/main.py +646 -0
  107. scalebox/sandbox_async/sandbox_api.py +365 -0
  108. scalebox/sandbox_async/utils.py +7 -0
  109. scalebox/sandbox_sync/__init__.py +2 -0
  110. scalebox/sandbox_sync/commands/__init__.py +0 -0
  111. scalebox/sandbox_sync/commands/command.py +300 -0
  112. scalebox/sandbox_sync/commands/command_handle.py +150 -0
  113. scalebox/sandbox_sync/commands/pty.py +181 -0
  114. scalebox/sandbox_sync/filesystem/__init__.py +0 -0
  115. scalebox/sandbox_sync/filesystem/filesystem.py +543 -0
  116. scalebox/sandbox_sync/filesystem/watch_handle.py +66 -0
  117. scalebox/sandbox_sync/main.py +790 -0
  118. scalebox/sandbox_sync/sandbox_api.py +356 -0
  119. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -0
  120. scalebox/test/README.md +329 -0
  121. scalebox/test/__init__.py +0 -0
  122. scalebox/test/aclient.py +72 -0
  123. scalebox/test/code_interpreter_centext.py +21 -0
  124. scalebox/test/code_interpreter_centext_sync.py +21 -0
  125. scalebox/test/code_interpreter_test.py +34 -0
  126. scalebox/test/code_interpreter_test_sync.py +34 -0
  127. scalebox/test/run_all_validation_tests.py +334 -0
  128. scalebox/test/run_code_interpreter_tests.sh +67 -0
  129. scalebox/test/run_tests.sh +230 -0
  130. scalebox/test/test_basic.py +78 -0
  131. scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -0
  132. scalebox/test/test_code_interpreter_e2basync_comprehensive.py +2655 -0
  133. scalebox/test/test_code_interpreter_e2bsync_comprehensive.py +3416 -0
  134. scalebox/test/test_code_interpreter_sync_comprehensive.py +3412 -0
  135. scalebox/test/test_e2b_first.py +11 -0
  136. scalebox/test/test_sandbox_async_comprehensive.py +738 -0
  137. scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -0
  138. scalebox/test/test_sandbox_sync_comprehensive.py +770 -0
  139. scalebox/test/test_sandbox_usage_examples.py +987 -0
  140. scalebox/test/testacreate.py +24 -0
  141. scalebox/test/testagetinfo.py +18 -0
  142. scalebox/test/testcodeinterpreter_async.py +508 -0
  143. scalebox/test/testcodeinterpreter_sync.py +239 -0
  144. scalebox/test/testcomputeuse.py +243 -0
  145. scalebox/test/testnovnc.py +12 -0
  146. scalebox/test/testsandbox_async.py +118 -0
  147. scalebox/test/testsandbox_sync.py +38 -0
  148. scalebox/utils/__init__.py +0 -0
  149. scalebox/utils/httpcoreclient.py +297 -0
  150. scalebox/utils/httpxclient.py +403 -0
  151. scalebox/version.py +16 -0
  152. scalebox_sdk-0.1.0.dist-info/METADATA +292 -0
  153. scalebox_sdk-0.1.0.dist-info/RECORD +157 -0
  154. scalebox_sdk-0.1.0.dist-info/WHEEL +5 -0
  155. scalebox_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  156. scalebox_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  157. scalebox_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,307 @@
1
+ from typing import Dict, List, Literal, Optional, Union, overload
2
+
3
+ import aiohttp
4
+
5
+ from ... import csx_connect
6
+ from ...connection_config import (
7
+ KEEPALIVE_PING_HEADER,
8
+ KEEPALIVE_PING_INTERVAL_SEC,
9
+ ConnectionConfig,
10
+ Username,
11
+ )
12
+ from ...exceptions import SandboxException
13
+ from ...generated import api_pb2, api_pb2_connect
14
+ from ...generated.rpc import handle_rpc_exception
15
+ from ...sandbox.commands.command_handle import CommandResult
16
+ from ...sandbox.commands.main import ProcessInfo
17
+ from ...sandbox_async.commands.command_handle import AsyncCommandHandle, Stderr, Stdout
18
+ from ...sandbox_async.utils import OutputHandler
19
+
20
+
21
+ class Commands:
22
+ """
23
+ Module for executing commands in the sandbox.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ envd_api_url: str,
29
+ connection_config: ConnectionConfig,
30
+ pool: aiohttp.ClientSession,
31
+ ) -> None:
32
+ self._connection_config = connection_config
33
+ self._rpc = api_pb2_connect.AsyncProcessClient(
34
+ envd_api_url,
35
+ http_client=pool
36
+ )
37
+ self._headers=connection_config.headers
38
+ self._pool = pool
39
+
40
+ async def list(
41
+ self,
42
+ request_timeout: Optional[float] = None,
43
+ ) -> List[ProcessInfo]:
44
+ """
45
+ Lists all running commands and PTY sessions.
46
+
47
+ :param request_timeout: Timeout for the request in **seconds**
48
+
49
+ :return: List of running commands and PTY sessions
50
+ """
51
+ try:
52
+ res = await self._rpc.list(
53
+ api_pb2.ListRequest(),
54
+ self._headers,
55
+ timeout_seconds=self._connection_config.get_request_timeout(
56
+ request_timeout
57
+ ),
58
+ )
59
+ return [
60
+ ProcessInfo(
61
+ pid=p.pid,
62
+ tag=p.tag,
63
+ cmd=p.config.cmd,
64
+ args=list(p.config.args),
65
+ envs=dict(p.config.envs),
66
+ cwd=p.config.cwd,
67
+ )
68
+ for p in res.processes
69
+ ]
70
+ except Exception as e:
71
+ raise handle_rpc_exception(e)
72
+
73
+ async def kill(
74
+ self,
75
+ pid: int,
76
+ request_timeout: Optional[float] = None,
77
+ ) -> bool:
78
+ """
79
+ Kill a running command specified by its process ID.
80
+ It uses `SIGKILL` signal to kill the command.
81
+
82
+ :param pid: Process ID of the command. You can get the list of processes using `sandbox.commands.list()`
83
+ :param request_timeout: Timeout for the request in **seconds**
84
+
85
+ :return: `True` if the command was killed, `False` if the command was not found
86
+ """
87
+ try:
88
+ await self._rpc.send_signal(
89
+ api_pb2.SendSignalRequest(
90
+ process=api_pb2.ProcessSelector(pid=pid),
91
+ signal=api_pb2.Signal.SIGNAL_SIGKILL,
92
+ ),
93
+ self._headers,
94
+ timeout_seconds=self._connection_config.get_request_timeout(
95
+ request_timeout
96
+ ),
97
+ )
98
+ return True
99
+ except Exception as e:
100
+ if "not found" in str(e):
101
+ return False
102
+ raise handle_rpc_exception(e)
103
+
104
+ async def send_stdin(
105
+ self,
106
+ pid: int,
107
+ data: str,
108
+ request_timeout: Optional[float] = None,
109
+ ) -> None:
110
+ """
111
+ Send data to command stdin.
112
+
113
+ :param pid Process ID of the command. You can get the list of processes using `sandbox.commands.list()`.
114
+ :param data: Data to send to the command
115
+ :param request_timeout: Timeout for the request in **seconds**
116
+ """
117
+ try:
118
+ await self._rpc.send_input(
119
+ api_pb2.SendInputRequest(
120
+ process=api_pb2.ProcessSelector(pid=pid),
121
+ input=api_pb2.ProcessInput(
122
+ stdin=data.encode(),
123
+ ),
124
+ ),
125
+ self._headers,
126
+ timeout_seconds=self._connection_config.get_request_timeout(
127
+ request_timeout
128
+ ),
129
+ )
130
+ except Exception as e:
131
+ print(e)
132
+ raise handle_rpc_exception(e)
133
+
134
+ @overload
135
+ async def run(
136
+ self,
137
+ cmd: str,
138
+ background: Union[Literal[False], None] = None,
139
+ envs: Optional[Dict[str, str]] = None,
140
+ user: Username = "user",
141
+ cwd: Optional[str] = None,
142
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
143
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
144
+ timeout: Optional[float] = 60,
145
+ request_timeout: Optional[float] = None,
146
+ ) -> CommandResult:
147
+ """
148
+ Start a new command and wait until it finishes executing.
149
+
150
+ :param cmd: Command to execute
151
+ :param background: **`False` if the command should be executed in the foreground**, `True` if the command should be executed in the background
152
+ :param envs: Environment variables used for the command
153
+ :param user: User to run the command as
154
+ :param cwd: Working directory to run the command
155
+ :param on_stdout: Callback for command stdout output
156
+ :param on_stderr: Callback for command stderr output
157
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
158
+ :param request_timeout: Timeout for the request in **seconds**
159
+
160
+ :return: `CommandResult` result of the command execution
161
+ """
162
+ ...
163
+
164
+ @overload
165
+ async def run(
166
+ self,
167
+ cmd: str,
168
+ background: Literal[True],
169
+ envs: Optional[Dict[str, str]] = None,
170
+ user: Username = "user",
171
+ cwd: Optional[str] = None,
172
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
173
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
174
+ timeout: Optional[float] = 60,
175
+ request_timeout: Optional[float] = None,
176
+ ) -> AsyncCommandHandle:
177
+ """
178
+ Start a new command and return a handle to interact with it.
179
+
180
+ :param cmd: Command to execute
181
+ :param background: `False` if the command should be executed in the foreground, **`True` if the command should be executed in the background**
182
+ :param envs: Environment variables used for the command
183
+ :param user: User to run the command as
184
+ :param cwd: Working directory to run the command
185
+ :param on_stdout: Callback for command stdout output
186
+ :param on_stderr: Callback for command stderr output
187
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
188
+ :param request_timeout: Timeout for the request in **seconds**
189
+
190
+ :return: `AsyncCommandHandle` handle to interact with the running command
191
+ """
192
+ ...
193
+
194
+ async def run(
195
+ self,
196
+ cmd: str,
197
+ background: Union[bool, None] = None,
198
+ envs: Optional[Dict[str, str]] = None,
199
+ user: Username = "user",
200
+ cwd: Optional[str] = None,
201
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
202
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
203
+ timeout: Optional[float] = 60,
204
+ request_timeout: Optional[float] = None,
205
+ ):
206
+ proc = await self._start(
207
+ cmd,
208
+ envs,
209
+ user,
210
+ cwd,
211
+ timeout,
212
+ request_timeout,
213
+ on_stdout=on_stdout,
214
+ on_stderr=on_stderr,
215
+ )
216
+ return proc if background else await proc.wait()
217
+
218
+ async def _start(
219
+ self,
220
+ cmd: str,
221
+ envs: Optional[Dict[str, str]] = None,
222
+ user: Username = "user",
223
+ cwd: Optional[str] = None,
224
+ timeout: Optional[float] = 60,
225
+ request_timeout: Optional[float] = None,
226
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
227
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
228
+ ) -> AsyncCommandHandle:
229
+ try:
230
+ events = self._rpc.start(
231
+ api_pb2.StartRequest(
232
+ process=api_pb2.ProcessConfig(
233
+ cmd="/bin/bash",
234
+ envs=envs,
235
+ args=["-l", "-c", cmd],
236
+ cwd=cwd,
237
+ ),
238
+ ),
239
+ self._headers,
240
+ timeout_seconds=self._connection_config.get_request_timeout(
241
+ request_timeout
242
+ ),
243
+ )
244
+ start_event = await events.__anext__()
245
+
246
+ if not start_event.HasField("event"):
247
+ raise SandboxException(
248
+ f"Failed to start process: expected start event, got {start_event}"
249
+ )
250
+
251
+ return AsyncCommandHandle(
252
+ pid=start_event.event.start.pid,
253
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
254
+ events=events,
255
+ on_stdout=on_stdout,
256
+ on_stderr=on_stderr,
257
+ )
258
+ except Exception as e:
259
+ raise handle_rpc_exception(e)
260
+
261
+ async def connect(
262
+ self,
263
+ pid: int,
264
+ timeout: Optional[float] = 60,
265
+ request_timeout: Optional[float] = None,
266
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
267
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
268
+ ) -> AsyncCommandHandle:
269
+ """
270
+ Connects to a running command.
271
+ You can use `AsyncCommandHandle.wait()` to wait for the command to finish and get execution results.
272
+
273
+ :param pid: Process ID of the command to connect to. You can get the list of processes using `sandbox.commands.list()`
274
+ :param request_timeout: Request timeout in **seconds**
275
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
276
+ :param on_stdout: Callback for command stdout output
277
+ :param on_stderr: Callback for command stderr output
278
+
279
+ :return: `AsyncCommandHandle` handle to interact with the running command
280
+ """
281
+ events = self._rpc.connect(
282
+ api_pb2.ConnectRequest(
283
+ process=api_pb2.ProcessSelector(pid=pid),
284
+ ),
285
+ self._headers,
286
+ timeout_seconds=self._connection_config.get_request_timeout(
287
+ request_timeout
288
+ ),
289
+ )
290
+
291
+ try:
292
+ start_event = await events.__anext__()
293
+
294
+ if not start_event.HasField("event"):
295
+ raise SandboxException(
296
+ f"Failed to connect to process: expected start event, got {start_event}"
297
+ )
298
+
299
+ return AsyncCommandHandle(
300
+ pid=start_event.event.start.pid,
301
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
302
+ events=events,
303
+ on_stdout=on_stdout,
304
+ on_stderr=on_stderr,
305
+ )
306
+ except Exception as e:
307
+ raise handle_rpc_exception(e)
@@ -0,0 +1,187 @@
1
+ import asyncio
2
+ import inspect
3
+ from typing import Any, AsyncIterator, Callable, Coroutine, Optional, Tuple, Union
4
+
5
+ from ...generated import api_pb2
6
+ from ...generated.rpc import handle_rpc_exception
7
+ from ...sandbox.commands.command_handle import (
8
+ CommandExitException,
9
+ CommandResult,
10
+ PtyOutput,
11
+ Stderr,
12
+ Stdout,
13
+ )
14
+ from ...sandbox_async.utils import OutputHandler
15
+
16
+
17
+ class AsyncCommandHandle:
18
+ """
19
+ Command execution handle.
20
+
21
+ It provides methods for waiting for the command to finish, retrieving stdout/stderr, and killing the command.
22
+ """
23
+
24
+ @property
25
+ def pid(self):
26
+ """
27
+ Command process ID.
28
+ """
29
+ return self._pid
30
+
31
+ @property
32
+ def stdout(self):
33
+ """
34
+ Command stdout output.
35
+ """
36
+ return self._stdout
37
+
38
+ @property
39
+ def stderr(self):
40
+ """
41
+ Command stderr output.
42
+ """
43
+ return self._stderr
44
+
45
+ @property
46
+ def error(self):
47
+ """
48
+ Command execution error message.
49
+ """
50
+ if self._result is None:
51
+ return None
52
+ return self._result.error
53
+
54
+ @property
55
+ def exit_code(self):
56
+ """
57
+ Command execution exit code.
58
+
59
+ `0` if the command finished successfully.
60
+
61
+ It is `None` if the command is still running.
62
+ """
63
+ if self._result is None:
64
+ return None
65
+ return self._result.exit_code
66
+
67
+ def __init__(
68
+ self,
69
+ pid: int,
70
+ handle_kill: Callable[[], Coroutine[Any, Any, bool]],
71
+ events: AsyncIterator[
72
+ Union[api_pb2.StartResponse, api_pb2.ConnectResponse]
73
+ ],
74
+ on_stdout: Optional[OutputHandler[Stdout]] = None,
75
+ on_stderr: Optional[OutputHandler[Stderr]] = None,
76
+ on_pty: Optional[OutputHandler[PtyOutput]] = None,
77
+ ):
78
+ self._pid = pid
79
+ self._handle_kill = handle_kill
80
+ self._events = events
81
+
82
+ self._stdout: str = ""
83
+ self._stderr: str = ""
84
+
85
+ self._on_stdout = on_stdout
86
+ self._on_stderr = on_stderr
87
+ self._on_pty = on_pty
88
+
89
+ self._result: Optional[CommandResult] = None
90
+ self._iteration_exception: Optional[Exception] = None
91
+
92
+ self._wait = asyncio.create_task(self._handle_events())
93
+
94
+ async def _iterate_events(
95
+ self,
96
+ ) -> AsyncIterator[
97
+ Union[
98
+ Tuple[Stdout, None, None],
99
+ Tuple[None, Stderr, None],
100
+ Tuple[None, None, PtyOutput],
101
+ ],
102
+ ]:
103
+ async for event in self._events:
104
+ if event.event.HasField("data"):
105
+ if event.event.data.stdout:
106
+ out = event.event.data.stdout.decode("utf-8", "replace")
107
+ self._stdout += out
108
+ yield out, None, None
109
+ if event.event.data.stderr:
110
+ out = event.event.data.stderr.decode("utf-8", "replace")
111
+ self._stderr += out
112
+ yield None, out, None
113
+ if event.event.data.pty:
114
+ yield None, None, event.event.data.pty
115
+ if event.event.HasField("end"):
116
+ self._result = CommandResult(
117
+ stdout=self._stdout,
118
+ stderr=self._stderr,
119
+ exit_code=event.event.end.exit_code,
120
+ error=event.event.end.error,
121
+ )
122
+
123
+ async def disconnect(self) -> None:
124
+ """
125
+ Disconnects from the command.
126
+
127
+ The command is not killed, but SDK stops receiving events from the command.
128
+ You can reconnect to the command using `sandbox.commands.connect` method.
129
+ """
130
+ self._wait.cancel()
131
+ # BUG: In Python 3.8 closing async generator can throw RuntimeError.
132
+ # await self._events.aclose()
133
+
134
+ async def _handle_events(self):
135
+ try:
136
+ async for stdout, stderr, pty in self._iterate_events():
137
+ if stdout is not None and self._on_stdout:
138
+ cb = self._on_stdout(stdout)
139
+ if inspect.isawaitable(cb):
140
+ await cb
141
+ elif stderr is not None and self._on_stderr:
142
+ cb = self._on_stderr(stderr)
143
+ if inspect.isawaitable(cb):
144
+ await cb
145
+ elif pty is not None and self._on_pty:
146
+ cb = self._on_pty(pty)
147
+ if inspect.isawaitable(cb):
148
+ await cb
149
+ except StopAsyncIteration:
150
+ pass
151
+ except Exception as e:
152
+ self._iteration_exception = handle_rpc_exception(e)
153
+
154
+ async def wait(self) -> CommandResult:
155
+ """
156
+ Wait for the command to finish and return the result.
157
+ If the command exits with a non-zero exit code, it throws a `CommandExitException`.
158
+
159
+ :return: `CommandResult` result of command execution
160
+ """
161
+ await self._wait
162
+ if self._iteration_exception:
163
+ raise self._iteration_exception
164
+
165
+ if self._result is None:
166
+ raise Exception("Command ended without an end event")
167
+
168
+ if self._result.exit_code != 0:
169
+ raise CommandExitException(
170
+ stdout=self._stdout,
171
+ stderr=self._stderr,
172
+ exit_code=self._result.exit_code,
173
+ error=self._result.error,
174
+ )
175
+
176
+ return self._result
177
+
178
+ async def kill(self) -> bool:
179
+ """
180
+ Kills the command.
181
+
182
+ It uses `SIGKILL` signal to kill the command
183
+
184
+ :return: `True` if the command was killed successfully, `False` if the command was not found
185
+ """
186
+ result = await self._handle_kill()
187
+ return result
@@ -0,0 +1,187 @@
1
+ from typing import Dict, Optional
2
+
3
+ import aiohttp
4
+
5
+ from ... import csx_connect
6
+ from ...connection_config import (
7
+ KEEPALIVE_PING_HEADER,
8
+ KEEPALIVE_PING_INTERVAL_SEC,
9
+ ConnectionConfig,
10
+ Username,
11
+ )
12
+ from ...exceptions import SandboxException
13
+ from ...generated import api_pb2, api_pb2_connect
14
+ from ...generated.rpc import authentication_header, handle_rpc_exception
15
+ from ...sandbox.commands.command_handle import PtySize
16
+ from ...sandbox_async.commands.command_handle import (
17
+ AsyncCommandHandle,
18
+ OutputHandler,
19
+ PtyOutput,
20
+ )
21
+
22
+
23
+ class Pty:
24
+ """
25
+ Module for interacting with PTYs (pseudo-terminals) in the sandbox.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ envd_api_url: str,
31
+ connection_config: ConnectionConfig,
32
+ pool: aiohttp.ClientSession,
33
+ ) -> None:
34
+ self._connection_config = connection_config
35
+ self._rpc = api_pb2_connect.AsyncProcessClient(
36
+ envd_api_url,
37
+ pool
38
+ )
39
+ self._headers = connection_config.headers
40
+ self._pool = pool
41
+
42
+ async def kill(
43
+ self,
44
+ pid: int,
45
+ request_timeout: Optional[float] = None,
46
+ ) -> bool:
47
+ """
48
+ Kill PTY.
49
+
50
+ :param pid: Process ID of the PTY
51
+ :param request_timeout: Timeout for the request in **seconds**
52
+
53
+ :return: `true` if the PTY was killed, `false` if the PTY was not found
54
+ """
55
+ try:
56
+ await self._rpc.send_signal(
57
+ api_pb2.SendSignalRequest(
58
+ process=api_pb2.ProcessSelector(pid=pid),
59
+ signal=api_pb2.Signal.SIGNAL_SIGKILL,
60
+ ),
61
+ self._headers,
62
+ timeout_seconds=self._connection_config.get_request_timeout(
63
+ request_timeout
64
+ ),
65
+ )
66
+ return True
67
+ except Exception as e:
68
+ if isinstance(e, csx_connect.ConnectException):
69
+ if e.status == csx_connect.Code.not_found:
70
+ return False
71
+ raise handle_rpc_exception(e)
72
+
73
+ async def send_stdin(
74
+ self,
75
+ pid: int,
76
+ data: bytes,
77
+ request_timeout: Optional[float] = None,
78
+ ) -> None:
79
+ """
80
+ Send input to a PTY.
81
+
82
+ :param pid: Process ID of the PTY
83
+ :param data: Input data to send
84
+ :param request_timeout: Timeout for the request in **seconds**
85
+ """
86
+ try:
87
+ await self._rpc.send_input(
88
+ api_pb2.SendInputRequest(
89
+ process=api_pb2.ProcessSelector(pid=pid),
90
+ input=api_pb2.ProcessInput(
91
+ pty=data,
92
+ ),
93
+ ),
94
+ self._headers,
95
+ timeout_seconds=self._connection_config.get_request_timeout(
96
+ request_timeout
97
+ ),
98
+ )
99
+ except Exception as e:
100
+ raise handle_rpc_exception(e)
101
+
102
+ async def create(
103
+ self,
104
+ size: PtySize,
105
+ on_data: OutputHandler[PtyOutput],
106
+ user: Username = "user",
107
+ cwd: Optional[str] = None,
108
+ envs: Optional[Dict[str, str]] = None,
109
+ timeout: Optional[float] = 60,
110
+ request_timeout: Optional[float] = None,
111
+ ) -> AsyncCommandHandle:
112
+ """
113
+ Start a new PTY (pseudo-terminal).
114
+
115
+ :param size: Size of the PTY
116
+ :param on_data: Callback to handle PTY data
117
+ :param user: User to use for the PTY
118
+ :param cwd: Working directory for the PTY
119
+ :param envs: Environment variables for the PTY
120
+ :param timeout: Timeout for the PTY in **seconds**
121
+ :param request_timeout: Timeout for the request in **seconds**
122
+
123
+ :return: Handle to interact with the PTY
124
+ """
125
+ envs = envs or {}
126
+ envs["TERM"] = "xterm-256color"
127
+ events = self._rpc.start(
128
+ api_pb2.StartRequest(
129
+ process=api_pb2.ProcessConfig(
130
+ cmd="/bin/bash",
131
+ envs=envs,
132
+ args=["-i", "-l"],
133
+ cwd=cwd,
134
+ ),
135
+ pty=api_pb2.PTY(
136
+ size=api_pb2.PTY.Size(rows=size.rows, cols=size.cols)
137
+ ),
138
+ ),
139
+ self._headers,
140
+ timeout_seconds=self._connection_config.get_request_timeout(
141
+ request_timeout
142
+ ),
143
+ )
144
+
145
+ try:
146
+ start_event = await events.__anext__()
147
+
148
+ if not start_event.HasField("event"):
149
+ raise SandboxException(
150
+ f"Failed to start process: expected start event, got {start_event}"
151
+ )
152
+
153
+ return AsyncCommandHandle(
154
+ pid=start_event.event.start.pid,
155
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
156
+ events=events,
157
+ on_pty=on_data,
158
+ )
159
+ except Exception as e:
160
+ raise handle_rpc_exception(e)
161
+
162
+ async def resize(
163
+ self,
164
+ pid: int,
165
+ size: PtySize,
166
+ request_timeout: Optional[float] = None,
167
+ ):
168
+ """
169
+ Resize PTY.
170
+ Call this when the terminal window is resized and the number of columns and rows has changed.
171
+
172
+ :param pid: Process ID of the PTY
173
+ :param size: New size of the PTY
174
+ :param request_timeout: Timeout for the request in **seconds**
175
+ """
176
+ await self._rpc.update(
177
+ api_pb2.UpdateRequest(
178
+ process=api_pb2.ProcessSelector(pid=pid),
179
+ pty=api_pb2.PTY(
180
+ size=api_pb2.PTY.Size(rows=size.rows, cols=size.cols),
181
+ ),
182
+ ),
183
+ self._headers,
184
+ timeout_seconds=self._connection_config.get_request_timeout(
185
+ request_timeout
186
+ ),
187
+ )