indent 0.1.11__py3-none-any.whl → 0.1.12__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.

Potentially problematic release.


This version of indent might be problematic. Click here for more details.

exponent/__init__.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.11'
32
- __version_tuple__ = version_tuple = (0, 1, 11)
31
+ __version__ = version = '0.1.12'
32
+ __version_tuple__ = version_tuple = (0, 1, 12)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import sys
3
+ from typing import Any, cast
3
4
 
4
5
  import click
5
6
 
@@ -18,6 +19,15 @@ from exponent.commands.utils import (
18
19
  print_exponent_message,
19
20
  )
20
21
  from exponent.core.config import Settings
22
+ from exponent.core.graphql.client import GraphQLClient
23
+ from exponent.core.graphql.mutations import (
24
+ CREATE_CLOUD_CHAT_FROM_REPOSITORY_MUTATION,
25
+ ENABLE_CLOUD_REPOSITORY_MUTATION,
26
+ INCREMENTAL_BUILD_CLOUD_REPOSITORY_MUTATION,
27
+ REBUILD_CLOUD_REPOSITORY_MUTATION,
28
+ START_CHAT_TURN_MUTATION,
29
+ )
30
+ from exponent.core.graphql.queries import GITHUB_REPOSITORIES_QUERY
21
31
  from exponent.utils.version import check_exponent_version_and_upgrade
22
32
 
23
33
 
@@ -26,6 +36,578 @@ def cloud_cli() -> None:
26
36
  pass
27
37
 
28
38
 
