ml-dash 0.6.2rc1__tar.gz → 0.6.4__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 (43) hide show
  1. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/PKG-INFO +12 -14
  2. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/README.md +10 -12
  3. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/pyproject.toml +2 -2
  4. ml_dash-0.6.4/src/ml_dash/__init__.py +59 -0
  5. ml_dash-0.6.4/src/ml_dash/auth/token_storage.py +303 -0
  6. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/auto_start.py +28 -15
  7. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/cli.py +16 -2
  8. ml_dash-0.6.4/src/ml_dash/cli_commands/api.py +174 -0
  9. ml_dash-0.6.4/src/ml_dash/cli_commands/download.py +876 -0
  10. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/cli_commands/list.py +164 -14
  11. ml_dash-0.6.4/src/ml_dash/cli_commands/login.py +232 -0
  12. ml_dash-0.6.4/src/ml_dash/cli_commands/profile.py +92 -0
  13. ml_dash-0.6.4/src/ml_dash/cli_commands/upload.py +1419 -0
  14. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/client.py +335 -82
  15. ml_dash-0.6.4/src/ml_dash/config.py +133 -0
  16. ml_dash-0.6.4/src/ml_dash/experiment.py +1423 -0
  17. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/files.py +339 -224
  18. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/log.py +7 -7
  19. ml_dash-0.6.4/src/ml_dash/metric.py +740 -0
  20. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/params.py +6 -6
  21. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/remote_auto_start.py +20 -17
  22. ml_dash-0.6.4/src/ml_dash/run.py +231 -0
  23. ml_dash-0.6.4/src/ml_dash/snowflake.py +173 -0
  24. ml_dash-0.6.4/src/ml_dash/storage.py +1099 -0
  25. ml_dash-0.6.2rc1/src/ml_dash/__init__.py +0 -87
  26. ml_dash-0.6.2rc1/src/ml_dash/auth/token_storage.py +0 -262
  27. ml_dash-0.6.2rc1/src/ml_dash/cli_commands/download.py +0 -769
  28. ml_dash-0.6.2rc1/src/ml_dash/cli_commands/login.py +0 -225
  29. ml_dash-0.6.2rc1/src/ml_dash/cli_commands/upload.py +0 -1248
  30. ml_dash-0.6.2rc1/src/ml_dash/config.py +0 -133
  31. ml_dash-0.6.2rc1/src/ml_dash/experiment.py +0 -1163
  32. ml_dash-0.6.2rc1/src/ml_dash/metric.py +0 -481
  33. ml_dash-0.6.2rc1/src/ml_dash/run.py +0 -85
  34. ml_dash-0.6.2rc1/src/ml_dash/storage.py +0 -1129
  35. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/LICENSE +0 -0
  36. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/auth/__init__.py +0 -0
  37. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/auth/constants.py +0 -0
  38. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/auth/device_flow.py +0 -0
  39. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/auth/device_secret.py +0 -0
  40. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/auth/exceptions.py +0 -0
  41. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/cli_commands/__init__.py +0 -0
  42. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/cli_commands/logout.py +0 -0
  43. {ml_dash-0.6.2rc1 → ml_dash-0.6.4}/src/ml_dash/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ml-dash
3
- Version: 0.6.2rc1
3
+ Version: 0.6.4
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
@@ -43,7 +43,7 @@ Requires-Dist: imageio-ffmpeg>=0.4.9
43
43
  Requires-Dist: scikit-image>=0.21.0
44
44
  Requires-Dist: rich>=13.0.0
45
45
  Requires-Dist: cryptography>=42.0.0
46
- Requires-Dist: params-proto>=3.0.0rc12
46
+ Requires-Dist: params-proto>=3.0.0
47
47
  Requires-Dist: keyring>=25.0.0 ; extra == 'auth'
48
48
  Requires-Dist: qrcode>=8.0.0 ; extra == 'auth'
49
49
  Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
@@ -122,16 +122,16 @@ This opens your browser for secure OAuth2 authentication. Your credentials are s
122
122
  #### Option A: Use the Pre-configured Singleton (Easiest)
123
123
 
