class CommSocket(object): """ Manages the Comm connection between IPython and the browser (client). Comms are 2 way, with the CommSocket being able to publish a message via the send_json method, and handle a message with on_message. On the JS side figure.send_message and figure.ws.onmessage do the sending and receiving respectively. """ def __init__(self, manager): self.supports_binary = None self.manager = manager self.uuid = str(uuid()) display(HTML("<div id=%r></div>" % self.uuid)) try: self.comm = Comm('matplotlib', data={'id': self.uuid}) except AttributeError: raise RuntimeError('Unable to create an IPython notebook Comm ' 'instance. Are you in the IPython notebook?') self.comm.on_msg(self.on_message) def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. if self.comm in self.manager.web_sockets: self.manager.remove_web_socket(self) self.comm.close() def send_json(self, content): self.comm.send({'data': json.dumps(content)}) def send_binary(self, blob): # The comm is ascii, so we always send the image in base64 # encoded data URL form. data_uri = "data:image/png;base64,{0}".format(b64encode(blob)) self.comm.send({'data': data_uri}) def on_message(self, message): # The 'supports_binary' message is relevant to the # websocket itself. The other messages get passed along # to matplotlib as-is. # Every message has a "type" and a "figure_id". message = json.loads(message['content']['data']) if message['type'] == 'closing': self.on_close() elif message['type'] == 'supports_binary': self.supports_binary = message['value'] else: self.manager.handle_json(message)
class CustomCommSocket(CommSocket): """ CustomCommSocket provides communication between the IPython kernel and a matplotlib canvas element in the notebook. A CustomCommSocket is required to delay communication between the kernel and the canvas element until the widget has been rendered in the notebook. """ def __init__(self, manager): self.supports_binary = None self.manager = manager self.uuid = str(uuid.uuid4()) self.html = "<div id=%r></div>" % self.uuid def start(self): try: self.comm = Comm('matplotlib', data={'id': self.uuid}) except AttributeError: raise RuntimeError('Unable to create an IPython notebook Comm ' 'instance. Are you in the IPython notebook?') self.comm.on_msg(self.on_message) self.comm.on_close(lambda close_message: self.manager.clearup_closed())
class Widget(LoggingConfigurable): #------------------------------------------------------------------------- # Class attributes #------------------------------------------------------------------------- _widget_construction_callback = None widgets = {} @staticmethod def on_widget_constructed(callback): """Registers a callback to be called when a widget is constructed. The callback must have the following signature: callback(widget)""" Widget._widget_construction_callback = callback @staticmethod def _call_widget_constructed(widget): """Static method, called when a widget is constructed.""" if Widget._widget_construction_callback is not None and callable( Widget._widget_construction_callback): Widget._widget_construction_callback(widget) #------------------------------------------------------------------------- # Traits #------------------------------------------------------------------------- model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""") _view_name = Unicode(help="""Default view registered in the front-end to use to represent the widget.""", sync=True) _comm = Instance('IPython.kernel.comm.Comm') closed = Bool(False) keys = List() def _keys_default(self): return [name for name in self.traits(sync=True)] _property_lock = Tuple((None, None)) _display_callbacks = Instance(CallbackDispatcher, ()) _msg_callbacks = Instance(CallbackDispatcher, ()) #------------------------------------------------------------------------- # (Con/de)structor #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" super(Widget, self).__init__(**kwargs) self.on_trait_change(self._handle_property_changed, self.keys) Widget._call_widget_constructed(self) def __del__(self): """Object disposal""" self.close() #------------------------------------------------------------------------- # Properties #------------------------------------------------------------------------- @property def comm(self): """Gets the Comm associated with this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" if self._comm is None: # Create a comm. self._comm = Comm(target_name=self.model_name) self._comm.on_msg(self._handle_msg) self._comm.on_close(self._close) Widget.widgets[self.model_id] = self # first update self.send_state() return self._comm @property def model_id(self): """Gets the model id of this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" return self.comm.comm_id #------------------------------------------------------------------------- # Methods #------------------------------------------------------------------------- def _close(self): """Private close - cleanup objects, registry entries""" del Widget.widgets[self.model_id] self._comm = None self.closed = True def close(self): """Close method. Closes the widget which closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" if not self.closed: self._comm.close() self._close() def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end. Parameters ---------- key : unicode (optional) A single property's name to sync with the front-end. """ self._send({"method": "update", "state": self.get_state()}) def get_state(self, key=None): """Gets the widget state, or a piece of it. Parameters ---------- key : unicode (optional) A single property's name to get. """ keys = self.keys if key is None else [key] return {k: self._pack_widgets(getattr(self, k)) for k in keys} def send(self, content): """Sends a custom msg to the widget model in the front-end. Parameters ---------- content : dict Content of the message to send. """ self._send({"method": "custom", "content": content}) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- callback: callable callback will be passed two arguments when a message arrives:: callback(widget, content) remove: bool True if the callback should be unregistered.""" self._msg_callbacks.register_callback(callback, remove=remove) def on_displayed(self, callback, remove=False): """(Un)Register a widget displayed callback. Parameters ---------- callback: method handler Must have a signature of:: callback(widget, **kwargs) kwargs from display are passed through without modification. remove: bool True if the callback should be unregistered.""" self._display_callbacks.register_callback(callback, remove=remove) #------------------------------------------------------------------------- # Support methods #------------------------------------------------------------------------- @contextmanager def _lock_property(self, key, value): """Lock a property-value pair. NOTE: This, in addition to the single lock for all state changes, is flawed. In the future we may want to look into buffering state changes back to the front-end.""" self._property_lock = (key, value) try: yield finally: self._property_lock = (None, None) def _should_send_property(self, key, value): """Check the property lock (property_lock)""" return key != self._property_lock[0] or \ value != self._property_lock[1] # Event handlers def _handle_msg(self, msg): """Called when a msg is received from the front-end""" data = msg['content']['data'] method = data['method'] if not method in ['backbone', 'custom']: self.log.error( 'Unknown front-end to back-end widget msg with method "%s"' % method) # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. if method == 'backbone' and 'sync_data' in data: sync_data = data['sync_data'] self._handle_receive_state(sync_data) # handles all methods # Handle a custom msg from the front-end elif method == 'custom': if 'content' in data: self._handle_custom_msg(data['content']) def _handle_receive_state(self, sync_data): """Called when a state is received from the front-end.""" for name in self.keys: if name in sync_data: value = self._unpack_widgets(sync_data[name]) with self._lock_property(name, value): setattr(self, name, value) def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) def _handle_property_changed(self, name, old, new): """Called when a property has been changed.""" # Make sure this isn't information that the front-end just sent us. if self._should_send_property(name, new): # Send new state to front-end self.send_state(key=name) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" self._display_callbacks(self, **kwargs) def _pack_widgets(self, x): """Recursively converts all widget instances to model id strings. Children widgets will be stored and transmitted to the front-end by their model ids. Return value must be JSON-able.""" if isinstance(x, dict): return {k: self._pack_widgets(v) for k, v in x.items()} elif isinstance(x, list): return [self._pack_widgets(v) for v in x] elif isinstance(x, Widget): return x.model_id else: return x # Value must be JSON-able def _unpack_widgets(self, x): """Recursively converts all model id strings to widget instances. Children widgets will be stored and transmitted to the front-end by their model ids.""" if isinstance(x, dict): return {k: self._unpack_widgets(v) for k, v in x.items()} elif isinstance(x, list): return [self._unpack_widgets(v) for v in x] elif isinstance(x, string_types): return x if x not in Widget.widgets else Widget.widgets[x] else: return x def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" # Show view. By sending a display message, the comm is opened and the # initial state is sent. self._send({"method": "display"}) self._handle_displayed(**kwargs) def _send(self, msg): """Sends a message to the model in the front-end.""" self.comm.send(msg)
class Widget(LoggingConfigurable): #------------------------------------------------------------------------- # Class attributes #------------------------------------------------------------------------- _widget_construction_callback = None widgets = {} widget_types = {} @staticmethod def on_widget_constructed(callback): """Registers a callback to be called when a widget is constructed. The callback must have the following signature: callback(widget)""" Widget._widget_construction_callback = callback @staticmethod def _call_widget_constructed(widget): """Static method, called when a widget is constructed.""" if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback): Widget._widget_construction_callback(widget) @staticmethod def handle_comm_opened(comm, msg): """Static method, called when a widget is constructed.""" widget_class = import_item(msg['content']['data']['widget_class']) widget = widget_class(comm=comm) #------------------------------------------------------------------------- # Traits #------------------------------------------------------------------------- _model_module = Unicode(None, allow_none=True, help="""A requirejs module name in which to find _model_name. If empty, look in the global registry.""") _model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""") _view_module = Unicode(help="""A requirejs module in which to find _view_name. If empty, look in the global registry.""", sync=True) _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end to use to represent the widget.""", sync=True) comm = Instance('IPython.kernel.comm.Comm') msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the front-end can send before receiving an idle msg from the back-end.""") version = Int(0, sync=True, help="""Widget's version""") keys = List() def _keys_default(self): return [name for name in self.traits(sync=True)] _property_lock = Tuple((None, None)) _send_state_lock = Int(0) _states_to_send = Set(allow_none=False) _display_callbacks = Instance(CallbackDispatcher, ()) _msg_callbacks = Instance(CallbackDispatcher, ()) #------------------------------------------------------------------------- # (Con/de)structor #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" self._model_id = kwargs.pop('model_id', None) super(Widget, self).__init__(**kwargs) Widget._call_widget_constructed(self) self.open() def __del__(self): """Object disposal""" self.close() #------------------------------------------------------------------------- # Properties #------------------------------------------------------------------------- def open(self): """Open a comm to the frontend if one isn't already open.""" if self.comm is None: args = dict(target_name='ipython.widget', data={'model_name': self._model_name, 'model_module': self._model_module}) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args) def _comm_changed(self, name, new): """Called when the comm is changed.""" if new is None: return self._model_id = self.model_id self.comm.on_msg(self._handle_msg) Widget.widgets[self.model_id] = self # first update self.send_state() @property def model_id(self): """Gets the model id of this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" return self.comm.comm_id #------------------------------------------------------------------------- # Methods #------------------------------------------------------------------------- def close(self): """Close method. Closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" if self.comm is not None: Widget.widgets.pop(self.model_id, None) self.comm.close() self.comm = None def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end. Parameters ---------- key : unicode, or iterable (optional) A single property's name or iterable of property names to sync with the front-end. """ self._send({ "method" : "update", "state" : self.get_state(key=key) }) def get_state(self, key=None): """Gets the widget state, or a piece of it. Parameters ---------- key : unicode or iterable (optional) A single property's name or iterable of property names to get. """ if key is None: keys = self.keys elif isinstance(key, string_types): keys = [key] elif isinstance(key, collections.Iterable): keys = key else: raise ValueError("key must be a string, an iterable of keys, or None") state = {} for k in keys: f = self.trait_metadata(k, 'to_json', self._trait_to_json) value = getattr(self, k) state[k] = f(value) return state def set_state(self, sync_data): """Called when a state is received from the front-end.""" for name in self.keys: if name in sync_data: json_value = sync_data[name] from_json = self.trait_metadata(name, 'from_json', self._trait_from_json) with self._lock_property(name, json_value): setattr(self, name, from_json(json_value)) def send(self, content): """Sends a custom msg to the widget model in the front-end. Parameters ---------- content : dict Content of the message to send. """ self._send({"method": "custom", "content": content}) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- callback: callable callback will be passed two arguments when a message arrives:: callback(widget, content) remove: bool True if the callback should be unregistered.""" self._msg_callbacks.register_callback(callback, remove=remove) def on_displayed(self, callback, remove=False): """(Un)Register a widget displayed callback. Parameters ---------- callback: method handler Must have a signature of:: callback(widget, **kwargs) kwargs from display are passed through without modification. remove: bool True if the callback should be unregistered.""" self._display_callbacks.register_callback(callback, remove=remove) #------------------------------------------------------------------------- # Support methods #------------------------------------------------------------------------- @contextmanager def _lock_property(self, key, value): """Lock a property-value pair. The value should be the JSON state of the property. NOTE: This, in addition to the single lock for all state changes, is flawed. In the future we may want to look into buffering state changes back to the front-end.""" self._property_lock = (key, value) try: yield finally: self._property_lock = (None, None) @contextmanager def hold_sync(self): """Hold syncing any state until the context manager is released""" # We increment a value so that this can be nested. Syncing will happen when # all levels have been released. self._send_state_lock += 1 try: yield finally: self._send_state_lock -=1 if self._send_state_lock == 0: self.send_state(self._states_to_send) self._states_to_send.clear() def _should_send_property(self, key, value): """Check the property lock (property_lock)""" to_json = self.trait_metadata(key, 'to_json', self._trait_to_json) if (key == self._property_lock[0] and to_json(value) == self._property_lock[1]): return False elif self._send_state_lock > 0: self._states_to_send.add(key) return False else: return True # Event handlers @_show_traceback def _handle_msg(self, msg): """Called when a msg is received from the front-end""" data = msg['content']['data'] method = data['method'] # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. if method == 'backbone': if 'sync_data' in data: sync_data = data['sync_data'] self.set_state(sync_data) # handles all methods # Handle a state request. elif method == 'request_state': self.send_state() # Handle a custom msg from the front-end. elif method == 'custom': if 'content' in data: self._handle_custom_msg(data['content']) # Catch remainder. else: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) def _notify_trait(self, name, old_value, new_value): """Called when a property has been changed.""" # Trigger default traitlet callback machinery. This allows any user # registered validation to be processed prior to allowing the widget # machinery to handle the state. LoggingConfigurable._notify_trait(self, name, old_value, new_value) # Send the state after the user registered callbacks for trait changes # have all fired (allows for user to validate values). if self.comm is not None and name in self.keys: # Make sure this isn't information that the front-end just sent us. if self._should_send_property(name, new_value): # Send new state to front-end self.send_state(key=name) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" self._display_callbacks(self, **kwargs) def _trait_to_json(self, x): """Convert a trait value to json Traverse lists/tuples and dicts and serialize their values as well. Replace any widgets with their model_id """ if isinstance(x, dict): return {k: self._trait_to_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): return [self._trait_to_json(v) for v in x] elif isinstance(x, Widget): return "IPY_MODEL_" + x.model_id else: return x # Value must be JSON-able def _trait_from_json(self, x): """Convert json values to objects Replace any strings representing valid model id values to Widget references. """ if isinstance(x, dict): return {k: self._trait_from_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): return [self._trait_from_json(v) for v in x] elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets: # we want to support having child widgets at any level in a hierarchy # trusting that a widget UUID will not appear out in the wild return Widget.widgets[x[10:]] else: return x def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" # Show view. if self._view_name is not None: self._send({"method": "display"}) self._handle_displayed(**kwargs) def _send(self, msg): """Sends a message to the model in the front-end.""" self.comm.send(msg)
class Widget(LoggingConfigurable): #------------------------------------------------------------------------- # Class attributes #------------------------------------------------------------------------- _widget_construction_callback = None widgets = {} @staticmethod def on_widget_constructed(callback): """Registers a callback to be called when a widget is constructed. The callback must have the following signature: callback(widget)""" Widget._widget_construction_callback = callback @staticmethod def _call_widget_constructed(widget): """Static method, called when a widget is constructed.""" if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback): Widget._widget_construction_callback(widget) #------------------------------------------------------------------------- # Traits #------------------------------------------------------------------------- _model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""") _view_name = Unicode('WidgetView', help="""Default view registered in the front-end to use to represent the widget.""", sync=True) _comm = Instance('IPython.kernel.comm.Comm') msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the front-end can send before receiving an idle msg from the back-end.""") keys = List() def _keys_default(self): return [name for name in self.traits(sync=True)] _property_lock = Tuple((None, None)) _display_callbacks = Instance(CallbackDispatcher, ()) _msg_callbacks = Instance(CallbackDispatcher, ()) #------------------------------------------------------------------------- # (Con/de)structor #------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" super(Widget, self).__init__(**kwargs) self.on_trait_change(self._handle_property_changed, self.keys) Widget._call_widget_constructed(self) def __del__(self): """Object disposal""" self.close() #------------------------------------------------------------------------- # Properties #------------------------------------------------------------------------- @property def comm(self): """Gets the Comm associated with this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" if self._comm is None: # Create a comm. self._comm = Comm(target_name=self._model_name) self._comm.on_msg(self._handle_msg) Widget.widgets[self.model_id] = self # first update self.send_state() return self._comm @property def model_id(self): """Gets the model id of this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" return self.comm.comm_id #------------------------------------------------------------------------- # Methods #------------------------------------------------------------------------- def close(self): """Close method. Closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" if self._comm is not None: Widget.widgets.pop(self.model_id, None) self._comm.close() self._comm = None def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end. Parameters ---------- key : unicode (optional) A single property's name to sync with the front-end. """ self._send({ "method" : "update", "state" : self.get_state() }) def get_state(self, key=None): """Gets the widget state, or a piece of it. Parameters ---------- key : unicode (optional) A single property's name to get. """ keys = self.keys if key is None else [key] state = {} for k in keys: f = self.trait_metadata(k, 'to_json') if f is None: f = self._trait_to_json value = getattr(self, k) state[k] = f(value) return state def send(self, content): """Sends a custom msg to the widget model in the front-end. Parameters ---------- content : dict Content of the message to send. """ self._send({"method": "custom", "content": content}) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- callback: callable callback will be passed two arguments when a message arrives:: callback(widget, content) remove: bool True if the callback should be unregistered.""" self._msg_callbacks.register_callback(callback, remove=remove) def on_displayed(self, callback, remove=False): """(Un)Register a widget displayed callback. Parameters ---------- callback: method handler Must have a signature of:: callback(widget, **kwargs) kwargs from display are passed through without modification. remove: bool True if the callback should be unregistered.""" self._display_callbacks.register_callback(callback, remove=remove) #------------------------------------------------------------------------- # Support methods #------------------------------------------------------------------------- @contextmanager def _lock_property(self, key, value): """Lock a property-value pair. NOTE: This, in addition to the single lock for all state changes, is flawed. In the future we may want to look into buffering state changes back to the front-end.""" self._property_lock = (key, value) try: yield finally: self._property_lock = (None, None) def _should_send_property(self, key, value): """Check the property lock (property_lock)""" return key != self._property_lock[0] or \ value != self._property_lock[1] # Event handlers @_show_traceback def _handle_msg(self, msg): """Called when a msg is received from the front-end""" data = msg['content']['data'] method = data['method'] if not method in ['backbone', 'custom']: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. if method == 'backbone' and 'sync_data' in data: sync_data = data['sync_data'] self._handle_receive_state(sync_data) # handles all methods # Handle a custom msg from the front-end elif method == 'custom': if 'content' in data: self._handle_custom_msg(data['content']) def _handle_receive_state(self, sync_data): """Called when a state is received from the front-end.""" for name in self.keys: if name in sync_data: f = self.trait_metadata(name, 'from_json') if f is None: f = self._trait_from_json value = f(sync_data[name]) with self._lock_property(name, value): setattr(self, name, value) def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) def _handle_property_changed(self, name, old, new): """Called when a property has been changed.""" # Make sure this isn't information that the front-end just sent us. if self._should_send_property(name, new): # Send new state to front-end self.send_state(key=name) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" self._display_callbacks(self, **kwargs) def _trait_to_json(self, x): """Convert a trait value to json Traverse lists/tuples and dicts and serialize their values as well. Replace any widgets with their model_id """ if isinstance(x, dict): return {k: self._trait_to_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): return [self._trait_to_json(v) for v in x] elif isinstance(x, Widget): return "IPY_MODEL_" + x.model_id else: return x # Value must be JSON-able def _trait_from_json(self, x): """Convert json values to objects Replace any strings representing valid model id values to Widget references. """ if isinstance(x, dict): return {k: self._trait_from_json(v) for k, v in x.items()} elif isinstance(x, (list, tuple)): return [self._trait_from_json(v) for v in x] elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets: # we want to support having child widgets at any level in a hierarchy # trusting that a widget UUID will not appear out in the wild return Widget.widgets[x] else: return x def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" # Show view. By sending a display message, the comm is opened and the # initial state is sent. self._send({"method": "display"}) self._handle_displayed(**kwargs) def _send(self, msg): """Sends a message to the model in the front-end.""" self.comm.send(msg)
class Visualization(object): def __init__(self, session=None, json=None, auth=None): self.session = session self.id = json.get('id') self.auth = auth if self.session.lgn.ipython_enabled: from IPython.kernel.comm import Comm self.comm = Comm('lightning', {'id': self.id}) self.comm_handlers = {} self.comm.on_msg(self._handle_comm_message) def _format_url(self, url): if not url.endswith('/'): url += '/' try: from urllib.parse import quote except ImportError: from urllib import quote return url + '?host=' + quote(self.session.host) def _update_image(self, image): url = self.session.host + '/sessions/' + str(self.session.id) + '/visualizations/' + str(self.id) + '/data/images' url = self._format_url(url) files = {'file': image} return requests.put(url, files=files, data={'type': 'image'}, auth=self.auth) def _append_image(self, image): url = self.session.host + '/sessions/' + str(self.session.id) + '/visualizations/' + str(self.id) + '/data/images' url = self._format_url(url) files = {'file': image} return requests.post(url, files=files, data={'type': 'image'}, auth=self.auth) def _append_data(self, data=None, field=None): payload = {'data': data} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} url = self.session.host + '/sessions/' + str(self.session.id) + '/visualizations/' + str(self.id) + '/data/' if field: url += field url = self._format_url(url) return requests.post(url, data=json.dumps(payload), headers=headers, auth=self.auth) def _update_data(self, data=None, field=None): payload = {'data': data} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} url = self.session.host + '/sessions/' + str(self.session.id) + '/visualizations/' + str(self.id) + '/data/' if field: url += field url = self._format_url(url) return requests.put(url, data=json.dumps(payload), headers=headers, auth=self.auth) def get_permalink(self): return self.session.host + '/visualizations/' + str(self.id) def get_publiclink(self): return self.get_permalink() + '/public/' def get_embed_link(self): return self._format_url(self.get_permalink() + '/embed') def get_html(self): r = requests.get(self.get_embed_link(), auth=self.auth) return r.text def open(self): webbrowser.open(self.get_publiclink()) def delete(self): url = self.get_permalink() return requests.delete(url) def on(self, event_name, handler): if self.session.lgn.ipython_enabled: self.comm_handlers[event_name] = handler else: raise Exception('The current implementation of this method is only compatible with IPython.') def _handle_comm_message(self, message): # Parsing logic taken from similar code in matplotlib message = json.loads(message['content']['data']) if message['type'] in self.comm_handlers: self.comm_handlers[message['type']](message['data']) @classmethod def _create(cls, session=None, data=None, images=None, type=None, options=None, description=None): if options is None: options = {} url = session.host + '/sessions/' + str(session.id) + '/visualizations' if not images: payload = {'data': data, 'type': type, 'options': options} if description: payload['description'] = description headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} r = requests.post(url, data=json.dumps(payload), headers=headers, auth=session.auth) if r.status_code == 404: raise Exception(r.text) elif not r.status_code == requests.codes.ok: raise Exception('Problem uploading data') viz = cls(session=session, json=r.json(), auth=session.auth) else: first_image, remaining_images = images[0], images[1:] files = {'file': first_image} payload = {'type': type, 'options': json.dumps(options)} if description: payload['description'] = description r = requests.post(url, files=files, data=payload, auth=session.auth) if r.status_code == 404: raise Exception(r.text) elif not r.status_code == requests.codes.ok: raise Exception('Problem uploading images') viz = cls(session=session, json=r.json(), auth=session.auth) for image in remaining_images: viz._append_image(image) return viz
class BrowserContext(object): """Represents an in-browser context.""" def __init__(self): """Constructor""" self._calls = 0 self._callbacks = {} # Push the Javascript to the front-end. with open(os.path.join(os.path.split(__file__)[0], 'backend_context.js'), 'r') as f: display(Javascript(data=f.read())) # Open communication with the front-end. self._comm = Comm(target_name='BrowserContext') self._comm.on_msg(self._on_msg) def _on_msg(self, msg): """Handle messages from the front-end""" data = msg['content']['data'] # If the message is a call invoke, run the function and send # the results. if 'callback' in data: guid = data['callback'] callback = callback_registry[guid] args = data['arguments'] args = [self.deserialize(a) for a in args] index = data['index'] results = callback(*args) return self.serialize(self._send('return', index=index, results=results)) # The message is not a call invoke, it must be an object # that is a response to a Python request. else: index = data['index'] immutable = data['immutable'] value = data['value'] if index in self._callbacks: self._callbacks[index].resolve({ 'immutable': immutable, 'value': value }) del self._callbacks[index] def serialize(self, obj): """Serialize an object for sending to the front-end.""" if hasattr(obj, '_jsid'): return {'immutable': False, 'value': obj._jsid} else: obj_json = {'immutable': True} try: json.dumps(obj) obj_json['value'] = obj except: pass if callable(obj): guid = str(uuid.uuid4()) callback_registry[guid] = obj obj_json['callback'] = guid return obj_json def deserialize(self, obj): """Deserialize an object from the front-end.""" if obj['immutable']: return obj['value'] else: guid = obj['value'] if not guid in object_registry: instance = JSObject(self, guid) object_registry[guid] = instance return object_registry[guid] # Message types def getattr(self, parent, child): return self._send('getattr', parent=parent, child=child) def setattr(self, parent, child, value): return self._send('setattr', parent=parent, child=child, value=value) def apply(self, parent, function, *pargs): return self._send('apply', parent=parent, function=function, args=pargs) def _send(self, method, **parameters): """Sends a message to the front-end and returns a promise.""" msg = { 'index': self._calls, 'method': method, } msg.update(parameters) promise = SimplePromise() self._callbacks[self._calls] = promise self._calls += 1 self._comm.send(msg) return promise
class Widget(LoggingConfigurable): # ------------------------------------------------------------------------- # Class attributes # ------------------------------------------------------------------------- _widget_construction_callback = None widgets = {} @staticmethod def on_widget_constructed(callback): """Registers a callback to be called when a widget is constructed. The callback must have the following signature: callback(widget)""" Widget._widget_construction_callback = callback @staticmethod def _call_widget_constructed(widget): """Static method, called when a widget is constructed.""" if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback): Widget._widget_construction_callback(widget) # ------------------------------------------------------------------------- # Traits # ------------------------------------------------------------------------- model_name = Unicode( "WidgetModel", help="""Name of the backbone model registered in the front-end to create and sync this widget with.""", ) _view_name = Unicode( help="""Default view registered in the front-end to use to represent the widget.""", sync=True, ) _comm = Instance("IPython.kernel.comm.Comm") closed = Bool(False) keys = List() def _keys_default(self): return [name for name in self.traits(sync=True)] _property_lock = Tuple((None, None)) _display_callbacks = Instance(CallbackDispatcher, ()) _msg_callbacks = Instance(CallbackDispatcher, ()) # ------------------------------------------------------------------------- # (Con/de)structor # ------------------------------------------------------------------------- def __init__(self, **kwargs): """Public constructor""" super(Widget, self).__init__(**kwargs) self.on_trait_change(self._handle_property_changed, self.keys) Widget._call_widget_constructed(self) def __del__(self): """Object disposal""" self.close() # ------------------------------------------------------------------------- # Properties # ------------------------------------------------------------------------- @property def comm(self): """Gets the Comm associated with this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" if self._comm is None: # Create a comm. self._comm = Comm(target_name=self.model_name) self._comm.on_msg(self._handle_msg) self._comm.on_close(self._close) Widget.widgets[self.model_id] = self # first update self.send_state() return self._comm @property def model_id(self): """Gets the model id of this widget. If a Comm doesn't exist yet, a Comm will be created automagically.""" return self.comm.comm_id # ------------------------------------------------------------------------- # Methods # ------------------------------------------------------------------------- def _close(self): """Private close - cleanup objects, registry entries""" del Widget.widgets[self.model_id] self._comm = None self.closed = True def close(self): """Close method. Closes the widget which closes the underlying comm. When the comm is closed, all of the widget views are automatically removed from the front-end.""" if not self.closed: self._comm.close() self._close() def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end. Parameters ---------- key : unicode (optional) A single property's name to sync with the front-end. """ self._send({"method": "update", "state": self.get_state()}) def get_state(self, key=None): """Gets the widget state, or a piece of it. Parameters ---------- key : unicode (optional) A single property's name to get. """ keys = self.keys if key is None else [key] return {k: self._pack_widgets(getattr(self, k)) for k in keys} def send(self, content): """Sends a custom msg to the widget model in the front-end. Parameters ---------- content : dict Content of the message to send. """ self._send({"method": "custom", "content": content}) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- callback: callable callback will be passed two arguments when a message arrives: callback(widget, content) remove: bool True if the callback should be unregistered.""" self._msg_callbacks.register_callback(callback, remove=remove) def on_displayed(self, callback, remove=False): """(Un)Register a widget displayed callback. Parameters ---------- callback: method handler Must have a signature of: callback(widget, **kwargs) kwargs from display are passed through without modification. remove: bool True if the callback should be unregistered.""" self._display_callbacks.register_callback(callback, remove=remove) # ------------------------------------------------------------------------- # Support methods # ------------------------------------------------------------------------- @contextmanager def _lock_property(self, key, value): """Lock a property-value pair. NOTE: This, in addition to the single lock for all state changes, is flawed. In the future we may want to look into buffering state changes back to the front-end.""" self._property_lock = (key, value) try: yield finally: self._property_lock = (None, None) def _should_send_property(self, key, value): """Check the property lock (property_lock)""" return key != self._property_lock[0] or value != self._property_lock[1] # Event handlers def _handle_msg(self, msg): """Called when a msg is received from the front-end""" data = msg["content"]["data"] method = data["method"] if not method in ["backbone", "custom"]: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one. if method == "backbone" and "sync_data" in data: sync_data = data["sync_data"] self._handle_receive_state(sync_data) # handles all methods # Handle a custom msg from the front-end elif method == "custom": if "content" in data: self._handle_custom_msg(data["content"]) def _handle_receive_state(self, sync_data): """Called when a state is received from the front-end.""" for name in self.keys: if name in sync_data: value = self._unpack_widgets(sync_data[name]) with self._lock_property(name, value): setattr(self, name, value) def _handle_custom_msg(self, content): """Called when a custom msg is received.""" self._msg_callbacks(self, content) def _handle_property_changed(self, name, old, new): """Called when a property has been changed.""" # Make sure this isn't information that the front-end just sent us. if self._should_send_property(name, new): # Send new state to front-end self.send_state(key=name) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" self._display_callbacks(self, **kwargs) def _pack_widgets(self, x): """Recursively converts all widget instances to model id strings. Children widgets will be stored and transmitted to the front-end by their model ids. Return value must be JSON-able.""" if isinstance(x, dict): return {k: self._pack_widgets(v) for k, v in x.items()} elif isinstance(x, list): return [self._pack_widgets(v) for v in x] elif isinstance(x, Widget): return x.model_id else: return x # Value must be JSON-able def _unpack_widgets(self, x): """Recursively converts all model id strings to widget instances. Children widgets will be stored and transmitted to the front-end by their model ids.""" if isinstance(x, dict): return {k: self._unpack_widgets(v) for k, v in x.items()} elif isinstance(x, list): return [self._unpack_widgets(v) for v in x] elif isinstance(x, string_types): return x if x not in Widget.widgets else Widget.widgets[x] else: return x def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" # Show view. By sending a display message, the comm is opened and the # initial state is sent. self._send({"method": "display"}) self._handle_displayed(**kwargs) def _send(self, msg): """Sends a message to the model in the front-end.""" self.comm.send(msg)
class CommSocket(object): """ Manages the Comm connection between IPython and the browser (client). Comms are 2 way, with the CommSocket being able to publish a message via the send_json method, and handle a message with on_message. On the JS side figure.send_message and figure.ws.onmessage do the sending and receiving respectively. """ def __init__(self, manager): self.supports_binary = None self.manager = manager self.uuid = str(uuid()) # Publish an output area with a unique ID. The javascript can then # hook into this area. display(HTML("<div id=%r></div>" % self.uuid)) try: self.comm = Comm("matplotlib", data={"id": self.uuid}) except AttributeError: raise RuntimeError( "Unable to create an IPython notebook Comm " "instance. Are you in the IPython notebook?" ) self.comm.on_msg(self.on_message) manager = self.manager self.comm.on_close(lambda close_message: manager.clearup_closed()) def is_open(self): return not self.comm._closed def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. self.comm.close() self.manager.clearup_closed() def send_json(self, content): self.comm.send({"data": json.dumps(content)}) def send_binary(self, blob): # The comm is ascii, so we always send the image in base64 # encoded data URL form. data = b64encode(blob) if six.PY3: data = data.decode("ascii") data_uri = "data:image/png;base64,{0}".format(data) self.comm.send({"data": data_uri}) def on_message(self, message): # The 'supports_binary' message is relevant to the # websocket itself. The other messages get passed along # to matplotlib as-is. # Every message has a "type" and a "figure_id". message = json.loads(message["content"]["data"]) if message["type"] == "closing": self.on_close() elif message["type"] == "supports_binary": self.supports_binary = message["value"] else: self.manager.handle_json(message)
class Visualization(object): def __init__(self, session=None, json=None, auth=None): self.session = session self.id = json.get("id") self.auth = auth if self.session.lgn.ipython_enabled: from IPython.kernel.comm import Comm self.comm = Comm("lightning", {"id": self.id}) self.comm_handlers = {} self.comm.on_msg(self._handle_comm_message) def _format_url(self, url): if not url.endswith("/"): url += "/" try: from urllib.parse import quote except ImportError: from urllib import quote return url + "?host=" + quote(self.session.host) def _update_image(self, image): url = ( self.session.host + "/sessions/" + str(self.session.id) + "/visualizations/" + str(self.id) + "/data/images" ) url = self._format_url(url) files = {"file": image} return requests.put(url, files=files, data={"type": "image"}, auth=self.auth) def _append_image(self, image): url = ( self.session.host + "/sessions/" + str(self.session.id) + "/visualizations/" + str(self.id) + "/data/images" ) url = self._format_url(url) files = {"file": image} return requests.post(url, files=files, data={"type": "image"}, auth=self.auth) def _append_data(self, data=None, field=None): payload = {"data": data} headers = {"Content-type": "application/json", "Accept": "text/plain"} url = self.session.host + "/sessions/" + str(self.session.id) + "/visualizations/" + str(self.id) + "/data/" if field: url += field url = self._format_url(url) return requests.post(url, data=json.dumps(payload), headers=headers, auth=self.auth) def _update_data(self, data=None, field=None): payload = {"data": data} headers = {"Content-type": "application/json", "Accept": "text/plain"} url = self.session.host + "/sessions/" + str(self.session.id) + "/visualizations/" + str(self.id) + "/data/" if field: url += field url = self._format_url(url) return requests.put(url, data=json.dumps(payload), headers=headers, auth=self.auth) def get_permalink(self): return self.session.host + "/visualizations/" + str(self.id) def get_embed_link(self): return self._format_url(self.get_permalink() + "/embed") def get_html(self): r = requests.get(self.get_embed_link(), auth=self.auth) return r.text def open(self): webbrowser.open(self.session.host + "/visualizations/" + str(self.id) + "/") def delete(self): url = self.get_permalink() return requests.delete(url) def on(self, event_name, handler): if self.session.lgn.ipython_enabled: self.comm_handlers[event_name] = handler else: raise Exception("The current implementation of this method is only compatible with IPython.") def _handle_comm_message(self, message): # Parsing logic taken from similar code in matplotlib message = json.loads(message["content"]["data"]) if message["type"] in self.comm_handlers: self.comm_handlers[message["type"]](message["data"]) @classmethod def _create(cls, session=None, data=None, images=None, type=None, options=None): if options is None: options = {} url = session.host + "/sessions/" + str(session.id) + "/visualizations" if not images: payload = {"data": data, "type": type, "options": options} headers = {"Content-type": "application/json", "Accept": "text/plain"} r = requests.post(url, data=json.dumps(payload), headers=headers, auth=session.auth) if r.status_code == 404: raise Exception(r.text) elif not r.status_code == requests.codes.ok: raise Exception("Problem uploading data") viz = cls(session=session, json=r.json(), auth=session.auth) else: first_image, remaining_images = images[0], images[1:] files = {"file": first_image} r = requests.post(url, files=files, data={"type": type, "options": json.dumps(options)}, auth=session.auth) if r.status_code == 404: raise Exception(r.text) elif not r.status_code == requests.codes.ok: raise Exception("Problem uploading images") viz = cls(session=session, json=r.json(), auth=session.auth) for image in remaining_images: viz._append_image(image) return viz
class Visualization(object): def __init__(self, session=None, json=None, auth=None): self.session = session self.id = json.get('id') self.auth = auth if self.session.lgn.ipython_enabled: from IPython.kernel.comm import Comm self.comm = Comm('lightning', {'id': self.id}) self.comm_handlers = {} self.comm.on_msg(self._handle_comm_message) def _format_url(self, url): if not url.endswith('/'): url += '/' try: from urllib.parse import quote except ImportError: from urllib import quote return url + '?host=' + quote(self.session.host) def _update_image(self, image): url = self.session.host + '/sessions/' + str( self.session.id) + '/visualizations/' + str( self.id) + '/data/images' url = self._format_url(url) files = {'file': image} return requests.put(url, files=files, data={'type': 'image'}, auth=self.auth) def _append_image(self, image): url = self.session.host + '/sessions/' + str( self.session.id) + '/visualizations/' + str( self.id) + '/data/images' url = self._format_url(url) files = {'file': image} return requests.post(url, files=files, data={'type': 'image'}, auth=self.auth) def _append_data(self, data=None, field=None): payload = {'data': data} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} url = self.session.host + '/sessions/' + str( self.session.id) + '/visualizations/' + str(self.id) + '/data/' if field: url += field url = self._format_url(url) return requests.post(url, data=json.dumps(payload), headers=headers, auth=self.auth) def _update_data(self, data=None, field=None): payload = {'data': data} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} url = self.session.host + '/sessions/' + str( self.session.id) + '/visualizations/' + str(self.id) + '/data/' if field: url += field url = self._format_url(url) return requests.put(url, data=json.dumps(payload), headers=headers, auth=self.auth) def get_permalink(self): return self.session.host + '/visualizations/' + str(self.id) def get_public_link(self): return self.get_permalink() + '/public/' def get_embed_link(self): return self._format_url(self.get_permalink() + '/embed') def get_html(self): r = requests.get(self.get_embed_link(), auth=self.auth) return r.text def open(self): webbrowser.open(self.get_public_link()) def delete(self): url = self.get_permalink() return requests.delete(url) def on(self, event_name, handler): if self.session.lgn.ipython_enabled: self.comm_handlers[event_name] = handler else: raise Exception( 'The current implementation of this method is only compatible with IPython.' ) def _handle_comm_message(self, message): # Parsing logic taken from similar code in matplotlib message = json.loads(message['content']['data']) if message['type'] in self.comm_handlers: self.comm_handlers[message['type']](message['data']) @classmethod def _create(cls, session=None, data=None, images=None, type=None, options=None, description=None): if options is None: options = {} url = session.host + '/sessions/' + str(session.id) + '/visualizations' if not images: payload = {'data': data, 'type': type, 'options': options} if description: payload['description'] = description headers = { 'Content-type': 'application/json', 'Accept': 'text/plain' } r = requests.post(url, data=json.dumps(payload), headers=headers, auth=session.auth) if r.status_code == 404: raise Exception(r.text) elif not r.status_code == requests.codes.ok: raise Exception('Problem uploading data') viz = cls(session=session, json=r.json(), auth=session.auth) else: first_image, remaining_images = images[0], images[1:] files = {'file': first_image} payload = {'type': type, 'options': json.dumps(options)} if description: payload['description'] = description r = requests.post(url, files=files, data=payload, auth=session.auth) if r.status_code == 404: raise Exception(r.text) elif not r.status_code == requests.codes.ok: raise Exception('Problem uploading images') viz = cls(session=session, json=r.json(), auth=session.auth) for image in remaining_images: viz._append_image(image) return viz