Skip to content

Commit f3297c6

Browse files
committed
Merge pull request #926 from plotly/DownloadComponent
Download component
1 parent 2b95f31 commit f3297c6

File tree

15 files changed

+459
-105
lines changed

15 files changed

+459
-105
lines changed

packages/dash-core-components/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## UNRELEASED
66

7+
### Added
8+
- [#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:
9+
- `dcc.send_file` - send a file from disk
10+
- `dcc.send_data_frame` - send a `DataFrame`, using one of its writer methods
11+
- `dcc.send_bytes` - send a bytestring or the result of a bytestring writer
12+
- `dcc.send_string` - send a string or the result of a string writer
13+
714
### Changed
815
- [#923](https://github.com/plotly/dash-core-components/pull/923)
916
Set autoComplete to off in `dcc.Dropdown`. This fixes [#808](https://github.com/plotly/dash-core-components/issues/808)

packages/dash-core-components/NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export(dccConfirmDialog)
55
export(dccConfirmDialogProvider)
66
export(dccDatePickerRange)
77
export(dccDatePickerSingle)
8+
export(dccDownload)
89
export(dccDropdown)
910
export(dccGraph)
1011
export(dccInput)

packages/dash-core-components/dash_core_components_base/__init__.py

+110-94
Original file line numberDiff line numberDiff line change
@@ -6,122 +6,138 @@
66
import dash as _dash
77

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

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

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

2225
from ._imports_ import * # noqa: F401, F403, E402
2326
from ._imports_ import __all__ # noqa: E402
27+
from .express import send_bytes, send_data_frame, send_file, send_string # noqa: F401, E402
2428

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

2731

2832
_this_module = _sys.modules[__name__]
2933

3034
async_resources = [
31-
'datepicker',
32-
'dropdown',
33-
'graph',
34-
'highlight',
35-
'markdown',
36-
'slider',
37-
'upload'
35+
"datepicker",
36+
"dropdown",
37+
"graph",
38+
"highlight",
39+
"markdown",
40+
"slider",
41+
"upload",
3842
]
3943

4044
_js_dist = []
4145

42-
_js_dist.extend([{
43-
'relative_package_path': 'async-{}.js'.format(async_resource),
44-
'external_url': (
45-
'https://unpkg.com/dash-core-components@{}'
46-
'/dash_core_components/async-{}.js'
47-
).format(__version__, async_resource),
48-
'namespace': 'dash_core_components',
49-
'async': True
50-
} for async_resource in async_resources])
46+
_js_dist.extend(
47+
[
48+
{
49+
"relative_package_path": "async-{}.js".format(async_resource),
50+
"external_url": (
51+
"https://unpkg.com/dash-core-components@{}"
52+
"/dash_core_components/async-{}.js"
53+
).format(__version__, async_resource),
54+
"namespace": "dash_core_components",
55+
"async": True,
56+
}
57+
for async_resource in async_resources
58+
]
59+
)
5160

52-
_js_dist.extend([{
53-
'relative_package_path': 'async-{}.js.map'.format(async_resource),
54-
'external_url': (
55-
'https://unpkg.com/dash-core-components@{}'
56-
'/dash_core_components/async-{}.js.map'
57-
).format(__version__, async_resource),
58-
'namespace': 'dash_core_components',
59-
'dynamic': True
60-
} for async_resource in async_resources])
61+
_js_dist.extend(
62+
[
63+
{
64+
"relative_package_path": "async-{}.js.map".format(async_resource),
65+
"external_url": (
66+
"https://unpkg.com/dash-core-components@{}"
67+
"/dash_core_components/async-{}.js.map"
68+
).format(__version__, async_resource),
69+
"namespace": "dash_core_components",
70+
"dynamic": True,
71+
}
72+
for async_resource in async_resources
73+
]
74+
)
6175

62-
_js_dist.extend([
63-
{
64-
'relative_package_path': '{}.min.js'.format(__name__),
65-
'external_url': (
66-
'https://unpkg.com/dash-core-components@{}'
67-
'/dash_core_components/dash_core_components.min.js'
68-
).format(__version__),
69-
'namespace': 'dash_core_components'
70-
},
71-
{
72-
'relative_package_path': '{}.min.js.map'.format(__name__),
73-
'external_url': (
74-
'https://unpkg.com/dash-core-components@{}'
75-
'/dash_core_components/dash_core_components.min.js.map'
76-
).format(__version__),
77-
'namespace': 'dash_core_components',
78-
'dynamic': True
79-
},
80-
{
81-
'relative_package_path': '{}-shared.js'.format(__name__),
82-
'external_url': (
83-
'https://unpkg.com/dash-core-components@{}'
84-
'/dash_core_components/dash_core_components-shared.js'
85-
).format(__version__),
86-
'namespace': 'dash_core_components'
87-
},
88-
{
89-
'relative_package_path': '{}-shared.js.map'.format(__name__),
90-
'external_url': (
91-
'https://unpkg.com/dash-core-components@{}'
92-
'/dash_core_components/dash_core_components-shared.js.map'
93-
).format(__version__),
94-
'namespace': 'dash_core_components',
95-
'dynamic': True
96-
},
97-
{
98-
'relative_package_path': 'plotly.min.js',
99-
'external_url': (
100-
'https://unpkg.com/dash-core-components@{}'
101-
'/dash_core_components/plotly.min.js'
102-
).format(__version__),
103-
'namespace': 'dash_core_components',
104-
'async': 'eager'
105-
},
106-
{
107-
'relative_package_path': 'async-plotlyjs.js',
108-
'external_url': (
109-
'https://unpkg.com/dash-core-components@{}'
110-
'/dash_core_components/async-plotlyjs.js'
111-
).format(__version__),
112-
'namespace': 'dash_core_components',
113-
'async': 'lazy'
114-
},
115-
{
116-
'relative_package_path': 'async-plotlyjs.js.map',
117-
'external_url': (
118-
'https://unpkg.com/dash-core-components@{}'
119-
'/dash_core_components/async-plotlyjs.js.map'
120-
).format(__version__),
121-
'namespace': 'dash_core_components',
122-
'dynamic': True
123-
},
124-
])
76+
_js_dist.extend(
77+
[
78+
{
79+
"relative_package_path": "{}.min.js".format(__name__),
80+
"external_url": (
81+
"https://unpkg.com/dash-core-components@{}"
82+
"/dash_core_components/dash_core_components.min.js"
83+
).format(__version__),
84+
"namespace": "dash_core_components",
85+
},
86+
{
87+
"relative_package_path": "{}.min.js.map".format(__name__),
88+
"external_url": (
89+
"https://unpkg.com/dash-core-components@{}"
90+
"/dash_core_components/dash_core_components.min.js.map"
91+
).format(__version__),
92+
"namespace": "dash_core_components",
93+
"dynamic": True,
94+
},
95+
{
96+
"relative_package_path": "{}-shared.js".format(__name__),
97+
"external_url": (
98+
"https://unpkg.com/dash-core-components@{}"
99+
"/dash_core_components/dash_core_components-shared.js"
100+
).format(__version__),
101+
"namespace": "dash_core_components",
102+
},
103+
{
104+
"relative_package_path": "{}-shared.js.map".format(__name__),
105+
"external_url": (
106+
"https://unpkg.com/dash-core-components@{}"
107+
"/dash_core_components/dash_core_components-shared.js.map"
108+
).format(__version__),
109+
"namespace": "dash_core_components",
110+
"dynamic": True,
111+
},
112+
{
113+
"relative_package_path": "plotly.min.js",
114+
"external_url": (
115+
"https://unpkg.com/dash-core-components@{}"
116+
"/dash_core_components/plotly.min.js"
117+
).format(__version__),
118+
"namespace": "dash_core_components",
119+
"async": "eager",
120+
},
121+
{
122+
"relative_package_path": "async-plotlyjs.js",
123+
"external_url": (
124+
"https://unpkg.com/dash-core-components@{}"
125+
"/dash_core_components/async-plotlyjs.js"
126+
).format(__version__),
127+
"namespace": "dash_core_components",
128+
"async": "lazy",
129+
},
130+
{
131+
"relative_package_path": "async-plotlyjs.js.map",
132+
"external_url": (
133+
"https://unpkg.com/dash-core-components@{}"
134+
"/dash_core_components/async-plotlyjs.js.map"
135+
).format(__version__),
136+
"namespace": "dash_core_components",
137+
"dynamic": True,
138+
},
139+
]
140+
)
125141

126142
for _component in __all__:
127-
setattr(locals()[_component], '_js_dist', _js_dist)
143+
setattr(locals()[_component], "_js_dist", _js_dist)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import io
2+
import ntpath
3+
import base64
4+
from functools import partial
5+
6+
# Py2 StringIO.StringIO handles unicode but io.StringIO doesn't
7+
try:
8+
from StringIO import StringIO as _StringIO
9+
py2 = True
10+
except ImportError:
11+
_StringIO = io.StringIO
12+
py2 = False
13+
14+
# region Utils for Download component
15+
16+
17+
def send_file(path, filename=None, type=None):
18+
"""
19+
Convert a file into the format expected by the Download component.
20+
:param path: path to the file to be sent
21+
:param filename: name of the file, if not provided the original filename is used
22+
:param type: type of the file (optional, passed to Blob in the javascript layer)
23+
:return: dict of file content (base64 encoded) and meta data used by the Download component
24+
"""
25+
# If filename is not set, read it from the path.
26+
if filename is None:
27+
filename = ntpath.basename(path)
28+
# Read the file contents and send it.
29+
with open(path, "rb") as f:
30+
return send_bytes(f.read(), filename, type)
31+
32+
33+
def send_bytes(src, filename, type=None, **kwargs):
34+
"""
35+
Convert data written to BytesIO into the format expected by the Download component.
36+
:param src: array of bytes or a writer that can write to BytesIO
37+
:param filename: the name of the file
38+
:param type: type of the file (optional, passed to Blob in the javascript layer)
39+
:return: dict of data frame content (base64 encoded) and meta data used by the Download component
40+
"""
41+
content = src if isinstance(src, bytes) else _io_to_str(io.BytesIO(), src, **kwargs)
42+
return dict(
43+
content=base64.b64encode(content).decode(),
44+
filename=filename,
45+
type=type,
46+
base64=True,
47+
)
48+
49+
50+
def send_string(src, filename, type=None, **kwargs):
51+
"""
52+
Convert data written to StringIO into the format expected by the Download component.
53+
:param src: a string or a writer that can write to StringIO
54+
:param filename: the name of the file
55+
:param type: type of the file (optional, passed to Blob in the javascript layer)
56+
:return: dict of data frame content (NOT base64 encoded) and meta data used by the Download component
57+
"""
58+
content = src if isinstance(src, str) else _io_to_str(_StringIO(), src, **kwargs)
59+
return dict(content=content, filename=filename, type=type, base64=False)
60+
61+
62+
def _io_to_str(data_io, writer, **kwargs):
63+
# Some pandas writers try to close the IO, we do not want that.
64+
data_io_close = data_io.close
65+
data_io.close = lambda: None
66+
# Write data content.
67+
writer(data_io, **kwargs)
68+
data_value = data_io.getvalue()
69+
data_io_close()
70+
return data_value
71+
72+
73+
def send_data_frame(writer, filename, type=None, **kwargs):
74+
"""
75+
Convert data frame into the format expected by the Download component.
76+
:param writer: a data frame writer
77+
:param filename: the name of the file
78+
:param type: type of the file (optional, passed to Blob in the javascript layer)
79+
:return: dict of data frame content (base64 encoded) and meta data used by the Download component
80+
81+
Examples
82+
--------
83+
84+
>>> df = pd.DataFrame({'a': [1, 2, 3, 4], 'b': [2, 1, 5, 6], 'c': ['x', 'x', 'y', 'y']})
85+
...
86+
>>> send_data_frame(df.to_csv, "mydf.csv") # download as csv
87+
>>> send_data_frame(df.to_json, "mydf.json") # download as json
88+
>>> send_data_frame(df.to_excel, "mydf.xls", index=False) # download as excel
89+
>>> send_data_frame(df.to_pickle, "mydf.pkl") # download as pickle
90+
91+
"""
92+
name = writer.__name__
93+
# Check if the provided writer is known.
94+
if name not in _data_frame_senders.keys():
95+
raise ValueError(
96+
"The provided writer ({}) is not supported, "
97+
"try calling send_string or send_bytes directly.".format(name)
98+
)
99+
# Send data frame using the appropriate send function.
100+
return _data_frame_senders[name](writer, filename, type, **kwargs)
101+
102+
103+
_data_frame_senders = {
104+
"to_csv": send_string,
105+
"to_json": send_string,
106+
"to_html": send_string,
107+
"to_excel": send_bytes,
108+
"to_feather": send_bytes,
109+
"to_parquet": send_bytes,
110+
"to_msgpack": send_bytes,
111+
"to_stata": send_bytes,
112+
"to_pickle": partial(send_bytes, compression=None) if py2 else send_bytes,
113+
}
114+
115+
# endregion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
numpy
12
pandas
3+
pyarrow<3;python_version<"3.7"
4+
pyarrow;python_version>="3.7"
25
xlrd<2
36
mimesis;python_version>="3.6"
47
virtualenv;python_version=="2.7"

0 commit comments

Comments
 (0)