ml-dash 0.6.6__py3-none-any.whl → 0.6.9__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/buffer.py +735 -0
- ml_dash/cli_commands/download.py +177 -0
- ml_dash/cli_commands/list.py +146 -0
- ml_dash/cli_commands/profile.py +141 -6
- ml_dash/cli_commands/upload.py +131 -0
- ml_dash/client.py +265 -20
- ml_dash/experiment.py +286 -126
- ml_dash/files.py +228 -70
- ml_dash/storage.py +403 -0
- ml_dash/track.py +263 -0
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.9.dist-info}/METADATA +81 -5
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.9.dist-info}/RECORD +14 -12
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.9.dist-info}/WHEEL +0 -0
- {ml_dash-0.6.6.dist-info → ml_dash-0.6.9.dist-info}/entry_points.txt +0 -0
ml_dash/files.py
CHANGED
|
@@ -56,6 +56,7 @@ class FileBuilder:
|
|
|
56
56
|
experiment.files.save_blob(b"xxx", to="data.bin")
|
|
57
57
|
experiment.files.save_torch(model, to="model.pt")
|
|
58
58
|
experiment.files.save_pkl(data, to="data.pkl")
|
|
59
|
+
experiment.files.save_image(array, to="frame.png")
|
|
59
60
|
experiment.files.save_fig(fig, to="plot.png")
|
|
60
61
|
experiment.files.save_video(frames, to="video.mp4")
|
|
61
62
|
"""
|
|
@@ -184,8 +185,9 @@ class FileBuilder:
|
|
|
184
185
|
- str (file path): Uploads existing file
|
|
185
186
|
- bytes: Saves as binary blob (requires 'to' parameter)
|
|
186
187
|
- dict/list: Saves as JSON (requires 'to' parameter)
|
|
188
|
+
- numpy.ndarray: Saves as image (requires 'to' parameter with image extension)
|
|
187
189
|
- None: Uses file_path from constructor (backwards compatibility)
|
|
188
|
-
to: Target filename (required for bytes/dict/list, optional for file paths)
|
|
190
|
+
to: Target filename (required for bytes/dict/list/arrays, optional for file paths)
|
|
189
191
|
description: Optional description
|
|
190
192
|
tags: Optional list of tags
|
|
191
193
|
metadata: Optional metadata dict
|
|
@@ -224,7 +226,17 @@ class FileBuilder:
|
|
|
224
226
|
raise ValueError("'to' parameter is required when saving dict/list")
|
|
225
227
|
return self.save_json(content, to=to)
|
|
226
228
|
|
|
227
|
-
|
|
229
|
+
# Check if content is a numpy array (save as image)
|
|
230
|
+
try:
|
|
231
|
+
import numpy as np
|
|
232
|
+
if isinstance(content, np.ndarray):
|
|
233
|
+
if not to:
|
|
234
|
+
raise ValueError("'to' parameter is required when saving numpy arrays")
|
|
235
|
+
return self.save_image(content, to=to)
|
|
236
|
+
except ImportError:
|
|
237
|
+
pass # numpy not available
|
|
238
|
+
|
|
239
|
+
raise ValueError(f"Unsupported content type: {type(content)}. Expected str (file path), bytes, dict, list, or numpy.ndarray.")
|
|
228
240
|
|
|
229
241
|
def _save_file(
|
|
230
242
|
self,
|
|
@@ -288,23 +300,27 @@ class FileBuilder:
|
|
|
288
300
|
temp_path = os.path.join(temp_dir, filename)
|
|
289
301
|
# Create parent directories if filename contains path
|
|
290
302
|
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
303
|
+
|
|
304
|
+
with open(temp_path, 'wb') as f:
|
|
305
|
+
f.write(data)
|
|
306
|
+
result = self._save_file(
|
|
307
|
+
fpath=temp_path,
|
|
308
|
+
prefix=prefix,
|
|
309
|
+
description=description,
|
|
310
|
+
tags=tags,
|
|
311
|
+
metadata=metadata
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Only clean up if NOT queued for buffered upload
|
|
315
|
+
if result.get("status") != "queued":
|
|
302
316
|
try:
|
|
303
317
|
os.unlink(temp_path)
|
|
304
318
|
os.rmdir(temp_dir)
|
|
305
319
|
except Exception:
|
|
306
320
|
pass
|
|
307
321
|
|
|
322
|
+
return result
|
|
323
|
+
|
|
308
324
|
def _save_json(
|
|
309
325
|
self,
|
|
310
326
|
content: Any,
|
|
@@ -323,23 +339,27 @@ class FileBuilder:
|
|
|
323
339
|
temp_path = os.path.join(temp_dir, filename)
|
|
324
340
|
# Create parent directories if filename contains path
|
|
325
341
|
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
342
|
+
|
|
343
|
+
with open(temp_path, 'w') as f:
|
|
344
|
+
json.dump(content, f, indent=2)
|
|
345
|
+
result = self._save_file(
|
|
346
|
+
fpath=temp_path,
|
|
347
|
+
prefix=prefix,
|
|
348
|
+
description=description,
|
|
349
|
+
tags=tags,
|
|
350
|
+
metadata=metadata
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Only clean up if NOT queued for buffered upload
|
|
354
|
+
if result.get("status") != "queued":
|
|
337
355
|
try:
|
|
338
356
|
os.unlink(temp_path)
|
|
339
357
|
os.rmdir(temp_dir)
|
|
340
358
|
except Exception:
|
|
341
359
|
pass
|
|
342
360
|
|
|
361
|
+
return result
|
|
362
|
+
|
|
343
363
|
def _save_torch(
|
|
344
364
|
self,
|
|
345
365
|
model: Any,
|
|
@@ -358,22 +378,26 @@ class FileBuilder:
|
|
|
358
378
|
temp_path = os.path.join(temp_dir, filename)
|
|
359
379
|
# Create parent directories if filename contains path
|
|
360
380
|
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
381
|
+
|
|
382
|
+
torch.save(model, temp_path)
|
|
383
|
+
result = self._save_file(
|
|
384
|
+
fpath=temp_path,
|
|
385
|
+
prefix=prefix,
|
|
386
|
+
description=description,
|
|
387
|
+
tags=tags,
|
|
388
|
+
metadata=metadata
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Only clean up if NOT queued for buffered upload
|
|
392
|
+
if result.get("status") != "queued":
|
|
371
393
|
try:
|
|
372
394
|
os.unlink(temp_path)
|
|
373
395
|
os.rmdir(temp_dir)
|
|
374
396
|
except Exception:
|
|
375
397
|
pass
|
|
376
398
|
|
|
399
|
+
return result
|
|
400
|
+
|
|
377
401
|
def _save_fig(
|
|
378
402
|
self,
|
|
379
403
|
fig: Any,
|
|
@@ -393,23 +417,27 @@ class FileBuilder:
|
|
|
393
417
|
temp_path = os.path.join(temp_dir, filename)
|
|
394
418
|
# Create parent directories if filename contains path
|
|
395
419
|
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
420
|
+
|
|
421
|
+
fig.savefig(temp_path, **kwargs)
|
|
422
|
+
plt.close(fig)
|
|
423
|
+
result = self._save_file(
|
|
424
|
+
fpath=temp_path,
|
|
425
|
+
prefix=prefix,
|
|
426
|
+
description=description,
|
|
427
|
+
tags=tags,
|
|
428
|
+
metadata=metadata
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Only clean up if NOT queued for buffered upload
|
|
432
|
+
if result.get("status") != "queued":
|
|
407
433
|
try:
|
|
408
434
|
os.unlink(temp_path)
|
|
409
435
|
os.rmdir(temp_dir)
|
|
410
436
|
except Exception:
|
|
411
437
|
pass
|
|
412
438
|
|
|
439
|
+
return result
|
|
440
|
+
|
|
413
441
|
def _save_pickle(
|
|
414
442
|
self,
|
|
415
443
|
content: Any,
|
|
@@ -428,23 +456,107 @@ class FileBuilder:
|
|
|
428
456
|
temp_path = os.path.join(temp_dir, filename)
|
|
429
457
|
# Create parent directories if filename contains path
|
|
430
458
|
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
|
459
|
+
|
|
460
|
+
with open(temp_path, 'wb') as f:
|
|
461
|
+
pickle.dump(content, f)
|
|
462
|
+
result = self._save_file(
|
|
463
|
+
fpath=temp_path,
|
|
464
|
+
prefix=prefix,
|
|
465
|
+
description=description,
|
|
466
|
+
tags=tags,
|
|
467
|
+
metadata=metadata
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Only clean up if NOT queued for buffered upload
|
|
471
|
+
if result.get("status") != "queued":
|
|
472
|
+
try:
|
|
473
|
+
os.unlink(temp_path)
|
|
474
|
+
os.rmdir(temp_dir)
|
|
475
|
+
except Exception:
|
|
476
|
+
pass
|
|
477
|
+
|
|
478
|
+
return result
|
|
479
|
+
|
|
480
|
+
def _save_image(
|
|
481
|
+
self,
|
|
482
|
+
array: Any,
|
|
483
|
+
filename: str,
|
|
484
|
+
prefix: str,
|
|
485
|
+
description: Optional[str],
|
|
486
|
+
tags: Optional[List[str]],
|
|
487
|
+
metadata: Optional[Dict[str, Any]],
|
|
488
|
+
quality: int = 95
|
|
489
|
+
) -> Dict[str, Any]:
|
|
490
|
+
"""Save numpy array as image file."""
|
|
491
|
+
import tempfile
|
|
492
|
+
import os
|
|
493
|
+
|
|
431
494
|
try:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
495
|
+
from PIL import Image
|
|
496
|
+
import numpy as np
|
|
497
|
+
except ImportError:
|
|
498
|
+
raise ImportError("PIL/Pillow is required for saving images. Install it with: pip install Pillow")
|
|
499
|
+
|
|
500
|
+
temp_dir = tempfile.mkdtemp()
|
|
501
|
+
temp_path = os.path.join(temp_dir, filename)
|
|
502
|
+
# Create parent directories if filename contains path
|
|
503
|
+
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
|
504
|
+
|
|
505
|
+
# Convert numpy array to PIL Image
|
|
506
|
+
# Handle different array shapes and dtypes
|
|
507
|
+
if array.dtype == np.uint8:
|
|
508
|
+
img = Image.fromarray(array)
|
|
509
|
+
else:
|
|
510
|
+
# Normalize to 0-255 range for non-uint8 arrays
|
|
511
|
+
if array.max() <= 1.0:
|
|
512
|
+
# Assume normalized float in [0, 1]
|
|
513
|
+
array_uint8 = (array * 255).astype(np.uint8)
|
|
514
|
+
else:
|
|
515
|
+
# Scale to 0-255
|
|
516
|
+
array_uint8 = ((array - array.min()) / (array.max() - array.min()) * 255).astype(np.uint8)
|
|
517
|
+
img = Image.fromarray(array_uint8)
|
|
518
|
+
|
|
519
|
+
# Handle JPEG-specific requirements
|
|
520
|
+
file_ext = os.path.splitext(filename)[1].lower()
|
|
521
|
+
save_kwargs = {}
|
|
522
|
+
if file_ext in ['.jpg', '.jpeg']:
|
|
523
|
+
# JPEG doesn't support alpha channel, convert RGBA to RGB
|
|
524
|
+
if img.mode in ('RGBA', 'LA', 'P'):
|
|
525
|
+
# Create white background
|
|
526
|
+
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
527
|
+
if img.mode == 'P':
|
|
528
|
+
img = img.convert('RGBA')
|
|
529
|
+
background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
|
530
|
+
img = background
|
|
531
|
+
elif img.mode not in ('RGB', 'L'):
|
|
532
|
+
# Convert other modes to RGB
|
|
533
|
+
img = img.convert('RGB')
|
|
534
|
+
# Set JPEG quality
|
|
535
|
+
save_kwargs['quality'] = quality
|
|
536
|
+
save_kwargs['optimize'] = True
|
|
537
|
+
|
|
538
|
+
# Save image to temp file
|
|
539
|
+
img.save(temp_path, **save_kwargs)
|
|
540
|
+
|
|
541
|
+
result = self._save_file(
|
|
542
|
+
fpath=temp_path,
|
|
543
|
+
prefix=prefix,
|
|
544
|
+
description=description,
|
|
545
|
+
tags=tags,
|
|
546
|
+
metadata=metadata
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Only clean up if NOT queued for buffered upload
|
|
550
|
+
# If queued, the buffer manager will clean up after upload
|
|
551
|
+
if result.get("status") != "queued":
|
|
442
552
|
try:
|
|
443
553
|
os.unlink(temp_path)
|
|
444
554
|
os.rmdir(temp_dir)
|
|
445
555
|
except Exception:
|
|
446
556
|
pass
|
|
447
557
|
|
|
558
|
+
return result
|
|
559
|
+
|
|
448
560
|
def list(self, pattern: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
449
561
|
"""
|
|
450
562
|
List files with optional glob pattern filtering.
|
|
@@ -881,6 +993,49 @@ class FileBuilder:
|
|
|
881
993
|
metadata=self._metadata
|
|
882
994
|
)
|
|
883
995
|
|
|
996
|
+
def save_image(
|
|
997
|
+
self,
|
|
998
|
+
array: Any,
|
|
999
|
+
*,
|
|
1000
|
+
to: str,
|
|
1001
|
+
quality: int = 95
|
|
1002
|
+
) -> Dict[str, Any]:
|
|
1003
|
+
"""
|
|
1004
|
+
Save numpy array as an image file.
|
|
1005
|
+
|
|
1006
|
+
Args:
|
|
1007
|
+
array: Numpy array representing the image (HxW or HxWxC)
|
|
1008
|
+
to: Target filename (must have image extension like .png, .jpg, .jpeg)
|
|
1009
|
+
quality: JPEG quality (1-100, default: 95). Only used for JPEG files.
|
|
1010
|
+
|
|
1011
|
+
Returns:
|
|
1012
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
1013
|
+
|
|
1014
|
+
Examples:
|
|
1015
|
+
# Save rendered frame from MuJoCo as PNG
|
|
1016
|
+
pixels = renderer.render()
|
|
1017
|
+
result = dxp.files("frames").save_image(pixels, to="frame_001.png")
|
|
1018
|
+
|
|
1019
|
+
# Save as JPEG with custom quality
|
|
1020
|
+
result = dxp.files("frames").save_image(pixels, to="frame_001.jpg", quality=85)
|
|
1021
|
+
|
|
1022
|
+
# Save numpy array as image
|
|
1023
|
+
import numpy as np
|
|
1024
|
+
img_array = np.random.rand(480, 640, 3) * 255
|
|
1025
|
+
result = dxp.files("images").save_image(img_array, to="random.png")
|
|
1026
|
+
"""
|
|
1027
|
+
prefix = '/' + self._path.lstrip('/') if self._path else self._prefix
|
|
1028
|
+
|
|
1029
|
+
return self._save_image(
|
|
1030
|
+
array=array,
|
|
1031
|
+
filename=to,
|
|
1032
|
+
prefix=prefix,
|
|
1033
|
+
description=self._description,
|
|
1034
|
+
tags=self._tags,
|
|
1035
|
+
metadata=self._metadata,
|
|
1036
|
+
quality=quality
|
|
1037
|
+
)
|
|
1038
|
+
|
|
884
1039
|
def save_fig(
|
|
885
1040
|
self,
|
|
886
1041
|
fig: Optional[Any] = None,
|
|
@@ -974,29 +1129,32 @@ class FileBuilder:
|
|
|
974
1129
|
# Create parent directories if filename contains path
|
|
975
1130
|
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
|
976
1131
|
|
|
1132
|
+
frames_ubyte = img_as_ubyte(frame_stack)
|
|
977
1133
|
try:
|
|
978
|
-
frames_ubyte =
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1134
|
+
iio.imwrite(temp_path, frames_ubyte, fps=fps, **imageio_kwargs)
|
|
1135
|
+
except iio.core.NeedDownloadError:
|
|
1136
|
+
import imageio.plugins.ffmpeg
|
|
1137
|
+
imageio.plugins.ffmpeg.download()
|
|
1138
|
+
iio.imwrite(temp_path, frames_ubyte, fps=fps, **imageio_kwargs)
|
|
1139
|
+
|
|
1140
|
+
result = self._save_file(
|
|
1141
|
+
fpath=temp_path,
|
|
1142
|
+
prefix=prefix,
|
|
1143
|
+
description=self._description,
|
|
1144
|
+
tags=self._tags,
|
|
1145
|
+
metadata=self._metadata
|
|
1146
|
+
)
|
|
985
1147
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
prefix=prefix,
|
|
989
|
-
description=self._description,
|
|
990
|
-
tags=self._tags,
|
|
991
|
-
metadata=self._metadata
|
|
992
|
-
)
|
|
993
|
-
finally:
|
|
1148
|
+
# Only clean up if NOT queued for buffered upload
|
|
1149
|
+
if result.get("status") != "queued":
|
|
994
1150
|
try:
|
|
995
1151
|
os.unlink(temp_path)
|
|
996
1152
|
os.rmdir(temp_dir)
|
|
997
1153
|
except Exception:
|
|
998
1154
|
pass
|
|
999
1155
|
|
|
1156
|
+
return result
|
|
1157
|
+
|
|
1000
1158
|
def duplicate(self, source: Union[str, Dict[str, Any]], to: str) -> Dict[str, Any]:
|
|
1001
1159
|
"""
|
|
1002
1160
|
Duplicate an existing file to a new path within the same experiment.
|