lightning-sdk 2025.12.17__py3-none-any.whl → 2026.1.27__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/k8s_api.py +75 -29
- lightning_sdk/api/studio_api.py +192 -37
- lightning_sdk/api/teamspace_api.py +180 -54
- lightning_sdk/api/utils.py +8 -0
- lightning_sdk/cli/cp/__init__.py +67 -0
- lightning_sdk/cli/cp/teamspace_uploads.py +93 -0
- lightning_sdk/cli/entrypoint.py +2 -0
- lightning_sdk/cli/groups.py +22 -0
- lightning_sdk/cli/legacy/clusters_menu.py +2 -2
- lightning_sdk/cli/legacy/deploy/_auth.py +7 -6
- lightning_sdk/cli/legacy/download.py +29 -98
- lightning_sdk/cli/legacy/run.py +13 -2
- lightning_sdk/cli/legacy/upload.py +24 -31
- lightning_sdk/cli/studio/__init__.py +4 -0
- lightning_sdk/cli/studio/cp.py +24 -65
- lightning_sdk/cli/studio/ls.py +57 -0
- lightning_sdk/cli/studio/rm.py +71 -0
- lightning_sdk/cli/utils/filesystem.py +103 -0
- lightning_sdk/cli/utils/logging.py +2 -1
- lightning_sdk/cli/utils/teamspace_selection.py +5 -0
- lightning_sdk/exceptions.py +31 -0
- lightning_sdk/job/base.py +1 -1
- lightning_sdk/k8s_cluster.py +9 -10
- lightning_sdk/lightning_cloud/__version__.py +1 -1
- lightning_sdk/lightning_cloud/openapi/__init__.py +43 -23
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +2 -1
- lightning_sdk/lightning_cloud/openapi/api/cloud_space_service_api.py +118 -1
- lightning_sdk/lightning_cloud/openapi/api/cluster_service_api.py +378 -536
- lightning_sdk/lightning_cloud/openapi/api/container_registry_service_api.py +456 -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/k8_s_cluster_service_api.py +246 -19
- lightning_sdk/lightning_cloud/openapi/api/lightningwork_service_api.py +116 -11
- lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +588 -2
- lightning_sdk/lightning_cloud/openapi/api/models_store_api.py +9 -1
- 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/api/{kubernetes_virtual_machine_service_api.py → virtual_machine_service_api.py} +82 -82
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +41 -22
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_cluster_capacity_reservation_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_machine_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/cluster_service_create_org_cluster_capacity_reservation_body.py +409 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_add_container_registry_response.py → cluster_service_report_machine_system_metrics_body.py} +23 -23
- lightning_sdk/lightning_cloud/openapi/models/container_registry_config_ecr.py +149 -0
- 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/externalv1_cloud_space_instance_status.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_config.py → jobs_service_duplicate_deployment_body.py} +51 -51
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_create_lit_logger_media_body.py +305 -0
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_lit_logger_media_body.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/lit_logger_service_update_metrics_stream_body.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/organizations_service_update_org_role_body.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_capacity_reservation_used_by.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_provider.py +2 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_environment_type.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_spec.py +79 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_type.py +0 -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_container_registry_scopes.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_delete_kubernetes_virtual_machine_response.py → v1_create_container_registry_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_create_lit_logger_media_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_org_cluster_capacity_reservation_response.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_sdk_command_history_request.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_cudo_direct_v1.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_refresh_container_registry_credentials_body.py → v1_delete_lit_logger_media_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/{kubernetes_virtual_machine_service_update_kubernetes_virtual_machine_body.py → v1_delete_org_cluster_capacity_reservation_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_virtual_machine_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_external_cluster_spec.py +27 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_external_search_user.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_generic_job_spec.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_validate_container_registry_response.py → v1_get_kubernetes_pod_logs_response.py} +37 -37
- lightning_sdk/lightning_cloud/openapi/models/{v1_get_machine_response.py → v1_get_kubernetes_pod_response.py} +23 -23
- 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_joinable_organization.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/{v1_container_registry_integration.py → v1_k8s_incident_setting.py} +49 -23
- lightning_sdk/lightning_cloud/openapi/models/v1_k8s_incident_type.py +108 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_settings_v1.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_direct_v1.py +53 -27
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_kubernetes_pod_logs_page.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_config.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_list_container_registries_response.py +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_response.py +43 -17
- lightning_sdk/lightning_cloud/openapi/models/v1_list_kubernetes_pods_sort_order.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_lit_logger_media_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_list_models_response.py +55 -3
- lightning_sdk/lightning_cloud/openapi/models/{v1_list_kubernetes_virtual_machines_response.py → v1_list_virtual_machines_response.py} +16 -16
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_logger_media.py +513 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_machine.py +27 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_machine_direct_v1.py +107 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_media_type.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_ai_pod_v1.py → v1_mithril_direct_v1.py} +51 -51
- lightning_sdk/lightning_cloud/openapi/models/v1_nebius_direct_v1.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_refresh_container_registry_credentials_response.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/{cluster_service_validate_container_registry_body.py → v1_report_cloud_space_instance_idle_state_response.py} +6 -6
- lightning_sdk/lightning_cloud/openapi/models/v1_report_machine_system_metrics_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/{v1_ecr_registry_details.py → v1_tenant_credentials.py} +53 -53
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +183 -157
- lightning_sdk/lightning_cloud/openapi/models/{v1_kubernetes_virtual_machine.py → v1_virtual_machine.py} +94 -68
- lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_vm_configuration.py → v1_vm_configuration.py} +20 -20
- lightning_sdk/lightning_cloud/openapi/models/{v1_kubevirt_provider_configuration.py → v1_vm_provider_configuration.py} +32 -32
- lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_create_virtual_machine_body.py +565 -0
- lightning_sdk/lightning_cloud/openapi/models/virtual_machine_service_update_virtual_machine_body.py +97 -0
- lightning_sdk/lightning_cloud/openapi/rest.py +2 -2
- lightning_sdk/lightning_cloud/rest_client.py +0 -2
- lightning_sdk/machine.py +3 -3
- lightning_sdk/studio.py +14 -4
- lightning_sdk/teamspace.py +28 -7
- lightning_sdk/utils/logging.py +2 -1
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/METADATA +1 -5
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/RECORD +125 -102
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/WHEEL +1 -1
- lightning_sdk/lightning_cloud/cli/__main__.py +0 -29
- lightning_sdk/lightning_cloud/openapi/models/kubernetes_virtual_machine_service_create_kubernetes_virtual_machine_body.py +0 -513
- lightning_sdk/lightning_cloud/openapi/models/v1_container_registry_info.py +0 -281
- lightning_sdk/lightning_cloud/openapi/models/v1_kubevirt_vm_resources.py +0 -201
- lightning_sdk/lightning_cloud/source_code/logs_socket_api.py +0 -103
- /lightning_sdk/lightning_cloud/openapi/models/{v1_list_filesystem_mmts_response.py → v1_list_filesystem_mm_ts_response.py} +0 -0
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/LICENSE +0 -0
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-2025.12.17.dist-info → lightning_sdk-2026.1.27.dist-info}/top_level.txt +0 -0
|
@@ -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")
|
lightning_sdk/cli/legacy/run.py
CHANGED
|
@@ -50,6 +50,15 @@ def run() -> None:
|
|
|
50
50
|
help="The organization owning the teamspace (if any). Defaults to the current organization.",
|
|
51
51
|
)
|
|
52
52
|
@click.option("--user", default=None, help="The user owning the teamspace (if any). Defaults to the current user.")
|
|
53
|
+
@click.option(
|
|
54
|
+
"--cloud-provider",
|
|
55
|
+
"--cloud_provider",
|
|
56
|
+
default=None,
|
|
57
|
+
help=(
|
|
58
|
+
"The provider to create the studio on. If set, must be in agreement with the provider from the "
|
|
59
|
+
"cloud_account (if specified)."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
53
62
|
@click.option(
|
|
54
63
|
"--cloud-account",
|
|
55
64
|
"--cloud_account",
|
|
@@ -96,7 +105,7 @@ def run() -> None:
|
|
|
96
105
|
)
|
|
97
106
|
@click.option(
|
|
98
107
|
"--entrypoint",
|
|
99
|
-
default=
|
|
108
|
+
default=None,
|
|
100
109
|
show_default=True,
|
|
101
110
|
help=(
|
|
102
111
|
"The entrypoint of your docker container. "
|
|
@@ -141,6 +150,7 @@ def job(
|
|
|
141
150
|
teamspace: Optional[str] = None,
|
|
142
151
|
org: Optional[str] = None,
|
|
143
152
|
user: Optional[str] = None,
|
|
153
|
+
cloud_provider: Optional[str] = None,
|
|
144
154
|
cloud_account: Optional[str] = None,
|
|
145
155
|
env: Sequence[str] = (),
|
|
146
156
|
interruptible: bool = False,
|
|
@@ -184,6 +194,7 @@ def job(
|
|
|
184
194
|
teamspace=resolved_teamspace,
|
|
185
195
|
org=org,
|
|
186
196
|
user=user,
|
|
197
|
+
cloud_provider=cloud_provider,
|
|
187
198
|
cloud_account=cloud_account,
|
|
188
199
|
env=env_dict,
|
|
189
200
|
interruptible=interruptible,
|
|
@@ -289,7 +300,7 @@ def job(
|
|
|
289
300
|
)
|
|
290
301
|
@click.option(
|
|
291
302
|
"--entrypoint",
|
|
292
|
-
default=
|
|
303
|
+
default=None,
|
|
293
304
|
show_default=True,
|
|
294
305
|
help=(
|
|
295
306
|
"The entrypoint of your docker 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")
|
|
@@ -10,6 +10,8 @@ def register_commands(group: click.Group) -> None:
|
|
|
10
10
|
from lightning_sdk.cli.studio.create import create_studio
|
|
11
11
|
from lightning_sdk.cli.studio.delete import delete_studio
|
|
12
12
|
from lightning_sdk.cli.studio.list import list_studios
|
|
13
|
+
from lightning_sdk.cli.studio.ls import ls_studio
|
|
14
|
+
from lightning_sdk.cli.studio.rm import rm_studio_file
|
|
13
15
|
from lightning_sdk.cli.studio.ssh import ssh_studio
|
|
14
16
|
from lightning_sdk.cli.studio.start import start_studio
|
|
15
17
|
from lightning_sdk.cli.studio.stop import stop_studio
|
|
@@ -24,3 +26,5 @@ def register_commands(group: click.Group) -> None:
|
|
|
24
26
|
group.add_command(switch_studio)
|
|
25
27
|
group.add_command(connect_studio)
|
|
26
28
|
group.add_command(cp_studio_file)
|
|
29
|
+
group.add_command(ls_studio)
|
|
30
|
+
group.add_command(rm_studio_file)
|
lightning_sdk/cli/studio/cp.py
CHANGED
|
@@ -2,22 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
5
|
+
from typing import Optional
|
|
6
6
|
|
|
7
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.
|
|
12
|
-
from lightning_sdk.cli.utils.studio_selection import StudiosMenu
|
|
13
|
-
from lightning_sdk.cli.utils.teamspace_selection import TeamspacesMenu
|
|
14
|
-
from lightning_sdk.studio import Studio
|
|
11
|
+
from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
@click.command("cp")
|
|
18
15
|
@click.argument("source", nargs=1)
|
|
19
16
|
@click.argument("destination", nargs=1)
|
|
20
|
-
|
|
17
|
+
@click.option("-r", "--recursive", is_flag=True, help="Copy directories recursively")
|
|
18
|
+
def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = None, recursive: bool = False) -> None:
|
|
21
19
|
"""Copy a Studio file.
|
|
22
20
|
|
|
23
21
|
SOURCE: Source file to copy from. For Studio files, use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
|
|
@@ -26,70 +24,29 @@ def cp_studio_file(source: str, destination: str, teamspace: Optional[str] = Non
|
|
|
26
24
|
|
|
27
25
|
Example:
|
|
28
26
|
lightning studio cp source.txt lit://<owner>/<my-teamspace>/studios/<my-studio>/destination.txt
|
|
27
|
+
lightning studio cp -r source_folder/ lit://<owner>/<my-teamspace>/studios/<my-studio>/destination_folder/
|
|
29
28
|
|
|
30
29
|
"""
|
|
31
|
-
return cp_impl(source=source, destination=destination)
|
|
30
|
+
return cp_impl(source=source, destination=destination, recursive=recursive)
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
def cp_impl(source: str, destination: str) -> None:
|
|
33
|
+
def cp_impl(source: str, destination: str, recursive: bool = False) -> None:
|
|
35
34
|
if "lit://" in source and "lit://" in destination:
|
|
36
35
|
raise ValueError("Both source and destination cannot be Studio files.")
|
|
37
36
|
elif "lit://" not in source and "lit://" not in destination:
|
|
38
37
|
raise ValueError("Either source or destination must be a Studio file.")
|
|
39
38
|
elif "lit://" in source:
|
|
40
39
|
# Download from Studio to local
|
|
41
|
-
cp_download(studio_path=source, local_path=destination)
|
|
40
|
+
cp_download(studio_path=source, local_path=destination, recursive=recursive)
|
|
42
41
|
else:
|
|
43
42
|
# Upload from local to Studio
|
|
44
|
-
cp_upload(local_file_path=source, studio_file_path=destination)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class StudioPathResult(TypedDict):
|
|
48
|
-
owner: Optional[str]
|
|
49
|
-
teamspace: Optional[str]
|
|
50
|
-
studio: Optional[str]
|
|
51
|
-
destination: Optional[str]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def parse_studio_path(studio_path: str) -> StudioPathResult:
|
|
55
|
-
path_string = studio_path.removeprefix("lit://")
|
|
56
|
-
if not path_string:
|
|
57
|
-
raise ValueError("Studio path cannot be empty after prefix")
|
|
58
|
-
|
|
59
|
-
result: StudioPathResult = {"owner": None, "teamspace": None, "studio": None, "destination": None}
|
|
60
|
-
|
|
61
|
-
if "/studios/" in path_string:
|
|
62
|
-
prefix_part, suffix_part = path_string.split("/studios/", 1)
|
|
63
|
-
|
|
64
|
-
# org and teamspace
|
|
65
|
-
if prefix_part:
|
|
66
|
-
org_ts_components = prefix_part.split("/")
|
|
67
|
-
if len(org_ts_components) == 2:
|
|
68
|
-
result["owner"], result["teamspace"] = org_ts_components
|
|
69
|
-
elif len(org_ts_components) == 1:
|
|
70
|
-
result["teamspace"] = org_ts_components[0]
|
|
71
|
-
else:
|
|
72
|
-
raise ValueError(f"Invalid format: '{prefix_part}'")
|
|
73
|
-
|
|
74
|
-
# studio and destination
|
|
75
|
-
path_parts = suffix_part.split("/")
|
|
76
|
-
|
|
77
|
-
else:
|
|
78
|
-
# studio and destination
|
|
79
|
-
path_parts = path_string.split("/")
|
|
80
|
-
|
|
81
|
-
if not path_parts or len(path_parts) < 2:
|
|
82
|
-
raise ValueError("Invalid: Missing studio name.")
|
|
83
|
-
|
|
84
|
-
result["studio"] = path_parts[0]
|
|
85
|
-
result["destination"] = "/".join(path_parts[1:])
|
|
86
|
-
|
|
87
|
-
return result
|
|
43
|
+
cp_upload(local_file_path=source, studio_file_path=destination, recursive=recursive)
|
|
88
44
|
|
|
89
45
|
|
|
90
46
|
def cp_upload(
|
|
91
47
|
local_file_path: str,
|
|
92
48
|
studio_file_path: str,
|
|
49
|
+
recursive: bool = False,
|
|
93
50
|
) -> None:
|
|
94
51
|
console = Console()
|
|
95
52
|
if not Path(local_file_path).exists():
|
|
@@ -103,6 +60,8 @@ def cp_upload(
|
|
|
103
60
|
console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
104
61
|
|
|
105
62
|
if Path(local_file_path).is_dir():
|
|
63
|
+
if not recursive:
|
|
64
|
+
raise ValueError(f"'{local_file_path}' is a directory. Use -r flag to copy directories recursively.")
|
|
106
65
|
selected_studio.upload_folder(local_file_path, studio_path_result["destination"])
|
|
107
66
|
else:
|
|
108
67
|
if studio_file_path.endswith(("/", "\\")):
|
|
@@ -126,6 +85,7 @@ def cp_upload(
|
|
|
126
85
|
def cp_download(
|
|
127
86
|
studio_path: str,
|
|
128
87
|
local_path: str,
|
|
88
|
+
recursive: bool = False,
|
|
129
89
|
) -> None:
|
|
130
90
|
console = Console()
|
|
131
91
|
studio_path_result = parse_studio_path(studio_path)
|
|
@@ -146,8 +106,18 @@ def cp_download(
|
|
|
146
106
|
|
|
147
107
|
console.print(f"Downloading from {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
148
108
|
if path_info["type"] == "directory":
|
|
109
|
+
if not recursive:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"'{studio_path_result['destination']}' is a directory. Use -r flag to copy directories recursively."
|
|
112
|
+
)
|
|
149
113
|
folder_name = os.path.basename(studio_path_result["destination"].rstrip("/"))
|
|
150
|
-
|
|
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
|
|
151
121
|
|
|
152
122
|
selected_studio.download_folder(studio_path_result["destination"], target_path)
|
|
153
123
|
console.print(f"See your folder at {target_path}")
|
|
@@ -161,14 +131,3 @@ def cp_download(
|
|
|
161
131
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
162
132
|
selected_studio.download_file(studio_path_result["destination"], target_path)
|
|
163
133
|
console.print(f"See your file at {target_path}")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def resolve_studio(studio_name: Optional[str], teamspace: Optional[str], owner: Optional[str]) -> Studio:
|
|
167
|
-
owner_menu = OwnerMenu()
|
|
168
|
-
resolved_owner = owner_menu(owner=owner)
|
|
169
|
-
|
|
170
|
-
teamspace_menu = TeamspacesMenu(resolved_owner)
|
|
171
|
-
resolved_teamspace = teamspace_menu(teamspace=teamspace)
|
|
172
|
-
|
|
173
|
-
studio_menu = StudiosMenu(resolved_teamspace)
|
|
174
|
-
return studio_menu(studio=studio_name)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Studio ls command."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command("ls")
|
|
9
|
+
@click.argument("path", nargs=1)
|
|
10
|
+
def ls_studio(path: str) -> None:
|
|
11
|
+
"""List contents of a directory in Studio.
|
|
12
|
+
|
|
13
|
+
PATH: Studio path in the format
|
|
14
|
+
lit://<owner>/<teamspace>/studios/<studio>/<directory-path>
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
lightning studio ls lit://<owner>/<my-teamspace>/studios/<my-studio>/data
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
return ls_impl(path=path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ls_impl(path: str) -> None:
|
|
24
|
+
if not path.startswith("lit://"):
|
|
25
|
+
raise ValueError("Path must be a Studio path starting with 'lit://'.")
|
|
26
|
+
|
|
27
|
+
studio_path_result = parse_studio_path(path)
|
|
28
|
+
selected_studio = resolve_studio(
|
|
29
|
+
studio_path_result["studio"], studio_path_result["teamspace"], studio_path_result["owner"]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
path_info = selected_studio._studio_api.get_path_info(
|
|
33
|
+
selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not path_info["exists"]:
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
f"The provided path does not exist in the studio: {studio_path_result['destination']} "
|
|
39
|
+
"Note that empty folders may not be detected as existing."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if path_info["type"] == "file":
|
|
43
|
+
# print the file name if it's a file (bash-like behavior)
|
|
44
|
+
print(studio_path_result["destination"])
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
tree = selected_studio._studio_api.get_tree(
|
|
48
|
+
selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
tree_items = tree.get("tree", [])
|
|
52
|
+
|
|
53
|
+
for item in tree_items:
|
|
54
|
+
name = item.get("path", "")
|
|
55
|
+
if item.get("type") == "tree":
|
|
56
|
+
name += "/"
|
|
57
|
+
print(name)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Studio rm command."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from lightning_sdk.cli.utils.filesystem import parse_studio_path, resolve_studio
|
|
7
|
+
from lightning_sdk.studio import Studio
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("rm")
|
|
11
|
+
@click.argument("path", nargs=1)
|
|
12
|
+
@click.option("-r", "--recursive", is_flag=True, help="Remove directories recursively")
|
|
13
|
+
@click.option("-f", "--force", is_flag=True, help="Ignore nonexistent files, never prompt")
|
|
14
|
+
def rm_studio_file(path: str, recursive: bool = False, force: bool = False) -> None:
|
|
15
|
+
"""Remove a Studio file or directory.
|
|
16
|
+
|
|
17
|
+
PATH: Studio path to remove. Use the format lit://<owner>/<my-teamspace>/studios/<my-studio>/<filepath>.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
lightning studio rm lit://<owner>/<my-teamspace>/studios/<my-studio>/file.txt
|
|
21
|
+
lightning studio rm -r lit://<owner>/<my-teamspace>/studios/<my-studio>/folder/
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
return rm_impl(path=path, recursive=recursive, force=force)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def rm_impl(path: str, recursive: bool = False, force: bool = False) -> None:
|
|
28
|
+
if "lit://" not in path:
|
|
29
|
+
raise ValueError("Path must be a Studio path starting with 'lit://'.")
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
studio_path_result = parse_studio_path(path)
|
|
33
|
+
|
|
34
|
+
selected_studio = resolve_studio(
|
|
35
|
+
studio_path_result["studio"], studio_path_result["teamspace"], studio_path_result["owner"]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# check if file/folder exists
|
|
39
|
+
path_info = selected_studio._studio_api.get_path_info(
|
|
40
|
+
selected_studio._studio.id, selected_studio._teamspace.id, path=studio_path_result["destination"]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not path_info["exists"]:
|
|
44
|
+
if force:
|
|
45
|
+
# silently ignore nonexistent files with -f flag
|
|
46
|
+
return
|
|
47
|
+
raise FileNotFoundError(
|
|
48
|
+
f"The provided path does not exist in the studio: '{studio_path_result['destination']}'. "
|
|
49
|
+
"Note that empty folders may not be detected as existing."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
console.print(f"Removing from {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
53
|
+
|
|
54
|
+
if path_info["type"] == "directory":
|
|
55
|
+
if not recursive:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"'{studio_path_result['destination']}' is a directory. Use -r flag to remove directories recursively."
|
|
58
|
+
)
|
|
59
|
+
rm_folder(selected_studio=selected_studio, path=studio_path_result["destination"], console=console)
|
|
60
|
+
else:
|
|
61
|
+
rm_file(selected_studio=selected_studio, path=studio_path_result["destination"], console=console)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def rm_file(selected_studio: Studio, path: str, console: Console) -> None:
|
|
65
|
+
selected_studio._studio_api.remove_file(selected_studio._studio.id, selected_studio._teamspace.id, path)
|
|
66
|
+
console.print(f"Removed file: {path}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def rm_folder(selected_studio: Studio, path: str, console: Console) -> None:
|
|
70
|
+
selected_studio._studio_api.remove_folder(selected_studio._studio.id, selected_studio._teamspace.id, path)
|
|
71
|
+
console.print(f"Removed directory: {path}")
|