import graphviz
import IPython


@IPython.core.magic.register_line_cell_magic
def dot(line, cell):
    return graphviz.Source(cell)

For Starters

The basic idea of a starter is:

  • pick a destination in the JupyterLab File Browser

  • click a button in the JupyterLab Launcher

  • see useful files

A slightly more accurate version is:

  • configure via traitlets

  • advertise to JupyterLab via the REST API

  • display in the JupyterLab Launcher

  • click a button in the JupyterLab Launcher

    • or immediately start with a Starter Tree URL

  • zero or more (but usually one) times:

    • gather more information from the user via react-jsonschema-form

    • perform further processing

    • copy files via the Contents API

    • see useful files in the JupyterLab File Browser

    • run JupyterLab Commands to do other things to JupyterLab

Which of these steps a particular starter performs depends primarily on its type.

Types of Starters

Copy

"type": "copy"

"src": "<an absolute or relative path>"

The simplest starter, copy, just… copies. It can copy a single file, or a directory of files (and subdirectories). The src attribute tells the starter where to get the files.

%%dot
digraph g { compound=true layout=dot rankdir=TB
    node[shape=none fontname="sans-serif"]
    graph[fontname="sans-serif" fontcolor="grey" color="none" fillcolor="#eeeeee" style=filled]
    label="a notional execution of a copy starter"
    subgraph cluster_files { label="Your Files"
        files
    }
    
    subgraph cluster_server { label="Notebook Server"
        get[label="/starters" fontname=monospace]
        post[label="/starters/{:name}/{:path}" fontname=monospace]
        contents
    }

    subgraph cluster_lab { label="JupyterLab"
        launcher
    }
    
    get -> launcher[label=①]
    launcher -> post[label=②]
    post -> contents[label=③]
    contents -> files[label=④]
    files -> contents[label=⑤]
    contents -> post[label=⑥]
    post -> launcher[label=⑦]
    launcher -> launcher[label=⑧]
}
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Input In [2], in <module>
----> 1 get_ipython().run_cell_magic('dot', '', 'digraph g { compound=true layout=dot rankdir=TB\n    node[shape=none fontname="sans-serif"]\n    graph[fontname="sans-serif" fontcolor="grey" color="none" fillcolor="#eeeeee" style=filled]\n    label="a notional execution of a copy starter"\n    subgraph cluster_files { label="Your Files"\n        files\n    }\n    \n    subgraph cluster_server { label="Notebook Server"\n        get[label="/starters" fontname=monospace]\n        post[label="/starters/{:name}/{:path}" fontname=monospace]\n        contents\n    }\n\n    subgraph cluster_lab { label="JupyterLab"\n        launcher\n    }\n    \n    get -> launcher[label=①]\n    launcher -> post[label=②]\n    post -> contents[label=③]\n    contents -> files[label=④]\n    files -> contents[label=⑤]\n    contents -> post[label=⑥]\n    post -> launcher[label=⑦]\n    launcher -> launcher[label=⑧]\n}\n')

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/IPython/core/displayhook.py:262, in DisplayHook.__call__(self, result)
    260 self.start_displayhook()
    261 self.write_output_prompt()
--> 262 format_dict, md_dict = self.compute_format_data(result)
    263 self.update_user_ns(result)
    264 self.fill_exec_result(result)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/IPython/core/displayhook.py:151, in DisplayHook.compute_format_data(self, result)
    121 def compute_format_data(self, result):
    122     """Compute format data of the object to be displayed.
    123 
    124     The format data is a generalization of the :func:`repr` of an object.
   (...)
    149 
    150     """
--> 151     return self.shell.display_formatter.format(result)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/IPython/core/formatters.py:148, in DisplayFormatter.format(self, obj, include, exclude)
    144 if self.ipython_display_formatter(obj):
    145     # object handled itself, don't proceed
    146     return {}, {}
--> 148 format_dict, md_dict = self.mimebundle_formatter(obj, include=include, exclude=exclude)
    150 if format_dict or md_dict:
    151     if include:

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/decorator.py:232, in decorate.<locals>.fun(*args, **kw)
    230 if not kwsyntax:
    231     args, kw = fix(args, kw, sig)
