Skip to content

Commit

Permalink
Support user download changes in webviz-core-components
Browse files Browse the repository at this point in the history
  • Loading branch information
DanSava committed Sep 18, 2020
1 parent 1e5b676 commit f692a6f
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 28 deletions.
35 changes: 27 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ a button toolbar. The default buttons to appear is stored in the class constant
`WebvizPluginABC.TOOLBAR_BUTTONS`. If you want to override which buttons should
appear, redefine this class constant in your subclass. To remove all buttons,
simply define it as an empty list. See [this section](#data-download-callback)
for more information regarding the `data_download` button.
for more information regarding downloading plugin data.

### Callbacks

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

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

```python
@app.callback(self.plugin_data_output,
[self.plugin_data_requested])
self.plugin_data_requested)
def _user_download_data(data_requested):
return WebvizPluginABC.plugin_data_compress(
[{'filename': 'some_file.txt',
return WebvizPluginABC.plugin_compressed_data(
file_name="webviz-data.zip",
content=[{'filename': 'some_file.txt',
'content': 'Some download data'}]
) if data_requested else ''
) if data_requested else {}
```

A typical raw CSV data download will look like:
```python
@app.callback(self.plugin_data_output,
self.plugin_data_requested)
def _user_download_data(data_requested):
if data_requested:
byte_io = io.BytesIO()
byte_io.write(get_data(self.csv_file).to_csv().encode('ascii'))
byte_io.seek(0)
return {
"file_name": "file_name.csv",
"content": base64.b64encode(byte_io.read()).decode("ascii"),
"mime_type": "text/csv"
}
return {}
```

By letting the plugin define the callback, the plugin author is able
to utilize the whole callback machinery, including e.g. state of the individual
components in the plugin. This way the data downloaded can e.g. depend on
Expand All @@ -170,8 +189,8 @@ The attributes `self.plugin_data_output` and `self.plugin_data_requested`
are Dash `Output` and `Input` instances respectively, and are provided by
the base class `WebvizPluginABC` (i.e. include them as shown here).

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

### User provided arguments
Expand Down
44 changes: 35 additions & 9 deletions webviz_config/_plugin_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,32 @@
import abc
import base64
import zipfile
import warnings

from uuid import uuid4
from typing import List, Optional, Type, Union

try:
# Python 3.8+
# pylint: disable=ungrouped-imports
from typing import TypedDict # type: ignore
except (ImportError, ModuleNotFoundError):
# Python < 3.8
from typing_extensions import TypedDict # type: ignore


import bleach
from dash.development.base_component import Component
from dash.dependencies import Input, Output
import webviz_core_components as wcc


class FileDefinition(TypedDict, total=False):
file_name: str
content: str
mime_type: str


class WebvizPluginABC(abc.ABC):
"""All webviz plugins need to subclass this abstract base class,
e.g.
Expand Down Expand Up @@ -40,7 +57,6 @@ def layout(self):
TOOLBAR_BUTTONS = [
"screenshot",
"expand",
"download_zip",
"contact_person",
"guided_tour",
]
Expand All @@ -62,6 +78,7 @@ def __init__(self, screenshot_filename: str = "webviz-screenshot.png") -> None:

self._plugin_uuid = uuid4()
self._screenshot_filename = screenshot_filename
self._add_download_button = False

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

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

@property
def plugin_data_requested(self) -> Input:
Expand All @@ -110,16 +125,27 @@ def _reformat_tour_steps(steps: List[dict]) -> List[dict]:
]

@staticmethod
def plugin_data_compress(content: List[dict]) -> str:
def plugin_compressed_data(file_name: str, content: List[FileDefinition]) -> FileDefinition:
byte_io = io.BytesIO()

with zipfile.ZipFile(byte_io, "w") as zipped_data:
for data in content:
zipped_data.writestr(data["filename"], data["content"])
zipped_data.writestr(data["file_name"], data["content"])

byte_io.seek(0)
return {
"file_name": file_name,
"content": base64.b64encode(byte_io.read()).decode("ascii"),
"mime_type": "application/zip"
}

return base64.b64encode(byte_io.read()).decode("ascii")
@staticmethod
def plugin_data_compress(content: List[dict]) -> FileDefinition:
warnings.warn(
"Use 'plugin_compressed_data' instead of 'plugin_data_compress'",
DeprecationWarning,
)
return WebvizPluginABC.plugin_compressed_data("webviz-data.zip", content)

def plugin_layout(
self, contact_person: Optional[dict] = None
Expand All @@ -145,8 +171,8 @@ def plugin_layout(
for key in contact_person:
contact_person[key] = bleach.clean(str(contact_person[key]))

if "download_zip" in buttons and not hasattr(self, "_add_download_button"):
buttons.remove("download_zip")
if self._add_download_button:
buttons.append("download")

if buttons:
# pylint: disable=no-member
Expand Down
13 changes: 8 additions & 5 deletions webviz_config/plugins/_example_data_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ def layout(self) -> html.H1:
return html.H1(self.title)

def set_callbacks(self, app: Dash) -> None:
@app.callback(self.plugin_data_output, [self.plugin_data_requested])
def _user_download_data(data_requested: bool) -> str:
@app.callback(self.plugin_data_output, self.plugin_data_requested)
def _user_download_data(data_requested: bool) -> dict:
return (
WebvizPluginABC.plugin_data_compress(
[{"filename": "some_file.txt", "content": "Some download data"}]
WebvizPluginABC.plugin_compressed_data(
file_name="webviz-data.zip",
content=[
{"filename": "some_file.txt", "content": "Some download data"}
],
)
if data_requested
else ""
else {}
)
13 changes: 7 additions & 6 deletions webviz_config/plugins/_table_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,19 +356,20 @@ def plot_input_callbacks(self) -> List[Input]:
return inputs

def set_callbacks(self, app: Dash) -> None:
@app.callback(self.plugin_data_output, [self.plugin_data_requested])
def _user_download_data(data_requested: Optional[int]) -> str:
@app.callback(self.plugin_data_output, self.plugin_data_requested)
def _user_download_data(data_requested: Optional[int]) -> dict:
return (
WebvizPluginABC.plugin_data_compress(
[
WebvizPluginABC.plugin_compressed_data(
file_name="webviz-data.zip",
content=[
{
"filename": "table_plotter.csv",
"content": get_data(self.csv_file).to_csv(),
}
]
],
)
if data_requested
else ""
else {}
)

@app.callback(self.plot_output_callbacks, self.plot_input_callbacks)
Expand Down

0 comments on commit f692a6f

Please sign in to comment.