124
124
  ```python
125
- from ml_dash import dxp
125
+ from ml_dash.auto_start import dxp
126
126
 
127
127
  # Start experiment (uploads to https://api.dash.ml by default)
128
128
  with dxp.run:
129
- dxp.log().info("Training started")
129
+ dxp.log("Training started", level="info")
130
130
  dxp.params.set(learning_rate=0.001, batch_size=32)
131
131
 
132
132
  for epoch in range(10):
133
133
  loss = train_one_epoch()
134
- dxp.metrics("loss").append(value=loss, epoch=epoch)
134
+ dxp.metrics("train").log(loss=loss, epoch=epoch)
135
135
  ```
136
136
 
137
137
  #### Option B: Create Your Own Experiment
@@ -140,12 +140,11 @@ with dxp.run:
140
140
  from ml_dash import Experiment
141
141
 
142
142
  with Experiment(
143
- name="my-experiment",
144
- project="my-project",
145
- remote="https://api.dash.ml" # token auto-loaded
143
+ prefix="alice/my-project/my-experiment",
144
+ dash_url="https://api.dash.ml", # token auto-loaded
146
145
  ).run as experiment:
147
- experiment.log().info("Hello!")
148
- experiment.params.set(lr=0.001)
146
+ experiment.log("Hello!", level="info")
147
+ experiment.params.set(lr=0.001)
149
148
  ```
150
149
 
151
150
  #### Option C: Local Mode (No Authentication Required)
@@ -154,11 +153,10 @@ with Experiment(
154
153
  from ml_dash import Experiment
155
154
 
156
155
  with Experiment(
157
- name="my-experiment",
158
- project="my-project",
159
- local_path=".ml-dash"
156
+ project="my-project", prefix="my-experiment", dash_root=".dash"
160
157
  ).run as experiment:
161
- experiment.log().info("Running locally")
158
+ experiment.log("Running locally", level="info")
159
+
162
160
  ```
163
161
 
164
162
  See [docs/getting-started.md](docs/getting-started.md) for more examples.
@@ -54,16 +54,16 @@ This opens your browser for secure OAuth2 authentication. Your credentials are s
54
54
  #### Option A: Use the Pre-configured Singleton (Easiest)
55
55
 
56
56
  ```python
57
- from ml_dash import dxp
57
+ from ml_dash.auto_start import dxp
58
58
 
59
59
  # Start experiment (uploads to https://api.dash.ml by default)
60
60
  with dxp.run:
61
- dxp.log().info("Training started")
61
+ dxp.log("Training started", level="info")
62
62
  dxp.params.set(learning_rate=0.001, batch_size=32)
63
63
 
64
64
  for epoch in range(10):
65
65
  loss = train_one_epoch()
66
- dxp.metrics("loss").append(value=loss, epoch=epoch)
66
+ dxp.metrics("train").log(loss=loss, epoch=epoch)
67
67
  ```
68
68
 
69
69
  #### Option B: Create Your Own Experiment
@@ -72,12 +72,11 @@ with dxp.run:
72
72
  from ml_dash import Experiment
73
73
 
74
74
  with Experiment(
75
- name="my-experiment",
76
- project="my-project",
77
- remote="https://api.dash.ml" # token auto-loaded
75
+ prefix="alice/my-project/my-experiment",
76
+ dash_url="https://api.dash.ml", # token auto-loaded
78
77
  ).run as experiment:
79
- experiment.log().info("Hello!")
80
- experiment.params.set(lr=0.001)
78
+ experiment.log("Hello!", level="info")
79
+ experiment.params.set(lr=0.001)
81
80
  ```
82
81
 
83
82
  #### Option C: Local Mode (No Authentication Required)
@@ -86,11 +85,10 @@ with Experiment(
86
85
  from ml_dash import Experiment
87
86
 
88
87
  with Experiment(
89
- name="my-experiment",
90
- project="my-project",
91
- local_path=".ml-dash"
88
+ project="my-project", prefix="my-experiment", dash_root=".dash"
92
89
  ).run as experiment:
93
- experiment.log().info("Running locally")
90
+ experiment.log("Running locally", level="info")
91
+
94
92
  ```
95
93
 
96
94
  See [docs/getting-started.md](docs/getting-started.md) for more examples.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ml-dash"
3
- version = "0.6.2rc1"
3
+ version = "0.6.4"
4
4
  description = "ML experiment tracking and data storage"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "scikit-image>=0.21.0",
32
32
  "rich>=13.0.0",
33
33
  "cryptography>=42.0.0",
34
- "params-proto>=3.0.0rc12",
34
+ "params-proto>=3.0.0",
35
35
  ]
36
36
 
