lightning-sdk 2026.1.22__py3-none-any.whl → 2026.1.27__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.
Files changed (58) hide show
  1. lightning_sdk/__version__.py +1 -1
  2. lightning_sdk/api/studio_api.py +7 -14
  3. lightning_sdk/api/teamspace_api.py +155 -48
  4. lightning_sdk/api/utils.py +8 -0
  5. lightning_sdk/cli/cp/__init__.py +14 -11
  6. lightning_sdk/cli/cp/teamspace_uploads.py +93 -0
  7. lightning_sdk/cli/legacy/download.py +29 -98
  8. lightning_sdk/cli/legacy/upload.py +24 -31
  9. lightning_sdk/cli/studio/cp.py +8 -5
  10. lightning_sdk/cli/studio/ls.py +1 -1
  11. lightning_sdk/cli/studio/rm.py +1 -1
  12. lightning_sdk/cli/utils/{studio_filesystem.py → filesystem.py} +43 -5
  13. lightning_sdk/exceptions.py +27 -0
  14. lightning_sdk/lightning_cloud/openapi/__init__.py +14 -12
  15. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
  16. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +5 -1
  17. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +163 -466
  18. lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +456 -0
  19. lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +5 -1
  20. lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +11 -11
  21. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +113 -0
  22. lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +113 -0
  23. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
  24. lightning_sdk/lightning_cloud/openapi/models/__init__.py +13 -12
  25. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → container_registry_config_ecr.py} +49 -23
  26. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_status.py → container_registry_provider.py} +14 -10
  27. lightning_sdk/lightning_cloud/openapi/models/container_registry_service_create_container_registry_body.py +201 -0
  28. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config_input.py → container_registry_service_refresh_container_registry_credentials_body.py} +21 -21
  29. lightning_sdk/lightning_cloud/openapi/models/jobs_service_duplicate_deployment_body.py +175 -0
  30. lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
  31. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
  32. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
  33. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +63 -89
  35. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_add_container_registry_body.py → v1_container_registry_config.py} +16 -16
  36. lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_container_registry_scopes.py} +39 -39
  37. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_create_container_registry_response.py} +6 -6
  38. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → v1_delete_org_cluster_capacity_reservation_response.py} +6 -6
  39. lightning_sdk/lightning_cloud/openapi/models/v1_describe_org_cluster_capacity_reservation_response.py +201 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
  41. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  42. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +1 -27
  43. lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
  44. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → v1_mithril_direct_v1.py} +51 -51
  45. lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
  46. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -53
  47. lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
  48. lightning_sdk/teamspace.py +28 -7
  49. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/METADATA +1 -1
  50. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/RECORD +55 -52
  51. lightning_sdk/lightning_cloud/openapi/models/v1_add_container_registry_response.py +0 -123
  52. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
  53. lightning_sdk/lightning_cloud/openapi/models/v1_ecr_registry_details.py +0 -201
  54. /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
  55. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/LICENSE +0 -0
  56. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/WHEEL +0 -0
  57. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/entry_points.txt +0 -0
  58. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,3 @@
1
1
  """Version information for lightning_sdk."""
2
2
 
3
- __version__ = "2026.01.22"
3
+ __version__ = "2026.01.27"
@@ -13,6 +13,7 @@ import requests
13
13
  from tqdm import tqdm
14
14
 
