ml-dash 0.5.9__tar.gz → 0.6.3__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 (42) hide show
  1. {ml_dash-0.5.9 → ml_dash-0.6.3}/PKG-INFO +49 -20
  2. {ml_dash-0.5.9 → ml_dash-0.6.3}/README.md +43 -19
  3. {ml_dash-0.5.9 → ml_dash-0.6.3}/pyproject.toml +7 -1
  4. ml_dash-0.6.3/src/ml_dash/__init__.py +59 -0
  5. ml_dash-0.6.3/src/ml_dash/auth/__init__.py +51 -0
  6. ml_dash-0.6.3/src/ml_dash/auth/constants.py +10 -0
  7. ml_dash-0.6.3/src/ml_dash/auth/device_flow.py +237 -0
  8. ml_dash-0.6.3/src/ml_dash/auth/device_secret.py +49 -0
  9. ml_dash-0.6.3/src/ml_dash/auth/exceptions.py +31 -0
  10. ml_dash-0.6.3/src/ml_dash/auth/token_storage.py +303 -0
  11. ml_dash-0.6.3/src/ml_dash/auto_start.py +65 -0
  12. {ml_dash-0.5.9 → ml_dash-0.6.3}/src/ml_dash/cli.py +29 -3
  13. ml_dash-0.6.3/src/ml_dash/cli_commands/api.py +165 -0
  14. ml_dash-0.6.3/src/ml_dash/cli_commands/download.py +859 -0
  15. {ml_dash-0.5.9 → ml_dash-0.6.3}/src/ml_dash/cli_commands/list.py +156 -47
  16. ml_dash-0.6.3/src/ml_dash/cli_commands/login.py +232 -0
  17. ml_dash-0.6.3/src/ml_dash/cli_commands/logout.py +54 -0
  18. ml_dash-0.6.3/src/ml_dash/cli_commands/profile.py +92 -0
  19. ml_dash-0.6.3/src/ml_dash/cli_commands/upload.py +1398 -0
  20. {ml_dash-0.5.9 → ml_dash-0.6.3}/src/ml_dash/client.py +170 -49
  21. ml_dash-0.6.3/src/ml_dash/config.py +133 -0
  22. ml_dash-0.6.3/src/ml_dash/experiment.py +1363 -0
  23. ml_dash-0.6.3/src/ml_dash/files.py +1496 -0
  24. {ml_dash-0.5.9 → ml_dash-0.6.3}/src/ml_dash/log.py +7 -7
  25. ml_dash-0.6.3/src/ml_dash/metric.py +740 -0
  26. {ml_dash-0.5.9 → ml_dash-0.6.3}/src/ml_dash/params.py +98 -9
  27. ml_dash-0.6.3/src/ml_dash/remote_auto_start.py +58 -0
  28. ml_dash-0.6.3/src/ml_dash/run.py +231 -0
  29. ml_dash-0.6.3/src/ml_dash/snowflake.py +173 -0
  30. ml_dash-0.6.3/src/ml_dash/storage.py +1099 -0
  31. ml_dash-0.5.9/src/ml_dash/__init__.py +0 -59
  32. ml_dash-0.5.9/src/ml_dash/auto_start.py +0 -42
  33. ml_dash-0.5.9/src/ml_dash/cli_commands/download.py +0 -797
  34. ml_dash-0.5.9/src/ml_dash/cli_commands/upload.py +0 -1298
  35. ml_dash-0.5.9/src/ml_dash/config.py +0 -119
  36. ml_dash-0.5.9/src/ml_dash/experiment.py +0 -1020
  37. ml_dash-0.5.9/src/ml_dash/files.py +0 -688
  38. ml_dash-0.5.9/src/ml_dash/metric.py +0 -292
  39. ml_dash-0.5.9/src/ml_dash/storage.py +0 -1115
  40. {ml_dash-0.5.9 → ml_dash-0.6.3}/LICENSE +0 -0
  41. {ml_dash-0.5.9 → ml_dash-0.6.3}/src/ml_dash/cli_commands/__init__.py +0 -0
  42. {ml_dash-0.5.9 → ml_dash-0.6.3}/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.5.9
3
+ Version: 0.6.3
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
@@ -42,6 +42,10 @@ Requires-Dist: imageio>=2.31.0
42
42
  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
