From db291937d6afa98490b659f445866ff6c43929fe Mon Sep 17 00:00:00 2001 From: palewire Date: Sun, 26 Sep 2021 17:03:47 -0700 Subject: [PATCH 01/26] Added experimental CLI for #4 --- nbclient/cli.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + setup.py | 4 +++ 3 files changed, 93 insertions(+) create mode 100644 nbclient/cli.py diff --git a/nbclient/cli.py b/nbclient/cli.py new file mode 100644 index 00000000..b1989481 --- /dev/null +++ b/nbclient/cli.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +""" +A command-line interface for running Jupyter Notebooks. +Usage: cli.py [OPTIONS] [NOTEBOOK_PATHS]... + Executes Jupyter Notebooks from the command line. + + Expects one or more file paths input as arguments. + + Errors are raised and printed to the console. + + Example: + $ python run.py ./src/notebooks.ipynb + + Options: + --help Show this message and exit. +""" +import click +import pathlib +import nbformat +from .client import NotebookClient +from .exceptions import CellExecutionError + + +@click.group(help="Run Jupyter Notebooks from the command line") +def cli(): + pass + + +@cli.command(help="Execute a notebook") +@click.argument('notebook_path', nargs=1, type=click.Path(exists=True)) +@click.option('-o', '--output', default=None, help='Where to output the result') +@click.option('-t', '--timeout', default=600, help='How long the script should run before it times out') +@click.option('--allow-errors/--no-allow-errors', default=False) +@click.option('--force-raise-errors/--no-force-raise-errors', default=True) +def execute(notebook_path, output, timeout, allow_errors, force_raise_errors): + """ + Executes Jupyter Notebooks from the command line. + + Expects one or more file paths input as arguments. + + Errors are raised and printed to the console. + + Example: + + $ python run.py ./src/notebooks.ipynb + """ + # Get the file name + 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( + # What we want it to run + nb, + timeout=timeout, + kernel_name='python3', + allow_errors=allow_errors, + force_raise_errors=force_raise_errors, + # Here's where the path gets set + resources={'metadata': {'path': path}} + ) + try: + # Run it + client.execute() + except CellExecutionError: + # If there's an error, print it to the terminal. + msg = f"Error executing {input_path}.\n" + click.echo(msg) + # And then raise it too + raise + finally: + if output: + # Once all that's done, write out the output notebook to the filesystem + with open(output, mode='w', encoding='utf-8') as f: + nbformat.write(nb, f) + + +if __name__ == '__main__': + cli() diff --git a/requirements.txt b/requirements.txt index fe1554de..74f4a0d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ jupyter_client>=6.1.5 nbformat>=5.0 async_generator; python_version<'3.7' nest_asyncio +click diff --git a/setup.py b/setup.py index 1d276068..bebf3c78 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,10 @@ def read_reqs(fname): python_requires=">=3.6.1", install_requires=requirements, extras_require=extras_require, + entry_points=""" + [console_scripts] + nbclient=nbclient.cli:cli + """, project_urls={ 'Documentation': 'https://nbclient.readthedocs.io', 'Funding': 'https://numfocus.org/', From b72e73825ec24c930934dba0119d16aced6888ae Mon Sep 17 00:00:00 2001 From: palewire Date: Sun, 26 Sep 2021 17:18:18 -0700 Subject: [PATCH 02/26] Lint fix --- nbclient/cli.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index b1989481..5c5b3d73 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -14,6 +14,7 @@ Options: --help Show this message and exit. """ + import click import pathlib import nbformat @@ -29,7 +30,9 @@ def cli(): @cli.command(help="Execute a notebook") @click.argument('notebook_path', nargs=1, type=click.Path(exists=True)) @click.option('-o', '--output', default=None, help='Where to output the result') -@click.option('-t', '--timeout', default=600, help='How long the script should run before it times out') +@click.option( + '-t', '--timeout', default=600, help='How long the script should run before it times out' +) @click.option('--allow-errors/--no-allow-errors', default=False) @click.option('--force-raise-errors/--no-force-raise-errors', default=True) def execute(notebook_path, output, timeout, allow_errors, force_raise_errors): @@ -59,14 +62,12 @@ def execute(notebook_path, output, timeout, allow_errors, force_raise_errors): # Configure nbclient to run the notebook client = NotebookClient( - # What we want it to run nb, timeout=timeout, kernel_name='python3', allow_errors=allow_errors, force_raise_errors=force_raise_errors, - # Here's where the path gets set - resources={'metadata': {'path': path}} + resources={'metadata': {'path': path}}, ) try: # Run it @@ -86,3 +87,4 @@ def execute(notebook_path, output, timeout, allow_errors, force_raise_errors): if __name__ == '__main__': cli() + From 6b418eebaa41cc6ad524a6929c7eca03193a2b2b Mon Sep 17 00:00:00 2001 From: palewire Date: Sun, 26 Sep 2021 17:18:59 -0700 Subject: [PATCH 03/26] Lint fix --- nbclient/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 5c5b3d73..8fb537ef 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- """ A command-line interface for running Jupyter Notebooks. -Usage: cli.py [OPTIONS] [NOTEBOOK_PATHS]... + +Usage: nblclient [OPTIONS] [NOTEBOOK_PATHS]... Executes Jupyter Notebooks from the command line. Expects one or more file paths input as arguments. @@ -9,7 +9,7 @@ Errors are raised and printed to the console. Example: - $ python run.py ./src/notebooks.ipynb + $ nbclient execute ./src/notebooks.ipynb Options: --help Show this message and exit. @@ -45,7 +45,7 @@ def execute(notebook_path, output, timeout, allow_errors, force_raise_errors): Example: - $ python run.py ./src/notebooks.ipynb + $ nbclient execute ./src/notebooks.ipynb """ # Get the file name name = notebook_path.replace(".ipynb", "") From 403b9906b85a91b4966428159ae67926294b45a1 Mon Sep 17 00:00:00 2001 From: palewire Date: Sun, 26 Sep 2021 17:30:32 -0700 Subject: [PATCH 04/26] black format --- nbclient/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 8fb537ef..9373d631 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -87,4 +87,3 @@ def execute(notebook_path, output, timeout, allow_errors, force_raise_errors): if __name__ == '__main__': cli() - From 22503d3814a9edd1bcdc234df2c57a4c254f9646 Mon Sep 17 00:00:00 2001 From: palewire Date: Sun, 26 Sep 2021 17:32:40 -0700 Subject: [PATCH 05/26] again --- nbclient/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 9373d631..397befdc 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -15,9 +15,11 @@ --help Show this message and exit. """ -import click import pathlib + +import click import nbformat + from .client import NotebookClient from .exceptions import CellExecutionError From c840fedbf3baea2b11e829db5a56e80d036f7dcc Mon Sep 17 00:00:00 2001 From: palewire Date: Tue, 28 Sep 2021 05:44:41 -0700 Subject: [PATCH 06/26] Switched to a traitlets style CLI draft --- nbclient/cli.py | 138 ++++++++++++++++++++++++----------------------- requirements.txt | 1 - setup.py | 9 ++-- 3 files changed, 77 insertions(+), 71 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 397befdc..6902d1fa 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -16,76 +16,82 @@ """ import pathlib - -import click +import logging import nbformat - +from nbclient import __version__ from .client import NotebookClient from .exceptions import CellExecutionError +from traitlets import default, Unicode, List +from traitlets.config import catch_config_error +from jupyter_core.application import JupyterApp -@click.group(help="Run Jupyter Notebooks from the command line") -def cli(): - pass - - -@cli.command(help="Execute a notebook") -@click.argument('notebook_path', nargs=1, type=click.Path(exists=True)) -@click.option('-o', '--output', default=None, help='Where to output the result') -@click.option( - '-t', '--timeout', default=600, help='How long the script should run before it times out' -) -@click.option('--allow-errors/--no-allow-errors', default=False) -@click.option('--force-raise-errors/--no-force-raise-errors', default=True) -def execute(notebook_path, output, timeout, allow_errors, force_raise_errors): +class NbClientApp(JupyterApp): """ - Executes Jupyter Notebooks from the command line. - - Expects one or more file paths input as arguments. - - Errors are raised and printed to the console. - - Example: - - $ nbclient execute ./src/notebooks.ipynb + An application used to execute a notebook files (``*.ipynb``) """ - # Get the file name - 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=timeout, - kernel_name='python3', - allow_errors=allow_errors, - force_raise_errors=force_raise_errors, - resources={'metadata': {'path': path}}, - ) - try: - # Run it - client.execute() - except CellExecutionError: - # If there's an error, print it to the terminal. - msg = f"Error executing {input_path}.\n" - click.echo(msg) - # And then raise it too - raise - finally: - if output: - # Once all that's done, write out the output notebook to the filesystem - with open(output, mode='w', encoding='utf-8') as f: - nbformat.write(nb, f) - - -if __name__ == '__main__': - cli() + version = __version__ + name = 'jupyter-execute' + + description = Unicode("An application used to execute a notebook files (*.ipynb)") + notebook = List( + [], + help="Path of notebooks to convert" + ).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) + self.notebooks = self.get_notebooks() + for notebook_path in self.notebooks: + self.run_notebook(notebook_path) + + def get_notebooks(self): + if self.extra_args: + notebooks = self.extra_args + else: + notebooks = self.notebooks + return notebooks + + def run_notebook(self, notebook_path): + # Log it + self.log.info(f"Executing {notebook_path}") + + # Get the file name + 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=600, + kernel_name='python3', + allow_errors=False, + force_raise_errors=True, + resources={'metadata': {'path': path}}, + ) + try: + # Run it + client.execute() + except CellExecutionError: + # If there's an error, print it to the terminal. + msg = f"Error executing {input_path}.\n" + self.log.error(msg) + # And then raise it too + raise + + +main = launch_new_instance = NbClientApp.launch_instance diff --git a/requirements.txt b/requirements.txt index 74f4a0d7..fe1554de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,3 @@ jupyter_client>=6.1.5 nbformat>=5.0 async_generator; python_version<'3.7' nest_asyncio -click diff --git a/setup.py b/setup.py index bebf3c78..dcf2824c 100644 --- a/setup.py +++ b/setup.py @@ -57,10 +57,11 @@ def read_reqs(fname): python_requires=">=3.6.1", install_requires=requirements, extras_require=extras_require, - entry_points=""" - [console_scripts] - nbclient=nbclient.cli:cli - """, + entry_points={ + 'console_scripts': [ + 'jupyter-execute = nbclient.cli:main', + ], + }, project_urls={ 'Documentation': 'https://nbclient.readthedocs.io', 'Funding': 'https://numfocus.org/', From 25bef1e5133b7777ee97c0d27f4c41da4a53bb9c Mon Sep 17 00:00:00 2001 From: palewire Date: Tue, 28 Sep 2021 05:53:22 -0700 Subject: [PATCH 07/26] Trim --- nbclient/cli.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 6902d1fa..bf59426d 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -1,20 +1,3 @@ -""" -A command-line interface for running Jupyter Notebooks. - -Usage: nblclient [OPTIONS] [NOTEBOOK_PATHS]... - Executes Jupyter Notebooks from the command line. - - Expects one or more file paths input as arguments. - - Errors are raised and printed to the console. - - Example: - $ nbclient execute ./src/notebooks.ipynb - - Options: - --help Show this message and exit. -""" - import pathlib import logging import nbformat From 49c3255949e483d6d17d6f796e698b3c13dac6c2 Mon Sep 17 00:00:00 2001 From: palewire Date: Tue, 28 Sep 2021 05:54:08 -0700 Subject: [PATCH 08/26] Trim --- nbclient/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index bf59426d..726ef104 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -71,7 +71,7 @@ def run_notebook(self, notebook_path): client.execute() except CellExecutionError: # If there's an error, print it to the terminal. - msg = f"Error executing {input_path}.\n" + msg = f"Error executing {input_path}" self.log.error(msg) # And then raise it too raise From 14ddeb4ab1e4ad4c0837c6d1b5a8cb12890ae6f9 Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 30 Sep 2021 05:39:04 -0700 Subject: [PATCH 09/26] Added more input options and a notebook that can be used to test error handling --- binder/error_notebook.ipynb | 47 +++++++++++++++++++ nbclient/cli.py | 93 ++++++++++++++++++++++++++++++++++--- 2 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 binder/error_notebook.ipynb diff --git a/binder/error_notebook.ipynb b/binder/error_notebook.ipynb new file mode 100644 index 00000000..22c5258b --- /dev/null +++ b/binder/error_notebook.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/cli.py b/nbclient/cli.py index 726ef104..52b3579b 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -1,12 +1,38 @@ import pathlib import logging import nbformat +from textwrap import dedent + from nbclient import __version__ from .client import NotebookClient from .exceptions import CellExecutionError -from traitlets import default, Unicode, List + from traitlets.config import catch_config_error -from jupyter_core.application import JupyterApp +from traitlets import default, Unicode, List, Integer, Bool + +from jupyter_core.application import JupyterApp, base_aliases, base_flags + + +nbclient_aliases = {} +nbclient_aliases.update(base_aliases) +nbclient_aliases.update({ + 'timeout' : 'NbClientApp.timeout', + 'startup_timeout' : 'NbClientApp.startup_timeout', + 'kernel_name' : 'NbClientApp.kernel_name', +}) + +nbclient_flags = {} +nbclient_flags.update(base_flags) +nbclient_flags.update({ + 'allow-errors' : ( + { + 'NbClientApp' : { + 'allow_errors' : True, + }, + }, + "Errors are ignored and execution is continued until the end of the notebook." + ), +}) class NbClientApp(JupyterApp): @@ -15,12 +41,64 @@ class NbClientApp(JupyterApp): """ version = __version__ name = 'jupyter-execute' + aliases = nbclient_aliases + flags = nbclient_flags description = Unicode("An application used to execute a notebook files (*.ipynb)") notebook = 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): @@ -43,7 +121,7 @@ def get_notebooks(self): def run_notebook(self, notebook_path): # Log it self.log.info(f"Executing {notebook_path}") - + # Get the file name name = notebook_path.replace(".ipynb", "") @@ -60,10 +138,11 @@ def run_notebook(self, notebook_path): # Configure nbclient to run the notebook client = NotebookClient( nb, - timeout=600, - kernel_name='python3', - allow_errors=False, - force_raise_errors=True, + 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}}, ) try: From 2f1cdc7ed31586da4b969d130ec58758b40bfa45 Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 30 Sep 2021 05:47:01 -0700 Subject: [PATCH 10/26] black --- nbclient/cli.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 52b3579b..cb8643db 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -15,40 +15,42 @@ nbclient_aliases = {} nbclient_aliases.update(base_aliases) -nbclient_aliases.update({ - 'timeout' : 'NbClientApp.timeout', - 'startup_timeout' : 'NbClientApp.startup_timeout', - 'kernel_name' : 'NbClientApp.kernel_name', -}) +nbclient_aliases.update( + { + 'timeout': 'NbClientApp.timeout', + 'startup_timeout': 'NbClientApp.startup_timeout', + 'kernel_name': 'NbClientApp.kernel_name', + } +) nbclient_flags = {} nbclient_flags.update(base_flags) -nbclient_flags.update({ - 'allow-errors' : ( - { - 'NbClientApp' : { - 'allow_errors' : True, +nbclient_flags.update( + { + 'allow-errors': ( + { + 'NbClientApp': { + 'allow_errors': True, + }, }, - }, - "Errors are ignored and execution is continued until the end of the notebook." + "Errors are ignored and execution is continued until the end of the notebook.", ), -}) + } +) class NbClientApp(JupyterApp): """ An application used to execute a notebook files (``*.ipynb``) """ + version = __version__ name = 'jupyter-execute' aliases = nbclient_aliases flags = nbclient_flags description = Unicode("An application used to execute a notebook files (*.ipynb)") - notebook = List( - [], - help="Path of notebooks to convert" - ).tag(config=True) + notebook = List([], help="Path of notebooks to convert").tag(config=True) timeout: int = Integer( None, allow_none=True, From cd12a23ecadbd0b32f3451b5ea12cfcab7a29528 Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 30 Sep 2021 05:52:22 -0700 Subject: [PATCH 11/26] lint --- nbclient/cli.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index cb8643db..bb93504d 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -1,17 +1,17 @@ -import pathlib import logging -import nbformat +import pathlib from textwrap import dedent +import nbformat +from jupyter_core.application import JupyterApp, base_aliases, base_flags +from traitlets import default, Unicode, List, Integer, Bool +from traitlets.config import catch_config_error + from nbclient import __version__ + from .client import NotebookClient from .exceptions import CellExecutionError -from traitlets.config import catch_config_error -from traitlets import default, Unicode, List, Integer, Bool - -from jupyter_core.application import JupyterApp, base_aliases, base_flags - nbclient_aliases = {} nbclient_aliases.update(base_aliases) From 49ad0051922765c80aebfec2ca9d1c00e497063c Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 30 Sep 2021 05:56:17 -0700 Subject: [PATCH 12/26] More --- nbclient/cli.py | 3 +-- binder/error_notebook.ipynb => nbclient/tests/Error.ipynb | 0 2 files changed, 1 insertion(+), 2 deletions(-) rename binder/error_notebook.ipynb => nbclient/tests/Error.ipynb (100%) diff --git a/nbclient/cli.py b/nbclient/cli.py index bb93504d..2e97dc22 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -4,7 +4,7 @@ import nbformat from jupyter_core.application import JupyterApp, base_aliases, base_flags -from traitlets import default, Unicode, List, Integer, Bool +from traitlets import Bool, Integer, List, Unicode, default from traitlets.config import catch_config_error from nbclient import __version__ @@ -12,7 +12,6 @@ from .client import NotebookClient from .exceptions import CellExecutionError - nbclient_aliases = {} nbclient_aliases.update(base_aliases) nbclient_aliases.update( diff --git a/binder/error_notebook.ipynb b/nbclient/tests/Error.ipynb similarity index 100% rename from binder/error_notebook.ipynb rename to nbclient/tests/Error.ipynb From 1d95ac880f270eb50ed382bc7ba28bbc456b7217 Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 30 Sep 2021 06:00:11 -0700 Subject: [PATCH 13/26] Trim --- nbclient/cli.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 2e97dc22..ec08ce2d 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -146,15 +146,7 @@ def run_notebook(self, notebook_path): kernel_name=self.kernel_name, resources={'metadata': {'path': path}}, ) - try: - # Run it - client.execute() - except CellExecutionError: - # If there's an error, print it to the terminal. - msg = f"Error executing {input_path}" - self.log.error(msg) - # And then raise it too - raise + client.execute() main = launch_new_instance = NbClientApp.launch_instance From 7f12d0eeec854a824055bda98810964bba5932c5 Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 30 Sep 2021 06:00:50 -0700 Subject: [PATCH 14/26] docstring --- nbclient/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbclient/cli.py b/nbclient/cli.py index ec08ce2d..f43af9c3 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -146,6 +146,8 @@ def run_notebook(self, notebook_path): kernel_name=self.kernel_name, resources={'metadata': {'path': path}}, ) + + # Run it client.execute() From bae30d85e33b4dc5f7da5664a57355c56019ff2a Mon Sep 17 00:00:00 2001 From: palewire Date: Fri, 1 Oct 2021 06:29:44 -0700 Subject: [PATCH 15/26] Fixed notebook variable name --- nbclient/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index f43af9c3..8652da2c 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -49,7 +49,7 @@ class NbClientApp(JupyterApp): flags = nbclient_flags description = Unicode("An application used to execute a notebook files (*.ipynb)") - notebook = List([], help="Path of notebooks to convert").tag(config=True) + notebooks = List([], help="Path of notebooks to convert").tag(config=True) timeout: int = Integer( None, allow_none=True, From 6f97a0659ec23a95d0777f820ff20e6e9e063dea Mon Sep 17 00:00:00 2001 From: palewire Date: Fri, 1 Oct 2021 06:40:08 -0700 Subject: [PATCH 16/26] Added better error message when no notebooks provided --- nbclient/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 8652da2c..8cd26f3e 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -1,3 +1,4 @@ +import sys import logging import pathlib from textwrap import dedent @@ -117,13 +118,12 @@ def get_notebooks(self): notebooks = self.extra_args else: notebooks = self.notebooks - return notebooks - def run_notebook(self, notebook_path): - # Log it - self.log.info(f"Executing {notebook_path}") + if not notebooks: + self.print_help() + print("jupyter-execute: error: expected path to notebook") + sys.exit(-1) - # Get the file name name = notebook_path.replace(".ipynb", "") # Get its parent directory so we can add it to the $PATH From 4de8526ecc10c482f7ea25c7956f4bec818bb3f7 Mon Sep 17 00:00:00 2001 From: palewire Date: Fri, 1 Oct 2021 08:07:21 -0700 Subject: [PATCH 17/26] Removed extra kwargs. Fixed something I broke --- nbclient/cli.py | 65 ++++++++++++++------------ nbclient/tests/{ => files}/Error.ipynb | 0 2 files changed, 35 insertions(+), 30 deletions(-) rename nbclient/tests/{ => files}/Error.ipynb (100%) diff --git a/nbclient/cli.py b/nbclient/cli.py index 8cd26f3e..efca0e0b 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -4,7 +4,7 @@ from textwrap import dedent import nbformat -from jupyter_core.application import JupyterApp, base_aliases, base_flags +from jupyter_core.application import JupyterApp from traitlets import Bool, Integer, List, Unicode, default from traitlets.config import catch_config_error @@ -13,30 +13,22 @@ from .client import NotebookClient from .exceptions import CellExecutionError -nbclient_aliases = {} -nbclient_aliases.update(base_aliases) -nbclient_aliases.update( - { - 'timeout': 'NbClientApp.timeout', - 'startup_timeout': 'NbClientApp.startup_timeout', - 'kernel_name': 'NbClientApp.kernel_name', - } -) - -nbclient_flags = {} -nbclient_flags.update(base_flags) -nbclient_flags.update( - { - 'allow-errors': ( - { - 'NbClientApp': { - 'allow_errors': True, - }, +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.", - ), - } -) + }, + "Errors are ignored and execution is continued until the end of the notebook.", + ), +} class NbClientApp(JupyterApp): @@ -109,20 +101,33 @@ def _log_level_default(self): @catch_config_error def initialize(self, argv=None): super().initialize(argv) + + # Get notebooks to run self.notebooks = self.get_notebooks() - for notebook_path in self.notebooks: - self.run_notebook(notebook_path) + + # If there are none, throw an error + if not self.notebooks: + self.print_help() + 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 - if not notebooks: - self.print_help() - print("jupyter-execute: error: expected path to notebook") - sys.exit(-1) + # 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", "") diff --git a/nbclient/tests/Error.ipynb b/nbclient/tests/files/Error.ipynb similarity index 100% rename from nbclient/tests/Error.ipynb rename to nbclient/tests/files/Error.ipynb From 29eefc1f73397c7d62bc0f9124628774e577802c Mon Sep 17 00:00:00 2001 From: palewire Date: Fri, 1 Oct 2021 08:13:21 -0700 Subject: [PATCH 18/26] Trim --- nbclient/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index efca0e0b..362ee4e1 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -107,7 +107,7 @@ def initialize(self, argv=None): # If there are none, throw an error if not self.notebooks: - self.print_help() + self.print_options() print("jupyter-execute: error: expected path to notebook") sys.exit(-1) @@ -156,4 +156,4 @@ def run_notebook(self, notebook_path): client.execute() -main = launch_new_instance = NbClientApp.launch_instance +main = NbClientApp.launch_instance From e066885fb4cc44d0781ff78a563f2470604869d5 Mon Sep 17 00:00:00 2001 From: palewire Date: Fri, 1 Oct 2021 08:15:15 -0700 Subject: [PATCH 19/26] go --- nbclient/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 362ee4e1..6fd1010f 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -107,7 +107,7 @@ def initialize(self, argv=None): # If there are none, throw an error if not self.notebooks: - self.print_options() + self.print_help() print("jupyter-execute: error: expected path to notebook") sys.exit(-1) From b1fc02aa7b3cd50317b9768b431892db9fe1a1d8 Mon Sep 17 00:00:00 2001 From: Ben Welsh Date: Fri, 1 Oct 2021 08:57:43 -0700 Subject: [PATCH 20/26] Update cli.py --- nbclient/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 6fd1010f..5a8257b7 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -107,7 +107,6 @@ def initialize(self, argv=None): # If there are none, throw an error if not self.notebooks: - self.print_help() print("jupyter-execute: error: expected path to notebook") sys.exit(-1) From a6661dcc049839a335fb704302cc2d34e225f8c9 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 1 Oct 2021 18:11:42 +0200 Subject: [PATCH 21/26] Add newline at end of file --- nbclient/tests/files/Skip Execution with Cell Tag.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 +} From f193ecd062b91501c6befc350f33652b89df6287 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 1 Oct 2021 18:16:00 +0200 Subject: [PATCH 22/26] Fix linter --- nbclient/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 5a8257b7..136b5c7d 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -1,6 +1,6 @@ -import sys import logging import pathlib +import sys from textwrap import dedent import nbformat @@ -11,7 +11,6 @@ from nbclient import __version__ from .client import NotebookClient -from .exceptions import CellExecutionError nbclient_aliases = { 'timeout': 'NbClientApp.timeout', From a3311a6e2ea227c13ccf6b5417a04aef9b98659e Mon Sep 17 00:00:00 2001 From: Ben Welsh Date: Fri, 1 Oct 2021 10:15:29 -0700 Subject: [PATCH 23/26] Typo --- nbclient/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbclient/cli.py b/nbclient/cli.py index 136b5c7d..02b3d20f 100644 --- a/nbclient/cli.py +++ b/nbclient/cli.py @@ -32,7 +32,7 @@ class NbClientApp(JupyterApp): """ - An application used to execute a notebook files (``*.ipynb``) + An application used to execute notebook files (``*.ipynb``) """ version = __version__ @@ -40,7 +40,7 @@ class NbClientApp(JupyterApp): aliases = nbclient_aliases flags = nbclient_flags - description = Unicode("An application used to execute a notebook files (*.ipynb)") + 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, From 3c0c1558198c5a91026cef91fe7903831d00d529 Mon Sep 17 00:00:00 2001 From: palewire Date: Sun, 31 Oct 2021 08:22:39 -0700 Subject: [PATCH 24/26] Tweaked top of docs --- docs/index.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e4f68e67..6c75e7c5 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 ---- From e0a250929f325fd041317740c7d9cceab40833c8 Mon Sep 17 00:00:00 2001 From: palewire Date: Sun, 31 Oct 2021 08:36:35 -0700 Subject: [PATCH 25/26] Docs --- docs/client.rst | 62 +++++++++++++++++++++++++++++++++++++++++++++++-- docs/index.rst | 6 ++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index aeb59b9b..f749c7bc 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,60 @@ 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 6c75e7c5..4019c7ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 From ef9563c6a287b8a901f9ff3f7a43dbe6650b8ebc Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 2 Nov 2021 09:13:41 +0100 Subject: [PATCH 26/26] Fix linting --- docs/client.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/client.rst b/docs/client.rst index f749c7bc..ee82f6b6 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -221,6 +221,3 @@ Other options allow you to modify the timeout length and dictate the kernel in u Equivalent to: [--NbClientApp.kernel_name] To see all available configurables, use `--help-all`. - - -