from __future__ import print_function # py 2.7 compat.
import base64
import urllib
# Widget definitions.
from IPython.html import widgets
# Traitlet needed to add synced attributes to the widget.
from IPython.utils.traitlets import Unicode
This is a custom widget that allows the user to upload file data to the notebook server. The file data is sent via a statefull value
attribute of the widget. The widget has an upload failed event that fires in the front-end and is echoed to the back-end using a custom msg.
class FileWidget(widgets.DOMWidget):
_view_name = Unicode('FilePickerView', sync=True)
value = Unicode(sync=True)
filename = Unicode(sync=True)
def __init__(self, **kwargs):
"""Constructor"""
widgets.DOMWidget.__init__(self, **kwargs) # Call the base.
# Allow the user to register error callbacks with the following signatures:
# callback()
# callback(sender)
self.errors = widgets.CallbackDispatcher(accepted_nargs=[0, 1])
# Listen for custom msgs
self.on_msg(self._handle_custom_msg)
def _handle_custom_msg(self, content):
"""Handle a msg from the front-end.
Parameters
----------
content: dict
Content of the msg."""
if 'event' in content and content['event'] == 'error':
self.errors()
self.errors(self)
%%javascript
require(
["jquery", "underscore", "widgets/js/manager"],
function($, _, wm, Dropzone){
var FilePickerView = IPython.DOMWidgetView.extend({
render: function(){
// Render the view.
this.$el.append($("<input/>", {type: "file"}));
},
events: {
change: "handle_file_change"
},
handle_file_change: function(evt) {
// Handle when the user has changed the file.
// Retrieve the first (and only!) File from the FileList object
var file = evt.target.files[0];
if (file) {
// Read the file's textual content and set value to those contents.
var that = this;
var file_reader = new FileReader();
file_reader.onload = function(e) {
that.model.set('value', e.target.result);
that.touch();
}
// by using a `data:` url, we get a nice portable representation which can be
// easily rendered or embedded.
file_reader.readAsDataURL(file);
} else {
// The file couldn't be opened. Send an error msg to the
// back-end.
this.send({event: 'error'});
}
// Set the filename of the file.
this.model.set('filename', file.name);
this.touch();
},
});
// Register the DatePickerView with the widget manager.
wm.WidgetManager.register_widget_view('FilePickerView', FilePickerView);
});
Once the data url gets back to the backend, we'll want to do some work against it: this gist provides this nice API.
import mimetypes
import re
import urllib
MIMETYPE_REGEX = r'[\w]+\/[\w\-\+\.]+'
_MIMETYPE_RE = re.compile('^{}$'.format(MIMETYPE_REGEX))
CHARSET_REGEX = r'[\w\-\+\.]+'
_CHARSET_RE = re.compile('^{}$'.format(CHARSET_REGEX))
DATA_URI_REGEX = (
r'data:' +
r'(?P<mimetype>{})?'.format(MIMETYPE_REGEX) +
r'(?:\;charset\=(?P<charset>{}))?'.format(CHARSET_REGEX) +
r'(?P<base64>\;base64)?' +
r',(?P<data>.*)')
_DATA_URI_RE = re.compile(r'^{}$'.format(DATA_URI_REGEX), re.DOTALL)
class DataURI(str):
@classmethod
def make(cls, mimetype, charset, base64, data):
parts = ['data:']
if mimetype is not None:
if not _MIMETYPE_RE.match(mimetype):
raise ValueError("Invalid mimetype: %r" % mimetype)
parts.append(mimetype)
if charset is not None:
if not _CHARSET_RE.match(charset):
raise ValueError("Invalid charset: %r" % charset)
parts.extend([';charset=', charset])
if base64:
parts.append(';base64')
encoded_data = data.encode('base64').replace('\n', '')
else:
encoded_data = urllib.quote(data)
parts.extend([',', encoded_data])
return cls(''.join(parts))
@classmethod
def from_file(cls, filename, charset=None, base64=True):
mimetype, _ = mimetypes.guess_type(filename, strict=False)
with open(filename) as fp:
data = fp.read()
return cls.make(mimetype, charset, base64, data)
def __new__(cls, *args, **kwargs):
uri = super(DataURI, cls).__new__(cls, *args, **kwargs)
uri._parse # Trigger any ValueErrors on instantiation.
return uri
def __repr__(self):
return 'DataURI(%s)' % (super(DataURI, self).__repr__(),)
def wrap(self, width=76):
return type(self)('\n'.join(textwrap.wrap(self, width)))
@property
def mimetype(self):
return self._parse[0]
@property
def charset(self):
return self._parse[1]
@property
def is_base64(self):
return self._parse[2]
@property
def data(self):
return self._parse[3]
@property
def _parse(self):
match = _DATA_URI_RE.match(self)
if not match:
raise ValueError("Not a valid data URI: %r" % self)
mimetype = match.group('mimetype') or None
charset = match.group('charset') or None
if match.group('base64'):
data = match.group('data').decode('base64')
else:
data = urllib.unquote(match.group('data'))
return mimetype, charset, bool(match.group('base64')), data
The following shows how the file widget can be used.
import struct
file_widget = FileWidget()
# Register an event to echo the filename when it has been changed.
def file_loading():
print("Loading %s" % file_widget.filename)
file_widget.on_trait_change(file_loading, 'filename')
# Register an event to echo the filename and contents when a file
# has been uploaded.
def file_loaded():
uri = DataURI(file_widget.value)
with open(file_widget.filename, "w+") as f:
f.write(uri.data)
print("Loaded, file contents saved to: %s" % file_widget.filename)
file_widget.on_trait_change(file_loaded, 'value')
# Register an event to print an error message when a file could not
# be opened. Since the error messages are not handled through
# traitlets but instead handled through custom msgs, the registration
# of the handler is different than the two examples above. Instead
# the API provided by the CallbackDispatcher must be used.
def file_failed():
print("Could not load file contents of %s" % file_widget.filename)
file_widget.errors.register_callback(file_failed)
file_widget
Loading thumbnail.png Loaded, file contents saved to: thumbnail.png