indent 0.1.26__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.
- exponent/__init__.py +34 -0
- exponent/cli.py +110 -0
- exponent/commands/cloud_commands.py +585 -0
- exponent/commands/common.py +411 -0
- exponent/commands/config_commands.py +334 -0
- exponent/commands/run_commands.py +222 -0
- exponent/commands/settings.py +56 -0
- exponent/commands/types.py +111 -0
- exponent/commands/upgrade.py +29 -0
- exponent/commands/utils.py +146 -0
- exponent/core/config.py +180 -0
- exponent/core/graphql/__init__.py +0 -0
- exponent/core/graphql/client.py +61 -0
- exponent/core/graphql/get_chats_query.py +47 -0
- exponent/core/graphql/mutations.py +160 -0
- exponent/core/graphql/queries.py +146 -0
- exponent/core/graphql/subscriptions.py +16 -0
- exponent/core/remote_execution/checkpoints.py +212 -0
- exponent/core/remote_execution/cli_rpc_types.py +499 -0
- exponent/core/remote_execution/client.py +999 -0
- exponent/core/remote_execution/code_execution.py +77 -0
- exponent/core/remote_execution/default_env.py +31 -0
- exponent/core/remote_execution/error_info.py +45 -0
- exponent/core/remote_execution/exceptions.py +10 -0
- exponent/core/remote_execution/file_write.py +35 -0
- exponent/core/remote_execution/files.py +330 -0
- exponent/core/remote_execution/git.py +268 -0
- exponent/core/remote_execution/http_fetch.py +94 -0
- exponent/core/remote_execution/languages/python_execution.py +239 -0
- exponent/core/remote_execution/languages/shell_streaming.py +226 -0
- exponent/core/remote_execution/languages/types.py +20 -0
- exponent/core/remote_execution/port_utils.py +73 -0
- exponent/core/remote_execution/session.py +128 -0
- exponent/core/remote_execution/system_context.py +26 -0
- exponent/core/remote_execution/terminal_session.py +375 -0
- exponent/core/remote_execution/terminal_types.py +29 -0
- exponent/core/remote_execution/tool_execution.py +595 -0
- exponent/core/remote_execution/tool_type_utils.py +39 -0
- exponent/core/remote_execution/truncation.py +296 -0
- exponent/core/remote_execution/types.py +635 -0
- exponent/core/remote_execution/utils.py +477 -0
- exponent/core/types/__init__.py +0 -0
- exponent/core/types/command_data.py +206 -0
- exponent/core/types/event_types.py +89 -0
- exponent/core/types/generated/__init__.py +0 -0
- exponent/core/types/generated/strategy_info.py +213 -0
- exponent/migration-docs/login.md +112 -0
- exponent/py.typed +4 -0
- exponent/utils/__init__.py +0 -0
- exponent/utils/colors.py +92 -0
- exponent/utils/version.py +289 -0
- indent-0.1.26.dist-info/METADATA +38 -0
- indent-0.1.26.dist-info/RECORD +55 -0
- indent-0.1.26.dist-info/WHEEL +4 -0
- indent-0.1.26.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
import signal
|
|
6
|
+
from collections.abc import AsyncGenerator, Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from exponent.core.remote_execution.default_env import get_process_env
|
|
10
|
+
from exponent.core.remote_execution.languages.types import (
|
|
11
|
+
ShellExecutionResult,
|
|
12
|
+
StreamedOutputPiece,
|
|
13
|
+
)
|
|
14
|
+
from exponent.core.remote_execution.utils import smart_decode
|
|
15
|
+
|
|
16
|
+
STDOUT_FD = 1
|
|
17
|
+
STDERR_FD = 2
|
|
18
|
+
MAX_TIMEOUT = 300
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_rc_file_source_command(shell_path: str) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Returns a command to source the user's shell rc file
|
|
24
|
+
Login profiles are already sourced via the -l flag
|
|
25
|
+
"""
|
|
26
|
+
# On Windows, shell behavior is different
|
|
27
|
+
if platform.system() == "Windows":
|
|
28
|
+
return "" # Windows shells don't typically use rc files in the same way
|
|
29
|
+
|
|
30
|
+
shell_name = os.path.basename(shell_path)
|
|
31
|
+
home_dir = os.path.expanduser("~")
|
|
32
|
+
|
|
33
|
+
if shell_name == "zsh":
|
|
34
|
+
zshrc = os.path.join(home_dir, ".zshrc")
|
|
35
|
+
if os.path.exists(zshrc):
|
|
36
|
+
return f"source {zshrc} 2>/dev/null || true; "
|
|
37
|
+
elif shell_name == "bash":
|
|
38
|
+
bashrc = os.path.join(home_dir, ".bashrc")
|
|
39
|
+
if os.path.exists(bashrc):
|
|
40
|
+
return f"source {bashrc} 2>/dev/null || true; "
|
|
41
|
+
|
|
42
|
+
return "" # No rc file found or unsupported shell
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def read_stream(
|
|
46
|
+
stream: asyncio.StreamReader, fd: int, output: list[tuple[int, str]]
|
|
47
|
+
) -> AsyncGenerator[StreamedOutputPiece, None]:
|
|
48
|
+
data: bytes = b""
|
|
49
|
+
encoding = "utf-8"
|
|
50
|
+
|
|
51
|
+
while True:
|
|
52
|
+
try:
|
|
53
|
+
data = await stream.read(50_000)
|
|
54
|
+
if not data:
|
|
55
|
+
break
|
|
56
|
+
chunk = data.decode(encoding=encoding, errors="replace")
|
|
57
|
+
output.append((fd, chunk))
|
|
58
|
+
yield StreamedOutputPiece(content=chunk)
|
|
59
|
+
except UnicodeDecodeError:
|
|
60
|
+
decode_result = smart_decode(data)
|
|
61
|
+
|
|
62
|
+
if decode_result is None:
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
# Store the detected encoding as a hint for how
|
|
66
|
+
# to decode the remaining chunks of data
|
|
67
|
+
chunk, encoding = decode_result
|
|
68
|
+
|
|
69
|
+
output.append((fd, chunk))
|
|
70
|
+
yield StreamedOutputPiece(content=chunk)
|
|
71
|
+
|
|
72
|
+
continue
|
|
73
|
+
except asyncio.CancelledError:
|
|
74
|
+
raise
|
|
75
|
+
except Exception:
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def execute_shell_streaming( # noqa: PLR0915
|
|
80
|
+
code: str,
|
|
81
|
+
working_directory: str,
|
|
82
|
+
timeout: int,
|
|
83
|
+
should_halt: Callable[[], bool] | None = None,
|
|
84
|
+
env: dict[str, str] | None = None,
|
|
85
|
+
) -> AsyncGenerator[StreamedOutputPiece | ShellExecutionResult, None]:
|
|
86
|
+
timeout_seconds = min(timeout, MAX_TIMEOUT)
|
|
87
|
+
|
|
88
|
+
shell_path = os.environ.get("SHELL") or shutil.which("bash") or shutil.which("sh")
|
|
89
|
+
|
|
90
|
+
if not shell_path:
|
|
91
|
+
process = await asyncio.create_subprocess_shell(
|
|
92
|
+
code,
|
|
93
|
+
stdout=asyncio.subprocess.PIPE,
|
|
94
|
+
stderr=asyncio.subprocess.PIPE,
|
|
95
|
+
cwd=working_directory,
|
|
96
|
+
env=get_process_env(env),
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
# Add rc file sourcing to the command
|
|
100
|
+
rc_source_cmd = get_rc_file_source_command(shell_path)
|
|
101
|
+
full_command = f"{rc_source_cmd}{code}"
|
|
102
|
+
|
|
103
|
+
process = await asyncio.create_subprocess_exec(
|
|
104
|
+
shell_path,
|
|
105
|
+
"-l",
|
|
106
|
+
"-c",
|
|
107
|
+
full_command,
|
|
108
|
+
stdout=asyncio.subprocess.PIPE,
|
|
109
|
+
stderr=asyncio.subprocess.PIPE,
|
|
110
|
+
cwd=working_directory,
|
|
111
|
+
env=get_process_env(env),
|
|
112
|
+
start_new_session=True if platform.system() != "Windows" else False,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
exit_code = None
|
|
116
|
+
output: list[tuple[int, str]] = []
|
|
117
|
+
halted = False
|
|
118
|
+
timed_out = False
|
|
119
|
+
assert process.stdout
|
|
120
|
+
assert process.stderr
|
|
121
|
+
|
|
122
|
+
async def monitor_halt() -> None:
|
|
123
|
+
nonlocal halted
|
|
124
|
+
|
|
125
|
+
while True:
|
|
126
|
+
if should_halt and should_halt():
|
|
127
|
+
# Send signal to process group for proper interrupt propagation
|
|
128
|
+
try:
|
|
129
|
+
if platform.system() != "Windows":
|
|
130
|
+
# Send SIGTERM to the process group
|
|
131
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
132
|
+
try:
|
|
133
|
+
await asyncio.wait_for(process.wait(), timeout=2.0)
|
|
134
|
+
except TimeoutError:
|
|
135
|
+
# Fall back to SIGKILL to process group
|
|
136
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
137
|
+
else:
|
|
138
|
+
# Windows fallback
|
|
139
|
+
process.terminate()
|
|
140
|
+
try:
|
|
141
|
+
await asyncio.wait_for(process.wait(), timeout=2.0)
|
|
142
|
+
except TimeoutError:
|
|
143
|
+
process.kill()
|
|
144
|
+
except (ProcessLookupError, OSError):
|
|
145
|
+
# Process or process group already terminated
|
|
146
|
+
pass
|
|
147
|
+
halted = True
|
|
148
|
+
break
|
|
149
|
+
if process.returncode is not None:
|
|
150
|
+
break
|
|
151
|
+
await asyncio.sleep(0.1)
|
|
152
|
+
|
|
153
|
+
def on_timeout() -> None:
|
|
154
|
+
nonlocal timed_out
|
|
155
|
+
timed_out = True
|
|
156
|
+
try:
|
|
157
|
+
process.kill()
|
|
158
|
+
except ProcessLookupError:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
halt_task = asyncio.create_task(monitor_halt()) if should_halt else None
|
|
163
|
+
timeout_handle = asyncio.get_running_loop().call_later(
|
|
164
|
+
timeout_seconds, on_timeout
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Stream stdout and stderr concurrently using wait
|
|
168
|
+
stdout_gen = read_stream(process.stdout, STDOUT_FD, output)
|
|
169
|
+
stderr_gen = read_stream(process.stderr, STDERR_FD, output)
|
|
170
|
+
|
|
171
|
+
stdout_task = asyncio.create_task(stdout_gen.__anext__())
|
|
172
|
+
stderr_task = asyncio.create_task(stderr_gen.__anext__())
|
|
173
|
+
pending = {stdout_task, stderr_task}
|
|
174
|
+
|
|
175
|
+
while pending:
|
|
176
|
+
done, pending = await asyncio.wait(
|
|
177
|
+
pending, return_when=asyncio.FIRST_COMPLETED
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
for task in done:
|
|
181
|
+
try:
|
|
182
|
+
piece = await task
|
|
183
|
+
yield piece
|
|
184
|
+
|
|
185
|
+
# Schedule next read from the same stream
|
|
186
|
+
# Don't check process.returncode here - we need to drain all buffered output
|
|
187
|
+
if task is stdout_task and not process.stdout.at_eof():
|
|
188
|
+
stdout_task = asyncio.create_task(stdout_gen.__anext__())
|
|
189
|
+
pending.add(stdout_task)
|
|
190
|
+
elif task is stderr_task and not process.stderr.at_eof():
|
|
191
|
+
stderr_task = asyncio.create_task(stderr_gen.__anext__())
|
|
192
|
+
pending.add(stderr_task)
|
|
193
|
+
except StopAsyncIteration:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
exit_code = await process.wait()
|
|
197
|
+
timeout_handle.cancel()
|
|
198
|
+
|
|
199
|
+
except asyncio.CancelledError:
|
|
200
|
+
process.kill()
|
|
201
|
+
raise
|
|
202
|
+
finally:
|
|
203
|
+
# Explicitly kill the process if it's still running
|
|
204
|
+
if process and process.returncode is None:
|
|
205
|
+
try:
|
|
206
|
+
if platform.system() != "Windows":
|
|
207
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
208
|
+
else:
|
|
209
|
+
process.terminate()
|
|
210
|
+
except (ProcessLookupError, OSError):
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
tasks_to_cancel: list[asyncio.Task[Any]] = [stdout_task, stderr_task]
|
|
214
|
+
if halt_task:
|
|
215
|
+
tasks_to_cancel.append(halt_task)
|
|
216
|
+
|
|
217
|
+
await asyncio.gather(*tasks_to_cancel, return_exceptions=True)
|
|
218
|
+
|
|
219
|
+
formatted_output = "".join([chunk for (_, chunk) in output]).strip() + "\n\n"
|
|
220
|
+
|
|
221
|
+
yield ShellExecutionResult(
|
|
222
|
+
output=formatted_output,
|
|
223
|
+
cancelled_for_timeout=timed_out,
|
|
224
|
+
exit_code=None if timed_out else exit_code,
|
|
225
|
+
halted=halted,
|
|
226
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class StreamedOutputPiece:
|
|
6
|
+
content: str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ShellExecutionResult:
|
|
11
|
+
output: str
|
|
12
|
+
cancelled_for_timeout: bool
|
|
13
|
+
exit_code: int | None
|
|
14
|
+
halted: bool = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PythonExecutionResult:
|
|
19
|
+
output: str
|
|
20
|
+
halted: bool = False
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
from exponent.core.remote_execution.types import PortInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_port_usage() -> list[PortInfo] | None:
|
|
9
|
+
"""
|
|
10
|
+
Get information about all listening ports on the system.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
List of PortInfo objects containing process name, port, protocol, pid, and uptime.
|
|
14
|
+
Returns None if there's a permission error.
|
|
15
|
+
Returns empty list if no listening ports are found.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
connections = psutil.net_connections(kind="tcp")
|
|
19
|
+
except (psutil.AccessDenied, PermissionError):
|
|
20
|
+
# If we don't have permission to see connections, return None
|
|
21
|
+
return None
|
|
22
|
+
except Exception:
|
|
23
|
+
# For any other unexpected errors, return None
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
port_info_list: list[PortInfo] = []
|
|
27
|
+
current_time = time.time()
|
|
28
|
+
|
|
29
|
+
for conn in connections:
|
|
30
|
+
# Only include TCP ports in LISTEN state
|
|
31
|
+
if conn.status != "LISTEN":
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
# Skip if no local address (shouldn't happen for LISTEN, but be safe)
|
|
35
|
+
if not conn.laddr:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
port = conn.laddr.port
|
|
39
|
+
pid = conn.pid
|
|
40
|
+
|
|
41
|
+
# Try to get process information
|
|
42
|
+
process_name = "unknown"
|
|
43
|
+
uptime_seconds = None
|
|
44
|
+
|
|
45
|
+
if pid:
|
|
46
|
+
try:
|
|
47
|
+
process = psutil.Process(pid)
|
|
48
|
+
process_name = process.name()
|
|
49
|
+
|
|
50
|
+
# Calculate uptime
|
|
51
|
+
create_time = process.create_time()
|
|
52
|
+
uptime_seconds = current_time - create_time
|
|
53
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
54
|
+
# Process disappeared or we don't have permission
|
|
55
|
+
pass
|
|
56
|
+
except Exception:
|
|
57
|
+
# Any other unexpected error, just skip process info
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
port_info = PortInfo(
|
|
61
|
+
process_name=process_name,
|
|
62
|
+
port=port,
|
|
63
|
+
protocol="TCP",
|
|
64
|
+
pid=pid,
|
|
65
|
+
uptime_seconds=uptime_seconds,
|
|
66
|
+
)
|
|
67
|
+
port_info_list.append(port_info)
|
|
68
|
+
|
|
69
|
+
# Limit to 50 ports to avoid bloating the heartbeat payload
|
|
70
|
+
if len(port_info_list) >= 50:
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
return port_info_list
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from httpx import AsyncClient, Request, Response
|
|
8
|
+
|
|
9
|
+
from exponent.core.remote_execution.exceptions import ExponentError
|
|
10
|
+
from exponent.core.remote_execution.languages.python_execution import Kernel
|
|
11
|
+
from exponent.core.remote_execution.utils import format_error_log
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from exponent.core.config import Settings
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SessionLog:
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.log_buffer: list[str] = []
|
|
22
|
+
self.max_size = 5
|
|
23
|
+
|
|
24
|
+
def append_log(self, log: str) -> None:
|
|
25
|
+
self.log_buffer.append(log)
|
|
26
|
+
self.log_buffer = self.log_buffer[-self.max_size :]
|
|
27
|
+
|
|
28
|
+
def get_logs(self) -> list[str]:
|
|
29
|
+
return self.log_buffer
|
|
30
|
+
|
|
31
|
+
async def log_request(self, request: Request) -> None:
|
|
32
|
+
self.append_log(f"Request: {request.method} {request.url}")
|
|
33
|
+
|
|
34
|
+
async def log_response(self, response: Response) -> None:
|
|
35
|
+
request = response.request
|
|
36
|
+
await response.aread()
|
|
37
|
+
self.append_log(
|
|
38
|
+
f"Response for request: {request.method} {request.url}\n"
|
|
39
|
+
f"Response: {response.status_code}, {response.text}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RemoteExecutionClientSession:
|
|
44
|
+
def __init__(
|
|
45
|
+
self, working_directory: str, base_url: str, base_ws_url: str, api_key: str
|
|
46
|
+
):
|
|
47
|
+
self.chat_uuid: str | None = None
|
|
48
|
+
|
|
49
|
+
self.working_directory = working_directory
|
|
50
|
+
self.kernel = Kernel(working_directory=working_directory)
|
|
51
|
+
self.api_log = SessionLog()
|
|
52
|
+
|
|
53
|
+
self.api_client = AsyncClient(
|
|
54
|
+
base_url=base_url,
|
|
55
|
+
headers={"API-KEY": api_key},
|
|
56
|
+
event_hooks={
|
|
57
|
+
"request": [self.api_log.log_request],
|
|
58
|
+
"response": [self.api_log.log_response],
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.ws_client = AsyncClient(
|
|
63
|
+
base_url=base_ws_url,
|
|
64
|
+
headers={"API-KEY": api_key},
|
|
65
|
+
event_hooks={
|
|
66
|
+
"request": [self.api_log.log_request],
|
|
67
|
+
"response": [self.api_log.log_response],
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def set_chat_uuid(self, chat_uuid: str) -> None:
|
|
72
|
+
self.chat_uuid = chat_uuid
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def send_exception_log(
|
|
76
|
+
exc: Exception,
|
|
77
|
+
session: RemoteExecutionClientSession | None = None,
|
|
78
|
+
settings: Optional["Settings"] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
error_log = format_error_log(
|
|
81
|
+
exc=exc,
|
|
82
|
+
chat_uuid=session.chat_uuid if session else None,
|
|
83
|
+
attachment_lines=session.api_log.get_logs() if session else None,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if session:
|
|
87
|
+
api_client = session.api_client
|
|
88
|
+
elif settings:
|
|
89
|
+
api_key = settings.api_key
|
|
90
|
+
if not api_key:
|
|
91
|
+
raise ValueError("No API key provided")
|
|
92
|
+
api_client = AsyncClient(
|
|
93
|
+
base_url=settings.get_base_api_url(),
|
|
94
|
+
headers={"API-KEY": api_key},
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError("No session or settings provided")
|
|
98
|
+
|
|
99
|
+
if not error_log:
|
|
100
|
+
return
|
|
101
|
+
try:
|
|
102
|
+
await api_client.post(
|
|
103
|
+
"/api/remote_execution/log_error",
|
|
104
|
+
content=error_log.model_dump_json(),
|
|
105
|
+
timeout=60,
|
|
106
|
+
)
|
|
107
|
+
except httpx.ConnectError:
|
|
108
|
+
logger.info("Failed to send error log")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@asynccontextmanager
|
|
112
|
+
async def get_session(
|
|
113
|
+
working_directory: str,
|
|
114
|
+
base_url: str,
|
|
115
|
+
base_ws_url: str,
|
|
116
|
+
api_key: str,
|
|
117
|
+
) -> AsyncGenerator[RemoteExecutionClientSession, None]:
|
|
118
|
+
session = RemoteExecutionClientSession(
|
|
119
|
+
working_directory, base_url, base_ws_url, api_key
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
yield session
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
await send_exception_log(exc, session=session, settings=None)
|
|
125
|
+
raise ExponentError(str(exc))
|
|
126
|
+
finally:
|
|
127
|
+
session.kernel.close()
|
|
128
|
+
await session.api_client.aclose()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
|
|
5
|
+
from exponent.core.remote_execution.git import get_git_info
|
|
6
|
+
from exponent.core.remote_execution.languages import python_execution
|
|
7
|
+
from exponent.core.remote_execution.port_utils import get_port_usage
|
|
8
|
+
from exponent.core.remote_execution.types import (
|
|
9
|
+
SystemInfo,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def get_system_info(working_directory: str) -> SystemInfo:
|
|
14
|
+
return SystemInfo(
|
|
15
|
+
name=getpass.getuser(),
|
|
16
|
+
cwd=working_directory,
|
|
17
|
+
os=platform.system(),
|
|
18
|
+
shell=_get_user_shell(),
|
|
19
|
+
git=await get_git_info(working_directory),
|
|
20
|
+
python_env=python_execution.get_python_env_info(),
|
|
21
|
+
port_usage=get_port_usage(),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_user_shell() -> str:
|
|
26
|
+
return os.environ.get("SHELL", "bash")
|