lightning-sdk 0.1.55__py3-none-any.whl → 0.1.57__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/ai_hub.py +22 -0
- lightning_sdk/api/ai_hub_api.py +21 -2
- lightning_sdk/api/deployment_api.py +4 -3
- lightning_sdk/api/job_api.py +5 -10
- lightning_sdk/api/mmt_api.py +1 -4
- lightning_sdk/api/studio_api.py +5 -7
- lightning_sdk/api/teamspace_api.py +7 -0
- lightning_sdk/api/utils.py +1 -27
- lightning_sdk/cli/ai_hub.py +61 -10
- lightning_sdk/cli/configure.py +137 -0
- lightning_sdk/cli/connect.py +47 -0
- lightning_sdk/cli/delete.py +83 -32
- lightning_sdk/cli/download.py +177 -90
- lightning_sdk/cli/entrypoint.py +50 -15
- lightning_sdk/cli/generate.py +51 -42
- lightning_sdk/cli/inspect.py +45 -3
- lightning_sdk/cli/jobs_menu.py +2 -1
- lightning_sdk/cli/list.py +139 -55
- lightning_sdk/cli/mmts_menu.py +2 -1
- lightning_sdk/cli/run.py +3 -9
- lightning_sdk/cli/serve.py +1 -2
- lightning_sdk/cli/start.py +2 -2
- lightning_sdk/cli/stop.py +5 -3
- lightning_sdk/cli/studios_menu.py +24 -1
- lightning_sdk/cli/switch.py +2 -2
- lightning_sdk/cli/teamspace_menu.py +2 -1
- lightning_sdk/cli/upload.py +6 -4
- lightning_sdk/helpers.py +20 -0
- lightning_sdk/job/job.py +1 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +9 -0
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +105 -0
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +226 -0
- lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +4 -4
- lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +7 -3
- lightning_sdk/lightning_cloud/openapi/api/projects_service_api.py +1 -5
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +9 -0
- lightning_sdk/lightning_cloud/openapi/models/agents_id_body.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/id_reportrestarttimings_body.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/id_visibility_body1.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/model_id_visibility_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/project_id_litregistry_body.py +2 -0
- lightning_sdk/lightning_cloud/openapi/models/setup.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_assistant.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_gcp_data_connection_setup.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_get_cluster_accelerator_demand_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_artifact.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_project.py +8 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_repository.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_report_restart_timings_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_restart_timing.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_setup_data_connection_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_deployment_visibility_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_update_metrics_stream_visibility_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_update_model_visibility_response.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_validate_deployment_image_request.py +27 -1
- lightning_sdk/machine.py +59 -27
- lightning_sdk/studio.py +5 -1
- lightning_sdk/teamspace.py +25 -0
- {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/METADATA +3 -1
- {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/RECORD +72 -61
- {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/top_level.txt +0 -0
lightning_sdk/cli/delete.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
3
6
|
from lightning_sdk.cli.exceptions import StudioCliError
|
|
4
7
|
from lightning_sdk.cli.job_and_mmt_action import _JobAndMMTAction
|
|
5
8
|
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
@@ -18,15 +21,7 @@ class _Delete(_JobAndMMTAction, _TeamspacesMenu):
|
|
|
18
21
|
teamspace: The teamspace to delete the container from. Should be specified as {owner}/{name}
|
|
19
22
|
If not provided, can be selected in an interactive menu.
|
|
20
23
|
"""
|
|
21
|
-
|
|
22
|
-
resolved_teamspace = self._resolve_teamspace(teamspace=teamspace)
|
|
23
|
-
try:
|
|
24
|
-
api.delete_container(container, resolved_teamspace.name, resolved_teamspace.owner.name)
|
|
25
|
-
print(f"Container {container} deleted successfully.")
|
|
26
|
-
except Exception as e:
|
|
27
|
-
raise StudioCliError(
|
|
28
|
-
f"Could not delete container {container} from project {resolved_teamspace.name}: {e}"
|
|
29
|
-
) from None
|
|
24
|
+
delete_container(container=container, teamspace=teamspace)
|
|
30
25
|
|
|
31
26
|
def job(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
32
27
|
"""Delete a job.
|
|
@@ -38,10 +33,7 @@ class _Delete(_JobAndMMTAction, _TeamspacesMenu):
|
|
|
38
33
|
If not specified can be selected interactively.
|
|
39
34
|
|
|
40
35
|
"""
|
|
41
|
-
job
|
|
42
|
-
|
|
43
|
-
job.delete()
|
|
44
|
-
print(f"Successfully deleted {job.name}!")
|
|
36
|
+
job(name=name, teamspace=teamspace)
|
|
45
37
|
|
|
46
38
|
def mmt(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
47
39
|
"""Delete a multi-machine job.
|
|
@@ -53,10 +45,7 @@ class _Delete(_JobAndMMTAction, _TeamspacesMenu):
|
|
|
53
45
|
If not specified can be selected interactively.
|
|
54
46
|
|
|
55
47
|
"""
|
|
56
|
-
mmt
|
|
57
|
-
|
|
58
|
-
mmt.delete()
|
|
59
|
-
print(f"Successfully deleted {mmt.name}!")
|
|
48
|
+
mmt(name=name, teamspace=teamspace)
|
|
60
49
|
|
|
61
50
|
def studio(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
62
51
|
"""Delete an existing studio.
|
|
@@ -68,18 +57,80 @@ class _Delete(_JobAndMMTAction, _TeamspacesMenu):
|
|
|
68
57
|
teamspace: The teamspace the studio is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.
|
|
69
58
|
If not specified, tries to infer from the environment (e.g. when run from within a Studio.)
|
|
70
59
|
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
60
|
+
studio(name=name, teamspace=teamspace)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@click.group()
|
|
64
|
+
def delete() -> None:
|
|
65
|
+
"""Delete resources on the Lightning AI platform."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# @delete.command(name="container")
|
|
69
|
+
# @click.option("--container", help="The name of the container to delete.")
|
|
70
|
+
# @click.option("--teamspace", default=None, help=("The teamspace to delete the container from. "
|
|
71
|
+
# "Should be specified as {owner}/{name} "
|
|
72
|
+
# "If not provided, can be selected in an interactive menu."),)
|
|
73
|
+
def delete_container(container: str, teamspace: Optional[str] = None) -> None:
|
|
74
|
+
"""Delete the docker container CONTAINER."""
|
|
75
|
+
api = LitContainer()
|
|
76
|
+
menu = _TeamspacesMenu()
|
|
77
|
+
resolved_teamspace = menu._resolve_teamspace(teamspace=teamspace)
|
|
78
|
+
try:
|
|
79
|
+
api.delete_container(container, resolved_teamspace.name, resolved_teamspace.owner.name)
|
|
80
|
+
Console().print(f"Container {container} deleted successfully.")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise StudioCliError(
|
|
83
|
+
f"Could not delete container {container} from project {resolved_teamspace.name}: {e}"
|
|
84
|
+
) from None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# @delete.command(name="job")
|
|
88
|
+
# @click.option("--name", help="The name of the job to delete.")
|
|
89
|
+
# @click.option("--teamspace", default=None, help=("The teamspace to delete the job from. "
|
|
90
|
+
# "Should be specified as {owner}/{name} "
|
|
91
|
+
# "If not provided, can be selected in an interactive menu."),)
|
|
92
|
+
def job(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
93
|
+
"""Delete a job."""
|
|
94
|
+
menu = _JobAndMMTAction()
|
|
95
|
+
job = menu.job(name=name, teamspace=teamspace)
|
|
96
|
+
|
|
97
|
+
job.delete()
|
|
98
|
+
Console().print(f"Successfully deleted {job.name}!")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# @delete.command(name="mmt")
|
|
102
|
+
# @click.option("--name", help="The name of the multi-machine job to delete.")
|
|
103
|
+
# @click.option("--teamspace", default=None, help=("The teamspace to delete the job from. "
|
|
104
|
+
# "Should be specified as {owner}/{name} "
|
|
105
|
+
# "If not provided, can be selected in an interactive menu."),)
|
|
106
|
+
def mmt(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
107
|
+
"""Delete a multi-machine job."""
|
|
108
|
+
menu = _JobAndMMTAction()
|
|
109
|
+
mmt = menu.mmt(name=name, teamspace=teamspace)
|
|
110
|
+
|
|
111
|
+
mmt.delete()
|
|
112
|
+
Console().print(f"Successfully deleted {mmt.name}!")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# @delete.command(name="studio")
|
|
116
|
+
# @click.option("--name", help="The name of the studio to delete.")
|
|
117
|
+
# @click.option("--teamspace", default=None, help=("The teamspace to delete the studio from. "
|
|
118
|
+
# "Should be specified as {owner}/{name} "
|
|
119
|
+
# "If not provided, can be selected in an interactive menu."),)
|
|
120
|
+
def studio(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
121
|
+
"""Delete an existing studio."""
|
|
122
|
+
if teamspace is not None:
|
|
123
|
+
ts_splits = teamspace.split("/")
|
|
124
|
+
if len(ts_splits) != 2:
|
|
125
|
+
raise ValueError(f"Teamspace should be of format <OWNER>/<TEAMSPACE_NAME> but got {teamspace}")
|
|
126
|
+
owner, teamspace = ts_splits
|
|
127
|
+
else:
|
|
128
|
+
owner, teamspace = None, None
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False)
|
|
132
|
+
except (RuntimeError, ValueError):
|
|
133
|
+
studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
|
|
134
|
+
|
|
135
|
+
studio.delete()
|
|
136
|
+
Console().print("Studio successfully deleted")
|
lightning_sdk/cli/download.py
CHANGED
|
@@ -3,6 +3,7 @@ import re
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
|
+
import click
|
|
6
7
|
from rich.console import Console
|
|
7
8
|
|
|
8
9
|
from lightning_sdk.api.lit_container_api import LitContainerApi
|
|
@@ -25,51 +26,7 @@ class _Downloads(_StudiosMenu, _TeamspacesMenu):
|
|
|
25
26
|
This should have the format <ORGANIZATION-NAME>/<TEAMSPACE-NAME>/<MODEL-NAME>.
|
|
26
27
|
download_dir: The directory where the Model should be downloaded.
|
|
27
28
|
"""
|
|
28
|
-
|
|
29
|
-
name=name,
|
|
30
|
-
download_dir=download_dir,
|
|
31
|
-
progress_bar=True,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
def _resolve_studio(self, studio: Optional[str]) -> Studio:
|
|
35
|
-
user = _get_authed_user()
|
|
36
|
-
# if no studio specify suggest/filter only user's studios
|
|
37
|
-
possible_studios = self._get_possible_studios(user, is_owner=studio is None)
|
|
38
|
-
|
|
39
|
-
try:
|
|
40
|
-
if studio:
|
|
41
|
-
team_name, studio_name = studio.split("/")
|
|
42
|
-
options = [st for st in possible_studios if st["teamspace"] == team_name and st["name"] == studio_name]
|
|
43
|
-
if len(options) == 1:
|
|
44
|
-
selected_studio = self._get_studio_from_name(studio, possible_studios)
|
|
45
|
-
# user can also use the partial studio name as secondary interactive selection
|
|
46
|
-
else:
|
|
47
|
-
# filter matching simple reg expressions or start with the team and studio name
|
|
48
|
-
possible_studios = filter(
|
|
49
|
-
lambda st: (re.match(team_name, st["teamspace"]) or team_name in st["teamspace"])
|
|
50
|
-
and (re.match(studio_name, st["name"]) or studio_name in st["name"]),
|
|
51
|
-
possible_studios,
|
|
52
|
-
)
|
|
53
|
-
if not possible_studios:
|
|
54
|
-
raise ValueError(
|
|
55
|
-
f"Could not find Studio like '{studio}', please consider update your filtering pattern."
|
|
56
|
-
)
|
|
57
|
-
selected_studio = self._get_studio_from_interactive_menu(list(possible_studios))
|
|
58
|
-
else:
|
|
59
|
-
selected_studio = self._get_studio_from_interactive_menu(possible_studios)
|
|
60
|
-
|
|
61
|
-
except KeyboardInterrupt:
|
|
62
|
-
raise KeyboardInterrupt from None
|
|
63
|
-
|
|
64
|
-
# give user friendlier error message
|
|
65
|
-
except Exception as e:
|
|
66
|
-
raise StudioCliError(
|
|
67
|
-
f"Could not find the given Studio {studio} to upload files to. "
|
|
68
|
-
"Please contact Lightning AI directly to resolve this issue."
|
|
69
|
-
) from e
|
|
70
|
-
|
|
71
|
-
with skip_studio_init():
|
|
72
|
-
return Studio(**selected_studio)
|
|
29
|
+
model(name=name, download_dir=download_dir)
|
|
73
30
|
|
|
74
31
|
def folder(self, path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
|
|
75
32
|
"""Download a folder from a Studio.
|
|
@@ -84,27 +41,9 @@ class _Downloads(_StudiosMenu, _TeamspacesMenu):
|
|
|
84
41
|
with filtered studios will be shown for final selection.
|
|
85
42
|
local_path: The path to the directory you want to download the folder to.
|
|
86
43
|
"""
|
|
87
|
-
|
|
88
|
-
if not local_path.is_dir():
|
|
89
|
-
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
90
|
-
|
|
91
|
-
resolved_studio = self._resolve_studio(studio)
|
|
44
|
+
folder(path=path, studio=studio, local_path=local_path)
|
|
92
45
|
|
|
93
|
-
|
|
94
|
-
local_path /= resolved_studio.name
|
|
95
|
-
path = ""
|
|
96
|
-
|
|
97
|
-
try:
|
|
98
|
-
if not path:
|
|
99
|
-
raise FileNotFoundError()
|
|
100
|
-
resolved_studio.download_folder(remote_path=path, target_path=str(local_path))
|
|
101
|
-
except Exception as e:
|
|
102
|
-
raise StudioCliError(
|
|
103
|
-
f"Could not download the folder from the given Studio {studio}. "
|
|
104
|
-
"Please contact Lightning AI directly to resolve this issue."
|
|
105
|
-
) from e
|
|
106
|
-
|
|
107
|
-
def file(self, path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
|
|
46
|
+
def file(self, path: str, studio: Optional[str] = None, local_path: str = ".") -> None:
|
|
108
47
|
"""Download a file from a Studio.
|
|
109
48
|
|
|
110
49
|
Args:
|
|
@@ -115,25 +54,7 @@ class _Downloads(_StudiosMenu, _TeamspacesMenu):
|
|
|
115
54
|
with filtered studios will be shown for final selection.
|
|
116
55
|
local_path: The path to the directory you want to download the file to.
|
|
117
56
|
"""
|
|
118
|
-
|
|
119
|
-
if not local_path.is_dir():
|
|
120
|
-
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
121
|
-
|
|
122
|
-
resolved_studio = self._resolve_studio(studio)
|
|
123
|
-
|
|
124
|
-
if not path:
|
|
125
|
-
local_path /= resolved_studio.name
|
|
126
|
-
path = ""
|
|
127
|
-
|
|
128
|
-
try:
|
|
129
|
-
if not path:
|
|
130
|
-
raise FileNotFoundError()
|
|
131
|
-
resolved_studio.download_file(remote_path=path, file_path=str(local_path / os.path.basename(path)))
|
|
132
|
-
except Exception as e:
|
|
133
|
-
raise StudioCliError(
|
|
134
|
-
f"Could not download the file from the given Studio {studio}. "
|
|
135
|
-
"Please contact Lightning AI directly to resolve this issue."
|
|
136
|
-
) from e
|
|
57
|
+
file(path=path, studio=studio, local_path=local_path)
|
|
137
58
|
|
|
138
59
|
def container(self, container: str, teamspace: Optional[str] = None, tag: str = "latest") -> None:
|
|
139
60
|
"""Download a docker container from a teamspace.
|
|
@@ -143,9 +64,175 @@ class _Downloads(_StudiosMenu, _TeamspacesMenu):
|
|
|
143
64
|
teamspace: The name of the teamspace to download the container from.
|
|
144
65
|
tag: The tag of the container to download.
|
|
145
66
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
67
|
+
download_container(container=container, teamspace=teamspace, tag=tag)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@click.group(name="download")
|
|
71
|
+
def download() -> None:
|
|
72
|
+
"""Download resources from Lightning AI."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# @download.command(name="model")
|
|
76
|
+
# @click.option(
|
|
77
|
+
# "--name",
|
|
78
|
+
# help=(
|
|
79
|
+
# "The name of the Model you want to download. "
|
|
80
|
+
# "This should have the format <ORGANIZATION-NAME>/<TEAMSPACE-NAME>/<MODEL-NAME>."
|
|
81
|
+
# ),
|
|
82
|
+
# )
|
|
83
|
+
# @click.option("--download-dir", default=".", help="The directory where the Model should be downloaded.")
|
|
84
|
+
def model(name: str, download_dir: str = ".") -> None:
|
|
85
|
+
"""Download a Model."""
|
|
86
|
+
download_model(
|
|
87
|
+
name=name,
|
|
88
|
+
download_dir=download_dir,
|
|
89
|
+
progress_bar=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# @download.command(name="folder")
|
|
94
|
+
# @click.option(
|
|
95
|
+
# "--path",
|
|
96
|
+
# default="",
|
|
97
|
+
# help=(
|
|
98
|
+
# "The relative path within the Studio you want to download. "
|
|
99
|
+
# "If you leave it empty it will download whole studio and locally creates a "
|
|
100
|
+
# "new folder with the same name as the selected studio."
|
|
101
|
+
# ),
|
|
102
|
+
# )
|
|
103
|
+
# @click.option(
|
|
104
|
+
# "--studio",
|
|
105
|
+
# default=None,
|
|
106
|
+
# help=(
|
|
107
|
+
# "The name of the studio to upload to. "
|
|
108
|
+
# "Will show a menu with user's owned studios for selection if not specified. "
|
|
109
|
+
# "If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
|
|
110
|
+
# "The teamspace and studio names can be regular expressions to match, "
|
|
111
|
+
# "a menu filtered studios will be shown for final selection."
|
|
112
|
+
# ),
|
|
113
|
+
# )
|
|
114
|
+
# @click.option("--local-path", default=".", help="The path to the directory you want to download the folder to.")
|
|
115
|
+
def folder(path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
|
|
116
|
+
"""Download a folder from a Studio."""
|
|
117
|
+
local_path = Path(local_path)
|
|
118
|
+
if not local_path.is_dir():
|
|
119
|
+
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
120
|
+
|
|
121
|
+
menu = _StudiosMenu()
|
|
122
|
+
resolved_studio = menu._resolve_studio(studio)
|
|
123
|
+
|
|
124
|
+
if not path:
|
|
125
|
+
local_path /= resolved_studio.name
|
|
126
|
+
path = ""
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if not path:
|
|
130
|
+
raise FileNotFoundError()
|
|
131
|
+
resolved_studio.download_folder(remote_path=path, target_path=str(local_path))
|
|
132
|
+
except Exception as e:
|
|
133
|
+
raise StudioCliError(
|
|
134
|
+
f"Could not download the folder from the given Studio {studio}. "
|
|
135
|
+
"Please contact Lightning AI directly to resolve this issue."
|
|
136
|
+
) from e
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# @download.command(name="file")
|
|
140
|
+
# @click.option(
|
|
141
|
+
# "--path",
|
|
142
|
+
# default="",
|
|
143
|
+
# help=(
|
|
144
|
+
# "The relative path within the Studio you want to download. "
|
|
145
|
+
# "If you leave it empty it will download whole studio and locally creates a new folder "
|
|
146
|
+
# "with the same name as the selected studio."
|
|
147
|
+
# ),
|
|
148
|
+
# )
|
|
149
|
+
# @click.option(
|
|
150
|
+
# "--studio",
|
|
151
|
+
# default=None,
|
|
152
|
+
# help=(
|
|
153
|
+
# "The name of the studio to upload to. "
|
|
154
|
+
# "Will show a menu with user's owned studios for selection if not specified. "
|
|
155
|
+
# "If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
|
|
156
|
+
# "The teamspace and studio names can be regular expressions to match, "
|
|
157
|
+
# "a menu filtered studios will be shown for final selection."
|
|
158
|
+
# ),
|
|
159
|
+
# )
|
|
160
|
+
# @click.option("--local-path", default=".", help="The path to the directory you want to download the folder to.")
|
|
161
|
+
def file(path: str = "", studio: Optional[str] = None, local_path: str = ".") -> None:
|
|
162
|
+
"""Download a file from a Studio."""
|
|
163
|
+
local_path = Path(local_path)
|
|
164
|
+
if not local_path.is_dir():
|
|
165
|
+
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
166
|
+
|
|
167
|
+
resolved_studio = _resolve_studio(studio)
|
|
168
|
+
|
|
169
|
+
if not path:
|
|
170
|
+
local_path /= resolved_studio.name
|
|
171
|
+
path = ""
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
if not path:
|
|
175
|
+
raise FileNotFoundError()
|
|
176
|
+
resolved_studio.download_file(remote_path=path, file_path=str(local_path / os.path.basename(path)))
|
|
177
|
+
except Exception as e:
|
|
178
|
+
raise StudioCliError(
|
|
179
|
+
f"Could not download the file from the given Studio {studio}. "
|
|
180
|
+
"Please contact Lightning AI directly to resolve this issue."
|
|
181
|
+
) from e
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# @download.command(name="container")
|
|
185
|
+
# @click.argument("container")
|
|
186
|
+
# @click.argument("teamspace", default=None, help="The name of the teamspace to download the container from")
|
|
187
|
+
# @click.argument("tag", default="latest", show_default=True, help="The tag of the container to download.")
|
|
188
|
+
def download_container(container: str, teamspace: Optional[str] = None, tag: str = "latest") -> None:
|
|
189
|
+
"""Download the docker container CONTAINER from a teamspace."""
|
|
190
|
+
console = Console()
|
|
191
|
+
menu = _TeamspacesMenu()
|
|
192
|
+
resolved_teamspace = menu._resolve_teamspace(teamspace)
|
|
193
|
+
with console.status("Downloading container..."):
|
|
194
|
+
api = LitContainerApi()
|
|
195
|
+
api.download_container(container, resolved_teamspace, tag)
|
|
196
|
+
console.print("Container downloaded successfully", style="green")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _resolve_studio(studio: Optional[str]) -> Studio:
|
|
200
|
+
user = _get_authed_user()
|
|
201
|
+
# if no studio specify suggest/filter only user's studios
|
|
202
|
+
menu = _StudiosMenu()
|
|
203
|
+
possible_studios = menu._get_possible_studios(user, is_owner=studio is None)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
if studio:
|
|
207
|
+
team_name, studio_name = studio.split("/")
|
|
208
|
+
options = [st for st in possible_studios if st["teamspace"] == team_name and st["name"] == studio_name]
|
|
209
|
+
if len(options) == 1:
|
|
210
|
+
selected_studio = menu._get_studio_from_name(studio, possible_studios)
|
|
211
|
+
# user can also use the partial studio name as secondary interactive selection
|
|
212
|
+
else:
|
|
213
|
+
# filter matching simple reg expressions or start with the team and studio name
|
|
214
|
+
possible_studios = filter(
|
|
215
|
+
lambda st: (re.match(team_name, st["teamspace"]) or team_name in st["teamspace"])
|
|
216
|
+
and (re.match(studio_name, st["name"]) or studio_name in st["name"]),
|
|
217
|
+
possible_studios,
|
|
218
|
+
)
|
|
219
|
+
if not possible_studios:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Could not find Studio like '{studio}', please consider update your filtering pattern."
|
|
222
|
+
)
|
|
223
|
+
selected_studio = menu._get_studio_from_interactive_menu(list(possible_studios))
|
|
224
|
+
else:
|
|
225
|
+
selected_studio = menu._get_studio_from_interactive_menu(possible_studios)
|
|
226
|
+
|
|
227
|
+
except KeyboardInterrupt:
|
|
228
|
+
raise KeyboardInterrupt from None
|
|
229
|
+
|
|
230
|
+
# give user friendlier error message
|
|
231
|
+
except Exception as e:
|
|
232
|
+
raise StudioCliError(
|
|
233
|
+
f"Could not find the given Studio {studio} to upload files to. "
|
|
234
|
+
"Please contact Lightning AI directly to resolve this issue."
|
|
235
|
+
) from e
|
|
236
|
+
|
|
237
|
+
with skip_studio_init():
|
|
238
|
+
return Studio(**selected_studio)
|
lightning_sdk/cli/entrypoint.py
CHANGED
|
@@ -2,15 +2,20 @@ import sys
|
|
|
2
2
|
from types import TracebackType
|
|
3
3
|
from typing import Type
|
|
4
4
|
|
|
5
|
+
import click
|
|
5
6
|
from fire import Fire
|
|
6
7
|
from lightning_utilities.core.imports import RequirementCache
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
7
10
|
|
|
8
11
|
from lightning_sdk.api.studio_api import _cloud_url
|
|
9
|
-
from lightning_sdk.cli.ai_hub import _AIHub
|
|
10
|
-
from lightning_sdk.cli.
|
|
11
|
-
from lightning_sdk.cli.
|
|
12
|
-
from lightning_sdk.cli.
|
|
13
|
-
from lightning_sdk.cli.
|
|
12
|
+
from lightning_sdk.cli.ai_hub import _AIHub, aihub
|
|
13
|
+
from lightning_sdk.cli.configure import _Configure, configure
|
|
14
|
+
from lightning_sdk.cli.connect import _Connect, connect
|
|
15
|
+
from lightning_sdk.cli.delete import _Delete, delete
|
|
16
|
+
from lightning_sdk.cli.download import _Downloads, download
|
|
17
|
+
from lightning_sdk.cli.generate import _Generate, generate
|
|
18
|
+
from lightning_sdk.cli.inspect import _Inspect, inspect
|
|
14
19
|
from lightning_sdk.cli.legacy import _LegacyLightningCLI
|
|
15
20
|
from lightning_sdk.cli.list import _List
|
|
16
21
|
from lightning_sdk.cli.run import _Run
|
|
@@ -41,28 +46,24 @@ class StudioCLI:
|
|
|
41
46
|
self.start = _Start()
|
|
42
47
|
self.switch = _Switch()
|
|
43
48
|
self.generate = _Generate()
|
|
49
|
+
self.connect = _Connect()
|
|
50
|
+
self.configure = _Configure()
|
|
44
51
|
|
|
45
52
|
sys.excepthook = _notify_exception
|
|
46
53
|
|
|
47
54
|
def login(self) -> None:
|
|
48
55
|
"""Login to Lightning AI Studios."""
|
|
49
|
-
|
|
50
|
-
auth.clear()
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
auth.authenticate()
|
|
54
|
-
except ConnectionError:
|
|
55
|
-
raise RuntimeError(f"Unable to connect to {_cloud_url()}. Please check your internet connection.") from None
|
|
56
|
+
return login()
|
|
56
57
|
|
|
57
58
|
def logout(self) -> None:
|
|
58
59
|
"""Logout from Lightning AI Studios."""
|
|
59
|
-
|
|
60
|
-
auth.clear()
|
|
60
|
+
return logout()
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def _notify_exception(exception_type: Type[BaseException], value: BaseException, tb: TracebackType) -> None: # No
|
|
64
64
|
"""CLI won't show tracebacks, just print the exception message."""
|
|
65
|
-
|
|
65
|
+
console = Console()
|
|
66
|
+
console.print(Panel(value))
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
def main_cli() -> None:
|
|
@@ -70,5 +71,39 @@ def main_cli() -> None:
|
|
|
70
71
|
Fire(StudioCLI(), name="lightning")
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
@click.group(name="lightning", help="Command line interface (CLI) to interact with/manage Lightning AI Studios.")
|
|
75
|
+
def main_cli_click() -> None:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# @main_cli_click.command
|
|
80
|
+
def login() -> None:
|
|
81
|
+
"""Login to Lightning AI Studios."""
|
|
82
|
+
auth = Auth()
|
|
83
|
+
auth.clear()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
auth.authenticate()
|
|
87
|
+
except ConnectionError:
|
|
88
|
+
raise RuntimeError(f"Unable to connect to {_cloud_url()}. Please check your internet connection.") from None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# @main_cli_click.command
|
|
92
|
+
def logout() -> None:
|
|
93
|
+
"""Logout from Lightning AI Studios."""
|
|
94
|
+
auth = Auth()
|
|
95
|
+
auth.clear()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# TODO: handle exception hook registration
|
|
99
|
+
main_cli_click.add_command(aihub)
|
|
100
|
+
main_cli_click.add_command(configure)
|
|
101
|
+
main_cli_click.add_command(connect)
|
|
102
|
+
main_cli_click.add_command(delete)
|
|
103
|
+
main_cli_click.add_command(download)
|
|
104
|
+
main_cli_click.add_command(generate)
|
|
105
|
+
main_cli_click.add_command(inspect)
|
|
106
|
+
|
|
107
|
+
|
|
73
108
|
if __name__ == "__main__":
|
|
74
109
|
main_cli()
|
lightning_sdk/cli/generate.py
CHANGED
|
@@ -1,58 +1,67 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
+
import click
|
|
3
4
|
from rich.console import Console
|
|
4
5
|
|
|
5
|
-
from lightning_sdk import
|
|
6
|
+
from lightning_sdk.cli.studios_menu import _StudiosMenu
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class _Generate:
|
|
9
10
|
"""Generate configs (such as ssh for studio) and print them to commandline."""
|
|
10
11
|
|
|
11
|
-
console = Console()
|
|
12
|
-
|
|
13
|
-
def _generate_ssh_config(self, name: str, studio_id: str) -> str:
|
|
14
|
-
"""Generate SSH config entry for the studio.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
name: Studio name
|
|
18
|
-
studio_id: Studio space ID
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
str: SSH config entry
|
|
22
|
-
"""
|
|
23
|
-
return f"""# ssh s_{studio_id}@ssh.lightning.ai
|
|
24
|
-
|
|
25
|
-
Host {name}
|
|
26
|
-
User s_{studio_id}
|
|
27
|
-
Hostname ssh.lightning.ai
|
|
28
|
-
IdentityFile ~/.ssh/lightning_rsa
|
|
29
|
-
IdentitiesOnly yes
|
|
30
|
-
ServerAliveInterval 15
|
|
31
|
-
ServerAliveCountMax 4
|
|
32
|
-
StrictHostKeyChecking no
|
|
33
|
-
UserKnownHostsFile=/dev/null"""
|
|
34
|
-
|
|
35
12
|
def ssh(self, name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
36
|
-
"""Get SSH config entry for a studio.
|
|
13
|
+
"""Get SSH config entry for a studio.
|
|
37
14
|
|
|
38
15
|
Args:
|
|
39
|
-
name: The name of the studio to
|
|
16
|
+
name: The name of the studio to obtain SSH config.
|
|
40
17
|
If not specified, tries to infer from the environment (e.g. when run from within a Studio.)
|
|
41
18
|
teamspace: The teamspace the studio is part of. Should be of format <OWNER>/<TEAMSPACE_NAME>.
|
|
42
19
|
If not specified, tries to infer from the environment (e.g. when run from within a Studio.)
|
|
43
20
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
21
|
+
ssh(name=name, teamspace=teamspace)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _generate_ssh_config(key_path: str, host: str, user: str) -> str:
|
|
25
|
+
return f"""Host {host}
|
|
26
|
+
User {user}
|
|
27
|
+
Hostname ssh.lightning.ai
|
|
28
|
+
IdentityFile {key_path}
|
|
29
|
+
IdentitiesOnly yes
|
|
30
|
+
ServerAliveInterval 15
|
|
31
|
+
ServerAliveCountMax 4
|
|
32
|
+
StrictHostKeyChecking no
|
|
33
|
+
UserKnownHostsFile=/dev/null
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.group(name="generate")
|
|
38
|
+
def generate() -> None:
|
|
39
|
+
"""Generate configs (such as ssh for studio) and print them to commandline."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# @generate.command(name="ssh")
|
|
43
|
+
# @click.option(
|
|
44
|
+
# "--name",
|
|
45
|
+
# default=None,
|
|
46
|
+
# help=(
|
|
47
|
+
# "The name of the studio to obtain SSH config. "
|
|
48
|
+
# "If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
|
|
49
|
+
# ),
|
|
50
|
+
# )
|
|
51
|
+
# @click.option(
|
|
52
|
+
# "--teamspace",
|
|
53
|
+
# default=None,
|
|
54
|
+
# help=(
|
|
55
|
+
# "The teamspace the studio is part of. "
|
|
56
|
+
# "Should be of format <OWNER>/<TEAMSPACE_NAME>. "
|
|
57
|
+
# "If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
|
|
58
|
+
# ),
|
|
59
|
+
# )
|
|
60
|
+
def ssh(name: Optional[str] = None, teamspace: Optional[str] = None) -> None:
|
|
61
|
+
"""Get SSH config entry for a studio."""
|
|
62
|
+
menu = _StudiosMenu()
|
|
63
|
+
studio = menu._get_studio(name=name, teamspace=teamspace)
|
|
64
|
+
|
|
65
|
+
conf = _generate_ssh_config(key_path="~/.ssh/lightning_rsa", user=f"s_{studio._studio.id}", host=studio.name)
|
|
66
|
+
# Print the SSH config
|
|
67
|
+
Console().print(f"# ssh s_{studio._studio.id}@ssh.lightning.ai\n\n" + conf)
|