lightning-sdk 2025.12.17__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 (130) hide show
  1. lightning_sdk/__version__.py +1 -1
  2. lightning_sdk/api/k8s_api.py +75 -29
  3. lightning_sdk/api/studio_api.py +192 -37
  4. lightning_sdk/api/teamspace_api.py +180 -54
  5. lightning_sdk/api/utils.py +8 -0
  6. lightning_sdk/cli/cp/__init__.py +67 -0
  7. lightning_sdk/cli/cp/teamspace_uploads.py +93 -0
  8. lightning_sdk/cli/entrypoint.py +2 -0
  9. lightning_sdk/cli/groups.py +22 -0
  10. lightning_sdk/cli/legacy/clusters_menu.py +2 -2
  11. lightning_sdk/cli/legacy/deploy/_auth.py +7 -6
  12. lightning_sdk/cli/legacy/download.py +29 -98
  13. lightning_sdk/cli/legacy/run.py +13 -2
  14. lightning_sdk/cli/legacy/upload.py +24 -31
  15. lightning_sdk/cli/studio/__init__.py +4 -0
  16. lightning_sdk/cli/studio/cp.py +24 -65
  17. lightning_sdk/cli/studio/ls.py +57 -0
  18. lightning_sdk/cli/studio/rm.py +71 -0
  19. lightning_sdk/cli/utils/filesystem.py +103 -0
  20. lightning_sdk/cli/utils/logging.py +2 -1
  21. lightning_sdk/cli/utils/teamspace_selection.py +5 -0
  22. lightning_sdk/exceptions.py +31 -0
  23. lightning_sdk/job/base.py +1 -1
  24. lightning_sdk/k8s_cluster.py +9 -10
  25. lightning_sdk/lightning_cloud/__version__.py +1 -1
  26. lightning_sdk/lightning_cloud/openapi/__init__.py +43 -23
  27. lightning_sdk/lightning_cloud/openapi/api/__init__.py +2 -1
  28. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +118 -1
  29. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +378 -536
  30. lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +456 -0
  31. lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +5 -1
  32. lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +11 -11
  33. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +113 -0
  34. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +246 -19
  35. lightning_sdk/lightning_cloud/openapi/api/lightningwork_service_api.py +116 -11
  36. lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +588 -2
  37. lightning_sdk/lightning_cloud/openapi/api/models_store_api.py +9 -1
  38. lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +113 -0
  39. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
  40. lightning_sdk/lightning_cloud/openapi/api/{kubernetes_virtual_machine_service_api.py → virtual_machine_service_api.py} +82 -82
  41. lightning_sdk/lightning_cloud/openapi/models/__init__.py +41 -22
  42. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_cluster_capacity_reservation_body.py +53 -1
  43. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_machine_body.py +53 -1
  44. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_org_cluster_capacity_reservation_body.py +409 -0
  45. lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → cluster_service_report_machine_system_metrics_body.py} +23 -23
  46. lightning_sdk/lightning_cloud/openapi/models/container_registry_config_ecr.py +149 -0
  47. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_status.py → container_registry_provider.py} +14 -10
  48. lightning_sdk/lightning_cloud/openapi/models/container_registry_service_create_container_registry_body.py +201 -0
  49. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config_input.py → container_registry_service_refresh_container_registry_credentials_body.py} +21 -21
  50. lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
  51. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → jobs_service_duplicate_deployment_body.py} +51 -51
  52. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_create_lit_logger_media_body.py +305 -0
  53. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_lit_logger_media_body.py +149 -0
  54. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_metrics_stream_body.py +53 -1
  55. lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
  56. lightning_sdk/lightning_cloud/openapi/models/v1_capacity_reservation_used_by.py +227 -0
  57. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +2 -1
  58. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +53 -1
  61. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +79 -27
  62. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +0 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +63 -89
  64. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_add_container_registry_body.py → v1_container_registry_config.py} +16 -16
  65. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_scopes.py +149 -0
  66. lightning_sdk/lightning_cloud/openapi/models/{v1_delete_kubernetes_virtual_machine_response.py → v1_create_container_registry_response.py} +6 -6
  67. lightning_sdk/lightning_cloud/openapi/models/v1_create_lit_logger_media_response.py +149 -0
  68. lightning_sdk/lightning_cloud/openapi/models/v1_create_org_cluster_capacity_reservation_response.py +201 -0
  69. lightning_sdk/lightning_cloud/openapi/models/v1_create_sdk_command_history_request.py +29 -3
  70. lightning_sdk/lightning_cloud/openapi/models/v1_cudo_direct_v1.py +175 -0
  71. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → v1_delete_lit_logger_media_response.py} +6 -6
  72. lightning_sdk/lightning_cloud/openapi/models/{kubernetes_virtual_machine_service_update_kubernetes_virtual_machine_body.py → v1_delete_org_cluster_capacity_reservation_response.py} +6 -6
  73. lightning_sdk/lightning_cloud/openapi/models/v1_delete_virtual_machine_response.py +97 -0
  74. lightning_sdk/lightning_cloud/openapi/models/v1_describe_org_cluster_capacity_reservation_response.py +201 -0
  75. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -27
  76. lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
  77. lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
  78. lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_get_kubernetes_pod_logs_response.py} +37 -37
  79. lightning_sdk/lightning_cloud/openapi/models/{v1_get_machine_response.py → v1_get_kubernetes_pod_response.py} +23 -23
  80. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  81. lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
  82. lightning_sdk/lightning_cloud/openapi/models/v1_joinable_organization.py +27 -1
  83. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → v1_k8s_incident_setting.py} +49 -23
  84. lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_type.py +108 -0
  85. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_settings_v1.py +53 -1
  86. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +53 -27
  87. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod.py +27 -1
  88. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod_logs_page.py +227 -0
  89. lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_config.py +53 -1
  90. lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
  91. lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_response.py +43 -17
  92. lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_sort_order.py +104 -0
  93. lightning_sdk/lightning_cloud/openapi/models/v1_list_lit_logger_media_response.py +149 -0
  94. lightning_sdk/lightning_cloud/openapi/models/v1_list_models_response.py +55 -3
  95. lightning_sdk/lightning_cloud/openapi/models/{v1_list_kubernetes_virtual_machines_response.py → v1_list_virtual_machines_response.py} +16 -16
  96. lightning_sdk/lightning_cloud/openapi/models/v1_lit_logger_media.py +513 -0
  97. lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -53
  98. lightning_sdk/lightning_cloud/openapi/models/v1_machine_direct_v1.py +107 -3
  99. lightning_sdk/lightning_cloud/openapi/models/v1_media_type.py +104 -0
  100. lightning_sdk/lightning_cloud/openapi/models/{v1_ai_pod_v1.py → v1_mithril_direct_v1.py} +51 -51
  101. lightning_sdk/lightning_cloud/openapi/models/v1_nebius_direct_v1.py +29 -3
  102. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
  103. lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
  104. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_report_cloud_space_instance_idle_state_response.py} +6 -6
  105. lightning_sdk/lightning_cloud/openapi/models/v1_report_machine_system_metrics_response.py +97 -0
  106. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_details.py → v1_tenant_credentials.py} +53 -53
  107. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +183 -157
  108. lightning_sdk/lightning_cloud/openapi/models/{v1_kubernetes_virtual_machine.py → v1_virtual_machine.py} +94 -68
  109. lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_vm_configuration.py → v1_vm_configuration.py} +20 -20
  110. lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_provider_configuration.py → v1_vm_provider_configuration.py} +32 -32
  111. lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_create_virtual_machine_body.py +565 -0
  112. lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_update_virtual_machine_body.py +97 -0
  113. lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
  114. lightning_sdk/lightning_cloud/rest_client.py +0 -2
  115. lightning_sdk/machine.py +3 -3
  116. lightning_sdk/studio.py +14 -4
  117. lightning_sdk/teamspace.py +28 -7
  118. lightning_sdk/utils/logging.py +2 -1
  119. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/METADATA +1 -5
  120. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/RECORD +125 -102
  121. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/WHEEL +1 -1
  122. lightning_sdk/lightning_cloud/cli/__main__.py +0 -29
  123. lightning_sdk/lightning_cloud/openapi/models/kubernetes_virtual_machine_service_create_kubernetes_virtual_machine_body.py +0 -513
  124. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
  125. lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_vm_resources.py +0 -201
  126. lightning_sdk/lightning_cloud/source_code/logs_socket_api.py +0 -103
  127. /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
  128. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/LICENSE +0 -0
  129. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/entry_points.txt +0 -0
  130. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/top_level.txt +0 -0
