ml-dash 0.6.1__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 +85 -0
- ml_dash/auth/__init__.py +51 -0
- ml_dash/auth/constants.py +10 -0
- ml_dash/auth/device_flow.py +237 -0
- ml_dash/auth/device_secret.py +49 -0
- ml_dash/auth/exceptions.py +31 -0
- ml_dash/auth/token_storage.py +262 -0
- ml_dash/auto_start.py +52 -0
- ml_dash/cli.py +79 -0
- ml_dash/cli_commands/__init__.py +1 -0
- ml_dash/cli_commands/download.py +769 -0
- ml_dash/cli_commands/list.py +319 -0
- ml_dash/cli_commands/login.py +225 -0
- ml_dash/cli_commands/logout.py +54 -0
- ml_dash/cli_commands/upload.py +1248 -0
- ml_dash/client.py +1003 -0
- ml_dash/config.py +133 -0
- ml_dash/experiment.py +1116 -0
- ml_dash/files.py +785 -0
- ml_dash/log.py +181 -0
- ml_dash/metric.py +481 -0
- ml_dash/params.py +277 -0
- ml_dash/py.typed +0 -0
- ml_dash/remote_auto_start.py +55 -0
- ml_dash/storage.py +1127 -0
- ml_dash-0.6.1.dist-info/METADATA +248 -0
- ml_dash-0.6.1.dist-info/RECORD +29 -0
- ml_dash-0.6.1.dist-info/WHEEL +4 -0
- ml_dash-0.6.1.dist-info/entry_points.txt +3 -0
ml_dash/files.py
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Files module for ML-Dash SDK.
|
|
3
|
+
|
|
4
|
+
Provides fluent API for file upload, download, list, and delete operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import mimetypes
|
|
9
|
+
from typing import Dict, Any, List, Optional, Union, TYPE_CHECKING
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .experiment import Experiment
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileBuilder:
|
|
17
|
+
"""
|
|
18
|
+
Fluent interface for file operations.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
# Upload file
|
|
22
|
+
experiment.files(file_path="./model.pt", prefix="/models").save()
|
|
23
|
+
|
|
24
|
+
# List files
|
|
25
|
+
files = experiment.files().list()
|
|
26
|
+
files = experiment.files(prefix="/models").list()
|
|
27
|
+
|
|
28
|
+
# Download file
|
|
29
|
+
experiment.files(file_id="123").download()
|
|
30
|
+
experiment.files(file_id="123", dest_path="./model.pt").download()
|
|
31
|
+
|
|
32
|
+
# Delete file
|
|
33
|
+
experiment.files(file_id="123").delete()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, experiment: 'Experiment', **kwargs):
|
|
37
|
+
"""
|
|
38
|
+
Initialize file builder.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
experiment: Parent experiment instance
|
|
42
|
+
**kwargs: File operation parameters
|
|
43
|
+
- file_path: Path to file to upload
|
|
44
|
+
- prefix: Logical path prefix (default: "/")
|
|
45
|
+
- description: Optional description
|
|
46
|
+
- tags: Optional list of tags
|
|
47
|
+
- bindrs: Optional list of bindrs
|
|
48
|
+
- metadata: Optional metadata dict
|
|
49
|
+
- file_id: File ID for download/delete/update operations
|
|
50
|
+
- dest_path: Destination path for download
|
|
51
|
+
"""
|
|
52
|
+
self._experiment = experiment
|
|
53
|
+
self._file_path = kwargs.get('file_path')
|
|
54
|
+
self._prefix = kwargs.get('prefix', '/')
|
|
55
|
+
self._description = kwargs.get('description')
|
|
56
|
+
self._tags = kwargs.get('tags', [])
|
|
57
|
+
self._bindrs = kwargs.get('bindrs', [])
|
|
58
|
+
self._metadata = kwargs.get('metadata')
|
|
59
|
+
self._file_id = kwargs.get('file_id')
|
|
60
|
+
self._dest_path = kwargs.get('dest_path')
|
|
61
|
+
|
|
62
|
+
def save(self) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Upload and save the file.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
RuntimeError: If experiment is not open or write-protected
|
|
71
|
+
ValueError: If file_path not provided or file doesn't exist
|
|
72
|
+
ValueError: If file size exceeds 100GB limit
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
result = experiment.files(file_path="./model.pt", prefix="/models").save()
|
|
76
|
+
# Returns: {"id": "123", "path": "/models", "filename": "model.pt", ...}
|
|
77
|
+
"""
|
|
78
|
+
if not self._experiment._is_open:
|
|
79
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
80
|
+
|
|
81
|
+
if self._experiment._write_protected:
|
|
82
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
83
|
+
|
|
84
|
+
if not self._file_path:
|
|
85
|
+
raise ValueError("file_path is required for save() operation")
|
|
86
|
+
|
|
87
|
+
file_path = Path(self._file_path)
|
|
88
|
+
if not file_path.exists():
|
|
89
|
+
raise ValueError(f"File not found: {self._file_path}")
|
|
90
|
+
|
|
91
|
+
if not file_path.is_file():
|
|
92
|
+
raise ValueError(f"Path is not a file: {self._file_path}")
|
|
93
|
+
|
|
94
|
+
# Check file size (max 100GB)
|
|
95
|
+
file_size = file_path.stat().st_size
|
|
96
|
+
MAX_FILE_SIZE = 100 * 1024 * 1024 * 1024 # 100GB in bytes
|
|
97
|
+
if file_size > MAX_FILE_SIZE:
|
|
98
|
+
raise ValueError(f"File size ({file_size} bytes) exceeds 100GB limit")
|
|
99
|
+
|
|
100
|
+
# Compute checksum
|
|
101
|
+
checksum = compute_sha256(str(file_path))
|
|
102
|
+
|
|
103
|
+
# Detect MIME type
|
|
104
|
+
content_type = get_mime_type(str(file_path))
|
|
105
|
+
|
|
106
|
+
# Get filename
|
|
107
|
+
filename = file_path.name
|
|
108
|
+
|
|
109
|
+
# Upload through experiment
|
|
110
|
+
return self._experiment._upload_file(
|
|
111
|
+
file_path=str(file_path),
|
|
112
|
+
prefix=self._prefix,
|
|
113
|
+
filename=filename,
|
|
114
|
+
description=self._description,
|
|
115
|
+
tags=self._tags,
|
|
116
|
+
metadata=self._metadata,
|
|
117
|
+
checksum=checksum,
|
|
118
|
+
content_type=content_type,
|
|
119
|
+
size_bytes=file_size
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def list(self) -> List[Dict[str, Any]]:
|
|
123
|
+
"""
|
|
124
|
+
List files with optional filters.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of file metadata dicts
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
RuntimeError: If experiment is not open
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
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
|
+
"""
|
|
137
|
+
if not self._experiment._is_open:
|
|
138
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
139
|
+
|
|
140
|
+
return self._experiment._list_files(
|
|
141
|
+
prefix=self._prefix if self._prefix != '/' else None,
|
|
142
|
+
tags=self._tags if self._tags else None
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def download(self) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Download file with automatic checksum verification.
|
|
148
|
+
|
|
149
|
+
If dest_path not provided, downloads to current directory with original filename.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Path to downloaded file
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
RuntimeError: If experiment is not open
|
|
156
|
+
ValueError: If file_id not provided
|
|
157
|
+
ValueError: If checksum verification fails
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
# Download to current directory with original filename
|
|
161
|
+
path = experiment.files(file_id="123").download()
|
|
162
|
+
|
|
163
|
+
# Download to custom path
|
|
164
|
+
path = experiment.files(file_id="123", dest_path="./model.pt").download()
|
|
165
|
+
"""
|
|
166
|
+
if not self._experiment._is_open:
|
|
167
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
168
|
+
|
|
169
|
+
if not self._file_id:
|
|
170
|
+
raise ValueError("file_id is required for download() operation")
|
|
171
|
+
|
|
172
|
+
return self._experiment._download_file(
|
|
173
|
+
file_id=self._file_id,
|
|
174
|
+
dest_path=self._dest_path
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def delete(self) -> Dict[str, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Delete file (soft delete).
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dict with id and deletedAt timestamp
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
RuntimeError: If experiment is not open or write-protected
|
|
186
|
+
ValueError: If file_id not provided
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
result = experiment.files(file_id="123").delete()
|
|
190
|
+
"""
|
|
191
|
+
if not self._experiment._is_open:
|
|
192
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
193
|
+
|
|
194
|
+
if self._experiment._write_protected:
|
|
195
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
196
|
+
|
|
197
|
+
if not self._file_id:
|
|
198
|
+
raise ValueError("file_id is required for delete() operation")
|
|
199
|
+
|
|
200
|
+
return self._experiment._delete_file(file_id=self._file_id)
|
|
201
|
+
|
|
202
|
+
def update(self) -> Dict[str, Any]:
|
|
203
|
+
"""
|
|
204
|
+
Update file metadata (description, tags, metadata).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Updated file metadata dict
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
RuntimeError: If experiment is not open or write-protected
|
|
211
|
+
ValueError: If file_id not provided
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
result = experiment.files(
|
|
215
|
+
file_id="123",
|
|
216
|
+
description="Updated description",
|
|
217
|
+
tags=["new", "tags"],
|
|
218
|
+
metadata={"updated": True}
|
|
219
|
+
).update()
|
|
220
|
+
"""
|
|
221
|
+
if not self._experiment._is_open:
|
|
222
|
+
raise RuntimeError("Experiment not open. Use experiment.open() or context manager.")
|
|
223
|
+
|
|
224
|
+
if self._experiment._write_protected:
|
|
225
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
226
|
+
|
|
227
|
+
if not self._file_id:
|
|
228
|
+
raise ValueError("file_id is required for update() operation")
|
|
229
|
+
|
|
230
|
+
return self._experiment._update_file(
|
|
231
|
+
file_id=self._file_id,
|
|
232
|
+
description=self._description,
|
|
233
|
+
tags=self._tags,
|
|
234
|
+
metadata=self._metadata
|
|
235
|
+
)
|
|
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.files(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 with desired filename
|
|
267
|
+
temp_dir = tempfile.mkdtemp()
|
|
268
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
269
|
+
try:
|
|
270
|
+
# Write JSON content to temp file
|
|
271
|
+
with open(temp_path, 'w') as f:
|
|
272
|
+
json.dump(content, f, indent=2)
|
|
273
|
+
|
|
274
|
+
# Save using existing save() method
|
|
275
|
+
original_file_path = self._file_path
|
|
276
|
+
self._file_path = temp_path
|
|
277
|
+
|
|
278
|
+
# Upload and get result
|
|
279
|
+
result = self.save()
|
|
280
|
+
|
|
281
|
+
# Restore original file_path
|
|
282
|
+
self._file_path = original_file_path
|
|
283
|
+
|
|
284
|
+
return result
|
|
285
|
+
finally:
|
|
286
|
+
# Clean up temp file and directory
|
|
287
|
+
try:
|
|
288
|
+
os.unlink(temp_path)
|
|
289
|
+
os.rmdir(temp_dir)
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
def save_torch(self, model: Any, file_name: str) -> Dict[str, Any]:
|
|
294
|
+
"""
|
|
295
|
+
Save PyTorch model to a file.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
model: PyTorch model or state dict to save
|
|
299
|
+
file_name: Name of the file to create (should end with .pt or .pth)
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
File metadata dict with id, path, filename, checksum, etc.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
RuntimeError: If experiment is not open or write-protected
|
|
306
|
+
ImportError: If torch is not installed
|
|
307
|
+
ValueError: If model cannot be saved
|
|
308
|
+
|
|
309
|
+
Examples:
|
|
310
|
+
import torch
|
|
311
|
+
model = torch.nn.Linear(10, 5)
|
|
312
|
+
result = experiment.files(prefix="/models").save_torch(model, "model.pt")
|
|
313
|
+
|
|
314
|
+
# Or save state dict
|
|
315
|
+
result = experiment.files(prefix="/models").save_torch(model.state_dict(), "model.pth")
|
|
316
|
+
"""
|
|
317
|
+
import tempfile
|
|
318
|
+
import os
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
import torch
|
|
322
|
+
except ImportError:
|
|
323
|
+
raise ImportError("PyTorch is not installed. Install it with: pip install torch")
|
|
324
|
+
|
|
325
|
+
if not self._experiment._is_open:
|
|
326
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
327
|
+
|
|
328
|
+
if self._experiment._write_protected:
|
|
329
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
330
|
+
|
|
331
|
+
# Create temporary file with desired filename
|
|
332
|
+
temp_dir = tempfile.mkdtemp()
|
|
333
|
+
temp_path = os.path.join(temp_dir, file_name)
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
# Save model to temp file
|
|
337
|
+
torch.save(model, temp_path)
|
|
338
|
+
|
|
339
|
+
# Save using existing save() method
|
|
340
|
+
original_file_path = self._file_path
|
|
341
|
+
self._file_path = temp_path
|
|
342
|
+
|
|
343
|
+
# Upload and get result
|
|
344
|
+
result = self.save()
|
|
345
|
+
|
|
346
|
+
# Restore original file_path
|
|
347
|
+
self._file_path = original_file_path
|
|
348
|
+
|
|
349
|
+
return result
|
|
350
|
+
finally:
|
|
351
|
+
# Clean up temp file and directory
|
|
352
|
+
try:
|
|
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)
|
|
607
|
+
except Exception:
|
|
608
|
+
pass
|
|
609
|
+
|
|
610
|
+
def duplicate(self, source: Union[str, Dict[str, Any]], to: str) -> Dict[str, Any]:
|
|
611
|
+
"""
|
|
612
|
+
Duplicate an existing file to a new path within the same experiment.
|
|
613
|
+
|
|
614
|
+
Useful for checkpoint rotation patterns where you save versioned checkpoints
|
|
615
|
+
and maintain a "latest" or "best" pointer.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
source: Source file - either file ID (str) or metadata dict with 'id' key
|
|
619
|
+
to: Target path like "models/latest.pt" or "/checkpoints/best.pt"
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
File metadata dict for the duplicated file with id, path, filename, checksum, etc.
|
|
623
|
+
|
|
624
|
+
Raises:
|
|
625
|
+
RuntimeError: If experiment is not open or write-protected
|
|
626
|
+
ValueError: If source file not found or target path invalid
|
|
627
|
+
|
|
628
|
+
Examples:
|
|
629
|
+
# Using file ID
|
|
630
|
+
dxp.files().duplicate("file-id-123", to="models/latest.pt")
|
|
631
|
+
|
|
632
|
+
# Using metadata dict from save_torch
|
|
633
|
+
snapshot = dxp.files(prefix="/models").save_torch(model, f"model_{epoch:05d}.pt")
|
|
634
|
+
dxp.files().duplicate(snapshot, to="models/latest.pt")
|
|
635
|
+
|
|
636
|
+
# Checkpoint rotation pattern
|
|
637
|
+
snap = dxp.files(prefix="/checkpoints").save_torch(model, f"model_{epoch:05d}.pt")
|
|
638
|
+
dxp.files().duplicate(snap, to="checkpoints/best.pt")
|
|
639
|
+
"""
|
|
640
|
+
import tempfile
|
|
641
|
+
import os
|
|
642
|
+
|
|
643
|
+
if not self._experiment._is_open:
|
|
644
|
+
raise RuntimeError("Experiment not open. Use experiment.run.start() or context manager.")
|
|
645
|
+
|
|
646
|
+
if self._experiment._write_protected:
|
|
647
|
+
raise RuntimeError("Experiment is write-protected and cannot be modified.")
|
|
648
|
+
|
|
649
|
+
# Extract source file ID
|
|
650
|
+
if isinstance(source, str):
|
|
651
|
+
source_id = source
|
|
652
|
+
elif isinstance(source, dict) and 'id' in source:
|
|
653
|
+
source_id = source['id']
|
|
654
|
+
else:
|
|
655
|
+
raise ValueError("source must be a file ID (str) or metadata dict with 'id' key")
|
|
656
|
+
|
|
657
|
+
if not source_id:
|
|
658
|
+
raise ValueError("Invalid source: file ID is empty")
|
|
659
|
+
|
|
660
|
+
# Parse target path into prefix and filename
|
|
661
|
+
to = to.lstrip('/')
|
|
662
|
+
if '/' in to:
|
|
663
|
+
target_prefix, target_filename = to.rsplit('/', 1)
|
|
664
|
+
target_prefix = '/' + target_prefix
|
|
665
|
+
else:
|
|
666
|
+
target_prefix = '/'
|
|
667
|
+
target_filename = to
|
|
668
|
+
|
|
669
|
+
if not target_filename:
|
|
670
|
+
raise ValueError(f"Invalid target path '{to}': must include filename")
|
|
671
|
+
|
|
672
|
+
# Download source file to temp location
|
|
673
|
+
temp_dir = tempfile.mkdtemp()
|
|
674
|
+
temp_path = os.path.join(temp_dir, target_filename)
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
# Download the source file
|
|
678
|
+
downloaded_path = self._experiment._download_file(
|
|
679
|
+
file_id=source_id,
|
|
680
|
+
dest_path=temp_path
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Save to new location using existing save() method
|
|
684
|
+
original_file_path = self._file_path
|
|
685
|
+
original_prefix = self._prefix
|
|
686
|
+
|
|
687
|
+
self._file_path = downloaded_path
|
|
688
|
+
self._prefix = target_prefix
|
|
689
|
+
|
|
690
|
+
# Upload and get result
|
|
691
|
+
result = self.save()
|
|
692
|
+
|
|
693
|
+
# Restore original values
|
|
694
|
+
self._file_path = original_file_path
|
|
695
|
+
self._prefix = original_prefix
|
|
696
|
+
|
|
697
|
+
return result
|
|
698
|
+
finally:
|
|
699
|
+
# Clean up temp file and directory
|
|
700
|
+
try:
|
|
701
|
+
if os.path.exists(temp_path):
|
|
702
|
+
os.unlink(temp_path)
|
|
703
|
+
os.rmdir(temp_dir)
|
|
704
|
+
except Exception:
|
|
705
|
+
pass
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def compute_sha256(file_path: str) -> str:
|
|
709
|
+
"""
|
|
710
|
+
Compute SHA256 checksum of a file.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
file_path: Path to file
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
Hex-encoded SHA256 checksum
|
|
717
|
+
|
|
718
|
+
Examples:
|
|
719
|
+
checksum = compute_sha256("./model.pt")
|
|
720
|
+
# Returns: "abc123def456..."
|
|
721
|
+
"""
|
|
722
|
+
sha256_hash = hashlib.sha256()
|
|
723
|
+
|
|
724
|
+
with open(file_path, "rb") as f:
|
|
725
|
+
# Read file in chunks to handle large files
|
|
726
|
+
for byte_block in iter(lambda: f.read(8192), b""):
|
|
727
|
+
sha256_hash.update(byte_block)
|
|
728
|
+
|
|
729
|
+
return sha256_hash.hexdigest()
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def get_mime_type(file_path: str) -> str:
|
|
733
|
+
"""
|
|
734
|
+
Detect MIME type of a file.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
file_path: Path to file
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
MIME type string (default: "application/octet-stream")
|
|
741
|
+
|
|
742
|
+
Examples:
|
|
743
|
+
mime_type = get_mime_type("./model.pt")
|
|
744
|
+
# Returns: "application/octet-stream"
|
|
745
|
+
|
|
746
|
+
mime_type = get_mime_type("./image.png")
|
|
747
|
+
# Returns: "image/png"
|
|
748
|
+
"""
|
|
749
|
+
mime_type, _ = mimetypes.guess_type(file_path)
|
|
750
|
+
return mime_type or "application/octet-stream"
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def verify_checksum(file_path: str, expected_checksum: str) -> bool:
|
|
754
|
+
"""
|
|
755
|
+
Verify SHA256 checksum of a file.
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
file_path: Path to file
|
|
759
|
+
expected_checksum: Expected SHA256 checksum (hex-encoded)
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
True if checksum matches, False otherwise
|
|
763
|
+
|
|
764
|
+
Examples:
|
|
765
|
+
is_valid = verify_checksum("./model.pt", "abc123...")
|
|
766
|
+
"""
|
|
767
|
+
actual_checksum = compute_sha256(file_path)
|
|
768
|
+
return actual_checksum == expected_checksum
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def generate_snowflake_id() -> str:
|
|
772
|
+
"""
|
|
773
|
+
Generate a simple Snowflake-like ID for local mode.
|
|
774
|
+
|
|
775
|
+
Not a true Snowflake ID, but provides unique IDs for local storage.
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
String representation of generated ID
|
|
779
|
+
"""
|
|
780
|
+
import time
|
|
781
|
+
import random
|
|
782
|
+
|
|
783
|
+
timestamp = int(time.time() * 1000)
|
|
784
|
+
random_bits = random.randint(0, 4095)
|
|
785
|
+
return str((timestamp << 12) | random_bits)
|