Skip to content
This repository was archived by the owner on Jun 3, 2024. It is now read-only.

Download component #926

Merged
merged 26 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f8d59a0
Added Download.react.js component.
emilhe Sep 12, 2020
745c4ad
Added integration tests and send_X utils methods, for now residing in…
emilhe Sep 12, 2020
1c89a6f
Path correction in test_download_file.py, all download related tests …
emilhe Sep 13, 2020
7b60798
Added excel as supported format (not included in tests though)
emilhe Sep 13, 2020
8733b82
Applied black for lint
emilhe Sep 13, 2020
8e193b9
Updated CHANGELOG.md
emilhe Sep 13, 2020
7e1e148
Updated package-lock.json
emilhe Sep 14, 2020
0fbddfc
Updated dev-requirements.txt
emilhe Sep 14, 2020
ca82ed7
Updated dev-requirements.txt
emilhe Sep 14, 2020
ee79812
Updated dev-requirements.txt
emilhe Sep 14, 2020
6866b3d
Update src/components/Download.react.js
emilhe Oct 20, 2020
42f7ab6
* Refactoring of express.py as per alexcjohnson's comments
emilhe Oct 20, 2020
3f0aaa9
Refactoring of express.py, send_string/send_bytes now accepts strings…
emilhe Nov 30, 2020
6098ccf
_data_frame_senders updated
emilhe Nov 30, 2020
3bb719d
Merge branch 'dev' into DownloadComponent
alexcjohnson Feb 1, 2021
d197c76
extend changelog for Download component
alexcjohnson Feb 1, 2021
2c719e2
only import the express functions we want to export
alexcjohnson Feb 1, 2021
af8f95f
add ids to Download tests
alexcjohnson Feb 2, 2021
0e19d3a
try pyarrow<3
alexcjohnson Feb 6, 2021
5b18f41
refactor explicit waits in download tests into until(exists)
alexcjohnson Feb 6, 2021
22c30cc
try to make download tests more robust
alexcjohnson Feb 6, 2021
2d9447f
download helpers Python version compatibility
alexcjohnson Feb 20, 2021
350a4f7
2/3 compatible exception class
alexcjohnson Feb 20, 2021
90aab2b
robustify stcp002
alexcjohnson Feb 20, 2021
37ce1a6
noise - see if percy issues are repeatable
alexcjohnson Feb 20, 2021
73059a3
noise
alexcjohnson Mar 4, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## UNRELEASED

### Added
- [#863](https://github.com/plotly/dash-core-components/pull/863) Adds a new `Download` component. Along with this several utility functions are added to help construct the appropriate data format:
- `dcc.send_file` - send a file from disk
- `dcc.send_data_frame` - send a `DataFrame`, using one of its writer methods
- `dcc.send_bytes` - send a bytestring or the result of a bytestring writer
- `dcc.send_string` - send a string or the result of a string writer

### Changed
- [#923](https://github.com/plotly/dash-core-components/pull/923)
Set autoComplete to off in `dcc.Dropdown`. This fixes [#808](https://github.com/plotly/dash-core-components/issues/808)
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export(dccConfirmDialog)
export(dccConfirmDialogProvider)
export(dccDatePickerRange)
export(dccDatePickerSingle)
export(dccDownload)
export(dccDropdown)
export(dccGraph)
export(dccInput)
Expand Down
204 changes: 110 additions & 94 deletions dash_core_components_base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,122 +6,138 @@
import dash as _dash

_basepath = _os.path.dirname(__file__)
_filepath = _os.path.abspath(_os.path.join(_basepath, 'package-info.json'))
_filepath = _os.path.abspath(_os.path.join(_basepath, "package-info.json"))
with open(_filepath) as f:
package = json.load(f)

package_name = package['name'].replace(' ', '_').replace('-', '_')
__version__ = package['version']
package_name = package["name"].replace(" ", "_").replace("-", "_")
__version__ = package["version"]

# Module imports trigger a dash.development import, need to check this first
if not hasattr(_dash, '__plotly_dash') and not hasattr(_dash, 'development'):
print("Dash was not successfully imported. Make sure you don't have a file "
"named \n'dash.py' in your current directory.", file=_sys.stderr)
print(
"Dash was not successfully imported. Make sure you don't have a file "
"named \n'dash.py' in your current directory.",
file=_sys.stderr,
)
_sys.exit(1)

from ._imports_ import * # noqa: F401, F403, E402
from ._imports_ import __all__ # noqa: E402
from .express import send_bytes, send_data_frame, send_file, send_string # noqa: F401, E402

_current_path = _os.path.dirname(_os.path.abspath(__file__))


_this_module = _sys.modules[__name__]

async_resources = [
'datepicker',
'dropdown',
'graph',
'highlight',
'markdown',
'slider',
'upload'
"datepicker",
"dropdown",
"graph",
"highlight",
"markdown",
"slider",
"upload",
]

_js_dist = []

_js_dist.extend([{
'relative_package_path': 'async-{}.js'.format(async_resource),
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/async-{}.js'
).format(__version__, async_resource),
'namespace': 'dash_core_components',
'async': True
} for async_resource in async_resources])
_js_dist.extend(
[
{
"relative_package_path": "async-{}.js".format(async_resource),
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/async-{}.js"
).format(__version__, async_resource),
"namespace": "dash_core_components",
"async": True,
}
for async_resource in async_resources
]
)

_js_dist.extend([{
'relative_package_path': 'async-{}.js.map'.format(async_resource),
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/async-{}.js.map'
).format(__version__, async_resource),
'namespace': 'dash_core_components',
'dynamic': True
} for async_resource in async_resources])
_js_dist.extend(
[
{
"relative_package_path": "async-{}.js.map".format(async_resource),
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/async-{}.js.map"
).format(__version__, async_resource),
"namespace": "dash_core_components",
"dynamic": True,
}
for async_resource in async_resources
]
)