@@ -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,13 +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
- _FileUploader,
14
16
  _get_model_version,
15
17
  _ModelFileUploader,
16
- _resolve_teamspace_remote_path,
17
18
  )
18
19
  from lightning_sdk.lightning_cloud.login import Auth
19
20
  from lightning_sdk.lightning_cloud.openapi import (
@@ -33,7 +34,6 @@ from lightning_sdk.lightning_cloud.openapi import (
33
34
  V1ExternalCluster,
34
35
  V1GCSFolderDataConnection,
35
36
  V1Job,
36
- V1LoginRequest,
37
37
  V1Model,
38
38
  V1ModelVersionArchive,
39
39
  V1MultiMachineJob,
@@ -392,6 +392,58 @@ class TeamspaceApi:
392
392
  response = self.models_api.models_store_list_model_versions(project_id=teamspace_id, model_id=model_id)
393
393
  return response.versions
394
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
+
395
447
  def upload_file(
396
448
  self,
397
449
  teamspace_id: str,
@@ -400,52 +452,58 @@ class TeamspaceApi:
400
452
  remote_path: str,
401
453
  progress_bar: bool,
402
454
  ) -> None:
403
- """Uploads file to given remote path in the Teamspace drive."""
404
- _FileUploader(
405
- client=self._client,
406
- teamspace_id=teamspace_id,
407
- cloud_account=cloud_account,
408
- file_path=file_path,
409
- remote_path=_resolve_teamspace_remote_path(remote_path),
410
- progress_bar=progress_bar,
411
- )()
455
+ """Uploads file to given remote path in the Teamspace drive /Uploads/."""
456
+ token = _authenticate_and_get_token(self._client)
457
+
458
+ query_params = {"token": token, "clusterId": cloud_account}
459
+ client_host = self._client.api_client.configuration.host
460
+ url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{remote_path}"
461
+
462
+ filesize = os.path.getsize(file_path)
463
+ with open(file_path, "rb") as f:
464
+ if progress_bar:
465
+ filesize = os.path.getsize(file_path)
466
+ with tqdm.wrapattr(
467
+ f,
468
+ "read",
469
+ desc=f"Uploading {os.path.split(file_path)[1]}",
470
+ total=filesize,
471
+ unit="B",
472
+ unit_scale=True,
473
+ unit_divisor=1000,
474
+ ) as wrapped_file:
475
+ r = requests.put(url, data=wrapped_file, params=query_params, timeout=30)
476
+ else:
477
+ r = requests.put(url, data=f, params=query_params, timeout=30)
478
+
479
+ if r.status_code == 200:
480
+ return
481
+ raise RuntimeError(f"Failed to upload file '{file_path}' to the Teamspace drive. Status code: {r.status_code}")
412
482
 
413
483
  def download_file(
414
484
  self,
415
485
  path: str,
416
486
  target_path: str,
417
487
  teamspace_id: str,
488
+ cloud_account: Optional[str] = None,
418
489
  progress_bar: bool = True,
419
490
  ) -> None:
420
- """Downloads a given file in Teamspace drive to a target location."""
491
+ """Downloads a given file in Teamspace drive /Uploads/ to a target location."""
421
492
  # TODO: Update this endpoint to permit basic auth
422
- auth = Auth()
423
- auth.authenticate()
424
- token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
425
-
426
- cluster_ids = [ca.cluster_id for ca in self.list_cloud_accounts(teamspace_id)]
427
-
428
- found = False
429
- for cluster_id in cluster_ids:
430
- query_params = {
431
- "clusterId": cluster_id,
432
- "key": _resolve_teamspace_remote_path(path),
433
- "token": token,
434
- }
493
+ token = _authenticate_and_get_token(self._client)
435
494
 
436
- r = requests.get(
437
- f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/download",
438
- params=query_params,
439
- stream=True,
440
- )
441
-
442
- if r.status_code == 200:
443
- found = True
444
- break
495
+ query_params = {
496
+ "token": token,
497
+ }
445
498
 
446
- if not found:
447
- raise FileNotFoundError(f"The provided path does not exist in the teamspace: {path}")
499
+ if cloud_account:
500
+ query_params["clusterId"] = cloud_account
448
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
+ )
449
507
  total_length = int(r.headers.get("content-length"))
450
508
 
451
509
  if progress_bar:
@@ -469,33 +527,101 @@ class TeamspaceApi:
469
527
  f.write(chunk)
470
528
  pbar_update(len(chunk))
471
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
+
472
565
  def download_folder(
473
566
  self,
474
567
  path: str,
475
568
  target_path: str,
476
569
  teamspace_id: str,
477
- cloud_account: str,
570
+ cloud_account: Optional[str] = None,
478
571
  progress_bar: bool = True,
572
+ num_workers: Optional[int] = None,
479
573
  ) -> None:
480
- """Downloads a given folder from Teamspace drive to a target location."""
574
+ """Downloads a given folder from Teamspace drive /Uploads/ to a target location."""
481
575
  # TODO: Update this endpoint to permit basic auth
482
- auth = Auth()
483
- auth.authenticate()
576
+ if num_workers is None:
577
+ num_workers = os.cpu_count() * 4
484
578
 
485
- 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)
486
583
 
