Dash all-in-one components pass instance configuration to callbacks

Callbacks included in all-in-one components are registered when the component’s class is loaded, usually this is done when starting the server. One problem of this approach is that you can’t pass instance variables to your callbacks. This is relevant if you need the callbacks to use configuration details that were set by the user when adding the all-in-one component to there dashboard:

my_aio_comp = MyAIO(aio_id="cool_component", custom_config=...)

There are some workaround, e.g. the dash pages show how to use a Redis database to store a dataframe. Also, when the configuration details are static like a string, you can add a dcc.Store to the AIO component, store the configuration details in that store and add it to the callbacks.

My issue here is that I have not been able to find such a workaround for Python object, e.g. a function handle.

I have a component that is a Dash interface for some API. I want to allow the user to pass a custom response parser to the AIO component that I can call before storing the response of the API in a dcc.Store and hand it back to the user. The raw output of the API can become to large to store in the store, however there is no general way to parse the API response as that will depend on what input the user provides. Does anyone have suggestions how to deal with this?

I have seen the possibility to put the callbacks inside a method and call that method in the __init__() method. Could this be a solution to pass variables to the AIO component by the user? If so, what happens when the user initialize multiple copies of the AIO component, will the same callbacks be registered multiple times?

Hello @Tobs,

Could you please elaborate a little more about how you are building the AIO?

Callbacks for the AIO component are going to be registered directly as an exact match, so how they are configured due to variables that you pass will change how it interacts. I’m not exactly sure how a user would pass a new callable to the function from the front end, as this would be dependent upon how you configured it.

However, if these values are always impacting the AIO, then they should be included into the AIO, imo.

Without seeing exactly how you are building it and what config adjustments you are trying to make, it is really hard to help too much more.

@jinnyzor
The structure could look like this:

class MyAIO(html.Div):
    class Ids:
        ...

    ids = Ids

    def __init__(self, aio_id: str, custom_parser: Callable):
        super().__init__(
            id=self.ids.parent_div(aio_id),
            children=[
                dcc.Store(id=self.ids.response(aio_id)),
                dcc.Store(id=self.ids.api_input(aio_id)),
            ],
        )

    @staticmethod
    @callback(
        Output(ids.response(MATCH), "data"),
        Input(ids.api_input(MATCH), "data"),
        prevent_initial_call=True,
    )
    def calculate(calculation_input):
        response = contact_api(calculation_input)
        return custom_parser(response)

and then you could create the component with

def my_parser(calculation_input: dict) -> float:
    return calculation_input["response"]["interesting_parameter"]


my_aio_comp = MyAIO(aio_id="cool_component", custom_parser=my_parser)

The idea is that the custom parser becomes available in the callback.

Note: The setup above doesn’t work, this is just to give a general idea

The tricky thing here is that the callback structure is still the same for all instances of this AIO component, so in general the design should still follow the AIO design rules (with callbacks as static methods and the MATCH keyword), however in it’s function body it needs access to an instance variable. I am looking for a solution for this conflicting/contradictory situation.

Have you tried bringing the callbacks into the __init__ and testing it?

If not, you could always just use a map to which functions you want the user to have and store the map key in the dcc.Store associated with the AIO.

I haven’t tried it yet. I have seen a more or less equivalent suggestion to have the callbacks in a method in the class, so something like:

class MyAIO():
    def __init__():
        ...
        self.callbacks()
        super.__init__()

    def callbacks(self):
        @callback()
        ...

I didn’t have time yet to experiment with it, but I am wondering if this kind of structure will mess up the callbacks, i.e. duplicating callbacks when adding multiple copies of the AIO component to the dashboard. Perhaps the callbacks need to be specific and not use the MATCH keyword. I guess I was looking for some inspiration on the forum.

I’m unsure if it will work or not. You can always try.

If it doesnt work for dynamically added components, then you could try the mapping of the functions. :slight_smile:

I still dont understand why you have an issue with dynamic settings, shouldnt the function be standard and then the info used to run the function be passed as an object or string?

Ah wait, there is maybe an important part that I forgot, oopsie on my side.

My context is that I maintain a library that contains the AIO component. The idea is that I write the complex behavior and encapsulate that in an AIO component, and that the users of my library (novice Python programmers that can build basic dashboards) access the functionality by using the AIO component.

In this design, the AIO component cannot have any awareness of specific dashboards, and this is why I am looking for a way to pass the Callable into the callbacks like this.

If the AIO is setup to operate the same way in each instance, ping and API and then return the queried info, I think you should allow for saving of that key in a dcc.Store that then gets utilized to run any number of functions that a developer will setup in the app.

Setting up a function map can be done as such:

funcs = {'funcName': funcName}

Then to call the function it would be:

funcs.get('funcName')(**kwargs)
1 Like

Well, the problem with the function map is again how to make that accessible inside the callbacks. But after thinking about it more, I did get the inspiration I was looking for, by making the function map a class variable it will indeed work. And because the function map is a dictionary, it can hold all the Callables without collision (when multiple AIO instances are defined).

Here is the MWE that I came up with

from dash import Output, State, Input, html, dcc, MATCH, callback
from typing import Callable


def contact_api(calculation_input: dict) -> dict:
    return {
        "input": calculation_input["input"],
        "output": calculation_input["input"]["x"] * calculation_input["input"]["y"],
    }


class MyAIO(html.Div):
    class Ids:
        @staticmethod
        def parent_div(aio_id: str) -> dict:
            return {
                "component": "MyAIO",
                "subcomponent": "parent_div",
                "aio_id": aio_id,
            }

        @staticmethod
        def response(aio_id: str) -> dict:
            return {
                "component": "MyAIO",
                "subcomponent": "store_response",
                "aio_id": aio_id,
            }

        @staticmethod
        def api_input(aio_id: str) -> dict:
            return {
                "component": "MyAIO",
                "subcomponent": "store_api_input",
                "aio_id": aio_id,
            }

        @staticmethod
        def parser(aio_id: str) -> dict:
            return {
                "component": "MyAIO",
                "subcomponent": "store_parser",
                "aio_id": aio_id,
            }

    ids = Ids
    function_map = {}

    def __init__(self, aio_id: str, custom_parser: Callable):
        MyAIO.function_map[custom_parser.__name__] = custom_parser
        super().__init__(
            id=self.ids.parent_div(aio_id),
            children=[
                dcc.Store(id=self.ids.response(aio_id)),
                dcc.Store(id=self.ids.api_input(aio_id)),
                dcc.Store(
                    id=self.ids.parser(aio_id),
                    data=custom_parser.__name__,
                ),
            ],
        )

    @staticmethod
    @callback(
        Output(ids.response(MATCH), "data"),
        Input(ids.api_input(MATCH), "data"),
        State(ids.parser(MATCH), "data"),
        prevent_initial_call=True,
    )
    def calculate(
        calculation_input,
        custom_parser: str,
    ):
        response = contact_api(calculation_input)
        return MyAIO.function_map[custom_parser](response)


if __name__ == "__main__":

    def my_parser(response: dict):
        return response["output"]

    component = MyAIO("aio_id", custom_parser=my_parser)
    # Simulate the callback execution
    print(
        MyAIO.calculate(
            {"input": {"x": 5, "y": 6}},
            "my_parser",
        )
    )
    # >> 30

Thanks for the brainstorm session, @jinnyzor

2 Likes