_js_dist.extend([
{
'relative_package_path': '{}.min.js'.format(__name__),
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/dash_core_components.min.js'
).format(__version__),
'namespace': 'dash_core_components'
},
{
'relative_package_path': '{}.min.js.map'.format(__name__),
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/dash_core_components.min.js.map'
).format(__version__),
'namespace': 'dash_core_components',
'dynamic': True
},
{
'relative_package_path': '{}-shared.js'.format(__name__),
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/dash_core_components-shared.js'
).format(__version__),
'namespace': 'dash_core_components'
},
{
'relative_package_path': '{}-shared.js.map'.format(__name__),
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/dash_core_components-shared.js.map'
).format(__version__),
'namespace': 'dash_core_components',
'dynamic': True
},
{
'relative_package_path': 'plotly.min.js',
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/plotly.min.js'
).format(__version__),
'namespace': 'dash_core_components',
'async': 'eager'
},
{
'relative_package_path': 'async-plotlyjs.js',
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/async-plotlyjs.js'
).format(__version__),
'namespace': 'dash_core_components',
'async': 'lazy'
},
{
'relative_package_path': 'async-plotlyjs.js.map',
'external_url': (
'https://unpkg.com/dash-core-components@{}'
'/dash_core_components/async-plotlyjs.js.map'
).format(__version__),
'namespace': 'dash_core_components',
'dynamic': True
},
])
_js_dist.extend(
[
{
"relative_package_path": "{}.min.js".format(__name__),
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/dash_core_components.min.js"
).format(__version__),
"namespace": "dash_core_components",
},
{
"relative_package_path": "{}.min.js.map".format(__name__),
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/dash_core_components.min.js.map"
).format(__version__),
"namespace": "dash_core_components",
"dynamic": True,
},
{
"relative_package_path": "{}-shared.js".format(__name__),
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/dash_core_components-shared.js"
).format(__version__),
"namespace": "dash_core_components",
},
{
"relative_package_path": "{}-shared.js.map".format(__name__),
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/dash_core_components-shared.js.map"
).format(__version__),
"namespace": "dash_core_components",
"dynamic": True,
},
{
"relative_package_path": "plotly.min.js",
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/plotly.min.js"
).format(__version__),
"namespace": "dash_core_components",
"async": "eager",
},
{
"relative_package_path": "async-plotlyjs.js",
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/async-plotlyjs.js"
).format(__version__),
"namespace": "dash_core_components",
"async": "lazy",
},
{
"relative_package_path": "async-plotlyjs.js.map",
"external_url": (
"https://unpkg.com/dash-core-components@{}"
"/dash_core_components/async-plotlyjs.js.map"
).format(__version__),
"namespace": "dash_core_components",
"dynamic": True,
},
]
)

