lyceum-cli 1.0.21__py3-none-any.whl → 1.0.23__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/__init__.py +1 -0
- lyceum/external/__init__.py +1 -0
- lyceum/external/auth/__init__.py +1 -0
- lyceum/external/auth/login.py +68 -60
- lyceum/external/compute/__init__.py +1 -0
- lyceum/external/compute/execution/__init__.py +1 -0
- lyceum/external/compute/execution/python.py +20 -16
- lyceum/external/compute/inference/batch.py +73 -64
- lyceum/external/compute/inference/chat.py +36 -41
- lyceum/external/compute/inference/models.py +20 -14
- lyceum/external/general/__init__.py +1 -0
- lyceum/external/vms/__init__.py +0 -0
- lyceum/external/vms/instances.py +303 -0
- lyceum/external/vms/management.py +253 -0
- lyceum/main.py +3 -1
- lyceum/shared/__init__.py +1 -0
- lyceum/shared/config.py +37 -32
- lyceum/shared/display.py +3 -5
- lyceum/shared/streaming.py +60 -44
- {lyceum_cli-1.0.21.dist-info → lyceum_cli-1.0.23.dist-info}/METADATA +1 -1
- lyceum_cli-1.0.23.dist-info/RECORD +28 -0
- lyceum_cloud_execution_api_client/__init__.py +1 -0
- lyceum_cloud_execution_api_client/api/__init__.py +1 -0
- lyceum_cli-1.0.21.dist-info/RECORD +0 -25
- {lyceum_cli-1.0.21.dist-info → lyceum_cli-1.0.23.dist-info}/WHEEL +0 -0
- {lyceum_cli-1.0.21.dist-info → lyceum_cli-1.0.23.dist-info}/entry_points.txt +0 -0
- {lyceum_cli-1.0.21.dist-info → lyceum_cli-1.0.23.dist-info}/top_level.txt +0 -0
lyceum/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
lyceum/external/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
lyceum/external/auth/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
lyceum/external/auth/login.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Authentication commands: login, logout, status
|
|
3
|
-
"""
|
|
1
|
+
"""Authentication commands: login, logout, status"""
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
import webbrowser
|
|
3
|
+
import socket
|
|
9
4
|
import threading
|
|
10
5
|
import time
|
|
11
|
-
|
|
12
|
-
from
|
|
13
|
-
import
|
|
6
|
+
import webbrowser
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
8
|
+
from urllib.parse import parse_qs, urlparse
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
14
12
|
|
|
15
13
|
from ...shared.config import config
|
|
16
14
|
|
|
@@ -24,37 +22,38 @@ callback_result = {"token": None, "error": None, "received": False}
|
|
|
24
22
|
|
|
25
23
|
class CallbackHandler(BaseHTTPRequestHandler):
|
|
26
24
|
"""HTTP handler for OAuth callback"""
|
|
27
|
-
|
|
25
|
+
|
|
28
26
|
def log_message(self, format, *args):
|
|
29
|
-
|
|
27
|
+
"""Suppress HTTP server logs"""
|
|
30
28
|
pass
|
|
31
|
-
|
|
29
|
+
|
|
32
30
|
def do_GET(self):
|
|
31
|
+
"""Handle GET request for OAuth callback"""
|
|
33
32
|
global callback_result
|
|
34
|
-
|
|
33
|
+
|
|
35
34
|
try:
|
|
36
35
|
# Parse the callback URL
|
|
37
36
|
parsed_url = urlparse(self.path)
|
|
38
37
|
query_params = parse_qs(parsed_url.query)
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
if parsed_url.path == "/callback":
|
|
41
40
|
# Extract token from query parameters
|
|
42
41
|
if "token" in query_params:
|
|
43
42
|
token = query_params["token"][0]
|
|
44
43
|
user_info = query_params.get("user", [None])[0]
|
|
45
44
|
refresh_token = query_params.get("refresh_token", [None])[0]
|
|
46
|
-
|
|
45
|
+
|
|
47
46
|
callback_result["token"] = token
|
|
48
47
|
callback_result["user"] = user_info
|
|
49
48
|
if refresh_token:
|
|
50
49
|
callback_result["refresh_token"] = refresh_token
|
|
51
50
|
callback_result["received"] = True
|
|
52
|
-
|
|
51
|
+
|
|
53
52
|
# Send success response
|
|
54
53
|
self.send_response(200)
|
|
55
54
|
self.send_header("Content-type", "text/html")
|
|
56
55
|
self.end_headers()
|
|
57
|
-
|
|
56
|
+
|
|
58
57
|
success_html = """
|
|
59
58
|
<!DOCTYPE html>
|
|
60
59
|
<html lang="en">
|
|
@@ -73,31 +72,31 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
73
72
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
74
73
|
</svg>
|
|
75
74
|
</div>
|
|
76
|
-
|
|
75
|
+
|
|
77
76
|
<!-- Header -->
|
|
78
77
|
<div class="mb-6">
|
|
79
78
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">Authentication Successful!</h1>
|
|
80
79
|
<p class="text-gray-600 text-lg">Welcome to Lyceum</p>
|
|
81
80
|
</div>
|
|
82
|
-
|
|
81
|
+
|
|
83
82
|
<!-- Instructions -->
|
|
84
83
|
<div class="space-y-3 mb-8">
|
|
85
84
|
<p class="text-gray-700">You can now close this browser tab and return to the CLI.</p>
|
|
86
85
|
<p class="text-sm text-gray-500">Your Lyceum CLI has been authenticated successfully and is ready to use.</p>
|
|
87
86
|
</div>
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
<!-- Close Message -->
|
|
90
89
|
<div class="w-full py-3 px-4 bg-gray-50 text-gray-700 rounded-md border border-gray-200">
|
|
91
90
|
You can close this window now
|
|
92
91
|
</div>
|
|
93
|
-
|
|
92
|
+
|
|
94
93
|
<!-- Lyceum Branding -->
|
|
95
94
|
<div class="mt-8 pt-6 border-t border-gray-200">
|
|
96
95
|
<p class="text-xs text-gray-400">Powered by Lyceum Technology</p>
|
|
97
96
|
</div>
|
|
98
97
|
</div>
|
|
99
98
|
</div>
|
|
100
|
-
|
|
99
|
+
|
|
101
100
|
<!-- Auto-close script -->
|
|
102
101
|
<script>
|
|
103
102
|
// Auto-close after 10 seconds
|
|
@@ -113,17 +112,17 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
113
112
|
except (BrokenPipeError, ConnectionResetError):
|
|
114
113
|
# Browser closed connection (expected when tab auto-closes)
|
|
115
114
|
pass
|
|
116
|
-
|
|
115
|
+
|
|
117
116
|
elif "error" in query_params:
|
|
118
117
|
error = query_params["error"][0]
|
|
119
118
|
callback_result["error"] = error
|
|
120
119
|
callback_result["received"] = True
|
|
121
|
-
|
|
120
|
+
|
|
122
121
|
# Send error response
|
|
123
122
|
self.send_response(400)
|
|
124
123
|
self.send_header("Content-type", "text/html")
|
|
125
124
|
self.end_headers()
|
|
126
|
-
|
|
125
|
+
|
|
127
126
|
error_html = f"""
|
|
128
127
|
<!DOCTYPE html>
|
|
129
128
|
<html lang="en">
|
|
@@ -142,13 +141,13 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
142
141
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
143
142
|
</svg>
|
|
144
143
|
</div>
|
|
145
|
-
|
|
144
|
+
|
|
146
145
|
<!-- Header -->
|
|
147
146
|
<div class="mb-6">
|
|
148
147
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">Authentication Failed</h1>
|
|
149
148
|
<p class="text-red-600 text-lg">Something went wrong</p>
|
|
150
149
|
</div>
|
|
151
|
-
|
|
150
|
+
|
|
152
151
|
<!-- Error Details -->
|
|
153
152
|
<div class="space-y-3 mb-8">
|
|
154
153
|
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md">
|
|
@@ -156,12 +155,12 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
156
155
|
</div>
|
|
157
156
|
<p class="text-gray-600">Please try again or contact support if the issue persists.</p>
|
|
158
157
|
</div>
|
|
159
|
-
|
|
158
|
+
|
|
160
159
|
<!-- Close Message -->
|
|
161
160
|
<div class="w-full py-3 px-4 bg-gray-50 text-gray-700 rounded-md border border-gray-200">
|
|
162
161
|
You can close this window now
|
|
163
162
|
</div>
|
|
164
|
-
|
|
163
|
+
|
|
165
164
|
<!-- Lyceum Branding -->
|
|
166
165
|
<div class="mt-8 pt-6 border-t border-gray-200">
|
|
167
166
|
<p class="text-xs text-gray-400">Powered by Lyceum Technology</p>
|
|
@@ -180,7 +179,7 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
180
179
|
# Missing parameters
|
|
181
180
|
callback_result["error"] = "Missing token or error parameter"
|
|
182
181
|
callback_result["received"] = True
|
|
183
|
-
|
|
182
|
+
|
|
184
183
|
self.send_response(400)
|
|
185
184
|
self.send_header("Content-type", "text/html")
|
|
186
185
|
self.end_headers()
|
|
@@ -253,11 +252,11 @@ class CallbackHandler(BaseHTTPRequestHandler):
|
|
|
253
252
|
except (BrokenPipeError, ConnectionResetError):
|
|
254
253
|
# Browser closed connection
|
|
255
254
|
pass
|
|
256
|
-
|
|
255
|
+
|
|
257
256
|
except Exception as e:
|
|
258
257
|
callback_result["error"] = str(e)
|
|
259
258
|
callback_result["received"] = True
|
|
260
|
-
|
|
259
|
+
|
|
261
260
|
self.send_response(500)
|
|
262
261
|
self.send_header("Content-type", "text/html")
|
|
263
262
|
self.end_headers()
|
|
@@ -309,24 +308,30 @@ def get_available_port():
|
|
|
309
308
|
|
|
310
309
|
@auth_app.command("login")
|
|
311
310
|
def login(
|
|
312
|
-
base_url:
|
|
313
|
-
dashboard_url:
|
|
314
|
-
|
|
315
|
-
|
|
311
|
+
base_url: str | None = typer.Option(None, "--url", help="API base URL (for development)"),
|
|
312
|
+
dashboard_url: str | None = typer.Option(
|
|
313
|
+
"https://dashboard.lyceum.technology", "--dashboard", help="Dashboard URL"
|
|
314
|
+
),
|
|
315
|
+
manual: bool = typer.Option(
|
|
316
|
+
False, "--manual", help="Use manual API key login instead of browser"
|
|
317
|
+
),
|
|
318
|
+
api_key: str | None = typer.Option(
|
|
319
|
+
None, "--api-key", "-k", help="API key for manual login"
|
|
320
|
+
),
|
|
316
321
|
):
|
|
317
322
|
"""Login to Lyceum via browser authentication"""
|
|
318
323
|
global callback_result
|
|
319
|
-
|
|
324
|
+
|
|
320
325
|
if manual:
|
|
321
326
|
# Legacy manual login
|
|
322
327
|
if not api_key:
|
|
323
328
|
api_key = typer.prompt("Enter your Lyceum API key", hide_input=True)
|
|
324
|
-
|
|
329
|
+
|
|
325
330
|
config.api_key = api_key
|
|
326
331
|
if base_url:
|
|
327
332
|
config.base_url = base_url
|
|
328
333
|
config.save()
|
|
329
|
-
|
|
334
|
+
|
|
330
335
|
# Test the connection
|
|
331
336
|
try:
|
|
332
337
|
import httpx
|
|
@@ -341,50 +346,53 @@ def login(
|
|
|
341
346
|
console.print(f"[red]❌ Authentication failed: {e}[/red]")
|
|
342
347
|
raise typer.Exit(1)
|
|
343
348
|
return
|
|
344
|
-
|
|
349
|
+
|
|
345
350
|
# OAuth-style browser login
|
|
346
351
|
try:
|
|
347
352
|
if base_url:
|
|
348
353
|
config.base_url = base_url
|
|
349
|
-
|
|
354
|
+
else:
|
|
355
|
+
# Reset to production URL if no custom URL specified
|
|
356
|
+
config.base_url = "https://api.lyceum.technology"
|
|
357
|
+
|
|
350
358
|
# Reset callback result
|
|
351
359
|
callback_result = {"token": None, "error": None, "received": False}
|
|
352
|
-
|
|
360
|
+
|
|
353
361
|
# Start callback server
|
|
354
362
|
callback_port = get_available_port()
|
|
355
363
|
callback_server = HTTPServer(('localhost', callback_port), CallbackHandler)
|
|
356
|
-
|
|
364
|
+
|
|
357
365
|
console.print(f"[dim]Starting callback server on port {callback_port}...[/dim]")
|
|
358
|
-
|
|
366
|
+
|
|
359
367
|
# Start server in background thread
|
|
360
368
|
server_thread = threading.Thread(target=callback_server.serve_forever, daemon=True)
|
|
361
369
|
server_thread.start()
|
|
362
|
-
|
|
370
|
+
|
|
363
371
|
# Construct login URL
|
|
364
372
|
callback_url = f"http://localhost:{callback_port}/callback"
|
|
365
373
|
login_url = f"{dashboard_url}/cli-login?callback={callback_url}"
|
|
366
|
-
|
|
374
|
+
|
|
367
375
|
console.print("[cyan]🌐 Opening browser for authentication...[/cyan]")
|
|
368
376
|
console.print(f"[dim]If browser doesn't open, visit: {login_url}[/dim]")
|
|
369
|
-
|
|
377
|
+
|
|
370
378
|
# Open browser
|
|
371
379
|
if not webbrowser.open(login_url):
|
|
372
380
|
console.print("[yellow]⚠️ Could not open browser automatically[/yellow]")
|
|
373
381
|
console.print(f"[yellow]Please manually open: {login_url}[/yellow]")
|
|
374
|
-
|
|
382
|
+
|
|
375
383
|
console.print("[dim]Waiting for authentication... (timeout: 120 seconds)[/dim]")
|
|
376
|
-
|
|
384
|
+
|
|
377
385
|
# Wait for callback with timeout
|
|
378
386
|
timeout = 120 # 2 minutes
|
|
379
387
|
start_time = time.time()
|
|
380
|
-
|
|
388
|
+
|
|
381
389
|
while not callback_result["received"] and (time.time() - start_time) < timeout:
|
|
382
390
|
time.sleep(0.5)
|
|
383
|
-
|
|
391
|
+
|
|
384
392
|
# Stop server
|
|
385
393
|
callback_server.shutdown()
|
|
386
394
|
callback_server.server_close()
|
|
387
|
-
|
|
395
|
+
|
|
388
396
|
if callback_result["received"]:
|
|
389
397
|
if callback_result["token"]:
|
|
390
398
|
# Save token and test connection
|
|
@@ -393,7 +401,7 @@ def login(
|
|
|
393
401
|
if callback_result.get("refresh_token"):
|
|
394
402
|
config.refresh_token = callback_result["refresh_token"]
|
|
395
403
|
config.save()
|
|
396
|
-
|
|
404
|
+
|
|
397
405
|
console.print("[green]✅ Authentication token received![/green]")
|
|
398
406
|
|
|
399
407
|
# Test the connection using health endpoint
|
|
@@ -427,14 +435,14 @@ def login(
|
|
|
427
435
|
console.print(f"[red]❌ Token validation failed: {e}[/red]")
|
|
428
436
|
console.print(f"[dim]Token saved but couldn't verify. Error type: {type(e).__name__}[/dim]")
|
|
429
437
|
raise typer.Exit(1)
|
|
430
|
-
|
|
438
|
+
|
|
431
439
|
elif callback_result["error"]:
|
|
432
440
|
console.print(f"[red]❌ Authentication failed: {callback_result['error']}[/red]")
|
|
433
441
|
raise typer.Exit(1)
|
|
434
442
|
else:
|
|
435
443
|
console.print("[red]❌ Authentication timed out. Please try again.[/red]")
|
|
436
444
|
raise typer.Exit(1)
|
|
437
|
-
|
|
445
|
+
|
|
438
446
|
except KeyboardInterrupt:
|
|
439
447
|
console.print("\n[yellow]Authentication cancelled by user.[/yellow]")
|
|
440
448
|
raise typer.Exit(1)
|
|
@@ -458,11 +466,11 @@ def status():
|
|
|
458
466
|
from ...shared.config import CONFIG_FILE
|
|
459
467
|
console.print(f"[dim]Config file: {CONFIG_FILE}[/dim]")
|
|
460
468
|
console.print(f"[dim]Base URL: {config.base_url}[/dim]")
|
|
461
|
-
|
|
469
|
+
|
|
462
470
|
if config.api_key:
|
|
463
|
-
console.print(
|
|
471
|
+
console.print("[green]✅ Authenticated[/green]")
|
|
464
472
|
console.print(f"[dim]API Key: {config.api_key[:8]}...[/dim]")
|
|
465
|
-
|
|
473
|
+
|
|
466
474
|
# Test connection
|
|
467
475
|
try:
|
|
468
476
|
import httpx
|
|
@@ -475,4 +483,4 @@ def status():
|
|
|
475
483
|
except Exception as e:
|
|
476
484
|
console.print(f"[red]❌ API connection failed: {e}[/red]")
|
|
477
485
|
else:
|
|
478
|
-
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Python execution commands
|
|
3
|
-
"""
|
|
1
|
+
"""Python execution commands"""
|
|
4
2
|
|
|
5
3
|
from pathlib import Path
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
7
6
|
import typer
|
|
8
7
|
from rich.console import Console
|
|
9
|
-
import httpx
|
|
10
8
|
|
|
11
9
|
from ....shared.config import config
|
|
12
10
|
from ....shared.streaming import stream_execution_output
|
|
@@ -19,11 +17,17 @@ python_app = typer.Typer(name="python", help="Python execution commands")
|
|
|
19
17
|
@python_app.command("run")
|
|
20
18
|
def run_python(
|
|
21
19
|
code_or_file: str = typer.Argument(..., help="Python code to execute or path to Python file"),
|
|
22
|
-
machine_type: str = typer.Option(
|
|
20
|
+
machine_type: str = typer.Option(
|
|
21
|
+
"cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"
|
|
22
|
+
),
|
|
23
23
|
timeout: int = typer.Option(60, "--timeout", "-t", help="Execution timeout in seconds"),
|
|
24
|
-
file_name:
|
|
25
|
-
requirements:
|
|
26
|
-
|
|
24
|
+
file_name: str | None = typer.Option(None, "--file-name", "-f", help="Name for the execution"),
|
|
25
|
+
requirements: str | None = typer.Option(
|
|
26
|
+
None, "--requirements", "-r", help="Requirements file path or pip requirements string"
|
|
27
|
+
),
|
|
28
|
+
imports: list[str] | None = typer.Option(
|
|
29
|
+
None, "--import", help="Pre-import modules (can be used multiple times)"
|
|
30
|
+
),
|
|
27
31
|
):
|
|
28
32
|
"""Execute Python code or file on Lyceum Cloud"""
|
|
29
33
|
try:
|
|
@@ -31,7 +35,7 @@ def run_python(
|
|
|
31
35
|
code_to_execute = code_or_file
|
|
32
36
|
if Path(code_or_file).exists():
|
|
33
37
|
console.print(f"[dim]Reading code from file: {code_or_file}[/dim]")
|
|
34
|
-
with open(code_or_file
|
|
38
|
+
with open(code_or_file) as f:
|
|
35
39
|
code_to_execute = f.read()
|
|
36
40
|
# Use filename as execution name if not provided
|
|
37
41
|
if not file_name:
|
|
@@ -43,7 +47,7 @@ def run_python(
|
|
|
43
47
|
# Check if it's a file path
|
|
44
48
|
if Path(requirements).exists():
|
|
45
49
|
console.print(f"[dim]Reading requirements from file: {requirements}[/dim]")
|
|
46
|
-
with open(requirements
|
|
50
|
+
with open(requirements) as f:
|
|
47
51
|
requirements_content = f.read()
|
|
48
52
|
else:
|
|
49
53
|
# Treat as direct pip requirements string
|
|
@@ -81,7 +85,7 @@ def run_python(
|
|
|
81
85
|
execution_id = data['execution_id']
|
|
82
86
|
streaming_url = data.get('streaming_url')
|
|
83
87
|
|
|
84
|
-
console.print(
|
|
88
|
+
console.print("[green]✅ Execution started![/green]")
|
|
85
89
|
console.print(f"[dim]Execution ID: {execution_id}[/dim]")
|
|
86
90
|
|
|
87
91
|
if 'pythia_decision' in data:
|
|
@@ -89,11 +93,11 @@ def run_python(
|
|
|
89
93
|
|
|
90
94
|
# Stream the execution output
|
|
91
95
|
success = stream_execution_output(execution_id, streaming_url)
|
|
92
|
-
|
|
96
|
+
|
|
93
97
|
if not success:
|
|
94
|
-
console.print(
|
|
98
|
+
console.print("[yellow]💡 You can check the execution later with: lyceum status[/yellow]")
|
|
95
99
|
raise typer.Exit(1)
|
|
96
|
-
|
|
100
|
+
|
|
97
101
|
except Exception as e:
|
|
98
102
|
console.print(f"[red]Error: {e}[/red]")
|
|
99
|
-
raise typer.Exit(1)
|
|
103
|
+
raise typer.Exit(1)
|