lyceum-cli 1.0.24__tar.gz → 1.0.26__tar.gz

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 (51) hide show
  1. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/PKG-INFO +1 -1
  2. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/auth/login.py +18 -18
  3. lyceum_cli-1.0.26/lyceum/external/compute/execution/docker_compose.py +263 -0
  4. lyceum_cli-1.0.26/lyceum/external/compute/execution/notebook.py +240 -0
  5. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/inference/batch.py +8 -10
  6. lyceum_cli-1.0.26/lyceum/external/vms/__init__.py +0 -0
  7. lyceum_cli-1.0.26/lyceum/external/vms/instances.py +301 -0
  8. lyceum_cli-1.0.26/lyceum/external/vms/management.py +383 -0
  9. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/main.py +5 -0
  10. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/shared/config.py +19 -24
  11. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/shared/display.py +12 -31
  12. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/shared/streaming.py +17 -45
  13. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cli.egg-info/PKG-INFO +1 -1
  14. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cli.egg-info/SOURCES.txt +6 -14
  15. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cli.egg-info/top_level.txt +0 -1
  16. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/setup.py +2 -2
  17. lyceum_cli-1.0.24/tests/__init__.py +0 -1
  18. lyceum_cli-1.0.24/tests/conftest.py +0 -200
  19. lyceum_cli-1.0.24/tests/unit/__init__.py +0 -1
  20. lyceum_cli-1.0.24/tests/unit/external/__init__.py +0 -1
  21. lyceum_cli-1.0.24/tests/unit/external/compute/__init__.py +0 -1
  22. lyceum_cli-1.0.24/tests/unit/external/compute/execution/__init__.py +0 -1
  23. lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_data.py +0 -33
  24. lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
  25. lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_python_helpers.py +0 -406
  26. lyceum_cli-1.0.24/tests/unit/external/compute/execution/test_python_run.py +0 -289
  27. lyceum_cli-1.0.24/tests/unit/shared/__init__.py +0 -1
  28. lyceum_cli-1.0.24/tests/unit/shared/test_config.py +0 -341
  29. lyceum_cli-1.0.24/tests/unit/shared/test_streaming.py +0 -259
  30. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/__init__.py +0 -0
  31. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/__init__.py +0 -0
  32. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/auth/__init__.py +0 -0
  33. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/__init__.py +0 -0
  34. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/execution/__init__.py +0 -0
  35. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/execution/config.py +0 -0
  36. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/execution/docker.py +0 -0
  37. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/execution/python.py +0 -0
  38. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/execution/workloads.py +0 -0
  39. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/inference/__init__.py +0 -0
  40. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/inference/chat.py +0 -0
  41. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/compute/inference/models.py +0 -0
  42. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/external/general/__init__.py +0 -0
  43. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/shared/__init__.py +0 -0
  44. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum/shared/imports.py +0 -0
  45. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cli.egg-info/dependency_links.txt +0 -0
  46. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cli.egg-info/entry_points.txt +0 -0
  47. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cli.egg-info/requires.txt +0 -0
  48. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cloud_execution_api_client/__init__.py +0 -0
  49. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cloud_execution_api_client/api/__init__.py +0 -0
  50. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/lyceum_cloud_execution_api_client/models/__init__.py +0 -0
  51. {lyceum_cli-1.0.24 → lyceum_cli-1.0.26}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lyceum-cli
3
- Version: 1.0.24
3
+ Version: 1.0.26
4
4
  Summary: Command-line interface for Lyceum Cloud Execution API
5
5
  Home-page: https://lyceum.technology
6
6
  Author: Lyceum Team