15
15
  from lightning_sdk.api.utils import (
16
+ _authenticate_and_get_token,
16
17
  _create_app,
17
18
  _DummyBody,
18
19
  _DummyResponse,
@@ -23,7 +24,6 @@ from lightning_sdk.api.utils import (
23
24
  _get_cloud_url as _cloud_url,
24
25
  )
25
26
  from lightning_sdk.constants import _LIGHTNING_DEBUG
26
- from lightning_sdk.lightning_cloud.login import Auth
27
27
  from lightning_sdk.lightning_cloud.openapi import (
28
28
  AssistantsServiceCreateAssistantBody,
29
29
  AssistantsServiceCreateAssistantManagedEndpointBody,
@@ -49,7 +49,6 @@ from lightning_sdk.lightning_cloud.openapi import (
49
49
  V1EnvVar,
50
50
  V1GetCloudSpaceInstanceStatusResponse,
51
51
  V1GetLongRunningCommandInCloudSpaceResponse,
52
- V1LoginRequest,
53
52
  V1ManagedEndpoint,
54
53
  V1ManagedModel,
55
54
  V1Plugin,
@@ -661,14 +660,8 @@ class StudioApi:
661
660
  self.stop_keeping_alive(teamspace_id=teamspace_id, studio_id=studio_id)
662
661
  self._client.cloud_space_service_delete_cloud_space(project_id=teamspace_id, id=studio_id)
663
662
 
664
- def _authenticate_and_get_token(self) -> str:
665
- """Authenticate and return a token for API requests."""
666
- auth = Auth()
667
- auth.authenticate()
668
- return self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
669
-
670
663
  def get_tree(self, studio_id: str, teamspace_id: str, path: str, query_params: Optional[dict] = None) -> None:
671
- token = self._authenticate_and_get_token()
664
+ token = _authenticate_and_get_token(self._client)
672
665
 
673
666
  if query_params is None:
674
667
  query_params = {
@@ -730,7 +723,7 @@ class StudioApi:
730
723
  progress_bar: bool,
731
724
  ) -> None:
732
725
  """Uploads file to given remote path on the studio."""
733
- token = self._authenticate_and_get_token()
726
+ token = _authenticate_and_get_token(self._client)
734
727
 
735
728
  query_params = {"token": token}
736
729
  client_host = self._client.api_client.configuration.host
@@ -768,7 +761,7 @@ class StudioApi:
768
761
  ) -> None:
769
762
  """Downloads a given file from a Studio to a target location."""
770
763
  # TODO: Update this endpoint to permit basic auth
771
- token = self._authenticate_and_get_token()
764
+ token = _authenticate_and_get_token(self._client)
772
765
 
773
766
  query_params = {
774
767
  "clusterId": cloud_account,
@@ -864,7 +857,7 @@ class StudioApi:
864
857
  print(f"No files found in {path}")
865
858
  return
866
859
 
867
- token = self._authenticate_and_get_token()
860
+ token = _authenticate_and_get_token(self._client)
868
861
 
869
862
  total_size = sum(f.get("size", 0) for f in files)
870
863
 
@@ -910,7 +903,7 @@ class StudioApi:
910
903
  if info["type"] != "file":
911
904
  raise IsADirectoryError(f"The path '{path}' is a directory. Use 'remove_folder()' to remove directories.")
912
905
 
913
- token = self._authenticate_and_get_token()
906
+ token = _authenticate_and_get_token(self._client)
914
907
 
915
908
  query_params = {"token": token}
916
909
  client_host = self._client.api_client.configuration.host
@@ -933,7 +926,7 @@ class StudioApi:
933
926
  if info["type"] == "file":
934
927
  raise ValueError(f"The path '{path}' is a file. Use 'remove_file()' to remove files.")
935
928
 
936
- token = self._authenticate_and_get_token()
929
+ token = _authenticate_and_get_token(self._client)
937
930
 
938
931
  query_params = {"token": token}
939
932
  client_host = self._client.api_client.configuration.host
@@ -1,5 +1,8 @@
1
+ import concurrent
1
2
  import os
2
3
  import re
4
+ import warnings
5
+ from concurrent.futures import ThreadPoolExecutor
3
6
  from pathlib import Path
4
7
  from typing import Dict, List, Optional, Tuple
5
8
 
@@ -7,12 +10,11 @@ import requests
7
10
  from tqdm.auto import tqdm
8
11
 
9
12
  from lightning_sdk.api.utils import (
13
+ _authenticate_and_get_token,
10
14
  _download_model_files,
11
- _download_teamspace_files,
12
15
  _DummyBody,
13
16
  _get_model_version,
14
17
  _ModelFileUploader,
15
- _resolve_teamspace_remote_path,
16
18
  )
17
19
  from lightning_sdk.lightning_cloud.login import Auth
18
20
  from lightning_sdk.lightning_cloud.openapi import (
@@ -32,7 +34,6 @@ from lightning_sdk.lightning_cloud.openapi import (
32
34
  V1ExternalCluster,
33
35
  V1GCSFolderDataConnection,
34
36
  V1Job,
35
- V1LoginRequest,
36
37
  V1Model,
37
38
  V1ModelVersionArchive,
38
39
  V1MultiMachineJob,
@@ -391,6 +392,58 @@ class TeamspaceApi:
391
392
  response = self.models_api.models_store_list_model_versions(project_id=teamspace_id, model_id=model_id)
392
393
  return response.versions
393
394
 
395
+ def get_uploads_tree(self, teamspace_id: str, path: str, query_params: Optional[dict] = None) -> None:
396
+ token = _authenticate_and_get_token(self._client)
397
+
398
+ if query_params is None:
399
+ query_params = {
400
+ "token": token,
401
+ }
402
+ else:
403
+ query_params["token"] = token
404
+ r = requests.get(
405
+ f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/trees/{path}",
406
+ params=query_params,
407
+ )
408
+ return r.json()
409
+
410
+ def get_path_info(self, teamspace_id: str, path: str = "") -> dict:
411
+ path = path.strip("/")
412
+
413
+ if "/" in path:
414
+ parent_path = path.rsplit("/", 1)[0]
415
+ target_name = path.rsplit("/", 1)[1]
416
+ else:
417
+ if path == "":
418
+ # root directory
419
+ return {"exists": True, "type": "directory", "size": None}
420
+ parent_path = ""
421
+ target_name = path
422
+
423
+ tree = self.get_uploads_tree(teamspace_id, path=parent_path)
424
+ tree_items = tree.get("tree", [])
425
+ for item in tree_items:
426
+ item_name = item.get("path", "")
427
+ if item_name == target_name:
428
+ item_type = item.get("type")
429
+ # if type == "blob" it's a file, if "tree" it's a directory
430
+ return {
431
+ "exists": True,
432
+ "type": "file" if item_type == "blob" else "directory",
433
+ "size": item.get("size", 0) if item_type == "blob" else None,
434
+ }
435
+ warnings.warn(f"If '{path}' is a directory, it may be empty and thus not detected.")
436
+ return {"exists": False, "type": None, "size": None}
437
+
438
+ def list_uploads_files(
439
+ self,
440
+ teamspace_id: str,
441
+ path: str = "",
442
+ ) -> List[Dict]:
443
+ """Recursively list all files in a /Uploads/ directory tree."""
444
+ path = path.strip("/")
445
+ return self.get_uploads_tree(teamspace_id, path, query_params={"recursive": "true"}).get("tree", [])
446
+
394
447
  def upload_file(
395
448
  self,
396
449
  teamspace_id: str,
@@ -399,10 +452,8 @@ class TeamspaceApi:
399
452
  remote_path: str,
400
453
  progress_bar: bool,
401
454
  ) -> None:
402
- """Uploads file to given remote path in the Teamspace drive."""
403
- auth = Auth()
404
- auth.authenticate()
405
- token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
455
+ """Uploads file to given remote path in the Teamspace drive /Uploads/."""
456
+ token = _authenticate_and_get_token(self._client)
406
457
 
407
458
  query_params = {"token": token, "clusterId": cloud_account}
408
459
  client_host = self._client.api_client.configuration.host
@@ -434,37 +485,25 @@ class TeamspaceApi:
434
485
  path: str,
435
486
  target_path: str,
436
487
  teamspace_id: str,
488
+ cloud_account: Optional[str] = None,
437
489
  progress_bar: bool = True,
438
490
  ) -> None:
439
- """Downloads a given file in Teamspace drive to a target location."""
491
+ """Downloads a given file in Teamspace drive /Uploads/ to a target location."""
440
492
  # TODO: Update this endpoint to permit basic auth
441
- auth = Auth()
442
- auth.authenticate()
443
- token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
444
-
445
- cluster_ids = [ca.cluster_id for ca in self.list_cloud_accounts(teamspace_id)]
446
-
447
- found = False
448
- for cluster_id in cluster_ids:
449
- query_params = {
450
- "clusterId": cluster_id,
451
- "key": _resolve_teamspace_remote_path(path),
452
- "token": token,
453
- }
454
-
455
- r = requests.get(
456
- f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/download",
457
- params=query_params,
458
- stream=True,
459
- )
493
+ token = _authenticate_and_get_token(self._client)
460
494
 
461
- if r.status_code == 200:
462
- found = True
463
- break
495
+ query_params = {
496
+ "token": token,
497
+ }
464
498
 
465
- if not found:
466
- raise FileNotFoundError(f"The provided path does not exist in the teamspace: {path}")
499
+ if cloud_account:
500
+ query_params["clusterId"] = cloud_account
467
501
 
502
+ r = requests.get(
503
+ f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{path}",
504
+ params=query_params,
505
+ stream=True,
506
+ )
468
507
  total_length = int(r.headers.get("content-length"))
469
508
 
470
509
  if progress_bar:
@@ -488,33 +527,101 @@ class TeamspaceApi:
488
527
  f.write(chunk)
489
528
  pbar_update(len(chunk))
490
529
 
530
+ def _download_single_file(
531
+ self,
532
+ file_info: Dict,
533
+ base_path: str,
534
+ download_dir: Path,
535
+ teamspace_id: str,
536
+ token: str,
537
+ cloud_account: Optional[str] = None,
538
+ pbar: Optional[tqdm] = True,
539
+ ) -> None:
540
+ """Download a single file from Teamspace drive /Uploads/ with progress tracking."""
541
+ relative_path = file_info["path"].lstrip("/")
542
+ local_file = download_dir / relative_path
543
+ local_file.parent.mkdir(parents=True, exist_ok=True)
544
+
545
+ file_path = os.path.join(base_path, relative_path) if base_path else relative_path
546
+
547
+ query_params = {
548
+ "token": token,
549
+ }
550
+ if cloud_account:
551
+ query_params["clusterId"] = cloud_account
552
+
553
+ r = requests.get(
554
+ f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{file_path}",
555
+ params=query_params,
556
+ stream=True,
557
+ )
558
+
559
+ with open(str(local_file), "wb") as f:
560
+ for chunk in r.iter_content(chunk_size=4096 * 8):
561
+ f.write(chunk)
562
+ if pbar:
563
+ pbar.update(len(chunk))
564
+
491
565
  def download_folder(
492
566
  self,
493
567
  path: str,
494
568
  target_path: str,
495
569
  teamspace_id: str,
496
- cloud_account: str,
570
+ cloud_account: Optional[str] = None,
497
571
  progress_bar: bool = True,
572
+ num_workers: Optional[int] = None,
498
573
  ) -> None:
499
- """Downloads a given folder from Teamspace drive to a target location."""
574
+ """Downloads a given folder from Teamspace drive /Uploads/ to a target location."""
500
575
  # TODO: Update this endpoint to permit basic auth
501
- auth = Auth()
502
- auth.authenticate()
576
+ if num_workers is None:
577
+ num_workers = os.cpu_count() * 4
503
578
 
504
- prefix = _resolve_teamspace_remote_path(path)
579
+ # Normalize the path
580
+ path = path.strip("/")
581
+ download_dir = Path(target_path)
582
+ download_dir.mkdir(parents=True, exist_ok=True)
505
583
 
506
- # ensure we only download as a directory and not the entire prefix
507
- if prefix.endswith("/") is False:
508
- prefix = prefix + "/"
584
+ files = self.list_uploads_files(teamspace_id, path)
509
585
 
510
- _download_teamspace_files(
511
- client=self._client,
512
- teamspace_id=teamspace_id,
513
- cluster_id=cloud_account,
514
- prefix=prefix,
515
- download_dir=Path(target_path),
516
- progress_bar=progress_bar,
517
- )
586
+ if not files:
587
+ print(f"No files found in {path}")
588
+ return
589
+
590
+ token = _authenticate_and_get_token(self._client)
591
+
592
+ total_size = sum(f.get("size", 0) for f in files)
593
+
594
+ pbar = None
595
+ if progress_bar:
596
+ pbar = tqdm(
597
+ desc="Downloading files",
598
+ total=total_size,
599
+ unit="B",
600
+ unit_scale=True,
601
+ unit_divisor=1000,
602
+ mininterval=1,
603
+ )
604
+
605
+ with ThreadPoolExecutor(max_workers=num_workers) as executor:
606
+ futures = [
607
+ executor.submit(
608
+ self._download_single_file,
609
+ file_info,
610
+ path,
611
+ download_dir,
612
+ teamspace_id,
613
+ token,
614
+ cloud_account,
615
+ pbar,
616
+ )
617
+ for file_info in files
618
+ ]
619
+ concurrent.futures.wait(futures)
620
+
621
+ if pbar:
622
+ pbar.set_description("Download complete")
623
+ pbar.refresh()
624
+ pbar.close()
518
625
 
519
626
  def get_secrets(self, teamspace_id: str) -> Dict[str, str]:
520
627
  """Get all secrets for a teamspace."""
@@ -15,6 +15,7 @@ import requests
15
15
  from tqdm.auto import tqdm
16
16
 
17
17
  from lightning_sdk.constants import __GLOBAL_LIGHTNING_UNIQUE_IDS_STORE__, _LIGHTNING_DEBUG
18
+ from lightning_sdk.lightning_cloud.login import Auth
18
19
  from lightning_sdk.lightning_cloud.openapi import (
19
20
  CloudSpaceServiceApi,
20
21
  CloudSpaceServiceCreateCloudSpaceAppInstanceBody,
@@ -29,6 +30,7 @@ from lightning_sdk.lightning_cloud.openapi import (
29
30
  StorageServiceUploadProjectArtifactPartsBody,
30
31
  V1CompletedPart,
31
32
  V1CompleteUpload,
33
+ V1LoginRequest,
32
34
  V1PathMapping,
33
35
  V1PresignedUrl,
34
36
  V1SignedUrl,
@@ -816,3 +818,9 @@ def to_iso_z(dt: datetime) -> str:
816
818
  dt = dt.replace(tzinfo=timezone.utc)
817
819
  return dt.astimezone(timezone.utc).isoformat(timespec="milliseconds")
818
820
  return dt.isoformat(timespec="milliseconds")
821
+
822
+
823
+ def _authenticate_and_get_token(client: Any) -> str:
824
+ auth = Auth()
825
+ auth.authenticate()
826
+ return client.auth_service_login(V1LoginRequest(auth.api_key)).token
@@ -4,6 +4,8 @@ from typing import Any, Literal, Optional
4
4
 
5
5
  import click
6
6
 
7
+ from lightning_sdk.cli.cp.teamspace_uploads import cp_download as teamspace_uploads_cp_download
8
+ from lightning_sdk.cli.cp.teamspace_uploads import cp_upload as teamspace_uploads_cp_upload
7
9
  from lightning_sdk.cli.studio.cp import cp_download as studio_cp_download
8
10
  from lightning_sdk.cli.studio.cp import cp_upload as studio_cp_upload
9
11
 
@@ -17,7 +19,7 @@ def parse_lit_url(url: str) -> tuple[str, list[str], Literal["studios", "uploads
17
19
 
18
20
  if path[2] == "studios":
19
21
  resource_type = "studios"
20
- elif "uploads" in path[3]:
22
+ elif path[2] == "uploads":
21
23
  resource_type = "uploads"
22
24
  else:
23
25
  raise ValueError("URL must contain either 'studios' or 'uploads'")
@@ -39,12 +41,16 @@ def route_cp_operation(source: str, destination: str, **options: Any) -> None:
39
41
  if source_is_lit:
40
42
  resource_type = parse_lit_url(source)
41
43
  if resource_type == "studios":
42
- return studio_cp_download(source, destination, options)
44
+ return studio_cp_download(source, destination, options.get("recursive", False))
45
+ if resource_type == "uploads":
46
+ return teamspace_uploads_cp_download(source, destination, options)
43
47
  raise ValueError(f"Resource type: {resource_type} is not supported")
44
48
  else:
45
49
  resource_type = parse_lit_url(destination)
46
50
  if resource_type == "studios":
47
- return studio_cp_upload(source, destination, options)
51
+ return studio_cp_upload(source, destination, options.get("recursive", False))
52
+ if resource_type == "uploads":
53
+ return teamspace_uploads_cp_upload(source, destination, options)
48
54
  raise ValueError(f"Resource type: {resource_type} is not supported")
49
55
 
50
56
 
@@ -52,13 +58,10 @@ def register_commands(command: click.Command) -> None:
52
58
  """Register cp command callback."""
53
59
 
54
60
  def new_callback(source: str, destination: Optional[str], recursive: bool, **kwargs: Any) -> None:
55
- try:
56
- route_cp_operation(
57
- source=source,
58
- destination=destination,
59
- recursive=recursive,
60
- )
61
- except Exception:
62
- raise
61
+ route_cp_operation(
62
+ source=source,
63
+ destination=destination,
64
+ recursive=recursive,
65
+ )
63
66
 
64
67
  command.callback = new_callback
@@ -0,0 +1,93 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from rich.console import Console
5
+
6
+ from lightning_sdk.api.utils import _get_cloud_url
7
+ from lightning_sdk.cli.utils.filesystem import parse_teamspace_uploads_path, resolve_teamspace
8
+
9
+
10
+ def cp_upload(
11
+ local_file_path: str,
12
+ teamspace_path: str,
13
+ options: dict[str, any],
14
+ ) -> None:
15
+ console = Console()
16
+ recursive = options.get("recursive", False)
17
+ cloud_account = options.get("cloud_account", None)
18
+ if not Path(local_file_path).exists():
19
+ raise FileNotFoundError(f"The provided path does not exist: {local_file_path}")
20
+
21
+ teamspace_path_result = parse_teamspace_uploads_path(teamspace_path)
22
+
23
+ selected_teamspace = resolve_teamspace(teamspace_path_result["teamspace"], teamspace_path_result["owner"])
24
+ console.print(f"Uploading to {selected_teamspace.owner.name}/{selected_teamspace.name}")
25
+
26
+ if Path(local_file_path).is_dir():
27
+ if not recursive:
28
+ raise ValueError(f"'{local_file_path}' is a directory. Use -r flag to copy directories recursively.")
29
+ selected_teamspace.upload_folder(
30
+ local_file_path, teamspace_path_result["destination"], cloud_account=cloud_account
31
+ )
32
+ else:
33
+ if teamspace_path.endswith(("/", "\\")):
34
+ # if destination ends with / or \, treat it as a directory
35
+ file_name = os.path.basename(local_file_path)
36
+ teamspace_path_result["destination"] = os.path.join(teamspace_path_result["destination"], file_name)
37
+ selected_teamspace.upload_file(
38
+ local_file_path, teamspace_path_result["destination"], cloud_account=cloud_account
39
+ )
40
+
41
+ studio_url = (
42
+ _get_cloud_url().replace(":443", "") + "/" + selected_teamspace.owner.name + "/" + selected_teamspace.name
43
+ )
44
+ console.print(f"See your file at {studio_url}")
45
+
46
+
47
+ def cp_download(
48
+ teamspace_path: str,
49
+ local_path: str,
50
+ options: dict[str, any],
51
+ ) -> None:
52
+ console = Console()
53
+ teamspace_path_result = parse_teamspace_uploads_path(teamspace_path)
54
+ recursive = options.get("recursive", False)
55
+
56
+ selected_teamspace = resolve_teamspace(teamspace_path_result["teamspace"], teamspace_path_result["owner"])
57
+
58
+ # check if file/folder exists
59
+ path_info = selected_teamspace._teamspace_api.get_path_info(
60
+ selected_teamspace._teamspace.id, path=teamspace_path_result["destination"]
61
+ )
62
+ if not path_info["exists"]:
63
+ raise FileNotFoundError(
64
+ f"The provided path does not exist in the teamspace drive: {teamspace_path_result['destination']} "
65
+ "Note that empty folders may not be detected as existing."
66
+ )
67
+
68
+ console.print(f"Downloading from {selected_teamspace.owner.name}/{selected_teamspace.name}")
69
+ if path_info["type"] == "directory":
70
+ if not recursive:
71
+ raise ValueError(
72
+ f"'{teamspace_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
73
+ )
74
+ folder_name = os.path.basename(teamspace_path_result["destination"].rstrip("/"))
75
+ if local_path in ("./", "."):
76
+ if folder_name == "":
77
+ folder_name = f"{selected_teamspace.name}_downloads"
78
+ target_path = os.path.join(local_path, folder_name)
79
+ else:
80
+ target_path = local_path
81
+
82
+ selected_teamspace.download_folder(teamspace_path_result["destination"], target_path)
83
+ console.print(f"See your folder at {target_path}")
84
+ else:
85
+ if os.path.isdir(local_path) or local_path.endswith(("/", "\\")):
86
+ # if local_path ends with / or \ or is a directory, treat it as a directory
87
+ file_name = os.path.basename(teamspace_path_result["destination"])
88
+ target_path = os.path.join(local_path, file_name)
89
+ else:
90
+ target_path = local_path
91
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
92
+ selected_teamspace.download_file(teamspace_path_result["destination"], target_path)
93
+ console.print(f"See your file at {target_path}")