clouditia 1.2.4__tar.gz → 1.3.0__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.4
2
2
  Name: clouditia
3
- Version: 1.2.4
3
+ Version: 1.3.0
4
4
  Summary: Execute Python and Shell code on remote GPU sessions
5
5
  Author-email: Aina KIKI-SAGBE <support@clouditia.com>
6
6
  Maintainer-email: Aina KIKI-SAGBE <support@clouditia.com>
@@ -81,6 +81,7 @@ print(result.output)
81
81
  - **Shell Commands**: Execute shell commands on the GPU pod
82
82
  - **Persistent Sessions**: Keep variables between executions with `start()`/`stop()`
83
83
  - **Variable Transfer**: Send and retrieve variables between local and remote
84
+ - **File Transfer**: Upload/download files and folders between local and remote
84
85
  - **Async Jobs**: Submit long-running tasks with real-time log monitoring
85
86
  - **Jupyter Magic**: Use `%%clouditia` magic in notebooks
86
87
  - **Decorator Support**: Use `@session.remote` to run functions on GPU
@@ -95,11 +96,12 @@ print(result.output)
95
96
  4. [Executing Python Code](#executing-python-code)
96
97
  5. [Shell Commands](#shell-commands)
97
98
  6. [Variable Transfer](#variable-transfer)
98
- 7. [Remote Functions (Decorator)](#remote-functions-decorator)
99
- 8. [Async Jobs (Long-Running Tasks)](#async-jobs-long-running-tasks)
100
- 9. [Jupyter Magic](#jupyter-magic)
101
- 10. [Error Handling](#error-handling)
102
- 11. [API Reference](#api-reference)
99
+ 7. [File Transfer](#file-transfer)
100
+ 8. [Remote Functions (Decorator)](#remote-functions-decorator)
101
+ 9. [Async Jobs (Long-Running Tasks)](#async-jobs-long-running-tasks)
102
+ 10. [Jupyter Magic](#jupyter-magic)
103
+ 11. [Error Handling](#error-handling)
104
+ 12. [API Reference](#api-reference)
103
105
 
104
106
  ---
105
107
 
@@ -377,6 +379,182 @@ print(f"Learning rate: {config['learning_rate']}")
377
379
 
378
380
  ---
379
381
 
382
+ ## File Transfer
383
+
384
+ Transfer files and folders between your local machine and the remote GPU session.
385
+
386
+ ### Uploading a Single File
387
+
388
+ ```python
389
+ # Upload a local file to the remote session
390
+ session.upload("./data.csv", "/home/coder/workspace/data.csv")
391
+
392
+ # Upload with custom path
393
+ session.upload("./model.pkl", "/home/coder/workspace/models/trained_model.pkl")
394
+
395
+ # Disable progress output
396
+ session.upload("./config.json", "/home/coder/workspace/config.json", show_progress=False)
397
+ ```
398
+
399
+ ### Downloading a Single File
400
+
401
+ ```python
402
+ # Download a file from the remote session
403
+ session.download("/home/coder/workspace/results.csv", "./results.csv")
404
+
405
+ # Download trained model
406
+ session.download("/home/coder/workspace/checkpoints/model.pt", "./local_model.pt")
407
+
408
+ # Download silently
409
+ session.download("/home/coder/workspace/logs.txt", "./logs.txt", show_progress=False)
410
+ ```
411
+
412
+ ### Uploading a Folder
413
+
414
+ Upload an entire directory with all its contents:
415
+
416
+ ```python
417
+ # Upload a project folder
418
+ session.upload_folder("./my_project", "/home/coder/workspace/project")
419
+
420
+ # Upload with exclusions (default excludes: __pycache__, .git, *.pyc, .DS_Store, node_modules)
421
+ session.upload_folder(
422
+ "./my_project",
423
+ "/home/coder/workspace/project",
424
+ exclude=["*.log", ".env", "__pycache__", ".git"]
425
+ )
426
+
427
+ # Upload data folder
428
+ session.upload_folder("./datasets", "/home/coder/workspace/data")
429
+ ```
430
+
431
+ ### Downloading a Folder
432
+
433
+ Download an entire directory with all its contents:
434
+
435
+ ```python
436
+ # Download results folder
437
+ session.download_folder("/home/coder/workspace/results", "./local_results")
438
+
439
+ # Download checkpoints
440
+ session.download_folder(
441
+ "/home/coder/workspace/checkpoints",
442
+ "./checkpoints",
443
+ exclude=["*.tmp", "*.log"]
444
+ )
445
+
446
+ # Download trained models
447
+ session.download_folder("/home/coder/workspace/models", "./downloaded_models")
448
+ ```
449
+
450
+ ### Listing Remote Files
451
+
452
+ ```python
453
+ # List files in a directory
454
+ files = session.list_files("/home/coder/workspace")
455
+ for f in files:
456
+ icon = "📁" if f["is_dir"] else "📄"
457
+ print(f"{icon} {f['name']} - {f['size']} bytes")
458
+
459
+ # Filter by pattern
460
+ python_files = session.list_files("/home/coder/workspace", pattern="*.py")
461
+ for f in python_files:
462
+ print(f"📄 {f['name']}")
463
+
464
+ # List with full details
465
+ files = session.list_files("/home/coder/workspace")
466
+ for f in files:
467
+ print(f"Name: {f['name']}")
468
+ print(f" Path: {f['path']}")
469
+ print(f" Size: {f['size']} bytes")
470
+ print(f" Is Directory: {f['is_dir']}")
471
+ print(f" Modified: {f['modified']}")
472
+ ```
473
+
474
+ ### Checking if a File Exists
475
+
476
+ ```python
477
+ # Check before downloading
478
+ if session.file_exists("/home/coder/workspace/model.pt"):
479
+ session.download("/home/coder/workspace/model.pt", "./model.pt")
480
+ print("Model downloaded!")
481
+ else:
482
+ print("Model not found, training required...")
483
+
484
+ # Check multiple files
485
+ files_to_check = ["config.json", "data.csv", "model.pt"]
486
+ for filename in files_to_check:
487
+ path = f"/home/coder/workspace/{filename}"
488
+ exists = session.file_exists(path)
489
+ status = "✓" if exists else "✗"
490
+ print(f"{status} {filename}")
491
+ ```
492
+
493
+ ### Complete Workflow Example
494
+
495
+ ```python
496
+ from clouditia import GPUSession
497
+
498
+ session = GPUSession("ck_your_api_key")
499
+
500
+ # 1. Upload training data and code
501
+ session.upload_folder("./training_code", "/home/coder/workspace/code")
502
+ session.upload("./data/train.csv", "/home/coder/workspace/data/train.csv")
503
+ session.upload("./data/test.csv", "/home/coder/workspace/data/test.csv")
504
+
505
+ # 2. Run training
506
+ result = session.run("""
507
+ import sys
508
+ sys.path.insert(0, '/home/coder/workspace/code')
509
+ from train import train_model
510
+
511
+ model = train_model('/home/coder/workspace/data/train.csv')
512
+ model.save('/home/coder/workspace/output/model.pt')
513
+ print("Training complete!")
514
+ """)
515
+
516
+ # 3. Check and download results
517
+ if session.file_exists("/home/coder/workspace/output/model.pt"):
518
+ session.download("/home/coder/workspace/output/model.pt", "./trained_model.pt")
519
+ print("Model saved locally!")
520
+
521
+ # 4. Download all outputs
522
+ session.download_folder("/home/coder/workspace/output", "./results")
523
+ print("All results downloaded!")
524
+
525
+ # 5. List what was created
526
+ files = session.list_files("/home/coder/workspace/output")
527
+ print(f"Created {len(files)} files during training")
528
+ ```
529
+
530
+ ### Working with Different File Types
531
+
532
+ ```python
533
+ # CSV files
534
+ session.upload("./data.csv", "/home/coder/workspace/data.csv")
535
+
536
+ # Pickle files (models, data)
537
+ session.upload("./model.pkl", "/home/coder/workspace/model.pkl")
538
+
539
+ # PyTorch models
540
+ session.download("/home/coder/workspace/checkpoint.pt", "./checkpoint.pt")
541
+
542
+ # JSON configuration
543
+ session.upload("./config.json", "/home/coder/workspace/config.json")
544
+
545
+ # Text files
546
+ session.upload("./requirements.txt", "/home/coder/workspace/requirements.txt")
547
+
548
+ # Binary files
549
+ session.upload("./image.png", "/home/coder/workspace/image.png")
550
+
551
+ # Any file type works!
552
+ session.upload("./data.parquet", "/home/coder/workspace/data.parquet")
553
+ session.upload("./weights.h5", "/home/coder/workspace/weights.h5")
554
+ ```
555
+
556
+ ---
557
+
380
558
  ## Remote Functions (Decorator)
381
559
 
382
560
  Use the `@session.remote` decorator to run functions on the GPU:
@@ -707,6 +885,12 @@ GPUSession(
707
885
  | `stop()` | Stop the persistent session |
708
886
  | `set(name, value)` | Send variable to remote |
709
887
  | `get(name)` | Retrieve variable from remote |
888
+ | `upload(local_path, remote_path, show_progress=True)` | Upload a file to remote session |
889
+ | `download(remote_path, local_path, show_progress=True)` | Download a file from remote session |
890
+ | `upload_folder(local_path, remote_path, exclude=None)` | Upload a folder to remote session |
891
+ | `download_folder(remote_path, local_path, exclude=None)` | Download a folder from remote session |
892
+ | `list_files(remote_path, pattern=None)` | List files in remote directory |
893
+ | `file_exists(remote_path)` | Check if a file exists on remote |
710
894
  | `submit(code, name=None, job_type="python")` | Submit async job |
711
895
  | `jobs(status=None, limit=10)` | List jobs |
712
896
  | `gpu_info()` | Get GPU information |
@@ -38,6 +38,7 @@ print(result.output)
38
38
  - **Shell Commands**: Execute shell commands on the GPU pod
39
39
  - **Persistent Sessions**: Keep variables between executions with `start()`/`stop()`
40
40
  - **Variable Transfer**: Send and retrieve variables between local and remote
41
+ - **File Transfer**: Upload/download files and folders between local and remote
41
42
  - **Async Jobs**: Submit long-running tasks with real-time log monitoring
42
43
  - **Jupyter Magic**: Use `%%clouditia` magic in notebooks
43
44
  - **Decorator Support**: Use `@session.remote` to run functions on GPU
@@ -52,11 +53,12 @@ print(result.output)
52
53
  4. [Executing Python Code](#executing-python-code)
53
54
  5. [Shell Commands](#shell-commands)
54
55
  6. [Variable Transfer](#variable-transfer)
55
- 7. [Remote Functions (Decorator)](#remote-functions-decorator)
56
- 8. [Async Jobs (Long-Running Tasks)](#async-jobs-long-running-tasks)
57
- 9. [Jupyter Magic](#jupyter-magic)
58
- 10. [Error Handling](#error-handling)
59
- 11. [API Reference](#api-reference)
56
+ 7. [File Transfer](#file-transfer)
57
+ 8. [Remote Functions (Decorator)](#remote-functions-decorator)
58
+ 9. [Async Jobs (Long-Running Tasks)](#async-jobs-long-running-tasks)
59
+ 10. [Jupyter Magic](#jupyter-magic)
60
+ 11. [Error Handling](#error-handling)
61
+ 12. [API Reference](#api-reference)
60
62
 
61
63
  ---
62
64
 
@@ -334,6 +336,182 @@ print(f"Learning rate: {config['learning_rate']}")
334
336
 
335
337
  ---
336
338
 
339
+ ## File Transfer
340
+
341
+ Transfer files and folders between your local machine and the remote GPU session.
342
+
343
+ ### Uploading a Single File
344
+
345
+ ```python
346
+ # Upload a local file to the remote session
347
+ session.upload("./data.csv", "/home/coder/workspace/data.csv")
348
+
349
+ # Upload with custom path
350
+ session.upload("./model.pkl", "/home/coder/workspace/models/trained_model.pkl")
351
+
352
+ # Disable progress output
353
+ session.upload("./config.json", "/home/coder/workspace/config.json", show_progress=False)
354
+ ```
355
+
356
+ ### Downloading a Single File
357
+
358
+ ```python
359
+ # Download a file from the remote session
360
+ session.download("/home/coder/workspace/results.csv", "./results.csv")
361
+
362
+ # Download trained model
363
+ session.download("/home/coder/workspace/checkpoints/model.pt", "./local_model.pt")
364
+
365
+ # Download silently
366
+ session.download("/home/coder/workspace/logs.txt", "./logs.txt", show_progress=False)
367
+ ```
368
+
369
+ ### Uploading a Folder
370
+
371
+ Upload an entire directory with all its contents:
372
+
373
+ ```python
374
+ # Upload a project folder
375
+ session.upload_folder("./my_project", "/home/coder/workspace/project")
376
+
377
+ # Upload with exclusions (default excludes: __pycache__, .git, *.pyc, .DS_Store, node_modules)
378
+ session.upload_folder(
379
+ "./my_project",
380
+ "/home/coder/workspace/project",
381
+ exclude=["*.log", ".env", "__pycache__", ".git"]
382
+ )
383
+
384
+ # Upload data folder
385
+ session.upload_folder("./datasets", "/home/coder/workspace/data")
386
+ ```
387
+
388
+ ### Downloading a Folder
389
+
390
+ Download an entire directory with all its contents:
391
+
392
+ ```python
393
+ # Download results folder
394
+ session.download_folder("/home/coder/workspace/results", "./local_results")
395
+
396
+ # Download checkpoints
397
+ session.download_folder(
398
+ "/home/coder/workspace/checkpoints",
399
+ "./checkpoints",
400
+ exclude=["*.tmp", "*.log"]
401
+ )
402
+
403
+ # Download trained models
404
+ session.download_folder("/home/coder/workspace/models", "./downloaded_models")
405
+ ```
406
+
407
+ ### Listing Remote Files
408
+
409
+ ```python
410
+ # List files in a directory
411
+ files = session.list_files("/home/coder/workspace")
412
+ for f in files:
413
+ icon = "📁" if f["is_dir"] else "📄"
414
+ print(f"{icon} {f['name']} - {f['size']} bytes")
415
+
416
+ # Filter by pattern
417
+ python_files = session.list_files("/home/coder/workspace", pattern="*.py")
418
+ for f in python_files:
419
+ print(f"📄 {f['name']}")
420
+
421
+ # List with full details
422
+ files = session.list_files("/home/coder/workspace")
423
+ for f in files:
424
+ print(f"Name: {f['name']}")
425
+ print(f" Path: {f['path']}")
426
+ print(f" Size: {f['size']} bytes")
427
+ print(f" Is Directory: {f['is_dir']}")
428
+ print(f" Modified: {f['modified']}")
429
+ ```
430
+
431
+ ### Checking if a File Exists
432
+
433
+ ```python
434
+ # Check before downloading
435
+ if session.file_exists("/home/coder/workspace/model.pt"):
436
+ session.download("/home/coder/workspace/model.pt", "./model.pt")
437
+ print("Model downloaded!")
438
+ else:
439
+ print("Model not found, training required...")
440
+
441
+ # Check multiple files
442
+ files_to_check = ["config.json", "data.csv", "model.pt"]
443
+ for filename in files_to_check:
444
+ path = f"/home/coder/workspace/{filename}"
445
+ exists = session.file_exists(path)
446
+ status = "✓" if exists else "✗"
447
+ print(f"{status} {filename}")
448
+ ```
449
+
450
+ ### Complete Workflow Example
451
+
452
+ ```python
453
+ from clouditia import GPUSession
454
+
455
+ session = GPUSession("ck_your_api_key")
456
+
457
+ # 1. Upload training data and code
458
+ session.upload_folder("./training_code", "/home/coder/workspace/code")
459
+ session.upload("./data/train.csv", "/home/coder/workspace/data/train.csv")
460
+ session.upload("./data/test.csv", "/home/coder/workspace/data/test.csv")
461
+
462
+ # 2. Run training
463
+ result = session.run("""
464
+ import sys
465
+ sys.path.insert(0, '/home/coder/workspace/code')
466
+ from train import train_model
467
+
468
+ model = train_model('/home/coder/workspace/data/train.csv')
469
+ model.save('/home/coder/workspace/output/model.pt')
470
+ print("Training complete!")
471
+ """)
472
+
473
+ # 3. Check and download results
474
+ if session.file_exists("/home/coder/workspace/output/model.pt"):
475
+ session.download("/home/coder/workspace/output/model.pt", "./trained_model.pt")
476
+ print("Model saved locally!")
477
+
478
+ # 4. Download all outputs
479
+ session.download_folder("/home/coder/workspace/output", "./results")
480
+ print("All results downloaded!")
481
+
482
+ # 5. List what was created
483
+ files = session.list_files("/home/coder/workspace/output")
484
+ print(f"Created {len(files)} files during training")
485
+ ```
486
+
487
+ ### Working with Different File Types
488
+
489
+ ```python
490
+ # CSV files
491
+ session.upload("./data.csv", "/home/coder/workspace/data.csv")
492
+
493
+ # Pickle files (models, data)
494
+ session.upload("./model.pkl", "/home/coder/workspace/model.pkl")
495
+
496
+ # PyTorch models
497
+ session.download("/home/coder/workspace/checkpoint.pt", "./checkpoint.pt")
498
+
499
+ # JSON configuration
500
+ session.upload("./config.json", "/home/coder/workspace/config.json")
501
+
502
+ # Text files
503
+ session.upload("./requirements.txt", "/home/coder/workspace/requirements.txt")
504
+
505
+ # Binary files
506
+ session.upload("./image.png", "/home/coder/workspace/image.png")
507
+
508
+ # Any file type works!
509
+ session.upload("./data.parquet", "/home/coder/workspace/data.parquet")
510
+ session.upload("./weights.h5", "/home/coder/workspace/weights.h5")
511
+ ```
512
+
513
+ ---
514
+
337
515
  ## Remote Functions (Decorator)
338
516
 
339
517
  Use the `@session.remote` decorator to run functions on the GPU:
@@ -664,6 +842,12 @@ GPUSession(
664
842
  | `stop()` | Stop the persistent session |
665
843
  | `set(name, value)` | Send variable to remote |
666
844
  | `get(name)` | Retrieve variable from remote |
845
+ | `upload(local_path, remote_path, show_progress=True)` | Upload a file to remote session |
846
+ | `download(remote_path, local_path, show_progress=True)` | Download a file from remote session |
847
+ | `upload_folder(local_path, remote_path, exclude=None)` | Upload a folder to remote session |
848
+ | `download_folder(remote_path, local_path, exclude=None)` | Download a folder from remote session |
849
+ | `list_files(remote_path, pattern=None)` | List files in remote directory |
850
+ | `file_exists(remote_path)` | Check if a file exists on remote |
667
851
  | `submit(code, name=None, job_type="python")` | Submit async job |
668
852
  | `jobs(status=None, limit=10)` | List jobs |
669
853
  | `gpu_info()` | Get GPU information |
@@ -15,7 +15,7 @@ Basic Usage:
15
15
  For more examples, see: https://clouditia.com/docs
16
16
  """
17
17
 
18
- __version__ = "1.2.4"
18
+ __version__ = "1.3.0"
19
19
  __author__ = "Aina KIKI-SAGBE"
20
20
  __email__ = "support@clouditia.com"
21
21
 
@@ -6,11 +6,15 @@ This module provides the GPUSession class for interacting with remote GPU sessio
6
6
 
7
7
  import base64
8
8
  import inspect
9
+ import io
9
10
  import json
11
+ import os
10
12
  import pickle
13
+ import tarfile
11
14
  import textwrap
12
15
  from functools import wraps
13
- from typing import Any, Callable, Dict, List, Optional
16
+ from pathlib import Path
17
+ from typing import Any, Callable, Dict, List, Optional, Union
14
18
 
15
19
  import requests
16
20
 
@@ -548,6 +552,487 @@ _tmp_value
548
552
  except Exception as e:
549
553
  raise ClouditiaError(f"Cannot deserialize value: {e}")
550
554
 
555
+ # ==================== FILE TRANSFER METHODS ====================
556
+
557
+ def upload(
558
+ self,
559
+ local_path: Union[str, Path],
560
+ remote_path: str,
561
+ show_progress: bool = True
562
+ ) -> bool:
563
+ """
564
+ Upload a file from local machine to the remote GPU session.
565
+
566
+ Args:
567
+ local_path: Path to the local file
568
+ remote_path: Destination path on the remote pod
569
+ show_progress: Show upload progress (default: True)
570
+
571
+ Returns:
572
+ True if upload succeeded.
573
+
574
+ Raises:
575
+ FileNotFoundError: If local file doesn't exist
576
+ ClouditiaError: If upload fails
577
+
578
+ Example:
579
+ >>> session.upload("./data.csv", "/workspace/data.csv")
580
+ >>> session.upload("model.pkl", "/workspace/models/model.pkl")
581
+ """
582
+ local_path = Path(local_path)
583
+
584
+ if not local_path.exists():
585
+ raise FileNotFoundError(f"Local file not found: {local_path}")
586
+
587
+ if not local_path.is_file():
588
+ raise ClouditiaError(f"Not a file: {local_path}. Use upload_folder() for directories.")
589
+
590
+ # Read and encode file
591
+ file_size = local_path.stat().st_size
592
+ if show_progress:
593
+ print(f"📤 Uploading {local_path.name} ({self._format_size(file_size)})...")
594
+
595
+ with open(local_path, 'rb') as f:
596
+ file_data = f.read()
597
+
598
+ file_b64 = base64.b64encode(file_data).decode()
599
+
600
+ # Create directory and write file on remote
601
+ remote_dir = os.path.dirname(remote_path)
602
+ code = f'''
603
+ import base64
604
+ import os
605
+
606
+ # Create directory if needed
607
+ os.makedirs("{remote_dir}", exist_ok=True)
608
+
609
+ # Decode and write file
610
+ file_data = base64.b64decode("{file_b64}")
611
+ with open("{remote_path}", "wb") as f:
612
+ f.write(file_data)
613
+
614
+ print(f"File written: {remote_path} ({{len(file_data)}} bytes)")
615
+ '''
616
+ result = self.run(code, stream=False)
617
+
618
+ if not result.success:
619
+ raise ClouditiaError(f"Upload failed: {result.error}")
620
+
621
+ if show_progress:
622
+ print(f"✅ Uploaded to {remote_path}")
623
+
624
+ return True
625
+
626
+ def download(
627
+ self,
628
+ remote_path: str,
629
+ local_path: Union[str, Path],
630
+ show_progress: bool = True
631
+ ) -> bool:
632
+ """
633
+ Download a file from the remote GPU session to local machine.
634
+
635
+ Args:
636
+ remote_path: Path to the file on the remote pod
637
+ local_path: Destination path on local machine
638
+ show_progress: Show download progress (default: True)
639
+
640
+ Returns:
641
+ True if download succeeded.
642
+
643
+ Raises:
644
+ ClouditiaError: If file doesn't exist or download fails
645
+
646
+ Example:
647
+ >>> session.download("/workspace/results.csv", "./results.csv")
648
+ >>> session.download("/workspace/model.pt", "./models/trained_model.pt")
649
+ """
650
+ local_path = Path(local_path)
651
+
652
+ if show_progress:
653
+ print(f"📥 Downloading {os.path.basename(remote_path)}...")
654
+
655
+ # Read and encode file on remote - use print to output the base64
656
+ code = f'''
657
+ import base64
658
+ import os
659
+
660
+ if not os.path.exists("{remote_path}"):
661
+ raise FileNotFoundError("File not found: {remote_path}")
662
+
663
+ if os.path.isdir("{remote_path}"):
664
+ raise IsADirectoryError("Path is a directory. Use download_folder() instead.")
665
+
666
+ with open("{remote_path}", "rb") as f:
667
+ file_data = f.read()
668
+
669
+ file_b64 = base64.b64encode(file_data).decode()
670
+ print("__FILEDATA_START__")
671
+ print(file_b64)
672
+ print("__FILEDATA_END__")
673
+ '''
674
+ result = self.run(code, stream=False)
675
+
676
+ if not result.success:
677
+ raise ClouditiaError(f"Download failed: {result.error}")
678
+
679
+ # Extract base64 data from output
680
+ output = result.output or ""
681
+ if "__FILEDATA_START__" not in output or "__FILEDATA_END__" not in output:
682
+ raise ClouditiaError("No data received from remote")
683
+
684
+ # Parse the base64 data between markers
685
+ try:
686
+ start = output.index("__FILEDATA_START__") + len("__FILEDATA_START__")
687
+ end = output.index("__FILEDATA_END__")
688
+ file_b64 = output[start:end].strip()
689
+ file_data = base64.b64decode(file_b64)
690
+ except Exception as e:
691
+ raise ClouditiaError(f"Failed to decode file data: {e}")
692
+
693
+ # Create local directory if needed
694
+ local_path.parent.mkdir(parents=True, exist_ok=True)
695
+
696
+ with open(local_path, 'wb') as f:
697
+ f.write(file_data)
698
+
699
+ if show_progress:
700
+ print(f"✅ Downloaded to {local_path} ({self._format_size(len(file_data))})")
701
+
702
+ return True
703
+
704
+ def upload_folder(
705
+ self,
706
+ local_path: Union[str, Path],
707
+ remote_path: str,
708
+ exclude: Optional[List[str]] = None,
709
+ show_progress: bool = True
710
+ ) -> bool:
711
+ """
712
+ Upload an entire folder from local machine to the remote GPU session.
713
+
714
+ The folder is compressed locally, transferred, and extracted on the remote.
715
+
716
+ Args:
717
+ local_path: Path to the local folder
718
+ remote_path: Destination path on the remote pod
719
+ exclude: List of patterns to exclude (e.g., ['__pycache__', '.git', '*.pyc'])
720
+ show_progress: Show upload progress (default: True)
721
+
722
+ Returns:
723
+ True if upload succeeded.
724
+
725
+ Raises:
726
+ FileNotFoundError: If local folder doesn't exist
727
+ ClouditiaError: If upload fails
728
+
729
+ Example:
730
+ >>> session.upload_folder("./my_project", "/workspace/project")
731
+ >>> session.upload_folder("./data", "/workspace/data", exclude=['*.tmp', '.git'])
732
+ """
733
+ local_path = Path(local_path)
734
+
735
+ if not local_path.exists():
736
+ raise FileNotFoundError(f"Local folder not found: {local_path}")
737
+
738
+ if not local_path.is_dir():
739
+ raise ClouditiaError(f"Not a directory: {local_path}. Use upload() for files.")
740
+
741
+ # Default exclusions
742
+ if exclude is None:
743
+ exclude = ['__pycache__', '.git', '*.pyc', '.DS_Store', 'node_modules']
744
+
745
+ if show_progress:
746
+ print(f"📦 Compressing {local_path.name}...")
747
+
748
+ # Create tar.gz in memory
749
+ tar_buffer = io.BytesIO()
750
+
751
+ def filter_func(tarinfo):
752
+ # Check exclusion patterns
753
+ for pattern in exclude:
754
+ if pattern.startswith('*'):
755
+ if tarinfo.name.endswith(pattern[1:]):
756
+ return None
757
+ elif pattern in tarinfo.name:
758
+ return None
759
+ return tarinfo
760
+
761
+ with tarfile.open(fileobj=tar_buffer, mode='w:gz') as tar:
762
+ tar.add(local_path, arcname=os.path.basename(local_path), filter=filter_func)
763
+
764
+ tar_data = tar_buffer.getvalue()
765
+ tar_b64 = base64.b64encode(tar_data).decode()
766
+
767
+ if show_progress:
768
+ print(f"📤 Uploading {self._format_size(len(tar_data))}...")
769
+
770
+ # Transfer and extract on remote
771
+ code = f'''
772
+ import base64
773
+ import tarfile
774
+ import io
775
+ import os
776
+
777
+ # Decode tar data
778
+ tar_data = base64.b64decode("{tar_b64}")
779
+
780
+ # Create destination directory
781
+ os.makedirs("{remote_path}", exist_ok=True)
782
+
783
+ # Extract
784
+ tar_buffer = io.BytesIO(tar_data)
785
+ with tarfile.open(fileobj=tar_buffer, mode='r:gz') as tar:
786
+ tar.extractall(path="{remote_path}")
787
+
788
+ # Count files
789
+ file_count = sum(1 for root, dirs, files in os.walk("{remote_path}") for f in files)
790
+ print(f"Extracted {{file_count}} files to {remote_path}")
791
+ '''
792
+ result = self.run(code, stream=False)
793
+
794
+ if not result.success:
795
+ raise ClouditiaError(f"Upload folder failed: {result.error}")
796
+
797
+ if show_progress:
798
+ print(f"✅ Folder uploaded to {remote_path}")
799
+
800
+ return True
801
+
802
+ def download_folder(
803
+ self,
804
+ remote_path: str,
805
+ local_path: Union[str, Path],
806
+ exclude: Optional[List[str]] = None,
807
+ show_progress: bool = True
808
+ ) -> bool:
809
+ """
810
+ Download an entire folder from the remote GPU session to local machine.
811
+
812
+ The folder is compressed on the remote, transferred, and extracted locally.
813
+
814
+ Args:
815
+ remote_path: Path to the folder on the remote pod
816
+ local_path: Destination path on local machine
817
+ exclude: List of patterns to exclude (e.g., ['__pycache__', '*.pyc'])
818
+ show_progress: Show download progress (default: True)
819
+
820
+ Returns:
821
+ True if download succeeded.
822
+
823
+ Raises:
824
+ ClouditiaError: If folder doesn't exist or download fails
825
+
826
+ Example:
827
+ >>> session.download_folder("/workspace/results", "./results")
828
+ >>> session.download_folder("/workspace/checkpoints", "./checkpoints", exclude=['*.tmp'])
829
+ """
830
+ local_path = Path(local_path)
831
+
832
+ # Default exclusions
833
+ if exclude is None:
834
+ exclude = ['__pycache__', '*.pyc', '.DS_Store']
835
+
836
+ if show_progress:
837
+ print(f"📦 Compressing remote folder...")
838
+
839
+ # Build exclude filter for Python
840
+ exclude_patterns = repr(exclude)
841
+
842
+ # Compress on remote and get base64
843
+ code = f'''
844
+ import base64
845
+ import tarfile
846
+ import io
847
+ import os
848
+ import fnmatch
849
+
850
+ if not os.path.exists("{remote_path}"):
851
+ raise FileNotFoundError("Folder not found: {remote_path}")
852
+
853
+ if not os.path.isdir("{remote_path}"):
854
+ raise NotADirectoryError("Path is not a directory. Use download() instead.")
855
+
856
+ exclude_patterns = {exclude_patterns}
857
+
858
+ def should_exclude(name):
859
+ for pattern in exclude_patterns:
860
+ if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(os.path.basename(name), pattern):
861
+ return True
862
+ return False
863
+
864
+ def filter_func(tarinfo):
865
+ if should_exclude(tarinfo.name):
866
+ return None
867
+ return tarinfo
868
+
869
+ # Create tar.gz in memory
870
+ tar_buffer = io.BytesIO()
871
+ with tarfile.open(fileobj=tar_buffer, mode='w:gz') as tar:
872
+ tar.add("{remote_path}", arcname=os.path.basename("{remote_path}"), filter=filter_func)
873
+
874
+ tar_data = tar_buffer.getvalue()
875
+ tar_b64 = base64.b64encode(tar_data).decode()
876
+ print(f"Compressed size: {{len(tar_data)}} bytes")
877
+ print("__FOLDERDATA_START__")
878
+ print(tar_b64)
879
+ print("__FOLDERDATA_END__")
880
+ '''
881
+ result = self.run(code, stream=False)
882
+
883
+ if not result.success:
884
+ raise ClouditiaError(f"Download folder failed: {result.error}")
885
+
886
+ # Extract base64 data from output
887
+ output = result.output or ""
888
+ if "__FOLDERDATA_START__" not in output or "__FOLDERDATA_END__" not in output:
889
+ raise ClouditiaError("No data received from remote")
890
+
891
+ if show_progress:
892
+ print(f"📥 Downloading...")
893
+
894
+ # Decode and extract locally
895
+ try:
896
+ start = output.index("__FOLDERDATA_START__") + len("__FOLDERDATA_START__")
897
+ end = output.index("__FOLDERDATA_END__")
898
+ tar_b64 = output[start:end].strip()
899
+ tar_data = base64.b64decode(tar_b64)
900
+ except Exception as e:
901
+ raise ClouditiaError(f"Failed to decode folder data: {e}")
902
+
903
+ # Create local directory
904
+ local_path.mkdir(parents=True, exist_ok=True)
905
+
906
+ # Extract
907
+ tar_buffer = io.BytesIO(tar_data)
908
+ with tarfile.open(fileobj=tar_buffer, mode='r:gz') as tar:
909
+ tar.extractall(path=local_path)
910
+
911
+ # Count files
912
+ file_count = sum(1 for root, dirs, files in os.walk(local_path) for f in files)
913
+
914
+ if show_progress:
915
+ print(f"✅ Downloaded to {local_path} ({file_count} files, {self._format_size(len(tar_data))})")
916
+
917
+ return True
918
+
919
+ def list_files(
920
+ self,
921
+ remote_path: str = "/workspace",
922
+ pattern: Optional[str] = None
923
+ ) -> List[Dict]:
924
+ """
925
+ List files in a remote directory.
926
+
927
+ Args:
928
+ remote_path: Directory path to list (default: /workspace)
929
+ pattern: Optional glob pattern to filter files (e.g., '*.py')
930
+
931
+ Returns:
932
+ List of dicts with file info (name, path, size, is_dir, modified).
933
+
934
+ Example:
935
+ >>> files = session.list_files("/workspace")
936
+ >>> for f in files:
937
+ ... print(f"{f['name']} - {f['size']} bytes")
938
+
939
+ >>> py_files = session.list_files("/workspace", pattern="*.py")
940
+ """
941
+ pattern_code = f'"{pattern}"' if pattern else 'None'
942
+
943
+ code = f'''
944
+ import os
945
+ import json
946
+ from datetime import datetime
947
+ import fnmatch
948
+
949
+ path = "{remote_path}"
950
+ pattern = {pattern_code}
951
+
952
+ if not os.path.exists(path):
953
+ raise FileNotFoundError(f"Path not found: {{path}}")
954
+
955
+ files = []
956
+ for item in os.listdir(path):
957
+ item_path = os.path.join(path, item)
958
+ stat = os.stat(item_path)
959
+
960
+ if pattern and not fnmatch.fnmatch(item, pattern):
961
+ continue
962
+
963
+ files.append({{
964
+ "name": item,
965
+ "path": item_path,
966
+ "size": stat.st_size,
967
+ "is_dir": os.path.isdir(item_path),
968
+ "modified": datetime.fromtimestamp(stat.st_mtime).isoformat()
969
+ }})
970
+
971
+ # Sort: directories first, then by name
972
+ files.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))
973
+
974
+ print("__LISTFILES_START__")
975
+ print(json.dumps(files))
976
+ print("__LISTFILES_END__")
977
+ '''
978
+ result = self.run(code, stream=False)
979
+
980
+ if not result.success:
981
+ raise ClouditiaError(f"Failed to list files: {result.error}")
982
+
983
+ output = result.output or ""
984
+ if "__LISTFILES_START__" in output and "__LISTFILES_END__" in output:
985
+ try:
986
+ start = output.index("__LISTFILES_START__") + len("__LISTFILES_START__")
987
+ end = output.index("__LISTFILES_END__")
988
+ files_json = output[start:end].strip()
989
+ return json.loads(files_json)
990
+ except (json.JSONDecodeError, ValueError):
991
+ return []
992
+
993
+ return []
994
+
995
+ def file_exists(self, remote_path: str) -> bool:
996
+ """
997
+ Check if a file or directory exists on the remote pod.
998
+
999
+ Args:
1000
+ remote_path: Path to check
1001
+
1002
+ Returns:
1003
+ True if path exists, False otherwise.
1004
+
1005
+ Example:
1006
+ >>> if session.file_exists("/workspace/model.pt"):
1007
+ ... session.download("/workspace/model.pt", "./model.pt")
1008
+ """
1009
+ code = f'''
1010
+ import os
1011
+ exists = os.path.exists("{remote_path}")
1012
+ print("__EXISTS_START__")
1013
+ print("TRUE" if exists else "FALSE")
1014
+ print("__EXISTS_END__")
1015
+ '''
1016
+ result = self.run(code, stream=False)
1017
+
1018
+ output = result.output or ""
1019
+ if "__EXISTS_START__" in output and "__EXISTS_END__" in output:
1020
+ start = output.index("__EXISTS_START__") + len("__EXISTS_START__")
1021
+ end = output.index("__EXISTS_END__")
1022
+ value = output[start:end].strip()
1023
+ return value == "TRUE"
1024
+ return False
1025
+
1026
+ def _format_size(self, size: int) -> str:
1027
+ """Format file size in human-readable format."""
1028
+ for unit in ['B', 'KB', 'MB', 'GB']:
1029
+ if size < 1024:
1030
+ return f"{size:.1f} {unit}"
1031
+ size /= 1024
1032
+ return f"{size:.1f} TB"
1033
+
1034
+ # ==================== END FILE TRANSFER METHODS ====================
1035
+
551
1036
  def submit(
552
1037
  self,
553
1038
  code: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clouditia
3
- Version: 1.2.4
3
+ Version: 1.3.0
4
4
  Summary: Execute Python and Shell code on remote GPU sessions
5
5
  Author-email: Aina KIKI-SAGBE <support@clouditia.com>
6
6
  Maintainer-email: Aina KIKI-SAGBE <support@clouditia.com>
@@ -81,6 +81,7 @@ print(result.output)
81
81
  - **Shell Commands**: Execute shell commands on the GPU pod
82
82
  - **Persistent Sessions**: Keep variables between executions with `start()`/`stop()`
83
83
  - **Variable Transfer**: Send and retrieve variables between local and remote
84
+ - **File Transfer**: Upload/download files and folders between local and remote
84
85
  - **Async Jobs**: Submit long-running tasks with real-time log monitoring
85
86
  - **Jupyter Magic**: Use `%%clouditia` magic in notebooks
86
87
  - **Decorator Support**: Use `@session.remote` to run functions on GPU
@@ -95,11 +96,12 @@ print(result.output)
95
96
  4. [Executing Python Code](#executing-python-code)
96
97
  5. [Shell Commands](#shell-commands)
97
98
  6. [Variable Transfer](#variable-transfer)
98
- 7. [Remote Functions (Decorator)](#remote-functions-decorator)
99
- 8. [Async Jobs (Long-Running Tasks)](#async-jobs-long-running-tasks)
100
- 9. [Jupyter Magic](#jupyter-magic)
101
- 10. [Error Handling](#error-handling)
102
- 11. [API Reference](#api-reference)
99
+ 7. [File Transfer](#file-transfer)
100
+ 8. [Remote Functions (Decorator)](#remote-functions-decorator)
101
+ 9. [Async Jobs (Long-Running Tasks)](#async-jobs-long-running-tasks)
102
+ 10. [Jupyter Magic](#jupyter-magic)
103
+ 11. [Error Handling](#error-handling)
104
+ 12. [API Reference](#api-reference)
103
105
 
104
106
  ---
105
107
 
@@ -377,6 +379,182 @@ print(f"Learning rate: {config['learning_rate']}")
377
379
 
378
380
  ---
379
381
 
382
+ ## File Transfer
383
+
384
+ Transfer files and folders between your local machine and the remote GPU session.
385
+
386
+ ### Uploading a Single File
387
+
388
+ ```python
389
+ # Upload a local file to the remote session
390
+ session.upload("./data.csv", "/home/coder/workspace/data.csv")
391
+
392
+ # Upload with custom path
393
+ session.upload("./model.pkl", "/home/coder/workspace/models/trained_model.pkl")
394
+
395
+ # Disable progress output
396
+ session.upload("./config.json", "/home/coder/workspace/config.json", show_progress=False)
397
+ ```
398
+
399
+ ### Downloading a Single File
400
+
401
+ ```python
402
+ # Download a file from the remote session
403
+ session.download("/home/coder/workspace/results.csv", "./results.csv")
404
+
405
+ # Download trained model
406
+ session.download("/home/coder/workspace/checkpoints/model.pt", "./local_model.pt")
407
+
408
+ # Download silently
409
+ session.download("/home/coder/workspace/logs.txt", "./logs.txt", show_progress=False)
410
+ ```
411
+
412
+ ### Uploading a Folder
413
+
414
+ Upload an entire directory with all its contents:
415
+
416
+ ```python
417
+ # Upload a project folder
418
+ session.upload_folder("./my_project", "/home/coder/workspace/project")
419
+
420
+ # Upload with exclusions (default excludes: __pycache__, .git, *.pyc, .DS_Store, node_modules)
421
+ session.upload_folder(
422
+ "./my_project",
423
+ "/home/coder/workspace/project",
424
+ exclude=["*.log", ".env", "__pycache__", ".git"]
425
+ )
426
+
427
+ # Upload data folder
428
+ session.upload_folder("./datasets", "/home/coder/workspace/data")
429
+ ```
430
+
431
+ ### Downloading a Folder
432
+
433
+ Download an entire directory with all its contents:
434
+
435
+ ```python
436
+ # Download results folder
437
+ session.download_folder("/home/coder/workspace/results", "./local_results")
438
+
439
+ # Download checkpoints
440
+ session.download_folder(
441
+ "/home/coder/workspace/checkpoints",
442
+ "./checkpoints",
443
+ exclude=["*.tmp", "*.log"]
444
+ )
445
+
446
+ # Download trained models
447
+ session.download_folder("/home/coder/workspace/models", "./downloaded_models")
448
+ ```
449
+
450
+ ### Listing Remote Files
451
+
452
+ ```python
453
+ # List files in a directory
454
+ files = session.list_files("/home/coder/workspace")
455
+ for f in files:
456
+ icon = "📁" if f["is_dir"] else "📄"
457
+ print(f"{icon} {f['name']} - {f['size']} bytes")
458
+
459
+ # Filter by pattern
460
+ python_files = session.list_files("/home/coder/workspace", pattern="*.py")
461
+ for f in python_files:
462
+ print(f"📄 {f['name']}")
463
+
464
+ # List with full details
465
+ files = session.list_files("/home/coder/workspace")
466
+ for f in files:
467
+ print(f"Name: {f['name']}")
468
+ print(f" Path: {f['path']}")
469
+ print(f" Size: {f['size']} bytes")
470
+ print(f" Is Directory: {f['is_dir']}")
471
+ print(f" Modified: {f['modified']}")
472
+ ```
473
+
474
+ ### Checking if a File Exists
475
+
476
+ ```python
477
+ # Check before downloading
478
+ if session.file_exists("/home/coder/workspace/model.pt"):
479
+ session.download("/home/coder/workspace/model.pt", "./model.pt")
480
+ print("Model downloaded!")
481
+ else:
482
+ print("Model not found, training required...")
483
+
484
+ # Check multiple files
485
+ files_to_check = ["config.json", "data.csv", "model.pt"]
486
+ for filename in files_to_check:
487
+ path = f"/home/coder/workspace/{filename}"
488
+ exists = session.file_exists(path)
489
+ status = "✓" if exists else "✗"
490
+ print(f"{status} {filename}")
491
+ ```
492
+
493
+ ### Complete Workflow Example
494
+
495
+ ```python
496
+ from clouditia import GPUSession
497
+
498
+ session = GPUSession("ck_your_api_key")
499
+
500
+ # 1. Upload training data and code
501
+ session.upload_folder("./training_code", "/home/coder/workspace/code")
502
+ session.upload("./data/train.csv", "/home/coder/workspace/data/train.csv")
503
+ session.upload("./data/test.csv", "/home/coder/workspace/data/test.csv")
504
+
505
+ # 2. Run training
506
+ result = session.run("""
507
+ import sys
508
+ sys.path.insert(0, '/home/coder/workspace/code')
509
+ from train import train_model
510
+
511
+ model = train_model('/home/coder/workspace/data/train.csv')
512
+ model.save('/home/coder/workspace/output/model.pt')
513
+ print("Training complete!")
514
+ """)
515
+
516
+ # 3. Check and download results
517
+ if session.file_exists("/home/coder/workspace/output/model.pt"):
518
+ session.download("/home/coder/workspace/output/model.pt", "./trained_model.pt")
519
+ print("Model saved locally!")
520
+
521
+ # 4. Download all outputs
522
+ session.download_folder("/home/coder/workspace/output", "./results")
523
+ print("All results downloaded!")
524
+
525
+ # 5. List what was created
526
+ files = session.list_files("/home/coder/workspace/output")
527
+ print(f"Created {len(files)} files during training")
528
+ ```
529
+
530
+ ### Working with Different File Types
531
+
532
+ ```python
533
+ # CSV files
534
+ session.upload("./data.csv", "/home/coder/workspace/data.csv")
535
+
536
+ # Pickle files (models, data)
537
+ session.upload("./model.pkl", "/home/coder/workspace/model.pkl")
538
+
539
+ # PyTorch models
540
+ session.download("/home/coder/workspace/checkpoint.pt", "./checkpoint.pt")
541
+
542
+ # JSON configuration
543
+ session.upload("./config.json", "/home/coder/workspace/config.json")
544
+
545
+ # Text files
546
+ session.upload("./requirements.txt", "/home/coder/workspace/requirements.txt")
547
+
548
+ # Binary files
549
+ session.upload("./image.png", "/home/coder/workspace/image.png")
550
+
551
+ # Any file type works!
552
+ session.upload("./data.parquet", "/home/coder/workspace/data.parquet")
553
+ session.upload("./weights.h5", "/home/coder/workspace/weights.h5")
554
+ ```
555
+
556
+ ---
557
+
380
558
  ## Remote Functions (Decorator)
381
559
 
382
560
  Use the `@session.remote` decorator to run functions on the GPU:
@@ -707,6 +885,12 @@ GPUSession(
707
885
  | `stop()` | Stop the persistent session |
708
886
  | `set(name, value)` | Send variable to remote |
709
887
  | `get(name)` | Retrieve variable from remote |
888
+ | `upload(local_path, remote_path, show_progress=True)` | Upload a file to remote session |
889
+ | `download(remote_path, local_path, show_progress=True)` | Download a file from remote session |
890
+ | `upload_folder(local_path, remote_path, exclude=None)` | Upload a folder to remote session |
891
+ | `download_folder(remote_path, local_path, exclude=None)` | Download a folder from remote session |
892
+ | `list_files(remote_path, pattern=None)` | List files in remote directory |
893
+ | `file_exists(remote_path)` | Check if a file exists on remote |
710
894
  | `submit(code, name=None, job_type="python")` | Submit async job |
711
895
  | `jobs(status=None, limit=10)` | List jobs |
712
896
  | `gpu_info()` | Get GPU information |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clouditia"
7
- version = "1.2.4"
7
+ version = "1.3.0"
8
8
  description = "Execute Python and Shell code on remote GPU sessions"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes