Skip to content

Commit 342f640

Browse files
committed
Support user download changes in webviz-core-components
1 parent 0ec2f4d commit 342f640

File tree

4 files changed

+79
-28
lines changed

4 files changed

+79
-28
lines changed

CONTRIBUTING.md

+27-8
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ a button toolbar. The default buttons to appear is stored in the class constant
8383
`WebvizPluginABC.TOOLBAR_BUTTONS`. If you want to override which buttons should
8484
appear, redefine this class constant in your subclass. To remove all buttons,
8585
simply define it as an empty list. See [this section](#data-download-callback)
86-
for more information regarding the `data_download` button.
86+
for more information regarding downloading plugin data.
8787

8888
### Callbacks
8989

@@ -150,17 +150,36 @@ There are three fundamental additions to the minimal example without callbacks:
150150

151151
There is a [data download button](#override-plugin-toolbar) provided by
152152
the `WebvizPluginABC` class. However, it will only appear if the corresponding
153-
callback is set. A typical data download callback will look like
153+
callback is set. A typical compressed data download callback will look like
154154

155155
```python
156156
@app.callback(self.plugin_data_output,
157-
[self.plugin_data_requested])
157+
self.plugin_data_requested)
158158
def _user_download_data(data_requested):
159-
return WebvizPluginABC.plugin_data_compress(
160-
[{'filename': 'some_file.txt',
159+
return WebvizPluginABC.plugin_compressed_data(
160+
file_name="webviz-data.zip",
161+
content=[{'filename': 'some_file.txt',
161162
'content': 'Some download data'}]
162-
) if data_requested else ''
163+
) if data_requested else {}
163164
```
165+
166+
A typical raw CSV data download will look like:
167+
```python
168+
@app.callback(self.plugin_data_output,
169+
self.plugin_data_requested)
170+
def _user_download_data(data_requested):
171+
if data_requested:
172+
byte_io = io.BytesIO()
173+
byte_io.write(get_data(self.csv_file).to_csv().encode('ascii'))
174+
byte_io.seek(0)
175+
return {
176+
"file_name": "file_name.csv",
177+
"content": base64.b64encode(byte_io.read()).decode("ascii"),
178+
"mime_type": "text/csv"
179+
}
180+
return {}
181+
```
182+
164183
By letting the plugin define the callback, the plugin author is able
165184
to utilize the whole callback machinery, including e.g. state of the individual
166185
components in the plugin. This way the data downloaded can e.g. depend on
@@ -170,8 +189,8 @@ The attributes `self.plugin_data_output` and `self.plugin_data_requested`
170189
are Dash `Output` and `Input` instances respectively, and are provided by
171190
the base class `WebvizPluginABC` (i.e. include them as shown here).
172191

173-
The function `WebvizPluginABC.plugin_data_compress` is a utility function
174-
which takes a list of dictionaries, giving filenames and corresponding data,
192+
The function `WebvizPluginABC.plugin_compressed_data` is a utility function
193+
which takes a file name and a list of dictionaries, containing file names and corresponding data,
175194
and compresses them to a zip archive which is then downloaded by the user.
176195

177196
### User provided arguments

webviz_config/_plugin_abc.py

+37-9
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,32 @@
22
import abc
33
import base64
44
import zipfile
5+
import warnings
6+
57
from uuid import uuid4
68
from typing import List, Optional, Type, Union
79

10+
try:
11+
# Python 3.8+
12+
# pylint: disable=ungrouped-imports
13+
from typing import TypedDict # type: ignore
14+
except (ImportError, ModuleNotFoundError):
15+
# Python < 3.8
16+
from typing_extensions import TypedDict # type: ignore
17+
18+
819
import bleach
920
from dash.development.base_component import Component
1021
from dash.dependencies import Input, Output
1122
import webviz_core_components as wcc
1223

1324

25+
class FileDefinition(TypedDict, total=False):
26+
file_name: str
27+
content: str
28+
mime_type: str
29+
30+
1431
class WebvizPluginABC(abc.ABC):
1532
"""All webviz plugins need to subclass this abstract base class,
1633
e.g.
@@ -40,7 +57,6 @@ def layout(self):
4057
TOOLBAR_BUTTONS = [
4158
"screenshot",
4259
"expand",
43-
"download_zip",
4460
"contact_person",
4561
"guided_tour",
4662
]
@@ -62,6 +78,7 @@ def __init__(self, screenshot_filename: str = "webviz-screenshot.png") -> None:
6278

6379
self._plugin_uuid = uuid4()
6480
self._screenshot_filename = screenshot_filename
81+
self._add_download_button = False
6582

6683
def uuid(self, element: str) -> str:
6784
"""Typically used to get a unique ID for some given element/component in
@@ -94,10 +111,8 @@ def _plugin_wrapper_id(self) -> str:
94111

95112
@property
96113
def plugin_data_output(self) -> Output:
97-
# pylint: disable=attribute-defined-outside-init
98-
# We do not have a __init__ method in this abstract base class
99114
self._add_download_button = True
100-
return Output(self._plugin_wrapper_id, "zip_base64")
115+
return Output(self._plugin_wrapper_id, "download")
101116

102117
@property
103118
def plugin_data_requested(self) -> Input:
@@ -110,16 +125,29 @@ def _reformat_tour_steps(steps: List[dict]) -> List[dict]:
110125
]
111126

112127
@staticmethod
113-
def plugin_data_compress(content: List[dict]) -> str:
128+
def plugin_compressed_data(
129+
file_name: str, content: List[FileDefinition]
130+
) -> FileDefinition:
114131
byte_io = io.BytesIO()
115132

116133
with zipfile.ZipFile(byte_io, "w") as zipped_data:
117134
for data in content:
118-
zipped_data.writestr(data["filename"], data["content"])
135+
zipped_data.writestr(data["file_name"], data["content"])
119136

120137
byte_io.seek(0)
138+
return {
139+
"file_name": file_name,
140+
"content": base64.b64encode(byte_io.read()).decode("ascii"),
141+
"mime_type": "application/zip",
142+
}
121143

122-
return base64.b64encode(byte_io.read()).decode("ascii")
144+
@staticmethod
145+
def plugin_data_compress(content: List[dict]) -> FileDefinition:
146+
warnings.warn(
147+
"Use 'plugin_compressed_data' instead of 'plugin_data_compress'",
148+
DeprecationWarning,
149+
)
150+
return WebvizPluginABC.plugin_compressed_data("webviz-data.zip", content)
123151

124152
def plugin_layout(
125153
self, contact_person: Optional[dict] = None
@@ -145,8 +173,8 @@ def plugin_layout(
145173
for key in contact_person:
146174
contact_person[key] = bleach.clean(str(contact_person[key]))
147175

148-
if "download_zip" in buttons and not hasattr(self, "_add_download_button"):
149-
buttons.remove("download_zip")
176+
if self._add_download_button:
177+
buttons.append("download")
150178

151179
if buttons:
152180
# pylint: disable=no-member

webviz_config/plugins/_example_data_download.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ def layout(self) -> html.H1:
1616
return html.H1(self.title)
1717

1818
def set_callbacks(self, app: Dash) -> None:
19-
@app.callback(self.plugin_data_output, [self.plugin_data_requested])
20-
def _user_download_data(data_requested: bool) -> str:
19+
@app.callback(self.plugin_data_output, self.plugin_data_requested)
20+
def _user_download_data(data_requested: bool) -> dict:
2121
return (
22-
WebvizPluginABC.plugin_data_compress(
23-
[{"filename": "some_file.txt", "content": "Some download data"}]
22+
WebvizPluginABC.plugin_compressed_data(
23+
file_name="webviz-data.zip",
24+
content=[
25+
{"filename": "some_file.txt", "content": "Some download data"}
26+
],
2427
)
2528
if data_requested
26-
else ""
29+
else {}
2730
)

webviz_config/plugins/_table_plotter.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -356,19 +356,20 @@ def plot_input_callbacks(self) -> List[Input]:
356356
return inputs
357357

358358
def set_callbacks(self, app: Dash) -> None:
359-
@app.callback(self.plugin_data_output, [self.plugin_data_requested])
360-
def _user_download_data(data_requested: Optional[int]) -> str:
359+
@app.callback(self.plugin_data_output, self.plugin_data_requested)
360+
def _user_download_data(data_requested: Optional[int]) -> dict:
361361
return (
362-
WebvizPluginABC.plugin_data_compress(
363-
[
362+
WebvizPluginABC.plugin_compressed_data(
363+
file_name="webviz-data.zip",
364+
content=[
364365
{
365366
"filename": "table_plotter.csv",
366367
"content": get_data(self.csv_file).to_csv(),
367368
}
368-
]
369+
],
369370
)
370371
if data_requested
371-
else ""
372+
else {}
372373
)
373374

374375
@app.callback(self.plot_output_callbacks, self.plot_input_callbacks)

0 commit comments

Comments
 (0)