37
37
  [project.scripts]
@@ -0,0 +1,59 @@
1
+ """
2
+ ML-Dash Python SDK
3
+
4
+ A simple and flexible SDK for ML experiment metricing and data storage.
5
+
6
+ Prefix format: {owner}/{project}/path.../[name]
7
+ - owner: First segment (e.g., your username)
8
+ - project: Second segment (e.g., project name)
9
+ - path: Remaining segments form the folder structure
10
+ - name: Derived from last segment (may be a seed/id)
11
+
12
+ Usage:
13
+
14
+ from ml_dash import Experiment
15
+
16
+ # Local mode - explicit configuration
17
+ with Experiment(
18
+ prefix="ge/my-project/experiments/exp1",
19
+ dash_root=".dash"
20
+ ).run as exp:
21
+ exp.log("Training started")
22
+ exp.params.set(lr=0.001)
23
+ exp.metrics("train").log(loss=0.5, step=0)
24
+
25
+ # Default: Remote mode (defaults to https://api.dash.ml)
26
+ with Experiment(prefix="ge/my-project/experiments/exp1").run as exp:
27
+ exp.log("Training started")
28
+ exp.params.set(lr=0.001)
29
+ exp.metrics("train").log(loss=0.5, step=0)
30
+
31
+ # Decorator style
32
+ from ml_dash import ml_dash_experiment
33
+
34
+ @ml_dash_experiment(prefix="ge/my-project/experiments/exp1")
35
+ def train_model(exp):
36
+ exp.log("Training started")
37
+ """
38
+
39
+ from .client import RemoteClient
40
+ from .experiment import Experiment, OperationMode, RunManager, ml_dash_experiment
41
+ from .log import LogBuilder, LogLevel
42
+ from .params import ParametersBuilder
43
+ from .run import RUN
44
+ from .storage import LocalStorage
45
+
46
+ __version__ = "0.6.4"
47
+
48
+ __all__ = [
49
+ "Experiment",
50
+ "ml_dash_experiment",
51
+ "OperationMode",
52
+ "RunManager",
53
+ "RemoteClient",
54
+ "LocalStorage",
55
+ "LogLevel",
56
+ "LogBuilder",
57
+ "ParametersBuilder",
58
+ "RUN",
59
+ ]
@@ -0,0 +1,303 @@
1
+ """Token storage backends for ml-dash authentication."""
2
+
3
+ import json
4
+ from abc import ABC, abstractmethod
5
+ from base64 import urlsafe_b64decode
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from .exceptions import StorageError
10
+
11
+
12
+ class TokenStorage(ABC):
13
+ """Abstract base class for token storage backends."""
14
+
15
+ @abstractmethod
16
+ def store(self, key: str, value: str) -> None:
17
+ """Store a token.
18
+
19
+ Args:
20
+ key: Storage key
21
+ value: Token string to store
22
+ """
23
+ pass
24
+
25
+ @abstractmethod
26
+ def load(self, key: str) -> Optional[str]:
27
+ """Load a token.
28
+
29
+ Args:
30
+ key: Storage key
31
+
32
+ Returns:
33
+ Token string or None if not found
34
+ """
35
+ pass
36
+
37
+ @abstractmethod
38
+ def delete(self, key: str) -> None:
39
+ """Delete a token.
40
+
41
+ Args:
42
+ key: Storage key
43
+ """
44
+ pass
45
+
46
+
47
+ class KeyringStorage(TokenStorage):
48
+ """OS keyring storage backend (macOS Keychain, Windows Credential Manager, Linux Secret Service)."""
49
+
50
+ SERVICE_NAME = "ml-dash"
51
+
52
+ def __init__(self):
53
+ """Initialize keyring storage."""
54
+ try:
55
+ import keyring
56
+
57
+ self.keyring = keyring
58
+ except ImportError:
59
+ raise StorageError(
60
+ "keyring library not installed. Install with: pip install keyring"
61
+ )
62
+
63
+ def store(self, key: str, value: str) -> None:
64
+ """Store token in OS keyring."""
65
+ try:
66
+ self.keyring.set_password(self.SERVICE_NAME, key, value)
67
+ except Exception as e:
68
+ raise StorageError(f"Failed to store token in keyring: {e}")
69
+
70
+ def load(self, key: str) -> Optional[str]:
71
+ """Load token from OS keyring."""
72
+ try:
73
+ return self.keyring.get_password(self.SERVICE_NAME, key)
74
+ except Exception as e:
75
+ raise StorageError(f"Failed to load token from keyring: {e}")
76
+
77
+ def delete(self, key: str) -> None:
78
+ """Delete token from OS keyring."""
79
+ try:
80
+ self.keyring.delete_password(self.SERVICE_NAME, key)
81
+ except Exception:
82
+ # Silently ignore if key doesn't exist
83
+ pass
84
+
85
+
86
+ class EncryptedFileStorage(TokenStorage):
87
+ """Encrypted file storage backend using Fernet symmetric encryption."""
88
+
89
+ def __init__(self, config_dir: Path):
90
+ """Initialize encrypted file storage.
91
+
92
+ Args:
93
+ config_dir: Configuration directory path
94
+ """
95
+ self.config_dir = Path(config_dir)
96
+ self.tokens_file = self.config_dir / "tokens.encrypted"
97
+ self.key_file = self.config_dir / "encryption.key"
98
+
99
+ try:
100
+ from cryptography.fernet import Fernet
101
+
102
+ self.Fernet = Fernet
103
+ except ImportError:
104
+ raise StorageError(
105
+ "cryptography library not installed. Install with: pip install cryptography"
106
+ )
107
+
108
+ # Ensure config directory exists
109
+ self.config_dir.mkdir(parents=True, exist_ok=True)
110
+
111
+ # Generate or load encryption key
112
+ if not self.key_file.exists():
113
+ key = self.Fernet.generate_key()
114
+ self.key_file.write_bytes(key)
115
+ self.key_file.chmod(0o600) # User read/write only
116
+ else:
117
+ key = self.key_file.read_bytes()
118
+
119
+ self.cipher = self.Fernet(key)
120
+
121
+ def _load_all(self) -> dict:
122
+ """Load all tokens from encrypted file."""
123
+ if not self.tokens_file.exists():
124
+ return {}
125
+
126
+ try:
127
+ encrypted = self.tokens_file.read_bytes()
128
+ decrypted = self.cipher.decrypt(encrypted)
129
+ return json.loads(decrypted)
130
+ except Exception as e:
131
+ raise StorageError(f"Failed to decrypt tokens file: {e}")
132
+
133
+ def _save_all(self, data: dict) -> None:
134
+ """Save all tokens to encrypted file."""
135
+ try:
136
+ plaintext = json.dumps(data).encode()
137
+ encrypted = self.cipher.encrypt(plaintext)
138
+ self.tokens_file.write_bytes(encrypted)
139
+ self.tokens_file.chmod(0o600) # User read/write only
140
+ except Exception as e:
141
+ raise StorageError(f"Failed to encrypt tokens file: {e}")
142
+
143
+ def store(self, key: str, value: str) -> None:
144
+ """Store token in encrypted file."""
145
+ all_tokens = self._load_all()
146
+ all_tokens[key] = value
147
+ self._save_all(all_tokens)
148
+
149
+ def load(self, key: str) -> Optional[str]:
150
+ """Load token from encrypted file."""
151
+ all_tokens = self._load_all()
152
+ return all_tokens.get(key)
153
+
154
+ def delete(self, key: str) -> None:
155
+ """Delete token from encrypted file."""
156
+ all_tokens = self._load_all()
157
+ if key in all_tokens:
158
+ del all_tokens[key]
159
+ self._save_all(all_tokens)
160
+
161
+
162
+ class PlaintextFileStorage(TokenStorage):
163
+ """Plaintext file storage backend (INSECURE - only for testing/fallback)."""
164
+
165
+ _warning_shown = False
166
+
167
+ def __init__(self, config_dir: Path):
168
+ """Initialize plaintext file storage.
169
+
170
+ Args:
171
+ config_dir: Configuration directory path
172
+ """
173
+ self.config_dir = Path(config_dir)
174
+ self.tokens_file = self.config_dir / "tokens.json"
175
+
176
+ # Ensure config directory exists
177
+ self.config_dir.mkdir(parents=True, exist_ok=True)
178
+
179
+ # Show security warning on first use
180
+ if not PlaintextFileStorage._warning_shown:
181
+ try:
182
+ from rich.console import Console
183
+
184
+ console = Console()
185
+ console.print(
186
+ "\n[bold red]WARNING: Storing tokens in plaintext![/bold red]\n"
187
+ "[yellow]Your authentication tokens are being stored unencrypted.[/yellow]\n"
188
+ "[yellow]This is insecure and only recommended for testing.[/yellow]\n\n"
189
+ "To use secure storage:\n"
190
+ " • Install keyring: pip install keyring\n"
191
+ " • Or encrypted storage will be used automatically\n"
192
+ )
193
+ except ImportError:
194
+ print("WARNING: Storing tokens in plaintext! This is insecure.")
195
+
196
+ PlaintextFileStorage._warning_shown = True
197
+
198
+ def _load_all(self) -> dict:
199
+ """Load all tokens from file."""
200
+ if not self.tokens_file.exists():
201
+ return {}
202
+
203
+ try:
204
+ with open(self.tokens_file, "r") as f:
205
+ return json.load(f)
206
+ except (json.JSONDecodeError, IOError):
207
+ return {}
208
+
209
+ def _save_all(self, data: dict) -> None:
210
+ """Save all tokens to file."""
211
+ with open(self.tokens_file, "w") as f:
212
+ json.dump(data, f, indent=2)
213
+ self.tokens_file.chmod(0o600) # User read/write only
214
+
215
+ def store(self, key: str, value: str) -> None:
216
+ """Store token in plaintext file."""
217
+ all_tokens = self._load_all()
218
+ all_tokens[key] = value
219
+ self._save_all(all_tokens)
220
+
221
+ def load(self, key: str) -> Optional[str]:
222
+ """Load token from plaintext file."""
223
+ all_tokens = self._load_all()
224
+ return all_tokens.get(key)
225
+
226
+ def delete(self, key: str) -> None:
227
+ """Delete token from plaintext file."""
228
+ all_tokens = self._load_all()
229
+ if key in all_tokens:
230
+ del all_tokens[key]
231
+ self._save_all(all_tokens)
232
+
233
+
234
+ def get_token_storage(config_dir: Optional[Path] = None) -> TokenStorage:
235
+ """Auto-detect and return appropriate storage backend.
236
+
237
+ Tries backends in order of security:
238
+ 1. KeyringStorage (OS keyring)
239
+ 2. EncryptedFileStorage (encrypted file)
240
+ 3. PlaintextFileStorage (plaintext file with warning)
241
+
242
+ Args:
243
+ config_dir: Configuration directory (defaults to ~/.dash)
244
+
245
+ Returns:
246
+ TokenStorage instance
247
+ """
248
+ if config_dir is None:
249
+ config_dir = Path.home() / ".dash"
250
+
251
+ # Try keyring first
252
+ try:
253
+ return KeyringStorage()
254
+ except (ImportError, StorageError):
255
+ pass
256
+
257
+ # Try encrypted file storage
258
+ try:
259
+ return EncryptedFileStorage(config_dir)
260
+ except (ImportError, StorageError):
261
+ pass
262
+
263
+ # Fallback to plaintext (with warning)
264
+ return PlaintextFileStorage(config_dir)
265
+
266
+
267
+ def decode_jwt_payload(token: str) -> dict:
268
+ """Decode JWT payload without verification (for display only).
269
+
270
+ Args:
271
+ token: JWT token string
272
+
273
+ Returns:
274
+ Decoded payload dict
275
+ """
276
+ try:
277
+ # JWT format: header.payload.signature
278
+ parts = token.split(".")
279
+ if len(parts) != 3:
280
+ return {}
281
+
282
+ # Decode payload (second part)
283
+ payload = parts[1]
284
+ # Add padding if needed
285
+ padding = 4 - len(payload) % 4
286
+ if padding != 4:
287
+ payload += "=" * padding
288
+
289
+ decoded = urlsafe_b64decode(payload)
290
+ return json.loads(decoded)
291
+ except Exception:
292
+ return {}
293
+
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
@@ -9,43 +9,56 @@ Usage:
9
9
  # First, authenticate
