Skip to content

Conversation

BSd3v
Copy link
Contributor

@BSd3v BSd3v commented Jun 27, 2025

This PR strives to allow callbacks to be exposed by the underlying server as an API by simply passing the callback a api_endpoint. The api will accept a body with the functional arguments provided as kwargs.

@BSd3v
Copy link
Contributor Author

BSd3v commented Jun 27, 2025

Here is an example of how to spin up the app and how to perform the api calls:

app.py

import time
import dash
from dash import html, dcc, Input, Output, ctx, callback
from flask import jsonify
import asyncio
import json

app = dash.Dash(__name__, use_async=True)
app.layout = html.Div([
    html.Button("Slow Callback", id="slow-btn"),
    html.Div(id="slow-output"),
    html.Button("Fast Callback", id="fast-btn"),
    html.Div(id="fast-output"),
])

async def get_async_data(n_clicks):
    # Simulate an async data fetch
    await asyncio.sleep(1)
    return f"Data fetched - {n_clicks}"

def get_data(n_clicks):
    # Simulate an async data fetch
    time.sleep(1)
    return f"Data fetched - {n_clicks}"

@app.callback(
    Output("slow-output", "children"),
    Input("slow-btn", "n_clicks"),
    prevent_initial_call=True,
    api_endpoint='/api/slow_callback',  # Example API path for the slow callback
)
def slow_callback(n_clicks):
    start = time.time()
    data = {}
    for i in range(5):
        data[f'step_{i}'] = get_data(n_clicks)
    ret = f"{json.dumps(data)} Time taken: {time.time() - start:.2f} seconds"
    if ctx:
        return ret
    return jsonify(ret)

@app.callback(
    Output("fast-output", "children"),
    Input("fast-btn", "n_clicks"),
    prevent_initial_call=True,
    api_endpoint='/api/fast_callback',  # Example API path for the fast callback
)
async def fast_callback(n_clicks):
    start = time.time()
    coros = [get_async_data(n_clicks) for _ in range(5)]
    results = await asyncio.gather(*coros)
    data = {f'step_{i}_async': result for i, result in enumerate(results)}
    ret = f"{json.dumps(data)} Time taken: {time.time() - start:.2f} seconds"
    if ctx:
        return ret
    return jsonify(ret)

app.setup_apis()

if __name__ == "__main__":
    app.run(debug=True)

api calls

import requests

resp = requests.post('http://127.0.0.1:8050/api/slow_callback',
                     json={"n_clicks": 1},
                    headers={"Content-Type": "application/json"})

print(resp.json())

resp = requests.post('http://127.0.0.1:8050/api/fast_callback',
                     json={"n_clicks": 1},
                    headers={"Content-Type": "application/json"})

print(resp.json())

@gvwilson gvwilson added feature something new P2 considered for next cycle community community contribution labels Jul 3, 2025
Copy link
Contributor

@T4rk1n T4rk1n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This look like a nice feature, just need to add a docstring to the new argument and a test.

@BSd3v
Copy link
Contributor Author

BSd3v commented Sep 1, 2025

@T4rk1n

Is there another way to setup the apis rather than calling app.setup_apis()

Copy link
Contributor

@T4rk1n T4rk1n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's only keep the GLOBAL_API_PATHS and use that for the registering.

)
callback_map = _kwargs.pop("callback_map", GLOBAL_CALLBACK_MAP)
callback_list = _kwargs.pop("callback_list", GLOBAL_CALLBACK_LIST)
callback_api_paths = _kwargs.pop("callback_api_paths", GLOBAL_API_PATHS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's only have the global one, having two variables for the same functionality creates issue like #3419 and add complexity.

def _setup_server(self):
if self._got_first_request["setup_server"]:
return

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's missing a call to setup_apis or is it meant to be called by the user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routes must be registered by the dev before the server is started, therefore it cant go here to automatically setup.

@T4rk1n T4rk1n merged commit bac3f36 into plotly:dev Sep 8, 2025
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
community community contribution feature something new P2 considered for next cycle
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants