lyceum-cli 1.0.25__py3-none-any.whl → 1.0.27__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 (34) hide show
  1. lyceum/external/auth/login.py +18 -18
  2. lyceum/external/compute/execution/docker.py +4 -2
  3. lyceum/external/compute/execution/docker_compose.py +263 -0
  4. lyceum/external/compute/execution/notebook.py +0 -2
  5. lyceum/external/compute/execution/python.py +2 -1
  6. lyceum/external/compute/inference/batch.py +8 -10
  7. lyceum/external/vms/instances.py +301 -0
  8. lyceum/external/vms/management.py +383 -0
  9. lyceum/main.py +3 -0
  10. lyceum/shared/config.py +19 -24
  11. lyceum/shared/display.py +12 -31
  12. lyceum/shared/streaming.py +17 -45
  13. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/METADATA +1 -1
  14. lyceum_cli-1.0.27.dist-info/RECORD +34 -0
  15. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/WHEEL +1 -1
  16. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/top_level.txt +0 -1
  17. lyceum/external/compute/execution/docker_config.py +0 -123
  18. lyceum/external/storage/files.py +0 -273
  19. lyceum_cli-1.0.25.dist-info/RECORD +0 -46
  20. tests/__init__.py +0 -1
  21. tests/conftest.py +0 -200
  22. tests/unit/__init__.py +0 -1
  23. tests/unit/external/__init__.py +0 -1
  24. tests/unit/external/compute/__init__.py +0 -1
  25. tests/unit/external/compute/execution/__init__.py +0 -1
  26. tests/unit/external/compute/execution/test_data.py +0 -33
  27. tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
  28. tests/unit/external/compute/execution/test_python_helpers.py +0 -406
  29. tests/unit/external/compute/execution/test_python_run.py +0 -289
  30. tests/unit/shared/__init__.py +0 -1
  31. tests/unit/shared/test_config.py +0 -341
  32. tests/unit/shared/test_streaming.py +0 -259
  33. /lyceum/external/{storage → vms}/__init__.py +0 -0
  34. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/entry_points.txt +0 -0
