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.
Files changed (72) hide show
  1. lightning_sdk/__init__.py +3 -2
  2. lightning_sdk/ai_hub.py +22 -0
  3. lightning_sdk/api/ai_hub_api.py +21 -2
  4. lightning_sdk/api/deployment_api.py +4 -3
  5. lightning_sdk/api/job_api.py +5 -10
  6. lightning_sdk/api/mmt_api.py +1 -4
  7. lightning_sdk/api/studio_api.py +5 -7
  8. lightning_sdk/api/teamspace_api.py +7 -0
  9. lightning_sdk/api/utils.py +1 -27
  10. lightning_sdk/cli/ai_hub.py +61 -10
  11. lightning_sdk/cli/configure.py +137 -0
  12. lightning_sdk/cli/connect.py +47 -0
  13. lightning_sdk/cli/delete.py +83 -32
  14. lightning_sdk/cli/download.py +177 -90
  15. lightning_sdk/cli/entrypoint.py +50 -15
  16. lightning_sdk/cli/generate.py +51 -42
  17. lightning_sdk/cli/inspect.py +45 -3
  18. lightning_sdk/cli/jobs_menu.py +2 -1
  19. lightning_sdk/cli/list.py +139 -55
  20. lightning_sdk/cli/mmts_menu.py +2 -1
  21. lightning_sdk/cli/run.py +3 -9
  22. lightning_sdk/cli/serve.py +1 -2
  23. lightning_sdk/cli/start.py +2 -2
  24. lightning_sdk/cli/stop.py +5 -3
  25. lightning_sdk/cli/studios_menu.py +24 -1
  26. lightning_sdk/cli/switch.py +2 -2
  27. lightning_sdk/cli/teamspace_menu.py +2 -1
  28. lightning_sdk/cli/upload.py +6 -4
  29. lightning_sdk/helpers.py +20 -0
  30. lightning_sdk/job/job.py +1 -1
  31. lightning_sdk/lightning_cloud/openapi/__init__.py +9 -0
  32. lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +105 -0
  33. lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +105 -0
  34. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +226 -0
  35. lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +4 -4
  36. lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +7 -3
  37. lightning_sdk/lightning_cloud/openapi/api/projects_service_api.py +1 -5
  38. lightning_sdk/lightning_cloud/openapi/models/__init__.py +9 -0
  39. lightning_sdk/lightning_cloud/openapi/models/agents_id_body.py +105 -1
  40. lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +29 -3
  41. lightning_sdk/lightning_cloud/openapi/models/id_reportrestarttimings_body.py +123 -0
  42. lightning_sdk/lightning_cloud/openapi/models/id_visibility_body1.py +149 -0
  43. lightning_sdk/lightning_cloud/openapi/models/model_id_visibility_body.py +27 -1
  44. lightning_sdk/lightning_cloud/openapi/models/project_id_litregistry_body.py +2 -0
  45. lightning_sdk/lightning_cloud/openapi/models/setup.py +149 -0
  46. lightning_sdk/lightning_cloud/openapi/models/v1_assistant.py +105 -1
  47. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
  48. lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +29 -3
  49. lightning_sdk/lightning_cloud/openapi/models/v1_gcp_data_connection_setup.py +123 -0
  50. lightning_sdk/lightning_cloud/openapi/models/v1_get_cluster_accelerator_demand_response.py +123 -0
  51. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
  52. lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +27 -1
  53. lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_artifact.py +27 -1
  54. lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_project.py +8 -0
  55. lightning_sdk/lightning_cloud/openapi/models/v1_lit_repository.py +27 -1
  56. lightning_sdk/lightning_cloud/openapi/models/v1_report_restart_timings_response.py +97 -0
  57. lightning_sdk/lightning_cloud/openapi/models/v1_restart_timing.py +175 -0
  58. lightning_sdk/lightning_cloud/openapi/models/v1_setup_data_connection_response.py +123 -0
  59. lightning_sdk/lightning_cloud/openapi/models/v1_update_deployment_visibility_response.py +97 -0
  60. lightning_sdk/lightning_cloud/openapi/models/v1_update_metrics_stream_visibility_response.py +27 -1
  61. lightning_sdk/lightning_cloud/openapi/models/v1_update_model_visibility_response.py +27 -1
  62. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +79 -1
  63. lightning_sdk/lightning_cloud/openapi/models/v1_validate_deployment_image_request.py +27 -1
  64. lightning_sdk/machine.py +59 -27
  65. lightning_sdk/studio.py +5 -1
  66. lightning_sdk/teamspace.py +25 -0
  67. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/METADATA +3 -1
  68. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/RECORD +72 -61
  69. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/LICENSE +0 -0
  70. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/WHEEL +0 -0
  71. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/entry_points.txt +0 -0
  72. {lightning_sdk-0.1.55.dist-info → lightning_sdk-0.1.57.dist-info}/top_level.txt +0 -0
@@ -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
- api = LitContainer()
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 = super().job(name=name, teamspace=teamspace)
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 = super().mmt(name=name, teamspace=teamspace)
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
- if teamspace is not None:
72
- ts_splits = teamspace.split("/")
73
- if len(ts_splits) != 2:
74
- raise ValueError(f"Teamspace should be of format <OWNER>/<TEAMSPACE_NAME> but got {teamspace}")
75
- owner, teamspace = ts_splits
76
- else:
77
- owner, teamspace = None, None
78
-
79
- try:
80
- studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False)
81
- except (RuntimeError, ValueError):
82
- studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
83
-
84
- studio.delete()
85
- print("Studio successfully deleted")
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")
@@ -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
- download_model(
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
- local_path = Path(local_path)
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
- if not path:
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
- local_path = Path(local_path)
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
- resolved_teamspace = self._resolve_teamspace(teamspace)
147
- console = Console()
148
- with console.status("Downloading container..."):
149
- api = LitContainerApi()
150
- api.download_container(container, resolved_teamspace, tag)
151
- console.print("Container downloaded successfully", style="green")
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)
@@ -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.delete import _Delete
11
- from lightning_sdk.cli.download import _Downloads
12
- from lightning_sdk.cli.generate import _Generate
13
- from lightning_sdk.cli.inspect import _Inspect
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
- auth = Auth()
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
- auth = Auth()
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
- print(value)
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()
@@ -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 Studio
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. Will start the studio if needed.
13
+ """Get SSH config entry for a studio.
37
14
 
38
15
  Args:
39
- name: The name of the studio to stop.
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
- if teamspace:
45
- ts_splits = teamspace.split("/")
46
- if len(ts_splits) != 2:
47
- raise ValueError(f"Teamspace should be of format <OWNER>/<TEAMSPACE_NAME> but got {teamspace}")
48
- owner, teamspace = ts_splits
49
- else:
50
- owner, teamspace = None, None
51
-
52
- try:
53
- studio = Studio(name=name, teamspace=teamspace, org=owner, user=None, create_ok=False)
54
- except (RuntimeError, ValueError):
55
- studio = Studio(name=name, teamspace=teamspace, org=None, user=owner, create_ok=False)
56
-
57
- # Print the SSH config
58
- self.console.print(self._generate_ssh_config(name, studio._studio.id))
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)