ml-dash 0.6.9__tar.gz → 0.6.11__tar.gz
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-0.6.9 → ml_dash-0.6.11}/PKG-INFO +1 -1
- {ml_dash-0.6.9 → ml_dash-0.6.11}/pyproject.toml +1 -1
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/__init__.py +49 -2
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/token_storage.py +0 -9
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auto_start.py +21 -6
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli.py +5 -1
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/profile.py +21 -29
- ml_dash-0.6.11/src/ml_dash/cli_commands/remove.py +161 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/client.py +151 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/experiment.py +8 -4
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/run.py +1 -1
- {ml_dash-0.6.9 → ml_dash-0.6.11}/LICENSE +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/README.md +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/__init__.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/constants.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/device_flow.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/device_secret.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/exceptions.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/buffer.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/__init__.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/api.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/create.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/download.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/list.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/login.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/logout.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/upload.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/config.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/files.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/log.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/metric.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/params.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/py.typed +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/remote_auto_start.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/snowflake.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/storage.py +0 -0
- {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/track.py +0 -0
|
@@ -36,14 +36,60 @@ Usage:
|
|
|
36
36
|
exp.log("Training started")
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
|
-
from .client import RemoteClient
|
|
39
|
+
from .client import RemoteClient, userinfo
|
|
40
40
|
from .experiment import Experiment, OperationMode, ml_dash_experiment
|
|
41
41
|
from .log import LogBuilder, LogLevel
|
|
42
42
|
from .params import ParametersBuilder
|
|
43
43
|
from .run import RUN
|
|
44
44
|
from .storage import LocalStorage
|
|
45
45
|
|
|
46
|
-
__version__ = "0.6.
|
|
46
|
+
__version__ = "0.6.10"
|
|
47
|
+
|
|
48
|
+
# Minimum version required - blocks older versions
|
|
49
|
+
MINIMUM_REQUIRED_VERSION = "0.6.10"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _check_version_compatibility():
|
|
53
|
+
"""
|
|
54
|
+
Enforce minimum version requirement.
|
|
55
|
+
|
|
56
|
+
Raises ImportError if installed version is below minimum required version.
|
|
57
|
+
This ensures users have the latest features (userinfo, namespace auto-detection, etc.)
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
from packaging import version
|
|
61
|
+
except ImportError:
|
|
62
|
+
# If packaging is not available, skip check
|
|
63
|
+
# (unlikely since it's a common dependency)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
current = version.parse(__version__)
|
|
67
|
+
minimum = version.parse(MINIMUM_REQUIRED_VERSION)
|
|
68
|
+
|
|
69
|
+
if current < minimum:
|
|
70
|
+
raise ImportError(
|
|
71
|
+
f"\n"
|
|
72
|
+
f"{'=' * 80}\n"
|
|
73
|
+
f"ERROR: ml-dash version {__version__} is too old!\n"
|
|
74
|
+
f"{'=' * 80}\n"
|
|
75
|
+
f"\n"
|
|
76
|
+
f"This version of ml-dash ({__version__}) is no longer supported.\n"
|
|
77
|
+
f"Minimum required version: {MINIMUM_REQUIRED_VERSION}\n"
|
|
78
|
+
f"\n"
|
|
79
|
+
f"Please upgrade to the latest version:\n"
|
|
80
|
+
f"\n"
|
|
81
|
+
f" pip install --upgrade ml-dash\n"
|
|
82
|
+
f"\n"
|
|
83
|
+
f"Or install specific version:\n"
|
|
84
|
+
f"\n"
|
|
85
|
+
f" pip install ml-dash>={MINIMUM_REQUIRED_VERSION}\n"
|
|
86
|
+
f"\n"
|
|
87
|
+
f"{'=' * 80}\n"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Enforce version check on import
|
|
92
|
+
_check_version_compatibility()
|
|
47
93
|
|
|
48
94
|
__all__ = [
|
|
49
95
|
"Experiment",
|
|
@@ -55,4 +101,5 @@ __all__ = [
|
|
|
55
101
|
"LogBuilder",
|
|
56
102
|
"ParametersBuilder",
|
|
57
103
|
"RUN",
|
|
104
|
+
"userinfo",
|
|
58
105
|
]
|
|
@@ -292,12 +292,3 @@ def decode_jwt_payload(token: str) -> dict:
|
|
|
292
292
|
return {}
|
|
293
293
|
|
|
294
294
|
|
|
295
|
-
def get_jwt_user():
|
|
296
|
-
# Load token
|
|
297
|
-
storage = get_token_storage()
|
|
298
|
-
token = storage.load("ml-dash-token")
|
|
299
|
-
|
|
300
|
-
if token:
|
|
301
|
-
user = decode_jwt_payload(token)
|
|
302
|
-
return user
|
|
303
|
-
return None
|
|
@@ -31,19 +31,34 @@ import atexit
|
|
|
31
31
|
# Token is auto-loaded from storage when first used
|
|
32
32
|
# If not authenticated, operations will fail with AuthenticationError
|
|
33
33
|
# Prefix format: {owner}/{project}/path...
|
|
34
|
-
# Using getpass to get current user as owner for local convenience
|
|
35
34
|
import getpass
|
|
36
35
|
from datetime import datetime
|
|
37
36
|
|
|
38
|
-
from .auth.token_storage import get_jwt_user
|
|
39
37
|
from .experiment import Experiment
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
39
|
+
# Get username for dxp namespace
|
|
40
|
+
# Note: We use userinfo for fresh data (recommended approach)
|
|
41
|
+
# Falls back to system username if not authenticated
|
|
42
|
+
try:
|
|
43
|
+
from .client import userinfo
|
|
44
|
+
_username = userinfo.username or getpass.getuser()
|
|
45
|
+
except Exception:
|
|
46
|
+
# If userinfo fails (e.g., no network), fall back to system user
|
|
47
|
+
_username = getpass.getuser()
|
|
48
|
+
|
|
44
49
|
_now = datetime.now()
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
# Create pre-configured singleton experiment in REMOTE mode
|
|
52
|
+
# - dash_url=True: Use default remote server (https://api.dash.ml)
|
|
53
|
+
# - dash_root=None: Remote-only mode (no local storage)
|
|
54
|
+
# - user: Uses authenticated username from userinfo (fresh from server)
|
|
55
|
+
# - Token is auto-loaded from storage when first used
|
|
56
|
+
# - If not authenticated, operations will fail with AuthenticationError
|
|
57
|
+
dxp = Experiment(
|
|
58
|
+
user=_username, # Use authenticated username for namespace
|
|
59
|
+
dash_url=True, # Use remote API (https://api.dash.ml)
|
|
60
|
+
dash_root=None, # Remote-only mode (no local .dash/)
|
|
61
|
+
)
|
|
47
62
|
|
|
48
63
|
|
|
49
64
|
# Register cleanup handler to complete experiment on Python exit (if still open)
|
|
@@ -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)
|
|
@@ -24,9 +24,9 @@ def add_parser(subparsers):
|
|
|
24
24
|
help="Output as JSON",
|
|
25
25
|
)
|
|
26
26
|
parser.add_argument(
|
|
27
|
-
"--
|
|
27
|
+
"--cached",
|
|
28
28
|
action="store_true",
|
|
29
|
-
help="
|
|
29
|
+
help="Use cached token data (default: fetch fresh from server)",
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
@@ -45,27 +45,17 @@ def _fetch_fresh_profile(remote_url: str, token: str) -> dict:
|
|
|
45
45
|
|
|
46
46
|
client = RemoteClient(remote_url, api_key=token)
|
|
47
47
|
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
query GetUserProfile {
|
|
51
|
-
me {
|
|
52
|
-
id
|
|
53
|
-
username
|
|
54
|
-
name
|
|
55
|
-
email
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
result = client.graphql_query(query)
|
|
61
|
-
me = result.get("me", {})
|
|
48
|
+
# Use the new get_current_user() method
|
|
49
|
+
user_data = client.get_current_user()
|
|
62
50
|
|
|
63
|
-
if
|
|
51
|
+
if user_data:
|
|
64
52
|
return {
|
|
65
|
-
"sub":
|
|
66
|
-
"username":
|
|
67
|
-
"name":
|
|
68
|
-
"email":
|
|
53
|
+
"sub": user_data.get("id"),
|
|
54
|
+
"username": user_data.get("username"),
|
|
55
|
+
"name": user_data.get("name"),
|
|
56
|
+
"email": user_data.get("email"),
|
|
57
|
+
"given_name": user_data.get("given_name"),
|
|
58
|
+
"family_name": user_data.get("family_name"),
|
|
69
59
|
}
|
|
70
60
|
except Exception as e:
|
|
71
61
|
# If API call fails, return None to fall back to token decoding
|
|
@@ -131,8 +121,13 @@ def cmd_profile(args) -> int:
|
|
|
131
121
|
info["authenticated"] = False
|
|
132
122
|
info["error"] = "Token expired. Please run 'ml-dash login' to re-authenticate."
|
|
133
123
|
else:
|
|
134
|
-
# Fetch fresh profile from server
|
|
135
|
-
if args.
|
|
124
|
+
# Fetch fresh profile from server by default, use cached token only if --cached flag is set
|
|
125
|
+
if args.cached:
|
|
126
|
+
# Use cached token data
|
|
127
|
+
info["user"] = token_payload
|
|
128
|
+
info["source"] = "token"
|
|
129
|
+
else:
|
|
130
|
+
# Fetch fresh data from server (default behavior)
|
|
136
131
|
fresh_profile = _fetch_fresh_profile(config.remote_url, token)
|
|
137
132
|
if fresh_profile:
|
|
138
133
|
info["user"] = fresh_profile
|
|
@@ -141,9 +136,6 @@ def cmd_profile(args) -> int:
|
|
|
141
136
|
info["user"] = token_payload
|
|
142
137
|
info["source"] = "token"
|
|
143
138
|
info["warning"] = "Could not fetch fresh profile from server, using cached token data"
|
|
144
|
-
else:
|
|
145
|
-
info["user"] = token_payload
|
|
146
|
-
info["source"] = "token"
|
|
147
139
|
|
|
148
140
|
if expiry_message:
|
|
149
141
|
info["token_status"] = expiry_message
|
|
@@ -199,9 +191,9 @@ def cmd_profile(args) -> int:
|
|
|
199
191
|
if info.get("warning"):
|
|
200
192
|
warning_text = f"\n[yellow]⚠ {info['warning']}[/yellow]"
|
|
201
193
|
|
|
202
|
-
# Show tip for
|
|
203
|
-
if source == "
|
|
204
|
-
tip_text = "\n[dim]Tip: Use --
|
|
194
|
+
# Show tip for using cached data
|
|
195
|
+
if source == "server":
|
|
196
|
+
tip_text = "\n[dim]Tip: Use --cached to use cached token data (faster but may be outdated)[/dim]"
|
|
205
197
|
else:
|
|
206
198
|
tip_text = None
|
|
207
199
|
|
|
@@ -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
|
|
@@ -6,6 +6,95 @@ from typing import Optional, Dict, Any, List
|
|
|
6
6
|
import httpx
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
class UserInfo:
|
|
10
|
+
"""
|
|
11
|
+
Singleton user info object that fetches current user from API server.
|
|
12
|
+
|
|
13
|
+
Fetches user info from API server on first access (lazy loading).
|
|
14
|
+
This queries the API for fresh user data, ensuring up-to-date information.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
>>> from ml_dash import userinfo
|
|
18
|
+
>>> if userinfo.username:
|
|
19
|
+
... print(f"Namespace: {userinfo.username}")
|
|
20
|
+
... print(f"Email: {userinfo.email}")
|
|
21
|
+
... print(f"Project: {userinfo.username}/my-project")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._data = None
|
|
26
|
+
self._fetched = False
|
|
27
|
+
|
|
28
|
+
def _fetch(self):
|
|
29
|
+
"""Fetch user info from API server (lazy loading)."""
|
|
30
|
+
if self._fetched:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
self._fetched = True
|
|
34
|
+
try:
|
|
35
|
+
client = RemoteClient("https://api.dash.ml")
|
|
36
|
+
self._data = client.get_current_user()
|
|
37
|
+
except Exception:
|
|
38
|
+
self._data = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def username(self) -> Optional[str]:
|
|
42
|
+
"""Username (namespace) - e.g., 'tom_tao_e4c2c9'"""
|
|
43
|
+
self._fetch()
|
|
44
|
+
return self._data.get("username") if self._data else None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def email(self) -> Optional[str]:
|
|
48
|
+
"""User email"""
|
|
49
|
+
self._fetch()
|
|
50
|
+
return self._data.get("email") if self._data else None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def name(self) -> Optional[str]:
|
|
54
|
+
"""Full name"""
|
|
55
|
+
self._fetch()
|
|
56
|
+
return self._data.get("name") if self._data else None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def given_name(self) -> Optional[str]:
|
|
60
|
+
"""First/given name"""
|
|
61
|
+
self._fetch()
|
|
62
|
+
return self._data.get("given_name") if self._data else None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def family_name(self) -> Optional[str]:
|
|
66
|
+
"""Last/family name"""
|
|
67
|
+
self._fetch()
|
|
68
|
+
return self._data.get("family_name") if self._data else None
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def picture(self) -> Optional[str]:
|
|
72
|
+
"""Profile picture URL"""
|
|
73
|
+
self._fetch()
|
|
74
|
+
return self._data.get("picture") if self._data else None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def id(self) -> Optional[str]:
|
|
78
|
+
"""User ID"""
|
|
79
|
+
self._fetch()
|
|
80
|
+
return self._data.get("id") if self._data else None
|
|
81
|
+
|
|
82
|
+
def __bool__(self) -> bool:
|
|
83
|
+
"""Return True if user is authenticated and data was fetched successfully."""
|
|
84
|
+
self._fetch()
|
|
85
|
+
return self._data is not None
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
self._fetch()
|
|
89
|
+
if self._data:
|
|
90
|
+
return f"UserInfo(username='{self.username}', email='{self.email}')"
|
|
91
|
+
return "UserInfo(not authenticated)"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Create singleton instance
|
|
95
|
+
userinfo = UserInfo()
|
|
96
|
+
|
|
97
|
+
|
|
9
98
|
def _serialize_value(value: Any) -> Any:
|
|
10
99
|
"""
|
|
11
100
|
Convert value to JSON-serializable format.
|
|
@@ -140,6 +229,44 @@ class RemoteClient:
|
|
|
140
229
|
except Exception:
|
|
141
230
|
return None
|
|
142
231
|
|
|
232
|
+
def get_current_user(self) -> Optional[Dict[str, Any]]:
|
|
233
|
+
"""
|
|
234
|
+
Get current authenticated user's info from server.
|
|
235
|
+
|
|
236
|
+
This queries the API server for fresh user data, ensuring up-to-date information.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
User info dict with keys: username, email, name, given_name, family_name, picture
|
|
240
|
+
Returns None if not authenticated or if query fails
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
>>> client = RemoteClient("https://api.dash.ml")
|
|
244
|
+
>>> user = client.get_current_user()
|
|
245
|
+
>>> print(user["username"]) # e.g., "tom_tao_e4c2c9"
|
|
246
|
+
>>> print(user["email"]) # e.g., "user@example.com"
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
self._ensure_authenticated()
|
|
250
|
+
|
|
251
|
+
# Query server for current user's complete profile
|
|
252
|
+
query = """
|
|
253
|
+
query GetCurrentUser {
|
|
254
|
+
me {
|
|
255
|
+
id
|
|
256
|
+
username
|
|
257
|
+
email
|
|
258
|
+
name
|
|
259
|
+
given_name
|
|
260
|
+
family_name
|
|
261
|
+
picture
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
"""
|
|
265
|
+
result = self.graphql_query(query)
|
|
266
|
+
return result.get("me")
|
|
267
|
+
except Exception:
|
|
268
|
+
return None
|
|
269
|
+
|
|
143
270
|
def _ensure_authenticated(self):
|
|
144
271
|
"""Check if authenticated, raise error if not."""
|
|
145
272
|
if not self.api_key:
|
|
@@ -222,6 +349,30 @@ class RemoteClient:
|
|
|
222
349
|
# Project not found - return None to let server auto-create it
|
|
223
350
|
return None
|
|
224
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
|
+
|
|
225
376
|
def _get_experiment_node_id(self, experiment_id: str) -> str:
|
|
226
377
|
"""
|
|
227
378
|
Resolve node ID from experiment ID using GraphQL.
|
|
@@ -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
|
|
@@ -223,7 +223,7 @@ class RUN:
|
|
|
223
223
|
# experiments/vision/resnet/train.py
|
|
224
224
|
from ml_dash import RUN
|
|
225
225
|
|
|
226
|
-
RUN
|
|
226
|
+
RUN(entry=__file__)
|
|
227
227
|
# Result: RUN.prefix = "vision/resnet", RUN.name = "resnet"
|
|
228
228
|
"""
|
|
229
229
|
# Use provided entry or try to auto-detect from caller
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|