ml-dash 0.0.11__py3-none-any.whl → 0.5.9__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.
Files changed (47) hide show
  1. ml_dash/__init__.py +59 -1
  2. ml_dash/auto_start.py +42 -0
  3. ml_dash/cli.py +67 -0
  4. ml_dash/cli_commands/__init__.py +1 -0
  5. ml_dash/cli_commands/download.py +797 -0
  6. ml_dash/cli_commands/list.py +343 -0
  7. ml_dash/cli_commands/upload.py +1298 -0
  8. ml_dash/client.py +955 -0
  9. ml_dash/config.py +114 -11
  10. ml_dash/experiment.py +1020 -0
  11. ml_dash/files.py +688 -0
  12. ml_dash/log.py +181 -0
  13. ml_dash/metric.py +292 -0
  14. ml_dash/params.py +188 -0
  15. ml_dash/storage.py +1115 -0
  16. ml_dash-0.5.9.dist-info/METADATA +244 -0
  17. ml_dash-0.5.9.dist-info/RECORD +20 -0
  18. ml_dash-0.5.9.dist-info/WHEEL +4 -0
  19. ml_dash-0.5.9.dist-info/entry_points.txt +3 -0
  20. ml_dash/app.py +0 -33
  21. ml_dash/file_events.py +0 -71
  22. ml_dash/file_handlers.py +0 -141
  23. ml_dash/file_utils.py +0 -5
  24. ml_dash/file_watcher.py +0 -30
  25. ml_dash/main.py +0 -60
  26. ml_dash/mime_types.py +0 -20
  27. ml_dash/schema/__init__.py +0 -110
  28. ml_dash/schema/archive.py +0 -165
  29. ml_dash/schema/directories.py +0 -59
  30. ml_dash/schema/experiments.py +0 -65
  31. ml_dash/schema/files/__init__.py +0 -204
  32. ml_dash/schema/files/file_helpers.py +0 -79
  33. ml_dash/schema/files/images.py +0 -27
  34. ml_dash/schema/files/metrics.py +0 -64
  35. ml_dash/schema/files/parameters.py +0 -50
  36. ml_dash/schema/files/series.py +0 -235
  37. ml_dash/schema/files/videos.py +0 -27
  38. ml_dash/schema/helpers.py +0 -66
  39. ml_dash/schema/projects.py +0 -65
  40. ml_dash/schema/schema_helpers.py +0 -19
  41. ml_dash/schema/users.py +0 -33
  42. ml_dash/sse.py +0 -18
  43. ml_dash-0.0.11.dist-info/METADATA +0 -67
  44. ml_dash-0.0.11.dist-info/RECORD +0 -30
  45. ml_dash-0.0.11.dist-info/WHEEL +0 -5
  46. ml_dash-0.0.11.dist-info/top_level.txt +0 -1
  47. /ml_dash/{example.py → py.typed} +0 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ List command for ML-Dash CLI.
