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
@@ -2,12 +2,12 @@ from typing import Optional
2
2
 
3
3
  from lightning_sdk.cli.legacy.jobs_menu import _JobsMenu
4
4
  from lightning_sdk.cli.legacy.mmts_menu import _MMTsMenu
5
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
5
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
6
6
  from lightning_sdk.job import Job
7
7
  from lightning_sdk.mmt import MMT
8
8
 
9
9
 
10
- class _JobAndMMTAction(_TeamspacesMenu, _JobsMenu, _MMTsMenu):
10
+ class _JobAndMMTAction(TeamspacesMenu, _JobsMenu, _MMTsMenu):
11
11
  """Inspect resources of the Lightning AI platform to get additional details as JSON."""
12
12
 
13
13
  def job(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> Job:
@@ -20,7 +20,7 @@ class _JobAndMMTAction(_TeamspacesMenu, _JobsMenu, _MMTsMenu):
20
20
  If not specified can be selected interactively.
21
21
 
22
22
  """
23
- resolved_teamspace = self._resolve_teamspace(teamspace)
23
+ resolved_teamspace = self(teamspace)
24
24
  return self._resolve_job(name, teamspace=resolved_teamspace)
25
25
 
26
26
  def mmt(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> MMT:
@@ -33,5 +33,5 @@ class _JobAndMMTAction(_TeamspacesMenu, _JobsMenu, _MMTsMenu):
33
33
  If not specified can be selected interactively.
34
34
 
35
35
  """
36
- resolved_teamspace = self._resolve_teamspace(teamspace)
36
+ resolved_teamspace = self(teamspace)
37
37
  return self._resolve_mmt(name, teamspace=resolved_teamspace)
@@ -8,7 +8,7 @@ from typing_extensions import Literal
8
8
 
9
9
  from lightning_sdk import Job, Machine, Studio, Teamspace
10
10
  from lightning_sdk.cli.legacy.clusters_menu import _ClustersMenu
11
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
11
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
12
12
  from lightning_sdk.lightning_cloud.openapi import V1MultiMachineJob
13
13
  from lightning_sdk.lit_container import LitContainer
14
14
  from lightning_sdk.utils.resolve import _get_authed_user
@@ -48,7 +48,7 @@ def studios(
48
48
  ) -> None:
49
49
  """List studios for a given teamspace."""
50
50
  studios = []
51
- menu = _TeamspacesMenu()
51
+ menu = TeamspacesMenu()
52
52
  if all and not teamspace:
53
53
  user = _get_authed_user()
54
54
  possible_teamspaces = menu._get_possible_teamspaces(user)
@@ -56,7 +56,7 @@ def studios(
56
56
  teamspace = Teamspace(**ts)
57
57
  studios.extend(teamspace.studios)
58
58
  else:
59
- resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
59
+ resolved_teamspace = menu(teamspace=teamspace)
60
60
  studios = resolved_teamspace.studios
61
61
 
62
62
  table = Table(
@@ -111,7 +111,7 @@ def jobs(
111
111
  ) -> None:
112
112
  """List jobs for a given teamspace."""
113
113
  jobs = []
114
- menu = _TeamspacesMenu()
114
+ menu = TeamspacesMenu()
115
115
  if all and not teamspace:
116
116
  user = _get_authed_user()
117
117
  possible_teamspaces = menu._get_possible_teamspaces(user)
@@ -119,7 +119,7 @@ def jobs(
119
119
  teamspace = Teamspace(**ts)
120
120
  jobs.extend(teamspace.jobs)
121
121
  else:
122
- resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
122
+ resolved_teamspace = menu(teamspace=teamspace)
123
123
  jobs = resolved_teamspace.jobs
124
124
 
125
125
  table = Table(pad_edge=True)
@@ -182,7 +182,7 @@ def mmts(
182
182
  ) -> None:
183
183
  """List multi-machine jobs for a given teamspace."""
184
184
  jobs = []
185
- menu = _TeamspacesMenu()
185
+ menu = TeamspacesMenu()
186
186
  if all and not teamspace:
187
187
  user = _get_authed_user()
188
188
  possible_teamspaces = menu._get_possible_teamspaces(user)
@@ -190,7 +190,7 @@ def mmts(
190
190
  teamspace = Teamspace(**ts)
191
191
  jobs.extend(teamspace.multi_machine_jobs)
192
192
  else:
193
- resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
193
+ resolved_teamspace = menu(teamspace=teamspace)
194
194
  jobs = resolved_teamspace.multi_machine_jobs
195
195
 
196
196
  table = Table(pad_edge=True)
@@ -242,9 +242,9 @@ def mmts(
242
242
  def containers(teamspace: Optional[str] = None, cloud_account: Optional[str] = None) -> None:
243
243
  """Display the list of available containers."""
244
244
  api = LitContainer()
245
- menu = _TeamspacesMenu()
245
+ menu = TeamspacesMenu()
246
246
  clusters_menu = _ClustersMenu()
247
- resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
247
+ resolved_teamspace = menu(teamspace=teamspace)
248
248
 
249
249
  if not cloud_account:
250
250
  cloud_account = clusters_menu._resolve_cluster(resolved_teamspace)
@@ -6,8 +6,8 @@ from typing import Optional
6
6
  import click
7
7
  from rich.console import Console
8
8
 
9
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
10
9
  from lightning_sdk.cli.legacy.upload import _upload_folder
10
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
11
11
  from lightning_sdk.studio import Studio
12
12
  from lightning_sdk.teamspace import Teamspace
13
13
  from lightning_sdk.utils.resolve import _get_studio_url
@@ -49,8 +49,8 @@ def open(path: str = ".", teamspace: Optional[str] = None, cloud_account: Option
49
49
  try:
50
50
  resolved_teamspace = Teamspace()
51
51
  except ValueError:
52
- menu = _TeamspacesMenu()
53
- resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
52
+ menu = TeamspacesMenu()
53
+ resolved_teamspace = menu(teamspace=teamspace)
54
54
 
55
55
  # default cloud account to current studios cloud account if run from studio
56
56
  # else it will fall back to teamspace default in the backend
@@ -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)
@@ -16,7 +16,7 @@ from lightning_sdk.api.lit_container_api import DockerNotRunningError, LCRAuthFa
16
16
  from lightning_sdk.api.utils import _get_cloud_url
17
17
  from lightning_sdk.cli.legacy.exceptions import StudioCliError
18
18
  from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
19
- from lightning_sdk.cli.legacy.teamspace_menu import _TeamspacesMenu
19
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
20
20
  from lightning_sdk.constants import _LIGHTNING_DEBUG
21
21
  from lightning_sdk.models import upload_model as _upload_model
22
22
  from lightning_sdk.studio import Studio
@@ -137,8 +137,8 @@ def upload_container(
137
137
  platform: Optional[str] = "linux/amd64",
138
138
  ) -> None:
139
139
  """Upload a container to Lightning AI's container registry."""
140
- menu = _TeamspacesMenu()
141
- teamspace = menu._resolve_teamspace(teamspace)
140
+ menu = TeamspacesMenu()
141
+ teamspace = menu(teamspace)
142
142
  console = Console()
143
143
  with Progress(
144
144
  SpinnerColumn(),
@@ -4,14 +4,15 @@ from typing import Optional
4
4
 
5
5
  import click
6
6
 
7
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
7
+ from lightning_sdk.cli.utils.save_to_config import save_teamspace_to_config
8
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
8
9
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
9
10
  from lightning_sdk.machine import CloudProvider
10
11
  from lightning_sdk.studio import Studio
11
12
 
12
13
 
13
14
  @click.command("create")
14
- @click.argument("studio_name", required=False)
15
+ @click.option("--name", help="The name of the studio to create. If not provided, a random name will be generated.")
15
16
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
16
17
  @click.option(
17
18
  "--cloud-provider",
@@ -24,7 +25,7 @@ from lightning_sdk.studio import Studio
24
25
  type=click.STRING,
25
26
  )
26
27
  def create_studio(
27
- studio_name: Optional[str] = None,
28
+ name: Optional[str] = None,
28
29
  teamspace: Optional[str] = None,
29
30
  cloud_provider: Optional[str] = None,
30
31
  cloud_account: Optional[str] = None,
@@ -32,37 +33,27 @@ def create_studio(
32
33
  """Create a new Studio.
33
34
 
34
35
  Example:
35
- lightning studio create [STUDIO_NAME]
36
-
37
- STUDIO_NAME: the name of the studio to create.
38
-
39
- If STUDIO_NAME is not provided, will try to infer from environment or use the default value from the config.
36
+ lightning studio create
40
37
  """
41
- if teamspace is not None:
42
- resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
43
- if resolved_teamspace is None:
44
- raise ValueError(
45
- f"Could not resolve teamspace: '{teamspace}'. Teamspace should be specified as 'owner/name'. "
46
- "Does the teamspace exist?"
47
- )
48
- else:
49
- resolved_teamspace = None
38
+ menu = TeamspacesMenu()
39
+
40
+ resolved_teamspace = menu(teamspace)
41
+ save_teamspace_to_config(resolved_teamspace, overwrite=False)
50
42
 
51
43
  if cloud_provider is not None:
52
44
  cloud_provider = CloudProvider(cloud_provider)
53
45
 
54
46
  try:
55
47
  studio = Studio(
56
- studio_name,
48
+ name=name,
57
49
  teamspace=resolved_teamspace,
58
50
  create_ok=True,
59
51
  cloud_provider=cloud_provider,
60
52
  cloud_account=cloud_account,
61
53
  )
62
- except (RuntimeError, ValueError, ApiException) as e:
63
- print(e)
64
- if studio_name:
65
- raise ValueError(f"Could not create Studio: '{studio_name}'. Does the Studio exist?") from None
66
- raise ValueError(f"Could not create Studio: '{studio_name}'. Please provide a Studio name") from None
54
+ except (RuntimeError, ValueError, ApiException):
55
+ if name:
56
+ raise ValueError(f"Could not create Studio: '{name}'. Does the Studio exist?") from None
57
+ raise ValueError(f"Could not create Studio: '{name}'. Please provide a Studio name") from None
67
58
 
68
59
  click.echo(f"Studio '{studio.name}' created successfully")
@@ -4,41 +4,42 @@ from typing import Optional
4
4
 
5
5
  import click
6
6
 
7
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
8
- from lightning_sdk.studio import Studio
7
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
8
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
9
 
10
10
 
11
11
  @click.command("delete")
12
- @click.argument("studio_name", required=False)
12
+ @click.option(
13
+ "--name",
14
+ help=(
15
+ "The name of the studio to start. "
16
+ "If not provided, will try to infer from environment, "
17
+ "use the default value from the config or prompt for interactive selection."
18
+ ),
19
+ )
13
20
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
14
- def delete_studio(studio_name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
21
+ def delete_studio(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
15
22
  """Delete a Studio.
16
23
 
17
24
  Example:
18
- lightning studio delete [STUDIO_NAME]
25
+ lightning studio delete --name my-studio
19
26
 
20
- STUDIO_NAME: the name of the studio to delete.
21
-
22
- If STUDIO_NAME is not provided, will try to infer from environment or use the default value from the config.
23
27
  """
24
- # missing studio_name and teamspace are handled by the studio class
25
- if teamspace is not None:
26
- resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
27
- if resolved_teamspace is None:
28
- raise ValueError(
29
- f"Could not resolve teamspace: '{teamspace}'. Teamspace should be specified as 'owner/name'. "
30
- "Does the teamspace exist?"
31
- )
32
- else:
33
- resolved_teamspace = None
34
-
35
- try:
36
- studio = Studio(studio_name, teamspace=resolved_teamspace, create_ok=False)
37
- studio.delete()
38
- except Exception:
39
- # TODO: make this a generic CLI error
40
- if studio_name:
41
- raise ValueError(f"Could not delete Studio: '{studio_name}'. Does the Studio exist?") from None
42
- raise ValueError("No studio name provided. Use 'lightning studio delete <name>' to delete a studio.") from None
28
+ menu = TeamspacesMenu()
29
+ resolved_teamspace = menu(teamspace=teamspace)
30
+
31
+ menu = StudiosMenu(resolved_teamspace)
32
+ studio = menu(studio=name)
33
+
34
+ studio_name = f"{studio.teamspace.owner.name}/{studio.teamspace.name}/{studio.name}"
35
+ confirmed = click.confirm(
36
+ f"Are you sure you want to delete studio '{studio_name}'?",
37
+ abort=True,
38
+ )
39
+ if not confirmed:
40
+ click.echo("Studio deletion cancelled")
41
+ return
42
+
43
+ studio.delete()
43
44
 
44
45
  click.echo(f"Studio '{studio.name}' deleted successfully")
@@ -6,8 +6,9 @@ import click
6
6
  from rich.table import Table
7
7
 
8
8
  from lightning_sdk.cli.utils.cloud_account_map import cloud_account_to_display_name
9
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
10
9
  from lightning_sdk.cli.utils.richt_print import rich_to_str
10
+ from lightning_sdk.cli.utils.save_to_config import save_teamspace_to_config
11
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
11
12
  from lightning_sdk.studio import Studio
12
13
 
13
14
 
@@ -26,11 +27,9 @@ def list_studios(teamspace: Optional[str] = None, sort_by: Optional[str] = None)
26
27
  lightning studio list --teamspace owner/teamspace
27
28
 
28
29
  """
29
- teamspace_resolved = resolve_teamspace_owner_name_format(teamspace)
30
-
31
- if teamspace_resolved is None:
32
- # TODO: make this a generic CLI error
33
- raise ValueError(f"Could not resolve teamspace: {teamspace}")
30
+ menu = TeamspacesMenu()
31
+ teamspace_resolved = menu(teamspace=teamspace)
32
+ save_teamspace_to_config(teamspace_resolved, overwrite=False)
34
33
 
35
34
  studios = teamspace_resolved.studios
36
35
 
@@ -9,14 +9,22 @@ from typing import List, Optional
9
9
 
10
10
  import click
11
11
 
12
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
12
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
13
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
14
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
13
15
  from lightning_sdk.lightning_cloud.login import Auth
14
- from lightning_sdk.studio import Studio
15
16
  from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH
16
17
 
17
18
 
18
19
  @click.command("ssh")
19
- @click.argument("studio_name", required=False)
20
+ @click.option(
21
+ "--name",
22
+ help=(
23
+ "The name of the studio to start. "
24
+ "If not provided, will try to infer from environment, "
25
+ "use the default value from the config or prompt for interactive selection."
26
+ ),
27
+ )
20
28
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)", type=click.STRING)
21
29
  @click.option(
22
30
  "--option",
@@ -25,33 +33,22 @@ from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH
25
33
  multiple=True,
26
34
  type=click.STRING,
27
35
  )
28
- def ssh_studio(
29
- studio_name: Optional[str] = None, teamspace: Optional[str] = None, option: Optional[List[str]] = None
30
- ) -> None:
36
+ def ssh_studio(name: Optional[str] = None, teamspace: Optional[str] = None, option: Optional[List[str]] = None) -> None:
31
37
  """SSH into a Studio.
32
38
 
33
39
  Example:
34
- lightning studio ssh [STUDIO_NAME]
35
-
36
- STUDIO_NAME: the name of the studio to SSH into.
37
-
38
- If STUDIO_NAME is not provided, will try to infer from environment or use the default value from the config.
40
+ lightning studio ssh --name my-studio
39
41
  """
40
42
  auth = Auth()
41
43
  auth.authenticate()
42
44
  ssh_private_key_path = _download_ssh_keys(auth.api_key, force_download=False)
43
45
 
44
- if teamspace is not None:
45
- resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
46
- if resolved_teamspace is None:
47
- raise ValueError(
48
- f"Could not resolve teamspace: '{teamspace}'. Teamspace should be specified as 'owner/name'. "
49
- "Does the teamspace exist?"
50
- )
51
- else:
52
- resolved_teamspace = None
53
-
54
- studio = Studio(studio_name, teamspace=resolved_teamspace)
46
+ menu = TeamspacesMenu()
47
+ resolved_teamspace = menu(teamspace=teamspace)
48
+
49
+ menu = StudiosMenu(resolved_teamspace)
50
+ studio = menu(studio=name)
51
+ save_studio_to_config(studio)
55
52
 
56
53
  ssh_options = " -o " + " -o ".join(option) if option else ""
57
54
  ssh_command = f"ssh -i {ssh_private_key_path}{ssh_options} s_{studio._studio.id}@ssh.lightning.ai"
@@ -4,14 +4,22 @@ from typing import Optional
4
4
 
5
5
  import click
6
6
 
7
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
8
- from lightning_sdk.lightning_cloud.openapi.rest import ApiException
7
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
8
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
9
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
10
  from lightning_sdk.machine import CloudProvider, Machine
10
11
  from lightning_sdk.studio import Studio
11
12
 
12
13
 
13
14
  @click.command("start")
14
- @click.argument("studio_name", required=False)
15
+ @click.option(
16
+ "--name",
17
+ help=(
18
+ "The name of the studio to start. "
19
+ "If not provided, will try to infer from environment, "
20
+ "use the default value from the config or prompt for interactive selection."
21
+ ),
22
+ )
15
23
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
16
24
  @click.option("--create", is_flag=True, help="Create the studio if it doesn't exist")
17
25
  @click.option(
@@ -34,7 +42,7 @@ from lightning_sdk.studio import Studio
34
42
  type=click.STRING,
35
43
  )
36
44
  def start_studio(
37
- studio_name: Optional[str] = None,
45
+ name: Optional[str] = None,
38
46
  teamspace: Optional[str] = None,
39
47
  create: bool = False,
40
48
  machine: Optional[str] = None,
@@ -45,37 +53,29 @@ def start_studio(
45
53
  """Start a Studio.
46
54
 
47
55
  Example:
48
- lightning studio start [STUDIO_NAME]
56
+ lightning studio start --name my-studio
49
57
 
50
- STUDIO_NAME: the name of the studio to start.
51
-
52
- If STUDIO_NAME is not provided, will try to infer from environment or use the default value from the config.
53
58
  """
54
- if teamspace is not None:
55
- resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
56
- if resolved_teamspace is None:
57
- raise ValueError(
58
- f"Could not resolve teamspace: '{teamspace}'. Teamspace should be specified as 'owner/name'. "
59
- "Does the teamspace exist?"
60
- )
61
- else:
62
- resolved_teamspace = None
59
+ menu = TeamspacesMenu()
60
+ resolved_teamspace = menu(teamspace=teamspace)
63
61
 
64
62
  if cloud_provider is not None:
65
63
  cloud_provider = CloudProvider(cloud_provider)
66
64
 
67
- try:
65
+ if not create:
66
+ menu = StudiosMenu(resolved_teamspace)
67
+ studio = menu(studio=name)
68
+ else:
68
69
  studio = Studio(
69
- studio_name,
70
+ name=name,
70
71
  teamspace=resolved_teamspace,
71
72
  create_ok=create,
72
73
  cloud_provider=cloud_provider,
73
74
  cloud_account=cloud_account,
74
75
  )
75
- except (RuntimeError, ValueError, ApiException):
76
- if studio_name:
77
- raise ValueError(f"Could not start Studio: '{studio_name}'. Does the Studio exist?") from None
78
- raise ValueError(f"Could not start Studio: '{studio_name}'. Please provide a Studio name") from None
79
76
 
77
+ save_studio_to_config(studio)
78
+
79
+ Studio.show_progress = True
80
80
  studio.start(machine, interruptible=interruptible)
81
81
  click.echo(f"Studio '{studio.name}' started successfully")
@@ -4,41 +4,37 @@ from typing import Optional
4
4
 
5
5
  import click
6
6
 
7
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
8
- from lightning_sdk.studio import Studio
7
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
8
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
9
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
10
 
10
11
 
11
12
  @click.command("stop")
12
- @click.argument("studio_name", required=False)
13
+ @click.option(
14
+ "--name",
15
+ help=(
16
+ "The name of the studio to start. "
17
+ "If not provided, will try to infer from environment, "
18
+ "use the default value from the config or prompt for interactive selection."
19
+ ),
20
+ )
13
21
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
14
- def stop_studio(studio_name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
22
+ def stop_studio(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
15
23
  """Stop a Studio.
16
24
 
17
25
  Example:
18
- lightning studio stop [STUDIO_NAME]
26
+ lightning studio stop --name my-studio
19
27
 
20
- STUDIO_NAME: the name of the studio to stop.
21
-
22
- If STUDIO_NAME is not provided, will try to infer from environment or use the default value from the config.
23
28
  """
24
29
  # missing studio_name and teamspace are handled by the studio class
25
- if teamspace is not None:
26
- resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
27
- if resolved_teamspace is None:
28
- raise ValueError(
29
- f"Could not resolve teamspace: '{teamspace}'. Teamspace should be specified as 'owner/name'. "
30
- "Does the teamspace exist?"
31
- )
32
- else:
33
- resolved_teamspace = None
34
-
35
- try:
36
- studio = Studio(studio_name, teamspace=resolved_teamspace)
37
- studio.stop()
38
- except Exception:
39
- # TODO: make this a generic CLI error
40
- if studio_name:
41
- raise ValueError(f"Could not stop studio: '{studio_name}'. Does the studio exist?") from None
42
- raise ValueError("No studio name provided. Use 'lightning studio stop <name>' to stop a studio.") from None
30
+ menu = TeamspacesMenu()
31
+ resolved_teamspace = menu(teamspace=teamspace)
32
+
33
+ menu = StudiosMenu(resolved_teamspace)
34
+ studio = menu(studio=name)
35
+
36
+ studio.stop()
37
+
38
+ save_studio_to_config(studio)
43
39
 
44
40
  click.echo(f"Studio '{studio.name}' stopped successfully")
@@ -4,14 +4,22 @@ from typing import Optional
4
4
 
5
5
  import click
6
6
 
7
- from lightning_sdk.cli.utils.resolve import resolve_teamspace_owner_name_format
8
- from lightning_sdk.lightning_cloud.openapi.rest import ApiException
7
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
8
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
9
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
10
  from lightning_sdk.machine import Machine
10
11
  from lightning_sdk.studio import Studio
11
12
 
12
13
 
13
14
  @click.command("switch")
14
- @click.argument("studio_name", required=False)
15
+ @click.option(
16
+ "--name",
17
+ help=(
18
+ "The name of the studio to start. "
19
+ "If not provided, will try to infer from environment, "
20
+ "use the default value from the config or prompt for interactive selection."
21
+ ),
22
+ )
15
23
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
16
24
  @click.option(
17
25
  "--machine",
@@ -20,33 +28,22 @@ from lightning_sdk.studio import Studio
20
28
  )
21
29
  @click.option("--interruptible", is_flag=True, help="Switch the studio to an interruptible instance.")
22
30
  def switch_studio(
23
- studio_name: Optional[str] = None,
31
+ name: Optional[str] = None,
24
32
  teamspace: Optional[str] = None,
25
33
  machine: Optional[str] = None,
26
34
  interruptible: bool = False,
27
35
  ) -> None:
28
36
  """Switch a Studio to a different machine type."""
29
- if teamspace is not None:
30
- resolved_teamspace = resolve_teamspace_owner_name_format(teamspace)
31
- if resolved_teamspace is None:
32
- raise ValueError(
33
- f"Could not resolve teamspace: '{teamspace}'. Teamspace should be specified as 'owner/name'. "
34
- "Does the teamspace exist?"
35
- )
36
- else:
37
- resolved_teamspace = None
38
-
39
- try:
40
- studio = Studio(
41
- studio_name,
42
- teamspace=resolved_teamspace,
43
- )
44
- except (RuntimeError, ValueError, ApiException):
45
- if studio_name:
46
- raise ValueError(f"Could not switch Studio: '{studio_name}'. Does the Studio exist?") from None
47
- raise ValueError(f"Could not switch Studio: '{studio_name}'. Please provide a Studio name") from None
37
+ menu = TeamspacesMenu()
38
+ resolved_teamspace = menu(teamspace=teamspace)
39
+
40
+ menu = StudiosMenu(resolved_teamspace)
41
+ studio = menu(studio=name)
48
42
 
49
43
  resolved_machine = Machine.from_str(machine)
44
+ Studio.show_progress = True
50
45
  studio.switch_machine(resolved_machine, interruptible=interruptible)
51
46
 
47
+ save_studio_to_config(studio)
48
+
52
49
  click.echo(f"Studio '{studio.name}' switched to machine '{resolved_machine}' successfully")
@@ -4,7 +4,7 @@ from lightning_sdk.teamspace import Teamspace
4
4
  from lightning_sdk.utils.resolve import _resolve_teamspace
5
5
 
6
6
 
7
- def resolve_teamspace_owner_name_format(teamspace_name: str) -> Optional[Teamspace]:
7
+ def resolve_teamspace_owner_name_format(teamspace_name: Optional[str]) -> Optional[Teamspace]:
8
8
  teamspace_resolved = None
9
9
  if teamspace_name is None:
10
10
  return _resolve_teamspace(None, None, None)
@@ -0,0 +1,27 @@
1
+ from lightning_sdk.organization import Organization
2
+ from lightning_sdk.studio import Studio
3
+ from lightning_sdk.teamspace import Teamspace
4
+ from lightning_sdk.utils.config import Config, DefaultConfigKeys
5
+
6
+
7
+ def save_teamspace_to_config(teamspace: Teamspace, overwrite: bool = False) -> None:
8
+ saved = _save_to_config_if_not_exists(DefaultConfigKeys.teamspace_name, teamspace.name, overwrite)
9
+ saved = _save_to_config_if_not_exists(DefaultConfigKeys.teamspace_owner, teamspace.owner.name, overwrite=saved)
10
+ saved = _save_to_config_if_not_exists(
11
+ DefaultConfigKeys.teamspace_owner_type,
12
+ "organization" if isinstance(teamspace.owner, Organization) else "user",
13
+ overwrite=saved,
14
+ )
15
+
16
+
17
+ def save_studio_to_config(studio: Studio, overwrite: bool = False) -> None:
18
+ saved = _save_to_config_if_not_exists(DefaultConfigKeys.studio, studio.name, overwrite)
19
+ save_teamspace_to_config(studio.teamspace, overwrite=saved)
20
+
21
+
22
+ def _save_to_config_if_not_exists(key: str, value: str, overwrite: bool = False) -> bool:
23
+ cfg = Config()
24
+ if overwrite or cfg.get(key) is None:
25
+ cfg.set(key, value)
26
+ return True
27
+ return False