487
- # ensure we only download as a directory and not the entire prefix
488
- if prefix.endswith("/") is False:
489
- prefix = prefix + "/"
584
+ files = self.list_uploads_files(teamspace_id, path)
490
585
 
491
- _download_teamspace_files(
492
- client=self._client,
493
- teamspace_id=teamspace_id,
494
- cluster_id=cloud_account,
495
- prefix=prefix,
496
- download_dir=Path(target_path),
497
- progress_bar=progress_bar,
498
- )
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()
499
625
 
500
626
  def get_secrets(self, teamspace_id: str) -> Dict[str, str]:
501
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
@@ -0,0 +1,67 @@
1
+ """CP CLI commands."""
2
+
3
+ from typing import Any, Literal, Optional
4
+
5
+ import click
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
9
+ from lightning_sdk.cli.studio.cp import cp_download as studio_cp_download
10
+ from lightning_sdk.cli.studio.cp import cp_upload as studio_cp_upload
11
+
12
+
13
+ def parse_lit_url(url: str) -> tuple[str, list[str], Literal["studios", "uploads"]]:
14
+ """Parse lit:// URL and extract resource type."""
15
+ if "://" not in url:
16
+ raise ValueError("URL must contain '://'")
17
+
18
+ path = url.split("://")[-1].split("/")
19
+
20
+ if path[2] == "studios":
21
+ resource_type = "studios"
22
+ elif path[2] == "uploads":
23
+ resource_type = "uploads"
24
+ else:
25
+ raise ValueError("URL must contain either 'studios' or 'uploads'")
26
+
27
+ return resource_type
28
+
29
+
30
+ def route_cp_operation(source: str, destination: str, **options: Any) -> None:
31
+ """Route copy operation based on URL structure."""
32
+ source_is_lit = source.startswith("lit://")
33
+ dest_is_lit = destination.startswith("lit://")
34
+
35
+ if source_is_lit and dest_is_lit:
36
+ raise ValueError("Cannot copy between two remote URLs. One path must be local.")
37
+
38
+ if not source_is_lit and not dest_is_lit:
39
+ raise ValueError("At least one path must be a lit://")
40
+
41
+ if source_is_lit:
42
+ resource_type = parse_lit_url(source)
43
+ if resource_type == "studios":
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)
47
+ raise ValueError(f"Resource type: {resource_type} is not supported")
48
+ else:
49
+ resource_type = parse_lit_url(destination)
50
+ if resource_type == "studios":
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)
54
+ raise ValueError(f"Resource type: {resource_type} is not supported")
55
+
56
+
57
+ def register_commands(command: click.Command) -> None:
58
+ """Register cp command callback."""
59
+
60
+ def new_callback(source: str, destination: Optional[str], recursive: bool, **kwargs: Any) -> None:
61
+ route_cp_operation(
62
+ source=source,
63
+ destination=destination,
64
+ recursive=recursive,
65
+ )
66
+
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}")
@@ -12,6 +12,7 @@ from lightning_sdk.api.studio_api import _cloud_url
12
12
  from lightning_sdk.cli.groups import (
13
13
  base_studio,
14
14
  config,
15
+ cp,
15
16
  # job,
16
17
  license,
17
18
  # mmt,
@@ -77,6 +78,7 @@ main_cli.add_command(studio)
77
78
  main_cli.add_command(vm)
78
79
  main_cli.add_command(base_studio)
79
80
  main_cli.add_command(license)
81
+ main_cli.add_command(cp)
80
82
 
81
83
  if os.environ.get("LIGHTNING_EXPERIMENTAL_CLI_ONLY", "0") != "1":
82
84
  #### LEGACY COMMANDS ####
@@ -4,6 +4,7 @@ import click
4
4
 
5
5
  from lightning_sdk.cli.base_studio import register_commands as register_base_studio_commands
6
6
  from lightning_sdk.cli.config import register_commands as register_config_commands
7
+ from lightning_sdk.cli.cp import register_commands as register_cp_commands
7
8
  from lightning_sdk.cli.job import register_commands as register_job_commands
8
9
  from lightning_sdk.cli.license import register_commands as register_license_commands
9
10
  from lightning_sdk.cli.mmt import register_commands as register_mmt_commands
@@ -46,6 +47,26 @@ def license() -> None: # noqa: A001
46
47
  """Manage Lightning AI Product Licenses."""
47
48
 
48
49
 
50
+ @click.command(name="cp")
51
+ @click.argument("source")
52
+ @click.argument("destination", required=False)
53
+ @click.option("--recursive", "-r", is_flag=True, help="Copy directories recursively")
54
+ @click.pass_context
55
+ def cp() -> None:
56
+ """Copy files between local filesystem, Studios, and teamspace drives.
57
+
58
+ \b
59
+ URL formats:
60
+ Studios: lit://<owner>/<teamspace>/studios/<studio-name>/<path>
61
+ Teamspace drives: lit://<owner>/<teamspace>/uploads/<path>
62
+
63
+ \b
64
+ Examples:
65
+ lightning studio cp source.txt lit://<owner>/<my-teamspace>/studios/<my-studio>/destination.txt
66
+ lightning studio cp -r source_folder/ lit://<owner>/<my-teamspace>/studios/<my-studio>/destination_folder/
67
+ """
68
+
69
+
49
70
  # Register config commands with the main config group
50
71
  register_job_commands(job)
51
72
  register_mmt_commands(mmt)
@@ -54,3 +75,4 @@ register_config_commands(config)
54
75
  register_vm_commands(vm)
55
76
  register_base_studio_commands(base_studio)
56
77
  register_license_commands(license)
78
+ register_cp_commands(cp)
@@ -32,8 +32,8 @@ class _ClustersMenu:
32
32
 
33
33
  cloud_account_api = CloudAccountApi()
34
34
 
35
- resolved_cluster_obj = cloud_account_api.get_cloud_account(
36
- cloud_account_id=selected_cluster_id, org_id=teamspace.owner.id, teamspace_id=teamspace.id
35
+ resolved_cluster_obj = cloud_account_api.get_cloud_account_non_org(
36
+ cloud_account_id=selected_cluster_id, teamspace_id=teamspace.id
37
37
  )
38
38
 
39
39
  return None if resolved_cluster_obj.spec.cluster_type == V1ClusterType.GLOBAL else resolved_cluster_obj.id
@@ -8,7 +8,7 @@ from urllib.parse import urlencode
8
8
  from rich.console import Console
9
9
  from rich.prompt import Confirm
10
10
 
11
- from lightning_sdk import Teamspace
11
+ from lightning_sdk import Organization, Teamspace
12
12
  from lightning_sdk.api import UserApi
13
13
  from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
14
14
  from lightning_sdk.lightning_cloud import env
@@ -74,13 +74,14 @@ def authenticate(mode: _AuthMode, shall_confirm: bool = True) -> None:
74
74
  def select_teamspace(teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
75
75
  if teamspace is None:
76
76
  menu = TeamspacesMenu()
77
- possible_teamspaces = menu._get_possible_teamspaces(_get_authed_user())
77
+ auth_user = _get_authed_user()
78
+ possible_teamspaces = menu._get_possible_teamspaces(auth_user)
78
79
  if len(possible_teamspaces) == 1:
79
- name = next(iter(possible_teamspaces.values()))["name"]
80
- return Teamspace(name=name, org=org, user=user)
81
-
80
+ teamspace_name = next(iter(possible_teamspaces.values()))
81
+ if isinstance(menu._owner, Organization):
82
+ return Teamspace(name=teamspace_name, org=menu._owner, user=None)
83
+ return Teamspace(name=teamspace_name, org=None, user=menu._owner)
82
84
  return menu(teamspace)
83
-
84
85
  return _resolve_teamspace(teamspace=teamspace, org=org, user=user)
85
86
 
86
87