├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── MANIFEST.in ├── Makefile ├── README.md ├── dashboards_bundlers ├── __init__.py ├── _version.py ├── server_download.py └── server_upload.py ├── etc ├── bundlers_intro.png └── notebooks │ ├── associations_demo │ ├── associations_demo.ipynb │ ├── data │ │ └── cars.csv │ └── images │ │ └── MjeLFmy6Lx8di.gif │ ├── hello_world.ipynb │ ├── hello_world_report.ipynb │ └── widget_binding.ipynb ├── requirements.txt ├── setup.py └── test ├── resources ├── env.ipynb ├── no_imports.ipynb ├── some.csv └── some.ipynb ├── test_server_download.py └── test_server_upload.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # OSX 60 | .DS_Store 61 | .ipynb_checkpoints/ 62 | etc/notebooks/local_dashboards/ 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.3 5 | - 3.4 6 | - 3.5 7 | - 3.6 8 | 9 | before_install: 10 | - pip install pip -U 11 | - pip install setuptools -U 12 | - pip install -U notebook 13 | 14 | script: 15 | - pip install -e . 16 | - python -B -m unittest discover -s test 17 | 18 | notifications: 19 | email: 20 | on_success: change 21 | on_failure: always 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.9.1 (2017-04-05) 4 | 5 | * Remove leftover setuptools script entrypoint 6 | 7 | ## 0.9.0 (2017-04-04) 8 | 9 | * Remove deprecated bundlers 10 | * Update to work with bundler API in notebook>=5.0.0 11 | * Update to install against notebook 5.0.0 12 | 13 | ## 0.8.1 (2016-09-09) 14 | 15 | * Fix SystemRoot env var on Windows 16 | * Fix missing license in package 17 | 18 | ## 0.8.0 (2016-06-16) 19 | 20 | * Use link attribute from dashboards server to redirect after deployment if available 21 | * Fix declarative widget errors from appearing in local dashboard views 22 | * Add an option to disable SSL verification for dashboard server deployment 23 | * Update example notebooks for compatibility with `jupyter_declarativewidgets` 0.6.0 24 | 25 | ## 0.7.0 (2016-04-28) 26 | 27 | * Fix local deploy compatibility with `jupyter-incubator/declarativewidgets>=0.5.0` 28 | 29 | ## 0.6.0 (2016-04-27) 30 | 31 | * Fix dashboard server bundle compatibility with `jupyter-incubator/declarativewidgets>=0.5.0` 32 | * Deprecate bundlers that rely on Thebe 33 | 34 | ## 0.5.0 (2016-04-26) 35 | 36 | * Add support for report layout in Thebe-based deployments 37 | * Add bundler to download a zip for deployment on `jupyter-incubator/dashboards_server` 38 | * Improve documentation about bundling associated assets 39 | * Make compatible with Jupyter Notebook 4.0.x to 4.2.x 40 | 41 | ## 0.4.0 (2016-03-04) 42 | 43 | * Fix authorization header format for deployment to dashboard server 44 | * Support bundling of frontend assets when deploying to dashboard server 45 | * Make forward compatible with `declarative widgets>=0.5.0` 46 | 47 | ## 0.3.1 (2016-02-23) 48 | 49 | * Fix compatibility with `jupyter_dashboards>=0.4.2` (internal lodash path change) 50 | 51 | ## 0.3.0 (2016-02-13) 52 | 53 | * Add bundler to deploy to `jupyter-incubator/dashboards_server` 54 | 55 | ## 0.2.2 (2016-02-07) 56 | 57 | * Fix compatibility with latest nbconvert (`jupyter nbconvert`) 58 | 59 | ## 0.2.1 (2016-01-26) 60 | 61 | * Hide stderr and Thebe errors in dashboard UI 62 | 63 | ## 0.2.0 (2016-01-21) 64 | 65 | * Separate `pip install` from `jupyter dashboards [install | activate | deactivate]` 66 | * Fix local deploy when base URL is set on Jupyter Notebook server 67 | 68 | ## 0.1.1 (2015-12-30) 69 | 70 | * Fix missing static assets in release 71 | 72 | ## 0.1.0 (2015-12-30) 73 | 74 | * First release with local dashboard and PHP app options 75 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Licensing terms 2 | 3 | This project is licensed under the terms of the Modified BSD License 4 | (also known as New or Revised or 3-Clause BSD), as follows: 5 | 6 | - Copyright (c) 2001-2015, IPython Development Team 7 | - Copyright (c) 2015-, Jupyter Development Team 8 | 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are met: 13 | 14 | Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | 17 | Redistributions in binary form must reproduce the above copyright notice, this 18 | list of conditions and the following disclaimer in the documentation and/or 19 | other materials provided with the distribution. 20 | 21 | Neither the name of the Jupyter Development Team nor the names of its 22 | contributors may be used to endorse or promote products derived from this 23 | software without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 29 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 32 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 33 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | 36 | ## About the Jupyter Development Team 37 | 38 | The Jupyter Development Team is the set of all contributors to the Jupyter project. 39 | This includes all of the Jupyter Subprojects, which are the different repositories 40 | under the [jupyter](https://github.com/jupyter/) GitHub organization. 41 | 42 | The core team that coordinates development on GitHub can be found here: 43 | https://github.com/jupyter/. 44 | 45 | ## Our copyright policy 46 | 47 | Jupyter uses a shared copyright model. Each contributor maintains copyright 48 | over their contributions to Jupyter. But, it is important to note that these 49 | contributions are typically only changes to the repositories. Thus, the Jupyter 50 | source code, in its entirety is not the copyright of any single person or 51 | institution. Instead, it is the collective copyright of the entire Jupyter 52 | Development Team. If individual contributors want to maintain a record of what 53 | changes/contributions they have specific copyright on, they should indicate 54 | their copyright in the commit message of the change, when they commit the 55 | change to one of the Jupyter repositories. 56 | 57 | With this in mind, the following banner should be used in any source code file 58 | to indicate the copyright and license terms: 59 | 60 | # Copyright (c) Jupyter Development Team. 61 | # Distributed under the terms of the Modified BSD License. 62 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include dashboards_bundlers * 2 | include LICENSE.md 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | .PHONY: activate build clean env help notebook nuke release sdist test 5 | 6 | SA:=source activate 7 | ENV:=dashboards-bundlers 8 | 9 | help: 10 | # http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 11 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 12 | 13 | activate: ## eval $(make activate) 14 | @echo "$(SA) $(ENV)" 15 | 16 | clean: ## Make a clean source tree 17 | @-rm -rf dist 18 | @-rm -rf *.egg-info 19 | @-rm -rf __pycache__ */__pycache__ */*/__pycache__ 20 | @-find . -name '*.pyc' -exec rm -fv {} \; 21 | 22 | build: env 23 | env: ## Make a dev environment 24 | @conda create -y -n $(ENV) -c conda-forge python=3 \ 25 | --file requirements.txt \ 26 | # Need to force notebook prerelease until 5.0 is out 27 | @$(SA) $(ENV) && \ 28 | pip install -U --pre notebook && \ 29 | pip install -e . && \ 30 | jupyter bundlerextension enable --sys-prefix --py dashboards_bundlers 31 | 32 | notebook: ## Make a notebook server 33 | $(SA) $(ENV) && jupyter notebook --notebook-dir=./etc/notebooks 34 | 35 | nuke: clean ## Make clean + remove conda env 36 | -conda env remove -n $(ENV) -y 37 | 38 | release: sdist ## Make a release on PyPI 39 | $(SA) $(ENV) && python setup.py sdist register upload 40 | 41 | sdist: ## Make a dist/*.tar.gz source distribution 42 | $(SA) $(ENV) && python setup.py sdist 43 | 44 | test: ## Make a test run 45 | $(SA) $(ENV) && python -B -m unittest discover -s test 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/jupyter_dashboards_bundlers.svg)](https://badge.fury.io/py/jupyter_dashboards_bundlers) [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) 2 | 3 | # [RETIRED] Jupyter Dashboards Bundlers 4 | 5 | **This project has been retired.** See the [proposal to move the project to jupyter-attic](https://github.com/jupyter/enhancement-proposals/blob/master/jupyter-dashboards-deployment-attic/jupyter-dashboards-deployment-attic.md), [announcement of the proposal on the mailing list](https://groups.google.com/forum/#!topic/jupyter/icEtCVLniRc), and [Steering Council vote on the proposal PR](https://github.com/jupyter/enhancement-proposals/pull/22) for more information. 6 | 7 | ---- 8 | 9 | The dashbaords bundler extension is an add-on for Jupyter Notebook. It takes a 10 | notebook with layout information from the [dashboard layout 11 | extention](https://github.com/jupyter/dashboards) and lets you download it or 12 | directly publish it to the experimental [dashboard 13 | server](https://github.com/jupyter-incubator/dashboards_server). 14 | 15 | ![Dashboard bundlers screenshot](etc/bundlers_intro.png) 16 | 17 | ## What It Gives You 18 | 19 | * *File → Download as → Jupyter Dashboards Server bundle (.zip)* - menu item to download the current notebook and its associated assets for manual deployment on a [Jupyter Dashboards](https://github.com/jupyter-incubator/dashboards_server) server. 20 | * *File → Deploy as → Dashboard on Jupyter Dashboards Server* - menu item to deploy the current notebook and its associated assets as a dashboard on a target [Jupyter Dashboards](https://github.com/jupyter-incubator/dashboards_server) server. 21 | 22 | ## Prerequisites 23 | 24 | * Jupyter Notebook >=5.0 running on Python 3.x or Python 2.7.x 25 | * Edge, Chrome, Firefox, or Safari 26 | 27 | If you are running an older version of the notebook server, you should use a 28 | version of this package prior to the 0.9 release. 29 | 30 | ## Installing and Enabling 31 | 32 | The following steps install the extension package using `pip` and enable the 33 | extension in the active Python environment. 34 | 35 | ```bash 36 | pip install jupyter_dashboards_bundlers 37 | jupyter bundlerextension enable --sys-prefix --py dashboards_bundlers 38 | ``` 39 | 40 | ## Disabling and Uninstalling 41 | 42 | The following steps deactivate the extension in the active Python environment 43 | and uninstall the package using `pip`. 44 | 45 | ```bash 46 | jupyter bundlerextension disable --sys-prefix --py dashboards_bundlers 47 | pip uninstall jupyter_dashboards_bundlers 48 | ``` 49 | 50 | ## Use It 51 | 52 | Currently, there are two bundlers available in this package. 53 | 54 | ### Download as → Jupyter Dashboards Server bundle (.zip) 55 | 56 | The first option bundles your notebook and any associated frontend assets into 57 | a zip file which you can manually deploy on a [Jupyter Dashboards 58 | Server](https://github.com/jupyter-incubator/dashboards_server). To use it: 59 | 60 | 1. Write a notebook. 61 | 2. Define a dashboard layout using the `jupyter_dashboards` extension. 62 | 3. If the notebook requires any frontend assets (e.g., CSS files), [associate 63 | them with the 64 | notebook](etc/notebooks/associations_demo/associations_demo.ipynb). 65 | 4. Click *File → Download as → Jupyter Dashboards Server bundle 66 | (.zip)*. 67 | 5. Install 68 | [jupyter-incubator/dashboards_server](https://github.com/jupyter-incubator/dashboards_server) 69 | by following the project README. 70 | 5. Unzip the bundle in the `data/` directory of the Jupyter Dashboard Server 71 | and run it. 72 | 73 | This bundler is compatible with: 74 | 75 | * `jupyter_declarativewidgets>=0.5.0` when deploying dashboards with declarative widgets 76 | * `ipywidgets>=5.0.0,<6.0.0` when deploying dashboards with ipywidgets 77 | 78 | ### Deploy as → Dashboard on Jupyter Dashboards Server 79 | 80 | The second option directly sends your notebook and any associated frontend 81 | assets to a [Jupyter Dashboards 82 | Server](https://github.com/jupyter-incubator/dashboards_server). To use it: 83 | 84 | 0. Run an instance of the Jupyter Dashboards Server by following the 85 | instructions in the 86 | [jupyter-incubator/dashboards_server](https://github.com/jupyter-incubator/dashboards_server) 87 | project README. 88 | 1. Set the following environment variables before launching your Jupyter 89 | Notebook server with the bundler extensions installed. 90 | * `DASHBOARD_SERVER_URL` - protocol, hostname, and port of the dashboard 91 | server to which to send dashboard notebooks 92 | * `DASHBOARD_REDIRECT_URL` (optional) - protocol, hostname, and port to use 93 | when redirecting the user's browser after upload if different from 94 | `DASHBOARD_SERVER_URL` and if upload response has no link property from 95 | the dashboard server (v0.6.0+) 96 | * `DASHBOARD_SERVER_AUTH_TOKEN` (optional) - upload token required by the 97 | dashboard server 98 | * `DASHBOARD_SERVER_NO_SSL_VERIFY` (optional) - skip verification of the 99 | dashboard server SSL certificate (for use in dev / trusted environments 100 | only!) 101 | 2. Write a notebook. 102 | 3. Define a dashboard layout using the `jupyter_dashboards` extension. 103 | 4. If the notebook requires any frontend assets (e.g., CSS files), [associate 104 | them with the 105 | notebook](etc/notebooks/associations_demo/associations_demo.ipynb). 106 | 5. Click *File → Deploy as → Dashboard on Jupyter Dashboard Server*. 107 | 6. Enjoy your dashboard after the redirect. 108 | 109 | This bundler is compatible with: 110 | 111 | * `jupyter_declarativewidgets>=0.5.0` when deploying dashboards with declarative widgets 112 | * `ipywidgets>=5.0.0,<6.0.0` when deploying dashboards with ipywidgets 113 | 114 | ## Caveats 115 | 116 | It is important to realize that kernels launched by your deployed dashboard 117 | will not being running in the same directory or possibly even the same 118 | environment as your original notebook. You must refer to external, kernel-side 119 | resources in a portable manner (e.g., put it in an external data store). You 120 | must also ensure your kernel environment has all the same 121 | libraries installed as your notebook authoring environment. 122 | 123 | It is also your responsibility to associate any frontend, dashboard-side assets 124 | with your notebook before packaging it for deployment. To aid in this task, the 125 | two bundlers here take advantage of the notebook association feature supported 126 | by the notebook bundler API. See the [associations 127 | demo](etc/notebooks/associations_demo/associations_demo.ipynb) for the markup 128 | you can use to refer to external files that should be included in your 129 | dashboard deployment. 130 | 131 | If you are using [declarative 132 | widgets](https://github.com/jupyter-incubator/declarativewidgets) in your 133 | dashboard, you should be mindful of the following when you deploy your 134 | dashboard: 135 | 136 | * You must run the entire notebook successfully before deploying. This action 137 | ensures all external Polymer components are properly installed on the 138 | notebook server and can be bundled with your converted notebook. 139 | -------------------------------------------------------------------------------- /dashboards_bundlers/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | 5 | def _jupyter_bundlerextension_paths(): 6 | '''API for notebook bundler installation on notebook 5.0+''' 7 | return [{ 8 | 'name': 'dashboards_server_upload', 9 | 'label': 'Dashboard on Jupyter Dashboards Server', 10 | 'module_name': 'dashboards_bundlers.server_upload', 11 | 'group': 'deploy' 12 | }, 13 | { 14 | 'name': 'dashboards_server_download', 15 | 'label': 'Jupyter Dashboards Server bundle (.zip)', 16 | 'module_name': 'dashboards_bundlers.server_download', 17 | 'group': 'download' 18 | }] 19 | -------------------------------------------------------------------------------- /dashboards_bundlers/_version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | version_info = (0, 10, 0, 'dev') 5 | __version__ = '.'.join(map(str, version_info)) 6 | -------------------------------------------------------------------------------- /dashboards_bundlers/server_download.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import os 5 | import shutil 6 | import tempfile 7 | from .server_upload import make_upload_bundle 8 | 9 | 10 | def bundle(handler, model): 11 | ''' 12 | Downloads a notebook, either by itself, or within a zip file with 13 | associated data and widget files, for manual deployment to a Jupyter 14 | Dashboard Server. 15 | ''' 16 | # Noteook implementation passes ContentManager models. This bundler 17 | # only works with local files anyway. 18 | abs_nb_path = os.path.join( 19 | handler.settings['contents_manager'].root_dir, 20 | model['path'] 21 | ) 22 | 23 | # Get name of notebook from filename 24 | notebook_basename = os.path.basename(abs_nb_path) 25 | notebook_name = os.path.splitext(notebook_basename)[0] 26 | 27 | # Python 2/3 compatible try/finally to cleanup a temp working directory 28 | tmp_dir = tempfile.mkdtemp() 29 | try: 30 | output_dir = os.path.join(tmp_dir, notebook_name) 31 | # Reuse the same logic we would use to send a zip file or notebook 32 | # file to a dashboard server, but send it back to the web browser 33 | # not to another server 34 | bundle_path = make_upload_bundle(abs_nb_path, output_dir, handler.tools) 35 | 36 | if bundle_path == abs_nb_path: 37 | # Send the notebook alone: it has no associated resources 38 | handler.set_header('Content-Disposition', 39 | 'attachment; filename="%s"' % notebook_basename) 40 | handler.set_header('Content-Type', 'application/json') 41 | else: 42 | # Send a zip of the notebook and its associated resources 43 | handler.set_header('Content-Disposition', 44 | 'attachment; filename="%s"' % (notebook_name + '.zip')) 45 | handler.set_header('Content-Type', 'application/zip') 46 | 47 | with open(bundle_path, 'rb') as bundle_file: 48 | handler.write(bundle_file.read()) 49 | handler.finish() 50 | 51 | finally: 52 | # We read and send synchronously, so we can clean up safely after finish 53 | shutil.rmtree(tmp_dir, True) 54 | -------------------------------------------------------------------------------- /dashboards_bundlers/server_upload.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import os 5 | import nbformat 6 | import requests 7 | import shutil 8 | import tempfile 9 | from jupyter_core.paths import jupyter_path 10 | from notebook.utils import url_path_join 11 | from os.path import join as pjoin 12 | from tornado import escape, web 13 | from tornado.log import access_log, app_log 14 | 15 | UPLOAD_ENDPOINT = '/_api/notebooks/' 16 | VIEW_ENDPOINT = '/dashboards/' 17 | 18 | 19 | def skip_ssl_verification(): 20 | return os.getenv('DASHBOARD_SERVER_NO_SSL_VERIFY', '').lower() in ['yes', 'true'] 21 | 22 | 23 | # Log a warning if SSL verification is off at the outset 24 | if skip_ssl_verification(): 25 | app_log.warn('Dashboard server SSL verification disabled') 26 | 27 | 28 | def bundle(handler, model): 29 | ''' 30 | Uploads a notebook to a Jupyter Dashboard Server, either by itself, or 31 | within a zip file with associated data and widget files 32 | ''' 33 | # Noteook implementation passes ContentManager models. This 34 | # bundler only works with local files anyway. 35 | abs_nb_path = os.path.join( 36 | handler.settings['contents_manager'].root_dir, 37 | model['path'] 38 | ) 39 | 40 | # Get name of notebook from filename 41 | notebook_basename = os.path.basename(abs_nb_path) 42 | notebook_name = os.path.splitext(notebook_basename)[0] 43 | 44 | # Python 2/3 compatible try/finally to cleanup a temp working directory 45 | tmp_dir = tempfile.mkdtemp() 46 | try: 47 | output_dir = os.path.join(tmp_dir, notebook_name) 48 | bundled = make_upload_bundle(abs_nb_path, output_dir, handler.tools) 49 | send_file(bundled, notebook_name, handler) 50 | finally: 51 | shutil.rmtree(tmp_dir, True) 52 | 53 | 54 | def get_extension_path(*parts): 55 | ''' 56 | Searches all known jupyter extension paths for the referenced directory. 57 | Returns the first hit or None if not found. 58 | ''' 59 | ext_path = pjoin(*parts) 60 | for root_path in jupyter_path(): 61 | full_path = pjoin(root_path, 'nbextensions', ext_path) 62 | if os.path.exists(full_path): 63 | return full_path 64 | 65 | 66 | def bundle_file_references(output_path, notebook_fn, tools): 67 | ''' 68 | Looks for files references in the notebook in the manner supported by 69 | notebook.bundler.tools. Adds those files to the output path if found. 70 | 71 | :param output_path: The output path of the dashboard being assembled 72 | :param notebook_fn: The absolute path to the notebook file being packaged 73 | ''' 74 | if tools is not None: 75 | referenced_files = tools.get_file_references(notebook_fn, 4) 76 | tools.copy_filelist(os.path.dirname(notebook_fn), output_path, 77 | referenced_files) 78 | 79 | 80 | def bundle_declarative_widgets(output_path, notebook_file, widget_folder='static'): 81 | ''' 82 | Adds frontend bower components dependencies into the bundle for the dashboard 83 | application. Creates the following directories under output_path: 84 | 85 | static/urth_widgets: Stores the js for urth_widgets which will be loaded in 86 | the frontend of the dashboard 87 | static/urth_components: The directory for all of the bower components of the 88 | dashboard. 89 | 90 | NOTE: This function is too specific to urth widgets. In the 91 | future we should investigate ways to make this more generic. 92 | 93 | :param output_path: The output path of the dashboard being assembled 94 | :param notebook_file: The absolute path to the notebook file being packaged 95 | :param widget_folder: Subfolder name in which the widgets should be contained. 96 | ''' 97 | # Check if any of the cells contain widgets, if not we do not to copy the 98 | # bower_components 99 | notebook = nbformat.read(notebook_file, 4) 100 | # Using find instead of a regex to help future-proof changes that might be 101 | # to how user's will use urth-core-import 102 | # (i.e. vs. ) 103 | any_cells_with_widgets = any(cell.get('source').find('urth-core-') != -1 104 | for cell in notebook.cells) 105 | if not any_cells_with_widgets: 106 | return 107 | 108 | # Directory of declarative widgets extension 109 | widgets_dir = get_extension_path('declarativewidgets') or get_extension_path('urth_widgets') 110 | if widgets_dir is None: 111 | raise web.HTTPError(500, 'Missing jupyter_declarativewidgets extension') 112 | 113 | # Root of declarative widgets within a dashboard app 114 | output_widgets_dir = pjoin(output_path, widget_folder, 'urth_widgets/') if widget_folder is not None else pjoin(output_path, 'urth_widgets/') 115 | # JavaScript entry point for widgets in dashboard app 116 | output_js_dir = pjoin(output_widgets_dir, 'js') 117 | # Web referenceable path from which all urth widget components will be served 118 | output_components_dir = pjoin(output_path, widget_folder, 'urth_components/') if widget_folder is not None else pjoin(output_path, 'urth_components/') 119 | 120 | # Copy declarative widgets js and installed bower components into the app 121 | # under output directory 122 | widgets_js_dir = pjoin(widgets_dir, 'js') 123 | shutil.copytree(widgets_js_dir, output_js_dir) 124 | 125 | # Widgets bower components could be under 'urth_components' or 126 | # 'bower_components' depending on the version of widgets being used. 127 | widgets_components_dir = pjoin(widgets_dir, 'urth_components') 128 | if not os.path.isdir(widgets_components_dir): 129 | widgets_components_dir = pjoin(widgets_dir, 'bower_components') 130 | 131 | # Install the widget components into the output components directory 132 | shutil.copytree(widgets_components_dir, output_components_dir) 133 | 134 | 135 | def make_upload_bundle(abs_nb_path, staging_dir, tools): 136 | ''' 137 | Assembles the notebook and resources it needs, returning the path to a 138 | zip file bundling the notebook and its requirements if there are any, 139 | the notebook's path otherwise. 140 | :param abs_nb_path: The path to the notebook 141 | :param staging_dir: Temporary work directory, created and removed by the 142 | caller 143 | ''' 144 | # Clean up bundle dir if it exists 145 | shutil.rmtree(staging_dir, True) 146 | os.makedirs(staging_dir) 147 | 148 | # Include the notebook as index.ipynb to make the final URL cleaner 149 | # and for consistency 150 | shutil.copy2(abs_nb_path, os.path.join(staging_dir, 'index.ipynb')) 151 | # Include frontend files referenced via the jupyter_cms bundle mechanism 152 | bundle_file_references(staging_dir, abs_nb_path, tools) 153 | bundle_declarative_widgets(staging_dir, abs_nb_path, widget_folder=None) 154 | 155 | # if nothing else was required, indicate to upload the notebook itself 156 | if len(os.listdir(staging_dir)) == 1: 157 | return abs_nb_path 158 | 159 | zip_file = shutil.make_archive(staging_dir, format='zip', 160 | root_dir=staging_dir, base_dir='.') 161 | return zip_file 162 | 163 | 164 | def send_file(file_path, dashboard_name, handler): 165 | ''' 166 | Posts a file to the Jupyter Dashboards Server to be served as a dashboard 167 | :param file_path: The path of the file to send 168 | :param dashboard_name: The dashboard name under which it should be made 169 | available 170 | ''' 171 | # Make information about the request Host header available for use in 172 | # constructing the urls 173 | segs = handler.request.host.split(':') 174 | hostname = segs[0] 175 | if len(segs) > 1: 176 | port = segs[1] 177 | else: 178 | port = '' 179 | protocol = handler.request.protocol 180 | 181 | # Treat empty as undefined 182 | dashboard_server = os.getenv('DASHBOARD_SERVER_URL') 183 | if dashboard_server: 184 | dashboard_server = dashboard_server.format(protocol=protocol, 185 | hostname=hostname, 186 | port=port) 187 | upload_url = url_path_join(dashboard_server, UPLOAD_ENDPOINT, 188 | escape.url_escape(dashboard_name, False)) 189 | with open(file_path, 'rb') as file_content: 190 | headers = {} 191 | token = os.getenv('DASHBOARD_SERVER_AUTH_TOKEN') 192 | if token: 193 | headers['Authorization'] = 'token {}'.format(token) 194 | result = requests.post(upload_url, files={'file': file_content}, 195 | headers=headers, timeout=60, verify=not 196 | skip_ssl_verification()) 197 | if result.status_code >= 400: 198 | raise web.HTTPError(result.status_code) 199 | 200 | # Redirect to link specified in response body 201 | res_body = result.json() 202 | if 'link' in res_body: 203 | redirect_link = res_body['link'] 204 | else: 205 | # Compute redirect link using environment variables 206 | # First try redirect URL as it might be different from 207 | # internal upload URL 208 | redirect_server = os.getenv('DASHBOARD_REDIRECT_URL') 209 | if redirect_server: 210 | redirect_root = redirect_server.format(hostname=hostname, 211 | port=port, 212 | protocol=protocol) 213 | else: 214 | redirect_root = dashboard_server 215 | 216 | redirect_link = url_path_join(redirect_root, VIEW_ENDPOINT, 217 | escape.url_escape(dashboard_name, 218 | False)) 219 | handler.redirect(redirect_link) 220 | else: 221 | access_log.debug('Can not deploy, DASHBOARD_SERVER_URL not set') 222 | raise web.HTTPError(500, log_message='No dashboard server configured') 223 | -------------------------------------------------------------------------------- /etc/bundlers_intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/dashboards_bundlers/886e98ca4c6a8bfc96cf56a8dd0143c20dae7111/etc/bundlers_intro.png -------------------------------------------------------------------------------- /etc/notebooks/associations_demo/associations_demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "extensions": { 7 | "jupyter_dashboards": { 8 | "version": 1, 9 | "views": { 10 | "grid_default": { 11 | "col": 0, 12 | "height": 4, 13 | "hidden": false, 14 | "row": 4, 15 | "width": 4 16 | }, 17 | "report_default": {} 18 | } 19 | } 20 | } 21 | }, 22 | "source": [ 23 | "# Local File Associations" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": { 29 | "extensions": { 30 | "jupyter_dashboards": { 31 | "version": 1, 32 | "views": { 33 | "grid_default": { 34 | "col": 0, 35 | "height": 4, 36 | "hidden": false, 37 | "row": 8, 38 | "width": 12 39 | }, 40 | "report_default": {} 41 | } 42 | } 43 | } 44 | }, 45 | "source": [ 46 | "This notebook demonstrates two syntaxes for associating local resources (e.g., data files, images) with a notebook. With the `jupyter_dashboards_bundlers` extension installed, you can download the notebook and its associated files in a zip." 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": { 52 | "extensions": { 53 | "jupyter_dashboards": { 54 | "version": 1, 55 | "views": { 56 | "grid_default": { 57 | "col": 4, 58 | "height": 4, 59 | "hidden": false, 60 | "row": 4, 61 | "width": 4 62 | }, 63 | "report_default": {} 64 | } 65 | } 66 | } 67 | }, 68 | "source": [ 69 | "## Syntax 1: Hidden Comment" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": { 75 | "extensions": { 76 | "jupyter_dashboards": { 77 | "version": 1, 78 | "views": { 79 | "grid_default": { 80 | "col": 0, 81 | "height": 4, 82 | "hidden": false, 83 | "row": 0, 84 | "width": 4 85 | }, 86 | "report_default": { 87 | "hidden": false 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "source": [ 94 | "To demonstrate the basic feature, let's first load a local CSV file." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": { 101 | "extensions": { 102 | "jupyter_dashboards": { 103 | "version": 1, 104 | "views": { 105 | "grid_default": { 106 | "col": 5, 107 | "height": 4, 108 | "hidden": false, 109 | "row": 0, 110 | "width": 4 111 | }, 112 | "report_default": { 113 | "hidden": false 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | "outputs": [], 120 | "source": [ 121 | "import pandas as pd\n", 122 | "cars = pd.read_csv('data/cars.csv')\n", 123 | "cars" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": { 129 | "extensions": { 130 | "jupyter_dashboards": { 131 | "version": 1, 132 | "views": { 133 | "grid_default": { 134 | "col": 0, 135 | "height": 4, 136 | "hidden": false, 137 | "row": 12, 138 | "width": 12 139 | }, 140 | "report_default": {} 141 | } 142 | } 143 | } 144 | }, 145 | "source": [ 146 | "Now let's imagine we want to send this notebook and the CSV file together to another Jupyter Notebook user. We can use the first association syntax, a Markdown/HTML comment, to associate the CSV file with the notebook." 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": { 152 | "extensions": { 153 | "jupyter_dashboards": { 154 | "version": 1, 155 | "views": { 156 | "grid_default": { 157 | "col": 8, 158 | "height": 4, 159 | "hidden": false, 160 | "row": 4, 161 | "width": 4 162 | }, 163 | "report_default": {} 164 | } 165 | } 166 | } 167 | }, 168 | "source": [ 169 | "" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": { 178 | "extensions": { 179 | "jupyter_dashboards": { 180 | "version": 1, 181 | "views": { 182 | "grid_default": { 183 | "col": 0, 184 | "height": 8, 185 | "hidden": false, 186 | "row": 16, 187 | "width": 12 188 | }, 189 | "report_default": {} 190 | } 191 | } 192 | } 193 | }, 194 | "source": [ 195 | "Since this syntax is based on a comment, it is not visible in the rendered notebook document. You must switch a cell to edit mode to see the comment. If you're reading this tutorial in a Jupyter Notebook server, you can double-click the blank cell right above this one to see the markup. If you're not, here's the hidden markup reproduced as a code block for your convenience:\n", 196 | "\n", 197 | "```\n", 198 | "\n", 204 | "```" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": { 210 | "extensions": { 211 | "jupyter_dashboards": { 212 | "version": 1, 213 | "views": { 214 | "grid_default": { 215 | "col": 0, 216 | "height": 5, 217 | "hidden": false, 218 | "row": 24, 219 | "width": 12 220 | }, 221 | "report_default": {} 222 | } 223 | } 224 | } 225 | }, 226 | "source": [ 227 | "As you can see, associated filespecs are stated line-by-line according to the same rules used by git (https://git-scm.com/docs/gitignore), with two differences:\n", 228 | "\n", 229 | "1. The filespecs represent files to *include*, not *exclude* as in `.gitignore`.\n", 230 | "2. The filespecs are restricted to paths relative to and rooted at the notebook. Absolute paths and ancestor references (e.g., `../`) are disallowed (and not useful) for portability reasons." 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": { 236 | "extensions": { 237 | "jupyter_dashboards": { 238 | "version": 1, 239 | "views": { 240 | "grid_default": { 241 | "col": 0, 242 | "height": 4, 243 | "hidden": false, 244 | "row": 29, 245 | "width": 4 246 | }, 247 | "report_default": {} 248 | } 249 | } 250 | } 251 | }, 252 | "source": [ 253 | "## Syntax 2: Fenced Code" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": { 259 | "extensions": { 260 | "jupyter_dashboards": { 261 | "version": 1, 262 | "views": { 263 | "grid_default": { 264 | "col": 0, 265 | "height": 23, 266 | "hidden": false, 267 | "row": 33, 268 | "width": 12 269 | }, 270 | "report_default": {} 271 | } 272 | } 273 | } 274 | }, 275 | "source": [ 276 | "Now let's include a reference to a local image using the second supported syntax, one that makes the reference visible in the notebook at all times.\n", 277 | "\n", 278 | "Here's the image:\n", 279 | "\n", 280 | "![clap](images/MjeLFmy6Lx8di.gif)\n", 281 | "\n", 282 | "And here's the reference expressed as a fenced code block in Markdown:\n", 283 | "\n", 284 | "```\n", 285 | "# Comments are still allowed. Of course, there can be more than one filespec here too.\n", 286 | "images/*.gif\n", 287 | "```\n", 288 | "\n", 289 | "Unlike the comment syntax, this syntax doesn't require a cell for itself." 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": { 295 | "collapsed": true, 296 | "extensions": { 297 | "jupyter_dashboards": { 298 | "version": 1, 299 | "views": { 300 | "grid_default": { 301 | "col": 0, 302 | "height": 4, 303 | "hidden": false, 304 | "row": 56, 305 | "width": 12 306 | }, 307 | "report_default": {} 308 | } 309 | } 310 | } 311 | }, 312 | "source": [ 313 | "## Bundle It!\n", 314 | "\n", 315 | "With the two file lists above in place, you can now download your notebook and the associated files all together. Click *File → Download As → Jupyter Dashboards Server bundle (.zip)*. The resulting zip retains the directory structure of your workspace here in Jupyter relative to the notebook file." 316 | ] 317 | } 318 | ], 319 | "metadata": { 320 | "extensions": { 321 | "jupyter_dashboards": { 322 | "activeView": "grid_default", 323 | "version": 1, 324 | "views": { 325 | "grid_default": { 326 | "cellMargin": 10, 327 | "defaultCellHeight": 20, 328 | "maxColumns": 12, 329 | "name": "grid", 330 | "type": "grid" 331 | }, 332 | "report_default": { 333 | "name": "report", 334 | "type": "report" 335 | } 336 | } 337 | } 338 | }, 339 | "kernelspec": { 340 | "display_name": "Python 3", 341 | "language": "python", 342 | "name": "python3" 343 | }, 344 | "language_info": { 345 | "codemirror_mode": { 346 | "name": "ipython", 347 | "version": 3 348 | }, 349 | "file_extension": ".py", 350 | "mimetype": "text/x-python", 351 | "name": "python", 352 | "nbconvert_exporter": "python", 353 | "pygments_lexer": "ipython3", 354 | "version": "3.6.0" 355 | } 356 | }, 357 | "nbformat": 4, 358 | "nbformat_minor": 1 359 | } 360 | -------------------------------------------------------------------------------- /etc/notebooks/associations_demo/data/cars.csv: -------------------------------------------------------------------------------- 1 | Year,Make,Model,Description,Price 2 | 1997,Ford,E350,"ac, abs, moon",3000.00 3 | 1999,Chevy,"Venture ""Extended Edition""","",4900.00 4 | 1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00 5 | 1996,Jeep,Grand Cherokee,"MUST SELL! 6 | air, moon roof, loaded",4799.00 7 | -------------------------------------------------------------------------------- /etc/notebooks/associations_demo/images/MjeLFmy6Lx8di.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter/dashboards_bundlers/886e98ca4c6a8bfc96cf56a8dd0143c20dae7111/etc/notebooks/associations_demo/images/MjeLFmy6Lx8di.gif -------------------------------------------------------------------------------- /etc/notebooks/hello_world.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "urth": { 7 | "dashboard": { 8 | "layout": { 9 | "col": 0, 10 | "height": 2, 11 | "row": 0, 12 | "width": 8 13 | } 14 | } 15 | } 16 | }, 17 | "source": [ 18 | "# Hello World" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "collapsed": false, 26 | "urth": { 27 | "dashboard": { 28 | "layout": { 29 | "col": 0, 30 | "height": 2, 31 | "row": 2, 32 | "width": 5 33 | } 34 | } 35 | } 36 | }, 37 | "outputs": [], 38 | "source": [ 39 | "print('Tick tock goes the kernel clock ...')" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": { 46 | "collapsed": true, 47 | "urth": { 48 | "dashboard": {} 49 | } 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "from IPython.display import HTML, display, clear_output\n", 54 | "import time" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": { 61 | "collapsed": true, 62 | "urth": { 63 | "dashboard": {} 64 | } 65 | }, 66 | "outputs": [], 67 | "source": [ 68 | "start = 0x1f550" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": { 75 | "collapsed": false, 76 | "urth": { 77 | "dashboard": { 78 | "layout": { 79 | "col": 5, 80 | "height": 2, 81 | "row": 2, 82 | "width": 4 83 | } 84 | } 85 | } 86 | }, 87 | "outputs": [], 88 | "source": [ 89 | "for i in range(11):\n", 90 | " clear_output()\n", 91 | " display(HTML('&#{};'.format(start + i)))\n", 92 | " time.sleep(0.5)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "collapsed": true 100 | }, 101 | "outputs": [], 102 | "source": [] 103 | } 104 | ], 105 | "metadata": { 106 | "kernelspec": { 107 | "display_name": "Python 3", 108 | "language": "python", 109 | "name": "python3" 110 | }, 111 | "language_info": { 112 | "codemirror_mode": { 113 | "name": "ipython", 114 | "version": 3 115 | }, 116 | "file_extension": ".py", 117 | "mimetype": "text/x-python", 118 | "name": "python", 119 | "nbconvert_exporter": "python", 120 | "pygments_lexer": "ipython3", 121 | "version": "3.4.3" 122 | }, 123 | "urth": { 124 | "dashboard": { 125 | "cellMargin": 10, 126 | "defaultCellHeight": 20, 127 | "layoutStrategy": "packed", 128 | "maxColumns": 12 129 | } 130 | } 131 | }, 132 | "nbformat": 4, 133 | "nbformat_minor": 0 134 | } 135 | -------------------------------------------------------------------------------- /etc/notebooks/hello_world_report.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "urth": { 7 | "dashboard": {} 8 | } 9 | }, 10 | "source": [ 11 | "# Hello World" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": { 18 | "collapsed": false, 19 | "urth": { 20 | "dashboard": { 21 | "layout": {} 22 | } 23 | } 24 | }, 25 | "outputs": [], 26 | "source": [ 27 | "print('Tick tock goes the kernel clock ...')" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": { 34 | "collapsed": true, 35 | "urth": { 36 | "dashboard": { 37 | "hidden": true 38 | } 39 | } 40 | }, 41 | "outputs": [], 42 | "source": [ 43 | "from IPython.display import HTML, display, clear_output\n", 44 | "import time" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": { 51 | "collapsed": true, 52 | "urth": { 53 | "dashboard": { 54 | "hidden": true 55 | } 56 | } 57 | }, 58 | "outputs": [], 59 | "source": [ 60 | "start = 0x1f550" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": { 67 | "collapsed": false, 68 | "urth": { 69 | "dashboard": { 70 | "layout": {} 71 | } 72 | } 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | "for i in range(11):\n", 77 | " clear_output()\n", 78 | " display(HTML('&#{};'.format(start + i)))\n", 79 | " time.sleep(0.5)" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": { 86 | "collapsed": true, 87 | "urth": { 88 | "dashboard": { 89 | "hidden": true 90 | } 91 | } 92 | }, 93 | "outputs": [], 94 | "source": [] 95 | } 96 | ], 97 | "metadata": { 98 | "kernelspec": { 99 | "display_name": "Python 3", 100 | "language": "python", 101 | "name": "python3" 102 | }, 103 | "language_info": { 104 | "codemirror_mode": { 105 | "name": "ipython", 106 | "version": 3 107 | }, 108 | "file_extension": ".py", 109 | "mimetype": "text/x-python", 110 | "name": "python", 111 | "nbconvert_exporter": "python", 112 | "pygments_lexer": "ipython3", 113 | "version": "3.4.4" 114 | }, 115 | "urth": { 116 | "dashboard": { 117 | "layout": "report" 118 | } 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 0 123 | } 124 | -------------------------------------------------------------------------------- /etc/notebooks/widget_binding.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "urth": { 7 | "dashboard": { 8 | "layout": { 9 | "col": 0, 10 | "height": 4, 11 | "row": 0, 12 | "width": 4 13 | } 14 | } 15 | } 16 | }, 17 | "source": [ 18 | "Enter text in the box. The label should update." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "collapsed": false, 26 | "urth": { 27 | "dashboard": { 28 | "hidden": true, 29 | "layout": {} 30 | } 31 | } 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "try:\n", 36 | " # declwidgets 0.6.0+\n", 37 | " import declarativewidgets\n", 38 | " declarativewidgets.init()\n", 39 | "except ImportError:\n", 40 | " # declwidgets<0.6.0\n", 41 | " pass" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": { 48 | "collapsed": false, 49 | "urth": { 50 | "dashboard": { 51 | "layout": { 52 | "col": 4, 53 | "height": 4, 54 | "row": 0, 55 | "width": 5 56 | } 57 | } 58 | } 59 | }, 60 | "outputs": [], 61 | "source": [ 62 | "%%html\n", 63 | "" 67 | ] 68 | } 69 | ], 70 | "metadata": { 71 | "kernelspec": { 72 | "display_name": "Python 3", 73 | "language": "python", 74 | "name": "python3" 75 | }, 76 | "language_info": { 77 | "codemirror_mode": { 78 | "name": "ipython", 79 | "version": 3 80 | }, 81 | "file_extension": ".py", 82 | "mimetype": "text/x-python", 83 | "name": "python", 84 | "nbconvert_exporter": "python", 85 | "pygments_lexer": "ipython3", 86 | "version": "3.5.1" 87 | }, 88 | "urth": { 89 | "dashboard": { 90 | "cellMargin": 10, 91 | "defaultCellHeight": 20, 92 | "layout": "grid", 93 | "layoutStrategy": "packed", 94 | "maxColumns": 12 95 | } 96 | }, 97 | "widgets": { 98 | "state": {}, 99 | "version": "1.1.2" 100 | } 101 | }, 102 | "nbformat": 4, 103 | "nbformat_minor": 0 104 | } 105 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | notebook>=5.0 2 | requests>=2.7 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import os 5 | from setuptools import setup 6 | 7 | # Get location of this file at runtime 8 | HERE = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | # Eval the version tuple and string from the source 11 | VERSION_NS = {} 12 | with open(os.path.join(HERE, 'dashboards_bundlers/_version.py')) as f: 13 | exec(f.read(), {}, VERSION_NS) 14 | 15 | setup_args = dict( 16 | name='jupyter_dashboards_bundlers', 17 | author='Jupyter Development Team', 18 | author_email='jupyter@googlegroups.com', 19 | description='Plugins for jupyter_cms to deploy and download notebooks as dashboard apps', 20 | long_description=''' 21 | This package adds a *Deploy as* and *Download as* menu items for bundling 22 | notebooks created using jupyter_dashboards as standalone web applications. 23 | 24 | See `the project README `_ 25 | for more information. 26 | ''', 27 | url='https://github.com/jupyter-incubator/dashboards_bundlers', 28 | version=VERSION_NS['__version__'], 29 | license='BSD', 30 | platforms=['Jupyter Notebook 5.x'], 31 | packages=[ 32 | 'dashboards_bundlers', 33 | ], 34 | include_package_data=True, 35 | install_requires=[ 36 | 'requests>=2.7', 37 | 'notebook>=5.0' 38 | ], 39 | classifiers=[ 40 | 'Intended Audience :: Developers', 41 | 'Intended Audience :: System Administrators', 42 | 'Intended Audience :: Science/Research', 43 | 'License :: OSI Approved :: BSD License', 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 2.7', 46 | 'Programming Language :: Python :: 3.3', 47 | 'Programming Language :: Python :: 3.4', 48 | 'Programming Language :: Python :: 3.5', 49 | 'Programming Language :: Python :: 3.6' 50 | ] 51 | ) 52 | 53 | if __name__ == '__main__': 54 | setup(**setup_args) 55 | -------------------------------------------------------------------------------- /test/resources/env.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "urth": { 7 | "dashboard": { 8 | "layout": { 9 | "col": 0, 10 | "height": 4, 11 | "row": 0, 12 | "width": 4 13 | } 14 | } 15 | } 16 | }, 17 | "source": [ 18 | "# Test clear_display" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "collapsed": false, 26 | "urth": { 27 | "dashboard": { 28 | "hidden": true 29 | } 30 | } 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "%matplotlib inline" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": { 41 | "collapsed": false, 42 | "urth": { 43 | "dashboard": { 44 | "hidden": true 45 | } 46 | } 47 | }, 48 | "outputs": [], 49 | "source": [ 50 | "import matplotlib.pyplot as plt\n", 51 | "from IPython.html.widgets import *\n", 52 | "import numpy as np" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": { 59 | "collapsed": false, 60 | "urth": { 61 | "dashboard": { 62 | "layout": { 63 | "col": 0, 64 | "height": 12, 65 | "row": 4, 66 | "width": 5 67 | } 68 | } 69 | } 70 | }, 71 | "outputs": [], 72 | "source": [ 73 | "@interact(points=['-', '10', '20', '30'])\n", 74 | "def render(points=None):\n", 75 | " if points == '-': return\n", 76 | " points = int(points)\n", 77 | " fig, ax = plt.subplots()\n", 78 | " x = np.random.randn(points)\n", 79 | " y = np.random.randn(points)\n", 80 | " ax.scatter(x, y)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": { 87 | "collapsed": true, 88 | "urth": { 89 | "dashboard": { 90 | "hidden": true 91 | } 92 | } 93 | }, 94 | "outputs": [], 95 | "source": ["urth-core-import"] 96 | } 97 | ], 98 | "metadata": { 99 | "kernelspec": { 100 | "display_name": "Python 2", 101 | "language": "python", 102 | "name": "python2" 103 | }, 104 | "language_info": { 105 | "codemirror_mode": { 106 | "name": "ipython", 107 | "version": 3 108 | }, 109 | "file_extension": ".py", 110 | "mimetype": "text/x-python", 111 | "name": "python", 112 | "nbconvert_exporter": "python", 113 | "pygments_lexer": "ipython2", 114 | "version": "2.7.10" 115 | }, 116 | "urth": { 117 | "dashboard": { 118 | "cellMargin": 10, 119 | "defaultCellHeight": 20, 120 | "maxColumns": 12, 121 | "max_cols": 4, 122 | "max_rows": 17 123 | } 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 0 128 | } 129 | -------------------------------------------------------------------------------- /test/resources/no_imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "urth": { 7 | "dashboard": { 8 | "layout": { 9 | "col": 0, 10 | "height": 4, 11 | "row": 0, 12 | "width": 4 13 | } 14 | } 15 | } 16 | }, 17 | "source": [ 18 | "# Test clear_display" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "collapsed": false, 26 | "urth": { 27 | "dashboard": { 28 | "hidden": true 29 | } 30 | } 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "%matplotlib inline" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": { 41 | "collapsed": false, 42 | "urth": { 43 | "dashboard": { 44 | "hidden": true 45 | } 46 | } 47 | }, 48 | "outputs": [], 49 | "source": [ 50 | "import matplotlib.pyplot as plt\n", 51 | "from IPython.html.widgets import *\n", 52 | "import numpy as np" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": { 59 | "collapsed": false, 60 | "urth": { 61 | "dashboard": { 62 | "layout": { 63 | "col": 0, 64 | "height": 12, 65 | "row": 4, 66 | "width": 5 67 | } 68 | } 69 | } 70 | }, 71 | "outputs": [], 72 | "source": [ 73 | "@interact(points=['-', '10', '20', '30'])\n", 74 | "def render(points=None):\n", 75 | " if points == '-': return\n", 76 | " points = int(points)\n", 77 | " fig, ax = plt.subplots()\n", 78 | " x = np.random.randn(points)\n", 79 | " y = np.random.randn(points)\n", 80 | " ax.scatter(x, y)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": { 87 | "collapsed": true, 88 | "urth": { 89 | "dashboard": { 90 | "hidden": true 91 | } 92 | } 93 | }, 94 | "outputs": [], 95 | "source": [] 96 | } 97 | ], 98 | "metadata": { 99 | "kernelspec": { 100 | "display_name": "Python 3", 101 | "language": "python", 102 | "name": "python3" 103 | }, 104 | "language_info": { 105 | "codemirror_mode": { 106 | "name": "ipython", 107 | "version": 3 108 | }, 109 | "file_extension": ".py", 110 | "mimetype": "text/x-python", 111 | "name": "python", 112 | "nbconvert_exporter": "python", 113 | "pygments_lexer": "ipython3", 114 | "version": "3.4.3" 115 | }, 116 | "urth": { 117 | "dashboard": { 118 | "cellMargin": 10, 119 | "defaultCellHeight": 20, 120 | "maxColumns": 12, 121 | "max_cols": 4, 122 | "max_rows": 17 123 | } 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 0 128 | } 129 | -------------------------------------------------------------------------------- /test/resources/some.csv: -------------------------------------------------------------------------------- 1 | hello,world 2 | -------------------------------------------------------------------------------- /test/resources/some.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "collapsed": true 7 | }, 8 | "source": [ 9 | "```\n", 10 | "some.csv\n", 11 | "```" 12 | ] 13 | } 14 | ], 15 | "metadata": { 16 | "kernelspec": { 17 | "display_name": "Python 3", 18 | "language": "python", 19 | "name": "python3" 20 | }, 21 | "language_info": { 22 | "codemirror_mode": { 23 | "name": "ipython", 24 | "version": 3 25 | }, 26 | "file_extension": ".py", 27 | "mimetype": "text/x-python", 28 | "name": "python", 29 | "nbconvert_exporter": "python", 30 | "pygments_lexer": "ipython3", 31 | "version": "3.4.3" 32 | } 33 | }, 34 | "nbformat": 4, 35 | "nbformat_minor": 0 36 | } 37 | -------------------------------------------------------------------------------- /test/test_server_download.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import shutil 5 | import tempfile 6 | import unittest 7 | from os.path import join as pjoin, isdir 8 | 9 | import dashboards_bundlers.server_download as converter 10 | import notebook.bundler.tools 11 | 12 | 13 | class MockContentsManager(object): 14 | def __init__(self): 15 | self.root_dir = '.' 16 | 17 | 18 | class MockHandler(object): 19 | def __init__(self, notebook_dir): 20 | self.settings = { 21 | 'base_url': '/', 22 | 'contents_manager': MockContentsManager() 23 | } 24 | self.headers = {} 25 | self.request = type('HTTPRequest', (object,), { 26 | 'protocol': 'http', 27 | 'host': 'fake-host:5555' 28 | }) 29 | self.written = False 30 | self.finished = False 31 | self.tools = notebook.bundler.tools 32 | 33 | def set_header(self, name, value): 34 | self.headers[name] = value 35 | 36 | def write(self, *args): 37 | self.written = True 38 | 39 | def finish(self): 40 | self.finished = True 41 | 42 | 43 | class TestServerDownload(unittest.TestCase): 44 | def setUp(self): 45 | self.tmp = tempfile.mkdtemp() 46 | 47 | def tearDown(self): 48 | shutil.rmtree(self.tmp, ignore_errors=True) 49 | 50 | def test_bundle_ipynb(self): 51 | '''Should initialize an ipynb file download.''' 52 | handler = MockHandler(self.tmp) 53 | converter.bundle(handler, {'path': 'test/resources/no_imports.ipynb'}) 54 | 55 | output_dir = pjoin(self.tmp, 'no_imports') 56 | self.assertFalse(isdir(output_dir), 57 | 'app directory should no longer exist') 58 | self.assertTrue(handler.written, 'data should be written') 59 | self.assertTrue(handler.finished, 'response should be finished') 60 | self.assertIn('application/json', handler.headers['Content-Type'], 61 | 'headers should set json content type') 62 | self.assertIn('no_imports.ipynb', 63 | handler.headers['Content-Disposition'], 64 | 'headers should name the ipynb file') 65 | 66 | def test_bundle_zip(self): 67 | '''Should bundle and initiate a zip file download.''' 68 | handler = MockHandler(self.tmp) 69 | converter.bundle(handler, {'path': 'test/resources/some.ipynb'}) 70 | 71 | output_dir = pjoin(self.tmp, 'some') 72 | self.assertFalse(isdir(output_dir), 73 | 'app directory should no longer exist') 74 | self.assertTrue(handler.written, 'data should be written') 75 | self.assertTrue(handler.finished, 'response should be finished') 76 | self.assertIn('application/zip', handler.headers['Content-Type'], 77 | 'headers should set zip content type') 78 | self.assertIn('some.zip', handler.headers['Content-Disposition'], 79 | 'headers should name the zip file') 80 | -------------------------------------------------------------------------------- /test/test_server_upload.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Jupyter Development Team. 2 | # Distributed under the terms of the Modified BSD License. 3 | 4 | import copy 5 | import errno 6 | import os 7 | import shutil 8 | import tempfile 9 | import unittest 10 | import zipfile 11 | from os.path import exists, join as pjoin 12 | 13 | import dashboards_bundlers.server_upload as converter 14 | import notebook.bundler.tools 15 | from jupyter_core.paths import jupyter_data_dir 16 | from tornado import web 17 | 18 | dashboard_link = 'http://notebook-server:3000/dashboards/test' 19 | 20 | 21 | class MockResult(object): 22 | def __init__(self, status_code, include_link=True): 23 | self.status_code = status_code 24 | if (include_link): 25 | self.json = lambda: {'link': dashboard_link} 26 | else: 27 | self.json = lambda: {} 28 | 29 | 30 | class MockPost(object): 31 | def __init__(self, status_code, include_result_link=True): 32 | self.args = None 33 | self.kwargs = None 34 | self.status_code = status_code 35 | self.include_result_link = include_result_link 36 | 37 | def __call__(self, *args, **kwargs): 38 | if self.args or self.kwargs: 39 | raise RuntimeError('MockPost already invoked') 40 | self.args = args 41 | self.kwargs = kwargs 42 | return MockResult(self.status_code, self.include_result_link) 43 | 44 | 45 | class MockZipPost(object): 46 | ''' 47 | Explicitly checks for a posted zip file 48 | ''' 49 | def __init__(self, status_code): 50 | self.args = None 51 | self.kwargs = None 52 | self.status_code = status_code 53 | 54 | def __call__(self, *args, **kwargs): 55 | if self.args or self.kwargs: 56 | raise RuntimeError('MockZipPost already invoked') 57 | self.args = args 58 | self.kwargs = kwargs 59 | uploaded_zip = zipfile.ZipFile(kwargs['files']['file'], 'r') 60 | self.zipped_files = uploaded_zip.namelist() 61 | return MockResult(self.status_code) 62 | 63 | 64 | class MockRequest(object): 65 | def __init__(self, host, protocol): 66 | self.host = host 67 | self.protocol = protocol 68 | 69 | 70 | class MockContentsManager(object): 71 | def __init__(self): 72 | self.root_dir = '.' 73 | 74 | 75 | class MockHandler(object): 76 | def __init__(self, host='notebook-server:8888', protocol='http'): 77 | self.settings = { 78 | 'base_url': '/', 79 | 'contents_manager': MockContentsManager() 80 | } 81 | self.request = MockRequest(host, protocol) 82 | self.last_redirect = None 83 | self.tools = notebook.bundler.tools 84 | 85 | def redirect(self, location): 86 | self.last_redirect = location 87 | 88 | 89 | class TestServerUpload(unittest.TestCase): 90 | def setUp(self): 91 | self.origin_env = copy.deepcopy(os.environ) 92 | converter.requests.post = MockPost(200) 93 | 94 | def tearDown(self): 95 | os.environ = self.origin_env 96 | 97 | def test_no_server(self): 98 | '''Should error if no server URL is set.''' 99 | handler = MockHandler('fake-host:8000', 'http') 100 | self.assertRaises(web.HTTPError, converter.bundle, handler, 101 | {'path': 'test/resources/no_imports.ipynb'}) 102 | 103 | def test_upload_notebook(self): 104 | '''Should POST the notebook and redirect to the dashboard server.''' 105 | os.environ['DASHBOARD_SERVER_URL'] = 'http://dashboard-server' 106 | handler = MockHandler() 107 | converter.bundle(handler, {'path': 'test/resources/no_imports.ipynb'}) 108 | 109 | args = converter.requests.post.args 110 | kwargs = converter.requests.post.kwargs 111 | self.assertEqual(args[0], 112 | 'http://dashboard-server/_api/notebooks/no_imports') 113 | self.assertTrue(kwargs['files']['file']) 114 | self.assertEqual(kwargs['headers'], {}) 115 | self.assertEqual(handler.last_redirect, dashboard_link) 116 | 117 | def test_upload_zip(self): 118 | ''' 119 | Should POST the notebook in a zip with resources and redirect to 120 | the dashboard server. 121 | ''' 122 | os.environ['DASHBOARD_SERVER_URL'] = 'http://dashboard-server' 123 | handler = MockHandler() 124 | converter.requests.post = MockZipPost(200) 125 | converter.bundle(handler, {'path': 'test/resources/some.ipynb'}) 126 | 127 | args = converter.requests.post.args 128 | kwargs = converter.requests.post.kwargs 129 | self.assertEqual(args[0], 130 | 'http://dashboard-server/_api/notebooks/some') 131 | self.assertTrue(kwargs['files']['file']) 132 | self.assertEqual(kwargs['headers'], {}) 133 | self.assertEqual(handler.last_redirect, dashboard_link) 134 | self.assertTrue('index.ipynb' in converter.requests.post.zipped_files) 135 | self.assertTrue('some.csv' in converter.requests.post.zipped_files) 136 | 137 | def test_upload_token(self): 138 | '''Should include an auth token in the request.''' 139 | os.environ['DASHBOARD_SERVER_URL'] = 'http://dashboard-server' 140 | os.environ['DASHBOARD_SERVER_AUTH_TOKEN'] = 'fake-token' 141 | handler = MockHandler() 142 | converter.bundle(handler, {'path': 'test/resources/no_imports.ipynb'}) 143 | 144 | kwargs = converter.requests.post.kwargs 145 | self.assertEqual(kwargs['headers'], 146 | {'Authorization': 'token fake-token'}) 147 | 148 | def test_url_interpolation(self): 149 | '''Should build the server URL from the request Host header.''' 150 | os.environ['DASHBOARD_SERVER_URL'] = '{protocol}://{hostname}:8889' 151 | handler = MockHandler('notebook-server:8888', 'https') 152 | converter.bundle(handler, {'path': 'test/resources/no_imports.ipynb'}) 153 | 154 | args = converter.requests.post.args 155 | self.assertEqual(args[0], 156 | 'https://notebook-server:8889/_api/notebooks/no_imports') 157 | self.assertEqual(handler.last_redirect, dashboard_link) 158 | 159 | def test_redirect_fallback(self): 160 | '''Should redirect to the given URL''' 161 | converter.requests.post = MockPost(200, False) 162 | os.environ['DASHBOARD_SERVER_URL'] = '{protocol}://{hostname}:8889' 163 | os.environ['DASHBOARD_REDIRECT_URL'] = 'http://{hostname}:3000' 164 | handler = MockHandler('notebook-server:8888', 'https') 165 | converter.bundle(handler, {'path': 'test/resources/no_imports.ipynb'}) 166 | 167 | args = converter.requests.post.args 168 | self.assertEqual(args[0], 169 | 'https://notebook-server:8889/_api/notebooks/no_imports') 170 | self.assertEqual(handler.last_redirect, 171 | 'http://notebook-server:3000/dashboards/no_imports') 172 | 173 | def test_ssl_verify(self): 174 | '''Should verify SSL certificate by default.''' 175 | handler = MockHandler() 176 | os.environ['DASHBOARD_SERVER_URL'] = '{protocol}://{hostname}:8889' 177 | converter.bundle(handler, {'path': 'test/resources/no_imports.ipynb'}) 178 | kwargs = converter.requests.post.kwargs 179 | self.assertEqual(kwargs['verify'], True) 180 | 181 | def test_no_ssl_verify(self): 182 | '''Should skip SSL certificate verification.''' 183 | os.environ['DASHBOARD_SERVER_NO_SSL_VERIFY'] = 'yes' 184 | os.environ['DASHBOARD_SERVER_URL'] = '{protocol}://{hostname}:8889' 185 | handler = MockHandler() 186 | converter.bundle(handler, {'path': 'test/resources/no_imports.ipynb'}) 187 | kwargs = converter.requests.post.kwargs 188 | self.assertEqual(kwargs['verify'], False) 189 | 190 | 191 | # Mock existence of declarative widgets 192 | DECL_WIDGETS_DIR = pjoin(jupyter_data_dir(), 'nbextensions/urth_widgets/') 193 | DECL_WIDGETS_JS_DIR = pjoin(DECL_WIDGETS_DIR, 'js') 194 | DECL_VIZ_DIR = pjoin(DECL_WIDGETS_DIR, 'components/urth-viz') 195 | DECL_CORE_DIR = pjoin(DECL_WIDGETS_DIR, 'components/urth-core') 196 | BOWER_COMPONENT_DIR = pjoin(jupyter_data_dir(), 197 | 'nbextensions/urth_widgets/urth_components/component-a') 198 | 199 | 200 | class TestBundleWidgets(unittest.TestCase): 201 | @classmethod 202 | def setUpClass(cls): 203 | for d in (DECL_WIDGETS_DIR, DECL_WIDGETS_JS_DIR, DECL_CORE_DIR, 204 | DECL_VIZ_DIR, BOWER_COMPONENT_DIR): 205 | try: 206 | os.makedirs(d) 207 | except OSError as ex: 208 | if ex.errno != errno.EEXIST: 209 | raise 210 | 211 | def setUp(self): 212 | self.tmp = tempfile.mkdtemp() 213 | 214 | def tearDown(self): 215 | shutil.rmtree(self.tmp, ignore_errors=True) 216 | 217 | def test_bundle_declarative_widgets(self): 218 | '''Should write declarative widgets to output.''' 219 | converter.bundle_declarative_widgets(self.tmp, 220 | 'test/resources/env.ipynb') 221 | self.assertTrue(exists(pjoin(self.tmp, 'static/urth_widgets')), 222 | 'urth_widgets should exist') 223 | self.assertTrue(exists(pjoin(self.tmp, 'static/urth_components')), 224 | 'urth_components should exist') 225 | 226 | def test_skip_declarative_widgets(self): 227 | '''Should not write declarative widgets to output.''' 228 | # Testing to make sure we do not add bower components unnecessarily 229 | converter.bundle_declarative_widgets(self.tmp, 230 | 'test/resources/no_imports.ipynb') 231 | self.assertFalse(exists(pjoin(self.tmp, 'static/urth_widgets')), 232 | 'urth_widgets should not exist') 233 | self.assertFalse(exists(pjoin(self.tmp, 'static/urth_components')), 234 | 'urth_components should not exist') 235 | --------------------------------------------------------------------------------