ml-dash 0.6.10__py3-none-any.whl → 0.6.12__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/buffer.py CHANGED
@@ -340,18 +340,21 @@ class BackgroundBufferManager:
340
340
  if items:
341
341
  print(f"[ML-Dash] Flushing {', '.join(items)}...", flush=True)
342
342
 
343
- # Flush logs immediately
344
- self._flush_logs()
343
+ # Flush logs immediately (loop until empty)
344
+ while not self._log_queue.empty():
345
+ self._flush_logs()
345
346
 
346
- # Flush all metrics immediately
347
+ # Flush all metrics immediately (loop until empty for each metric)
347
348
  for metric_name in list(self._metric_queues.keys()):
348
- self._flush_metric(metric_name)
349
+ while not self._metric_queues[metric_name].empty():
350
+ self._flush_metric(metric_name)
349
351
 
350
352
  # Flush all tracks immediately
351
353
  self.flush_tracks()
352
354
 
353
- # Flush files immediately
354
- self._flush_files()
355
+ # Flush files immediately (loop until empty)
356
+ while not self._file_queue.empty():
357
+ self._flush_files()
355
358
 
356
359
  if log_count > 0 or metric_count > 0 or track_count > 0 or file_count > 0:
357
360
  print("[ML-Dash] ✓ Flush complete", flush=True)
ml_dash/cli.py CHANGED
@@ -25,7 +25,7 @@ def create_parser() -> argparse.ArgumentParser:
25
25
  )
26
26
 
27
27
  # Import and add command parsers
28
- from .cli_commands import upload, download, list as list_cmd, login, logout, profile, api, create
28
+ from .cli_commands import upload, download, list as list_cmd, login, logout, profile, api, create, remove
29
29
 
30
30
  # Authentication commands
31
31
  login.add_parser(subparsers)
@@ -37,6 +37,7 @@ def create_parser() -> argparse.ArgumentParser:
37
37
 
38
38
  # Project commands
39
39
  create.add_parser(subparsers)
40
+ remove.add_parser(subparsers)
40
41
 
41
42
  # Data commands
42
43
  upload.add_parser(subparsers)
@@ -77,6 +78,9 @@ def main(argv: Optional[List[str]] = None) -> int:
77
78
  elif args.command == "create":
78
79
  from .cli_commands import create
79
80
  return create.cmd_create(args)
81
+ elif args.command == "remove":
82
+ from .cli_commands import remove
83
+ return remove.cmd_remove(args)
80
84
  elif args.command == "upload":
81
85
  from .cli_commands import upload
82
86
  return upload.cmd_upload(args)
