lightning-sdk 2026.1.22__py3-none-any.whl → 2026.1.30__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 (61) hide show
  1. lightning_sdk/__version__.py +1 -1
  2. lightning_sdk/api/studio_api.py +38 -39
  3. lightning_sdk/api/teamspace_api.py +189 -72
  4. lightning_sdk/api/utils.py +69 -1
  5. lightning_sdk/cli/cp/__init__.py +14 -11
  6. lightning_sdk/cli/cp/teamspace_uploads.py +95 -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} +49 -6
  13. lightning_sdk/exceptions.py +27 -0
  14. lightning_sdk/lightning_cloud/openapi/__init__.py +17 -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 +286 -468
  18. lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +579 -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 +16 -12
  25. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → cluster_service_get_cluster_capacity_reservation_body.py} +6 -6
  26. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → container_registry_config_ecr.py} +49 -23
  27. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_status.py → container_registry_provider.py} +14 -10
  28. lightning_sdk/lightning_cloud/openapi/models/container_registry_service_create_container_registry_body.py +201 -0
  29. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config_input.py → container_registry_service_refresh_container_registry_credentials_body.py} +21 -21
  30. lightning_sdk/lightning_cloud/openapi/models/jobs_service_duplicate_deployment_body.py +175 -0
  31. lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
  32. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
  33. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
  34. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
  35. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +63 -89
  36. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_add_container_registry_body.py → v1_container_registry_config.py} +16 -16
  37. lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_container_registry_scopes.py} +39 -39
  38. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_create_container_registry_response.py} +6 -6
  39. lightning_sdk/lightning_cloud/openapi/models/v1_delete_org_cluster_capacity_reservation_response.py +97 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_describe_org_cluster_capacity_reservation_response.py +201 -0
  41. lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
  42. lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → v1_get_cluster_capacity_reservation_response.py} +23 -23
  43. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  44. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +1 -27
  45. lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
  46. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → v1_mithril_direct_v1.py} +51 -51
  47. lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
  48. lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +27 -1
  49. lightning_sdk/lightning_cloud/openapi/models/v1_update_container_registry_response.py +97 -0
  50. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -105
  51. lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
  52. lightning_sdk/teamspace.py +28 -7
  53. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/METADATA +1 -1
  54. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/RECORD +59 -53
  55. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
  56. lightning_sdk/lightning_cloud/openapi/models/v1_ecr_registry_details.py +0 -201
  57. /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
  58. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/LICENSE +0 -0
  59. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/WHEEL +0 -0
  60. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/entry_points.txt +0 -0
  61. {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.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.30"
@@ -13,17 +13,20 @@ import requests
13
13
  from tqdm import tqdm
14
14
 
15
15
  from lightning_sdk.api.utils import (
16
+ _MAX_SIZE_MULTI_PART_CHUNK,
17
+ _authenticate_and_get_token,
16
18
  _create_app,
17
19
  _DummyBody,
18
20
  _DummyResponse,
21
+ _FileUploader,
19
22
  _machine_to_compute_name,
20
23
  _sanitize_studio_remote_path,
24
+ _SinglePartFileUploader,
21
25
  )
22
26
  from lightning_sdk.api.utils import (
23
27
  _get_cloud_url as _cloud_url,
24
28
  )
25
29
  from lightning_sdk.constants import _LIGHTNING_DEBUG
26
- from lightning_sdk.lightning_cloud.login import Auth
27
30
  from lightning_sdk.lightning_cloud.openapi import (
28
31
  AssistantsServiceCreateAssistantBody,
29
32
  AssistantsServiceCreateAssistantManagedEndpointBody,
@@ -49,7 +52,6 @@ from lightning_sdk.lightning_cloud.openapi import (
49
52
  V1EnvVar,
50
53
  V1GetCloudSpaceInstanceStatusResponse,
51
54
  V1GetLongRunningCommandInCloudSpaceResponse,
52
- V1LoginRequest,
53
55
  V1ManagedEndpoint,
54
56
  V1ManagedModel,
55
57
  V1Plugin,
@@ -661,14 +663,8 @@ class StudioApi:
661
663
  self.stop_keeping_alive(teamspace_id=teamspace_id, studio_id=studio_id)
662
664
  self._client.cloud_space_service_delete_cloud_space(project_id=teamspace_id, id=studio_id)
663
665
 
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
666
  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()
667
+ token = _authenticate_and_get_token(self._client)
672
668
 
673
669
  if query_params is None:
674
670
  query_params = {
@@ -729,33 +725,36 @@ class StudioApi:
729
725
  remote_path: str,
730
726
  progress_bar: bool,
731
727
  ) -> None:
732
- """Uploads file to given remote path on the studio."""
733
- token = self._authenticate_and_get_token()
734
-
735
- query_params = {"token": token}
736
- client_host = self._client.api_client.configuration.host
737
- url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/blobs/{remote_path}"
738
-
739
- filesize = os.path.getsize(file_path)
740
- with open(file_path, "rb") as f:
741
- if progress_bar:
742
- filesize = os.path.getsize(file_path)
743
- with tqdm.wrapattr(
744
- f,
745
- "read",
746
- desc=f"Uploading {os.path.split(file_path)[1]}",
747
- total=filesize,
748
- unit="B",
749
- unit_scale=True,
750
- unit_divisor=1000,
751
- ) as wrapped_file:
752
- r = requests.put(url, data=wrapped_file, params=query_params, timeout=30)
753
- else:
754
- r = requests.put(url, data=f, params=query_params, timeout=30)
728
+ """Uploads file to given remote path in the studio.
755
729
 
756
- if r.status_code == 200:
757
- return
758
- raise RuntimeError(f"Failed to upload file '{file_path}' to the Studio. Status code: {r.status_code}")
730
+ Uses single-part upload for files <= 5MB, multipart upload for larger files.
731
+ """
732
+ file_size = os.path.getsize(file_path)
733
+ multipart_threshold = int(os.environ.get("LIGHTNING_MULTIPART_THRESHOLD", _MAX_SIZE_MULTI_PART_CHUNK))
734
+
735
+ if file_size <= multipart_threshold:
736
+ token = _authenticate_and_get_token(self._client)
737
+
738
+ query_params = {"token": token}
739
+ client_host = self._client.api_client.configuration.host
740
+ url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/blobs/{remote_path}"
741
+
742
+ _SinglePartFileUploader(
743
+ client=self._client,
744
+ file_path=file_path,
745
+ url=url,
746
+ query_params=query_params,
747
+ progress_bar=progress_bar,
748
+ )()
749
+ else:
750
+ _FileUploader(
751
+ client=self._client,
752
+ teamspace_id=teamspace_id,
753
+ cloud_account=cloud_account,
754
+ file_path=file_path,
755
+ remote_path=_sanitize_studio_remote_path(remote_path, studio_id),
756
+ progress_bar=progress_bar,
757
+ )()
759
758
 
760
759
  def download_file(
761
760
  self,
@@ -768,7 +767,7 @@ class StudioApi:
768
767
  ) -> None:
769
768
  """Downloads a given file from a Studio to a target location."""
770
769
  # TODO: Update this endpoint to permit basic auth
771
- token = self._authenticate_and_get_token()
770
+ token = _authenticate_and_get_token(self._client)
772
771
 
773
772
  query_params = {
774
773
  "clusterId": cloud_account,
@@ -864,7 +863,7 @@ class StudioApi:
864
863
  print(f"No files found in {path}")
865
864
  return
866
865
 
867
- token = self._authenticate_and_get_token()
866
+ token = _authenticate_and_get_token(self._client)
868
867
 
869
868
  total_size = sum(f.get("size", 0) for f in files)
870
869
 
@@ -910,7 +909,7 @@ class StudioApi:
910
909
  if info["type"] != "file":
911
910
  raise IsADirectoryError(f"The path '{path}' is a directory. Use 'remove_folder()' to remove directories.")
912
911
 
913
- token = self._authenticate_and_get_token()
912
+ token = _authenticate_and_get_token(self._client)
914
913
 
915
914
  query_params = {"token": token}
916
915
  client_host = self._client.api_client.configuration.host
@@ -933,7 +932,7 @@ class StudioApi:
933
932
  if info["type"] == "file":
934
933
  raise ValueError(f"The path '{path}' is a file. Use 'remove_file()' to remove files.")
935
934
 
936
- token = self._authenticate_and_get_token()
935
+ token = _authenticate_and_get_token(self._client)
937
936
 
938
937
  query_params = {"token": token}
939
938
  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,15 @@ import requests
7
10
  from tqdm.auto import tqdm
8
11
 
9
12
  from lightning_sdk.api.utils import (
13
+ _MAX_SIZE_MULTI_PART_CHUNK,
14
+ _authenticate_and_get_token,
10
15
  _download_model_files,
11
- _download_teamspace_files,
12
16
  _DummyBody,
17
+ _FileUploader,
13
18
  _get_model_version,
14
19
  _ModelFileUploader,
15
20
  _resolve_teamspace_remote_path,
21
+ _SinglePartFileUploader,
16
22
  )
17
23
  from lightning_sdk.lightning_cloud.login import Auth
18
24
  from lightning_sdk.lightning_cloud.openapi import (
@@ -32,7 +38,6 @@ from lightning_sdk.lightning_cloud.openapi import (
32
38
  V1ExternalCluster,
33
39
  V1GCSFolderDataConnection,
34
40
  V1Job,
35
- V1LoginRequest,
36
41
  V1Model,
37
42
  V1ModelVersionArchive,
38
43
  V1MultiMachineJob,
@@ -391,6 +396,58 @@ class TeamspaceApi:
391
396
  response = self.models_api.models_store_list_model_versions(project_id=teamspace_id, model_id=model_id)
392
397
  return response.versions
393
398
 
399
+ def get_uploads_tree(self, teamspace_id: str, path: str, query_params: Optional[dict] = None) -> None:
400
+ token = _authenticate_and_get_token(self._client)
401
+
402
+ if query_params is None:
403
+ query_params = {
404
+ "token": token,
405
+ }
406
+ else:
407
+ query_params["token"] = token
408
+ r = requests.get(
409
+ f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/trees/{path}",
410
+ params=query_params,
411
+ )
412
+ return r.json()
413
+
414
+ def get_path_info(self, teamspace_id: str, path: str = "") -> dict:
415
+ path = path.strip("/")
416
+
417
+ if "/" in path:
418
+ parent_path = path.rsplit("/", 1)[0]
419
+ target_name = path.rsplit("/", 1)[1]
420
+ else:
421
+ if path == "":
422
+ # root directory
423
+ return {"exists": True, "type": "directory", "size": None}
424
+ parent_path = ""
425
+ target_name = path
426
+
427
+ tree = self.get_uploads_tree(teamspace_id, path=parent_path)
428
+ tree_items = tree.get("tree", [])
429
+ for item in tree_items:
430
+ item_name = item.get("path", "")
431
+ if item_name == target_name:
432
+ item_type = item.get("type")
433
+ # if type == "blob" it's a file, if "tree" it's a directory
434
+ return {
435
+ "exists": True,
436
+ "type": "file" if item_type == "blob" else "directory",
437
+ "size": item.get("size", 0) if item_type == "blob" else None,
438
+ }
439
+ warnings.warn(f"If '{path}' is a directory, it may be empty and thus not detected.")
440
+ return {"exists": False, "type": None, "size": None}
441
+
442
+ def list_uploads_files(
443
+ self,
444
+ teamspace_id: str,
445
+ path: str = "",
446
+ ) -> List[Dict]:
447
+ """Recursively list all files in a /Uploads/ directory tree."""
448
+ path = path.strip("/")
449
+ return self.get_uploads_tree(teamspace_id, path, query_params={"recursive": "true"}).get("tree", [])
450
+
394
451
  def upload_file(
395
452
  self,
396
453
  teamspace_id: str,
@@ -398,73 +455,65 @@ class TeamspaceApi:
398
455
  file_path: str,
399
456
  remote_path: str,
400
457
  progress_bar: bool,
458
+ headers: Optional[Dict[str, str]] = None,
401
459
  ) -> 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
406
-
407
- query_params = {"token": token, "clusterId": cloud_account}
408
- client_host = self._client.api_client.configuration.host
409
- url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{remote_path}"
410
-
411
- filesize = os.path.getsize(file_path)
412
- with open(file_path, "rb") as f:
413
- if progress_bar:
414
- filesize = os.path.getsize(file_path)
415
- with tqdm.wrapattr(
416
- f,
417
- "read",
418
- desc=f"Uploading {os.path.split(file_path)[1]}",
419
- total=filesize,
420
- unit="B",
421
- unit_scale=True,
422
- unit_divisor=1000,
423
- ) as wrapped_file:
424
- r = requests.put(url, data=wrapped_file, params=query_params, timeout=30)
425
- else:
426
- r = requests.put(url, data=f, params=query_params, timeout=30)
427
-
428
- if r.status_code == 200:
429
- return
430
- raise RuntimeError(f"Failed to upload file '{file_path}' to the Teamspace drive. Status code: {r.status_code}")
460
+ """Uploads file to given remote path in the Teamspace drive.
461
+
462
+ Uses single-part upload for files <= 5MB, multipart upload for larger files.
463
+ """
464
+ file_size = os.path.getsize(file_path)
465
+
466
+ multipart_threshold = int(os.environ.get("LIGHTNING_MULTIPART_THRESHOLD", _MAX_SIZE_MULTI_PART_CHUNK))
467
+
468
+ if file_size <= multipart_threshold:
469
+ token = _authenticate_and_get_token(self._client)
470
+
471
+ query_params = {"token": token, "clusterId": cloud_account}
472
+ client_host = self._client.api_client.configuration.host
473
+ url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/blobs/{remote_path}"
474
+
475
+ _SinglePartFileUploader(
476
+ client=self._client,
477
+ file_path=file_path,
478
+ url=url,
479
+ query_params=query_params,
480
+ progress_bar=progress_bar,
481
+ headers=headers,
482
+ )()
483
+ else:
484
+ _FileUploader(
485
+ client=self._client,
486
+ teamspace_id=teamspace_id,
487
+ cloud_account=cloud_account,
488
+ file_path=file_path,
489
+ remote_path=_resolve_teamspace_remote_path(remote_path),
490
+ progress_bar=progress_bar,
491
+ )()
431
492
 
432
493
  def download_file(
433
494
  self,
434
495
  path: str,
435
496
  target_path: str,
436
497
  teamspace_id: str,
498
+ cloud_account: Optional[str] = None,
437
499
  progress_bar: bool = True,
438
500
  ) -> None:
439
- """Downloads a given file in Teamspace drive to a target location."""
501
+ """Downloads a given file in Teamspace drive /Uploads/ to a target location."""
440
502
  # 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
503
+ token = _authenticate_and_get_token(self._client)
444
504
 
445
- cluster_ids = [ca.cluster_id for ca in self.list_cloud_accounts(teamspace_id)]
505
+ query_params = {
506
+ "token": token,
507
+ }
446
508
 
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
- )
460
-
461
- if r.status_code == 200:
462
- found = True
463
- break
464
-
465
- if not found:
466
- raise FileNotFoundError(f"The provided path does not exist in the teamspace: {path}")
509
+ if cloud_account:
510
+ query_params["clusterId"] = cloud_account
467
511
 
512
+ r = requests.get(
513
+ f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{path}",
514
+ params=query_params,
515
+ stream=True,
516
+ )
468
517
  total_length = int(r.headers.get("content-length"))
469
518
 
470
519
  if progress_bar:
@@ -488,33 +537,101 @@ class TeamspaceApi:
488
537
  f.write(chunk)
489
538
  pbar_update(len(chunk))
490
539
 
540
+ def _download_single_file(
541
+ self,
542
+ file_info: Dict,
543
+ base_path: str,
544
+ download_dir: Path,
545
+ teamspace_id: str,
546
+ token: str,
547
+ cloud_account: Optional[str] = None,
548
+ pbar: Optional[tqdm] = True,
549
+ ) -> None:
550
+ """Download a single file from Teamspace drive /Uploads/ with progress tracking."""
551
+ relative_path = file_info["path"].lstrip("/")
552
+ local_file = download_dir / relative_path
553
+ local_file.parent.mkdir(parents=True, exist_ok=True)
554
+
555
+ file_path = os.path.join(base_path, relative_path) if base_path else relative_path
556
+
557
+ query_params = {
558
+ "token": token,
559
+ }
560
+ if cloud_account:
561
+ query_params["clusterId"] = cloud_account
562
+
563
+ r = requests.get(
564
+ f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{file_path}",
565
+ params=query_params,
566
+ stream=True,
567
+ )
568
+
569
+ with open(str(local_file), "wb") as f:
570
+ for chunk in r.iter_content(chunk_size=4096 * 8):
571
+ f.write(chunk)
572
+ if pbar:
573
+ pbar.update(len(chunk))
574
+
491
575
  def download_folder(
492
576
  self,
493
577
  path: str,
494
578
  target_path: str,
495
579
  teamspace_id: str,
496
- cloud_account: str,
580
+ cloud_account: Optional[str] = None,
497
581
  progress_bar: bool = True,
582
+ num_workers: Optional[int] = None,
498
583
  ) -> None:
499
- """Downloads a given folder from Teamspace drive to a target location."""
584
+ """Downloads a given folder from Teamspace drive /Uploads/ to a target location."""
500
585
  # TODO: Update this endpoint to permit basic auth
501
- auth = Auth()
502
- auth.authenticate()
586
+ if num_workers is None:
587
+ num_workers = os.cpu_count() * 4
503
588
 
504
- prefix = _resolve_teamspace_remote_path(path)
589
+ # Normalize the path
590
+ path = path.strip("/")
591
+ download_dir = Path(target_path)
592
+ download_dir.mkdir(parents=True, exist_ok=True)
505
593
 
506
- # ensure we only download as a directory and not the entire prefix
507
- if prefix.endswith("/") is False:
508
- prefix = prefix + "/"
594
+ files = self.list_uploads_files(teamspace_id, path)
509
595
 
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
- )
596
+ if not files:
597
+ print(f"No files found in {path}")
598
+ return
599
+
600
+ token = _authenticate_and_get_token(self._client)
601
+
602
+ total_size = sum(f.get("size", 0) for f in files)
603
+
604
+ pbar = None
605
+ if progress_bar:
606
+ pbar = tqdm(
607
+ desc="Downloading files",
608
+ total=total_size,
609
+ unit="B",
610
+ unit_scale=True,
611
+ unit_divisor=1000,
612
+ mininterval=1,
613
+ )
614
+
615
+ with ThreadPoolExecutor(max_workers=num_workers) as executor:
616
+ futures = [
617
+ executor.submit(
618
+ self._download_single_file,
619
+ file_info,
620
+ path,
621
+ download_dir,
622
+ teamspace_id,
623
+ token,
624
+ cloud_account,
625
+ pbar,
626
+ )
627
+ for file_info in files
628
+ ]
629
+ concurrent.futures.wait(futures)
630
+
631
+ if pbar:
632
+ pbar.set_description("Download complete")
633
+ pbar.refresh()
634
+ pbar.close()
518
635
 
519
636
  def get_secrets(self, teamspace_id: str) -> Dict[str, str]:
520
637
  """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,
@@ -57,6 +59,66 @@ _MAX_BATCH_SIZE = 50
57
59
  _MAX_WORKERS = 10
58
60
 
59
61
 
62
+ class _SinglePartFileUploader:
63
+ """A class handling upload files to studio and teamspace drive with new endpoint."""
64
+
65
+ def __init__(
66
+ self,
67
+ client: LightningClient,
68
+ file_path: str,
69
+ url: str,
70
+ query_params: Dict[str, str],
71
+ progress_bar: bool,
72
+ headers: Optional[Dict[str, str]] = None,
73
+ ) -> None:
74
+ self.client = client
75
+ self.local_path = file_path
76
+ self.url = url
77
+ self.query_params = query_params
78
+ self.headers = headers
79
+ self.filesize = os.path.getsize(file_path)
80
+
81
+ if progress_bar:
82
+ self.progress_bar = tqdm(
83
+ desc=f"Uploading {os.path.split(file_path)[1]}",
84
+ total=self.filesize,
85
+ unit="B",
86
+ unit_scale=True,
87
+ unit_divisor=1000,
88
+ position=-1,
89
+ mininterval=1,
90
+ )
91
+ else:
92
+ self.progress_bar = None
93
+
94
+ def __call__(self) -> None:
95
+ self._upload_with_retry()
96
+
97
+ @backoff.on_exception(
98
+ backoff.expo, (requests.exceptions.HTTPError, requests.exceptions.RequestException), max_tries=10
99
+ )
100
+ def _upload_with_retry(self) -> None:
101
+ with open(self.local_path, "rb") as f:
102
+ if self.progress_bar is not None:
103
+ with tqdm.wrapattr(
104
+ f,
105
+ "read",
106
+ desc=f"Uploading {os.path.split(self.local_path)[1]}",
107
+ total=self.filesize,
108
+ unit="B",
109
+ unit_scale=True,
110
+ unit_divisor=1000,
111
+ ) as wrapped_file:
112
+ r = requests.put(
113
+ self.url, data=wrapped_file, params=self.query_params, timeout=30, headers=self.headers
114
+ )
115
+ else:
116
+ r = requests.put(self.url, data=f, params=self.query_params, timeout=30, headers=self.headers)
117
+
118
+ if r.status_code != 200:
119
+ raise RuntimeError(f"Failed to upload file '{self.local_path}'. Status code: {r.status_code}")
120
+
121
+
60
122
  class _FileUploader:
61
123
  """A class handling the upload to studios.
62
124
 
@@ -356,7 +418,7 @@ def _sanitize_studio_remote_path(path: str, studio_id: str) -> str:
356
418
 
357
419
 
358
420
  def _resolve_teamspace_remote_path(path: str) -> str:
359
- return f"/Uploads/{path.replace('/teamspace/uploads/', '')}"
421
+ return f"{path.replace('/teamspace/', '')}"
360
422
 
361
423
 
362
424
  _DOWNLOAD_REQUEST_CHUNK_SIZE = 10 * _BYTES_PER_MB
@@ -816,3 +878,9 @@ def to_iso_z(dt: datetime) -> str:
816
878
  dt = dt.replace(tzinfo=timezone.utc)
817
879
  return dt.astimezone(timezone.utc).isoformat(timespec="milliseconds")
818
880
  return dt.isoformat(timespec="milliseconds")
881
+
882
+
883
+ def _authenticate_and_get_token(client: Any) -> str:
884
+ auth = Auth()
885
+ auth.authenticate()
886
+ 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