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/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)