lightning-sdk 2025.8.18.post0__py3-none-any.whl → 2025.8.21__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/__init__.py +1 -1
- lightning_sdk/api/llm_api.py +6 -2
- lightning_sdk/api/studio_api.py +113 -36
- lightning_sdk/api/utils.py +108 -18
- lightning_sdk/cli/legacy/create.py +9 -11
- lightning_sdk/cli/legacy/start.py +1 -0
- lightning_sdk/cli/legacy/switch.py +1 -0
- lightning_sdk/cli/studio/start.py +1 -0
- lightning_sdk/cli/studio/switch.py +1 -0
- lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +113 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
- lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +15 -15
- lightning_sdk/lightning_cloud/openapi/models/id_codeconfig_body.py +3 -81
- lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_list_aggregated_pod_metrics_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +157 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_project_cluster_binding.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_quote_annual_upsell_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_cloud_space_instance_config_request.py +3 -81
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -131
- lightning_sdk/llm/llm.py +2 -2
- lightning_sdk/studio.py +39 -6
- lightning_sdk/utils/progress.py +284 -0
- {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/RECORD +37 -34
- {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
lightning_sdk/api/llm_api.py
CHANGED
|
@@ -146,7 +146,6 @@ class LLMApi:
|
|
|
146
146
|
{"contentType": "text", "parts": [prompt]},
|
|
147
147
|
],
|
|
148
148
|
},
|
|
149
|
-
"max_tokens": max_completion_tokens,
|
|
150
149
|
"conversation_id": conversation_id,
|
|
151
150
|
"billing_project_id": billing_project_id,
|
|
152
151
|
"name": name,
|
|
@@ -159,6 +158,9 @@ class LLMApi:
|
|
|
159
158
|
"parent_message_id": kwargs.get("parent_message_id", ""),
|
|
160
159
|
"tools": tools,
|
|
161
160
|
}
|
|
161
|
+
if max_completion_tokens is not None:
|
|
162
|
+
body["max_completion_tokens"] = max_completion_tokens
|
|
163
|
+
|
|
162
164
|
if images:
|
|
163
165
|
for image in images:
|
|
164
166
|
url = image
|
|
@@ -203,7 +205,6 @@ class LLMApi:
|
|
|
203
205
|
{"contentType": "text", "parts": [prompt]},
|
|
204
206
|
],
|
|
205
207
|
},
|
|
206
|
-
"max_completion_tokens": max_completion_tokens,
|
|
207
208
|
"conversation_id": conversation_id,
|
|
208
209
|
"billing_project_id": billing_project_id,
|
|
209
210
|
"name": name,
|
|
@@ -216,6 +217,9 @@ class LLMApi:
|
|
|
216
217
|
"parent_message_id": kwargs.get("parent_message_id", ""),
|
|
217
218
|
"sent_at": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="microseconds"),
|
|
218
219
|
}
|
|
220
|
+
if max_completion_tokens is not None:
|
|
221
|
+
body["max_completion_tokens"] = max_completion_tokens
|
|
222
|
+
|
|
219
223
|
if images:
|
|
220
224
|
for image in images:
|
|
221
225
|
url = image
|
lightning_sdk/api/studio_api.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
import tempfile
|
|
4
3
|
import time
|
|
5
|
-
import
|
|
4
|
+
from pathlib import Path
|
|
6
5
|
from threading import Event, Thread
|
|
7
6
|
from typing import Any, Dict, Generator, List, Mapping, Optional, Tuple, Union
|
|
8
7
|
|
|
@@ -12,6 +11,7 @@ from tqdm import tqdm
|
|
|
12
11
|
|
|
13
12
|
from lightning_sdk.api.utils import (
|
|
14
13
|
_create_app,
|
|
14
|
+
_download_studio_files,
|
|
15
15
|
_DummyBody,
|
|
16
16
|
_DummyResponse,
|
|
17
17
|
_FileUploader,
|
|
@@ -205,6 +205,32 @@ class StudioApi:
|
|
|
205
205
|
instance_id = code_status.in_use.cloud_space_instance_id
|
|
206
206
|
print(f"Studio started | {teamspace_id=} {studio_id=} {instance_id=}")
|
|
207
207
|
|
|
208
|
+
def start_studio_async(
|
|
209
|
+
self,
|
|
210
|
+
studio_id: str,
|
|
211
|
+
teamspace_id: str,
|
|
212
|
+
machine: Union[Machine, str],
|
|
213
|
+
interruptible: bool = False,
|
|
214
|
+
max_runtime: Optional[int] = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Start an existing Studio without blocking."""
|
|
217
|
+
# need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
|
|
218
|
+
optional_kwargs_compute_body = {}
|
|
219
|
+
|
|
220
|
+
if max_runtime is not None:
|
|
221
|
+
optional_kwargs_compute_body["requested_run_duration_seconds"] = str(max_runtime)
|
|
222
|
+
self._client.cloud_space_service_start_cloud_space_instance(
|
|
223
|
+
IdStartBody(
|
|
224
|
+
compute_config=V1UserRequestedComputeConfig(
|
|
225
|
+
name=_machine_to_compute_name(machine),
|
|
226
|
+
spot=interruptible,
|
|
227
|
+
**optional_kwargs_compute_body,
|
|
228
|
+
)
|
|
229
|
+
),
|
|
230
|
+
teamspace_id,
|
|
231
|
+
studio_id,
|
|
232
|
+
)
|
|
233
|
+
|
|
208
234
|
def stop_studio(self, studio_id: str, teamspace_id: str) -> None:
|
|
209
235
|
"""Stop an existing Studio."""
|
|
210
236
|
self.stop_keeping_alive(teamspace_id=teamspace_id, studio_id=studio_id)
|
|
@@ -289,6 +315,79 @@ class StudioApi:
|
|
|
289
315
|
break
|
|
290
316
|
time.sleep(1)
|
|
291
317
|
|
|
318
|
+
def switch_studio_machine_with_progress(
|
|
319
|
+
self,
|
|
320
|
+
studio_id: str,
|
|
321
|
+
teamspace_id: str,
|
|
322
|
+
machine: Union[Machine, str],
|
|
323
|
+
interruptible: bool,
|
|
324
|
+
progress: Any, # StudioProgressTracker - avoid circular import
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Switches given Studio to a new machine type with progress tracking."""
|
|
327
|
+
progress.update_progress(10, "Requesting machine switch...")
|
|
328
|
+
|
|
329
|
+
self._request_switch(
|
|
330
|
+
studio_id=studio_id, teamspace_id=teamspace_id, machine=machine, interruptible=interruptible
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
progress.update_progress(20, "Waiting for machine allocation...")
|
|
334
|
+
|
|
335
|
+
# Wait until it's time to switch
|
|
336
|
+
requested_was_found = False
|
|
337
|
+
startup_status = None
|
|
338
|
+
base_progress = 20
|
|
339
|
+
max_wait_progress = 60
|
|
340
|
+
wait_counter = 0
|
|
341
|
+
|
|
342
|
+
while True:
|
|
343
|
+
status = self.get_studio_status(studio_id, teamspace_id)
|
|
344
|
+
requested_machine = status.requested
|
|
345
|
+
|
|
346
|
+
if requested_machine is not None:
|
|
347
|
+
requested_was_found = True
|
|
348
|
+
startup_status = requested_machine.startup_status
|
|
349
|
+
|
|
350
|
+
# if the requested machine was found in the past, use the in_use status instead.
|
|
351
|
+
# it might be that it either was cancelled or it actually is ready.
|
|
352
|
+
# Either way, since we're actually blocking below for the in use startup status
|
|
353
|
+
# it's safe to switch at this point
|
|
354
|
+
elif requested_was_found:
|
|
355
|
+
in_use_machine = status.in_use
|
|
356
|
+
if in_use_machine is not None:
|
|
357
|
+
startup_status = in_use_machine.startup_status
|
|
358
|
+
|
|
359
|
+
if startup_status and startup_status.initial_restore_finished:
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
# Update progress gradually while waiting
|
|
363
|
+
wait_counter += 1
|
|
364
|
+
current_progress = min(base_progress + (wait_counter * 2), max_wait_progress)
|
|
365
|
+
progress.update_progress(current_progress, "Allocating new machine...")
|
|
366
|
+
time.sleep(1)
|
|
367
|
+
|
|
368
|
+
progress.update_progress(70, "Starting machine switch...")
|
|
369
|
+
self._client.cloud_space_service_switch_cloud_space_instance(teamspace_id, studio_id)
|
|
370
|
+
|
|
371
|
+
progress.update_progress(80, "Configuring new machine...")
|
|
372
|
+
|
|
373
|
+
# Wait until the new machine is ready to use
|
|
374
|
+
switch_counter = 0
|
|
375
|
+
while True:
|
|
376
|
+
in_use = self.get_studio_status(studio_id, teamspace_id).in_use
|
|
377
|
+
if in_use is None:
|
|
378
|
+
continue
|
|
379
|
+
startup_status = in_use.startup_status
|
|
380
|
+
if startup_status and startup_status.top_up_restore_finished:
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
# Update progress while waiting for machine to be ready
|
|
384
|
+
switch_counter += 1
|
|
385
|
+
current_progress = min(80 + switch_counter, 95)
|
|
386
|
+
progress.update_progress(current_progress, "Finalizing machine setup...")
|
|
387
|
+
time.sleep(1)
|
|
388
|
+
|
|
389
|
+
progress.complete("Machine switch completed successfully")
|
|
390
|
+
|
|
292
391
|
def get_machine(self, studio_id: str, teamspace_id: str, cloud_account_id: str, org_id: str) -> Machine:
|
|
293
392
|
"""Get the current machine type the given Studio is running on."""
|
|
294
393
|
response: V1CloudSpaceInstanceConfig = self._client.cloud_space_service_get_cloud_space_instance_config(
|
|
@@ -569,46 +668,24 @@ class StudioApi:
|
|
|
569
668
|
progress_bar: bool = True,
|
|
570
669
|
) -> None:
|
|
571
670
|
"""Downloads a given folder from a Studio to a target location."""
|
|
572
|
-
# TODO:
|
|
671
|
+
# TODO: implement resumable downloads
|
|
573
672
|
auth = Auth()
|
|
574
673
|
auth.authenticate()
|
|
575
|
-
token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
|
|
576
674
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
"
|
|
581
|
-
}
|
|
675
|
+
prefix = _sanitize_studio_remote_path(path, studio_id)
|
|
676
|
+
# ensure we only download as a directory and not the entire prefix
|
|
677
|
+
if prefix.endswith("/") is False:
|
|
678
|
+
prefix = prefix + "/"
|
|
582
679
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
680
|
+
_download_studio_files(
|
|
681
|
+
client=self._client,
|
|
682
|
+
teamspace_id=teamspace_id,
|
|
683
|
+
cluster_id=cloud_account,
|
|
684
|
+
prefix=prefix,
|
|
685
|
+
download_dir=Path(target_path),
|
|
686
|
+
progress_bar=progress_bar,
|
|
587
687
|
)
|
|
588
688
|
|
|
589
|
-
if progress_bar:
|
|
590
|
-
pbar = tqdm(
|
|
591
|
-
desc=f"Downloading {os.path.split(path)[1]}",
|
|
592
|
-
unit="B",
|
|
593
|
-
unit_scale=True,
|
|
594
|
-
unit_divisor=1000,
|
|
595
|
-
)
|
|
596
|
-
|
|
597
|
-
pbar_update = pbar.update
|
|
598
|
-
else:
|
|
599
|
-
pbar_update = lambda x: None
|
|
600
|
-
|
|
601
|
-
if target_path:
|
|
602
|
-
os.makedirs(target_path, exist_ok=True)
|
|
603
|
-
|
|
604
|
-
with tempfile.TemporaryFile() as f:
|
|
605
|
-
for chunk in r.iter_content(chunk_size=4096 * 8):
|
|
606
|
-
f.write(chunk)
|
|
607
|
-
pbar_update(len(chunk))
|
|
608
|
-
|
|
609
|
-
with zipfile.ZipFile(f) as z:
|
|
610
|
-
z.extractall(target_path)
|
|
611
|
-
|
|
612
689
|
def install_plugin(self, studio_id: str, teamspace_id: str, plugin_name: str) -> str:
|
|
613
690
|
"""Installs the given plugin."""
|
|
614
691
|
resp: V1Plugin = self._client.cloud_space_service_install_plugin(
|
lightning_sdk/api/utils.py
CHANGED
|
@@ -6,7 +6,7 @@ import re
|
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
7
|
from functools import partial
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict, Union
|
|
10
10
|
|
|
11
11
|
import backoff
|
|
12
12
|
import requests
|
|
@@ -19,6 +19,7 @@ from lightning_sdk.lightning_cloud.openapi import (
|
|
|
19
19
|
ModelsStoreApi,
|
|
20
20
|
ProjectIdStorageBody,
|
|
21
21
|
StorageCompleteBody,
|
|
22
|
+
StorageServiceApi,
|
|
22
23
|
UploadIdCompleteBody,
|
|
23
24
|
UploadIdPartsBody,
|
|
24
25
|
V1CompletedPart,
|
|
@@ -367,38 +368,40 @@ _DOWNLOAD_REQUEST_CHUNK_SIZE = 10 * _BYTES_PER_MB
|
|
|
367
368
|
_DOWNLOAD_MIN_CHUNK_SIZE = 100 * _BYTES_PER_KB
|
|
368
369
|
|
|
369
370
|
|
|
371
|
+
class _RefreshResponse(TypedDict):
|
|
372
|
+
url: str
|
|
373
|
+
size: int
|
|
374
|
+
|
|
375
|
+
|
|
370
376
|
class _FileDownloader:
|
|
371
377
|
def __init__(
|
|
372
378
|
self,
|
|
373
|
-
client: LightningClient,
|
|
374
|
-
model_id: str,
|
|
375
|
-
version: str,
|
|
376
379
|
teamspace_id: str,
|
|
377
380
|
remote_path: str,
|
|
378
381
|
file_path: str,
|
|
379
382
|
executor: ThreadPoolExecutor,
|
|
380
383
|
num_workers: int = 20,
|
|
381
384
|
progress_bar: Optional[tqdm] = None,
|
|
385
|
+
url: Optional[str] = None,
|
|
386
|
+
size: Optional[int] = None,
|
|
387
|
+
refresh_fn: Optional[Callable[[], _RefreshResponse]] = None,
|
|
382
388
|
) -> None:
|
|
383
|
-
self.api = ModelsStoreApi(client.api_client)
|
|
384
|
-
self.model_id = model_id
|
|
385
|
-
self.version = version
|
|
386
389
|
self.teamspace_id = teamspace_id
|
|
387
390
|
self.local_path = file_path
|
|
388
391
|
self.remote_path = remote_path
|
|
389
392
|
self.progress_bar = progress_bar
|
|
390
393
|
self.num_workers = num_workers
|
|
391
|
-
self._url =
|
|
392
|
-
self._size =
|
|
394
|
+
self._url = url
|
|
395
|
+
self._size = size
|
|
393
396
|
self.executor = executor
|
|
397
|
+
self.refresh_fn = refresh_fn
|
|
394
398
|
|
|
395
399
|
@backoff.on_exception(backoff.expo, ApiException, max_tries=10)
|
|
396
400
|
def refresh(self) -> None:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
self._size = int(response.size)
|
|
401
|
+
if self.refresh_fn is not None:
|
|
402
|
+
response = self.refresh_fn()
|
|
403
|
+
self._url = response["url"]
|
|
404
|
+
self._size = response["size"]
|
|
402
405
|
|
|
403
406
|
@property
|
|
404
407
|
def url(self) -> str:
|
|
@@ -413,6 +416,11 @@ class _FileDownloader:
|
|
|
413
416
|
return
|
|
414
417
|
self.progress_bar.update(n)
|
|
415
418
|
|
|
419
|
+
def update_filename(self, desc: str) -> None:
|
|
420
|
+
if self.progress_bar is None:
|
|
421
|
+
return
|
|
422
|
+
self.progress_bar.set_description(f"{(desc[:72] + '...') if len(desc) > 75 else desc:<75.75}")
|
|
423
|
+
|
|
416
424
|
@backoff.on_exception(backoff.expo, (requests.exceptions.HTTPError), max_tries=10)
|
|
417
425
|
def _download_chunk(self, filename: str, start_end: Tuple[int]) -> None:
|
|
418
426
|
start, end = start_end
|
|
@@ -447,6 +455,8 @@ class _FileDownloader:
|
|
|
447
455
|
f.write(b"\x00" * remaining_size)
|
|
448
456
|
|
|
449
457
|
def _multipart_download(self, filename: str, num_workers: int) -> None:
|
|
458
|
+
self.update_filename(f"Downloading {self.remote_path}")
|
|
459
|
+
|
|
450
460
|
num_chunks = num_workers
|
|
451
461
|
chunk_size = math.ceil(self.size / num_chunks)
|
|
452
462
|
|
|
@@ -464,7 +474,8 @@ class _FileDownloader:
|
|
|
464
474
|
concurrent.futures.wait(futures)
|
|
465
475
|
|
|
466
476
|
def download(self) -> None:
|
|
467
|
-
self.
|
|
477
|
+
if self.url is None:
|
|
478
|
+
self.refresh()
|
|
468
479
|
|
|
469
480
|
tmp_filename = f"{self.local_path}.download"
|
|
470
481
|
|
|
@@ -539,6 +550,15 @@ def _download_model_files(
|
|
|
539
550
|
mininterval=1,
|
|
540
551
|
)
|
|
541
552
|
|
|
553
|
+
def refresh_fn(filename: str) -> _RefreshResponse:
|
|
554
|
+
resp = api.models_store_get_model_file_url(
|
|
555
|
+
project_id=response.project_id,
|
|
556
|
+
model_id=response.model_id,
|
|
557
|
+
version=response.version,
|
|
558
|
+
filepath=filename,
|
|
559
|
+
)
|
|
560
|
+
return {"url": resp.url, "size": int(resp.size)}
|
|
561
|
+
|
|
542
562
|
with ThreadPoolExecutor(max_workers=min(num_workers, len(response.filepaths))) as file_executor, ThreadPoolExecutor(
|
|
543
563
|
max_workers=num_workers
|
|
544
564
|
) as part_executor:
|
|
@@ -549,15 +569,13 @@ def _download_model_files(
|
|
|
549
569
|
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
550
570
|
|
|
551
571
|
file_downloader = _FileDownloader(
|
|
552
|
-
client=client,
|
|
553
|
-
model_id=response.model_id,
|
|
554
|
-
version=response.version,
|
|
555
572
|
teamspace_id=response.project_id,
|
|
556
573
|
remote_path=filepath,
|
|
557
574
|
file_path=str(local_file),
|
|
558
575
|
num_workers=num_workers,
|
|
559
576
|
progress_bar=pbar,
|
|
560
577
|
executor=part_executor,
|
|
578
|
+
refresh_fn=lambda f=filepath: refresh_fn(f),
|
|
561
579
|
)
|
|
562
580
|
|
|
563
581
|
futures.append(file_executor.submit(file_downloader.download))
|
|
@@ -568,6 +586,78 @@ def _download_model_files(
|
|
|
568
586
|
return response.filepaths
|
|
569
587
|
|
|
570
588
|
|
|
589
|
+
def _download_studio_files(
|
|
590
|
+
client: LightningClient,
|
|
591
|
+
teamspace_id: str,
|
|
592
|
+
cluster_id: str,
|
|
593
|
+
prefix: str,
|
|
594
|
+
download_dir: Path,
|
|
595
|
+
progress_bar: bool,
|
|
596
|
+
num_workers: int = os.cpu_count() * 4,
|
|
597
|
+
) -> None:
|
|
598
|
+
api = StorageServiceApi(client.api_client)
|
|
599
|
+
response = None
|
|
600
|
+
|
|
601
|
+
pbar = None
|
|
602
|
+
if progress_bar:
|
|
603
|
+
pbar = tqdm(
|
|
604
|
+
desc="Downloading files",
|
|
605
|
+
unit="B",
|
|
606
|
+
unit_scale=True,
|
|
607
|
+
unit_divisor=1000,
|
|
608
|
+
position=-1,
|
|
609
|
+
mininterval=1,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
def refresh_fn(filename: str) -> _RefreshResponse:
|
|
613
|
+
resp = api.storage_service_list_project_artifacts(
|
|
614
|
+
project_id=teamspace_id,
|
|
615
|
+
cluster_id=cluster_id,
|
|
616
|
+
page_token="",
|
|
617
|
+
include_download_url=True,
|
|
618
|
+
prefix=prefix + filename,
|
|
619
|
+
page_size=1,
|
|
620
|
+
)
|
|
621
|
+
return {"url": resp.artifacts[0].url, "size": int(resp.artifacts[0].size_bytes)}
|
|
622
|
+
|
|
623
|
+
with ThreadPoolExecutor(max_workers=num_workers) as file_executor, ThreadPoolExecutor(
|
|
624
|
+
max_workers=num_workers
|
|
625
|
+
) as part_executor:
|
|
626
|
+
while response is None or (response is not None and response.next_page_token != ""):
|
|
627
|
+
response = api.storage_service_list_project_artifacts(
|
|
628
|
+
project_id=teamspace_id,
|
|
629
|
+
cluster_id=cluster_id,
|
|
630
|
+
page_token=response.next_page_token if response is not None else "",
|
|
631
|
+
include_download_url=True,
|
|
632
|
+
prefix=prefix,
|
|
633
|
+
page_size=1000,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
page_futures = []
|
|
637
|
+
for file in response.artifacts:
|
|
638
|
+
local_file = download_dir / file.filename
|
|
639
|
+
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
640
|
+
|
|
641
|
+
file_downloader = _FileDownloader(
|
|
642
|
+
teamspace_id=teamspace_id,
|
|
643
|
+
remote_path=file.filename,
|
|
644
|
+
file_path=str(local_file),
|
|
645
|
+
num_workers=num_workers,
|
|
646
|
+
progress_bar=pbar,
|
|
647
|
+
executor=part_executor,
|
|
648
|
+
url=file.url,
|
|
649
|
+
size=int(file.size_bytes),
|
|
650
|
+
refresh_fn=lambda f=file: refresh_fn(f.filename),
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
page_futures.append(file_executor.submit(file_downloader.download))
|
|
654
|
+
|
|
655
|
+
if page_futures:
|
|
656
|
+
concurrent.futures.wait(page_futures)
|
|
657
|
+
|
|
658
|
+
pbar.set_description("Download complete")
|
|
659
|
+
|
|
660
|
+
|
|
571
661
|
def _create_app(
|
|
572
662
|
client: CloudSpaceServiceApi,
|
|
573
663
|
studio_id: str,
|
|
@@ -7,10 +7,8 @@ import click
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
9
|
from lightning_sdk import Machine, Studio
|
|
10
|
-
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
11
10
|
from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
|
|
12
11
|
from lightning_sdk.machine import CloudProvider
|
|
13
|
-
from lightning_sdk.utils.resolve import _resolve_deprecated_provider
|
|
14
12
|
|
|
15
13
|
_MACHINE_VALUES = tuple(
|
|
16
14
|
[machine.name for machine in Machine.__dict__.values() if isinstance(machine, Machine) and machine._include_in_cli]
|
|
@@ -83,14 +81,6 @@ def studio(
|
|
|
83
81
|
menu = _TeamspacesMenu()
|
|
84
82
|
teamspace_resolved = menu._resolve_teamspace(teamspace)
|
|
85
83
|
|
|
86
|
-
cloud_provider = str(_resolve_deprecated_provider(cloud_provider, provider))
|
|
87
|
-
|
|
88
|
-
if cloud_provider is not None:
|
|
89
|
-
cloud_account_api = CloudAccountApi()
|
|
90
|
-
cloud_account = cloud_account_api.resolve_cloud_account(
|
|
91
|
-
teamspace_resolved.id, cloud_account, cloud_provider, teamspace_resolved.default_cloud_account
|
|
92
|
-
)
|
|
93
|
-
|
|
94
84
|
# default cloud account to current studios cloud account if run from studio
|
|
95
85
|
# else it will fall back to teamspace default in the backend
|
|
96
86
|
if cloud_account is None:
|
|
@@ -107,11 +97,19 @@ def studio(
|
|
|
107
97
|
console.print(f"Studio with name {name} already exists. Using {new_name} instead.")
|
|
108
98
|
name = new_name
|
|
109
99
|
|
|
110
|
-
studio = Studio(
|
|
100
|
+
studio = Studio(
|
|
101
|
+
name=name,
|
|
102
|
+
teamspace=teamspace_resolved,
|
|
103
|
+
cloud_account=cloud_account,
|
|
104
|
+
create_ok=True,
|
|
105
|
+
cloud_provider=cloud_provider,
|
|
106
|
+
provider=provider,
|
|
107
|
+
)
|
|
111
108
|
|
|
112
109
|
console.print(f"Created Studio {studio.name}.")
|
|
113
110
|
|
|
114
111
|
if start is not None:
|
|
115
112
|
start_machine = getattr(Machine, start, start)
|
|
113
|
+
Studio.show_progress = True
|
|
116
114
|
studio.start(start_machine)
|
|
117
115
|
console.print(f"Started Studio {studio.name} on machine {start}")
|
|
@@ -77,5 +77,6 @@ def start_studio(
|
|
|
77
77
|
raise ValueError(f"Could not start Studio: '{studio_name}'. Does the Studio exist?") from None
|
|
78
78
|
raise ValueError(f"Could not start Studio: '{studio_name}'. Please provide a Studio name") from None
|
|
79
79
|
|
|
80
|
+
Studio.show_progress = True
|
|
80
81
|
studio.start(machine, interruptible=interruptible)
|
|
81
82
|
click.echo(f"Studio '{studio.name}' started successfully")
|
|
@@ -47,6 +47,7 @@ def switch_studio(
|
|
|
47
47
|
raise ValueError(f"Could not switch Studio: '{studio_name}'. Please provide a Studio name") from None
|
|
48
48
|
|
|
49
49
|
resolved_machine = Machine.from_str(machine)
|
|
50
|
+
Studio.show_progress = True
|
|
50
51
|
studio.switch_machine(resolved_machine, interruptible=interruptible)
|
|
51
52
|
|
|
52
53
|
click.echo(f"Studio '{studio.name}' switched to machine '{resolved_machine}' successfully")
|
|
@@ -638,6 +638,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_like_status import V1LikeSt
|
|
|
638
638
|
from lightning_sdk.lightning_cloud.openapi.models.v1_list_affiliate_links_response import V1ListAffiliateLinksResponse
|
|
639
639
|
from lightning_sdk.lightning_cloud.openapi.models.v1_list_agent_job_artifacts_response import V1ListAgentJobArtifactsResponse
|
|
640
640
|
from lightning_sdk.lightning_cloud.openapi.models.v1_list_agent_jobs_response import V1ListAgentJobsResponse
|
|
641
|
+
from lightning_sdk.lightning_cloud.openapi.models.v1_list_aggregated_pod_metrics_response import V1ListAggregatedPodMetricsResponse
|
|
641
642
|
from lightning_sdk.lightning_cloud.openapi.models.v1_list_assistants_response import V1ListAssistantsResponse
|
|
642
643
|
from lightning_sdk.lightning_cloud.openapi.models.v1_list_blog_posts_response import V1ListBlogPostsResponse
|
|
643
644
|
from lightning_sdk.lightning_cloud.openapi.models.v1_list_cloud_space_apps_response import V1ListCloudSpaceAppsResponse
|
|
@@ -852,6 +853,7 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_quest import V1Quest
|
|
|
852
853
|
from lightning_sdk.lightning_cloud.openapi.models.v1_quest_status import V1QuestStatus
|
|
853
854
|
from lightning_sdk.lightning_cloud.openapi.models.v1_queue_server_type import V1QueueServerType
|
|
854
855
|
from lightning_sdk.lightning_cloud.openapi.models.v1_quotas import V1Quotas
|
|
856
|
+
from lightning_sdk.lightning_cloud.openapi.models.v1_quote_annual_upsell_response import V1QuoteAnnualUpsellResponse
|
|
855
857
|
from lightning_sdk.lightning_cloud.openapi.models.v1_quote_subscription_response import V1QuoteSubscriptionResponse
|
|
856
858
|
from lightning_sdk.lightning_cloud.openapi.models.v1_r2_data_connection import V1R2DataConnection
|
|
857
859
|
from lightning_sdk.lightning_cloud.openapi.models.v1_refresh_index_response import V1RefreshIndexResponse
|
|
@@ -1356,6 +1356,91 @@ class BillingServiceApi(object):
|
|
|
1356
1356
|
_request_timeout=params.get('_request_timeout'),
|
|
1357
1357
|
collection_formats=collection_formats)
|
|
1358
1358
|
|
|
1359
|
+
def billing_service_quote_annual_upsell(self, **kwargs) -> 'V1QuoteAnnualUpsellResponse': # noqa: E501
|
|
1360
|
+
"""billing_service_quote_annual_upsell # noqa: E501
|
|
1361
|
+
|
|
1362
|
+
This method makes a synchronous HTTP request by default. To make an
|
|
1363
|
+
asynchronous HTTP request, please pass async_req=True
|
|
1364
|
+
>>> thread = api.billing_service_quote_annual_upsell(async_req=True)
|
|
1365
|
+
>>> result = thread.get()
|
|
1366
|
+
|
|
1367
|
+
:param async_req bool
|
|
1368
|
+
:return: V1QuoteAnnualUpsellResponse
|
|
1369
|
+
If the method is called asynchronously,
|
|
1370
|
+
returns the request thread.
|
|
1371
|
+
"""
|
|
1372
|
+
kwargs['_return_http_data_only'] = True
|
|
1373
|
+
if kwargs.get('async_req'):
|
|
1374
|
+
return self.billing_service_quote_annual_upsell_with_http_info(**kwargs) # noqa: E501
|
|
1375
|
+
else:
|
|
1376
|
+
(data) = self.billing_service_quote_annual_upsell_with_http_info(**kwargs) # noqa: E501
|
|
1377
|
+
return data
|
|
1378
|
+
|
|
1379
|
+
def billing_service_quote_annual_upsell_with_http_info(self, **kwargs) -> 'V1QuoteAnnualUpsellResponse': # noqa: E501
|
|
1380
|
+
"""billing_service_quote_annual_upsell # noqa: E501
|
|
1381
|
+
|
|
1382
|
+
This method makes a synchronous HTTP request by default. To make an
|
|
1383
|
+
asynchronous HTTP request, please pass async_req=True
|
|
1384
|
+
>>> thread = api.billing_service_quote_annual_upsell_with_http_info(async_req=True)
|
|
1385
|
+
>>> result = thread.get()
|
|
1386
|
+
|
|
1387
|
+
:param async_req bool
|
|
1388
|
+
:return: V1QuoteAnnualUpsellResponse
|
|
1389
|
+
If the method is called asynchronously,
|
|
1390
|
+
returns the request thread.
|
|
1391
|
+
"""
|
|
1392
|
+
|
|
1393
|
+
all_params = [] # noqa: E501
|
|
1394
|
+
all_params.append('async_req')
|
|
1395
|
+
all_params.append('_return_http_data_only')
|
|
1396
|
+
all_params.append('_preload_content')
|
|
1397
|
+
all_params.append('_request_timeout')
|
|
1398
|
+
|
|
1399
|
+
params = locals()
|
|
1400
|
+
for key, val in six.iteritems(params['kwargs']):
|
|
1401
|
+
if key not in all_params:
|
|
1402
|
+
raise TypeError(
|
|
1403
|
+
"Got an unexpected keyword argument '%s'"
|
|
1404
|
+
" to method billing_service_quote_annual_upsell" % key
|
|
1405
|
+
)
|
|
1406
|
+
params[key] = val
|
|
1407
|
+
del params['kwargs']
|
|
1408
|
+
|
|
1409
|
+
collection_formats = {}
|
|
1410
|
+
|
|
1411
|
+
path_params = {}
|
|
1412
|
+
|
|
1413
|
+
query_params = []
|
|
1414
|
+
|
|
1415
|
+
header_params = {}
|
|
1416
|
+
|
|
1417
|
+
form_params = []
|
|
1418
|
+
local_var_files = {}
|
|
1419
|
+
|
|
1420
|
+
body_params = None
|
|
1421
|
+
# HTTP header `Accept`
|
|
1422
|
+
header_params['Accept'] = self.api_client.select_header_accept(
|
|
1423
|
+
['application/json']) # noqa: E501
|
|
1424
|
+
|
|
1425
|
+
# Authentication setting
|
|
1426
|
+
auth_settings = [] # noqa: E501
|
|
1427
|
+
|
|
1428
|
+
return self.api_client.call_api(
|
|
1429
|
+
'/v1/billing/annual-upsell', 'GET',
|
|
1430
|
+
path_params,
|
|
1431
|
+
query_params,
|
|
1432
|
+
header_params,
|
|
1433
|
+
body=body_params,
|
|
1434
|
+
post_params=form_params,
|
|
1435
|
+
files=local_var_files,
|
|
1436
|
+
response_type='V1QuoteAnnualUpsellResponse', # noqa: E501
|
|
1437
|
+
auth_settings=auth_settings,
|
|
1438
|
+
async_req=params.get('async_req'),
|
|
1439
|
+
_return_http_data_only=params.get('_return_http_data_only'),
|
|
1440
|
+
_preload_content=params.get('_preload_content', True),
|
|
1441
|
+
_request_timeout=params.get('_request_timeout'),
|
|
1442
|
+
collection_formats=collection_formats)
|
|
1443
|
+
|
|
1359
1444
|
def billing_service_quote_subscription(self, **kwargs) -> 'V1QuoteSubscriptionResponse': # noqa: E501
|
|
1360
1445
|
"""billing_service_quote_subscription # noqa: E501
|
|
1361
1446
|
|