lightning-sdk 2025.12.17__py3-none-any.whl → 2026.1.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. lightning_sdk/__version__.py +1 -1
  2. lightning_sdk/api/k8s_api.py +75 -29
  3. lightning_sdk/api/studio_api.py +192 -37
  4. lightning_sdk/api/teamspace_api.py +180 -54
  5. lightning_sdk/api/utils.py +8 -0
  6. lightning_sdk/cli/cp/__init__.py +67 -0
  7. lightning_sdk/cli/cp/teamspace_uploads.py +93 -0
  8. lightning_sdk/cli/entrypoint.py +2 -0
  9. lightning_sdk/cli/groups.py +22 -0
  10. lightning_sdk/cli/legacy/clusters_menu.py +2 -2
  11. lightning_sdk/cli/legacy/deploy/_auth.py +7 -6
  12. lightning_sdk/cli/legacy/download.py +29 -98
  13. lightning_sdk/cli/legacy/run.py +13 -2
  14. lightning_sdk/cli/legacy/upload.py +24 -31
  15. lightning_sdk/cli/studio/__init__.py +4 -0
  16. lightning_sdk/cli/studio/cp.py +24 -65
  17. lightning_sdk/cli/studio/ls.py +57 -0
  18. lightning_sdk/cli/studio/rm.py +71 -0
  19. lightning_sdk/cli/utils/filesystem.py +103 -0
  20. lightning_sdk/cli/utils/logging.py +2 -1
  21. lightning_sdk/cli/utils/teamspace_selection.py +5 -0
  22. lightning_sdk/exceptions.py +31 -0
  23. lightning_sdk/job/base.py +1 -1
  24. lightning_sdk/k8s_cluster.py +9 -10
  25. lightning_sdk/lightning_cloud/__version__.py +1 -1
  26. lightning_sdk/lightning_cloud/openapi/__init__.py +43 -23
  27. lightning_sdk/lightning_cloud/openapi/api/__init__.py +2 -1
  28. lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +118 -1
  29. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +378 -536
  30. lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +456 -0
  31. lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +5 -1
  32. lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +11 -11
  33. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +113 -0
  34. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +246 -19
  35. lightning_sdk/lightning_cloud/openapi/api/lightningwork_service_api.py +116 -11
  36. lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +588 -2
  37. lightning_sdk/lightning_cloud/openapi/api/models_store_api.py +9 -1
  38. lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +113 -0
  39. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
  40. lightning_sdk/lightning_cloud/openapi/api/{kubernetes_virtual_machine_service_api.py → virtual_machine_service_api.py} +82 -82
  41. lightning_sdk/lightning_cloud/openapi/models/__init__.py +41 -22
  42. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_cluster_capacity_reservation_body.py +53 -1
  43. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_machine_body.py +53 -1
  44. lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_org_cluster_capacity_reservation_body.py +409 -0
  45. lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → cluster_service_report_machine_system_metrics_body.py} +23 -23
  46. lightning_sdk/lightning_cloud/openapi/models/container_registry_config_ecr.py +149 -0
  47. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_status.py → container_registry_provider.py} +14 -10
  48. lightning_sdk/lightning_cloud/openapi/models/container_registry_service_create_container_registry_body.py +201 -0
  49. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config_input.py → container_registry_service_refresh_container_registry_credentials_body.py} +21 -21
  50. lightning_sdk/lightning_cloud/openapi/models/externalv1_cloud_space_instance_status.py +27 -1
  51. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → jobs_service_duplicate_deployment_body.py} +51 -51
  52. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_create_lit_logger_media_body.py +305 -0
  53. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_lit_logger_media_body.py +149 -0
  54. lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_metrics_stream_body.py +53 -1
  55. lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
  56. lightning_sdk/lightning_cloud/openapi/models/v1_capacity_reservation_used_by.py +227 -0
  57. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +2 -1
  58. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +53 -1
  61. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +79 -27
  62. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +0 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +63 -89
  64. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_add_container_registry_body.py → v1_container_registry_config.py} +16 -16
  65. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_scopes.py +149 -0
  66. lightning_sdk/lightning_cloud/openapi/models/{v1_delete_kubernetes_virtual_machine_response.py → v1_create_container_registry_response.py} +6 -6
  67. lightning_sdk/lightning_cloud/openapi/models/v1_create_lit_logger_media_response.py +149 -0
  68. lightning_sdk/lightning_cloud/openapi/models/v1_create_org_cluster_capacity_reservation_response.py +201 -0
  69. lightning_sdk/lightning_cloud/openapi/models/v1_create_sdk_command_history_request.py +29 -3
  70. lightning_sdk/lightning_cloud/openapi/models/v1_cudo_direct_v1.py +175 -0
  71. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → v1_delete_lit_logger_media_response.py} +6 -6
  72. lightning_sdk/lightning_cloud/openapi/models/{kubernetes_virtual_machine_service_update_kubernetes_virtual_machine_body.py → v1_delete_org_cluster_capacity_reservation_response.py} +6 -6
  73. lightning_sdk/lightning_cloud/openapi/models/v1_delete_virtual_machine_response.py +97 -0
  74. lightning_sdk/lightning_cloud/openapi/models/v1_describe_org_cluster_capacity_reservation_response.py +201 -0
  75. lightning_sdk/lightning_cloud/openapi/models/v1_external_cluster_spec.py +27 -27
  76. lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
  77. lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
  78. lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_get_kubernetes_pod_logs_response.py} +37 -37
  79. lightning_sdk/lightning_cloud/openapi/models/{v1_get_machine_response.py → v1_get_kubernetes_pod_response.py} +23 -23
  80. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  81. lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
  82. lightning_sdk/lightning_cloud/openapi/models/v1_joinable_organization.py +27 -1
  83. lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → v1_k8s_incident_setting.py} +49 -23
  84. lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_type.py +108 -0
  85. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_settings_v1.py +53 -1
  86. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +53 -27
  87. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod.py +27 -1
  88. lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod_logs_page.py +227 -0
  89. lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_config.py +53 -1
  90. lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
  91. lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_response.py +43 -17
  92. lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_sort_order.py +104 -0
  93. lightning_sdk/lightning_cloud/openapi/models/v1_list_lit_logger_media_response.py +149 -0
  94. lightning_sdk/lightning_cloud/openapi/models/v1_list_models_response.py +55 -3
  95. lightning_sdk/lightning_cloud/openapi/models/{v1_list_kubernetes_virtual_machines_response.py → v1_list_virtual_machines_response.py} +16 -16
  96. lightning_sdk/lightning_cloud/openapi/models/v1_lit_logger_media.py +513 -0
  97. lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -53
  98. lightning_sdk/lightning_cloud/openapi/models/v1_machine_direct_v1.py +107 -3
  99. lightning_sdk/lightning_cloud/openapi/models/v1_media_type.py +104 -0
  100. lightning_sdk/lightning_cloud/openapi/models/{v1_ai_pod_v1.py → v1_mithril_direct_v1.py} +51 -51
  101. lightning_sdk/lightning_cloud/openapi/models/v1_nebius_direct_v1.py +29 -3
  102. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
  103. lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
  104. lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_report_cloud_space_instance_idle_state_response.py} +6 -6
  105. lightning_sdk/lightning_cloud/openapi/models/v1_report_machine_system_metrics_response.py +97 -0
  106. lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_details.py → v1_tenant_credentials.py} +53 -53
  107. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +183 -157
  108. lightning_sdk/lightning_cloud/openapi/models/{v1_kubernetes_virtual_machine.py → v1_virtual_machine.py} +94 -68
  109. lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_vm_configuration.py → v1_vm_configuration.py} +20 -20
  110. lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_provider_configuration.py → v1_vm_provider_configuration.py} +32 -32
  111. lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_create_virtual_machine_body.py +565 -0
  112. lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_update_virtual_machine_body.py +97 -0
  113. lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
  114. lightning_sdk/lightning_cloud/rest_client.py +0 -2
  115. lightning_sdk/machine.py +3 -3
  116. lightning_sdk/studio.py +14 -4
  117. lightning_sdk/teamspace.py +28 -7
  118. lightning_sdk/utils/logging.py +2 -1
  119. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/METADATA +1 -5
  120. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/RECORD +125 -102
  121. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/WHEEL +1 -1
  122. lightning_sdk/lightning_cloud/cli/__main__.py +0 -29
  123. lightning_sdk/lightning_cloud/openapi/models/kubernetes_virtual_machine_service_create_kubernetes_virtual_machine_body.py +0 -513
  124. lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
  125. lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_vm_resources.py +0 -201
  126. lightning_sdk/lightning_cloud/source_code/logs_socket_api.py +0 -103
  127. /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
  128. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/LICENSE +0 -0
  129. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/entry_points.txt +0 -0
  130. {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,3 @@
1
1
  """Version information for lightning_sdk."""
2
2
 
3
- __version__ = "2025.12.17"
3
+ __version__ = "2026.01.27"
@@ -1,8 +1,8 @@
1
1
  import json
2
2
  import logging
3
- from typing import Any, Dict, TypedDict, Union
4
-
5
- import pandas as pd
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]) -> Union[pd.DataFrame, pd.Series]:
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 DataFrame or Series.
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
- df = pd.DataFrame.from_records(cluster_metrics)
77
- if df.empty:
78
- return df
79
-
80
- df["hour"] = pd.to_datetime(df["timestamp"]).dt.floor("h")
81
-
82
- # Calculate the mean of num_allocated_gpus for each hour
83
- aggregated = df.groupby("hour", as_index=False)["num_allocated_gpus"].mean()
84
- # Merge the aggregated values back into the original DataFrame
85
- df = df.merge(aggregated, on="hour", suffixes=("", "_mean"))
86
-
87
- # Replace the original num_allocated_gpus with the mean values
88
- df["num_allocated_gpus"] = df["num_allocated_gpus_mean"]
89
-
90
- # We group the data by hour and take the first occurrence to avoid duplicates
91
- df = df.drop_duplicates(subset="hour", keep="first")
92
-
93
- # Convert timestamp to hourly floor and rename columns
94
- df["billed_gpus"] = df.apply(_calculate_billed_k8s_gpus, axis=1)
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
- with pd.option_context("display.max_rows", None, "display.max_columns", None):
100
- print(df)
101
- return df
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}")
@@ -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
- auth = Auth()
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
- "token": token,
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
- _FileUploader(
702
- client=self._client,
703
- teamspace_id=teamspace_id,
704
- cloud_account=cloud_account,
705
- file_path=file_path,
706
- remote_path=_sanitize_studio_remote_path(remote_path, studio_id),
707
- progress_bar=progress_bar,
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
- auth = Auth()
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/download",
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
- prefix = _sanitize_studio_remote_path(path, studio_id)
774
- # ensure we only download as a directory and not the entire prefix
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
- _download_teamspace_files(
779
- client=self._client,
780
- teamspace_id=teamspace_id,
781
- cluster_id=cloud_account,
782
- prefix=prefix,
783
- download_dir=Path(target_path),
784
- progress_bar=progress_bar,
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."""