From fe6b30ac34a4c97e99e9d5362cec64b5952e8c54 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 26 Jun 2025 20:26:34 -0400 Subject: [PATCH 1/2] allowing callbacks to be exposed as api's by providing a endpoint. --- dash/_callback.py | 16 +++++++++++++++- dash/dash.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/dash/_callback.py b/dash/_callback.py index b0f7bdad5f..4e66351557 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -72,6 +72,7 @@ def is_no_update(obj): GLOBAL_CALLBACK_LIST = [] GLOBAL_CALLBACK_MAP = {} GLOBAL_INLINE_SCRIPTS = [] +GLOBAL_API_PATHS = {} # pylint: disable=too-many-locals @@ -87,6 +88,7 @@ def callback( cache_args_to_ignore: Optional[list] = None, cache_ignore_triggered=True, on_error: Optional[Callable[[Exception], Any]] = None, + api_path: Optional[str] = None, **_kwargs, ) -> Callable[..., Any]: """ @@ -178,6 +180,7 @@ def callback( ) 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) if background: background_spec: Any = { @@ -217,12 +220,14 @@ def callback( callback_list, callback_map, config_prevent_initial_callbacks, + callback_api_paths, *_args, **_kwargs, background=background_spec, manager=manager, running=running, on_error=on_error, + api_path=api_path, ) @@ -585,7 +590,12 @@ def _prepare_response( # pylint: disable=too-many-branches,too-many-statements def register_callback( - callback_list, callback_map, config_prevent_initial_callbacks, *_args, **_kwargs + callback_list, + callback_map, + config_prevent_initial_callbacks, + callback_api_paths, + *_args, + **_kwargs, ): ( output, @@ -638,6 +648,10 @@ def register_callback( # pylint: disable=too-many-locals def wrap_func(func): + if _kwargs.get("api_path"): + api_path = _kwargs.get("api_path") + callback_api_paths[api_path] = func + if background is None: background_key = None else: diff --git a/dash/dash.py b/dash/dash.py index e7b194705e..a54cf94a51 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -568,6 +568,7 @@ def __init__( # pylint: disable=too-many-statements self.callback_map = {} # same deps as a list to catch duplicate outputs, and to send to the front end self._callback_list = [] + self.callback_api_paths = {} # list of inline scripts self._inline_scripts = [] @@ -778,6 +779,42 @@ def _setup_routes(self): # catch-all for front-end routes, used by dcc.Location self._add_url("", self.index) + def setup_apis(self): + # Copy over global callback data structures assigned with `dash.callback` + for k in list(_callback.GLOBAL_API_PATHS): + if k in self.callback_api_paths: + raise DuplicateCallback( + f"The callback `{k}` provided with `dash.callback` was already " + "assigned with `app.callback`." + ) + self.callback_api_paths[k] = _callback.GLOBAL_API_PATHS.pop(k) + + def make_parse_body(func): + def _parse_body(): + if flask.request.is_json: + data = flask.request.get_json() + return flask.jsonify(func(**data)) + return flask.jsonify({}) + + return _parse_body + + def make_parse_body_async(func): + async def _parse_body_async(): + if flask.request.is_json: + data = flask.request.get_json() + result = await func(**data) + return flask.jsonify(result) + return flask.jsonify({}) + + return _parse_body_async + + for path, func in self.callback_api_paths.items(): + print(path) + if asyncio.iscoroutinefunction(func): + self._add_url(path, make_parse_body_async(func), ["POST"]) + else: + self._add_url(path, make_parse_body(func), ["POST"]) + def _setup_plotlyjs(self): # pylint: disable=import-outside-toplevel from plotly.offline import get_plotlyjs_version @@ -1346,6 +1383,7 @@ def callback(self, *_args, **_kwargs) -> Callable[..., Any]: config_prevent_initial_callbacks=self.config.prevent_initial_callbacks, callback_list=self._callback_list, callback_map=self.callback_map, + callback_api_paths=self.callback_api_paths, **_kwargs, ) @@ -1496,6 +1534,7 @@ def dispatch(self): def _setup_server(self): if self._got_first_request["setup_server"]: return + self._got_first_request["setup_server"] = True # Apply _force_eager_loading overrides from modules From d177dcf66ddfde11dbdd6927e1539f4dd703ccd5 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 26 Jun 2025 20:29:02 -0400 Subject: [PATCH 2/2] change `api_path` -> `api_endpoint` --- dash/_callback.py | 10 +++++----- dash/dash.py | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index 4e66351557..f2343d7f94 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -88,7 +88,7 @@ def callback( cache_args_to_ignore: Optional[list] = None, cache_ignore_triggered=True, on_error: Optional[Callable[[Exception], Any]] = None, - api_path: Optional[str] = None, + api_endpoint: Optional[str] = None, **_kwargs, ) -> Callable[..., Any]: """ @@ -227,7 +227,7 @@ def callback( manager=manager, running=running, on_error=on_error, - api_path=api_path, + api_endpoint=api_endpoint, ) @@ -648,9 +648,9 @@ def register_callback( # pylint: disable=too-many-locals def wrap_func(func): - if _kwargs.get("api_path"): - api_path = _kwargs.get("api_path") - callback_api_paths[api_path] = func + if _kwargs.get("api_endpoint"): + api_endpoint = _kwargs.get("api_endpoint") + callback_api_paths[api_endpoint] = func if background is None: background_key = None diff --git a/dash/dash.py b/dash/dash.py index a54cf94a51..89e75646cd 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -809,7 +809,6 @@ async def _parse_body_async(): return _parse_body_async for path, func in self.callback_api_paths.items(): - print(path) if asyncio.iscoroutinefunction(func): self._add_url(path, make_parse_body_async(func), ["POST"]) else: