-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Api exposure callbacks #3347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Api exposure callbacks #3347
Changes from 8 commits
fe6b30a
d177dcf
90cf329
c2dd48b
7191e83
9bb570b
3b8512c
bd5afc0
ed91bd6
7835b33
52d5ffd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -569,6 +569,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 = [] | ||
|
|
@@ -783,6 +784,41 @@ def _setup_routes(self): | |
| # catch-all for front-end routes, used by dcc.Location | ||
| self._add_url("<path:path>", 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(): | ||
| 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 | ||
|
|
@@ -1364,6 +1400,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, | ||
| ) | ||
|
|
||
|
|
@@ -1516,6 +1553,7 @@ def dispatch(self): | |
| def _setup_server(self): | ||
| if self._got_first_request["setup_server"]: | ||
| return | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| self._got_first_request["setup_server"] = True | ||
|
|
||
| # Apply _force_eager_loading overrides from modules | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| from dash import ( | ||
| Dash, | ||
| Input, | ||
| Output, | ||
| html, | ||
| ctx, | ||
| ) | ||
| import requests | ||
| import json | ||
| from flask import jsonify | ||
|
|
||
| test_string = ( | ||
| '{"step_0": "Data fetched - 1", "step_1": "Data fetched - 1", "step_2": "Data fetched - 1", ' | ||
| '"step_3": "Data fetched - 1", "step_4": "Data fetched - 1"}' | ||
| ) | ||
|
|
||
|
|
||
| def test_apib001_api_callback(dash_duo): | ||
|
|
||
| app = Dash(__name__) | ||
| app.layout = html.Div( | ||
| [ | ||
| html.Button("Slow Callback", id="slow-btn"), | ||
| html.Div(id="slow-output"), | ||
| ] | ||
| ) | ||
|
|
||
| def get_data(n_clicks): | ||
| # Simulate an async data fetch | ||
| 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): | ||
| data = {} | ||
| for i in range(5): | ||
| data[f"step_{i}"] = get_data(n_clicks) | ||
| ret = f"{json.dumps(data)}" | ||
| if ctx: | ||
| return ret | ||
| return jsonify(ret) | ||
|
|
||
| app.setup_apis() | ||
|
|
||
| dash_duo.start_server(app) | ||
|
|
||
| dash_duo.wait_for_element("#slow-btn").click() | ||
| dash_duo.wait_for_text_to_equal("#slow-output", test_string) | ||
| r = requests.post( | ||
| dash_duo.server_url + "/api/slow_callback", | ||
| json={"n_clicks": 1}, | ||
| headers={"Content-Type": "application/json"}, | ||
| ) | ||
| assert r.status_code == 200 | ||
| assert r.json() == test_string |
There was a problem hiding this comment.
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.