Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bundler extension API #1579

Merged
merged 15 commits into from
Aug 13, 2016
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
" [AMD modules](https://en.wikipedia.org/wiki/Asynchronous_module_definition)\n",
" that exports a function `load_ipython_extension`\n",
"- server extension: an importable Python module\n",
" - that implements `load_jupyter_server_extension`"
" - that implements `load_jupyter_server_extension`\n",
"- bundler extension: an importable Python module with generated File -> Download as / Deploy as menu item trigger\n",
" - that implements `bundle`"
]
},
{
Expand Down Expand Up @@ -105,11 +107,12 @@
"metadata": {},
"source": [
"## Did it work? Check by listing Jupyter Extensions.\n",
"After running one or more extension installation steps, you can list what is presently known about nbextensions or server extension. The following commands will list which extensions are available, whether they are enabled, and other extension details:\n",
"After running one or more extension installation steps, you can list what is presently known about nbextensions, server extensions, or bundler extensions. The following commands will list which extensions are available, whether they are enabled, and other extension details:\n",
"\n",
"```shell\n",
"jupyter nbextension list\n",
"jupyter serverextension list\n",
"jupyter bundlerextension list\n",
"```"
]
},
Expand Down Expand Up @@ -255,6 +258,98 @@
"jupyter serverextension enable --py my_fancy_module [--sys-prefix|--system]\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example - Bundler extension"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Creating a Python package with a bundlerextension\n",
"\n",
"Here is a bundler extension that adds a *Download as -> Notebook Tarball (tar.gz)* option to the notebook *File* menu. It assumes this directory structure:\n",
"\n",
"```\n",
"- setup.py\n",
"- MANIFEST.in\n",
"- my_tarball_bundler/\n",
" - __init__.py\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Defining the bundler extension\n",
"\n",
"This example shows that the bundler extension and its `bundle` function are defined in the `__init__.py` file.\n",
"\n",
"#### `my_tarball_bundler/__init__.py`\n",
"\n",
"```python\n",
"import tarfile\n",
"import io\n",
"import os\n",
"import nbformat\n",
"\n",
"def _jupyter_bundlerextension_paths():\n",
" \"\"\"Declare bundler extensions provided by this package.\"\"\"\n",
" return [{\n",
" # unique bundler name\n",
" \"name\": \"tarball_bundler\",\n",
" # module containing bundle function\n",
" \"module_name\": \"my_tarball_bundler\",\n",
" # human-redable menu item label\n",
" \"label\" : \"Notebook Tarball (tar.gz)\",\n",
" # group under 'deploy' or 'download' menu\n",
" \"group\" : \"download\",\n",
" }]\n",
"\n",
"\n",
"def bundle(handler, model):\n",
" \"\"\"Create a compressed tarball containing the notebook document.\n",
" \n",
" Parameters\n",
" ----------\n",
" handler : tornado.web.RequestHandler\n",
" Handler that serviced the bundle request\n",
" model : dict\n",
" Notebook model from the configured ContentManager\n",
" \"\"\"\n",
" notebook_filename = model['name']\n",
" notebook_content = nbformat.writes(model['content']).encode('utf-8')\n",
" notebook_name = os.path.splitext(notebook_filename)[0]\n",
" tar_filename = '{}.tar.gz'.format(notebook_name)\n",
" \n",
" info = tarfile.TarInfo(notebook_filename)\n",
" info.size = len(notebook_content)\n",
"\n",
" with io.BytesIO() as tar_buffer:\n",
" with tarfile.open(tar_filename, \"w:gz\", fileobj=tar_buffer) as tar:\n",
" tar.addfile(info, io.BytesIO(notebook_content))\n",
" \n",
" # Set headers to trigger browser download\n",
" handler.set_header('Content-Disposition',\n",
" 'attachment; filename=\"{}\"'.format(tar_filename))\n",
" handler.set_header('Content-Type', 'application/gzip')\n",
" \n",
" # Return the buffer value as the response\n",
" handler.finish(tar_buffer.getvalue())\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"See [Extending the Notebook](../../extending) for more documentation about writing nbextensions, server extensions, and bundler extensions."
]
}
],
"metadata": {
Expand All @@ -277,5 +372,5 @@
}
},
"nbformat": 4,
"nbformat_minor": 0
"nbformat_minor": 1
}
142 changes: 142 additions & 0 deletions docs/source/extending/bundler_extensions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
Custom bundler extensions
=========================

The notebook server supports the writing of *bundler extensions* that transform, package, and download/deploy notebook files. As a developer, you need only write a single Python function to implement a bundler. The notebook server automatically generates a *File -> Download as* or *File -> Deploy as* menu item in the notebook front-end to trigger your bundler.

Here are some examples of what you can implement using bundler extensions:

* Convert a notebook file to a HTML document and publish it as a post on a blog site
* Create a snapshot of the current notebook environment and bundle that definition plus notebook into a zip download
* `Deploy a notebook as a standalone, interactive dashboard <https://github.com/jupyter-incubator/dashboards_bundlers>`_

To implement a bundler extension, you must do all of the following:

* Declare bundler extension metadata in your Python package
* Write a `bundle` function that responds to bundle requests
* Instruct your users on how to enable/disable your bundler extension

The following sections describe these steps in detail.

Declaring bundler metadata
--------------------------

