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
lightning_sdk/__version__.py
CHANGED
lightning_sdk/api/k8s_api.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
import
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List, TypedDict
|
|
6
6
|
|
|
7
7
|
from lightning_sdk.api.utils import ApiException
|
|
8
8
|
from lightning_sdk.lightning_cloud.rest_client import LightningClient
|
|
@@ -63,42 +63,88 @@ class K8sClusterApi:
|
|
|
63
63
|
except Exception:
|
|
64
64
|
return str(e.reason)
|
|
65
65
|
|
|
66
|
-
def get_billing_usage(self, print_data: bool = False, **kwargs: Dict[str, Any]) ->
|
|
66
|
+
def get_billing_usage(self, print_data: bool = False, **kwargs: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
67
67
|
"""Gets the k8s usage metrics.
|
|
68
68
|
|
|
69
69
|
Returns:
|
|
70
|
-
The k8s usage metrics as a
|
|
70
|
+
The k8s usage metrics as a list of dictionaries.
|
|
71
71
|
"""
|
|
72
72
|
try:
|
|
73
73
|
response = self._client.k8_s_cluster_service_list_cluster_metrics(self.cloud_account, **kwargs)
|
|
74
74
|
cluster_metrics = [entry.to_dict() for entry in response.cluster_metrics]
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
76
|
+
if not cluster_metrics:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
# Parse timestamps and floor to hour, then group by hour
|
|
80
|
+
hourly_data = defaultdict(lambda: {"allocated_gpus": [], "first_entry": None})
|
|
81
|
+
|
|
82
|
+
for entry in cluster_metrics:
|
|
83
|
+
# Parse timestamp and floor to hour
|
|
84
|
+
timestamp = entry["timestamp"]
|
|
85
|
+
if isinstance(timestamp, str):
|
|
86
|
+
timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
87
|
+
hour = timestamp.replace(minute=0, second=0, microsecond=0)
|
|
88
|
+
|
|
89
|
+
# Store allocated GPUs for averaging
|
|
90
|
+
hourly_data[hour]["allocated_gpus"].append(entry["num_allocated_gpus"])
|
|
91
|
+
|
|
92
|
+
# Keep first entry for each hour (for other fields)
|
|
93
|
+
if hourly_data[hour]["first_entry"] is None:
|
|
94
|
+
hourly_data[hour]["first_entry"] = entry
|
|
95
|
+
|
|
96
|
+
# Build result list with aggregated data
|
|
97
|
+
result = []
|
|
98
|
+
for hour, data in sorted(hourly_data.items()):
|
|
99
|
+
entry = data["first_entry"]
|
|
100
|
+
|
|
101
|
+
# Calculate mean of allocated GPUs for this hour
|
|
102
|
+
mean_allocated_gpus = sum(data["allocated_gpus"]) / len(data["allocated_gpus"])
|
|
103
|
+
|
|
104
|
+
# Create row with mean allocated GPUs
|
|
105
|
+
row = {
|
|
106
|
+
"hour": hour,
|
|
107
|
+
"num_gpus": entry["num_gpus"],
|
|
108
|
+
"num_requested_gpus": entry["num_requested_gpus"],
|
|
109
|
+
"num_allocated_gpus": mean_allocated_gpus,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Calculate billed GPUs
|
|
113
|
+
row["billed_gpus"] = _calculate_billed_k8s_gpus(
|
|
114
|
+
{
|
|
115
|
+
"num_allocated_gpus": mean_allocated_gpus,
|
|
116
|
+
"num_requested_gpus": row["num_requested_gpus"],
|
|
117
|
+
"num_gpus": row["num_gpus"],
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
result.append(row)
|
|
95
122
|
|
|
96
|
-
# Keep only the required columns
|
|
97
|
-
df = df[["hour", "num_gpus", "num_requested_gpus", "num_allocated_gpus", "billed_gpus"]]
|
|
98
123
|
if print_data:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
# Print using rich table (local import)
|
|
125
|
+
from rich.console import Console
|
|
126
|
+
from rich.table import Table
|
|
127
|
+
|
|
128
|
+
table = Table(title="K8s Billing Usage")
|
|
129
|
+
table.add_column("Hour", style="cyan")
|
|
130
|
+
table.add_column("Available GPUs", justify="right", style="green")
|
|
131
|
+
table.add_column("Requested GPUs", justify="right", style="yellow")
|
|
132
|
+
table.add_column("Allocated GPUs (Avg)", justify="right", style="magenta")
|
|
133
|
+
table.add_column("Billed GPUs", justify="right", style="red")
|
|
134
|
+
|
|
135
|
+
for row in result:
|
|
136
|
+
table.add_row(
|
|
137
|
+
str(row["hour"]),
|
|
138
|
+
str(row["num_gpus"]),
|
|
139
|
+
str(row["num_requested_gpus"]),
|
|
140
|
+
f"{row['num_allocated_gpus']:.2f}",
|
|
141
|
+
str(row["billed_gpus"]),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
console = Console()
|
|
145
|
+
console.print(table)
|
|
146
|
+
|
|
147
|
+
return result
|
|
102
148
|
except ApiException as e:
|
|
103
149
|
msg = self._parse_request_failure_body(e)
|
|
104
150
|
logger.error(f"Failed to retrieve Kubernetes usage data: {msg}")
|
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import concurrent
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import time
|
|
4
5
|
import warnings
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from threading import Event, Thread
|
|
7
9
|
from typing import Any, Dict, Generator, List, Mapping, Optional, Tuple, Union
|
|
@@ -11,11 +13,10 @@ import requests
|
|
|
11
13
|
from tqdm import tqdm
|
|
12
14
|
|
|
13
15
|
from lightning_sdk.api.utils import (
|
|
16
|
+
_authenticate_and_get_token,
|
|
14
17
|
_create_app,
|
|
15
|
-
_download_teamspace_files,
|
|
16
18
|
_DummyBody,
|
|
17
19
|
_DummyResponse,
|
|
18
|
-
_FileUploader,
|
|
19
20
|
_machine_to_compute_name,
|
|
20
21
|
_sanitize_studio_remote_path,
|
|
21
22
|
)
|
|
@@ -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,
|
|
@@ -410,6 +409,21 @@ class StudioApi:
|
|
|
410
409
|
|
|
411
410
|
progress.complete("Machine switch completed successfully")
|
|
412
411
|
|
|
412
|
+
def machine_is_supported(self, machine: Machine, teamspace_id: str, cloud_account_id: str, org_id: str) -> bool:
|
|
413
|
+
"""Check if the machine is available in provided cloud_account."""
|
|
414
|
+
accelerators = self._get_machines_for_cloud_account(
|
|
415
|
+
teamspace_id=teamspace_id, cloud_account_id=cloud_account_id, org_id=org_id
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
for accelerator in accelerators:
|
|
419
|
+
if accelerator.accelerator_type == "GPU":
|
|
420
|
+
accelerator_resources_count = accelerator.resources.gpu
|
|
421
|
+
else:
|
|
422
|
+
accelerator_resources_count = accelerator.resources.cpu
|
|
423
|
+
if machine.accelerator_count == accelerator_resources_count and machine.family == accelerator.family:
|
|
424
|
+
return True
|
|
425
|
+
return False
|
|
426
|
+
|
|
413
427
|
def machine_has_capacity(self, machine: Machine, teamspace_id: str, cloud_account_id: str, org_id: str) -> bool:
|
|
414
428
|
"""Check capacity of the requested machine."""
|
|
415
429
|
accelerators = self._get_machines_for_cloud_account(
|
|
@@ -646,14 +660,15 @@ class StudioApi:
|
|
|
646
660
|
self.stop_keeping_alive(teamspace_id=teamspace_id, studio_id=studio_id)
|
|
647
661
|
self._client.cloud_space_service_delete_cloud_space(project_id=teamspace_id, id=studio_id)
|
|
648
662
|
|
|
649
|
-
def get_tree(self, studio_id: str, teamspace_id: str, path: str) -> None:
|
|
650
|
-
|
|
651
|
-
auth.authenticate()
|
|
652
|
-
token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
|
|
663
|
+
def get_tree(self, studio_id: str, teamspace_id: str, path: str, query_params: Optional[dict] = None) -> None:
|
|
664
|
+
token = _authenticate_and_get_token(self._client)
|
|
653
665
|
|
|
654
|
-
query_params
|
|
655
|
-
|
|
656
|
-
|
|
666
|
+
if query_params is None:
|
|
667
|
+
query_params = {
|
|
668
|
+
"token": token,
|
|
669
|
+
}
|
|
670
|
+
else:
|
|
671
|
+
query_params["token"] = token
|
|
657
672
|
r = requests.get(
|
|
658
673
|
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/trees/{path}",
|
|
659
674
|
params=query_params,
|
|
@@ -688,6 +703,16 @@ class StudioApi:
|
|
|
688
703
|
warnings.warn(f"If '{path}' is a directory, it may be empty and thus not detected.")
|
|
689
704
|
return {"exists": False, "type": None, "size": None}
|
|
690
705
|
|
|
706
|
+
def list_files(
|
|
707
|
+
self,
|
|
708
|
+
studio_id: str,
|
|
709
|
+
teamspace_id: str,
|
|
710
|
+
path: str = "",
|
|
711
|
+
) -> List[Dict]:
|
|
712
|
+
"""Recursively list all files in a directory tree."""
|
|
713
|
+
path = path.strip("/")
|
|
714
|
+
return self.get_tree(studio_id, teamspace_id, path, query_params={"recursive": "true"}).get("tree", [])
|
|
715
|
+
|
|
691
716
|
def upload_file(
|
|
692
717
|
self,
|
|
693
718
|
studio_id: str,
|
|
@@ -698,14 +723,32 @@ class StudioApi:
|
|
|
698
723
|
progress_bar: bool,
|
|
699
724
|
) -> None:
|
|
700
725
|
"""Uploads file to given remote path on the studio."""
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
726
|
+
token = _authenticate_and_get_token(self._client)
|
|
727
|
+
|
|
728
|
+
query_params = {"token": token}
|
|
729
|
+
client_host = self._client.api_client.configuration.host
|
|
730
|
+
url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/blobs/{remote_path}"
|
|
731
|
+
|
|
732
|
+
filesize = os.path.getsize(file_path)
|
|
733
|
+
with open(file_path, "rb") as f:
|
|
734
|
+
if progress_bar:
|
|
735
|
+
filesize = os.path.getsize(file_path)
|
|
736
|
+
with tqdm.wrapattr(
|
|
737
|
+
f,
|
|
738
|
+
"read",
|
|
739
|
+
desc=f"Uploading {os.path.split(file_path)[1]}",
|
|
740
|
+
total=filesize,
|
|
741
|
+
unit="B",
|
|
742
|
+
unit_scale=True,
|
|
743
|
+
unit_divisor=1000,
|
|
744
|
+
) as wrapped_file:
|
|
745
|
+
r = requests.put(url, data=wrapped_file, params=query_params, timeout=30)
|
|
746
|
+
else:
|
|
747
|
+
r = requests.put(url, data=f, params=query_params, timeout=30)
|
|
748
|
+
|
|
749
|
+
if r.status_code == 200:
|
|
750
|
+
return
|
|
751
|
+
raise RuntimeError(f"Failed to upload file '{file_path}' to the Studio. Status code: {r.status_code}")
|
|
709
752
|
|
|
710
753
|
def download_file(
|
|
711
754
|
self,
|
|
@@ -718,9 +761,7 @@ class StudioApi:
|
|
|
718
761
|
) -> None:
|
|
719
762
|
"""Downloads a given file from a Studio to a target location."""
|
|
720
763
|
# TODO: Update this endpoint to permit basic auth
|
|
721
|
-
|
|
722
|
-
auth.authenticate()
|
|
723
|
-
token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
|
|
764
|
+
token = _authenticate_and_get_token(self._client)
|
|
724
765
|
|
|
725
766
|
query_params = {
|
|
726
767
|
"clusterId": cloud_account,
|
|
@@ -729,7 +770,7 @@ class StudioApi:
|
|
|
729
770
|
}
|
|
730
771
|
|
|
731
772
|
r = requests.get(
|
|
732
|
-
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/
|
|
773
|
+
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/blobs/{path}",
|
|
733
774
|
params=query_params,
|
|
734
775
|
stream=True,
|
|
735
776
|
)
|
|
@@ -756,6 +797,39 @@ class StudioApi:
|
|
|
756
797
|
f.write(chunk)
|
|
757
798
|
pbar_update(len(chunk))
|
|
758
799
|
|
|
800
|
+
def _download_single_studio_file(
|
|
801
|
+
self,
|
|
802
|
+
file_info: Dict,
|
|
803
|
+
base_path: str,
|
|
804
|
+
download_dir: Path,
|
|
805
|
+
studio_id: str,
|
|
806
|
+
teamspace_id: str,
|
|
807
|
+
token: str,
|
|
808
|
+
pbar: Optional[tqdm],
|
|
809
|
+
) -> None:
|
|
810
|
+
"""Download a single file from Studio with progress tracking."""
|
|
811
|
+
relative_path = file_info["path"].lstrip("/")
|
|
812
|
+
local_file = download_dir / relative_path
|
|
813
|
+
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
814
|
+
|
|
815
|
+
file_path = os.path.join(base_path, relative_path) if base_path else relative_path
|
|
816
|
+
|
|
817
|
+
query_params = {
|
|
818
|
+
"token": token,
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
r = requests.get(
|
|
822
|
+
f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/blobs/{file_path}",
|
|
823
|
+
params=query_params,
|
|
824
|
+
stream=True,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
with open(str(local_file), "wb") as f:
|
|
828
|
+
for chunk in r.iter_content(chunk_size=4096 * 8):
|
|
829
|
+
f.write(chunk)
|
|
830
|
+
if pbar:
|
|
831
|
+
pbar.update(len(chunk))
|
|
832
|
+
|
|
759
833
|
def download_folder(
|
|
760
834
|
self,
|
|
761
835
|
path: str,
|
|
@@ -764,25 +838,106 @@ class StudioApi:
|
|
|
764
838
|
teamspace_id: str,
|
|
765
839
|
cloud_account: str,
|
|
766
840
|
progress_bar: bool = True,
|
|
841
|
+
num_workers: Optional[int] = None,
|
|
767
842
|
) -> None:
|
|
768
843
|
"""Downloads a given folder from a Studio to a target location."""
|
|
769
844
|
# TODO: implement resumable downloads
|
|
770
|
-
auth = Auth()
|
|
771
|
-
auth.authenticate()
|
|
772
845
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if prefix.endswith("/") is False:
|
|
776
|
-
prefix = prefix + "/"
|
|
846
|
+
if num_workers is None:
|
|
847
|
+
num_workers = os.cpu_count() * 4
|
|
777
848
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
849
|
+
# Normalize the path
|
|
850
|
+
path = path.strip("/")
|
|
851
|
+
download_dir = Path(target_path)
|
|
852
|
+
download_dir.mkdir(parents=True, exist_ok=True)
|
|
853
|
+
|
|
854
|
+
files = self.list_files(studio_id, teamspace_id, path)
|
|
855
|
+
|
|
856
|
+
if not files:
|
|
857
|
+
print(f"No files found in {path}")
|
|
858
|
+
return
|
|
859
|
+
|
|
860
|
+
token = _authenticate_and_get_token(self._client)
|
|
861
|
+
|
|
862
|
+
total_size = sum(f.get("size", 0) for f in files)
|
|
863
|
+
|
|
864
|
+
pbar = None
|
|
865
|
+
if progress_bar:
|
|
866
|
+
pbar = tqdm(
|
|
867
|
+
desc="Downloading files",
|
|
868
|
+
total=total_size,
|
|
869
|
+
unit="B",
|
|
870
|
+
unit_scale=True,
|
|
871
|
+
unit_divisor=1000,
|
|
872
|
+
mininterval=1,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
876
|
+
futures = [
|
|
877
|
+
executor.submit(
|
|
878
|
+
self._download_single_studio_file,
|
|
879
|
+
file_info,
|
|
880
|
+
path,
|
|
881
|
+
download_dir,
|
|
882
|
+
studio_id,
|
|
883
|
+
teamspace_id,
|
|
884
|
+
token,
|
|
885
|
+
pbar,
|
|
886
|
+
)
|
|
887
|
+
for file_info in files
|
|
888
|
+
]
|
|
889
|
+
concurrent.futures.wait(futures)
|
|
890
|
+
|
|
891
|
+
if pbar:
|
|
892
|
+
pbar.set_description("Download complete")
|
|
893
|
+
pbar.refresh()
|
|
894
|
+
pbar.close()
|
|
895
|
+
|
|
896
|
+
def remove_file(self, studio_id: str, teamspace_id: str, path: str) -> None:
|
|
897
|
+
"""Removes a file from a Studio."""
|
|
898
|
+
info = self.get_path_info(studio_id, teamspace_id, path=path)
|
|
899
|
+
|
|
900
|
+
if not info["exists"]:
|
|
901
|
+
raise FileNotFoundError(f"The path '{path}' does not exist in the Studio.")
|
|
902
|
+
|
|
903
|
+
if info["type"] != "file":
|
|
904
|
+
raise IsADirectoryError(f"The path '{path}' is a directory. Use 'remove_folder()' to remove directories.")
|
|
905
|
+
|
|
906
|
+
token = _authenticate_and_get_token(self._client)
|
|
907
|
+
|
|
908
|
+
query_params = {"token": token}
|
|
909
|
+
client_host = self._client.api_client.configuration.host
|
|
910
|
+
url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/blobs/{path}"
|
|
911
|
+
|
|
912
|
+
r = requests.delete(url, params=query_params, timeout=30)
|
|
913
|
+
|
|
914
|
+
if r.status_code == 204:
|
|
915
|
+
return
|
|
916
|
+
|
|
917
|
+
raise RuntimeError(f"Failed to remove file '{path}' from the Studio. Status code: {r.status_code}")
|
|
918
|
+
|
|
919
|
+
def remove_folder(self, studio_id: str, teamspace_id: str, path: str) -> None:
|
|
920
|
+
"""Removes a folder (directory) from a Studio."""
|
|
921
|
+
info = self.get_path_info(studio_id, teamspace_id, path=path)
|
|
922
|
+
|
|
923
|
+
if not info["exists"]:
|
|
924
|
+
raise FileNotFoundError(f"The path '{path}' does not exist in the Studio.")
|
|
925
|
+
|
|
926
|
+
if info["type"] == "file":
|
|
927
|
+
raise ValueError(f"The path '{path}' is a file. Use 'remove_file()' to remove files.")
|
|
928
|
+
|
|
929
|
+
token = _authenticate_and_get_token(self._client)
|
|
930
|
+
|
|
931
|
+
query_params = {"token": token}
|
|
932
|
+
client_host = self._client.api_client.configuration.host
|
|
933
|
+
url = f"{client_host}/v1/projects/{teamspace_id}/artifacts/cloudspaces/{studio_id}/trees/{path}"
|
|
934
|
+
|
|
935
|
+
r = requests.delete(url, params=query_params, timeout=30)
|
|
936
|
+
|
|
937
|
+
if r.status_code == 204:
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
raise RuntimeError(f"Failed to remove folder '{path}' from the Studio. Status code: {r.status_code}")
|
|
786
941
|
|
|
787
942
|
def install_plugin(self, studio_id: str, teamspace_id: str, plugin_name: str) -> str:
|
|
788
943
|
"""Installs the given plugin."""
|