diff --git a/docs/client.rst b/docs/client.rst index aeb59b9b..ee82f6b6 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -17,8 +17,9 @@ Executing notebooks can be very helpful, for example, to run all notebooks in Python library in one step, or as a way to automate the data analysis in projects involving more than one notebook. -Executing notebooks using the Python API interface --------------------------------------------------- +Using the Python API interface +------------------------------ + This section will illustrate the Python API interface. Example @@ -166,3 +167,57 @@ a UI. If you can't view widget results after execution, you may need to select :menuselection:`Trust Notebook` under the :menuselection:`File` menu. + +Using a command-line interface +------------------------------ + +This section will illustrate how to run notebooks from your terminal. It supports the most basic use case. For more sophisticated execution options, consider the `papermill `_ library. + +This library's command line tool is available by running `jupyter execute`. It expects notebooks as input arguments and accepts optional flags to modify the default behavior. + +Running a notebook is this easy.:: + + jupyter execute notebook.ipynb + +You can pass more than one notebook as well.:: + + jupyter execute notebook.ipynb notebook2.ipynb + +By default, notebook errors will be raised and printed into the terminal. You can suppress them by passing the ``--allow-errors`` flag.:: + + jupyter execute notebook.ipynb --allow-errors + +Other options allow you to modify the timeout length and dictate the kernel in use. A full set of options is available via the help command.:: + + jupyter execute --help + + An application used to execute notebook files (*.ipynb) + + Options + ======= + The options below are convenience aliases to configurable class-options, + as listed in the "Equivalent to" description-line of the aliases. + To see all configurable class-options for some , use: + --help-all + + --allow-errors + Errors are ignored and execution is continued until the end of the notebook. + Equivalent to: [--NbClientApp.allow_errors=True] + --timeout= + The time to wait (in seconds) for output from executions. If a cell + execution takes longer, a TimeoutError is raised. ``-1`` will disable the + timeout. + Default: None + Equivalent to: [--NbClientApp.timeout] + --startup_timeout= + The time to wait (in seconds) for the kernel to start. If kernel startup + takes longer, a RuntimeError is raised. + Default: 60 + Equivalent to: [--NbClientApp.startup_timeout] + --kernel_name= + Name of kernel to use to execute the cells. If not set, use the kernel_spec + embedded in the notebook. + Default: '' + Equivalent to: [--NbClientApp.kernel_name] + + To see all available configurables, use `--help-all`. diff --git a/docs/index.rst b/docs/index.rst index e4f68e67..4019c7ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,12 +13,12 @@ Welcome to nbclient --- -**NBClient**, a client library for programmatic notebook execution, is a tool for running Jupyter Notebooks in -different execution contexts. NBClient was spun out of `nbconvert `_'s -former ``ExecutePreprocessor``. - **NBClient** lets you **execute** notebooks. +A client library for programmatic notebook execution, **NBClient** is a tool for running Jupyter Notebooks in +different execution contexts, including the command line. NBClient was spun out of `nbconvert `_'s +former ``ExecutePreprocessor``. + Demo ---- @@ -30,9 +30,7 @@ To demo **NBClient** interactively, click the Binder link below: Origins ------- -This library used to be part of `nbconvert `_ and was extracted into its own -library for easier updating and importing by downstream libraries and -applications. +This library used to be part of `nbconvert `_ and was extracted into its ownlibrary for easier updating and importing by downstream libraries and applications. Python Version Support ---------------------- @@ -63,7 +61,7 @@ this documentation section will help you. .. toctree:: :maxdepth: 3 - :caption: Reference + :caption: Table of Contents reference/index.rst reference/nbclient.tests.rst diff --git a/nbclient/cli.py b/nbclient/cli.py new file mode 100644 index 00000000..02b3d20f --- /dev/null +++ b/nbclient/cli.py @@ -0,0 +1,157 @@ +import logging +import pathlib +import sys +from textwrap import dedent + +import nbformat +from jupyter_core.application import JupyterApp +from traitlets import Bool, Integer, List, Unicode, default +from traitlets.config import catch_config_error + +from nbclient import __version__ + +from .client import NotebookClient + +nbclient_aliases = { + 'timeout': 'NbClientApp.timeout', + 'startup_timeout': 'NbClientApp.startup_timeout', + 'kernel_name': 'NbClientApp.kernel_name', +} + +nbclient_flags = { + 'allow-errors': ( + { + 'NbClientApp': { + 'allow_errors': True, + }, + }, + "Errors are ignored and execution is continued until the end of the notebook.", + ), +} + + +class NbClientApp(JupyterApp): + """ + An application used to execute notebook files (``*.ipynb``) + """ + + version = __version__ + name = 'jupyter-execute' + aliases = nbclient_aliases + flags = nbclient_flags + + description = Unicode("An application used to execute notebook files (*.ipynb)") + notebooks = List([], help="Path of notebooks to convert").tag(config=True) + timeout: int = Integer( + None, + allow_none=True, + help=dedent( + """ + The time to wait (in seconds) for output from executions. + If a cell execution takes longer, a TimeoutError is raised. + ``-1`` will disable the timeout. + """ + ), + ).tag(config=True) + startup_timeout: int = Integer( + 60, + help=dedent( + """ + The time to wait (in seconds) for the kernel to start. + If kernel startup takes longer, a RuntimeError is + raised. + """ + ), + ).tag(config=True) + allow_errors: bool = Bool( + False, + help=dedent( + """ + When a cell raises an error the default behavior is that + execution is stopped and a `CellExecutionError` + is raised. + If this flag is provided, errors are ignored and execution + is continued until the end of the notebook. + """ + ), + ).tag(config=True) + skip_cells_with_tag: str = Unicode( + 'skip-execution', + help=dedent( + """ + Name of the cell tag to use to denote a cell that should be skipped. + """ + ), + ).tag(config=True) + kernel_name: str = Unicode( + '', + help=dedent( + """ + Name of kernel to use to execute the cells. + If not set, use the kernel_spec embedded in the notebook. + """ + ), + ).tag(config=True) + + @default('log_level') + def _log_level_default(self): + return logging.INFO + + @catch_config_error + def initialize(self, argv=None): + super().initialize(argv) + + # Get notebooks to run + self.notebooks = self.get_notebooks() + + # If there are none, throw an error + if not self.notebooks: + print("jupyter-execute: error: expected path to notebook") + sys.exit(-1) + + # Loop and run them one by one + [self.run_notebook(path) for path in self.notebooks] + + def get_notebooks(self): + # If notebooks were provided from the command line, use those + if self.extra_args: + notebooks = self.extra_args + # If not, look to the class attribute + else: + notebooks = self.notebooks + + # Return what we got. + return notebooks + + def run_notebook(self, notebook_path): + # Log it + self.log.info(f"Executing {notebook_path}") + + name = notebook_path.replace(".ipynb", "") + + # Get its parent directory so we can add it to the $PATH + path = pathlib.Path(notebook_path).parent.absolute() + + # Set the intput file paths + input_path = f"{name}.ipynb" + + # Open up the notebook we're going to run + with open(input_path) as f: + nb = nbformat.read(f, as_version=4) + + # Configure nbclient to run the notebook + client = NotebookClient( + nb, + timeout=self.timeout, + startup_timeout=self.startup_timeout, + skip_cells_with_tag=self.skip_cells_with_tag, + allow_errors=self.allow_errors, + kernel_name=self.kernel_name, + resources={'metadata': {'path': path}}, + ) + + # Run it + client.execute() + + +main = NbClientApp.launch_instance diff --git a/nbclient/tests/files/Error.ipynb b/nbclient/tests/files/Error.ipynb new file mode 100644 index 00000000..22c5258b --- /dev/null +++ b/nbclient/tests/files/Error.ipynb @@ -0,0 +1,47 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "d200673b", + "metadata": {}, + "outputs": [ + { + "ename": "ZeroDivisionError", + "evalue": "division by zero", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_1277493/182040962.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;36m0\u001b[0m\u001b[0;34m/\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" + ] + } + ], + "source": [ + "0/0" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/nbclient/tests/files/Skip Execution with Cell Tag.ipynb b/nbclient/tests/files/Skip Execution with Cell Tag.ipynb index 8b9c49a6..e88860e3 100644 --- a/nbclient/tests/files/Skip Execution with Cell Tag.ipynb +++ b/nbclient/tests/files/Skip Execution with Cell Tag.ipynb @@ -34,4 +34,4 @@ "metadata": {}, "nbformat": 4, "nbformat_minor": 1 -} \ No newline at end of file +} diff --git a/setup.py b/setup.py index 1d276068..dcf2824c 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,11 @@ def read_reqs(fname): python_requires=">=3.6.1", install_requires=requirements, extras_require=extras_require, + entry_points={ + 'console_scripts': [ + 'jupyter-execute = nbclient.cli:main', + ], + }, project_urls={ 'Documentation': 'https://nbclient.readthedocs.io', 'Funding': 'https://numfocus.org/',