10
10
  # $ ml-dash login
11
11
 
12
- from ml_dash import dxp
12
+ from ml_dash.auto_start import dxp
13
13
 
14
14
  # Use with statement (recommended)
15
15
  with dxp.run:
16
- dxp.log().info("Hello from dxp!")
16
+ dxp.log("Hello from dxp!", level="info")
17
17
  dxp.params.set(lr=0.001)
18
- dxp.metrics("loss").append(step=0, value=0.5)
18
+ dxp.metrics("train").log(loss=0.5, step=0)
19
19
  # Automatically completes on exit from with block
20
20
 
21
21
  # Or start/complete manually
22
22
  dxp.run.start()
23
- dxp.log().info("Training...")
23
+ dxp.log("Training...", level="info")
24
24
  dxp.run.complete()
25
25
  """
26
26
 
27
27
  import atexit
28
- from .experiment import Experiment
29
28
 
30
29
  # Create pre-configured singleton experiment in remote mode
31
30
  # Uses default remote server (https://api.dash.ml)
32
31
  # Token is auto-loaded from storage when first used
33
32
  # If not authenticated, operations will fail with AuthenticationError
33
+ # Prefix format: {owner}/{project}/path...
34
+ # Using getpass to get current user as owner for local convenience
35
+ import getpass
36
+ from datetime import datetime
37
+
38
+ from .auth.token_storage import get_jwt_user
39
+ from .experiment import Experiment
40
+
41
+ _user = get_jwt_user()
42
+ # Fallback to system username if not authenticated
43
+ _username = _user["username"] if _user else getpass.getuser()
44
+ _now = datetime.now()
45
+
34
46
  dxp = Experiment(
35
- name="dxp",
36
- project="scratch",
37
- remote="https://api.dash.ml",
47
+ prefix=f"{_username}/scratch/{_now:%Y-%m-%d/%H%M%S}",
48
+ dash_url="https://api.dash.ml",
38
49
  )
39
50
 
51
+
40
52
  # Register cleanup handler to complete experiment on Python exit (if still open)
41
53
  def _cleanup():
42
- """Complete the dxp experiment on exit if still open."""
43
- if dxp._is_open:
44
- try:
45
- dxp.run.complete()
46
- except Exception:
47
- # Silently ignore errors during cleanup
48
- pass
54
+ """Complete the dxp experiment on exit if still open."""
55
+ if dxp._is_open:
56
+ try:
57
+ dxp.run.complete()
58
+ except Exception:
59
+ # Silently ignore errors during cleanup
60
+ pass
61
+
49
62
 
50
63
  atexit.register(_cleanup)
51
64
 
@@ -9,7 +9,11 @@ def create_parser() -> argparse.ArgumentParser:
9
9
  """Create the main CLI argument parser."""
10
10
  parser = argparse.ArgumentParser(
11
11
  prog="ml-dash",
12
- description="ML-Dash: ML experiment tracking and data storage CLI",
12
+ description=(
13
+ "ML-Dash: ML experiment tracking and data storage CLI\n\n"
14
+ "View your experiments, statistics, and plots online at:\n"
15
+ " https://dash.ml\n"
16
+ ),
13
17
  formatter_class=argparse.RawDescriptionHelpFormatter,
14
18
  )
15
19
 
@@ -21,11 +25,15 @@ def create_parser() -> argparse.ArgumentParser:
21
25
  )
22
26
 
23
27
  # Import and add command parsers
24
- from .cli_commands import upload, download, list as list_cmd, login, logout
28
+ from .cli_commands import upload, download, list as list_cmd, login, logout, profile, api
25
29
 
26
30
  # Authentication commands
27
31
  login.add_parser(subparsers)
28
32
  logout.add_parser(subparsers)
33
+ profile.add_parser(subparsers)
34
+
35
+ # API commands
36
+ api.add_parser(subparsers)
29
37
 
30
38
  # Data commands
31
39
  upload.add_parser(subparsers)
@@ -60,6 +68,9 @@ def main(argv: Optional[List[str]] = None) -> int:
60
68
  elif args.command == "logout":
61
69
  from .cli_commands import logout
62
70
  return logout.cmd_logout(args)
71
+ elif args.command == "profile":
72
+ from .cli_commands import profile
73
+ return profile.cmd_profile(args)
63
74
  elif args.command == "upload":
64
75
  from .cli_commands import upload
65
76
  return upload.cmd_upload(args)
@@ -69,6 +80,9 @@ def main(argv: Optional[List[str]] = None) -> int:
69
80
  elif args.command == "list":
70
81
  from .cli_commands import list as list_cmd
71
82
  return list_cmd.cmd_list(args)
83
+ elif args.command == "api":
84
+ from .cli_commands import api
85
+ return api.cmd_api(args)
72
86
 
73
87
  # Unknown command (shouldn't happen due to subparsers)
74
88
  parser.print_help()