├── .gitignore ├── MANIFEST.in ├── resources └── basic_arch.png ├── jupyter_react ├── __init__.py └── component.py ├── setup.py ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | js/node_modules 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include jupyter_react/static/* 2 | -------------------------------------------------------------------------------- /resources/basic_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timbr-io/jupyter-react/HEAD/resources/basic_arch.png -------------------------------------------------------------------------------- /jupyter_react/__init__.py: -------------------------------------------------------------------------------- 1 | from .component import * 2 | 3 | def _jupyter_nbextension_paths(): 4 | return [{ 5 | 'section': 'notebook', 6 | 'src': 'static', 7 | 'dest': 'jupyter_react', 8 | 'require': 'jupyter_react/index' 9 | }] 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='jupyter-react', 4 | version='0.1.7', 5 | description='React component extension for Jupyter Notebooks', 6 | url='https://github.com/timbr-io/jupyter-react', 7 | author='Chris Helm', 8 | author_email='chelm@timbr.io', 9 | license='MIT', 10 | packages=['jupyter_react'], 11 | zip_safe=False, 12 | install_requires=[ 13 | "ipython", 14 | "traitlets" 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Timbr.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyter-react 2 | 3 | This repo actually has nothing to do with React, but rather is a base class meant for pairing up with JS based front-end UI components (see https://github.com/timbr-io/jupyter-react-js). The only thing in this module is a "Component" class. This class can be created with a "module" name that matches the name of a JS UI component and opens up a line of commuination called an "IPython Comm". Using the comm messages can be pased back and forth and property and actions can be taken as a result of UI interaction. 4 | 5 | 6 | 7 | ## Example 8 | 9 | ```python 10 | # Create a custom module in python 11 | 12 | from jupyter_react import Component 13 | 14 | class MyThing(Component): 15 | module = 'AnyJSClassName' 16 | comm_channel = 'custom.name' 17 | 18 | def __init__(self, **kwargs): 19 | super(MyThing, self).__init__(target_name='custom.name', **kwargs) 20 | self.on_msg(self._handle_msg) 21 | 22 | def _handle_msg(self, msg): 23 | print msg 24 | ``` 25 | 26 | ```python 27 | # In Jupyter / IPython instantiate the class and display it 28 | 29 | from mything import MyThing 30 | from IPython.display import display 31 | 32 | mything = MyThing(props={}) 33 | display(mything) 34 | ``` 35 | 36 | Once a component is "displayed" a message is passed over the comm to the front-end. If the front-end has registered a handler for the same `comm target\_name` then it will be called when the class is created. You can see more about how to build the front-end code here: https://github.com/timbr-io/jupyter-react-js. 37 | 38 | ## Methods 39 | 40 | ### open(props) 41 | @param props - dict of props to be passed to the front-end js when a comm is opened. 42 | 43 | Called immediately when a class is instantiated, this method opens a comm to the front-end and passes the props to the JS. 44 | 45 | ### close 46 | Closes the comm. 47 | 48 | ### send(data) 49 | @param data - dict of data to send over as a message comm 50 | 51 | Sends a message over the comm. 52 | 53 | ### on\_msg(callback) 54 | @param callback - method to be called when a message is received over the comm 55 | -------------------------------------------------------------------------------- /jupyter_react/component.py: -------------------------------------------------------------------------------- 1 | from ipykernel.comm import Comm 2 | from IPython.display import display, Javascript 3 | from IPython.core.getipython import get_ipython 4 | from traitlets import Instance, List, observe 5 | from traitlets.config import LoggingConfigurable 6 | import uuid 7 | 8 | # Taken from ipywidgets callback pattern, cause its nice 9 | class CallbackDispatcher(LoggingConfigurable): 10 | callbacks = List() 11 | 12 | def __call__(self, *args, **kwargs): 13 | value = None 14 | for callback in self.callbacks: 15 | try: 16 | local_value = callback(*args, **kwargs) 17 | except Exception as e: 18 | ip = get_ipython() 19 | if ip is None: 20 | self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True) 21 | else: 22 | ip.showtraceback() 23 | else: 24 | value = local_value if local_value is not None else value 25 | return value 26 | 27 | def register_callback(self, callback, remove=False): 28 | # (Un)Register the callback. 29 | if remove and callback in self.callbacks: 30 | self.callbacks.remove(callback) 31 | elif not remove and callback not in self.callbacks: 32 | self.callbacks.append(callback) 33 | 34 | 35 | class Component(LoggingConfigurable): 36 | comm = Instance('ipykernel.comm.Comm', allow_none=True) 37 | _module = None 38 | _msg_callbacks = Instance(CallbackDispatcher, ()) 39 | 40 | @property 41 | def module(self): 42 | if self._module is not None: 43 | return self._module 44 | else: 45 | return self.__class__.__name__ 46 | 47 | def __init__(self, target_name='jupyter.react', props={}, comm=None): 48 | self.target_name = target_name 49 | self.props = props 50 | if comm is None: 51 | self.open(props) 52 | else: 53 | self.comm = comm 54 | self.comm.on_close(self.close) 55 | 56 | def open(self, props): 57 | props['module'] = self.module 58 | args = dict(target_name=self.target_name, data=props) 59 | args['comm_id'] = 'jupyter_react.{}.{}'.format( uuid.uuid4(), props['module'] ) 60 | self.comm = Comm(**args) 61 | 62 | @observe('comm') 63 | def _comm_changed(self, change): 64 | if change['new'] is None: 65 | return 66 | self.comm.on_msg(self._handle_msg) 67 | 68 | def __del__(self): 69 | self.comm.close() 70 | self.close(None) 71 | 72 | def close(self, msg): 73 | if self.comm is not None: 74 | self.comm = None 75 | self._ipython_display_ = None 76 | 77 | def send(self, data): 78 | self.comm.send( data ) 79 | 80 | def _ipython_display_(self, **kwargs): 81 | self.send({"method": "display"}) 82 | 83 | def _handle_msg(self, msg): 84 | if 'content' in msg: 85 | self._msg_callbacks(self, msg['content'], msg['buffers']) 86 | 87 | def on_msg(self, callback, remove=False): 88 | self._msg_callbacks.register_callback(callback, remove=remove) 89 | 90 | --------------------------------------------------------------------------------