ml-dash 0.6.2__py3-none-any.whl → 0.6.2rc1__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.
- ml_dash/__init__.py +64 -36
- ml_dash/auth/token_storage.py +226 -267
- ml_dash/auto_start.py +15 -28
- ml_dash/cli.py +2 -16
- ml_dash/cli_commands/download.py +667 -757
- ml_dash/cli_commands/list.py +13 -146
- ml_dash/cli_commands/login.py +183 -190
- ml_dash/cli_commands/upload.py +1141 -1291
- ml_dash/client.py +6 -79
- ml_dash/config.py +119 -119
- ml_dash/experiment.py +1034 -1234
- ml_dash/files.py +224 -339
- ml_dash/log.py +7 -7
- ml_dash/metric.py +100 -359
- ml_dash/params.py +6 -6
- ml_dash/remote_auto_start.py +17 -20
- ml_dash/run.py +65 -211
- ml_dash/storage.py +1081 -1051
- {ml_dash-0.6.2.dist-info → ml_dash-0.6.2rc1.dist-info}/METADATA +14 -12
- ml_dash-0.6.2rc1.dist-info/RECORD +30 -0
- {ml_dash-0.6.2.dist-info → ml_dash-0.6.2rc1.dist-info}/WHEEL +1 -1
- ml_dash/cli_commands/api.py +0 -165
- ml_dash/cli_commands/profile.py +0 -92
- ml_dash/snowflake.py +0 -173
- ml_dash-0.6.2.dist-info/RECORD +0 -33
- {ml_dash-0.6.2.dist-info → ml_dash-0.6.2rc1.dist-info}/entry_points.txt +0 -0
ml_dash/cli_commands/list.py
CHANGED
|
@@ -252,9 +252,9 @@ def cmd_list(args: argparse.Namespace) -> int:
|
|
|
252
252
|
config = Config()
|
|
253
253
|
|
|
254
254
|
# Get remote URL (command line > config)
|
|
255
|
-
remote_url = args.
|
|
255
|
+
remote_url = args.remote or config.remote_url
|
|
256
256
|
if not remote_url:
|
|
257
|
-
console.print("[red]Error:[/red] --
|
|
257
|
+
console.print("[red]Error:[/red] --remote URL is required (or set in config)")
|
|
258
258
|
return 1
|
|
259
259
|
|
|
260
260
|
# Get API key (command line > config > auto-loaded from storage)
|
|
@@ -274,135 +274,15 @@ def cmd_list(args: argparse.Namespace) -> int:
|
|
|
274
274
|
if args.tags:
|
|
275
275
|
tags_filter = [tag.strip() for tag in args.tags.split(',')]
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
# Simple project pattern: "tut*" -> "*/tut*/*"
|
|
287
|
-
search_pattern = f"*/{args.project}/*"
|
|
288
|
-
else:
|
|
289
|
-
# Full or partial pattern: use as-is
|
|
290
|
-
search_pattern = args.project
|
|
291
|
-
|
|
292
|
-
experiments = remote_client.search_experiments_graphql(search_pattern)
|
|
293
|
-
|
|
294
|
-
# Apply status filter if specified (server doesn't support it in searchExperiments yet)
|
|
295
|
-
if args.status:
|
|
296
|
-
experiments = [
|
|
297
|
-
exp for exp in experiments
|
|
298
|
-
if exp.get('status') == args.status
|
|
299
|
-
]
|
|
300
|
-
|
|
301
|
-
# Apply tags filter if specified
|
|
302
|
-
if tags_filter:
|
|
303
|
-
experiments = [
|
|
304
|
-
exp for exp in experiments
|
|
305
|
-
if any(tag in exp.get('tags', []) for tag in tags_filter)
|
|
306
|
-
]
|
|
307
|
-
|
|
308
|
-
if args.json:
|
|
309
|
-
# JSON output
|
|
310
|
-
output = {
|
|
311
|
-
"pattern": search_pattern,
|
|
312
|
-
"experiments": experiments,
|
|
313
|
-
"count": len(experiments)
|
|
314
|
-
}
|
|
315
|
-
console.print(json.dumps(output, indent=2))
|
|
316
|
-
return 0
|
|
317
|
-
|
|
318
|
-
# Human-readable output
|
|
319
|
-
if not experiments:
|
|
320
|
-
console.print(f"[yellow]No experiments match pattern: {search_pattern}[/yellow]")
|
|
321
|
-
return 0
|
|
322
|
-
|
|
323
|
-
# Group experiments by project for better display
|
|
324
|
-
from collections import defaultdict
|
|
325
|
-
projects_map = defaultdict(list)
|
|
326
|
-
for exp in experiments:
|
|
327
|
-
project_slug = exp.get('project', {}).get('slug', 'unknown')
|
|
328
|
-
projects_map[project_slug].append(exp)
|
|
329
|
-
|
|
330
|
-
# Display each project's experiments
|
|
331
|
-
for project_slug in sorted(projects_map.keys()):
|
|
332
|
-
project_experiments = projects_map[project_slug]
|
|
333
|
-
console.print(f"\n[bold]Experiments in project: {project_slug}[/bold]\n")
|
|
334
|
-
|
|
335
|
-
# Create table
|
|
336
|
-
table = Table(box=box.ROUNDED)
|
|
337
|
-
table.add_column("Experiment", style="cyan", no_wrap=True)
|
|
338
|
-
table.add_column("Status", justify="center")
|
|
339
|
-
table.add_column("Metrics", justify="right")
|
|
340
|
-
table.add_column("Logs", justify="right")
|
|
341
|
-
table.add_column("Files", justify="right")
|
|
342
|
-
|
|
343
|
-
if args.detailed:
|
|
344
|
-
table.add_column("Tags", style="dim")
|
|
345
|
-
table.add_column("Started", style="dim")
|
|
346
|
-
|
|
347
|
-
for exp in project_experiments:
|
|
348
|
-
status = exp.get('status', 'UNKNOWN')
|
|
349
|
-
status_style = _get_status_style(status)
|
|
350
|
-
|
|
351
|
-
# Count metrics
|
|
352
|
-
metrics_count = len(exp.get('metrics', []))
|
|
353
|
-
|
|
354
|
-
# Count logs
|
|
355
|
-
log_metadata = exp.get('logMetadata') or {}
|
|
356
|
-
logs_count = log_metadata.get('totalLogs', 0)
|
|
357
|
-
|
|
358
|
-
# Count files
|
|
359
|
-
files_count = len(exp.get('files', []))
|
|
360
|
-
|
|
361
|
-
row = [
|
|
362
|
-
exp['name'],
|
|
363
|
-
f"[{status_style}]{status}[/{status_style}]",
|
|
364
|
-
str(metrics_count),
|
|
365
|
-
str(logs_count),
|
|
366
|
-
str(files_count),
|
|
367
|
-
]
|
|
368
|
-
|
|
369
|
-
if args.detailed:
|
|
370
|
-
# Add tags
|
|
371
|
-
tags = exp.get('tags', [])
|
|
372
|
-
tags_str = ', '.join(tags[:3])
|
|
373
|
-
if len(tags) > 3:
|
|
374
|
-
tags_str += f" +{len(tags) - 3}"
|
|
375
|
-
row.append(tags_str or '-')
|
|
376
|
-
|
|
377
|
-
# Add started time (createdAt doesn't exist, use startedAt)
|
|
378
|
-
started_at = exp.get('startedAt', '')
|
|
379
|
-
row.append(_format_timestamp(started_at) if started_at else '-')
|
|
380
|
-
|
|
381
|
-
table.add_row(*row)
|
|
382
|
-
|
|
383
|
-
console.print(table)
|
|
384
|
-
console.print(f"[dim]Total: {len(project_experiments)} experiment(s)[/dim]")
|
|
385
|
-
|
|
386
|
-
console.print(f"\n[dim]Grand total: {len(experiments)} experiment(s) across {len(projects_map)} project(s)[/dim]\n")
|
|
387
|
-
return 0
|
|
388
|
-
|
|
389
|
-
except Exception as e:
|
|
390
|
-
console.print(f"[red]Error searching experiments:[/red] {e}")
|
|
391
|
-
if args.verbose:
|
|
392
|
-
import traceback
|
|
393
|
-
console.print(traceback.format_exc())
|
|
394
|
-
return 1
|
|
395
|
-
else:
|
|
396
|
-
# No wildcards, use existing list method for exact project match
|
|
397
|
-
return list_experiments(
|
|
398
|
-
remote_client=remote_client,
|
|
399
|
-
project=args.project,
|
|
400
|
-
status_filter=args.status,
|
|
401
|
-
tags_filter=tags_filter,
|
|
402
|
-
output_json=args.json,
|
|
403
|
-
detailed=args.detailed,
|
|
404
|
-
verbose=args.verbose
|
|
405
|
-
)
|
|
277
|
+
return list_experiments(
|
|
278
|
+
remote_client=remote_client,
|
|
279
|
+
project=args.project,
|
|
280
|
+
status_filter=args.status,
|
|
281
|
+
tags_filter=tags_filter,
|
|
282
|
+
output_json=args.json,
|
|
283
|
+
detailed=args.detailed,
|
|
284
|
+
verbose=args.verbose
|
|
285
|
+
)
|
|
406
286
|
else:
|
|
407
287
|
return list_projects(
|
|
408
288
|
remote_client=remote_client,
|
|
@@ -420,27 +300,14 @@ def add_parser(subparsers) -> None:
|
|
|
420
300
|
)
|
|
421
301
|
|
|
422
302
|
# Remote configuration
|
|
423
|
-
parser.add_argument(
|
|
424
|
-
"--dash-url",
|
|
425
|
-
type=str,
|
|
426
|
-
help="ML-Dash server URL (defaults to config or https://api.dash.ml)",
|
|
427
|
-
)
|
|
303
|
+
parser.add_argument("--remote", type=str, help="Remote server URL")
|
|
428
304
|
parser.add_argument(
|
|
429
305
|
"--api-key",
|
|
430
306
|
type=str,
|
|
431
307
|
help="JWT authentication token (auto-loaded from storage if not provided)"
|
|
432
308
|
)
|
|
433
309
|
# Filtering options
|
|
434
|
-
parser.add_argument(
|
|
435
|
-
"-p",
|
|
436
|
-
"--pref",
|
|
437
|
-
"--prefix",
|
|
438
|
-
"--proj",
|
|
439
|
-
"--project",
|
|
440
|
-
dest="project",
|
|
441
|
-
type=str,
|
|
442
|
-
help="List experiments in this project (supports glob: 'tutorial*', 'test-?', 'proj-[0-9]*')"
|
|
443
|
-
)
|
|
310
|
+
parser.add_argument("--project", type=str, help="List experiments in this project")
|
|
444
311
|
parser.add_argument("--status", type=str,
|
|
445
312
|
choices=["COMPLETED", "RUNNING", "FAILED", "ARCHIVED"],
|
|
446
313
|
help="Filter experiments by status")
|
ml_dash/cli_commands/login.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Login command for ml-dash CLI."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
import webbrowser
|
|
5
|
+
from typing import Optional
|
|
4
6
|
|
|
5
7
|
from rich.console import Console
|
|
6
8
|
from rich.panel import Panel
|
|
@@ -8,225 +10,216 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
8
10
|
|
|
9
11
|
from ml_dash.auth.device_flow import DeviceFlowClient
|
|
10
12
|
from ml_dash.auth.device_secret import get_or_create_device_secret
|
|
13
|
+
from ml_dash.auth.token_storage import get_token_storage
|
|
11
14
|
from ml_dash.auth.exceptions import (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
DeviceCodeExpiredError,
|
|
16
|
+
AuthorizationDeniedError,
|
|
17
|
+
TokenExchangeError,
|
|
15
18
|
)
|
|
16
|
-
from ml_dash.auth.token_storage import get_token_storage
|
|
17
19
|
from ml_dash.config import config
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def add_parser(subparsers):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
parser.add_argument(
|
|
45
|
-
"--no-browser",
|
|
46
|
-
action="store_true",
|
|
47
|
-
help="Don't automatically open browser for authorization",
|
|
48
|
-
)
|
|
23
|
+
"""Add login command parser.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
subparsers: Subparsers object from argparse
|
|
27
|
+
"""
|
|
28
|
+
parser = subparsers.add_parser(
|
|
29
|
+
"login",
|
|
30
|
+
help="Authenticate with ml-dash using device authorization flow",
|
|
31
|
+
description="Login to ml-dash server using OAuth2 device authorization flow",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--remote",
|
|
36
|
+
type=str,
|
|
37
|
+
help="ML-Dash server URL (e.g., https://api.ml-dash.com)",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--no-browser",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Don't automatically open browser for authorization",
|
|
44
|
+
)
|
|
49
45
|
|
|
50
46
|
|
|
51
47
|
def generate_qr_code_ascii(url: str) -> str:
|
|
52
|
-
|
|
48
|
+
"""Generate ASCII QR code for the given URL.
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
Args:
|
|
51
|
+
url: URL to encode in QR code
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
Returns:
|
|
54
|
+
ASCII art QR code string
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
import qrcode
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
qr = qrcode.QRCode(border=1)
|
|
60
|
+
qr.add_data(url)
|
|
61
|
+
qr.make(fit=True)
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
# Generate ASCII art
|
|
64
|
+
output = []
|
|
65
|
+
for row in qr.get_matrix():
|
|
66
|
+
line = ""
|
|
67
|
+
for cell in row:
|
|
68
|
+
line += "██" if cell else " "
|
|
69
|
+
output.append(line)
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
return "\n".join(output)
|
|
72
|
+
except ImportError:
|
|
73
|
+
return "[QR code unavailable - install qrcode: pip install qrcode]"
|
|
74
|
+
except Exception:
|
|
75
|
+
return "[QR code generation failed]"
|
|
80
76
|
|
|
81
77
|
|
|
82
78
|
def cmd_login(args) -> int:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
args: Parsed command-line arguments
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
Exit code (0 for success, 1 for failure)
|
|
90
|
-
"""
|
|
91
|
-
console = Console()
|
|
92
|
-
|
|
93
|
-
# Get remote URL
|
|
94
|
-
remote_url = args.dash_url or config.remote_url
|
|
95
|
-
if not remote_url:
|
|
96
|
-
console.print(
|
|
97
|
-
"[red]Error: No remote URL configured.[/red]\n\n"
|
|
98
|
-
"Please specify with --dash-url or set default:\n"
|
|
99
|
-
" ml-dash login --dash-url https://api.dash.ml"
|
|
100
|
-
)
|
|
101
|
-
return 1
|
|
79
|
+
"""Execute login command.
|
|
102
80
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
console.print("[bold]Initializing device authorization...[/bold]\n")
|
|
81
|
+
Args:
|
|
82
|
+
args: Parsed command-line arguments
|
|
106
83
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
84
|
+
Returns:
|
|
85
|
+
Exit code (0 for success, 1 for failure)
|
|
86
|
+
"""
|
|
87
|
+
console = Console()
|
|
111
88
|
|
|
112
|
-
#
|
|
113
|
-
|
|
89
|
+
# Get remote URL
|
|
90
|
+
remote_url = args.remote or config.remote_url
|
|
91
|
+
if not remote_url:
|
|
92
|
+
console.print(
|
|
93
|
+
"[red]Error: No remote URL configured.[/red]\n\n"
|
|
94
|
+
"Please specify with --remote or set default:\n"
|
|
95
|
+
" ml-dash login --remote https://api.ml-dash.com"
|
|
96
|
+
)
|
|
97
|
+
return 1
|
|
114
98
|
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
try:
|
|
100
|
+
# Initialize device flow
|
|
101
|
+
console.print("[bold]Initializing device authorization...[/bold]\n")
|
|
117
102
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
f"[bold cyan]2. Enter this code:[/bold cyan]\n\n"
|
|
123
|
-
f" [bold green]{flow.user_code}[/bold green]\n\n"
|
|
124
|
-
)
|
|
103
|
+
device_secret = get_or_create_device_secret(config)
|
|
104
|
+
device_client = DeviceFlowClient(
|
|
105
|
+
device_secret=device_secret, ml_dash_server_url=remote_url
|
|
106
|
+
)
|
|
125
107
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
panel_content += f"[bold cyan]Or scan QR code:[/bold cyan]\n\n{qr_code}\n\n"
|
|
108
|
+
# Start device flow with vuer-auth
|
|
109
|
+
flow = device_client.start_device_flow()
|
|
129
110
|
|
|
130
|
-
|
|
111
|
+
# Generate QR code
|
|
112
|
+
qr_code = generate_qr_code_ascii(flow.verification_uri_complete)
|
|
131
113
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.print(panel)
|
|
139
|
-
console.print()
|
|
140
|
-
|
|
141
|
-
# Auto-open browser unless disabled
|
|
142
|
-
if not args.no_browser:
|
|
143
|
-
try:
|
|
144
|
-
webbrowser.open(flow.verification_uri_complete)
|
|
145
|
-
console.print("[dim]✓ Opened browser automatically[/dim]\n")
|
|
146
|
-
except Exception:
|
|
147
|
-
# Silent failure - user can manually open URL
|
|
148
|
-
pass
|
|
149
|
-
|
|
150
|
-
# Poll for authorization with progress indicator
|
|
151
|
-
console.print("[bold]Waiting for authorization...[/bold]")
|
|
152
|
-
|
|
153
|
-
with Progress(
|
|
154
|
-
SpinnerColumn(),
|
|
155
|
-
TextColumn("[progress.description]{task.description}"),
|
|
156
|
-
console=console,
|
|
157
|
-
transient=True,
|
|
158
|
-
) as progress:
|
|
159
|
-
task = progress.add_task("Polling", total=None)
|
|
160
|
-
|
|
161
|
-
def update_progress(elapsed: int):
|
|
162
|
-
progress.update(task, description=f"Waiting ({elapsed}s)")
|
|
163
|
-
|
|
164
|
-
try:
|
|
165
|
-
vuer_auth_token = device_client.poll_for_token(
|
|
166
|
-
max_attempts=120, progress_callback=update_progress
|
|
114
|
+
# Display rich UI with QR code
|
|
115
|
+
panel_content = (
|
|
116
|
+
f"[bold cyan]1. Visit this URL:[/bold cyan]\n\n"
|
|
117
|
+
f" {flow.verification_uri}\n\n"
|
|
118
|
+
f"[bold cyan]2. Enter this code:[/bold cyan]\n\n"
|
|
119
|
+
f" [bold green]{flow.user_code}[/bold green]\n\n"
|
|
167
120
|
)
|
|
168
|
-
except DeviceCodeExpiredError:
|
|
169
|
-
console.print(
|
|
170
|
-
"\n[red]✗ Device code expired[/red]\n\n"
|
|
171
|
-
"The authorization code expired after 10 minutes.\n"
|
|
172
|
-
"Please run 'ml-dash login' again."
|
|
173
|
-
)
|
|
174
|
-
return 1
|
|
175
|
-
except AuthorizationDeniedError:
|
|
176
|
-
console.print(
|
|
177
|
-
"\n[red]✗ Authorization denied[/red]\n\n"
|
|
178
|
-
"You declined the authorization request in your browser.\n\n"
|
|
179
|
-
"To try again:\n"
|
|
180
|
-
" ml-dash login"
|
|
181
|
-
)
|
|
182
|
-
return 1
|
|
183
|
-
except TimeoutError:
|
|
184
|
-
console.print(
|
|
185
|
-
"\n[red]✗ Authorization timed out[/red]\n\n"
|
|
186
|
-
"No response after 10 minutes.\n\n"
|
|
187
|
-
"Please run 'ml-dash login' again."
|
|
188
|
-
)
|
|
189
|
-
return 1
|
|
190
121
|
|
|
191
|
-
|
|
122
|
+
# Add QR code if available
|
|
123
|
+
if "unavailable" not in qr_code and "failed" not in qr_code:
|
|
124
|
+
panel_content += f"[bold cyan]Or scan QR code:[/bold cyan]\n\n{qr_code}\n\n"
|
|
192
125
|
|
|
193
|
-
|
|
194
|
-
console.print("[bold]Exchanging token with ml-dash server...[/bold]")
|
|
126
|
+
panel_content += f"[dim]Code expires in {flow.expires_in // 60} minutes[/dim]"
|
|
195
127
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
128
|
+
panel = Panel(
|
|
129
|
+
panel_content,
|
|
130
|
+
title="[bold blue]DEVICE AUTHORIZATION REQUIRED[/bold blue]",
|
|
131
|
+
border_style="blue",
|
|
132
|
+
expand=False,
|
|
133
|
+
)
|
|
134
|
+
console.print(panel)
|
|
135
|
+
console.print()
|
|
136
|
+
|
|
137
|
+
# Auto-open browser unless disabled
|
|
138
|
+
if not args.no_browser:
|
|
139
|
+
try:
|
|
140
|
+
webbrowser.open(flow.verification_uri_complete)
|
|
141
|
+
console.print("[dim]✓ Opened browser automatically[/dim]\n")
|
|
142
|
+
except Exception:
|
|
143
|
+
# Silent failure - user can manually open URL
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Poll for authorization with progress indicator
|
|
147
|
+
console.print("[bold]Waiting for authorization...[/bold]")
|
|
148
|
+
|
|
149
|
+
with Progress(
|
|
150
|
+
SpinnerColumn(),
|
|
151
|
+
TextColumn("[progress.description]{task.description}"),
|
|
152
|
+
console=console,
|
|
153
|
+
transient=True,
|
|
154
|
+
) as progress:
|
|
155
|
+
task = progress.add_task("Polling", total=None)
|
|
156
|
+
|
|
157
|
+
def update_progress(elapsed: int):
|
|
158
|
+
progress.update(task, description=f"Waiting ({elapsed}s)")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
vuer_auth_token = device_client.poll_for_token(
|
|
162
|
+
max_attempts=120, progress_callback=update_progress
|
|
163
|
+
)
|
|
164
|
+
except DeviceCodeExpiredError:
|
|
165
|
+
console.print(
|
|
166
|
+
"\n[red]✗ Device code expired[/red]\n\n"
|
|
167
|
+
"The authorization code expired after 10 minutes.\n"
|
|
168
|
+
"Please run 'ml-dash login' again."
|
|
169
|
+
)
|
|
170
|
+
return 1
|
|
171
|
+
except AuthorizationDeniedError:
|
|
172
|
+
console.print(
|
|
173
|
+
"\n[red]✗ Authorization denied[/red]\n\n"
|
|
174
|
+
"You declined the authorization request in your browser.\n\n"
|
|
175
|
+
"To try again:\n"
|
|
176
|
+
" ml-dash login"
|
|
177
|
+
)
|
|
178
|
+
return 1
|
|
179
|
+
except TimeoutError:
|
|
180
|
+
console.print(
|
|
181
|
+
"\n[red]✗ Authorization timed out[/red]\n\n"
|
|
182
|
+
"No response after 10 minutes.\n\n"
|
|
183
|
+
"Please run 'ml-dash login' again."
|
|
184
|
+
)
|
|
185
|
+
return 1
|
|
186
|
+
|
|
187
|
+
console.print("[green]✓ Authorization successful![/green]\n")
|
|
188
|
+
|
|
189
|
+
# Exchange vuer-auth token for ml-dash token
|
|
190
|
+
console.print("[bold]Exchanging token with ml-dash server...[/bold]")
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
ml_dash_token = device_client.exchange_token(vuer_auth_token)
|
|
194
|
+
except TokenExchangeError as e:
|
|
195
|
+
console.print(f"\n[red]✗ Token exchange failed:[/red] {e}\n")
|
|
196
|
+
return 1
|
|
197
|
+
|
|
198
|
+
# Store ml-dash permanent token
|
|
199
|
+
storage = get_token_storage()
|
|
200
|
+
storage.store("ml-dash-token", ml_dash_token)
|
|
201
|
+
|
|
202
|
+
console.print("[green]✓ Token exchanged successfully![/green]\n")
|
|
203
|
+
|
|
204
|
+
# Success message
|
|
205
|
+
console.print(
|
|
206
|
+
"[bold green]✓ Logged in successfully![/bold green]\n\n"
|
|
207
|
+
"Your authentication token has been securely stored.\n"
|
|
208
|
+
"You can now use ml-dash commands without --api-key.\n\n"
|
|
209
|
+
"Examples:\n"
|
|
210
|
+
" ml-dash upload ./experiments\n"
|
|
211
|
+
" ml-dash download ./output\n"
|
|
212
|
+
" ml-dash list"
|
|
213
|
+
)
|
|
221
214
|
|
|
222
|
-
|
|
215
|
+
return 0
|
|
223
216
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
217
|
+
except KeyboardInterrupt:
|
|
218
|
+
console.print("\n\n[yellow]Login cancelled by user.[/yellow]")
|
|
219
|
+
return 1
|
|
220
|
+
except Exception as e:
|
|
221
|
+
console.print(f"\n[red]✗ Unexpected error:[/red] {e}")
|
|
222
|
+
import traceback
|
|
230
223
|
|
|
231
|
-
|
|
232
|
-
|
|
224
|
+
console.print(f"\n[dim]{traceback.format_exc()}[/dim]")
|
|
225
|
+
return 1
|