lyceum-cli 1.0.24__py3-none-any.whl → 1.0.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.
- lyceum/external/auth/login.py +18 -18
- lyceum/external/compute/execution/docker_compose.py +263 -0
- lyceum/external/compute/execution/notebook.py +240 -0
- lyceum/external/compute/inference/batch.py +8 -10
- lyceum/external/vms/__init__.py +0 -0
- lyceum/external/vms/instances.py +301 -0
- lyceum/external/vms/management.py +383 -0
- lyceum/main.py +5 -0
- lyceum/shared/config.py +19 -24
- lyceum/shared/display.py +12 -31
- lyceum/shared/streaming.py +17 -45
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.26.dist-info}/METADATA +1 -1
- lyceum_cli-1.0.26.dist-info/RECORD +34 -0
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.26.dist-info}/WHEEL +1 -1
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.26.dist-info}/top_level.txt +0 -1
- lyceum_cli-1.0.24.dist-info/RECORD +0 -42
- tests/__init__.py +0 -1
- tests/conftest.py +0 -200
- tests/unit/__init__.py +0 -1
- tests/unit/external/__init__.py +0 -1
- tests/unit/external/compute/__init__.py +0 -1
- tests/unit/external/compute/execution/__init__.py +0 -1
- tests/unit/external/compute/execution/test_data.py +0 -33
- tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
- tests/unit/external/compute/execution/test_python_helpers.py +0 -406
- tests/unit/external/compute/execution/test_python_run.py +0 -289
- tests/unit/shared/__init__.py +0 -1
- tests/unit/shared/test_config.py +0 -341
- tests/unit/shared/test_streaming.py +0 -259
- {lyceum_cli-1.0.24.dist-info → lyceum_cli-1.0.26.dist-info}/entry_points.txt +0 -0
lyceum/external/auth/login.py
CHANGED
|
@@ -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]
|
|
341
|
+
console.print("[green]Successfully authenticated![/green]")
|
|
342
342
|
else:
|
|
343
|
-
console.print(f"[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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
440
|
+
console.print(f"[red]Authentication failed: {callback_result['error']}[/red]")
|
|
441
441
|
raise typer.Exit(1)
|
|
442
442
|
else:
|
|
443
|
-
console.print("[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]
|
|
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]
|
|
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]
|
|
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]
|
|
480
|
+
console.print("[green]API connection working[/green]")
|
|
481
481
|
else:
|
|
482
|
-
console.print(f"[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]
|
|
484
|
+
console.print(f"[red]API connection failed: {e}[/red]")
|
|
485
485
|
else:
|
|
486
|
-
console.print("[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
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|