Skip to content

Commit 3db9a20

Browse files
tchatonethanwharristhomaspre-commit-ci[bot]
authored andcommitted
[App] Enable listing at project level 2/n (#16622)
Co-authored-by: Ethan Harris <[email protected]> Co-authored-by: thomas <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> (cherry picked from commit 03bb29c)
1 parent f816fb4 commit 3db9a20

File tree

6 files changed

+193
-78
lines changed

6 files changed

+193
-78
lines changed

src/lightning_app/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
1313

1414
- Added FileSystem abstraction to simply manipulation of files ([#16581](https://github.com/Lightning-AI/lightning/pull/16581))
1515

16+
- Enabled `ls` and `cp` (download) at project level ([#16622](https://github.com/Lightning-AI/lightning/pull/16622))
1617
- Added Storage Commands ([#16606](https://github.com/Lightning-AI/lightning/pull/16606))
1718
* `ls`: List files from your Cloud Platform Filesystem
1819
* `cd`: Change the current directory within your Cloud Platform filesystem (terminal session based)

src/lightning_app/cli/commands/cd.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ def cd(path: Optional[Union[Tuple[str], str]]) -> None:
3737

3838
root = "/"
3939

40-
live.stop()
41-
4240
if isinstance(path, Tuple) and len(path) > 0:
4341
path = " ".join(path)
4442

src/lightning_app/cli/commands/cp.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,22 @@
1818
from functools import partial
1919
from multiprocessing.pool import ApplyResult
2020
from pathlib import Path
21-
from time import sleep
22-
from typing import Optional, Tuple
21+
from typing import Optional, Tuple, Union
2322

2423
import click
2524
import requests
2625
import rich
2726
import urllib3
28-
from lightning_cloud.openapi import IdArtifactsBody
27+
from lightning_cloud.openapi import Externalv1LightningappInstance, IdArtifactsBody, V1CloudSpace
2928
from rich.live import Live
3029
from rich.progress import BarColumn, DownloadColumn, Progress, Task, TextColumn
3130
from rich.spinner import Spinner
3231
from rich.text import Text
3332

34-
from lightning_app.cli.commands.pwd import _pwd
35-
from lightning_app.source_code import FileUploader
36-
from lightning_app.utilities.app_helpers import Logger
37-
from lightning_app.utilities.network import LightningClient
33+
from lightning.app.cli.commands.pwd import _pwd
34+
from lightning.app.source_code import FileUploader
35+
from lightning.app.utilities.app_helpers import Logger
36+
from lightning.app.utilities.network import LightningClient
3837

3938
logger = Logger(__name__)
4039

@@ -59,8 +58,8 @@ def cp(src_path: str, dst_path: str, r: bool = False, recursive: bool = False) -
5958

6059
client = LightningClient()
6160

62-
src_path, src_remote = _sanetize_path(src_path, pwd)
63-
dst_path, dst_remote = _sanetize_path(dst_path, pwd)
61+
src_path, src_remote = _sanitize_path(src_path, pwd)
62+
dst_path, dst_remote = _sanitize_path(dst_path, pwd)
6463

6564
if src_remote and dst_remote:
6665
return _error_and_exit("Moving files remotely isn't supported yet. Please, open a Github issue.")
@@ -103,9 +102,6 @@ def _upload_files(live, client: LightningClient, local_src: str, remote_dst: str
103102

104103
live.stop()
105104

106-
# Sleep to avoid rich live collision.
107-
sleep(1)
108-
109105
progress = _get_progress_bar()
110106

111107
total_size = sum([Path(path).stat().st_size for path in upload_paths])
@@ -140,14 +136,15 @@ def _upload(source_file: str, presigned_url: ApplyResult, progress: Progress, ta
140136

141137

142138
def _download_files(live, client, remote_src: str, local_dst: str, pwd: str):
143-
project_id, app_id = _get_project_app_ids(pwd)
139+
project_id, lit_resource = _get_project_id_and_resource(pwd)
144140

145141
download_paths = []
146142
download_urls = []
147143
total_size = []
148144

149-
response = client.lightningapp_instance_service_list_lightningapp_instance_artifacts(project_id, app_id)
150-
for artifact in response.artifacts:
145+
prefix = _get_prefix("/".join(pwd.split("/")[3:]), lit_resource)
146+
147+
for artifact in _collect_artifacts(client, project_id, prefix, include_download_url=True):
151148
path = os.path.join(local_dst, artifact.filename.replace(remote_src, ""))
152149
path = Path(path).resolve()
153150
os.makedirs(path.parent, exist_ok=True)
@@ -157,14 +154,15 @@ def _download_files(live, client, remote_src: str, local_dst: str, pwd: str):
157154

158155
live.stop()
159156

160-
# Sleep to avoid rich live collision.
161-
sleep(1)
157+
if not download_paths:
158+
print("There were no files to download.")
159+
return
162160

163161
progress = progress = _get_progress_bar()
164162

165163
progress.start()
166164

167-
task_id = progress.add_task("download", filename=path, total=sum(total_size))
165+
task_id = progress.add_task("download", filename="", total=sum(total_size))
168166

169167
_download_file_fn = partial(_download_file, progress=progress, task_id=task_id)
170168

@@ -193,7 +191,7 @@ def _download_file(path: str, url: str, progress: Progress, task_id: Task) -> No
193191
progress.update(task_id, advance=len(chunk))
194192

195193

196-
def _sanetize_path(path: str, pwd: str) -> Tuple[str, bool]:
194+
def _sanitize_path(path: str, pwd: str) -> Tuple[str, bool]:
197195
is_remote = _is_remote(path)
198196
if is_remote:
199197
path = _remove_remote(path)
@@ -217,6 +215,7 @@ def _error_and_exit(msg: str) -> str:
217215
sys.exit(0)
218216

219217

218+
# TODO: To be removed when upload is supported for CloudSpaces.
220219
def _get_project_app_ids(pwd: str) -> Tuple[str, str]:
221220
"""Convert a root path to a project id and app id."""
222221
# TODO: Handle project level
@@ -234,6 +233,35 @@ def _get_project_app_ids(pwd: str) -> Tuple[str, str]:
234233
return project_id, lit_app.id
235234

236235

236+
def _get_project_id_and_resource(pwd: str) -> Tuple[str, Union[Externalv1LightningappInstance, V1CloudSpace]]:
237+
"""Convert a root path to a project id and app id."""
238+
# TODO: Handle project level
239+
project_name, resource_name, *_ = pwd.split("/")[1:3]
240+
241+
# 1. Collect the projects of the user
242+
client = LightningClient()
243+
projects = client.projects_service_list_memberships()
244+
project_id = [project.project_id for project in projects.memberships if project.name == project_name][0]
245+
246+
# 2. Collect resources
247+
lit_apps = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project_id).lightningapps
248+
249+
lit_cloud_spaces = client.cloud_space_service_list_cloud_spaces(project_id=project_id).cloudspaces
250+
251+
lit_ressources = [lit_resource for lit_resource in lit_cloud_spaces if lit_resource.name == resource_name]
252+
253+
if len(lit_ressources) == 0:
254+
255+
lit_ressources = [lit_resource for lit_resource in lit_apps if lit_resource.name == resource_name]
256+
257+
if len(lit_ressources) == 0:
258+
259+
print(f"ERROR: There isn't any Lightning Ressource matching the name {resource_name}.")
260+
sys.exit(0)
261+
262+
return project_id, lit_ressources[0]
263+
264+
237265
def _get_progress_bar():
238266
return Progress(
239267
TextColumn("[bold blue]{task.description}", justify="left"),

src/lightning_app/cli/commands/ls.py

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import click
2020
import rich
21+
from lightning_cloud.openapi import Externalv1LightningappInstance
2122
from rich.console import Console
2223
from rich.live import Live
2324
from rich.spinner import Spinner
@@ -44,9 +45,7 @@ def ls(path: Optional[str] = None) -> List[str]:
4445

4546
root = "/"
4647

47-
with Live(Spinner("point", text=Text("pending...", style="white")), transient=True) as live:
48-
49-
live.stop()
48+
with Live(Spinner("point", text=Text("pending...", style="white")), transient=True):
5049

5150
if not os.path.exists(_LIGHTNING_CONNECTION_FOLDER):
5251
os.makedirs(_LIGHTNING_CONNECTION_FOLDER)
@@ -76,45 +75,74 @@ def ls(path: Optional[str] = None) -> List[str]:
7675

7776
lit_apps = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project_id).lightningapps
7877

78+
lit_cloud_spaces = client.cloud_space_service_list_cloud_spaces(project_id=project_id).cloudspaces
79+
7980
if len(splits) == 1:
80-
app_names = sorted([lit_app.name for lit_app in lit_apps])
81-
_print_names_with_colors(app_names, [_FOLDER_COLOR] * len(app_names))
82-
return app_names
81+
apps = [lit_app.name for lit_app in lit_apps]
82+
cloud_spaces = [lit_cloud_space.name for lit_cloud_space in lit_cloud_spaces]
83+
ressource_names = sorted(set(cloud_spaces + apps))
84+
_print_names_with_colors(ressource_names, [_FOLDER_COLOR] * len(ressource_names))
85+
return ressource_names
86+
87+
lit_ressources = [lit_resource for lit_resource in lit_cloud_spaces if lit_resource.name == splits[1]]
88+
89+
if len(lit_ressources) == 0:
8390

84-
lit_apps = [lit_app for lit_app in lit_apps if lit_app.name == splits[1]]
91+
lit_ressources = [lit_resource for lit_resource in lit_apps if lit_resource.name == splits[1]]
8592

86-
if len(lit_apps) != 1:
87-
print(f"ERROR: There isn't any Lightning App matching the name {splits[1]}.")
88-
sys.exit(0)
93+
if len(lit_ressources) == 0:
8994

90-
lit_app = lit_apps[0]
95+
print(f"ERROR: There isn't any Lightning Ressource matching the name {splits[1]}.")
96+
sys.exit(0)
97+
98+
lit_resource = lit_ressources[0]
99+
100+
app_paths = []
101+
app_colors = []
102+
103+
cloud_spaces_paths = []
104+
cloud_spaces_colors = []
91105

92-
paths = []
93-
colors = []
94106
depth = len(splits)
95-
subpath = "/".join(splits[2:])
96-
# TODO: Replace with project level endpoints
97-
for artifact in _collect_artifacts(client, project_id, lit_app.id):
98-
path = os.path.join(project_id, lit_app.name, artifact.filename)
107+
108+
prefix = "/".join(splits[2:])
109+
prefix = _get_prefix(prefix, lit_resource)
110+
111+
for artifact in _collect_artifacts(client=client, project_id=project_id, prefix=prefix):
112+
113+
if str(artifact.filename).startswith("/"):
114+
artifact.filename = artifact.filename[1:]
115+
116+
path = os.path.join(project_id, prefix[1:], artifact.filename)
117+
99118
artifact_splits = path.split("/")
100119

101-
if len(artifact_splits) < depth + 1:
120+
if len(artifact_splits) <= depth + 1:
102121
continue
103122

104-
if not str(artifact.filename).startswith(subpath):
105-
continue
123+
path = artifact_splits[depth + 1]
106124

107-
path = artifact_splits[depth]
125+
paths = app_paths if isinstance(lit_resource, Externalv1LightningappInstance) else cloud_spaces_paths
126+
colors = app_colors if isinstance(lit_resource, Externalv1LightningappInstance) else cloud_spaces_colors
108127

109128
if path not in paths:
110129
paths.append(path)
111130

112131
# display files otherwise folders
113132
colors.append(_FILE_COLOR if len(artifact_splits) == depth + 1 else _FOLDER_COLOR)
114133

115-
_print_names_with_colors(paths, colors)
134+
if app_paths and cloud_spaces_paths:
135+
if app_paths:
136+
rich.print("Lightning App")
137+
_print_names_with_colors(app_paths, app_colors)
138+
139+
if cloud_spaces_paths:
140+
rich.print("Lightning CloudSpaces")
141+
_print_names_with_colors(cloud_spaces_paths, cloud_spaces_colors)
142+
else:
143+
_print_names_with_colors(app_paths + cloud_spaces_paths, app_colors + cloud_spaces_colors)
116144

117-
return paths
145+
return app_paths + cloud_spaces_paths
118146

119147

120148
def _add_colors(filename: str, color: Optional[str] = None) -> str:
@@ -156,21 +184,64 @@ def _print_names_with_colors(names: List[str], colors: List[str], padding: int =
156184
def _collect_artifacts(
157185
client: LightningClient,
158186
project_id: str,
159-
app_id: str,
187+
prefix: str = "",
160188
page_token: Optional[str] = "",
189+
cluster_id: Optional[str] = None,
190+
page_size: int = 100_000,
161191
tokens=None,
192+
include_download_url: bool = False,
162193
) -> Generator:
163194
if tokens is None:
164195
tokens = []
165196

166-
if page_token in tokens:
167-
return
168-
169-
response = client.lightningapp_instance_service_list_lightningapp_instance_artifacts(
170-
project_id, app_id, page_token=page_token
171-
)
172-
yield from response.artifacts
173-
174-
if response.next_page_token != "":
175-
tokens.append(page_token)
176-
yield from _collect_artifacts(client, project_id, app_id, page_token=response.next_page_token, tokens=tokens)
197+
if cluster_id is None:
198+
clusters = client.projects_service_list_project_cluster_bindings(project_id)
199+
for cluster in clusters.clusters:
200+
yield from _collect_artifacts(
201+
client,
202+
project_id,
203+
prefix=prefix,
204+
cluster_id=cluster.cluster_id,
205+
page_token=page_token,
206+
tokens=tokens,
207+
page_size=page_size,
208+
include_download_url=include_download_url,
209+
)
210+
else:
211+
212+
if page_token in tokens:
213+
return
214+
215+
response = client.lightningapp_instance_service_list_project_artifacts(
216+
project_id,
217+
prefix=prefix,
218+
cluster_id=cluster_id,
219+
page_token=page_token,
220+
include_download_url=include_download_url,
221+
page_size=str(page_size),
222+
)
223+
yield from response.artifacts
224+
225+
if response.next_page_token != "":
226+
tokens.append(page_token)
227+
yield from _collect_artifacts(
228+
client,
229+
project_id,
230+
prefix=prefix,
231+
cluster_id=cluster_id,
232+
page_token=response.next_page_token,
233+
tokens=tokens,
234+
)
235+
236+
237+
def _add_resource_prefix(prefix: str, resource_path: str):
238+
if resource_path in prefix:
239+
return prefix
240+
return "/" + os.path.join(resource_path, prefix)
241+
242+
243+
def _get_prefix(prefix: str, lit_resource) -> str:
244+
if isinstance(lit_resource, Externalv1LightningappInstance):
245+
return _add_resource_prefix(prefix, f"lightningapps/{lit_resource.id}")
246+
247+
return _add_resource_prefix(prefix, f"cloudspaces/{lit_resource.id}")

tests/tests_app/cli/test_cp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ def test_cp_cloud_to_local(tmpdir, monkeypatch):
7171
memberships=[V1Membership(name="project-0")]
7272
)
7373

74+
clusters = MagicMock()
75+
clusters.clusters = [MagicMock()]
76+
client.projects_service_list_project_cluster_bindings.return_value = clusters
77+
7478
client.lightningapp_instance_service_list_lightningapp_instances.return_value = V1ListLightningappInstancesResponse(
7579
lightningapps=[
7680
Externalv1LightningappInstance(
@@ -80,7 +84,7 @@ def test_cp_cloud_to_local(tmpdir, monkeypatch):
8084
]
8185
)
8286

83-
client.lightningapp_instance_service_list_lightningapp_instance_artifacts.return_value = (
87+
client.lightningapp_instance_service_list_project_artifacts.return_value = (
8488
V1ListLightningappInstanceArtifactsResponse(
8589
artifacts=[
8690
V1LightningappInstanceArtifact(

0 commit comments

Comments
 (0)