+ Requires-Dist: cryptography>=42.0.0
46
+ Requires-Dist: params-proto>=3.0.0
47
+ Requires-Dist: keyring>=25.0.0 ; extra == 'auth'
48
+ Requires-Dist: qrcode>=8.0.0 ; extra == 'auth'
45
49
  Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
46
50
  Requires-Dist: pytest-asyncio>=0.23.0 ; extra == 'dev'
47
51
  Requires-Dist: sphinx>=7.2.0 ; extra == 'dev'
@@ -58,17 +62,19 @@ Requires-Dist: linkify-it-py>=2.0.0 ; extra == 'dev'
58
62
  Requires-Dist: ruff>=0.3.0 ; extra == 'dev'
59
63
  Requires-Dist: mypy>=1.9.0 ; extra == 'dev'
60
64
  Requires-Python: >=3.9
65
+ Provides-Extra: auth
61
66
  Provides-Extra: dev
62
67
  Description-Content-Type: text/markdown
63
68
 
64
69
  # ML-Dash
65
70
 
66
- A simple and flexible SDK for ML experiment metricing and data storage.
71
+ A simple and flexible SDK for ML experiment tracking and data storage.
67
72
 
68
73
  ## Features
69
74
 
70
- - **Three Usage Styles**: Decorator, context manager, or direct instantiation
75
+ - **Three Usage Styles**: Pre-configured singleton (dxp), context manager, or direct instantiation
71
76
  - **Dual Operation Modes**: Remote (API server) or local (filesystem)
77
+ - **OAuth2 Authentication**: Secure device flow authentication for CLI and SDK
72
78
  - **Auto-creation**: Automatically creates namespace, project, and folder hierarchy
73
79
  - **Upsert Behavior**: Updates existing experiments or creates new ones
74
80
  - **Experiment Lifecycle**: Automatic status tracking (RUNNING, COMPLETED, FAILED, CANCELLED)
@@ -87,50 +93,73 @@ A simple and flexible SDK for ML experiment metricing and data storage.
87
93
  <td>
88
94
 
89
95
  ```bash
90
- uv add ml-dash
96
+ uv add ml-dash==0.6.2rc1
91
97
  ```
92
98
 
93
99
  </td>
94
100
  <td>
95
101
 
96
102
  ```bash
97
- pip install ml-dash
103
+ pip install ml-dash==0.6.2rc1
98
104
  ```
99
105
 
100
106
  </td>
101
107
  </tr>
102
108
  </table>
103
109
 
104
- ## Getting Started
110
+ ## Quick Start
105
111
 
106
- ### Remote Mode (with API Server)
112
+ ### 1. Authenticate (Required for Remote Mode)
113
+
114
+ ```bash
115
+ ml-dash login
116
+ ```
117
+
118
+ This opens your browser for secure OAuth2 authentication. Your credentials are stored securely in your system keychain.
119
+
120
+ ### 2. Start Tracking Experiments
121
+
122
+ #### Option A: Use the Pre-configured Singleton (Easiest)
123
+
124
+ ```python
125
+ from ml_dash.auto_start import dxp
126
+
127
+ # Start experiment (uploads to https://api.dash.ml by default)
128
+ with dxp.run:
129
+ dxp.log("Training started", level="info")
130
+ dxp.params.set(learning_rate=0.001, batch_size=32)
131
+
132
+ for epoch in range(10):
133
+ loss = train_one_epoch()
134
+ dxp.metrics("train").log(loss=loss, epoch=epoch)
135
+ ```
136
+
137
+ #### Option B: Create Your Own Experiment
107
138
 
108
139
  ```python
109
140
  from ml_dash import Experiment
110
141
 
111
142
  with Experiment(
112
- name="my-experiment",
113
- project="my-project",
114
- remote="https://api.dash.ml",
115
- api_key="your-jwt-token"
116
- ) as experiment:
117
- print(f"Experiment ID: {experiment.id}")
143
+ prefix="alice/my-project/my-experiment",
144
+ dash_url="https://api.dash.ml", # token auto-loaded
145
+ ).run as experiment:
146
+ experiment.log("Hello!", level="info")
147
+ experiment.params.set(lr=0.001)
118
148
  ```
119
149
 
120
- ### Local Mode (Filesystem)
150
+ #### Option C: Local Mode (No Authentication Required)
121
151
 
