ml-dash 0.6.2__tar.gz → 0.6.2rc1__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.2 → ml_dash-0.6.2rc1}/PKG-INFO +14 -12
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/README.md +12 -10
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/pyproject.toml +2 -2
- ml_dash-0.6.2rc1/src/ml_dash/__init__.py +87 -0
- ml_dash-0.6.2rc1/src/ml_dash/auth/token_storage.py +262 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/auto_start.py +15 -28
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/cli.py +2 -16
- ml_dash-0.6.2rc1/src/ml_dash/cli_commands/download.py +769 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/cli_commands/list.py +13 -146
- ml_dash-0.6.2rc1/src/ml_dash/cli_commands/login.py +225 -0
- ml_dash-0.6.2rc1/src/ml_dash/cli_commands/upload.py +1248 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/client.py +6 -79
- ml_dash-0.6.2rc1/src/ml_dash/config.py +133 -0
- ml_dash-0.6.2rc1/src/ml_dash/experiment.py +1163 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/files.py +224 -339
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/log.py +7 -7
- ml_dash-0.6.2rc1/src/ml_dash/metric.py +481 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/params.py +6 -6
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/remote_auto_start.py +17 -20
- ml_dash-0.6.2rc1/src/ml_dash/run.py +85 -0
- ml_dash-0.6.2rc1/src/ml_dash/storage.py +1129 -0
- ml_dash-0.6.2/src/ml_dash/__init__.py +0 -59
- ml_dash-0.6.2/src/ml_dash/auth/token_storage.py +0 -303
- ml_dash-0.6.2/src/ml_dash/cli_commands/api.py +0 -165
- ml_dash-0.6.2/src/ml_dash/cli_commands/download.py +0 -859
- ml_dash-0.6.2/src/ml_dash/cli_commands/login.py +0 -232
- ml_dash-0.6.2/src/ml_dash/cli_commands/profile.py +0 -92
- ml_dash-0.6.2/src/ml_dash/cli_commands/upload.py +0 -1398
- ml_dash-0.6.2/src/ml_dash/config.py +0 -133
- ml_dash-0.6.2/src/ml_dash/experiment.py +0 -1363
- ml_dash-0.6.2/src/ml_dash/metric.py +0 -740
- ml_dash-0.6.2/src/ml_dash/run.py +0 -231
- ml_dash-0.6.2/src/ml_dash/snowflake.py +0 -173
- ml_dash-0.6.2/src/ml_dash/storage.py +0 -1099
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/LICENSE +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/auth/__init__.py +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/auth/constants.py +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/auth/device_flow.py +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/auth/device_secret.py +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/auth/exceptions.py +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/cli_commands/__init__.py +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/src/ml_dash/cli_commands/logout.py +0 -0
- {ml_dash-0.6.2 → ml_dash-0.6.2rc1}/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.
|
|
3
|
+
Version: 0.6.2rc1
|
|
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
|
|
46
|
+
Requires-Dist: params-proto>=3.0.0rc12
|
|
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
|
|
125
|
+
from ml_dash import dxp
|
|
126
126
|
|
|
127
127
|
# Start experiment (uploads to https://api.dash.ml by default)
|
|
128
128
|
with dxp.run:
|
|
129
|
-
dxp.log("Training started"
|
|
129
|
+
dxp.log().info("Training started")
|
|
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("
|
|
134
|
+
dxp.metrics("loss").append(value=loss, epoch=epoch)
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
#### Option B: Create Your Own Experiment
|
|
@@ -140,11 +140,12 @@ with dxp.run:
|
|
|
140
140
|
from ml_dash import Experiment
|
|
141
141
|
|
|
142
142
|
with Experiment(
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
name="my-experiment",
|
|
144
|
+
project="my-project",
|
|
145
|
+
remote="https://api.dash.ml" # token auto-loaded
|
|
145
146
|
).run as experiment:
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
experiment.log().info("Hello!")
|
|
148
|
+
experiment.params.set(lr=0.001)
|
|
148
149
|
```
|
|
149
150
|
|
|
150
151
|
#### Option C: Local Mode (No Authentication Required)
|
|
@@ -153,10 +154,11 @@ with Experiment(
|
|
|
153
154
|
from ml_dash import Experiment
|
|
154
155
|
|
|
155
156
|
with Experiment(
|
|
156
|
-
|
|
157
|
+
name="my-experiment",
|
|
158
|
+
project="my-project",
|
|
159
|
+
local_path=".ml-dash"
|
|
157
160
|
).run as experiment:
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
experiment.log().info("Running locally")
|
|
160
162
|
```
|
|
161
163
|
|
|
162
164
|
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
|
|
57
|
+
from ml_dash import dxp
|
|
58
58
|
|
|
59
59
|
# Start experiment (uploads to https://api.dash.ml by default)
|
|
60
60
|
with dxp.run:
|
|
61
|
-
dxp.log("Training started"
|
|
61
|
+
dxp.log().info("Training started")
|
|
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("
|
|
66
|
+
dxp.metrics("loss").append(value=loss, epoch=epoch)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
#### Option B: Create Your Own Experiment
|
|
@@ -72,11 +72,12 @@ with dxp.run:
|
|
|
72
72
|
from ml_dash import Experiment
|
|
73
73
|
|
|
74
74
|
with Experiment(
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
name="my-experiment",
|
|
76
|
+
project="my-project",
|
|
77
|
+
remote="https://api.dash.ml" # token auto-loaded
|
|
77
78
|
).run as experiment:
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
experiment.log().info("Hello!")
|
|
80
|
+
experiment.params.set(lr=0.001)
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
#### Option C: Local Mode (No Authentication Required)
|
|
@@ -85,10 +86,11 @@ with Experiment(
|
|
|
85
86
|
from ml_dash import Experiment
|
|
86
87
|
|
|
87
88
|
with Experiment(
|
|
88
|
-
|
|
89
|
+
name="my-experiment",
|
|
90
|
+
project="my-project",
|
|
91
|
+
local_path=".ml-dash"
|
|
89
92
|
).run as experiment:
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
experiment.log().info("Running locally")
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
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.
|
|
3
|
+
version = "0.6.2rc1"
|
|
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
|
|
34
|
+
"params-proto>=3.0.0rc12",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.scripts]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ML-Dash Python SDK
|
|
3
|
+
|
|
4
|
+
A simple and flexible SDK for ML experiment metricing and data storage.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
|
|
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
|
|
16
|
+
|
|
17
|
+
# Local mode - explicit configuration
|
|
18
|
+
from ml_dash import Experiment
|
|
19
|
+
|
|
20
|
+
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")
|
|
37
|
+
|
|
38
|
+
# Decorator style
|
|
39
|
+
from ml_dash import ml_dash_experiment
|
|
40
|
+
|
|
41
|
+
@ml_dash_experiment(
|
|
42
|
+
name="my-experiment",
|
|
43
|
+
project="my-project"
|
|
44
|
+
)
|
|
45
|
+
def train_model(experiment):
|
|
46
|
+
experiment.log("Training started")
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from .experiment import Experiment, ml_dash_experiment, OperationMode, RunManager
|
|
50
|
+
from .client import RemoteClient
|
|
51
|
+
from .storage import LocalStorage
|
|
52
|
+
from .log import LogLevel, LogBuilder
|
|
53
|
+
from .params import ParametersBuilder
|
|
54
|
+
from .run import RUN
|
|
55
|
+
from .auto_start import dxp
|
|
56
|
+
|
|
57
|
+
__version__ = "0.1.0"
|
|
58
|
+
|
|
59
|
+
__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",
|
|
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}'")
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Token storage backends for ml-dash authentication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .exceptions import StorageError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenStorage(ABC):
|
|
12
|
+
"""Abstract base class for token storage backends."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def store(self, key: str, value: str) -> None:
|
|
16
|
+
"""Store a token.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
key: Storage key
|
|
20
|
+
value: Token string to store
|
|
21
|
+
"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def load(self, key: str) -> Optional[str]:
|
|
26
|
+
"""Load a token.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
key: Storage key
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Token string or None if not found
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def delete(self, key: str) -> None:
|
|
38
|
+
"""Delete a token.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
key: Storage key
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
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
|
|
83
|
+
|
|
84
|
+
|
|
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)
|
|
159
|
+
|
|
160
|
+
|
|
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)
|
|
230
|
+
|
|
231
|
+
|
|
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)
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
config_dir: Configuration directory (defaults to ~/.ml-dash)
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
TokenStorage instance
|
|
245
|
+
"""
|
|
246
|
+
if config_dir is None:
|
|
247
|
+
config_dir = Path.home() / ".ml-dash"
|
|
248
|
+
|
|
249
|
+
# Try keyring first
|
|
250
|
+
try:
|
|
251
|
+
return KeyringStorage()
|
|
252
|
+
except (ImportError, StorageError):
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
# Try encrypted file storage
|
|
256
|
+
try:
|
|
257
|
+
return EncryptedFileStorage(config_dir)
|
|
258
|
+
except (ImportError, StorageError):
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
# Fallback to plaintext (with warning)
|
|
262
|
+
return PlaintextFileStorage(config_dir)
|
|
@@ -9,56 +9,43 @@ Usage:
|
|
|
9
9
|
# First, authenticate
|
|
10
10
|
# $ ml-dash login
|
|
11
11
|
|
|
12
|
-
from ml_dash
|
|
12
|
+
from ml_dash import dxp
|
|
13
13
|
|
|
14
14
|
# Use with statement (recommended)
|
|
15
15
|
with dxp.run:
|
|
16
|
-
dxp.log("Hello from dxp!"
|
|
16
|
+
dxp.log().info("Hello from dxp!")
|
|
17
17
|
dxp.params.set(lr=0.001)
|
|
18
|
-
dxp.metrics("
|
|
18
|
+
dxp.metrics("loss").append(step=0, value=0.5)
|
|
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("Training..."
|
|
23
|
+
dxp.log().info("Training...")
|
|
24
24
|
dxp.run.complete()
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
import atexit
|
|
28
|
+
from .experiment import Experiment
|
|
28
29
|
|
|
29
30
|
# Create pre-configured singleton experiment in remote mode
|
|
30
31
|
# Uses default remote server (https://api.dash.ml)
|
|
31
32
|
# Token is auto-loaded from storage when first used
|
|
32
33
|
# 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
|
-
|
|
46
34
|
dxp = Experiment(
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
name="dxp",
|
|
36
|
+
project="scratch",
|
|
37
|
+
remote="https://api.dash.ml",
|
|
49
38
|
)
|
|
50
39
|
|
|
51
|
-
|
|
52
40
|
# Register cleanup handler to complete experiment on Python exit (if still open)
|
|
53
41
|
def _cleanup():
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
62
49
|
|
|
63
50
|
atexit.register(_cleanup)
|
|
64
51
|
|
|
@@ -9,11 +9,7 @@ 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=
|
|
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
|
-
),
|
|
12
|
+
description="ML-Dash: ML experiment tracking and data storage CLI",
|
|
17
13
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
18
14
|
)
|
|
19
15
|
|
|
@@ -25,15 +21,11 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
25
21
|
)
|
|
26
22
|
|
|
27
23
|
# Import and add command parsers
|
|
28
|
-
from .cli_commands import upload, download, list as list_cmd, login, logout
|
|
24
|
+
from .cli_commands import upload, download, list as list_cmd, login, logout
|
|
29
25
|
|
|
30
26
|
# Authentication commands
|
|
31
27
|
login.add_parser(subparsers)
|
|
32
28
|
logout.add_parser(subparsers)
|
|
33
|
-
profile.add_parser(subparsers)
|
|
34
|
-
|
|
35
|
-
# API commands
|
|
36
|
-
api.add_parser(subparsers)
|
|
37
29
|
|
|
38
30
|
# Data commands
|
|
39
31
|
upload.add_parser(subparsers)
|
|
@@ -68,9 +60,6 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
68
60
|
elif args.command == "logout":
|
|
69
61
|
from .cli_commands import logout
|
|
70
62
|
return logout.cmd_logout(args)
|
|
71
|
-
elif args.command == "profile":
|
|
72
|
-
from .cli_commands import profile
|
|
73
|
-
return profile.cmd_profile(args)
|
|
74
63
|
elif args.command == "upload":
|
|
75
64
|
from .cli_commands import upload
|
|
76
65
|
return upload.cmd_upload(args)
|
|
@@ -80,9 +69,6 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
80
69
|
elif args.command == "list":
|
|
81
70
|
from .cli_commands import list as list_cmd
|
|
82
71
|
return list_cmd.cmd_list(args)
|
|
83
|
-
elif args.command == "api":
|
|
84
|
-
from .cli_commands import api
|
|
85
|
-
return api.cmd_api(args)
|
|
86
72
|
|
|
87
73
|
# Unknown command (shouldn't happen due to subparsers)
|
|
88
74
|
parser.print_help()
|