|
| 1 | +Custom bundler extensions |
| 2 | +========================= |
| 3 | + |
| 4 | +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. |
| 5 | + |
| 6 | +Here are some examples of what you can implement using bundler extensions: |
| 7 | + |
| 8 | +* Convert a notebook file to a HTML document and publish it as a post on a blog site |
| 9 | +* Create a snapshot of the current notebook environment and bundle that definition plus notebook into a zip download |
| 10 | +* `Deploy a notebook as a standalone, interactive dashboard <https://github.com/jupyter-incubator/dashboards_bundlers>`_ |
| 11 | + |
| 12 | +To implement a bundler extension, you must do all of the following: |
| 13 | + |
| 14 | +* Declare bundler extension metadata in your Python package |
| 15 | +* Write a `bundle` function that responds to bundle requests |
| 16 | +* Instruct your users on how to enable/disable your bundler extension |
| 17 | + |
| 18 | +The following sections describe these steps in detail. |
| 19 | + |
| 20 | +Declaring bundler metadata |
| 21 | +-------------------------- |
| 22 | + |
| 23 | +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`.) |
| 24 | + |
| 25 | +.. code:: python |
| 26 | +
|
| 27 | + # in mypackage.hello_bundler |
| 28 | +
|
| 29 | + def _jupyter_bundlerextension_paths(): |
| 30 | + """Example "hello world" bundler extension""" |
| 31 | + return [{ |
| 32 | + 'name': 'hello_bundler', # unique bundler name |
| 33 | + 'label': 'Hello Bundler', # human-redable menu item label |
| 34 | + 'module_name': 'mypackage.hello_bundler', # module containing bundle() |
| 35 | + 'group': 'deploy' # group under 'deploy' or 'download' menu |
| 36 | + }] |
| 37 | + |
| 38 | +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. |
| 39 | + |
| 40 | +Writing the `bundle` function |
| 41 | +----------------------------- |
| 42 | + |
| 43 | +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. |
| 44 | + |
| 45 | +You must implement a `bundle` function that matches the signature of the following example: |
| 46 | + |
| 47 | +.. code:: python |
| 48 | +
|
| 49 | + # in mypackage.hello_bundler |
| 50 | +
|
| 51 | + def bundle(handler, model): |
| 52 | + """Transform, convert, bundle, etc. the notebook referenced by the given |
| 53 | + model. |
| 54 | + |
| 55 | + Then issue a Tornado web response using the `handler` to redirect |
| 56 | + the user's browser, download a file, show a HTML page, etc. This function |
| 57 | + must finish the handler response before returning either explicitly or by |
| 58 | + raising an exception. |
| 59 | + |
| 60 | + Parameters |
| 61 | + ---------- |
| 62 | + handler : tornado.web.RequestHandler |
| 63 | + Handler that serviced the bundle request |
| 64 | + model : dict |
| 65 | + Notebook model from the configured ContentManager |
| 66 | + """ |
| 67 | + handler.finish('I bundled {}!'.format(model['path'])) |
| 68 | + |
| 69 | +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. |
| 70 | + |
| 71 | +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. |
| 72 | + |
| 73 | +.. code:: python |
| 74 | +
|
| 75 | + from tornado import gen |
| 76 | +
|
| 77 | + @gen.coroutine |
| 78 | + def bundle(handler, model): |
| 79 | + # simulate a long running IO op (e.g., deploying to a remote host) |
| 80 | + yield gen.sleep(10) |
| 81 | +
|
| 82 | + # now respond |
| 83 | + handler.finish('I spent 10 seconds bundling {}!'.format(model['path'])) |
| 84 | +
|
| 85 | +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. |
| 86 | + |
| 87 | +For more details about the data flow from menu item click to bundle function invocation, see :ref:`bundler-details`. |
| 88 | + |
| 89 | +.. _enabling-bundlers: |
| 90 | + |
| 91 | +Enabling/disabling bundler extensions |
| 92 | +------------------------------------- |
| 93 | + |
| 94 | +The notebook server includes a command line interface (CLI) for enabling and disabling bundler extensions. |
| 95 | + |
| 96 | +You should document the basic commands for enabling and disabling your bundler. One possible command for enabling the `hello_bundler` example is the following: |
| 97 | + |
| 98 | +.. code:: bash |
| 99 | +
|
| 100 | + jupyter bundlerextension enable --py mypackage.hello_bundler --sys-prefix |
| 101 | + |
| 102 | +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. |
| 103 | + |
| 104 | +The corresponding command to later disable the bundler extension is the following: |
| 105 | + |
| 106 | +.. code:: bash |
| 107 | +
|
| 108 | + jupyter bundlerextension disable --py mypackage.hello_bundler --sys-prefix |
| 109 | +
|
| 110 | +For more help using the `bundlerextension` subcommand, run the following. |
| 111 | + |
| 112 | +.. code:: bash |
| 113 | +
|
| 114 | + jupyter bundlerextension --help |
| 115 | + |
| 116 | +The output describes options for listing enabled bundlers, configuring bundlers for single users, configuring bundlers system-wide, etc. |
| 117 | + |
| 118 | +Example: IPython Notebook bundle (.zip) |
| 119 | +--------------------------------------- |
| 120 | + |
| 121 | +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: |
| 122 | + |
| 123 | +.. code:: bash |
| 124 | +
|
| 125 | + jupyter bundlerextension enable --py notebook.bundler.zip_bundler --sys-prefix |
| 126 | + jupyter bundlerextension enable --py notebook.bundler.tarball_bundler --sys-prefix |
| 127 | +
|
| 128 | +.. _bundler-details: |
| 129 | + |
| 130 | +Bundler invocation details |
| 131 | +-------------------------- |
| 132 | + |
| 133 | +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: |
| 134 | + |
| 135 | +1. User opens a notebook document |
| 136 | +2. Notebook front-end JavaScript loads notebook configuration |
| 137 | +3. Bundler front-end JS creates menu items for all bundler extensions in the config |
| 138 | +4. User clicks a bundler menu item |
| 139 | +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) |
| 140 | +6. Bundle handler validates the notebook path and bundler `name` |
| 141 | +7. Bundle handler delegates the request to the `bundle` function in the bundler's `module_name` |
| 142 | +8. `bundle` function finishes the HTTP request |
0 commit comments