Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
c457ab7
initial push
BSd3v Sep 9, 2025
4ebc657
work to modularize the dash eco-system and decouple from Flask
BSd3v Sep 9, 2025
9dff791
fix favicon
BSd3v Sep 9, 2025
c319b18
removing changelog entry
BSd3v Sep 9, 2025
7de2a41
fixing issue with debug true for FastAPI
BSd3v Sep 9, 2025
2cd769e
fixing `catchall` for path routes
BSd3v Sep 9, 2025
686f32f
fixing pages for use with `fastapi`
BSd3v Sep 9, 2025
660e257
fixing issue with flask pages
BSd3v Sep 10, 2025
581f8a5
Merge pull request #3 from BSd3v/modularize-dash-server
BSd3v Sep 10, 2025
9eb9dd0
Merge branch 'dev' into bring-your-own-server
BSd3v Sep 10, 2025
0fa5c99
fixing for lint
BSd3v Sep 11, 2025
1088331
fixing issue with failing test due to `endpoint` name
BSd3v Sep 11, 2025
4920e33
fixing `run` command to trigger `devtools` properly
BSd3v Sep 11, 2025
9ffba5a
fixing issue with lint and debug ui
BSd3v Sep 11, 2025
908aacd
fixing issue with `_app` when using dispatch, need to keep in context
BSd3v Sep 11, 2025
9491c7f
fixing issue with catchall
BSd3v Sep 11, 2025
39ad7bd
fixing issue with args and cancelling callbacks
BSd3v Sep 11, 2025
7bf69a7
fixing issues with pages metadata and flaky tests
BSd3v Sep 11, 2025
10681dc
fixing issues with relativate paths
BSd3v Sep 11, 2025
4944d6d
∙ - initial quart factory
Sep 11, 2025
3b0f47e
Quart factory ready
Sep 12, 2025
1112f77
fixing for lint
BSd3v Sep 12, 2025
8c52bbb
fixing issue with apps overwriting other paths
BSd3v Sep 12, 2025
aabeeb7
removing print
BSd3v Sep 12, 2025
5659cd7
cleanup
Sep 12, 2025
b05e376
reverting `render_index` -> `index` and making catch for outside of a…
BSd3v Sep 12, 2025
ed0dc3b
∙ - initial quart factory
Sep 11, 2025
141527c
Quart factory ready
Sep 12, 2025
3e38d41
fixing `prune_errors` test
BSd3v Sep 12, 2025
381fb0c
adjustments for flask api_endpoint declared in callback defs
BSd3v Sep 12, 2025
a27927a
updated QuartRequestAdapter & QuartFactory to latest changes
Sep 12, 2025
fbc3935
checkout
Sep 12, 2025
1824e11
Removed redundant Response return
Sep 12, 2025
b14f6d2
fix for fastapi `api_endpoint` registering
BSd3v Sep 12, 2025
fbefbc9
Merge pull request #4 from chgiesse/quart-factory
BSd3v Sep 12, 2025
5ef796b
shifting from `server_factory` to `backend`
BSd3v Sep 12, 2025
a4ca566
adding missing files
BSd3v Sep 12, 2025
708773f
fixing issue with server not declared
BSd3v Sep 12, 2025
b7bceba
Update dash/dash.py
BSd3v Sep 12, 2025
9873079
Update dash/dash.py
BSd3v Sep 12, 2025
9f4d291
Update dash/backend/quart.py
BSd3v Sep 12, 2025
da86e86
Update dash/dash.py
BSd3v Sep 12, 2025
4c60740
Update dash/dash.py
BSd3v Sep 12, 2025
84cb5e5
update for caller_name
BSd3v Sep 12, 2025
29cf823
Update dash/dash.py
BSd3v Sep 12, 2025
5d0f4dc
Update dash/dash.py
BSd3v Sep 12, 2025
86f4528
adjustments for matching types
BSd3v Sep 12, 2025
2a88385
Update dash/backend/registry.py
BSd3v Sep 12, 2025
df76ed6
Merge branch 'factory-backend' of github.com:BSd3v/dash into factory-…
BSd3v Sep 12, 2025
bc51c0d
Update dash/backend/registry.py
BSd3v Sep 12, 2025
1b4d0d3
fixing another type check
BSd3v Sep 12, 2025
f867f98
fixing for lint
BSd3v Sep 13, 2025
0ed81ce
fixing failing test
BSd3v Sep 13, 2025
6bd342a
fixing issue with fastapi and component suites
BSd3v Sep 13, 2025
b1c9953
adjustments to fix issues with caller_name and init the app a couple …
BSd3v Sep 13, 2025
bd40b56
adjustments for failing tests
BSd3v Sep 13, 2025
4e50430
format dash
BSd3v Sep 13, 2025
0d32e65
removing `FlaskDashServer` from import and using `get_backend('flask'…
BSd3v Sep 14, 2025
1b3f61e
reverting change to callable(title) process
BSd3v Sep 14, 2025
c6805b5
fixing for lint
BSd3v Sep 14, 2025
8c78089
adding custom error handling per backend, tests and adjustments to th…
BSd3v Sep 16, 2025
5211f6f
adjusments for formatting
BSd3v Sep 16, 2025
6a34208
adjustment to retest backend
BSd3v Sep 16, 2025
1a2b531
adding missing reqs association
BSd3v Sep 16, 2025
465e45e
fixing minor linting issues
BSd3v Sep 16, 2025
c43a583
Add global Request Adapter (#6)
chgiesse Sep 17, 2025
c4795ed
fixes for failing tests
BSd3v Sep 17, 2025
567d0f8
fixing formatting
BSd3v Sep 17, 2025
a855c6d
fixing issues
BSd3v Sep 17, 2025
79afb0b
fixing async validation
BSd3v Sep 17, 2025
77e22a3
adjustments for request_adapter
BSd3v Sep 17, 2025
f7331d3
adding test for custom dash server
BSd3v Sep 17, 2025
8b58cf4
fixing issue with `request_adapter`
BSd3v Sep 17, 2025
b7d4af2
adjusting error handling for fastapi
BSd3v Sep 17, 2025
4cf4686
adjustments for handling issues with `debug` for `fastapi`
BSd3v Sep 17, 2025
dfe0ac7
fixing for lint
BSd3v Sep 17, 2025
cd02cc5
adjustment for delayed config
BSd3v Sep 18, 2025
16b3c9e
fix typing error
BSd3v Sep 18, 2025
493d150
fixes for pages
BSd3v Sep 22, 2025
26f01b2
Merge branch 'dev' into bring-your-own-server
BSd3v Oct 29, 2025
a10f86b
Decouple flask (#7)
chgiesse Nov 12, 2025
02e0be5
Merge branch 'dev' into bring-your-own-server
BSd3v Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fixing for lint
  • Loading branch information
BSd3v committed Sep 13, 2025
commit f867f98fd791ec790d755f75eb0a6e5b8a986117
4 changes: 3 additions & 1 deletion dash/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# python
import contextvars
from .registry import *
from .registry import get_backend # pylint: disable=unused-import

__all__ = ["set_request_adapter", "get_request_adapter", "get_backend"]

_request_adapter_var = contextvars.ContextVar("request_adapter")

Expand Down
16 changes: 12 additions & 4 deletions dash/backend/base_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ def __call__(self, server, *args, **kwargs) -> Any:
return server(*args, **kwargs)

@abstractmethod
def create_app(self, name: str = "__main__", config=None) -> Any: # pragma: no cover - interface
def create_app(
self, name: str = "__main__", config=None
) -> Any: # pragma: no cover - interface
pass

@abstractmethod
Expand All @@ -22,7 +24,9 @@ def register_error_handlers(self, app) -> None: # pragma: no cover - interface
pass

@abstractmethod
def add_url_rule(self, app, rule: str, view_func, endpoint=None, methods=None) -> None: # pragma: no cover - interface
def add_url_rule(
self, app, rule: str, view_func, endpoint=None, methods=None
) -> None: # pragma: no cover - interface
pass

@abstractmethod
Expand All @@ -34,11 +38,15 @@ def after_request(self, app, func) -> None: # pragma: no cover - interface
pass

@abstractmethod
def run(self, app, host: str, port: int, debug: bool, **kwargs) -> None: # pragma: no cover - interface
def run(
self, app, host: str, port: int, debug: bool, **kwargs
) -> None: # pragma: no cover - interface
pass

@abstractmethod
def make_response(self, data, mimetype=None, content_type=None) -> Any: # pragma: no cover - interface
def make_response(
self, data, mimetype=None, content_type=None
) -> Any: # pragma: no cover - interface
pass

@abstractmethod
Expand Down
10 changes: 7 additions & 3 deletions dash/backend/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ async def catchall(request: Request):
# pylint: disable=protected-access
dash_app._add_url("{path:path}", catchall, methods=["GET"])

def add_url_rule(self, app, rule, view_func, endpoint=None, methods=None, include_in_schema=False):
def add_url_rule(
self, app, rule, view_func, endpoint=None, methods=None, include_in_schema=False
):
if rule == "":
rule = "/"
if isinstance(view_func, str):
Expand Down Expand Up @@ -307,8 +309,11 @@ def register_callback_api_routes(self, app, callback_api_paths):
sig = inspect.signature(handler)
param_names = list(sig.parameters.keys())
fields = {name: (Optional[Any], None) for name in param_names}
Model = create_model(f"Payload_{endpoint}", **fields)
Model = create_model(
f"Payload_{endpoint}", **fields
) # pylint: disable=cell-var-from-loop

# pylint: disable=cell-var-from-loop
async def view_func(request: Request, body: Model):
kwargs = body.dict(exclude_unset=True)
if inspect.iscoroutinefunction(handler):
Expand All @@ -317,7 +322,6 @@ async def view_func(request: Request, body: Model):
result = handler(**kwargs)
return JSONResponse(content=result)


app.add_api_route(
route,
view_func,
Expand Down
9 changes: 7 additions & 2 deletions dash/backend/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import sys
import mimetypes
import time
import flask
import inspect
import flask
from dash.fingerprint import check_fingerprint
from dash import _validate
from dash.exceptions import PreventUpdate, InvalidResourceError
Expand Down Expand Up @@ -213,18 +213,23 @@ def register_callback_api_routes(self, app, callback_api_paths):
methods = ["POST"]

if inspect.iscoroutinefunction(handler):

async def view_func(*args, handler=handler, **kwargs):
data = flask.request.get_json()
result = await handler(**data) if data else await handler()
return flask.jsonify(result)

else:

def view_func(*args, handler=handler, **kwargs):
data = flask.request.get_json()
result = handler(**data) if data else handler()
return flask.jsonify(result)

# Flask 2.x+ supports async views natively
app.add_url_rule(route, endpoint=endpoint, view_func=view_func, methods=methods)
app.add_url_rule(
route, endpoint=endpoint, view_func=view_func, methods=methods
)


class FlaskRequestAdapter:
Expand Down
91 changes: 58 additions & 33 deletions dash/backend/quart.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
from .base_server import BaseDashServer
from quart import Quart, Request, Response, jsonify, request
from dash.exceptions import PreventUpdate, InvalidResourceError
from dash.backend import set_request_adapter
from dash.fingerprint import check_fingerprint
from dash import _validate
from contextvars import copy_context
import inspect
import pkgutil
import mimetypes
import sys
import time
from contextvars import copy_context

try:
import quart
from quart import Quart, Response, jsonify, request, Blueprint
except ImportError:
quart = None
Quart = None
Response = None
jsonify = None
request = None
Blueprint = None
from dash.exceptions import PreventUpdate, InvalidResourceError
from dash.backend import set_request_adapter
from dash.fingerprint import check_fingerprint
from dash import _validate
from .base_server import BaseDashServer


class QuartDashServer(BaseDashServer):
Expand All @@ -24,7 +34,7 @@ def __init__(self) -> None:
super().__init__()

def __call__(self, server, *args, **kwargs):
return super().__call__(server, *args, **kwargs)
return server(*args, **kwargs)

def create_app(self, name="__main__", config=None):
app = Quart(name)
Expand All @@ -36,8 +46,6 @@ def create_app(self, name="__main__", config=None):
def register_assets_blueprint(
self, app, blueprint_name, assets_url_path, assets_folder
):
from quart import Blueprint

bp = Blueprint(
blueprint_name,
__name__,
Expand All @@ -53,15 +61,15 @@ async def _wrap_errors(_error_request, error):
return tb, 500

def register_timing_hooks(self, app, _first_run): # parity with Flask factory
from quart import g

@app.before_request
async def _before_request(): # pragma: no cover - timing infra
g.timing_information = {"__dash_server": {"dur": time.time(), "desc": None}}
quart.g.timing_information = {
"__dash_server": {"dur": time.time(), "desc": None}
}

@app.after_request
async def _after_request(response): # pragma: no cover - timing infra
timing_information = getattr(g, "timing_information", None)
timing_information = getattr(quart.g, "timing_information", None)
if timing_information is None:
return response
dash_total = timing_information.get("__dash_server", None)
Expand Down Expand Up @@ -90,7 +98,7 @@ async def _invalid_resource(err):
return err.args[0], 404

def _html_response_wrapper(self, view_func):
async def wrapped(*args, **kwargs):
async def wrapped(*_args, **_kwargs):
html_val = view_func() if callable(view_func) else view_func
if inspect.iscoroutine(html_val): # handle async function returning html
html_val = await html_val
Expand All @@ -105,21 +113,25 @@ def add_url_rule(self, app, rule, view_func, endpoint=None, methods=None):
)

def setup_index(self, dash_app):
async def index():
async def index(*args, **kwargs):
adapter = QuartRequestAdapter()
set_request_adapter(adapter)
adapter.set_request(request)
return Response(dash_app.index(), content_type="text/html")
adapter.set_request()
return Response(dash_app.index(*args, **kwargs), content_type="text/html")

# pylint: disable=protected-access
dash_app._add_url("", index, methods=["GET"])

def setup_catchall(self, dash_app):
async def catchall(path): # noqa: ARG001 - path is unused but kept for route signature
async def catchall(
path, *args, **kwargs
): # noqa: ARG001 - path is unused but kept for route signature, pylint: disable=unused-argument
adapter = QuartRequestAdapter()
set_request_adapter(adapter)
adapter.set_request(request)
return Response(dash_app.index(), content_type="text/html")
adapter.set_request()
return Response(dash_app.index(*args, **kwargs), content_type="text/html")

# pylint: disable=protected-access
dash_app._add_url("<path:path>", catchall, methods=["GET"])

def before_request(self, app, func):
Expand All @@ -135,7 +147,7 @@ async def _after(response):
return response

def run(self, app, host, port, debug, **kwargs):
self.config = {'debug': debug, **kwargs} if debug else kwargs
self.config = {"debug": debug, **kwargs} if debug else kwargs
app.run(host=host, port=port, debug=debug, **kwargs)

def make_response(self, data, mimetype=None, content_type=None):
Expand All @@ -147,7 +159,9 @@ def jsonify(self, obj):
def get_request_adapter(self):
return QuartRequestAdapter

def serve_component_suites(self, dash_app, package_name, fingerprinted_path, req): # noqa: ARG002 unused req preserved for interface parity
def serve_component_suites(
self, dash_app, package_name, fingerprinted_path
): # noqa: ARG002 unused req preserved for interface parity
path_in_pkg, has_fingerprint = check_fingerprint(fingerprinted_path)
_validate.validate_js_path(dash_app.registered_paths, package_name, path_in_pkg)
extension = "." + path_in_pkg.split(".")[-1]
Expand All @@ -170,24 +184,30 @@ def serve_component_suites(self, dash_app, package_name, fingerprinted_path, req
def setup_component_suites(self, dash_app):
async def serve(package_name, fingerprinted_path):
return self.serve_component_suites(
dash_app, package_name, fingerprinted_path, request
dash_app, package_name, fingerprinted_path
)

# pylint: disable=protected-access
dash_app._add_url(
"_dash-component-suites/<string:package_name>/<path:fingerprinted_path>",
serve,
)

# pylint: disable=unused-argument
def dispatch(self, app, dash_app, use_async=True): # Quart always async
async def _dispatch():
adapter = QuartRequestAdapter()
set_request_adapter(adapter)
adapter.set_request(request)
adapter.set_request()
body = await request.get_json()
# pylint: disable=protected-access
g = dash_app._initialize_context(body, adapter)
# pylint: disable=protected-access
func = dash_app._prepare_callback(g, body)
# pylint: disable=protected-access
args = dash_app._inputs_to_vals(g.inputs_list + g.states_list)
ctx = copy_context()
# pylint: disable=protected-access
partial_func = dash_app._execute_callback(func, args, g.outputs_list, g)
response_data = ctx.run(partial_func)
if inspect.iscoroutine(response_data): # if user callback is async
Expand All @@ -209,20 +229,25 @@ def register_callback_api_routes(self, app, callback_api_paths):

def _make_view_func(handler):
if inspect.iscoroutinefunction(handler):

async def async_view_func(*args, **kwargs):
data = await request.get_json()
result = await handler(**data) if data else await handler()
return jsonify(result)

return async_view_func
else:
async def sync_view_func(*args, **kwargs):
data = await request.get_json()
result = handler(**data) if data else handler()
return jsonify(result)
return sync_view_func

async def sync_view_func(*args, **kwargs):
data = await request.get_json()
result = handler(**data) if data else handler()
return jsonify(result)

return sync_view_func

view_func = _make_view_func(handler)
app.add_url_rule(route, endpoint=endpoint, view_func=view_func, methods=methods)
app.add_url_rule(
route, endpoint=endpoint, view_func=view_func, methods=methods
)

def _serve_default_favicon(self):
return Response(
Expand All @@ -234,7 +259,7 @@ class QuartRequestAdapter:
def __init__(self) -> None:
self._request = None

def set_request(self, request: Request) -> None:
def set_request(self) -> None:
self._request = request

# Accessors (instance-based)
Expand Down
17 changes: 11 additions & 6 deletions dash/backend/registry.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import importlib

_backend_imports = {
'flask': ('dash.backend.flask', 'FlaskDashServer'),
'fastapi': ('dash.backend.fastapi', 'FastAPIDashServer'),
'quart': ('dash.backend.quart', 'QuartDashServer'),
"flask": ("dash.backend.flask", "FlaskDashServer"),
"fastapi": ("dash.backend.fastapi", "FastAPIDashServer"),
"quart": ("dash.backend.quart", "QuartDashServer"),
}


def register_backend(name, module_path, class_name):
"""Register a new backend by name."""
_backend_imports[name.lower()] = (module_path, class_name)


def get_backend(name):
try:
module_name, class_name = _backend_imports[name.lower()]
Expand All @@ -18,7 +20,10 @@ def get_backend(name):
except KeyError as e:
raise ValueError(f"Unknown backend: {name}") from e
except ImportError as e:
raise ImportError(f"Could not import module '{module_name}' for backend '{name}': {e}") from e
raise ImportError(
f"Could not import module '{module_name}' for backend '{name}': {e}"
) from e
except AttributeError as e:
raise AttributeError(f"Module '{module_name}' does not have class '{class_name}' for backend '{name}': {e}") from e

raise AttributeError(
f"Module '{module_name}' does not have class '{class_name}' for backend '{name}': {e}"
) from e
Loading
Loading