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.
Files changed (37) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/llm_api.py +6 -2
  3. lightning_sdk/api/studio_api.py +113 -36
  4. lightning_sdk/api/utils.py +108 -18
  5. lightning_sdk/cli/legacy/create.py +9 -11
  6. lightning_sdk/cli/legacy/start.py +1 -0
  7. lightning_sdk/cli/legacy/switch.py +1 -0
  8. lightning_sdk/cli/studio/start.py +1 -0
  9. lightning_sdk/cli/studio/switch.py +1 -0
  10. lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
  11. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
  12. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +113 -0
  13. lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
  14. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +15 -15
  15. lightning_sdk/lightning_cloud/openapi/models/id_codeconfig_body.py +3 -81
  16. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +27 -1
  17. lightning_sdk/lightning_cloud/openapi/models/project_id_storage_body.py +27 -1
  18. lightning_sdk/lightning_cloud/openapi/models/storage_complete_body.py +27 -1
  19. lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +27 -1
  20. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +79 -1
  21. lightning_sdk/lightning_cloud/openapi/models/v1_list_aggregated_pod_metrics_response.py +123 -0
  22. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +79 -1
  23. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
  24. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +157 -1
  25. lightning_sdk/lightning_cloud/openapi/models/v1_project_cluster_binding.py +27 -1
  26. lightning_sdk/lightning_cloud/openapi/models/v1_quote_annual_upsell_response.py +201 -0
  27. lightning_sdk/lightning_cloud/openapi/models/v1_update_cloud_space_instance_config_request.py +3 -81
  28. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -131
  29. lightning_sdk/llm/llm.py +2 -2
  30. lightning_sdk/studio.py +39 -6
  31. lightning_sdk/utils/progress.py +284 -0
  32. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/METADATA +1 -1
  33. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/RECORD +37 -34
  34. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/LICENSE +0 -0
  35. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/WHEEL +0 -0
  36. {lightning_sdk-2025.8.18.post0.dist-info → lightning_sdk-2025.8.21.dist-info}/entry_points.txt +0 -0
  37. {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
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "User",
33
33
  ]
34
34
 
35
- __version__ = "2025.08.18.post0"
35
+ __version__ = "2025.08.21"
36
36
  _check_version_and_prompt_upgrade(__version__)
37
37
  _set_tqdm_envvars_noninteractive()
@@ -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
@@ -1,8 +1,7 @@
1
1
  import json
2
2
  import os
3
- import tempfile
4
3
  import time
5
- import zipfile
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: Update this endpoint to permit basic auth
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
- query_params = {
578
- "clusterId": cloud_account,
579
- "prefix": _sanitize_studio_remote_path(path, studio_id),
580
- "token": token,
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
- r = requests.get(
584
- f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/download",
585
- params=query_params,
586
- stream=True,
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(
@@ -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 = 0
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
- response = self.api.models_store_get_model_file_url(
398
- project_id=self.teamspace_id, model_id=self.model_id, version=self.version, filepath=self.remote_path
399
- )
400
- self._url = response.url
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.refresh()
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(name=name, teamspace=teamspace_resolved, cloud_account=cloud_account, create_ok=True)
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}")
@@ -103,4 +103,5 @@ def studio(
103
103
  except KeyError:
104
104
  resolved_machine = machine
105
105
 
106
+ Studio.show_progress = True
106
107
  studio.start(resolved_machine)
@@ -59,4 +59,5 @@ def studio(name: str, teamspace: Optional[str] = None, machine: str = "CPU") ->
59
59
  except KeyError:
60
60
  resolved_machine = machine
61
61
 
62
+ Studio.show_progress = True
62
63
  studio.switch_machine(resolved_machine)
@@ -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