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.
Files changed (121) hide show
  1. lightning_sdk/__init__.py +5 -3
  2. lightning_sdk/api/deployment_api.py +23 -11
  3. lightning_sdk/api/job_api.py +42 -7
  4. lightning_sdk/api/lit_container_api.py +23 -3
  5. lightning_sdk/api/mmt_api.py +46 -8
  6. lightning_sdk/api/pipeline_api.py +50 -0
  7. lightning_sdk/api/teamspace_api.py +2 -2
  8. lightning_sdk/api/utils.py +15 -5
  9. lightning_sdk/cli/ai_hub.py +30 -65
  10. lightning_sdk/cli/coloring.py +60 -0
  11. lightning_sdk/cli/configure.py +25 -40
  12. lightning_sdk/cli/connect.py +7 -20
  13. lightning_sdk/cli/create.py +83 -0
  14. lightning_sdk/cli/delete.py +72 -75
  15. lightning_sdk/cli/docker.py +22 -0
  16. lightning_sdk/cli/download.py +78 -113
  17. lightning_sdk/cli/entrypoint.py +44 -65
  18. lightning_sdk/cli/generate.py +28 -43
  19. lightning_sdk/cli/inspect.py +22 -50
  20. lightning_sdk/cli/list.py +281 -222
  21. lightning_sdk/cli/mmts_menu.py +1 -1
  22. lightning_sdk/cli/open.py +62 -0
  23. lightning_sdk/cli/run.py +430 -263
  24. lightning_sdk/cli/serve.py +128 -191
  25. lightning_sdk/cli/start.py +55 -36
  26. lightning_sdk/cli/stop.py +97 -55
  27. lightning_sdk/cli/switch.py +53 -36
  28. lightning_sdk/cli/upload.py +318 -255
  29. lightning_sdk/deployment/__init__.py +2 -0
  30. lightning_sdk/deployment/deployment.py +33 -8
  31. lightning_sdk/lightning_cloud/openapi/__init__.py +23 -0
  32. lightning_sdk/lightning_cloud/openapi/api/__init__.py +1 -0
  33. lightning_sdk/lightning_cloud/openapi/api/assistants_service_api.py +10 -6
  34. lightning_sdk/lightning_cloud/openapi/api/jobs_service_api.py +355 -4
  35. lightning_sdk/lightning_cloud/openapi/api/lit_logger_service_api.py +4 -4
  36. lightning_sdk/lightning_cloud/openapi/api/lit_registry_service_api.py +14 -2
  37. lightning_sdk/lightning_cloud/openapi/api/pipelines_service_api.py +674 -0
  38. lightning_sdk/lightning_cloud/openapi/api/storage_service_api.py +303 -4
  39. lightning_sdk/lightning_cloud/openapi/models/__init__.py +22 -0
  40. lightning_sdk/lightning_cloud/openapi/models/agents_id_body.py +17 -69
  41. lightning_sdk/lightning_cloud/openapi/models/cluster_id_capacityreservations_body.py +27 -1
  42. lightning_sdk/lightning_cloud/openapi/models/create.py +27 -1
  43. lightning_sdk/lightning_cloud/openapi/models/create_deployment_request_defines_a_spec_for_the_job_that_allows_for_autoscaling_jobs.py +53 -1
  44. lightning_sdk/lightning_cloud/openapi/models/deployments_id_body.py +105 -1
  45. lightning_sdk/lightning_cloud/openapi/models/id_visibility_body1.py +1 -27
  46. lightning_sdk/lightning_cloud/openapi/models/id_visibility_body2.py +149 -0
  47. lightning_sdk/lightning_cloud/openapi/models/org_id_memberships_body.py +27 -1
  48. lightning_sdk/lightning_cloud/openapi/models/orgs_id_body.py +157 -1
  49. lightning_sdk/lightning_cloud/openapi/models/pipelines_id_body.py +461 -0
  50. lightning_sdk/lightning_cloud/openapi/models/project_id_pipelines_body.py +227 -0
  51. lightning_sdk/lightning_cloud/openapi/models/projects_id_body.py +157 -1
  52. lightning_sdk/lightning_cloud/openapi/models/slurm_jobs_body.py +79 -1
  53. lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body.py +1 -27
  54. lightning_sdk/lightning_cloud/openapi/models/uploads_upload_id_body1.py +175 -0
  55. lightning_sdk/lightning_cloud/openapi/models/v1_agent_job.py +79 -1
  56. lightning_sdk/lightning_cloud/openapi/models/v1_assistant.py +17 -69
  57. lightning_sdk/lightning_cloud/openapi/models/v1_capacity_block_offering.py +27 -1
  58. lightning_sdk/lightning_cloud/openapi/models/v1_cloud_space_artifact_event_type.py +1 -1
  59. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_accelerator.py +131 -1
  60. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_capacity_reservation.py +79 -1
  61. lightning_sdk/lightning_cloud/openapi/models/v1_cluster_security_options.py +27 -1
  62. lightning_sdk/lightning_cloud/openapi/models/v1_complete_upload_temporary_artifact_request.py +175 -0
  63. lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_request.py +461 -0
  64. lightning_sdk/lightning_cloud/openapi/models/v1_create_deployment_template_request.py +27 -1
  65. lightning_sdk/lightning_cloud/openapi/models/v1_create_job_request.py +201 -0
  66. lightning_sdk/lightning_cloud/openapi/models/v1_create_managed_endpoint_response.py +149 -0
  67. lightning_sdk/lightning_cloud/openapi/models/v1_create_multi_machine_job_request.py +253 -0
  68. lightning_sdk/lightning_cloud/openapi/models/v1_data_connection.py +27 -1
  69. lightning_sdk/lightning_cloud/openapi/models/v1_delete_pipeline_response.py +149 -0
  70. lightning_sdk/lightning_cloud/openapi/models/v1_deployment.py +105 -1
  71. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_details.py +175 -0
  72. lightning_sdk/lightning_cloud/openapi/models/v1_deployment_template.py +53 -1
  73. lightning_sdk/lightning_cloud/openapi/models/v1_filestore_data_connection.py +201 -0
  74. lightning_sdk/lightning_cloud/openapi/models/v1_filesystem_job.py +53 -1
  75. lightning_sdk/lightning_cloud/openapi/models/v1_filesystem_mmt.py +53 -1
  76. lightning_sdk/lightning_cloud/openapi/models/v1_find_capacity_block_offering_response.py +29 -3
  77. lightning_sdk/lightning_cloud/openapi/models/v1_job.py +133 -3
  78. lightning_sdk/lightning_cloud/openapi/models/v1_job_artifacts_type.py +103 -0
  79. lightning_sdk/lightning_cloud/openapi/models/v1_job_spec.py +53 -1
  80. lightning_sdk/lightning_cloud/openapi/models/v1_job_timing.py +27 -1
  81. lightning_sdk/lightning_cloud/openapi/models/v1_list_pipelines_response.py +123 -0
  82. lightning_sdk/lightning_cloud/openapi/models/v1_lit_registry_artifact.py +27 -1
  83. lightning_sdk/lightning_cloud/openapi/models/v1_lit_repository.py +29 -1
  84. lightning_sdk/lightning_cloud/openapi/models/v1_managed_model.py +27 -1
  85. lightning_sdk/lightning_cloud/openapi/models/v1_multi_machine_job.py +27 -1
  86. lightning_sdk/lightning_cloud/openapi/models/v1_multi_machine_job_state.py +2 -0
  87. lightning_sdk/lightning_cloud/openapi/models/v1_organization.py +209 -1
  88. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline.py +513 -0
  89. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_schedule.py +149 -0
  90. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_step.py +253 -0
  91. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_step_status.py +331 -0
  92. lightning_sdk/lightning_cloud/openapi/models/v1_pipeline_step_type.py +104 -0
  93. lightning_sdk/lightning_cloud/openapi/models/v1_project_settings.py +157 -1
  94. lightning_sdk/lightning_cloud/openapi/models/v1_restart_timing.py +27 -1
  95. lightning_sdk/lightning_cloud/openapi/models/v1_rule_resource.py +1 -0
  96. lightning_sdk/lightning_cloud/openapi/models/v1_shared_filesystem.py +201 -0
  97. lightning_sdk/lightning_cloud/openapi/models/v1_slurm_job.py +27 -1
  98. lightning_sdk/lightning_cloud/openapi/models/v1_update_job_visibility_response.py +97 -0
  99. lightning_sdk/lightning_cloud/openapi/models/v1_upload_temporary_artifact_request.py +123 -0
  100. lightning_sdk/lightning_cloud/openapi/models/v1_user_features.py +95 -355
  101. lightning_sdk/lightning_cloud/openapi/models/validate.py +27 -1
  102. lightning_sdk/lightning_cloud/rest_client.py +4 -2
  103. lightning_sdk/machine.py +25 -1
  104. lightning_sdk/models.py +18 -12
  105. lightning_sdk/pipeline/__init__.py +4 -0
  106. lightning_sdk/pipeline/pipeline.py +109 -0
  107. lightning_sdk/pipeline/types.py +268 -0
  108. lightning_sdk/pipeline/utils.py +69 -0
  109. lightning_sdk/plugin.py +9 -10
  110. lightning_sdk/serve.py +134 -0
  111. lightning_sdk/services/utilities.py +2 -2
  112. lightning_sdk/studio.py +5 -1
  113. lightning_sdk/teamspace.py +1 -1
  114. lightning_sdk/utils/resolve.py +12 -1
  115. {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/METADATA +6 -8
  116. {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/RECORD +120 -88
  117. lightning_sdk/cli/legacy.py +0 -135
  118. {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/LICENSE +0 -0
  119. {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/WHEEL +0 -0
  120. {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/entry_points.txt +0 -0
  121. {lightning_sdk-0.1.58.dist-info → lightning_sdk-0.2.1.dist-info}/top_level.txt +0 -0
@@ -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
- _studio_upload_status_path = "~/.lightning/studios/uploads"
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
- def model(self, name: str, path: str = ".", cloud_account: Optional[str] = None) -> None:
29
- """Upload a Model.
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
- Args:
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
- try:
44
- if studio is None:
45
- selected_studio = self._get_studio_from_interactive_menu(possible_studios)
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
- if os.path.exists(curr_path):
248
- os.remove(curr_path)
249
- if os.path.exists(dirpath):
250
- os.removedirs(dirpath)
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
- # no previous download exists
265
- if not os.path.isfile(curr_path):
266
- return state_dict
267
-
268
- menu = TerminalMenu(
269
- [
270
- "no, I accept that this may cause overwriting existing files",
271
- "yes, continue previous upload",
272
- ],
273
- title=f"Found an incomplete upload for {studio.teamspace.name}/{studio.name}:{remote_path}. "
274
- "Should we resume the previous upload?",
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
- index = menu.show()
277
- if index == 0: # selected to start new upload
278
- return state_dict
279
-
280
- # at this point we know we want to resume the previous upload
281
- with open(curr_path) as f:
282
- return json.load(f)
283
-
284
- def _global_upload_progress(self, upload_state: Dict[str, str]) -> bool:
285
- return len(upload_state) > 10
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",