ml-dash 0.5.0__py3-none-any.whl → 0.5.2__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 +2 -1
- ml_dash/auto_start.py +42 -0
- ml_dash/client.py +33 -0
- ml_dash/experiment.py +163 -59
- ml_dash/files.py +123 -3
- ml_dash/metric.py +93 -0
- ml_dash/params.py +2 -2
- ml_dash/storage.py +24 -5
- {ml_dash-0.5.0.dist-info → ml_dash-0.5.2.dist-info}/METADATA +8 -5
- ml_dash-0.5.2.dist-info/RECORD +13 -0
- ml_dash-0.5.0.dist-info/RECORD +0 -12
- {ml_dash-0.5.0.dist-info → ml_dash-0.5.2.dist-info}/WHEEL +0 -0
ml_dash/__init__.py
CHANGED
|
@@ -38,7 +38,7 @@ Usage:
|
|
|
38
38
|
experiment.log("Training started")
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
|
-
from .experiment import Experiment, ml_dash_experiment, OperationMode
|
|
41
|
+
from .experiment import Experiment, ml_dash_experiment, OperationMode, RunManager
|
|
42
42
|
from .client import RemoteClient
|
|
43
43
|
from .storage import LocalStorage
|
|
44
44
|
from .log import LogLevel, LogBuilder
|
|
@@ -50,6 +50,7 @@ __all__ = [
|
|
|
50
50
|
"Experiment",
|
|
51
51
|
"ml_dash_experiment",
|
|
52
52
|
"OperationMode",
|
|
53
|
+
"RunManager",
|
|
53
54
|
"RemoteClient",
|
|
54
55
|
"LocalStorage",
|
|
55
56
|
"LogLevel",
|
ml_dash/auto_start.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-start module for ML-Dash SDK.
|
|
3
|
+
|
|
4
|
+
Provides a pre-configured, auto-started experiment singleton named 'dxp'.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from ml_dash.auto_start import dxp
|
|
8
|
+
|
|
9
|
+
# Ready to use immediately - no need to open/start
|
|
10
|
+
dxp.log("Hello from dxp!")
|
|
11
|
+
dxp.params.set(lr=0.001)
|
|
12
|
+
dxp.metrics("loss").append(step=0, value=0.5)
|
|
13
|
+
|
|
14
|
+
# Automatically closed on Python exit
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import atexit
|
|
18
|
+
from .experiment import Experiment
|
|
19
|
+
|
|
20
|
+
# Create pre-configured singleton experiment
|
|
21
|
+
dxp = Experiment(
|
|
22
|
+
name="dxp",
|
|
23
|
+
project="scratch",
|
|
24
|
+
local_path=".ml-dash"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Auto-start the experiment on import
|
|
28
|
+
dxp.run.start()
|
|
29
|
+
|
|
30
|
+
# Register cleanup handler to complete experiment on Python exit
|
|
31
|
+
def _cleanup():
|
|
32
|
+
"""Complete the dxp experiment on exit if still open."""
|
|
33
|
+
if dxp._is_open:
|
|
34
|
+
try:
|
|
35
|
+
dxp.run.complete()
|
|
36
|
+
except Exception:
|
|
37
|
+
# Silently ignore errors during cleanup
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
atexit.register(_cleanup)
|
|
41
|
+
|
|
42
|
+
__all__ = ["dxp"]
|
ml_dash/client.py
CHANGED
|
@@ -35,6 +35,7 @@ class RemoteClient:
|
|
|
35
35
|
name: str,
|
|
36
36
|
description: Optional[str] = None,
|
|
37
37
|
tags: Optional[List[str]] = None,
|
|
38
|
+
bindrs: Optional[List[str]] = None,
|
|
38
39
|
folder: Optional[str] = None,
|
|
39
40
|
write_protected: bool = False,
|
|
40
41
|
metadata: Optional[Dict[str, Any]] = None,
|
|
@@ -47,6 +48,7 @@ class RemoteClient:
|
|
|
47
48
|
name: Experiment name
|
|
48
49
|
description: Optional description
|
|
49
50
|
tags: Optional list of tags
|
|
51
|
+
bindrs: Optional list of bindrs
|
|
50
52
|
folder: Optional folder path
|
|
51
53
|
write_protected: If True, experiment becomes immutable
|
|
52
54
|
metadata: Optional metadata dict
|
|
@@ -65,6 +67,8 @@ class RemoteClient:
|
|
|
65
67
|
payload["description"] = description
|
|
66
68
|
if tags is not None:
|
|
67
69
|
payload["tags"] = tags
|
|
70
|
+
if bindrs is not None:
|
|
71
|
+
payload["bindrs"] = bindrs
|
|
68
72
|
if folder is not None:
|
|
69
73
|
payload["folder"] = folder
|
|
70
74
|
if write_protected:
|
|
@@ -79,6 +83,35 @@ class RemoteClient:
|
|
|
79
83
|
response.raise_for_status()
|
|
80
84
|
return response.json()
|
|
81
85
|
|
|
86
|
+
def update_experiment_status(
|
|
87
|
+
self,
|
|
88
|
+
experiment_id: str,
|
|
89
|
+
status: str,
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Update experiment status.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
experiment_id: Experiment ID
|
|
96
|
+
status: Status value - "RUNNING" | "COMPLETED" | "FAILED" | "CANCELLED"
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Response dict with updated experiment data
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
httpx.HTTPStatusError: If request fails
|
|
103
|
+
"""
|
|
104
|
+
payload = {
|
|
105
|
+
"status": status,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
response = self._client.patch(
|
|
109
|
+
f"/experiments/{experiment_id}/status",
|
|
110
|
+
json=payload,
|
|
111
|
+
)
|
|
112
|
+
response.raise_for_status()
|
|
113
|
+
return response.json()
|
|
114
|
+
|
|
82
115
|
def create_log_entries(
|
|
83
116
|
self,
|
|
84
117
|
experiment_id: str,
|
ml_dash/experiment.py
CHANGED
|
@@ -27,6 +27,74 @@ class OperationMode(Enum):
|
|
|
27
27
|
HYBRID = "hybrid" # Future: sync local to remote
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class RunManager:
|
|
31
|
+
"""
|
|
32
|
+
Lifecycle manager for experiments.
|
|
33
|
+
|
|
34
|
+
Supports three usage patterns:
|
|
35
|
+
1. Method calls: experiment.run.start(), experiment.run.complete()
|
|
36
|
+
2. Context manager: with Experiment(...).run as exp:
|
|
37
|
+
3. Decorator: @exp.run or @Experiment(...).run
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, experiment: "Experiment"):
|
|
41
|
+
"""
|
|
42
|
+
Initialize RunManager.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
experiment: Parent Experiment instance
|
|
46
|
+
"""
|
|
47
|
+
self._experiment = experiment
|
|
48
|
+
|
|
49
|
+
def start(self) -> "Experiment":
|
|
50
|
+
"""
|
|
51
|
+
Start the experiment (sets status to RUNNING).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The experiment instance for chaining
|
|
55
|
+
"""
|
|
56
|
+
return self._experiment._open()
|
|
57
|
+
|
|
58
|
+
def complete(self) -> None:
|
|
59
|
+
"""Mark experiment as completed (status: COMPLETED)."""
|
|
60
|
+
self._experiment._close(status="COMPLETED")
|
|
61
|
+
|
|
62
|
+
def fail(self) -> None:
|
|
63
|
+
"""Mark experiment as failed (status: FAILED)."""
|
|
64
|
+
self._experiment._close(status="FAILED")
|
|
65
|
+
|
|
66
|
+
def cancel(self) -> None:
|
|
67
|
+
"""Mark experiment as cancelled (status: CANCELLED)."""
|
|
68
|
+
self._experiment._close(status="CANCELLED")
|
|
69
|
+
|
|
70
|
+
def __enter__(self) -> "Experiment":
|
|
71
|
+
"""Context manager entry - starts the experiment."""
|
|
72
|
+
return self.start()
|
|
73
|
+
|
|
74
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
75
|
+
"""Context manager exit - completes or fails the experiment."""
|
|
76
|
+
if exc_type is not None:
|
|
77
|
+
self.fail()
|
|
78
|
+
else:
|
|
79
|
+
self.complete()
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def __call__(self, func: Callable) -> Callable:
|
|
83
|
+
"""
|
|
84
|
+
Decorator support for wrapping functions with experiment lifecycle.
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
@exp.run
|
|
88
|
+
def train(exp):
|
|
89
|
+
exp.log("Training...")
|
|
90
|
+
"""
|
|
91
|
+
@functools.wraps(func)
|
|
92
|
+
def wrapper(*args, **kwargs):
|
|
93
|
+
with self as exp:
|
|
94
|
+
return func(exp, *args, **kwargs)
|
|
95
|
+
return wrapper
|
|
96
|
+
|
|
97
|
+
|
|
30
98
|
class Experiment:
|
|
31
99
|
"""
|
|
32
100
|
ML-Dash experiment for metricing experiments.
|
|
@@ -65,14 +133,16 @@ class Experiment:
|
|
|
65
133
|
*,
|
|
66
134
|
description: Optional[str] = None,
|
|
67
135
|
tags: Optional[List[str]] = None,
|
|
136
|
+
bindrs: Optional[List[str]] = None,
|
|
68
137
|
folder: Optional[str] = None,
|
|
69
|
-
write_protected: bool = False,
|
|
70
138
|
metadata: Optional[Dict[str, Any]] = None,
|
|
71
139
|
# Mode configuration
|
|
72
140
|
remote: Optional[str] = None,
|
|
73
141
|
api_key: Optional[str] = None,
|
|
74
142
|
user_name: Optional[str] = None,
|
|
75
143
|
local_path: Optional[str] = None,
|
|
144
|
+
# Internal parameters
|
|
145
|
+
_write_protected: bool = False,
|
|
76
146
|
):
|
|
77
147
|
"""
|
|
78
148
|
Initialize an ML-Dash experiment.
|
|
@@ -82,20 +152,22 @@ class Experiment:
|
|
|
82
152
|
project: Project name
|
|
83
153
|
description: Optional experiment description
|
|
84
154
|
tags: Optional list of tags
|
|
155
|
+
bindrs: Optional list of bindrs
|
|
85
156
|
folder: Optional folder path (e.g., "/experiments/baseline")
|
|
86
|
-
write_protected: If True, experiment becomes immutable after creation
|
|
87
157
|
metadata: Optional metadata dict
|
|
88
158
|
remote: Remote API URL (e.g., "http://localhost:3000")
|
|
89
159
|
api_key: JWT token for authentication (if not provided, will be generated from user_name)
|
|
90
160
|
user_name: Username for authentication (generates API key if api_key not provided)
|
|
91
161
|
local_path: Local storage root path (for local mode)
|
|
162
|
+
_write_protected: Internal parameter - if True, experiment becomes immutable after creation
|
|
92
163
|
"""
|
|
93
164
|
self.name = name
|
|
94
165
|
self.project = project
|
|
95
166
|
self.description = description
|
|
96
167
|
self.tags = tags
|
|
168
|
+
self.bindrs = bindrs
|
|
97
169
|
self.folder = folder
|
|
98
|
-
self.
|
|
170
|
+
self._write_protected = _write_protected
|
|
99
171
|
self.metadata = metadata
|
|
100
172
|
|
|
101
173
|
# Generate API key from username if not provided
|
|
@@ -168,9 +240,9 @@ class Experiment:
|
|
|
168
240
|
|
|
169
241
|
return token
|
|
170
242
|
|
|
171
|
-
def
|
|
243
|
+
def _open(self) -> "Experiment":
|
|
172
244
|
"""
|
|
173
|
-
|
|
245
|
+
Internal method to open the experiment (create or update on server/filesystem).
|
|
174
246
|
|
|
175
247
|
Returns:
|
|
176
248
|
self for chaining
|
|
@@ -185,8 +257,9 @@ class Experiment:
|
|
|
185
257
|
name=self.name,
|
|
186
258
|
description=self.description,
|
|
187
259
|
tags=self.tags,
|
|
260
|
+
bindrs=self.bindrs,
|
|
188
261
|
folder=self.folder,
|
|
189
|
-
write_protected=self.
|
|
262
|
+
write_protected=self._write_protected,
|
|
190
263
|
metadata=self.metadata,
|
|
191
264
|
)
|
|
192
265
|
self._experiment_data = response
|
|
@@ -199,6 +272,7 @@ class Experiment:
|
|
|
199
272
|
name=self.name,
|
|
200
273
|
description=self.description,
|
|
201
274
|
tags=self.tags,
|
|
275
|
+
bindrs=self.bindrs,
|
|
202
276
|
folder=self.folder,
|
|
203
277
|
metadata=self.metadata,
|
|
204
278
|
)
|
|
@@ -206,8 +280,13 @@ class Experiment:
|
|
|
206
280
|
self._is_open = True
|
|
207
281
|
return self
|
|
208
282
|
|
|
209
|
-
def
|
|
210
|
-
"""
|
|
283
|
+
def _close(self, status: str = "COMPLETED"):
|
|
284
|
+
"""
|
|
285
|
+
Internal method to close the experiment and update status.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
status: Status to set - "COMPLETED" (default), "FAILED", or "CANCELLED"
|
|
289
|
+
"""
|
|
211
290
|
if not self._is_open:
|
|
212
291
|
return
|
|
213
292
|
|
|
@@ -215,16 +294,65 @@ class Experiment:
|
|
|
215
294
|
if self._storage:
|
|
216
295
|
self._storage.flush()
|
|
217
296
|
|
|
297
|
+
# Update experiment status in remote mode
|
|
298
|
+
if self._client and self._experiment_id:
|
|
299
|
+
try:
|
|
300
|
+
self._client.update_experiment_status(
|
|
301
|
+
experiment_id=self._experiment_id,
|
|
302
|
+
status=status
|
|
303
|
+
)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
# Log error but don't fail the close operation
|
|
306
|
+
print(f"Warning: Failed to update experiment status: {e}")
|
|
307
|
+
|
|
218
308
|
self._is_open = False
|
|
219
309
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
310
|
+
@property
|
|
311
|
+
def run(self) -> RunManager:
|
|
312
|
+
"""
|
|
313
|
+
Get the RunManager for lifecycle operations.
|
|
223
314
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
315
|
+
Usage:
|
|
316
|
+
# Method calls
|
|
317
|
+
experiment.run.start()
|
|
318
|
+
experiment.run.complete()
|
|
319
|
+
|
|
320
|
+
# Context manager
|
|
321
|
+
with Experiment(...).run as exp:
|
|
322
|
+
exp.log("Training...")
|
|
323
|
+
|
|
324
|
+
# Decorator
|
|
325
|
+
@experiment.run
|
|
326
|
+
def train(exp):
|
|
327
|
+
exp.log("Training...")
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
RunManager instance
|
|
331
|
+
"""
|
|
332
|
+
return RunManager(self)
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def params(self) -> ParametersBuilder:
|
|
336
|
+
"""
|
|
337
|
+
Get a ParametersBuilder for parameter operations.
|
|
338
|
+
|
|
339
|
+
Usage:
|
|
340
|
+
# Set parameters
|
|
341
|
+
experiment.params.set(lr=0.001, batch_size=32)
|
|
342
|
+
|
|
343
|
+
# Get parameters
|
|
344
|
+
params = experiment.params.get()
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
ParametersBuilder instance
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
RuntimeError: If experiment is not open
|
|
351
|
+
"""
|
|
352
|
+
if not self._is_open:
|
|
353
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
354
|
+
|
|
355
|
+
return ParametersBuilder(self)
|
|
228
356
|
|
|
229
357
|
def log(
|
|
230
358
|
self,
|
|
@@ -568,31 +696,6 @@ class Experiment:
|
|
|
568
696
|
|
|
569
697
|
return result
|
|
570
698
|
|
|
571
|
-
def parameters(self) -> ParametersBuilder:
|
|
572
|
-
"""
|
|
573
|
-
Get a ParametersBuilder for fluent parameter operations.
|
|
574
|
-
|
|
575
|
-
Returns:
|
|
576
|
-
ParametersBuilder instance for chaining
|
|
577
|
-
|
|
578
|
-
Raises:
|
|
579
|
-
RuntimeError: If experiment is not open
|
|
580
|
-
|
|
581
|
-
Examples:
|
|
582
|
-
# Set parameters
|
|
583
|
-
experiment.parameters().set(
|
|
584
|
-
model={"lr": 0.001, "batch_size": 32},
|
|
585
|
-
optimizer="adam"
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
# Get parameters
|
|
589
|
-
params = experiment.parameters().get() # Flattened
|
|
590
|
-
params = experiment.parameters().get(flatten=False) # Nested
|
|
591
|
-
"""
|
|
592
|
-
if not self._is_open:
|
|
593
|
-
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
594
|
-
|
|
595
|
-
return ParametersBuilder(self)
|
|
596
699
|
|
|
597
700
|
def _write_params(self, flattened_params: Dict[str, Any]) -> None:
|
|
598
701
|
"""
|
|
@@ -642,48 +745,49 @@ class Experiment:
|
|
|
642
745
|
|
|
643
746
|
return params
|
|
644
747
|
|
|
645
|
-
|
|
646
|
-
|
|
748
|
+
@property
|
|
749
|
+
def metrics(self) -> 'MetricsManager':
|
|
647
750
|
"""
|
|
648
|
-
Get a
|
|
751
|
+
Get a MetricsManager for metric operations.
|
|
649
752
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
tags: Optional tags for categorization
|
|
654
|
-
metadata: Optional structured metadata
|
|
753
|
+
Supports two usage patterns:
|
|
754
|
+
1. Named: experiment.metrics("loss").append(value=0.5, step=1)
|
|
755
|
+
2. Unnamed: experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
655
756
|
|
|
656
757
|
Returns:
|
|
657
|
-
|
|
758
|
+
MetricsManager instance
|
|
658
759
|
|
|
659
760
|
Raises:
|
|
660
761
|
RuntimeError: If experiment is not open
|
|
661
762
|
|
|
662
763
|
Examples:
|
|
663
|
-
#
|
|
664
|
-
experiment.
|
|
764
|
+
# Named metric
|
|
765
|
+
experiment.metrics("train_loss").append(value=0.5, step=100)
|
|
766
|
+
|
|
767
|
+
# Unnamed (name in append call)
|
|
768
|
+
experiment.metrics.append(name="train_loss", value=0.5, step=100)
|
|
665
769
|
|
|
666
770
|
# Append batch
|
|
667
|
-
experiment.
|
|
771
|
+
experiment.metrics("metrics").append_batch([
|
|
668
772
|
{"loss": 0.5, "acc": 0.8, "step": 1},
|
|
669
773
|
{"loss": 0.4, "acc": 0.85, "step": 2}
|
|
670
774
|
])
|
|
671
775
|
|
|
672
776
|
# Read data
|
|
673
|
-
data = experiment.
|
|
777
|
+
data = experiment.metrics("train_loss").read(start_index=0, limit=100)
|
|
674
778
|
|
|
675
779
|
# Get statistics
|
|
676
|
-
stats = experiment.
|
|
780
|
+
stats = experiment.metrics("train_loss").stats()
|
|
677
781
|
"""
|
|
678
|
-
from .metric import
|
|
782
|
+
from .metric import MetricsManager
|
|
679
783
|
|
|
680
784
|
if not self._is_open:
|
|
681
785
|
raise RuntimeError(
|
|
682
|
-
"Cannot use
|
|
683
|
-
"Use 'with Experiment(...) as experiment:' or call experiment.
|
|
786
|
+
"Cannot use metrics on closed experiment. "
|
|
787
|
+
"Use 'with Experiment(...).run as experiment:' or call experiment.run.start() first."
|
|
684
788
|
)
|
|
685
789
|
|
|
686
|
-
return
|
|
790
|
+
return MetricsManager(self)
|
|
687
791
|
|
|
688
792
|
def _append_to_metric(
|
|
689
793
|
self,
|
|
@@ -908,7 +1012,7 @@ def ml_dash_experiment(
|
|
|
908
1012
|
def decorator(func: Callable) -> Callable:
|
|
909
1013
|
@functools.wraps(func)
|
|
910
1014
|
def wrapper(*args, **func_kwargs):
|
|
911
|
-
with Experiment(name=name, project=project, **kwargs) as experiment:
|
|
1015
|
+
with Experiment(name=name, project=project, **kwargs).run as experiment:
|
|
912
1016
|
# Inject experiment into function kwargs
|
|
913
1017
|
func_kwargs['experiment'] = experiment
|
|
914
1018
|
return func(*args, **func_kwargs)
|
ml_dash/files.py
CHANGED
|
@@ -44,6 +44,7 @@ class FileBuilder:
|
|
|
44
44
|
- prefix: Logical path prefix (default: "/")
|
|
45
45
|
- description: Optional description
|
|
46
46
|
- tags: Optional list of tags
|
|
47
|
+
- bindrs: Optional list of bindrs
|
|
47
48
|
- metadata: Optional metadata dict
|
|
48
49
|
- file_id: File ID for download/delete/update operations
|
|
49
50
|
- dest_path: Destination path for download
|
|
@@ -53,6 +54,7 @@ class FileBuilder:
|
|
|
53
54
|
self._prefix = kwargs.get('prefix', '/')
|
|
54
55
|
self._description = kwargs.get('description')
|
|
55
56
|
self._tags = kwargs.get('tags', [])
|
|
57
|
+
self._bindrs = kwargs.get('bindrs', [])
|
|
56
58
|
self._metadata = kwargs.get('metadata')
|
|
57
59
|
self._file_id = kwargs.get('file_id')
|
|
58
60
|
self._dest_path = kwargs.get('dest_path')
|
|
@@ -76,7 +78,7 @@ class FileBuilder:
|
|
|
76
78
|
if not self._experiment._is_open:
|
|
77
79
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
78
80
|
|
|
79
|
-
if self._experiment.
|
|
81
|
+
if self._experiment._write_protected:
|
|
80
82
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
81
83
|
|
|
82
84
|
if not self._file_path:
|
|
@@ -189,7 +191,7 @@ class FileBuilder:
|
|
|
189
191
|
if not self._experiment._is_open:
|
|
190
192
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
191
193
|
|
|
192
|
-
if self._experiment.
|
|
194
|
+
if self._experiment._write_protected:
|
|
193
195
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
194
196
|
|
|
195
197
|
if not self._file_id:
|
|
@@ -219,7 +221,7 @@ class FileBuilder:
|
|
|
219
221
|
if not self._experiment._is_open:
|
|
220
222
|
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
221
223
|
|
|
222
|
-
if self._experiment.
|
|
224
|
+
if self._experiment._write_protected:
|
|
223
225
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
224
226
|
|
|
225
227
|
if not self._file_id:
|
|
@@ -232,6 +234,124 @@ class FileBuilder:
|
|
|
232
234
|
metadata=self._metadata
|
|
233
235
|
)
|
|
234
236
|
|
|
237
|
+
def save_json(self, content: Any, file_name: str) -> Dict[str, Any]:
|
|
238
|
+
"""
|
|
239
|
+
Save JSON content to a file.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
content: Content to save as JSON (dict, list, or any JSON-serializable object)
|
|
243
|
+
file_name: Name of the file to create
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
RuntimeError: If experiment is not open or write-protected
|
|
250
|
+
ValueError: If content is not JSON-serializable
|
|
251
|
+
|
|
252
|
+
Examples:
|
|
253
|
+
config = {"model": "resnet50", "lr": 0.001}
|
|
254
|
+
result = experiment.file(prefix="/configs").save_json(config, "config.json")
|
|
255
|
+
"""
|
|
256
|
+
import json
|
|
257
|
+
import tempfile
|
|
258
|
+
import os
|
|
259
|
+
|
|
260
|
+
if not self._experiment._is_open:
|
|
261
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
262
|
+
|
|
263
|
+
if self._experiment._write_protected:
|
|
264
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
265
|
+
|
|
266
|
+
# Create temporary file
|
|
267
|
+
temp_fd, temp_path = tempfile.mkstemp(suffix='.json', text=True)
|
|
268
|
+
try:
|
|
269
|
+
# Write JSON content to temp file
|
|
270
|
+
with os.fdopen(temp_fd, 'w') as f:
|
|
271
|
+
json.dump(content, f, indent=2)
|
|
272
|
+
|
|
273
|
+
# Save using existing save() method
|
|
274
|
+
original_file_path = self._file_path
|
|
275
|
+
self._file_path = temp_path
|
|
276
|
+
|
|
277
|
+
# Upload and get result
|
|
278
|
+
result = self.save()
|
|
279
|
+
|
|
280
|
+
# Restore original file_path
|
|
281
|
+
self._file_path = original_file_path
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
finally:
|
|
285
|
+
# Clean up temp file
|
|
286
|
+
try:
|
|
287
|
+
os.unlink(temp_path)
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
def save_torch(self, model: Any, file_name: str) -> Dict[str, Any]:
|
|
292
|
+
"""
|
|
293
|
+
Save PyTorch model to a file.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
model: PyTorch model or state dict to save
|
|
297
|
+
file_name: Name of the file to create (should end with .pt or .pth)
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
RuntimeError: If experiment is not open or write-protected
|
|
304
|
+
ImportError: If torch is not installed
|
|
305
|
+
ValueError: If model cannot be saved
|
|
306
|
+
|
|
307
|
+
Examples:
|
|
308
|
+
import torch
|
|
309
|
+
model = torch.nn.Linear(10, 5)
|
|
310
|
+
result = experiment.file(prefix="/models").save_torch(model, "model.pt")
|
|
311
|
+
|
|
312
|
+
# Or save state dict
|
|
313
|
+
result = experiment.file(prefix="/models").save_torch(model.state_dict(), "model.pth")
|
|
314
|
+
"""
|
|
315
|
+
import tempfile
|
|
316
|
+
import os
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
import torch
|
|
320
|
+
except ImportError:
|
|
321
|
+
raise ImportError("PyTorch is not installed. Install it with: pip install torch")
|
|
322
|
+
|
|
323
|
+
if not self._experiment._is_open:
|
|
324
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
325
|
+
|
|
326
|
+
if self._experiment._write_protected:
|
|
327
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
328
|
+
|
|
329
|
+
# Create temporary file
|
|
330
|
+
temp_fd, temp_path = tempfile.mkstemp(suffix='.pt')
|
|
331
|
+
os.close(temp_fd) # Close the file descriptor
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
# Save model to temp file
|
|
335
|
+
torch.save(model, temp_path)
|
|
336
|
+
|
|
337
|
+
# Save using existing save() method
|
|
338
|
+
original_file_path = self._file_path
|
|
339
|
+
self._file_path = temp_path
|
|
340
|
+
|
|
341
|
+
# Upload and get result
|
|
342
|
+
result = self.save()
|
|
343
|
+
|
|
344
|
+
# Restore original file_path
|
|
345
|
+
self._file_path = original_file_path
|
|
346
|
+
|
|
347
|
+
return result
|
|
348
|
+
finally:
|
|
349
|
+
# Clean up temp file
|
|
350
|
+
try:
|
|
351
|
+
os.unlink(temp_path)
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
|
|
235
355
|
|
|
236
356
|
def compute_sha256(file_path: str) -> str:
|
|
237
357
|
"""
|
ml_dash/metric.py
CHANGED
|
@@ -11,6 +11,99 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from .experiment import Experiment
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class MetricsManager:
|
|
15
|
+
"""
|
|
16
|
+
Manager for metric operations that supports both named and unnamed usage.
|
|
17
|
+
|
|
18
|
+
Supports two usage patterns:
|
|
19
|
+
1. Named: experiment.metrics("loss").append(value=0.5, step=1)
|
|
20
|
+
2. Unnamed: experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
# With explicit metric name
|
|
24
|
+
experiment.metrics("train_loss").append(value=0.5, step=100)
|
|
25
|
+
|
|
26
|
+
# Without specifying name upfront (name in append call)
|
|
27
|
+
experiment.metrics.append(name="train_loss", value=0.5, step=100)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, experiment: 'Experiment'):
|
|
31
|
+
"""
|
|
32
|
+
Initialize MetricsManager.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
experiment: Parent Experiment instance
|
|
36
|
+
"""
|
|
37
|
+
self._experiment = experiment
|
|
38
|
+
|
|
39
|
+
def __call__(self, name: str, description: Optional[str] = None,
|
|
40
|
+
tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None) -> 'MetricBuilder':
|
|
41
|
+
"""
|
|
42
|
+
Get a MetricBuilder for a specific metric name.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
name: Metric name (unique within experiment)
|
|
46
|
+
description: Optional metric description
|
|
47
|
+
tags: Optional tags for categorization
|
|
48
|
+
metadata: Optional structured metadata
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
MetricBuilder instance for the named metric
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
experiment.metrics("loss").append(value=0.5, step=1)
|
|
55
|
+
"""
|
|
56
|
+
return MetricBuilder(self._experiment, name, description, tags, metadata)
|
|
57
|
+
|
|
58
|
+
def append(self, name: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Append a data point to a metric (name specified in call).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
name: Metric name
|
|
64
|
+
data: Data dict (alternative to kwargs)
|
|
65
|
+
**kwargs: Data as keyword arguments
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Response dict with metric metadata
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
experiment.metrics.append(name="loss", value=0.5, step=1)
|
|
72
|
+
experiment.metrics.append(name="loss", data={"value": 0.5, "step": 1})
|
|
73
|
+
"""
|
|
74
|
+
if data is None:
|
|
75
|
+
data = kwargs
|
|
76
|
+
return self._experiment._append_to_metric(name, data, None, None, None)
|
|
77
|
+
|
|
78
|
+
def append_batch(self, name: str, data_points: List[Dict[str, Any]],
|
|
79
|
+
description: Optional[str] = None,
|
|
80
|
+
tags: Optional[List[str]] = None,
|
|
81
|
+
metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Append multiple data points to a metric.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Metric name
|
|
87
|
+
data_points: List of data point dicts
|
|
88
|
+
description: Optional metric description
|
|
89
|
+
tags: Optional tags for categorization
|
|
90
|
+
metadata: Optional structured metadata
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Response dict with metric metadata
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
experiment.metrics.append_batch(
|
|
97
|
+
name="loss",
|
|
98
|
+
data_points=[
|
|
99
|
+
{"value": 0.5, "step": 1},
|
|
100
|
+
{"value": 0.4, "step": 2}
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
"""
|
|
104
|
+
return self._experiment._append_batch_to_metric(name, data_points, description, tags, metadata)
|
|
105
|
+
|
|
106
|
+
|
|
14
107
|
class MetricBuilder:
|
|
15
108
|
"""
|
|
16
109
|
Builder for metric operations.
|
ml_dash/params.py
CHANGED
|
@@ -62,9 +62,9 @@ class ParametersBuilder:
|
|
|
62
62
|
experiment.parameters().set(**{"model.lr": 0.001, "model.batch_size": 32})
|
|
63
63
|
"""
|
|
64
64
|
if not self._experiment._is_open:
|
|
65
|
-
raise RuntimeError("Experiment not open. Use experiment.
|
|
65
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
66
66
|
|
|
67
|
-
if self._experiment.
|
|
67
|
+
if self._experiment._write_protected:
|
|
68
68
|
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
69
69
|
|
|
70
70
|
# Flatten the kwargs
|
ml_dash/storage.py
CHANGED
|
@@ -43,6 +43,7 @@ class LocalStorage:
|
|
|
43
43
|
name: str,
|
|
44
44
|
description: Optional[str] = None,
|
|
45
45
|
tags: Optional[List[str]] = None,
|
|
46
|
+
bindrs: Optional[List[str]] = None,
|
|
46
47
|
folder: Optional[str] = None,
|
|
47
48
|
metadata: Optional[Dict[str, Any]] = None,
|
|
48
49
|
) -> Path:
|
|
@@ -54,6 +55,7 @@ class LocalStorage:
|
|
|
54
55
|
name: Experiment name
|
|
55
56
|
description: Optional description
|
|
56
57
|
tags: Optional tags
|
|
58
|
+
bindrs: Optional bindrs
|
|
57
59
|
folder: Optional folder path (used for organization)
|
|
58
60
|
metadata: Optional metadata
|
|
59
61
|
|
|
@@ -79,6 +81,7 @@ class LocalStorage:
|
|
|
79
81
|
"project": project,
|
|
80
82
|
"description": description,
|
|
81
83
|
"tags": tags or [],
|
|
84
|
+
"bindrs": bindrs or [],
|
|
82
85
|
"folder": folder,
|
|
83
86
|
"metadata": metadata,
|
|
84
87
|
"created_at": datetime.utcnow().isoformat() + "Z",
|
|
@@ -99,6 +102,8 @@ class LocalStorage:
|
|
|
99
102
|
existing["description"] = description
|
|
100
103
|
if tags is not None:
|
|
101
104
|
existing["tags"] = tags
|
|
105
|
+
if bindrs is not None:
|
|
106
|
+
existing["bindrs"] = bindrs
|
|
102
107
|
if folder is not None:
|
|
103
108
|
existing["folder"] = folder
|
|
104
109
|
if metadata is not None:
|
|
@@ -288,7 +293,7 @@ class LocalStorage:
|
|
|
288
293
|
"""
|
|
289
294
|
Write file to local storage.
|
|
290
295
|
|
|
291
|
-
Copies file to: files/<file_id>/<filename>
|
|
296
|
+
Copies file to: files/<prefix>/<file_id>/<filename>
|
|
292
297
|
Updates .files_metadata.json with file metadata
|
|
293
298
|
|
|
294
299
|
Args:
|
|
@@ -317,8 +322,14 @@ class LocalStorage:
|
|
|
317
322
|
# Generate Snowflake ID for file
|
|
318
323
|
file_id = generate_snowflake_id()
|
|
319
324
|
|
|
320
|
-
#
|
|
321
|
-
|
|
325
|
+
# Normalize prefix (remove leading slashes to avoid absolute paths)
|
|
326
|
+
normalized_prefix = prefix.lstrip("/") if prefix else ""
|
|
327
|
+
|
|
328
|
+
# Create prefix directory, then file directory
|
|
329
|
+
prefix_dir = files_dir / normalized_prefix if normalized_prefix else files_dir
|
|
330
|
+
prefix_dir.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
|
|
332
|
+
file_dir = prefix_dir / file_id
|
|
322
333
|
file_dir.mkdir(parents=True, exist_ok=True)
|
|
323
334
|
|
|
324
335
|
# Copy file
|
|
@@ -363,7 +374,11 @@ class LocalStorage:
|
|
|
363
374
|
if existing_index is not None:
|
|
364
375
|
# Overwrite: remove old file and update metadata
|
|
365
376
|
old_file = files_metadata["files"][existing_index]
|
|
366
|
-
|
|
377
|
+
old_prefix = old_file["path"].lstrip("/") if old_file["path"] else ""
|
|
378
|
+
if old_prefix:
|
|
379
|
+
old_file_dir = files_dir / old_prefix / old_file["id"]
|
|
380
|
+
else:
|
|
381
|
+
old_file_dir = files_dir / old_file["id"]
|
|
367
382
|
if old_file_dir.exists():
|
|
368
383
|
shutil.rmtree(old_file_dir)
|
|
369
384
|
files_metadata["files"][existing_index] = file_metadata
|
|
@@ -470,7 +485,11 @@ class LocalStorage:
|
|
|
470
485
|
raise FileNotFoundError(f"File {file_id} not found")
|
|
471
486
|
|
|
472
487
|
# Get source file
|
|
473
|
-
|
|
488
|
+
file_prefix = file_metadata["path"].lstrip("/") if file_metadata["path"] else ""
|
|
489
|
+
if file_prefix:
|
|
490
|
+
source_file = files_dir / file_prefix / file_id / file_metadata["filename"]
|
|
491
|
+
else:
|
|
492
|
+
source_file = files_dir / file_id / file_metadata["filename"]
|
|
474
493
|
if not source_file.exists():
|
|
475
494
|
raise FileNotFoundError(f"File {file_id} not found on disk")
|
|
476
495
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ml-dash
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary: ML experiment
|
|
5
|
-
Keywords: machine-learning,experiment-
|
|
3
|
+
Version: 0.5.2
|
|
4
|
+
Summary: ML experiment tracking and data storage
|
|
5
|
+
Keywords: machine-learning,experiment-tracking,mlops,data-storage
|
|
6
6
|
Author: Ge Yang, Tom Tao
|
|
7
7
|
License: MIT License
|
|
8
8
|
|
|
@@ -67,6 +67,9 @@ A simple and flexible SDK for ML experiment metricing and data storage.
|
|
|
67
67
|
- **Dual Operation Modes**: Remote (API server) or local (filesystem)
|
|
68
68
|
- **Auto-creation**: Automatically creates namespace, project, and folder hierarchy
|
|
69
69
|
- **Upsert Behavior**: Updates existing experiments or creates new ones
|
|
70
|
+
- **Experiment Lifecycle**: Automatic status tracking (RUNNING, COMPLETED, FAILED, CANCELLED)
|
|
71
|
+
- **Organized File Storage**: Prefix-based file organization with unique snowflake IDs
|
|
72
|
+
- **Rich Metadata**: Tags, bindrs, descriptions, and custom metadata support
|
|
70
73
|
- **Simple API**: Minimal configuration, maximum flexibility
|
|
71
74
|
|
|
72
75
|
## Installation
|
|
@@ -94,7 +97,7 @@ pip install ml-dash
|
|
|
94
97
|
</tr>
|
|
95
98
|
</table>
|
|
96
99
|
|
|
97
|
-
##
|
|
100
|
+
## Getting Started
|
|
98
101
|
|
|
99
102
|
### Remote Mode (with API Server)
|
|
100
103
|
|
|
@@ -104,7 +107,7 @@ from ml_dash import Experiment
|
|
|
104
107
|
with Experiment(
|
|
105
108
|
name="my-experiment",
|
|
106
109
|
project="my-project",
|
|
107
|
-
remote="https://
|
|
110
|
+
remote="https://api.dash.ml",
|
|
108
111
|
api_key="your-jwt-token"
|
|
109
112
|
) as experiment:
|
|
110
113
|
print(f"Experiment ID: {experiment.id}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ml_dash/__init__.py,sha256=o_LrWVJBY_VkUGhSBs5wdb_NqEsHD1AK9HGsjZGxHxQ,1414
|
|
2
|
+
ml_dash/auto_start.py,sha256=c3XcXFpZdvjtWauEoK5043Gw9k0L_5IDq4fdiB2ha88,959
|
|
3
|
+
ml_dash/client.py,sha256=vhWcS5o2n3o4apEjVeLmu7flCEzxBbBOoLSQNcAx_ew,17267
|
|
4
|
+
ml_dash/experiment.py,sha256=zdGB3oZsFNFyg9olRazWk7dTO7tfy-vTa4neFq5i2CY,30552
|
|
5
|
+
ml_dash/files.py,sha256=FZGHqf5VZ6Hgrpbng-AwTZyw04-Zof252YSjF6nRWX8,13667
|
|
6
|
+
ml_dash/log.py,sha256=0yXaNnFwYeBI3tRLHX3kkqWRpg0MbSGwmgjnOfsElCk,5350
|
|
7
|
+
ml_dash/metric.py,sha256=LMb6-T08VAl6UBAv6FlZee6LleVLjFaBNc19b17NlfI,9662
|
|
8
|
+
ml_dash/params.py,sha256=xaByDSVar4D1pZqxTANkMPeZTL5-V7ewJe5TXfPLhMQ,5980
|
|
9
|
+
ml_dash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
ml_dash/storage.py,sha256=UTuux2nfclLrrtlkC6TsOvDB_wIbSDvYGg8Gtbvk6mc,30471
|
|
11
|
+
ml_dash-0.5.2.dist-info/WHEEL,sha256=X16MKk8bp2DRsAuyteHJ-9qOjzmnY0x1aj0P1ftqqWA,78
|
|
12
|
+
ml_dash-0.5.2.dist-info/METADATA,sha256=ZbZgasRMgq0iQ5VqPGbySV-pm9HJJlDNU8g4Zs6nFiM,6043
|
|
13
|
+
ml_dash-0.5.2.dist-info/RECORD,,
|
ml_dash-0.5.0.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
ml_dash/__init__.py,sha256=5tT0Lmf0SS3J7BOwJGVai8FOjdpjKGBJCEYL5nXnkLA,1384
|
|
2
|
-
ml_dash/client.py,sha256=1T85L-YOCVRakzZlKCKaW6-kSDpo8gx_pdG8wvvi9Tc,16391
|
|
3
|
-
ml_dash/experiment.py,sha256=MKQsEs1MQ07xbPHvWRvatyvLOXuiDKxJx7QWPfpmygM,27658
|
|
4
|
-
ml_dash/files.py,sha256=WKWbcug6XADwZruYQio1EdSstmfTsty9-2-t2KPWz38,9719
|
|
5
|
-
ml_dash/log.py,sha256=0yXaNnFwYeBI3tRLHX3kkqWRpg0MbSGwmgjnOfsElCk,5350
|
|
6
|
-
ml_dash/metric.py,sha256=PcEd6_HTLDpf-kBIDeQq2LlTRAS7xDx6EvSBpin5iuY,6456
|
|
7
|
-
ml_dash/params.py,sha256=W-JkY1Mz7KdmvDjQ0HFV2QnpBov7Gf4dl70fuBnXTdo,5974
|
|
8
|
-
ml_dash/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
ml_dash/storage.py,sha256=iVGPgRJnsUzxfTh12QCUKyPC-SiK4QNDWHsUBLxP0I0,29538
|
|
10
|
-
ml_dash-0.5.0.dist-info/WHEEL,sha256=X16MKk8bp2DRsAuyteHJ-9qOjzmnY0x1aj0P1ftqqWA,78
|
|
11
|
-
ml_dash-0.5.0.dist-info/METADATA,sha256=j1GGNmDvmp8REzN8AIsdiLRimiZqPz7OuEpAhf71Kys,5809
|
|
12
|
-
ml_dash-0.5.0.dist-info/RECORD,,
|
|
File without changes
|