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