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.
Files changed (55) hide show
  1. exponent/__init__.py +34 -0
  2. exponent/cli.py +110 -0
  3. exponent/commands/cloud_commands.py +585 -0
  4. exponent/commands/common.py +411 -0
  5. exponent/commands/config_commands.py +334 -0
  6. exponent/commands/run_commands.py +222 -0
  7. exponent/commands/settings.py +56 -0
  8. exponent/commands/types.py +111 -0
  9. exponent/commands/upgrade.py +29 -0
  10. exponent/commands/utils.py +146 -0
  11. exponent/core/config.py +180 -0
  12. exponent/core/graphql/__init__.py +0 -0
  13. exponent/core/graphql/client.py +61 -0
  14. exponent/core/graphql/get_chats_query.py +47 -0
  15. exponent/core/graphql/mutations.py +160 -0
  16. exponent/core/graphql/queries.py +146 -0
  17. exponent/core/graphql/subscriptions.py +16 -0
  18. exponent/core/remote_execution/checkpoints.py +212 -0
  19. exponent/core/remote_execution/cli_rpc_types.py +499 -0
  20. exponent/core/remote_execution/client.py +999 -0
  21. exponent/core/remote_execution/code_execution.py +77 -0
  22. exponent/core/remote_execution/default_env.py +31 -0
  23. exponent/core/remote_execution/error_info.py +45 -0
  24. exponent/core/remote_execution/exceptions.py +10 -0
  25. exponent/core/remote_execution/file_write.py +35 -0
  26. exponent/core/remote_execution/files.py +330 -0
  27. exponent/core/remote_execution/git.py +268 -0
  28. exponent/core/remote_execution/http_fetch.py +94 -0
  29. exponent/core/remote_execution/languages/python_execution.py +239 -0
  30. exponent/core/remote_execution/languages/shell_streaming.py +226 -0
  31. exponent/core/remote_execution/languages/types.py +20 -0
  32. exponent/core/remote_execution/port_utils.py +73 -0
  33. exponent/core/remote_execution/session.py +128 -0
  34. exponent/core/remote_execution/system_context.py +26 -0
  35. exponent/core/remote_execution/terminal_session.py +375 -0
  36. exponent/core/remote_execution/terminal_types.py +29 -0
  37. exponent/core/remote_execution/tool_execution.py +595 -0
  38. exponent/core/remote_execution/tool_type_utils.py +39 -0
  39. exponent/core/remote_execution/truncation.py +296 -0
  40. exponent/core/remote_execution/types.py +635 -0
  41. exponent/core/remote_execution/utils.py +477 -0
  42. exponent/core/types/__init__.py +0 -0
  43. exponent/core/types/command_data.py +206 -0
  44. exponent/core/types/event_types.py +89 -0
  45. exponent/core/types/generated/__init__.py +0 -0
  46. exponent/core/types/generated/strategy_info.py +213 -0
  47. exponent/migration-docs/login.md +112 -0
  48. exponent/py.typed +4 -0
  49. exponent/utils/__init__.py +0 -0
  50. exponent/utils/colors.py +92 -0
  51. exponent/utils/version.py +289 -0
  52. indent-0.1.26.dist-info/METADATA +38 -0
  53. indent-0.1.26.dist-info/RECORD +55 -0
  54. indent-0.1.26.dist-info/WHEEL +4 -0
  55. indent-0.1.26.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,334 @@