@@ -0,0 +1,161 @@
1
+ """Remove command for ml-dash CLI - delete projects."""
2
+
3
+ import argparse
4
+ from typing import Optional
5
+
6
+ from rich.console import Console
7
+
8
+ from ml_dash.client import RemoteClient
9
+ from ml_dash.config import config
10
+
11
+
12
+ def add_parser(subparsers):
13
+ """Add remove command parser."""
14
+ parser = subparsers.add_parser(
15
+ "remove",
16
+ help="Delete a project",
17
+ description="""Delete a project from ml-dash.
18
+
19
+ WARNING: This will delete the project and all its experiments, metrics, files, and logs.
20
+ This action cannot be undone.
21
+
22
+ Examples:
23
+ # Delete a project in current user's namespace
24
+ ml-dash remove -p my-project
25
+
26
+ # Delete a project in a specific namespace
27
+ ml-dash remove -p geyang/old-project
28
+
29
+ # Skip confirmation prompt (use with caution!)
30
+ ml-dash remove -p my-project -y
31
+ """,
32
+ formatter_class=argparse.RawDescriptionHelpFormatter,
33
+ )
34
+ parser.add_argument(
35
+ "-p", "--prefix",
36
+ type=str,
37
+ required=True,
38
+ help="Project name or namespace/project",
39
+ )
40
+ parser.add_argument(
41
+ "-y", "--yes",
42
+ action="store_true",
43
+ help="Skip confirmation prompt",
44
+ )
45
+ parser.add_argument(
46
+ "--dash-url",
47
+ type=str,
48
+ help="ML-Dash server URL (default: https://api.dash.ml)",
49
+ )
50
+
51
+
52
+ def cmd_remove(args) -> int:
53
+ """Execute remove command."""
54
+ console = Console()
55
+
56
+ # Get remote URL
57
+ remote_url = args.dash_url or config.remote_url or "https://api.dash.ml"
58
+
59
+ # Parse the prefix
60
+ prefix = args.prefix.strip("/")
61
+ parts = prefix.split("/")
62
+
63
+ if len(parts) > 2:
64
+ console.print(
65
+ f"[red]Error:[/red] Prefix can have at most 2 parts (namespace/project).\n"
66
+ f"Got: {args.prefix}\n\n"
67
+ f"Examples:\n"
68
+ f" ml-dash remove -p my-project\n"
69
+ f" ml-dash remove -p geyang/old-project"
70
+ )
71
+ return 1
72
+
73
+ if len(parts) == 1:
74
+ # Format: project (use current user's namespace)
75
+ namespace = None
76
+ project_name = parts[0]
77
+ else:
78
+ # Format: namespace/project
79
+ namespace = parts[0]
80
+ project_name = parts[1]
81
+
82
+ return _remove_project(
83
+ namespace=namespace,
84
+ project_name=project_name,
85
+ dash_url=remote_url,
86
+ skip_confirm=args.yes,
87
+ console=console,
88
+ )
89
+
90
+
91
+ def _remove_project(
92
+ namespace: Optional[str],
93
+ project_name: str,
94
+ dash_url: str,
95
+ skip_confirm: bool,
96
+ console: Console,
97
+ ) -> int:
98
+ """Remove a project."""
99
+ try:
100
+ # Initialize client (namespace will be auto-fetched from server if not provided)
101
+ client = RemoteClient(base_url=dash_url, namespace=namespace)
102
+
103
+ # Get namespace (triggers server query if not set)
104
+ namespace = client.namespace
105
+
106
+ if not namespace:
107
+ console.print("[red]Error:[/red] Could not determine namespace. Please login first.")
108
+ return 1
109
+
110
+ full_path = f"{namespace}/{project_name}"
111
+
112
+ # Get project ID to verify it exists
113
+ project_id = client._get_project_id(project_name)
114
+ if not project_id:
115
+ console.print(f"[yellow]⚠[/yellow] Project '[bold]{full_path}[/bold]' not found.")
116
+ return 1
117
+
118
+ # Confirmation prompt (unless -y flag is used)
119
+ if not skip_confirm:
120
+ console.print(
121
+ f"\n[red bold]⚠ WARNING ⚠[/red bold]\n\n"
122
+ f"You are about to delete project: [bold]{full_path}[/bold]\n"
123
+ f"This will permanently delete:\n"
124
+ f" • All experiments in this project\n"
125
+ f" • All metrics and logs\n"
126
+ f" • All uploaded files\n\n"
127
+ f"[red]This action CANNOT be undone.[/red]\n"
128
+ )
129
+ confirm = console.input("Type the project name to confirm deletion: ")
130
+ if confirm.strip() != project_name:
131
+ console.print("\n[yellow]Deletion cancelled.[/yellow]")
132
+ return 0
133
+
134
+ console.print(f"\n[dim]Deleting project '{full_path}'...[/dim]")
135
+
136
+ # Delete project using client method
137
+ result = client.delete_project(project_name)
138
+
139
+ # Success message
140
+ console.print(f"[green]✓[/green] {result.get('message', 'Project deleted successfully!')}")
141
+ console.print(f" Name: [bold]{project_name}[/bold]")
142
+ console.print(f" Namespace: [bold]{namespace}[/bold]")
143
+ console.print(f" Project ID: {project_id}")
144
+ console.print(f" Deleted nodes: {result.get('deleted', 0)}")
145
+ console.print(f" Deleted experiments: {result.get('experiments', 0)}")
146
+
147
+ return 0
148
+
149
+ except Exception as e:
150
+ # Check if it's a 404 not found
151
+ if hasattr(e, 'response') and hasattr(e.response, 'status_code') and e.response.status_code == 404:
152
+ console.print(f"[yellow]⚠[/yellow] Project '[bold]{project_name}[/bold]' not found in namespace '[bold]{namespace}[/bold]'")
153
+ return 1
154
+
155
+ # Check if it's a 403 forbidden
156
+ if hasattr(e, 'response') and hasattr(e.response, 'status_code') and e.response.status_code == 403:
157
+ console.print(f"[red]Error:[/red] Permission denied. You don't have permission to delete this project.")
158
+ return 1
159
+
160
+ console.print(f"[red]Error deleting project:[/red] {e}")
161
+ return 1
@@ -67,7 +67,7 @@ class UploadResult:
67
67
  class UploadState:
68
68
  """Tracks upload state for resume functionality."""
69
69
 
70
- local_path: str
70
+ dash_root: str
71
71
  remote_url: str
