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.
- lightning_sdk/__version__.py +1 -1
- lightning_sdk/api/studio_api.py +38 -39
- lightning_sdk/api/teamspace_api.py +189 -72
- lightning_sdk/api/utils.py +69 -1
- lightning_sdk/cli/cp/__init__.py +14 -11
- lightning_sdk/cli/cp/teamspace_uploads.py +95 -0
- lightning_sdk/cli/legacy/download.py +29 -98
- lightning_sdk/cli/legacy/upload.py +24 -31
- lightning_sdk/cli/studio/cp.py +8 -5
- lightning_sdk/cli/studio/ls.py +1 -1
- lightning_sdk/cli/studio/rm.py +1 -1
- lightning_sdk/cli/utils/{studio_filesystem.py → filesystem.py} +49 -6
- lightning_sdk/exceptions.py +27 -0
- lightning_sdk/lightning_cloud/openapi/__init__.py +17 -12
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +286 -468
- lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +579 -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/organizations_service_api.py +113 -0
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +16 -12
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → cluster_service_get_cluster_capacity_reservation_body.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → container_registry_config_ecr.py} +49 -23
- 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/jobs_service_duplicate_deployment_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -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_validate_container_registry_response.py → v1_container_registry_scopes.py} +39 -39
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_create_container_registry_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_org_cluster_capacity_reservation_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_generic_job_spec.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → v1_get_cluster_capacity_reservation_response.py} +23 -23
- lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → v1_mithril_direct_v1.py} +51 -51
- lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_update_container_registry_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -105
- lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
- lightning_sdk/teamspace.py +28 -7
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/METADATA +1 -1
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/RECORD +59 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
- lightning_sdk/lightning_cloud/openapi/models/v1_ecr_registry_details.py +0 -201
- /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/LICENSE +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/WHEEL +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/top_level.txt +0 -0
lightning_sdk/__version__.py
CHANGED
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
505
|
+
query_params = {
|
|
506
|
+
"token": token,
|
|
507
|
+
}
|
|
446
508
|
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
502
|
-
|
|
586
|
+
if num_workers is None:
|
|
587
|
+
num_workers = os.cpu_count() * 4
|
|
503
588
|
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
if prefix.endswith("/") is False:
|
|
508
|
-
prefix = prefix + "/"
|
|
594
|
+
files = self.list_uploads_files(teamspace_id, path)
|
|
509
595
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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."""
|
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,
|
|
@@ -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"
|
|
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
|
lightning_sdk/cli/cp/__init__.py
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|