@@ -338,12 +338,12 @@ def login(
338
338
  headers = {"Authorization": f"Bearer {config.api_key}"}
339
339
  response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
340
340
  if response.status_code == 200:
341
- console.print("[green]Successfully authenticated![/green]")
341
+ console.print("[green]Successfully authenticated![/green]")
342
342
  else:
343
- console.print(f"[red]Authentication failed: HTTP {response.status_code}[/red]")
343
+ console.print(f"[red]Authentication failed: HTTP {response.status_code}[/red]")
344
344
  raise typer.Exit(1)
345
345
  except Exception as e:
346
- console.print(f"[red]Authentication failed: {e}[/red]")
346
+ console.print(f"[red]Authentication failed: {e}[/red]")
347
347
  raise typer.Exit(1)
348
348
  return
349
349
 
@@ -377,7 +377,7 @@ def login(
377
377
 
378
378
  # Open browser
379
379
  if not webbrowser.open(login_url):
380
- console.print("[yellow]⚠️ Could not open browser automatically[/yellow]")
380
+ console.print("[yellow]Could not open browser automatically[/yellow]")
381
381
  console.print(f"[yellow]Please manually open: {login_url}[/yellow]")
382
382
 
383
383
  console.print("[dim]Waiting for authentication... (timeout: 120 seconds)[/dim]")
@@ -402,7 +402,7 @@ def login(
402
402
  config.refresh_token = callback_result["refresh_token"]
403
403
  config.save()
404
404
 
405
- console.print("[green]Authentication token received![/green]")
405
+ console.print("[green]Authentication token received![/green]")
406
406
 
407
407
  # Test the connection using health endpoint
408
408
  try:
@@ -417,37 +417,37 @@ def login(
417
417
  headers=headers
418
418
  )
419
419
  if response.status_code == 200:
420
- console.print("[green]Successfully authenticated![/green]")
420
+ console.print("[green]Successfully authenticated![/green]")
421
421
  if callback_result.get("user"):
422
422
  console.print(f"[dim]Logged in as: {callback_result['user']}[/dim]")
423
423
  else:
424
- console.print(f"[red]Token validation failed: HTTP {response.status_code}[/red]")
424
+ console.print(f"[red]Token validation failed: HTTP {response.status_code}[/red]")
425
425
  console.print(f"[dim]Response: {response.text}[/dim]")
426
426
  raise typer.Exit(1)
427
427
  finally:
428
428
  client.close()
429
429
  except httpx.TimeoutException as e:
430
- console.print(f"[yellow]⚠️ Token validation timed out: {e}[/yellow]")
430
+ console.print(f"[yellow]Token validation timed out: {e}[/yellow]")
431
431
  console.print(f"[yellow]Token saved but couldn't verify connectivity to {config.base_url}[/yellow]")
432
432
  console.print("[dim]You can test the connection later with 'lyceum auth status'[/dim]")
433
433
  # Don't exit with error - token was received successfully
434
434
  except Exception as e:
435
- console.print(f"[red]Token validation failed: {e}[/red]")
435
+ console.print(f"[red]Token validation failed: {e}[/red]")
436
436
  console.print(f"[dim]Token saved but couldn't verify. Error type: {type(e).__name__}[/dim]")
437
437
  raise typer.Exit(1)
438
438
 
439
439
  elif callback_result["error"]:
440
- console.print(f"[red]Authentication failed: {callback_result['error']}[/red]")
440
+ console.print(f"[red]Authentication failed: {callback_result['error']}[/red]")
441
441
  raise typer.Exit(1)
442
442
  else:
443
- console.print("[red]Authentication timed out. Please try again.[/red]")
443
+ console.print("[red]Authentication timed out. Please try again.[/red]")
444
444
  raise typer.Exit(1)
445
445
 
446
446
  except KeyboardInterrupt:
447
447
  console.print("\n[yellow]Authentication cancelled by user.[/yellow]")
448
448
  raise typer.Exit(1)
449
449
  except Exception as e:
450
- console.print(f"[red]Authentication error: {e}[/red]")
450
+ console.print(f"[red]Authentication error: {e}[/red]")
451
451
  raise typer.Exit(1)
452
452
 
453
453
 
@@ -457,7 +457,7 @@ def logout():
457
457
  from ...shared.config import CONFIG_FILE
458
458
  if CONFIG_FILE.exists():
459
459
  CONFIG_FILE.unlink()
460
- console.print("[green]Logged out successfully![/green]")
460
+ console.print("[green]Logged out successfully![/green]")
461
461
 
462
462
 
463
463
  @auth_app.command("status")
@@ -468,7 +468,7 @@ def status():
468
468
  console.print(f"[dim]Base URL: {config.base_url}[/dim]")
469
469
 
470
470
  if config.api_key:
471
- console.print("[green]Authenticated[/green]")
471
+ console.print("[green]Authenticated[/green]")
472
472
  console.print(f"[dim]API Key: {config.api_key[:8]}...[/dim]")
473
473
 
474
474
  # Test connection
@@ -477,10 +477,10 @@ def status():
477
477
  headers = {"Authorization": f"Bearer {config.api_key}"}
478
478
  response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
479
479
  if response.status_code == 200:
480
- console.print("[green]API connection working[/green]")
480
+ console.print("[green]API connection working[/green]")
481
481
  else:
482
- console.print(f"[yellow]⚠️ API connection issues: HTTP {response.status_code}[/yellow]")
482
+ console.print(f"[yellow]API connection issues: HTTP {response.status_code}[/yellow]")
483
483
  except Exception as e:
484
- console.print(f"[red]API connection failed: {e}[/red]")
484
+ console.print(f"[red]API connection failed: {e}[/red]")
485
485
  else:
486
- console.print("[red]Not authenticated. Run 'lyceum login' first.[/red]")
486
+ console.print("[red]Not authenticated. Run 'lyceum auth login' first.[/red]")
@@ -183,7 +183,8 @@ def run_docker(
183
183
  console.print(f"[dim]To stream logs:[/dim] lyceum docker logs {execution_id}")
184
184
  else:
185
185
  # Default: stream output in real-time (like docker run)
186
- success = stream_execution_output(execution_id, streaming_url, status)
186
+ status.stop()
187
+ success = stream_execution_output(execution_id, streaming_url)
187
188
 
188
189
  # Show execution ID at the end
189
190
  console.print(f"[dim]Execution ID: {execution_id}[/dim]")
@@ -216,9 +217,10 @@ def docker_logs(
216
217
 
217
218
  status.start()
218
219
  status.update("Connecting to execution...")
220
+ status.stop()
219
221
 
220
222
  # Pass None for streaming_url to use the default v2 endpoint
221
- success = stream_execution_output(execution_id, None, status)
223
+ success = stream_execution_output(execution_id, None)
222
224
 
223
225
  console.print(f"[dim]Execution ID: {execution_id}[/dim]")
224
226
 
@@ -0,0 +1,263 @@
1
+ """
2
+ Docker Compose execution commands
3
+ """
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import httpx
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ from ....shared.config import config
14
+ from ....shared.streaming import StatusLine, stream_execution_output
15
+
16
+ console = Console()
17
+
18
+
19
+ compose_app = typer.Typer(name="compose", help="Docker Compose execution commands")
20
+
21
+
22
+ @compose_app.command("run")
23
+ def run_docker_compose(
24
+ compose_file: Path = typer.Argument(
25
+ ...,
26
+ help="Path to docker-compose.yml file",
27
+ exists=True,
28
+ file_okay=True,
29
+ dir_okay=False,
30
+ readable=True
31
+ ),
32
+ machine_type: str = typer.Option(
33
+ "cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"
34
+ ),
35
+ timeout: int = typer.Option(
36
+ 300, "--timeout", "-t", help="Execution timeout in seconds"
37
+ ),
38
+ file_name: Optional[str] = typer.Option(
39
+ None, "--file-name", "-f", help="Name for the execution"
40
+ ),
41
+ detach: bool = typer.Option(
42
+ False, "--detach", "-d", help="Run in background and print execution ID"
43
+ ),
44
+ callback_url: Optional[str] = typer.Option(
45
+ None, "--callback", help="Webhook URL for completion notification"
46
+ ),
47
+ registry_creds: Optional[str] = typer.Option(
48
+ None, "--registry-creds", help="Docker registry credentials as JSON string"
49
+ ),
50
+ registry_type: Optional[str] = typer.Option(
51
+ None, "--registry-type", help="Registry credential type: basic, aws, etc."
52
+ ),
53
+ ):
54
+ """Execute a Docker Compose application on Lyceum Cloud.
55
+
56
+ By default, streams container output in real-time.
57
+ Use --detach to run in background and return immediately.
58
+
59
+ Examples:
60
+ lyceum compose run docker-compose.yml
61
+ lyceum compose run ./app/docker-compose.yml -m a100
62
+ lyceum compose run compose.yml --registry-type basic --registry-creds '{"username":"user","password":"pass"}'
63
+ """
64
+ status = StatusLine()
65
+
66
+ try:
67
+ config.get_client()
68
+
69
+ status.start()
70
+ status.update("Validating configuration...")
71
+
72
+ # Read the compose file
73
+ try:
74
+ compose_file_content = compose_file.read_text()
75
+ except Exception as e:
76
+ status.stop()
77
+ console.print(f"[red]Error reading compose file: {e}[/red]")
78
+ raise typer.Exit(1)
79
+
80
+ # Parse registry credentials
81
+ registry_credentials = None
82
+ if registry_creds:
83
+ try:
84
+ registry_credentials = json.loads(registry_creds)
85
+ except json.JSONDecodeError:
86
+ status.stop()
87
+ console.print(
88
+ "[red]Error: Invalid JSON format for registry credentials[/red]"
89
+ )
90
+ raise typer.Exit(1)
91
+
92
+ # Validate registry credentials and type
93
+ if (registry_creds and not registry_type) or (
94
+ registry_type and not registry_creds
95
+ ):
96
+ status.stop()
97
+ console.print(
98
+ "[red]Error: Both --registry-creds and --registry-type must be provided together[/red]"
99
+ )
100
+ raise typer.Exit(1)
101
+
102
+ status.update(f"Starting Docker Compose from {compose_file.name}...")
103
+
104
+ # Build request for v2 compose API
105
+ compose_request = {
106
+ "compose_file_code": compose_file_content,
107
+ "timeout": timeout,
108
+ "execution_type": machine_type,
109
+ }
110
+
111
+ if file_name:
112
+ compose_request["file_name"] = file_name
113
+ else:
114
+ compose_request["file_name"] = compose_file.name
115
+
116
+ # Handle registry credentials
117
+ if registry_type and registry_credentials:
118
+ compose_request["docker_registry_credential_type"] = registry_type
119
+
120
+ if registry_type == "aws":
121
+ creds = registry_credentials
122
+ compose_request.update(
123
+ {
124
+ "aws_access_key_id": creds.get("aws_access_key_id"),
125
+ "aws_secret_access_key": creds.get("aws_secret_access_key"),
126
+ "aws_session_token": creds.get("aws_session_token"),
127
+ "aws_region": creds.get("region", "us-east-1"),
128
+ }
129
+ )
130
+ elif registry_type == "basic":
131
+ creds = registry_credentials
132
+ compose_request.update(
133
+ {
134
+ "docker_username": creds.get("username"),
135
+ "docker_password": creds.get("password"),
136
+ }
137
+ )
138
+
139
+ # Call the v2 compose endpoint
140
+ status.update("Submitting execution...")
141
+
142
+ response = httpx.post(
143
+ f"{config.base_url}/api/v2/external/execution/compose/start",
144
+ json=compose_request,
145
+ headers={"Authorization": f"Bearer {config.api_key}"},
146
+ timeout=30.0,
147
+ )
148
+
149
+ if response.status_code != 200:
150
+ status.stop()
151
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
152
+ if response.status_code == 401:
153
+ console.print(
154
+ "[red]Authentication failed. Your session may have expired.[/red]"
155
+ )
156
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
157
+ elif response.content:
158
+ console.print(f"[red]{response.content.decode()}[/red]")
159
+ raise typer.Exit(1)
160
+
161
+ result = response.json()
162
+ execution_id = result.get("execution_id")
163
+ streaming_url = result.get("streaming_url")
164
+
165
+ if detach:
166
+ # Detached mode - just return the execution info
167
+ status.stop()
168
+ console.print(f"[dim]Compose file: {compose_file.name}[/dim]")
169
+ console.print(f"[dim]Machine: {machine_type}[/dim]")
170
+ console.print(f"[dim]Execution ID: {execution_id}[/dim]")
171
+ console.print("")
172
+ console.print(f"[dim]To stream logs:[/dim] lyceum compose logs {execution_id}")
173
+ else:
174
+ # Default: stream output in real-time
175
+ status.stop()
176
+ success = stream_execution_output(execution_id, streaming_url)
177
+
178
+ # Show execution ID at the end
179
+ console.print(f"[dim]Execution ID: {execution_id}[/dim]")
180
+
181
+ if not success:
182
+ raise typer.Exit(1)
183
+
184
+ except typer.Exit:
185
+ status.stop()
186
+ raise
187
+ except Exception as e:
188
+ status.stop()
189
+ console.print(f"[red]Error: {e}[/red]")
190
+ raise typer.Exit(1)
191
+
192
+
193
+ @compose_app.command("logs")
194
+ def compose_logs(
195
+ execution_id: str = typer.Argument(..., help="Execution ID to stream logs from"),
196
+ ):
197
+ """Stream logs from a running or completed Docker Compose execution.
198
+
199
+ Examples:
200
+ lyceum compose logs 9d73319c-6f1c-4b4c-90e4-044244353ce4
201
+ """
202
+ status = StatusLine()
203
+
204
+ try:
205
+ config.get_client()
206
+
207
+ status.start()
208
+ status.update("Connecting to execution...")
209
+ status.stop()
210
+
211
+ # Pass None for streaming_url to use the default v2 endpoint
212
+ success = stream_execution_output(execution_id, None)
213
+
214
+ console.print(f"[dim]Execution ID: {execution_id}[/dim]")
215
+
216
+ if not success:
217
+ raise typer.Exit(1)
218
+
219
+ except typer.Exit:
220
+ status.stop()
221
+ raise
222
+ except Exception as e:
223
+ status.stop()
224
+ console.print(f"[red]Error: {e}[/red]")
225
+ raise typer.Exit(1)
226
+
227
+
228
+ @compose_app.command("registry-examples")
229
+ def show_registry_examples():
230
+ """Show examples of Docker registry credential formats"""
231
+ console.print("[bold cyan]Docker Registry Credential Examples[/bold cyan]\n")
232
+
233
+ console.print("[bold]1. Docker Hub (basic)[/bold]")
234
+ console.print("Type: [green]basic[/green]")
235
+ console.print(
236
+ 'Credentials: [yellow]\'{"username": "myuser", "password": "mypassword"}\'[/yellow]\n'
237
+ )
238
+
239
+ console.print("[bold]2. AWS ECR (aws)[/bold]")
240
+ console.print("Type: [green]aws[/green]")
241
+ console.print(
242
+ 'Credentials: [yellow]\'{"region": "us-west-2", "aws_access_key_id": "AKIAI...", "aws_secret_access_key": "wJalrX...", "session_token": "optional..."}\'[/yellow]\n'
243
+ )
244
+
245
+ console.print("[bold]3. Private Registry (basic)[/bold]")
246
+ console.print("Type: [green]basic[/green]")
247
+ console.print(
248
+ 'Credentials: [yellow]\'{"username": "admin", "password": "secret"}\'[/yellow]\n'
249
+ )
250
+
251
+ console.print("[bold]Example Commands:[/bold]")
252
+ console.print("# Docker Hub:")
253
+ console.print(
254
+ "[dim]lyceum compose run docker-compose.yml --registry-type basic --registry-creds '{\"username\": \"myuser\", \"password\": \"mytoken\"}'[/dim]"
255
+ )
256
+ console.print("\n# AWS ECR:")
257
+ console.print(
258
+ "[dim]lyceum compose run docker-compose.yml --registry-type aws --registry-creds '{\"region\": \"us-west-2\", \"aws_access_key_id\": \"AKIAI...\", \"aws_secret_access_key\": \"wJalrX...\"}'[/dim]"
259
+ )
260
+ console.print("\n# Private Registry:")
261
+ console.print(
262
+ "[dim]lyceum compose run docker-compose.yml --registry-type basic --registry-creds '{\"username\": \"admin\", \"password\": \"secret\"}'[/dim]"
263
+ )
@@ -238,5 +238,3 @@ def list_notebooks():
238
238
  status.stop()
239
239
  console.print(f"[red]Error: {e}[/red]")
240
240
  raise typer.Exit(1)
241
-
242
-
@@ -352,8 +352,9 @@ def run_python(
352
352
  status.start()
353
353
 
354
354
  execution_id, streaming_url = submit_execution(payload, status)
355
+ status.stop()
355
356
 
356
- success = stream_execution_output(execution_id, streaming_url, status)
357
+ success = stream_execution_output(execution_id, streaming_url)
357
358
 
358
359
  # Show execution ID at the end
359
360
  console.print(f"[dim]Execution ID: {execution_id}[/dim]")
@@ -6,9 +6,10 @@ from pathlib import Path
6
6
  import httpx
7
7
  import typer
8
8
  from rich.console import Console
9
+ from rich.table import Table
9
10
 
10
11
  from ....shared.config import config
11
- from ....shared.display import create_table, format_timestamp, truncate_id
12
+ from ....shared.display import format_timestamp, truncate_id
12
13
 
13
14
  console = Console()
14
15
 
@@ -218,15 +219,12 @@ def list_batches(
218
219
  console.print("[dim]No batch jobs found[/dim]")
219
220
  return
220
221
 
221
- columns = [
222
- {"header": "Batch ID", "style": "cyan", "no_wrap": True, "max_width": 16},
223
- {"header": "Status", "style": "yellow"},
224
- {"header": "Endpoint", "style": "green"},
225
- {"header": "Requests", "style": "magenta", "justify": "center"},
226
- {"header": "Created", "style": "dim"}
227
- ]
228
-
229
- table = create_table("Batch Jobs", columns)
222
+ table = Table(title="Batch Jobs")
223
+ table.add_column("Batch ID", style="cyan", no_wrap=True, max_width=16)
224
+ table.add_column("Status", style="yellow")
225
+ table.add_column("Endpoint", style="green")
226
+ table.add_column("Requests", style="magenta", justify="center")
227
+ table.add_column("Created", style="dim")
230
228
 
231
229
  for batch in batches:
232
230
  batch_id = batch['id']