ml-dash 0.5.2__tar.gz → 0.5.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ml-dash
3
- Version: 0.5.2
3
+ Version: 0.5.6
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
@@ -38,6 +38,9 @@ Classifier: Programming Language :: Python :: 3.13
38
38
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
39
39
  Requires-Dist: httpx>=0.27.0
40
40
  Requires-Dist: pyjwt>=2.8.0
41
+ Requires-Dist: imageio>=2.31.0
42
+ Requires-Dist: imageio-ffmpeg>=0.4.9
43
+ Requires-Dist: scikit-image>=0.21.0
41
44
  Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
42
45
  Requires-Dist: pytest-asyncio>=0.23.0 ; extra == 'dev'
43
46
  Requires-Dist: sphinx>=7.2.0 ; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ml-dash"
3
- version = "0.5.2"
3
+ version = "0.5.6"
4
4
  description = "ML experiment tracking and data storage"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -26,6 +26,9 @@ classifiers = [
26
26
  dependencies = [
27
27
  "httpx>=0.27.0",
28
28
  "pyjwt>=2.8.0",
29
+ "imageio>=2.31.0",
30
+ "imageio-ffmpeg>=0.4.9",
31
+ "scikit-image>=0.21.0",
29
32
  ]
30
33
 
31
34
  [project.optional-dependencies]
@@ -460,7 +460,7 @@ class Experiment:
460
460
  timestamp=log_entry["timestamp"]
461
461
  )
462
462
 
463
- def file(self, **kwargs) -> FileBuilder:
463
+ def files(self, **kwargs) -> FileBuilder:
464
464
  """
465
465
  Get a FileBuilder for fluent file operations.
466
466
 
@@ -472,17 +472,17 @@ class Experiment:
472
472
 
473
473
  Examples:
474
474
  # Upload file
475
- experiment.file(file_path="./model.pt", prefix="/models").save()
475
+ experiment.files(file_path="./model.pt", prefix="/models").save()
476
476
 
477
477
  # List files
478
- files = experiment.file().list()
479
- files = experiment.file(prefix="/models").list()
478
+ files = experiment.files().list()
479
+ files = experiment.files(prefix="/models").list()
480
480
 
481
481
  # Download file
482
- experiment.file(file_id="123").download()
482
+ experiment.files(file_id="123").download()
483
483
 
484
484
  # Delete file
485
- experiment.file(file_id="123").delete()
485
+ experiment.files(file_id="123").delete()
486
486
  """
487
487
  if not self._is_open:
488
488
  raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
@@ -791,7 +791,7 @@ class Experiment:
791
791
 
