1"""Tornado handlers for the sessions web service. 2 3Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#sessions-api 4""" 5# Copyright (c) Jupyter Development Team. 6# Distributed under the terms of the Modified BSD License. 7import asyncio 8import json 9 10try: 11 from jupyter_client.jsonutil import json_default 12except ImportError: 13 from jupyter_client.jsonutil import date_default as json_default 14from jupyter_client.kernelspec import NoSuchKernel 15from tornado import web 16 17from ...base.handlers import APIHandler 18from jupyter_server.utils import ensure_async 19from jupyter_server.utils import url_path_join 20 21 22class SessionRootHandler(APIHandler): 23 @web.authenticated 24 async def get(self): 25 # Return a list of running sessions 26 sm = self.session_manager 27 sessions = await ensure_async(sm.list_sessions()) 28 self.finish(json.dumps(sessions, default=json_default)) 29 30 @web.authenticated 31 async def post(self): 32 # Creates a new session 33 # (unless a session already exists for the named session) 34 sm = self.session_manager 35 36 model = self.get_json_body() 37 if model is None: 38 raise web.HTTPError(400, "No JSON data provided") 39 40 if "notebook" in model and "path" in model["notebook"]: 41 self.log.warning("Sessions API changed, see updated swagger docs") 42 model["path"] = model["notebook"]["path"] 43 model["type"] = "notebook" 44 45 try: 46 path = model["path"] 47 except KeyError as e: 48 raise web.HTTPError(400, "Missing field in JSON data: path") from e 49 50 try: 51 mtype = model["type"] 52 except KeyError as e: 53 raise web.HTTPError(400, "Missing field in JSON data: type") from e 54 55 name = model.get("name", None) 56 kernel = model.get("kernel", {}) 57 kernel_name = kernel.get("name", None) 58 kernel_id = kernel.get("id", None) 59 60 if not kernel_id and not kernel_name: 61 self.log.debug("No kernel specified, using default kernel") 62 kernel_name = None 63 64 exists = await ensure_async(sm.session_exists(path=path)) 65 if exists: 66 model = await sm.get_session(path=path) 67 else: 68 try: 69 model = await sm.create_session( 70 path=path, kernel_name=kernel_name, kernel_id=kernel_id, name=name, type=mtype 71 ) 72 except NoSuchKernel: 73 msg = ( 74 "The '%s' kernel is not available. Please pick another " 75 "suitable kernel instead, or install that kernel." % kernel_name 76 ) 77 status_msg = "%s not found" % kernel_name 78 self.log.warning("Kernel not found: %s" % kernel_name) 79 self.set_status(501) 80 self.finish(json.dumps(dict(message=msg, short_message=status_msg))) 81 return 82 except Exception as e: 83 raise web.HTTPError(500, str(e)) from e 84 85 location = url_path_join(self.base_url, "api", "sessions", model["id"]) 86 self.set_header("Location", location) 87 self.set_status(201) 88 self.finish(json.dumps(model, default=json_default)) 89 90 91class SessionHandler(APIHandler): 92 @web.authenticated 93 async def get(self, session_id): 94 # Returns the JSON model for a single session 95 sm = self.session_manager 96 model = await sm.get_session(session_id=session_id) 97 self.finish(json.dumps(model, default=json_default)) 98 99 @web.authenticated 100 async def patch(self, session_id): 101 """Patch updates sessions: 102 103 - path updates session to track renamed paths 104 - kernel.name starts a new kernel with a given kernelspec 105 """ 106 sm = self.session_manager 107 km = self.kernel_manager 108 model = self.get_json_body() 109 if model is None: 110 raise web.HTTPError(400, "No JSON data provided") 111 112 # get the previous session model 113 before = await sm.get_session(session_id=session_id) 114 115 changes = {} 116 if "notebook" in model and "path" in model["notebook"]: 117 self.log.warning("Sessions API changed, see updated swagger docs") 118 model["path"] = model["notebook"]["path"] 119 model["type"] = "notebook" 120 if "path" in model: 121 changes["path"] = model["path"] 122 if "name" in model: 123 changes["name"] = model["name"] 124 if "type" in model: 125 changes["type"] = model["type"] 126 if "kernel" in model: 127 # Kernel id takes precedence over name. 128 if model["kernel"].get("id") is not None: 129 kernel_id = model["kernel"]["id"] 130 if kernel_id not in km: 131 raise web.HTTPError(400, "No such kernel: %s" % kernel_id) 132 changes["kernel_id"] = kernel_id 133 elif model["kernel"].get("name") is not None: 134 kernel_name = model["kernel"]["name"] 135 kernel_id = await sm.start_kernel_for_session( 136 session_id, 137 kernel_name=kernel_name, 138 name=before["name"], 139 path=before["path"], 140 type=before["type"], 141 ) 142 changes["kernel_id"] = kernel_id 143 144 await sm.update_session(session_id, **changes) 145 model = await sm.get_session(session_id=session_id) 146 147 if model["kernel"]["id"] != before["kernel"]["id"]: 148 # kernel_id changed because we got a new kernel 149 # shutdown the old one 150 fut = asyncio.ensure_future(ensure_async(km.shutdown_kernel(before["kernel"]["id"]))) 151 # If we are not using pending kernels, wait for the kernel to shut down 152 if not getattr(km, "use_pending_kernels", None): 153 await fut 154 self.finish(json.dumps(model, default=json_default)) 155 156 @web.authenticated 157 async def delete(self, session_id): 158 # Deletes the session with given session_id 159 sm = self.session_manager 160 try: 161 await sm.delete_session(session_id) 162 except KeyError as e: 163 # the kernel was deleted but the session wasn't! 164 raise web.HTTPError(410, "Kernel deleted before session") from e 165 self.set_status(204) 166 self.finish() 167 168 169# ----------------------------------------------------------------------------- 170# URL to handler mappings 171# ----------------------------------------------------------------------------- 172 173_session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)" 174 175default_handlers = [ 176 (r"/api/sessions/%s" % _session_id_regex, SessionHandler), 177 (r"/api/sessions", SessionRootHandler), 178] 179