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.
Files changed (37) hide show
  1. {ml_dash-0.6.9 → ml_dash-0.6.11}/PKG-INFO +1 -1
  2. {ml_dash-0.6.9 → ml_dash-0.6.11}/pyproject.toml +1 -1
  3. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/__init__.py +49 -2
  4. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/token_storage.py +0 -9
  5. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auto_start.py +21 -6
  6. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli.py +5 -1
  7. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/profile.py +21 -29
  8. ml_dash-0.6.11/src/ml_dash/cli_commands/remove.py +161 -0
  9. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/client.py +151 -0
  10. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/experiment.py +8 -4
  11. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/run.py +1 -1
  12. {ml_dash-0.6.9 → ml_dash-0.6.11}/LICENSE +0 -0
  13. {ml_dash-0.6.9 → ml_dash-0.6.11}/README.md +0 -0
  14. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/__init__.py +0 -0
  15. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/constants.py +0 -0
  16. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/device_flow.py +0 -0
  17. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/device_secret.py +0 -0
  18. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/auth/exceptions.py +0 -0
  19. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/buffer.py +0 -0
  20. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/__init__.py +0 -0
  21. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/api.py +0 -0
  22. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/create.py +0 -0
  23. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/download.py +0 -0
  24. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/list.py +0 -0
  25. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/login.py +0 -0
  26. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/logout.py +0 -0
  27. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/cli_commands/upload.py +0 -0
  28. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/config.py +0 -0
  29. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/files.py +0 -0
  30. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/log.py +0 -0
  31. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/metric.py +0 -0
  32. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/params.py +0 -0
  33. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/py.typed +0 -0
  34. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/remote_auto_start.py +0 -0
  35. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/snowflake.py +0 -0
  36. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/storage.py +0 -0
  37. {ml_dash-0.6.9 → ml_dash-0.6.11}/src/ml_dash/track.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ml-dash
3
- Version: 0.6.9
3
+ Version: 0.6.11
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ml-dash"
3
- version = "0.6.9"
3
+ version = "0.6.11"
4
4
  description = "ML experiment tracking and data storage"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -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.4"
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
- _user = get_jwt_user()
42
- # Fallback to system username if not authenticated
43
- _username = _user["username"] if _user else getpass.getuser()
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
- dxp = Experiment()
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
- "--refresh",
27
+ "--cached",
28
28
  action="store_true",
29
- help="Fetch fresh profile from server (not from cached token)",
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
- # Query for full user profile
49
- query = """
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 me:
51
+ if user_data:
64
52
  return {
65
- "sub": me.get("id"),
66
- "username": me.get("username"),
67
- "name": me.get("name"),
68
- "email": me.get("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 if requested, or fall back to token
135
- if args.refresh:
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 refreshing
203
- if source == "token":
204
- tip_text = "\n[dim]Tip: Use --refresh to fetch fresh data from server[/dim]"
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=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
@@ -223,7 +223,7 @@ class RUN:
223
223
  # experiments/vision/resnet/train.py
224
224
  from ml_dash import RUN
225
225
 
226
- RUN.__post_init__(entry=__file__)
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