lightning-sdk 2025.8.19.post0__py3-none-any.whl → 2025.8.26__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 (61) 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 +168 -2
  4. lightning_sdk/api/teamspace_api.py +60 -30
  5. lightning_sdk/api/user_api.py +49 -1
  6. lightning_sdk/api/utils.py +1 -1
  7. lightning_sdk/cli/config/set.py +6 -18
  8. lightning_sdk/cli/legacy/create.py +12 -14
  9. lightning_sdk/cli/legacy/delete.py +3 -3
  10. lightning_sdk/cli/legacy/deploy/_auth.py +4 -4
  11. lightning_sdk/cli/legacy/download.py +7 -7
  12. lightning_sdk/cli/legacy/job_and_mmt_action.py +4 -4
  13. lightning_sdk/cli/legacy/list.py +9 -9
  14. lightning_sdk/cli/legacy/open.py +3 -3
  15. lightning_sdk/cli/legacy/start.py +1 -0
  16. lightning_sdk/cli/legacy/switch.py +1 -0
  17. lightning_sdk/cli/legacy/upload.py +3 -3
  18. lightning_sdk/cli/studio/create.py +14 -23
  19. lightning_sdk/cli/studio/delete.py +28 -27
  20. lightning_sdk/cli/studio/list.py +5 -6
  21. lightning_sdk/cli/studio/ssh.py +19 -22
  22. lightning_sdk/cli/studio/start.py +23 -23
  23. lightning_sdk/cli/studio/stop.py +22 -26
  24. lightning_sdk/cli/studio/switch.py +20 -23
  25. lightning_sdk/cli/utils/resolve.py +1 -1
  26. lightning_sdk/cli/utils/save_to_config.py +27 -0
  27. lightning_sdk/cli/utils/studio_selection.py +106 -0
  28. lightning_sdk/cli/utils/teamspace_selection.py +125 -0
  29. lightning_sdk/lightning_cloud/openapi/__init__.py +3 -0
  30. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +170 -0
  31. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
  32. lightning_sdk/lightning_cloud/openapi/models/__init__.py +3 -0
  33. lightning_sdk/lightning_cloud/openapi/models/assistant_id_conversations_body.py +15 -15
  34. lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
  35. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
  36. lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
  37. lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
  38. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
  39. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
  40. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
  41. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +281 -21
  42. lightning_sdk/lightning_cloud/openapi/models/v1_project_cluster_binding.py +27 -1
  43. lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
  44. lightning_sdk/lightning_cloud/openapi/models/v1_quote_annual_upsell_response.py +201 -0
  45. lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
  46. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +1 -27
  47. lightning_sdk/llm/llm.py +2 -2
  48. lightning_sdk/llm/public_assistants.py +4 -0
  49. lightning_sdk/studio.py +92 -28
  50. lightning_sdk/teamspace.py +25 -2
  51. lightning_sdk/user.py +19 -1
  52. lightning_sdk/utils/config.py +6 -0
  53. lightning_sdk/utils/names.py +1179 -0
  54. lightning_sdk/utils/progress.py +284 -0
  55. lightning_sdk/utils/resolve.py +6 -6
  56. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/METADATA +1 -1
  57. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/RECORD +61 -53
  58. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/LICENSE +0 -0
  59. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/WHEEL +0 -0
  60. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.dist-info}/entry_points.txt +0 -0
  61. {lightning_sdk-2025.8.19.post0.dist-info → lightning_sdk-2025.8.26.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.19.post0"
35
+ __version__ = "2025.08.26"
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
@@ -11,7 +11,7 @@ from tqdm import tqdm
11
11
 
12
12
  from lightning_sdk.api.utils import (
13
13
  _create_app,
14
- _download_studio_files,
14
+ _download_teamspace_files,
15
15
  _DummyBody,
16
16
  _DummyResponse,
17
17
  _FileUploader,
@@ -25,6 +25,7 @@ from lightning_sdk.constants import _LIGHTNING_DEBUG
25
25
  from lightning_sdk.lightning_cloud.login import Auth
26
26
  from lightning_sdk.lightning_cloud.openapi import (
27
27
  CloudspaceIdRunsBody,
28
+ CloudspacesIdBody,
28
29
  Externalv1LightningappInstance,
29
30
  IdCodeconfigBody,
30
31
  IdExecuteBody1,
@@ -40,6 +41,7 @@ from lightning_sdk.lightning_cloud.openapi import (
40
41
  V1CloudSpaceState,
41
42
  V1ClusterAccelerator,
42
43
  V1EndpointType,
44
+ V1EnvVar,
43
45
  V1GetCloudSpaceInstanceStatusResponse,
44
46
  V1GetLongRunningCommandInCloudSpaceResponse,
45
47
  V1LoginRequest,
@@ -205,6 +207,32 @@ class StudioApi:
205
207
  instance_id = code_status.in_use.cloud_space_instance_id
206
208
  print(f"Studio started | {teamspace_id=} {studio_id=} {instance_id=}")
207
209
 
210
+ def start_studio_async(
211
+ self,
212
+ studio_id: str,
213
+ teamspace_id: str,
214
+ machine: Union[Machine, str],
215
+ interruptible: bool = False,
216
+ max_runtime: Optional[int] = None,
217
+ ) -> None:
218
+ """Start an existing Studio without blocking."""
219
+ # need to go via kwargs for typing compatibility since autogenerated apis accept None but aren't typed with None
220
+ optional_kwargs_compute_body = {}
221
+
222
+ if max_runtime is not None:
223
+ optional_kwargs_compute_body["requested_run_duration_seconds"] = str(max_runtime)
224
+ self._client.cloud_space_service_start_cloud_space_instance(
225
+ IdStartBody(
226
+ compute_config=V1UserRequestedComputeConfig(
227
+ name=_machine_to_compute_name(machine),
228
+ spot=interruptible,
229
+ **optional_kwargs_compute_body,
230
+ )
231
+ ),
232
+ teamspace_id,
233
+ studio_id,
234
+ )
235
+
208
236
  def stop_studio(self, studio_id: str, teamspace_id: str) -> None:
209
237
  """Stop an existing Studio."""
210
238
  self.stop_keeping_alive(teamspace_id=teamspace_id, studio_id=studio_id)
@@ -289,6 +317,79 @@ class StudioApi:
289
317
  break
290
318
  time.sleep(1)
291
319
 
320
+ def switch_studio_machine_with_progress(
321
+ self,
322
+ studio_id: str,
323
+ teamspace_id: str,
324
+ machine: Union[Machine, str],
325
+ interruptible: bool,
326
+ progress: Any, # StudioProgressTracker - avoid circular import
327
+ ) -> None:
328
+ """Switches given Studio to a new machine type with progress tracking."""
329
+ progress.update_progress(10, "Requesting machine switch...")
330
+
331
+ self._request_switch(
332
+ studio_id=studio_id, teamspace_id=teamspace_id, machine=machine, interruptible=interruptible
333
+ )
334
+
335
+ progress.update_progress(20, "Waiting for machine allocation...")
336
+
337
+ # Wait until it's time to switch
338
+ requested_was_found = False
339
+ startup_status = None
340
+ base_progress = 20
341
+ max_wait_progress = 60
342
+ wait_counter = 0
343
+
344
+ while True:
345
+ status = self.get_studio_status(studio_id, teamspace_id)
346
+ requested_machine = status.requested
347
+
348
+ if requested_machine is not None:
349
+ requested_was_found = True
350
+ startup_status = requested_machine.startup_status
351
+
352
+ # if the requested machine was found in the past, use the in_use status instead.
353
+ # it might be that it either was cancelled or it actually is ready.
354
+ # Either way, since we're actually blocking below for the in use startup status
355
+ # it's safe to switch at this point
356
+ elif requested_was_found:
357
+ in_use_machine = status.in_use
358
+ if in_use_machine is not None:
359
+ startup_status = in_use_machine.startup_status
360
+
361
+ if startup_status and startup_status.initial_restore_finished:
362
+ break
363
+
364
+ # Update progress gradually while waiting
365
+ wait_counter += 1
366
+ current_progress = min(base_progress + (wait_counter * 2), max_wait_progress)
367
+ progress.update_progress(current_progress, "Allocating new machine...")
368
+ time.sleep(1)
369
+
370
+ progress.update_progress(70, "Starting machine switch...")
371
+ self._client.cloud_space_service_switch_cloud_space_instance(teamspace_id, studio_id)
372
+
373
+ progress.update_progress(80, "Configuring new machine...")
374
+
375
+ # Wait until the new machine is ready to use
376
+ switch_counter = 0
377
+ while True:
378
+ in_use = self.get_studio_status(studio_id, teamspace_id).in_use
379
+ if in_use is None:
380
+ continue
381
+ startup_status = in_use.startup_status
382
+ if startup_status and startup_status.top_up_restore_finished:
383
+ break
384
+
385
+ # Update progress while waiting for machine to be ready
386
+ switch_counter += 1
387
+ current_progress = min(80 + switch_counter, 95)
388
+ progress.update_progress(current_progress, "Finalizing machine setup...")
389
+ time.sleep(1)
390
+
391
+ progress.complete("Machine switch completed successfully")
392
+
292
393
  def get_machine(self, studio_id: str, teamspace_id: str, cloud_account_id: str, org_id: str) -> Machine:
293
394
  """Get the current machine type the given Studio is running on."""
294
395
  response: V1CloudSpaceInstanceConfig = self._client.cloud_space_service_get_cloud_space_instance_config(
@@ -578,7 +679,7 @@ class StudioApi:
578
679
  if prefix.endswith("/") is False:
579
680
  prefix = prefix + "/"
580
681
 
581
- _download_studio_files(
682
+ _download_teamspace_files(
582
683
  client=self._client,
583
684
  teamspace_id=teamspace_id,
584
685
  cluster_id=cloud_account,
@@ -841,3 +942,68 @@ class StudioApi:
841
942
  plugin_type=plugin_type,
842
943
  **other_arguments,
843
944
  )
945
+
946
+ def _update_cloudspace(self, studio: V1CloudSpace, teamspace_id: str, key: str, value: Any) -> None:
947
+ body = CloudspacesIdBody(
948
+ code_url=studio.code_url,
949
+ data_connection_mounts=studio.data_connection_mounts,
950
+ description=studio.description,
951
+ display_name=studio.display_name,
952
+ env=studio.env,
953
+ featured=studio.featured,
954
+ hide_files=studio.hide_files,
955
+ is_cloudspace_private=studio.is_cloudspace_private,
956
+ is_code_private=studio.is_code_private,
957
+ is_favorite=studio.is_favorite,
958
+ is_published=studio.is_published,
959
+ license=studio.license,
960
+ license_url=studio.license_url,
961
+ message=studio.message,
962
+ multi_user_edit=studio.multi_user_edit,
963
+ operating_cost=studio.operating_cost,
964
+ paper_authors=studio.paper_authors,
965
+ paper_org=studio.paper_org,
966
+ paper_org_avatar_url=studio.paper_org_avatar_url,
967
+ paper_url=studio.paper_url,
968
+ switch_to_default_machine_on_idle=studio.switch_to_default_machine_on_idle,
969
+ tags=studio.tags,
970
+ thumbnail_file_type=studio.thumbnail_file_type,
971
+ user_metadata=studio.user_metadata,
972
+ )
973
+
974
+ setattr(body, key, value)
975
+
976
+ self._client.cloud_space_service_update_cloud_space(
977
+ id=studio.id,
978
+ project_id=teamspace_id,
979
+ body=body,
980
+ )
981
+
982
+ def set_env(
983
+ self,
984
+ studio: V1CloudSpace,
985
+ teamspace_id: str,
986
+ new_env: Dict[str, str],
987
+ partial: bool = True,
988
+ ) -> None:
989
+ """Set the environment variables for the Studio.
990
+
991
+ Args:
992
+ new_env: The new environment variables to set.
993
+ partial: Whether to only set the environment variables that are provided.
994
+ If False, existing environment variables that are not in new_env will be removed.
995
+ If True, existing environment variables that are not in new_env will be kept.
996
+ """
997
+ updated_env_dict = {}
998
+ if partial:
999
+ updated_env_dict = {env.name: env.value for env in studio.env}
1000
+ updated_env_dict.update(new_env)
1001
+ else:
1002
+ updated_env_dict = new_env
1003
+
1004
+ updated_env = [V1EnvVar(name=key, value=value) for key, value in updated_env_dict.items()]
1005
+
1006
+ self._update_cloudspace(studio, teamspace_id, "env", updated_env)
1007
+
1008
+ def get_env(self, studio: V1CloudSpace) -> Dict[str, str]:
1009
+ return {env.name: env.value for env in studio.env}
@@ -1,6 +1,5 @@
1
1
  import os
2
- import tempfile
3
- import zipfile
2
+ import re
4
3
  from pathlib import Path
5
4
  from typing import Dict, List, Optional, Tuple
6
5
 
@@ -9,6 +8,7 @@ from tqdm.auto import tqdm
9
8
 
10
9
  from lightning_sdk.api.utils import (
11
10
  _download_model_files,
11
+ _download_teamspace_files,
12
12
  _DummyBody,
13
13
  _FileUploader,
14
14
  _get_model_version,
@@ -22,6 +22,8 @@ from lightning_sdk.lightning_cloud.openapi import (
22
22
  ModelsStoreApi,
23
23
  ProjectIdAgentsBody,
24
24
  ProjectIdModelsBody,
25
+ ProjectIdSecretsBody,
26
+ SecretsIdBody,
25
27
  V1Assistant,
26
28
  V1CloudSpace,
27
29
  V1ClusterAccelerator,
@@ -34,6 +36,8 @@ from lightning_sdk.lightning_cloud.openapi import (
34
36
  V1Project,
35
37
  V1ProjectClusterBinding,
36
38
  V1PromptSuggestion,
39
+ V1Secret,
40
+ V1SecretType,
37
41
  V1UpstreamOpenAI,
38
42
  )
39
43
  from lightning_sdk.lightning_cloud.rest_client import LightningClient
@@ -421,39 +425,65 @@ class TeamspaceApi:
421
425
  # TODO: Update this endpoint to permit basic auth
422
426
  auth = Auth()
423
427
  auth.authenticate()
424
- token = self._client.auth_service_login(V1LoginRequest(auth.api_key)).token
425
428
 
426
- query_params = {
427
- "clusterId": cloud_account,
428
- "prefix": _resolve_teamspace_remote_path(path),
429
- "token": token,
430
- }
429
+ prefix = _resolve_teamspace_remote_path(path)
431
430
 
432
- r = requests.get(
433
- f"{self._client.api_client.configuration.host}/v1/projects/{teamspace_id}/artifacts/download",
434
- params=query_params,
435
- stream=True,
431
+ # ensure we only download as a directory and not the entire prefix
432
+ if prefix.endswith("/") is False:
433
+ prefix = prefix + "/"
434
+
435
+ _download_teamspace_files(
436
+ client=self._client,
437
+ teamspace_id=teamspace_id,
438
+ cluster_id=cloud_account,
439
+ prefix=prefix,
440
+ download_dir=Path(target_path),
441
+ progress_bar=progress_bar,
436
442
  )
437
443
 
438
- if progress_bar:
439
- pbar = tqdm(
440
- desc=f"Downloading {os.path.split(path)[1]}",
441
- unit="B",
442
- unit_scale=True,
443
- unit_divisor=1000,
444
- )
444
+ def get_secrets(self, teamspace_id: str) -> Dict[str, str]:
445
+ """Get all secrets for a teamspace."""
446
+ secrets = self._get_secrets(teamspace_id)
447
+ # this returns encrypted values for security. It doesn't make sense to show them,
448
+ # so we just return a placeholder
449
+ # not a security issue to replace in the client as we get the encrypted values from the server.
450
+ return {secret.name: "***REDACTED***" for secret in secrets if secret.type == V1SecretType.UNSPECIFIED}
445
451
 
446
- pbar_update = pbar.update
447
- else:
448
- pbar_update = lambda x: None
452
+ def set_secret(self, teamspace_id: str, key: str, value: str) -> None:
453
+ """Set a secret for a teamspace.
449
454
 
450
- if target_path:
451
- os.makedirs(target_path, exist_ok=True)
455
+ This will replace the existing secret if it exists and create a new one if it doesn't.
456
+ """
457
+ secrets = self._get_secrets(teamspace_id)
458
+ for secret in secrets:
459
+ if secret.name == key:
460
+ return self._update_secret(teamspace_id, secret.id, value)
461
+ return self._create_secret(teamspace_id, key, value)
462
+
463
+ def _get_secrets(self, teamspace_id: str) -> List[V1Secret]:
464
+ return self._client.secret_service_list_secrets(project_id=teamspace_id).secrets
465
+
466
+ def _update_secret(self, teamspace_id: str, secret_id: str, value: str) -> None:
467
+ self._client.secret_service_update_secret(
468
+ body=SecretsIdBody(value=value),
469
+ project_id=teamspace_id,
470
+ id=secret_id,
471
+ )
452
472
 
453
- with tempfile.TemporaryFile() as f:
454
- for chunk in r.iter_content(chunk_size=4096 * 8):
455
- f.write(chunk)
456
- pbar_update(len(chunk))
473
+ def _create_secret(
474
+ self,
475
+ teamspace_id: str,
476
+ key: str,
477
+ value: str,
478
+ ) -> None:
479
+ self._client.secret_service_create_secret(
480
+ body=ProjectIdSecretsBody(name=key, value=value, type=V1SecretType.UNSPECIFIED), project_id=teamspace_id
481
+ )
457
482
 
458
- with zipfile.ZipFile(f) as z:
459
- z.extractall(target_path)
483
+ def verify_secret_name(self, name: str) -> bool:
484
+ """Verify if a secret name is valid.
485
+
486
+ A valid secret name starts with a letter or underscore, followed by letters, digits, or underscores.
487
+ """
488
+ pattern = r"^[A-Za-z_][A-Za-z0-9_]*$"
489
+ return re.match(pattern, name) is not None
@@ -1,15 +1,20 @@
1
- from typing import List, Union
1
+ import re
2
+ from typing import Dict, List, Union
2
3
 
3
4
  from lightning_sdk.lightning_cloud.login import Auth
4
5
  from lightning_sdk.lightning_cloud.openapi import (
6
+ SecretsIdBody1,
5
7
  V1CloudSpace,
8
+ V1CreateUserSecretRequest,
6
9
  V1GetUserResponse,
7
10
  V1ListCloudSpacesResponse,
8
11
  V1Membership,
9
12
  V1Organization,
10
13
  V1SearchUser,
14
+ V1Secret,
11
15
  V1UserFeatures,
12
16
  )
17
+ from lightning_sdk.lightning_cloud.openapi.models.v1_secret_type import V1SecretType
13
18
  from lightning_sdk.lightning_cloud.rest_client import LightningClient
14
19
 
15
20
 
@@ -69,3 +74,46 @@ class UserApi:
69
74
  def _get_feature_flags(self) -> V1UserFeatures:
70
75
  resp: V1GetUserResponse = self._client.auth_service_get_user()
71
76
  return resp.features
77
+
78
+ def get_secrets(self) -> Dict[str, str]:
79
+ """Get all secrets for the current user."""
80
+ secrets = self._get_secrets()
81
+ # this returns encrypted values for security. It doesn't make sense to show them,
82
+ # so we just return a placeholder
83
+ # not a security issue to replace in the client as we get the encrypted values from the server.
84
+ return {secret.name: "***REDACTED***" for secret in secrets if secret.type == V1SecretType.UNSPECIFIED}
85
+
86
+ def set_secret(self, key: str, value: str) -> None:
87
+ """Set a secret for the current user.
88
+
89
+ This will replace the existing secret if it exists and create a new one if it doesn't.
90
+ """
91
+ secrets = self._get_secrets()
92
+ for secret in secrets:
93
+ if secret.name == key:
94
+ return self._update_secret(secret.id, value)
95
+ return self._create_secret(key, value)
96
+
97
+ def _get_secrets(self) -> List[V1Secret]:
98
+ return self._client.secret_service_list_user_secrets().secrets
99
+
100
+ def _update_secret(self, secret_id: str, value: str) -> None:
101
+ self._client.secret_service_update_user_secret(
102
+ body=SecretsIdBody1(value=value),
103
+ id=secret_id,
104
+ )
105
+
106
+ def _create_secret(
107
+ self,
108
+ key: str,
109
+ value: str,
110
+ ) -> None:
111
+ self._client.secret_service_create_user_secret(body=V1CreateUserSecretRequest(name=key, value=value))
112
+
113
+ def verify_secret_name(self, name: str) -> bool:
114
+ """Verify if a secret name is valid.
115
+
116
+ A valid secret name starts with a letter or underscore, followed by letters, digits, or underscores.
117
+ """
118
+ pattern = r"^[A-Za-z_][A-Za-z0-9_]*$"
119
+ return re.match(pattern, name) is not None
@@ -586,7 +586,7 @@ def _download_model_files(
586
586
  return response.filepaths
587
587
 
588
588
 
589
- def _download_studio_files(
589
+ def _download_teamspace_files(
590
590
  client: LightningClient,
591
591
  teamspace_id: str,
592
592
  cluster_id: str,
@@ -1,8 +1,8 @@
1
1
  import click
2
2
 
3
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
3
+ from lightning_sdk.cli.utils.save_to_config import save_teamspace_to_config
4
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
4
5
  from lightning_sdk.machine import CloudProvider
5
- from lightning_sdk.organization import Organization
6
6
  from lightning_sdk.studio import Studio
7
7
  from lightning_sdk.utils.config import Config, DefaultConfigKeys
8
8
  from lightning_sdk.utils.resolve import _resolve_org, _resolve_user
@@ -59,23 +59,11 @@ def set_studio(studio_name: str) -> None:
59
59
  @click.argument("teamspace_name")
60
60
  def set_teamspace(teamspace_name: str) -> None:
61
61
  """Set the default teamspace name in the config."""
62
- config = Config()
63
-
64
- teamspace_resolved = resolve_teamspace_owner_name_format(teamspace_name)
62
+ menu = TeamspacesMenu()
63
+ teamspace_resolved = menu(teamspace=teamspace_name)
65
64
 
66
- if teamspace_resolved is None:
67
- # TODO: make this a generic CLI error
68
- raise ValueError(
69
- f"Could not resolve teamspace: '{teamspace_name}'. "
70
- "Teamspace should be specified as 'owner/name'. Does the teamspace exist?"
71
- )
72
-
73
- setattr(config, DefaultConfigKeys.teamspace_name, teamspace_resolved.name)
74
- setattr(config, DefaultConfigKeys.teamspace_owner, teamspace_resolved.owner.name)
75
- if isinstance(teamspace_resolved.owner, Organization):
76
- setattr(config, DefaultConfigKeys.teamspace_owner_type, "organization")
77
- else:
78
- setattr(config, DefaultConfigKeys.teamspace_owner_type, "user")
65
+ # explicit user action, so overwrite the config
66
+ save_teamspace_to_config(teamspace_resolved, overwrite=True)
79
67
 
80
68
 
81
69
  @set_value.command("cloud-account")
@@ -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
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
10
+ from lightning_sdk.cli.utils.teamspace_selection 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]
@@ -80,16 +78,8 @@ def studio(
80
78
 
81
79
  NAME: the name of the studio to create. If already present within teamspace, will add a random suffix.
82
80
  """
83
- menu = _TeamspacesMenu()
84
- teamspace_resolved = menu._resolve_teamspace(teamspace)
85
-
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
- )
81
+ menu = TeamspacesMenu()
82
+ teamspace_resolved = menu(teamspace)
93
83
 
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
@@ -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}")
@@ -5,7 +5,7 @@ from rich.console import Console
5
5
 
6
6
  from lightning_sdk.cli.legacy.exceptions import StudioCliError
7
7
  from lightning_sdk.cli.legacy.job_and_mmt_action import _JobAndMMTAction
8
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
8
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
9
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
10
10
  from lightning_sdk.lit_container import LitContainer
11
11
  from lightning_sdk.studio import Studio
@@ -30,8 +30,8 @@ def delete() -> None:
30
30
  def container(name: str, teamspace: Optional[str] = None) -> None:
31
31
  """Delete the docker container NAME."""
32
32
  api = LitContainer()
33
- menu = _TeamspacesMenu()
34
- resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
33
+ menu = TeamspacesMenu()
34
+ resolved_teamspace = menu(teamspace=teamspace)
35
35
  try:
36
36
  api.delete_container(name, resolved_teamspace.name, resolved_teamspace.owner.name)
37
37
  Console().print(f"Container {name} deleted successfully.")
@@ -10,7 +10,7 @@ from rich.prompt import Confirm
10
10
 
11
11
  from lightning_sdk import Teamspace
12
12
  from lightning_sdk.api import UserApi
13
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
13
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
14
14
  from lightning_sdk.lightning_cloud import env
15
15
  from lightning_sdk.lightning_cloud.login import Auth, AuthServer
16
16
  from lightning_sdk.lightning_cloud.openapi import V1CloudSpace
@@ -74,13 +74,13 @@ def authenticate(mode: _AuthMode, shall_confirm: bool = True) -> None:
74
74
  def select_teamspace(teamspace: Optional[str], org: Optional[str], user: Optional[str]) -> Teamspace:
75
75
  if teamspace is None:
76
76
  user = _get_authed_user()
77
- menu = _TeamspacesMenu()
77
+ menu = TeamspacesMenu()
78
78
  possible_teamspaces = menu._get_possible_teamspaces(user)
79
79
  if len(possible_teamspaces) == 1:
80
80
  name = next(iter(possible_teamspaces.values()))["name"]
81
81
  return Teamspace(name=name, org=org, user=user)
82
82
 
83
- return menu._resolve_teamspace(teamspace)
83
+ return menu(teamspace)
84
84
 
85
85
  return _resolve_teamspace(teamspace=teamspace, org=org, user=user)
86
86
 
@@ -180,7 +180,7 @@ class _Onboarding:
180
180
  return select_teamspace(teamspace, org, user)
181
181
 
182
182
  # Run only when user hasn't completed onboarding yet.
183
- menu = _TeamspacesMenu()
183
+ menu = TeamspacesMenu()
184
184
  self._wait_user_onboarding()
185
185
  # Onboarding has been completed - user already selected organization if they could
186
186
  possible_teamspaces = menu._get_possible_teamspaces(self.user)
@@ -11,7 +11,7 @@ from lightning_sdk.api.license_api import LicenseApi
11
11
  from lightning_sdk.api.lit_container_api import LitContainerApi
12
12
  from lightning_sdk.cli.legacy.exceptions import StudioCliError
13
13
  from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
14
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
14
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
15
15
  from lightning_sdk.models import download_model
16
16
  from lightning_sdk.studio import Studio
17
17
  from lightning_sdk.utils.resolve import _get_authed_user, skip_studio_init
@@ -110,8 +110,8 @@ def folder(
110
110
  path = _expand_remote_path(path)
111
111
  resolved_downloader = _resolve_studio(studio)
112
112
  elif teamspace:
113
- menu = _TeamspacesMenu()
114
- resolved_downloader = menu._resolve_teamspace(teamspace)
113
+ menu = TeamspacesMenu()
114
+ resolved_downloader = menu(teamspace)
115
115
  else:
116
116
  raise ValueError("Either --studio or --teamspace must be provided")
117
117
 
@@ -173,8 +173,8 @@ def file(path: str = "", studio: Optional[str] = None, teamspace: Optional[str]
173
173
  if studio:
174
174
  resolved_downloader = _resolve_studio(studio)
175
175
  elif teamspace:
176
- menu = _TeamspacesMenu()
177
- resolved_downloader = menu._resolve_teamspace(teamspace)
176
+ menu = TeamspacesMenu()
177
+ resolved_downloader = menu(teamspace)
178
178
  else:
179
179
  raise ValueError("Either --studio or --teamspace must be provided")
180
180
 
@@ -214,8 +214,8 @@ def download_container(
214
214
  CONTAINER: The name of the container to download.
215
215
  """
216
216
  console = Console()
217
- menu = _TeamspacesMenu()
218
- resolved_teamspace = menu._resolve_teamspace(teamspace)
217
+ menu = TeamspacesMenu()
218
+ resolved_teamspace = menu(teamspace)
219
219
  with console.status("Downloading container..."):
220
220
  api = LitContainerApi()
221
221
  api.download_container(container, resolved_teamspace, tag, cloud_account)