lightning-sdk 0.1.58__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightning_sdk/__init__.py +5 -3
- lightning_sdk/api/deployment_api.py +23 -11
- lightning_sdk/api/job_api.py +42 -7
- lightning_sdk/api/lit_container_api.py +23 -3
- lightning_sdk/api/mmt_api.py +46 -8
- lightning_sdk/api/pipeline_api.py +50 -0
- lightning_sdk/api/teamspace_api.py +2 -2
- lightning_sdk/api/utils.py +15 -5
- lightning_sdk/cli/ai_hub.py +30 -65
- lightning_sdk/cli/coloring.py +60 -0
- lightning_sdk/cli/configure.py +25 -40
- lightning_sdk/cli/connect.py +7 -20
- lightning_sdk/cli/create.py +83 -0
- lightning_sdk/cli/delete.py +72 -75
- lightning_sdk/cli/docker.py +22 -0
- lightning_sdk/cli/download.py +78 -113
- lightning_sdk/cli/entrypoint.py +44 -65
- lightning_sdk/cli/generate.py +28 -43
- lightning_sdk/cli/inspect.py +22 -50
- lightning_sdk/cli/list.py +281 -222
- lightning_sdk/cli/mmts_menu.py +1 -1
- lightning_sdk/cli/open.py +62 -0
- lightning_sdk/cli/run.py +430 -263
- lightning_sdk/cli/serve.py +128 -191
- lightning_sdk/cli/start.py +55 -36
- lightning_sdk/cli/stop.py +97 -55
- lightning_sdk/cli/switch.py +53 -36
- lightning_sdk/cli/upload.py +318 -255
- lightning_sdk/deployment/__init__.py +2 -0
- lightning_sdk/deployment/deployment.py +33 -8
- lightning_sdk/lightning_cloud/openapi/__init__.py +23 -0
- lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
- lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +10 -6
- lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +355 -4
- lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +4 -4
- lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +14 -2
- lightning_sdk/lightning_cloud/openapi/api/pipelines_service_api.py +674 -0
- lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +303 -4
- lightning_sdk/lightning_cloud/openapi/models/__init__.py +22 -0
- lightning_sdk/lightning_cloud/openapi/models/agents_id_body.py +17 -69
- lightning_sdk/lightning_cloud/openapi/models/cluster_id_capacityreservations_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/create.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/create_deployment_request_defines_a_spec_for_the_job_that_allows_for_autoscaling_jobs.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/id_visibility_body1.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/id_visibility_body2.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/org_id_memberships_body.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +157 -1
- lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +461 -0
- lightning_sdk/lightning_cloud/openapi/models/project_id_pipelines_body.py +227 -0
- lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +157 -1
- lightning_sdk/lightning_cloud/openapi/models/slurm_jobs_body.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body.py +1 -27
- lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_agent_job.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_assistant.py +17 -69
- lightning_sdk/lightning_cloud/openapi/models/v1_capacity_block_offering.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_artifact_event_type.py +1 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +131 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +79 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload_temporary_artifact_request.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +461 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_template_request.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_create_job_request.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_managed_endpoint_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_create_multi_machine_job_request.py +253 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_data_connection.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_delete_pipeline_response.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +105 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_details.py +175 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_deployment_template.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_filestore_data_connection.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_filesystem_job.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_filesystem_mmt.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_find_capacity_block_offering_response.py +29 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_job.py +133 -3
- lightning_sdk/lightning_cloud/openapi/models/v1_job_artifacts_type.py +103 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +53 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_job_timing.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_list_pipelines_response.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_artifact.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_lit_repository.py +29 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_multi_machine_job.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_multi_machine_job_state.py +2 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +209 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +513 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_schedule.py +149 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_step.py +253 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_step_status.py +331 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_step_type.py +104 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +157 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_restart_timing.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_rule_resource.py +1 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_shared_filesystem.py +201 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_slurm_job.py +27 -1
- lightning_sdk/lightning_cloud/openapi/models/v1_update_job_visibility_response.py +97 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_upload_temporary_artifact_request.py +123 -0
- lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +95 -355
- lightning_sdk/lightning_cloud/openapi/models/validate.py +27 -1
- lightning_sdk/lightning_cloud/rest_client.py +4 -2
- lightning_sdk/machine.py +25 -1
- lightning_sdk/models.py +18 -12
- lightning_sdk/pipeline/__init__.py +4 -0
- lightning_sdk/pipeline/pipeline.py +109 -0
- lightning_sdk/pipeline/types.py +268 -0
- lightning_sdk/pipeline/utils.py +69 -0
- lightning_sdk/plugin.py +9 -10
- lightning_sdk/serve.py +134 -0
- lightning_sdk/services/utilities.py +2 -2
- lightning_sdk/studio.py +5 -1
- lightning_sdk/teamspace.py +1 -1
- lightning_sdk/utils/resolve.py +12 -1
- {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/METADATA +6 -8
- {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/RECORD +120 -88
- lightning_sdk/cli/legacy.py +0 -135
- {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/LICENSE +0 -0
- {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/WHEEL +0 -0
- {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/entry_points.txt +0 -0
- {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/top_level.txt +0 -0
lightning_sdk/cli/upload.py
CHANGED
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Dict, Generator, List, Optional
|
|
6
6
|
|
|
7
|
+
import click
|
|
7
8
|
import rich
|
|
8
9
|
from rich.console import Console
|
|
9
10
|
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
@@ -15,271 +16,333 @@ from lightning_sdk.api.utils import _get_cloud_url
|
|
|
15
16
|
from lightning_sdk.cli.exceptions import StudioCliError
|
|
16
17
|
from lightning_sdk.cli.studios_menu import _StudiosMenu
|
|
17
18
|
from lightning_sdk.cli.teamspace_menu import _TeamspacesMenu
|
|
18
|
-
from lightning_sdk.models import upload_model
|
|
19
|
+
from lightning_sdk.models import upload_model as _upload_model
|
|
19
20
|
from lightning_sdk.studio import Studio
|
|
20
21
|
from lightning_sdk.utils.resolve import _get_authed_user, skip_studio_init
|
|
21
22
|
|
|
23
|
+
_STUDIO_UPLOAD_STATUS_PATH = "~/.lightning/studios/uploads"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.group("upload")
|
|
27
|
+
def upload() -> None:
|
|
28
|
+
"""Upload assets to Lightning AI."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@upload.command("model")
|
|
32
|
+
@click.argument("name")
|
|
33
|
+
@click.option(
|
|
34
|
+
"--path",
|
|
35
|
+
default=".",
|
|
36
|
+
help="The path to the file or directory you want to upload. Defaults to the current directory.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--cloud-account",
|
|
40
|
+
"--cloud_account",
|
|
41
|
+
default=None,
|
|
42
|
+
help="The name of the cloud account to store the Model in.",
|
|
43
|
+
)
|
|
44
|
+
def model(name: str, path: str = ".", cloud_account: Optional[str] = None) -> None:
|
|
45
|
+
"""Upload a model a teamspace.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
lightning upload model NAME
|
|
49
|
+
|
|
50
|
+
NAME: the name of the model to upload (Should be of format <ORGANIZATION-NAME>/<TEAMSPACE-NAME>/<MODEL-NAME>).
|
|
51
|
+
"""
|
|
52
|
+
_upload_model(name, path, cloud_account=cloud_account)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@upload.command("folder")
|
|
56
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
57
|
+
@click.option(
|
|
58
|
+
"--studio",
|
|
59
|
+
default=None,
|
|
60
|
+
help=(
|
|
61
|
+
"The name of the studio to upload to. "
|
|
62
|
+
"Will show a menu for selection if not specified. "
|
|
63
|
+
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>"
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
@click.option(
|
|
67
|
+
"--remote-path",
|
|
68
|
+
"--remote_path",
|
|
69
|
+
default=None,
|
|
70
|
+
help=(
|
|
71
|
+
"The path where the uploaded file should appear on your Studio. "
|
|
72
|
+
"Has to be within your Studio's home directory and will be relative to that. "
|
|
73
|
+
"If not specified, will use the name of the folder you want to upload and place it in your home directory."
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
def folder(path: str, studio: Optional[str], remote_path: Optional[str]) -> None:
|
|
77
|
+
"""Upload a folder to a Studio."""
|
|
78
|
+
_folder(path=path, studio=studio, remote_path=remote_path)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@upload.command("file")
|
|
82
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
83
|
+
@click.option(
|
|
84
|
+
"--studio",
|
|
85
|
+
default=None,
|
|
86
|
+
help=(
|
|
87
|
+
"The name of the studio to upload to. "
|
|
88
|
+
"Will show a menu for selection if not specified. "
|
|
89
|
+
"If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>"
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
@click.option(
|
|
93
|
+
"--remote-path",
|
|
94
|
+
"--remote_path",
|
|
95
|
+
default=None,
|
|
96
|
+
help=(
|
|
97
|
+
"The path where the uploaded file should appear on your Studio. "
|
|
98
|
+
"Has to be within your Studio's home directory and will be relative to that. "
|
|
99
|
+
"If not specified, will use the name of the file you want to upload and place it in your home directory."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
def file(path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
|
|
103
|
+
"""Upload a file to a Studio."""
|
|
104
|
+
_file(path=path, studio=studio, remote_path=remote_path)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@upload.command("container")
|
|
108
|
+
@click.argument("container")
|
|
109
|
+
@click.option("--tag", default="latest", help="The tag of the container to upload.")
|
|
110
|
+
@click.option(
|
|
111
|
+
"--teamspace",
|
|
112
|
+
default=None,
|
|
113
|
+
help=(
|
|
114
|
+
"The teamspace the studio is part of. "
|
|
115
|
+
"Should be of format <OWNER>/<TEAMSPACE_NAME>. "
|
|
116
|
+
"If not specified, tries to infer from the environment (e.g. when run from within a Studio.)"
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
def upload_container(container: str, tag: str = "latest", teamspace: Optional[str] = None) -> None:
|
|
120
|
+
"""Upload a container to Lightning AI's container registry."""
|
|
121
|
+
menu = _TeamspacesMenu()
|
|
122
|
+
teamspace = menu._resolve_teamspace(teamspace)
|
|
123
|
+
api = LitContainerApi()
|
|
124
|
+
console = Console()
|
|
125
|
+
with Progress(
|
|
126
|
+
SpinnerColumn(),
|
|
127
|
+
TextColumn("[progress.description]{task.description}"),
|
|
128
|
+
TimeElapsedColumn(),
|
|
129
|
+
console=console,
|
|
130
|
+
transient=False,
|
|
131
|
+
) as progress:
|
|
132
|
+
push_task = progress.add_task("Pushing Docker image", total=None)
|
|
133
|
+
try:
|
|
134
|
+
console.print("Authenticating with Lightning Container Registry...")
|
|
135
|
+
try:
|
|
136
|
+
api.authenticate()
|
|
137
|
+
console.print("Authenticated with Lightning Container Registry", style="green")
|
|
138
|
+
except Exception:
|
|
139
|
+
# let the push with retry take control of auth moving forward
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
lines = api.upload_container(container, teamspace, tag)
|
|
143
|
+
_print_docker_push(lines, console, progress, push_task)
|
|
144
|
+
except LCRAuthFailedError:
|
|
145
|
+
console.print("Re-authenticating with Lightning Container Registry...")
|
|
146
|
+
if not api.authenticate(reauth=True):
|
|
147
|
+
raise StudioCliError("Failed to authenticate with Lightning Container Registry") from None
|
|
148
|
+
console.print("Authenticated with Lightning Container Registry", style="green")
|
|
149
|
+
lines = api.upload_container(container, teamspace, tag)
|
|
150
|
+
_print_docker_push(lines, console, progress, push_task)
|
|
151
|
+
progress.update(push_task, description="[green]Container pushed![/green]")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _folder(path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
|
|
155
|
+
"""Upload a folder to a Studio."""
|
|
156
|
+
console = Console()
|
|
157
|
+
if remote_path is None:
|
|
158
|
+
remote_path = os.path.basename(path)
|
|
159
|
+
|
|
160
|
+
if not Path(path).exists():
|
|
161
|
+
raise FileNotFoundError(f"The provided path does not exist: {path}.")
|
|
162
|
+
if not Path(path).is_dir():
|
|
163
|
+
raise StudioCliError(f"The provided path is not a folder: {path}. Use `lightning upload file` instead.")
|
|
164
|
+
|
|
165
|
+
menu = _StudiosMenu()
|
|
166
|
+
selected_studio = menu._resolve_studio(studio)
|
|
167
|
+
|
|
168
|
+
console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
169
|
+
|
|
170
|
+
_upload_folder(path, remote_path, selected_studio)
|
|
171
|
+
|
|
172
|
+
studio_url = (
|
|
173
|
+
_get_cloud_url().replace(":443", "")
|
|
174
|
+
+ "/"
|
|
175
|
+
+ selected_studio.owner.name
|
|
176
|
+
+ "/"
|
|
177
|
+
+ selected_studio.teamspace.name
|
|
178
|
+
+ "/studios/"
|
|
179
|
+
+ selected_studio.name
|
|
180
|
+
)
|
|
181
|
+
console.print(f"See your files at {studio_url}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _upload_folder(path: str, remote_path: str, studio: Studio) -> None:
|
|
185
|
+
pairs = {}
|
|
186
|
+
for root, _, files in os.walk(path):
|
|
187
|
+
rel_root = os.path.relpath(root, path)
|
|
188
|
+
for f in files:
|
|
189
|
+
pairs[os.path.join(root, f)] = os.path.join(remote_path, rel_root, f)
|
|
190
|
+
|
|
191
|
+
upload_state = _resolve_previous_upload_state(studio, remote_path, pairs)
|
|
192
|
+
|
|
193
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
194
|
+
futures = _start_parallel_upload(executor, studio, upload_state)
|
|
195
|
+
|
|
196
|
+
update_fn = tqdm(total=len(upload_state)).update if _global_upload_progress(upload_state) else lambda x: None
|
|
197
|
+
|
|
198
|
+
for f in concurrent.futures.as_completed(futures):
|
|
199
|
+
upload_state.pop(f.result())
|
|
200
|
+
_dump_current_upload_state(studio, remote_path, upload_state)
|
|
201
|
+
update_fn(1)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _file(path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
|
|
205
|
+
"""Upload a file to a Studio."""
|
|
206
|
+
console = Console()
|
|
207
|
+
if remote_path is None:
|
|
208
|
+
remote_path = os.path.basename(path)
|
|
209
|
+
|
|
210
|
+
if Path(path).is_dir():
|
|
211
|
+
raise StudioCliError(f"The provided path is a folder: {path}. Use `lightning upload folder` instead.")
|
|
212
|
+
if not Path(path).exists():
|
|
213
|
+
raise FileNotFoundError(f"The provided path does not exist: {path}.")
|
|
214
|
+
|
|
215
|
+
menu = _StudiosMenu()
|
|
216
|
+
selected_studio = menu._resolve_studio(studio)
|
|
217
|
+
|
|
218
|
+
console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
219
|
+
|
|
220
|
+
_single_file_upload(selected_studio, path, remote_path, True)
|
|
221
|
+
|
|
222
|
+
studio_url = (
|
|
223
|
+
_get_cloud_url().replace(":443", "")
|
|
224
|
+
+ "/"
|
|
225
|
+
+ selected_studio.owner.name
|
|
226
|
+
+ "/"
|
|
227
|
+
+ selected_studio.teamspace.name
|
|
228
|
+
+ "/studios/"
|
|
229
|
+
+ selected_studio.name
|
|
230
|
+
)
|
|
231
|
+
console.print(f"See your file at {studio_url}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _resolve_studio(studio: Optional[str]) -> Studio:
|
|
235
|
+
user = _get_authed_user()
|
|
236
|
+
menu = _StudiosMenu()
|
|
237
|
+
possible_studios = menu._get_possible_studios(user)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
if studio is None:
|
|
241
|
+
selected_studio = menu._get_studio_from_interactive_menu(possible_studios)
|
|
242
|
+
else:
|
|
243
|
+
selected_studio = menu._get_studio_from_name(studio, possible_studios)
|
|
244
|
+
|
|
245
|
+
except KeyboardInterrupt:
|
|
246
|
+
raise KeyboardInterrupt from None
|
|
247
|
+
|
|
248
|
+
# give user friendlier error message
|
|
249
|
+
except Exception as e:
|
|
250
|
+
raise StudioCliError(
|
|
251
|
+
f"Could not find the given Studio {studio} to upload files to. "
|
|
252
|
+
"Please contact Lightning AI directly to resolve this issue."
|
|
253
|
+
) from e
|
|
254
|
+
|
|
255
|
+
with skip_studio_init():
|
|
256
|
+
return Studio(**selected_studio)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _print_docker_push(lines: Generator, console: Console, progress: Progress, push_task: rich.progress.TaskID) -> None:
|
|
260
|
+
for line in lines:
|
|
261
|
+
if "status" in line:
|
|
262
|
+
console.print(line["status"], style="bright_black")
|
|
263
|
+
progress.update(push_task, description="Pushing Docker image")
|
|
264
|
+
elif "aux" in line:
|
|
265
|
+
console.print(line["aux"], style="bright_black")
|
|
266
|
+
elif "error" in line:
|
|
267
|
+
progress.stop()
|
|
268
|
+
console.print(f"\n[red]{line}[/red]")
|
|
269
|
+
return
|
|
270
|
+
elif "finish" in line:
|
|
271
|
+
console.print(f"Container available at [i]{line['url']}[/i]")
|
|
272
|
+
return
|
|
273
|
+
else:
|
|
274
|
+
console.print(line, style="bright_black")
|
|
22
275
|
|
|
23
|
-
class _Uploads(_StudiosMenu, _TeamspacesMenu):
|
|
24
|
-
"""Upload files and folders to Lightning AI."""
|
|
25
276
|
|
|
26
|
-
|
|
277
|
+
def _start_parallel_upload(
|
|
278
|
+
executor: concurrent.futures.ThreadPoolExecutor, studio: Studio, upload_state: Dict[str, str]
|
|
279
|
+
) -> List[concurrent.futures.Future]:
|
|
280
|
+
# only add progress bar on individual uploads with less than 10 files
|
|
281
|
+
progress_bar = not _global_upload_progress(upload_state)
|
|
27
282
|
|
|
28
|
-
|
|
29
|
-
|
|
283
|
+
futures = []
|
|
284
|
+
for k, v in upload_state.items():
|
|
285
|
+
futures.append(
|
|
286
|
+
executor.submit(_single_file_upload, studio=studio, local_path=k, remote_path=v, progress_bar=progress_bar)
|
|
287
|
+
)
|
|
30
288
|
|
|
31
|
-
|
|
32
|
-
name: The name of the Model you want to upload.
|
|
33
|
-
This should have the format <ORGANIZATION-NAME>/<TEAMSPACE-NAME>/<MODEL-NAME>.
|
|
34
|
-
path: The path to the file or directory you want to upload. Defaults to the current directory.
|
|
35
|
-
cloud_account: The name of the cloud account to store the Model in.
|
|
36
|
-
"""
|
|
37
|
-
upload_model(name, path, cloud_account=cloud_account)
|
|
289
|
+
return futures
|
|
38
290
|
|
|
39
|
-
def _resolve_studio(self, studio: Optional[str]) -> Studio:
|
|
40
|
-
user = _get_authed_user()
|
|
41
|
-
possible_studios = self._get_possible_studios(user)
|
|
42
291
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
else:
|
|
47
|
-
selected_studio = self._get_studio_from_name(studio, possible_studios)
|
|
48
|
-
|
|
49
|
-
except KeyboardInterrupt:
|
|
50
|
-
raise KeyboardInterrupt from None
|
|
51
|
-
|
|
52
|
-
# give user friendlier error message
|
|
53
|
-
except Exception as e:
|
|
54
|
-
raise StudioCliError(
|
|
55
|
-
f"Could not find the given Studio {studio} to upload files to. "
|
|
56
|
-
"Please contact Lightning AI directly to resolve this issue."
|
|
57
|
-
) from e
|
|
58
|
-
|
|
59
|
-
with skip_studio_init():
|
|
60
|
-
return Studio(**selected_studio)
|
|
61
|
-
|
|
62
|
-
def folder(self, path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
|
|
63
|
-
"""Upload a file or folder to a Studio.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
path: The path to the file or directory you want to upload
|
|
67
|
-
studio: The name of the studio to upload to. Will show a menu for selection if not specified.
|
|
68
|
-
If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>
|
|
69
|
-
remote_path: The path where the uploaded file should appear on your Studio.
|
|
70
|
-
Has to be within your Studio's home directory and will be relative to that.
|
|
71
|
-
If not specified, will use the file or directory name of the path you want to upload
|
|
72
|
-
and place it in your home directory.
|
|
73
|
-
"""
|
|
74
|
-
console = Console()
|
|
75
|
-
if remote_path is None:
|
|
76
|
-
remote_path = os.path.basename(path)
|
|
77
|
-
|
|
78
|
-
if not Path(path).exists():
|
|
79
|
-
raise FileNotFoundError(f"The provided path does not exist: {path}.")
|
|
80
|
-
if not Path(path).is_dir():
|
|
81
|
-
raise StudioCliError(f"The provided path is not a folder: {path}. Use `lightning upload file` instead.")
|
|
82
|
-
|
|
83
|
-
selected_studio = self._resolve_studio(studio)
|
|
84
|
-
|
|
85
|
-
console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
86
|
-
|
|
87
|
-
pairs = {}
|
|
88
|
-
for root, _, files in os.walk(path):
|
|
89
|
-
rel_root = os.path.relpath(root, path)
|
|
90
|
-
for f in files:
|
|
91
|
-
pairs[os.path.join(root, f)] = os.path.join(remote_path, rel_root, f)
|
|
92
|
-
|
|
93
|
-
upload_state = self._resolve_previous_upload_state(selected_studio, remote_path, pairs)
|
|
94
|
-
|
|
95
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
96
|
-
futures = self._start_parallel_upload(executor, selected_studio, upload_state)
|
|
97
|
-
|
|
98
|
-
update_fn = (
|
|
99
|
-
tqdm(total=len(upload_state)).update if self._global_upload_progress(upload_state) else lambda x: None
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
for f in concurrent.futures.as_completed(futures):
|
|
103
|
-
upload_state.pop(f.result())
|
|
104
|
-
self._dump_current_upload_state(selected_studio, remote_path, upload_state)
|
|
105
|
-
update_fn(1)
|
|
106
|
-
|
|
107
|
-
studio_url = (
|
|
108
|
-
_get_cloud_url().replace(":443", "")
|
|
109
|
-
+ "/"
|
|
110
|
-
+ selected_studio.owner.name
|
|
111
|
-
+ "/"
|
|
112
|
-
+ selected_studio.teamspace.name
|
|
113
|
-
+ "/studios/"
|
|
114
|
-
+ selected_studio.name
|
|
115
|
-
)
|
|
116
|
-
console.print(f"See your files at {studio_url}")
|
|
117
|
-
|
|
118
|
-
def file(self, path: str, studio: Optional[str] = None, remote_path: Optional[str] = None) -> None:
|
|
119
|
-
"""Upload a file to a Studio.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
path: The path to the file you want to upload
|
|
123
|
-
studio: The name of the studio to upload to. Will show a menu for selection if not specified.
|
|
124
|
-
If provided, should be in the form of <TEAMSPACE-NAME>/<STUDIO-NAME>
|
|
125
|
-
remote_path: The path where the uploaded file should appear on your Studio.
|
|
126
|
-
Has to be within your Studio's home directory and will be relative to that.
|
|
127
|
-
If not specified, will use the name of the file you want to upload
|
|
128
|
-
and place it in your home directory.
|
|
129
|
-
"""
|
|
130
|
-
console = Console()
|
|
131
|
-
if remote_path is None:
|
|
132
|
-
remote_path = os.path.basename(path)
|
|
133
|
-
|
|
134
|
-
if Path(path).is_dir():
|
|
135
|
-
raise StudioCliError(f"The provided path is a folder: {path}. Use `lightning upload folder` instead.")
|
|
136
|
-
if not Path(path).exists():
|
|
137
|
-
raise FileNotFoundError(f"The provided path does not exist: {path}.")
|
|
138
|
-
|
|
139
|
-
selected_studio = self._resolve_studio(studio)
|
|
140
|
-
|
|
141
|
-
console.print(f"Uploading to {selected_studio.teamspace.name}/{selected_studio.name}")
|
|
142
|
-
|
|
143
|
-
self._single_file_upload(selected_studio, path, remote_path, True)
|
|
144
|
-
|
|
145
|
-
studio_url = (
|
|
146
|
-
_get_cloud_url().replace(":443", "")
|
|
147
|
-
+ "/"
|
|
148
|
-
+ selected_studio.owner.name
|
|
149
|
-
+ "/"
|
|
150
|
-
+ selected_studio.teamspace.name
|
|
151
|
-
+ "/studios/"
|
|
152
|
-
+ selected_studio.name
|
|
153
|
-
)
|
|
154
|
-
console.print(f"See your file at {studio_url}")
|
|
155
|
-
|
|
156
|
-
def container(self, container: str, tag: str = "latest", teamspace: Optional[str] = None) -> None:
|
|
157
|
-
"""Upload a container to Lightning AI's container registry."""
|
|
158
|
-
menu = _TeamspacesMenu()
|
|
159
|
-
teamspace = menu._resolve_teamspace(teamspace)
|
|
160
|
-
api = LitContainerApi()
|
|
161
|
-
console = Console()
|
|
162
|
-
with Progress(
|
|
163
|
-
SpinnerColumn(),
|
|
164
|
-
TextColumn("[progress.description]{task.description}"),
|
|
165
|
-
TimeElapsedColumn(),
|
|
166
|
-
console=console,
|
|
167
|
-
transient=False,
|
|
168
|
-
) as progress:
|
|
169
|
-
push_task = progress.add_task("Pushing Docker image", total=None)
|
|
170
|
-
try:
|
|
171
|
-
console.print("Authenticating with Lightning Container Registry...")
|
|
172
|
-
try:
|
|
173
|
-
api.authenticate()
|
|
174
|
-
console.print("Authenticated with Lightning Container Registry", style="green")
|
|
175
|
-
except Exception:
|
|
176
|
-
# let the push with retry take control of auth moving forward
|
|
177
|
-
pass
|
|
178
|
-
|
|
179
|
-
lines = api.upload_container(container, teamspace, tag)
|
|
180
|
-
self._print_docker_push(lines, console, progress, push_task)
|
|
181
|
-
except LCRAuthFailedError:
|
|
182
|
-
console.print("Re-authenticating with Lightning Container Registry...")
|
|
183
|
-
if not api.authenticate(reauth=True):
|
|
184
|
-
raise StudioCliError("Failed to authenticate with Lightning Container Registry") from None
|
|
185
|
-
console.print("Authenticated with Lightning Container Registry", style="green")
|
|
186
|
-
lines = api.upload_container(container, teamspace, tag)
|
|
187
|
-
self._print_docker_push(lines, console, progress, push_task)
|
|
188
|
-
progress.update(push_task, description="[green]Container pushed![/green]")
|
|
189
|
-
|
|
190
|
-
@staticmethod
|
|
191
|
-
def _print_docker_push(
|
|
192
|
-
lines: Generator, console: Console, progress: Progress, push_task: rich.progress.TaskID
|
|
193
|
-
) -> None:
|
|
194
|
-
for line in lines:
|
|
195
|
-
if "status" in line:
|
|
196
|
-
console.print(line["status"], style="bright_black")
|
|
197
|
-
progress.update(push_task, description="Pushing Docker image")
|
|
198
|
-
elif "aux" in line:
|
|
199
|
-
console.print(line["aux"], style="bright_black")
|
|
200
|
-
elif "error" in line:
|
|
201
|
-
progress.stop()
|
|
202
|
-
console.print(f"\n[red]{line}[/red]")
|
|
203
|
-
return
|
|
204
|
-
elif "finish" in line:
|
|
205
|
-
console.print(f"Container available at [i]{line['url']}[/i]")
|
|
206
|
-
return
|
|
207
|
-
else:
|
|
208
|
-
console.print(line, style="bright_black")
|
|
209
|
-
|
|
210
|
-
def _start_parallel_upload(
|
|
211
|
-
self, executor: concurrent.futures.ThreadPoolExecutor, studio: Studio, upload_state: Dict[str, str]
|
|
212
|
-
) -> List[concurrent.futures.Future]:
|
|
213
|
-
# only add progress bar on individual uploads with less than 10 files
|
|
214
|
-
progress_bar = not self._global_upload_progress(upload_state)
|
|
215
|
-
|
|
216
|
-
futures = []
|
|
217
|
-
for k, v in upload_state.items():
|
|
218
|
-
futures.append(
|
|
219
|
-
executor.submit(
|
|
220
|
-
self._single_file_upload, studio=studio, local_path=k, remote_path=v, progress_bar=progress_bar
|
|
221
|
-
)
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
return futures
|
|
225
|
-
|
|
226
|
-
def _single_file_upload(self, studio: Studio, local_path: str, remote_path: str, progress_bar: bool) -> str:
|
|
227
|
-
studio.upload_file(local_path, remote_path, progress_bar)
|
|
228
|
-
return local_path
|
|
229
|
-
|
|
230
|
-
def _dump_current_upload_state(self, studio: Studio, remote_path: str, state_dict: Dict[str, str]) -> None:
|
|
231
|
-
"""Dumps the current upload state so that we can safely resume later."""
|
|
232
|
-
curr_path = os.path.abspath(
|
|
233
|
-
os.path.expandvars(
|
|
234
|
-
os.path.expanduser(
|
|
235
|
-
os.path.join(self._studio_upload_status_path, studio._studio.id, remote_path + ".json")
|
|
236
|
-
)
|
|
237
|
-
)
|
|
238
|
-
)
|
|
292
|
+
def _single_file_upload(studio: Studio, local_path: str, remote_path: str, progress_bar: bool) -> str:
|
|
293
|
+
studio.upload_file(local_path, remote_path, progress_bar)
|
|
294
|
+
return local_path
|
|
239
295
|
|
|
240
|
-
dirpath = os.path.dirname(curr_path)
|
|
241
|
-
if state_dict:
|
|
242
|
-
os.makedirs(os.path.dirname(curr_path), exist_ok=True)
|
|
243
|
-
with open(curr_path, "w") as f:
|
|
244
|
-
json.dump(state_dict, f, indent=4)
|
|
245
|
-
return
|
|
246
296
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _resolve_previous_upload_state(
|
|
253
|
-
self, studio: Studio, remote_path: str, state_dict: Dict[str, str]
|
|
254
|
-
) -> Dict[str, str]:
|
|
255
|
-
"""Resolves potential previous uploads to continue if possible."""
|
|
256
|
-
curr_path = os.path.abspath(
|
|
257
|
-
os.path.expandvars(
|
|
258
|
-
os.path.expanduser(
|
|
259
|
-
os.path.join(self._studio_upload_status_path, studio._studio.id, remote_path + ".json")
|
|
260
|
-
)
|
|
261
|
-
)
|
|
297
|
+
def _dump_current_upload_state(studio: Studio, remote_path: str, state_dict: Dict[str, str]) -> None:
|
|
298
|
+
"""Dumps the current upload state so that we can safely resume later."""
|
|
299
|
+
curr_path = os.path.abspath(
|
|
300
|
+
os.path.expandvars(
|
|
301
|
+
os.path.expanduser(os.path.join(_STUDIO_UPLOAD_STATUS_PATH, studio._studio.id, remote_path + ".json"))
|
|
262
302
|
)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
dirpath = os.path.dirname(curr_path)
|
|
306
|
+
if state_dict:
|
|
307
|
+
os.makedirs(os.path.dirname(curr_path), exist_ok=True)
|
|
308
|
+
with open(curr_path, "w") as f:
|
|
309
|
+
json.dump(state_dict, f, indent=4)
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
if os.path.exists(curr_path):
|
|
313
|
+
os.remove(curr_path)
|
|
314
|
+
if os.path.exists(dirpath):
|
|
315
|
+
os.removedirs(dirpath)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _resolve_previous_upload_state(studio: Studio, remote_path: str, state_dict: Dict[str, str]) -> Dict[str, str]:
|
|
319
|
+
"""Resolves potential previous uploads to continue if possible."""
|
|
320
|
+
curr_path = os.path.abspath(
|
|
321
|
+
os.path.expandvars(
|
|
322
|
+
os.path.expanduser(os.path.join(_STUDIO_UPLOAD_STATUS_PATH, studio._studio.id, remote_path + ".json"))
|
|
275
323
|
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# no previous download exists
|
|
327
|
+
if not os.path.isfile(curr_path):
|
|
328
|
+
return state_dict
|
|
329
|
+
|
|
330
|
+
menu = TerminalMenu(
|
|
331
|
+
[
|
|
332
|
+
"no, I accept that this may cause overwriting existing files",
|
|
333
|
+
"yes, continue previous upload",
|
|
334
|
+
],
|
|
335
|
+
title=f"Found an incomplete upload for {studio.teamspace.name}/{studio.name}:{remote_path}. "
|
|
336
|
+
"Should we resume the previous upload?",
|
|
337
|
+
)
|
|
338
|
+
index = menu.show()
|
|
339
|
+
if index == 0: # selected to start new upload
|
|
340
|
+
return state_dict
|
|
341
|
+
|
|
342
|
+
# at this point we know we want to resume the previous upload
|
|
343
|
+
with open(curr_path) as f:
|
|
344
|
+
return json.load(f)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _global_upload_progress(upload_state: Dict[str, str]) -> bool:
|
|
348
|
+
return len(upload_state) > 10
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from lightning_sdk.api.deployment_api import (
|
|
2
2
|
AutoScaleConfig,
|
|
3
|
+
AutoScalingMetric,
|
|
3
4
|
BasicAuth,
|
|
4
5
|
Env,
|
|
5
6
|
ExecHealthCheck,
|
|
@@ -13,6 +14,7 @@ from lightning_sdk.deployment.deployment import Deployment
|
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"AutoScaleConfig",
|
|
17
|
+
"AutoScalingMetric",
|
|
16
18
|
"BasicAuth",
|
|
17
19
|
"Env",
|
|
18
20
|
"ExecHealthCheck",
|