lightning-sdk 2025.8.21__py3-none-any.whl → 2025.8.28__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 (54) hide show
  1. lightning_sdk/__init__.py +1 -1
  2. lightning_sdk/api/studio_api.py +69 -2
  3. lightning_sdk/api/teamspace_api.py +60 -30
  4. lightning_sdk/api/user_api.py +49 -1
  5. lightning_sdk/api/utils.py +1 -1
  6. lightning_sdk/cli/config/set.py +6 -18
  7. lightning_sdk/cli/legacy/create.py +3 -3
  8. lightning_sdk/cli/legacy/delete.py +3 -3
  9. lightning_sdk/cli/legacy/deploy/_auth.py +4 -4
  10. lightning_sdk/cli/legacy/download.py +7 -7
  11. lightning_sdk/cli/legacy/job_and_mmt_action.py +4 -4
  12. lightning_sdk/cli/legacy/list.py +9 -9
  13. lightning_sdk/cli/legacy/open.py +3 -3
  14. lightning_sdk/cli/legacy/upload.py +3 -3
  15. lightning_sdk/cli/studio/create.py +16 -24
  16. lightning_sdk/cli/studio/delete.py +28 -27
  17. lightning_sdk/cli/studio/list.py +29 -15
  18. lightning_sdk/cli/studio/ssh.py +19 -22
  19. lightning_sdk/cli/studio/start.py +25 -25
  20. lightning_sdk/cli/studio/stop.py +25 -28
  21. lightning_sdk/cli/studio/switch.py +21 -24
  22. lightning_sdk/cli/utils/resolve.py +1 -1
  23. lightning_sdk/cli/utils/richt_print.py +24 -0
  24. lightning_sdk/cli/utils/save_to_config.py +27 -0
  25. lightning_sdk/cli/utils/studio_selection.py +106 -0
  26. lightning_sdk/cli/utils/teamspace_selection.py +125 -0
  27. lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
  28. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
  29. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
  30. lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
  31. lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
  32. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
  33. lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
  34. lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
  35. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
  36. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
  37. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
  38. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +145 -41
  39. lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
  40. lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
  41. lightning_sdk/llm/public_assistants.py +4 -0
  42. lightning_sdk/studio.py +54 -22
  43. lightning_sdk/teamspace.py +25 -2
  44. lightning_sdk/user.py +19 -1
  45. lightning_sdk/utils/config.py +6 -0
  46. lightning_sdk/utils/names.py +1179 -0
  47. lightning_sdk/utils/progress.py +2 -2
  48. lightning_sdk/utils/resolve.py +17 -6
  49. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/METADATA +1 -1
  50. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/RECORD +54 -48
  51. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/LICENSE +0 -0
  52. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/WHEEL +0 -0
  53. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/entry_points.txt +0 -0
  54. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.28.dist-info}/top_level.txt +0 -0
@@ -4,14 +4,16 @@ 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.richt_print import studio_name_link
8
+ from lightning_sdk.cli.utils.save_to_config import save_teamspace_to_config
9
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
8
10
  from lightning_sdk.lightning_cloud.openapi.rest import ApiException
9
11
  from lightning_sdk.machine import CloudProvider
10
12
  from lightning_sdk.studio import Studio
11
13
 
12
14
 
13
15
  @click.command("create")
14
- @click.argument("studio_name", required=False)
16
+ @click.option("--name", help="The name of the studio to create. If not provided, a random name will be generated.")
15
17
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
16
18
  @click.option(
17
19
  "--cloud-provider",
@@ -24,7 +26,7 @@ from lightning_sdk.studio import Studio
24
26
  type=click.STRING,
25
27
  )
