ml-dash 0.6.2rc1__py3-none-any.whl → 0.6.3__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/__init__.py CHANGED
@@ -3,85 +3,57 @@ ML-Dash Python SDK
3
3
 
4
4
  A simple and flexible SDK for ML experiment metricing and data storage.
5
5
 
6
- Usage:
7
-
8
- # Quickest - dxp (pre-configured remote singleton)
9
- # Requires: ml-dash login
10
- from ml_dash import dxp
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
11
 
12
- with dxp.run:
13
- dxp.params.set(lr=0.001)
14
- dxp.log().info("Training started")
15
- # Auto-completes on exit from with block
12
+ Usage:
16
13
 
17
- # Local mode - explicit configuration
18
14
  from ml_dash import Experiment
19
15
 
16
+ # Local mode - explicit configuration
20
17
  with Experiment(
21
- name="my-experiment",
22
- project="my-project",
23
- local_path=".ml-dash"
24
- ) as experiment:
25
- experiment.log("Training started")
26
- experiment.params.set(lr=0.001)
27
- experiment.metrics("loss").append(step=0, value=0.5)
28
-
29
- # Remote mode - explicit configuration
30
- with Experiment(
31
- name="my-experiment",
32
- project="my-project",
33
- remote="https://api.dash.ml",
34
- api_key="your-jwt-token"
35
- ) as experiment:
36
- experiment.log("Training started")
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)
37
30
 
38
31
  # Decorator style
39
32
  from ml_dash import ml_dash_experiment
40
33
 
