-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
Copy pathdash.py
2134 lines (1808 loc) · 80.5 KB
/
dash.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import functools
import os
import sys
import collections
import importlib
from contextvars import copy_context
from importlib.machinery import ModuleSpec
import pkgutil
import threading
import re
import logging
import time
import mimetypes
import hashlib
import base64
import traceback
from urllib.parse import urlparse
from textwrap import dedent
import flask
from flask_compress import Compress
from pkg_resources import get_distribution, parse_version
from dash import dcc
from dash import html
from dash import dash_table
from .fingerprint import build_fingerprint, check_fingerprint
from .resources import Scripts, Css
from .dependencies import (
Output,
Input,
)
from .development.base_component import ComponentRegistry
from .exceptions import (
PreventUpdate,
InvalidResourceError,
ProxyError,
DuplicateCallback,
)
from .version import __version__
from ._configs import get_combined_config, pathname_configs, pages_folder_config
from ._utils import (
AttributeDict,
format_tag,
generate_hash,
inputs_to_dict,
inputs_to_vals,
interpolate_str,
patch_collections_abc,
split_callback_id,
to_json,
convert_to_AttributeDict,
gen_salt,
)
from . import _callback
from . import _get_paths
from . import _dash_renderer
from . import _validate
from . import _watch
from . import _get_app
from ._grouping import map_grouping, grouping_len, update_args_group
from . import _pages
from ._pages import (
_parse_path_variables,
_parse_query_string,
)
_flask_compress_version = parse_version(get_distribution("flask-compress").version)
# Add explicit mapping for map files
mimetypes.add_type("application/json", ".map", True)
_default_index = """<!DOCTYPE html>
<html>
<head>
{%metas%}
<title>{%title%}</title>
{%favicon%}
{%css%}
</head>
<body>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>"""
_app_entry = """
<div id="react-entry-point">
<div class="_dash-loading">
Loading...
</div>
</div>
"""
_re_index_entry = "{%app_entry%}", "{%app_entry%}"
_re_index_config = "{%config%}", "{%config%}"
_re_index_scripts = "{%scripts%}", "{%scripts%}"
_re_index_entry_id = 'id="react-entry-point"', "#react-entry-point"
_re_index_config_id = 'id="_dash-config"', "#_dash-config"
_re_index_scripts_id = 'src="[^"]*dash[-_]renderer[^"]*"', "dash-renderer"
_re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer"
_ID_CONTENT = "_pages_content"
_ID_LOCATION = "_pages_location"
_ID_STORE = "_pages_store"
_ID_DUMMY = "_pages_dummy"
# Handles the case in a newly cloned environment where the components are not yet generated.
try:
page_container = html.Div(
[
dcc.Location(id=_ID_LOCATION),
html.Div(id=_ID_CONTENT),
dcc.Store(id=_ID_STORE),
html.Div(id=_ID_DUMMY),
]
)
except AttributeError:
page_container = None
def _get_traceback(secret, error: Exception):
try:
# pylint: disable=import-outside-toplevel
from werkzeug.debug import tbtools
except ImportError:
tbtools = None
def _get_skip(text, divider=2):
skip = 0
for i, line in enumerate(text):
if "%% callback invoked %%" in line:
skip = int((i + 1) / divider)
break
return skip
# werkzeug<2.1.0
if hasattr(tbtools, "get_current_traceback"):
tb = tbtools.get_current_traceback()
skip = _get_skip(tb.plaintext.splitlines())
return tbtools.get_current_traceback(skip=skip).render_full()
if hasattr(tbtools, "DebugTraceback"):
tb = tbtools.DebugTraceback(error) # pylint: disable=no-member
skip = _get_skip(tb.render_traceback_text().splitlines())
# pylint: disable=no-member
return tbtools.DebugTraceback(error, skip=skip).render_debugger_html(
True, secret, True
)
tb = traceback.format_exception(type(error), error, error.__traceback__)
skip = _get_skip(tb, 1)
return tb[0] + "".join(tb[skip:])
# Singleton signal to not update an output, alternative to PreventUpdate
no_update = _callback.NoUpdate() # pylint: disable=protected-access
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments, too-many-locals
class Dash:
"""Dash is a framework for building analytical web applications.
No JavaScript required.
If a parameter can be set by an environment variable, that is listed as:
env: ``DASH_****``
Values provided here take precedence over environment variables.
:param name: The name Flask should use for your app. Even if you provide
your own ``server``, ``name`` will be used to help find assets.
Typically ``__name__`` (the magic global var, not a string) is the
best value to use. Default ``'__main__'``, env: ``DASH_APP_NAME``
:type name: string
:param server: Sets the Flask server for your app. There are three options:
``True`` (default): Dash will create a new server
``False``: The server will be added later via ``app.init_app(server)``
where ``server`` is a ``flask.Flask`` instance.
``flask.Flask``: use this pre-existing Flask server.
:type server: boolean or flask.Flask
:param assets_folder: a path, relative to the current working directory,
for extra files to be used in the browser. Default ``'assets'``.
All .js and .css files will be loaded immediately unless excluded by
``assets_ignore``, and other files such as images will be served if
requested.
:type assets_folder: string
:param pages_folder: a path, relative to the current working directory,
for pages of a multi-page app. Default ``'pages'``.
:type pages_folder: string
:param use_pages: Default False, or True if you set a non-default ``pages_folder``.
When True, the ``pages`` feature for multi-page apps is enabled.
:type pages: boolean
:param assets_url_path: The local urls for assets will be:
``requests_pathname_prefix + assets_url_path + '/' + asset_path``
where ``asset_path`` is the path to a file inside ``assets_folder``.
Default ``'assets'``.
:type asset_url_path: string
:param assets_ignore: A regex, as a string to pass to ``re.compile``, for
assets to omit from immediate loading. Ignored files will still be
served if specifically requested. You cannot use this to prevent access
to sensitive files.
:type assets_ignore: string
:param assets_external_path: an absolute URL from which to load assets.
Use with ``serve_locally=False``. assets_external_path is joined
with assets_url_path to determine the absolute url to the
asset folder. Dash can still find js and css to automatically load
if you also keep local copies in your assets folder that Dash can index,
but external serving can improve performance and reduce load on
the Dash server.
env: ``DASH_ASSETS_EXTERNAL_PATH``
:type assets_external_path: string
:param include_assets_files: Default ``True``, set to ``False`` to prevent
immediate loading of any assets. Assets will still be served if
specifically requested. You cannot use this to prevent access
to sensitive files. env: ``DASH_INCLUDE_ASSETS_FILES``
:type include_assets_files: boolean
:param url_base_pathname: A local URL prefix to use app-wide.
Default ``'/'``. Both `requests_pathname_prefix` and
`routes_pathname_prefix` default to `url_base_pathname`.
env: ``DASH_URL_BASE_PATHNAME``
:type url_base_pathname: string
:param requests_pathname_prefix: A local URL prefix for file requests.
Defaults to `url_base_pathname`, and must end with
`routes_pathname_prefix`. env: ``DASH_REQUESTS_PATHNAME_PREFIX``
:type requests_pathname_prefix: string
:param routes_pathname_prefix: A local URL prefix for JSON requests.
Defaults to ``url_base_pathname``, and must start and end
with ``'/'``. env: ``DASH_ROUTES_PATHNAME_PREFIX``
:type routes_pathname_prefix: string
:param serve_locally: If ``True`` (default), assets and dependencies
(Dash and Component js and css) will be served from local URLs.
If ``False`` we will use CDN links where available.
:type serve_locally: boolean
:param compress: Use gzip to compress files and data served by Flask.
Default ``False``
:type compress: boolean
:param meta_tags: html <meta> tags to be added to the index page.
Each dict should have the attributes and values for one tag, eg:
``{'name': 'description', 'content': 'My App'}``
:type meta_tags: list of dicts
:param index_string: Override the standard Dash index page.
Must contain the correct insertion markers to interpolate various
content into it depending on the app config and components used.
See https://dash.plotly.com/external-resources for details.
:type index_string: string
:param external_scripts: Additional JS files to load with the page.
Each entry can be a string (the URL) or a dict with ``src`` (the URL)
and optionally other ``<script>`` tag attributes such as ``integrity``
and ``crossorigin``.
:type external_scripts: list of strings or dicts
:param external_stylesheets: Additional CSS files to load with the page.
Each entry can be a string (the URL) or a dict with ``href`` (the URL)
and optionally other ``<link>`` tag attributes such as ``rel``,
``integrity`` and ``crossorigin``.
:type external_stylesheets: list of strings or dicts
:param suppress_callback_exceptions: Default ``False``: check callbacks to
ensure referenced IDs exist and props are valid. Set to ``True``
if your layout is dynamic, to bypass these checks.
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
:type suppress_callback_exceptions: boolean
:param prevent_initial_callbacks: Default ``False``: Sets the default value
of ``prevent_initial_call`` for all callbacks added to the app.
Normally all callbacks are fired when the associated outputs are first
added to the page. You can disable this for individual callbacks by
setting ``prevent_initial_call`` in their definitions, or set it
``True`` here in which case you must explicitly set it ``False`` for
those callbacks you wish to have an initial call. This setting has no
effect on triggering callbacks when their inputs change later on.
:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
and redo buttons for stepping through the history of the app state.
:type show_undo_redo: boolean
:param extra_hot_reload_paths: A list of paths to watch for changes, in
addition to assets and known Python and JS code, if hot reloading is
enabled.
:type extra_hot_reload_paths: list of strings
:param plugins: Extend Dash functionality by passing a list of objects
with a ``plug`` method, taking a single argument: this app, which will
be called after the Flask server is attached.
:type plugins: list of objects
:param title: Default ``Dash``. Configures the document.title
(the text that appears in a browser tab).
:param update_title: Default ``Updating...``. Configures the document.title
(the text that appears in a browser tab) text when a callback is being run.
Set to None or '' if you don't want the document.title to change or if you
want to control the document.title through a separate component or
clientside callback.
:param long_callback_manager: Deprecated, use ``background_callback_manager``
instead.
:param background_callback_manager: Background callback manager instance
to support the ``@callback(..., background=True)`` decorator.
One of ``DiskcacheManager`` or ``CeleryManager`` currently supported.
"""
def __init__( # pylint: disable=too-many-statements
self,
name=None,
server=True,
assets_folder="assets",
pages_folder="pages",
use_pages=False,
assets_url_path="assets",
assets_ignore="",
assets_external_path=None,
eager_loading=False,
include_assets_files=True,
url_base_pathname=None,
requests_pathname_prefix=None,
routes_pathname_prefix=None,
serve_locally=True,
compress=None,
meta_tags=None,
index_string=_default_index,
external_scripts=None,
external_stylesheets=None,
suppress_callback_exceptions=None,
prevent_initial_callbacks=False,
show_undo_redo=False,
extra_hot_reload_paths=None,
plugins=None,
title="Dash",
update_title="Updating...",
long_callback_manager=None,
background_callback_manager=None,
**obsolete,
):
_validate.check_obsolete(obsolete)
# We have 3 cases: server is either True (we create the server), False
# (defer server creation) or a Flask app instance (we use their server)
if isinstance(server, flask.Flask):
self.server = server
if name is None:
name = getattr(server, "name", "__main__")
elif isinstance(server, bool):
name = name if name else "__main__"
self.server = flask.Flask(name) if server else None
else:
raise ValueError("server must be a Flask app or a boolean")
if (
self.server is not None
and not hasattr(self.server.config, "COMPRESS_ALGORITHM")
and _flask_compress_version >= parse_version("1.6.0")
):
# flask-compress==1.6.0 changed default to ['br', 'gzip']
# and non-overridable default compression with Brotli is
# causing performance issues
self.server.config["COMPRESS_ALGORITHM"] = ["gzip"]
base_prefix, routes_prefix, requests_prefix = pathname_configs(
url_base_pathname, routes_pathname_prefix, requests_pathname_prefix
)
self.config = AttributeDict(
name=name,
assets_folder=os.path.join(
flask.helpers.get_root_path(name), assets_folder
),
assets_url_path=assets_url_path,
assets_ignore=assets_ignore,
assets_external_path=get_combined_config(
"assets_external_path", assets_external_path, ""
),
pages_folder=pages_folder_config(name, pages_folder, use_pages),
eager_loading=eager_loading,
include_assets_files=get_combined_config(
"include_assets_files", include_assets_files, True
),
url_base_pathname=base_prefix,
routes_pathname_prefix=routes_prefix,
requests_pathname_prefix=requests_prefix,
serve_locally=serve_locally,
compress=get_combined_config("compress", compress, False),
meta_tags=meta_tags or [],
external_scripts=external_scripts or [],
external_stylesheets=external_stylesheets or [],
suppress_callback_exceptions=get_combined_config(
"suppress_callback_exceptions", suppress_callback_exceptions, False
),
prevent_initial_callbacks=prevent_initial_callbacks,
show_undo_redo=show_undo_redo,
extra_hot_reload_paths=extra_hot_reload_paths or [],
title=title,
update_title=update_title,
)
self.config.set_read_only(
[
"name",
"assets_folder",
"assets_url_path",
"eager_loading",
"serve_locally",
"compress",
],
"Read-only: can only be set in the Dash constructor",
)
self.config.finalize(
"Invalid config key. Some settings are only available "
"via the Dash constructor"
)
_get_paths.CONFIG = self.config
_pages.CONFIG = self.config
self.pages_folder = pages_folder
self.use_pages = True if pages_folder != "pages" else use_pages
# keep title as a class property for backwards compatibility
self.title = title
# list of dependencies - this one is used by the back end for dispatching
self.callback_map = {}
# same deps as a list to catch duplicate outputs, and to send to the front end
self._callback_list = []
# list of inline scripts
self._inline_scripts = []
# index_string has special setter so can't go in config
self._index_string = ""
self.index_string = index_string
self._favicon = None
# default renderer string
self.renderer = "var renderer = new DashRenderer();"
# static files from the packages
self.css = Css(serve_locally)
self.scripts = Scripts(serve_locally, eager_loading)
self.registered_paths = collections.defaultdict(set)
# urls
self.routes = []
self._layout = None
self._layout_is_function = False
self.validation_layout = None
self._extra_components = []
self._setup_dev_tools()
self._hot_reload = AttributeDict(
hash=None,
hard=False,
lock=threading.RLock(),
watch_thread=None,
changed_assets=[],
)
self._assets_files = []
self._long_callback_count = 0
self._background_manager = background_callback_manager or long_callback_manager
self.logger = logging.getLogger(name)
self.logger.addHandler(logging.StreamHandler(stream=sys.stdout))
if isinstance(plugins, patch_collections_abc("Iterable")):
for plugin in plugins:
plugin.plug(self)
if self.server is not None:
self.init_app()
self.logger.setLevel(logging.INFO)
def init_app(self, app=None, **kwargs):
"""Initialize the parts of Dash that require a flask app."""
config = self.config
config.update(kwargs)
config.set_read_only(
[
"url_base_pathname",
"routes_pathname_prefix",
"requests_pathname_prefix",
],
"Read-only: can only be set in the Dash constructor or during init_app()",
)
if app is not None:
self.server = app
bp_prefix = config.routes_pathname_prefix.replace("/", "_").replace(".", "_")
assets_blueprint_name = f"{bp_prefix}dash_assets"
self.server.register_blueprint(
flask.Blueprint(
assets_blueprint_name,
config.name,
static_folder=self.config.assets_folder,
static_url_path=config.routes_pathname_prefix
+ self.config.assets_url_path.lstrip("/"),
)
)
if config.compress:
# gzip
Compress(self.server)
@self.server.errorhandler(PreventUpdate)
def _handle_error(_):
"""Handle a halted callback and return an empty 204 response."""
return "", 204
self.server.before_first_request(self._setup_server)
# add a handler for components suites errors to return 404
self.server.errorhandler(InvalidResourceError)(self._invalid_resources_handler)
self._add_url(
"_dash-component-suites/<string:package_name>/<path:fingerprinted_path>",
self.serve_component_suites,
)
self._add_url("_dash-layout", self.serve_layout)
self._add_url("_dash-dependencies", self.dependencies)
self._add_url("_dash-update-component", self.dispatch, ["POST"])
self._add_url("_reload-hash", self.serve_reload_hash)
self._add_url("_favicon.ico", self._serve_default_favicon)
self._add_url("", self.index)
# catch-all for front-end routes, used by dcc.Location
self._add_url("<path:path>", self.index)
_get_app.APP = self
self.enable_pages()
def _add_url(self, name, view_func, methods=("GET",)):
full_name = self.config.routes_pathname_prefix + name
self.server.add_url_rule(
full_name, view_func=view_func, endpoint=full_name, methods=list(methods)
)
# record the url in Dash.routes so that it can be accessed later
# e.g. for adding authentication with flask_login
self.routes.append(full_name)
@property
def layout(self):
return self._layout
def _layout_value(self):
layout = self._layout() if self._layout_is_function else self._layout
# Add any extra components
if self._extra_components:
layout = html.Div(children=[layout] + self._extra_components)
return layout
@layout.setter
def layout(self, value):
_validate.validate_layout_type(value)
self._layout_is_function = callable(value)
self._layout = value
# for using flask.has_request_context() to deliver a full layout for
# validation inside a layout function - track if a user might be doing this.
if (
self._layout_is_function
and not self.validation_layout
and not self.config.suppress_callback_exceptions
):
def simple_clone(c, children=None):
cls = type(c)
# in Py3 we can use the __init__ signature to reduce to just
# required args and id; in Py2 this doesn't work so we just
# empty out children.
sig = getattr(cls.__init__, "__signature__", None)
props = {
p: getattr(c, p)
for p in c._prop_names # pylint: disable=protected-access
if hasattr(c, p)
and (
p == "id" or not sig or sig.parameters[p].default == c.REQUIRED
)
}
if props.get("children", children):
props["children"] = children or []
return cls(**props)
layout_value = self._layout_value()
_validate.validate_layout(value, layout_value)
self.validation_layout = simple_clone(
# pylint: disable=protected-access
layout_value,
[simple_clone(c) for c in layout_value._traverse_ids()],
)
@property
def index_string(self):
return self._index_string
@index_string.setter
def index_string(self, value):
checks = (_re_index_entry, _re_index_config, _re_index_scripts)
_validate.validate_index("index string", checks, value)
self._index_string = value
def serve_layout(self):
layout = self._layout_value()
# TODO - Set browser cache limit - pass hash into frontend
return flask.Response(
to_json(layout),
mimetype="application/json",
)
def _config(self):
# pieces of config needed by the front end
config = {
"url_base_pathname": self.config.url_base_pathname,
"requests_pathname_prefix": self.config.requests_pathname_prefix,
"ui": self._dev_tools.ui,
"props_check": self._dev_tools.props_check,
"show_undo_redo": self.config.show_undo_redo,
"suppress_callback_exceptions": self.config.suppress_callback_exceptions,
"update_title": self.config.update_title,
"children_props": ComponentRegistry.children_props,
}
if self._dev_tools.hot_reload:
config["hot_reload"] = {
# convert from seconds to msec as used by js `setInterval`
"interval": int(self._dev_tools.hot_reload_interval * 1000),
"max_retry": self._dev_tools.hot_reload_max_retry,
}
if self.validation_layout and not self.config.suppress_callback_exceptions:
validation_layout = self.validation_layout
# Add extra components
if self._extra_components:
validation_layout = html.Div(
children=[validation_layout] + self._extra_components
)
config["validation_layout"] = validation_layout
return config
def serve_reload_hash(self):
_reload = self._hot_reload
with _reload.lock:
hard = _reload.hard
changed = _reload.changed_assets
_hash = _reload.hash
_reload.hard = False
_reload.changed_assets = []
return flask.jsonify(
{
"reloadHash": _hash,
"hard": hard,
"packages": list(self.registered_paths.keys()),
"files": list(changed),
}
)
def _collect_and_register_resources(self, resources):
# now needs the app context.
# template in the necessary component suite JS bundles
# add the version number of the package as a query parameter
# for cache busting
def _relative_url_path(relative_package_path="", namespace=""):
if any(
relative_package_path.startswith(x + "/")
for x in ["dcc", "html", "dash_table"]
):
relative_package_path = relative_package_path.replace("dash.", "")
version = importlib.import_module(
f"{namespace}.{os.path.split(relative_package_path)[0]}"
).__version__
else:
version = importlib.import_module(namespace).__version__
module_path = os.path.join(
os.path.dirname(sys.modules[namespace].__file__), relative_package_path
)
modified = int(os.stat(module_path).st_mtime)
fingerprint = build_fingerprint(relative_package_path, version, modified)
return f"{self.config.requests_pathname_prefix}_dash-component-suites/{namespace}/{fingerprint}"
srcs = []
for resource in resources:
is_dynamic_resource = resource.get("dynamic", False)
if "relative_package_path" in resource:
paths = resource["relative_package_path"]
paths = [paths] if isinstance(paths, str) else paths
for rel_path in paths:
if any(x in rel_path for x in ["dcc", "html", "dash_table"]):
rel_path = rel_path.replace("dash.", "")
self.registered_paths[resource["namespace"]].add(rel_path)
if not is_dynamic_resource:
srcs.append(
_relative_url_path(
relative_package_path=rel_path,
namespace=resource["namespace"],
)
)
elif "external_url" in resource:
if not is_dynamic_resource:
if isinstance(resource["external_url"], str):
srcs.append(resource["external_url"])
else:
srcs += resource["external_url"]
elif "absolute_path" in resource:
raise Exception("Serving files from absolute_path isn't supported yet")
elif "asset_path" in resource:
static_url = self.get_asset_url(resource["asset_path"])
# Add a cache-busting query param
static_url += f"?m={resource['ts']}"
srcs.append(static_url)
return srcs
def _generate_css_dist_html(self):
external_links = self.config.external_stylesheets
links = self._collect_and_register_resources(self.css.get_all_css())
return "\n".join(
[
format_tag("link", link, opened=True)
if isinstance(link, dict)
else f'<link rel="stylesheet" href="{link}">'
for link in (external_links + links)
]
)
def _generate_scripts_html(self):
# Dash renderer has dependencies like React which need to be rendered
# before every other script. However, the dash renderer bundle
# itself needs to be rendered after all of the component's
# scripts have rendered.
# The rest of the scripts can just be loaded after React but before
# dash renderer.
# pylint: disable=protected-access
mode = "dev" if self._dev_tools["props_check"] is True else "prod"
deps = []
for js_dist_dependency in _dash_renderer._js_dist_dependencies:
dep = {}
for key, value in js_dist_dependency.items():
dep[key] = value[mode] if isinstance(value, dict) else value
deps.append(dep)
dev = self._dev_tools.serve_dev_bundles
srcs = (
self._collect_and_register_resources(
self.scripts._resources._filter_resources(deps, dev_bundles=dev)
)
+ self.config.external_scripts
+ self._collect_and_register_resources(
self.scripts.get_all_scripts(dev_bundles=dev)
+ self.scripts._resources._filter_resources(
_dash_renderer._js_dist, dev_bundles=dev
)
+ self.scripts._resources._filter_resources(
dcc._js_dist, dev_bundles=dev
)
+ self.scripts._resources._filter_resources(
html._js_dist, dev_bundles=dev
)
+ self.scripts._resources._filter_resources(
dash_table._js_dist, dev_bundles=dev
)
)
)
self._inline_scripts.extend(_callback.GLOBAL_INLINE_SCRIPTS)
_callback.GLOBAL_INLINE_SCRIPTS.clear()
return "\n".join(
[
format_tag("script", src)
if isinstance(src, dict)
else f'<script src="{src}"></script>'
for src in srcs
]
+ [f"<script>{src}</script>" for src in self._inline_scripts]
)
def _generate_config_html(self):
return f'<script id="_dash-config" type="application/json">{to_json(self._config())}</script>'
def _generate_renderer(self):
return f'<script id="_dash-renderer" type="application/javascript">{self.renderer}</script>'
def _generate_meta_html(self):
meta_tags = self.config.meta_tags
has_ie_compat = any(
x.get("http-equiv", "") == "X-UA-Compatible" for x in meta_tags
)
has_charset = any("charset" in x for x in meta_tags)
has_viewport = any(x.get("name") == "viewport" for x in meta_tags)
tags = []
if not has_ie_compat:
tags.append('<meta http-equiv="X-UA-Compatible" content="IE=edge">')
if not has_charset:
tags.append('<meta charset="UTF-8">')
if not has_viewport:
tags.append(
'<meta name="viewport" content="width=device-width, initial-scale=1">'
)
tags += [format_tag("meta", x, opened=True) for x in meta_tags]
return "\n ".join(tags)
def _pages_meta_tags(self):
start_page, path_variables = self._path_to_page(flask.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 = self.get_asset_url(image)
assets_image_url = (
"".join([flask.request.url_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
title = start_page.get("title", self.title)
if callable(title):
title = title(**path_variables) if path_variables else title()
description = start_page.get("description", "")
if callable(description):
description = (
description(**path_variables) if path_variables else description()
)
return dedent(
f"""
<meta name="description" content="{description}" />
<!-- Twitter Card data -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="{flask.request.url}">
<meta property="twitter:title" content="{title}">
<meta property="twitter:description" content="{description}">
<meta property="twitter:image" content="{image_url}">
<!-- Open Graph data -->
<meta property="og:title" content="{title}" />
<meta property="og:type" content="website" />
<meta property="og:description" content="{description}" />
<meta property="og:image" content="{image_url}">
"""
)
# Serve the JS bundles for each package
def serve_component_suites(self, package_name, fingerprinted_path):
path_in_pkg, has_fingerprint = check_fingerprint(fingerprinted_path)
_validate.validate_js_path(self.registered_paths, package_name, path_in_pkg)
extension = "." + path_in_pkg.split(".")[-1]
mimetype = mimetypes.types_map.get(extension, "application/octet-stream")
package = sys.modules[package_name]
self.logger.debug(
"serving -- package: %s[%s] resource: %s => location: %s",
package_name,
package.__version__,
path_in_pkg,
package.__path__,
)
response = flask.Response(
pkgutil.get_data(package_name, path_in_pkg), mimetype=mimetype
)
if has_fingerprint:
# Fingerprinted resources are good forever (1 year)
# No need for ETag as the fingerprint changes with each build
response.cache_control.max_age = 31536000 # 1 year
else:
# Non-fingerprinted resources are given an ETag that
# will be used / check on future requests
response.add_etag()
tag = response.get_etag()[0]
request_etag = flask.request.headers.get("If-None-Match")
if f'"{tag}"' == request_etag:
response = flask.Response(None, status=304)
return response
def index(self, *args, **kwargs): # pylint: disable=unused-argument
scripts = self._generate_scripts_html()
css = self._generate_css_dist_html()
config = self._generate_config_html()
metas = self._generate_meta_html()
renderer = self._generate_renderer()
# use self.title instead of app.config.title for backwards compatibility
title = self.title
pages_metas = ""
if self.use_pages:
pages_metas = self._pages_meta_tags()
if self._favicon:
favicon_mod_time = os.path.getmtime(
os.path.join(self.config.assets_folder, self._favicon)
)
favicon_url = f"{self.get_asset_url(self._favicon)}?m={favicon_mod_time}"
else:
prefix = self.config.requests_pathname_prefix
favicon_url = f"{prefix}_favicon.ico?v={__version__}"
favicon = format_tag(
"link",
{"rel": "icon", "type": "image/x-icon", "href": favicon_url},
opened=True,
)
index = self.interpolate_index(
metas=pages_metas + metas,
title=title,
css=css,
config=config,
scripts=scripts,
app_entry=_app_entry,
favicon=favicon,
renderer=renderer,
)
checks = (
_re_index_entry_id,
_re_index_config_id,
_re_index_scripts_id,
_re_renderer_scripts_id,
)
_validate.validate_index("index", checks, index)
return index
def interpolate_index(
self,
metas="",
title="",
css="",
config="",
scripts="",
app_entry="",
favicon="",
renderer="",
):
"""Called to create the initial HTML string that is loaded on page.
Override this method to provide you own custom HTML.
:Example:
class MyDash(dash.Dash):