39
+ async def enable_cloud_repository(
40
+ api_key: str,
41
+ base_api_url: str,
42
+ base_ws_url: str,
43
+ org_name: str,
44
+ repo_name: str,
45
+ ) -> dict[str, Any]:
46
+ graphql_client = GraphQLClient(
47
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
48
+ )
49
+
50
+ variables = {
51
+ "orgName": org_name,
52
+ "repoName": repo_name,
53
+ }
54
+
55
+ result = await graphql_client.execute(
56
+ ENABLE_CLOUD_REPOSITORY_MUTATION,
57
+ variables,
58
+ "EnableCloudRepository",
59
+ timeout=120,
60
+ )
61
+
62
+ return cast(dict[str, Any], result["enableCloudRepository"])
63
+
64
+
65
+ async def incremental_build_cloud_repository(
66
+ api_key: str,
67
+ base_api_url: str,
68
+ base_ws_url: str,
69
+ org_name: str,
70
+ repo_name: str,
71
+ ) -> dict[str, Any]:
72
+ graphql_client = GraphQLClient(
73
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
74
+ )
75
+
76
+ variables = {
77
+ "orgName": org_name,
78
+ "repoName": repo_name,
79
+ }
80
+
81
+ result = await graphql_client.execute(
82
+ INCREMENTAL_BUILD_CLOUD_REPOSITORY_MUTATION,
83
+ variables,
84
+ "IncrementalBuildCloudRepository",
85
+ timeout=120,
86
+ )
87
+
88
+ return cast(dict[str, Any], result["incrementalBuildCloudRepository"])
89
+
90
+
91
+ async def rebuild_cloud_repository(
92
+ api_key: str,
93
+ base_api_url: str,
94
+ base_ws_url: str,
95
+ org_name: str,
96
+ repo_name: str,
97
+ ) -> dict[str, Any]:
98
+ graphql_client = GraphQLClient(
99
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
100
+ )
101
+
102
+ variables = {
103
+ "orgName": org_name,
104
+ "repoName": repo_name,
105
+ }
106
+
107
+ result = await graphql_client.execute(
108
+ REBUILD_CLOUD_REPOSITORY_MUTATION,
109
+ variables,
110
+ "RebuildCloudRepository",
111
+ timeout=120,
112
+ )
113
+
114
+ return cast(dict[str, Any], result["rebuildCloudRepository"])
115
+
116
+
117
+ async def list_github_repositories(
118
+ api_key: str,
119
+ base_api_url: str,
120
+ base_ws_url: str,
121
+ ) -> dict[str, Any]:
122
+ graphql_client = GraphQLClient(
123
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
124
+ )
125
+
126
+ result = await graphql_client.execute(
127
+ GITHUB_REPOSITORIES_QUERY,
128
+ None,
129
+ "GithubRepositories",
130
+ timeout=120,
131
+ )
132
+
133
+ return cast(dict[str, Any], result["githubRepositories"])
134
+
135
+
136
+ async def create_cloud_chat_from_repository(
137
+ api_key: str,
138
+ base_api_url: str,
139
+ base_ws_url: str,
140
+ repository_id: str,
141
+ ) -> dict[str, Any]:
142
+ graphql_client = GraphQLClient(
143
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
144
+ )
145
+
146
+ variables = {
147
+ "repositoryId": repository_id,
148
+ }
149
+
150
+ result = await graphql_client.execute(
151
+ CREATE_CLOUD_CHAT_FROM_REPOSITORY_MUTATION,
152
+ variables,
153
+ "CreateCloudChatFromRepository",
154
+ timeout=120,
155
+ )
156
+
157
+ return cast(dict[str, Any], result["createCloudChat"])
158
+
159
+
160
+ async def start_chat_turn_with_prompt(
161
+ api_key: str,
162
+ base_api_url: str,
163
+ base_ws_url: str,
164
+ chat_uuid: str,
165
+ prompt: str,
166
+ ) -> dict[str, Any]:
167
+ graphql_client = GraphQLClient(
168
+ api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
169
+ )
170
+
171
+ variables = {
172
+ "chatInput": {"prompt": {"message": prompt, "attachments": []}},
173
+ "parentUuid": None,
174
+ "chatConfig": {
175
+ "chatUuid": chat_uuid,
176
+ "exponentModel": "PREMIUM",
177
+ "requireConfirmation": False,
178
+ "readOnly": False,
179
+ "depthLimit": 20,
180
+ },
181
+ }
182
+
183
+ result = await graphql_client.execute(
184
+ START_CHAT_TURN_MUTATION, variables, "StartChatTurnMutation", timeout=120
185
+ )
186
+
187
+ return cast(dict[str, Any], result["startChatReply"])
188
+
189
+
190
+ @cloud_cli.command(hidden=True)
191
+ @click.option(
192
+ "--org-name",
193
+ help="GitHub organization name",
194
+ required=True,
195
+ )
196
+ @click.option(
197
+ "--repo-name",
198
+ help="GitHub repository name",
199
+ required=True,
200
+ )
201
+ @use_settings
202
+ def enable_repo(
203
+ settings: Settings,
204
+ org_name: str,
205
+ repo_name: str,
206
+ ) -> None:
207
+ """Test utility for enabling cloud repository."""
208
+ check_exponent_version_and_upgrade(settings)
209
+
210
+ if not settings.api_key:
211
+ redirect_to_login(settings)
212
+ return
213
+
214
+ loop = asyncio.get_event_loop()
215
+
216
+ api_key = settings.api_key
217
+ base_api_url = settings.get_base_api_url()
218
+ base_ws_url = settings.get_base_ws_url()
219
+
220
+ try:
221
+ result = loop.run_until_complete(
222
+ enable_cloud_repository(
223
+ api_key, base_api_url, base_ws_url, org_name, repo_name
224
+ )
225
+ )
226
+
227
+ if result["__typename"] == "ContainerImage":
228
+ click.secho(
229
+ f"✓ Successfully enabled repository {org_name}/{repo_name}", fg="green"
230
+ )
231
+ click.echo(f" Build ref: {result.get('buildRef', 'N/A')}")
232
+ click.echo(f" Created at: {result.get('createdAt', 'N/A')}")
233
+ click.echo(f" Updated at: {result.get('updatedAt', 'N/A')}")
234
+ else:
235
+ click.secho(
236
+ f"✗ Failed to enable repository: {result.get('message', 'Unknown error')}",
237
+ fg="red",
238
+ )
239
+ click.echo(f" Error type: {result['__typename']}")
240
+
241
+ except Exception as e:
242
+ click.secho(f"✗ Error enabling repository: {e!s}", fg="red")
243
+ sys.exit(1)
244
+
245
+
246
+ @cloud_cli.command(hidden=True)
247
+ @click.option(
248
+ "--org-name",
249
+ help="GitHub organization name",
250
+ required=True,
251
+ )
252
+ @click.option(
253
+ "--repo-name",
254
+ help="GitHub repository name",
255
+ required=True,
256
+ )
257
+ @use_settings
258
+ def incremental_build(
259
+ settings: Settings,
260
+ org_name: str,
261
+ repo_name: str,
262
+ ) -> None:
263
+ """Test utility for incremental build of cloud repository."""
264
+ check_exponent_version_and_upgrade(settings)
265
+
266
+ if not settings.api_key:
267
+ redirect_to_login(settings)
268
+ return
269
+
270
+ loop = asyncio.get_event_loop()
271
+
272
+ api_key = settings.api_key
273
+ base_api_url = settings.get_base_api_url()
274
+ base_ws_url = settings.get_base_ws_url()
275
+
276
+ try:
277
+ result = loop.run_until_complete(
278
+ incremental_build_cloud_repository(
279
+ api_key, base_api_url, base_ws_url, org_name, repo_name
280
+ )
281
+ )
282
+
283
+ if result["__typename"] == "ContainerImage":
284
+ click.secho(
285
+ f"✓ Successfully triggered incremental build for {org_name}/{repo_name}",
286
+ fg="green",
287
+ )
288
+ click.echo(f" Build ref: {result.get('buildRef', 'N/A')}")
289
+ click.echo(f" Created at: {result.get('createdAt', 'N/A')}")
290
+ click.echo(f" Updated at: {result.get('updatedAt', 'N/A')}")
291
+ else:
292
+ click.secho(
293
+ f"✗ Failed to trigger incremental build: {result.get('message', 'Unknown error')}",
294
+ fg="red",
295
+ )
296
+ click.echo(f" Error type: {result['__typename']}")
297
+
298
+ except Exception as e:
299
+ click.secho(f"✗ Error triggering incremental build: {e!s}", fg="red")
300
+ sys.exit(1)
301
+
302
+
303
+ @cloud_cli.command(hidden=True)
304
+ @click.option(
305
+ "--org-name",
306
+ help="GitHub organization name",
307
+ required=True,
308
+ )
309
+ @click.option(
310
+ "--repo-name",
311
+ help="GitHub repository name",
312
+ required=True,
313
+ )
314
+ @use_settings
315
+ def rebuild(
316
+ settings: Settings,
317
+ org_name: str,
318
+ repo_name: str,
319
+ ) -> None:
320
+ """Test utility for full rebuild of cloud repository."""
321
+ check_exponent_version_and_upgrade(settings)
322
+
323
+ if not settings.api_key:
324
+ redirect_to_login(settings)
325
+ return
326
+
327
+ loop = asyncio.get_event_loop()
328
+
329
+ api_key = settings.api_key
330
+ base_api_url = settings.get_base_api_url()
331
+ base_ws_url = settings.get_base_ws_url()
332
+
333
+ try:
334
+ result = loop.run_until_complete(
335
+ rebuild_cloud_repository(
336
+ api_key, base_api_url, base_ws_url, org_name, repo_name
337
+ )
338
+ )
339
+
340
+ if result["__typename"] == "ContainerImage":
341
+ click.secho(
342
+ f"✓ Successfully triggered rebuild for {org_name}/{repo_name}",
343
+ fg="green",
344
+ )
345
+ click.echo(f" Build ref: {result.get('buildRef', 'N/A')}")
346
+ click.echo(f" Created at: {result.get('createdAt', 'N/A')}")
347
+ click.echo(f" Updated at: {result.get('updatedAt', 'N/A')}")
348
+ else:
349
+ click.secho(
350
+ f"✗ Failed to trigger rebuild: {result.get('message', 'Unknown error')}",
351
+ fg="red",
352
+ )
353
+ click.echo(f" Error type: {result['__typename']}")
354
+
355
+ except Exception as e:
356
+ click.secho(f"✗ Error triggering rebuild: {e!s}", fg="red")
357
+ sys.exit(1)
358
+
359
+
360
+ @cloud_cli.command(hidden=True)
361
+ @use_settings
362
+ def list_repos(
363
+ settings: Settings,
364
+ ) -> None:
365
+ """Test utility for listing GitHub repositories."""
366
+ check_exponent_version_and_upgrade(settings)
367
+
368
+ if not settings.api_key:
369
+ redirect_to_login(settings)
370
+ return
371
+
372
+ loop = asyncio.get_event_loop()
373
+
374
+ api_key = settings.api_key
375
+ base_api_url = settings.get_base_api_url()
376
+ base_ws_url = settings.get_base_ws_url()
377
+
378
+ try:
379
+ result = loop.run_until_complete(
380
+ list_github_repositories(api_key, base_api_url, base_ws_url)
381
+ )
382
+
383
+ if result["__typename"] == "GithubRepositories":
384
+ repositories = result.get("repositories", [])
385
+ if repositories:
386
+ click.secho(f"✓ Found {len(repositories)} repositories:", fg="green")
387
+ for repo in repositories:
388
+ click.echo(
389
+ f"\n Repository: {repo['githubOrgName']}/{repo['githubRepoName']}"
390
+ )
391
+ click.echo(f" ID: {repo['id']}")
392
+ if repo.get("baseHost"):
393
+ click.echo(f" Base Host: {repo['baseHost']}")
394
+ if repo.get("containerImageId"):
395
+ click.echo(
396
+ f" Container Image ID: {repo['containerImageId']}"
397
+ )
398
+ click.echo(f" Created: {repo['createdAt']}")
399
+ click.echo(f" Updated: {repo['updatedAt']}")
400
+ else:
401
+ click.secho("No repositories found", fg="yellow")
402
+ else:
403
+ click.secho(
404
+ f"✗ Failed to list repositories: {result.get('message', 'Unknown error')}",
405
+ fg="red",
406
+ )
407
+ click.echo(f" Error type: {result['__typename']}")
408
+
409
+ except Exception as e:
410
+ click.secho(f"✗ Error listing repositories: {e!s}", fg="red")
411
+ sys.exit(1)
412
+
413
+
414
+ def filter_repositories(
415
+ repositories: list[dict[str, Any]], org_name: str | None, repo_name: str | None
416
+ ) -> list[dict[str, Any]]:
417
+ """Filter repositories by organization and/or repository name."""
418
+ if not (org_name or repo_name):
419
+ return repositories
420
+
421
+ filtered = []
422
+ for repo in repositories:
423
+ if org_name and repo["githubOrgName"] != org_name:
424
+ continue
425
+ if repo_name and repo["githubRepoName"] != repo_name:
426
+ continue
427
+ filtered.append(repo)
428
+
429
+ return filtered
430
+
431
+
432
+ def select_repository_interactive(repositories: list[dict[str, Any]]) -> dict[str, Any]:
433
+ """Interactively select a repository from a list."""
434
+ if len(repositories) == 1:
435
+ selected = repositories[0]
436
+ click.secho(
437
+ f"Using repository: {selected['githubOrgName']}/{selected['githubRepoName']}",
438
+ fg="cyan",
439
+ )
440
+ return selected
441
+
442
+ # Show numbered list for selection
443
+ click.secho("Available repositories:", fg="cyan")
444
+ for i, repo in enumerate(repositories, 1):
445
+ click.echo(f" {i}. {repo['githubOrgName']}/{repo['githubRepoName']}")
446
+
447
+ # Get user selection
448
+ while True:
449
+ try:
450
+ choice = click.prompt("Select a repository (number)", type=int)
451
+ if 1 <= choice <= len(repositories):
452
+ return cast(dict[str, Any], repositories[choice - 1])
453
+ else:
454
+ click.secho(
455
+ f"Please enter a number between 1 and {len(repositories)}",
456
+ fg="red",
457
+ )
458
+ except (ValueError, KeyboardInterrupt):
459
+ click.secho("\nCancelled", fg="yellow")
460
+ sys.exit(0)
461
+
462
+
463
+ def send_initial_prompt(
464
+ loop: asyncio.AbstractEventLoop,
465
+ api_key: str,
466
+ base_api_url: str,
467
+ base_ws_url: str,
468
+ chat_uuid: str,
469
+ prompt: str,
470
+ ) -> None:
471
+ """Send an initial prompt to the chat if provided."""
472
+ click.secho(
473
+ f"\nSending initial prompt: {prompt[:50]}{'...' if len(prompt) > 50 else ''}",
474
+ fg="cyan",
475
+ )
476
+
477
+ prompt_result = loop.run_until_complete(
478
+ start_chat_turn_with_prompt(
479
+ api_key, base_api_url, base_ws_url, chat_uuid, prompt
480
+ )
481
+ )
482
+
483
+ if prompt_result["__typename"] == "Chat":
484
+ click.secho("✓ Prompt sent successfully", fg="green")
485
+ else:
486
+ click.secho(
487
+ f"⚠ Failed to send prompt: {prompt_result.get('message', 'Unknown error')}",
488
+ fg="yellow",
489
+ )
490
+ click.echo(f" Error type: {prompt_result['__typename']}")
491
+
492
+
493
+ def fetch_repositories(
494
+ loop: asyncio.AbstractEventLoop,
495
+ api_key: str,
496
+ base_api_url: str,
497
+ base_ws_url: str,
498
+ ) -> list[dict[str, Any]]:
499
+ """Fetch the list of GitHub repositories."""
500
+ result = loop.run_until_complete(
501
+ list_github_repositories(api_key, base_api_url, base_ws_url)
502
+ )
503
+
504
+ if result["__typename"] != "GithubRepositories":
505
+ click.secho(
506
+ f"✗ Failed to list repositories: {result.get('message', 'Unknown error')}",
507
+ fg="red",
508
+ )
509
+ sys.exit(1)
510
+
511
+ repositories = result.get("repositories", [])
512
+ if not repositories:
513
+ click.secho("No repositories found", fg="yellow")
514
+ sys.exit(1)
515
+
516
+ return cast(list[dict[str, Any]], repositories)
517
+
518
+
519
+ @cloud_cli.command(hidden=True)
520
+ @click.option(
521
+ "--org-name",
522
+ help="GitHub organization name (optional, for filtering)",
523
+ required=False,
524
+ )
525
+ @click.option(
526
+ "--repo-name",
527
+ help="GitHub repository name (optional, for direct selection)",
528
+ required=False,
529
+ )
530
+ @click.option(
531
+ "--prompt",
532
+ help="Initial prompt to send to the chat after creation",
533
+ required=False,
534
+ )
535
+ @use_settings
536
+ def create_chat(
537
+ settings: Settings,
538
+ org_name: str | None,
539
+ repo_name: str | None,
540
+ prompt: str | None,
541
+ ) -> None:
542
+ """Create a cloud chat for a GitHub repository with optional initial prompt."""
543
+ check_exponent_version_and_upgrade(settings)
544
+
545
+ if not settings.api_key:
546
+ redirect_to_login(settings)
547
+ return
548
+
549
+ loop = asyncio.get_event_loop()
550
+ api_key = settings.api_key
551
+ base_url = settings.base_url
552
+ base_api_url = settings.get_base_api_url()
553
+ base_ws_url = settings.get_base_ws_url()
554
+
555
+ try:
556
+ # Fetch repositories
557
+ repositories = fetch_repositories(loop, api_key, base_api_url, base_ws_url)
558
+
559
+ # Filter if criteria provided
560
+ filtered_repos = filter_repositories(repositories, org_name, repo_name)
561
+
562
+ if not filtered_repos:
563
+ click.secho(
564
+ f"No repositories found matching {org_name}/{repo_name or '*'}",
565
+ fg="yellow",
566
+ )
567
+ sys.exit(1)
568
+
569
+ # Select repository
570
+ selected_repo = select_repository_interactive(filtered_repos)
571
+
572
+ # Create cloud chat
573
+ click.secho(
574
+ f"\nCreating cloud chat for {selected_repo['githubOrgName']}/{selected_repo['githubRepoName']}...",
575
+ fg="cyan",
576
+ )
577
+
578
+ chat_result = loop.run_until_complete(
579
+ create_cloud_chat_from_repository(
580
+ api_key, base_api_url, base_ws_url, selected_repo["id"]
581
+ )
582
+ )
583
+
584
+ if chat_result["__typename"] != "Chat":
585
+ click.secho(
586
+ f"✗ Failed to create cloud chat: {chat_result.get('message', 'Unknown error')}",
587
+ fg="red",
588
+ )
589
+ click.echo(f" Error type: {chat_result['__typename']}")
590
+ sys.exit(1)
591
+
592
+ # Success - handle chat creation
593
+ chat_uuid = chat_result["chatUuid"]
594
+ click.secho(f"✓ Successfully created cloud chat: {chat_uuid}", fg="green")
595
+ click.echo(f"\nChat URL: {base_url}/chats/{chat_uuid}")
596
+
597
+ # Send initial prompt if provided
598
+ if prompt:
599
+ send_initial_prompt(
600
+ loop, api_key, base_api_url, base_ws_url, chat_uuid, prompt
601
+ )
602
+
603
+ # Open browser
604
+ launch_exponent_browser(settings.environment, base_url, chat_uuid)
605
+
606
+ except Exception as e:
607
+ click.secho(f"✗ Error creating cloud chat: {e!s}", fg="red")
608
+ sys.exit(1)
609
+
610
+
29
611
  @cloud_cli.command(hidden=True)
