Execute a notebook via nbconvert and collect output.
       :returns (parsed nb object, execution errors)

def run_notebook(notebook_path):
    """Execute a notebook via nbconvert and collect output.
       :returns (parsed nb object, execution errors)
    """
    dirname, __ = os.path.split(notebook_path)
    os.chdir(dirname)
    with tempfile.NamedTemporaryFile(suffix=".ipynb") as fout:
        args = ["jupyter-nbconvert", "--to", "notebook", "--execute", "--allow-errors",
          "--ExecutePreprocessor.timeout=300",
          "--output", fout.name, notebook_path]
        try:
            subprocess.check_call(args)
        except subprocess.CalledProcessError as e:
            if e.returncode == 1:
              # print the message and ignore error with code 1 as this indicates there were errors in the notebook
              print(e.output)
              pass
            else:
              # all other codes indicate some other problem, rethrow
              raise

        fout.seek(0)
        nb = nbformat.read(fout, nbformat.current_nbformat)

    errors = [output for cell in nb.cells if "outputs" in cell
                     for output in cell["outputs"]\
                     if output.output_type == "error"]

    return nb, errors