lightning-sdk 2025.9.29__py3-none-any.whl → 2025.10.8__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 +3 -2
- lightning_sdk/base_studio.py +23 -12
- lightning_sdk/cli/base_studio/__init__.py +10 -0
- lightning_sdk/cli/base_studio/list.py +45 -0
- lightning_sdk/cli/entrypoint.py +2 -0
- lightning_sdk/cli/groups.py +7 -0
- lightning_sdk/cli/legacy/download.py +3 -4
- lightning_sdk/cli/legacy/upload.py +2 -3
- lightning_sdk/cli/studio/__init__.py +2 -0
- lightning_sdk/cli/studio/connect.py +189 -0
- lightning_sdk/cli/studio/ssh.py +3 -54
- lightning_sdk/cli/utils/ssh_connection.py +59 -0
- lightning_sdk/studio.py +34 -5
- lightning_sdk/utils/progress.py +32 -33
- {lightning_sdk-2025.9.29.dist-info → lightning_sdk-2025.10.8.dist-info}/METADATA +1 -1
- {lightning_sdk-2025.9.29.dist-info → lightning_sdk-2025.10.8.dist-info}/RECORD +20 -16
- {lightning_sdk-2025.9.29.dist-info → lightning_sdk-2025.10.8.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.9.29.dist-info → lightning_sdk-2025.10.8.dist-info}/WHEEL +0 -0
- {lightning_sdk-2025.9.29.dist-info → lightning_sdk-2025.10.8.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.9.29.dist-info → lightning_sdk-2025.10.8.dist-info}/top_level.txt +0 -0
lightning_sdk/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from lightning_sdk.mmt import MMT
|
|
|
9
9
|
from lightning_sdk.organization import Organization
|
|
10
10
|
from lightning_sdk.plugin import JobsPlugin, MultiMachineTrainingPlugin, Plugin, SlurmJobsPlugin
|
|
11
11
|
from lightning_sdk.status import Status
|
|
12
|
-
from lightning_sdk.studio import Studio
|
|
12
|
+
from lightning_sdk.studio import VM, Studio
|
|
13
13
|
from lightning_sdk.teamspace import ConnectionType, FolderLocation, Teamspace
|
|
14
14
|
from lightning_sdk.user import User
|
|
15
15
|
|
|
@@ -32,8 +32,9 @@ __all__ = [
|
|
|
32
32
|
"Studio",
|
|
33
33
|
"Teamspace",
|
|
34
34
|
"User",
|
|
35
|
+
"VM",
|
|
35
36
|
]
|
|
36
37
|
|
|
37
|
-
__version__ = "2025.
|
|
38
|
+
__version__ = "2025.10.08"
|
|
38
39
|
_check_version_and_prompt_upgrade(__version__)
|
|
39
40
|
_set_tqdm_envvars_noninteractive()
|
lightning_sdk/base_studio.py
CHANGED
|
@@ -16,6 +16,8 @@ class BaseStudioInfo:
|
|
|
16
16
|
name: str
|
|
17
17
|
managed_id: str
|
|
18
18
|
description: str
|
|
19
|
+
creator: str
|
|
20
|
+
enabled: bool
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class BaseStudio:
|
|
@@ -80,20 +82,29 @@ class BaseStudio:
|
|
|
80
82
|
disabled=disabled,
|
|
81
83
|
)
|
|
82
84
|
|
|
83
|
-
def list(self, managed: bool = True) -> List[BaseStudioInfo]:
|
|
85
|
+
def list(self, managed: bool = True, include_disabled: bool = False) -> List[BaseStudioInfo]:
|
|
84
86
|
"""List all base studios in the organization.
|
|
85
87
|
|
|
88
|
+
Args:
|
|
89
|
+
managed: Whether to filter for managed base studios.
|
|
90
|
+
include_disabled: Whether to include disabled base studios in the results.
|
|
91
|
+
|
|
86
92
|
Returns:
|
|
87
|
-
List[
|
|
93
|
+
List[BaseStudioInfo]: A list of base studio templates.
|
|
88
94
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
templates = self._base_studio_api.get_all_base_studios(self._org.id, managed).templates
|
|
96
|
+
|
|
97
|
+
return [
|
|
98
|
+
BaseStudioInfo(
|
|
99
|
+
id=template.id,
|
|
100
|
+
name=template.name,
|
|
101
|
+
managed_id=template.managed_id,
|
|
102
|
+
description=template.description,
|
|
103
|
+
creator="⚡ Lightning AI"
|
|
104
|
+
if template.managed_id
|
|
105
|
+
else UserApi()._get_user_by_id(template.user_id).username,
|
|
106
|
+
enabled=not template.disabled,
|
|
98
107
|
)
|
|
99
|
-
|
|
108
|
+
for template in templates
|
|
109
|
+
if include_disabled or not template.disabled
|
|
110
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Base Studio CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_commands(group: click.Group) -> None:
|
|
7
|
+
"""Register base studio commands with the given group."""
|
|
8
|
+
from lightning_sdk.cli.base_studio.list import list_base_studios
|
|
9
|
+
|
|
10
|
+
group.add_command(list_base_studios)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Base Studio list command."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from lightning_sdk.base_studio import BaseStudio
|
|
7
|
+
from lightning_sdk.cli.utils.richt_print import rich_to_str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("list")
|
|
11
|
+
@click.option("--include-disabled", help="Include disabled Base Studios in the list.", is_flag=True)
|
|
12
|
+
def list_base_studios(include_disabled: bool) -> None:
|
|
13
|
+
"""List Base Studios in an org.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
lightning base-studio list
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
return list_impl(include_disabled=include_disabled)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_impl(include_disabled: bool) -> None:
|
|
23
|
+
base_studio_cls = BaseStudio()
|
|
24
|
+
base_studios = base_studio_cls.list(include_disabled=include_disabled) + base_studio_cls.list(
|
|
25
|
+
managed=False, include_disabled=include_disabled
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
table = Table(
|
|
29
|
+
pad_edge=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
table.add_column("Name")
|
|
33
|
+
table.add_column("Description")
|
|
34
|
+
table.add_column("Creator")
|
|
35
|
+
table.add_column("Enabled")
|
|
36
|
+
|
|
37
|
+
for base_studio in base_studios:
|
|
38
|
+
table.add_row(
|
|
39
|
+
base_studio.name.lower().replace(" ", "-"),
|
|
40
|
+
base_studio.description or "",
|
|
41
|
+
base_studio.creator,
|
|
42
|
+
"Yes" if base_studio.enabled else "No",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
click.echo(rich_to_str(table), color=True)
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -17,6 +17,7 @@ from lightning_sdk.api.studio_api import _cloud_url
|
|
|
17
17
|
|
|
18
18
|
# Import legacy groups directly from groups.py
|
|
19
19
|
from lightning_sdk.cli.groups import (
|
|
20
|
+
base_studio,
|
|
20
21
|
config,
|
|
21
22
|
# job,
|
|
22
23
|
# mmt,
|
|
@@ -85,6 +86,7 @@ main_cli.add_command(config)
|
|
|
85
86
|
# main_cli.add_command(mmt)
|
|
86
87
|
main_cli.add_command(studio)
|
|
87
88
|
main_cli.add_command(vm)
|
|
89
|
+
main_cli.add_command(base_studio)
|
|
88
90
|
if os.environ.get("LIGHTNING_EXPERIMENTAL_CLI_ONLY", "0") != "1":
|
|
89
91
|
#### LEGACY COMMANDS ####
|
|
90
92
|
# these commands are currently supported for backwards compatibility, but will potentially be removed in the future.
|
lightning_sdk/cli/groups.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
|
+
from lightning_sdk.cli.base_studio import register_commands as register_base_studio_commands
|
|
5
6
|
from lightning_sdk.cli.config import register_commands as register_config_commands
|
|
6
7
|
from lightning_sdk.cli.job import register_commands as register_job_commands
|
|
7
8
|
from lightning_sdk.cli.mmt import register_commands as register_mmt_commands
|
|
@@ -34,9 +35,15 @@ def vm() -> None:
|
|
|
34
35
|
"""Manage Lightning AI VMs."""
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
@click.group(name="base-studio")
|
|
39
|
+
def base_studio() -> None:
|
|
40
|
+
"""Manage Lightning AI Base Studios."""
|
|
41
|
+
|
|
42
|
+
|
|
37
43
|
# Register config commands with the main config group
|
|
38
44
|
register_job_commands(job)
|
|
39
45
|
register_mmt_commands(mmt)
|
|
40
46
|
register_studio_commands(studio)
|
|
41
47
|
register_config_commands(config)
|
|
42
48
|
register_vm_commands(vm)
|
|
49
|
+
register_base_studio_commands(base_studio)
|
|
@@ -14,7 +14,7 @@ from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
|
|
|
14
14
|
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
15
15
|
from lightning_sdk.models import download_model
|
|
16
16
|
from lightning_sdk.studio import Studio
|
|
17
|
-
from lightning_sdk.utils.resolve import _get_authed_user
|
|
17
|
+
from lightning_sdk.utils.resolve import _get_authed_user
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _expand_remote_path(path: str) -> str:
|
|
@@ -242,7 +242,7 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
|
|
|
242
242
|
and (re.match(studio_name, st["name"]) or studio_name in st["name"]),
|
|
243
243
|
possible_studios,
|
|
244
244
|
)
|
|
245
|
-
if not possible_studios:
|
|
245
|
+
if not len(possible_studios):
|
|
246
246
|
raise ValueError(
|
|
247
247
|
f"Could not find Studio like '{studio}', please consider update your filtering pattern."
|
|
248
248
|
)
|
|
@@ -260,8 +260,7 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
|
|
|
260
260
|
"Please contact Lightning AI directly to resolve this issue."
|
|
261
261
|
) from e
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
return Studio(**selected_studio)
|
|
263
|
+
return Studio(**selected_studio)
|
|
265
264
|
|
|
266
265
|
|
|
267
266
|
@download.command(name="licenses")
|
|
@@ -20,7 +20,7 @@ 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
|
|
23
|
-
from lightning_sdk.utils.resolve import _get_authed_user
|
|
23
|
+
from lightning_sdk.utils.resolve import _get_authed_user
|
|
24
24
|
|
|
25
25
|
_STUDIO_UPLOAD_STATUS_PATH = "~/.lightning/studios/uploads"
|
|
26
26
|
|
|
@@ -285,8 +285,7 @@ def _resolve_studio(studio: Optional[str]) -> Studio:
|
|
|
285
285
|
"Please contact Lightning AI directly to resolve this issue."
|
|
286
286
|
) from e
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
return Studio(**selected_studio)
|
|
288
|
+
return Studio(**selected_studio)
|
|
290
289
|
|
|
291
290
|
|
|
292
291
|
def _print_docker_push(lines: Generator, console: Console, progress: Progress, push_task: rich.progress.TaskID) -> None:
|
|
@@ -5,6 +5,7 @@ import click
|
|
|
5
5
|
|
|
6
6
|
def register_commands(group: click.Group) -> None:
|
|
7
7
|
"""Register studio commands with the given group."""
|
|
8
|
+
from lightning_sdk.cli.studio.connect import connect_studio
|
|
8
9
|
from lightning_sdk.cli.studio.create import create_studio
|
|
9
10
|
from lightning_sdk.cli.studio.delete import delete_studio
|
|
10
11
|
from lightning_sdk.cli.studio.list import list_studios
|
|
@@ -20,3 +21,4 @@ def register_commands(group: click.Group) -> None:
|
|
|
20
21
|
group.add_command(start_studio)
|
|
21
22
|
group.add_command(stop_studio)
|
|
22
23
|
group.add_command(switch_studio)
|
|
24
|
+
group.add_command(connect_studio)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Studio connect command."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Dict, Optional, Set
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from lightning_sdk.base_studio import BaseStudio
|
|
10
|
+
from lightning_sdk.cli.utils.richt_print import studio_name_link
|
|
11
|
+
from lightning_sdk.cli.utils.save_to_config import save_studio_to_config, save_teamspace_to_config
|
|
12
|
+
from lightning_sdk.cli.utils.ssh_connection import configure_ssh_internal
|
|
13
|
+
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
14
|
+
from lightning_sdk.lightning_cloud.openapi.rest import ApiException
|
|
15
|
+
from lightning_sdk.machine import CloudProvider, Machine
|
|
16
|
+
from lightning_sdk.studio import Studio
|
|
17
|
+
from lightning_sdk.utils.names import random_unique_name
|
|
18
|
+
|
|
19
|
+
DEFAULT_MACHINE = "CPU"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _split_gpus_spec(gpus: str) -> tuple[str, int]:
|
|
23
|
+
machine_name, machine_val = gpus.split(":", 1)
|
|
24
|
+
machine_name = machine_name.strip()
|
|
25
|
+
machine_val = machine_val.strip()
|
|
26
|
+
|
|
27
|
+
if not machine_val.isdigit() or int(machine_val) <= 0:
|
|
28
|
+
raise ValueError(f"Invalid GPU count '{machine_val}'. Must be a positive integer.")
|
|
29
|
+
|
|
30
|
+
machine_num = int(machine_val)
|
|
31
|
+
return machine_name, machine_num
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _construct_available_gpus(machine_options: Dict[str, str]) -> Set[str]:
|
|
35
|
+
# returns available gpus:count
|
|
36
|
+
available_gpus = set()
|
|
37
|
+
for v in machine_options.values():
|
|
38
|
+
if "_X_" in v:
|
|
39
|
+
gpu_type_num = v.replace("_X_", ":")
|
|
40
|
+
available_gpus.add(gpu_type_num)
|
|
41
|
+
else:
|
|
42
|
+
available_gpus.add(v)
|
|
43
|
+
return available_gpus
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_machine_from_gpus(gpus: str) -> Machine:
|
|
47
|
+
machine_name = gpus
|
|
48
|
+
machine_num = 1
|
|
49
|
+
|
|
50
|
+
if ":" in gpus:
|
|
51
|
+
machine_name, machine_num = _split_gpus_spec(gpus)
|
|
52
|
+
|
|
53
|
+
machine_options = {
|
|
54
|
+
m.name.lower(): m.name for m in Machine.__dict__.values() if isinstance(m, Machine) and m._include_in_cli
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if machine_num == 1:
|
|
58
|
+
# e.g. gpus=L4 or gpus=L4:1
|
|
59
|
+
gpu_key = machine_name.lower()
|
|
60
|
+
try:
|
|
61
|
+
return machine_options[gpu_key]
|
|
62
|
+
except KeyError:
|
|
63
|
+
available = ", ".join(_construct_available_gpus(machine_options))
|
|
64
|
+
raise ValueError(f"Invalid GPU type '{machine_name}'. Available options: {available}") from None
|
|
65
|
+
|
|
66
|
+
# Else: e.g. gpus=L4:4
|
|
67
|
+
gpu_key = f"{machine_name.lower()}_x_{machine_num}"
|
|
68
|
+
try:
|
|
69
|
+
return machine_options[gpu_key]
|
|
70
|
+
except KeyError:
|
|
71
|
+
available = ", ".join(_construct_available_gpus(machine_options))
|
|
72
|
+
raise ValueError(f"Invalid GPU configuration '{gpus}'. Available options: {available}") from None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_base_studio_id(studio_type: Optional[str]) -> Optional[str]:
|
|
76
|
+
base_studios = BaseStudio()
|
|
77
|
+
base_studios = base_studios.list()
|
|
78
|
+
template_id = None
|
|
79
|
+
|
|
80
|
+
if base_studios and len(base_studios):
|
|
81
|
+
# if not specified by user, use the first existing template studio
|
|
82
|
+
template_id = base_studios[0].id
|
|
83
|
+
# else, try to match the provided studio_type to base studio name
|
|
84
|
+
if studio_type:
|
|
85
|
+
normalized_studio_type = studio_type.lower().replace(" ", "-")
|
|
86
|
+
match = next(
|
|
87
|
+
(s for s in base_studios if s.name.lower().replace(" ", "-") == normalized_studio_type),
|
|
88
|
+
None,
|
|
89
|
+
)
|
|
90
|
+
if match:
|
|
91
|
+
template_id = match.id
|
|
92
|
+
|
|
93
|
+
return template_id
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@click.command("connect")
|
|
97
|
+
@click.argument("name", required=False)
|
|
98
|
+
@click.option("--teamspace", help="Override default teamspace (format: owner/teamspace)")
|
|
99
|
+
@click.option(
|
|
100
|
+
"--cloud-provider",
|
|
101
|
+
help="The cloud provider to start the studio on. Defaults to teamspace default.",
|
|
102
|
+
type=click.Choice(m.name for m in list(CloudProvider)),
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--cloud-account",
|
|
106
|
+
help="The cloud account to create the studio on. Defaults to teamspace default.",
|
|
107
|
+
type=click.STRING,
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--machine",
|
|
111
|
+
help="The machine type to start the studio on. Defaults to CPU-4",
|
|
112
|
+
type=click.Choice(m.name for m in Machine.__dict__.values() if isinstance(m, Machine) and m._include_in_cli),
|
|
113
|
+
)
|
|
114
|
+
@click.option(
|
|
115
|
+
"--gpus",
|
|
116
|
+
help="The number and type of GPUs to start the studio on (format: TYPE:COUNT, e.g. L4:4)",
|
|
117
|
+
type=click.STRING,
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--studio-type",
|
|
121
|
+
help="The base studio template name to use for creating the studio. "
|
|
122
|
+
"Must be lowercase and hyphenated (use '-' instead of spaces). "
|
|
123
|
+
"Run 'lightning base-studio list' to see all available templates. "
|
|
124
|
+
"Defaults to the first available template.",
|
|
125
|
+
type=click.STRING,
|
|
126
|
+
)
|
|
127
|
+
def connect_studio(
|
|
128
|
+
name: Optional[str] = None,
|
|
129
|
+
teamspace: Optional[str] = None,
|
|
130
|
+
cloud_provider: Optional[str] = None,
|
|
131
|
+
cloud_account: Optional[str] = None,
|
|
132
|
+
machine: Optional[str] = None,
|
|
133
|
+
gpus: Optional[str] = None,
|
|
134
|
+
studio_type: Optional[str] = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Connect to a Studio.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
lightning studio connect
|
|
140
|
+
"""
|
|
141
|
+
menu = TeamspacesMenu()
|
|
142
|
+
|
|
143
|
+
resolved_teamspace = menu(teamspace)
|
|
144
|
+
save_teamspace_to_config(resolved_teamspace, overwrite=False)
|
|
145
|
+
|
|
146
|
+
if cloud_provider is not None:
|
|
147
|
+
cloud_provider = CloudProvider(cloud_provider)
|
|
148
|
+
|
|
149
|
+
name = name or random_unique_name()
|
|
150
|
+
|
|
151
|
+
# check for available base studios
|
|
152
|
+
template_id = _get_base_studio_id(studio_type)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
studio = Studio(
|
|
156
|
+
name=name,
|
|
157
|
+
teamspace=resolved_teamspace,
|
|
158
|
+
create_ok=True,
|
|
159
|
+
cloud_provider=cloud_provider,
|
|
160
|
+
cloud_account=cloud_account,
|
|
161
|
+
studio_type=template_id,
|
|
162
|
+
)
|
|
163
|
+
except (RuntimeError, ValueError, ApiException):
|
|
164
|
+
raise ValueError(f"Could not create Studio: '{name}'") from None
|
|
165
|
+
|
|
166
|
+
click.echo(f"Connecting to Studio '{studio_name_link(studio)}' ...")
|
|
167
|
+
|
|
168
|
+
Studio.show_progress = True
|
|
169
|
+
|
|
170
|
+
if machine and gpus:
|
|
171
|
+
raise click.UsageError("Options --machine and --gpu are mutually exclusive. Provide only one.")
|
|
172
|
+
elif gpus:
|
|
173
|
+
machine = _get_machine_from_gpus(gpus.strip())
|
|
174
|
+
elif not machine:
|
|
175
|
+
machine = DEFAULT_MACHINE
|
|
176
|
+
|
|
177
|
+
save_studio_to_config(studio)
|
|
178
|
+
# by default, interruptible is False
|
|
179
|
+
studio.start(machine=machine, interruptible=False)
|
|
180
|
+
|
|
181
|
+
ssh_private_key_path = configure_ssh_internal()
|
|
182
|
+
|
|
183
|
+
ssh_option = "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=ERROR"
|
|
184
|
+
try:
|
|
185
|
+
ssh_command = f"ssh -i {ssh_private_key_path} {ssh_option} s_{studio._studio.id}@ssh.lightning.ai"
|
|
186
|
+
subprocess.run(ssh_command.split())
|
|
187
|
+
except Exception as ex:
|
|
188
|
+
print(f"Failed to establish SSH connection: {ex}")
|
|
189
|
+
sys.exit(1)
|
lightning_sdk/cli/studio/ssh.py
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
"""Studio SSH command."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
import platform
|
|
5
3
|
import subprocess
|
|
6
|
-
import uuid
|
|
7
|
-
from pathlib import Path
|
|
8
4
|
from typing import List, Optional
|
|
9
5
|
|
|
10
6
|
import click
|
|
11
7
|
|
|
12
8
|
from lightning_sdk.cli.utils.save_to_config import save_studio_to_config
|
|
9
|
+
from lightning_sdk.cli.utils.ssh_connection import configure_ssh_internal
|
|
13
10
|
from lightning_sdk.cli.utils.studio_selection import StudiosMenu
|
|
14
11
|
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
15
|
-
from lightning_sdk.lightning_cloud.login import Auth
|
|
16
|
-
from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH
|
|
17
12
|
|
|
18
13
|
|
|
19
14
|
@click.command("ssh")
|
|
@@ -43,9 +38,7 @@ def ssh_studio(name: Optional[str] = None, teamspace: Optional[str] = None, opti
|
|
|
43
38
|
|
|
44
39
|
|
|
45
40
|
def ssh_impl(name: Optional[str], teamspace: Optional[str], option: Optional[List[str]], vm: bool) -> None:
|
|
46
|
-
|
|
47
|
-
auth.authenticate()
|
|
48
|
-
ssh_private_key_path = _download_ssh_keys(auth.api_key, force_download=False)
|
|
41
|
+
ssh_private_key_path = configure_ssh_internal()
|
|
49
42
|
|
|
50
43
|
menu = TeamspacesMenu()
|
|
51
44
|
resolved_teamspace = menu(teamspace=teamspace)
|
|
@@ -63,53 +56,9 @@ def ssh_impl(name: Optional[str], teamspace: Optional[str], option: Optional[Lis
|
|
|
63
56
|
subprocess.run(ssh_command.split())
|
|
64
57
|
except Exception:
|
|
65
58
|
# redownload the keys to be sure they are up to date
|
|
66
|
-
|
|
59
|
+
ssh_private_key_path = configure_ssh_internal(force_download=True)
|
|
67
60
|
try:
|
|
68
61
|
subprocess.run(ssh_command.split())
|
|
69
62
|
except Exception:
|
|
70
63
|
# TODO: make this a generic CLI error
|
|
71
64
|
raise RuntimeError("Failed to establish SSH connection") from None
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _download_ssh_keys(
|
|
75
|
-
api_key: str,
|
|
76
|
-
force_download: bool = False,
|
|
77
|
-
ssh_key_name: str = "lightning_rsa",
|
|
78
|
-
) -> None:
|
|
79
|
-
"""Download the SSH key for a User."""
|
|
80
|
-
ssh_private_key_path = os.path.join(os.path.expanduser(os.path.dirname(_DEFAULT_CONFIG_FILE_PATH)), ssh_key_name)
|
|
81
|
-
|
|
82
|
-
os.makedirs(os.path.dirname(ssh_private_key_path), exist_ok=True)
|
|
83
|
-
|
|
84
|
-
if not os.path.isfile(ssh_private_key_path) or force_download:
|
|
85
|
-
key_id = str(uuid.uuid4())
|
|
86
|
-
_download_file(
|
|
87
|
-
f"https://lightning.ai/setup/ssh-gen?t={api_key}&id={key_id}&machineName={platform.node()}",
|
|
88
|
-
ssh_private_key_path,
|
|
89
|
-
overwrite=True,
|
|
90
|
-
chmod=0o600,
|
|
91
|
-
)
|
|
92
|
-
_download_file(
|
|
93
|
-
f"https://lightning.ai/setup/ssh-public?t={api_key}&id={key_id}",
|
|
94
|
-
ssh_private_key_path + ".pub",
|
|
95
|
-
overwrite=True,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
return ssh_private_key_path
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _download_file(url: str, local_path: Path, overwrite: bool = True, chmod: Optional[int] = None) -> None:
|
|
102
|
-
"""Download a file from a URL."""
|
|
103
|
-
import requests
|
|
104
|
-
|
|
105
|
-
if os.path.isfile(local_path) and not overwrite:
|
|
106
|
-
raise FileExistsError(f"The file {local_path} already exists and overwrite is set to False.")
|
|
107
|
-
|
|
108
|
-
response = requests.get(url, stream=True)
|
|
109
|
-
response.raise_for_status()
|
|
110
|
-
|
|
111
|
-
with open(local_path, "wb") as file:
|
|
112
|
-
for chunk in response.iter_content(chunk_size=8192):
|
|
113
|
-
file.write(chunk)
|
|
114
|
-
if chmod is not None:
|
|
115
|
-
os.chmod(local_path, 0o600)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from lightning_sdk.lightning_cloud.login import Auth
|
|
8
|
+
from lightning_sdk.utils.config import _DEFAULT_CONFIG_FILE_PATH
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def configure_ssh_internal(force_download: bool = False) -> str:
|
|
12
|
+
"""Internal function to configure SSH without Click decorators."""
|
|
13
|
+
auth = Auth()
|
|
14
|
+
auth.authenticate()
|
|
15
|
+
return download_ssh_keys(auth.api_key, force_download=force_download)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def download_ssh_keys(
|
|
19
|
+
api_key: str | None,
|
|
20
|
+
force_download: bool = False,
|
|
21
|
+
ssh_key_name: str = "lightning_rsa",
|
|
22
|
+
) -> str:
|
|
23
|
+
"""Download the SSH key for a User."""
|
|
24
|
+
ssh_private_key_path = os.path.join(os.path.expanduser(os.path.dirname(_DEFAULT_CONFIG_FILE_PATH)), ssh_key_name)
|
|
25
|
+
|
|
26
|
+
os.makedirs(os.path.dirname(ssh_private_key_path), exist_ok=True)
|
|
27
|
+
|
|
28
|
+
if not os.path.isfile(ssh_private_key_path) or force_download:
|
|
29
|
+
key_id = str(uuid.uuid4())
|
|
30
|
+
download_file(
|
|
31
|
+
f"https://lightning.ai/setup/ssh-gen?t={api_key}&id={key_id}&machineName={platform.node()}",
|
|
32
|
+
Path(ssh_private_key_path),
|
|
33
|
+
overwrite=True,
|
|
34
|
+
chmod=0o600,
|
|
35
|
+
)
|
|
36
|
+
download_file(
|
|
37
|
+
f"https://lightning.ai/setup/ssh-public?t={api_key}&id={key_id}",
|
|
38
|
+
Path(ssh_private_key_path + ".pub"),
|
|
39
|
+
overwrite=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return ssh_private_key_path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def download_file(url: str, local_path: Path, overwrite: bool = True, chmod: Optional[int] = None) -> None:
|
|
46
|
+
"""Download a file from a URL."""
|
|
47
|
+
import requests
|
|
48
|
+
|
|
49
|
+
if os.path.isfile(local_path) and not overwrite:
|
|
50
|
+
raise FileExistsError(f"The file {local_path} already exists and overwrite is set to False.")
|
|
51
|
+
|
|
52
|
+
response = requests.get(url, stream=True)
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
|
|
55
|
+
with open(local_path, "wb") as file:
|
|
56
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
57
|
+
file.write(chunk)
|
|
58
|
+
if chmod is not None:
|
|
59
|
+
os.chmod(local_path, 0o600)
|
lightning_sdk/studio.py
CHANGED
|
@@ -8,6 +8,7 @@ from tqdm.auto import tqdm
|
|
|
8
8
|
|
|
9
9
|
from lightning_sdk.api.cloud_account_api import CloudAccountApi
|
|
10
10
|
from lightning_sdk.api.studio_api import StudioApi
|
|
11
|
+
from lightning_sdk.base_studio import BaseStudio
|
|
11
12
|
from lightning_sdk.constants import _LIGHTNING_DEBUG
|
|
12
13
|
from lightning_sdk.lightning_cloud.openapi import V1ClusterType
|
|
13
14
|
from lightning_sdk.machine import CloudProvider, Machine
|
|
@@ -51,6 +52,8 @@ class Studio:
|
|
|
51
52
|
If not specified, falls backto the teamspace default cloud account.
|
|
52
53
|
create_ok: whether the studio will be created if it does not yet exist. Defaults to True
|
|
53
54
|
provider: the provider of the machine, the studio should be created on.
|
|
55
|
+
studio_type: Type of studio to create. Only effective during initial creation;
|
|
56
|
+
ignored for existing studios.
|
|
54
57
|
|
|
55
58
|
Note:
|
|
56
59
|
Since a teamspace can either be owned by an org or by a user directly,
|
|
@@ -78,6 +81,7 @@ class Studio:
|
|
|
78
81
|
source: Optional[str] = None,
|
|
79
82
|
disable_secrets: bool = False,
|
|
80
83
|
provider: Optional[Union[CloudProvider, str]] = None, # deprecated in favor of cloud_provider
|
|
84
|
+
studio_type: Optional[str] = None, # for base studio templates
|
|
81
85
|
) -> None:
|
|
82
86
|
self._studio_api = StudioApi()
|
|
83
87
|
self._cloud_account_api = CloudAccountApi()
|
|
@@ -113,6 +117,25 @@ class Studio:
|
|
|
113
117
|
default_cloud_account=self._teamspace.default_cloud_account,
|
|
114
118
|
)
|
|
115
119
|
|
|
120
|
+
self._studio_type = None
|
|
121
|
+
if studio_type:
|
|
122
|
+
self._base_studio = BaseStudio()
|
|
123
|
+
self._available_base_studios = self._base_studio.list()
|
|
124
|
+
for bst in self._available_base_studios:
|
|
125
|
+
if (
|
|
126
|
+
bst.id == studio_type
|
|
127
|
+
or bst.name == studio_type
|
|
128
|
+
or bst.name.lower().replace(" ", "-") == studio_type
|
|
129
|
+
):
|
|
130
|
+
self._studio_type = bst.id
|
|
131
|
+
|
|
132
|
+
if not self._studio_type:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Could not find studio type with ID or name '{studio_type}'. "
|
|
135
|
+
f"Available studio types: "
|
|
136
|
+
f"{[bst.name.lower().replace(' ', '-') for bst in self._available_base_studios]}"
|
|
137
|
+
)
|
|
138
|
+
|
|
116
139
|
# Resolve studio name if not provided: explicit → env (LIGHTNING_CLOUD_SPACE_ID) → config defaults
|
|
117
140
|
if name is None and not getattr(self._skip_init, "value", False):
|
|
118
141
|
studio_id = os.environ.get("LIGHTNING_CLOUD_SPACE_ID", None)
|
|
@@ -150,6 +173,7 @@ class Studio:
|
|
|
150
173
|
cloud_account=_cloud_account,
|
|
151
174
|
source=source,
|
|
152
175
|
disable_secrets=self._disable_secrets,
|
|
176
|
+
cloud_space_environment_template_id=self._studio_type,
|
|
153
177
|
)
|
|
154
178
|
else:
|
|
155
179
|
raise e
|
|
@@ -264,10 +288,11 @@ class Studio:
|
|
|
264
288
|
else:
|
|
265
289
|
interruptible = self.teamspace.start_studios_on_interruptible
|
|
266
290
|
|
|
291
|
+
new_machine = machine
|
|
292
|
+
if not isinstance(machine, Machine):
|
|
293
|
+
new_machine = Machine.from_str(machine)
|
|
294
|
+
|
|
267
295
|
if status == Status.Running:
|
|
268
|
-
new_machine = machine
|
|
269
|
-
if not isinstance(machine, Machine):
|
|
270
|
-
new_machine = Machine.from_str(machine)
|
|
271
296
|
if new_machine != self.machine:
|
|
272
297
|
raise RuntimeError(
|
|
273
298
|
f"Requested to start {self._cls_name} on {new_machine}, "
|
|
@@ -289,7 +314,11 @@ class Studio:
|
|
|
289
314
|
with StudioProgressTracker("start", show_progress=True) as progress:
|
|
290
315
|
# Start the studio without blocking
|
|
291
316
|
self._studio_api.start_studio_async(
|
|
292
|
-
self._studio.id,
|
|
317
|
+
self._studio.id,
|
|
318
|
+
self._teamspace.id,
|
|
319
|
+
new_machine,
|
|
320
|
+
interruptible=interruptible,
|
|
321
|
+
max_runtime=max_runtime,
|
|
293
322
|
)
|
|
294
323
|
|
|
295
324
|
# Track progress through completion
|
|
@@ -299,7 +328,7 @@ class Studio:
|
|
|
299
328
|
else:
|
|
300
329
|
# Use the blocking version if no progress is needed
|
|
301
330
|
self._studio_api.start_studio(
|
|
302
|
-
self._studio.id, self._teamspace.id,
|
|
331
|
+
self._studio.id, self._teamspace.id, new_machine, interruptible=interruptible, max_runtime=max_runtime
|
|
303
332
|
)
|
|
304
333
|
|
|
305
334
|
self._setup()
|
lightning_sdk/utils/progress.py
CHANGED
|
@@ -17,30 +17,31 @@ from lightning_sdk.lightning_cloud.openapi.models.v1_get_cloud_space_instance_st
|
|
|
17
17
|
class StartupPhase(Enum):
|
|
18
18
|
"""Studio startup phase messages."""
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
SETTING_UP_ENVIRONMENT = "Setting up Studio environment..."
|
|
23
|
-
RESTORING_STATE = "Restoring Studio state..."
|
|
24
|
-
FINALIZING_SETUP = "Finalizing Studio setup..."
|
|
25
|
-
COMPLETED = "Studio started successfully"
|
|
20
|
+
STARTING_STUDIO = "Starting Studio..."
|
|
21
|
+
GETTING_MACHINE = "Getting a machine..."
|
|
26
22
|
|
|
23
|
+
SWITCHING_STUDIO = "Switching Studio..."
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
SETTING_UP_MACHINE = "Setting up machine..."
|
|
26
|
+
RESTORING_STUDIO = "Restoring Studio..."
|
|
27
|
+
PREPARING_STUDIO = "Preparing Studio..."
|
|
28
|
+
RESTORING_BASE_STUDIO = "Restoring Base Studio..."
|
|
29
|
+
SETTING_UP_BASE_STUDIO = "Setting up Base Studio..."
|
|
30
|
+
DONE = "Done"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_switching_progress_message(percentage: int, is_base_studio: bool) -> str:
|
|
29
34
|
"""Get progress message for switching studios."""
|
|
30
35
|
percentage = max(0, min(100, round(percentage)))
|
|
31
36
|
|
|
32
37
|
if percentage > 98:
|
|
33
|
-
message =
|
|
38
|
+
message = StartupPhase.DONE.value
|
|
34
39
|
elif percentage > 80:
|
|
35
|
-
if
|
|
36
|
-
message = "Setting up Base Studio..." if is_base_studio else "Preparing Studio..."
|
|
37
|
-
else:
|
|
38
|
-
message = "Restoring Studio..."
|
|
40
|
+
message = StartupPhase.RESTORING_BASE_STUDIO.value if is_base_studio else StartupPhase.RESTORING_STUDIO.value
|
|
39
41
|
elif percentage > 60:
|
|
40
|
-
message =
|
|
42
|
+
message = StartupPhase.SETTING_UP_MACHINE.value
|
|
41
43
|
else:
|
|
42
|
-
message =
|
|
43
|
-
|
|
44
|
+
message = StartupPhase.SWITCHING_STUDIO.value
|
|
44
45
|
return f"({percentage}%) {message}"
|
|
45
46
|
|
|
46
47
|
|
|
@@ -110,15 +111,13 @@ class StudioProgressTracker:
|
|
|
110
111
|
if self.progress:
|
|
111
112
|
self.progress.stop()
|
|
112
113
|
|
|
113
|
-
def update_progress(
|
|
114
|
-
self, percentage: int, message: str = "", is_base_studio: bool = False, is_new_cloud_space: bool = False
|
|
115
|
-
) -> None:
|
|
114
|
+
def update_progress(self, percentage: int, message: str = "", is_base_studio: bool = False) -> None:
|
|
116
115
|
"""Update progress bar with current percentage and message."""
|
|
117
116
|
if not self.progress or self.task_id is None:
|
|
118
117
|
return
|
|
119
118
|
|
|
120
119
|
if self.operation_type == "switch":
|
|
121
|
-
display_message = get_switching_progress_message(percentage, is_base_studio
|
|
120
|
+
display_message = get_switching_progress_message(percentage, is_base_studio)
|
|
122
121
|
else:
|
|
123
122
|
display_message = message or f"{self.operation_type.capitalize()}ing Studio..."
|
|
124
123
|
|
|
@@ -153,7 +152,7 @@ class StudioProgressTracker:
|
|
|
153
152
|
message_stability_delay = 3.0 # Seconds to wait before changing message
|
|
154
153
|
|
|
155
154
|
# Show initial progress immediately
|
|
156
|
-
self.update_progress(5, StartupPhase.
|
|
155
|
+
self.update_progress(5, StartupPhase.STARTING_STUDIO.value)
|
|
157
156
|
|
|
158
157
|
while True:
|
|
159
158
|
try:
|
|
@@ -163,7 +162,7 @@ class StudioProgressTracker:
|
|
|
163
162
|
# Default fallback progress based on time
|
|
164
163
|
time_based_progress = min(95, int((elapsed / timeout) * 100))
|
|
165
164
|
current_progress = max(last_progress, time_based_progress)
|
|
166
|
-
current_message = StartupPhase.
|
|
165
|
+
current_message = StartupPhase.STARTING_STUDIO.value
|
|
167
166
|
|
|
168
167
|
# Check if we have detailed status information
|
|
169
168
|
if hasattr(status, "in_use") and status.in_use:
|
|
@@ -178,7 +177,7 @@ class StudioProgressTracker:
|
|
|
178
177
|
hasattr(startup_status, "top_up_restore_finished")
|
|
179
178
|
and startup_status.top_up_restore_finished
|
|
180
179
|
):
|
|
181
|
-
self.complete(StartupPhase.
|
|
180
|
+
self.complete(StartupPhase.DONE.value)
|
|
182
181
|
break
|
|
183
182
|
|
|
184
183
|
# Check other phases in descending priority
|
|
@@ -186,14 +185,14 @@ class StudioProgressTracker:
|
|
|
186
185
|
hasattr(startup_status, "initial_restore_finished")
|
|
187
186
|
and startup_status.initial_restore_finished
|
|
188
187
|
):
|
|
189
|
-
current_progress = max(current_progress,
|
|
190
|
-
current_message = StartupPhase.
|
|
188
|
+
current_progress = max(current_progress, 80)
|
|
189
|
+
current_message = StartupPhase.PREPARING_STUDIO.value
|
|
191
190
|
elif hasattr(startup_status, "container_ready") and startup_status.container_ready:
|
|
192
|
-
current_progress = max(current_progress, 70)
|
|
193
|
-
current_message = StartupPhase.RESTORING_STATE.value
|
|
194
|
-
elif hasattr(startup_status, "machine_ready") and startup_status.machine_ready:
|
|
195
191
|
current_progress = max(current_progress, 60)
|
|
196
|
-
current_message = StartupPhase.
|
|
192
|
+
current_message = StartupPhase.SETTING_UP_MACHINE.value
|
|
193
|
+
elif hasattr(startup_status, "machine_ready") and startup_status.machine_ready:
|
|
194
|
+
current_progress = max(current_progress, 30)
|
|
195
|
+
current_message = StartupPhase.GETTING_MACHINE.value
|
|
197
196
|
|
|
198
197
|
# Check general phase information
|
|
199
198
|
if hasattr(in_use, "phase") and in_use.phase:
|
|
@@ -201,7 +200,7 @@ class StudioProgressTracker:
|
|
|
201
200
|
|
|
202
201
|
if phase == "CLOUD_SPACE_INSTANCE_STATE_RUNNING":
|
|
203
202
|
current_progress = max(current_progress, 80)
|
|
204
|
-
current_message = StartupPhase.
|
|
203
|
+
current_message = StartupPhase.SETTING_UP_MACHINE.value
|
|
205
204
|
elif phase == "CLOUD_SPACE_INSTANCE_STATE_PENDING":
|
|
206
205
|
# Track time in pending phase for smoother progress
|
|
207
206
|
if "pending" not in phase_start_times:
|
|
@@ -211,7 +210,7 @@ class StudioProgressTracker:
|
|
|
211
210
|
# Progress more smoothly through pending phase (10-60%)
|
|
212
211
|
pending_progress = 10 + min(50, int((pending_elapsed / 60) * 50))
|
|
213
212
|
current_progress = max(current_progress, pending_progress)
|
|
214
|
-
current_message = StartupPhase.
|
|
213
|
+
current_message = StartupPhase.GETTING_MACHINE.value
|
|
215
214
|
|
|
216
215
|
# Check for requested machine status (pre-allocation)
|
|
217
216
|
elif hasattr(status, "requested") and status.requested:
|
|
@@ -222,7 +221,7 @@ class StudioProgressTracker:
|
|
|
222
221
|
# Progress through allocation phase (5-30%)
|
|
223
222
|
allocation_progress = 5 + min(25, int((allocation_elapsed / 30) * 25))
|
|
224
223
|
current_progress = max(current_progress, allocation_progress)
|
|
225
|
-
current_message = StartupPhase.
|
|
224
|
+
current_message = StartupPhase.GETTING_MACHINE.value
|
|
226
225
|
|
|
227
226
|
# Ensure progress never decreases and moves smoothly
|
|
228
227
|
if current_progress > last_progress:
|
|
@@ -262,12 +261,12 @@ class StudioProgressTracker:
|
|
|
262
261
|
|
|
263
262
|
# Only update message if enough time has passed
|
|
264
263
|
current_time = time.time()
|
|
265
|
-
should_update_message = StartupPhase.
|
|
264
|
+
should_update_message = StartupPhase.GETTING_MACHINE.value != self._last_message and (
|
|
266
265
|
current_time - last_message_time >= message_stability_delay or last_message_time == 0
|
|
267
266
|
)
|
|
268
267
|
|
|
269
268
|
if should_update_message:
|
|
270
|
-
self.update_progress(fallback_progress, StartupPhase.
|
|
269
|
+
self.update_progress(fallback_progress, StartupPhase.GETTING_MACHINE.value)
|
|
271
270
|
last_message_time = current_time
|
|
272
271
|
else:
|
|
273
272
|
# Update progress but keep existing message
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
docs/source/conf.py,sha256=r8yX20eC-4mHhMTd0SbQb5TlSWHhO6wnJ0VJ_FBFpag,13249
|
|
2
|
-
lightning_sdk/__init__.py,sha256=
|
|
2
|
+
lightning_sdk/__init__.py,sha256=MLcIAWdrZoDrz0HJ7KhaLRbFrSg8Qs_XFS8p2vr2cwQ,1235
|
|
3
3
|
lightning_sdk/agents.py,sha256=ly6Ma1j0ZgGPFyvPvMN28JWiB9dATIstFa5XM8pMi6I,1577
|
|
4
4
|
lightning_sdk/ai_hub.py,sha256=iI1vNhgcz_Ff1c3rN1ogN7dK-r-HXRj6NMtS2cA14UA,6925
|
|
5
|
-
lightning_sdk/base_studio.py,sha256=
|
|
5
|
+
lightning_sdk/base_studio.py,sha256=FIjHUiZtRf42m5YeqYYaJxcdgOtJVAJLZU42gzmldO8,4109
|
|
6
6
|
lightning_sdk/constants.py,sha256=ztl1PTUBULnqTf3DyKUSJaV_O20hNtUYT6XvAYIrmIk,749
|
|
7
7
|
lightning_sdk/helpers.py,sha256=KWMWnORHItIIA3PGn71YPs-7RjzGi8IXa2kQ5Qo4U8M,2459
|
|
8
8
|
lightning_sdk/lit_container.py,sha256=8ys49TXI9MO89jLTA7MwDrKrssTRARAIF9OVmolDLq0,5273
|
|
@@ -14,7 +14,7 @@ lightning_sdk/plugin.py,sha256=f3P5-xZY6x-MX0Fs2z_Q2erSxPSiHZARO0BVkCezHw4,15192
|
|
|
14
14
|
lightning_sdk/sandbox.py,sha256=_NvnWotEXW2rBiVFZZ4krKXxVjuAqfNh04qELSM0-Pg,5786
|
|
15
15
|
lightning_sdk/serve.py,sha256=uW7zLhQ3X90ifetpxzTb8FNxifv5vIs7qZlgfEjVKzk,11794
|
|
16
16
|
lightning_sdk/status.py,sha256=lLGAuSvXBoXQFEEsEYwdCi0RcSNatUn5OPjJVjDtoM0,386
|
|
17
|
-
lightning_sdk/studio.py,sha256=
|
|
17
|
+
lightning_sdk/studio.py,sha256=pij-pT-Cd11j5-LMAHxORLRP9a-Gcow87ZkTPcluu38,34350
|
|
18
18
|
lightning_sdk/teamspace.py,sha256=iYaEaZH8N8Yj4XXj6M4zIH0tsjPVrAf_BzAAv6Jt9VI,26556
|
|
19
19
|
lightning_sdk/user.py,sha256=TSYh38rxoi7qKOfrK2JYh_Nknya2Kbz2ngDIY85fFOY,1778
|
|
20
20
|
lightning_sdk/api/__init__.py,sha256=xrp_RNECJGQtL5rZHF69WOzEuEIbWSLtjWAJAz4R5K4,500
|
|
@@ -35,8 +35,10 @@ lightning_sdk/api/teamspace_api.py,sha256=UZ6Ka6TL7zggs5qhQB4wl47VRusVTMpth1-toG
|
|
|
35
35
|
lightning_sdk/api/user_api.py,sha256=o9gZmtvqNfj4kdSpo2fyyRuFAP7bM5w7mx0Oj9__Ads,4702
|
|
36
36
|
lightning_sdk/api/utils.py,sha256=1NJgW4HCfzMqgnAqbMA7RRr2v3iM3KTuPIUQK5klDeQ,27127
|
|
37
37
|
lightning_sdk/cli/__init__.py,sha256=lksw08t_ZIuHOH47LCIqSVHeZ8cUXI2aJWHYhyujYHM,32
|
|
38
|
-
lightning_sdk/cli/entrypoint.py,sha256=
|
|
39
|
-
lightning_sdk/cli/groups.py,sha256=
|
|
38
|
+
lightning_sdk/cli/entrypoint.py,sha256=wMAyzfKRqZ6EZ73SxpuJDk3u8J3o-I5vF0VnESJh2QE,4646
|
|
39
|
+
lightning_sdk/cli/groups.py,sha256=3l9_U-8T613RJ2fHZy_-iJbtpq4fH8auWH7eFGSdZmg,1373
|
|
40
|
+
lightning_sdk/cli/base_studio/__init__.py,sha256=npBEmXWNVuiawm98TFVNHZf038kD4aW3gzVHsjbUPNI,272
|
|
41
|
+
lightning_sdk/cli/base_studio/list.py,sha256=DCXzSIY4IB0lHEstP0PA-8e7VqQQSUXL6OVv8VvwBKM,1244
|
|
40
42
|
lightning_sdk/cli/config/__init__.py,sha256=_aJ7uZZOdjomZ-ABmz8Cu3fLmnSujnQK4hKAEKM4aF4,395
|
|
41
43
|
lightning_sdk/cli/config/get.py,sha256=JXGRNVGxELS6Es2RESgStzTKnjg5rr-M6yOmx2ty9A8,1606
|
|
42
44
|
lightning_sdk/cli/config/set.py,sha256=9p8q8nUWGWNcAcflQ3hS_tj-59jY7cmkzccuiZl92h4,3117
|
|
@@ -50,7 +52,7 @@ lightning_sdk/cli/legacy/connect.py,sha256=0qJ0yvykoPBkmCvvEbWKppt7wlUmRq3LNNGEs
|
|
|
50
52
|
lightning_sdk/cli/legacy/create.py,sha256=ftp7cP8dqtO3h35mZUkpUjFW1tHwKxPQoUOPsabcy0M,3682
|
|
51
53
|
lightning_sdk/cli/legacy/delete.py,sha256=IC04CNqJFIEfgl6Tlt4uEl3t81yZCb8VYLRwy3B8xiA,3816
|
|
52
54
|
lightning_sdk/cli/legacy/docker_cli.py,sha256=EozLC4qnvHhgukmnu35itfI5n7v-abCpwKPkPy3eOV4,997
|
|
53
|
-
lightning_sdk/cli/legacy/download.py,sha256=
|
|
55
|
+
lightning_sdk/cli/legacy/download.py,sha256=_Yaw1slwjE7ygDomjEJliXLTE-Gfj1d3zkH3daYVASk,11311
|
|
54
56
|
lightning_sdk/cli/legacy/entrypoint.py,sha256=dk_BKvNzQMYREdAGTTdHF8nZWp_EuvCii-WOz4YFwrk,3784
|
|
55
57
|
lightning_sdk/cli/legacy/exceptions.py,sha256=QUF3OMAMZwBikvlusimSHSBjb6ywvHpfAumJBEaodSw,169
|
|
56
58
|
lightning_sdk/cli/legacy/generate.py,sha256=3tLaqALiuWG3AEIQtNHChMNpaJJU9zV5VYGshiEmKug,1582
|
|
@@ -66,17 +68,18 @@ lightning_sdk/cli/legacy/stop.py,sha256=jSiuxIunDnL3A3DQ1m8GZx01Qt4Zx5vIA3u7U81C
|
|
|
66
68
|
lightning_sdk/cli/legacy/studios_menu.py,sha256=TA9rO6_fFHGMz0Nt4rJ6iV80X5pZE4xShrSiyXoU-oQ,4129
|
|
67
69
|
lightning_sdk/cli/legacy/switch.py,sha256=G3y9BmP-0d_OKJub5fMZ0ddnGZkOFBtregMkx_KWxcw,1945
|
|
68
70
|
lightning_sdk/cli/legacy/teamspace_menu.py,sha256=u5_lyB1aCD8KJIsOuxi0bvPi75McU5EbluMLyD_NBUw,4104
|
|
69
|
-
lightning_sdk/cli/legacy/upload.py,sha256=
|
|
71
|
+
lightning_sdk/cli/legacy/upload.py,sha256=fFJct9F_1vLqTMLtSjuv4BN3AbUpeGebwv3ZXH0NiqI,13722
|
|
70
72
|
lightning_sdk/cli/legacy/deploy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
71
73
|
lightning_sdk/cli/legacy/deploy/_auth.py,sha256=wpwhkF2tmURlhEBaZ3QgOnHQELy3XLPt2d4LWb7JosY,7700
|
|
72
74
|
lightning_sdk/cli/legacy/deploy/devbox.py,sha256=SdOtYdGefzldn-WCa6sz31zysyf2nP5D7O2x1DE9z_E,6839
|
|
73
75
|
lightning_sdk/cli/legacy/deploy/serve.py,sha256=bxN04mHlArjHtxR_QkCM0S_RPmL3WIPsgnPgFadSXwI,15478
|
|
74
76
|
lightning_sdk/cli/mmt/__init__.py,sha256=CLgr-ZHHLS6Db_JGqpxbn4G2pYrKi4Qn-uhi8e0kFNc,145
|
|
75
|
-
lightning_sdk/cli/studio/__init__.py,sha256=
|
|
77
|
+
lightning_sdk/cli/studio/__init__.py,sha256=jqUuEniYlf3fwJni-MGLII3oprpsBSQEFqzBJwPIxMk,925
|
|
78
|
+
lightning_sdk/cli/studio/connect.py,sha256=yb4Ga4lLjrg5C0sLPwAoEkkTpT7rv165AwqBOsYBIWc,6488
|
|
76
79
|
lightning_sdk/cli/studio/create.py,sha256=kTxLtvTFjo-SgkdzOIngdXu_6yYLHwVbkfMhHjpIFkI,2505
|
|
77
80
|
lightning_sdk/cli/studio/delete.py,sha256=Gi1KLCrSgjTiyoQe9NRvF4ZpsNyeM07KHnBxfdJg2FE,1483
|
|
78
81
|
lightning_sdk/cli/studio/list.py,sha256=ieLiHRIfAb7E5e_r48BeTC03ib8RY6maeF7nkmrqaek,3301
|
|
79
|
-
lightning_sdk/cli/studio/ssh.py,sha256=
|
|
82
|
+
lightning_sdk/cli/studio/ssh.py,sha256=zHpVUe2QiFvM91Pj5kzNCnacavwrptxNsPpxyxpKLno,2194
|
|
80
83
|
lightning_sdk/cli/studio/start.py,sha256=GHfV_1I1XwV8UHOWBP2WVfd1B2BHyw1lQ9HMv6Eomes,3236
|
|
81
84
|
lightning_sdk/cli/studio/stop.py,sha256=hVXIyrv68rvrwIQgyEPUKt3Th8S1iipOVflopPnoVOY,1398
|
|
82
85
|
lightning_sdk/cli/studio/switch.py,sha256=x6F1biDtfR0dk1t7X3NSkmfj5jkzGJe6-nvj3SonQlQ,2069
|
|
@@ -87,6 +90,7 @@ lightning_sdk/cli/utils/owner_selection.py,sha256=PbKbFoGblxi60lF_kZxnlqxjCmI9r9
|
|
|
87
90
|
lightning_sdk/cli/utils/resolve.py,sha256=RWQ8MgXEJCdsoY50y8Pa9B-A6NA_SvxOqL5pVKfvSr8,917
|
|
88
91
|
lightning_sdk/cli/utils/richt_print.py,sha256=psSY65nLAQbxK_K0w93Qq9857aUUbm77FLS1sc3Oecg,1262
|
|
89
92
|
lightning_sdk/cli/utils/save_to_config.py,sha256=mMjjfA4Yv1e7MtE03iADTXcTSAuUL0Ws9OZObx6ufo0,1164
|
|
93
|
+
lightning_sdk/cli/utils/ssh_connection.py,sha256=cfppkWOLRLVSE-LhkENRANgdPNpT3U6beg8Lyx1xoxo,1984
|
|
90
94
|
lightning_sdk/cli/utils/studio_selection.py,sha256=z0SMd82sOt_mfTiQZgVY27MxLkMAjM__SWhlOlfXJT0,4347
|
|
91
95
|
lightning_sdk/cli/utils/teamspace_selection.py,sha256=n-FFfWLTV63SxoK6Fi2VQikBIfKYqMNTew9b9v-1UQk,4945
|
|
92
96
|
lightning_sdk/cli/vm/__init__.py,sha256=tAJpluJVDOF3fUQyRwko5ZYIpqglczMyRv9Kz4OVaIk,711
|
|
@@ -1203,11 +1207,11 @@ lightning_sdk/utils/config.py,sha256=LfCAbko_f-1TgFoYD1KT68tZHEIgn3M2y1HUqTMruUo
|
|
|
1203
1207
|
lightning_sdk/utils/dynamic.py,sha256=glUTO1JC9APtQ6Gr9SO02a3zr56-sPAXM5C3NrTpgyQ,1959
|
|
1204
1208
|
lightning_sdk/utils/enum.py,sha256=h2JRzqoBcSlUdanFHmkj_j5DleBHAu1esQYUsdNI-hU,4106
|
|
1205
1209
|
lightning_sdk/utils/names.py,sha256=1EuXbIh7wldkDp1FG10oz9vIOyWrpGWeFFVy-DQBgzA,18162
|
|
1206
|
-
lightning_sdk/utils/progress.py,sha256=
|
|
1210
|
+
lightning_sdk/utils/progress.py,sha256=bLWw39fzq29PMWoFXaPIVfoS3Ug245950oWOFJ2ZaiU,12596
|
|
1207
1211
|
lightning_sdk/utils/resolve.py,sha256=4TyEnIgIrvkSvYk5i5PmcIogD_5Y9pBhiphRLfLMttc,10477
|
|
1208
|
-
lightning_sdk-2025.
|
|
1209
|
-
lightning_sdk-2025.
|
|
1210
|
-
lightning_sdk-2025.
|
|
1211
|
-
lightning_sdk-2025.
|
|
1212
|
-
lightning_sdk-2025.
|
|
1213
|
-
lightning_sdk-2025.
|
|
1212
|
+
lightning_sdk-2025.10.8.dist-info/LICENSE,sha256=uFIuZwj5z-4TeF2UuacPZ1o17HkvKObT8fY50qN84sg,1064
|
|
1213
|
+
lightning_sdk-2025.10.8.dist-info/METADATA,sha256=N8b-kMsJedFH9daVWxNta30bWOraumpFMRESf8W-Sh4,4130
|
|
1214
|
+
lightning_sdk-2025.10.8.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
|
1215
|
+
lightning_sdk-2025.10.8.dist-info/entry_points.txt,sha256=OoZa4Fc8NMs6GSN0cdA1J8e6couzAcL82CbM1yo4f_M,122
|
|
1216
|
+
lightning_sdk-2025.10.8.dist-info/top_level.txt,sha256=ps8doKILFXmN7F1mHncShmnQoTxKBRPIcchC8TpoBw4,19
|
|
1217
|
+
lightning_sdk-2025.10.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|