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.
- lightning_sdk/__version__.py +1 -1
- lightning_sdk/api/k8s_api.py +75 -29
- lightning_sdk/api/studio_api.py +192 -37
- lightning_sdk/api/teamspace_api.py +180 -54
- lightning_sdk/api/utils.py +8 -0
- lightning_sdk/cli/cp/__init__.py +67 -0
- lightning_sdk/cli/cp/teamspace_uploads.py +93 -0
- lightning_sdk/cli/entrypoint.py +2 -0
- lightning_sdk/cli/groups.py +22 -0
- lightning_sdk/cli/legacy/clusters_menu.py +2 -2
- lightning_sdk/cli/legacy/deploy/_auth.py +7 -6
- lightning_sdk/cli/legacy/download.py +29 -98
- lightning_sdk/cli/legacy/run.py +13 -2
- lightning_sdk/cli/legacy/upload.py +24 -31
- lightning_sdk/cli/studio/__init__.py +4 -0
- lightning_sdk/cli/studio/cp.py +24 -65
- lightning_sdk/cli/studio/ls.py +57 -0
- lightning_sdk/cli/studio/rm.py +71 -0
- lightning_sdk/cli/utils/filesystem.py +103 -0
- lightning_sdk/cli/utils/logging.py +2 -1
- lightning_sdk/cli/utils/teamspace_selection.py +5 -0
- lightning_sdk/exceptions.py +31 -0
- lightning_sdk/job/base.py +1 -1
- lightning_sdk/k8s_cluster.py +9 -10
- lightning_sdk/lightning_cloud/__version__.py +1 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +43 -23
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +2 -1
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +118 -1
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +378 -536
- lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +456 -0
- lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +11 -11
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +113 -0
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +246 -19
- lightning_sdk/lightning_cloud/openapi/api/lightningwork_service_api.py +116 -11
- lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +588 -2
- lightning_sdk/lightning_cloud/openapi/api/models_store_api.py +9 -1
- lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +113 -0
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/{kubernetes_virtual_machine_service_api.py → virtual_machine_service_api.py} +82 -82
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +41 -22
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_cluster_capacity_reservation_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_machine_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_org_cluster_capacity_reservation_body.py +409 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → cluster_service_report_machine_system_metrics_body.py} +23 -23
- lightning_sdk/lightning_cloud/openapi/models/container_registry_config_ecr.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_status.py → container_registry_provider.py} +14 -10
- lightning_sdk/lightning_cloud/openapi/models/container_registry_service_create_container_registry_body.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config_input.py → container_registry_service_refresh_container_registry_credentials_body.py} +21 -21
- lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → jobs_service_duplicate_deployment_body.py} +51 -51
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_create_lit_logger_media_body.py +305 -0
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_lit_logger_media_body.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_metrics_stream_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_capacity_reservation_used_by.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +2 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +79 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +0 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +63 -89
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_add_container_registry_body.py → v1_container_registry_config.py} +16 -16
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_scopes.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_delete_kubernetes_virtual_machine_response.py → v1_create_container_registry_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_create_lit_logger_media_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_org_cluster_capacity_reservation_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_sdk_command_history_request.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cudo_direct_v1.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → v1_delete_lit_logger_media_response.py} +6 -6
- 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
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_virtual_machine_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_describe_org_cluster_capacity_reservation_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_get_kubernetes_pod_logs_response.py} +37 -37
- lightning_sdk/lightning_cloud/openapi/models/{v1_get_machine_response.py → v1_get_kubernetes_pod_response.py} +23 -23
- lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_joinable_organization.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → v1_k8s_incident_setting.py} +49 -23
- lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_type.py +108 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_settings_v1.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +53 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod_logs_page.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_config.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_response.py +43 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_sort_order.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_lit_logger_media_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_models_response.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/{v1_list_kubernetes_virtual_machines_response.py → v1_list_virtual_machines_response.py} +16 -16
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_logger_media.py +513 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_machine_direct_v1.py +107 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_media_type.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_ai_pod_v1.py → v1_mithril_direct_v1.py} +51 -51
- lightning_sdk/lightning_cloud/openapi/models/v1_nebius_direct_v1.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_report_cloud_space_instance_idle_state_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_report_machine_system_metrics_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_details.py → v1_tenant_credentials.py} +53 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +183 -157
- lightning_sdk/lightning_cloud/openapi/models/{v1_kubernetes_virtual_machine.py → v1_virtual_machine.py} +94 -68
- lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_vm_configuration.py → v1_vm_configuration.py} +20 -20
- lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_provider_configuration.py → v1_vm_provider_configuration.py} +32 -32
- lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_create_virtual_machine_body.py +565 -0
- lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_update_virtual_machine_body.py +97 -0
- lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
- lightning_sdk/lightning_cloud/rest_client.py +0 -2
- lightning_sdk/machine.py +3 -3
- lightning_sdk/studio.py +14 -4
- lightning_sdk/teamspace.py +28 -7
- lightning_sdk/utils/logging.py +2 -1
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/METADATA +1 -5
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/RECORD +125 -102
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/WHEEL +1 -1
- lightning_sdk/lightning_cloud/cli/__main__.py +0 -29
- lightning_sdk/lightning_cloud/openapi/models/kubernetes_virtual_machine_service_create_kubernetes_virtual_machine_body.py +0 -513
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
- lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_vm_resources.py +0 -201
- lightning_sdk/lightning_cloud/source_code/logs_socket_api.py +0 -103
- /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
447
|
-
|
|
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
|
-
|
|
483
|
-
|
|
576
|
+
if num_workers is None:
|
|
577
|
+
num_workers = os.cpu_count() * 4
|
|
484
578
|
|
|
485
|
-
|
|
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
|
-
|
|
488
|
-
if prefix.endswith("/") is False:
|
|
489
|
-
prefix = prefix + "/"
|
|
584
|
+
files = self.list_uploads_files(teamspace_id, path)
|
|
490
585
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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."""
|
lightning_sdk/api/utils.py
CHANGED
|
@@ -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}")
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -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 ####
|
lightning_sdk/cli/groups.py
CHANGED
|
@@ -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.
|
|
36
|
-
cloud_account_id=selected_cluster_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
|
-
|
|
77
|
+
auth_user = _get_authed_user()
|
|
78
|
+
possible_teamspaces = menu._get_possible_teamspaces(auth_user)
|
|
78
79
|
if len(possible_teamspaces) == 1:
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|