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 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.write_protected = write_protected
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 open(self) -> "Experiment":
243
+ def _open(self) -> "Experiment":
172
244
  """
173
- Open the experiment (create or update on server/filesystem).
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.write_protected,
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 close(self):
210
- """Close the experiment."""
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
- def __enter__(self) -> "Experiment":
221
- """Context manager entry."""
222
- return self.open()
310
+ @property
311
+ def run(self) -> RunManager:
312
+ """
313
+ Get the RunManager for lifecycle operations.
223
314
 
224
- def __exit__(self, exc_type, exc_val, exc_tb):
225
- """Context manager exit."""
226
- self.close()
227
- return False
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
- def metric(self, name: str, description: Optional[str] = None,
646
- tags: Optional[List[str]] = None, metadata: Optional[Dict[str, Any]] = None) -> 'MetricBuilder':
748
+ @property
749
+ def metrics(self) -> 'MetricsManager':
647
750
  """
648
- Get a MetricBuilder for fluent metric operations.
751
+ Get a MetricsManager for metric operations.
649
752
 
650
- Args:
651
- name: Metric name (unique within experiment)
652
- description: Optional metric description
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
- MetricBuilder instance for chaining
758
+ MetricsManager instance
658
759
 
659
760
  Raises:
660
761
  RuntimeError: If experiment is not open
661
762
 
662
763
  Examples:
663
- # Append single data point
664
- experiment.metric(name="train_loss").append(value=0.5, step=100)
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.metric(name="metrics").append_batch([
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.metric(name="train_loss").read(start_index=0, limit=100)
777
+ data = experiment.metrics("train_loss").read(start_index=0, limit=100)
674
778
 
675
779
  # Get statistics
676
- stats = experiment.metric(name="train_loss").stats()
780
+ stats = experiment.metrics("train_loss").stats()
677
781
  """
678
- from .metric import MetricBuilder
782
+ from .metric import MetricsManager
679
783
 
680
784
  if not self._is_open:
681
785
  raise RuntimeError(
682
- "Cannot use metric on closed experiment. "
683
- "Use 'with Experiment(...) as experiment:' or call experiment.open() first."
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 MetricBuilder(self, name, description, tags, metadata)
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.write_protected:
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.write_protected:
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.write_protected:
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.open() or context manager.")
65
+ raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
66
66
 
67
- if self._experiment.write_protected:
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
- # Create file directory
321
- file_dir = files_dir / file_id
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
- old_file_dir = files_dir / old_file["id"]
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
- source_file = files_dir / file_id / file_metadata["filename"]
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.0
4
- Summary: ML experiment metricing and data storage
5
- Keywords: machine-learning,experiment-metricing,mlops,data-storage
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
- ## Quick Start
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://cu3thurmv3.us-east-1.awsapprunner.com",
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,,
@@ -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,,