lightning-sdk 2026.1.22__py3-none-any.whl → 2026.1.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightning_sdk/__version__.py +1 -1
- lightning_sdk/api/studio_api.py +7 -14
- lightning_sdk/api/teamspace_api.py +155 -48
- lightning_sdk/api/utils.py +8 -0
- lightning_sdk/cli/cp/__init__.py +14 -11
- lightning_sdk/cli/cp/teamspace_uploads.py +93 -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} +43 -5
- lightning_sdk/exceptions.py +27 -0
- lightning_sdk/lightning_cloud/openapi/__init__.py +14 -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 +163 -466
- 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/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 +13 -12
- 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/{cluster_service_refresh_container_registry_credentials_body.py → v1_delete_org_cluster_capacity_reservation_response.py} +6 -6
- 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_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_user_features.py +53 -53
- 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.27.dist-info}/METADATA +1 -1
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/RECORD +55 -52
- lightning_sdk/lightning_cloud/openapi/models/v1_add_container_registry_response.py +0 -123
- 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.27.dist-info}/LICENSE +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/WHEEL +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.27.dist-info}/top_level.txt +0 -0
lightning_sdk/__version__.py
CHANGED
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -13,6 +13,7 @@ import requests
|
|
|
13
13
|
from tqdm import tqdm
|
|
14
14
|
|
|
15
15
|
from lightning_sdk.api.utils import (
|
|
16
|
+
_authenticate_and_get_token,
|
|
16
17
|
_create_app,
|
|
17
18
|
_DummyBody,
|
|
18
19
|
_DummyResponse,
|
|
@@ -23,7 +24,6 @@ from lightning_sdk.api.utils import (
|
|
|
23
24
|
_get_cloud_url as _cloud_url,
|
|
24
25
|
)
|
|
25
26
|
from lightning_sdk.constants import _LIGHTNING_DEBUG
|
|
26
|
-
from lightning_sdk.lightning_cloud.login import Auth
|
|
27
27
|
from lightning_sdk.lightning_cloud.openapi import (
|
|
28
28
|
AssistantsServiceCreateAssistantBody,
|
|
29
29
|
AssistantsServiceCreateAssistantManagedEndpointBody,
|
|
@@ -49,7 +49,6 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
49
49
|
V1EnvVar,
|
|
50
50
|
V1GetCloudSpaceInstanceStatusResponse,
|
|
51
51
|
V1GetLongRunningCommandInCloudSpaceResponse,
|
|
52
|
-
V1LoginRequest,
|
|
53
52
|
V1ManagedEndpoint,
|
|
54
53
|
V1ManagedModel,
|
|
55
54
|
V1Plugin,
|
|
@@ -661,14 +660,8 @@ class StudioApi:
|
|
|
661
660
|
self.stop_keeping_alive(teamspace_id=teamspace_id, studio_id=studio_id)
|
|
662
661
|
self._client.cloud_space_service_delete_cloud_space(project_id=teamspace_id, id=studio_id)
|
|
663
662
|
|
|
664
|
-
def _authenticate_and_get_token(self) -> str:
|
|
665
|
-
"""Authenticate and return a token for API requests."""
|
|
666
|
-
auth = Auth()
|
|
667
|
-
auth.authenticate()
|
|
668
|
-
return self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
|
|
669
|
-
|
|
670
663
|
def get_tree(self, studio_id: str, teamspace_id: str, path: str, query_params: Optional[dict] = None) -> None:
|
|
671
|
-
token = self.
|
|
664
|
+
token = _authenticate_and_get_token(self._client)
|
|
672
665
|
|
|
673
666
|
if query_params is None:
|
|
674
667
|
query_params = {
|
|
@@ -730,7 +723,7 @@ class StudioApi:
|
|
|
730
723
|
progress_bar: bool,
|
|
731
724
|
) -> None:
|
|
732
725
|
"""Uploads file to given remote path on the studio."""
|
|
733
|
-
token = self.
|
|
726
|
+
token = _authenticate_and_get_token(self._client)
|
|
734
727
|
|
|
735
728
|
query_params = {"token": token}
|
|
736
729
|
client_host = self._client.api_client.configuration.host
|
|
@@ -768,7 +761,7 @@ class StudioApi:
|
|
|
768
761
|
) -> None:
|
|
769
762
|
"""Downloads a given file from a Studio to a target location."""
|
|
770
763
|
# TODO: Update this endpoint to permit basic auth
|
|
771
|
-
token = self.
|
|
764
|
+
token = _authenticate_and_get_token(self._client)
|
|
772
765
|
|
|
773
766
|
query_params = {
|
|
774
767
|
"clusterId": cloud_account,
|
|
@@ -864,7 +857,7 @@ class StudioApi:
|
|
|
864
857
|
print(f"No files found in {path}")
|
|
865
858
|
return
|
|
866
859
|
|
|
867
|
-
token = self.
|
|
860
|
+
token = _authenticate_and_get_token(self._client)
|
|
868
861
|
|
|
869
862
|
total_size = sum(f.get("size", 0) for f in files)
|
|
870
863
|
|
|
@@ -910,7 +903,7 @@ class StudioApi:
|
|
|
910
903
|
if info["type"] != "file":
|
|
911
904
|
raise IsADirectoryError(f"The path '{path}' is a directory. Use 'remove_folder()' to remove directories.")
|
|
912
905
|
|
|
913
|
-
token = self.
|
|
906
|
+
token = _authenticate_and_get_token(self._client)
|
|
914
907
|
|
|
915
908
|
query_params = {"token": token}
|
|
916
909
|
client_host = self._client.api_client.configuration.host
|
|
@@ -933,7 +926,7 @@ class StudioApi:
|
|
|
933
926
|
if info["type"] == "file":
|
|
934
927
|
raise ValueError(f"The path '{path}' is a file. Use 'remove_file()' to remove files.")
|
|
935
928
|
|
|
936
|
-
token = self.
|
|
929
|
+
token = _authenticate_and_get_token(self._client)
|
|
937
930
|
|
|
938
931
|
query_params = {"token": token}
|
|
939
932
|
client_host = self._client.api_client.configuration.host
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import concurrent
|
|
1
2
|
import os
|
|
2
3
|
import re
|
|
4
|
+
import warnings
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
6
|
from pathlib import Path
|
|
4
7
|
from typing import Dict, List, Optional, Tuple
|
|
5
8
|
|
|
@@ -7,12 +10,11 @@ import requests
|
|
|
7
10
|
from tqdm.auto import tqdm
|
|
8
11
|
|
|
9
12
|
from lightning_sdk.api.utils import (
|
|
13
|
+
_authenticate_and_get_token,
|
|
10
14
|
_download_model_files,
|
|
11
|
-
_download_teamspace_files,
|
|
12
15
|
_DummyBody,
|
|
13
16
|
_get_model_version,
|
|
14
17
|
_ModelFileUploader,
|
|
15
|
-
_resolve_teamspace_remote_path,
|
|
16
18
|
)
|
|
17
19
|
from lightning_sdk.lightning_cloud.login import Auth
|
|
18
20
|
from lightning_sdk.lightning_cloud.openapi import (
|
|
@@ -32,7 +34,6 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
32
34
|
V1ExternalCluster,
|
|
33
35
|
V1GCSFolderDataConnection,
|
|
34
36
|
V1Job,
|
|
35
|
-
V1LoginRequest,
|
|
36
37
|
V1Model,
|
|
37
38
|
V1ModelVersionArchive,
|
|
38
39
|
V1MultiMachineJob,
|
|
@@ -391,6 +392,58 @@ class TeamspaceApi:
|
|
|
391
392
|
response = self.models_api.models_store_list_model_versions(project_id=teamspace_id, model_id=model_id)
|
|
392
393
|
return response.versions
|
|
393
394
|
|
|
395
|
+
def get_uploads_tree(self, teamspace_id: str, path: str, query_params: Optional[dict] = None) -> None:
|
|
396
|
+
token = _authenticate_and_get_token(self._client)
|
|
397
|
+
|
|
398
|
+
if query_params is None:
|
|
399
|
+
query_params = {
|
|
400
|
+
"token": token,
|
|
401
|
+
}
|
|
402
|
+
else:
|
|
403
|
+
query_params["token"] = token
|
|
404
|
+
r = requests.get(
|
|
405
|
+
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/trees/{path}",
|
|
406
|
+
params=query_params,
|
|
407
|
+
)
|
|
408
|
+
return r.json()
|
|
409
|
+
|
|
410
|
+
def get_path_info(self, teamspace_id: str, path: str = "") -> dict:
|
|
411
|
+
path = path.strip("/")
|
|
412
|
+
|
|
413
|
+
if "/" in path:
|
|
414
|
+
parent_path = path.rsplit("/", 1)[0]
|
|
415
|
+
target_name = path.rsplit("/", 1)[1]
|
|
416
|
+
else:
|
|
417
|
+
if path == "":
|
|
418
|
+
# root directory
|
|
419
|
+
return {"exists": True, "type": "directory", "size": None}
|
|
420
|
+
parent_path = ""
|
|
421
|
+
target_name = path
|
|
422
|
+
|
|
423
|
+
tree = self.get_uploads_tree(teamspace_id, path=parent_path)
|
|
424
|
+
tree_items = tree.get("tree", [])
|
|
425
|
+
for item in tree_items:
|
|
426
|
+
item_name = item.get("path", "")
|
|
427
|
+
if item_name == target_name:
|
|
428
|
+
item_type = item.get("type")
|
|
429
|
+
# if type == "blob" it's a file, if "tree" it's a directory
|
|
430
|
+
return {
|
|
431
|
+
"exists": True,
|
|
432
|
+
"type": "file" if item_type == "blob" else "directory",
|
|
433
|
+
"size": item.get("size", 0) if item_type == "blob" else None,
|
|
434
|
+
}
|
|
435
|
+
warnings.warn(f"If '{path}' is a directory, it may be empty and thus not detected.")
|
|
436
|
+
return {"exists": False, "type": None, "size": None}
|
|
437
|
+
|
|
438
|
+
def list_uploads_files(
|
|
439
|
+
self,
|
|
440
|
+
teamspace_id: str,
|
|
441
|
+
path: str = "",
|
|
442
|
+
) -> List[Dict]:
|
|
443
|
+
"""Recursively list all files in a /Uploads/ directory tree."""
|
|
444
|
+
path = path.strip("/")
|
|
445
|
+
return self.get_uploads_tree(teamspace_id, path, query_params={"recursive": "true"}).get("tree", [])
|
|
446
|
+
|
|
394
447
|
def upload_file(
|
|
395
448
|
self,
|
|
396
449
|
teamspace_id: str,
|
|
@@ -399,10 +452,8 @@ class TeamspaceApi:
|
|
|
399
452
|
remote_path: str,
|
|
400
453
|
progress_bar: bool,
|
|
401
454
|
) -> None:
|
|
402
|
-
"""Uploads file to given remote path in the Teamspace drive
|
|
403
|
-
|
|
404
|
-
auth.authenticate()
|
|
405
|
-
token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
|
|
455
|
+
"""Uploads file to given remote path in the Teamspace drive /Uploads/."""
|
|
456
|
+
token = _authenticate_and_get_token(self._client)
|
|
406
457
|
|
|
407
458
|
query_params = {"token": token, "clusterId": cloud_account}
|
|
408
459
|
client_host = self._client.api_client.configuration.host
|
|
@@ -434,37 +485,25 @@ class TeamspaceApi:
|
|
|
434
485
|
path: str,
|
|
435
486
|
target_path: str,
|
|
436
487
|
teamspace_id: str,
|
|
488
|
+
cloud_account: Optional[str] = None,
|
|
437
489
|
progress_bar: bool = True,
|
|
438
490
|
) -> None:
|
|
439
|
-
"""Downloads a given file in Teamspace drive to a target location."""
|
|
491
|
+
"""Downloads a given file in Teamspace drive /Uploads/ to a target location."""
|
|
440
492
|
# TODO: Update this endpoint to permit basic auth
|
|
441
|
-
|
|
442
|
-
auth.authenticate()
|
|
443
|
-
token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
|
|
444
|
-
|
|
445
|
-
cluster_ids = [ca.cluster_id for ca in self.list_cloud_accounts(teamspace_id)]
|
|
446
|
-
|
|
447
|
-
found = False
|
|
448
|
-
for cluster_id in cluster_ids:
|
|
449
|
-
query_params = {
|
|
450
|
-
"clusterId": cluster_id,
|
|
451
|
-
"key": _resolve_teamspace_remote_path(path),
|
|
452
|
-
"token": token,
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
r = requests.get(
|
|
456
|
-
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/download",
|
|
457
|
-
params=query_params,
|
|
458
|
-
stream=True,
|
|
459
|
-
)
|
|
493
|
+
token = _authenticate_and_get_token(self._client)
|
|
460
494
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
495
|
+
query_params = {
|
|
496
|
+
"token": token,
|
|
497
|
+
}
|
|
464
498
|
|
|
465
|
-
if
|
|
466
|
-
|
|
499
|
+
if cloud_account:
|
|
500
|
+
query_params["clusterId"] = cloud_account
|
|
467
501
|
|
|
502
|
+
r = requests.get(
|
|
503
|
+
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{path}",
|
|
504
|
+
params=query_params,
|
|
505
|
+
stream=True,
|
|
506
|
+
)
|
|
468
507
|
total_length = int(r.headers.get("content-length"))
|
|
469
508
|
|
|
470
509
|
if progress_bar:
|
|
@@ -488,33 +527,101 @@ class TeamspaceApi:
|
|
|
488
527
|
f.write(chunk)
|
|
489
528
|
pbar_update(len(chunk))
|
|
490
529
|
|
|
530
|
+
def _download_single_file(
|
|
531
|
+
self,
|
|
532
|
+
file_info: Dict,
|
|
533
|
+
base_path: str,
|
|
534
|
+
download_dir: Path,
|
|
535
|
+
teamspace_id: str,
|
|
536
|
+
token: str,
|
|
537
|
+
cloud_account: Optional[str] = None,
|
|
538
|
+
pbar: Optional[tqdm] = True,
|
|
539
|
+
) -> None:
|
|
540
|
+
"""Download a single file from Teamspace drive /Uploads/ with progress tracking."""
|
|
541
|
+
relative_path = file_info["path"].lstrip("/")
|
|
542
|
+
local_file = download_dir / relative_path
|
|
543
|
+
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
544
|
+
|
|
545
|
+
file_path = os.path.join(base_path, relative_path) if base_path else relative_path
|
|
546
|
+
|
|
547
|
+
query_params = {
|
|
548
|
+
"token": token,
|
|
549
|
+
}
|
|
550
|
+
if cloud_account:
|
|
551
|
+
query_params["clusterId"] = cloud_account
|
|
552
|
+
|
|
553
|
+
r = requests.get(
|
|
554
|
+
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/uploads/blobs/{file_path}",
|
|
555
|
+
params=query_params,
|
|
556
|
+
stream=True,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
with open(str(local_file), "wb") as f:
|
|
560
|
+
for chunk in r.iter_content(chunk_size=4096 * 8):
|
|
561
|
+
f.write(chunk)
|
|
562
|
+
if pbar:
|
|
563
|
+
pbar.update(len(chunk))
|
|
564
|
+
|
|
491
565
|
def download_folder(
|
|
492
566
|
self,
|
|
493
567
|
path: str,
|
|
494
568
|
target_path: str,
|
|
495
569
|
teamspace_id: str,
|
|
496
|
-
cloud_account: str,
|
|
570
|
+
cloud_account: Optional[str] = None,
|
|
497
571
|
progress_bar: bool = True,
|
|
572
|
+
num_workers: Optional[int] = None,
|
|
498
573
|
) -> None:
|
|
499
|
-
"""Downloads a given folder from Teamspace drive to a target location."""
|
|
574
|
+
"""Downloads a given folder from Teamspace drive /Uploads/ to a target location."""
|
|
500
575
|
# TODO: Update this endpoint to permit basic auth
|
|
501
|
-
|
|
502
|
-
|
|
576
|
+
if num_workers is None:
|
|
577
|
+
num_workers = os.cpu_count() * 4
|
|
503
578
|
|
|
504
|
-
|
|
579
|
+
# Normalize the path
|
|
580
|
+
path = path.strip("/")
|
|
581
|
+
download_dir = Path(target_path)
|
|
582
|
+
download_dir.mkdir(parents=True, exist_ok=True)
|
|
505
583
|
|
|
506
|
-
|
|
507
|
-
if prefix.endswith("/") is False:
|
|
508
|
-
prefix = prefix + "/"
|
|
584
|
+
files = self.list_uploads_files(teamspace_id, path)
|
|
509
585
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
586
|
+
if not files:
|
|
587
|
+
print(f"No files found in {path}")
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
token = _authenticate_and_get_token(self._client)
|
|
591
|
+
|
|
592
|
+
total_size = sum(f.get("size", 0) for f in files)
|
|
593
|
+
|
|
594
|
+
pbar = None
|
|
595
|
+
if progress_bar:
|
|
596
|
+
pbar = tqdm(
|
|
597
|
+
desc="Downloading files",
|
|
598
|
+
total=total_size,
|
|
599
|
+
unit="B",
|
|
600
|
+
unit_scale=True,
|
|
601
|
+
unit_divisor=1000,
|
|
602
|
+
mininterval=1,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
606
|
+
futures = [
|
|
607
|
+
executor.submit(
|
|
608
|
+
self._download_single_file,
|
|
609
|
+
file_info,
|
|
610
|
+
path,
|
|
611
|
+
download_dir,
|
|
612
|
+
teamspace_id,
|
|
613
|
+
token,
|
|
614
|
+
cloud_account,
|
|
615
|
+
pbar,
|
|
616
|
+
)
|
|
617
|
+
for file_info in files
|
|
618
|
+
]
|
|
619
|
+
concurrent.futures.wait(futures)
|
|
620
|
+
|
|
621
|
+
if pbar:
|
|
622
|
+
pbar.set_description("Download complete")
|
|
623
|
+
pbar.refresh()
|
|
624
|
+
pbar.close()
|
|
518
625
|
|
|
519
626
|
def get_secrets(self, teamspace_id: str) -> Dict[str, str]:
|
|
520
627
|
"""Get all secrets for a teamspace."""
|
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
|
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
|
|
@@ -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}")
|