1
1
from __future__ import print_function
2
2
3
+ import os
3
4
import sys
4
5
import collections
5
6
import importlib
6
7
import json
7
8
import pkgutil
8
9
import warnings
10
+ import re
11
+
9
12
from functools import wraps
10
13
11
14
import plotly
19
22
from .development .base_component import Component
20
23
from . import exceptions
21
24
from ._utils import AttributeDict as _AttributeDict
25
+ from ._utils import interpolate_str as _interpolate
26
+
27
+ _default_index = '''
28
+ <!DOCTYPE html>
29
+ <html>
30
+ <head>
31
+ {%metas%}
32
+ <title>{%title%}</title>
33
+ {%favicon%}
34
+ {%css%}
35
+ </head>
36
+ <body>
37
+ {%app_entry%}
38
+ <footer>
39
+ {%config%}
40
+ {%scripts%}
41
+ </footer>
42
+ </body>
43
+ </html>
44
+ '''
45
+
46
+ _app_entry = '''
47
+ <div id="react-entry-point">
48
+ <div class="_dash-loading">
49
+ Loading...
50
+ </div>
51
+ </div>
52
+ '''
53
+
54
+ _re_index_entry = re .compile (r'{%app_entry%}' )
55
+ _re_index_config = re .compile (r'{%config%}' )
56
+ _re_index_scripts = re .compile (r'{%scripts%}' )
57
+
58
+ _re_index_entry_id = re .compile (r'id="react-entry-point"' )
59
+ _re_index_config_id = re .compile (r'id="_dash-config"' )
60
+ _re_index_scripts_id = re .compile (r'src=".*dash[-_]renderer.*"' )
22
61
23
62
24
63
# pylint: disable=too-many-instance-attributes
@@ -29,8 +68,13 @@ def __init__(
29
68
name = '__main__' ,
30
69
server = None ,
31
70
static_folder = 'static' ,
71
+ assets_folder = None ,
72
+ assets_url_path = '/assets' ,
73
+ include_assets_files = True ,
32
74
url_base_pathname = '/' ,
33
75
compress = True ,
76
+ meta_tags = None ,
77
+ index_string = _default_index ,
34
78
** kwargs ):
35
79
36
80
# pylint-disable: too-many-instance-attributes
@@ -42,20 +86,35 @@ def __init__(
42
86
See https://github.com/plotly/dash/issues/141 for details.
43
87
''' , DeprecationWarning )
44
88
45
- name = name or 'dash'
89
+ self ._assets_folder = assets_folder or os .path .join (
90
+ flask .helpers .get_root_path (name ), 'assets'
91
+ )
92
+
46
93
# allow users to supply their own flask server
47
94
self .server = server or Flask (name , static_folder = static_folder )
48
95
96
+ self .server .register_blueprint (
97
+ flask .Blueprint ('assets' , 'assets' ,
98
+ static_folder = self ._assets_folder ,
99
+ static_url_path = assets_url_path ))
100
+
49
101
self .url_base_pathname = url_base_pathname
50
102
self .config = _AttributeDict ({
51
103
'suppress_callback_exceptions' : False ,
52
104
'routes_pathname_prefix' : url_base_pathname ,
53
- 'requests_pathname_prefix' : url_base_pathname
105
+ 'requests_pathname_prefix' : url_base_pathname ,
106
+ 'include_assets_files' : include_assets_files ,
107
+ 'assets_external_path' : '' ,
54
108
})
55
109
56
110
# list of dependencies
57
111
self .callback_map = {}
58
112
113
+ self ._index_string = ''
114
+ self .index_string = index_string
115
+ self ._meta_tags = meta_tags or []
116
+ self ._favicon = None
117
+
59
118
if compress :
60
119
# gzip
61
120
Compress (self .server )
@@ -149,12 +208,26 @@ def layout(self, value):
149
208
# pylint: disable=protected-access
150
209
self .css ._update_layout (layout_value )
151
210
self .scripts ._update_layout (layout_value )
152
- self ._collect_and_register_resources (
153
- self .scripts .get_all_scripts ()
154
- )
155
- self ._collect_and_register_resources (
156
- self .css .get_all_css ()
211
+
212
+ @property
213
+ def index_string (self ):
214
+ return self ._index_string
215
+
216
+ @index_string .setter
217
+ def index_string (self , value ):
218
+ checks = (
219
+ (_re_index_entry .search (value ), 'app_entry' ),
220
+ (_re_index_config .search (value ), 'config' ,),
221
+ (_re_index_scripts .search (value ), 'scripts' ),
157
222
)
223
+ missing = [missing for check , missing in checks if not check ]
224
+ if missing :
225
+ raise Exception (
226
+ 'Did you forget to include {} in your index string ?' .format (
227
+ ', ' .join ('{%' + x + '%}' for x in missing )
228
+ )
229
+ )
230
+ self ._index_string = value
158
231
159
232
def serve_layout (self ):
160
233
layout = self ._layout_value ()
@@ -180,6 +253,7 @@ def serve_routes(self):
180
253
)
181
254
182
255
def _collect_and_register_resources (self , resources ):
256
+ # now needs the app context.
183
257
# template in the necessary component suite JS bundles
184
258
# add the version number of the package as a query parameter
185
259
# for cache busting
@@ -217,8 +291,12 @@ def _relative_url_path(relative_package_path='', namespace=''):
217
291
srcs .append (url )
218
292
elif 'absolute_path' in resource :
219
293
raise Exception (
220
- 'Serving files form absolute_path isn\' t supported yet'
294
+ 'Serving files from absolute_path isn\' t supported yet'
221
295
)
296
+ elif 'asset_path' in resource :
297
+ static_url = flask .url_for ('assets.static' ,
298
+ filename = resource ['asset_path' ])
299
+ srcs .append (static_url )
222
300
return srcs
223
301
224
302
def _generate_css_dist_html (self ):
@@ -260,6 +338,20 @@ def _generate_config_html(self):
260
338
'</script>'
261
339
).format (json .dumps (self ._config ()))
262
340
341
+ def _generate_meta_html (self ):
342
+ has_charset = any ('charset' in x for x in self ._meta_tags )
343
+
344
+ tags = []
345
+ if not has_charset :
346
+ tags .append ('<meta charset="UTF-8"/>' )
347
+ for meta in self ._meta_tags :
348
+ attributes = []
349
+ for k , v in meta .items ():
350
+ attributes .append ('{}="{}"' .format (k , v ))
351
+ tags .append ('<meta {} />' .format (' ' .join (attributes )))
352
+
353
+ return '\n ' .join (tags )
354
+
263
355
# Serve the JS bundles for each package
264
356
def serve_component_suites (self , package_name , path_in_package_dist ):
265
357
if package_name not in self .registered_paths :
@@ -294,28 +386,83 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument
294
386
scripts = self ._generate_scripts_html ()
295
387
css = self ._generate_css_dist_html ()
296
388
config = self ._generate_config_html ()
389
+ metas = self ._generate_meta_html ()
297
390
title = getattr (self , 'title' , 'Dash' )
298
- return '''
299
- <!DOCTYPE html>
300
- <html>
301
- <head>
302
- <meta charset="UTF-8">
303
- <title>{}</title>
304
- {}
305
- </head>
306
- <body>
307
- <div id="react-entry-point">
308
- <div class="_dash-loading">
309
- Loading...
310
- </div>
311
- </div>
312
- <footer>
313
- {}
314
- {}
315
- </footer>
316
- </body>
317
- </html>
318
- ''' .format (title , css , config , scripts )
391
+ if self ._favicon :
392
+ favicon = '<link rel="icon" type="image/x-icon" href="{}">' .format (
393
+ flask .url_for ('assets.static' , filename = self ._favicon ))
394
+ else :
395
+ favicon = ''
396
+
397
+ index = self .interpolate_index (
398
+ metas = metas , title = title , css = css , config = config ,
399
+ scripts = scripts , app_entry = _app_entry , favicon = favicon )
400
+
401
+ checks = (
402
+ (_re_index_entry_id .search (index ), '#react-entry-point' ),
403
+ (_re_index_config_id .search (index ), '#_dash-configs' ),
404
+ (_re_index_scripts_id .search (index ), 'dash-renderer' ),
405
+ )
406
+ missing = [missing for check , missing in checks if not check ]
407
+
408
+ if missing :
409
+ plural = 's' if len (missing ) > 1 else ''
410
+ raise Exception (
411
+ 'Missing element{pl} {ids} in index.' .format (
412
+ ids = ', ' .join (missing ),
413
+ pl = plural
414
+ )
415
+ )
416
+
417
+ return index
418
+
419
+ def interpolate_index (self ,
420
+ metas = '' , title = '' , css = '' , config = '' ,
421
+ scripts = '' , app_entry = '' , favicon = '' ):
422
+ """
423
+ Called to create the initial HTML string that is loaded on page.
424
+ Override this method to provide you own custom HTML.
425
+
426
+ :Example:
427
+
428
+ class MyDash(dash.Dash):
429
+ def interpolate_index(self, **kwargs):
430
+ return '''
431
+ <!DOCTYPE html>
432
+ <html>
433
+ <head>
434
+ <title>My App</title>
435
+ </head>
436
+ <body>
437
+ <div id="custom-header">My custom header</div>
438
+ {app_entry}
439
+ {config}
440
+ {scripts}
441
+ <div id="custom-footer">My custom footer</div>
442
+ </body>
443
+ </html>
444
+ '''.format(
445
+ app_entry=kwargs.get('app_entry'),
446
+ config=kwargs.get('config'),
447
+ scripts=kwargs.get('scripts'))
448
+
449
+ :param metas: Collected & formatted meta tags.
450
+ :param title: The title of the app.
451
+ :param css: Collected & formatted css dependencies as <link> tags.
452
+ :param config: Configs needed by dash-renderer.
453
+ :param scripts: Collected & formatted scripts tags.
454
+ :param app_entry: Where the app will render.
455
+ :param favicon: A favicon <link> tag if found in assets folder.
456
+ :return: The interpolated HTML string for the index.
457
+ """
458
+ return _interpolate (self .index_string ,
459
+ metas = metas ,
460
+ title = title ,
461
+ css = css ,
462
+ config = config ,
463
+ scripts = scripts ,
464
+ favicon = favicon ,
465
+ app_entry = app_entry )
319
466
320
467
def dependencies (self ):
321
468
return flask .jsonify ([
@@ -558,6 +705,9 @@ def dispatch(self):
558
705
return self .callback_map [target_id ]['callback' ](* args )
559
706
560
707
def _setup_server (self ):
708
+ if self .config .include_assets_files :
709
+ self ._walk_assets_directory ()
710
+
561
711
# Make sure `layout` is set before running the server
562
712
value = getattr (self , 'layout' )
563
713
if value is None :
@@ -567,9 +717,45 @@ def _setup_server(self):
567
717
'at the time that `run_server` was called. '
568
718
'Make sure to set the `layout` attribute of your application '
569
719
'before running the server.' )
720
+
570
721
self ._generate_scripts_html ()
571
722
self ._generate_css_dist_html ()
572
723
724
+ def _walk_assets_directory (self ):
725
+ walk_dir = self ._assets_folder
726
+ slash_splitter = re .compile (r'[\\/]+' )
727
+
728
+ def add_resource (p ):
729
+ res = {'asset_path' : p }
730
+ if self .config .assets_external_path :
731
+ res ['external_url' ] = '{}{}' .format (
732
+ self .config .assets_external_path , path )
733
+ return res
734
+
735
+ for current , _ , files in os .walk (walk_dir ):
736
+ if current == walk_dir :
737
+ base = ''
738
+ else :
739
+ s = current .replace (walk_dir , '' ).lstrip ('\\ ' ).lstrip ('/' )
740
+ splitted = slash_splitter .split (s )
741
+ if len (splitted ) > 1 :
742
+ base = '/' .join (slash_splitter .split (s ))
743
+ else :
744
+ base = splitted [0 ]
745
+
746
+ for f in sorted (files ):
747
+ if base :
748
+ path = '/' .join ([base , f ])
749
+ else :
750
+ path = f
751
+
752
+ if f .endswith ('js' ):
753
+ self .scripts .append_script (add_resource (path ))
754
+ elif f .endswith ('css' ):
755
+ self .css .append_css (add_resource (path ))
756
+ elif f == 'favicon.ico' :
757
+ self ._favicon = path
758
+
573
759
def run_server (self ,
574
760
port = 8050 ,
575
761
debug = False ,
0 commit comments