--> 232 return caller(func, *(extras + args), **kw)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/IPython/core/formatters.py:222, in catch_format_error(method, self, *args, **kwargs)
    220 """show traceback on failed format call"""
    221 try:
--> 222     r = method(self, *args, **kwargs)
    223 except NotImplementedError:
    224     # don't warn on NotImplementedErrors
    225     return self._check_return(None, args[0])

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/IPython/core/formatters.py:973, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    970     method = get_real_method(obj, self.print_method)
    972     if method is not None:
--> 973         return method(include=include, exclude=exclude)
    974     return None
    975 else:

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/jupyter_integration.py:98, in JupyterIntegration._repr_mimebundle_(self, include, exclude, **_)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/jupyter_integration.py:98, in <dictcomp>(.0)
     96 include = set(include) if include is not None else {self._jupyter_mimetype}
     97 include -= set(exclude or [])
---> 98 return {mimetype: getattr(self, method_name)()
     99         for mimetype, method_name in MIME_TYPES.items()
    100         if mimetype in include}

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/jupyter_integration.py:112, in JupyterIntegration._repr_image_svg_xml(self)
    110 def _repr_image_svg_xml(self) -> str:
    111     """Return the rendered graph as SVG string."""
--> 112     return self.pipe(format='svg', encoding=SVG_ENCODING)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/piping.py:99, in Pipe.pipe(self, format, renderer, formatter, quiet, engine, encoding)
     52 def pipe(self,
     53          format: typing.Optional[str] = None,
     54          renderer: typing.Optional[str] = None,
   (...)
     57          engine: typing.Optional[str] = None,
     58          encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
     59     """Return the source piped through the Graphviz layout command.
     60 
     61     Args:
   (...)
     97         '<?xml version='
     98     """
---> 99     return self._pipe_legacy(format,
    100                              renderer=renderer,
    101                              formatter=formatter,
    102                              quiet=quiet,
    103                              engine=engine,
    104                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/_tools.py:172, in deprecate_positional_args.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    163     wanted = ', '.join(f'{name}={value!r}'
    164                        for name, value in deprecated.items())
    165     warnings.warn(f'The signature of {func.__name__} will be reduced'
    166                   f' to {supported_number} positional args'
    167                   f' {list(supported)}: pass {wanted}'
    168                   ' as keyword arg(s)',
    169                   stacklevel=stacklevel,
    170                   category=category)
--> 172 return func(*args, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/piping.py:114, in Pipe._pipe_legacy(self, format, renderer, formatter, quiet, engine, encoding)
    106 @_tools.deprecate_positional_args(supported_number=2)
    107 def _pipe_legacy(self,
    108                  format: typing.Optional[str] = None,
   (...)
    112                  engine: typing.Optional[str] = None,
    113                  encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]:
--> 114     return self._pipe_future(format,
    115                              renderer=renderer,
    116                              formatter=formatter,
    117                              quiet=quiet,
    118                              engine=engine,
    119                              encoding=encoding)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/piping.py:139, in Pipe._pipe_future(self, format, renderer, formatter, quiet, engine, encoding)
    136 if encoding is not None:
    137     if codecs.lookup(encoding) is codecs.lookup(self.encoding):
    138         # common case: both stdin and stdout need the same encoding
--> 139         return self._pipe_lines_string(*args, encoding=encoding, **kwargs)
    140     try:
    141         raw = self._pipe_lines(*args, input_encoding=self.encoding, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/backend/piping.py:196, in pipe_lines_string(engine, format, input_lines, encoding, renderer, formatter, quiet)
    192 cmd = dot_command.command(engine, format,
    193                           renderer=renderer, formatter=formatter)
    194 kwargs = {'input_lines': input_lines, 'encoding': encoding}
--> 196 proc = execute.run_check(cmd, capture_output=True, quiet=quiet, **kwargs)
    197 return proc.stdout

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/backend/execute.py:83, in run_check(cmd, input_lines, encoding, capture_output, quiet, **kwargs)
     81     assert kwargs.get('input') is None
     82     assert iter(input_lines) is input_lines
---> 83     proc = _run_input_lines(cmd, input_lines, kwargs=kwargs)
     84 else:
     85     proc = subprocess.run(cmd, **kwargs)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/site-packages/graphviz/backend/execute.py:109, in _run_input_lines(cmd, input_lines, kwargs)
    106 for line in input_lines:
    107     stdin_write(line)
--> 109 stdout, stderr = popen.communicate()
    110 return subprocess.CompletedProcess(popen.args, popen.returncode,
    111                                    stdout=stdout, stderr=stderr)

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/subprocess.py:1149, in Popen.communicate(self, input, timeout)
   1146     endtime = None
   1148 try:
-> 1149     stdout, stderr = self._communicate(input, endtime, timeout)
   1150 except KeyboardInterrupt:
   1151     # https://bugs.python.org/issue25942
   1152     # See the detailed comment in .wait().
   1153     if timeout is not None:

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/subprocess.py:2000, in Popen._communicate(self, input, endtime, orig_timeout)
   1993     self._check_timeout(endtime, orig_timeout,
   1994                         stdout, stderr,
   1995                         skip_check_and_raise=True)
   1996     raise RuntimeError(  # Impossible :)
   1997         '_check_timeout(..., skip_check_and_raise=True) '
   1998         'failed to raise TimeoutExpired.')
-> 2000 ready = selector.select(timeout)
   2001 self._check_timeout(endtime, orig_timeout, stdout, stderr)
   2003 # XXX Rewrite these to use non-blocking I/O on the file
   2004 # objects; they are no longer using C stdio!

File ~/checkouts/readthedocs.org/user_builds/jupyterstarters/conda/stable/lib/python3.10/selectors.py:416, in _PollLikeSelector.select(self, timeout)
    414 ready = []
    415 try:
--> 416     fd_event_list = self._selector.poll(timeout)
    417 except InterruptedError:
    418     return ready

KeyboardInterrupt: 

copy, like all the starters, makes use of the Contents API directly. Existing files will not be overwritten.

Python

"type": "python"

"callable": "<a dotted notation python function>"

A Python Starter is a function. This type has the fewest limitations, as it has full access to the StarterManager (and by extension, it’s parent, the NotebookApp). This powers both the Cookiecutter the Notebook starters, with the latter directly using the notebook server’s Kernel Manager to start short-lifespan kernels.

%%dot
digraph g { compound=true layout=dot rankdir=TB
    node[shape=none fontname="sans-serif"]
    graph[fontname="sans-serif" fontcolor="grey" color="none" fillcolor="#eeeeee" style=filled]
    label="a notional execution of a python starter"
    subgraph cluster_files { label="Your Files"
        files
    }
    
    subgraph cluster_server { label="Notebook Server"
        get[label="/starters" fontname=monospace]
        post[label="/starters/{:name}/{:path}" fontname=monospace]
        contents
        callable             
    }

    subgraph cluster_lab { label="JupyterLab"
        launcher
    }
    
    get -> launcher[label=①]
    launcher -> post[label=②]
    post -> callable[label=③]
    callable -> contents[label=④]
    contents -> files[label=⑤]
    files -> contents[label=⑥]
    contents -> callable[label=⑦]
    callable -> post[label=⑧]
    post -> launcher[label=⑨]
    launcher -> launcher[label=⑩]
}

Notebook

"type": "notebook"

A notebook can be a starter. Each starter run gets its own, private kernel which can persist between interactions with the user. Communication with the server manager is handled through manipulating a copy of the notebook, specfically the notebook metadata. The advantages of this approach over the Python starter is:

  • works with any installed kernel

  • state is maintained between successive re-executions

  • jupyterlab-starters provides authoring support for editing and validating the starter

%%dot
digraph g { compound=true layout=dot rankdir=TB title="woooo"
    node[shape=none fontname="sans-serif"]
    graph[fontname="sans-serif" fontcolor="grey" color="none" fillcolor="#eeeeee" style=filled]
    label="a notional execution of a notebook starter"
    subgraph cluster_files { label="Your Files"
        files
    }
    
    subgraph cluster_server { label="Notebook Server"
        get[label="/starters" fontname=monospace]
        post[label="/starters/cookiecutter/{:path}" fontname=monospace]
        contents
        kernel
        tmpdir
    }

    subgraph cluster_lab { label="JupyterLab"
        launcher
        form1[label="initial form"]
        form2[label="dynamic form"]
    }

    get -> launcher[label=①]
    launcher -> form1[label=②]
    form1 -> post[label=③]
    post -> tmpdir[label=④]
    tmpdir -> post[label=⑤]
    tmpdir -> kernel[label=⑥]
    kernel -> tmpdir[label=⑦]
    post -> form2[label=⑧]
    form2 -> post[label=⑨]
    post -> tmpdir[label=⑩]
    tmpdir -> kernel[label=⑪]
    kernel -> tmpdir[label=⑫]
    tmpdir -> contents[label=⑬]
    contents -> files[label=⑭]
    files -> contents[label=⑮]
    contents -> post[label=⑯]
    post -> launcher[label=⑰]
    launcher -> launcher[label=⑲]
}

Built-ins

Cookiecutter

The cookiecutter starter will be available if cookiecutter is installed in the same Python environment as the notebook server.

Find more cookiecutter URLs on GitHub by topic or advanced search.

One of the original motivations for Jupyter Starters was a way to provide a convenient, consistent, web-based experience for the cookiecutter ecosystem. Briefly, a cookiecutter is:

  • a repository, zip archive, or directory that contains

    • cookiecutter.json

    • a (potentially nested) directory that uses Jinja2 to describe file names and contents

What they may lack in dynamism, the make up for in consistency and robustness.

%%dot
digraph g { compound=true layout=dot rankdir=TB title="woooo"
    node[shape=none fontname="sans-serif"]
    graph[fontname="sans-serif" fontcolor="grey" color="none" fillcolor="#eeeeee" style=filled]
    label="a notional execution of the cookiecutter starter"
    subgraph cluster_files { label="Your Files"
        files
    }
    
    subgraph cluster_server { label="Notebook Server"
        get[label="/starters" fontname=monospace]
        post[label="/starters/cookiecutter/{:path}" fontname=monospace]
        contents
        cookiecutter
    }

    subgraph cluster_lab { label="JupyterLab"
        launcher
        form1[label="template form"]
        form2[label="cookiecutter form"]
    }
    get -> launcher[label=①]
    launcher -> form1[label=②]
    form1 -> post[label=③]
    post -> cookiecutter[label=④]
    cookiecutter -> git[label=⑤]
    git -> cookiecutter[label=⑥]
    cookiecutter -> post[label=⑧]
    post -> form2[label=⑨]
    form2 -> post[label=⑩]
    post -> cookiecutter[label=⑪]
    cookiecutter -> contents[label=⑫]
    contents -> files[label=⑬]
    files -> contents[label=⑭]
    contents -> post[label=⑮]
    post -> launcher[label=⑯]
    launcher -> launcher[label=⑰]
}

Under the hood, the cookiecutter starter is implemented as a Python starter, and can be seen as tutorial in how to create a starter from a complex piece of existing functionality.

Extras

Starter Tree URL

By specifying a special URL when starting JupyterLab, you can immediately start a Starter, without requiring the launcher. The pattern is:

{:protocol}://{:host}:{:port}{:base-url}/lab{:whatever}?starter/{:starter-name}{:starter-path}

For example:

http://localhost:8888/lab?starter=cookiecutter/

On Binder, this path is determined by the urlpath GET parameter, for example:

https://mybinder.org/v2/gh/deathbeds/jupyterlab-starters/master?urlpath=lab%3Fstarter%2Fcookiecutter%2Fexamples%2F