@@ -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]")
@@ -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
+ )
@@ -0,0 +1,240 @@
1
+ """
2
+ Jupyter Notebook execution commands
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import httpx
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from ....shared.config import config
12
+ from ....shared.streaming import StatusLine
13
+
14
+ console = Console()
15
+
16
+ notebook_app = typer.Typer(name="notebook", help="Launch Jupyter notebooks on Lyceum Cloud")
17
+
18
+ # Pre-built Jupyter notebook image (linux/amd64)
19
+ JUPYTER_IMAGE = "jupyter/base-notebook:latest"
20
+
21
+
22
+ @notebook_app.command("launch")
23
+ def launch_notebook(
24
+ machine_type: str = typer.Option(
25
+ "cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"
26
+ ),
27
+ timeout: int = typer.Option(
28
+ 600, "--timeout", "-t", help="Session timeout in seconds (max: 600)"
29
+ ),
30
+ image: Optional[str] = typer.Option(
31
+ None, "--image", "-i", help="Custom Jupyter image (default: jupyter/base-notebook)"
32
+ ),
33
+ token: str = typer.Option(
34
+ "lyceum", "--token", help="Jupyter notebook token for authentication"
35
+ ),
36
+ port: int = typer.Option(
37
+ 8888, "--port", "-p", help="Port for Jupyter server"
38
+ ),
39
+ ):
40
+ """Launch a Jupyter notebook server on Lyceum Cloud.
41
+
42
+ Starts a Jupyter notebook that you can access in your browser.
43
+ The notebook URL will be printed once the server is ready.
44
+
45
+ Examples:
46
+ lyceum notebook launch
47
+ lyceum notebook launch -m h100
48
+ lyceum notebook launch -m a100 --timeout 7200
49
+ lyceum notebook launch --image jupyter/scipy-notebook
50
+ """
51
+ status = StatusLine()
52
+
53
+ try:
54
+ config.get_client()
55
+
56
+ status.start()
57
+ status.update("Preparing notebook environment...")
58
+
59
+ jupyter_image = image or JUPYTER_IMAGE
60
+
61
+ # Build the Jupyter start command
62
+ # Using start-notebook.sh with custom options
63
+ jupyter_cmd = [
64
+ "start-notebook.sh",
65
+ f"--NotebookApp.token={token}",
66
+ f"--port={port}",
67
+ "--ip=0.0.0.0",
68
+ "--no-browser",
69
+ ]
70
+
71
+ # Build request for v2 image API
72
+ image_request = {
73
+ "docker_image_ref": jupyter_image,
74
+ "docker_run_cmd": jupyter_cmd,
75
+ "timeout": timeout,
76
+ "execution_type": machine_type,
77
+ }
78
+
79
+ status.update(f"Starting Jupyter on {machine_type}...")
80
+
81
+ response = httpx.post(
82
+ f"{config.base_url}/api/v2/external/execution/image/start",
83
+ json=image_request,
84
+ headers={"Authorization": f"Bearer {config.api_key}"},
85
+ timeout=30.0,
86
+ )
87
+
88
+ if response.status_code != 200:
89
+ status.stop()
90
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
91
+ if response.status_code == 401:
92
+ console.print(
93
+ "[red]Authentication failed. Your session may have expired.[/red]"
94
+ )
95
+ console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
96
+ elif response.content:
97
+ console.print(f"[red]{response.content.decode()}[/red]")
98
+ raise typer.Exit(1)
99
+
100
+ result = response.json()
101
+ execution_id = result.get("execution_id")
102
+
103
+ # Build the notebook URL immediately
104
+ notebook_url = f"https://{execution_id}-{port}.port.lyceum.technology"
105
+ full_url = f"{notebook_url}/?token={token}"
106
+
107
+ status.stop()
108
+
109
+ # Print URL immediately so user can click it
110
+ console.print()
111
+ console.print("[bold green]Notebook starting![/bold green]")
112
+ console.print()
113
+ console.print(f"[cyan]{full_url}[/cyan]")
114
+ console.print()
115
+ console.print(f"[dim]Execution ID:[/dim] {execution_id}")
116
+ console.print()
117
+ console.print("[dim]To stop:[/dim] lyceum notebook stop " + execution_id)
118
+
119
+ except typer.Exit:
120
+ status.stop()
121
+ raise
122
+ except Exception as e:
123
+ status.stop()
124
+ console.print(f"[red]Error: {e}[/red]")
125
+ raise typer.Exit(1)
126
+
127
+
128
+ @notebook_app.command("stop")
129
+ def stop_notebook(
130
+ execution_id: str = typer.Argument(..., help="Execution ID of the notebook to stop"),
131
+ ):
132
+ """Stop a running Jupyter notebook.
133
+
134
+ Examples:
135
+ lyceum notebook stop 9d73319c-6f1c-4b4c-90e4-044244353ce4
136
+ """
137
+ status = StatusLine()
138
+
139
+ try:
140
+ config.get_client()
141
+
142
+ status.start()
143
+ status.update("Stopping notebook...")
144
+
145
+ response = httpx.post(
146
+ f"{config.base_url}/api/v2/external/workloads/abort/{execution_id}",
147
+ headers={"Authorization": f"Bearer {config.api_key}"},
148
+ timeout=30.0,
149
+ )
150
+
151
+ status.stop()
152
+
153
+ if response.status_code == 200:
154
+ console.print(f"[green]Notebook {execution_id} stopped.[/green]")
155
+ elif response.status_code == 404:
156
+ console.print(f"[yellow]Notebook {execution_id} not found or already stopped.[/yellow]")
157
+ else:
158
+ console.print(f"[red]Error stopping notebook: HTTP {response.status_code}[/red]")
159
+ if response.content:
160
+ console.print(f"[red]{response.content.decode()}[/red]")
161
+ raise typer.Exit(1)
162
+
163
+ except typer.Exit:
164
+ status.stop()
165
+ raise
166
+ except Exception as e:
167
+ status.stop()
168
+ console.print(f"[red]Error: {e}[/red]")
169
+ raise typer.Exit(1)
170
+
171
+
172
+ @notebook_app.command("list")
173
+ def list_notebooks():
174
+ """List running Jupyter notebooks.
175
+
176
+ Shows all active notebook sessions with their URLs and status.
177
+ """
178
+ status = StatusLine()
179
+
180
+ try:
181
+ config.get_client()
182
+
183
+ status.start()
184
+ status.update("Fetching running notebooks...")
185
+
186
+ # Use the workloads API to get running executions
187
+ response = httpx.get(
188
+ f"{config.base_url}/api/v2/external/workloads/list",
189
+ headers={"Authorization": f"Bearer {config.api_key}"},
190
+ timeout=30.0,
191
+ )
192
+
193
+ status.stop()
194
+
195
+ if response.status_code != 200:
196
+ console.print(f"[red]Error: HTTP {response.status_code}[/red]")
197
+ if response.content:
198
+ console.print(f"[red]{response.content.decode()}[/red]")
199
+ raise typer.Exit(1)
200
+
201
+ executions = response.json()
202
+
203
+ # Filter for notebook executions (those running on port 8888 or with jupyter)
204
+ # Since we don't have image info, show all running executions
205
+ notebooks = [
206
+ e for e in executions
207
+ if e.get("status") in ["running", "pending", "queued", "starting"]
208
+ ]
209
+
210
+ if not notebooks:
211
+ console.print("[dim]No running workloads found.[/dim]")
212
+ return
213
+
214
+ console.print(f"[bold]Running Workloads ({len(notebooks)})[/bold]")
215
+ console.print()
216
+
217
+ for nb in notebooks:
218
+ exec_id = nb.get("execution_id", "unknown")
219
+ status_val = nb.get("status", "unknown")
220
+ created = nb.get("created_at", "")
221
+ file_name = nb.get("file_name", "")
222
+
223
+ # Assume port 8888 for Jupyter
224
+ url = f"https://{exec_id}-8888.port.lyceum.technology"
225
+
226
+ console.print(f"[cyan]{exec_id}[/cyan]")
227
+ if file_name:
228
+ console.print(f" Name: {file_name}")
229
+ console.print(f" Status: {status_val}")
230
+ console.print(f" URL: {url}")
231
+ if created:
232
+ console.print(f" Created: {created}")
233
+ console.print()
234
+
235
+ except typer.Exit:
236
+ raise
237
+ except Exception as e:
238
+ status.stop()
239
+ console.print(f"[red]Error: {e}[/red]")
240
+ raise typer.Exit(1)
@@ -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']
File without changes