for _component in __all__:
setattr(locals()[_component], '_js_dist', _js_dist)
setattr(locals()[_component], "_js_dist", _js_dist)
115 changes: 115 additions & 0 deletions dash_core_components_base/express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import io
import ntpath
import base64
from functools import partial

# Py2 StringIO.StringIO handles unicode but io.StringIO doesn't
try:
from StringIO import StringIO as _StringIO
py2 = True
except ImportError:
_StringIO = io.StringIO
py2 = False

# region Utils for Download component


def send_file(path, filename=None, type=None):
"""
Convert a file into the format expected by the Download component.
:param path: path to the file to be sent
:param filename: name of the file, if not provided the original filename is used
:param type: type of the file (optional, passed to Blob in the javascript layer)
:return: dict of file content (base64 encoded) and meta data used by the Download component
"""
# If filename is not set, read it from the path.
if filename is None:
filename = ntpath.basename(path)
# Read the file contents and send it.
with open(path, "rb") as f:
return send_bytes(f.read(), filename, type)


def send_bytes(src, filename, type=None, **kwargs):
"""
Convert data written to BytesIO into the format expected by the Download component.
:param src: array of bytes or a writer that can write to BytesIO
:param filename: the name of the file
:param type: type of the file (optional, passed to Blob in the javascript layer)
:return: dict of data frame content (base64 encoded) and meta data used by the Download component
"""
content = src if isinstance(src, bytes) else _io_to_str(io.BytesIO(), src, **kwargs)
return dict(
content=base64.b64encode(content).decode(),
filename=filename,
type=type,
base64=True,
)


def send_string(src, filename, type=None, **kwargs):
"""
Convert data written to StringIO into the format expected by the Download component.
:param src: a string or a writer that can write to StringIO
:param filename: the name of the file
:param type: type of the file (optional, passed to Blob in the javascript layer)
:return: dict of data frame content (NOT base64 encoded) and meta data used by the Download component
"""
content = src if isinstance(src, str) else _io_to_str(_StringIO(), src, **kwargs)
return dict(content=content, filename=filename, type=type, base64=False)


def _io_to_str(data_io, writer, **kwargs):
# Some pandas writers try to close the IO, we do not want that.
data_io_close = data_io.close
data_io.close = lambda: None
# Write data content.
writer(data_io, **kwargs)
data_value = data_io.getvalue()
data_io_close()
return data_value


def send_data_frame(writer, filename, type=None, **kwargs):
"""
Convert data frame into the format expected by the Download component.
:param writer: a data frame writer
:param filename: the name of the file
:param type: type of the file (optional, passed to Blob in the javascript layer)
:return: dict of data frame content (base64 encoded) and meta data used by the Download component

Examples
--------

>>> df = pd.DataFrame({'a': [1, 2, 3, 4], 'b': [2, 1, 5, 6], 'c': ['x', 'x', 'y', 'y']})
...
>>> send_data_frame(df.to_csv, "mydf.csv") # download as csv
>>> send_data_frame(df.to_json, "mydf.json") # download as json
>>> send_data_frame(df.to_excel, "mydf.xls", index=False) # download as excel
>>> send_data_frame(df.to_pickle, "mydf.pkl") # download as pickle

"""
name = writer.__name__
# Check if the provided writer is known.
if name not in _data_frame_senders.keys():
raise ValueError(
"The provided writer ({}) is not supported, "
"try calling send_string or send_bytes directly.".format(name)
)
# Send data frame using the appropriate send function.
return _data_frame_senders[name](writer, filename, type, **kwargs)


_data_frame_senders = {
"to_csv": send_string,
"to_json": send_string,
"to_html": send_string,
"to_excel": send_bytes,
"to_feather": send_bytes,
"to_parquet": send_bytes,
"to_msgpack": send_bytes,
"to_stata": send_bytes,
"to_pickle": partial(send_bytes, compression=None) if py2 else send_bytes,
}

# endregion
3 changes: 3 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
numpy
pandas
pyarrow<3;python_version<"3.7"
pyarrow;python_version>="3.7"
xlrd<2
mimesis;python_version>="3.6"
virtualenv;python_version=="2.7"
Loading