26
28
  def create_studio(
27
- studio_name: Optional[str] = None,
29
+ name: Optional[str] = None,
28
30
  teamspace: Optional[str] = None,
29
31
  cloud_provider: Optional[str] = None,
30
32
  cloud_account: Optional[str] = None,
@@ -32,37 +34,27 @@ def create_studio(
32
34
  """Create a new Studio.
33
35
 
34
36
  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.
37
+ lightning studio create
40
38
  """
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
39
+ menu = TeamspacesMenu()
40
+
41
+ resolved_teamspace = menu(teamspace)
42
+ save_teamspace_to_config(resolved_teamspace, overwrite=False)
50
43
 
51
44
  if cloud_provider is not None:
52
45
  cloud_provider = CloudProvider(cloud_provider)
53
46
 
54
47
  try:
55
48
  studio = Studio(
56
- studio_name,
49
+ name=name,
57
50
  teamspace=resolved_teamspace,
58
51
  create_ok=True,
59
52
  cloud_provider=cloud_provider,
60
53
  cloud_account=cloud_account,
61
54
  )
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
55
+ except (RuntimeError, ValueError, ApiException):
56
+ if name:
57
+ raise ValueError(f"Could not create Studio: '{name}'. Does the Studio exist?") from None
58
+ raise ValueError(f"Could not create Studio: '{name}'. Please provide a Studio name") from None
67
59
 
68
- click.echo(f"Studio '{studio.name}' created successfully")
60
+ click.echo(f"Studio {studio_name_link(studio)} 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,31 +6,40 @@ 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
- from lightning_sdk.cli.utils.richt_print import rich_to_str
9
+ from lightning_sdk.cli.utils.richt_print import rich_to_str, studio_name_link
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
13
+ from lightning_sdk.utils.resolve import _get_authed_user, prevent_refetch_studio
12
14
 
13
15
 
14
16
  @click.command("list")
15
17
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
18
+ @click.option(
19
+ "--all",
20
+ is_flag=True,
21
+ flag_value=True,
22
+ default=False,
23
+ help="List all studios, not just the ones belonging to the authed user",
24
+ )
16
25
  @click.option(
17
26
  "--sort-by",
18
27
  default=None,
19
28
  type=click.Choice(["name", "teamspace", "status", "machine", "cloud-account"], case_sensitive=False),
20
29
  help="the attribute to sort the studios by.",
21
30
  )
22
- def list_studios(teamspace: Optional[str] = None, sort_by: Optional[str] = None) -> None:
31
+ def list_studios(teamspace: Optional[str] = None, all: bool = False, sort_by: Optional[str] = None) -> None: # noqa: A002
23
32
  """List Studios in a teamspace.
24
33
 
25
34
  Example:
26
35
  lightning studio list --teamspace owner/teamspace
27
36
 
28
37
  """
29
- teamspace_resolved = resolve_teamspace_owner_name_format(teamspace)
38
+ menu = TeamspacesMenu()
39
+ teamspace_resolved = menu(teamspace=teamspace)
40
+ save_teamspace_to_config(teamspace_resolved, overwrite=False)
30
41
 
31
- if teamspace_resolved is None:
32
- # TODO: make this a generic CLI error
33
- raise ValueError(f"Could not resolve teamspace: {teamspace}")
42
+ user = _get_authed_user()
34
43
 
35
44
  studios = teamspace_resolved.studios
36
45
 
@@ -43,14 +52,19 @@ def list_studios(teamspace: Optional[str] = None, sort_by: Optional[str] = None)
43
52
  table.add_column("Machine")
44
53
  table.add_column("Cloud account")
45
54
 
46
- for studio in sorted(studios, key=_sort_studios_key(sort_by)):
47
- table.add_row(
48
- studio.name,
49
- f"{studio.teamspace.owner.name}/{studio.teamspace.name}",
50
- str(studio.status),
51
- str(studio.machine) if studio.machine is not None else None, # when None the cell is empty
52
- str(cloud_account_to_display_name(studio.cloud_account, studio.teamspace.id)),
53
- )
55
+ for studio in sorted(
56
+ filter(lambda s: all or s._studio.user_id == user.id, studios), key=_sort_studios_key(sort_by)
57
+ ):
58
+ with prevent_refetch_studio(studio):
59
+ table.add_row(
60
+ # cannot convert to ascii here, as the final rich table has to be converted to ascii
61
+ # otherwise the lack of support for linking in some terminals causes formatting issues.
62
+ studio_name_link(studio, to_ascii=False),
63
+ f"{studio.teamspace.owner.name}/{studio.teamspace.name}",
64
+ str(studio.status),
65
+ str(studio.machine) if studio.machine is not None else None, # when None the cell is empty
66
+ str(cloud_account_to_display_name(studio.cloud_account, studio.teamspace.id)),
67
+ )
54
68
 
55
69
  click.echo(rich_to_str(table), color=True)
56
70
 
@@ -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,23 @@ 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.richt_print import studio_name_link
8
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
9
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
10
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
11
  from lightning_sdk.machine import CloudProvider, Machine
10
12
  from lightning_sdk.studio import Studio
11
13
 
12
14
 
13
15
  @click.command("start")
14
- @click.argument("studio_name", required=False)
16
+ @click.option(
17
+ "--name",
18
+ help=(
19
+ "The name of the studio to start. "
20
+ "If not provided, will try to infer from environment, "
21
+ "use the default value from the config or prompt for interactive selection."
22
+ ),
23
+ )
15
24
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
16
25
  @click.option("--create", is_flag=True, help="Create the studio if it doesn't exist")
17
26
  @click.option(
@@ -34,10 +43,10 @@ from lightning_sdk.studio import Studio
34
43
  type=click.STRING,
35
44
  )
36
45
  def start_studio(
37
- studio_name: Optional[str] = None,
46
+ name: Optional[str] = None,
38
47
  teamspace: Optional[str] = None,
39
48
  create: bool = False,
40
- machine: Optional[str] = None,
49
+ machine: str = "CPU",
41
50
  interruptible: bool = False,
42
51
  cloud_provider: Optional[str] = None,
43
52
  cloud_account: Optional[str] = None,
@@ -45,38 +54,29 @@ def start_studio(
45
54
  """Start a Studio.
46
55
 
47
56
  Example:
48
- lightning studio start [STUDIO_NAME]
57
+ lightning studio start --name my-studio
49
58
 
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
59
  """
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
60
+ menu = TeamspacesMenu()
61
+ resolved_teamspace = menu(teamspace=teamspace)
63
62
 
64
63
  if cloud_provider is not None:
65
64
  cloud_provider = CloudProvider(cloud_provider)
66
65
 
67
- try:
66
+ if not create:
67
+ menu = StudiosMenu(resolved_teamspace)
68
+ studio = menu(studio=name)
69
+ else:
68
70
  studio = Studio(
69
- studio_name,
71
+ name=name,
70
72
  teamspace=resolved_teamspace,
71
73
  create_ok=create,
72
74
  cloud_provider=cloud_provider,
73
75
  cloud_account=cloud_account,
74
76
  )
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
77
+
78
+ save_studio_to_config(studio)
79
79
 
80
80
  Studio.show_progress = True
81
81
  studio.start(machine, interruptible=interruptible)
82
- click.echo(f"Studio '{studio.name}' started successfully")
82
+ click.echo(f"Studio {studio_name_link(studio)} started successfully")
@@ -4,41 +4,38 @@ 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.richt_print import studio_name_link
8
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
9
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
10
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
11
 
10
12
 
11
13
  @click.command("stop")
12
- @click.argument("studio_name", required=False)
14
+ @click.option(
15
+ "--name",
16
+ help=(
17
+ "The name of the studio to start. "
18
+ "If not provided, will try to infer from environment, "
19
+ "use the default value from the config or prompt for interactive selection."
20
+ ),
21
+ )
13
22
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
14
- def stop_studio(studio_name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
23
+ def stop_studio(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
15
24
  """Stop a Studio.
16
25
 
17
26
  Example:
18
- lightning studio stop [STUDIO_NAME]
27
+ lightning studio stop --name my-studio
19
28
 
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
29
  """
24
30
  # 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
43
-
44
- click.echo(f"Studio '{studio.name}' stopped successfully")
31
+ menu = TeamspacesMenu()
32
+ resolved_teamspace = menu(teamspace=teamspace)
33
+
34
+ menu = StudiosMenu(resolved_teamspace)
35
+ studio = menu(studio=name)
36
+
37
+ studio.stop()
38
+
39
+ save_studio_to_config(studio)
40
+
41
+ click.echo(f"Studio {studio_name_link(studio)} stopped successfully")
@@ -4,14 +4,23 @@ 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.richt_print import studio_name_link
8
+ from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
9
+ from lightning_sdk.cli.utils.studio_selection import StudiosMenu
10
+ from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
9
11
  from lightning_sdk.machine import Machine
10
12
  from lightning_sdk.studio import Studio
11
13
 
12
14
 
13
15
  @click.command("switch")
14
- @click.argument("studio_name", required=False)
16
+ @click.option(
17
+ "--name",
18
+ help=(
19
+ "The name of the studio to start. "
20
+ "If not provided, will try to infer from environment, "
21
+ "use the default value from the config or prompt for interactive selection."
22
+ ),
23
+ )
15
24
  @click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
16
25
  @click.option(
17
26
  "--machine",
@@ -20,34 +29,22 @@ from lightning_sdk.studio import Studio
20
29
  )
21
30
  @click.option("--interruptible", is_flag=True, help="Switch the studio to an interruptible instance.")
22
31
  def switch_studio(
23
- studio_name: Optional[str] = None,
32
+ name: Optional[str] = None,
24
33
  teamspace: Optional[str] = None,
25
34
  machine: Optional[str] = None,
26
35
  interruptible: bool = False,
27
36
  ) -> None:
28
37
  """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
38
+ menu = TeamspacesMenu()
39
+ resolved_teamspace = menu(teamspace=teamspace)
40
+
41
+ menu = StudiosMenu(resolved_teamspace)
42
+ studio = menu(studio=name)
48
43
 
49
44
  resolved_machine = Machine.from_str(machine)
50
45
  Studio.show_progress = True
51
46
  studio.switch_machine(resolved_machine, interruptible=interruptible)
52
47
 
53
- click.echo(f"Studio '{studio.name}' switched to machine '{resolved_machine}' successfully")
48
+ save_studio_to_config(studio)
49
+
50
+ click.echo(f"Studio {studio_name_link(studio)} 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)
@@ -3,9 +3,33 @@ from typing import Any
3
3
 
4
4
  from rich.console import Console
5
5
 
6
+ from lightning_sdk.studio import Studio
7
+ from lightning_sdk.utils.resolve import _get_studio_url
8
+
6
9
 
7
10
  def rich_to_str(*renderables: Any) -> str:
8
11
  with open(os.devnull, "w") as f:
9
12
  console = Console(file=f, record=True)
10
13
  console.print(*renderables)
11
14
  return console.export_text(styles=True)
15
+
16
+
17
+ # not supported on all terminals (e.g. older ones). in that case it's a name without a link
18
+ # see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#hyperlinks-aka-html-like-anchors-in-terminal-emulators
19
+ # for details and
20
+ # https://github.com/Alhadis/OSC8-Adoption/blob/main/README.md for status of OSC8 adoption
21
+ def studio_name_link(studio: Studio, to_ascii: bool = True) -> str:
22
+ """Hyperlink a studio name.
23
+
24
+ Args:
25
+ studio: the studio whose name to print and link to the studio url
26
+ to_ascii: whether return a plain ascii string with characters for linking converted to ascii as well.
27
+ if False, returns the rich markup directly.
28
+ """
29
+ url = _get_studio_url(studio)
30
+
31
+ studio_link_markup = f"[link={url}]{studio.name}[/link]"
32
+ if not to_ascii:
33
+ return studio_link_markup
34
+
35
+ return rich_to_str(studio_link_markup).strip("\n")
@@ -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