ml-dash 0.6.2__py3-none-any.whl → 0.6.2rc1__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,57 +3,85 @@ ML-Dash Python SDK
3
3
 
4
4
  A simple and flexible SDK for ML experiment metricing and data storage.
5
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
6
  Usage:
13
7
 
14
- from ml_dash import Experiment
8
+ # Quickest - dxp (pre-configured remote singleton)
9
+ # Requires: ml-dash login
10
+ from ml_dash import dxp
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
15
16
 
16
17
  # Local mode - explicit configuration
18
+ from ml_dash import Experiment
19
+
17
20
  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)
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")
30
37
 
31
38
  # Decorator style
32
39
  from ml_dash import ml_dash_experiment
33
40
 
34
- @ml_dash_experiment(prefix="ge/my-project/experiments/exp1")
35
- def train_model(exp):
36
- exp.log("Training started")
41
+ @ml_dash_experiment(
42
+ name="my-experiment",
43
+ project="my-project"
44
+ )
45
+ def train_model(experiment):
46
+ experiment.log("Training started")
37
47
  """
38
48
 
49
+ from .experiment import Experiment, ml_dash_experiment, OperationMode, RunManager
39
50
  from .client import RemoteClient
40
- from .experiment import Experiment, OperationMode, RunManager, ml_dash_experiment
41
- from .log import LogBuilder, LogLevel
51
+ from .storage import LocalStorage
52
+ from .log import LogLevel, LogBuilder
42
53
  from .params import ParametersBuilder
43
54
  from .run import RUN
44
- from .storage import LocalStorage
55
+ from .auto_start import dxp
45
56
 
46
- __version__ = "0.6.2"
57
+ __version__ = "0.1.0"
47
58
 
48
59
  __all__ = [
49
- "Experiment",
50
- "ml_dash_experiment",
51
- "OperationMode",
52
- "RunManager",
53
- "RemoteClient",
54
- "LocalStorage",
55
- "LogLevel",
56
- "LogBuilder",
57
- "ParametersBuilder",
58
- "RUN",
60
+ "Experiment",
61
+ "ml_dash_experiment",
62
+ "OperationMode",
63
+ "RunManager",
64
+ "RemoteClient",
65
+ "LocalStorage",
66
+ "LogLevel",
67
+ "LogBuilder",
68
+ "ParametersBuilder",
69
+ "RUN",
70
+ "dxp",
59
71
  ]
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,7 +2,6 @@
2
2
 
3
3
  import json
4
4
  from abc import ABC, abstractmethod
5
- from base64 import urlsafe_b64decode
6
5
  from pathlib import Path
7
6
  from typing import Optional
8
7
 
@@ -10,294 +9,254 @@ from .exceptions import StorageError
10
9
 
11
10
 
12
11
  class TokenStorage(ABC):
13
- """Abstract base class for token storage backends."""
12
+ """Abstract base class for token storage backends."""
14
13
 
15
- @abstractmethod
16
- def store(self, key: str, value: str) -> None:
17
- """Store a token.
14
+ @abstractmethod
15
+ def store(self, key: str, value: str) -> None:
16
+ """Store a token.
18
17
 
19
- Args:
20
- key: Storage key
21
- value: Token string to store
22
- """
23
- pass
18
+ Args:
19
+ key: Storage key
20
+ value: Token string to store
21
+ """
22
+ pass
24
23
 
25
- @abstractmethod
26
- def load(self, key: str) -> Optional[str]:
27
- """Load a token.
24
+ @abstractmethod
25
+ def load(self, key: str) -> Optional[str]:
26
+ """Load a token.
28
27
 
29
- Args:
30
- key: Storage key
28
+ Args:
29
+ key: Storage key
31
30
 
32
- Returns:
33
- Token string or None if not found
34
- """
35
- pass
31
+ Returns:
32
+ Token string or None if not found
33
+ """
34
+ pass
36
35
 
37
- @abstractmethod
38
- def delete(self, key: str) -> None:
39
- """Delete a token.
36
+ @abstractmethod
37
+ def delete(self, key: str) -> None:
38
+ """Delete a token.
40
39
 
41
- Args:
42
- key: Storage key
43
- """
44
- pass
40
+ Args:
41
+ key: Storage key
42
+ """
43
+ pass
45
44
 
46
45
 
47
46
  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
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
84
83
 
85
84
 
86
85
  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)
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)
160
159
 
161
160
 
162
161
  class PlaintextFileStorage(TokenStorage):
163
- """Plaintext file storage backend (INSECURE - only for testing/fallback)."""
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)
164
230
 
165
- _warning_shown = False
166
231
 
167
- def __init__(self, config_dir: Path):
168
- """Initialize plaintext file storage.
232
+ def get_token_storage(config_dir: Optional[Path] = None) -> TokenStorage:
233
+ """Auto-detect and return appropriate storage backend.
234
+
235
+ Tries backends in order of security:
236
+ 1. KeyringStorage (OS keyring)
237
+ 2. EncryptedFileStorage (encrypted file)
238
+ 3. PlaintextFileStorage (plaintext file with warning)
169
239
 
170
240
  Args:
171
- config_dir: Configuration directory path
241
+ config_dir: Configuration directory (defaults to ~/.ml-dash)
242
+
243
+ Returns:
244
+ TokenStorage instance
172
245
  """
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 {}
246
+ if config_dir is None:
247
+ config_dir = Path.home() / ".ml-dash"
202
248
 
249
+ # Try keyring first
203
250
  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)
251
+ return KeyringStorage()
252
+ except (ImportError, StorageError):
253
+ pass
232
254
 
255
+ # Try encrypted file storage
256
+ try:
257
+ return EncryptedFileStorage(config_dir)
258
+ except (ImportError, StorageError):
259
+ pass
233
260
 
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
261
+ # Fallback to plaintext (with warning)
262
+ return PlaintextFileStorage(config_dir)