72
72
  completed_experiments: List[str] = field(
73
73
  default_factory=list
@@ -79,7 +79,7 @@ class UploadState:
79
79
  def to_dict(self) -> Dict[str, Any]:
80
80
  """Convert to dictionary for JSON serialization."""
81
81
  return {
82
- "local_path": self.local_path,
82
+ "dash_root": self.dash_root,
83
83
  "remote_url": self.remote_url,
84
84
  "completed_experiments": self.completed_experiments,
85
85
  "failed_experiments": self.failed_experiments,
@@ -91,7 +91,7 @@ class UploadState:
91
91
  def from_dict(cls, data: Dict[str, Any]) -> "UploadState":
92
92
  """Create from dictionary."""
93
93
  return cls(
94
- local_path=data["local_path"],
94
+ dash_root=data["dash_root"],
95
95
  remote_url=data["remote_url"],
96
96
  completed_experiments=data.get("completed_experiments", []),
97
97
  failed_experiments=data.get("failed_experiments", []),
@@ -265,18 +265,18 @@ def add_parser(subparsers) -> argparse.ArgumentParser:
265
265
 
266
266
 
267
267
  def discover_experiments(
268
- local_path: Path,
268
+ dash_root: Path,
269
269
  project_filter: Optional[str] = None,
270
270
  experiment_filter: Optional[str] = None,
271
271
  ) -> List[ExperimentInfo]:
272
272
  """
273
273
  Discover experiments in local storage directory.
274
274
 
275
- Supports both flat (local_path/project/experiment) and folder-based
276
- (local_path/folder/project/experiment) hierarchies.
275
+ Supports both flat (dash_root/project/experiment) and folder-based
276
+ (dash_root/folder/project/experiment) hierarchies.
277
277
 
278
278
  Args:
279
- local_path: Root path of local storage
279
+ dash_root: Root path of local storage
280
280
  project_filter: Either a simple project name (e.g., "proj1") or a glob
281
281
  pattern for the full path (e.g., "tom/*/exp*"). If the
282
282
  filter contains '/', '*', or '?', it's treated as a glob
@@ -289,15 +289,15 @@ def discover_experiments(
289
289
  """
290
290
  import fnmatch
291
291
 
292
- local_path = Path(local_path)
292
+ dash_root = Path(dash_root)
293
293
 
294
- if not local_path.exists():
294
+ if not dash_root.exists():
295
295
  return []
296
296
 
297
297
  experiments = []
298
298
 
299
299
  # Find all experiment.json files recursively
300
- for exp_json in local_path.rglob("*/experiment.json"):
300
+ for exp_json in dash_root.rglob("*/experiment.json"):
301
301
  exp_dir = exp_json.parent
302
302
 
303
303
  # Read prefix from experiment.json first
@@ -313,7 +313,7 @@ def discover_experiments(
313
313
  # This handles nested folders correctly
314
314
  # Prefix format: owner/project/folder.../experiment
315
315
  try:
316
- relative_path = exp_dir.relative_to(local_path)
316
+ relative_path = exp_dir.relative_to(dash_root)
317
317
  full_relative_path = str(relative_path)
318
318
 
319
319
  if prefix:
@@ -1234,9 +1234,9 @@ def cmd_upload(args: argparse.Namespace) -> int:
1234
1234
  api_key = args.api_key or config.api_key
1235
1235
 
1236
1236
  # Discover experiments
1237
- local_path = Path(args.path)
1238
- if not local_path.exists():
1239
- console.print(f"[red]Error:[/red] Local storage path does not exist: {local_path}")
1237
+ dash_root = Path(args.path)
1238
+ if not dash_root.exists():
1239
+ console.print(f"[red]Error:[/red] Local storage path does not exist: {dash_root}")
1240
1240
  return 1
1241
1241
 
1242
1242
  # Handle state file for resume functionality
@@ -1247,7 +1247,7 @@ def cmd_upload(args: argparse.Namespace) -> int:
1247
1247
  upload_state = UploadState.load(state_file)
1248
1248
  if upload_state:
1249
1249
  # Validate state matches current upload
1250
- if upload_state.local_path != str(local_path.absolute()):
1250
+ if upload_state.dash_root != str(dash_root.absolute()):
1251
1251
  console.print(
1252
1252
  "[yellow]Warning:[/yellow] State file local path doesn't match. Starting fresh upload."
1253
1253
  )
@@ -1273,13 +1273,13 @@ def cmd_upload(args: argparse.Namespace) -> int:
1273
1273
  # Create new state if not resuming
1274
1274
  if not upload_state:
1275
1275
  upload_state = UploadState(
1276
- local_path=str(local_path.absolute()),
1276
+ dash_root=str(dash_root.absolute()),
1277
1277
  remote_url=remote_url,
1278
1278
  )
1279
1279
 
1280
- console.print(f"[bold]Scanning local storage:[/bold] {local_path.absolute()}")
1280
+ console.print(f"[bold]Scanning local storage:[/bold] {dash_root.absolute()}")
1281
1281
  experiments = discover_experiments(
1282
- local_path,
1282
+ dash_root,
1283
1283
  project_filter=args.pref, # Using --prefix/-p argument
1284
1284
  experiment_filter=None,
1285
1285
  )
@@ -1398,7 +1398,7 @@ def cmd_upload(args: argparse.Namespace) -> int:
1398
1398
 
1399
1399
  # Initialize remote client and local storage
1400
1400
  remote_client = RemoteClient(base_url=remote_url, namespace=namespace, api_key=api_key)
1401
- local_storage = LocalStorage(root_path=local_path)
1401
+ local_storage = LocalStorage(root_path=dash_root)
1402
1402
 
1403
1403
  # Upload experiments with progress tracking
1404
1404
  console.print(f"\n[bold]Uploading to:[/bold] {remote_url}")
ml_dash/client.py CHANGED
@@ -349,6 +349,30 @@ class RemoteClient:
349
349
  # Project not found - return None to let server auto-create it
350
350
  return None
351
351
 
352
+ def delete_project(self, project_slug: str) -> Dict[str, Any]:
353
+ """
354
+ Delete a project and all its experiments, metrics, files, and logs.
355
+
356
+ Args:
357
+ project_slug: Project slug
358
+
359
+ Returns:
360
+ Dict with projectId, deleted count, experiments count, and message
361
+
362
+ Raises:
363
+ httpx.HTTPStatusError: If request fails
364
+ ValueError: If project not found
365
+ """
366
+ # Get project ID first
367
+ project_id = self._get_project_id(project_slug)
368
+ if not project_id:
369
+ raise ValueError(f"Project '{project_slug}' not found in namespace '{self.namespace}'")
370
+
371
+ # Delete using project-specific endpoint
372
+ response = self._client.delete(f"projects/{project_id}")
373
+ response.raise_for_status()
374
+ return response.json()
375
+
352
376
  def _get_experiment_node_id(self, experiment_id: str) -> str:
353
377
  """
354
378
  Resolve node ID from experiment ID using GraphQL.
@@ -439,7 +463,7 @@ class RemoteClient:
439
463
  if not project_id:
440
464
  # Create the project first since we need its ID for folders
441
465
  project_response = self._client.post(
442
- f"/namespaces/{self.namespace}/nodes",
466
+ f"namespaces/{self.namespace}/nodes",
443
467
  json={
444
468
  "type": "PROJECT",
445
469
  "name": project,
@@ -459,7 +483,7 @@ class RemoteClient:
459
483
  # Create folder (server handles upsert)
460
484
  # NOTE: Do NOT pass experimentId for project-level folders
461
485
  folder_response = self._client.post(
462
- f"/namespaces/{self.namespace}/nodes",
486
+ f"namespaces/{self.namespace}/nodes",
463
487
  json={
464
488
  "type": "FOLDER",
465
489
  "projectId": project_id,
@@ -501,7 +525,7 @@ class RemoteClient:
501
525
 
502
526
  # Call unified node creation API
503
527
  response = self._client.post(
504
- f"/namespaces/{self.namespace}/nodes",
528
+ f"namespaces/{self.namespace}/nodes",
505
529
  json=payload,
506
530
  )
507
531
  response.raise_for_status()
@@ -541,7 +565,7 @@ class RemoteClient:
541
565
  payload = {"status": status}
542
566
 
543
567
  response = self._client.patch(
544
- f"/nodes/{node_id}",
568
+ f"nodes/{node_id}",
545
569
  json=payload,
546
570
  )
547
571
  response.raise_for_status()
@@ -578,7 +602,7 @@ class RemoteClient:
578
602
  httpx.HTTPStatusError: If request fails
579
603
  """
580
604
  response = self._client.post(
581
- f"/experiments/{experiment_id}/logs",
605
+ f"experiments/{experiment_id}/logs",
582
606
  json={"logs": logs}
583
607
  )
584
608
  response.raise_for_status()
@@ -614,7 +638,7 @@ class RemoteClient:
614
638
  httpx.HTTPStatusError: If request fails
615
639
  """
616
640
  response = self._client.post(
617
- f"/experiments/{experiment_id}/parameters",
641
+ f"experiments/{experiment_id}/parameters",
618
642
  json={"data": data}
619
643
  )
620
644
  response.raise_for_status()
@@ -634,7 +658,7 @@ class RemoteClient:
634
658
  Raises:
635
659
  httpx.HTTPStatusError: If request fails or parameters don't exist
636
660
  """
637
- response = self._client.get(f"/experiments/{experiment_id}/parameters")
661
+ response = self._client.get(f"experiments/{experiment_id}/parameters")
638
662
  response.raise_for_status()
639
663
  result = response.json()
640
664
  return result.get("data", {})
@@ -911,7 +935,7 @@ class RemoteClient:
911
935
 
912
936
  # Create folder (server will return existing if duplicate)
913
937
  folder_response = self._client.post(
914
- f"/namespaces/{self.namespace}/nodes",
938
+ f"namespaces/{self.namespace}/nodes",
915
939
  json={
916
940
  "type": "FOLDER",
917
941
  "projectId": project_id,
@@ -950,7 +974,7 @@ class RemoteClient:
950
974
 
951
975
  # Call unified node creation API
952
976
  response = self._client.post(
953
- f"/namespaces/{self.namespace}/nodes",
977
+ f"namespaces/{self.namespace}/nodes",
954
978
  files=files,
955
979
  data=data
956
980
  )
@@ -1068,7 +1092,7 @@ class RemoteClient:
1068
1092
  httpx.HTTPStatusError: If request fails
1069
1093
  """
1070
1094
  # file_id is actually the node ID in the new system
1071
- response = self._client.get(f"/nodes/{file_id}")
1095
+ response = self._client.get(f"nodes/{file_id}")
1072
1096
  response.raise_for_status()
1073
1097
  return response.json()
1074
1098
 
@@ -1103,7 +1127,7 @@ class RemoteClient:
1103
1127
  dest_path = filename
1104
1128
 
1105
1129
  # Download file using node API
1106
- response = self._client.get(f"/nodes/{file_id}/download")
1130
+ response = self._client.get(f"nodes/{file_id}/download")
1107
1131
  response.raise_for_status()
1108
1132
 
1109
1133
  # Write to file
@@ -1135,7 +1159,7 @@ class RemoteClient:
1135
1159
  Raises:
1136
1160
  httpx.HTTPStatusError: If request fails
1137
1161
  """
1138
- response = self._client.delete(f"/nodes/{file_id}")
1162
+ response = self._client.delete(f"nodes/{file_id}")
1139
1163
  response.raise_for_status()
1140
1164
  return response.json()
1141
1165
 
@@ -1172,7 +1196,7 @@ class RemoteClient:
1172
1196
  payload["metadata"] = metadata
1173
1197
 
1174
1198
  response = self._client.patch(
1175
- f"/nodes/{file_id}",
1199
+ f"nodes/{file_id}",
1176
1200
  json=payload
1177
1201
  )
1178
1202
  response.raise_for_status()
@@ -1213,7 +1237,7 @@ class RemoteClient:
1213
1237
  payload["metadata"] = metadata
1214
1238
 
1215
1239
  response = self._client.post(
1216
- f"/experiments/{experiment_id}/metrics/{metric_name}/append",
1240
+ f"experiments/{experiment_id}/metrics/{metric_name}/append",
1217
1241
  json=payload
1218
1242
  )
1219
1243
  response.raise_for_status()
@@ -1254,7 +1278,7 @@ class RemoteClient:
1254
1278
  payload["metadata"] = metadata
1255
1279
 
1256
1280
  response = self._client.post(
1257
- f"/experiments/{experiment_id}/metrics/{metric_name}/append-batch",
1281
+ f"experiments/{experiment_id}/metrics/{metric_name}/append-batch",
1258
1282
  json=payload
1259
1283
  )
1260
1284
  response.raise_for_status()
@@ -1283,7 +1307,7 @@ class RemoteClient:
1283
1307
  httpx.HTTPStatusError: If request fails
1284
1308
  """
1285
1309
  response = self._client.get(
1286
- f"/experiments/{experiment_id}/metrics/{metric_name}/data",
1310
+ f"experiments/{experiment_id}/metrics/{metric_name}/data",
1287
1311
  params={"startIndex": start_index, "limit": limit}
1288
1312
  )
1289
1313
  response.raise_for_status()
@@ -1308,7 +1332,7 @@ class RemoteClient:
1308
1332
  httpx.HTTPStatusError: If request fails
1309
1333
  """
1310
1334
  response = self._client.get(
1311
- f"/experiments/{experiment_id}/metrics/{metric_name}/stats"
1335
+ f"experiments/{experiment_id}/metrics/{metric_name}/stats"
1312
1336
  )
1313
1337
  response.raise_for_status()
1314
1338
  return response.json()
@@ -1329,7 +1353,7 @@ class RemoteClient:
1329
1353
  Raises:
1330
1354
  httpx.HTTPStatusError: If request fails
1331
1355
  """
1332
- response = self._client.get(f"/experiments/{experiment_id}/metrics")
1356
+ response = self._client.get(f"experiments/{experiment_id}/metrics")
1333
1357
  response.raise_for_status()
1334
1358
  return response.json()["metrics"]
1335
1359
 
@@ -1644,7 +1668,7 @@ class RemoteClient:
1644
1668
  expected_checksum = file_metadata.get("physicalFile", {}).get("checksum")
1645
1669
 
1646
1670
  # Stream download using node API
1647
- with self._client.stream("GET", f"/nodes/{file_id}/download") as response:
1671
+ with self._client.stream("GET", f"nodes/{file_id}/download") as response:
1648
1672
  response.raise_for_status()
1649
1673
 
1650
1674
  with open(dest_path, "wb") as f:
@@ -1712,7 +1736,7 @@ class RemoteClient:
1712
1736
  if search is not None:
1713
1737
  params["search"] = search
1714
1738
 
1715
- response = self._client.get(f"/experiments/{experiment_id}/logs", params=params)
1739
+ response = self._client.get(f"experiments/{experiment_id}/logs", params=params)
1716
1740
  response.raise_for_status()
1717
1741
  return response.json()
1718
1742
 
@@ -1750,7 +1774,7 @@ class RemoteClient:
1750
1774
  params["bufferOnly"] = "true"
1751
1775
 
1752
1776
  response = self._client.get(
1753
- f"/experiments/{experiment_id}/metrics/{metric_name}/data",
1777
+ f"experiments/{experiment_id}/metrics/{metric_name}/data",
1754
1778
  params=params
1755
1779
  )
1756
1780
  response.raise_for_status()
@@ -1777,7 +1801,7 @@ class RemoteClient:
1777
1801
  httpx.HTTPStatusError: If request fails
1778
1802
  """
1779
1803
  response = self._client.get(
1780
- f"/experiments/{experiment_id}/metrics/{metric_name}/chunks/{chunk_number}"
1804
+ f"experiments/{experiment_id}/metrics/{metric_name}/chunks/{chunk_number}"
1781
1805
  )
1782
1806
  response.raise_for_status()
1783
1807
  return response.json()
@@ -1821,7 +1845,7 @@ class RemoteClient:
1821
1845
  payload["metadata"] = metadata
1822
1846
 
1823
1847
  response = self._client.post(
1824
- f"/experiments/{experiment_id}/tracks",
1848
+ f"experiments/{experiment_id}/tracks",
1825
1849
  json=payload,
1826
1850
  )
1827
1851
  response.raise_for_status()
@@ -1854,7 +1878,7 @@ class RemoteClient:
1854
1878
  topic_encoded = urllib.parse.quote(topic, safe='')
1855
1879
 
1856
1880
  response = self._client.post(
1857
- f"/experiments/{experiment_id}/tracks/{topic_encoded}/append",
1881
+ f"experiments/{experiment_id}/tracks/{topic_encoded}/append",
1858
1882
  json={"timestamp": timestamp, "data": data},
1859
1883
  )
1860
1884
  response.raise_for_status()
@@ -1888,7 +1912,7 @@ class RemoteClient:
1888
1912
  serialized_entries = [_serialize_value(entry) for entry in entries]
1889
1913
 
1890
1914
  response = self._client.post(
1891
- f"/experiments/{experiment_id}/tracks/{topic_encoded}/append_batch",
1915
+ f"experiments/{experiment_id}/tracks/{topic_encoded}/append_batch",
1892
1916
  json={"entries": serialized_entries},
1893
1917
  )
1894
1918
  response.raise_for_status()
@@ -1933,7 +1957,7 @@ class RemoteClient:
1933
1957
  params["columns"] = ",".join(columns)
1934
1958
 
1935
1959
  response = self._client.get(
1936
- f"/experiments/{experiment_id}/tracks/{topic_encoded}/data",
1960
+ f"experiments/{experiment_id}/tracks/{topic_encoded}/data",
1937
1961
  params=params,
1938
1962
  )
1939
1963
  response.raise_for_status()
@@ -1966,7 +1990,7 @@ class RemoteClient:
1966
1990
  params["topic"] = topic_filter
1967
1991
 
1968
1992
  response = self._client.get(
1969
- f"/experiments/{experiment_id}/tracks",
1993
+ f"experiments/{experiment_id}/tracks",
1970
1994
  params=params,
1971
1995
  )
1972
1996
  response.raise_for_status()
ml_dash/experiment.py CHANGED
@@ -259,15 +259,17 @@ class Experiment:
259
259
  from rich.console import Console
260
260
 
261
261
  console = Console()
262
+ experiment_url = f"https://dash.ml/{self.run.prefix}"
262
263
  console.print(
263
264
  f"[dim]✓ Experiment started: [bold]{self.run.name}[/bold] (project: {self.run.project})[/dim]\n"
264
265
  f"[dim]View your data, statistics, and plots online at:[/dim] "
265
- f"[link=https://dash.ml]https://dash.ml[/link]"
266
+ f"[link={experiment_url}]{experiment_url}[/link]"
266
267
  )
267
268
  except ImportError:
268
269
  # Fallback if rich is not available
270
+ experiment_url = f"https://dash.ml/{self.run.prefix}"
269
271
  print(f"✓ Experiment started: {self.run.name} (project: {self.run.project})")
270
- print("View your data at: https://dash.ml")
272
+ print(f"View your data at: {experiment_url}")
271
273
 
272
274
  except Exception as e:
273
275
  # Check if it's an authentication error
@@ -381,18 +383,20 @@ class Experiment:
381
383
  from rich.console import Console
382
384
 
383
385
  console = Console()
386
+ experiment_url = f"https://dash.ml/{self.run.prefix}"
384
387
  console.print(
385
388
  f"[{status_color}]{status_emoji} Experiment {status.lower()}: "
386
389
  f"[bold]{self.run.name}[/bold] (project: {self.run.project})[/{status_color}]\n"
387
390
  f"[dim]View results, statistics, and plots online at:[/dim] "
388
- f"[link=https://dash.ml]https://dash.ml[/link]"
391
+ f"[link={experiment_url}]{experiment_url}[/link]"
389
392
  )
390
393
  except ImportError:
391
394
  # Fallback if rich is not available
395
+ experiment_url = f"https://dash.ml/{self.run.prefix}"
392
396
  print(
393
397
  f"{status_emoji} Experiment {status.lower()}: {self.run.name} (project: {self.run.project})"
394
398
  )
395
- print("View results at: https://dash.ml")
399
+ print(f"View results at: {experiment_url}")
396
400
 
397
401
  except Exception as e:
398
402
  # Log error but don't fail the close operation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ml-dash
3
- Version: 0.6.10
3
+ Version: 0.6.12
4
4
  Summary: ML experiment tracking and data storage
5
5
  Keywords: machine-learning,experiment-tracking,mlops,data-storage
6
6
  Author: Ge Yang, Tom Tao
@@ -6,8 +6,8 @@ ml_dash/auth/device_secret.py,sha256=qUsz6M9S1GEIukvmz57eJEp57srSx74O4MU9mZEeDlE
6
6
  ml_dash/auth/exceptions.py,sha256=IeBwUzoaTyFtPwd4quFOIel49inIzuabe_ChEeEXEWI,725
7
7
  ml_dash/auth/token_storage.py,sha256=9YQXGrn41UVyc1wUvZYbTYLzxSt5NGOyNFNjeX28bjA,7976
8
8
  ml_dash/auto_start.py,sha256=mYNjLGI2kyylIfOX5wGOR74gb9UlXg1n5OUQu7aw5SE,2412
9
- ml_dash/buffer.py,sha256=i4-PZ703_yuKJPAXpmWBGm8jHAAdIBqiA0NIZQEc3wo,26201
10
- ml_dash/cli.py,sha256=X8LsQA8Wfa1XuXsbvePaGo6NYer7f8CNzy33VT3jrqg,2740
9
+ ml_dash/buffer.py,sha256=R3QaD55s1an3tJvzyXfrEuBxeJsN4o4QNdE__BoIr9w,26437
10
+ ml_dash/cli.py,sha256=Vd0taM5MQrhvxqL2KQhklZ00wxZLdWl6Qw1IPkjPNuw,2897
11
11
  ml_dash/cli_commands/__init__.py,sha256=bjAmV7MsW-bhtW_4SnLJ0Cfkt9h82vMDC8ebW1Ke8KE,38
12
12
  ml_dash/cli_commands/api.py,sha256=NekZEJGWNpIfB6YrsrOw7kw7rZKjVudwgJWPZIy6ANQ,4535
13
13
  ml_dash/cli_commands/create.py,sha256=i6LPA6vefpHUeXncBNzQsLnwF5EM2uF6IX4g7vTDYXo,4400
@@ -16,10 +16,11 @@ ml_dash/cli_commands/list.py,sha256=H442wOAcWYtDwq6BS7lpZbkKhqfTXBCHGctbw8zT1Zw,
16
16
  ml_dash/cli_commands/login.py,sha256=zX-urtUrfzg2qOGtKNYQgj6UloN9kzj4zEO6h_xwuNs,6782
17
17
  ml_dash/cli_commands/logout.py,sha256=lTUUNyRXqvo61qNkCd4KBrPUujDAHnNqsHkU6bHie0U,1332
18
18
  ml_dash/cli_commands/profile.py,sha256=PoRO1XA4bnOINptj4AO0SyNDBADeryPJBfgC74327e4,5997
19
- ml_dash/cli_commands/upload.py,sha256=SSUfXC3qoNpoFvPM_ia-ing_N50LNiAvMy9op6FM9Ew,49664
20
- ml_dash/client.py,sha256=WgdQRwI9OzEB9dtBtkjFOWNP1t1jj7wr4gvcDUYzZBM,65806
19
+ ml_dash/cli_commands/remove.py,sha256=AtDlUWkNeGcnZWN0Wbg6XoyYhFHkCFMPdxsGA33v38c,5325
20
+ ml_dash/cli_commands/upload.py,sha256=8i1ZVkQQ7VBzGV3GxlW-71GaETtTdUshexII2GTz5JM,49640
21
+ ml_dash/client.py,sha256=sQokhvofq56JxpmUBVUsfjjn5qKqyUXM9F3uxsUbUbA,66608
21
22
  ml_dash/config.py,sha256=oz2xvoBh2X_xUXWr92cPD5nFxXMT5LxVNypv5B5O0fA,3116
22
- ml_dash/experiment.py,sha256=sq5Bu-vcKhzTQlUsI6SzIHUZyUkxn7ZSMp_AbhFe4Bw,43462
23
+ ml_dash/experiment.py,sha256=JqNYsoAOsvqIRMVl6gHFodlZ79b0X2rUFGz6NRixKic,43726
23
24
  ml_dash/files.py,sha256=tGJCTxPfd9vmfvIEqstZjzLvqmHzMZffPXHz0jU9bYU,54441
24
25
  ml_dash/log.py,sha256=E-DLg0vejVLLEyShJ_r0LneDMI0XU7XTH5iKWYJe9jI,5298
25
26
  ml_dash/metric.py,sha256=ghD1jnuv6dbjV1Jlo7q0mx9UEzpdto2Y1-oDWrSfg04,25809
@@ -30,7 +31,7 @@ ml_dash/run.py,sha256=Hlt_YHaN95TC3TDejzLjFmW9EWyKWi6ruibw-eiPm2U,8833
30
31
  ml_dash/snowflake.py,sha256=14rEpRU5YltsmmmZW0EMUy_hdv5S5ME9gWVtmdmwfiU,4917
31
32
  ml_dash/storage.py,sha256=x1W-dK6wQY36-LVOJ4kA8Dn07ObNQuIErQWJ3b0PoGY,44910
32
33
  ml_dash/track.py,sha256=Dfg1ZnmKZ_FlE5ZfG8Qld_wN4RIMs3nrOxrxwf3thiY,8164
33
- ml_dash-0.6.10.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
34
- ml_dash-0.6.10.dist-info/entry_points.txt,sha256=dYs2EHX1uRNO7AQGNnVaJJpgiy0Z9q7tiy4fHSyaf3Q,46
35
- ml_dash-0.6.10.dist-info/METADATA,sha256=z9L0TSRTGJUSZ1gDYNR3Pj5IW3oh0AYeMFjEplnhFYA,9536
36
- ml_dash-0.6.10.dist-info/RECORD,,
34
+ ml_dash-0.6.12.dist-info/WHEEL,sha256=z-mOpxbJHqy3cq6SvUThBZdaLGFZzdZPtgWLcP2NKjQ,79
35
+ ml_dash-0.6.12.dist-info/entry_points.txt,sha256=dYs2EHX1uRNO7AQGNnVaJJpgiy0Z9q7tiy4fHSyaf3Q,46
36
+ ml_dash-0.6.12.dist-info/METADATA,sha256=u31huYFuxz_NMIuFiaUyE373GXMRbYCzpBB61D6KFu4,9536
37
+ ml_dash-0.6.12.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.15
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any