indent 0.1.13__py3-none-any.whl → 0.1.28__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 (31) hide show
  1. exponent/__init__.py +2 -2
  2. exponent/cli.py +0 -2
  3. exponent/commands/cloud_commands.py +2 -87
  4. exponent/commands/common.py +25 -40
  5. exponent/commands/config_commands.py +0 -87
  6. exponent/commands/run_commands.py +5 -2
  7. exponent/core/config.py +1 -1
  8. exponent/core/container_build/__init__.py +0 -0
  9. exponent/core/container_build/types.py +25 -0
  10. exponent/core/graphql/mutations.py +2 -31
  11. exponent/core/graphql/queries.py +0 -3
  12. exponent/core/remote_execution/cli_rpc_types.py +201 -5
  13. exponent/core/remote_execution/client.py +355 -92
  14. exponent/core/remote_execution/code_execution.py +26 -7
  15. exponent/core/remote_execution/default_env.py +31 -0
  16. exponent/core/remote_execution/languages/shell_streaming.py +11 -6
  17. exponent/core/remote_execution/port_utils.py +73 -0
  18. exponent/core/remote_execution/system_context.py +2 -0
  19. exponent/core/remote_execution/terminal_session.py +517 -0
  20. exponent/core/remote_execution/terminal_types.py +29 -0
  21. exponent/core/remote_execution/tool_execution.py +228 -18
  22. exponent/core/remote_execution/tool_type_utils.py +39 -0
  23. exponent/core/remote_execution/truncation.py +9 -1
  24. exponent/core/remote_execution/types.py +71 -19
  25. exponent/utils/version.py +8 -7
  26. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/METADATA +5 -2
  27. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/RECORD +29 -24
  28. exponent/commands/workflow_commands.py +0 -111
  29. exponent/core/graphql/github_config_queries.py +0 -56
  30. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/WHEEL +0 -0
  31. {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,10 @@
1
1
  from collections.abc import AsyncGenerator, Callable
2
2
 
3
+ from exponent.core.remote_execution.cli_rpc_types import (
4
+ StreamingCodeExecutionRequest,
5
+ StreamingCodeExecutionResponse,
6
+ StreamingCodeExecutionResponseChunk,
7
+ )
3
8
  from exponent.core.remote_execution.languages.python_execution import (
4
9
  execute_python_streaming,
5
10
  )
@@ -8,13 +13,9 @@ from exponent.core.remote_execution.languages.shell_streaming import (
8
13
  )
9
14
  from exponent.core.remote_execution.languages.types import StreamedOutputPiece
10
15
  from exponent.core.remote_execution.session import RemoteExecutionClientSession
11
- from exponent.core.remote_execution.types import (
12
- StreamingCodeExecutionRequest,
13
- StreamingCodeExecutionResponse,
14
- StreamingCodeExecutionResponseChunk,
15
- )
16
16
 
17
17
  EMPTY_OUTPUT_STRING = "(No output)"
18
+ MAX_OUTPUT_LENGTH = 50000 # Maximum characters to keep in final output
18
19
 
19
20
 
20
21
  async def execute_code_streaming(
@@ -34,9 +35,18 @@ async def execute_code_streaming(
34
35
  content=output.content, correlation_id=request.correlation_id
35
36
  )
36
37
  else:
38
+ final_output = output.output or EMPTY_OUTPUT_STRING
39
+ truncated = len(final_output) > MAX_OUTPUT_LENGTH
40
+ if truncated:
41
+ final_output = final_output[-MAX_OUTPUT_LENGTH:]
42
+ final_output = (
43
+ f"[Truncated to last {MAX_OUTPUT_LENGTH} characters]\n\n"
44
+ + final_output
45
+ )
37
46
  yield StreamingCodeExecutionResponse(
38
47
  correlation_id=request.correlation_id,
39
- content=output.output or EMPTY_OUTPUT_STRING,
48
+ content=final_output,
49
+ truncated=truncated,
40
50
  halted=output.halted,
41
51
  )
42
52
 
@@ -49,9 +59,18 @@ async def execute_code_streaming(
49
59
  content=shell_output.content, correlation_id=request.correlation_id
50
60
  )
51
61
  else:
62
+ final_output = shell_output.output or EMPTY_OUTPUT_STRING
63
+ truncated = len(final_output) > MAX_OUTPUT_LENGTH
64
+ if truncated:
65
+ final_output = final_output[-MAX_OUTPUT_LENGTH:]
66
+ final_output = (
67
+ f"[Truncated to last {MAX_OUTPUT_LENGTH} characters]\n\n"
68
+ + final_output
69
+ )
52
70
  yield StreamingCodeExecutionResponse(
53
71
  correlation_id=request.correlation_id,
54
- content=shell_output.output or EMPTY_OUTPUT_STRING,
72
+ content=final_output,
73
+ truncated=truncated,
55
74
  halted=shell_output.halted,
56
75
  exit_code=shell_output.exit_code,
57
76
  cancelled_for_timeout=shell_output.cancelled_for_timeout,
@@ -0,0 +1,31 @@
1
+ import os
2
+
3
+
4
+ def get_default_env() -> dict[str, str]:
5
+ """
6
+ Returns default environment variables for CLI-spawned processes.
7
+ These are merged with the parent process environment.
8
+ """
9
+ return {
10
+ "GIT_EDITOR": "true",
11
+ }
12
+
13
+
14
+ def get_process_env(env_overrides: dict[str, str] | None = None) -> dict[str, str]:
15
+ """
16
+ Returns the complete environment for spawned processes.
17
+ Merges parent environment with default variables, then applies overrides.
18
+
19
+ Priority order (lowest to highest):
20
+ 1. Parent process environment (os.environ)
21
+ 2. Default environment variables (get_default_env())
22
+ 3. Explicit overrides (env_overrides parameter)
23
+
24
+ Args:
25
+ env_overrides: Optional dict of environment variables that override defaults
26
+ """
27
+ env = os.environ.copy()
28
+ env.update(get_default_env())
29
+ if env_overrides:
30
+ env.update(env_overrides)
31
+ return env
@@ -6,6 +6,7 @@ import signal
6
6
  from collections.abc import AsyncGenerator, Callable
7
7
  from typing import Any
8
8
 
9
+ from exponent.core.remote_execution.default_env import get_process_env
9
10
  from exponent.core.remote_execution.languages.types import (
10
11
  ShellExecutionResult,
11
12
  StreamedOutputPiece,
@@ -49,10 +50,10 @@ async def read_stream(
49
50
 
50
51
  while True:
51
52
  try:
52
- data = await stream.read(4096)
53
+ data = await stream.read(50_000)
53
54
  if not data:
54
55
  break
55
- chunk = data.decode(encoding=encoding)
56
+ chunk = data.decode(encoding=encoding, errors="replace")
56
57
  output.append((fd, chunk))
57
58
  yield StreamedOutputPiece(content=chunk)
58
59
  except UnicodeDecodeError:
@@ -80,6 +81,7 @@ async def execute_shell_streaming( # noqa: PLR0915
80
81
  working_directory: str,
81
82
  timeout: int,
82
83
  should_halt: Callable[[], bool] | None = None,
84
+ env: dict[str, str] | None = None,
83
85
  ) -> AsyncGenerator[StreamedOutputPiece | ShellExecutionResult, None]:
84
86
  timeout_seconds = min(timeout, MAX_TIMEOUT)
85
87
 
@@ -91,6 +93,7 @@ async def execute_shell_streaming( # noqa: PLR0915
91
93
  stdout=asyncio.subprocess.PIPE,
92
94
  stderr=asyncio.subprocess.PIPE,
93
95
  cwd=working_directory,
96
+ env=get_process_env(env),
94
97
  )
95
98
  else:
96
99
  # Add rc file sourcing to the command
@@ -105,6 +108,7 @@ async def execute_shell_streaming( # noqa: PLR0915
105
108
  stdout=asyncio.subprocess.PIPE,
106
109
  stderr=asyncio.subprocess.PIPE,
107
110
  cwd=working_directory,
111
+ env=get_process_env(env),
108
112
  start_new_session=True if platform.system() != "Windows" else False,
109
113
  )
110
114
 
@@ -149,7 +153,10 @@ async def execute_shell_streaming( # noqa: PLR0915
149
153
  def on_timeout() -> None:
150
154
  nonlocal timed_out
151
155
  timed_out = True
152
- process.kill()
156
+ try:
157
+ process.kill()
158
+ except ProcessLookupError:
159
+ pass
153
160
 
154
161
  try:
155
162
  halt_task = asyncio.create_task(monitor_halt()) if should_halt else None
@@ -175,10 +182,8 @@ async def execute_shell_streaming( # noqa: PLR0915
175
182
  piece = await task
176
183
  yield piece
177
184
 
178
- if process.returncode is not None:
179
- break
180
-
181
185
  # Schedule next read from the same stream
186
+ # Don't check process.returncode here - we need to drain all buffered output
182
187
  if task is stdout_task and not process.stdout.at_eof():
183
188
  stdout_task = asyncio.create_task(stdout_gen.__anext__())
184
189
  pending.add(stdout_task)
@@ -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
@@ -4,6 +4,7 @@ import platform
4
4
 
5
5
  from exponent.core.remote_execution.git import get_git_info
6
6
  from exponent.core.remote_execution.languages import python_execution
7
+ from exponent.core.remote_execution.port_utils import get_port_usage
7
8
  from exponent.core.remote_execution.types import (
8
9
  SystemInfo,
9
10
  )
@@ -17,6 +18,7 @@ async def get_system_info(working_directory: str) -> SystemInfo:
17
18
  shell=_get_user_shell(),
18
19
  git=await get_git_info(working_directory),
19
20
  python_env=python_execution.get_python_env_info(),
21
+ port_usage=get_port_usage(),
20
22
  )
21
23
 
22
24