30
612
  @click.option(
31
613
  "--cloud-config-id",
@@ -224,14 +224,6 @@ def run_until_complete(coro: Coroutine[Any, Any, Any]) -> Any:
224
224
  sys.exit(1)
225
225
 
226
226
 
227
- async def run_client_connection(
228
- client: RemoteExecutionClient,
229
- chat_uuid: str,
230
- connection_tracker: ConnectionTracker | None = None,
231
- ) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
232
- return await client.run_connection(chat_uuid, connection_tracker)
233
-
234
-
235
227
  async def create_cloud_chat(
236
228
  api_key: str, base_api_url: str, base_ws_url: str, config_uuid: str
237
229
  ) -> str:
@@ -312,6 +304,7 @@ async def start_client(
312
304
  prompt: str | None = None,
313
305
  workflow_id: str | None = None,
314
306
  connection_tracker: ConnectionTracker | None = None,
307
+ timeout_seconds: int | None = None,
315
308
  ) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
316
309
  async with RemoteExecutionClient.session(
317
310
  api_key=api_key,
@@ -320,7 +313,9 @@ async def start_client(
320
313
  working_directory=os.getcwd(),
321
314
  file_cache=file_cache,
322
315
  ) as client:
323
- main_coro = run_client_connection(client, chat_uuid, connection_tracker)
316
+ main_coro = client.run_connection(
317
+ chat_uuid, connection_tracker, timeout_seconds
318
+ )
324
319
  aux_coros: list[Coroutine[Any, Any, None]] = []
325
320
 
326
321
  if prompt: