Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 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
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
107 changes: 107 additions & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
# This output will be 'true' if files in the 'table_related_paths' list changed, 'false' otherwise.
table_paths_changed: ${{ steps.filter.outputs.table_related_paths }}
background_cb_changed: ${{ steps.filter.outputs.background_paths }}
backend_cb_changed: ${{ steps.filter.outputs.backend_paths }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -37,6 +38,9 @@ jobs:
- 'tests/background_callback/**'
- 'tests/async_tests/**'
- 'requirements/**'
backend_paths:
- 'dash/backend/**'
- 'tests/backend/**'

build:
name: Build Dash Package
Expand Down Expand Up @@ -271,6 +275,109 @@ jobs:
cd bgtests
pytest --headless --nopercyfinalize tests/async_tests -v -s

backend-tests:
name: Run Backend Callback Tests (Python ${{ matrix.python-version }})
needs: [build, changes_filter]
if: |
(github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) ||
needs.changes_filter.outputs.backend_cb_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.12"]

services:
redis:
image: redis:6
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
REDIS_URL: redis://localhost:6379
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install Node.js dependencies
run: npm ci

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'

- name: Download built Dash packages
uses: actions/download-artifact@v4
with:
name: dash-packages
path: packages/

- name: Install Dash packages
run: |
python -m pip install --upgrade pip wheel
python -m pip install "setuptools<78.0.0"
python -m pip install "selenium==4.32.0"
find packages -name dash-*.whl -print -exec sh -c 'pip install "{}[async,ci,testing,dev,celery,diskcache,fastapi,quart]"' \;

- name: Install Google Chrome
run: |
sudo apt-get update
sudo apt-get install -y google-chrome-stable

- name: Install ChromeDriver
run: |
echo "Determining Chrome version..."
CHROME_BROWSER_VERSION=$(google-chrome --version)
echo "Installed Chrome Browser version: $CHROME_BROWSER_VERSION"
CHROME_MAJOR_VERSION=$(echo "$CHROME_BROWSER_VERSION" | cut -f 3 -d ' ' | cut -f 1 -d '.')
echo "Detected Chrome Major version: $CHROME_MAJOR_VERSION"
if [ "$CHROME_MAJOR_VERSION" -ge 115 ]; then
echo "Fetching ChromeDriver version for Chrome $CHROME_MAJOR_VERSION using CfT endpoint..."
CHROMEDRIVER_VERSION_STRING=$(curl -sS "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION}")
if [ -z "$CHROMEDRIVER_VERSION_STRING" ]; then
echo "Could not automatically find ChromeDriver version for Chrome $CHROME_MAJOR_VERSION via LATEST_RELEASE. Please check CfT endpoints."
exit 1
fi
CHROMEDRIVER_URL="https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROMEDRIVER_VERSION_STRING}/linux64/chromedriver-linux64.zip"
else
echo "Fetching ChromeDriver version for Chrome $CHROME_MAJOR_VERSION using older method..."
CHROMEDRIVER_VERSION_STRING=$(curl -sS "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION}")
CHROMEDRIVER_URL="https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION_STRING}/chromedriver_linux64.zip"
fi
echo "Using ChromeDriver version string: $CHROMEDRIVER_VERSION_STRING"
echo "Downloading ChromeDriver from: $CHROMEDRIVER_URL"
wget -q -O chromedriver.zip "$CHROMEDRIVER_URL"
unzip -o chromedriver.zip -d /tmp/
sudo mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/chromedriver || sudo mv /tmp/chromedriver /usr/local/bin/chromedriver
sudo chmod +x /usr/local/bin/chromedriver
echo "/usr/local/bin" >> $GITHUB_PATH
shell: bash

- name: Build/Setup test components
run: npm run setup-tests.py

- name: Run Backend Callback Tests
run: |
mkdir bgtests
cp -r tests bgtests/tests
cd bgtests
touch __init__.py
pytest --headless --nopercyfinalize tests/backend_tests -v -s

table-unit:
name: Table Unit/Lint Tests (Python ${{ matrix.python-version }})
needs: [build, changes_filter]
Expand Down
19 changes: 9 additions & 10 deletions dash/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


import asyncio
import flask
from dash.backend import get_request_adapter

from .dependencies import (
handle_callback_args,
Expand Down Expand Up @@ -376,7 +376,7 @@ def _get_callback_manager(
" and store results on redis.\n"
)

old_job = flask.request.args.getlist("oldJob")
old_job = get_request_adapter().get_args().getlist("oldJob")

if old_job:
for job in old_job:
Expand Down Expand Up @@ -436,7 +436,7 @@ def _setup_background_callback(

def _progress_background_callback(response, callback_manager, background):
progress_outputs = background.get("progress")
cache_key = flask.request.args.get("cacheKey")
cache_key = get_request_adapter().get_args().get("cacheKey")

if progress_outputs:
# Get the progress before the result as it would be erased after the results.
Expand All @@ -453,8 +453,8 @@ def _update_background_callback(
"""Set up the background callback and manage jobs."""
callback_manager = _get_callback_manager(kwargs, background)

cache_key = flask.request.args.get("cacheKey")
job_id = flask.request.args.get("job")
cache_key = get_request_adapter().get_args().get("cacheKey")
job_id = get_request_adapter().get_args().get("job")

_progress_background_callback(response, callback_manager, background)

Expand All @@ -474,8 +474,8 @@ def _handle_rest_background_callback(
multi,
has_update=False,
):
cache_key = flask.request.args.get("cacheKey")
job_id = flask.request.args.get("job")
cache_key = get_request_adapter().get_args().get("cacheKey")
job_id = get_request_adapter().get_args().get("job")
# Must get job_running after get_result since get_results terminates it.
job_running = callback_manager.job_running(job_id)
if not job_running and output_value is callback_manager.UNDEFINED:
Expand Down Expand Up @@ -688,11 +688,10 @@ def add_context(*args, **kwargs):
)

response: dict = {"multi": True}

jsonResponse = None
try:
if background is not None:
if not flask.request.args.get("cacheKey"):
if not get_request_adapter().get_args().get("cacheKey"):
return _setup_background_callback(
kwargs,
background,
Expand Down Expand Up @@ -763,7 +762,7 @@ async def async_add_context(*args, **kwargs):

try:
if background is not None:
if not flask.request.args.get("cacheKey"):
if not get_request_adapter().get_args().get("cacheKey"):
return _setup_background_callback(
kwargs,
background,
Expand Down
8 changes: 8 additions & 0 deletions dash/_callback_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ def path(self):
"""
return _get_from_context("path", "")

@property
@has_context
def args(self):
"""
Query parameters of the callback request as a dictionary-like object.
"""
return _get_from_context("args", "")

@property
@has_context
def remote(self):
Expand Down
10 changes: 5 additions & 5 deletions dash/_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,15 +389,15 @@ def _path_to_page(path_id):
return {}, None


def _page_meta_tags(app):
start_page, path_variables = _path_to_page(flask.request.path.strip("/"))
def _page_meta_tags(app, request):
request_path = request.get_path()
start_page, path_variables = _path_to_page(request_path.strip("/"))

# use the supplied image_url or create url based on image in the assets folder
image = start_page.get("image", "")
if image:
image = app.get_asset_url(image)
assets_image_url = (
"".join([flask.request.url_root, image.lstrip("/")]) if image else None
"".join([request.get_root(), image.lstrip("/")]) if image else None
)
supplied_image_url = start_page.get("image_url")
image_url = supplied_image_url if supplied_image_url else assets_image_url
Expand All @@ -413,7 +413,7 @@ def _page_meta_tags(app):
return [
{"name": "description", "content": description},
{"property": "twitter:card", "content": "summary_large_image"},
{"property": "twitter:url", "content": flask.request.url},
{"property": "twitter:url", "content": request.get_url()},
{"property": "twitter:title", "content": title},
{"property": "twitter:description", "content": description},
{"property": "twitter:image", "content": image_url or ""},
Expand Down
5 changes: 5 additions & 0 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ def set_read_only(self, names, msg="Attribute is read-only"):
else:
object.__setattr__(self, "_read_only", new_read_only)

def unset_read_only(self, keys):
if hasattr(self, "_read_only"):
for key in keys:
self._read_only.pop(key, None)

def finalize(self, msg="Object is final: No new keys may be added."):
"""Prevent any new keys being set."""
object.__setattr__(self, "_final", msg)
Expand Down
15 changes: 15 additions & 0 deletions dash/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# python
import contextvars
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")


def set_request_adapter(adapter):
_request_adapter_var.set(adapter)


def get_request_adapter():
return _request_adapter_var.get()
58 changes: 58 additions & 0 deletions dash/backend/base_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from abc import ABC, abstractmethod
from typing import Any


class BaseDashServer(ABC):
def __call__(self, server, *args, **kwargs) -> Any:
# Default: WSGI
return server(*args, **kwargs)

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

@abstractmethod
def register_assets_blueprint(
self, app, blueprint_name: str, assets_url_path: str, assets_folder: str
) -> None: # pragma: no cover - interface
pass

@abstractmethod
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
pass

@abstractmethod
def before_request(self, app, func) -> None: # pragma: no cover - interface
pass

@abstractmethod
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
pass

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

@abstractmethod
def jsonify(self, obj) -> Any: # pragma: no cover - interface
pass

@abstractmethod
def get_request_adapter(self) -> Any: # pragma: no cover - interface
pass
Loading
Loading