alphai 0.0.7__py3-none-any.whl → 0.1.0__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.
- alphai/__init__.py +8 -4
- alphai/auth.py +362 -0
- alphai/cli.py +1015 -0
- alphai/client.py +400 -0
- alphai/config.py +88 -0
- alphai/docker.py +764 -0
- alphai/utils.py +192 -0
- alphai-0.1.0.dist-info/METADATA +394 -0
- alphai-0.1.0.dist-info/RECORD +12 -0
- {alphai-0.0.7.dist-info → alphai-0.1.0.dist-info}/WHEEL +2 -1
- alphai-0.1.0.dist-info/entry_points.txt +2 -0
- alphai-0.1.0.dist-info/top_level.txt +1 -0
- alphai/alphai.py +0 -786
- alphai/api/client.py +0 -0
- alphai/benchmarking/benchmarker.py +0 -37
- alphai/client/__init__.py +0 -0
- alphai/client/client.py +0 -382
- alphai/profilers/__init__.py +0 -0
- alphai/profilers/configs_base.py +0 -7
- alphai/profilers/jax.py +0 -37
- alphai/profilers/pytorch.py +0 -83
- alphai/profilers/pytorch_utils.py +0 -419
- alphai/util.py +0 -19
- alphai-0.0.7.dist-info/LICENSE +0 -201
- alphai-0.0.7.dist-info/METADATA +0 -125
- alphai-0.0.7.dist-info/RECORD +0 -16
alphai/__init__.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"""alphai - A CLI tool and Python package for the runalph.ai platform."""
|
|
2
2
|
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__author__ = "American Data Science"
|
|
5
|
+
__email__ = "support@americandatascience.com"
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
from .client import AlphAIClient
|
|
8
|
+
from .config import Config
|
|
9
|
+
|
|
10
|
+
__all__ = ["AlphAIClient", "Config", "__version__"]
|
alphai/auth.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Authentication management for alphai CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import socket
|
|
6
|
+
import webbrowser
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import httpx
|
|
13
|
+
import questionary
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.prompt import Prompt
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
|
|
18
|
+
from .config import Config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
22
|
+
"""HTTP request handler for OAuth callback."""
|
|
23
|
+
|
|
24
|
+
def do_GET(self):
|
|
25
|
+
"""Handle GET request from OAuth callback."""
|
|
26
|
+
# Parse the query parameters
|
|
27
|
+
parsed_url = urllib.parse.urlparse(self.path)
|
|
28
|
+
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
29
|
+
|
|
30
|
+
# Extract token from query parameters
|
|
31
|
+
if 'token' in query_params:
|
|
32
|
+
self.server.token = query_params['token'][0]
|
|
33
|
+
self.send_response(302) # Redirect
|
|
34
|
+
|
|
35
|
+
# Get the API URL for redirection
|
|
36
|
+
api_url = getattr(self.server, 'api_url', 'https://runalph.ai')
|
|
37
|
+
|
|
38
|
+
# Try to use server-hosted success page first, fallback to direct redirect
|
|
39
|
+
try:
|
|
40
|
+
# Check if server has a success page
|
|
41
|
+
success_url = f"{api_url}/auth/cli/success"
|
|
42
|
+
with httpx.Client() as client:
|
|
43
|
+
response = client.head(success_url, timeout=2.0)
|
|
44
|
+
if response.status_code == 200:
|
|
45
|
+
# Server has a success page, use it
|
|
46
|
+
redirect_url = f"{success_url}?redirect_to={urllib.parse.quote(api_url)}"
|
|
47
|
+
else:
|
|
48
|
+
# No success page, redirect directly to dashboard
|
|
49
|
+
redirect_url = api_url
|
|
50
|
+
except:
|
|
51
|
+
# Network error or timeout, redirect directly to dashboard
|
|
52
|
+
redirect_url = api_url
|
|
53
|
+
|
|
54
|
+
self.send_header('Location', redirect_url)
|
|
55
|
+
self.end_headers()
|
|
56
|
+
|
|
57
|
+
elif 'error' in query_params:
|
|
58
|
+
self.server.error = query_params['error'][0]
|
|
59
|
+
self.send_response(400)
|
|
60
|
+
self.send_header('Content-type', 'text/html')
|
|
61
|
+
self.end_headers()
|
|
62
|
+
|
|
63
|
+
# Simple error message for errors
|
|
64
|
+
self.wfile.write(f'''
|
|
65
|
+
<!DOCTYPE html>
|
|
66
|
+
<html>
|
|
67
|
+
<head>
|
|
68
|
+
<title>Authentication Error</title>
|
|
69
|
+
<style>
|
|
70
|
+
body {{ font-family: system-ui; text-align: center; padding: 2rem; }}
|
|
71
|
+
.error {{ color: #ef4444; }}
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<h1 class="error">Authentication Error</h1>
|
|
76
|
+
<p>Please return to your terminal and try again.</p>
|
|
77
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
'''.encode('utf-8'))
|
|
81
|
+
else:
|
|
82
|
+
self.send_response(400)
|
|
83
|
+
self.send_header('Content-type', 'text/html')
|
|
84
|
+
self.end_headers()
|
|
85
|
+
self.wfile.write(b'<html><body><h2>Invalid callback</h2></body></html>')
|
|
86
|
+
|
|
87
|
+
def log_message(self, format, *args):
|
|
88
|
+
"""Suppress log messages."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AuthManager:
|
|
93
|
+
"""Manage authentication for the alphai CLI."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, config: Config):
|
|
96
|
+
"""Initialize the auth manager with configuration."""
|
|
97
|
+
self.config = config
|
|
98
|
+
self.console = Console()
|
|
99
|
+
|
|
100
|
+
def login_with_token(self, token: str) -> bool:
|
|
101
|
+
"""Login with a provided bearer token."""
|
|
102
|
+
if not token.strip():
|
|
103
|
+
self.console.print("[red]Error: Empty token provided[/red]")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# Validate the token
|
|
107
|
+
if self.validate_token(token):
|
|
108
|
+
self.config.set_bearer_token(token)
|
|
109
|
+
return True
|
|
110
|
+
else:
|
|
111
|
+
self.console.print("[red]Error: Invalid token[/red]")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
def interactive_login(self) -> bool:
|
|
115
|
+
"""Perform interactive login."""
|
|
116
|
+
self.console.print(Panel(
|
|
117
|
+
"[bold]alphai Authentication[/bold]\n\n"
|
|
118
|
+
"You can get your token from: https://runalph.ai/account/tokens\n"
|
|
119
|
+
"Or set the ALPHAI_BEARER_TOKEN environment variable.",
|
|
120
|
+
title="Authentication Required",
|
|
121
|
+
title_align="left"
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
# Check if token is in environment first
|
|
125
|
+
env_token = os.getenv("ALPHAI_BEARER_TOKEN")
|
|
126
|
+
if env_token:
|
|
127
|
+
if self.validate_token(env_token):
|
|
128
|
+
self.config.set_bearer_token(env_token)
|
|
129
|
+
self.console.print("[green]✓ Using token from environment variable[/green]")
|
|
130
|
+
return True
|
|
131
|
+
else:
|
|
132
|
+
self.console.print("[yellow]Warning: Invalid token in environment variable[/yellow]")
|
|
133
|
+
|
|
134
|
+
# Use questionary for arrow key menu selection
|
|
135
|
+
method = questionary.select(
|
|
136
|
+
"Choose your authentication method:",
|
|
137
|
+
choices=[
|
|
138
|
+
questionary.Choice("Browser login (recommended)", value="browser"),
|
|
139
|
+
questionary.Choice("Token login", value="token")
|
|
140
|
+
],
|
|
141
|
+
style=questionary.Style([
|
|
142
|
+
('question', 'bold'),
|
|
143
|
+
('selected', 'fg:#00aa00 bold'),
|
|
144
|
+
('pointer', 'fg:#00aa00 bold'),
|
|
145
|
+
('highlighted', 'fg:#00aa00'),
|
|
146
|
+
('answer', 'fg:#00aa00 bold')
|
|
147
|
+
])
|
|
148
|
+
).ask()
|
|
149
|
+
|
|
150
|
+
if not method: # User cancelled (Ctrl+C)
|
|
151
|
+
self.console.print("[yellow]Authentication cancelled[/yellow]")
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
if method == "browser":
|
|
155
|
+
self.console.print("[blue]Starting browser authentication...[/blue]")
|
|
156
|
+
return self.browser_login()
|
|
157
|
+
else:
|
|
158
|
+
# Fallback to manual token entry
|
|
159
|
+
self.console.print("[blue]Manual token authentication[/blue]")
|
|
160
|
+
token = Prompt.ask(
|
|
161
|
+
"Enter your bearer token",
|
|
162
|
+
password=True,
|
|
163
|
+
show_default=False
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if not token:
|
|
167
|
+
self.console.print("[red]No token provided[/red]")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
return self.login_with_token(token)
|
|
171
|
+
|
|
172
|
+
def browser_login(self, port: int = 8080) -> bool:
|
|
173
|
+
"""Perform browser-based login using OAuth flow."""
|
|
174
|
+
# Try to find an available port
|
|
175
|
+
for attempt_port in range(port, port + 10):
|
|
176
|
+
try:
|
|
177
|
+
# Create HTTP server to handle callback
|
|
178
|
+
httpd = HTTPServer(('localhost', attempt_port), CallbackHandler)
|
|
179
|
+
httpd.timeout = 60 # 1 minute timeout
|
|
180
|
+
httpd.token = None
|
|
181
|
+
httpd.error = None
|
|
182
|
+
httpd.api_url = self.config.api_url # Pass api_url to server
|
|
183
|
+
break
|
|
184
|
+
except OSError:
|
|
185
|
+
continue
|
|
186
|
+
else:
|
|
187
|
+
self.console.print("[red]Error: Could not find an available port for callback[/red]")
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
redirect_uri = f"http://localhost:{attempt_port}"
|
|
191
|
+
|
|
192
|
+
# Construct the authentication URL
|
|
193
|
+
auth_url = f"{self.config.api_url}/auth/cli"
|
|
194
|
+
auth_params = {
|
|
195
|
+
"redirect_uri": redirect_uri,
|
|
196
|
+
"response_type": "token",
|
|
197
|
+
"hostname": socket.gethostname() # Get the machine's hostname
|
|
198
|
+
}
|
|
199
|
+
full_auth_url = f"{auth_url}?{urllib.parse.urlencode(auth_params)}"
|
|
200
|
+
|
|
201
|
+
self.console.print(Panel(
|
|
202
|
+
f"[bold]Browser Authentication[/bold]\n\n"
|
|
203
|
+
f"Opening browser for authentication...\n"
|
|
204
|
+
f"If the browser doesn't open automatically, visit:\n"
|
|
205
|
+
f"{full_auth_url}\n\n"
|
|
206
|
+
f"Waiting for authentication callback on port {attempt_port}...",
|
|
207
|
+
title="Browser Login",
|
|
208
|
+
title_align="left"
|
|
209
|
+
))
|
|
210
|
+
|
|
211
|
+
# Open browser
|
|
212
|
+
try:
|
|
213
|
+
webbrowser.open(full_auth_url)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self.console.print(f"[yellow]Warning: Could not open browser automatically: {e}[/yellow]")
|
|
216
|
+
self.console.print(f"[yellow]Please visit: {full_auth_url}[/yellow]")
|
|
217
|
+
|
|
218
|
+
# Start server in a separate thread
|
|
219
|
+
server_thread = threading.Thread(target=httpd.handle_request)
|
|
220
|
+
server_thread.daemon = True
|
|
221
|
+
server_thread.start()
|
|
222
|
+
|
|
223
|
+
# Wait for callback with progress indicator
|
|
224
|
+
start_time = time.time()
|
|
225
|
+
while server_thread.is_alive() and time.time() - start_time < 60:
|
|
226
|
+
time.sleep(0.5)
|
|
227
|
+
# Show a simple progress indicator
|
|
228
|
+
elapsed = int(time.time() - start_time)
|
|
229
|
+
if elapsed % 5 == 0 and elapsed > 0:
|
|
230
|
+
self.console.print(f"[dim]Still waiting... ({elapsed}s elapsed)[/dim]")
|
|
231
|
+
|
|
232
|
+
# Check results
|
|
233
|
+
if hasattr(httpd, 'token') and httpd.token:
|
|
234
|
+
self.console.print("[green]✓ Received authentication token[/green]")
|
|
235
|
+
return self.login_with_token(httpd.token)
|
|
236
|
+
elif hasattr(httpd, 'error') and httpd.error:
|
|
237
|
+
self.console.print(f"[red]Authentication error: {httpd.error}[/red]")
|
|
238
|
+
return False
|
|
239
|
+
else:
|
|
240
|
+
self.console.print("[red]Authentication timed out or failed[/red]")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def validate_token(self, token: str) -> bool:
|
|
244
|
+
"""Validate a bearer token by making a test API call."""
|
|
245
|
+
try:
|
|
246
|
+
with httpx.Client() as client:
|
|
247
|
+
headers = {
|
|
248
|
+
"Authorization": f"Bearer {token}",
|
|
249
|
+
"Content-Type": "application/json"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Test the token by trying to get organizations
|
|
253
|
+
response = client.get(
|
|
254
|
+
f"{self.config.api_url}/api/orgs",
|
|
255
|
+
headers=headers,
|
|
256
|
+
timeout=10.0
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Check if the response is successful
|
|
260
|
+
if response.status_code in (200, 201):
|
|
261
|
+
return True
|
|
262
|
+
elif response.status_code == 401:
|
|
263
|
+
self.console.print("[red]Error: Invalid or expired token[/red]")
|
|
264
|
+
return False
|
|
265
|
+
elif response.status_code == 403:
|
|
266
|
+
self.console.print("[red]Error: Access forbidden - check your permissions[/red]")
|
|
267
|
+
return False
|
|
268
|
+
else:
|
|
269
|
+
self.console.print(f"[red]Error: API returned status {response.status_code}[/red]")
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
except httpx.ConnectError:
|
|
273
|
+
self.console.print(f"[red]Error: Could not connect to API at {self.config.api_url}[/red]")
|
|
274
|
+
return False
|
|
275
|
+
except httpx.TimeoutException:
|
|
276
|
+
self.console.print("[red]Error: Request timed out[/red]")
|
|
277
|
+
return False
|
|
278
|
+
except Exception as e:
|
|
279
|
+
self.console.print(f"[red]Error validating token: {e}[/red]")
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
def refresh_token(self) -> bool:
|
|
283
|
+
"""Refresh the current token if possible."""
|
|
284
|
+
# This would be implemented if the API supports token refresh
|
|
285
|
+
# For now, we'll just validate the existing token
|
|
286
|
+
if self.config.bearer_token:
|
|
287
|
+
return self.validate_token(self.config.bearer_token)
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def get_user_info(self) -> Optional[dict]:
|
|
291
|
+
"""Get information about the currently authenticated user."""
|
|
292
|
+
if not self.config.bearer_token:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
with httpx.Client() as client:
|
|
297
|
+
headers = {
|
|
298
|
+
"Authorization": f"Bearer {self.config.bearer_token}",
|
|
299
|
+
"Content-Type": "application/json"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# Try to get user info (this endpoint may not exist in the actual API)
|
|
303
|
+
response = client.get(
|
|
304
|
+
f"{self.config.api_url}/api/user",
|
|
305
|
+
headers=headers,
|
|
306
|
+
timeout=10.0
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if response.status_code == 200:
|
|
310
|
+
return response.json()
|
|
311
|
+
else:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
except Exception:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
def is_authenticated(self) -> bool:
|
|
318
|
+
"""Check if user is currently authenticated with a valid token."""
|
|
319
|
+
if not self.config.bearer_token:
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
# Validate the current token silently (without console output)
|
|
323
|
+
try:
|
|
324
|
+
with httpx.Client() as client:
|
|
325
|
+
headers = {
|
|
326
|
+
"Authorization": f"Bearer {self.config.bearer_token}",
|
|
327
|
+
"Content-Type": "application/json"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
response = client.get(
|
|
331
|
+
f"{self.config.api_url}/api/orgs",
|
|
332
|
+
headers=headers,
|
|
333
|
+
timeout=10.0
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return response.status_code in (200, 201)
|
|
337
|
+
except Exception:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
def check_existing_authentication(self) -> bool:
|
|
341
|
+
"""Check and validate existing authentication, providing user feedback."""
|
|
342
|
+
if not self.config.bearer_token:
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
self.console.print("[blue]Checking existing authentication...[/blue]")
|
|
346
|
+
|
|
347
|
+
if self.validate_token(self.config.bearer_token):
|
|
348
|
+
self.console.print("[green]✓ Already authenticated and token is valid[/green]")
|
|
349
|
+
|
|
350
|
+
# Try to get user info for additional context
|
|
351
|
+
user_info = self.get_user_info()
|
|
352
|
+
if user_info:
|
|
353
|
+
email = user_info.get('email', 'Unknown')
|
|
354
|
+
self.console.print(f"[green]✓ Logged in as: {email}[/green]")
|
|
355
|
+
|
|
356
|
+
return True
|
|
357
|
+
else:
|
|
358
|
+
self.console.print("[yellow]⚠ Existing token is invalid or expired[/yellow]")
|
|
359
|
+
# Clear the invalid token
|
|
360
|
+
self.config.clear_bearer_token()
|
|
361
|
+
self.config.save()
|
|
362
|
+
return False
|