lyceum-cli 1.0.14__py3-none-any.whl → 1.0.18__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 +0 -0
- lyceum/external/__init__.py +0 -0
- lyceum/external/auth/__init__.py +0 -0
- lyceum/external/auth/login.py +462 -0
- lyceum/external/compute/__init__.py +0 -0
- lyceum/external/compute/execution/__init__.py +0 -0
- lyceum/external/compute/execution/python.py +99 -0
- lyceum/external/compute/inference/__init__.py +2 -0
- lyceum/external/compute/inference/batch.py +315 -0
- lyceum/external/compute/inference/chat.py +220 -0
- lyceum/external/compute/inference/models.py +242 -0
- lyceum/external/general/__init__.py +0 -0
- lyceum/main.py +52 -0
- lyceum/shared/__init__.py +0 -0
- lyceum/shared/config.py +144 -0
- lyceum/shared/display.py +195 -0
- lyceum/shared/streaming.py +134 -0
- {lyceum_cli-1.0.14.dist-info → lyceum_cli-1.0.18.dist-info}/METADATA +1 -1
- lyceum_cli-1.0.18.dist-info/RECORD +25 -0
- lyceum_cli-1.0.18.dist-info/top_level.txt +2 -0
- lyceum_cloud_execution_api_client/__init__.py +0 -0
- lyceum_cloud_execution_api_client/api/__init__.py +0 -0
- lyceum_cloud_execution_api_client/models/__init__.py +105 -0
- lyceum_cli-1.0.14.dist-info/RECORD +0 -5
- lyceum_cli-1.0.14.dist-info/top_level.txt +0 -1
- {lyceum_cli-1.0.14.dist-info → lyceum_cli-1.0.18.dist-info}/WHEEL +0 -0
- {lyceum_cli-1.0.14.dist-info → lyceum_cli-1.0.18.dist-info}/entry_points.txt +0 -0
lyceum/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication commands: login, logout, status
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
import webbrowser
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
12
|
+
from urllib.parse import urlparse, parse_qs
|
|
13
|
+
import socket
|
|
14
|
+
|
|
15
|
+
from ...shared.config import config
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
auth_app = typer.Typer(name="auth", help="Authentication commands")
|
|
20
|
+
|
|
21
|
+
# Global variables for callback server
|
|
22
|
+
callback_result = {"token": None, "error": None, "received": False}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
26
|
+
"""HTTP handler for OAuth callback"""
|
|
27
|
+
|
|
28
|
+
def log_message(self, format, *args):
|
|
29
|
+
# Suppress HTTP server logs
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def do_GET(self):
|
|
33
|
+
global callback_result
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Parse the callback URL
|
|
37
|
+
parsed_url = urlparse(self.path)
|
|
38
|
+
query_params = parse_qs(parsed_url.query)
|
|
39
|
+
|
|
40
|
+
if parsed_url.path == "/callback":
|
|
41
|
+
# Extract token from query parameters
|
|
42
|
+
if "token" in query_params:
|
|
43
|
+
token = query_params["token"][0]
|
|
44
|
+
user_info = query_params.get("user", [None])[0]
|
|
45
|
+
refresh_token = query_params.get("refresh_token", [None])[0]
|
|
46
|
+
|
|
47
|
+
callback_result["token"] = token
|
|
48
|
+
callback_result["user"] = user_info
|
|
49
|
+
if refresh_token:
|
|
50
|
+
callback_result["refresh_token"] = refresh_token
|
|
51
|
+
callback_result["received"] = True
|
|
52
|
+
|
|
53
|
+
# Send success response
|
|
54
|
+
self.send_response(200)
|
|
55
|
+
self.send_header("Content-type", "text/html")
|
|
56
|
+
self.end_headers()
|
|
57
|
+
|
|
58
|
+
success_html = """
|
|
59
|
+
<!DOCTYPE html>
|
|
60
|
+
<html lang="en">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="UTF-8">
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
64
|
+
<title>Lyceum CLI - Authentication Success</title>
|
|
65
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
66
|
+
</head>
|
|
67
|
+
<body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
|
68
|
+
<div class="max-w-md w-full space-y-8">
|
|
69
|
+
<div class="bg-white rounded-lg shadow-md p-8 text-center">
|
|
70
|
+
<!-- Success Icon -->
|
|
71
|
+
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-6">
|
|
72
|
+
<svg class="h-8 w-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
73
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
74
|
+
</svg>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Header -->
|
|
78
|
+
<div class="mb-6">
|
|
79
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-2">Authentication Successful!</h1>
|
|
80
|
+
<p class="text-gray-600 text-lg">Welcome to Lyceum</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Instructions -->
|
|
84
|
+
<div class="space-y-3 mb-8">
|
|
85
|
+
<p class="text-gray-700">You can now close this browser tab and return to the CLI.</p>
|
|
86
|
+
<p class="text-sm text-gray-500">Your Lyceum CLI has been authenticated successfully and is ready to use.</p>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Close Button -->
|
|
90
|
+
<button
|
|
91
|
+
onclick="window.close()"
|
|
92
|
+
class="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 transition-colors"
|
|
93
|
+
>
|
|
94
|
+
Close Tab
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
<!-- Lyceum Branding -->
|
|
98
|
+
<div class="mt-8 pt-6 border-t border-gray-200">
|
|
99
|
+
<p class="text-xs text-gray-400">Powered by Lyceum Technology</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Auto-close script -->
|
|
105
|
+
<script>
|
|
106
|
+
// Auto-close after 10 seconds
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
window.close();
|
|
109
|
+
}, 10000);
|
|
110
|
+
</script>
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
self.wfile.write(success_html.encode())
|
|
116
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
117
|
+
# Browser closed connection (expected when tab auto-closes)
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
elif "error" in query_params:
|
|
121
|
+
error = query_params["error"][0]
|
|
122
|
+
callback_result["error"] = error
|
|
123
|
+
callback_result["received"] = True
|
|
124
|
+
|
|
125
|
+
# Send error response
|
|
126
|
+
self.send_response(400)
|
|
127
|
+
self.send_header("Content-type", "text/html")
|
|
128
|
+
self.end_headers()
|
|
129
|
+
|
|
130
|
+
error_html = f"""
|
|
131
|
+
<!DOCTYPE html>
|
|
132
|
+
<html lang="en">
|
|
133
|
+
<head>
|
|
134
|
+
<meta charset="UTF-8">
|
|
135
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
136
|
+
<title>Lyceum CLI - Authentication Error</title>
|
|
137
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
138
|
+
</head>
|
|
139
|
+
<body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
|
140
|
+
<div class="max-w-md w-full space-y-8">
|
|
141
|
+
<div class="bg-white rounded-lg shadow-md p-8 text-center">
|
|
142
|
+
<!-- Error Icon -->
|
|
143
|
+
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-red-100 mb-6">
|
|
144
|
+
<svg class="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
145
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
146
|
+
</svg>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Header -->
|
|
150
|
+
<div class="mb-6">
|
|
151
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-2">Authentication Failed</h1>
|
|
152
|
+
<p class="text-red-600 text-lg">Something went wrong</p>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<!-- Error Details -->
|
|
156
|
+
<div class="space-y-3 mb-8">
|
|
157
|
+
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md">
|
|
158
|
+
<p class="text-sm">{error}</p>
|
|
159
|
+
</div>
|
|
160
|
+
<p class="text-gray-600">Please try again or contact support if the issue persists.</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!-- Close Button -->
|
|
164
|
+
<button
|
|
165
|
+
onclick="window.close()"
|
|
166
|
+
class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors"
|
|
167
|
+
>
|
|
168
|
+
Close Tab
|
|
169
|
+
</button>
|
|
170
|
+
|
|
171
|
+
<!-- Lyceum Branding -->
|
|
172
|
+
<div class="mt-8 pt-6 border-t border-gray-200">
|
|
173
|
+
<p class="text-xs text-gray-400">Powered by Lyceum Technology</p>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</body>
|
|
178
|
+
</html>
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
self.wfile.write(error_html.encode())
|
|
182
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
183
|
+
# Browser closed connection
|
|
184
|
+
pass
|
|
185
|
+
else:
|
|
186
|
+
# Missing parameters
|
|
187
|
+
callback_result["error"] = "Missing token or error parameter"
|
|
188
|
+
callback_result["received"] = True
|
|
189
|
+
|
|
190
|
+
self.send_response(400)
|
|
191
|
+
self.send_header("Content-type", "text/html")
|
|
192
|
+
self.end_headers()
|
|
193
|
+
invalid_html = """
|
|
194
|
+
<!DOCTYPE html>
|
|
195
|
+
<html lang="en">
|
|
196
|
+
<head>
|
|
197
|
+
<meta charset="UTF-8">
|
|
198
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
199
|
+
<title>Lyceum CLI - Invalid Parameters</title>
|
|
200
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
201
|
+
</head>
|
|
202
|
+
<body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
|
203
|
+
<div class="max-w-md w-full space-y-8">
|
|
204
|
+
<div class="bg-white rounded-lg shadow-md p-8 text-center">
|
|
205
|
+
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-yellow-100 mb-6">
|
|
206
|
+
<svg class="h-8 w-8 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
207
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.268 16c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
208
|
+
</svg>
|
|
209
|
+
</div>
|
|
210
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-2">Invalid Parameters</h1>
|
|
211
|
+
<p class="text-gray-600 mb-8">The authentication callback received invalid parameters.</p>
|
|
212
|
+
<button onclick="window.close()" class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors">Close Tab</button>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</body>
|
|
216
|
+
</html>
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
self.wfile.write(invalid_html.encode())
|
|
220
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
221
|
+
# Browser closed connection
|
|
222
|
+
pass
|
|
223
|
+
else:
|
|
224
|
+
# Invalid path
|
|
225
|
+
self.send_response(404)
|
|
226
|
+
self.send_header("Content-type", "text/html")
|
|
227
|
+
self.end_headers()
|
|
228
|
+
notfound_html = """
|
|
229
|
+
<!DOCTYPE html>
|
|
230
|
+
<html lang="en">
|
|
231
|
+
<head>
|
|
232
|
+
<meta charset="UTF-8">
|
|
233
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
234
|
+
<title>Lyceum CLI - Page Not Found</title>
|
|
235
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
236
|
+
</head>
|
|
237
|
+
<body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
|
238
|
+
<div class="max-w-md w-full space-y-8">
|
|
239
|
+
<div class="bg-white rounded-lg shadow-md p-8 text-center">
|
|
240
|
+
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-gray-100 mb-6">
|
|
241
|
+
<svg class="h-8 w-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
242
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.419-1.007-5.866-2.609C6.107 12.398 6 12.202 6 12s.107-.398.134-.609C7.581 10.007 9.66 9 12 9s4.419 1.007 5.866 2.609c.027.203.134.4.134.609s-.107.406-.134.609A7.962 7.962 0 0117 15z"></path>
|
|
243
|
+
</svg>
|
|
244
|
+
</div>
|
|
245
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-2">Page Not Found</h1>
|
|
246
|
+
<p class="text-gray-600 mb-8">The requested page could not be found.</p>
|
|
247
|
+
<button onclick="window.close()" class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors">Close Tab</button>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</body>
|
|
251
|
+
</html>
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
self.wfile.write(notfound_html.encode())
|
|
255
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
256
|
+
# Browser closed connection
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
callback_result["error"] = str(e)
|
|
261
|
+
callback_result["received"] = True
|
|
262
|
+
|
|
263
|
+
self.send_response(500)
|
|
264
|
+
self.send_header("Content-type", "text/html")
|
|
265
|
+
self.end_headers()
|
|
266
|
+
server_error_html = f"""
|
|
267
|
+
<!DOCTYPE html>
|
|
268
|
+
<html lang="en">
|
|
269
|
+
<head>
|
|
270
|
+
<meta charset="UTF-8">
|
|
271
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
272
|
+
<title>Lyceum CLI - Server Error</title>
|
|
273
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
274
|
+
</head>
|
|
275
|
+
<body class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
|
276
|
+
<div class="max-w-md w-full space-y-8">
|
|
277
|
+
<div class="bg-white rounded-lg shadow-md p-8 text-center">
|
|
278
|
+
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-red-100 mb-6">
|
|
279
|
+
<svg class="h-8 w-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
280
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
281
|
+
</svg>
|
|
282
|
+
</div>
|
|
283
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-2">Server Error</h1>
|
|
284
|
+
<p class="text-red-600 mb-4">An unexpected error occurred</p>
|
|
285
|
+
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md mb-8">
|
|
286
|
+
<p class="text-sm">{e}</p>
|
|
287
|
+
</div>
|
|
288
|
+
<button onclick="window.close()" class="w-full py-2 px-4 bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 transition-colors">Close Tab</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</body>
|
|
292
|
+
</html>
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
self.wfile.write(server_error_html.encode())
|
|
296
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
297
|
+
# Browser closed connection
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def get_available_port():
|
|
302
|
+
"""Find an available port for the callback server"""
|
|
303
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
304
|
+
s.bind(('', 0))
|
|
305
|
+
s.listen(1)
|
|
306
|
+
port = s.getsockname()[1]
|
|
307
|
+
return port
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@auth_app.command("login")
|
|
311
|
+
def login(
|
|
312
|
+
base_url: Optional[str] = typer.Option(None, "--url", help="API base URL (for development)"),
|
|
313
|
+
dashboard_url: Optional[str] = typer.Option("https://dashboard.lyceum.technology", "--dashboard", help="Dashboard URL"),
|
|
314
|
+
manual: bool = typer.Option(False, "--manual", help="Use manual API key login instead of browser"),
|
|
315
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", "-k", help="API key for manual login"),
|
|
316
|
+
):
|
|
317
|
+
"""Login to Lyceum via browser authentication"""
|
|
318
|
+
global callback_result
|
|
319
|
+
|
|
320
|
+
if manual:
|
|
321
|
+
# Legacy manual login
|
|
322
|
+
if not api_key:
|
|
323
|
+
api_key = typer.prompt("Enter your Lyceum API key", hide_input=True)
|
|
324
|
+
|
|
325
|
+
config.api_key = api_key
|
|
326
|
+
if base_url:
|
|
327
|
+
config.base_url = base_url
|
|
328
|
+
config.save()
|
|
329
|
+
|
|
330
|
+
# Test the connection
|
|
331
|
+
try:
|
|
332
|
+
import httpx
|
|
333
|
+
headers = {"Authorization": f"Bearer {config.api_key}"}
|
|
334
|
+
response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
|
|
335
|
+
if response.status_code == 200:
|
|
336
|
+
console.print("[green]✅ Successfully authenticated![/green]")
|
|
337
|
+
else:
|
|
338
|
+
console.print(f"[red]❌ Authentication failed: HTTP {response.status_code}[/red]")
|
|
339
|
+
raise typer.Exit(1)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
console.print(f"[red]❌ Authentication failed: {e}[/red]")
|
|
342
|
+
raise typer.Exit(1)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# OAuth-style browser login
|
|
346
|
+
try:
|
|
347
|
+
if base_url:
|
|
348
|
+
config.base_url = base_url
|
|
349
|
+
|
|
350
|
+
# Reset callback result
|
|
351
|
+
callback_result = {"token": None, "error": None, "received": False}
|
|
352
|
+
|
|
353
|
+
# Start callback server
|
|
354
|
+
callback_port = get_available_port()
|
|
355
|
+
callback_server = HTTPServer(('localhost', callback_port), CallbackHandler)
|
|
356
|
+
|
|
357
|
+
console.print(f"[dim]Starting callback server on port {callback_port}...[/dim]")
|
|
358
|
+
|
|
359
|
+
# Start server in background thread
|
|
360
|
+
server_thread = threading.Thread(target=callback_server.serve_forever, daemon=True)
|
|
361
|
+
server_thread.start()
|
|
362
|
+
|
|
363
|
+
# Construct login URL
|
|
364
|
+
callback_url = f"http://localhost:{callback_port}/callback"
|
|
365
|
+
login_url = f"{dashboard_url}/cli-login?callback={callback_url}"
|
|
366
|
+
|
|
367
|
+
console.print("[cyan]🌐 Opening browser for authentication...[/cyan]")
|
|
368
|
+
console.print(f"[dim]If browser doesn't open, visit: {login_url}[/dim]")
|
|
369
|
+
|
|
370
|
+
# Open browser
|
|
371
|
+
if not webbrowser.open(login_url):
|
|
372
|
+
console.print("[yellow]⚠️ Could not open browser automatically[/yellow]")
|
|
373
|
+
console.print(f"[yellow]Please manually open: {login_url}[/yellow]")
|
|
374
|
+
|
|
375
|
+
console.print("[dim]Waiting for authentication... (timeout: 120 seconds)[/dim]")
|
|
376
|
+
|
|
377
|
+
# Wait for callback with timeout
|
|
378
|
+
timeout = 120 # 2 minutes
|
|
379
|
+
start_time = time.time()
|
|
380
|
+
|
|
381
|
+
while not callback_result["received"] and (time.time() - start_time) < timeout:
|
|
382
|
+
time.sleep(0.5)
|
|
383
|
+
|
|
384
|
+
# Stop server
|
|
385
|
+
callback_server.shutdown()
|
|
386
|
+
callback_server.server_close()
|
|
387
|
+
|
|
388
|
+
if callback_result["received"]:
|
|
389
|
+
if callback_result["token"]:
|
|
390
|
+
# Save token and test connection
|
|
391
|
+
config.api_key = callback_result["token"]
|
|
392
|
+
# Also save refresh_token if provided
|
|
393
|
+
if callback_result.get("refresh_token"):
|
|
394
|
+
config.refresh_token = callback_result["refresh_token"]
|
|
395
|
+
config.save()
|
|
396
|
+
|
|
397
|
+
console.print("[green]✅ Authentication token received![/green]")
|
|
398
|
+
|
|
399
|
+
# Test the connection using health endpoint
|
|
400
|
+
try:
|
|
401
|
+
import httpx
|
|
402
|
+
headers = {"Authorization": f"Bearer {config.api_key}"}
|
|
403
|
+
response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
|
|
404
|
+
if response.status_code == 200:
|
|
405
|
+
console.print("[green]✅ Successfully authenticated![/green]")
|
|
406
|
+
if callback_result.get("user"):
|
|
407
|
+
console.print(f"[dim]Logged in as: {callback_result['user']}[/dim]")
|
|
408
|
+
else:
|
|
409
|
+
console.print(f"[red]❌ Token validation failed: HTTP {response.status_code}[/red]")
|
|
410
|
+
raise typer.Exit(1)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
console.print(f"[red]❌ Token validation failed: {e}[/red]")
|
|
413
|
+
raise typer.Exit(1)
|
|
414
|
+
|
|
415
|
+
elif callback_result["error"]:
|
|
416
|
+
console.print(f"[red]❌ Authentication failed: {callback_result['error']}[/red]")
|
|
417
|
+
raise typer.Exit(1)
|
|
418
|
+
else:
|
|
419
|
+
console.print("[red]❌ Authentication timed out. Please try again.[/red]")
|
|
420
|
+
raise typer.Exit(1)
|
|
421
|
+
|
|
422
|
+
except KeyboardInterrupt:
|
|
423
|
+
console.print("\n[yellow]Authentication cancelled by user.[/yellow]")
|
|
424
|
+
raise typer.Exit(1)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
console.print(f"[red]❌ Authentication error: {e}[/red]")
|
|
427
|
+
raise typer.Exit(1)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@auth_app.command("logout")
|
|
431
|
+
def logout():
|
|
432
|
+
"""Logout and remove stored credentials"""
|
|
433
|
+
from ...shared.config import CONFIG_FILE
|
|
434
|
+
if CONFIG_FILE.exists():
|
|
435
|
+
CONFIG_FILE.unlink()
|
|
436
|
+
console.print("[green]✅ Logged out successfully![/green]")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@auth_app.command("status")
|
|
440
|
+
def status():
|
|
441
|
+
"""Show current configuration and authentication status"""
|
|
442
|
+
from ...shared.config import CONFIG_FILE
|
|
443
|
+
console.print(f"[dim]Config file: {CONFIG_FILE}[/dim]")
|
|
444
|
+
console.print(f"[dim]Base URL: {config.base_url}[/dim]")
|
|
445
|
+
|
|
446
|
+
if config.api_key:
|
|
447
|
+
console.print(f"[green]✅ Authenticated[/green]")
|
|
448
|
+
console.print(f"[dim]API Key: {config.api_key[:8]}...[/dim]")
|
|
449
|
+
|
|
450
|
+
# Test connection
|
|
451
|
+
try:
|
|
452
|
+
import httpx
|
|
453
|
+
headers = {"Authorization": f"Bearer {config.api_key}"}
|
|
454
|
+
response = httpx.get(f"{config.base_url}/api/v2/external/machine-types", headers=headers, timeout=10.0)
|
|
455
|
+
if response.status_code == 200:
|
|
456
|
+
console.print("[green]✅ API connection working[/green]")
|
|
457
|
+
else:
|
|
458
|
+
console.print(f"[yellow]⚠️ API connection issues: HTTP {response.status_code}[/yellow]")
|
|
459
|
+
except Exception as e:
|
|
460
|
+
console.print(f"[red]❌ API connection failed: {e}[/red]")
|
|
461
|
+
else:
|
|
462
|
+
console.print("[red]❌ Not authenticated. Run 'lyceum login' first.[/red]")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python execution commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ....shared.config import config
|
|
12
|
+
from ....shared.streaming import stream_execution_output
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
python_app = typer.Typer(name="python", help="Python execution commands")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@python_app.command("run")
|
|
20
|
+
def run_python(
|
|
21
|
+
code_or_file: str = typer.Argument(..., help="Python code to execute or path to Python file"),
|
|
22
|
+
machine_type: str = typer.Option("cpu", "--machine", "-m", help="Machine type (cpu, a100, h100, etc.)"),
|
|
23
|
+
timeout: int = typer.Option(60, "--timeout", "-t", help="Execution timeout in seconds"),
|
|
24
|
+
file_name: Optional[str] = typer.Option(None, "--file-name", "-f", help="Name for the execution"),
|
|
25
|
+
requirements: Optional[str] = typer.Option(None, "--requirements", "-r", help="Requirements file path or pip requirements string"),
|
|
26
|
+
imports: Optional[list[str]] = typer.Option(None, "--import", help="Pre-import modules (can be used multiple times)"),
|
|
27
|
+
):
|
|
28
|
+
"""Execute Python code or file on Lyceum Cloud"""
|
|
29
|
+
try:
|
|
30
|
+
# Check if it's a file path
|
|
31
|
+
code_to_execute = code_or_file
|
|
32
|
+
if Path(code_or_file).exists():
|
|
33
|
+
console.print(f"[dim]Reading code from file: {code_or_file}[/dim]")
|
|
34
|
+
with open(code_or_file, 'r') as f:
|
|
35
|
+
code_to_execute = f.read()
|
|
36
|
+
# Use filename as execution name if not provided
|
|
37
|
+
if not file_name:
|
|
38
|
+
file_name = Path(code_or_file).name
|
|
39
|
+
|
|
40
|
+
# Handle requirements
|
|
41
|
+
requirements_content = None
|
|
42
|
+
if requirements:
|
|
43
|
+
# Check if it's a file path
|
|
44
|
+
if Path(requirements).exists():
|
|
45
|
+
console.print(f"[dim]Reading requirements from file: {requirements}[/dim]")
|
|
46
|
+
with open(requirements, 'r') as f:
|
|
47
|
+
requirements_content = f.read()
|
|
48
|
+
else:
|
|
49
|
+
# Treat as direct pip requirements string
|
|
50
|
+
requirements_content = requirements
|
|
51
|
+
|
|
52
|
+
# Create execution request payload
|
|
53
|
+
payload = {
|
|
54
|
+
"code": code_to_execute,
|
|
55
|
+
"nbcode": 0,
|
|
56
|
+
"execution_type": machine_type,
|
|
57
|
+
"timeout": timeout,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if file_name:
|
|
61
|
+
payload["file_name"] = file_name
|
|
62
|
+
if requirements_content:
|
|
63
|
+
payload["requirements_content"] = requirements_content
|
|
64
|
+
if imports:
|
|
65
|
+
payload["prior_imports"] = imports
|
|
66
|
+
|
|
67
|
+
# Make API request
|
|
68
|
+
response = httpx.post(
|
|
69
|
+
f"{config.base_url}/api/v2/external/execution/streaming/start",
|
|
70
|
+
headers={"Authorization": f"Bearer {config.api_key}"},
|
|
71
|
+
json=payload,
|
|
72
|
+
timeout=30.0
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if response.status_code != 200:
|
|
76
|
+
console.print(f"[red]Error: HTTP {response.status_code}[/red]")
|
|
77
|
+
console.print(f"[red]{response.content.decode()}[/red]")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
|
|
80
|
+
data = response.json()
|
|
81
|
+
execution_id = data['execution_id']
|
|
82
|
+
streaming_url = data.get('streaming_url')
|
|
83
|
+
|
|
84
|
+
console.print(f"[green]✅ Execution started![/green]")
|
|
85
|
+
console.print(f"[dim]Execution ID: {execution_id}[/dim]")
|
|
86
|
+
|
|
87
|
+
if 'pythia_decision' in data:
|
|
88
|
+
console.print(f"[dim]Pythia recommendation: {data['pythia_decision']}[/dim]")
|
|
89
|
+
|
|
90
|
+
# Stream the execution output
|
|
91
|
+
success = stream_execution_output(execution_id, streaming_url)
|
|
92
|
+
|
|
93
|
+
if not success:
|
|
94
|
+
console.print(f"[yellow]💡 You can check the execution later with: lyceum status[/yellow]")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
99
|
+
raise typer.Exit(1)
|