41
- @ml_dash_experiment(
42
- name="my-experiment",
43
- project="my-project"
44
- )
45
- def train_model(experiment):
46
- experiment.log("Training started")
34
+ @ml_dash_experiment(prefix="ge/my-project/experiments/exp1")
35
+ def train_model(exp):
36
+ exp.log("Training started")
47
37
  """
48
38
 
49
- from .experiment import Experiment, ml_dash_experiment, OperationMode, RunManager
50
39
  from .client import RemoteClient
51
- from .storage import LocalStorage
52
- from .log import LogLevel, LogBuilder
40
+ from .experiment import Experiment, OperationMode, RunManager, ml_dash_experiment
41
+ from .log import LogBuilder, LogLevel
53
42
  from .params import ParametersBuilder
54
43
  from .run import RUN
55
- from .auto_start import dxp
44
+ from .storage import LocalStorage
56
45
 
57
- __version__ = "0.1.0"
46
+ __version__ = "0.6.3"
58
47
 
59
48
  __all__ = [
60
- "Experiment",
61
- "ml_dash_experiment",
62
- "OperationMode",
63
- "RunManager",
64
- "RemoteClient",
65
- "LocalStorage",
66
- "LogLevel",
67
- "LogBuilder",
68
- "ParametersBuilder",
69
- "RUN",
70
- "dxp",
49
+ "Experiment",
50
+ "ml_dash_experiment",
51
+ "OperationMode",
52
+ "RunManager",
53
+ "RemoteClient",
54
+ "LocalStorage",
55
+ "LogLevel",
56
+ "LogBuilder",
57
+ "ParametersBuilder",
58
+ "RUN",
71
59
  ]
72
-
73
- # Hidden for now - rdxp (remote auto-start singleton)
74
- # Will be exposed in a future release
75
- #
76
- # # Lazy-load rdxp to avoid auto-connecting to server on package import
77
- # _rdxp = None
78
- #
79
- # def __getattr__(name):
80
- # """Lazy-load rdxp only when accessed."""
81
- # if name == "rdxp":
82
- # global _rdxp
83
- # if _rdxp is None:
84
- # from .remote_auto_start import rdxp as _loaded_rdxp
85
- # _rdxp = _loaded_rdxp
86
- # return _rdxp
87
- # raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  from abc import ABC, abstractmethod
5
+ from base64 import urlsafe_b64decode
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
@@ -9,254 +10,294 @@ from .exceptions import StorageError
9
10
 
10
11
 
11
12
  class TokenStorage(ABC):
12
- """Abstract base class for token storage backends."""
13
+ """Abstract base class for token storage backends."""
13
14
 
14
- @abstractmethod
15
- def store(self, key: str, value: str) -> None:
16
- """Store a token.
15
+ @abstractmethod
16
+ def store(self, key: str, value: str) -> None:
17
+ """Store a token.
17
18
 
18
- Args:
19
- key: Storage key
20
- value: Token string to store
21
- """
22
- pass
19
+ Args:
20
+ key: Storage key
21
+ value: Token string to store
22
+ """
23
+ pass
23
24
 
24
- @abstractmethod
25
- def load(self, key: str) -> Optional[str]:
26
- """Load a token.
25
+ @abstractmethod
26
+ def load(self, key: str) -> Optional[str]:
27
+ """Load a token.
27
28
 
28
- Args:
29
- key: Storage key
29
+ Args:
30
+ key: Storage key
30
31
 
31
- Returns:
32
- Token string or None if not found
33
- """
34
- pass
32
+ Returns:
33
+ Token string or None if not found
34
+ """
35
+ pass
35
36
 
36
- @abstractmethod
37
- def delete(self, key: str) -> None:
38
- """Delete a token.
37
+ @abstractmethod
38
+ def delete(self, key: str) -> None:
39
+ """Delete a token.
39
40
 
40
- Args:
41
- key: Storage key
42
- """
43
- pass
41
+ Args:
42
+ key: Storage key
43
+ """
44
+ pass
44
45
 
45
46
 
46
47
  class KeyringStorage(TokenStorage):
47
- """OS keyring storage backend (macOS Keychain, Windows Credential Manager, Linux Secret Service)."""
48
-
49
- SERVICE_NAME = "ml-dash"
50
-
51
- def __init__(self):
52
- """Initialize keyring storage."""
53
- try:
54
- import keyring
55
- self.keyring = keyring
56
- except ImportError:
57
- raise StorageError(
58
- "keyring library not installed. "
59
- "Install with: pip install keyring"
60
- )
61
-
62
- def store(self, key: str, value: str) -> None:
63
- """Store token in OS keyring."""
64
- try:
65
- self.keyring.set_password(self.SERVICE_NAME, key, value)
66
- except Exception as e:
67
- raise StorageError(f"Failed to store token in keyring: {e}")
68
-
69
- def load(self, key: str) -> Optional[str]:
70
- """Load token from OS keyring."""
71
- try:
72
- return self.keyring.get_password(self.SERVICE_NAME, key)
73
- except Exception as e:
74
- raise StorageError(f"Failed to load token from keyring: {e}")
75
-
76
- def delete(self, key: str) -> None:
77
- """Delete token from OS keyring."""
78
- try:
79
- self.keyring.delete_password(self.SERVICE_NAME, key)
80
- except Exception:
81
- # Silently ignore if key doesn't exist
82
- pass
48
+ """OS keyring storage backend (macOS Keychain, Windows Credential Manager, Linux Secret Service)."""
83
49
 
50
+ SERVICE_NAME = "ml-dash"
84
51
 
85
- class EncryptedFileStorage(TokenStorage):
86
- """Encrypted file storage backend using Fernet symmetric encryption."""
87
-
88
- def __init__(self, config_dir: Path):
89
- """Initialize encrypted file storage.
90
-
91
- Args:
92
- config_dir: Configuration directory path
93
- """
94
- self.config_dir = Path(config_dir)
95
- self.tokens_file = self.config_dir / "tokens.encrypted"
96
- self.key_file = self.config_dir / "encryption.key"
97
-
98
- try:
99
- from cryptography.fernet import Fernet
100
- self.Fernet = Fernet
101
- except ImportError:
102
- raise StorageError(
103
- "cryptography library not installed. "
104
- "Install with: pip install cryptography"
105
- )
106
-
107
- # Ensure config directory exists
108
- self.config_dir.mkdir(parents=True, exist_ok=True)
109
-
110
- # Generate or load encryption key
111
- if not self.key_file.exists():
112
- key = self.Fernet.generate_key()
113
- self.key_file.write_bytes(key)
114
- self.key_file.chmod(0o600) # User read/write only
115
- else:
116
- key = self.key_file.read_bytes()
117
-
118
- self.cipher = self.Fernet(key)
119
-
120
- def _load_all(self) -> dict:
121
- """Load all tokens from encrypted file."""
122
- if not self.tokens_file.exists():
123
- return {}
124
-
125
- try:
126
- encrypted = self.tokens_file.read_bytes()
127
- decrypted = self.cipher.decrypt(encrypted)
128
- return json.loads(decrypted)
129
- except Exception as e:
130
- raise StorageError(f"Failed to decrypt tokens file: {e}")
131
-
132
- def _save_all(self, data: dict) -> None:
133
- """Save all tokens to encrypted file."""
134
- try:
135
- plaintext = json.dumps(data).encode()
136
- encrypted = self.cipher.encrypt(plaintext)
137
- self.tokens_file.write_bytes(encrypted)
138
- self.tokens_file.chmod(0o600) # User read/write only
139
- except Exception as e:
140
- raise StorageError(f"Failed to encrypt tokens file: {e}")
141
-
142
- def store(self, key: str, value: str) -> None:
143
- """Store token in encrypted file."""
144
- all_tokens = self._load_all()
145
- all_tokens[key] = value
146
- self._save_all(all_tokens)
147
-
148
- def load(self, key: str) -> Optional[str]:
149
- """Load token from encrypted file."""
150
- all_tokens = self._load_all()
151
- return all_tokens.get(key)
152
-
153
- def delete(self, key: str) -> None:
154
- """Delete token from encrypted file."""
155
- all_tokens = self._load_all()
156
- if key in all_tokens:
157
- del all_tokens[key]
158
- self._save_all(all_tokens)
52
+ def __init__(self):
53
+ """Initialize keyring storage."""
54
+ try:
55
+ import keyring
159
56
 
57
+ self.keyring = keyring
58
+ except ImportError:
59
+ raise StorageError(
60
+ "keyring library not installed. Install with: pip install keyring"
61
+ )
160
62
 
161
- class PlaintextFileStorage(TokenStorage):
162
- """Plaintext file storage backend (INSECURE - only for testing/fallback)."""
163
-
164
- _warning_shown = False
165
-
166
- def __init__(self, config_dir: Path):
167
- """Initialize plaintext file storage.
168
-
169
- Args:
170
- config_dir: Configuration directory path
171
- """
172
- self.config_dir = Path(config_dir)
173
- self.tokens_file = self.config_dir / "tokens.json"
174
-
175
- # Ensure config directory exists
176
- self.config_dir.mkdir(parents=True, exist_ok=True)
177
-
178
- # Show security warning on first use
179
- if not PlaintextFileStorage._warning_shown:
180
- try:
181
- from rich.console import Console
182
- console = Console()
183
- console.print(
184
- "\n[bold red]WARNING: Storing tokens in plaintext![/bold red]\n"
185
- "[yellow]Your authentication tokens are being stored unencrypted.[/yellow]\n"
186
- "[yellow]This is insecure and only recommended for testing.[/yellow]\n\n"
187
- "To use secure storage:\n"
188
- " • Install keyring: pip install keyring\n"
189
- " • Or encrypted storage will be used automatically\n"
190
- )
191
- except ImportError:
192
- print("WARNING: Storing tokens in plaintext! This is insecure.")
193
-
194
- PlaintextFileStorage._warning_shown = True
195
-
196
- def _load_all(self) -> dict:
197
- """Load all tokens from file."""
198
- if not self.tokens_file.exists():
199
- return {}
200
-
201
- try:
202
- with open(self.tokens_file, "r") as f:
203
- return json.load(f)
204
- except (json.JSONDecodeError, IOError):
205
- return {}
206
-
207
- def _save_all(self, data: dict) -> None:
208
- """Save all tokens to file."""
209
- with open(self.tokens_file, "w") as f:
210
- json.dump(data, f, indent=2)
211
- self.tokens_file.chmod(0o600) # User read/write only
212
-
213
- def store(self, key: str, value: str) -> None:
214
- """Store token in plaintext file."""
215
- all_tokens = self._load_all()
216
- all_tokens[key] = value
217
- self._save_all(all_tokens)
218
-
219
- def load(self, key: str) -> Optional[str]:
220
- """Load token from plaintext file."""
221
- all_tokens = self._load_all()
222
- return all_tokens.get(key)
223
-
224
- def delete(self, key: str) -> None:
225
- """Delete token from plaintext file."""
226
- all_tokens = self._load_all()
227
- if key in all_tokens:
228
- del all_tokens[key]
229
- self._save_all(all_tokens)
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}")
230
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}")
231
76
 
232
- def get_token_storage(config_dir: Optional[Path] = None) -> TokenStorage:
233
- """Auto-detect and return appropriate storage backend.
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
234
84
 
235
- Tries backends in order of security:
236
- 1. KeyringStorage (OS keyring)
237
- 2. EncryptedFileStorage (encrypted file)
238
- 3. PlaintextFileStorage (plaintext file with warning)
239
85
 
240
- Args:
241
- config_dir: Configuration directory (defaults to ~/.ml-dash)
86
+ class EncryptedFileStorage(TokenStorage):
87
+ """Encrypted file storage backend using Fernet symmetric encryption."""
242
88
 
243
- Returns:
244
- TokenStorage instance
89
+ def __init__(self, config_dir: Path):
90
+ """Initialize encrypted file storage.
91
+
92
+ Args:
93
+ config_dir: Configuration directory path
245
94
  """
246
- if config_dir is None:
247
- config_dir = Path.home() / ".ml-dash"
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"
248
98
 
249
- # Try keyring first
250
99
  try:
251
- return KeyringStorage()
252
- except (ImportError, StorageError):
253
- pass
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 {}
254
202
 
255
- # Try encrypted file storage
256
203
  try:
257
- return EncryptedFileStorage(config_dir)
258
- except (ImportError, StorageError):
259
- pass
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)
260
232
 
261
- # Fallback to plaintext (with warning)
262
- return PlaintextFileStorage(config_dir)
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