emilhe / dash-extensions

The dash-extensions package is a collection of utility functions, syntax extensions, and Dash components that aim to improve the Dash development experience
https://www.dash-extensions.com/
MIT License
417 stars 59 forks source link

Support for dash.callback with ServersideOutput #205

Closed jonnyrobbie closed 2 years ago

jonnyrobbie commented 2 years ago

I noticed that somewhere in the documentation, it is mentioned that long_callback is not supported with serverside output. But long_callback has been deprecated by plotly in favour of background callback, which is part of built in @app.callback decorator. Is background callback thus supported as well thanks to that?

Also, there's a new syntax where you don't have to reference created app object with callback, but can rererence the dash module itself like @dash.callback(...). Is that also supported with ServersideOutput and ServersiteOutputTransform?

emilhe commented 2 years ago

I haven't had the time to try out the long callback with the new syntax, so I am not sure. But I don't see any immediate blockers.

Yes, you should be able to use the syntax without the app reference, i.e. something like,

from dash_extensions.enrich import callback

@callback(...)
jonnyrobbie commented 2 years ago

Ok, I may need to investigate a bit more (create a MWE), but it seems like it may be incompatible with dash.callback(progress=...) argument. When you pass in that argument, the callback function expects another argument at the front of the callback function signature and that may be throwing enrich off:

Traceback (most recent call last):
  File "/usr/local/anaconda3/envs/soe/lib/python3.7/site-packages/dash/long_callback/managers/diskcache_manager.py", line 163, in job_fn
    user_callback_output = fn(*maybe_progress, *user_callback_args)
  File "/usr/local/anaconda3/envs/soe/lib/python3.7/site-packages/dash_extensions/enrich.py", line 1172, in decorated_function
    filtered_args = [arg for i, arg in enumerate(args) if not is_trigger[i]]
  File "/usr/local/anaconda3/envs/soe/lib/python3.7/site-packages/dash_extensions/enrich.py", line 1172, in <listcomp>
    filtered_args = [arg for i, arg in enumerate(args) if not is_trigger[i]]
IndexError: list index out of range
jonnyrobbie commented 2 years ago

Ok, here's a MWE (slightly altered mwe from your repo):

import time
import plotly.express as px
import diskcache
from dash_extensions.enrich import DashProxy, Output, Input, State, ServersideOutput, html, dcc, \
    ServersideOutputTransform
from dash import DiskcacheManager

cache = diskcache.Cache(".")
background_callback_manager = DiskcacheManager(cache)

app = DashProxy(transforms=[ServersideOutputTransform()])
app.layout = html.Div(
    [
        html.Button("Query data", id="btn"),
        html.Div("", id="progress"),
        dcc.Dropdown(id="dd"),
        dcc.Graph(id="graph"),
        dcc.Loading(dcc.Store(id="store"), fullscreen=True, type="dot"),
    ]
)

@app.callback(ServersideOutput("store", "data"), Input("btn", "n_clicks"), prevent_initial_call=True, background=True, progress=[Output("progress", "children")], manager=background_callback_manager)
def query_data(set_progress, n_clicks):
    set_progress(("0%"))
    time.sleep(1)
    set_progress(("50%"))
    time.sleep(1)  # emulate slow database operation
    set_progress(("100%"))
    return px.data.gapminder()  # no JSON serialization here

@app.callback(Output("dd", "options"),  Output("dd", "value"), Input("store", "data"), prevent_initial_call=True)
def update_dd(df):
    options = [{"label": column, "value": column} for column in df["year"]]   # no JSON de-serialization here
    return options, options[0]['value']

@app.callback(Output("graph", "figure"), [Input("dd", "value"), State("store", "data")], prevent_initial_call=True)
def update_graph(value, df):
    df = df.query("year == {}".format(value))  # no JSON de-serialization here
    return px.sunburst(df, path=["continent", "country"], values="pop", color="lifeExp", hover_data=["iso_alpha"])

if __name__ == "__main__":
    app.run_server()
emilhe commented 2 years ago

@jonnyrobbie Thanks for the example, and the PR. That helped my understand the root cause of the issue. I have just pushed a 0.1.6rc4 pre release to pypi, which includes a possible fix. Could you check if it works as intended for your use case?

jonnyrobbie commented 2 years ago

Yep, 0.1.6rc4 seems to be working fine, thanks.