def bkapp_command(): with pull_session(session_id='1234567890', url="http://localhost:5006/myapp") as session: session.request_server_info() cur_doc = session.document button = cur_doc.get_model_by_name('button_text') cur_doc.remove_root(button) import json from bokeh.events import Event data = '{"event_name": "document_update", "event_values" : {"x": 10, "y": 20, "sx": 200, "sy": 37}}' json.loads(data) event = json.loads(data, object_hook=Event.decode_json) event = json.loads(data) cur_doc.apply_json_event(event) from bokeh.protocol import Protocol # from bokeh.events import DocumentUpdate # document_update_event = DocumentUpdate() # protocol = Protocol() # message = protocol.create("PATCH-DOC", [document_update_event]) # message.apply_to_document(cur_doc) #session._connection.send_message(message) from bokeh.document.events import MessageSentEvent document_patched_event = MessageSentEvent( document=cur_doc, msg_type='append_dataset', msg_data='append_dataset_data') protocol = Protocol() message = protocol.create("PATCH-DOC", [document_patched_event]) # message.apply_to_document(cur_doc) session._connection.send_message(message) return ''
def test_create_then_parse(self): sample = self._sample_doc() msg = Protocol("1.0").create("PUSH-DOC", sample) copy = document.Document() msg.push_to_document(copy) assert len(sample.roots) == 2 assert len(copy.roots) == 2
def patch_event(image): """ Generates a bokeh patch event message given an InteractiveImage instance. Uses the bokeh messaging protocol for bokeh>=0.12.10 and a custom patch for previous versions. Parameters ---------- image: InteractiveImage InteractiveImage instance with a plot Returns ------- msg: str JSON message containing patch events to update the plot """ if bokeh_version > '0.12.9': event_obj = image.doc.callbacks if bokeh_version >= '2.4' else image.doc events = list(event_obj._held_events) if not events: return None if bokeh_version > '2.0.0': protocol = Protocol() else: protocol = Protocol("1.0") msg = protocol.create("PATCH-DOC", events) event_obj._held_events = [] return msg data = dict(image.ds.data) data['image'] = [data['image'][0].tolist()] return json.dumps({'events': [{'attr': u'data', 'kind': 'ModelChanged', 'model': image.ds.ref, 'new': data}], 'references': []})
def test_create_reply_then_parse(self): sample = self._sample_doc() msg = Protocol("1.0").create("PULL-DOC-REPLY", 'fakereqid', sample) copy = document.Document() msg.push_to_document(copy) assert len(sample.roots) == 2 assert len(copy.roots) == 2
async def open(self, path, *args, **kwargs): _, context = _APPS[path] token = self._token if self.selected_subprotocol != 'bokeh': self.close() raise ProtocolError("Subprotocol header is not 'bokeh'") elif token is None: self.close() raise ProtocolError("No token received in subprotocol header") session_id = get_session_id(token) await context.create_session_if_needed(session_id, self.request, token) session = context.get_session(session_id) try: protocol = Protocol() self.receiver = Receiver(protocol) self.handler = ProtocolHandler() self.connection = self.new_connection(protocol, context, session) except ProtocolError as e: self.close() raise e msg = self.connection.protocol.create('ACK') await self.send_message(msg)
async def _async_open(self, session_id: str) -> None: try: await self.application_context.create_session_if_needed( session_id, self.request) session = self.application_context.get_session(session_id) protocol = Protocol() self.receiver = Receiver(protocol) log.debug("Receiver created for %r", protocol) self.handler = ProtocolHandler() log.debug("ProtocolHandler created for %r", protocol) self.connection = self._new_connection(protocol, self, self.application_context, session) log.info("ServerConnection created") except Exception as e: log.error("Could not create new server session, reason: %s", e) self.close() raise e msg = self.connection.protocol.create('ACK') await self._send_bokeh_message(msg)
def __init__(self, session, websocket_url, io_loop=None): ''' Opens a websocket connection to the server. ''' self._url = websocket_url self._session = session self._protocol = Protocol("1.0") self._receiver = Receiver(self._protocol) self._socket = None self._state = NOT_YET_CONNECTED() if io_loop is None: # We can't use IOLoop.current because then we break # when running inside a notebook since ipython also uses it io_loop = IOLoop() self._loop = io_loop self._until_predicate = None self._protocol = Protocol("1.0") self._server_info = None
def __init__(self, session, websocket_url, io_loop=None): ''' Opens a websocket connection to the server. ''' self._url = websocket_url self._session = session self._protocol = Protocol("1.0") self._receiver = Receiver(self._protocol) self._socket = None self._state = self.NOT_YET_CONNECTED() if io_loop is None: # We can't use IOLoop.current because then we break # when running inside a notebook since ipython also uses it io_loop = IOLoop() self._loop = io_loop self._until_predicate = None self._protocol = Protocol("1.0") self._server_info = None
def test_create_multiple_docs(self): sample1 = self._sample_doc() obj1 = next(iter(sample1.roots)) event1 = ModelChangedEvent(sample1, obj1, 'foo', obj1.foo, 42, 42) sample2 = self._sample_doc() obj2 = next(iter(sample2.roots)) event2 = ModelChangedEvent(sample2, obj2, 'foo', obj2.foo, 42, 42) with pytest.raises(ValueError): Protocol("1.0").create("PATCH-DOC", [event1, event2])
def diff(doc, binary=True, events=None): """ Returns a json diff required to update an existing plot with the latest plot data. """ events = list(doc._held_events) if events is None else events if not events: return None msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary) doc._held_events = [e for e in doc._held_events if e not in events] return msg
def _document_patched(self, event): if event.setter is self: return msg = Protocol().create("PATCH-DOC", [event]) self.send({"msg": "patch", "payload": msg.header_json}) self.send({"msg": "patch", "payload": msg.metadata_json}) self.send({"msg": "patch", "payload": msg.content_json}) for header, buffer in msg.buffers: self.send({"msg": "patch", "payload": json.dumps(header)}) self.send({"msg": "patch"}, [buffer])
def diff(self, plot, binary=True): """ Returns a json diff required to update an existing plot with the latest plot data. """ events = list(plot.document._held_events) if not events: return None msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary) plot.document._held_events = [] return msg
def test_create_then_apply_model_changed(self): sample = self._sample_doc() foos = [] for r in sample.roots: foos.append(r.foo) assert foos == [ 2, 2 ] obj = next(iter(sample.roots)) assert obj.foo == 2 event = ModelChangedEvent(sample, obj, 'foo', obj.foo, 42, 42) msg = Protocol("1.0").create("PATCH-DOC", [event]) copy = document.Document.from_json_string(sample.to_json_string()) msg.apply_to_document(copy) foos = [] for r in copy.roots: foos.append(r.foo) foos.sort() assert foos == [ 2, 42 ]
def _send_notebook_diff(self): events = list(self.document._held_events) msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=True) self.document._held_events = [] if msg is None: return self.server_comm.send(msg.header_json) self.server_comm.send(msg.metadata_json) self.server_comm.send(msg.content_json) for header, payload in msg.buffers: self.server_comm.send(json.dumps(header)) self.server_comm.send(buffers=[payload])
def diff(self, plot, binary=True, individual=False): """ Returns a json diff required to update an existing plot with the latest plot data. """ events = list(plot.document._held_events) if not events: return None if individual: msgs = [] for event in events: msg = Protocol("1.0").create("PATCH-DOC", [event], use_buffers=binary) msgs.append(msg) else: msgs = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary) plot.document._held_events = [] return msgs
def test_create_then_apply_model_changed(self): sample = self._sample_doc() foos = [] for r in sample.roots: foos.append(r.foo) assert foos == [2, 2] obj = next(iter(sample.roots)) assert obj.foo == 2 event = ModelChangedEvent(sample, obj, 'foo', obj.foo, 42, 42) msg = Protocol("1.0").create("PATCH-DOC", [event]) copy = document.Document.from_json_string(sample.to_json_string()) msg.apply_to_document(copy) foos = [] for r in copy.roots: foos.append(r.foo) foos.sort() assert foos == [2, 42]
def diff(doc, binary=True, events=None): """ Returns a json diff required to update an existing plot with the latest plot data. """ events = list(doc._held_events) if events is None else events if not events or state._hold: return None # Patch ColumnDataChangedEvents which reference non-existing columns for e in events: if (hasattr(e, 'hint') and isinstance(e.hint, ColumnDataChangedEvent) and e.hint.cols is not None): e.hint.cols = None msg = Protocol().create("PATCH-DOC", events, use_buffers=binary) doc._held_events = [e for e in doc._held_events if e not in events] return msg
def diff(self, plot, serialize=True, binary=False): """ Returns a json diff required to update an existing plot with the latest plot data. """ if binary: events = list(plot.document._held_events) if not events: return None msg = Protocol("1.0").create("PATCH-DOC", events) plot.document._held_events = [] return msg else: plotobjects = [h for handles in plot.traverse(lambda x: x.current_handles, [lambda x: x._updated]) for h in handles] plot.traverse(lambda x: setattr(x, '_updated', False)) patch = compute_static_patch(plot.document, plotobjects) processed = self._apply_post_render_hooks(patch, plot, 'json') return serialize_json(processed) if serialize else processed
def diff(doc, binary=True, events=None): """ Returns a json diff required to update an existing plot with the latest plot data. """ events = list(doc._held_events) if events is None else events if not events or state._hold: return None # Filter ColumnDataChangedEvents which reference non-existing # columns, later event will include the changes fixed_events = [] for e in events: if isinstance(e.hint, ColumnDataChangedEvent) and e.hint.cols is not None: e.hint.cols = None fixed_events.append(e) msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary) doc._held_events = [e for e in doc._held_events if e not in events] return msg
def test_patch_event_contains_setter(self): sample = self._sample_doc() root = None other_root = None for r in sample.roots: if r.child is not None: root = r else: other_root = r assert root is not None assert other_root is not None new_child = AnotherModelInTestPatchDoc(bar=56) cds = ColumnDataSource(data={'a': [0, 1, 2]}) sample.add_root(cds) mock_session = object() def sample_document_callback_assert(event): """Asserts that setter is correctly set on event""" assert event.setter is mock_session sample.on_change(sample_document_callback_assert) # Model property changed event = ModelChangedEvent(sample, root, 'child', root.child, new_child, new_child) msg = Protocol("1.0").create("PATCH-DOC", [event]) msg.apply_to_document(sample, mock_session) # RootAdded event2 = RootAddedEvent(sample, root) msg2 = Protocol("1.0").create("PATCH-DOC", [event2]) msg2.apply_to_document(sample, mock_session) # RootRemoved event3 = RootRemovedEvent(sample, root) msg3 = Protocol("1.0").create("PATCH-DOC", [event3]) msg3.apply_to_document(sample, mock_session) # ColumnsStreamed event4 = ModelChangedEvent(sample, cds, 'data', 10, None, None, hint=ColumnsStreamedEvent( sample, cds, {"a": [3]}, None, mock_session)) msg4 = Protocol("1.0").create("PATCH-DOC", [event4]) msg4.apply_to_document(sample, mock_session) # ColumnsPatched event5 = ModelChangedEvent(sample, cds, 'data', 10, None, None, hint=ColumnsPatchedEvent( sample, cds, {"a": [(0, 11)]})) msg5 = Protocol("1.0").create("PATCH-DOC", [event5]) msg5.apply_to_document(sample, mock_session)
def test_create_reply(self): sample = self._sample_doc() Protocol("1.0").create("PULL-DOC-REPLY", 'fakereqid', sample)
def test_create_req(self): Protocol("1.0").create("PULL-DOC-REQ")
def test_create(self): sample = self._sample_doc() Protocol("1.0").create("PUSH-DOC", sample)
def test_patch_event_contains_setter(self): sample = self._sample_doc() root = None other_root = None for r in sample.roots: if r.child is not None: root = r else: other_root = r assert root is not None assert other_root is not None new_child = AnotherModelInTestPatchDoc(bar=56) cds = ColumnDataSource(data={'a': np.array([0., 1., 2.])}) sample.add_root(cds) mock_session = object() def sample_document_callback_assert(event): """Asserts that setter is correctly set on event""" assert event.setter is mock_session sample.on_change(sample_document_callback_assert) # Model property changed event = ModelChangedEvent(sample, root, 'child', root.child, new_child, new_child) msg = Protocol("1.0").create("PATCH-DOC", [event]) msg.apply_to_document(sample, mock_session) assert msg.buffers == [] # RootAdded event2 = RootAddedEvent(sample, root) msg2 = Protocol("1.0").create("PATCH-DOC", [event2]) msg2.apply_to_document(sample, mock_session) assert msg2.buffers == [] # RootRemoved event3 = RootRemovedEvent(sample, root) msg3 = Protocol("1.0").create("PATCH-DOC", [event3]) msg3.apply_to_document(sample, mock_session) assert msg3.buffers == [] # ColumnsStreamed event4 = ModelChangedEvent(sample, cds, 'data', 10, None, None, hint=ColumnsStreamedEvent(sample, cds, {"a": [3]}, None, mock_session)) msg4 = Protocol("1.0").create("PATCH-DOC", [event4]) msg4.apply_to_document(sample, mock_session) assert msg4.buffers == [] # ColumnsPatched event5 = ModelChangedEvent(sample, cds, 'data', 10, None, None, hint=ColumnsPatchedEvent(sample, cds, {"a": [(0, 11)]})) msg5 = Protocol("1.0").create("PATCH-DOC", [event5]) msg5.apply_to_document(sample, mock_session) assert msg5.buffers == [] # ColumnDataChanged, use_buffers=False event6 = ModelChangedEvent(sample, cds, 'data', {'a': np.array([0., 1.])}, None, None, hint=ColumnDataChangedEvent(sample, cds)) msg6 = Protocol("1.0").create("PATCH-DOC", [event6], use_buffers=False) msg6.apply_to_document(sample, mock_session) assert msg6.buffers == [] print(cds.data) # ColumnDataChanged, use_buffers=True event7 = ModelChangedEvent(sample, cds, 'data', {'a': np.array([0., 1.])}, None, None, hint=ColumnDataChangedEvent(sample, cds)) msg7 = Protocol("1.0").create("PATCH-DOC", [event7]) # can't test apply, doc not set up to *receive* binary buffers # msg7.apply_to_document(sample, mock_session) assert len(msg7.buffers) == 1 buf = msg7.buffers.pop() assert len(buf) == 2 assert isinstance(buf[0], dict) assert list(buf[0]) == ['id'] # reports CDS buffer *as it is* Normally events called by setter and # value in local object would have been already mutated. assert buf[1] == np.array([11., 1., 2., 3]).tobytes()
def __init__(self, **properties): super().__init__(**properties) self._protocol = Protocol()
# External imports # Bokeh imports from bokeh.protocol import Protocol from bokeh.protocol.exceptions import ValidationError from bokeh.util.string import decode_utf8 # Module under test from bokeh.protocol import receiver #----------------------------------------------------------------------------- # Setup #----------------------------------------------------------------------------- _proto = Protocol("1.0") #----------------------------------------------------------------------------- # General API #----------------------------------------------------------------------------- def test_creation(): receiver.Receiver(None) def test_validation_success(): msg = _proto.create('ACK') r = receiver.Receiver(_proto) partial = r.consume(decode_utf8(msg.header_json)).result()
def test_patch_event_contains_setter(self): sample = self._sample_doc() root = None other_root = None for r in sample.roots: if r.child is not None: root = r else: other_root = r assert root is not None assert other_root is not None new_child = AnotherModelInTestPatchDoc(bar=56) cds = ColumnDataSource(data={'a': np.array([0., 1., 2.])}) sample.add_root(cds) mock_session = object() def sample_document_callback_assert(event): """Asserts that setter is correctly set on event""" assert event.setter is mock_session sample.on_change(sample_document_callback_assert) # Model property changed event = ModelChangedEvent(sample, root, 'child', root.child, new_child, new_child) msg = Protocol("1.0").create("PATCH-DOC", [event]) msg.apply_to_document(sample, mock_session) assert msg.buffers == [] # RootAdded event2 = RootAddedEvent(sample, root) msg2 = Protocol("1.0").create("PATCH-DOC", [event2]) msg2.apply_to_document(sample, mock_session) assert msg2.buffers == [] # RootRemoved event3 = RootRemovedEvent(sample, root) msg3 = Protocol("1.0").create("PATCH-DOC", [event3]) msg3.apply_to_document(sample, mock_session) assert msg3.buffers == [] # ColumnsStreamed event4 = ModelChangedEvent(sample, cds, 'data', 10, None, None, hint=ColumnsStreamedEvent( sample, cds, {"a": [3]}, None, mock_session)) msg4 = Protocol("1.0").create("PATCH-DOC", [event4]) msg4.apply_to_document(sample, mock_session) assert msg4.buffers == [] # ColumnsPatched event5 = ModelChangedEvent(sample, cds, 'data', 10, None, None, hint=ColumnsPatchedEvent( sample, cds, {"a": [(0, 11)]})) msg5 = Protocol("1.0").create("PATCH-DOC", [event5]) msg5.apply_to_document(sample, mock_session) assert msg5.buffers == [] # ColumnDataChanged, use_buffers=False event6 = ModelChangedEvent(sample, cds, 'data', {'a': np.array([0., 1.])}, None, None, hint=ColumnDataChangedEvent(sample, cds)) msg6 = Protocol("1.0").create("PATCH-DOC", [event6], use_buffers=False) msg6.apply_to_document(sample, mock_session) assert msg6.buffers == [] print(cds.data) # ColumnDataChanged, use_buffers=True event7 = ModelChangedEvent(sample, cds, 'data', {'a': np.array([0., 1.])}, None, None, hint=ColumnDataChangedEvent(sample, cds)) msg7 = Protocol("1.0").create("PATCH-DOC", [event7]) # can't test apply, doc not set up to *receive* binary buffers # msg7.apply_to_document(sample, mock_session) assert len(msg7.buffers) == 1 buf = msg7.buffers.pop() assert len(buf) == 2 assert isinstance(buf[0], dict) assert list(buf[0]) == ['id'] # reports CDS buffer *as it is* Normally events called by setter and # value in local object would have been already mutated. assert buf[1] == np.array([11., 1., 2., 3]).tobytes()
class ClientConnection(object): ''' A Bokeh low-level class used to implement ClientSession; use ClientSession to connect to the server. ''' def __init__(self, session, websocket_url, io_loop=None): ''' Opens a websocket connection to the server. ''' self._url = websocket_url self._session = session self._protocol = Protocol("1.0") self._receiver = Receiver(self._protocol) self._socket = None self._state = NOT_YET_CONNECTED() if io_loop is None: # We can't use IOLoop.current because then we break # when running inside a notebook since ipython also uses it io_loop = IOLoop() self._loop = io_loop self._until_predicate = None self._protocol = Protocol("1.0") self._server_info = None @property def url(self): ''' The URL of the websocket this Connection is to. ''' return self._url @property def io_loop(self): ''' The Tornado ``IOLoop`` this connection is using. ''' return self._loop @property def connected(self): ''' Whether we've connected the Websocket and have exchanged initial handshake messages. ''' return isinstance(self._state, CONNECTED_AFTER_ACK) def connect(self): def connected_or_closed(): # we should be looking at the same state here as the 'connected' property above, so connected # means both connected and that we did our initial message exchange return isinstance(self._state, CONNECTED_AFTER_ACK) or isinstance( self._state, DISCONNECTED) self._loop_until(connected_or_closed) def close(self, why="closed"): ''' Close the Websocket connection. ''' if self._socket is not None: self._socket.close(1000, why) def loop_until_closed(self): ''' Execute a blocking loop that runs and exectutes event callbacks until the connection is closed (e.g. by hitting Ctrl-C). While this method can be used to run Bokeh application code "outside" the Bokeh server, this practice is HIGHLY DISCOURAGED for any real use case. ''' if isinstance(self._state, NOT_YET_CONNECTED): # we don't use self._transition_to_disconnected here # because _transition is a coroutine self._tell_session_about_disconnect() self._state = DISCONNECTED() else: def closed(): return isinstance(self._state, DISCONNECTED) self._loop_until(closed) @gen.coroutine def send_message(self, message): if self._socket is None: log.info("We're disconnected, so not sending message %r", message) else: try: sent = yield message.send(self._socket) log.debug("Sent %r [%d bytes]", message, sent) except WebSocketError as e: # A thing that happens is that we detect the # socket closing by getting a None from # read_message, but the network socket can be down # with many messages still in the read buffer, so # we'll process all those incoming messages and # get write errors trying to send change # notifications during that processing. # this is just debug level because it's completely normal # for it to happen when the socket shuts down. log.debug("Error sending message to server: %r", e) # error is almost certainly because # socket is already closed, but be sure, # because once we fail to send a message # we can't recover self.close(why="received error while sending") # don't re-throw the error - there's nothing to # do about it. raise gen.Return(None) def _send_patch_document(self, session_id, event): # XXX This will cause the client to always send all columns when a CDS # is mutated in place. Additionally we set use_buffers=False below as # well, to suppress using the binary array transport. Real Bokeh server # apps running inside a server can handle these updates much more # efficiently from bokeh.document.events import ColumnDataChangedEvent if hasattr(event, 'hint') and isinstance(event.hint, ColumnDataChangedEvent): event.hint.cols = None msg = self._protocol.create('PATCH-DOC', [event], use_buffers=False) self.send_message(msg) def _send_message_wait_for_reply(self, message): waiter = WAITING_FOR_REPLY(message.header['msgid']) self._state = waiter send_result = [] def message_sent(future): send_result.append(future) self._loop.add_future(self.send_message(message), message_sent) def have_send_result_or_disconnected(): return len(send_result) > 0 or self._state != waiter self._loop_until(have_send_result_or_disconnected) def have_reply_or_disconnected(): return self._state != waiter or waiter.reply is not None self._loop_until(have_reply_or_disconnected) return waiter.reply def push_doc(self, document): ''' Push a document to the server, overwriting any existing server-side doc. Args: document : (Document) A Document to push to the server Returns: The server reply ''' msg = self._protocol.create('PUSH-DOC', document) reply = self._send_message_wait_for_reply(msg) if reply is None: raise RuntimeError("Connection to server was lost") elif reply.header['msgtype'] == 'ERROR': raise RuntimeError("Failed to push document: " + reply.content['text']) else: return reply def pull_doc(self, document): ''' Pull a document from the server, overwriting the passed-in document Args: document : (Document) The document to overwrite with server content. Returns: None ''' msg = self._protocol.create('PULL-DOC-REQ') reply = self._send_message_wait_for_reply(msg) if reply is None: raise RuntimeError("Connection to server was lost") elif reply.header['msgtype'] == 'ERROR': raise RuntimeError("Failed to pull document: " + reply.content['text']) else: reply.push_to_document(document) def _send_request_server_info(self): msg = self._protocol.create('SERVER-INFO-REQ') reply = self._send_message_wait_for_reply(msg) if reply is None: raise RuntimeError( "Did not get a reply to server info request before disconnect") return reply.content def request_server_info(self): ''' Ask for information about the server. Returns: A dictionary of server attributes. ''' if self._server_info is None: self._server_info = self._send_request_server_info() return self._server_info def force_roundtrip(self): ''' Force a round-trip request/reply to the server, sometimes needed to avoid race conditions. Mostly useful for testing. Outside of test suites, this method hurts performance and should not be needed. Returns: None ''' self._send_request_server_info() def _loop_until(self, predicate): self._until_predicate = predicate try: # this runs self._next ONE time, but # self._next re-runs itself until # the predicate says to quit. self._loop.add_callback(self._next) self._loop.start() except KeyboardInterrupt: self.close("user interruption") @gen.coroutine def _next(self): if self._until_predicate is not None and self._until_predicate(): log.debug("Stopping client loop in state %s due to True from %s", self._state.__class__.__name__, self._until_predicate.__name__) self._until_predicate = None self._loop.stop() raise gen.Return(None) else: log.debug("Running state " + self._state.__class__.__name__) yield self._state.run(self) @gen.coroutine def _transition(self, new_state): log.debug("transitioning to state " + new_state.__class__.__name__) self._state = new_state yield self._next() @gen.coroutine def _transition_to_disconnected(self): self._tell_session_about_disconnect() yield self._transition(DISCONNECTED()) @gen.coroutine def _connect_async(self): versioned_url = "%s?bokeh-protocol-version=1.0&bokeh-session-id=%s" % ( self._url, self._session.id) request = HTTPRequest(versioned_url) try: socket = yield websocket_connect(request) self._socket = WebSocketClientConnectionWrapper(socket) except Exception as e: log.info("Failed to connect to server: %r", e) if self._socket is None: yield self._transition_to_disconnected() else: yield self._transition(CONNECTED_BEFORE_ACK()) @gen.coroutine def _wait_for_ack(self): message = yield self._pop_message() if message and message.msgtype == 'ACK': log.debug("Received %r", message) yield self._transition(CONNECTED_AFTER_ACK()) elif message is None: yield self._transition_to_disconnected() else: raise ProtocolError("Received %r instead of ACK" % message) @gen.coroutine def _handle_messages(self): message = yield self._pop_message() if message is None: yield self._transition_to_disconnected() else: if message.msgtype == 'PATCH-DOC': log.debug("Got PATCH-DOC, applying to session") self._session._handle_patch(message) else: log.debug("Ignoring %r", message) # we don't know about whatever message we got, ignore it. yield self._next() @gen.coroutine def _pop_message(self): while True: if self._socket is None: raise gen.Return(None) # log.debug("Waiting for fragment...") fragment = None try: fragment = yield self._socket.read_message() except Exception as e: # this happens on close, so debug level since it's "normal" log.debug("Error reading from socket %r", e) # log.debug("... got fragment %r", fragment) if fragment is None: # XXX Tornado doesn't give us the code and reason log.info("Connection closed by server") raise gen.Return(None) try: message = yield self._receiver.consume(fragment) if message is not None: log.debug("Received message %r" % message) raise gen.Return(message) except (MessageError, ProtocolError, ValidationError) as e: log.error("%r", e, exc_info=True) self.close(why="error parsing message from server") def _tell_session_about_disconnect(self): if self._session: self._session._notify_disconnected()
# Imports #----------------------------------------------------------------------------- # Bokeh imports import bokeh.document as document from bokeh.core.properties import Instance, Int from bokeh.model import Model # Module under test from bokeh.protocol import Protocol # isort:skip #----------------------------------------------------------------------------- # Setup #----------------------------------------------------------------------------- proto = Protocol() #----------------------------------------------------------------------------- # General API #----------------------------------------------------------------------------- class AnotherModelInTestPullDoc(Model): bar = Int(1) class SomeModelInTestPullDoc(Model): foo = Int(2) child = Instance(Model)
class ClientConnection(object): """ A Bokeh-private class used to implement ClientSession; use ClientSession to connect to the server.""" class NOT_YET_CONNECTED(object): @gen.coroutine def run(self, connection): yield connection._connect_async() class CONNECTED_BEFORE_ACK(object): @gen.coroutine def run(self, connection): yield connection._wait_for_ack() class CONNECTED_AFTER_ACK(object): @gen.coroutine def run(self, connection): yield connection._handle_messages() class DISCONNECTED(object): @gen.coroutine def run(self, connection): raise gen.Return(None) class WAITING_FOR_REPLY(object): def __init__(self, reqid): self._reqid = reqid self._reply = None @property def reply(self): return self._reply @gen.coroutine def run(self, connection): message = yield connection._pop_message() if message is None: yield connection._transition_to_disconnected() elif 'reqid' in message.header and message.header['reqid'] == self._reqid: self._reply = message yield connection._transition(connection.CONNECTED_AFTER_ACK()) else: yield connection._next() def __init__(self, session, websocket_url, io_loop=None): ''' Opens a websocket connection to the server. ''' self._url = websocket_url self._session = session self._protocol = Protocol("1.0") self._receiver = Receiver(self._protocol) self._socket = None self._state = self.NOT_YET_CONNECTED() if io_loop is None: # We can't use IOLoop.current because then we break # when running inside a notebook since ipython also uses it io_loop = IOLoop() self._loop = io_loop self._until_predicate = None self._protocol = Protocol("1.0") self._server_info = None @property def url(self): return self._url @property def io_loop(self): return self._loop @property def connected(self): """True if we've connected the websocket and exchanged initial handshake messages.""" return isinstance(self._state, self.CONNECTED_AFTER_ACK) def connect(self): def connected_or_closed(): # we should be looking at the same state here as the 'connected' property above, so connected # means both connected and that we did our initial message exchange return isinstance(self._state, self.CONNECTED_AFTER_ACK) or isinstance(self._state, self.DISCONNECTED) self._loop_until(connected_or_closed) def close(self, why="closed"): if self._socket is not None: self._socket.close(1000, why) def loop_until_closed(self): if isinstance(self._state, self.NOT_YET_CONNECTED): # we don't use self._transition_to_disconnected here # because _transition is a coroutine self._tell_session_about_disconnect() self._state = self.DISCONNECTED() else: def closed(): return isinstance(self._state, self.DISCONNECTED) self._loop_until(closed) @gen.coroutine def send_message(self, message): if self._socket is None: log.info("We're disconnected, so not sending message %r", message) else: try: sent = yield message.send(self._socket) log.debug("Sent %r [%d bytes]", message, sent) except WebSocketError as e: # A thing that happens is that we detect the # socket closing by getting a None from # read_message, but the network socket can be down # with many messages still in the read buffer, so # we'll process all those incoming messages and # get write errors trying to send change # notifications during that processing. # this is just debug level because it's completely normal # for it to happen when the socket shuts down. log.debug("Error sending message to server: %r", e) # error is almost certainly because # socket is already closed, but be sure, # because once we fail to send a message # we can't recover self.close(why="received error while sending") # don't re-throw the error - there's nothing to # do about it. raise gen.Return(None) def _send_patch_document(self, session_id, event): msg = self._protocol.create('PATCH-DOC', [event]) self.send_message(msg) def _send_message_wait_for_reply(self, message): waiter = self.WAITING_FOR_REPLY(message.header['msgid']) self._state = waiter send_result = [] def message_sent(future): send_result.append(future) self._loop.add_future(self.send_message(message), message_sent) def have_send_result_or_disconnected(): return len(send_result) > 0 or self._state != waiter self._loop_until(have_send_result_or_disconnected) def have_reply_or_disconnected(): return self._state != waiter or waiter.reply is not None self._loop_until(have_reply_or_disconnected) return waiter.reply def push_doc(self, document): ''' Push a document to the server, overwriting any existing server-side doc. Args: document : bokeh.document.Document the Document to push to the server Returns: The server reply ''' msg = self._protocol.create('PUSH-DOC', document) reply = self._send_message_wait_for_reply(msg) if reply is None: raise RuntimeError("Connection to server was lost") elif reply.header['msgtype'] == 'ERROR': raise RuntimeError("Failed to push document: " + reply.content['text']) else: return reply def pull_doc(self, document): ''' Pull a document from the server, overwriting the passed-in document Args: document : bokeh.document.Document The document to overwrite with server content. Returns: None ''' msg = self._protocol.create('PULL-DOC-REQ') reply = self._send_message_wait_for_reply(msg) if reply is None: raise RuntimeError("Connection to server was lost") elif reply.header['msgtype'] == 'ERROR': raise RuntimeError("Failed to pull document: " + reply.content['text']) else: reply.push_to_document(document) def _send_request_server_info(self): msg = self._protocol.create('SERVER-INFO-REQ') reply = self._send_message_wait_for_reply(msg) if reply is None: raise RuntimeError("Did not get a reply to server info request before disconnect") return reply.content def request_server_info(self): ''' Ask for information about the server. Returns: A dictionary of server attributes. ''' if self._server_info is None: self._server_info = self._send_request_server_info() return self._server_info def force_roundtrip(self): ''' Force a round-trip request/reply to the server, sometimes needed to avoid race conditions. Outside of test suites, this method probably hurts performance and shouldn't be needed. Returns: None ''' self._send_request_server_info() def _loop_until(self, predicate): self._until_predicate = predicate try: # this runs self._next ONE time, but # self._next re-runs itself until # the predicate says to quit. self._loop.add_callback(self._next) self._loop.start() except KeyboardInterrupt: self.close("user interruption") @gen.coroutine def _next(self): if self._until_predicate is not None and self._until_predicate(): log.debug("Stopping client loop in state %s due to True from %s", self._state.__class__.__name__, self._until_predicate.__name__) self._until_predicate = None self._loop.stop() raise gen.Return(None) else: log.debug("Running state " + self._state.__class__.__name__) yield self._state.run(self) @gen.coroutine def _transition(self, new_state): log.debug("transitioning to state " + new_state.__class__.__name__) self._state = new_state yield self._next() @gen.coroutine def _transition_to_disconnected(self): self._tell_session_about_disconnect() yield self._transition(self.DISCONNECTED()) @gen.coroutine def _connect_async(self): versioned_url = "%s?bokeh-protocol-version=1.0&bokeh-session-id=%s" % (self._url, self._session.id) request = HTTPRequest(versioned_url) try: socket = yield websocket_connect(request) self._socket = _WebSocketClientConnectionWrapper(socket) except Exception as e: log.info("Failed to connect to server: %r", e) if self._socket is None: yield self._transition_to_disconnected() else: yield self._transition(self.CONNECTED_BEFORE_ACK()) @gen.coroutine def _wait_for_ack(self): message = yield self._pop_message() if message and message.msgtype == 'ACK': log.debug("Received %r", message) yield self._transition(self.CONNECTED_AFTER_ACK()) elif message is None: yield self._transition_to_disconnected() else: raise ProtocolError("Received %r instead of ACK" % message) @gen.coroutine def _handle_messages(self): message = yield self._pop_message() if message is None: yield self._transition_to_disconnected() else: if message.msgtype == 'PATCH-DOC': log.debug("Got PATCH-DOC, applying to session") self._session._handle_patch(message) else: log.debug("Ignoring %r", message) # we don't know about whatever message we got, ignore it. yield self._next() @gen.coroutine def _pop_message(self): while True: if self._socket is None: raise gen.Return(None) # log.debug("Waiting for fragment...") fragment = None try: fragment = yield self._socket.read_message() except Exception as e: # this happens on close, so debug level since it's "normal" log.debug("Error reading from socket %r", e) # log.debug("... got fragment %r", fragment) if fragment is None: # XXX Tornado doesn't give us the code and reason log.info("Connection closed by server") raise gen.Return(None) try: message = yield self._receiver.consume(fragment) if message is not None: log.debug("Received message %r" % message) raise gen.Return(message) except (MessageError, ProtocolError, ValidationError) as e: log.error("%r", e, exc_info=True) self.close(why="error parsing message from server") def _tell_session_about_disconnect(self): if self._session: self._session._notify_disconnected()
def test_create_model_changed(self): sample = self._sample_doc() obj = next(iter(sample.roots)) event = ModelChangedEvent(sample, obj, 'foo', obj.foo, 42, 42) Protocol("1.0").create("PATCH-DOC", [event])
def test_create_no_events(self): with pytest.raises(ValueError): Protocol("1.0").create("PATCH-DOC", [])