792
792
  def _append_to_metric(
793
793
  self,
794
- name: str,
794
+ name: Optional[str],
795
795
  data: Dict[str, Any],
796
796
  description: Optional[str],
797
797
  tags: Optional[List[str]],
@@ -801,7 +801,7 @@ class Experiment:
801
801
  Internal method to append a single data point to a metric.
802
802
 
803
803
  Args:
804
- name: Metric name
804
+ name: Metric name (can be None for unnamed metrics)
805
805
  data: Data point (flexible schema)
806
806
  description: Optional metric description
807
807
  tags: Optional tags
@@ -839,7 +839,7 @@ class Experiment:
839
839
 
840
840
  def _append_batch_to_metric(
841
841
  self,
842
- name: str,
842
+ name: Optional[str],
843
843
  data_points: List[Dict[str, Any]],
844
844
  description: Optional[str],
845
845
  tags: Optional[List[str]],
@@ -849,7 +849,7 @@ class Experiment:
849
849
  Internal method to append multiple data points to a metric.
850
850
 
851
851
  Args:
852
- name: Metric name
852
+ name: Metric name (can be None for unnamed metrics)
853
853
  data_points: List of data points
854
854
  description: Optional metric description
855
855
  tags: Optional tags
@@ -6,7 +6,7 @@ Provides fluent API for file upload, download, list, and delete operations.
6
6
 
7
7
  import hashlib
8
8
  import mimetypes
9
- from typing import Dict, Any, List, Optional, TYPE_CHECKING
9
+ from typing import Dict, Any, List, Optional, Union, TYPE_CHECKING
10
10
  from pathlib import Path
11
11
 
12
12
  if TYPE_CHECKING:
@@ -19,18 +19,18 @@ class FileBuilder:
19
19
 
20
20
  Usage:
21
21
  # Upload file
22
- experiment.file(file_path="./model.pt", prefix="/models").save()
22
+ experiment.files(file_path="./model.pt", prefix="/models").save()
23
23
 
24
24
  # List files
25
- files = experiment.file().list()
26
- files = experiment.file(prefix="/models").list()
25
+ files = experiment.files().list()
26
+ files = experiment.files(prefix="/models").list()
27
27
 
28
28
  # Download file
29
- experiment.file(file_id="123").download()
30
- experiment.file(file_id="123", dest_path="./model.pt").download()
29
+ experiment.files(file_id="123").download()
30
+ experiment.files(file_id="123", dest_path="./model.pt").download()
31
31
 
32
32
  # Delete file
33
- experiment.file(file_id="123").delete()
33
+ experiment.files(file_id="123").delete()
34
34
  """
35
35
 
36
36
  def __init__(self, experiment: 'Experiment', **kwargs):
@@ -72,7 +72,7 @@ class FileBuilder:
72
72
  ValueError: If file size exceeds 5GB limit
73
73
 
74
74
  Examples:
75
- result = experiment.file(file_path="./model.pt", prefix="/models").save()
75
+ result = experiment.files(file_path="./model.pt", prefix="/models").save()
76
76
  # Returns: {"id": "123", "path": "/models", "filename": "model.pt", ...}
77
77
  """
78
78
  if not self._experiment._is_open:
@@ -130,9 +130,9 @@ class FileBuilder:
130
130
  RuntimeError: If experiment is not open
131
131
 
132
132
  Examples:
133
- files = experiment.file().list() # All files
134
- files = experiment.file(prefix="/models").list() # Filter by prefix
135
- files = experiment.file(tags=["checkpoint"]).list() # Filter by tags
133
+ files = experiment.files().list() # All files
134
+ files = experiment.files(prefix="/models").list() # Filter by prefix
135
+ files = experiment.files(tags=["checkpoint"]).list() # Filter by tags
136
136
  """
137
137
  if not self._experiment._is_open:
138
138
  raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
@@ -158,10 +158,10 @@ class FileBuilder:
158
158
 
159
159
  Examples:
160
160
  # Download to current directory with original filename
161
- path = experiment.file(file_id="123").download()
161
+ path = experiment.files(file_id="123").download()
162
162
 
163
163
  # Download to custom path
164
- path = experiment.file(file_id="123", dest_path="./model.pt").download()
164
+ path = experiment.files(file_id="123", dest_path="./model.pt").download()
165
165
  """
166
166
  if not self._experiment._is_open:
167
167
  raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
@@ -186,7 +186,7 @@ class FileBuilder:
186
186
  ValueError: If file_id not provided
187
187
 
188
188
  Examples:
189
- result = experiment.file(file_id="123").delete()
189
+ result = experiment.files(file_id="123").delete()
190
190
  """
191
191
  if not self._experiment._is_open:
192
192
  raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
@@ -211,7 +211,7 @@ class FileBuilder:
211
211
  ValueError: If file_id not provided
212
212
 
213
213
  Examples:
214
- result = experiment.file(
214
+ result = experiment.files(
215
215
  file_id="123",
216
216
  description="Updated description",
217
217
  tags=["new", "tags"],
@@ -251,7 +251,7 @@ class FileBuilder:
251
251
 
252
252
  Examples:
253
253
  config = {"model": "resnet50", "lr": 0.001}
254
- result = experiment.file(prefix="/configs").save_json(config, "config.json")
254
+ result = experiment.files(prefix="/configs").save_json(config, "config.json")
255
255
  """
256
256
  import json
257
257
  import tempfile
@@ -263,11 +263,12 @@ class FileBuilder:
263
263
  if self._experiment._write_protected:
264
264
  raise RuntimeError("Experiment is write-protected and cannot be modified.")
265
265
 
266
- # Create temporary file
267
- temp_fd, temp_path = tempfile.mkstemp(suffix='.json', text=True)
266
+ # Create temporary file with desired filename
267
+ temp_dir = tempfile.mkdtemp()
268
+ temp_path = os.path.join(temp_dir, file_name)
268
269
  try:
269
270
  # Write JSON content to temp file
270
- with os.fdopen(temp_fd, 'w') as f:
271
+ with open(temp_path, 'w') as f:
271
272
  json.dump(content, f, indent=2)
272
273
 
273
274
  # Save using existing save() method
@@ -282,9 +283,10 @@ class FileBuilder:
282
283
 
283
284
  return result
284
285
  finally:
285
- # Clean up temp file
286
+ # Clean up temp file and directory
286
287
  try:
287
288
  os.unlink(temp_path)
289
+ os.rmdir(temp_dir)
288
290
  except Exception:
289
291
  pass
290
292
 
@@ -307,10 +309,10 @@ class FileBuilder:
307
309
  Examples:
308
310
  import torch
309
311
  model = torch.nn.Linear(10, 5)
310
- result = experiment.file(prefix="/models").save_torch(model, "model.pt")
312
+ result = experiment.files(prefix="/models").save_torch(model, "model.pt")
311
313
 
312
314
  # Or save state dict
313
- result = experiment.file(prefix="/models").save_torch(model.state_dict(), "model.pth")
315
+ result = experiment.files(prefix="/models").save_torch(model.state_dict(), "model.pth")
314
316
  """
315
317
  import tempfile
316
318
  import os
@@ -326,9 +328,9 @@ class FileBuilder:
326
328
  if self._experiment._write_protected:
327
329
  raise RuntimeError("Experiment is write-protected and cannot be modified.")
328
330
 
329
- # Create temporary file
330
- temp_fd, temp_path = tempfile.mkstemp(suffix='.pt')
331
- os.close(temp_fd) # Close the file descriptor
331
+ # Create temporary file with desired filename
332
+ temp_dir = tempfile.mkdtemp()
333
+ temp_path = os.path.join(temp_dir, file_name)
332
334
 
333
335
  try:
334
336
  # Save model to temp file
@@ -346,9 +348,262 @@ class FileBuilder:
346
348
 
347
349
  return result
348
350
  finally:
349
- # Clean up temp file
351
+ # Clean up temp file and directory
350
352
  try:
351
353
  os.unlink(temp_path)
354
+ os.rmdir(temp_dir)
355
+ except Exception:
356
+ pass
357
+
358
+ def save_pkl(self, content: Any, file_name: str) -> Dict[str, Any]:
359
+ """
360
+ Save Python object to a pickle file.
361
+
362
+ Args:
363
+ content: Python object to pickle (must be pickle-serializable)
364
+ file_name: Name of the file to create (should end with .pkl or .pickle)
365
+
366
+ Returns:
367
+ File metadata dict with id, path, filename, checksum, etc.
368
+
369
+ Raises:
370
+ RuntimeError: If experiment is not open or write-protected
371
+ ValueError: If content cannot be pickled
372
+
373
+ Examples:
374
+ data = {"model": "resnet50", "weights": np.array([1, 2, 3])}
375
+ result = experiment.files(prefix="/data").save_pkl(data, "data.pkl")
376
+
377
+ # Or save any Python object
378
+ result = experiment.files(prefix="/models").save_pkl(trained_model, "model.pickle")
379
+ """
380
+ import pickle
381
+ import tempfile
382
+ import os
383
+
384
+ if not self._experiment._is_open:
385
+ raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
386
+
387
+ if self._experiment._write_protected:
388
+ raise RuntimeError("Experiment is write-protected and cannot be modified.")
389
+
390
+ # Create temporary file with desired filename
391
+ temp_dir = tempfile.mkdtemp()
392
+ temp_path = os.path.join(temp_dir, file_name)
393
+ try:
394
+ # Write pickled content to temp file
395
+ with open(temp_path, 'wb') as f:
396
+ pickle.dump(content, f)
397
+
398
+ # Save using existing save() method
399
+ original_file_path = self._file_path
400
+ self._file_path = temp_path
401
+
402
+ # Upload and get result
403
+ result = self.save()
404
+
405
+ # Restore original file_path
406
+ self._file_path = original_file_path
407
+
408
+ return result
409
+ finally:
410
+ # Clean up temp file and directory
411
+ try:
412
+ os.unlink(temp_path)
413
+ os.rmdir(temp_dir)
414
+ except Exception:
415
+ pass
416
+
417
+ def save_fig(self, fig: Optional[Any] = None, file_name: str = "figure.png", **kwargs) -> Dict[str, Any]:
418
+ """
419
+ Save matplotlib figure to a file.
420
+
421
+ Args:
422
+ fig: Matplotlib figure object. If None, uses plt.gcf() (current figure)
423
+ file_name: Name of file to create (extension determines format: .png, .pdf, .svg, .jpg)
424
+ **kwargs: Additional arguments passed to fig.savefig():
425
+ - dpi: Resolution (int or 'figure')
426
+ - transparent: Make background transparent (bool)
427
+ - bbox_inches: 'tight' to auto-crop (str or Bbox)
428
+ - quality: JPEG quality 0-100 (int)
429
+ - format: Override format detection (str)
430
+
431
+ Returns:
432
+ File metadata dict with id, path, filename, checksum, etc.
433
+
434
+ Raises:
435
+ RuntimeError: If experiment not open or write-protected
436
+ ImportError: If matplotlib not installed
437
+
438
+ Examples:
439
+ import matplotlib.pyplot as plt
440
+
441
+ # Use current figure
442
+ plt.plot([1, 2, 3], [1, 4, 9])
443
+ result = experiment.files(prefix="/plots").save_fig(file_name="plot.png")
444
+
445
+ # Specify figure explicitly
446
+ fig, ax = plt.subplots()
447
+ ax.plot([1, 2, 3])
448
+ result = experiment.files(prefix="/plots").save_fig(fig=fig, file_name="plot.pdf", dpi=150)
449
+ """
450
+ import tempfile
451
+ import os
452
+
453
+ try:
454
+ import matplotlib.pyplot as plt
455
+ except ImportError:
456
+ raise ImportError("Matplotlib is not installed. Install it with: pip install matplotlib")
457
+
458
+ if not self._experiment._is_open:
459
+ raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
460
+
461
+ if self._experiment._write_protected:
462
+ raise RuntimeError("Experiment is write-protected and cannot be modified.")
463
+
464
+ # Get figure
465
+ if fig is None:
466
+ fig = plt.gcf()
467
+
468
+ # Create temporary file with desired filename
469
+ temp_dir = tempfile.mkdtemp()
470
+ temp_path = os.path.join(temp_dir, file_name)
471
+
472
+ try:
473
+ # Save figure to temp file
474
+ fig.savefig(temp_path, **kwargs)
475
+
476
+ # Close figure to prevent memory leaks
477
+ plt.close(fig)
478
+
479
+ # Save using existing save() method
480
+ original_file_path = self._file_path
481
+ self._file_path = temp_path
482
+
483
+ # Upload and get result
484
+ result = self.save()
485
+
486
+ # Restore original file_path
487
+ self._file_path = original_file_path
488
+
489
+ return result
490
+ finally:
491
+ # Clean up temp file and directory
492
+ try:
493
+ os.unlink(temp_path)
494
+ os.rmdir(temp_dir)
495
+ except Exception:
496
+ pass
497
+
498
+ def save_video(
499
+ self,
500
+ frame_stack: Union[List, Any],
501
+ file_name: str,
502
+ fps: int = 20,
503
+ **imageio_kwargs
504
+ ) -> Dict[str, Any]:
505
+ """
506
+ Save video frame stack to a file.
507
+
508
+ Args:
509
+ frame_stack: List of numpy arrays or stacked array (shape: [N, H, W] or [N, H, W, C])
510
+ file_name: Name of file to create (extension determines format: .mp4, .gif, .avi, .webm)
511
+ fps: Frames per second (default: 20)
512
+ **imageio_kwargs: Additional arguments passed to imageio.v3.imwrite():
513
+ - codec: Video codec (e.g., 'libx264', 'h264')
514
+ - quality: Quality level (int, higher is better)
515
+ - pixelformat: Pixel format (e.g., 'yuv420p')
516
+ - macro_block_size: Macro block size for encoding
517
+
518
+ Returns:
519
+ File metadata dict with id, path, filename, checksum, etc.
520
+
521
+ Raises:
522
+ RuntimeError: If experiment not open or write-protected
523
+ ImportError: If imageio or scikit-image not installed
524
+ ValueError: If frame_stack is empty or invalid format
525
+
526
+ Examples:
527
+ import numpy as np
528
+
529
+ # Grayscale frames (float values 0-1)
530
+ frames = [np.random.rand(480, 640) for _ in range(30)]
531
+ result = experiment.files(prefix="/videos").save_video(frames, "output.mp4")
532
+
533
+ # RGB frames with custom FPS
534
+ frames = [np.random.rand(480, 640, 3) for _ in range(60)]
535
+ result = experiment.files(prefix="/videos").save_video(frames, "output.mp4", fps=30)
536
+
537
+ # Save as GIF
538
+ frames = [np.random.rand(200, 200) for _ in range(20)]
539
+ result = experiment.files(prefix="/videos").save_video(frames, "animation.gif")
540
+
541
+ # With custom codec and quality
542
+ result = experiment.files(prefix="/videos").save_video(
543
+ frames, "output.mp4", fps=30, codec='libx264', quality=8
544
+ )
545
+ """
546
+ import tempfile
547
+ import os
548
+
549
+ try:
550
+ import imageio.v3 as iio
551
+ except ImportError:
552
+ raise ImportError("imageio is not installed. Install it with: pip install imageio imageio-ffmpeg")
553
+
554
+ try:
555
+ from skimage import img_as_ubyte
556
+ except ImportError:
557
+ raise ImportError("scikit-image is not installed. Install it with: pip install scikit-image")
558
+
559
+ if not self._experiment._is_open:
560
+ raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
561
+
562
+ if self._experiment._write_protected:
563
+ raise RuntimeError("Experiment is write-protected and cannot be modified.")
564
+
565
+ # Validate frame_stack
566
+ try:
567
+ # Handle both list and numpy array
568
+ if len(frame_stack) == 0:
569
+ raise ValueError("frame_stack is empty")
570
+ except TypeError:
571
+ raise ValueError("frame_stack must be a list or numpy array")
572
+
573
+ # Create temporary file with desired filename
574
+ temp_dir = tempfile.mkdtemp()
575
+ temp_path = os.path.join(temp_dir, file_name)
576
+
577
+ try:
578
+ # Convert frames to uint8 format (handles float32/64, grayscale, RGB, etc.)
579
+ # img_as_ubyte automatically scales [0.0, 1.0] floats to [0, 255] uint8
580
+ frames_ubyte = img_as_ubyte(frame_stack)
581
+
582
+ # Encode video to temp file
583
+ try:
584
+ iio.imwrite(temp_path, frames_ubyte, fps=fps, **imageio_kwargs)
585
+ except iio.core.NeedDownloadError:
586
+ # Auto-download FFmpeg if not available
587
+ import imageio.plugins.ffmpeg
588
+ imageio.plugins.ffmpeg.download()
589
+ iio.imwrite(temp_path, frames_ubyte, fps=fps, **imageio_kwargs)
590
+
591
+ # Save using existing save() method
592
+ original_file_path = self._file_path
593
+ self._file_path = temp_path
594
+
595
+ # Upload and get result
596
+ result = self.save()
597
+
598
+ # Restore original file_path
599
+ self._file_path = original_file_path
600
+
601
+ return result
602
+ finally:
603
+ # Clean up temp file and directory
604
+ try:
605
+ os.unlink(temp_path)
606
+ os.rmdir(temp_dir)
352
607
  except Exception:
353
608
  pass
354
609
 
@@ -15,16 +15,20 @@ class MetricsManager:
15
15
  """
16
16
  Manager for metric operations that supports both named and unnamed usage.
17
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)
18
+ Supports three usage patterns:
19
+ 1. Named via call: experiment.metrics("loss").append(value=0.5, step=1)
20
+ 2. Named via argument: experiment.metrics.append(name="loss", value=0.5, step=1)
21
+ 3. Unnamed: experiment.metrics.append(value=0.5, step=1) # name=None
21
22
 
22
23
  Usage:
23
- # With explicit metric name
24
+ # With explicit metric name (via call)
24
25
  experiment.metrics("train_loss").append(value=0.5, step=100)
25
26
 
26
- # Without specifying name upfront (name in append call)
27
+ # With explicit metric name (via argument)
27
28
  experiment.metrics.append(name="train_loss", value=0.5, step=100)
29
+
30
+ # Without name (uses None as metric name)
31
+ experiment.metrics.append(value=0.5, step=100)
28
32
  """
29
33
 
30
34
  def __init__(self, experiment: 'Experiment'):
@@ -55,12 +59,12 @@ class MetricsManager:
55
59
  """
56
60
  return MetricBuilder(self._experiment, name, description, tags, metadata)
57
61
 
58
- def append(self, name: str, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
62
+ def append(self, name: Optional[str] = None, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
59
63
  """
60
- Append a data point to a metric (name specified in call).
64
+ Append a data point to a metric (name can be optional).
61
65
 
62
66
  Args:
63
- name: Metric name
67
+ name: Metric name (optional, can be None for unnamed metrics)
64
68
  data: Data dict (alternative to kwargs)
65
69
  **kwargs: Data as keyword arguments
66
70
 
@@ -69,13 +73,14 @@ class MetricsManager:
69
73
 
70
74
  Examples:
71
75
  experiment.metrics.append(name="loss", value=0.5, step=1)
76
+ experiment.metrics.append(value=0.5, step=1) # name=None
72
77
  experiment.metrics.append(name="loss", data={"value": 0.5, "step": 1})
73
78
  """
74
79
  if data is None:
75
80
  data = kwargs
76
81
  return self._experiment._append_to_metric(name, data, None, None, None)
77
82
 
78
- def append_batch(self, name: str, data_points: List[Dict[str, Any]],
83
+ def append_batch(self, name: Optional[str] = None, data_points: Optional[List[Dict[str, Any]]] = None,
79
84
  description: Optional[str] = None,
80
85
  tags: Optional[List[str]] = None,
81
86
  metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@@ -83,7 +88,7 @@ class MetricsManager:
83
88
  Append multiple data points to a metric.
84
89
 
85
90
  Args:
86
- name: Metric name
91
+ name: Metric name (optional, can be None for unnamed metrics)
87
92
  data_points: List of data point dicts
88
93
  description: Optional metric description
89
94
  tags: Optional tags for categorization
@@ -100,7 +105,15 @@ class MetricsManager:
100
105
  {"value": 0.4, "step": 2}
101
106
  ]
102
107
  )
108
+ experiment.metrics.append_batch(
109
+ data_points=[
110
+ {"value": 0.5, "step": 1},
111
+ {"value": 0.4, "step": 2}
112
+ ]
113
+ ) # name=None
103
114
  """
115
+ if data_points is None:
116
+ data_points = []
104
117
  return self._experiment._append_batch_to_metric(name, data_points, description, tags, metadata)
105
118
 
106
119
 
@@ -636,7 +636,7 @@ class LocalStorage:
636
636
  self,
637
637
  project: str,
638
638
  experiment: str,
639
- metric_name: str,
639
+ metric_name: Optional[str],
640
640
  data: Dict[str, Any],
641
641
  description: Optional[str] = None,
642
642
  tags: Optional[List[str]] = None,
@@ -653,7 +653,7 @@ class LocalStorage:
653
653
  Args:
654
654
  project: Project name
655
655
  experiment: Experiment name
656
- metric_name: Metric name
656
+ metric_name: Metric name (None for unnamed metrics)
657
657
  data: Data point (flexible schema)
658
658
  description: Optional metric description
659
659
  tags: Optional tags
@@ -666,7 +666,9 @@ class LocalStorage:
666
666
  metrics_dir = experiment_dir / "metrics"
667
667
  metrics_dir.mkdir(parents=True, exist_ok=True)
668
668
 
669
- metric_dir = metrics_dir / metric_name
669
+ # Convert None to string for directory name
670
+ dir_name = str(metric_name) if metric_name is not None else "None"
671
+ metric_dir = metrics_dir / dir_name
670
672
  metric_dir.mkdir(exist_ok=True)
671
673
 
672
674
  data_file = metric_dir / "data.jsonl"
@@ -720,7 +722,7 @@ class LocalStorage:
720
722
  self,
721
723
  project: str,
722
724
  experiment: str,
723
- metric_name: str,
725
+ metric_name: Optional[str],
724
726
  data_points: List[Dict[str, Any]],
725
727
  description: Optional[str] = None,
726
728
  tags: Optional[List[str]] = None,
@@ -732,7 +734,7 @@ class LocalStorage:
732
734
  Args:
733
735
  project: Project name
734
736
  experiment: Experiment name
735
- metric_name: Metric name
737
+ metric_name: Metric name (None for unnamed metrics)
736
738
  data_points: List of data points
737
739
  description: Optional metric description
738
740
  tags: Optional tags
@@ -745,7 +747,9 @@ class LocalStorage:
745
747
  metrics_dir = experiment_dir / "metrics"
746
748
  metrics_dir.mkdir(parents=True, exist_ok=True)
747
749
 
748
- metric_dir = metrics_dir / metric_name
750
+ # Convert None to string for directory name
751
+ dir_name = str(metric_name) if metric_name is not None else "None"
752
+ metric_dir = metrics_dir / dir_name
749
753
  metric_dir.mkdir(exist_ok=True)
750
754
 
751
755
  data_file = metric_dir / "data.jsonl"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes