class WidgetCommSocket(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: # Jupyter/IPython 4.0 from ipykernel.comm import Comm except: # IPython <=3.0 from IPython.kernel.comm import Comm 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 NbAggCommSocket(CommSocket): """ NbAggCommSocket subclasses the matplotlib CommSocket allowing the opening of a comms channel to be delayed until the plot is displayed. """ def __init__(self, manager, target=None): self.supports_binary = None self.manager = manager self.target = uuid.uuid4().hex if target is None else target self.html = "<div id=%r></div>" % self.target def start(self): try: # Jupyter/IPython 4.0 from ipykernel.comm import Comm except: # IPython <=3.0 from IPython.kernel.comm import Comm try: self.comm = Comm('matplotlib', data={'id': self.target}) 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())
def buttonCallback(self, *args): if len(args) > 0: args[0].actionPerformed() arguments = dict(target_name='beakerx.tag.run') comm = Comm(**arguments) msg = {'runByTag': args[0].tag} state = {'state': msg} comm.send(data=state, buffers=[])
def _on_action_details(self, msg): params = msg['content']['data']['content'] graphics_object = None for item in self.chart.graphics_list: if item.uid == params['itemId']: graphics_object = item action_type = params['params']['actionType'] if action_type == 'onclick' or action_type == 'onkey': self.details = GraphicsActionObject(graphics_object, params['params']) arguments = dict(target_name='beakerx.tag.run') comm = Comm(**arguments) msg = {'runByTag': params['params']['tag']} state = {'state': msg} comm.send(data=state, buffers=[])
def exec_javascript(source, **kwargs): """Execute the passed javascript source inside the notebook environment. Requires a prior call to ``init_comms()``. """ global exec_js_comm if exec_js_comm is None: from ipykernel.comm import Comm comm = Comm("flowly_exec_js", {}) if kwargs: source = source.format(**kwargs) comm.send({"source": source})
class JupyterComm(Comm): """ JupyterComm provides a Comm for the notebook which is initialized the first time data is pushed to the frontend. """ template = """ <script> function msg_handler(msg) {{ var msg = msg.content.data; {msg_handler} }} if ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null)) {{ comm_manager = Jupyter.notebook.kernel.comm_manager; comm_manager.register_target("{comm_id}", function(comm) {{ comm.on_msg(msg_handler);}}); }} </script> <div id="fig_{comm_id}"> {init_frame} </div> """ def init(self): from ipykernel.comm import Comm as IPyComm if self._comm: return self._comm = IPyComm(target_name=self.id, data={}) self._comm.on_msg(self._handle_msg) @classmethod def decode(cls, msg): """ Decodes messages following Jupyter messaging protocol. If JSON decoding fails data is assumed to be a regular string. """ return msg['content']['data'] def send(self, data=None, buffers=[]): """ Pushes data across comm socket. """ if not self._comm: self.init() self.comm.send(data, buffers=buffers)
def connect(): """ establish connection to frontend notebook """ if not is_notebook(): print('Python session is not running in a Notebook Kernel') return global _comm kernel = get_ipython().kernel kernel.comm_manager.register_target('tdb', handle_comm_opened) # initiate connection to frontend. _comm = Comm(target_name='tdb', data={}) # bind recv handler _comm.on_msg(None)
def connect(): """ establish connection to frontend notebook """ if not is_notebook(): print('Python session is not running in a Notebook Kernel') return global _comm kernel=get_ipython().kernel kernel.comm_manager.register_target('tdb',handle_comm_opened) # initiate connection to frontend. _comm=Comm(target_name='tdb',data={}) # bind recv handler _comm.on_msg(None)
def update_cell_contents(comm: Comm, result: Dict[str, Any]) -> None: # J_LOGGER.info(Javascript("Jupyter.notebook.get_cells()")) def _transform_jupytext_cells(jupytext_cells) -> List[Dict[str, Any]]: return [{ "index": i, "output": [], **{ k: v for (k, v) in x.items() if k not in {"outputs", "metadata"} } } for i, x in enumerate(result["cells"])] comm.send({ "command": "start_sync_notebook", "cells": _transform_jupytext_cells(result["cells"]) })
def handle_msg(self, tabledisplay, params, list): if params['event'] == 'DOUBLE_CLICK': self.doubleClickListener(params['row'], params['column'], tabledisplay) self.model = self.chart.transform() if params['event'] == 'CONTEXT_MENU_CLICK': func = self.contextMenuListeners.get(params['itemKey']) if func is not None: func(params['row'], params['column'], tabledisplay) self.model = self.chart.transform() if params['event'] == 'actiondetails': if params['params']['actionType'] == 'DOUBLE_CLICK': arguments = dict(target_name='beaker.tag.run') comm = Comm(**arguments) msg = {'runByTag': self.chart.doubleClickTag} state = {'state': msg} comm.send(data=state, buffers=[])
def make_comm() -> None: global _JupyterComm J_LOGGER.info("IPYTHON: Registering Comms") comm_target_name = COMM_NAME jupyter_comm = Comm(target_name=comm_target_name) def _get_command(msg) -> Optional[str]: return msg["content"]["data"].get("command", None) @jupyter_comm.on_msg def _recv(msg): if _get_command(msg) == "merge_notebooks": J_LOGGER.info("GOT UPDATE STATUS") merge_notebooks(jupyter_comm, msg["content"]["data"]) return J_LOGGER.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") J_LOGGER.info(msg) J_LOGGER.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") # store comm for access in this thread later _JupyterComm = jupyter_comm J_LOGGER.info("==> Success") return _JupyterComm
def open(self): """Open a comm to the frontend if one isn't already open.""" if self.comm is None: state, buffer_keys, buffers = self._split_state_buffers(self.get_state()) args = dict(target_name='ipython.widget', data=state, ) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args) if buffers: # FIXME: workaround ipykernel missing binary message support in open-on-init # send state with binary elements as second message self.send_state()
def open(self): """Open a comm to the frontend if one isn't already open.""" if self.comm is None: state, buffer_paths, buffers = _remove_buffers(self.get_state()) args = dict(target_name='jupyter.widget', data={ 'state': state, 'buffer_paths': buffer_paths }, buffers=buffers, metadata={'version': __protocol_version__}) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args)
def locate_jpd_comm(da_id, app, app_path): global local_comms comm = local_comms.get(da_id, None ) if comm is None: comm = Comm(target_name='jpd_%s' %da_id, data={'jpd_type':'inform', 'da_id':da_id}) @comm.on_msg def callback(msg, app=app, da_id=da_id, app_path=app_path): # TODO see if app and da_id args needed for this closure content = msg['content'] data = content['data'] stem = data['stem'] args = data['args'] #app_path = "app/endpoints/%s" % da_id response, mimetype = app.process_view(stem, args, app_path) comm.send({'jpd_type':'response', 'da_id':da_id, 'app':str(app), 'response':response, 'mimetype':mimetype}) local_comms[da_id] = comm return comm
def run(self): while True: client, addr = self.m_socket.accept() buffered_message = '' appId = '' appName = '' sparkUser = '' numCores = 0 totalCores = 0 while client: message = client.recv(4096).decode() if not message: break buffered_message = buffered_message + message last_sep_pos = buffered_message.rfind(self.SEP) if last_sep_pos > 0: send_message = buffered_message[:last_sep_pos] buffered_message = buffered_message[last_sep_pos + 5:] else: send_message = "" for msg in [ json.loads(x) for x in send_message.split(self.SEP) if x ]: if msg['msgtype'] == "sparkApplicationStart": try: appId = msg['appId'] appName = msg['appName'] sparkUser = msg['sparkUser'] except Exception: raise msg elif msg['msgtype'] == "sparkExecutorAdded": numCores = msg['numCores'] totalCores = msg['totalCores'] else: msg['application'] = dict( appId=appId, appName=appName, sparkUser=sparkUser, numCores=numCores, totalCores=totalCores, ) comm = Comm('spark-monitor', data='open from server') comm.send(data=msg) client.close()
class JupyterComm(Comm): """ JupyterComm provides a Comm for the notebook which is initialized the first time data is pushed to the frontend. """ js_template = """ function msg_handler(msg) {{ var buffers = msg.buffers; var msg = msg.content.data; {msg_handler} }} window.PyViz.comm_manager.register_target('{plot_id}', '{comm_id}', msg_handler); """ def init(self): from ipykernel.comm import Comm as IPyComm if self._comm: return self._comm = IPyComm(target_name=self.id, data={}) self._comm.on_msg(self._handle_msg) @classmethod def decode(cls, msg): """ Decodes messages following Jupyter messaging protocol. If JSON decoding fails data is assumed to be a regular string. """ return msg['content']['data'] def close(self): """ Closes the comm connection """ if self._comm: self._comm.close() def send(self, data=None, buffers=[]): """ Pushes data across comm socket. """ if not self._comm: self.init() self.comm.send(data, buffers=buffers)
def send_data(self): """ Send the spec and dataset metadata to the frontend """ data = { "spec": self.spec, "size": self.size, "totalSteps": self.total_steps, "currentStep": self.current_step, "currentProgress": (self.current_progress / self.size) * 100, "totalProgress": (self.total_progress / (self.size * self.total_steps)) * 100, "dataSet": self.data_set, "runTime": self.runtime, } data_comm = Comm(target_name="plyto", data=data) data_comm.send(data=data)
def init(self): from ipykernel.comm import Comm as IPyComm if self._comm: return self._comm = IPyComm(target_name=self.id, data={}) self._comm.on_msg(self._handle_msg) if self._on_open: self._on_open({})
def add_widget(self, widget_id, client_id, widget_type, comm_target): """Add a widget to manager. # Returns: """ comm = self.find_comm( comm_target=comm_target) # Try to find a comm object if not comm: # Create comm if does not exist comm = Comm(target_name=comm_target) self._comms[widget_id] = comm @comm.on_msg def handle_msg(msg): comm_id = msg["content"]["comm_id"] data = msg["content"]["data"] nonlocal self widget = self.find_widget_by_comm_id(comm_id) self.last_active = widget widget.msg_data = data widget.parse_data(data) if data == "dispose": widget.isDisposed = True self.callback_manager.run(comm_id, data) @comm.on_close def handle_close(msg): comm_id = msg["content"]["comm_id"] nonlocal self widget = self.find_widget_by_comm_id(comm_id) widget.commOpen = False if widget_id not in self.widgets: self.widgets[widget_id] = Widget( widget_type=widget_type, widget_id=widget_id, client_id=client_id, model=None, comm=comm, isDisposed=False, msg_data=None, ) self.last_active = self.widgets[widget_id] # make sure that comm is open comm.open()
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=self.get_state()) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args)
def urlArg(self, argName): arguments = dict(target_name='beakerx.geturlarg') comm = Comm(**arguments) state = { 'name': 'URL_ARG', 'arg_name': argName } data = { 'state': state, 'url': self._url, 'type': 'python' } comm.send(data=data, buffers=[]) data = self._queue.get() params = json.loads(data) return params['argValue']
def __init__(self): if self._comm is None: self._comm = Comm(target_name="KBaseJobs", data={}) self._comm.on_msg(self._handle_comm_message) if self._jm is None: self._jm = JobManager() if self._msg_map is None: self._msg_map = { MESSAGE_TYPE["CANCEL"]: self._cancel_jobs, MESSAGE_TYPE["CELL_JOB_STATUS"]: self._get_job_states_by_cell_id, MESSAGE_TYPE["INFO"]: self._get_job_info, MESSAGE_TYPE["LOGS"]: self._get_job_logs, MESSAGE_TYPE["RETRY"]: self._retry_jobs, MESSAGE_TYPE["START_UPDATE"]: self._modify_job_updates, MESSAGE_TYPE["STATUS"]: self._get_job_states, MESSAGE_TYPE["STATUS_ALL"]: self._get_all_job_states, MESSAGE_TYPE["STOP_UPDATE"]: self._modify_job_updates, }
def __init__(self): if self._comm is None: self._comm = Comm(target_name="KBaseJobs", data={}) self._comm.on_msg(self._handle_comm_message) if self._jm is None: self._jm = jobmanager.JobManager() if self._msg_map is None: self._msg_map = { "all_status": self._lookup_all_job_states, "job_status": self._lookup_job_state, "job_info": self._lookup_job_info, "start_update_loop": self.start_job_status_loop, "stop_update_loop": self.stop_job_status_loop, "start_job_update": self._modify_job_update, "stop_job_update": self._modify_job_update, "cancel_job": self._cancel_job, "job_logs": self._get_job_logs, "job_logs_latest": self._get_job_logs }
class AppViewer(object): _dash_comm = Comm(target_name='dash_viewer') def __init__(self, port=8050): self.server_process = None self.uid = str(uuid.uuid4()) self.port = port self.stderr_queue = StdErrorQueue() def show(self, app): def run(): # Serve App sys.stdout = self.stderr_queue sys.stderr = self.stderr_queue app.run_server(debug=False, port=self.port) # Terminate any existing server process self.terminate() # Start new server process in separate process self.server_process = multiprocessing.Process(target=run) self.server_process.daemon = True self.server_process.start() # Wait for server to start started = False retries = 0 while not started and retries < 100: try: out = self.stderr_queue.queue.get(timeout=.1) try: out = out.decode() except AttributeError: pass if 'Running on' in out: started = True except Empty: retries += 1 pass if started: # Update front-end extension self._dash_comm.send({ 'type': 'show', 'uid': self.uid, 'url': 'http://localhost:{}'.format(self.port) }) else: # Failed to start development server raise ConnectionError('Unable to start Dash server') def terminate(self): if self.server_process: self.server_process.terminate()
def stream_df_as_arrow(df, spark, limit=5000): # df = df.repartition(2000)#.cache() # cos = pa.output_stream(sink,compression='gzip') # get the aliases and change the names of the columns to show the aliases # this avoids duplicate columns aliased_schema = get_schema_with_aliases(df) renamed_schema_fields = [] for i in range(len(aliased_schema)): aliased_field = aliased_schema[i] fieldname: str = "" if aliased_field["tablealias"] != "": fieldname = aliased_field["tablealias"] + "." + aliased_field[ "name"] else: fieldname = aliased_field["name"] renamed_schema_fields.append(fieldname) comm = Comm(target_name="inspect_df") # see if we have any results renamed_df = df.toDF(*renamed_schema_fields) row_iterator = renamed_df.toLocalIterator() row_num = 0 row_buff = [] chunk_size = 500 for row in row_iterator: if (row_num > 2000): break row_num += 1 logging.debug(row_num) row_buff.append(row) if row_num % chunk_size == 0: batches = spark.createDataFrame( row_buff, renamed_df.schema)._collectAsArrow() if len(batches) > 0: sink = pa.BufferOutputStream() writer = pa.RecordBatchStreamWriter(sink, batches[0].schema) for batch in batches: writer.write_batch(batch) comm.send(data="test", buffers=[sink.getvalue()]) row_buff = [] # send the last batch batches = spark.createDataFrame(row_buff, renamed_df.schema)._collectAsArrow() if len(batches) > 0: sink = pa.BufferOutputStream() writer = pa.RecordBatchStreamWriter(sink, batches[0].schema) for batch in batches: writer.write_batch(batch) comm.send(data="test", buffers=[sink.getvalue()]) comm.close(data="closing comm")
def __init__(self, width, height): self.id = uuid.uuid4() self.width = width self.height = height context = {'id': self.id.hex, 'width': width, 'height': height} display( HTML(''' <canvas id="canvas_%(id)s" width="%(width)d" height="%(height)d"></canvas> <script> Jupyter.notebook.kernel.comm_manager.register_target("canvas_%(id)s", (comm, msg) => { const ctx = canvas_%(id)s.getContext("2d"); comm.on_msg((msg) => { const eventName = msg.content.data[0]; const eventData = msg.content.data[1]; switch (eventName) { // Properties case 'lineWidth': case 'strokeStyle': case 'fillStyle': ctx[eventName] = eventData[0]; break; // Methods case 'beginPath': case 'lineTo': case 'moveTo': case 'stroke': case 'fill': ctx[eventName].apply(ctx, eventData); break; default: console.log('Unknown event: ' + eventName); } }); }); </script> ''' % context)) self.comm = Comm(f'canvas_{self.id.hex}')
def set_comm(self, midas_instance_name: str, logger_id: str): if self.is_in_ipynb: self.comm = Comm(target_name=MIDAS_CELL_COMM_NAME) self.comm.send({ "type": "initialize", "name": midas_instance_name, "loggerId": logger_id }) self.comm.on_msg(self.handle_msg) else: self.comm = MockComm()
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 ipykernel.comm import Comm self.comm = Comm('lightning', {'id': self.id}) self.comm_handlers = {} self.comm.on_msg(self._handle_comm_message)
def test_tile_map_layer(self, mock_send): """ :type mock_send:Mock :param mock_send: :return: """ layer = TileMapLayer() layer._map = MapView() comm = Comm() comm.kernel = Kernel() layer.comm = comm layer.url = "http://test.com" expected = { 'method': 'update', 'state': { 'url': 'http://test.com' }, 'buffer_paths': [] } mock_send.assert_called_with(data=expected, buffers=[])
def __init__(self): self.comm = None folder = os.path.dirname(__file__) with open(os.path.join(folder, "../js", "initIPython.js"), "r") as fd: initIPython = fd.read() display_javascript(Javascript(initIPython)) css = """ <style> div.output_area img, div.output_area svg { max-width: 100%; height: 100%; } </style> """ display_html(HTML(css)) time.sleep(0.5) self.comm = Comm(target_name='nvd3_stat', data={'event': 'open'})
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)
class CallbackComm: ''' Interface for handling callbacks via ipykernel's Comm class. ''' def __init__( self, target_name ): self.__comm = Comm( target_name = target_name ) def send_line( self, line ): ''' Send a single line of text. ''' self.__comm.send( data = { 'out': line } ) def send_data( self, data ): ''' Send arbitrary data. ''' self.__comm.send( data = data ) def close( self, status ): ''' Send final status message and close the comm channel. ''' self.__comm.close( data = { 'done': status } )
class JulynterComm(object): """Julynter comm hadler""" # pylint: disable=useless-object-inheritance def __init__(self, shell=None): self.shell = shell self.name = 'julynter.comm' self.comm = None def register(self): """Register comm""" self.comm = Comm(self.name) self.comm.on_msg(self.receive) self.send({'operation': 'init'}) def receive(self, msg): """Receive lint request""" def send(self, data): """Receive send results""" self.comm.send(data)
def test_heat_layer(self, mock_send): """ :type mock_send:Mock :param mock_send: :return: """ layer = EchartsLayer() layer._map = MapView() comm = Comm() comm.kernel = Kernel() layer.comm = comm layer.option = {"test": 1} expected = { 'method': 'update', 'state': { 'option': { "test": 1 } }, 'buffer_paths': [] } mock_send.assert_called_with(data=expected, buffers=[])
def get_comms(target_name): ''' Create a Jupyter comms object for a specific target, that can be used to update Bokeh documents in the notebook. Args: target_name (str) : the target name the Comms object should connect to Returns Jupyter Comms ''' from ipykernel.comm import Comm return Comm(target_name=target_name, data={})
def test_map_v_layer(self, mock_send): """ :type mock_send:Mock :type layer:MapVLayer :param mock_send: :return: """ layer = MapVLayer(data_set=[{ "geometry": { "type": "Point", "coordinates": [117.6516476632447, 24.79141797359827] }, "count": 14.491465292723886 }, { "geometry": { "type": "Point", "coordinates": [115.40463990328632, 29.776387718326674] }, "count": 14.067846279906583 }, { "geometry": { "type": "Point", "coordinates": [114.3864486463097, 28.931467637939697] }, "count": 8.496995944766768 }]) layer._map = MapView() comm = Comm() comm.kernel = Kernel() layer.comm = comm layer.shadow_blur = 50 sb_expected = { 'method': 'update', 'state': { 'shadow_blur': 50 }, 'buffer_paths': [] } mock_send.assert_called_with(data=sb_expected, buffers=[])
def _send_comm_message(self, msg_type, content): """ Sends a ipykernel.Comm message to the KBaseJobs channel with the given msg_type and content. These just get encoded into the message itself. """ msg = { 'msg_type': msg_type, 'content': content } if self._comm is None: self._comm = Comm(target_name='KBaseJobs', data={}) self._comm.on_msg(self._handle_comm_message) self._comm.send(msg)
class GalyleoClient: """ The Dashboard Client. This is the client which sends the tables to the dashboard and handles requests coming from the dashboard for tables. """ def __init__(self): """Initialize the client. No parameters. This initializes communications with the JupyterLab Galyleo Communications Manager """ self._comm_ = Comm(target_name='galyleo_data', data={'foo': 1}) def send_data_to_dashboard(self, galyleo_table, dashboard_name: str = None) -> None: """ The routine to send a GalyleoTable to the dashboard, optionally specifying a specific dashboard to send the data to. If None is specified, sends to all the dashboards. The table must not have more than galyleo_constants.MAX_NUMBER_ROWS, nor be (in JSON form) > galyleo_constants.MAX_DATA_SIZE. If either of these conditions apply, a DataSizeExceeded exception is thrown. NOTE: this sends data to one or more open dashboard editors in JupyterLab. If there are no dashboard editors open, it will have no effect. Args: galyleo_table: the table to send to the dashboard dashboard_name: name of the dashboard editor to send it to (if None, sent to all) """ # Very simple. Convert the table to a dictionary and send it to the dashboard # and wrap it in a payload to send to the dashboard if (len(galyleo_table.data) > MAX_TABLE_ROWS): raise DataSizeExceeded( f"{len(table.rows)} rows is greater than the maximum permitted, {MAX_TABLE_ROWS}" ) string_form = galyleo_table.to_json() if (len(string_form) > MAX_DATA_SIZE): raise DataSizeExceeded( f"{len(string_form)} bytes is greater than the maximum permitted, {MAX_DATA_SIZE}" ) table_record = galyleo_table.as_dictionary() if (dashboard_name): table_record["dashboard"] = dashboard_name self._comm_.send(table_record)
def setup(self, conn): """Set up the plugin connection.""" self.comm = self.comm or Comm(target_name="imjoy_comm_" + self.id, data={"id": self.id}) def on_disconnect(): if not conn.opt.daemon: conn.exit(1) def emit(msg): """Emit a message to the socketio server.""" self.comm.send(msg) def comm_plugin_message(msg): """Handle plugin message.""" data = msg["content"]["data"] # emit({'type': 'logging', 'details': data}) # if not self.conn.executed: # self.emit({'type': 'message', 'data': {"type": "interfaceSetAsRemote"}}) if data["type"] == "import": emit({"type": "importSuccess", "url": data["url"]}) elif data["type"] == "disconnect": conn.abort.set() try: if "exit" in conn.interface and callable( conn.interface["exit"]): conn.interface["exit"]() except Exception as exc: # pylint: disable=broad-except logger.error("Error when exiting: %s", exc) elif data["type"] == "execute": if not conn.executed: self.queue.put(data) else: logger.debug("Skip execution") emit({"type": "executeSuccess"}) elif data["type"] == "message": _data = data["data"] self.queue.put(_data) logger.debug("Added task to the queue") self.comm.on_msg(comm_plugin_message) self.comm.on_close(on_disconnect) conn.default_exit = lambda: None conn.emit = emit emit({"type": "initialized", "dedicatedThread": True}) logger.info("Plugin %s initialized", conn.opt.id)
def open(self): """Open a comm to the frontend if one isn't already open.""" if self.comm is None: state, buffer_paths, buffers = _remove_buffers(self.get_state()) args = dict(target_name='jupyter.widget', data={'state': state, 'buffer_paths': buffer_paths}, buffers=buffers, metadata={'version': __protocol_version__} ) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args)
def open(self): """Open a comm to the frontend if one isn't already open.""" if self.comm is None: state, buffer_keys, buffers = self._split_state_buffers(self.get_state()) args = dict(target_name='jupyter.widget', data=state) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args) if buffers: # FIXME: workaround ipykernel missing binary message support in open-on-init # send state with binary elements as second message self.send_state()
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._ext_close = False def _on_close(close_message): self._ext_close = True manager.remove_comm(close_message['content']['comm_id']) manager.clearup_closed() self.comm.on_close(_on_close)
def start(self): try: # Jupyter/IPython 4.0 from ipykernel.comm import Comm except: # IPython <=3.0 from IPython.kernel.comm import Comm 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 Component(LoggingConfigurable): comm = Instance('ipykernel.comm.Comm', allow_none=True) _module = None _msg_callbacks = Instance(CallbackDispatcher, ()) @property def module(self): if self._module is not None: return self._module else: return self.__class__.__name__ def __init__(self, target_name='jupyter.react', props={}, comm=None): self.target_name = target_name self.props = props if comm is None: self.open(props) else: self.comm = comm self.comm.on_close(self.close) def open(self, props): props['module'] = self.module args = dict(target_name=self.target_name, data=props) args['comm_id'] = 'jupyter_react.{}.{}'.format( uuid.uuid4(), props['module'] ) self.comm = Comm(**args) @observe('comm') def _comm_changed(self, change): if change['new'] is None: return self.comm.on_msg(self._handle_msg) def __del__(self): self.comm.close() self.close(None) def close(self, msg): if self.comm is not None: self.comm = None self._ipython_display_ = None def send(self, data): self.comm.send( data ) def _ipython_display_(self, **kwargs): self.send({"method": "display"}) def _handle_msg(self, msg): if 'content' in msg: self._msg_callbacks(self, msg['content'], msg['buffers']) def on_msg(self, callback, remove=False): self._msg_callbacks.register_callback(callback, remove=remove)
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 ipykernel.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
class JobManager(object): """ The KBase Job Manager clsas. This handles all jobs and makes their status available. On status lookups, it feeds the results to the KBaseJobs channel that the front end listens to. """ __instance = None # keys = job_id, values = { refresh = T/F, job = Job object } _running_jobs = dict() _lookup_timer = None _comm = None _log = kblogging.get_logger(__name__) # TODO: should this not be done globally? _running_lookup_loop = False def __new__(cls): if JobManager.__instance is None: JobManager.__instance = object.__new__(cls) return JobManager.__instance def initialize_jobs(self): """ Initializes this JobManager. This is expected to be run by a running Narrative, and naturally linked to a workspace. So it does the following steps. 1. app_util.system_variable('workspace_id') 2. get list of jobs with that ws id from UJS (also gets tag, cell_id, run_id) 3. initialize the Job objects by running NJS.get_job_params on each of those (also gets app_id) 4. start the status lookup loop. """ ws_id = system_variable('workspace_id') try: nar_jobs = clients.get('user_and_job_state').list_jobs2({ 'authstrat': 'kbaseworkspace', 'authparams': [str(ws_id)] }) except Exception as e: kblogging.log_event(self._log, 'init_error', {'err': str(e)}) new_e = transform_job_exception(e) error = { 'error': 'Unable to get initial jobs list', 'message': getattr(new_e, 'message', 'Unknown reason'), 'code': getattr(new_e, 'code', -1), 'source': getattr(new_e, 'source', 'jobmanager'), 'name': getattr(new_e, 'name', type(e).__name__), 'service': 'user_and_job_state' } self._send_comm_message('job_init_err', error) raise new_e for info in nar_jobs: job_id = info[0] user_info = info[1] job_meta = info[10] try: job_info = clients.get('job_service').get_job_params(job_id)[0] self._running_jobs[job_id] = { 'refresh': True, 'job': Job.from_state(job_id, job_info, user_info[0], app_id=job_info.get('app_id'), tag=job_meta.get('tag', 'release'), cell_id=job_meta.get('cell_id', None), run_id=job_meta.get('run_id', None)) } except Exception as e: kblogging.log_event(self._log, 'init_error', {'err': str(e)}) new_e = transform_job_exception(e) error = { 'error': 'Unable to get job info on initial lookup', 'job_id': job_id, 'message': getattr(new_e, 'message', 'Unknown reason'), 'code': getattr(new_e, 'code', -1), 'source': getattr(new_e, 'source', 'jobmanager'), 'name': getattr(new_e, 'name', type(e).__name__), 'service': 'job_service' } self._send_comm_message('job_init_lookup_err', error) raise new_e # should crash and burn on any of these. if not self._running_lookup_loop: # only keep one loop at a time in cause this gets called again! if self._lookup_timer is not None: self._lookup_timer.cancel() self._running_lookup_loop = True self._lookup_job_status_loop() else: self._lookup_all_job_status() def list_jobs(self): """ List all job ids, their info, and status in a quick HTML format. """ try: status_set = list() for job_id in self._running_jobs: job = self._running_jobs[job_id]['job'] job_state = job.state() job_params = job.parameters() job_state['app_id'] = job_params[0].get('app_id', 'Unknown App') job_state['owner'] = job.owner status_set.append(job_state) if not len(status_set): return "No running jobs!" status_set = sorted(status_set, key=lambda s: s['creation_time']) for i in range(len(status_set)): status_set[i]['creation_time'] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(status_set[i]['creation_time']/1000), "%Y-%m-%d %H:%M:%S") exec_start = status_set[i].get('exec_start_time', None) if 'finish_time' in status_set[i]: finished = status_set[i].get('finish_time', None) if finished is not None and exec_start: delta = datetime.datetime.fromtimestamp(finished/1000.0) - datetime.datetime.fromtimestamp(exec_start/1000.0) delta = delta - datetime.timedelta(microseconds=delta.microseconds) status_set[i]['run_time'] = str(delta) status_set[i]['finish_time'] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(status_set[i]['finish_time']/1000), "%Y-%m-%d %H:%M:%S") elif exec_start: delta = datetime.datetime.utcnow() - datetime.datetime.utcfromtimestamp(exec_start/1000.0) delta = delta - datetime.timedelta(microseconds=delta.microseconds) status_set[i]['run_time'] = str(delta) else: status_set[i]['run_time'] = 'Not started' tmpl = """ <table class="table table-bordered table-striped table-condensed"> <tr> <th>Id</th> <th>Name</th> <th>Submitted</th> <th>Submitted By</th> <th>Status</th> <th>Run Time</th> <th>Complete Time</th> </tr> {% for j in jobs %} <tr> <td>{{ j.job_id|e }}</td> <td>{{ j.app_id|e }}</td> <td>{{ j.creation_time|e }}</td> <td>{{ j.owner|e }}</td> <td>{{ j.job_state|e }}</td> <td>{{ j.run_time|e }}</td> <td>{% if j.finish_time %}{{ j.finish_time|e }}{% else %}Incomplete{% endif %}</td> </tr> {% endfor %} </table> """ return HTML(Template(tmpl).render(jobs=status_set)) except Exception as e: kblogging.log_event(self._log, "list_jobs.error", {'err': str(e)}) raise def get_jobs_list(self): """ A convenience method for fetching an unordered list of all running Jobs. """ return [j['job'] for j in self._running_jobs.values()] # def _get_existing_job(self, job_tuple): # """ # creates a Job object from a job_id that already exists. # If no job exists, raises an Exception. # Parameters: # ----------- # job_tuple : The expected 5-tuple representing a Job. The format is: # (job_id, set of job inputs (as JSON), version tag, cell id that started the job, run id of the job) # """ # # remove the prefix (if present) and take the last element in the split # job_id = job_tuple[0].split(':')[-1] # try: # job_info = clients.get('job_service').get_job_params(job_id)[0] # return Job.from_state(job_id, job_info, app_id=job_tuple[1], tag=job_tuple[2], cell_id=job_tuple[3], run_id=job_tuple[4]) # except Exception as e: # kblogging.log_event(self._log, "get_existing_job.error", {'job_id': job_id, 'err': str(e)}) # raise def _construct_job_status(self, job_id): """ Always creates a Job Status. It'll embed error messages into the status if there are problems. """ state = {} widget_info = None app_spec = {} job = self.get_job(job_id) if job is None: state = { 'job_state': 'error', 'error': { 'error': 'Job does not seem to exist, or it is otherwise unavailable.', 'message': 'Job does not exist', 'name': 'Job Error', 'code': -1, 'exception': { 'error_message': 'job not found in JobManager', 'error_type': 'ValueError', 'error_stacktrace': '' } }, 'cell_id': None, 'run_id': None } return { 'state': state, 'app_spec': app_spec, 'widget_info': widget_info, 'owner': None } try: app_spec = job.app_spec() except Exception as e: kblogging.log_event(self._log, "lookup_job_status.error", {'err': str(e)}) try: state = job.state() except Exception as e: kblogging.log_event(self._log, "lookup_job_status.error", {'err': str(e)}) new_e = transform_job_exception(e) e_type = type(e).__name__ e_message = str(new_e).replace('<', '<').replace('>', '>') e_trace = traceback.format_exc().replace('<', '<').replace('>', '>') e_code = getattr(new_e, "code", -2) e_source = getattr(new_e, "source", "JobManager") state = { 'job_state': 'error', 'error': { 'error': 'Unable to find current job state. Please try again later, or contact KBase.', 'message': 'Unable to return job state', 'name': 'Job Error', 'code': e_code, 'source': e_source, 'exception': { 'error_message': e_message, 'error_type': e_type, 'error_stacktrace': e_trace, } }, 'creation_time': 0, 'cell_id': job.cell_id, 'run_id': job.run_id, 'job_id': job_id } if state.get('finished', 0) == 1: try: widget_info = job.get_viewer_params(state) except Exception as e: # Can't get viewer params new_e = transform_job_exception(e) kblogging.log_event(self._log, "lookup_job_status.error", {'err': str(e)}) state['job_state'] = 'error' state['error'] = { 'error': 'Unable to generate App output viewer!\nThe App appears to have completed successfully,\nbut we cannot construct its output viewer.\nPlease contact the developer of this App for assistance.', 'message': 'Unable to build output viewer parameters!', 'name': 'App Error', 'code': getattr(new_e, "code", -1), 'source': getattr(new_e, "source", "JobManager") } if 'canceling' in self._running_jobs[job_id]: state['job_state'] = 'canceling' return {'state': state, 'spec': app_spec, 'widget_info': widget_info, 'owner': job.owner} def _lookup_job_status(self, job_id): """ Will raise a ValueError if job_id doesn't exist. Sends the status over the comm channel as the usual job_status message. """ status = self._construct_job_status(job_id) self._send_comm_message('job_status', status) def _lookup_all_job_status(self, ignore_refresh_flag=False): """ Looks up status for all jobs. Once job info is acquired, it gets pushed to the front end over the 'KBaseJobs' channel. """ status_set = dict() # grab the list of running job ids, so we don't run into update-while-iterating problems. for job_id in self._running_jobs.keys(): if self._running_jobs[job_id]['refresh'] or ignore_refresh_flag: status_set[job_id] = self._construct_job_status(job_id) self._send_comm_message('job_status_all', status_set) def _lookup_job_status_loop(self): """ Initialize a loop that will look up job info. This uses a Timer thread on a 10 second loop to update things. """ self._lookup_all_job_status() self._lookup_timer = threading.Timer(10, self._lookup_job_status_loop) self._lookup_timer.start() def cancel_job_lookup_loop(self): """ Cancels a running timer if one's still alive. """ if self._lookup_timer: self._lookup_timer.cancel() self._lookup_timer = None self._running_lookup_loop = False def register_new_job(self, job): """ Registers a new Job with the manager - should only be invoked when a new Job gets started. This stores the Job locally and pushes it over the comm channel to the Narrative where it gets serialized. Parameters: ----------- job : biokbase.narrative.jobs.job.Job object The new Job that was started. """ self._running_jobs[job.job_id] = {'job': job, 'refresh': True} # push it forward! create a new_job message. self._lookup_job_status(job.job_id) self._send_comm_message('new_job', {}) def get_job(self, job_id): """ Returns a Job with the given job_id. Raises a ValueError if not found. """ if job_id in self._running_jobs: return self._running_jobs[job_id]['job'] else: raise ValueError('No job present with id {}'.format(job_id)) def _handle_comm_message(self, msg): """ Handles comm messages that come in from the other end of the KBaseJobs channel. All messages (of any use) should have a 'request_type' property. Possible types: * all_status refresh all jobs that are flagged to be looked up. Will send a message back with all lookup status. * job_status refresh the single job given in the 'job_id' field. Sends a message back with that single job's status, or an error message. * stop_update_loop stop the running refresh loop, if there's one going (might be one more pass, depending on the thread state) * start_update_loop reinitialize the refresh loop. * stop_job_update flag the given job id (should be an accompanying 'job_id' field) that the front end knows it's in a terminal state and should no longer have its status looked up in the refresh cycle. * start_job_update remove the flag that gets set by stop_job_update (needs an accompanying 'job_id' field) """ if 'request_type' in msg['content']['data']: r_type = msg['content']['data']['request_type'] job_id = msg['content']['data'].get('job_id', None) if job_id is not None and job_id not in self._running_jobs: # If it's not a real job, just silently ignore the request. # Maybe return an error? Yeah. Let's do that. # self._send_comm_message('job_comm_error', {'job_id': job_id, 'message': 'Unknown job id', 'request_type': r_type}) # TODO: perhaps we should implement request/response here. All we really need is to thread a message # id through self._send_comm_message('job_does_not_exist', {'job_id': job_id, 'request_type': r_type}) return if r_type == 'all_status': self._lookup_all_job_status(ignore_refresh_flag=True) elif r_type == 'job_status': if job_id is not None: self._lookup_job_status(job_id) elif r_type == 'stop_update_loop': if self._lookup_timer is not None: self._lookup_timer.cancel() elif r_type == 'start_update_loop': self._lookup_job_status_loop() elif r_type == 'stop_job_update': if job_id is not None: self._running_jobs[job_id]['refresh'] = False elif r_type == 'start_job_update': if job_id is not None: self._running_jobs[job_id]['refresh'] = True elif r_type == 'delete_job': if job_id is not None: try: self.delete_job(job_id) except Exception as e: self._send_comm_message('job_comm_error', {'message': str(e), 'request_type': r_type, 'job_id': job_id}) elif r_type == 'cancel_job': if job_id is not None: try: self.cancel_job(job_id) except Exception as e: self._send_comm_message('job_comm_error', {'message': str(e), 'request_type': r_type, 'job_id': job_id}) elif r_type == 'job_logs': if job_id is not None: first_line = msg['content']['data'].get('first_line', 0) num_lines = msg['content']['data'].get('num_lines', None) self._get_job_logs(job_id, first_line=first_line, num_lines=num_lines) else: raise ValueError('Need a job id to fetch jobs!') elif r_type == 'job_logs_latest': if job_id is not None: num_lines = msg['content']['data'].get('num_lines', None) self._get_latest_job_logs(job_id, num_lines=num_lines) else: self._send_comm_message('job_comm_error', {'message': 'Unknown message', 'request_type': r_type}) raise ValueError('Unknown KBaseJobs message "{}"'.format(r_type)) def _get_latest_job_logs(self, job_id, num_lines=None): job = self.get_job(job_id) if job is None: raise ValueError('job "{}" not found while fetching logs!'.format(job_id)) (max_lines, logs) = job.log() first_line = 0 if num_lines is not None and max_lines > num_lines: first_line = max_lines - num_lines logs = logs[first_line:] self._send_comm_message('job_logs', {'job_id': job_id, 'first': first_line, 'max_lines': max_lines, 'lines': logs, 'latest': True}) def _get_job_logs(self, job_id, first_line=0, num_lines=None): job = self.get_job(job_id) if job is None: raise ValueError('job "{}" not found!'.format(job_id)) (max_lines, log_slice) = job.log(first_line=first_line, num_lines=num_lines) self._send_comm_message('job_logs', {'job_id': job_id, 'first': first_line, 'max_lines': max_lines, 'lines': log_slice, 'latest': False}) def delete_job(self, job_id): """ If the job_id doesn't exist, raises a ValueError. Attempts to delete a job, and cancels it first. If the job cannot be canceled, raises an exception. If it can be canceled but not deleted, it gets canceled, then raises an exception. """ if job_id is None: raise ValueError('Job id required for deletion!') if job_id not in self._running_jobs: self._send_comm_message('job_does_not_exist', {'job_id': job_id, 'source': 'delete_job'}) return # raise ValueError('Attempting to cancel a Job that does not exist!') try: self.cancel_job(job_id) except Exception as e: raise try: clients.get('user_and_job_state').delete_job(job_id) except Exception as e: raise del self._running_jobs[job_id] self._send_comm_message('job_deleted', {'job_id': job_id}) def cancel_job(self, job_id): """ Cancels a running job, placing it in a canceled state. Does NOT delete the job. Raises an exception if the current user doesn't have permission to cancel the job. """ if job_id is None: raise ValueError('Job id required for cancellation!') if job_id not in self._running_jobs: self._send_comm_message('job_does_not_exist', {'job_id': job_id, 'source': 'cancel_job'}) return try: job = self.get_job(job_id) state = job.state() if state.get('canceled', 0) == 1 or state.get('finished', 0) == 1: # It's already finished, don't try to cancel it again. return except Exception as e: raise ValueError('Unable to get Job state') # Stop updating the job status while we try to cancel. # Also, set it to have a special state of 'canceling' while we're doing the cancel is_refreshing = self._running_jobs[job_id].get('refresh', False) self._running_jobs[job_id]['refresh'] = False self._running_jobs[job_id]['canceling'] = True try: clients.get('job_service').cancel_job({'job_id': job_id}) except Exception as e: new_e = transform_job_exception(e) error = { 'error': 'Unable to get cancel job', 'message': getattr(new_e, 'message', 'Unknown reason'), 'code': getattr(new_e, 'code', -1), 'source': getattr(new_e, 'source', 'jobmanager'), 'name': getattr(new_e, 'name', type(e).__name__), 'request_type': 'cancel_job', 'job_id': job_id } self._send_comm_message('job_comm_error', error) raise(e) finally: self._running_jobs[job_id]['refresh'] = is_refreshing del self._running_jobs[job_id]['canceling'] # # self._send_comm_message('job_canceled', {'job_id': job_id}) # Rather than a separate message, how about triggering a job-status message: self._lookup_job_status(job_id) def _send_comm_message(self, msg_type, content): """ Sends a ipykernel.Comm message to the KBaseJobs channel with the given msg_type and content. These just get encoded into the message itself. """ msg = { 'msg_type': msg_type, 'content': content } if self._comm is None: self._comm = Comm(target_name='KBaseJobs', data={}) self._comm.on_msg(self._handle_comm_message) self._comm.send(msg)
def init(self): from ipykernel.comm import Comm as IPyComm if self._comm: return self._comm = IPyComm(target_name=self.id, data={}) self._comm.on_msg(self._handle_msg)
def runByTag(self, tag): arguments = dict(target_name='beakerx.tag.run') comm = Comm(**arguments) msg = {'runByTag': tag} state = {'state': msg} comm.send(data=state, buffers=[])
def init_autotranslation_comm(self): self._comm = Comm(target_name='beakerx.autotranslation') self._comm.open()
class JobManager(object): """ The KBase Job Manager class. This handles all jobs and makes their status available. On status lookups, it feeds the results to the KBaseJobs channel that the front end listens to. """ __instance = None # keys = job_id, values = { refresh = T/F, job = Job object } _running_jobs = dict() # keys = job_id, values = state from either Job object or NJS (these are identical) _completed_job_states = dict() _lookup_timer = None _comm = None _log = kblogging.get_logger(__name__) # TODO: should this not be done globally? _running_lookup_loop = False def __new__(cls): if JobManager.__instance is None: JobManager.__instance = object.__new__(cls) return JobManager.__instance def initialize_jobs(self, start_lookup_thread=True): """ Initializes this JobManager. This is expected to be run by a running Narrative, and naturally linked to a workspace. So it does the following steps. 1. app_util.system_variable('workspace_id') 2. get list of jobs with that ws id from UJS (also gets tag, cell_id, run_id) 3. initialize the Job objects by running NJS.get_job_params (also gets app_id) 4. start the status lookup loop. """ the_time = int(round(time.time() * 1000)) self._send_comm_message('start', {'time': the_time}) ws_id = system_variable('workspace_id') try: nar_jobs = clients.get('user_and_job_state').list_jobs2({ 'authstrat': 'kbaseworkspace', 'authparams': [str(ws_id)] }) except Exception as e: kblogging.log_event(self._log, 'init_error', {'err': str(e)}) new_e = transform_job_exception(e) error = { 'error': 'Unable to get initial jobs list', 'message': getattr(new_e, 'message', 'Unknown reason'), 'code': getattr(new_e, 'code', -1), 'source': getattr(new_e, 'source', 'jobmanager'), 'name': getattr(new_e, 'name', type(e).__name__), 'service': 'user_and_job_state' } self._send_comm_message('job_init_err', error) raise new_e job_ids = [j[0] for j in nar_jobs] job_states = clients.get('job_service').check_jobs({ 'job_ids': job_ids, 'with_job_params': 1 }) job_param_info = job_states.get('job_params', {}) job_check_error = job_states.get('check_error', {}) error_jobs = dict() for info in nar_jobs: job_id = info[0] user_info = info[1] job_meta = info[10] try: if job_id in job_param_info: job_info = job_param_info[job_id] job = Job.from_state(job_id, job_info, user_info[0], app_id=job_info.get('app_id'), tag=job_meta.get('tag', 'release'), cell_id=job_meta.get('cell_id', None), run_id=job_meta.get('run_id', None), token_id=job_meta.get('token_id', None), meta=job_meta) # Note that when jobs for this narrative are initially loaded, # they are set to not be refreshed. Rather, if a client requests # updates via the start_job_update message, the refresh flag will # be set to True. self._running_jobs[job_id] = { 'refresh': 0, 'job': job } elif job_id in job_check_error: job_err_state = { 'job_state': 'error', 'error': { 'error': 'KBase execution engine returned an error while looking up this job.', 'message': job_check_error[job_id].get('message', 'No error message available'), 'name': 'Job Error', 'code': job_check_error[job_id].get('code', -999), 'exception': { 'error_message': 'Job lookup in execution engine failed', 'error_type': job_check_error[job_id].get('name', 'unknown'), 'error_stacktrace': job_check_error[job_id].get('error', '') } }, 'cell_id': job_meta.get('cell_id', None), 'run_id': job_meta.get('run_id', None), } error_jobs[job_id] = job_err_state except Exception as e: kblogging.log_event(self._log, 'init_error', {'err': str(e)}) new_e = transform_job_exception(e) error = { 'error': 'Unable to get job info on initial lookup', 'job_id': job_id, 'message': getattr(new_e, 'message', 'Unknown reason'), 'code': getattr(new_e, 'code', -1), 'source': getattr(new_e, 'source', 'jobmanager'), 'name': getattr(new_e, 'name', type(e).__name__), 'service': 'job_service' } self._send_comm_message('job_init_lookup_err', error) raise new_e # should crash and burn on any of these. if len(job_check_error): err_str = 'Unable to find info for some jobs on initial lookup' err_type = 'job_init_partial_err' if len(job_check_error) == len(nar_jobs): err_str = 'Unable to get info for any job on initial lookup' err_type = 'job_init_lookup_err' error = { 'error': err_str, 'job_errors': error_jobs, 'message': 'Job information was unavailable from the server', 'code': -2, 'source': 'jobmanager', 'name': 'jobmanager', 'service': 'job_service', } self._send_comm_message(err_type, error) if not self._running_lookup_loop and start_lookup_thread: # only keep one loop at a time in cause this gets called again! if self._lookup_timer is not None: self._lookup_timer.cancel() self._running_lookup_loop = True self._lookup_job_status_loop() else: self._lookup_all_job_status() def _create_jobs(self, job_ids): """ TODO: error handling Makes a bunch of Job objects from job_ids. Initially used to make Child jobs from some parent, but will eventually be adapted to all jobs on startup. Just slaps them all into _running_jobs """ job_states = clients.get('job_service').check_jobs({'job_ids': job_ids, 'with_job_params': 1}) for job_id in job_ids: ujs_info = clients.get('user_and_job_state').get_job_info2(job_id) if job_id in job_ids and job_id not in self._running_jobs: job_info = job_states.get('job_params', {}).get(job_id, {}) job_meta = ujs_info[10] job = Job.from_state(job_id, # the id job_info, # params, etc. ujs_info[2], # owner id app_id=job_info.get('app_id', job_info.get('method')), tag=job_meta.get('tag', 'release'), cell_id=job_meta.get('cell_id', None), run_id=job_meta.get('run_id', None), token_id=job_meta.get('token_id', None), meta=job_meta) # Note that when jobs for this narrative are initially loaded, # they are set to not be refreshed. Rather, if a client requests # updates via the start_job_update message, the refresh flag will # be set to True. self._running_jobs[job_id] = { 'refresh': 0, 'job': job } def list_jobs(self): """ List all job ids, their info, and status in a quick HTML format. """ try: status_set = list() for job_id in self._running_jobs: job = self._running_jobs[job_id]['job'] job_state = self._get_job_state(job_id) job_state['app_id'] = job.app_id job_state['owner'] = job.owner status_set.append(job_state) if not len(status_set): return "No running jobs!" status_set = sorted(status_set, key=lambda s: s['creation_time']) for i in range(len(status_set)): status_set[i]['creation_time'] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(status_set[i]['creation_time']/1000), "%Y-%m-%d %H:%M:%S") exec_start = status_set[i].get('exec_start_time', None) if 'finish_time' in status_set[i]: finished = status_set[i].get('finish_time', None) if finished is not None and exec_start: delta = datetime.datetime.fromtimestamp(finished/1000.0) - datetime.datetime.fromtimestamp(exec_start/1000.0) delta = delta - datetime.timedelta(microseconds=delta.microseconds) status_set[i]['run_time'] = str(delta) status_set[i]['finish_time'] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(status_set[i]['finish_time']/1000), "%Y-%m-%d %H:%M:%S") elif exec_start: delta = datetime.datetime.utcnow() - datetime.datetime.utcfromtimestamp(exec_start/1000.0) delta = delta - datetime.timedelta(microseconds=delta.microseconds) status_set[i]['run_time'] = str(delta) else: status_set[i]['run_time'] = 'Not started' tmpl = """ <table class="table table-bordered table-striped table-condensed"> <tr> <th>Id</th> <th>Name</th> <th>Submitted</th> <th>Submitted By</th> <th>Status</th> <th>Run Time</th> <th>Complete Time</th> </tr> {% for j in jobs %} <tr> <td>{{ j.job_id|e }}</td> <td>{{ j.app_id|e }}</td> <td>{{ j.creation_time|e }}</td> <td>{{ j.owner|e }}</td> <td>{{ j.job_state|e }}</td> <td>{{ j.run_time|e }}</td> <td>{% if j.finish_time %}{{ j.finish_time|e }}{% else %}Incomplete{% endif %}</td> </tr> {% endfor %} </table> """ return HTML(Template(tmpl).render(jobs=status_set)) except Exception as e: kblogging.log_event(self._log, "list_jobs.error", {'err': str(e)}) raise def get_jobs_list(self): """ A convenience method for fetching an unordered list of all running Jobs. """ return [j['job'] for j in self._running_jobs.values()] def _construct_job_status(self, job, state): """ Creates a Job status dictionary with structure: { owner: string (username), spec: app_spec (from NMS, via biokbase.narrative.jobs.specmanager) widget_info: (if not finished, None, else...) job.get_viewer_params result state: { job_state: string, error (if present): dict of error info, cell_id: string/None, run_id: string/None, awe_job_id: string/None, canceled: 0/1 creation_time: epoch second exec_start_time: epoch/none, finish_time: epoch/none, finished: 0/1, job_id: string, status: (from UJS) [ timestamp(last_update, string), stage (string), status (string), progress (string/None), est_complete (string/None), complete (0/1), error (0/1) ], ujs_url: string } } """ widget_info = None app_spec = {} if job is None: state = { 'job_state': 'error', 'error': { 'error': 'Job does not seem to exist, or it is otherwise unavailable.', 'message': 'Job does not exist', 'name': 'Job Error', 'code': -1, 'exception': { 'error_message': 'job not found in JobManager', 'error_type': 'ValueError', 'error_stacktrace': '' } }, 'cell_id': None, 'run_id': None, } return { 'state': state, 'app_spec': app_spec, 'widget_info': widget_info, 'owner': None } # try: # app_spec = job.app_spec() # except Exception as e: # kblogging.log_event(self._log, "lookup_job_status.error", {'err': str(e)}) if state is None: kblogging.log_event(self._log, "lookup_job_status.error", {'err': 'Unable to get job state for job {}'.format(job.job_id)}) state = { 'job_state': 'error', 'error': { 'error': 'Unable to find current job state. Please try again later, or contact KBase.', 'message': 'Unable to return job state', 'name': 'Job Error', 'code': -1, 'source': 'JobManager._construct_job_status', 'exception': { 'error_message': 'No state provided during lookup', 'error_type': 'null-state', 'error_stacktrace': '', } }, 'creation_time': 0, 'cell_id': job.cell_id, 'run_id': job.run_id, 'job_id': job.job_id } elif 'lookup_error' in state: kblogging.log_event(self._log, "lookup_job_status.error", { 'err': 'Problem while getting state for job {}'.format(job.job_id), 'info': str(state['lookup_error']) }) state = { 'job_state': 'error', 'error': { 'error': 'Unable to fetch current state. Please try again later, or contact KBase.', 'message': 'Error while looking up job state', 'name': 'Job Error', 'code': -1, 'source': 'JobManager._construct_job_status', 'exception': { 'error_message': 'Error while fetching job state', 'error_type': 'failed-lookup', }, 'error_response': state['lookup_error'], 'creation_time': 0, 'cell_id': job.cell_id, 'run_id': job.run_id, 'job_id': job.job_id } } if state.get('finished', 0) == 1: try: widget_info = job.get_viewer_params(state) except Exception as e: # Can't get viewer params new_e = transform_job_exception(e) kblogging.log_event(self._log, "lookup_job_status.error", {'err': str(e)}) state['job_state'] = 'error' state['error'] = { 'error': 'Unable to generate App output viewer!\nThe App appears to have completed successfully,\nbut we cannot construct its output viewer.\nPlease contact the developer of this App for assistance.', 'message': 'Unable to build output viewer parameters!', 'name': 'App Error', 'code': getattr(new_e, "code", -1), 'source': getattr(new_e, "source", "JobManager") } if 'canceling' in self._running_jobs[job.job_id]: state['job_state'] = 'canceling' state.update({ 'child_jobs': self._child_job_states( state.get('sub_jobs', []), job.meta.get('batch_app'), job.meta.get('batch_tag') ) }) if 'batch_size' in job.meta: state.update({'batch_size': job.meta['batch_size']}) return {'state': state, 'spec': app_spec, 'widget_info': widget_info, 'owner': job.owner, 'listener_count': self._running_jobs[job.job_id]['refresh']} def _child_job_states(self, sub_job_list, app_id, app_tag): """ Fetches state for all jobs in the list. These are expected to be child jobs, with no actual Job object associated. So if they're done, we need to do the output mapping out of band. But the check_jobs call with params will return the app id. So that helps. app_id = the id of the app that all the child jobs are running (format: module/method, like "MEGAHIT/run_megahit") app_tag = one of "release", "beta", "dev" (the above two aren't stored with the subjob metadata, and won't until we back some more on KBParallel - I want to lobby for pushing toward just starting everything up at once from here and letting HTCondor deal with allocation) sub_job_list = list of ids of jobs to look up """ if not sub_job_list: return [] sub_job_list = sorted(sub_job_list) job_info = clients.get('job_service').check_jobs({'job_ids': sub_job_list, 'with_job_params': 1}) child_job_states = list() for job_id in sub_job_list: params = job_info['job_params'][job_id] # if it's error, get the error. if job_id in job_info['check_error']: error = job_info['check_error'][job_id] error.update({'job_id': job_id}) child_job_states.append(error) continue # if it's done, get the output mapping. state = job_info['job_states'][job_id] if state.get('finished', 0) == 1: try: widget_info = Job.map_viewer_params( state, params['params'], app_id, app_tag ) except ValueError: widget_info = {} state.update({'widget_info': widget_info}) child_job_states.append(state) return child_job_states def _construct_job_status_set(self, job_ids): job_states = self._get_all_job_states(job_ids) status_set = dict() for job_id in job_ids: job = None if job_id in self._running_jobs: job = self._running_jobs[job_id]['job'] status_set[job_id] = self._construct_job_status(job, job_states.get(job_id, None)) return status_set def _verify_job_parentage(self, parent_job_id, child_job_id): """ Validate job relationships. 1. Make sure parent exists, and the child id is in its list of sub jobs. 2. If child doesn't exist, create it and add it to the list. If parent doesn't exist, or child isn't an actual child, raise an exception """ if parent_job_id not in self._running_jobs: raise ValueError('Parent job id {} not found, cannot validate child job {}.'.format(parent_job_id, child_job_id)) if child_job_id not in self._running_jobs: parent_job = self.get_job(parent_job_id) parent_state = parent_job.state() if child_job_id not in parent_state.get('sub_jobs', []): raise ValueError('Child job id {} is not a child of parent job {}'.format(child_job_id, parent_job_id)) else: self._create_jobs([child_job_id]) # injects its app id and version child_job = self.get_job(child_job_id) child_job.app_id = parent_job.meta.get('batch_app') child_job.tag = parent_job.meta.get('batch_tag', 'release') def _lookup_job_status(self, job_id, parent_job_id=None): """ Will raise a ValueError if job_id doesn't exist. Sends the status over the comm channel as the usual job_status message. """ # if parent_job is real, and job_id (the child) is not, just add it to the # list of running jobs and work as normal. if parent_job_id is not None: self._verify_job_parentage(parent_job_id, job_id) job = self._running_jobs.get(job_id, {}).get('job', None) state = self._get_job_state(job_id) status = self._construct_job_status(job, state) self._send_comm_message('job_status', status) def _lookup_job_info(self, job_id, parent_job_id=None): """ Will raise a ValueError if job_id doesn't exist. Sends the info over the comm channel as this packet: { app_id: module/name, app_name: random string, job_id: string, job_params: dictionary } """ # if parent_job is real, and job_id (the child) is not, just add it to the # list of running jobs and work as normal. if parent_job_id is not None: self._verify_job_parentage(parent_job_id, job_id) job = self.get_job(job_id) info = { 'app_id': job.app_id, 'app_name': job.app_spec()['info']['name'], 'job_id': job_id, 'job_params': job.inputs } self._send_comm_message('job_info', info) def _lookup_all_job_status(self, ignore_refresh_flag=False): """ Looks up status for all jobs. Once job info is acquired, it gets pushed to the front end over the 'KBaseJobs' channel. """ jobs_to_lookup = list() # grab the list of running job ids, so we don't run into update-while-iterating problems. for job_id in self._running_jobs.keys(): if self._running_jobs[job_id]['refresh'] > 0 or ignore_refresh_flag: jobs_to_lookup.append(job_id) if len(jobs_to_lookup) > 0: status_set = self._construct_job_status_set(jobs_to_lookup) self._send_comm_message('job_status_all', status_set) return len(jobs_to_lookup) def _start_job_status_loop(self): kblogging.log_event(self._log, 'starting job status loop', {}) if self._lookup_timer is None: self._lookup_job_status_loop() def _lookup_job_status_loop(self): """ Initialize a loop that will look up job info. This uses a Timer thread on a 10 second loop to update things. """ refreshing_jobs = self._lookup_all_job_status() # Automatically stop when there are no more jobs requesting a refresh. if refreshing_jobs == 0: self.cancel_job_lookup_loop() else: self._lookup_timer = threading.Timer(10, self._lookup_job_status_loop) self._lookup_timer.start() def cancel_job_lookup_loop(self): """ Cancels a running timer if one's still alive. """ if self._lookup_timer: self._lookup_timer.cancel() self._lookup_timer = None self._running_lookup_loop = False def register_new_job(self, job): """ Registers a new Job with the manager - should only be invoked when a new Job gets started. This stores the Job locally and pushes it over the comm channel to the Narrative where it gets serialized. Parameters: ----------- job : biokbase.narrative.jobs.job.Job object The new Job that was started. """ self._running_jobs[job.job_id] = {'job': job, 'refresh': 0} # push it forward! create a new_job message. self._lookup_job_status(job.job_id) self._send_comm_message('new_job', { 'job_id': job.job_id }) def get_job(self, job_id): """ Returns a Job with the given job_id. Raises a ValueError if not found. """ if job_id in self._running_jobs: return self._running_jobs[job_id]['job'] else: raise ValueError('No job present with id {}'.format(job_id)) def _handle_comm_message(self, msg): """ Handles comm messages that come in from the other end of the KBaseJobs channel. All messages (of any use) should have a 'request_type' property. Possible types: * all_status refresh all jobs that are flagged to be looked up. Will send a message back with all lookup status. * job_status refresh the single job given in the 'job_id' field. Sends a message back with that single job's status, or an error message. * stop_update_loop stop the running refresh loop, if there's one going (might be one more pass, depending on the thread state) * start_update_loop reinitialize the refresh loop. * stop_job_update flag the given job id (should be an accompanying 'job_id' field) that the front end knows it's in a terminal state and should no longer have its status looked up in the refresh cycle. * start_job_update remove the flag that gets set by stop_job_update (needs an accompanying 'job_id' field) * job_info from the given 'job_id' field, returns some basic info about the job, including the app id, version, app name, and key-value pairs for inputs and parameters (in the parameters id namespace specified by the app spec). """ if 'request_type' in msg['content']['data']: r_type = msg['content']['data']['request_type'] job_id = msg['content']['data'].get('job_id', None) parent_job_id = msg['content']['data'].get('parent_job_id', None) if job_id is not None and job_id not in self._running_jobs and not parent_job_id: # If it's not a real job, just silently ignore the request. # Unless it has a parent job id, then its a child job, so things get muddled. If there's 100+ child jobs, # then this might get tricky to look up all of them. Let it pass through and fail if it's not real. # # TODO: perhaps we should implement request/response here. All we really need is to thread a message # id through self._send_comm_message('job_does_not_exist', {'job_id': job_id, 'request_type': r_type}) return elif parent_job_id is not None: try: self._verify_job_parentage(parent_job_id, job_id) except ValueError as e: self._send_comm_message('job_does_not_exist', {'job_id': job_id, 'parent_job_id': parent_job_id, 'request_type': r_type}) if r_type == 'all_status': self._lookup_all_job_status(ignore_refresh_flag=True) elif r_type == 'job_status': if job_id is not None: self._lookup_job_status(job_id, parent_job_id=parent_job_id) elif r_type == 'job_info': if job_id is not None: self._lookup_job_info(job_id, parent_job_id=parent_job_id) elif r_type == 'stop_update_loop': self.cancel_job_lookup_loop() elif r_type == 'start_update_loop': self._start_job_status_loop() elif r_type == 'stop_job_update': if job_id is not None: if self._running_jobs[job_id]['refresh'] > 0: self._running_jobs[job_id]['refresh'] -= 1 elif r_type == 'start_job_update': if job_id is not None: self._running_jobs[job_id]['refresh'] += 1 self._start_job_status_loop() elif r_type == 'delete_job': if job_id is not None: try: self.delete_job(job_id, parent_job_id=parent_job_id) except Exception as e: self._send_comm_message('job_comm_error', {'message': str(e), 'request_type': r_type, 'job_id': job_id}) elif r_type == 'cancel_job': if job_id is not None: try: self.cancel_job(job_id, parent_job_id=parent_job_id) except Exception as e: self._send_comm_message('job_comm_error', {'message': str(e), 'request_type': r_type, 'job_id': job_id}) elif r_type == 'job_logs': if job_id is not None: first_line = msg['content']['data'].get('first_line', 0) num_lines = msg['content']['data'].get('num_lines', None) self._get_job_logs(job_id, parent_job_id=parent_job_id, first_line=first_line, num_lines=num_lines) else: raise ValueError('Need a job id to fetch jobs!') elif r_type == 'job_logs_latest': if job_id is not None: num_lines = msg['content']['data'].get('num_lines', None) try: self._get_latest_job_logs(job_id, parent_job_id=parent_job_id, num_lines=num_lines) except Exception as e: self._send_comm_message('job_comm_error', { 'job_id': job_id, 'message': str(e), 'request_type': r_type}) else: raise ValueError('Need a job id to fetch jobs!') else: self._send_comm_message('job_comm_error', {'message': 'Unknown message', 'request_type': r_type}) raise ValueError('Unknown KBaseJobs message "{}"'.format(r_type)) def _get_latest_job_logs(self, job_id, parent_job_id=None, num_lines=None): job = self.get_job(job_id) if job is None: raise ValueError('job "{}" not found while fetching logs!'.format(job_id)) (max_lines, logs) = job.log() first_line = 0 if num_lines is not None and max_lines > num_lines: first_line = max_lines - num_lines logs = logs[first_line:] self._send_comm_message('job_logs', { 'job_id': job_id, 'first': first_line, 'max_lines': max_lines, 'lines': logs, 'latest': True}) def _get_job_logs(self, job_id, parent_job_id=None, first_line=0, num_lines=None): # if parent_job is real, and job_id (the child) is not, just add it to the # list of running jobs and work as normal. job = self.get_job(job_id) if job is None: raise ValueError('job "{}" not found!'.format(job_id)) (max_lines, log_slice) = job.log(first_line=first_line, num_lines=num_lines) self._send_comm_message('job_logs', {'job_id': job_id, 'first': first_line, 'max_lines': max_lines, 'lines': log_slice, 'latest': False}) def delete_job(self, job_id, parent_job_id=None): """ If the job_id doesn't exist, raises a ValueError. Attempts to delete a job, and cancels it first. If the job cannot be canceled, raises an exception. If it can be canceled but not deleted, it gets canceled, then raises an exception. """ if job_id is None: raise ValueError('Job id required for deletion!') if not parent_job_id and job_id not in self._running_jobs: self._send_comm_message('job_does_not_exist', {'job_id': job_id, 'source': 'delete_job'}) return # raise ValueError('Attempting to cancel a Job that does not exist!') try: self.cancel_job(job_id, parent_job_id=parent_job_id) except Exception: raise try: clients.get('user_and_job_state').delete_job(job_id) except Exception: raise if job_id in self._running_jobs: del self._running_jobs[job_id] if job_id in self._completed_job_states: del self._completed_job_states[job_id] self._send_comm_message('job_deleted', {'job_id': job_id}) def cancel_job(self, job_id, parent_job_id=None): """ Cancels a running job, placing it in a canceled state. Does NOT delete the job. Raises an exception if the current user doesn't have permission to cancel the job. """ if job_id is None: raise ValueError('Job id required for cancellation!') if not parent_job_id and job_id not in self._running_jobs: self._send_comm_message('job_does_not_exist', {'job_id': job_id, 'source': 'cancel_job'}) return try: state = self._get_job_state(job_id, parent_job_id=parent_job_id) if state.get('canceled', 0) == 1 or state.get('finished', 0) == 1: # It's already finished, don't try to cancel it again. return except Exception as e: raise ValueError('Unable to get Job state') # Stop updating the job status while we try to cancel. # Also, set it to have a special state of 'canceling' while we're doing the cancel if not parent_job_id: is_refreshing = self._running_jobs[job_id].get('refresh', 0) self._running_jobs[job_id]['refresh'] = 0 self._running_jobs[job_id]['canceling'] = True try: clients.get('job_service').cancel_job({'job_id': job_id}) except Exception as e: new_e = transform_job_exception(e) error = { 'error': 'Unable to get cancel job', 'message': getattr(new_e, 'message', 'Unknown reason'), 'code': getattr(new_e, 'code', -1), 'source': getattr(new_e, 'source', 'jobmanager'), 'name': getattr(new_e, 'name', type(e).__name__), 'request_type': 'cancel_job', 'job_id': job_id } self._send_comm_message('job_comm_error', error) raise(e) finally: if not parent_job_id: self._running_jobs[job_id]['refresh'] = is_refreshing del self._running_jobs[job_id]['canceling'] # Rather than a separate message, how about triggering a job-status message: self._lookup_job_status(job_id, parent_job_id=parent_job_id) def _send_comm_message(self, msg_type, content): """ Sends a ipykernel.Comm message to the KBaseJobs channel with the given msg_type and content. These just get encoded into the message itself. """ msg = { 'msg_type': msg_type, 'content': content } if self._comm is None: self._comm = Comm(target_name='KBaseJobs', data={}) self._comm.on_msg(self._handle_comm_message) self._comm.send(msg) def _get_all_job_states(self, job_ids=None): """ Returns the state for all running jobs """ # 1. Get list of ids if job_ids is None: job_ids = self._running_jobs.keys() # 1.5 Go through job ids and remove ones that aren't found. job_ids = [j for j in job_ids if j in self._running_jobs] # 2. Foreach, check if in completed cache. If so, grab the status. If not, enqueue id # for batch lookup. job_states = dict() jobs_to_lookup = list() for job_id in job_ids: if job_id in self._completed_job_states: job_states[job_id] = dict(self._completed_job_states[job_id]) else: jobs_to_lookup.append(job_id) # 3. Lookup those jobs what need it. Cache 'em as we go, if finished. try: fetched_states = clients.get('job_service').check_jobs({'job_ids': jobs_to_lookup}) except Exception as e: kblogging.log_event(self._log, 'get_all_job_states_error', {'err': str(e)}) return {} error_states = fetched_states.get('check_errors', {}) fetched_states = fetched_states.get('job_states', {}) for job_id in jobs_to_lookup: if job_id in fetched_states: state = fetched_states[job_id] state['cell_id'] = self._running_jobs[job_id]['job'].cell_id state['run_id'] = self._running_jobs[job_id]['job'].run_id if state.get('finished', 0) == 1: self._completed_job_states[state['job_id']] = dict(state) job_states[state['job_id']] = state elif job_id in error_states: error = error_states[job_id] job_states[state['job_id']] = {'lookup_error': error} return job_states def _get_job_state(self, job_id, parent_job_id=None): if parent_job_id is not None: self._verify_job_parentage(parent_job_id, job_id) if job_id is None or job_id not in self._running_jobs: raise ValueError('job_id {} not found'.format(job_id)) if job_id in self._completed_job_states: return dict(self._completed_job_states[job_id]) state = self._running_jobs[job_id]['job'].state() if state.get('finished', 0) == 1: self._completed_job_states[job_id] = dict(state) return dict(state)
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._ext_close = False def _on_close(close_message): self._ext_close = True manager.remove_comm(close_message['content']['comm_id']) manager.clearup_closed() self.comm.on_close(_on_close) def is_open(self): return not (self._ext_close or self.comm._closed) def on_close(self): # When the socket is closed, deregister the websocket with # the FigureManager. if self.is_open(): try: self.comm.close() except KeyError: # apparently already cleaned it up? pass 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() self.manager.clearup_closed() elif message['type'] == 'supports_binary': self.supports_binary = message['value'] else: self.manager.handle_json(message)
def open(self, props): props['module'] = self.module args = dict(target_name=self.target_name, data=props) args['comm_id'] = 'jupyter_react.{}.{}'.format( uuid.uuid4(), props['module'] ) self.comm = Comm(**args)
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 = {} 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.""" class_name = str(msg['content']['data']['widget_class']) if class_name in Widget.widget_types: widget_class = Widget.widget_types[class_name] else: widget_class = import_item(class_name) widget = widget_class(comm=comm) #------------------------------------------------------------------------- # Traits #------------------------------------------------------------------------- _model_module = Unicode('jupyter-js-widgets', help="""A requirejs module name in which to find _model_name. If empty, look in the global registry.""").tag(sync=True) _model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""").tag(sync=True) _view_module = Unicode(None, allow_none=True, help="""A requirejs module in which to find _view_name. If empty, look in the global registry.""").tag(sync=True) _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end to use to represent the widget.""").tag(sync=True) comm = Instance('ipykernel.comm.Comm', allow_none=True) msg_throttle = Int(3, help="""Maximum number of msgs the front-end can send before receiving an idle msg from the back-end.""").tag(sync=True) keys = List() def _keys_default(self): return [name for name in self.traits(sync=True)] _property_lock = Dict() _holding_sync = False _states_to_send = Set() _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: state, buffer_keys, buffers = self._split_state_buffers(self.get_state()) args = dict(target_name='jupyter.widget', data=state) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args) if buffers: # FIXME: workaround ipykernel missing binary message support in open-on-init # send state with binary elements as second message self.send_state() 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 @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 self._ipython_display_ = None def _split_state_buffers(self, state): """Return (state_without_buffers, buffer_keys, buffers) for binary message parts""" buffer_keys, buffers = [], [] for k, v in list(state.items()): if isinstance(v, _binary_types): state.pop(k) buffers.append(v) buffer_keys.append(k) return state, buffer_keys, buffers 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. """ state = self.get_state(key=key) state, buffer_keys, buffers = self._split_state_buffers(state) msg = {'method': 'update', 'state': state, 'buffers': buffer_keys} self._send(msg, buffers=buffers) 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. Returns ------- state : dict of states metadata : dict metadata for each field: {key: metadata} """ 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 = {} traits = self.traits() if not PY3 else {} # no need to construct traits on PY3 for k in keys: to_json = self.trait_metadata(k, 'to_json', self._trait_to_json) value = to_json(getattr(self, k), self) if not PY3 and isinstance(traits[k], Bytes) and isinstance(value, bytes): value = memoryview(value) state[k] = value return state def set_state(self, sync_data): """Called when a state is received from the front-end.""" # The order of these context managers is important. Properties must # be locked when the hold_trait_notification context manager is # released and notifications are fired. with self._lock_property(**sync_data), self.hold_trait_notifications(): for name in sync_data: if name in self.keys: from_json = self.trait_metadata(name, 'from_json', self._trait_from_json) self.set_trait(name, from_json(sync_data[name], self)) def send(self, content, buffers=None): """Sends a custom msg to the widget model in the front-end. Parameters ---------- content : dict Content of the message to send. buffers : list of binary buffers Binary buffers to send with message """ self._send({"method": "custom", "content": content}, buffers=buffers) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- callback: callable callback will be passed three arguments when a message arrives:: callback(widget, content, buffers) 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) def add_traits(self, **traits): """Dynamically add trait attributes to the Widget.""" super(Widget, self).add_traits(**traits) for name, trait in traits.items(): if trait.get_metadata('sync'): self.keys.append(name) self.send_state(name) def notify_change(self, change): """Called when a property has changed.""" # Send the state before the user registered callbacks for trait changes # have all fired. name = change['name'] 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, change['new']): # Send new state to front-end self.send_state(key=name) LoggingConfigurable.notify_change(self, change) #------------------------------------------------------------------------- # Support methods #------------------------------------------------------------------------- @contextmanager def _lock_property(self, **properties): """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 = properties try: yield finally: self._property_lock = {} @contextmanager def hold_sync(self): """Hold syncing any state until the outermost context manager exits""" if self._holding_sync is True: yield else: try: self._holding_sync = True yield finally: self._holding_sync = False 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 in self._property_lock and to_json(value, self) == self._property_lock[key]): return False elif self._holding_sync: 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: # get binary buffers too sync_data = data['sync_data'] for i,k in enumerate(data.get('buffer_keys', [])): sync_data[k] = msg['buffers'][i] 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'], msg['buffers']) # Catch remainder. else: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) def _handle_custom_msg(self, content, buffers): """Called when a custom msg is received.""" self._msg_callbacks(self, content, buffers) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" self._display_callbacks(self, **kwargs) @staticmethod def _trait_to_json(x, self): """Convert a trait value to json.""" return x @staticmethod def _trait_from_json(x, self): """Convert json values to objects.""" return x def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" def loud_error(message): self.log.warn(message) sys.stderr.write('%s\n' % message) # Show view. if self._view_name is not None: validated = Widget._version_validated # Before the user tries to display a widget. Validate that the # widget front-end is what is expected. if validated is None: loud_error('Widget Javascript not detected. It may not be installed properly.') elif not validated: loud_error('The installed widget Javascript is the wrong version.') # TODO: delete this sending of a comm message when the display statement # below works. Then add a 'text/plain' mimetype to the dictionary below. self._send({"method": "display"}) # The 'application/vnd.jupyter.widget' mimetype has not been registered yet. # See the registration process and naming convention at # http://tools.ietf.org/html/rfc6838 # and the currently registered mimetypes at # http://www.iana.org/assignments/media-types/media-types.xhtml. # We don't have a 'text/plain' entry so that the display message will be # will be invisible in the current notebook. data = { 'application/vnd.jupyter.widget': self._model_id } display(data, raw=True) self._handle_displayed(**kwargs) def _send(self, msg, buffers=None): """Sends a message to the model in the front-end.""" self.comm.send(data=msg, buffers=buffers)
class BeakerX: def __init__(self): BeakerX.pandas_display_table() self._comm = None self._queue = Queue() self._server = BeakerxZMQServer(self._queue) self._url = self._server.url @staticmethod def pandas_display_default(): pandas.DataFrame._ipython_display_ = None @staticmethod def pandas_display_table(): pandas.DataFrame._ipython_display_ = TableDisplayWrapper() def set4(self, var, val, unset, sync): args = {'name': var, 'sync': sync} if not unset: val = transform(val) args['value'] = json.dumps(val, cls=DataFrameEncoder) state = {'state': args} if self._comm is None: self.init_autotranslation_comm() self._comm.send(data=state) def init_autotranslation_comm(self): self._comm = Comm(target_name='beakerx.autotranslation') self._comm.open() def get(self, var): result = autotranslation_get(var) if result == 'undefined': raise NameError('name \'' + var + '\' is not defined on the beakerx object') return transformBack(json.loads(result)) def set_session(self, id): self.session_id = id def register_output(self): ip = IPython.InteractiveShell.instance() ip.display_formatter.formatters['application/json'] = MyJSONFormatter(parent=ip.display_formatter) def set(self, var, val): autotranslation_update(var, val) return self.set4(var, val, False, True) def unset(self, var): return self.set4(var, None, True, True) def isDefined(self, var): return autotranslation_get(var) != 'undefined' def createOutputContainer(self): return OutputContainer() def showProgressUpdate(self): return "WARNING: python3 language plugin does not support progress updates" def evaluate(self, filter): args = {'filter': filter, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/evaluate', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = json.loads(conn.read().decode()) return transformBack(result) def evaluateCode(self, evaluator, code): args = {'evaluator': evaluator, 'code': code, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/evaluateCode', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = json.loads(conn.read().decode()) return transformBack(result) def showStatus(self, msg): args = {'msg': msg, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/showStatus', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = conn.read() return result == "true" def clearStatus(self, msg): args = {'msg': msg, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/clearStatus', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = conn.read() return result == "true" def showTransientStatus(self, msg): args = {'msg': msg, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/showTransientStatus', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = conn.read() return result == "true" def getEvaluators(self): req = urllib.request.Request(self.core_url + '/rest/notebookctrl/getEvaluators?' + urllib.parse.urlencode({ 'session': self.session_id})) conn = self._beaker_url_opener.open(req) result = json.loads(conn.read().decode()) return transformBack(result) def getVersion(self): req = urllib.request.Request( self.core_url + '/rest/util/version?' + urllib.parse.urlencode({'session': self.session_id})) conn = self._beaker_url_opener.open(req) return transformBack(conn.read().decode()) def getVersionNumber(self): req = urllib.request.Request( self.core_url + '/rest/util/getVersionInfo?' + urllib.parse.urlencode({'session': self.session_id})) conn = self._beaker_url_opener.open(req) result = json.loads(conn.read().decode()) return transformBack(result['version']) def getCodeCells(self, filter): req = urllib.request.Request(self.core_url + '/rest/notebookctrl/getCodeCells?' + urllib.parse.urlencode({ 'filter': filter})) conn = self._beaker_url_opener.open(req) result = json.loads(conn.read().decode()) return transformBack(result) def setCodeCellBody(self, name, body): args = {'name': name, 'body': body, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/setCodeCellBody', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = conn.read() return result == "true" def setCodeCellEvaluator(self, name, evaluator): args = {'name': name, 'evaluator': evaluator, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/setCodeCellEvaluator', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = conn.read() return result == "true" def setCodeCellTags(self, name, tags): args = {'name': name, 'tags': tags, 'session': self.session_id} req = urllib.request.Request(self.core_url + '/rest/notebookctrl/setCodeCellTags', urllib.parse.urlencode(args).encode('utf8')) conn = self._beaker_url_opener.open(req) result = conn.read() return result == "true" def runByTag(self, tag): arguments = dict(target_name='beakerx.tag.run') comm = Comm(**arguments) msg = {'runByTag': tag} state = {'state': msg} comm.send(data=state, buffers=[]) def urlArg(self, argName): arguments = dict(target_name='beakerx.geturlarg') comm = Comm(**arguments) state = { 'name': 'URL_ARG', 'arg_name': argName } data = { 'state': state, 'url': self._url, 'type': 'python' } comm.send(data=data, buffers=[]) data = self._queue.get() params = json.loads(data) return params['argValue'] def __setattr__(self, name, value): if 'session_id' == name: self.__dict__['session_id'] = value return if '_comm' == name: self.__dict__['_comm'] = value return if '_url' == name: self.__dict__['_url'] = value return if '_queue' == name: self.__dict__['_queue'] = value return if '_server' == name: self.__dict__['_server'] = value return return self.set(name, value) def __getattr__(self, name): if '_comm' == name: return self.__dict__['_comm'] if '_url' == name: return self.__dict__['_url'] if '_queue' == name: return self.__dict__['_queue'] if '_server' == name: return self.__dict__['_server'] return self.get(name) def __contains__(self, name): return self.isDefined(name) def __delattr__(self, name): return self.unset(name)
class Widget(LoggingHasTraits): #------------------------------------------------------------------------- # Class attributes #------------------------------------------------------------------------- _widget_construction_callback = None # widgets is a dictionary of all active widget objects widgets = {} # widget_types is a registry of widgets by module, version, and name: widget_types = WidgetRegistry() @classmethod def close_all(cls): for widget in list(cls.widgets.values()): widget.close() @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.""" version = msg.get('metadata', {}).get('version', '') if version.split('.')[0] != PROTOCOL_VERSION_MAJOR: raise ValueError("Incompatible widget protocol versions: received version %r, expected version %r"%(version, __protocol_version__)) data = msg['content']['data'] state = data['state'] # Find the widget class to instantiate in the registered widgets widget_class = Widget.widget_types.get(state['_model_module'], state['_model_module_version'], state['_model_name'], state['_view_module'], state['_view_module_version'], state['_view_name']) widget = widget_class(comm=comm) if 'buffer_paths' in data: _put_buffers(state, data['buffer_paths'], msg['buffers']) widget.set_state(state) @staticmethod def get_manager_state(drop_defaults=False, widgets=None): """Returns the full state for a widget manager for embedding :param drop_defaults: when True, it will not include default value :param widgets: list with widgets to include in the state (or all widgets when None) :return: """ state = {} if widgets is None: widgets = Widget.widgets.values() for widget in widgets: state[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults) return {'version_major': 2, 'version_minor': 0, 'state': state} def _get_embed_state(self, drop_defaults=False): state = { 'model_name': self._model_name, 'model_module': self._model_module, 'model_module_version': self._model_module_version } model_state, buffer_paths, buffers = _remove_buffers(self.get_state(drop_defaults=drop_defaults)) state['state'] = model_state if len(buffers) > 0: state['buffers'] = [{'encoding': 'base64', 'path': p, 'data': standard_b64encode(d).decode('ascii')} for p, d in zip(buffer_paths, buffers)] return state def get_view_spec(self): return dict(version_major=2, version_minor=0, model_id=self._model_id) #------------------------------------------------------------------------- # Traits #------------------------------------------------------------------------- _model_name = Unicode('WidgetModel', help="Name of the model.", read_only=True).tag(sync=True) _model_module = Unicode('@jupyter-widgets/base', help="The namespace for the model.", read_only=True).tag(sync=True) _model_module_version = Unicode(__jupyter_widgets_base_version__, help="A semver requirement for namespace version containing the model.", read_only=True).tag(sync=True) _view_name = Unicode(None, allow_none=True, help="Name of the view.").tag(sync=True) _view_module = Unicode(None, allow_none=True, help="The namespace for the view.").tag(sync=True) _view_module_version = Unicode('', help="A semver requirement for the namespace version containing the view.").tag(sync=True) _view_count = Int(None, allow_none=True, help="EXPERIMENTAL: The number of views of the model displayed in the frontend. This attribute is experimental and may change or be removed in the future. None signifies that views will not be tracked. Set this to 0 to start tracking view creation/deletion.").tag(sync=True) comm = Instance('ipykernel.comm.Comm', allow_none=True) keys = List(help="The traits which are synced.") @default('keys') def _default_keys(self): return [name for name in self.traits(sync=True)] _property_lock = Dict() _holding_sync = False _states_to_send = Set() _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: state, buffer_paths, buffers = _remove_buffers(self.get_state()) args = dict(target_name='jupyter.widget', data={'state': state, 'buffer_paths': buffer_paths}, buffers=buffers, metadata={'version': __protocol_version__} ) if self._model_id is not None: args['comm_id'] = self._model_id self.comm = Comm(**args) @observe('comm') def _comm_changed(self, change): """Called when the comm is changed.""" if change['new'] is None: return self._model_id = self.model_id self.comm.on_msg(self._handle_msg) Widget.widgets[self.model_id] = self @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 self._ipython_display_ = None def send_state(self, key=None): """Sends the widget state, or a piece of it, to the front-end, if it exists. Parameters ---------- key : unicode, or iterable (optional) A single property's name or iterable of property names to sync with the front-end. """ state = self.get_state(key=key) if len(state) > 0: if self._property_lock: # we need to keep this dict up to date with the front-end values for name, value in state.items(): if name in self._property_lock: self._property_lock[name] = value state, buffer_paths, buffers = _remove_buffers(state) msg = {'method': 'update', 'state': state, 'buffer_paths': buffer_paths} self._send(msg, buffers=buffers) def get_state(self, key=None, drop_defaults=False): """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. Returns ------- state : dict of states metadata : dict metadata for each field: {key: metadata} """ 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 = {} traits = self.traits() for k in keys: to_json = self.trait_metadata(k, 'to_json', self._trait_to_json) value = to_json(getattr(self, k), self) if not PY3 and isinstance(traits[k], Bytes) and isinstance(value, bytes): value = memoryview(value) if not drop_defaults or not self._compare(value, traits[k].default_value): state[k] = value return state def _is_numpy(self, x): return x.__class__.__name__ == 'ndarray' and x.__class__.__module__ == 'numpy' def _compare(self, a, b): if self._is_numpy(a) or self._is_numpy(b): import numpy as np return np.array_equal(a, b) else: return a == b def set_state(self, sync_data): """Called when a state is received from the front-end.""" # The order of these context managers is important. Properties must # be locked when the hold_trait_notification context manager is # released and notifications are fired. with self._lock_property(**sync_data), self.hold_trait_notifications(): for name in sync_data: if name in self.keys: from_json = self.trait_metadata(name, 'from_json', self._trait_from_json) self.set_trait(name, from_json(sync_data[name], self)) def send(self, content, buffers=None): """Sends a custom msg to the widget model in the front-end. Parameters ---------- content : dict Content of the message to send. buffers : list of binary buffers Binary buffers to send with message """ self._send({"method": "custom", "content": content}, buffers=buffers) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- callback: callable callback will be passed three arguments when a message arrives:: callback(widget, content, buffers) 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) def add_traits(self, **traits): """Dynamically add trait attributes to the Widget.""" super(Widget, self).add_traits(**traits) for name, trait in traits.items(): if trait.get_metadata('sync'): self.keys.append(name) self.send_state(name) def notify_change(self, change): """Called when a property has changed.""" # Send the state to the frontend before the user-registered callbacks # are called. name = change['name'] if self.comm is not None and self.comm.kernel is not None: # Make sure this isn't information that the front-end just sent us. if name in self.keys and self._should_send_property(name, getattr(self, name)): # Send new state to front-end self.send_state(key=name) super(Widget, self).notify_change(change) def __repr__(self): return self._gen_repr_from_keys(self._repr_keys()) #------------------------------------------------------------------------- # Support methods #------------------------------------------------------------------------- @contextmanager def _lock_property(self, **properties): """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 = properties try: yield finally: self._property_lock = {} @contextmanager def hold_sync(self): """Hold syncing any state until the outermost context manager exits""" if self._holding_sync is True: yield else: try: self._holding_sync = True yield finally: self._holding_sync = False 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 in self._property_lock: # model_state, buffer_paths, buffers split_value = _remove_buffers({ key: to_json(value, self)}) split_lock = _remove_buffers({ key: self._property_lock[key]}) # A roundtrip conversion through json in the comparison takes care of # idiosyncracies of how python data structures map to json, for example # tuples get converted to lists. if (jsonloads(jsondumps(split_value[0])) == split_lock[0] and split_value[1] == split_lock[1] and _buffer_list_equal(split_value[2], split_lock[2])): return False if self._holding_sync: 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'] if method == 'update': if 'state' in data: state = data['state'] if 'buffer_paths' in data: _put_buffers(state, data['buffer_paths'], msg['buffers']) self.set_state(state) # 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'], msg['buffers']) # Catch remainder. else: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) def _handle_custom_msg(self, content, buffers): """Called when a custom msg is received.""" self._msg_callbacks(self, content, buffers) def _handle_displayed(self, **kwargs): """Called when a view has been displayed for this widget instance""" self._display_callbacks(self, **kwargs) @staticmethod def _trait_to_json(x, self): """Convert a trait value to json.""" return x @staticmethod def _trait_from_json(x, self): """Convert json values to objects.""" return x def _ipython_display_(self, **kwargs): """Called when `IPython.display.display` is called on the widget.""" plaintext = repr(self) if len(plaintext) > 110: plaintext = plaintext[:110] + '…' data = { 'text/plain': plaintext, } if self._view_name is not None: # The 'application/vnd.jupyter.widget-view+json' mimetype has not been registered yet. # See the registration process and naming convention at # http://tools.ietf.org/html/rfc6838 # and the currently registered mimetypes at # http://www.iana.org/assignments/media-types/media-types.xhtml. data['application/vnd.jupyter.widget-view+json'] = { 'version_major': 2, 'version_minor': 0, 'model_id': self._model_id } display(data, raw=True) if self._view_name is not None: self._handle_displayed(**kwargs) def _send(self, msg, buffers=None): """Sends a message to the model in the front-end.""" if self.comm is not None and self.comm.kernel is not None: self.comm.send(data=msg, buffers=buffers) def _repr_keys(self): traits = self.traits() for key in sorted(self.keys): # Exclude traits that start with an underscore if key[0] == '_': continue # Exclude traits who are equal to their default value value = getattr(self, key) trait = traits[key] if self._compare(value, trait.default_value): continue elif (isinstance(trait, (Container, Dict)) and trait.default_value == Undefined and (value is None or len(value) == 0)): # Empty container, and dynamic default will be empty continue yield key def _gen_repr_from_keys(self, keys): class_name = self.__class__.__name__ signature = ', '.join( '%s=%r' % (key, getattr(self, key)) for key in keys ) return '%s(%s)' % (class_name, signature)
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(str(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.""", sync=True) _model_name = Unicode('WidgetModel', help="""Name of the backbone model registered in the front-end to create and sync this widget with.""", sync=True) _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('ipykernel.comm.Comm', allow_none=True) 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 = Dict() _holding_sync = False _states_to_send = Set() _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=self.get_state()) 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 @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. """ state = self.get_state(key=key) buffer_keys, buffers = [], [] for k, v in state.items(): if isinstance(v, memoryview): state.pop(k) buffers.append(v) buffer_keys.append(k) msg = {'method': 'update', 'state': state, 'buffers': buffer_keys} self._send(msg, buffers=buffers) 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. Returns ------- state : dict of states metadata : dict metadata for each field: {key: metadata} """ 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: to_json = self.trait_metadata(k, 'to_json', self._trait_to_json) state[k] = to_json(getattr(self, k), self) return state def set_state(self, sync_data): """Called when a state is received from the front-end.""" # The order of these context managers is important. Properties must # be locked when the hold_trait_notification context manager is # released and notifications are fired. with self._lock_property(**sync_data), self.hold_trait_notifications(): for name in sync_data: if name in self.keys: from_json = self.trait_metadata(name, 'from_json', self._trait_from_json) # traitlets < 4.1 don't support read-only attributes if hasattr(self, 'set_trait'): self.set_trait(name, from_json(sync_data[name], self)) else: setattr(self, name, from_json(sync_data[name], self)) def send(self, content, buffers=None): """Sends a custom msg to the widget model in the front-end. Parameters ---------- content : dict Content of the message to send. buffers : list of binary buffers Binary buffers to send with message """ self._send({"method": "custom", "content": content}, buffers=buffers) def on_msg(self, callback, remove=False): """(Un)Register a custom msg receive callback. Parameters ---------- callback: callable callback will be passed three arguments when a message arrives:: callback(widget, content, buffers) 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) def add_traits(self, **traits): """Dynamically add trait attributes to the Widget.""" super(Widget, self).add_traits(**traits) for name, trait in traits.items(): if trait.get_metadata('sync'): self.keys.append(name) self.send_state(name) #------------------------------------------------------------------------- # Support methods #------------------------------------------------------------------------- @contextmanager def _lock_property(self, **properties): """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 = properties try: yield finally: self._property_lock = {} @contextmanager def hold_sync(self): """Hold syncing any state until the outermost context manager exits""" if self._holding_sync is True: yield else: try: self._holding_sync = True yield finally: self._holding_sync = False 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 in self._property_lock and to_json(value, self) == self._property_lock[key]): return False elif self._holding_sync: 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: # get binary buffers too sync_data = data['sync_data'] for i,k in enumerate(data.get('buffer_keys', [])): sync_data[k] = msg['buffers'][i] 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'], msg['buffers']) # Catch remainder. else: self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method) def _handle_custom_msg(self, content, buffers): """Called when a custom msg is received.""" self._msg_callbacks(self, content, buffers) 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) @staticmethod def _trait_to_json(x, self): """Convert a trait value to json.""" return x @staticmethod def _trait_from_json(x, self): """Convert json values to objects.""" 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, buffers=None): """Sends a message to the model in the front-end.""" self.comm.send(data=msg, buffers=buffers)