lightning-sdk 2026.1.22__py3-none-any.whl → 2026.1.30__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/__version__.py +1 -1
- lightning_sdk/api/studio_api.py +38 -39
- lightning_sdk/api/teamspace_api.py +189 -72
- lightning_sdk/api/utils.py +69 -1
- lightning_sdk/cli/cp/__init__.py +14 -11
- lightning_sdk/cli/cp/teamspace_uploads.py +95 -0
- lightning_sdk/cli/legacy/download.py +29 -98
- lightning_sdk/cli/legacy/upload.py +24 -31
- lightning_sdk/cli/studio/cp.py +8 -5
- lightning_sdk/cli/studio/ls.py +1 -1
- lightning_sdk/cli/studio/rm.py +1 -1
- lightning_sdk/cli/utils/{studio_filesystem.py → filesystem.py} +49 -6
- lightning_sdk/exceptions.py +27 -0
- lightning_sdk/lightning_cloud/openapi/__init__.py +17 -12
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +286 -468
- lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +579 -0
- lightning_sdk/lightning_cloud/openapi/api/data_connection_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/api/file_system_service_api.py +11 -11
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +113 -0
- lightning_sdk/lightning_cloud/openapi/api/organizations_service_api.py +113 -0
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +5 -1
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +16 -12
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → cluster_service_get_cluster_capacity_reservation_body.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → container_registry_config_ecr.py} +49 -23
- lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_status.py → container_registry_provider.py} +14 -10
- lightning_sdk/lightning_cloud/openapi/models/container_registry_service_create_container_registry_body.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config_input.py → container_registry_service_refresh_container_registry_credentials_body.py} +21 -21
- lightning_sdk/lightning_cloud/openapi/models/jobs_service_duplicate_deployment_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry.py +63 -89
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_add_container_registry_body.py → v1_container_registry_config.py} +16 -16
- lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_container_registry_scopes.py} +39 -39
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_create_container_registry_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_org_cluster_capacity_reservation_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_describe_org_cluster_capacity_reservation_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → v1_get_cluster_capacity_reservation_response.py} +23 -23
- lightning_sdk/lightning_cloud/openapi/models/v1_job.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → v1_mithril_direct_v1.py} +51 -51
- lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_slack_notifier.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_update_container_registry_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +53 -105
- lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
- lightning_sdk/teamspace.py +28 -7
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/METADATA +1 -1
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/RECORD +59 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
- lightning_sdk/lightning_cloud/openapi/models/v1_ecr_registry_details.py +0 -201
- /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/LICENSE +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/WHEEL +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2026.1.22.dist-info → lightning_sdk-2026.1.30.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from lightning_sdk.api.utils import _get_cloud_url
|
|
7
|
+
from lightning_sdk.cli.utils.filesystem import parse_teamspace_uploads_path, path_join, resolve_teamspace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cp_upload(
|
|
11
|
+
local_file_path: str,
|
|
12
|
+
teamspace_path: str,
|
|
13
|
+
options: dict[str, any],
|
|
14
|
+
) -> None:
|
|
15
|
+
console = Console()
|
|
16
|
+
recursive = options.get("recursive", False)
|
|
17
|
+
cloud_account = options.get("cloud_account", None)
|
|
18
|
+
if not Path(local_file_path).exists():
|
|
19
|
+
raise FileNotFoundError(f"The provided path does not exist: {local_file_path}")
|
|
20
|
+
|
|
21
|
+
teamspace_path_result = parse_teamspace_uploads_path(teamspace_path)
|
|
22
|
+
|
|
23
|
+
teamspace_path_result["destination"] = path_join("Uploads", teamspace_path_result["destination"])
|
|
24
|
+
|
|
25
|
+
selected_teamspace = resolve_teamspace(teamspace_path_result["teamspace"], teamspace_path_result["owner"])
|
|
26
|
+
console.print(f"Uploading to {selected_teamspace.owner.name}/{selected_teamspace.name}")
|
|
27
|
+
|
|
28
|
+
if Path(local_file_path).is_dir():
|
|
29
|
+
if not recursive:
|
|
30
|
+
raise ValueError(f"'{local_file_path}' is a directory. Use -r flag to copy directories recursively.")
|
|
31
|
+
selected_teamspace.upload_folder(
|
|
32
|
+
local_file_path, teamspace_path_result["destination"], cloud_account=cloud_account
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
if teamspace_path.endswith(("/", "\\")):
|
|
36
|
+
# if destination ends with / or \, treat it as a directory
|
|
37
|
+
file_name = os.path.basename(local_file_path)
|
|
38
|
+
teamspace_path_result["destination"] = path_join(teamspace_path_result["destination"], file_name)
|
|
39
|
+
selected_teamspace.upload_file(
|
|
40
|
+
local_file_path, teamspace_path_result["destination"], cloud_account=cloud_account
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
studio_url = (
|
|
44
|
+
_get_cloud_url().replace(":443", "") + "/" + selected_teamspace.owner.name + "/" + selected_teamspace.name
|
|
45
|
+
)
|
|
46
|
+
console.print(f"See your file at {studio_url}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cp_download(
|
|
50
|
+
teamspace_path: str,
|
|
51
|
+
local_path: str,
|
|
52
|
+
options: dict[str, any],
|
|
53
|
+
) -> None:
|
|
54
|
+
console = Console()
|
|
55
|
+
teamspace_path_result = parse_teamspace_uploads_path(teamspace_path)
|
|
56
|
+
recursive = options.get("recursive", False)
|
|
57
|
+
|
|
58
|
+
selected_teamspace = resolve_teamspace(teamspace_path_result["teamspace"], teamspace_path_result["owner"])
|
|
59
|
+
|
|
60
|
+
# check if file/folder exists
|
|
61
|
+
path_info = selected_teamspace._teamspace_api.get_path_info(
|
|
62
|
+
selected_teamspace._teamspace.id, path=teamspace_path_result["destination"]
|
|
63
|
+
)
|
|
64
|
+
if not path_info["exists"]:
|
|
65
|
+
raise FileNotFoundError(
|
|
66
|
+
f"The provided path does not exist in the teamspace drive: {teamspace_path_result['destination']} "
|
|
67
|
+
"Note that empty folders may not be detected as existing."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
console.print(f"Downloading from {selected_teamspace.owner.name}/{selected_teamspace.name}")
|
|
71
|
+
if path_info["type"] == "directory":
|
|
72
|
+
if not recursive:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"'{teamspace_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
|
|
75
|
+
)
|
|
76
|
+
folder_name = os.path.basename(teamspace_path_result["destination"].rstrip("/"))
|
|
77
|
+
if local_path in ("./", "."):
|
|
78
|
+
if folder_name == "":
|
|
79
|
+
folder_name = f"{selected_teamspace.name}_downloads"
|
|
80
|
+
target_path = os.path.join(local_path, folder_name)
|
|
81
|
+
else:
|
|
82
|
+
target_path = local_path
|
|
83
|
+
|
|
84
|
+
selected_teamspace.download_folder(teamspace_path_result["destination"], target_path)
|
|
85
|
+
console.print(f"See your folder at {target_path}")
|
|
86
|
+
else:
|
|
87
|
+
if os.path.isdir(local_path) or local_path.endswith(("/", "\\")):
|
|
88
|
+
# if local_path ends with / or \ or is a directory, treat it as a directory
|
|
89
|
+
file_name = os.path.basename(teamspace_path_result["destination"])
|
|
90
|
+
target_path = os.path.join(local_path, file_name)
|
|
91
|
+
else:
|
|
92
|
+
target_path = local_path
|
|
93
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
94
|
+
selected_teamspace.download_file(teamspace_path_result["destination"], target_path)
|
|
95
|
+
console.print(f"See your file at {target_path}")
|
|
@@ -12,6 +12,7 @@ from lightning_sdk.api.lit_container_api import LitContainerApi
|
|
|
12
12
|
from lightning_sdk.cli.legacy.exceptions import StudioCliError
|
|
13
13
|
from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
|
|
14
14
|
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
15
|
+
from lightning_sdk.exceptions import DeprecatedCommand, DeprecatedError
|
|
15
16
|
from lightning_sdk.models import download_model
|
|
16
17
|
from lightning_sdk.studio import Studio
|
|
17
18
|
from lightning_sdk.utils.resolve import _get_authed_user
|
|
@@ -63,134 +64,64 @@ def model(name: str, download_dir: str = ".") -> None:
|
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
@download.command(
|
|
67
|
-
|
|
67
|
+
@download.command(
|
|
68
|
+
name="folder",
|
|
69
|
+
cls=DeprecatedCommand,
|
|
70
|
+
message="Studio downloads via 'lightning download folder' are deprecated. Use 'lightning studio cp -r' instead.",
|
|
71
|
+
)
|
|
72
|
+
@click.argument("path", required=False, nargs=-1)
|
|
68
73
|
@click.option(
|
|
69
74
|
"--studio",
|
|
70
75
|
default=None,
|
|
71
|
-
|
|
72
|
-
"The name of the studio to download from. "
|
|
73
|
-
"Will show a menu with user's owned studios for selection if not specified. "
|
|
74
|
-
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
|
|
75
|
-
"The teamspace and studio names can be regular expressions to match, "
|
|
76
|
-
"a menu filtered studios will be shown for final selection."
|
|
77
|
-
),
|
|
76
|
+
hidden=True,
|
|
78
77
|
)
|
|
79
78
|
@click.option(
|
|
80
79
|
"--teamspace",
|
|
81
80
|
default=None,
|
|
82
|
-
|
|
81
|
+
hidden=True,
|
|
83
82
|
)
|
|
84
83
|
@click.option(
|
|
85
84
|
"--local-path",
|
|
86
85
|
"--local_path",
|
|
87
|
-
default=
|
|
88
|
-
|
|
89
|
-
help="The path to the directory you want to download the folder to.",
|
|
86
|
+
default=None,
|
|
87
|
+
hidden=True,
|
|
90
88
|
)
|
|
91
89
|
def folder(
|
|
92
90
|
path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = "."
|
|
93
91
|
) -> None:
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
PATH: The relative path within the Studio or drive folder you want to download.
|
|
100
|
-
Defaults to the entire Studio or drive folder.
|
|
101
|
-
"""
|
|
102
|
-
local_path = Path(local_path)
|
|
103
|
-
if not local_path.is_dir():
|
|
104
|
-
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
105
|
-
|
|
106
|
-
if studio and teamspace:
|
|
107
|
-
raise ValueError("Either --studio or --teamspace must be provided, not both")
|
|
108
|
-
|
|
109
|
-
if studio:
|
|
110
|
-
path = _expand_remote_path(path)
|
|
111
|
-
resolved_downloader = _resolve_studio(studio)
|
|
112
|
-
elif teamspace:
|
|
113
|
-
menu = TeamspacesMenu()
|
|
114
|
-
resolved_downloader = menu(teamspace)
|
|
115
|
-
else:
|
|
116
|
-
raise ValueError("Either --studio or --teamspace must be provided")
|
|
117
|
-
|
|
118
|
-
if not path:
|
|
119
|
-
local_path /= resolved_downloader.name
|
|
120
|
-
path = ""
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
if not path and teamspace:
|
|
124
|
-
raise FileNotFoundError()
|
|
125
|
-
resolved_downloader.download_folder(remote_path=path, target_path=str(local_path))
|
|
126
|
-
except Exception as e:
|
|
127
|
-
raise StudioCliError(
|
|
128
|
-
f"Could not download the folder from the given Studio {studio} or Teamspace {teamspace}. "
|
|
129
|
-
"Please contact Lightning AI directly to resolve this issue."
|
|
130
|
-
) from e
|
|
92
|
+
"""[DEPRECATED] Use 'lightning studio cp -r' instead."""
|
|
93
|
+
raise DeprecatedError(
|
|
94
|
+
"Studio downloads via 'lightning download folder' are deprecated. Use 'lightning studio cp -r' instead."
|
|
95
|
+
)
|
|
131
96
|
|
|
132
97
|
|
|
133
|
-
@download.command(
|
|
134
|
-
|
|
98
|
+
@download.command(
|
|
99
|
+
name="file",
|
|
100
|
+
cls=DeprecatedCommand,
|
|
101
|
+
message="Studio downloads via 'lightning download file' are deprecated. Use 'lightning studio cp' instead.",
|
|
102
|
+
)
|
|
103
|
+
@click.argument("path", required=False, nargs=-1)
|
|
135
104
|
@click.option(
|
|
136
105
|
"--studio",
|
|
137
106
|
default=None,
|
|
138
|
-
|
|
139
|
-
"The name of the studio to download from. "
|
|
140
|
-
"Will show a menu with user's owned studios for selection if not specified. "
|
|
141
|
-
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME> where the names are case-sensitive. "
|
|
142
|
-
"The teamspace and studio names can be regular expressions to match, "
|
|
143
|
-
"a menu filtered studios will be shown for final selection."
|
|
144
|
-
),
|
|
107
|
+
hidden=True,
|
|
145
108
|
)
|
|
146
109
|
@click.option(
|
|
147
110
|
"--teamspace",
|
|
148
111
|
default=None,
|
|
149
|
-
|
|
112
|
+
hidden=True,
|
|
150
113
|
)
|
|
151
114
|
@click.option(
|
|
152
115
|
"--local-path",
|
|
153
116
|
"--local_path",
|
|
154
|
-
default=
|
|
155
|
-
|
|
156
|
-
help="The path to the directory you want to download the file to.",
|
|
117
|
+
default=None,
|
|
118
|
+
hidden=True,
|
|
157
119
|
)
|
|
158
120
|
def file(path: str = "", studio: Optional[str] = None, teamspace: Optional[str] = None, local_path: str = ".") -> None:
|
|
159
|
-
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
PATH: The relative path to the file within the Studio or Teamspace drive file you want to download.
|
|
165
|
-
"""
|
|
166
|
-
local_path = Path(local_path)
|
|
167
|
-
if not local_path.is_dir():
|
|
168
|
-
raise NotADirectoryError(f"'{local_path}' is not a directory")
|
|
169
|
-
|
|
170
|
-
if studio and teamspace:
|
|
171
|
-
raise ValueError("Either --studio or --teamspace must be provided, not both")
|
|
172
|
-
|
|
173
|
-
if studio:
|
|
174
|
-
resolved_downloader = _resolve_studio(studio)
|
|
175
|
-
elif teamspace:
|
|
176
|
-
menu = TeamspacesMenu()
|
|
177
|
-
resolved_downloader = menu(teamspace)
|
|
178
|
-
else:
|
|
179
|
-
raise ValueError("Either --studio or --teamspace must be provided")
|
|
180
|
-
|
|
181
|
-
if not path:
|
|
182
|
-
local_path /= resolved_downloader.name
|
|
183
|
-
path = ""
|
|
184
|
-
|
|
185
|
-
try:
|
|
186
|
-
if not path:
|
|
187
|
-
raise FileNotFoundError()
|
|
188
|
-
resolved_downloader.download_file(remote_path=path, file_path=str(local_path / os.path.basename(path)))
|
|
189
|
-
except Exception as e:
|
|
190
|
-
raise StudioCliError(
|
|
191
|
-
f"Could not download the file from the given Studio {studio} or Teamspace {teamspace}. "
|
|
192
|
-
"Please contact Lightning AI directly to resolve this issue."
|
|
193
|
-
) from e
|
|
121
|
+
"""[DEPRECATED] Use 'lightning studio cp' instead."""
|
|
122
|
+
raise DeprecatedError(
|
|
123
|
+
"Studio downloads via 'lightning download file' are deprecated. Use 'lightning studio cp' instead."
|
|
124
|
+
)
|
|
194
125
|
|
|
195
126
|
|
|
196
127
|
@download.command(name="container")
|
|
@@ -18,6 +18,7 @@ from lightning_sdk.cli.legacy.exceptions import StudioCliError
|
|
|
18
18
|
from lightning_sdk.cli.legacy.studios_menu import _StudiosMenu
|
|
19
19
|
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
20
20
|
from lightning_sdk.constants import _LIGHTNING_DEBUG
|
|
21
|
+
from lightning_sdk.exceptions import DeprecatedCommand, DeprecatedError
|
|
21
22
|
from lightning_sdk.models import upload_model as _upload_model
|
|
22
23
|
from lightning_sdk.studio import Studio
|
|
23
24
|
from lightning_sdk.utils.resolve import _get_authed_user
|
|
@@ -54,56 +55,48 @@ def model(name: str, path: str = ".", cloud_account: Optional[str] = None) -> No
|
|
|
54
55
|
_upload_model(name, path, cloud_account=cloud_account)
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
@upload.command(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
default=None,
|
|
62
|
-
help=(
|
|
63
|
-
"The name of the studio to upload to. "
|
|
64
|
-
"Will show a menu for selection if not specified. "
|
|
65
|
-
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>"
|
|
66
|
-
),
|
|
58
|
+
@upload.command(
|
|
59
|
+
name="folder",
|
|
60
|
+
cls=DeprecatedCommand,
|
|
61
|
+
message="Studio uploads via 'lightning upload folder' are deprecated. Use 'lightning studio cp -r' instead.",
|
|
67
62
|
)
|
|
63
|
+
@click.argument("path", type=click.Path(), required=False, nargs=-1)
|
|
64
|
+
@click.option("--studio", default=None, hidden=True)
|
|
68
65
|
@click.option(
|
|
69
66
|
"--remote-path",
|
|
70
67
|
"--remote_path",
|
|
71
68
|
default=None,
|
|
72
|
-
|
|
73
|
-
"The path where the uploaded file should appear on your Studio. "
|
|
74
|
-
"Has to be within your Studio's home directory and will be relative to that. "
|
|
75
|
-
"If not specified, will use the name of the folder you want to upload and place it in your home directory."
|
|
76
|
-
),
|
|
69
|
+
hidden=True,
|
|
77
70
|
)
|
|
78
71
|
def folder(path: str, studio: Optional[str], remote_path: Optional[str]) -> None:
|
|
79
|
-
"""
|
|
80
|
-
|
|
72
|
+
"""[DEPRECATED] Use 'lightning studio cp -r' instead."""
|
|
73
|
+
raise DeprecatedError(
|
|
74
|
+
"Studio uploads via 'lightning upload folder' are deprecated. Use 'lightning studio cp -r' instead."
|
|
75
|
+
)
|
|
81
76
|
|
|
82
77
|
|
|
83
|
-
@upload.command(
|
|
84
|
-
|
|
78
|
+
@upload.command(
|
|
79
|
+
name="file",
|
|
80
|
+
cls=DeprecatedCommand,
|
|
81
|
+
message="Studio uploads via 'lightning upload file' are deprecated. Use 'lightning studio cp' instead.",
|
|
82
|
+
)
|
|
83
|
+
@click.argument("path", type=click.Path(), required=False, nargs=-1)
|
|
85
84
|
@click.option(
|
|
86
85
|
"--studio",
|
|
87
86
|
default=None,
|
|
88
|
-
|
|
89
|
-
"The name of the studio to upload to. "
|
|
90
|
-
"Will show a menu for selection if not specified. "
|
|
91
|
-
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>"
|
|
92
|
-
),
|
|
87
|
+
hidden=True,
|
|
93
88
|
)
|
|
94
89
|
@click.option(
|
|
95
90
|
"--remote-path",
|
|
96
91
|
"--remote_path",
|
|
97
92
|
default=None,
|
|
98
|
-
|
|
99
|
-
"The path where the uploaded file should appear on your Studio. "
|
|
100
|
-
"Has to be within your Studio's home directory and will be relative to that. "
|
|
101
|
-
"If not specified, will use the name of the file you want to upload and place it in your home directory."
|
|
102
|
-
),
|
|
93
|
+
hidden=True,
|
|
103
94
|
)
|
|
104
95
|
def file(path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
|
|
105
|
-
"""
|
|
106
|
-
|
|
96
|
+
"""[DEPRECATED] Use 'lightning studio cp' instead."""
|
|
97
|
+
raise DeprecatedError(
|
|
98
|
+
"Studio uploads via 'lightning upload file' are deprecated. Use 'lightning studio cp' instead."
|
|
99
|
+
)
|
|
107
100
|
|
|
108
101
|
|
|
109
102
|
@upload.command("container")
|
lightning_sdk/cli/studio/cp.py
CHANGED
|
@@ -8,7 +8,7 @@ import click
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
|
|
10
10
|
from lightning_sdk.api.utils import _get_cloud_url
|
|
11
|
-
from lightning_sdk.cli.utils.
|
|
11
|
+
from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@click.command("cp")
|
|
@@ -111,10 +111,13 @@ def cp_download(
|
|
|
111
111
|
f"'{studio_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
|
|
112
112
|
)
|
|
113
113
|
folder_name = os.path.basename(studio_path_result["destination"].rstrip("/"))
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
if local_path in ("./", "."):
|
|
115
|
+
if folder_name == "":
|
|
116
|
+
# handle root directory case (e.g. lit://lightning-ai/gpt-oss/studios/manual-lime-ylu2/)
|
|
117
|
+
folder_name = selected_studio.name
|
|
118
|
+
target_path = os.path.join(local_path, folder_name)
|
|
119
|
+
else:
|
|
120
|
+
target_path = local_path
|
|
118
121
|
|
|
119
122
|
selected_studio.download_folder(studio_path_result["destination"], target_path)
|
|
120
123
|
console.print(f"See your folder at {target_path}")
|
lightning_sdk/cli/studio/ls.py
CHANGED
lightning_sdk/cli/studio/rm.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import click
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
|
|
6
|
-
from lightning_sdk.cli.utils.
|
|
6
|
+
from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
|
|
7
7
|
from lightning_sdk.studio import Studio
|
|
8
8
|
|
|
9
9
|
|
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Optional, TypedDict
|
|
2
3
|
|
|
3
4
|
from lightning_sdk.cli.utils.owner_selection import OwnerMenu
|
|
4
5
|
from lightning_sdk.cli.utils.studio_selection import StudiosMenu
|
|
5
6
|
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
6
7
|
from lightning_sdk.studio import Studio
|
|
8
|
+
from lightning_sdk.teamspace import Teamspace
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
class
|
|
11
|
+
class PathResult(TypedDict):
|
|
10
12
|
owner: Optional[str]
|
|
11
13
|
teamspace: Optional[str]
|
|
12
14
|
studio: Optional[str]
|
|
13
15
|
destination: Optional[str]
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
def
|
|
18
|
+
def path_join(*args: Any) -> str:
|
|
19
|
+
return os.path.join(*args).replace("\\", "/")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_studio_path(studio_path: str) -> PathResult:
|
|
17
23
|
path_string = studio_path.removeprefix("lit://")
|
|
18
24
|
if not path_string:
|
|
19
25
|
raise ValueError("Studio path cannot be empty after prefix")
|
|
20
26
|
|
|
21
|
-
result:
|
|
27
|
+
result: PathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
|
|
22
28
|
|
|
23
29
|
if "/studios/" in path_string:
|
|
24
30
|
prefix_part, suffix_part = path_string.split("/studios/", 1)
|
|
@@ -54,12 +60,49 @@ def parse_studio_path(studio_path: str) -> StudioPathResult:
|
|
|
54
60
|
return result
|
|
55
61
|
|
|
56
62
|
|
|
57
|
-
def
|
|
63
|
+
def parse_teamspace_uploads_path(teamspace_path: str) -> PathResult:
|
|
64
|
+
path_string = teamspace_path.removeprefix("lit://")
|
|
65
|
+
if not path_string:
|
|
66
|
+
raise ValueError("Teamspace path cannot be empty after prefix")
|
|
67
|
+
|
|
68
|
+
result: PathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
|
|
69
|
+
|
|
70
|
+
if "/uploads/" in path_string:
|
|
71
|
+
prefix_part, suffix_part = path_string.split("/uploads/", 1)
|
|
72
|
+
|
|
73
|
+
# org and teamspace
|
|
74
|
+
if prefix_part:
|
|
75
|
+
org_ts_components = prefix_part.split("/")
|
|
76
|
+
if len(org_ts_components) == 2:
|
|
77
|
+
result["owner"], result["teamspace"] = org_ts_components
|
|
78
|
+
elif len(org_ts_components) == 1:
|
|
79
|
+
result["teamspace"] = org_ts_components[0]
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(f"Invalid format: '{prefix_part}'")
|
|
82
|
+
|
|
83
|
+
# studio and destination
|
|
84
|
+
path_parts = suffix_part.split("/")
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError("Invalid teamspace uploads path: missing '/uploads/' segment")
|
|
88
|
+
|
|
89
|
+
if not path_parts:
|
|
90
|
+
raise ValueError("Invalid: Missing teamspace name.")
|
|
91
|
+
|
|
92
|
+
result["destination"] = suffix_part
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_teamspace(teamspace: Optional[str], owner: Optional[str]) -> Teamspace:
|
|
58
98
|
owner_menu = OwnerMenu()
|
|
59
99
|
resolved_owner = owner_menu(owner=owner)
|
|
60
100
|
|
|
61
101
|
teamspace_menu = TeamspacesMenu(resolved_owner)
|
|
62
|
-
|
|
102
|
+
return teamspace_menu(teamspace=teamspace)
|
|
103
|
+
|
|
63
104
|
|
|
105
|
+
def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
|
|
106
|
+
resolved_teamspace = resolve_teamspace(teamspace, owner)
|
|
64
107
|
studio_menu = StudiosMenu(resolved_teamspace)
|
|
65
108
|
return studio_menu(studio=studio_name)
|
lightning_sdk/exceptions.py
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
class OutOfCapacityError(RuntimeError):
|
|
2
7
|
"""Raised when the requested machine is not available in the selected cloud account."""
|
|
3
8
|
|
|
4
9
|
|
|
5
10
|
class NotSupportedError(RuntimeError):
|
|
6
11
|
"""Raised when the requested machine is not supported in the selected cloud account."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeprecatedError(RuntimeError):
|
|
15
|
+
"""Raised when a deprecated feature is used."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeprecatedCommand(click.Command):
|
|
19
|
+
"""Custom exception for deprecated commands."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *args: Any, message: str, **kwargs: Any) -> None:
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self.deprecated_message = message
|
|
24
|
+
|
|
25
|
+
def get_help(self, ctx: click.Context) -> str:
|
|
26
|
+
if self.deprecated_message:
|
|
27
|
+
raise DeprecatedError(self.deprecated_message)
|
|
28
|
+
return super().get_help(ctx)
|
|
29
|
+
|
|
30
|
+
def invoke(self, ctx: click.Context) -> Any:
|
|
31
|
+
if self.deprecated_message:
|
|
32
|
+
raise DeprecatedError(self.deprecated_message)
|
|
33
|
+
return super().invoke(ctx)
|