diff --git a/jsfileupload/__init__.py b/jsfileupload/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bd7a85290b077bcb22329c144b75a25fe9e630a0 --- /dev/null +++ b/jsfileupload/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + +from .upload_widget import FileUpload +from ._version import __version__, version_info + +from .nbextension import _jupyter_nbextension_paths diff --git a/jsfileupload/_frontend.py b/jsfileupload/_frontend.py new file mode 100644 index 0000000000000000000000000000000000000000..a5bc60466f653df5348396654c024d7e929ccc93 --- /dev/null +++ b/jsfileupload/_frontend.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + +""" +Information about the frontend package of the widgets. +""" + +module_name = "jsfileupload" +module_version = "^0.1.0" diff --git a/jsfileupload/_version.py b/jsfileupload/_version.py new file mode 100644 index 0000000000000000000000000000000000000000..fdb1be357a757978d8a6036dbfe2dc914f5e5d40 --- /dev/null +++ b/jsfileupload/_version.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + +version_info = (0, 1, 0, 'dev') +__version__ = ".".join(map(str, version_info)) diff --git a/jsfileupload/nbextension/__init__.py b/jsfileupload/nbextension/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b525f3a00601fefa46355ec4bb29cd3a21cd57a3 --- /dev/null +++ b/jsfileupload/nbextension/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC) +# Distributed under the terms of the Modified BSD License. + +def _jupyter_nbextension_paths(): + return [{ + 'section': 'notebook', + 'src': 'nbextension/static', + 'dest': 'jsfileupload', + 'require': 'jsfileupload/extension' + }] diff --git a/jsfileupload/nbextension/static/extension.js b/jsfileupload/nbextension/static/extension.js new file mode 100644 index 0000000000000000000000000000000000000000..fed306efe40f66961d39363d4e339e87f09ccae6 --- /dev/null +++ b/jsfileupload/nbextension/static/extension.js @@ -0,0 +1,17 @@ +// Entry point for the notebook bundle containing custom model definitions. +// +define(function() { + "use strict"; + + window['requirejs'].config({ + map: { + '*': { + 'jsfileupload': 'nbextensions/jsfileupload/index', + }, + } + }); + // Export the required load_ipython_extension function + return { + load_ipython_extension : function() {} + }; +}); diff --git a/jsfileupload/tests/__init__.py b/jsfileupload/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/jsfileupload/tests/conftest.py b/jsfileupload/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..f4ecae680762509d8c56db66878127e22a0fa505 --- /dev/null +++ b/jsfileupload/tests/conftest.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + +import pytest + +from ipykernel.comm import Comm +from ipywidgets import Widget + +class MockComm(Comm): + """A mock Comm object. + + Can be used to inspect calls to Comm's open/send/close methods. + """ + comm_id = 'a-b-c-d' + kernel = 'Truthy' + + def __init__(self, *args, **kwargs): + self.log_open = [] + self.log_send = [] + self.log_close = [] + super(MockComm, self).__init__(*args, **kwargs) + + def open(self, *args, **kwargs): + self.log_open.append((args, kwargs)) + + def send(self, *args, **kwargs): + self.log_send.append((args, kwargs)) + + def close(self, *args, **kwargs): + self.log_close.append((args, kwargs)) + +_widget_attrs = {} +undefined = object() + + +@pytest.fixture +def mock_comm(): + _widget_attrs['_comm_default'] = getattr(Widget, '_comm_default', undefined) + Widget._comm_default = lambda self: MockComm() + _widget_attrs['_ipython_display_'] = Widget._ipython_display_ + def raise_not_implemented(*args, **kwargs): + raise NotImplementedError() + Widget._ipython_display_ = raise_not_implemented + + yield MockComm() + + for attr, value in _widget_attrs.items(): + if value is undefined: + delattr(Widget, attr) + else: + setattr(Widget, attr, value) diff --git a/jsfileupload/tests/test_example.py b/jsfileupload/tests/test_example.py new file mode 100644 index 0000000000000000000000000000000000000000..112621d894159fbc262d03655c865e05d3cd6de6 --- /dev/null +++ b/jsfileupload/tests/test_example.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + +import pytest + +from ..example import ExampleWidget + + +def test_example_creation_blank(): + w = ExampleWidget() + assert w.value == 'Hello World' diff --git a/jsfileupload/tests/test_nbextension_path.py b/jsfileupload/tests/test_nbextension_path.py new file mode 100644 index 0000000000000000000000000000000000000000..91936058938f438ac5c0f1d69ded57525ec1205f --- /dev/null +++ b/jsfileupload/tests/test_nbextension_path.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + + +def test_nbextension_path(): + # Check that magic function can be imported from package root: + from jsfileupload import _jupyter_nbextension_paths + # Ensure that it can be called without incident: + path = _jupyter_nbextension_paths() + # Some sanity checks: + assert len(path) == 1 + assert isinstance(path[0], dict) diff --git a/jsfileupload/upload_widget.py b/jsfileupload/upload_widget.py new file mode 100644 index 0000000000000000000000000000000000000000..5bbc0b5d392b5d096156f4e41ad24420fae66206 --- /dev/null +++ b/jsfileupload/upload_widget.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + +""" +FileUpload Widget using the Jupyter Notebook Server RestAPI. +Can handle large files by uploading files in chunks. +""" +from ipywidgets import ValueWidget +from ipywidgets import register, widget_serialization +from ipywidgets.widgets.trait_types import InstanceDict +from ipywidgets.widgets.widget_button import ButtonStyle +from ipywidgets.widgets.widget_description import DescriptionWidget + +from traitlets import ( + default, Unicode, Dict, List, Int, Bool, Bytes, CaselessStrEnum +) +from ._frontend import module_name, module_version + + +class FileUpload(DescriptionWidget, ValueWidget): + """FileUpload Widget. Uploads via the Jupyter Notebook Server RestAPI. + + The widget is able to upload large files by uploading them in chunks. + The file contents will however not be directly available over the widget, + but will have to be read into the notebook seperately. + """ + _model_name = Unicode('FileUploadModel').tag(sync=True) + _model_module = Unicode(module_name).tag(sync=True) + _model_module_version = Unicode(module_version).tag(sync=True) + _view_name = Unicode('FileUploadView').tag(sync=True) + _view_module = Unicode(module_name).tag(sync=True) + _view_module_version = Unicode(module_version).tag(sync=True) + + accept = Unicode(help='File types to accept, empty string for all').tag(sync=True) + multiple = Bool(help='If True, allow for multiple files upload').tag(sync=True) + disabled = Bool(help='Enable or disable button').tag(sync=True) + icon = Unicode('folder', help="Font-awesome icon name, without the 'fa-' prefix.").tag(sync=True) + button_style = CaselessStrEnum( + values=['primary', 'success', 'info', 'warning', 'danger', ''], default_value='', + help="""Use a predefined styling for the button.""").tag(sync=True) + style = InstanceDict(ButtonStyle).tag(sync=True, **widget_serialization) + metadata = List(Dict(), help='List of file metadata').tag(sync=True) + + # Needed for uploading using the Notebook Server RestAPI. + token = Unicode(help='Jupyter API token').tag(sync=True) + upload_url = Unicode('http://localhost:8888/api/contents/', + help='http(s)://<notebook_url>/api/contents/<path>').tag(sync=True) + + # Variables set on the JavaScript side. + files = List().tag(sync=True) + responses = List().tag(sync=True) + finished = Bool(False).tag(sync=True) + _upload = Bool(False).tag(sync=True) + + + def __init__(self, upload_url='http://localhost:8888/api/contents/', token='', *args, **kwargs): + """Args: + upload_url (str): Jupyter notebook URL appended by api/contents/<path>/. Directories on <path> must already exist. + token (str): Jupyter notebook authentication token. + """ + super(FileUpload, self).__init__(*args, **kwargs) + self.upload_url = upload_url + self.token = token + + @default('description') + def _default_description(self): + return 'Browse' + + def upload(self): + """Uploads file(s) via the JS fetch API.""" + self._upload = True \ No newline at end of file