Skip to content

Commit 104290e

Browse files
tchatonethanwharrisjustusschockthomas
authored
[App] Add rm one level below project level (#16740)
Co-authored-by: Ethan Harris <[email protected]> Co-authored-by: Justus Schock <[email protected]> Co-authored-by: thomas <[email protected]>
1 parent c407441 commit 104290e

File tree

11 files changed

+236
-8
lines changed

11 files changed

+236
-8
lines changed

examples/app_commands_and_api/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from command import CustomCommand, CustomConfig
22

33
from lightning import LightningFlow
4-
from lightning_app.api import Get, Post
5-
from lightning_app.core.app import LightningApp
4+
from lightning.app.api import Get, Post
5+
from lightning.app.core.app import LightningApp
66

77

88
async def handler():

src/lightning/app/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
99

1010
### Added
1111

12+
- Added Storage Commands ([#16740](https://github.com/Lightning-AI/lightning/pull/16740))
13+
* `rm`: Delete files from your Cloud Platform Filesystem
14+
1215
- Added `lightning connect data` to register data connection to private s3 buckets ([#16738](https://github.com/Lightning-AI/lightning/pull/16738))
1316

1417

src/lightning/app/cli/commands/cp.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def cp(src_path: str, dst_path: str, r: bool = False, recursive: bool = False) -
5757
if pwd == "/" or len(pwd.split("/")) == 1:
5858
return _error_and_exit("Uploading files at the project level isn't allowed yet.")
5959

60-
client = LightningClient()
60+
client = LightningClient(retry=False)
6161

6262
src_path, src_remote = _sanitize_path(src_path, pwd)
6363
dst_path, dst_remote = _sanitize_path(dst_path, pwd)
@@ -87,6 +87,9 @@ def _upload_files(live, client: LightningClient, local_src: str, remote_dst: str
8787
else:
8888
project_id = _get_project_id_from_name(remote_dst)
8989

90+
if len(remote_splits) > 2:
91+
remote_dst = os.path.join(*remote_splits[2:])
92+
9093
local_src = Path(local_src).resolve()
9194
upload_paths = []
9295

@@ -101,6 +104,8 @@ def _upload_files(live, client: LightningClient, local_src: str, remote_dst: str
101104

102105
clusters = client.projects_service_list_project_cluster_bindings(project_id)
103106

107+
live.stop()
108+
104109
for upload_path in upload_paths:
105110
for cluster in clusters.clusters:
106111
filename = str(upload_path).replace(str(os.getcwd()), "")[1:]

src/lightning/app/cli/commands/ls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import Generator, List, Optional
1919

2020
import click
21+
import lightning_cloud
2122
import rich
2223
from lightning_cloud.openapi import Externalv1LightningappInstance
2324
from rich.console import Console
@@ -255,7 +256,7 @@ def _collect_artifacts(
255256
page_token=response.next_page_token,
256257
tokens=tokens,
257258
)
258-
except Exception:
259+
except lightning_cloud.openapi.rest.ApiException:
259260
# Note: This is triggered when the request is wrong.
260261
# This is currently happening due to looping through the user clusters.
261262
pass

src/lightning/app/cli/commands/rm.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright The Lightning AI team.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
17+
import click
18+
import lightning_cloud
19+
import rich
20+
21+
from lightning.app.cli.commands.ls import _add_colors, _get_prefix
22+
from lightning.app.cli.commands.pwd import _pwd
23+
from lightning.app.utilities.app_helpers import Logger
24+
from lightning.app.utilities.cli_helpers import _error_and_exit
25+
from lightning.app.utilities.network import LightningClient
26+
27+
logger = Logger(__name__)
28+
29+
30+
@click.argument("rm_path", required=True)
31+
@click.option("-r", required=False, hidden=True)
32+
@click.option("--recursive", required=False, hidden=True)
33+
def rm(rm_path: str, r: bool = False, recursive: bool = False) -> None:
34+
"""Delete files on the Lightning Cloud filesystem."""
35+
36+
root = _pwd()
37+
38+
if rm_path in (".", ".."):
39+
return _error_and_exit('rm "." and ".." may not be removed')
40+
41+
if ".." in rm_path:
42+
return _error_and_exit('rm ".." or higher may not be removed')
43+
44+
root = os.path.join(root, rm_path)
45+
splits = [split for split in root.split("/") if split != ""]
46+
47+
if root == "/" or len(splits) == 1:
48+
return _error_and_exit("rm at the project level isn't supported")
49+
50+
client = LightningClient(retry=False)
51+
projects = client.projects_service_list_memberships()
52+
53+
project = [project for project in projects.memberships if project.name == splits[0]]
54+
55+
# This happens if the user changes cluster and the project doesn't exist.
56+
if len(project) == 0:
57+
return _error_and_exit(
58+
f"There isn't any Lightning Project matching the name {splits[0]}." " HINT: Use `lightning cd`."
59+
)
60+
61+
project_id = project[0].project_id
62+
63+
# Parallelise calls
64+
lit_apps = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project_id, async_req=True)
65+
lit_cloud_spaces = client.cloud_space_service_list_cloud_spaces(project_id=project_id, async_req=True)
66+
67+
lit_apps = lit_apps.get().lightningapps
68+
lit_cloud_spaces = lit_cloud_spaces.get().cloudspaces
69+
70+
lit_ressources = [lit_resource for lit_resource in lit_cloud_spaces if lit_resource.name == splits[1]]
71+
72+
if len(lit_ressources) == 0:
73+
74+
lit_ressources = [lit_resource for lit_resource in lit_apps if lit_resource.name == splits[1]]
75+
76+
if len(lit_ressources) == 0:
77+
_error_and_exit(f"There isn't any Lightning Ressource matching the name {splits[1]}.")
78+
79+
lit_resource = lit_ressources[0]
80+
81+
prefix = "/".join(splits[2:])
82+
prefix = _get_prefix(prefix, lit_resource)
83+
84+
clusters = client.projects_service_list_project_cluster_bindings(project_id)
85+
succeeded = False
86+
87+
for cluster in clusters.clusters:
88+
try:
89+
client.lightningapp_instance_service_delete_project_artifact(
90+
project_id=project_id,
91+
cluster_id=cluster.cluster_id,
92+
filename=prefix,
93+
)
94+
succeeded = True
95+
break
96+
except lightning_cloud.openapi.rest.ApiException:
97+
pass
98+
99+
prefix = os.path.join(*splits)
100+
101+
if succeeded:
102+
rich.print(_add_colors(f"Successfuly deleted `{prefix}`.", color="green"))
103+
else:
104+
return _error_and_exit(f"No file or folder named `{prefix}` was found.")

src/lightning/app/cli/lightning_cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from lightning.app.cli.commands.logs import logs
3939
from lightning.app.cli.commands.ls import ls
4040
from lightning.app.cli.commands.pwd import pwd
41+
from lightning.app.cli.commands.rm import rm
4142
from lightning.app.cli.connect.app import (
4243
_list_app_commands,
4344
_retrieve_connection_to_an_app,
@@ -144,6 +145,7 @@ def disconnect() -> None:
144145
_main.command(hidden=True)(cd)
145146
_main.command(hidden=True)(cp)
146147
_main.command(hidden=True)(pwd)
148+
_main.command(hidden=True)(rm)
147149
show.command()(logs)
148150

149151

src/lightning/app/testing/testing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def run_app_in_cloud(
282282
token = res.json()["token"]
283283

284284
# 3. Disconnect from the App if any.
285-
Popen("lightning disconnect", shell=True).wait()
285+
Popen("lightning logout", shell=True).wait()
286286

287287
# 4. Launch the application in the cloud from the Lightning CLI.
288288
with tempfile.TemporaryDirectory() as tmpdir:
@@ -392,6 +392,9 @@ def run_app_in_cloud(
392392

393393
admin_page.locator(f'[data-cy="{name}"]').click()
394394

395+
app_url = admin_page.url
396+
admin_page.goto(app_url + "/logs")
397+
395398
client = LightningClient()
396399
project_id = _get_project(client).project_id
397400

src/lightning/app/utilities/cli_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,6 @@ def _check_environment_and_redirect():
357357
return
358358

359359

360-
def _error_and_exit(msg: str) -> str:
360+
def _error_and_exit(msg: str) -> None:
361361
rich.print(f"[red]ERROR[/red]: {msg}")
362362
sys.exit(0)

tests/integrations_app/public/test_commands_and_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def test_commands_and_api_example_cloud() -> None:
2323
cmd_2 = "python -m lightning command with client --name=this"
2424
cmd_3 = "python -m lightning command without client --name=is"
2525
cmd_4 = "python -m lightning command without client --name=awesome"
26-
cmd_5 = "lightning disconnect app"
26+
cmd_5 = "lightning logout"
2727
process = Popen(" && ".join([cmd_1, cmd_2, cmd_3, cmd_4, cmd_5]), shell=True)
2828
process.wait()
2929

tests/tests_app/cli/test_rm.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import os
2+
import sys
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
from lightning_cloud.openapi import (
7+
Externalv1LightningappInstance,
8+
V1LightningappInstanceArtifact,
9+
V1ListCloudSpacesResponse,
10+
V1ListLightningappInstanceArtifactsResponse,
11+
V1ListLightningappInstancesResponse,
12+
V1ListMembershipsResponse,
13+
V1Membership,
14+
)
15+
16+
from lightning.app.cli.commands import cd, ls, rm
17+
18+
19+
@pytest.mark.skipif(sys.platform == "win32", reason="not supported on windows yet")
20+
def test_rm(monkeypatch):
21+
"""This test validates rm behaves as expected."""
22+
23+
if os.path.exists(cd._CD_FILE):
24+
os.remove(cd._CD_FILE)
25+
26+
client = MagicMock()
27+
client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
28+
memberships=[
29+
V1Membership(name="project-0", project_id="project-id-0"),
30+
V1Membership(name="project-1", project_id="project-id-1"),
31+
V1Membership(name="project 2", project_id="project-id-2"),
32+
]
33+
)
34+
35+
client.lightningapp_instance_service_list_lightningapp_instances().get.return_value = (
36+
V1ListLightningappInstancesResponse(
37+
lightningapps=[
38+
Externalv1LightningappInstance(
39+
name="app-name-0",
40+
id="app-id-0",
41+
),
42+
Externalv1LightningappInstance(
43+
name="app-name-1",
44+
id="app-id-1",
45+
),
46+
Externalv1LightningappInstance(
47+
name="app name 2",
48+
id="app-id-1",
49+
),
50+
]
51+
)
52+
)
53+
54+
client.cloud_space_service_list_cloud_spaces().get.return_value = V1ListCloudSpacesResponse(cloudspaces=[])
55+
56+
clusters = MagicMock()
57+
clusters.clusters = [MagicMock()]
58+
client.projects_service_list_project_cluster_bindings.return_value = clusters
59+
60+
def fn(*args, prefix, **kwargs):
61+
splits = [split for split in prefix.split("/") if split != ""]
62+
if len(splits) == 2:
63+
return V1ListLightningappInstanceArtifactsResponse(
64+
artifacts=[
65+
V1LightningappInstanceArtifact(filename="file_1.txt"),
66+
V1LightningappInstanceArtifact(filename="folder_1/file_2.txt"),
67+
V1LightningappInstanceArtifact(filename="folder_2/folder_3/file_3.txt"),
68+
V1LightningappInstanceArtifact(filename="folder_2/file_4.txt"),
69+
]
70+
)
71+
elif splits[-1] == "folder_1":
72+
return V1ListLightningappInstanceArtifactsResponse(
73+
artifacts=[V1LightningappInstanceArtifact(filename="file_2.txt")]
74+
)
75+
elif splits[-1] == "folder_2":
76+
return V1ListLightningappInstanceArtifactsResponse(
77+
artifacts=[
78+
V1LightningappInstanceArtifact(filename="folder_3/file_3.txt"),
79+
V1LightningappInstanceArtifact(filename="file_4.txt"),
80+
]
81+
)
82+
elif splits[-1] == "folder_3":
83+
return V1ListLightningappInstanceArtifactsResponse(
84+
artifacts=[
85+
V1LightningappInstanceArtifact(filename="file_3.txt"),
86+
]
87+
)
88+
89+
client.lightningapp_instance_service_list_project_artifacts = fn
90+
91+
client.lightningapp_instance_service_delete_project_artifact = MagicMock()
92+
93+
monkeypatch.setattr(rm, "LightningClient", MagicMock(return_value=client))
94+
monkeypatch.setattr(ls, "LightningClient", MagicMock(return_value=client))
95+
96+
assert ls.ls() == ["project-0", "project-1", "project 2"]
97+
assert "/project-0" == cd.cd("project-0", verify=False)
98+
99+
assert f"/project-0{os.sep}app-name-1" == cd.cd("app-name-1", verify=False)
100+
101+
assert f"/project-0{os.sep}app-name-1{os.sep}folder_1" == cd.cd("folder_1", verify=False)
102+
103+
rm.rm("file_2.txt")
104+
105+
kwargs = client.lightningapp_instance_service_delete_project_artifact._mock_call_args.kwargs
106+
assert kwargs["project_id"] == "project-id-0"
107+
assert kwargs["filename"] == "/lightningapps/app-id-1/folder_1/file_2.txt"
108+
109+
os.remove(cd._CD_FILE)

0 commit comments

Comments
 (0)