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.
@@ -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.dash_url or config.remote_url
255
+ remote_url = args.remote or config.remote_url
256
256
  if not remote_url:
257
- console.print("[red]Error:[/red] --dash-url is required (or set in config)")
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
- # Check if pattern contains wildcards
278
- has_wildcards = any(c in args.project for c in ['*', '?', '['])
279
-
280
- if has_wildcards:
281
- # Use searchExperiments GraphQL query for glob patterns
282
- try:
283
- # Expand simple project patterns to full namespace/project/experiment format
284
- # If pattern has no slashes, assume it's just a project pattern
285
- if '/' not in args.project:
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")
@@ -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
- AuthorizationDeniedError,
13
- DeviceCodeExpiredError,
14
- TokenExchangeError,
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
- """Add login command parser.
22
-
23
- Args:
24
- subparsers: Subparsers object from argparse
25
- """
26
- parser = subparsers.add_parser(
27
- "login",
28
- help="Authenticate with ml-dash using device authorization flow",
29
- description=(
30
- "Login to ml-dash server using OAuth2 device authorization flow.\n\n"
31
- "After logging in, you can:\n"
32
- " • Upload/download experiments via CLI\n"
33
- " • View projects, experiments, and statistics at https://dash.ml\n"
34
- " • Create interactive plots and dashboards\n"
35
- ),
36
- )
37
-
38
- parser.add_argument(
39
- "--dash-url",
40
- type=str,
41
- help="ML-Dash server URL (e.g., https://api.dash.ml)",
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
- """Generate ASCII QR code for the given URL.
48
+ """Generate ASCII QR code for the given URL.
53
49
 
54
- Args:
55
- url: URL to encode in QR code
50
+ Args:
51
+ url: URL to encode in QR code
56
52
 
57
- Returns:
58
- ASCII art QR code string
59
- """
60
- try:
61
- import qrcode
53
+ Returns:
54
+ ASCII art QR code string
55
+ """
56
+ try:
57
+ import qrcode
62
58
 
63
- qr = qrcode.QRCode(border=1)
64
- qr.add_data(url)
65
- qr.make(fit=True)
59
+ qr = qrcode.QRCode(border=1)
60
+ qr.add_data(url)
61
+ qr.make(fit=True)
66
62
 
67
- # Generate ASCII art
68
- output = []
69
- for row in qr.get_matrix():
70
- line = ""
71
- for cell in row:
72
- line += "██" if cell else " "
73
- output.append(line)
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
- return "\n".join(output)
76
- except ImportError:
77
- return "[QR code unavailable - install qrcode: pip install qrcode]"
78
- except Exception:
79
- return "[QR code generation failed]"
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
- """Execute login command.
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
- try:
104
- # Initialize device flow
105
- console.print("[bold]Initializing device authorization...[/bold]\n")
81
+ Args:
82
+ args: Parsed command-line arguments
106
83
 
107
- device_secret = get_or_create_device_secret(config)
108
- device_client = DeviceFlowClient(
109
- device_secret=device_secret, ml_dash_server_url=remote_url
110
- )
84
+ Returns:
85
+ Exit code (0 for success, 1 for failure)
86
+ """
87
+ console = Console()
111
88
 
112
- # Start device flow with vuer-auth
113
- flow = device_client.start_device_flow()
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
- # Generate QR code
116
- qr_code = generate_qr_code_ascii(flow.verification_uri_complete)
99
+ try:
100
+ # Initialize device flow
101
+ console.print("[bold]Initializing device authorization...[/bold]\n")
117
102
 
118
- # Display rich UI with QR code
119
- panel_content = (
120
- f"[bold cyan]1. Visit this URL:[/bold cyan]\n\n"
121
- f" {flow.verification_uri}\n\n"
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
- # Add QR code if available
127
- if "unavailable" not in qr_code and "failed" not in qr_code:
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
- panel_content += f"[dim]Code expires in {flow.expires_in // 60} minutes[/dim]"
111
+ # Generate QR code
112
+ qr_code = generate_qr_code_ascii(flow.verification_uri_complete)
131
113
 
132
- panel = Panel(
133
- panel_content,
134
- title="[bold blue]DEVICE AUTHORIZATION REQUIRED[/bold blue]",
135
- border_style="blue",
136
- expand=False,
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
- console.print("[green]✓ Authorization successful![/green]\n")
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
- # Exchange vuer-auth token for ml-dash token
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
- try:
197
- ml_dash_token = device_client.exchange_token(vuer_auth_token)
198
- except TokenExchangeError as e:
199
- console.print(f"\n[red]✗ Token exchange failed:[/red] {e}\n")
200
- return 1
201
-
202
- # Store ml-dash permanent token
203
- storage = get_token_storage()
204
- storage.store("ml-dash-token", ml_dash_token)
205
-
206
- console.print("[green]✓ Token exchanged successfully![/green]\n")
207
-
208
- # Success message
209
- console.print(
210
- "[bold green]✓ Logged in successfully![/bold green]\n\n"
211
- "Your authentication token has been securely stored.\n"
212
- "You can now use ml-dash commands without --api-key.\n\n"
213
- "[bold cyan]View your data online:[/bold cyan]\n"
214
- " [link=https://dash.ml]https://dash.ml[/link]\n\n"
215
- "Access your projects, experiments, statistics, and interactive plots.\n\n"
216
- "[bold]CLI Commands:[/bold]\n"
217
- " ml-dash upload ./experiments\n"
218
- " ml-dash download ./output\n"
219
- " ml-dash list"
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
- return 0
215
+ return 0
223
216
 
224
- except KeyboardInterrupt:
225
- console.print("\n\n[yellow]Login cancelled by user.[/yellow]")
226
- return 1
227
- except Exception as e:
228
- console.print(f"\n[red]✗ Unexpected error:[/red] {e}")
229
- import traceback
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
- console.print(f"\n[dim]{traceback.format_exc()}[/dim]")
232
- return 1
224
+ console.print(f"\n[dim]{traceback.format_exc()}[/dim]")
225
+ return 1