122
152
  ```python
123
153
  from ml_dash import Experiment
124
154
 
125
155
  with Experiment(
126
- name="my-experiment",
127
- project="my-project",
128
- local_path=".ml-dash"
129
- ) as experiment:
130
- pass # Your code here
156
+ project="my-project", prefix="my-experiment", dash_root=".dash"
157
+ ).run as experiment:
158
+ experiment.log("Running locally", level="info")
159
+
131
160
  ```
132
161
 
133
- See [examples/](examples/) for more complete examples.
162
+ See [docs/getting-started.md](docs/getting-started.md) for more examples.
134
163
 
135
164
  ## Development Setup
136
165
 
@@ -1,11 +1,12 @@
1
1
  # ML-Dash
2
2
 
3
- A simple and flexible SDK for ML experiment metricing and data storage.
3
+ A simple and flexible SDK for ML experiment tracking and data storage.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Three Usage Styles**: Decorator, context manager, or direct instantiation
7
+ - **Three Usage Styles**: Pre-configured singleton (dxp), context manager, or direct instantiation
8
8
  - **Dual Operation Modes**: Remote (API server) or local (filesystem)
9
+ - **OAuth2 Authentication**: Secure device flow authentication for CLI and SDK
9
10
  - **Auto-creation**: Automatically creates namespace, project, and folder hierarchy
10
11
  - **Upsert Behavior**: Updates existing experiments or creates new ones
11
12
  - **Experiment Lifecycle**: Automatic status tracking (RUNNING, COMPLETED, FAILED, CANCELLED)
@@ -24,50 +25,73 @@ A simple and flexible SDK for ML experiment metricing and data storage.
24
25
  <td>
25
26
 
26
27
  ```bash
27
- uv add ml-dash
28
+ uv add ml-dash==0.6.2rc1
28
29
  ```
29
30
 
30
31
  </td>
31
32
  <td>
32
33
 
33
34
  ```bash
34
- pip install ml-dash
35
+ pip install ml-dash==0.6.2rc1
35
36
  ```
36
37
 
37
38
  </td>
38
39
  </tr>
39
40
  </table>
40
41
 
41
- ## Getting Started
42
+ ## Quick Start
42
43
 
43
- ### Remote Mode (with API Server)
44
+ ### 1. Authenticate (Required for Remote Mode)
45
+
46
+ ```bash
47
+ ml-dash login
48
+ ```
49
+
50
+ This opens your browser for secure OAuth2 authentication. Your credentials are stored securely in your system keychain.
51
+
52
+ ### 2. Start Tracking Experiments
53
+
54
+ #### Option A: Use the Pre-configured Singleton (Easiest)
55
+
56
+ ```python
57
+ from ml_dash.auto_start import dxp
58
+
59
+ # Start experiment (uploads to https://api.dash.ml by default)
60
+ with dxp.run:
61
+ dxp.log("Training started", level="info")
62
+ dxp.params.set(learning_rate=0.001, batch_size=32)
63
+
64
+ for epoch in range(10):
65
+ loss = train_one_epoch()
66
+ dxp.metrics("train").log(loss=loss, epoch=epoch)
67
+ ```
68
+
69
+ #### Option B: Create Your Own Experiment
44
70
 
45
71
  ```python
46
72
  from ml_dash import Experiment
47
73
 
48
74
  with Experiment(
49
- name="my-experiment",
50
- project="my-project",
51
- remote="https://api.dash.ml",
52
- api_key="your-jwt-token"
53
- ) as experiment:
54
- print(f"Experiment ID: {experiment.id}")
75
+ prefix="alice/my-project/my-experiment",
76
+ dash_url="https://api.dash.ml", # token auto-loaded
77
+ ).run as experiment:
78
+ experiment.log("Hello!", level="info")
79
+ experiment.params.set(lr=0.001)
55
80
  ```
56
81
 
57
- ### Local Mode (Filesystem)
82
+ #### Option C: Local Mode (No Authentication Required)
58
83
 
59
84
  ```python
60
85
  from ml_dash import Experiment
61
86
 
62
87
  with Experiment(
63
- name="my-experiment",
64
- project="my-project",
65
- local_path=".ml-dash"
66
- ) as experiment:
67
- pass # Your code here
88
+ project="my-project", prefix="my-experiment", dash_root=".dash"
89
+ ).run as experiment:
90
+ experiment.log("Running locally", level="info")
91
+
68
92
  ```
69
93
 
70
- See [examples/](examples/) for more complete examples.
94
+ See [docs/getting-started.md](docs/getting-started.md) for more examples.
71
95
 