1
+ import json
2
+ import sys
3
+
4
+ import click
5
+
6
+ from exponent.commands.common import (
7
+ redirect_to_login,
8
+ refresh_api_key_task,
9
+ run_until_complete,
10
+ set_login_complete,
11
+ )
12
+ from exponent.commands.settings import use_settings
13
+ from exponent.commands.types import exponent_cli_group
14
+ from exponent.core.config import Settings
15
+ from exponent.core.graphql.client import GraphQLClient
16
+ from exponent.core.graphql.get_chats_query import GET_CHATS_QUERY
17
+ from exponent.core.graphql.subscriptions import AUTHENTICATED_USER_SUBSCRIPTION
18
+ from exponent.utils.version import (
19
+ get_installed_metadata,
20
+ get_installed_version,
21
+ get_installer,
22
+ get_python_path,
23
+ get_sys_executable,
24
+ )
25
+
26
+
27
+ @exponent_cli_group()
28
+ def config_cli() -> None:
29
+ """Manage Indent configuration settings."""
30
+ pass
31
+
32
+
33
+ @config_cli.command(hidden=True)
34
+ @use_settings
35
+ def debug(
36
+ settings: Settings,
37
+ ) -> None:
38
+ click.echo("Settings:")
39
+ click.echo(settings)
40
+ click.echo("\nInstalled Version:")
41
+ click.echo(get_installed_version())
42
+ click.echo("\nPython Version:")
43
+ click.echo(sys.version)
44
+ click.echo("\nPython Path:")
45
+ click.echo(get_python_path())
46
+ click.echo("\nSys Executable Path:")
47
+ click.echo(get_sys_executable())
48
+ click.echo("\nInstaller:")
49
+ click.echo(get_installer())
50
+ click.echo("\nInstalled Metadata:")
51
+ click.echo(get_installed_metadata())
52
+
53
+
54
+ @config_cli.command(hidden=True)
55
+ @click.option(
56
+ "--set-git-warning-disabled",
57
+ is_flag=True,
58
+ help="Disable the git warning for running Indent from a non-git repository",
59
+ )
60
+ @click.option(
61
+ "--set-git-warning-enabled",
62
+ is_flag=True,
63
+ help="Enable the git warning for running Indent from a non-git repository",
64
+ )
65
+ @click.option(
66
+ "--set-auto-upgrade-enabled",
67
+ is_flag=True,
68
+ help="Enable automatic upgrades",
69
+ )
70
+ @click.option(
71
+ "--set-auto-upgrade-disabled",
72
+ is_flag=True,
73
+ help="Disable automatic upgrades",
74
+ )
75
+ @click.option(
76
+ "--set-base-api-url-override",
77
+ required=False,
78
+ hidden=True,
79
+ help="Override base API URL",
80
+ )
81
+ @click.option(
82
+ "--clear-base-api-url-override",
83
+ is_flag=True,
84
+ hidden=True,
85
+ help="Clear base API URL override",
86
+ )
87
+ @click.option(
88
+ "--set-base-ws-url-override",
89
+ required=False,
90
+ hidden=True,
91
+ help="Override base WS URL",
92
+ )
93
+ @click.option(
94
+ "--clear-base-ws-url-override",
95
+ is_flag=True,
96
+ hidden=True,
97
+ help="Clear base WS URL override",
98
+ )
99
+ @click.option(
100
+ "--set-use-default-colors",
101
+ is_flag=True,
102
+ help="Use default colors",
103
+ )
104
+ @click.option(
105
+ "--clear-use-default-colors",
106
+ is_flag=True,
107
+ help="Clear use default colors",
108
+ )
109
+ @use_settings
110
+ def config( # noqa: PLR0913
111
+ settings: Settings,
112
+ set_git_warning_disabled: bool,
113
+ set_git_warning_enabled: bool,
114
+ set_auto_upgrade_enabled: bool,
115
+ set_auto_upgrade_disabled: bool,
116
+ clear_base_api_url_override: bool,
117
+ clear_base_ws_url_override: bool,
118
+ set_use_default_colors: bool,
119
+ clear_use_default_colors: bool,
120
+ set_base_api_url_override: str | None = None,
121
+ set_base_ws_url_override: str | None = None,
122
+ ) -> None:
123
+ """Display current Indent configuration."""
124
+
125
+ num_options_set = sum(
126
+ [
127
+ set_git_warning_disabled,
128
+ set_git_warning_enabled,
129
+ set_auto_upgrade_enabled,
130
+ set_auto_upgrade_disabled,
131
+ clear_base_ws_url_override,
132
+ clear_base_api_url_override,
133
+ set_base_api_url_override is not None,
134
+ set_base_ws_url_override is not None,
135
+ set_use_default_colors,
136
+ clear_use_default_colors,
137
+ ]
138
+ )
139
+ if num_options_set == 0:
140
+ click.secho(
141
+ json.dumps(settings.get_config_file_settings(), indent=2),
142
+ fg="green",
143
+ )
144
+ return
145
+
146
+ if num_options_set > 1:
147
+ click.secho("Cannot set multiple options at the same time.", fg="red")
148
+ return
149
+
150
+ if set_git_warning_enabled:
151
+ settings.options.git_warning_disabled = False
152
+ settings.write_settings_to_config_file()
153
+
154
+ click.secho(
155
+ "Git warning enabled. Indent will now check for a git repository.\n",
156
+ fg="yellow",
157
+ )
158
+
159
+ if set_git_warning_disabled:
160
+ settings.options.git_warning_disabled = True
161
+ settings.write_settings_to_config_file()
162
+
163
+ click.secho(
164
+ "Git warning disabled. Indent will no longer check for a git repository.\n",
165
+ fg="yellow",
166
+ )
167
+
168
+ if set_auto_upgrade_enabled:
169
+ settings.options.auto_upgrade = True
170
+ settings.write_settings_to_config_file()
171
+
172
+ click.secho(
173
+ "Automatic upgrades enabled. Indent will now check for updates.\n",
174
+ fg="yellow",
175
+ )
176
+
177
+ if set_auto_upgrade_disabled:
178
+ settings.options.auto_upgrade = False
179
+ settings.write_settings_to_config_file()
180
+
181
+ click.secho(
182
+ "Automatic upgrades disabled. Indent will no longer check for updates.\n",
183
+ fg="yellow",
184
+ )
185
+
186
+ if clear_base_api_url_override:
187
+ settings.options.base_api_url_override = None
188
+ settings.write_settings_to_config_file()
189
+
190
+ click.secho(
191
+ "API URL override cleared.",
192
+ fg="yellow",
193
+ )
194
+
195
+ if clear_base_ws_url_override:
196
+ settings.options.base_ws_url_override = None
197
+ settings.write_settings_to_config_file()
198
+
199
+ click.secho(
200
+ "WS URL override cleared.",
201
+ fg="yellow",
202
+ )
203
+
204
+ if set_base_api_url_override:
205
+ settings.options.base_api_url_override = set_base_api_url_override
206
+ settings.write_settings_to_config_file()
207
+
208
+ click.secho(
209
+ "API URL override set.",
210
+ fg="yellow",
211
+ )
212
+
213
+ if set_base_ws_url_override:
214
+ settings.options.base_ws_url_override = set_base_ws_url_override
215
+ settings.write_settings_to_config_file()
216
+
217
+ click.secho(
218
+ "WS URL override set.",
219
+ fg="yellow",
220
+ )
221
+
222
+ if set_use_default_colors:
223
+ settings.options.use_default_colors = True
224
+ settings.write_settings_to_config_file()
225
+
226
+ click.secho(
227
+ "Use default colors set.",
228
+ fg="yellow",
229
+ )
230
+
231
+ if clear_use_default_colors:
232
+ settings.options.use_default_colors = False
233
+ settings.write_settings_to_config_file()
234
+
235
+
236
+ @config_cli.command()
237
+ @click.option("--key", help="Your Indent API Key")
238
+ @use_settings
239
+ def login(settings: Settings, key: str) -> None:
240
+ """Log in to Indent."""
241
+
242
+ if not key:
243
+ redirect_to_login(settings, "provided")
244
+ return
245
+
246
+ click.echo("Verifying API key...")
247
+ run_until_complete(
248
+ set_login_complete(key, settings.get_base_api_url(), settings.get_base_ws_url())
249
+ )
250
+ click.secho("Success!", fg="green")
251
+
252
+ click.echo(f"Saving API Key to {settings.config_file_path}")
253
+
254
+ if settings.api_key and settings.api_key != key:
255
+ click.confirm("Detected existing API Key, continue? ", default=True, abort=True)
256
+
257
+ settings.update_api_key(key)
258
+ settings.write_settings_to_config_file()
259
+
260
+ click.echo("API Key saved.")
261
+
262
+
263
+ @config_cli.command(hidden=True)
264
+ @use_settings
265
+ def get_chats(
266
+ settings: Settings,
267
+ ) -> None:
268
+ if not settings.api_key:
269
+ redirect_to_login(settings)
270
+ return
271
+
272
+ run_until_complete(
273
+ get_chats_task(
274
+ api_key=settings.api_key,
275
+ base_api_url=settings.get_base_api_url(),
276
+ base_ws_url=settings.get_base_ws_url(),
277
+ )
278
+ )
279
+
280
+
281
+ @config_cli.command(hidden=True)
282
+ @use_settings
283
+ def get_authenticated_user(
284
+ settings: Settings,
285
+ ) -> None:
286
+ if not settings.api_key:
287
+ redirect_to_login(settings)
288
+ return
289
+
290
+ run_until_complete(
291
+ get_authenticated_user_task(
292
+ api_key=settings.api_key,
293
+ base_api_url=settings.get_base_api_url(),
294
+ base_ws_url=settings.get_base_ws_url(),
295
+ )
296
+ )
297
+
298
+
299
+ async def get_chats_task(
300
+ api_key: str,
301
+ base_api_url: str,
302
+ base_ws_url: str,
303
+ ) -> None:
304
+ graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
305
+ result = await graphql_client.execute(GET_CHATS_QUERY)
306
+ click.echo(result)
307
+
308
+
309
+ async def get_authenticated_user_task(
310
+ api_key: str,
311
+ base_api_url: str,
312
+ base_ws_url: str,
313
+ ) -> None:
314
+ graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
315
+ async for it in graphql_client.subscribe(AUTHENTICATED_USER_SUBSCRIPTION):
316
+ click.echo(it)
317
+
318
+
319
+ @config_cli.command(hidden=True)
320
+ @use_settings
321
+ def refresh_key(settings: Settings) -> None:
322
+ """Refresh your API key."""
323
+ if not settings.api_key:
324
+ redirect_to_login(settings)
325
+ return
326
+
327
+ click.echo("Refreshing API key...")
328
+ run_until_complete(
329
+ refresh_api_key_task(
330
+ api_key=settings.api_key,
331
+ base_api_url=settings.get_base_api_url(),
332
+ base_ws_url=settings.get_base_ws_url(),
333
+ )
334
+ )
@@ -0,0 +1,222 @@
1
+ import asyncio
2
+ import sys
3
+ import time
4
+
5
+ import click
6
+
7
+ from exponent.commands.common import (
8
+ check_inside_git_repo,
9
+ check_running_from_home_directory,
10
+ check_ssl,
11
+ create_chat,
12
+ inside_ssh_session,
13
+ redirect_to_login,
14
+ start_client,
15
+ )
16
+ from exponent.commands.settings import use_settings
17
+ from exponent.commands.types import exponent_cli_group
18
+ from exponent.commands.utils import (
19
+ ConnectionTracker,
20
+ Spinner,
21
+ launch_exponent_browser,
22
+ print_exponent_message,
23
+ )
24
+ from exponent.core.config import Settings
25
+ from exponent.core.remote_execution.client import (
26
+ REMOTE_EXECUTION_CLIENT_EXIT_INFO,
27
+ SwitchCLIChat,
28
+ WSDisconnected,
29
+ )
30
+ from exponent.core.remote_execution.types import ChatSource
31
+ from exponent.core.remote_execution.utils import assert_unreachable
32
+ from exponent.utils.version import check_exponent_version_and_upgrade
33
+
34
+ try:
35
+ # this is an optional dependency for python <3.11
36
+ from async_timeout import timeout
37
+ except ImportError: # pragma: no cover
38
+ from asyncio import timeout
39
+
40
+
41
+ @exponent_cli_group()
42
+ def run_cli() -> None:
43
+ """Run AI-powered chat sessions."""
44
+ pass
45
+
46
+
47
+ @run_cli.command()
48
+ @click.option(
49
+ "--chat-id",
50
+ help="ID of an existing chat session to reconnect",
51
+ required=False,
52
+ prompt_required=False,
53
+ prompt="Enter the chat ID to reconnect to",
54
+ )
55
+ @click.option(
56
+ "--prompt",
57
+ help="Start a chat with a given prompt.",
58
+ )
59
+ @click.option(
60
+ "--workflow-id",
61
+ hidden=True,
62
+ required=False,
63
+ )
64
+ @click.option(
65
+ "--timeout-seconds",
66
+ type=int,
67
+ help="Number of seconds without receiving a request before shutting down",
68
+ envvar="INDENT_TIMEOUT_SECONDS",
69
+ )
70
+ @use_settings
71
+ def run(
72
+ settings: Settings,
73
+ chat_id: str | None = None,
74
+ prompt: str | None = None,
75
+ workflow_id: str | None = None,
76
+ timeout_seconds: int | None = None,
77
+ ) -> None:
78
+ """[default] Start or reconnect to an Indent session."""
79
+ check_exponent_version_and_upgrade(settings)
80
+
81
+ if not settings.api_key:
82
+ redirect_to_login(settings)
83
+ return
84
+
85
+ loop = asyncio.get_event_loop()
86
+
87
+ check_running_from_home_directory()
88
+ check_ssl()
89
+ loop.run_until_complete(check_inside_git_repo(settings))
90
+
91
+ api_key = settings.api_key
92
+ base_url = settings.base_url
93
+ base_api_url = settings.get_base_api_url()
94
+ base_ws_url = settings.get_base_ws_url()
95
+
96
+ chat_uuid = chat_id or loop.run_until_complete(
97
+ create_chat(api_key, base_api_url, base_ws_url, ChatSource.CLI_RUN)
98
+ )
99
+
100
+ if isinstance(timeout_seconds, int) and timeout_seconds <= 0:
101
+ click.secho("Error: --timeout-seconds must be a positive integer", fg="red")
102
+ sys.exit(1)
103
+
104
+ if chat_uuid is None:
105
+ sys.exit(1)
106
+
107
+ if (
108
+ (not inside_ssh_session())
109
+ and (not workflow_id)
110
+ # If the user specified a chat ID, they probably don't want to re-launch the chat
111
+ and (not chat_id)
112
+ ):
113
+ # Open the chat in the browser
114
+ launch_exponent_browser(settings.environment, base_url, chat_uuid)
115
+
116
+ while True:
117
+ result = run_chat(
118
+ loop, api_key, chat_uuid, settings, prompt, workflow_id, timeout_seconds
119
+ )
120
+ if result is None or isinstance(result, WSDisconnected):
121
+ # NOTE: None here means that handle_connection_changes exited
122
+ # first. We should likely have a different message for this.
123
+ if result and result.error_message:
124
+ click.secho(f"Error: {result.error_message}", fg="red")
125
+ sys.exit(10)
126
+ else:
127
+ click.echo("Disconnected upon user request, shutting down...")
128
+ break
129
+ elif isinstance(result, SwitchCLIChat):
130
+ chat_uuid = result.new_chat_uuid
131
+ click.echo("\nSwitching chats...")
132
+ else:
133
+ assert_unreachable(result)
134
+
135
+
136
+ def run_chat(
137
+ loop: asyncio.AbstractEventLoop,
138
+ api_key: str,
139
+ chat_uuid: str,
140
+ settings: Settings,
141
+ prompt: str | None,
142
+ workflow_id: str | None,
143
+ timeout_seconds: int | None,
144
+ ) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO | None:
145
+ start_ts = time.time()
146
+ base_url = settings.base_url
147
+ base_api_url = settings.get_base_api_url()
148
+ base_ws_url = settings.get_base_ws_url()
149
+
150
+ print_exponent_message(base_url, chat_uuid)
151
+ click.echo()
152
+
153
+ connection_tracker = ConnectionTracker()
154
+
155
+ client_fut = loop.create_task(
156
+ start_client(
157
+ api_key,
158
+ base_url,
159
+ base_api_url,
160
+ base_ws_url,
161
+ chat_uuid,
162
+ prompt=prompt,
163
+ workflow_id=workflow_id,
164
+ connection_tracker=connection_tracker,
165
+ timeout_seconds=timeout_seconds,
166
+ )
167
+ )
168
+
169
+ conn_fut = loop.create_task(handle_connection_changes(connection_tracker, start_ts))
170
+
171
+ try:
172
+ done, _ = loop.run_until_complete(
173
+ asyncio.wait({client_fut, conn_fut}, return_when=asyncio.FIRST_COMPLETED)
174
+ )
175
+
176
+ if client_fut in done:
177
+ return client_fut.result()
178
+ else:
179
+ return None
180
+ finally:
181
+ for task in asyncio.all_tasks(loop):
182
+ task.cancel()
183
+
184
+ try:
185
+ loop.run_until_complete(asyncio.wait(asyncio.all_tasks(loop)))
186
+ except asyncio.CancelledError:
187
+ pass
188
+
189
+
190
+ async def handle_connection_changes(
191
+ connection_tracker: ConnectionTracker, start_ts: float
192
+ ) -> None:
193
+ try:
194
+ async with timeout(5):
195
+ assert await connection_tracker.next_change()
196
+ click.echo(ready_message(start_ts))
197
+ except TimeoutError:
198
+ spinner = Spinner("Connecting...")
199
+ spinner.show()
200
+ assert await connection_tracker.next_change()
201
+ spinner.hide()
202
+ click.echo(ready_message(start_ts))
203
+
204
+ while True:
205
+ assert not await connection_tracker.next_change()
206
+
207
+ click.echo("Disconnected...", nl=False)
208
+ await asyncio.sleep(1)
209
+ spinner = Spinner("Reconnecting...")
210
+ spinner.show()
211
+ assert await connection_tracker.next_change()
212
+ spinner.hide()
213
+ click.echo("\x1b[1;32m✓ Reconnected", nl=False)
214
+ sys.stdout.flush()
215
+ await asyncio.sleep(1)
216
+ click.echo("\r\x1b[0m\x1b[2K", nl=False)
217
+ sys.stdout.flush()
218
+
219
+
220
+ def ready_message(start_ts: float) -> str:
221
+ elapsed = round(time.time() - start_ts, 2)
222
+ return f"\x1b[32m✓\x1b[0m Ready in {elapsed}s"
@@ -0,0 +1,56 @@
1
+ from collections.abc import Callable
2
+ from functools import wraps
3
+ from typing import Any
4
+
5
+ import click
6
+
7
+ from exponent.commands.utils import (
8
+ print_editable_install_forced_prod_warning,
9
+ print_editable_install_warning,
10
+ )
11
+ from exponent.core.config import (
12
+ Environment,
13
+ get_settings,
14
+ is_editable_install,
15
+ )
16
+
17
+
18
+ def use_settings(f: Callable[..., Any]) -> Callable[..., Any]:
19
+ @click.option(
20
+ "--prod",
21
+ is_flag=True,
22
+ hidden=True,
23
+ help="Use production URLs even if in editable mode",
24
+ )
25
+ @click.option(
26
+ "--staging",
27
+ is_flag=True,
28
+ hidden=True,
29
+ help="Use staging URLs even if in editable mode",
30
+ )
31
+ @wraps(f)
32
+ def decorated_function(*args: Any, **kwargs: Any) -> Any:
33
+ prod = kwargs.pop("prod", False)
34
+ staging = kwargs.pop("staging", False)
35
+ settings = get_settings(use_prod=prod, use_staging=staging)
36
+
37
+ if is_editable_install() and not (prod or staging):
38
+ assert settings.environment == Environment.development
39
+ print_editable_install_warning(settings)
40
+
41
+ return f(*args, settings=settings, **kwargs)
42
+
43
+ return decorated_function
44
+
45
+
46
+ def use_prod_settings(f: Callable[..., Any]) -> Callable[..., Any]:
47
+ @wraps(f)
48
+ def decorated_function(*args: Any, **kwargs: Any) -> Any:
49
+ settings = get_settings(use_prod=True)
50
+
51
+ if is_editable_install():
52
+ print_editable_install_forced_prod_warning(settings)
53
+
54
+ return f(*args, settings=settings, **kwargs)
55
+
56
+ return decorated_function