Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def run_notebook(self, config: RunNotebookConfig):
def configure_commands(self):
# 3. Returns a list of dictionaries with the format:
# {"command_name": CustomClientCommand(method=self.custom_server_handler)}
return [{"run-notebook": RunNotebook(method=self.run_notebook)}]
return [{"run notebook": RunNotebook(method=self.run_notebook)}]

def configure_layout(self):
# 4. Dynamically display the notebooks in the Lightning App View.
Expand Down
55 changes: 53 additions & 2 deletions docs/source-app/workflows/build_command_line_interface/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,68 @@
Develop a CLI without client side code
**************************************

1. Implement a simple CLI
^^^^^^^^^^^^^^^^^^^^^^^^^

In order to create your first CLI, you need to override the :class:`~lightning_app.core.flow.LightningFlow.configure_commands` hook and return a list of dictionaries where the keys are the commands and the values are the server-side handlers.

.. literalinclude:: example_command.py

After copy-pasting the code above to a file ``app.py``, execute the following command in your terminal in your first terminal.

2. Run the App
^^^^^^^^^^^^^^

.. code-block::

lightning run app app.py

And you find the following:
And you can find the following in your terminal:

.. code-block::

Your Lightning App is starting. This won't take long.
INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view
[]

In another terminal, you can trigger the command line exposed by your application.
3. Connect to a running App
^^^^^^^^^^^^^^^^^^^^^^^^^^^

In another terminal, you connect to the running application.
When you connect to an application, Lightning CLI is replaced by the App CLI. To exit the App CLI, you need to run lightning disconnect.

.. code-block::

lightning connect localhost

And list of the available commands:

.. code-block::

lightning --help
You are connected to the cloud Lightning App: localhost.
Usage: lightning [OPTIONS] COMMAND [ARGS]...

--help Show this message and exit.

Lightning App Commands
add Description

And you can find the arguments of the commands.

.. code-block::

lightning add --help
You are connected to the cloud Lightning App: localhost.
Usage: lightning add [ARGS]...

Options
name: Add description

4. Execute a command
^^^^^^^^^^^^^^^^^^^^

And then you can trigger the command line exposed by your application.

.. code-block::

