Questions related to PanelFrontend contribution #13523
Replies: 6 comments
-
Here is the app running panel-lightning-app-poc.mp4 |
Beta Was this translation helpful? Give feedback.
-
A workaround for sharing global state for display in the Panel app would be to save it. For example as a pickle or in a cache like diskcache. |
Beta Was this translation helpful? Give feedback.
-
Lets move the discussion to #13531. |
Beta Was this translation helpful? Give feedback.
-
Hey @MarcSkovMadsen
I started to work on the following example, but Panel doesn't seem to render. I guess I am doing something wrong. import datetime
import sys
import time
import lightning as L
import lightning_app as lapp
import panel as pn
from lightning_app.core.flow import LightningFlow
from lightning_app.utilities.app_helpers import pretty_state
from lightning_app.utilities.imports import requires
from lightning.app.frontend.frontend import Frontend
from typing import Callable, Optional
from lightning.app.utilities.state import AppState
import subprocess
import os
import inspect
class PanelFrontend(Frontend):
@requires("panel")
def __init__(self, render_fn: Callable) -> None:
super().__init__()
if inspect.ismethod(render_fn):
raise TypeError(
"The `state` doesn't support `render_fn` being a method. Please, use a pure function."
)
self.render_fn = render_fn
self._process: Optional[subprocess.Popen] = None
def start_server(self, host: str, port: int) -> None:
env = os.environ.copy()
env["LIGHTNING_FLOW_NAME"] = self.flow.name
env["LIGHTNING_RENDER_FUNCTION"] = self.render_fn.__name__
env["LIGHTNING_RENDER_MODULE_FILE"] = inspect.getmodule(self.render_fn).__file__
env["HOST"] = str(host)
env["PORT"] = str(port)
self._process = subprocess.Popen(
[
sys.executable,
"-m",
"panel",
"serve",
os.path.join(os.path.dirname(lapp.frontend.__file__), "root.lit_panel.py"),
"--address",
str(host),
"--port",
str(port),
"--autoreload",
],
env=env,
)
def stop_server(self) -> None:
if self._process is None:
raise RuntimeError("Server is not running. Call `StreamlitFrontend.start_server()` first.")
self._process.kill()
class CounterWork(L.LightningWork):
def __init__(self, parallel=True, **kwargs):
super().__init__(parallel=parallel, **kwargs)
self.counter = 0
def run(self):
while self.counter >= 0:
time.sleep(5)
self.counter += 1
class PanelApp(LightningFlow):
def __init__(self):
super().__init__()
self.submits = 0
def configure_layout(self):
return PanelFrontend(render_fn=render_fn)
def render_fn(state: AppState):
submit = pn.widgets.Button(name="Submit")
@pn.depends(submit, watch=True)
def update(_):
state.submits += 1
@pn.depends(submit)
def submits(_):
return state.submits
@pn.depends(submit)
def local_state(_):
return state._state
@pn.depends(submit)
def global_state(_):
"How do I get this?"
return {"works": "How do I get the global app state?"}
column = pn.Column(
submit,
"Submits",
submits,
"Local State",
pn.panel(local_state, height=300),
"Global State",
pn.panel(global_state, height=300),
)
pn.serve(
{"/": column},
websocket_origin="*",
host=os.getenv("HOST", None),
port=int(os.getenv("PORT", None)),
show=False,
)
class Flow(L.LightningFlow):
def __init__(self):
super().__init__()
self.w = CounterWork()
self.lit_panel = PanelApp()
self._last_update = datetime.datetime.now()
def run(self):
self.w.run()
self.lit_panel.run()
now = datetime.datetime.now()
if (now - self._last_update).seconds>=5:
print(f"Global state: {pretty_state(self.state)} \n")
self._last_update=now
def configure_layout(self):
tab1 = {"name": "Home", "content": self.lit_panel}
return tab1
app = L.LightningApp(Flow()) |
Beta Was this translation helpful? Give feedback.
-
"""This file gets run by streamlit, which we launch within Lightning.
From here, we will call the render function that the user provided in ``configure_layout``.
"""
import os
import pydoc
from typing import Callable, Union
from lightning_app.core.flow import LightningFlow
from lightning_app.utilities.app_helpers import StreamLitStatePlugin
from lightning_app.utilities.state import AppState
app_state = AppState()
def _get_render_fn_from_environment() -> Callable:
render_fn_name = os.environ["LIGHTNING_RENDER_FUNCTION"]
render_fn_module_file = os.environ["LIGHTNING_RENDER_MODULE_FILE"]
module = pydoc.importfile(render_fn_module_file)
return getattr(module, render_fn_name)
def _app_state_to_flow_scope(state: AppState, flow: Union[str, LightningFlow]) -> AppState:
"""Returns a new AppState with the scope reduced to the given flow, as if the given flow as the root."""
flow_name = flow.name if isinstance(flow, LightningFlow) else flow
flow_name_parts = flow_name.split(".")[1:] # exclude root
flow_state = state
for part in flow_name_parts:
flow_state = getattr(flow_state, part)
return flow_state
def main():
# Fetch the information of which flow attaches to this streamlit instance
flow_state = _app_state_to_flow_scope(app_state, flow=os.environ["LIGHTNING_FLOW_NAME"])
# Call the provided render function.
# Pass it the state, scoped to the current flow.
render_fn = _get_render_fn_from_environment()
render_fn(flow_state)
if __name__ == "__main__":
main() |
Beta Was this translation helpful? Give feedback.
-
This one demonstrates the principle @tchaton
lightning-frontend.mp4
import datetime
import sys
import time
import lightning as L
import panel as pn
from lightning_app.core.flow import LightningFlow
from lightning_app.utilities.app_helpers import pretty_state
from lightning_app.utilities.imports import requires
from lightning.app.frontend.frontend import Frontend
from typing import Callable, Optional
from lightning.app.utilities.state import AppState
import subprocess
import os
class PanelFrontend(Frontend):
@requires("panel")
def __init__(self, render_script="root.lit_panel.py") -> None:
super().__init__()
self._render_script = render_script
self._process: Optional[subprocess.Popen] = None
def start_server(self, host: str, port: int) -> None:
env = os.environ.copy()
env["LIGHTNING_FLOW_NAME"] = self.flow.name
env["HOST"] = str(host)
env["PORT"] = str(port)
self._process = subprocess.Popen(
[
sys.executable,
"-m",
"panel",
"serve",
self._render_script,
"--address",
str(host),
"--port",
str(port),
"--autoreload",
],
env=env,
)
def stop_server(self) -> None:
if self._process is None:
raise RuntimeError("Server is not running. Call `StreamlitFrontend.start_server()` first.")
self._process.kill()
class CounterWork(L.LightningWork):
def __init__(self, parallel=True, **kwargs):
super().__init__(parallel=parallel, **kwargs)
self.counter = 0
def run(self):
while self.counter >= 0:
time.sleep(1)
self.counter += 1
class PanelApp(LightningFlow):
def __init__(self):
super().__init__()
self.submits = 0
def configure_layout(self):
return PanelFrontend()
class Flow(L.LightningFlow):
def __init__(self):
super().__init__()
self.w = CounterWork()
self.lit_panel = PanelApp()
self._last_update = datetime.datetime.now()
def run(self):
self.w.run()
self.lit_panel.run()
now = datetime.datetime.now()
if (now - self._last_update).seconds>=5:
print(f"Global state: {pretty_state(self.state)} \n")
self._last_update=now
def configure_layout(self):
tab1 = {"name": "Home", "content": self.lit_panel}
return tab1
flow = Flow()
app = L.LightningApp(flow)
import panel as pn
from lightning.app.utilities.state import AppState
import datetime as dt
from lightning_app.utilities.app_helpers import pretty_state
if "initial_request" not in pn.state.cache:
pn.state.cache["initial_request"]=False
pn.pane.Markdown("Fast intial response").servable()
else:
pn.extension(sizing_mode="stretch_width")
app_state = AppState()
last_update = pn.pane.Markdown().servable()
json_pane = pn.pane.JSON(theme="light", depth=4).servable()
def update():
last_update.object = str(dt.datetime.now().isoformat())
app_state._state = None
app_state._request_state()
state = pretty_state(app_state._state)
json_pane.object = state
update()
pn.state.add_periodic_callback(update, period=1000) lightning run app app.py |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
UPDATE: DISCUSSION MOVED TO #13531.
Context
I would like to contribute an intermediate example of a Panel app. I would like to contribute a PanelFrontend similar to the StreamlitFrontend or GradioFrontend. See #13335 for more background info.
POC
I've created the below POC. It consists of one flow that logs its state (i.e. the global app state) and two components. One
CounterWork
LightningWork
that runs an infinite loop and increments the attributecounter
every 5 seconds. OnePanelApp
LightningWork
that runs a Panel server with a submit button that updates thesubmit
attribute when clickedMy questions
PanelApp
access the global app state? I would like to display it in the app. (I can see the StreamlitFrontend can access anAppState
. But to me it seems like a cumbersome workaround due to Streamlit specifics.)counter
value on theCounterWork
to thePanelApp
? Should I add something likeself.lit_panel.run(counter=self.w.counter)
to the Flows run method? (I tried but it seems to slow down the app significantly).Frontend
would not be a good idea as I see it as it does not hold state. I.e. thesubmits
attribute would not be a part of the global app state. Furthermore as I understand it, aFrontend
cannot run on GPUs.pn.serve
I could also do something more like the StreamlitFrontend. I.e. usesubprocess.Popen
to runpanel serve some_script.py
and use theAppState
with apanel_plugin
. Would that be a better approach?Beta Was this translation helpful? Give feedback.
All reactions