├── LICENSE ├── README.md ├── examples ├── main_notebook.ipynb ├── sub_notebook.ipynb └── widget_notebook.ipynb ├── pyproject.toml └── src └── subnotebook ├── __init__.py └── _subnotebook.py /LICENSE: -------------------------------------------------------------------------------- 1 | This is the MIT license: http://www.opensource.org/licenses/mit-license.php 2 | 3 | Copyright (c) David Brochart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or 12 | substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 17 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Subnotebook 2 | 3 | Run a notebook as you would call a Python function. 4 | 5 | Two modes are currently supported: 6 | 7 | - An in-process mode, where the subnotebook is executed in the same kernel your 8 | interpreter is running in (but in a different name space). This allows to pass 9 | parameters and get results back, including widgets. 10 | ```python 11 | from subnotebook import run_nb 12 | 13 | ab, ba, slider, output = run_nb('sub_notebook.ipynb', a='c', b='d') 14 | ``` 15 | 16 | - An out-of-process mode, where the subnotebook is executed and served using 17 | [Voila](https://voila.readthedocs.io), and included in the main notebook as an 18 | IFrame. This mode only allows to display the outputs of the subnotebook, which 19 | is useful for offloading the main notebook but also to offer a UI to resources 20 | you would not have access to, such as big data or protected data. 21 | ```python 22 | from subnotebook import display_nb 23 | 24 | display_nb('widget_notebook.ipynb') 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/main_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from subnotebook import run_nb, display_nb" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": { 16 | "scrolled": false 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "ab, ba, slider, output = run_nb('sub_notebook.ipynb', a='c', b='d')" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "print(ab, ba)" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "slider" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "output" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "display_nb('widget_notebook.ipynb')" 57 | ] 58 | } 59 | ], 60 | "metadata": { 61 | "kernelspec": { 62 | "display_name": "Python 3", 63 | "language": "python", 64 | "name": "python3" 65 | }, 66 | "language_info": { 67 | "codemirror_mode": { 68 | "name": "ipython", 69 | "version": 3 70 | }, 71 | "file_extension": ".py", 72 | "mimetype": "text/x-python", 73 | "name": "python", 74 | "nbconvert_exporter": "python", 75 | "pygments_lexer": "ipython3", 76 | "version": "3.8.2" 77 | } 78 | }, 79 | "nbformat": 4, 80 | "nbformat_minor": 4 81 | } 82 | -------------------------------------------------------------------------------- /examples/sub_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import ipywidgets\n", 10 | "from subnotebook import Return, default_value" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "default_value(a='a', b='b')" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "slider = ipywidgets.IntSlider()\n", 29 | "output = ipywidgets.Output()\n", 30 | "\n", 31 | "def on_value_change(change):\n", 32 | " with output:\n", 33 | " print(change['new'])\n", 34 | "\n", 35 | "slider.observe(on_value_change, names='value')" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "ab = a + b\n", 45 | "ba = b + a" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "Return(ab, ba, slider, output)" 55 | ] 56 | } 57 | ], 58 | "metadata": { 59 | "kernelspec": { 60 | "display_name": "Python 3", 61 | "language": "python", 62 | "name": "python3" 63 | }, 64 | "language_info": { 65 | "codemirror_mode": { 66 | "name": "ipython", 67 | "version": 3 68 | }, 69 | "file_extension": ".py", 70 | "mimetype": "text/x-python", 71 | "name": "python", 72 | "nbconvert_exporter": "python", 73 | "pygments_lexer": "ipython3", 74 | "version": "3.8.2" 75 | } 76 | }, 77 | "nbformat": 4, 78 | "nbformat_minor": 4 79 | } 80 | -------------------------------------------------------------------------------- /examples/widget_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import ipywidgets\n", 10 | "\n", 11 | "slider = ipywidgets.FloatSlider(description='x', value=4)\n", 12 | "text = ipywidgets.FloatText(disabled=True, description='x^2')\n", 13 | "\n", 14 | "def compute(*ignore):\n", 15 | " text.value = str(slider.value ** 2)\n", 16 | "\n", 17 | "slider.observe(compute, 'value')\n", 18 | "\n", 19 | "ipywidgets.VBox([slider, text])" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "kernelspec": { 25 | "display_name": "Python 3", 26 | "language": "python", 27 | "name": "python3" 28 | }, 29 | "language_info": { 30 | "codemirror_mode": { 31 | "name": "ipython", 32 | "version": 3 33 | }, 34 | "file_extension": ".py", 35 | "mimetype": "text/x-python", 36 | "name": "python", 37 | "nbconvert_exporter": "python", 38 | "pygments_lexer": "ipython3", 39 | "version": "3.8.2" 40 | } 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 2 44 | } 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "subnotebook" 7 | version = "0.0.1" 8 | description = "Call notebooks as functions" 9 | readme = "README.md" 10 | authors = [ 11 | {name = "David Brochart", email = "david.brochart@gmail.com"}, 12 | ] 13 | license = {file = "LICENSE"} 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Typing :: Typed", 19 | "Topic :: Database", 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | ] 29 | requires-python = ">= 3.8" 30 | dependencies = [ 31 | "nbformat >=5", 32 | ] 33 | 34 | [project.urls] 35 | Source = "https://github.com/davidbrochart/subnotebook" 36 | Issues = "https://github.com/davidbrochart/subnotebook/issues" 37 | 38 | [tool.hatch.build.targets.wheel] 39 | ignore-vcs = true 40 | packages = ["src/subnotebook"] 41 | -------------------------------------------------------------------------------- /src/subnotebook/__init__.py: -------------------------------------------------------------------------------- 1 | from ._subnotebook import Return as Return 2 | from ._subnotebook import default_value as default_value 3 | from ._subnotebook import display_nb as display_nb 4 | from ._subnotebook import open_nb as open_nb 5 | from ._subnotebook import run_nb as run_nb 6 | -------------------------------------------------------------------------------- /src/subnotebook/_subnotebook.py: -------------------------------------------------------------------------------- 1 | import nbformat 2 | import inspect 3 | #import voila.app 4 | #import nest_asyncio 5 | import subprocess 6 | from IPython.display import IFrame, display 7 | import atexit 8 | 9 | 10 | voila_processes = [] 11 | 12 | 13 | def run_nb(path, **kwargs): 14 | nb = open_nb(path) 15 | results = nb.run(**kwargs) 16 | return results 17 | 18 | 19 | def open_nb(path): 20 | nb = nbformat.read(path, nbformat.NO_CONVERT) 21 | subnb = SubNotebook(nb) 22 | return subnb 23 | 24 | 25 | def default_value(**kwargs): 26 | frame = inspect.currentframe() 27 | try: 28 | out_locals = frame.f_back.f_locals 29 | finally: 30 | del frame 31 | for k, v in kwargs.items(): 32 | if k not in out_locals: 33 | out_locals[k] = v 34 | 35 | 36 | class Return: 37 | Result_i = 0 38 | def __init__(self, *args): 39 | frame = inspect.currentframe() 40 | try: 41 | out_locals = frame.f_back.f_locals 42 | finally: 43 | del frame 44 | for i, arg in enumerate(args): 45 | result_i = self.Result_i + i 46 | out_locals[f'__result_{result_i}__'] = arg 47 | Return.Result_i += len(args) 48 | 49 | 50 | class SubNotebook: 51 | 52 | def __init__(self, nb): 53 | self.nb = nb 54 | self.kwargs = {} 55 | 56 | def run(self, **kwargs): 57 | self.namespace = kwargs 58 | for cell in self.nb.cells: 59 | if cell['cell_type'] == 'code': 60 | exec(cell['source'], self.namespace) 61 | results = self.get_results() 62 | return results 63 | 64 | def get_results(self): 65 | results = [self.namespace[name] for name in self.namespace if name.startswith('__result_')] 66 | if len(results) == 1: 67 | return results[0] 68 | return tuple(results) 69 | 70 | 71 | def get_lines(std_pipe): 72 | '''Generator that yields lines from a standard pipe as they are printed.''' 73 | for line in iter(std_pipe.readline, ''): 74 | yield line 75 | std_pipe.close() 76 | 77 | 78 | def display_nb(path, server_address='http://*:*/', width='100%', height=1000): 79 | #nest_asyncio.apply() 80 | #voila_app = voila.app.Voila() 81 | #voila_app.initialize([path, '--no-browser', "--Voila.tornado_settings={'headers':{'Content-Security-Policy':\"frame-ancestors 'self' " + server_address + "\"}}"]) 82 | #voila_app.start() 83 | #print(voila_app.server_url) 84 | 85 | cmd = ['voila', '--no-browser', "--Voila.tornado_settings={'headers':{'Content-Security-Policy':\"frame-ancestors 'self' " + server_address + "\"}}", path] 86 | voila = subprocess.Popen(cmd, stderr=subprocess.PIPE, universal_newlines=True) 87 | voila_processes.append(voila) 88 | 89 | # wait until server is ready and get the address 90 | for line in get_lines(voila.stderr): 91 | if line.startswith('http://'): 92 | voila_address = line.strip() 93 | break 94 | 95 | display(IFrame(src=voila_address, width=width, height=height)) 96 | 97 | 98 | def kill_processes(processes): 99 | for p in processes: 100 | p.kill() 101 | 102 | 103 | atexit.register(kill_processes, voila_processes) 104 | --------------------------------------------------------------------------------