Expand All @@ -39,6 +82,14 @@ In your first terminal, **Received name: my_name** and **["my_name"]** are print
Received name: my_name
["my_name]

5. Disconnect
^^^^^^^^^^^^^

.. code-block::

lightning disconnect
You are disconnected of the local Lightning App.

----

**********
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
Develop a CLI with client side code
***********************************


In the previous section, we learned how to create a simple command line interface. In more realistic use-cases, an app builder wants to provide more complex functionalities where trusted code is executed on the client side.

Lightning provides a flexible way to create complex CLI without effort.

1. Implement a complex CLI
^^^^^^^^^^^^^^^^^^^^^^^^^^

In the example below, we create a CLI to dynamically run notebooks with the following structures.

.. code-block:: python
Expand All @@ -28,6 +32,10 @@ And in the ``app.py``, add the following code:

.. literalinclude:: app.py


2. Run the App and check the API documentation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

In your first terminal, run the following command and open the ``http://127.0.0.1:7501/view`` in browser.

.. code-block:: python
Expand All @@ -36,19 +44,69 @@ In your first terminal, run the following command and open the ``http://127.0.0.
Your Lightning App is starting. This won't take long.
INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view

And a second terminal, run a first notebook
3. Connect to a running App
^^^^^^^^^^^^^^^^^^^^^^^^^^^

In another terminal, you connect to the running application.
When you connect to an application, Lightning CLI is replaced by the App CLI. To exit the App CLI, you need to run lightning disconnect.

.. code-block::

lightning connect localhost

Storing `run_notebook` under /Users/thomas/.lightning/lightning_connection/commands/run_notebook.py
You can review all the downloaded commands under /Users/thomas/.lightning/lightning_connection/commands folder.
You are connected to the local Lightning App.

And list of the available commands:

.. code-block::

lightning --help

You are connected to the cloud Lightning App: localhost.
Usage: lightning [OPTIONS] COMMAND [ARGS]...

--help Show this message and exit.

Lightning App Commands
run notebook Description


And you can find the arguments of the commands.

.. code-block::

lightning run notebook --help

You are connected to the cloud Lightning App: localhost.
usage: notebook [-h] [--name NAME] [--cloud_compute CLOUD_COMPUTE]

Run Notebook Parser

optional arguments:
-h, --help show this help message and exit
--name NAME
--cloud_compute CLOUD_COMPUTE

4. Execute a command
^^^^^^^^^^^^^^^^^^^^

And then you can trigger the command line exposed by your application.

Run a first notebook with the following command:

.. code-block:: python

lightning run-notebook --name="my_notebook"
lightning run notebook --name="my_notebook"
WARNING: Lightning Command Line Interface is an experimental feature and unannounced changes are likely.
The notebook my_notebook was created.

And run a second notebook.
And run a second notebook by changing its name as follows:

.. code-block:: python

lightning run-notebook --name="my_notebook_2"
lightning run notebook --name="my_notebook_2"
WARNING: Lightning Command Line Interface is an experimental feature and unannounced changes are likely.
The notebook my_notebook_2 was created.

Expand All @@ -57,12 +115,20 @@ Here is a recording of the Lightning App described above.
.. raw:: html

<br />
<video id="background-video" autoplay loop muted controls poster="https://pl-flash-data.s3.amazonaws.com/assets_lightning/commands.png" width="100%">
<source src="https://pl-flash-data.s3.amazonaws.com/assets_lightning/commands.mp4" type="video/mp4" width="100%">
<video id="background-video" autoplay loop muted controls poster="https://pl-flash-data.s3.amazonaws.com/assets_lightning/commands_1.png" width="100%">
<source src="https://pl-flash-data.s3.amazonaws.com/assets_lightning/commands_1.mp4" type="video/mp4" width="100%">
</video>
<br />
<br />

5. Disconnect from the App
^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block::

lightning disconnect
You are disconnected of the local Lightning App.

----

**********
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion examples/app_commands_and_api/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ class CustomConfig(BaseModel):
class CustomCommand(ClientCommand):
def run(self):
parser = ArgumentParser()
parser.add_argument("--name", type=str)
parser.add_argument("--name", type=str, required=True)
args = parser.parse_args()
self.invoke_handler(config=CustomConfig(name=args.name))
Empty file.
94 changes: 94 additions & 0 deletions src/lightning_app/cli/commands/app_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import os
import sys
from typing import Dict, Optional

import requests

from lightning_app.cli.commands.connection import _resolve_command_path
from lightning_app.utilities.cli_helpers import _retrieve_application_url_and_available_commands
from lightning_app.utilities.commands.base import _download_command
from lightning_app.utilities.enum import OpenAPITags


def _run_app_command(app_name: str, app_id: Optional[str]):
"""Execute a function in a running App from its name."""
# 1: Collect the url and comments from the running application
url, api_commands, _ = _retrieve_application_url_and_available_commands(app_id)
if url is None or api_commands is None:
raise Exception("We couldn't find any matching running App.")

if not api_commands:
raise Exception("This application doesn't expose any commands yet.")

full_command = "_".join(sys.argv)

has_found = False
for command in list(api_commands):
if command in full_command:
has_found = True
break

if not has_found:
raise Exception(f"The provided command isn't available in {list(api_commands)}")

# 2: Send the command from the user
metadata = api_commands[command]

# 3: Execute the command
if metadata["tag"] == OpenAPITags.APP_COMMAND:
_handle_command_without_client(command, metadata, url)
else:
_handle_command_with_client(command, metadata, app_name, app_id, url)

if sys.argv[-1] != "--help":
print("Your command execution was successful.")


def _handle_command_without_client(command: str, metadata: Dict, url: str) -> None:
# TODO: Improve what is current supported
supported_params = list(metadata["parameters"])
if "--help" == sys.argv[-1]:
print(f"Usage: lightning {command} [ARGS]...")
print(" ")
print("Options")
for param in supported_params:
print(f" {param}: Add description")
return

provided_params = [param.replace("--", "") for param in sys.argv[2:]]

if any("=" not in param for param in provided_params):
raise Exception("Please, use --x=y syntax when providing the command arguments.")

for param in provided_params:
if param.split("=")[0] not in supported_params:
raise Exception(f"Some arguments need to be provided. The keys are {supported_params}.")

# TODO: Encode the parameters and validate their type.
query_parameters = "&".join(provided_params)
resp = requests.post(url + f"/command/{command}?{query_parameters}")
assert resp.status_code == 200, resp.json()


def _handle_command_with_client(command: str, metadata: Dict, app_name: str, app_id: Optional[str], url: str):
debug_mode = bool(int(os.getenv("DEBUG", "0")))

if app_name == "localhost":
target_file = metadata["cls_path"]
else:
target_file = _resolve_command_path(command) if debug_mode else _resolve_command_path(command)

if debug_mode:
print(target_file)

client_command = _download_command(
command,
metadata["cls_path"],
metadata["cls_name"],
app_id,
debug_mode=debug_mode,
target_file=target_file if debug_mode else _resolve_command_path(command),
)
client_command._setup(command_name=command, app_url=url)
sys.argv = sys.argv[len(command.split("_")) :]
client_command.run()
Loading