Skip to content

Commit 1c5133c

Browse files
tchatonBorda
authored andcommitted
[App] Improve lightning connect experience (#16035)
(cherry picked from commit e522a12)
1 parent e30d93a commit 1c5133c

File tree

11 files changed

+171
-151
lines changed

11 files changed

+171
-151
lines changed

examples/app_installation_commands/app.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ def run(self):
1313
print("lmdb successfully installed")
1414
print("accessing a module in a Work or Flow body works!")
1515

16-
@property
17-
def ready(self) -> bool:
18-
return True
16+
17+
class RootFlow(L.LightningFlow):
18+
def __init__(self, work):
19+
super().__init__()
20+
self.work = work
21+
22+
def run(self):
23+
self.work.run()
1924

2025

2126
print(f"accessing an object in main code body works!: version={lmdb.version()}")
@@ -24,4 +29,4 @@ def ready(self) -> bool:
2429
# run on a cloud machine
2530
compute = L.CloudCompute("cpu")
2631
worker = YourComponent(cloud_compute=compute)
27-
app = L.LightningApp(worker)
32+
app = L.LightningApp(RootFlow(worker))

src/lightning_app/CHANGELOG.md

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

1212
- Added `Lightning{Flow,Work}.lightningignores` attributes to programmatically ignore files before uploading to the cloud ([#15818](https://github.com/Lightning-AI/lightning/pull/15818))
1313

14+
- Added a progres bar while connecting to an app through the CLI ([#16035](https://github.com/Lightning-AI/lightning/pull/16035))
15+
1416

1517
### Changed
1618

src/lightning_app/cli/commands/connection.py

Lines changed: 130 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import click
99
import psutil
1010
from lightning_utilities.core.imports import package_available
11+
from rich.progress import Progress
1112

1213
from lightning_app.utilities.cli_helpers import _LightningAppOpenAPIRetriever
1314
from lightning_app.utilities.cloud import _get_project
@@ -16,15 +17,33 @@
1617
from lightning_app.utilities.network import LightningClient
1718

1819
_HOME = os.path.expanduser("~")
19-
_PPID = str(psutil.Process(os.getpid()).ppid())
20+
_PPID = os.getenv("LIGHTNING_CONNECT_PPID", str(psutil.Process(os.getpid()).ppid()))
2021
_LIGHTNING_CONNECTION = os.path.join(_HOME, ".lightning", "lightning_connection")
2122
_LIGHTNING_CONNECTION_FOLDER = os.path.join(_LIGHTNING_CONNECTION, _PPID)
2223

2324

2425
@click.argument("app_name_or_id", required=True)
25-
@click.option("-y", "--yes", required=False, is_flag=True, help="Whether to download the commands automatically.")
26-
def connect(app_name_or_id: str, yes: bool = False):
27-
"""Connect to a Lightning App."""
26+
def connect(app_name_or_id: str):
27+
"""Connect your local terminal to a running lightning app.
28+
29+
After connecting, the lightning CLI will respond to commands exposed by the app.
30+
31+
Example:
32+
33+
\b
34+
# connect to an app named pizza-cooker-123
35+
lightning connect pizza-cooker-123
36+
\b
37+
# this will now show the commands exposed by pizza-cooker-123
38+
lightning --help
39+
\b
40+
# while connected, you can run the cook-pizza command exposed
41+
# by pizza-cooker-123.BTW, this should arguably generate an exception :-)
42+
lightning cook-pizza --flavor pineapple
43+
\b
44+
# once done, disconnect and go back to the standard lightning CLI commands
45+
lightning disconnect
46+
"""
2847
from lightning_app.utilities.commands.base import _download_command
2948

3049
_clean_lightning_connection()
@@ -47,51 +66,64 @@ def connect(app_name_or_id: str, yes: bool = False):
4766
click.echo(f"You are already connected to the cloud Lightning App: {app_name_or_id}.")
4867
else:
4968
disconnect()
50-
connect(app_name_or_id, yes)
69+
connect(app_name_or_id)
5170

5271
elif app_name_or_id.startswith("localhost"):
5372

54-
if app_name_or_id != "localhost":
55-
raise Exception("You need to pass localhost to connect to the local Lightning App.")
73+
with Progress() as progress_bar:
74+
connecting = progress_bar.add_task("[magenta]Setting things up for you...", total=1.0)
5675

57-
retriever = _LightningAppOpenAPIRetriever(None)
76+
if app_name_or_id != "localhost":
77+
raise Exception("You need to pass localhost to connect to the local Lightning App.")
5878

59-
if retriever.api_commands is None:
60-
raise Exception(f"The commands weren't found. Is your app {app_name_or_id} running ?")
79+
retriever = _LightningAppOpenAPIRetriever(None)
6180

62-
commands_folder = os.path.join(_LIGHTNING_CONNECTION_FOLDER, "commands")
63-
if not os.path.exists(commands_folder):
64-
os.makedirs(commands_folder)
81+
if retriever.api_commands is None:
82+
raise Exception(f"Connection wasn't successful. Is your app {app_name_or_id} running?")
6583

66-
_write_commands_metadata(retriever.api_commands)
84+
increment = 1 / (1 + len(retriever.api_commands))
6785

68-
with open(os.path.join(commands_folder, "openapi.json"), "w") as f:
69-
json.dump(retriever.openapi, f)
86+
progress_bar.update(connecting, advance=increment)
7087

71-
_install_missing_requirements(retriever, yes)
88+
commands_folder = os.path.join(_LIGHTNING_CONNECTION_FOLDER, "commands")
89+
if not os.path.exists(commands_folder):
90+
os.makedirs(commands_folder)
7291

73-
for command_name, metadata in retriever.api_commands.items():
74-
if "cls_path" in metadata:
75-
target_file = os.path.join(commands_folder, f"{command_name.replace(' ','_')}.py")
76-
_download_command(
77-
command_name,
78-
metadata["cls_path"],
79-
metadata["cls_name"],
80-
None,
81-
target_file=target_file,
82-
)
83-
repr_command_name = command_name.replace("_", " ")
84-
click.echo(f"Storing `{repr_command_name}` at {target_file}")
85-
else:
86-
with open(os.path.join(commands_folder, f"{command_name}.txt"), "w") as f:
87-
f.write(command_name)
92+
_write_commands_metadata(retriever.api_commands)
93+
94+
with open(os.path.join(commands_folder, "openapi.json"), "w") as f:
95+
json.dump(retriever.openapi, f)
8896

89-
click.echo(f"You can review all the downloaded commands at {commands_folder}")
97+
_install_missing_requirements(retriever)
98+
99+
for command_name, metadata in retriever.api_commands.items():
100+
if "cls_path" in metadata:
101+
target_file = os.path.join(commands_folder, f"{command_name.replace(' ','_')}.py")
102+
_download_command(
103+
command_name,
104+
metadata["cls_path"],
105+
metadata["cls_name"],
106+
None,
107+
target_file=target_file,
108+
)
109+
else:
110+
with open(os.path.join(commands_folder, f"{command_name}.txt"), "w") as f:
111+
f.write(command_name)
112+
113+
progress_bar.update(connecting, advance=increment)
90114

91115
with open(connected_file, "w") as f:
92116
f.write(app_name_or_id + "\n")
93117

94-
click.echo("You are connected to the local Lightning App.")
118+
click.echo("The lightning CLI now responds to app commands. Use 'lightning --help' to see them.")
119+
click.echo(" ")
120+
121+
Popen(
122+
f"LIGHTNING_CONNECT_PPID={_PPID} {sys.executable} -m lightning --help",
123+
shell=True,
124+
stdout=sys.stdout,
125+
stderr=sys.stderr,
126+
).wait()
95127

96128
elif matched_connection_path:
97129

@@ -101,40 +133,39 @@ def connect(app_name_or_id: str, yes: bool = False):
101133
commands = os.path.join(_LIGHTNING_CONNECTION_FOLDER, "commands")
102134
shutil.copytree(matched_commands, commands)
103135
shutil.copy(matched_connected_file, connected_file)
104-
copied_files = [el for el in os.listdir(commands) if os.path.splitext(el)[1] == ".py"]
105-
click.echo("Found existing connection, reusing cached commands")
106-
for target_file in copied_files:
107-
pretty_command_name = os.path.splitext(target_file)[0].replace("_", " ")
108-
click.echo(f"Storing `{pretty_command_name}` at {os.path.join(commands, target_file)}")
109136

110-
click.echo(f"You can review all the commands at {commands}")
137+
click.echo("The lightning CLI now responds to app commands. Use 'lightning --help' to see them.")
111138
click.echo(" ")
112-
click.echo(f"You are connected to the cloud Lightning App: {app_name_or_id}.")
113139

114-
else:
140+
Popen(
141+
f"LIGHTNING_CONNECT_PPID={_PPID} {sys.executable} -m lightning --help",
142+
shell=True,
143+
stdout=sys.stdout,
144+
stderr=sys.stderr,
145+
).wait()
115146

116-
retriever = _LightningAppOpenAPIRetriever(app_name_or_id)
147+
else:
148+
with Progress() as progress_bar:
149+
connecting = progress_bar.add_task("[magenta]Setting things up for you...", total=1.0)
150+
151+
retriever = _LightningAppOpenAPIRetriever(app_name_or_id)
152+
153+
if not retriever.api_commands:
154+
client = LightningClient()
155+
project = _get_project(client)
156+
apps = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project.project_id)
157+
click.echo(
158+
"We didn't find a matching App. Here are the available Apps that you can"
159+
f"connect to {[app.name for app in apps.lightningapps]}."
160+
)
161+
return
117162

118-
if not retriever.api_commands:
119-
client = LightningClient()
120-
project = _get_project(client)
121-
apps = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project.project_id)
122-
click.echo(
123-
"We didn't find a matching App. Here are the available Apps that could be "
124-
f"connected to {[app.name for app in apps.lightningapps]}."
125-
)
126-
return
163+
increment = 1 / (1 + len(retriever.api_commands))
127164

128-
_install_missing_requirements(retriever, yes)
165+
progress_bar.update(connecting, advance=increment)
129166

130-
if not yes:
131-
yes = click.confirm(
132-
f"The Lightning App `{app_name_or_id}` provides a command-line (CLI). "
133-
"Do you want to proceed and install its CLI ?"
134-
)
135-
click.echo(" ")
167+
_install_missing_requirements(retriever)
136168

137-
if yes:
138169
commands_folder = os.path.join(_LIGHTNING_CONNECTION_FOLDER, "commands")
139170
if not os.path.exists(commands_folder):
140171
os.makedirs(commands_folder)
@@ -151,26 +182,25 @@ def connect(app_name_or_id: str, yes: bool = False):
151182
retriever.app_id,
152183
target_file=target_file,
153184
)
154-
pretty_command_name = command_name.replace("_", " ")
155-
click.echo(f"Storing `{pretty_command_name}` at {target_file}")
156185
else:
157186
with open(os.path.join(commands_folder, f"{command_name}.txt"), "w") as f:
158187
f.write(command_name)
159188

160-
click.echo(f"You can review all the downloaded commands at {commands_folder}")
161-
162-
click.echo(" ")
163-
click.echo("The client interface has been successfully installed. ")
164-
click.echo("You can now run the following commands:")
165-
for command in retriever.api_commands:
166-
pretty_command_name = command.replace("_", " ")
167-
click.echo(f" lightning {pretty_command_name}")
189+
progress_bar.update(connecting, advance=increment)
168190

169191
with open(connected_file, "w") as f:
170192
f.write(retriever.app_name + "\n")
171193
f.write(retriever.app_id + "\n")
194+
195+
click.echo("The lightning CLI now responds to app commands. Use 'lightning --help' to see them.")
172196
click.echo(" ")
173-
click.echo(f"You are connected to the cloud Lightning App: {app_name_or_id}.")
197+
198+
Popen(
199+
f"LIGHTNING_CONNECT_PPID={_PPID} {sys.executable} -m lightning --help",
200+
shell=True,
201+
stdout=sys.stdout,
202+
stderr=sys.stderr,
203+
).wait()
174204

175205

176206
def disconnect(logout: bool = False):
@@ -244,22 +274,37 @@ def _list_app_commands(echo: bool = True) -> List[str]:
244274
click.echo("The current Lightning App doesn't have commands.")
245275
return []
246276

277+
app_info = metadata[command_names[0]].get("app_info", None)
278+
279+
title, description, on_connect_end = "Lightning", None, None
280+
if app_info:
281+
title = app_info.get("title")
282+
description = app_info.get("description")
283+
on_connect_end = app_info.get("on_connect_end")
284+
247285
if echo:
248-
click.echo("Usage: lightning [OPTIONS] COMMAND [ARGS]...")
249-
click.echo("")
250-
click.echo(" --help Show this message and exit.")
286+
click.echo(f"{title} App")
287+
if description:
288+
click.echo("")
289+
click.echo("Description:")
290+
if description.endswith("\n"):
291+
description = description[:-2]
292+
click.echo(f" {description}")
251293
click.echo("")
252-
click.echo("Lightning App Commands")
294+
click.echo("Commands:")
253295
max_length = max(len(n) for n in command_names)
254296
for command_name in command_names:
255297
padding = (max_length + 1 - len(command_name)) * " "
256298
click.echo(f" {command_name}{padding}{metadata[command_name].get('description', '')}")
299+
if "LIGHTNING_CONNECT_PPID" in os.environ and on_connect_end:
300+
if on_connect_end.endswith("\n"):
301+
on_connect_end = on_connect_end[:-2]
302+
click.echo(on_connect_end)
257303
return command_names
258304

259305

260306
def _install_missing_requirements(
261307
retriever: _LightningAppOpenAPIRetriever,
262-
yes_global: bool = False,
263308
fail_if_missing: bool = False,
264309
):
265310
requirements = set()
@@ -281,20 +326,15 @@ def _install_missing_requirements(
281326
sys.exit(0)
282327

283328
for req in missing_requirements:
284-
if not yes_global:
285-
yes = click.confirm(
286-
f"The Lightning App CLI `{retriever.app_id}` requires `{req}`. Do you want to install it ?"
287-
)
288-
else:
289-
print(f"Installing missing `{req}` requirement.")
290-
yes = yes_global
291-
if yes:
292-
std_out_out = get_logfile("output.log")
293-
with open(std_out_out, "wb") as stdout:
294-
Popen(
295-
f"{sys.executable} -m pip install {req}", shell=True, stdout=stdout, stderr=sys.stderr
296-
).wait()
297-
print()
329+
std_out_out = get_logfile("output.log")
330+
with open(std_out_out, "wb") as stdout:
331+
Popen(
332+
f"{sys.executable} -m pip install {req}",
333+
shell=True,
334+
stdout=stdout,
335+
stderr=stdout,
336+
).wait()
337+
os.remove(std_out_out)
298338

299339

300340
def _clean_lightning_connection():

src/lightning_app/cli/lightning_cli.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ def main() -> None:
7676
else:
7777
message = f"You are connected to the cloud Lightning App: {app_name}."
7878

79-
click.echo(" ")
80-
8179
if (len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"]) or len(sys.argv) == 1:
8280
_list_app_commands()
8381
else:

src/lightning_app/components/database/server.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from lightning_app.components.database.utilities import _create_database, _Delete, _Insert, _SelectAll, _Update
1515
from lightning_app.core.work import LightningWork
1616
from lightning_app.storage import Drive
17+
from lightning_app.utilities.app_helpers import Logger
1718
from lightning_app.utilities.imports import _is_sqlmodel_available
1819
from lightning_app.utilities.packaging.build_config import BuildConfig
1920

@@ -23,6 +24,9 @@
2324
SQLModel = object
2425

2526

27+
logger = Logger(__name__)
28+
29+
2630
# Required to avoid Uvicorn Server overriding Lightning App signal handlers.
2731
# Discussions: https://github.com/encode/uvicorn/discussions/1708
2832
class _DatabaseUvicornServer(uvicorn.Server):
@@ -167,7 +171,7 @@ def store_database(self):
167171
drive = Drive("lit://database", component_name=self.name, root_folder=tmpdir)
168172
drive.put(os.path.basename(tmp_db_filename))
169173

170-
print("Stored the database to the Drive.")
174+
logger.debug("Stored the database to the Drive.")
171175
except Exception:
172176
print(traceback.print_exc())
173177

src/lightning_app/runners/multiprocess.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def dispatch(self, *args: Any, open_ui: bool = True, **kwargs: Any):
8282

8383
if is_overridden("configure_commands", self.app.root):
8484
commands = _prepare_commands(self.app)
85-
apis += _commands_to_api(commands)
85+
apis += _commands_to_api(commands, info=self.app.info)
8686

8787
kwargs = dict(
8888
apis=apis,

0 commit comments

Comments
 (0)