Skip to content

Commit 4c35867

Browse files
authored
[App] Introduce Commands (#13602)
1 parent a8d7b44 commit 4c35867

File tree

29 files changed

+858
-63
lines changed

29 files changed

+858
-63
lines changed

.github/workflows/ci-app_cloud_e2e_test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
- custom_work_dependencies
5555
- drive
5656
- payload
57+
- commands
5758
timeout-minutes: 35
5859
steps:
5960
- uses: actions/checkout@v2
@@ -155,7 +156,7 @@ jobs:
155156
shell: bash
156157
run: |
157158
mkdir -p ${VIDEO_LOCATION}
158-
HEADLESS=1 python -m pytest tests/tests_app_examples/test_${{ matrix.app_name }}.py::test_${{ matrix.app_name }}_example_cloud --timeout=900 --capture=no -v --color=yes
159+
HEADLESS=1 PACKAGE_LIGHTNING=1 python -m pytest tests/tests_app_examples/test_${{ matrix.app_name }}.py::test_${{ matrix.app_name }}_example_cloud --timeout=900 --capture=no -v --color=yes
159160
# Delete the artifacts if successful
160161
rm -r ${VIDEO_LOCATION}/${{ matrix.app_name }}
161162

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ celerybeat-schedule
109109

110110
# dotenv
111111
.env
112+
.env_stagging
112113

113114
# virtualenv
114115
.venv
@@ -160,3 +161,5 @@ tags
160161
.tags
161162
src/lightning_app/ui/*
162163
*examples/template_react_ui*
164+
hars*
165+
artifacts/*

examples/app_commands/.lightning

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name: app-commands

examples/app_commands/app.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from command import CustomCommand, CustomConfig
2+
3+
from lightning import LightningFlow
4+
from lightning_app.core.app import LightningApp
5+
6+
7+
class ChildFlow(LightningFlow):
8+
def trigger_method(self, name: str):
9+
print(f"Hello {name}")
10+
11+
def configure_commands(self):
12+
return [{"nested_trigger_command": self.trigger_method}]
13+
14+
15+
class FlowCommands(LightningFlow):
16+
def __init__(self):
17+
super().__init__()
18+
self.names = []
19+
self.child_flow = ChildFlow()
20+
21+
def run(self):
22+
if len(self.names):
23+
print(self.names)
24+
25+
def trigger_without_client_command(self, name: str):
26+
self.names.append(name)
27+
28+
def trigger_with_client_command(self, config: CustomConfig):
29+
self.names.append(config.name)
30+
31+
def configure_commands(self):
32+
commands = [
33+
{"trigger_without_client_command": self.trigger_without_client_command},
34+
{"trigger_with_client_command": CustomCommand(self.trigger_with_client_command)},
35+
]
36+
return commands + self.child_flow.configure_commands()
37+
38+
39+
app = LightningApp(FlowCommands())

examples/app_commands/command.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from argparse import ArgumentParser
2+
3+
from pydantic import BaseModel
4+
5+
from lightning.app.utilities.commands import ClientCommand
6+
7+
8+
class CustomConfig(BaseModel):
9+
name: str
10+
11+
12+
class CustomCommand(ClientCommand):
13+
def run(self):
14+
parser = ArgumentParser()
15+
parser.add_argument("--name", type=str)
16+
args = parser.parse_args()
17+
self.invoke_handler(config=CustomConfig(name=args.name))

src/lightning_app/CHANGELOG.md

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

99
### Added
1010

11+
- Add support for `Lightning App Commands` through the `configure_commands` hook on the Lightning Flow and the `ClientCommand` ([#13602](https://github.com/Lightning-AI/lightning/pull/13602))
12+
13+
### Changed
14+
1115
- Update the Lightning App docs ([#13537](https://github.com/PyTorchLightning/pytorch-lightning/pull/13537))
1216

1317
### Changed

src/lightning_app/cli/lightning_cli.py

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import logging
22
import os
3+
import sys
4+
from argparse import ArgumentParser
35
from pathlib import Path
46
from typing import List, Tuple, Union
7+
from uuid import uuid4
58

69
import click
10+
import requests
711
from requests.exceptions import ConnectionError
812

913
from lightning_app import __version__ as ver
1014
from lightning_app.cli import cmd_init, cmd_install, cmd_pl_init, cmd_react_ui_init
1115
from lightning_app.core.constants import get_lightning_cloud_url, LOCAL_LAUNCH_ADMIN_VIEW
1216
from lightning_app.runners.runtime import dispatch
1317
from lightning_app.runners.runtime_type import RuntimeType
14-
from lightning_app.utilities.cli_helpers import _format_input_env_variables
18+
from lightning_app.utilities.cli_helpers import (
19+
_format_input_env_variables,
20+
_retrieve_application_url_and_available_commands,
21+
)
1522
from lightning_app.utilities.install_components import register_all_external_components
1623
from lightning_app.utilities.login import Auth
24+
from lightning_app.utilities.state import headers_for
1725

1826
logger = logging.getLogger(__name__)
1927

@@ -26,14 +34,23 @@ def get_app_url(runtime_type: RuntimeType, *args) -> str:
2634
return "http://127.0.0.1:7501/admin" if LOCAL_LAUNCH_ADMIN_VIEW else "http://127.0.0.1:7501/view"
2735

2836

37+
def main():
38+
if len(sys.argv) == 1:
39+
_main()
40+
elif sys.argv[1] in _main.commands.keys() or sys.argv[1] == "--help":
41+
_main()
42+
else:
43+
app_command()
44+
45+
2946
@click.group()
3047
@click.version_option(ver)
31-
def main():
48+
def _main():
3249
register_all_external_components()
3350
pass
3451

3552

36-
@main.command()
53+
@_main.command()
3754
def login():
3855
"""Log in to your Lightning.ai account."""
3956
auth = Auth()
@@ -46,7 +63,7 @@ def login():
4663
exit(1)
4764

4865

49-
@main.command()
66+
@_main.command()
5067
def logout():
5168
"""Log out of your Lightning.ai account."""
5269
Auth().clear()
@@ -93,7 +110,7 @@ def on_before_run(*args):
93110
click.echo("Application is ready in the cloud")
94111

95112

96-
@main.group()
113+
@_main.group()
97114
def run():
98115
"""Run your application."""
99116

@@ -125,31 +142,83 @@ def run_app(
125142
_run_app(file, cloud, without_server, no_cache, name, blocking, open_ui, env)
126143

127144

128-
@main.group(hidden=True)
145+
def app_command():
146+
"""Execute a function in a running application from its name."""
147+
from lightning_app.utilities.commands.base import _download_command
148+
149+
logger.warn("Lightning Commands are a beta feature and APIs aren't stable yet.")
150+
151+
debug_mode = bool(int(os.getenv("DEBUG", "0")))
152+
153+
parser = ArgumentParser()
154+
parser.add_argument("--app_id", default=None, type=str, help="Optional argument to identify an application.")
155+
hparams, argv = parser.parse_known_args()
156+
157+
# 1: Collect the url and comments from the running application
158+
url, commands = _retrieve_application_url_and_available_commands(hparams.app_id)
159+
if url is None or commands is None:
160+
raise Exception("We couldn't find any matching running app.")
161+
162+
if not commands:
163+
raise Exception("This application doesn't expose any commands yet.")
164+
165+
command = argv[0]
166+
167+
command_names = [c["command"] for c in commands]
168+
if command not in command_names:
169+
raise Exception(f"The provided command {command} isn't available in {command_names}")
170+
171+
# 2: Send the command from the user
172+
command_metadata = [c for c in commands if c["command"] == command][0]
173+
params = command_metadata["params"]
174+
175+
# 3: Execute the command
176+
if not command_metadata["is_client_command"]:
177+
# TODO: Improve what is supported there.
178+
kwargs = {k.split("=")[0].replace("--", ""): k.split("=")[1] for k in argv[1:]}
179+
for param in params:
180+
if param not in kwargs:
181+
raise Exception(f"The argument --{param}=X hasn't been provided.")
182+
json = {
183+
"command_name": command,
184+
"command_arguments": kwargs,
185+
"affiliation": command_metadata["affiliation"],
186+
"id": str(uuid4()),
187+
}
188+
resp = requests.post(url + "/api/v1/commands", json=json, headers=headers_for({}))
189+
assert resp.status_code == 200, resp.json()
190+
else:
191+
client_command, models = _download_command(command_metadata, hparams.app_id, debug_mode=debug_mode)
192+
client_command._setup(metadata=command_metadata, models=models, app_url=url)
193+
sys.argv = argv
194+
client_command.run()
195+
196+
197+
@_main.group(hidden=True)
129198
def fork():
130199
"""Fork an application."""
131200
pass
132201

133202

134-
@main.group(hidden=True)
203+
@_main.group(hidden=True)
135204
def stop():
136205
"""Stop your application."""
137206
pass
138207

139208

140-
@main.group(hidden=True)
209+
@_main.group(hidden=True)
141210
def delete():
142211
"""Delete an application."""
143212
pass
144213

145214

146-
@main.group(name="list", hidden=True)
215+
@_main.group(name="list", hidden=True)
147216
def get_list():
148217
"""List your applications."""
149218
pass
150219

151220

152-
@main.group()
221+
@_main.group()
153222
def install():
154223
"""Install Lightning apps and components."""
155224

@@ -207,7 +276,7 @@ def install_component(name, yes, version):
207276
cmd_install.gallery_component(name, yes, version)
208277

209278

210-
@main.group()
279+
@_main.group()
211280
def init():
212281
"""Init a Lightning app and component."""
213282

src/lightning_app/components/python/tracer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,6 @@ def __init__(
9393
:language: python
9494
"""
9595
super().__init__(**kwargs)
96-
if not os.path.exists(script_path):
97-
raise FileNotFoundError(f"The provided `script_path` {script_path}` wasn't found.")
9896
self.script_path = str(script_path)
9997
if isinstance(script_args, str):
10098
script_args = script_args.split(" ")
@@ -105,6 +103,8 @@ def __init__(
105103
setattr(self, name, None)
106104

107105
def run(self, **kwargs):
106+
if not os.path.exists(self.script_path):
107+
raise FileNotFoundError(f"The provided `script_path` {self.script_path}` wasn't found.")
108108
kwargs = {k: v.value if isinstance(v, Payload) else v for k, v in kwargs.items()}
109109
init_globals = globals()
110110
init_globals.update(kwargs)

0 commit comments

Comments
 (0)