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 +9 -6
- ml_dash/cli.py +5 -1
- ml_dash/cli_commands/remove.py +161 -0
- ml_dash/cli_commands/upload.py +19 -19
- ml_dash/client.py +51 -27
- ml_dash/experiment.py +8 -4
- {ml_dash-0.6.10.dist-info → ml_dash-0.6.12.dist-info}/METADATA +1 -1
- {ml_dash-0.6.10.dist-info → ml_dash-0.6.12.dist-info}/RECORD +10 -9
- {ml_dash-0.6.10.dist-info → ml_dash-0.6.12.dist-info}/WHEEL +2 -2
- {ml_dash-0.6.10.dist-info → ml_dash-0.6.12.dist-info}/entry_points.txt +0 -0
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.
|
|
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.
|
|
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.
|
|
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
|
ml_dash/cli_commands/upload.py
CHANGED
|
@@ -67,7 +67,7 @@ class UploadResult:
|
|
|
67
67
|
class UploadState:
|
|
68
68
|
"""Tracks upload state for resume functionality."""
|
|
69
69
|
|
|
70
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
276
|
-
(
|
|
275
|
+
Supports both flat (dash_root/project/experiment) and folder-based
|
|
276
|
+
(dash_root/folder/project/experiment) hierarchies.
|
|
277
277
|
|
|
278
278
|
Args:
|
|
279
|
-
|
|
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
|
-
|
|
292
|
+
dash_root = Path(dash_root)
|
|
293
293
|
|
|
294
|
-
if not
|
|
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
|
|
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(
|
|
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
|
-
|
|
1238
|
-
if not
|
|
1239
|
-
console.print(f"[red]Error:[/red] Local storage path does not exist: {
|
|
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.
|
|
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
|
-
|
|
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] {
|
|
1280
|
+
console.print(f"[bold]Scanning local storage:[/bold] {dash_root.absolute()}")
|
|
1281
1281
|
experiments = discover_experiments(
|
|
1282
|
-
|
|
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=
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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=
|
|
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:
|
|
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=
|
|
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:
|
|
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
|
|
@@ -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=
|
|
10
|
-
ml_dash/cli.py,sha256=
|
|
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/
|
|
20
|
-
ml_dash/
|
|
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=
|
|
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.
|
|
34
|
-
ml_dash-0.6.
|
|
35
|
-
ml_dash-0.6.
|
|
36
|
-
ml_dash-0.6.
|
|
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,,
|
|
File without changes
|