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.
- lightning_sdk/__init__.py +1 -1
- lightning_sdk/api/studio_api.py +69 -2
- lightning_sdk/api/teamspace_api.py +60 -30
- lightning_sdk/api/user_api.py +49 -1
- lightning_sdk/api/utils.py +1 -1
- lightning_sdk/cli/config/set.py +6 -18
- lightning_sdk/cli/legacy/create.py +3 -3
- lightning_sdk/cli/legacy/delete.py +3 -3
- lightning_sdk/cli/legacy/deploy/_auth.py +4 -4
- lightning_sdk/cli/legacy/download.py +7 -7
- lightning_sdk/cli/legacy/job_and_mmt_action.py +4 -4
- lightning_sdk/cli/legacy/list.py +9 -9
- lightning_sdk/cli/legacy/open.py +3 -3
- lightning_sdk/cli/legacy/upload.py +3 -3
- lightning_sdk/cli/studio/create.py +14 -23
- lightning_sdk/cli/studio/delete.py +28 -27
- lightning_sdk/cli/studio/list.py +5 -6
- lightning_sdk/cli/studio/ssh.py +19 -22
- lightning_sdk/cli/studio/start.py +22 -23
- lightning_sdk/cli/studio/stop.py +22 -26
- lightning_sdk/cli/studio/switch.py +19 -23
- lightning_sdk/cli/utils/resolve.py +1 -1
- lightning_sdk/cli/utils/save_to_config.py +27 -0
- lightning_sdk/cli/utils/studio_selection.py +106 -0
- lightning_sdk/cli/utils/teamspace_selection.py +125 -0
- lightning_sdk/lightning_cloud/openapi/__init__.py +2 -0
- lightning_sdk/lightning_cloud/openapi/api/billing_service_api.py +85 -0
- lightning_sdk/lightning_cloud/openapi/api/k8_s_cluster_service_api.py +101 -0
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +2 -0
- lightning_sdk/lightning_cloud/openapi/models/externalv1_user_status.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_metrics.py +270 -36
- lightning_sdk/lightning_cloud/openapi/models/v1_container_metrics.py +21 -21
- lightning_sdk/lightning_cloud/openapi/models/v1_list_cluster_metric_timestamps_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_namespace_metrics.py +11 -11
- lightning_sdk/lightning_cloud/openapi/models/v1_namespace_user_metrics.py +16 -16
- lightning_sdk/lightning_cloud/openapi/models/v1_node_metrics.py +156 -26
- lightning_sdk/lightning_cloud/openapi/models/v1_pod_metrics.py +145 -41
- lightning_sdk/lightning_cloud/openapi/models/v1_purchase_annual_upsell_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_storage_asset.py +107 -3
- lightning_sdk/llm/public_assistants.py +4 -0
- lightning_sdk/studio.py +53 -22
- lightning_sdk/teamspace.py +25 -2
- lightning_sdk/user.py +19 -1
- lightning_sdk/utils/config.py +6 -0
- lightning_sdk/utils/names.py +1179 -0
- lightning_sdk/utils/progress.py +2 -2
- lightning_sdk/utils/resolve.py +6 -6
- {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/RECORD +53 -47
- {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.8.21.dist-info → lightning_sdk-2025.8.26.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
8
|
-
from lightning_sdk.
|
|
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.
|
|
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(
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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")
|
lightning_sdk/cli/studio/list.py
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
lightning_sdk/cli/studio/ssh.py
CHANGED
|
@@ -9,14 +9,22 @@ from typing import List, Optional
|
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
|
|
12
|
-
from lightning_sdk.cli.utils.
|
|
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.
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
8
|
-
from lightning_sdk.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
65
|
+
if not create:
|
|
66
|
+
menu = StudiosMenu(resolved_teamspace)
|
|
67
|
+
studio = menu(studio=name)
|
|
68
|
+
else:
|
|
68
69
|
studio = Studio(
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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)
|
lightning_sdk/cli/studio/stop.py
CHANGED
|
@@ -4,41 +4,37 @@ from typing import Optional
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
-
from lightning_sdk.cli.utils.
|
|
8
|
-
from lightning_sdk.
|
|
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.
|
|
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(
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
8
|
-
from lightning_sdk.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|