lightning-sdk 2025.8.21__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 (53) 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 +14 -23
  16. lightning_sdk/cli/studio/delete.py +28 -27
  17. lightning_sdk/cli/studio/list.py +5 -6
  18. lightning_sdk/cli/studio/ssh.py +19 -22
  19. lightning_sdk/cli/studio/start.py +22 -23
  20. lightning_sdk/cli/studio/stop.py +22 -26
  21. lightning_sdk/cli/studio/switch.py +19 -23
  22. lightning_sdk/cli/utils/resolve.py +1 -1
  23. lightning_sdk/cli/utils/save_to_config.py +27 -0
  24. lightning_sdk/cli/utils/studio_selection.py +106 -0
  25. lightning_sdk/cli/utils/teamspace_selection.py +125 -0
  26. lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
  27. lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
  28. lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
  29. lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
  30. lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
  31. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
  32. lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
  33. lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
  34. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
  35. lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
  36. lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
  37. lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +145 -41
  38. lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
  39. lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
  40. lightning_sdk/llm/public_assistants.py +4 -0
  41. lightning_sdk/studio.py +53 -22
  42. lightning_sdk/teamspace.py +25 -2
  43. lightning_sdk/user.py +19 -1
  44. lightning_sdk/utils/config.py +6 -0
  45. lightning_sdk/utils/names.py +1179 -0
  46. lightning_sdk/utils/progress.py +2 -2
  47. lightning_sdk/utils/resolve.py +6 -6
  48. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/METADATA +1 -1
  49. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/RECORD +53 -47
  50. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/LICENSE +0 -0
  51. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/WHEEL +0 -0
  52. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/entry_points.txt +0 -0
  53. {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/top_level.txt +0 -0
@@ -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,28 @@ 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
76
+
77
+ save_studio_to_config(studio)
79
78
 
80
79
  Studio.show_progress = True
81
80
  studio.start(machine, interruptible=interruptible)
@@ -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,34 +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)
50
44
  Studio.show_progress = True
51
45
  studio.switch_machine(resolved_machine, interruptible=interruptible)
52
46
 
47
+ save_studio_to_config(studio)
48
+
53
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
@@ -0,0 +1,106 @@
1
+ import os
2
+ from contextlib import suppress
3
+ from typing import Dict, List, Optional
4
+
5
+ import click
6
+ from simple_term_menu import TerminalMenu
7
+
8
+ from lightning_sdk.cli.legacy.exceptions import StudioCliError
9
+ from lightning_sdk.studio import Studio
10
+ from lightning_sdk.teamspace import Teamspace
11
+ from lightning_sdk.utils.resolve import _get_authed_user
12
+
13
+
14
+ class StudiosMenu:
15
+ """This class is used to select a studio from a list of possible studios within a teamspace.
16
+
17
+ It can be used to select a studio from a list of possible studios, or to resolve a studio from a name.
18
+ """
19
+
20
+ def __init__(self, teamspace: Teamspace) -> None:
21
+ """Initialize the StudiosMenu with a teamspace.
22
+
23
+ Args:
24
+ teamspace: The teamspace to list studios from
25
+ """
26
+ self.teamspace = teamspace
27
+
28
+ def _get_studio_from_interactive_menu(self, possible_studios: Dict[str, Studio]) -> Studio:
29
+ studio_names = sorted(possible_studios.keys())
30
+ terminal_menu = self._prepare_terminal_menu_studios(studio_names)
31
+ terminal_menu.show()
32
+
33
+ selected_name = studio_names[terminal_menu.chosen_menu_index]
34
+ return possible_studios[selected_name]
35
+
36
+ def _get_studio_from_name(self, studio: str, possible_studios: Dict[str, Studio]) -> Studio:
37
+ if studio in possible_studios:
38
+ return possible_studios[studio]
39
+
40
+ click.echo(f"Could not find Studio {studio}, please select it from the list:")
41
+ return self._get_studio_from_interactive_menu(possible_studios)
42
+
43
+ @staticmethod
44
+ def _prepare_terminal_menu_studios(studio_names: List[str], title: Optional[str] = None) -> TerminalMenu:
45
+ if title is None:
46
+ title = "Please select a Studio out of the following:"
47
+
48
+ return TerminalMenu(studio_names, title=title, clear_menu_on_exit=True)
49
+
50
+ def _get_possible_studios(self) -> Dict[str, Studio]:
51
+ """Get all available studios in the teamspace."""
52
+ studios = {}
53
+
54
+ user = _get_authed_user()
55
+ for studio in self.teamspace.studios:
56
+ if studio._studio.user_id == user.id:
57
+ studios[studio.name] = studio
58
+ return studios
59
+
60
+ def __call__(self, studio: Optional[str] = None) -> Studio:
61
+ """Select a studio from the teamspace.
62
+
63
+ Args:
64
+ studio: Optional studio name to select. If not provided, will show interactive menu.
65
+
66
+ Returns:
67
+ Selected Studio object
68
+
69
+ Raises:
70
+ StudioCliError: If studio selection fails
71
+ """
72
+ try:
73
+ # try to resolve the studio from the name, environment or config
74
+ resolved_studio = None
75
+
76
+ with suppress(Exception):
77
+ resolved_studio = Studio(name=studio, teamspace=self.teamspace, create_ok=False)
78
+
79
+ if resolved_studio is not None:
80
+ return resolved_studio
81
+
82
+ if os.environ.get("LIGHTNING_NON_INTERACTIVE", "0") == "1" and studio is None:
83
+ raise ValueError(
84
+ "Studio selection is not supported in non-interactive mode. Please provide a studio name."
85
+ )
86
+
87
+ click.echo(f"Listing studios in teamspace {self.teamspace.owner.name}/{self.teamspace.name}...")
88
+
89
+ possible_studios = self._get_possible_studios()
90
+
91
+ if not possible_studios:
92
+ raise ValueError(f"No studios found in teamspace {self.teamspace.name}")
93
+
94
+ if studio is None:
95
+ return self._get_studio_from_interactive_menu(possible_studios)
96
+
97
+ return self._get_studio_from_name(studio, possible_studios)
98
+
99
+ except KeyboardInterrupt:
100
+ raise KeyboardInterrupt from None
101
+
102
+ except Exception as e:
103
+ raise StudioCliError(
104
+ "Could not resolve a Studio. "
105
+ "Please pass it as an argument or contact Lightning AI directly to resolve this issue."
106
+ ) from e