3
+
4
+ Allows users to discover projects and experiments on the remote server.
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ from typing import Optional, List, Dict, Any
10
+ from datetime import datetime
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+ from rich import box
14
+
15
+ from ..client import RemoteClient
16
+ from ..config import Config
17
+
18
+ console = Console()
19
+
20
+
21
+ def _format_timestamp(iso_timestamp: str) -> str:
22
+ """Format ISO timestamp as human-readable relative time."""
23
+ try:
24
+ dt = datetime.fromisoformat(iso_timestamp.replace('Z', '+00:00'))
25
+ now = datetime.now(dt.tzinfo)
26
+ diff = now - dt
27
+
28
+ if diff.days > 365:
29
+ years = diff.days // 365
30
+ return f"{years} year{'s' if years > 1 else ''} ago"
31
+ elif diff.days > 30:
32
+ months = diff.days // 30
33
+ return f"{months} month{'s' if months > 1 else ''} ago"
34
+ elif diff.days > 0:
35
+ return f"{diff.days} day{'s' if diff.days > 1 else ''} ago"
36
+ elif diff.seconds > 3600:
37
+ hours = diff.seconds // 3600
38
+ return f"{hours} hour{'s' if hours > 1 else ''} ago"
39
+ elif diff.seconds > 60:
40
+ minutes = diff.seconds // 60
41
+ return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
42
+ else:
43
+ return "just now"
44
+ except:
45
+ return iso_timestamp
46
+
47
+
48
+ def _get_status_style(status: str) -> str:
49
+ """Get rich style for status."""
50
+ status_styles = {
51
+ "COMPLETED": "green",
52
+ "RUNNING": "yellow",
53
+ "FAILED": "red",
54
+ "ARCHIVED": "dim",
55
+ }
56
+ return status_styles.get(status, "white")
57
+
58
+
59
+ def list_projects(
60
+ remote_client: RemoteClient,
61
+ namespace: str,
62
+ output_json: bool = False,
63
+ verbose: bool = False
64
+ ) -> int:
65
+ """
66
+ List all projects for the user.
67
+
68
+ Args:
69
+ remote_client: Remote API client
70
+ namespace: Namespace slug
71
+ output_json: Output as JSON
72
+ verbose: Show verbose output
73
+
74
+ Returns:
75
+ Exit code (0 for success, 1 for error)
76
+ """
77
+ try:
78
+ # Get projects via GraphQL
79
+ projects = remote_client.list_projects_graphql(namespace)
80
+
81
+ if output_json:
82
+ # JSON output
83
+ output = {
84
+ "namespace": namespace,
85
+ "projects": projects,
86
+ "count": len(projects)
87
+ }
88
+ console.print(json.dumps(output, indent=2))
89
+ return 0
90
+
91
+ # Human-readable output
92
+ if not projects:
93
+ console.print(f"[yellow]No projects found for namespace: {namespace}[/yellow]")
94
+ return 0
95
+
96
+ console.print(f"\n[bold]Projects for {namespace}[/bold]\n")
97
+
98
+ # Create table
99
+ table = Table(box=box.ROUNDED)
100
+ table.add_column("Project", style="cyan", no_wrap=True)
101
+ table.add_column("Experiments", justify="right")
102
+ table.add_column("Description", style="dim")
103
+
104
+ for project in projects:
105
+ exp_count = project.get('experimentCount', 0)
106
+ description = project.get('description', '') or ''
107
+ if len(description) > 50:
108
+ description = description[:47] + "..."
109
+
110
+ table.add_row(
111
+ project['slug'],
112
+ str(exp_count),
113
+ description
114
+ )
115
+
116
+ console.print(table)
117
+ console.print(f"\n[dim]Total: {len(projects)} project(s)[/dim]\n")
118
+
119
+ return 0
120
+
121
+ except Exception as e:
122
+ console.print(f"[red]Error listing projects:[/red] {e}")
123
+ if verbose:
124
+ import traceback
125
+ console.print(traceback.format_exc())
126
+ return 1
127
+
128
+
129
+ def list_experiments(
130
+ remote_client: RemoteClient,
131
+ namespace: str,
132
+ project: str,
133
+ status_filter: Optional[str] = None,
134
+ tags_filter: Optional[List[str]] = None,
135
+ output_json: bool = False,
136
+ detailed: bool = False,
137
+ verbose: bool = False
138
+ ) -> int:
139
+ """
140
+ List experiments in a project.
141
+
142
+ Args:
143
+ remote_client: Remote API client
144
+ namespace: Namespace slug
145
+ project: Project slug
146
+ status_filter: Filter by status (COMPLETED, RUNNING, FAILED, ARCHIVED)
147
+ tags_filter: Filter by tags
148
+ output_json: Output as JSON
149
+ detailed: Show detailed information
150
+ verbose: Show verbose output
151
+
152
+ Returns:
153
+ Exit code (0 for success, 1 for error)
154
+ """
155
+ try:
156
+ # Get experiments via GraphQL
157
+ experiments = remote_client.list_experiments_graphql(
158
+ namespace, project, status=status_filter
159
+ )
160
+
161
+ # Filter by tags if specified
162
+ if tags_filter:
163
+ experiments = [
164
+ exp for exp in experiments
165
+ if any(tag in exp.get('tags', []) for tag in tags_filter)
166
+ ]
167
+
168
+ if output_json:
169
+ # JSON output
170
+ output = {
171
+ "namespace": namespace,
172
+ "project": project,
173
+ "experiments": experiments,
174
+ "count": len(experiments)
175
+ }
176
+ console.print(json.dumps(output, indent=2))
177
+ return 0
178
+
179
+ # Human-readable output
180
+ if not experiments:
181
+ console.print(f"[yellow]No experiments found in project: {project}[/yellow]")
182
+ return 0
183
+
184
+ console.print(f"\n[bold]Experiments in project: {project}[/bold]\n")
185
+
186
+ # Create table
187
+ table = Table(box=box.ROUNDED)
188
+ table.add_column("Experiment", style="cyan", no_wrap=True)
189
+ table.add_column("Status", justify="center")
190
+ table.add_column("Metrics", justify="right")
191
+ table.add_column("Logs", justify="right")
192
+ table.add_column("Files", justify="right")
193
+
194
+ if detailed:
195
+ table.add_column("Tags", style="dim")
196
+ table.add_column("Created", style="dim")
197
+
198
+ for exp in experiments:
199
+ status = exp.get('status', 'UNKNOWN')
200
+ status_style = _get_status_style(status)
201
+
202
+ # Count metrics
203
+ metrics_count = len(exp.get('metrics', []))
204
+
205
+ # Count logs
206
+ log_metadata = exp.get('logMetadata') or {}
207
+ logs_count = log_metadata.get('totalLogs', 0)
208
+
209
+ # Count files
210
+ files_count = len(exp.get('files', []))
211
+
212
+ row = [
213
+ exp['name'],
214
+ f"[{status_style}]{status}[/{status_style}]",
215
+ str(metrics_count),
216
+ str(logs_count),
217
+ str(files_count),
218
+ ]
219
+
220
+ if detailed:
221
+ # Add tags
222
+ tags = exp.get('tags', [])
223
+ tags_str = ', '.join(tags[:3])
224
+ if len(tags) > 3:
225
+ tags_str += f" +{len(tags) - 3}"
226
+ row.append(tags_str or '-')
227
+
228
+ # Add created time
229
+ created_at = exp.get('createdAt', '')
230
+ row.append(_format_timestamp(created_at) if created_at else '-')
231
+
232
+ table.add_row(*row)
233
+
234
+ console.print(table)
235
+ console.print(f"\n[dim]Total: {len(experiments)} experiment(s)[/dim]\n")
236
+
237
+ return 0
238
+
239
+ except Exception as e:
240
+ console.print(f"[red]Error listing experiments:[/red] {e}")
241
+ if verbose:
242
+ import traceback
243
+ console.print(traceback.format_exc())
244
+ return 1
245
+
246
+
247
+ def cmd_list(args: argparse.Namespace) -> int:
248
+ """
249
+ Execute list command.
250
+
251
+ Args:
252
+ args: Parsed command-line arguments
253
+
254
+ Returns:
255
+ Exit code (0 for success, 1 for error)
256
+ """
257
+ # Load config
258
+ config = Config()
259
+
260
+ # Get remote URL (command line > config)
261
+ remote_url = args.remote or config.remote_url
262
+ if not remote_url:
263
+ console.print("[red]Error:[/red] --remote URL is required (or set in config)")
264
+ return 1
265
+
266
+ # Get API key (command line > config > generate from username)
267
+ api_key = args.api_key or config.api_key
268
+
269
+ # If no API key, try to generate from username
270
+ if not api_key:
271
+ if args.username:
272
+ from .upload import generate_api_key_from_username
273
+ api_key = generate_api_key_from_username(args.username)
274
+ if args.verbose:
275
+ console.print(f"[dim]Generated API key from username: {args.username}[/dim]")
276
+ else:
277
+ console.print("[red]Error:[/red] --api-key or --username is required")
278
+ return 1
279
+
280
+ # Get namespace (defaults to username or config)
281
+ namespace = args.namespace or args.username or config.namespace
282
+ if not namespace:
283
+ console.print("[red]Error:[/red] --namespace or --username is required")
284
+ return 1
285
+
286
+ # Create remote client
287
+ try:
288
+ remote_client = RemoteClient(base_url=remote_url, api_key=api_key)
289
+ except Exception as e:
290
+ console.print(f"[red]Error connecting to remote:[/red] {e}")
291
+ return 1
292
+
293
+ # List projects or experiments
294
+ if args.project:
295
+ # Parse tags if provided
296
+ tags_filter = None
297
+ if args.tags:
298
+ tags_filter = [tag.strip() for tag in args.tags.split(',')]
299
+
300
+ return list_experiments(
301
+ remote_client=remote_client,
302
+ namespace=namespace,
303
+ project=args.project,
304
+ status_filter=args.status,
305
+ tags_filter=tags_filter,
306
+ output_json=args.json,
307
+ detailed=args.detailed,
308
+ verbose=args.verbose
309
+ )
310
+ else:
311
+ return list_projects(
312
+ remote_client=remote_client,
313
+ namespace=namespace,
314
+ output_json=args.json,
315
+ verbose=args.verbose
316
+ )
317
+
318
+
319
+ def add_parser(subparsers) -> None:
320
+ """Add list command parser to subparsers."""
321
+ parser = subparsers.add_parser(
322
+ "list",
323
+ help="List projects and experiments on remote server",
324
+ description="Discover projects and experiments available on the remote ML-Dash server."
325
+ )
326
+
327
+ # Remote configuration
328
+ parser.add_argument("--remote", type=str, help="Remote server URL")
329
+ parser.add_argument("--api-key", type=str, help="JWT authentication token")
330
+ parser.add_argument("--username", type=str, help="Username for auto-generating API key")
331
+ parser.add_argument("--namespace", type=str, help="Namespace slug (defaults to username)")
332
+
333
+ # Filtering options
334
+ parser.add_argument("--project", type=str, help="List experiments in this project")
335
+ parser.add_argument("--status", type=str,
336
+ choices=["COMPLETED", "RUNNING", "FAILED", "ARCHIVED"],
337
+ help="Filter experiments by status")
338
+ parser.add_argument("--tags", type=str, help="Filter experiments by tags (comma-separated)")
339
+
340
+ # Output options
341
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
342
+ parser.add_argument("--detailed", action="store_true", help="Show detailed information")
343
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")