72
96
  ## Development Setup
73
97
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ml-dash"
3
- version = "0.5.9"
3
+ version = "0.6.3"
4
4
  description = "ML experiment tracking and data storage"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -30,12 +30,18 @@ dependencies = [
30
30
  "imageio-ffmpeg>=0.4.9",
31
31
  "scikit-image>=0.21.0",
32
32
  "rich>=13.0.0",
33
+ "cryptography>=42.0.0",
34
+ "params-proto>=3.0.0",
33
35
  ]
34
36
 
35
37
  [project.scripts]
36
38
  ml-dash = "ml_dash.cli:main"
37
39
 
38
40
  [project.optional-dependencies]
41
+ auth = [
42
+ "keyring>=25.0.0",
43
+ "qrcode>=8.0.0",
44
+ ]
39
45
  dev = [
40
46
  "pytest>=8.0.0",
41
47
  "pytest-asyncio>=0.23.0",
@@ -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.3"
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,51 @@
1
+ """Authentication module for ml-dash."""
2
+
3
+ from .constants import VUER_AUTH_URL, CLIENT_ID, DEFAULT_SCOPE
4
+ from .device_flow import DeviceFlowClient, DeviceFlowResponse
5
+ from .device_secret import (
6
+ generate_device_secret,
7
+ hash_device_secret,
8
+ get_or_create_device_secret,
9
+ )
10
+ from .token_storage import (
11
+ TokenStorage,
12
+ KeyringStorage,
13
+ EncryptedFileStorage,
14
+ PlaintextFileStorage,
15
+ get_token_storage,
16
+ )
17
+ from .exceptions import (
18
+ AuthenticationError,
19
+ NotAuthenticatedError,
20
+ DeviceCodeExpiredError,
21
+ AuthorizationDeniedError,
22
+ TokenExchangeError,
23
+ StorageError,
24
+ )
25
+
26
+ __all__ = [
27
+ # Constants
28
+ "VUER_AUTH_URL",
29
+ "CLIENT_ID",
30
+ "DEFAULT_SCOPE",
31
+ # Device Flow
32
+ "DeviceFlowClient",
33
+ "DeviceFlowResponse",
34
+ # Device Secret
35
+ "generate_device_secret",
36
+ "hash_device_secret",
37
+ "get_or_create_device_secret",
38
+ # Token Storage
39
+ "TokenStorage",
40
+ "KeyringStorage",
41
+ "EncryptedFileStorage",
42
+ "PlaintextFileStorage",
43
+ "get_token_storage",
44
+ # Exceptions
45
+ "AuthenticationError",
46
+ "NotAuthenticatedError",
47
+ "DeviceCodeExpiredError",
48
+ "AuthorizationDeniedError",
49
+ "TokenExchangeError",
50
+ "StorageError",
51
+ ]
@@ -0,0 +1,10 @@
1
+ """Authentication constants for ml-dash."""
2
+
3
+ # Vuer-auth server URL
4
+ VUER_AUTH_URL = "https://auth.vuer.ai"
5
+
6
+ # OAuth client ID for ml-dash
7
+ CLIENT_ID = "ml-dash-client"
8
+
9
+ # Default OAuth scopes (no offline_access since we get permanent tokens)
10
+ DEFAULT_SCOPE = "openid profile email"
@@ -0,0 +1,237 @@
1
+ """Device authorization flow (RFC 8628) client for ml-dash."""
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Optional
6
+
7
+ import httpx
8
+
9
+ from .constants import VUER_AUTH_URL, CLIENT_ID, DEFAULT_SCOPE
10
+ from .device_secret import hash_device_secret
11
+ from .exceptions import (
12
+ DeviceCodeExpiredError,
13
+ AuthorizationDeniedError,
14
+ TokenExchangeError,
15
+ )
16
+
17
+
18
+ @dataclass
19
+ class DeviceFlowResponse:
20
+ """Response from device flow initiation."""
21
+
22
+ user_code: str
23
+ device_code: str
24
+ verification_uri: str
25
+ verification_uri_complete: str
26
+ expires_in: int
27
+ interval: int
28
+
29
+
30
+ class DeviceFlowClient:
31
+ """Client for OAuth 2.0 Device Authorization Flow (RFC 8628)."""
32
+
33
+ def __init__(self, device_secret: str, ml_dash_server_url: str):
34
+ """Initialize device flow client.
35
+
36
+ Args:
37
+ device_secret: Persistent device secret for this client
38
+ ml_dash_server_url: ML-Dash server URL for token exchange
39
+ """
40
+ self.device_secret = device_secret
41
+ self.ml_dash_server_url = ml_dash_server_url.rstrip("/")
42
+
43
+ def start_device_flow(self, scope: str = DEFAULT_SCOPE) -> DeviceFlowResponse:
44
+ """Initiate device authorization flow with vuer-auth.
45
+
46
+ Args:
47
+ scope: OAuth scopes to request
48
+
49
+ Returns:
50
+ DeviceFlowResponse with user code and verification URI
51
+
52
+ Raises:
53
+ httpx.HTTPError: If request fails
54
+ """
55
+ response = httpx.post(
56
+ f"{VUER_AUTH_URL}/api/device-flow/start",
57
+ json={
58
+ "client_id": CLIENT_ID,
59
+ "scope": scope,
60
+ "device_secret_hash": hash_device_secret(self.device_secret),
61
+ },
62
+ timeout=10.0,
63
+ )
64
+ response.raise_for_status()
65
+ data = response.json()
66
+
67
+ return DeviceFlowResponse(
68
+ user_code=data["user_code"],
69
+ device_code=data.get("device_code", ""),
70
+ verification_uri=data["verification_uri"],
71
+ verification_uri_complete=data.get(
72
+ "verification_uri_complete",
73
+ f"{data['verification_uri']}?code={data['user_code'].replace('-', '')}"
74
+ ),
75
+ expires_in=data.get("expires_in", 600),
76
+ interval=data.get("interval", 5),
77
+ )
78
+
79
+ def poll_for_token(
80
+ self,
81
+ max_attempts: int = 120,
82
+ progress_callback: Optional[callable] = None,
83
+ ) -> str:
84
+ """Poll vuer-auth for authorization completion.
85
+
86
+ Args:
87
+ max_attempts: Maximum polling attempts (default: 120 = 10 minutes at 5s intervals)
88
+ progress_callback: Optional callback(elapsed_seconds) for progress updates
89
+
90
+ Returns:
91
+ Vuer-auth access token (JWT)
92
+
93
+ Raises:
94
+ DeviceCodeExpiredError: If device code expires
95
+ AuthorizationDeniedError: If user denies authorization
96
+ TimeoutError: If polling times out
97
+ """
98
+ device_secret_hash = hash_device_secret(self.device_secret)
99
+
100
+ for attempt in range(max_attempts):
101
+ elapsed = attempt * 5 # 5 second intervals
102
+
103
+ if progress_callback:
104
+ progress_callback(elapsed)
105
+
106
+ try:
107
+ response = httpx.post(
108
+ f"{VUER_AUTH_URL}/api/device-flow/poll",
109
+ json={
110
+ "client_id": CLIENT_ID,
111
+ "device_secret_hash": device_secret_hash,
112
+ },
113
+ timeout=10.0,
114
+ )
115
+
116
+ if response.status_code == 200:
117
+ # Authorization successful
118
+ data = response.json()
119
+ return data["access_token"]
120
+
121
+ # Check error responses
122
+ error_data = response.json()
123
+ error = error_data.get("error")
124
+
125
+ if error == "authorization_pending":
126
+ # Still waiting for user authorization
127
+ time.sleep(5)
128
+ continue
129
+ elif error == "expired_token":
130
+ raise DeviceCodeExpiredError(
131
+ "Device code expired. Please run 'ml-dash login' again."
132
+ )
133
+ elif error == "access_denied":
134
+ raise AuthorizationDeniedError(
135
+ "User denied authorization request."
136
+ )
137
+ elif error == "slow_down":
138
+ # Server requests slower polling
139
+ time.sleep(10)
140
+ continue
141
+ else:
142
+ # Unknown error
143
+ raise TokenExchangeError(f"Device flow error: {error}")
144
+
145
+ except httpx.HTTPError as e:
146
+ # Network error, retry
147
+ time.sleep(5)
148
+ continue
149
+
150
+ raise TimeoutError(
151
+ "Authorization timed out after 10 minutes. Please run 'ml-dash login' again."
152
+ )
153
+
154
+ def exchange_token(self, vuer_auth_token: str) -> str:
155
+ """Exchange vuer-auth token for ml-dash permanent token.
156
+
157
+ This calls the ml-dash server's token exchange endpoint.
158
+ The server will:
159
+ 1. Decode the vuer-auth JWT
160
+ 2. Validate signature and expiry
161
+ 3. Extract username from claims
162
+ 4. Generate a permanent ml-dash token for that username
163
+ 5. Return the ml-dash token
164
+
165
+ Args:
166
+ vuer_auth_token: Temporary vuer-auth JWT access token
167
+
168
+ Returns:
169
+ Permanent ml-dash token string
170
+
171
+ Raises:
172
+ TokenExchangeError: If exchange fails
173
+ httpx.HTTPError: If request fails
174
+ """
175
+ try:
176
+ response = httpx.post(
177
+ f"{self.ml_dash_server_url}/api/auth/exchange",
178
+ headers={"Authorization": f"Bearer {vuer_auth_token}"},
179
+ timeout=10.0,
180
+ )
181
+ response.raise_for_status()
182
+ data = response.json()
183
+
184
+ ml_dash_token = data.get("ml_dash_token")
185
+ if not ml_dash_token:
186
+ raise TokenExchangeError(
187
+ "Server response missing ml_dash_token field"
188
+ )
189
+
190
+ return ml_dash_token
191
+
192
+ except httpx.HTTPStatusError as e:
193
+ if e.response.status_code == 401:
194
+ raise TokenExchangeError(
195
+ "Vuer-auth token invalid or expired. Please try logging in again."
196
+ )
197
+ elif e.response.status_code == 404:
198
+ raise TokenExchangeError(
199
+ "Token exchange endpoint not found. "
200
+ "Please ensure ml-dash server is up to date."
201
+ )
202
+ else:
203
+ raise TokenExchangeError(
204
+ f"Token exchange failed: {e.response.status_code} {e.response.text}"
205
+ )
206
+ except httpx.HTTPError as e:
207
+ raise TokenExchangeError(f"Network error during token exchange: {e}")
208
+
209
+ def authenticate(
210
+ self, progress_callback: Optional[callable] = None
211
+ ) -> str:
212
+ """Complete full device authorization flow.
213
+
214
+ This is a convenience method that:
215
+ 1. Starts device flow with vuer-auth
216
+ 2. Polls for authorization
217
+ 3. Exchanges vuer-auth token for ml-dash token
218
+
219
+ Args:
220
+ progress_callback: Optional callback(elapsed_seconds) for progress updates
221
+
222
+ Returns:
223
+ Permanent ml-dash token string
224
+
225
+ Raises:
226
+ Various authentication exceptions
227
+ """
228
+ # Step 1: Start device flow
229
+ flow = self.start_device_flow()
230
+
231
+ # Step 2: Poll for authorization (caller should display user_code)
232
+ vuer_auth_token = self.poll_for_token(progress_callback=progress_callback)
233
+
234
+ # Step 3: Exchange for ml-dash token
235
+ ml_dash_token = self.exchange_token(vuer_auth_token)
236
+
237
+ return ml_dash_token
@@ -0,0 +1,49 @@
1
+ """Device secret generation and management for ml-dash."""
2
+
3
+ import hashlib
4
+ import secrets
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from ml_dash.config import Config
9
+
10
+
11
+ def generate_device_secret() -> str:
12
+ """Generate a cryptographically secure device secret.
13
+
14
+ Returns:
15
+ A 64-character hexadecimal string (256 bits of entropy)
16
+ """
17
+ return secrets.token_hex(32) # 32 bytes = 256 bits
18
+
19
+
20
+ def hash_device_secret(secret: str) -> str:
21
+ """Hash device secret using SHA256.
22
+
23
+ Args:
24
+ secret: The device secret to hash
25
+
26
+ Returns:
27
+ SHA256 hash as hexadecimal string
28
+ """
29
+ return hashlib.sha256(secret.encode()).hexdigest()
30
+
31
+
32
+ def get_or_create_device_secret(config: "Config") -> str:
33
+ """Load device secret from config or generate a new one.
34
+
35
+ Args:
36
+ config: ML-Dash configuration object
37
+
38
+ Returns:
39
+ Device secret string
40
+ """
41
+ device_secret = config.device_secret
42
+
43
+ if not device_secret:
44
+ # Generate new device secret
45
+ device_secret = generate_device_secret()
46
+ config.set("device_secret", device_secret)
47
+ config.save()
48
+
49
+ return device_secret