You must provide information about the bundler extension(s) your package provides by implementing a `_jupyter_bundlerextensions_paths` function. This function can reside anywhere in your package so long as it can be imported when enabling the bundler extension. (See :ref:`enabling-bundlers`.)

.. code:: python

# in mypackage.hello_bundler

def _jupyter_bundlerextension_paths():
"""Example "hello world" bundler extension"""
return [{
'name': 'hello_bundler', # unique bundler name
'label': 'Hello Bundler', # human-redable menu item label
'module_name': 'mypackage.hello_bundler', # module containing bundle()
'group': 'deploy' # group under 'deploy' or 'download' menu
}]

Note that the return value is a list. By returning multiple dictionaries in the list, you allow users to enable/disable sets of bundlers all at once.

Writing the `bundle` function
-----------------------------

At runtime, a menu item with the given label appears either in the *File -> Deploy as* or *File -> Download as* menu depending on the `group` value in your metadata. When a user clicks the menu item, a new browser tab opens and notebook server invokes a `bundle` function in the `module_name` specified in the metadata.

You must implement a `bundle` function that matches the signature of the following example:

.. code:: python

# in mypackage.hello_bundler

def bundle(handler, model):
"""Transform, convert, bundle, etc. the notebook referenced by the given
model.

Then issue a Tornado web response using the `handler` to redirect
the user's browser, download a file, show a HTML page, etc. This function
must finish the handler response before returning either explicitly or by
raising an exception.

Parameters
----------
handler : tornado.web.RequestHandler
Handler that serviced the bundle request
model : dict
Notebook model from the configured ContentManager
"""
handler.finish('I bundled {}!'.format(model['path']))

Your `bundle` function is free to do whatever it wants with the request and respond in any manner. For example, it may read additional query parameters from the request, issue a redirect to another site, run a local process (e.g., `nbconvert`), make a HTTP request to another service, etc.

The caller of the `bundle` function is `@tornado.gen.coroutine` decorated and wraps its call with `torando.gen.maybe_future`. This behavior means you may handle the web request synchronously, as in the example above, or asynchronously using `@tornado.gen.coroutine` and `yield`, as in the example below.

.. code:: python

from tornado import gen

@gen.coroutine
def bundle(handler, model):
# simulate a long running IO op (e.g., deploying to a remote host)
yield gen.sleep(10)

# now respond
handler.finish('I spent 10 seconds bundling {}!'.format(model['path']))

You should prefer the second, asynchronous approach when your bundle operation is long-running and would otherwise block the notebook server main loop if handled synchronously.

For more details about the data flow from menu item click to bundle function invocation, see :ref:`bundler-details`.

.. _enabling-bundlers:

Enabling/disabling bundler extensions
-------------------------------------

The notebook server includes a command line interface (CLI) for enabling and disabling bundler extensions.

You should document the basic commands for enabling and disabling your bundler. One possible command for enabling the `hello_bundler` example is the following:

.. code:: bash

jupyter bundlerextension enable --py mypackage.hello_bundler --sys-prefix

The above updates the notebook configuration file in the current conda/virtualenv environment (`--sys-prefix`) with the metadata returned by the `mypackage.hellow_bundler._jupyter_bundlerextension_paths` function.

The corresponding command to later disable the bundler extension is the following:

.. code:: bash

jupyter bundlerextension disable --py mypackage.hello_bundler --sys-prefix

For more help using the `bundlerextension` subcommand, run the following.

.. code:: bash

jupyter bundlerextension --help

The output describes options for listing enabled bundlers, configuring bundlers for single users, configuring bundlers system-wide, etc.

Example: IPython Notebook bundle (.zip)
---------------------------------------

The `hello_bundler` example in this documentation is simplisitic in the name of brevity. For more meaningful examples, see `notebook/bundler/zip_bundler.py` and `notebook/bundler/tarball_bundler.py`. You can enable them to try them like so:

.. code:: bash

jupyter bundlerextension enable --py notebook.bundler.zip_bundler --sys-prefix
jupyter bundlerextension enable --py notebook.bundler.tarball_bundler --sys-prefix

.. _bundler-details:

Bundler invocation details
--------------------------

Support for bundler extensions comes from Python modules in `notebook/bundler` and JavaScript in `notebook/static/notebook/js/menubar.js`. The flow of data between the various components proceeds roughly as follows:

1. User opens a notebook document
2. Notebook front-end JavaScript loads notebook configuration
3. Bundler front-end JS creates menu items for all bundler extensions in the config
4. User clicks a bundler menu item
5. JS click handler opens a new browser window/tab to `<notebook base_url>/bundle/<path/to/notebook>?bundler=<name>` (i.e., a HTTP GET request)
6. Bundle handler validates the notebook path and bundler `name`
7. Bundle handler delegates the request to the `bundle` function in the bundler's `module_name`
8. `bundle` function finishes the HTTP request
2 changes: 1 addition & 1 deletion docs/source/extending/handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,4 @@ following:

References:
1. `Peter Parente's
Mindtrove <http://mindtrove.info/#nb-server-exts>`__
Mindtrove <http://mindtrove.info/4-ways-to-extend-jupyter-notebook/#nb-server-exts>`__
1 change: 1 addition & 0 deletions docs/source/extending/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ override the notebook's defaults with your own custom behavior.
handlers
frontend_extensions
keymaps
bundler_extensions
Empty file added notebook/bundler/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions notebook/bundler/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from .bundlerextensions import main

if __name__ == '__main__':
main()
Loading