-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathwebviz_assets.py
129 lines (100 loc) · 4.67 KB
/
webviz_assets.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
import re
import shutil
import pathlib
from typing import Optional
from tqdm import tqdm
from dash import Dash
import flask
class WebvizAssets:
"""Dash applications by default host static resources from a folder called
./assets, relative to the root application folder.
In order to facilitate hot reloading, and fast building of Webviz
applications from the configuration file, this class facilitates handling
of static assets.
Individual plugins can add assets to a common instance of WebvizAssets
by calling the .add(filename) function. This adds the resource, and
at the same time returns the resource URI which the plugin can use.
If the webviz instance is in non-portable mode, the Flask/Dash application
is routed to the actual location of the files, making hot reload and
testing fast.
When creating a portable webviz instance however, the files are copied
over the ./resources/assets folder, and normal Dash usage applies.
In both portable and non-portable mode, WebvizAssets makes sure there are
no name conflicts (i.e. it supports multiple assets on different paths,
but with same filename) and also assignes URI friendly resource IDs.
"""
def __init__(self) -> None:
self._assets: dict = {}
self._portable = False
@property
def portable(self) -> bool:
return self._portable
@portable.setter
def portable(self, portable: bool) -> None:
self._portable = portable
def _base_folder(self) -> str:
return "assets" if self.portable else "temp"
def add(self, filename: pathlib.Path) -> str:
"""Calling this function makes the filename given as input
available as a hosted asset when the application is running.
The returned string is a URI which the plugin optionally
can use internally (e.g. as "src" in image elements).
Calling this function with the same input path
multiple times will return the same URI.
Filenames added to WebvizAssets that ends with .css or .js
are loaded automatically in the browser by Dash,
both in non-portable and portable mode.
"""
path = pathlib.Path(filename)
if filename not in self._assets.values():
assigned_id = self._generate_id(path.name)
self._assets[assigned_id] = filename
else:
assigned_id = {v: k for k, v in self._assets.items()}[filename]
return str(pathlib.Path(self._base_folder()) / assigned_id)
def directly_host_assets(self, app: Dash) -> None:
"""In non-portable mode, this function can be called by the
application. It routes the Dash application to the added assets on
disk, making hot reloading and more interactive development of the
application possible.
"""
if self._portable:
raise RuntimeError(
"The function WebvizAssets.directly_host_assets() "
"method is only meaningful in a non-portable settings."
)
@app.server.route(f"/{self._base_folder()}/<path:asset_id>")
def _send_file(asset_id: str) -> Optional[flask.wrappers.Response]:
if asset_id in self._assets: # Only serve white listed resources
path = pathlib.Path(self._assets[asset_id])
return flask.send_from_directory(str(path.parent), path.name)
return None
# Add .css and .js files to auto-loaded Dash assets
for asset_id, asset_path in self._assets.items():
if asset_path.suffix == ".css":
app.config.external_stylesheets.append(
f"./{self._base_folder()}/{asset_id}"
)
elif asset_path.suffix == ".js":
app.config.external_scripts.append(
f"./{self._base_folder()}/{asset_id}"
)
def make_portable(self, asset_folder: pathlib.Path) -> None:
"""Copy over all added assets to the given folder (asset_folder)."""
for assigned_id, filename in tqdm(
self._assets.items(),
bar_format="{l_bar} {bar} | Copied {n_fmt}/{total_fmt}",
):
tqdm.write(f"Copying over {filename}")
shutil.copyfile(filename, asset_folder / assigned_id)
def _generate_id(self, filename: str) -> str:
"""From the filename, create a safe resource id not already present"""
asset_id = base_id = re.sub(
"[^-a-z0-9._]+", "", filename.lower().replace(" ", "_")
)
count = 1
while asset_id in self._assets:
count += 1
asset_id = f"{base_id}{count}"
return asset_id
WEBVIZ_ASSETS = WebvizAssets()