##// END OF EJS Templates
escape URLs in Location headers
MinRK -
Show More
@@ -1,195 +1,195 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import logging
19 import logging
20 from tornado import web
20 from tornado import web
21
21
22 from zmq.utils import jsonapi
22 from zmq.utils import jsonapi
23
23
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join
25 from IPython.html.utils import url_path_join, url_escape
26
26
27 from ...base.handlers import IPythonHandler, json_errors
27 from ...base.handlers import IPythonHandler, json_errors
28 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
28 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Kernel handlers
31 # Kernel handlers
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34
34
35 class MainKernelHandler(IPythonHandler):
35 class MainKernelHandler(IPythonHandler):
36
36
37 @web.authenticated
37 @web.authenticated
38 @json_errors
38 @json_errors
39 def get(self):
39 def get(self):
40 km = self.kernel_manager
40 km = self.kernel_manager
41 self.finish(jsonapi.dumps(km.list_kernels(self.ws_url)))
41 self.finish(jsonapi.dumps(km.list_kernels(self.ws_url)))
42
42
43 @web.authenticated
43 @web.authenticated
44 @json_errors
44 @json_errors
45 def post(self):
45 def post(self):
46 km = self.kernel_manager
46 km = self.kernel_manager
47 kernel_id = km.start_kernel()
47 kernel_id = km.start_kernel()
48 model = km.kernel_model(kernel_id, self.ws_url)
48 model = km.kernel_model(kernel_id, self.ws_url)
49 location = url_path_join(self.base_kernel_url, 'api', 'kernels', kernel_id)
49 location = url_path_join(self.base_kernel_url, 'api', 'kernels', kernel_id)
50 self.set_header('Location', location)
50 self.set_header('Location', url_escape(location))
51 self.set_status(201)
51 self.set_status(201)
52 self.finish(jsonapi.dumps(model))
52 self.finish(jsonapi.dumps(model))
53
53
54
54
55 class KernelHandler(IPythonHandler):
55 class KernelHandler(IPythonHandler):
56
56
57 SUPPORTED_METHODS = ('DELETE', 'GET')
57 SUPPORTED_METHODS = ('DELETE', 'GET')
58
58
59 @web.authenticated
59 @web.authenticated
60 @json_errors
60 @json_errors
61 def get(self, kernel_id):
61 def get(self, kernel_id):
62 km = self.kernel_manager
62 km = self.kernel_manager
63 km._check_kernel_id(kernel_id)
63 km._check_kernel_id(kernel_id)
64 model = km.kernel_model(kernel_id, self.ws_url)
64 model = km.kernel_model(kernel_id, self.ws_url)
65 self.finish(jsonapi.dumps(model))
65 self.finish(jsonapi.dumps(model))
66
66
67 @web.authenticated
67 @web.authenticated
68 @json_errors
68 @json_errors
69 def delete(self, kernel_id):
69 def delete(self, kernel_id):
70 km = self.kernel_manager
70 km = self.kernel_manager
71 km.shutdown_kernel(kernel_id)
71 km.shutdown_kernel(kernel_id)
72 self.set_status(204)
72 self.set_status(204)
73 self.finish()
73 self.finish()
74
74
75
75
76 class KernelActionHandler(IPythonHandler):
76 class KernelActionHandler(IPythonHandler):
77
77
78 @web.authenticated
78 @web.authenticated
79 @json_errors
79 @json_errors
80 def post(self, kernel_id, action):
80 def post(self, kernel_id, action):
81 km = self.kernel_manager
81 km = self.kernel_manager
82 if action == 'interrupt':
82 if action == 'interrupt':
83 km.interrupt_kernel(kernel_id)
83 km.interrupt_kernel(kernel_id)
84 self.set_status(204)
84 self.set_status(204)
85 if action == 'restart':
85 if action == 'restart':
86 km.restart_kernel(kernel_id)
86 km.restart_kernel(kernel_id)
87 model = km.kernel_model(kernel_id, self.ws_url)
87 model = km.kernel_model(kernel_id, self.ws_url)
88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_kernel_url, kernel_id))
89 self.write(jsonapi.dumps(model))
89 self.write(jsonapi.dumps(model))
90 self.finish()
90 self.finish()
91
91
92
92
93 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
93 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
94
94
95 def create_stream(self):
95 def create_stream(self):
96 km = self.kernel_manager
96 km = self.kernel_manager
97 meth = getattr(km, 'connect_%s' % self.channel)
97 meth = getattr(km, 'connect_%s' % self.channel)
98 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
98 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
99
99
100 def initialize(self, *args, **kwargs):
100 def initialize(self, *args, **kwargs):
101 self.zmq_stream = None
101 self.zmq_stream = None
102
102
103 def on_first_message(self, msg):
103 def on_first_message(self, msg):
104 try:
104 try:
105 super(ZMQChannelHandler, self).on_first_message(msg)
105 super(ZMQChannelHandler, self).on_first_message(msg)
106 except web.HTTPError:
106 except web.HTTPError:
107 self.close()
107 self.close()
108 return
108 return
109 try:
109 try:
110 self.create_stream()
110 self.create_stream()
111 except web.HTTPError:
111 except web.HTTPError:
112 # WebSockets don't response to traditional error codes so we
112 # WebSockets don't response to traditional error codes so we
113 # close the connection.
113 # close the connection.
114 if not self.stream.closed():
114 if not self.stream.closed():
115 self.stream.close()
115 self.stream.close()
116 self.close()
116 self.close()
117 else:
117 else:
118 self.zmq_stream.on_recv(self._on_zmq_reply)
118 self.zmq_stream.on_recv(self._on_zmq_reply)
119
119
120 def on_message(self, msg):
120 def on_message(self, msg):
121 msg = jsonapi.loads(msg)
121 msg = jsonapi.loads(msg)
122 self.session.send(self.zmq_stream, msg)
122 self.session.send(self.zmq_stream, msg)
123
123
124 def on_close(self):
124 def on_close(self):
125 # This method can be called twice, once by self.kernel_died and once
125 # This method can be called twice, once by self.kernel_died and once
126 # from the WebSocket close event. If the WebSocket connection is
126 # from the WebSocket close event. If the WebSocket connection is
127 # closed before the ZMQ streams are setup, they could be None.
127 # closed before the ZMQ streams are setup, they could be None.
128 if self.zmq_stream is not None and not self.zmq_stream.closed():
128 if self.zmq_stream is not None and not self.zmq_stream.closed():
129 self.zmq_stream.on_recv(None)
129 self.zmq_stream.on_recv(None)
130 self.zmq_stream.close()
130 self.zmq_stream.close()
131
131
132
132
133 class IOPubHandler(ZMQChannelHandler):
133 class IOPubHandler(ZMQChannelHandler):
134 channel = 'iopub'
134 channel = 'iopub'
135
135
136 def create_stream(self):
136 def create_stream(self):
137 super(IOPubHandler, self).create_stream()
137 super(IOPubHandler, self).create_stream()
138 km = self.kernel_manager
138 km = self.kernel_manager
139 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
139 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
140 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
140 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
141
141
142 def on_close(self):
142 def on_close(self):
143 km = self.kernel_manager
143 km = self.kernel_manager
144 if self.kernel_id in km:
144 if self.kernel_id in km:
145 km.remove_restart_callback(
145 km.remove_restart_callback(
146 self.kernel_id, self.on_kernel_restarted,
146 self.kernel_id, self.on_kernel_restarted,
147 )
147 )
148 km.remove_restart_callback(
148 km.remove_restart_callback(
149 self.kernel_id, self.on_restart_failed, 'dead',
149 self.kernel_id, self.on_restart_failed, 'dead',
150 )
150 )
151 super(IOPubHandler, self).on_close()
151 super(IOPubHandler, self).on_close()
152
152
153 def _send_status_message(self, status):
153 def _send_status_message(self, status):
154 msg = self.session.msg("status",
154 msg = self.session.msg("status",
155 {'execution_state': status}
155 {'execution_state': status}
156 )
156 )
157 self.write_message(jsonapi.dumps(msg, default=date_default))
157 self.write_message(jsonapi.dumps(msg, default=date_default))
158
158
159 def on_kernel_restarted(self):
159 def on_kernel_restarted(self):
160 logging.warn("kernel %s restarted", self.kernel_id)
160 logging.warn("kernel %s restarted", self.kernel_id)
161 self._send_status_message('restarting')
161 self._send_status_message('restarting')
162
162
163 def on_restart_failed(self):
163 def on_restart_failed(self):
164 logging.error("kernel %s restarted failed!", self.kernel_id)
164 logging.error("kernel %s restarted failed!", self.kernel_id)
165 self._send_status_message('dead')
165 self._send_status_message('dead')
166
166
167 def on_message(self, msg):
167 def on_message(self, msg):
168 """IOPub messages make no sense"""
168 """IOPub messages make no sense"""
169 pass
169 pass
170
170
171
171
172 class ShellHandler(ZMQChannelHandler):
172 class ShellHandler(ZMQChannelHandler):
173 channel = 'shell'
173 channel = 'shell'
174
174
175
175
176 class StdinHandler(ZMQChannelHandler):
176 class StdinHandler(ZMQChannelHandler):
177 channel = 'stdin'
177 channel = 'stdin'
178
178
179
179
180 #-----------------------------------------------------------------------------
180 #-----------------------------------------------------------------------------
181 # URL to handler mappings
181 # URL to handler mappings
182 #-----------------------------------------------------------------------------
182 #-----------------------------------------------------------------------------
183
183
184
184
185 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
185 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
186 _kernel_action_regex = r"(?P<action>restart|interrupt)"
186 _kernel_action_regex = r"(?P<action>restart|interrupt)"
187
187
188 default_handlers = [
188 default_handlers = [
189 (r"/api/kernels", MainKernelHandler),
189 (r"/api/kernels", MainKernelHandler),
190 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
190 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
191 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
191 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
192 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
192 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
193 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
193 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
194 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
194 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
195 ]
195 ]
@@ -1,271 +1,273 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the notebooks web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from IPython.html.utils import url_path_join
23 from IPython.html.utils import url_path_join, url_escape
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25
25
26 from IPython.html.base.handlers import IPythonHandler, json_errors
26 from IPython.html.base.handlers import IPythonHandler, json_errors
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Notebook web service handlers
29 # Notebook web service handlers
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32
32
33 class NotebookHandler(IPythonHandler):
33 class NotebookHandler(IPythonHandler):
34
34
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
35 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36
36
37 def notebook_location(self, name, path=''):
37 def notebook_location(self, name, path=''):
38 """Return the full URL location of a notebook based.
38 """Return the full URL location of a notebook based.
39
39
40 Parameters
40 Parameters
41 ----------
41 ----------
42 name : unicode
42 name : unicode
43 The base name of the notebook, such as "foo.ipynb".
43 The base name of the notebook, such as "foo.ipynb".
44 path : unicode
44 path : unicode
45 The URL path of the notebook.
45 The URL path of the notebook.
46 """
46 """
47 return url_path_join(self.base_project_url, 'api', 'notebooks', path, name)
47 return url_escape(url_path_join(
48 self.base_project_url, 'api', 'notebooks', path, name
49 ))
48
50
49 def _finish_model(self, model, location=True):
51 def _finish_model(self, model, location=True):
50 """Finish a JSON request with a model, setting relevant headers, etc."""
52 """Finish a JSON request with a model, setting relevant headers, etc."""
51 if location:
53 if location:
52 location = self.notebook_location(model['name'], model['path'])
54 location = self.notebook_location(model['name'], model['path'])
53 self.set_header('Location', location)
55 self.set_header('Location', location)
54 self.set_header('Last-Modified', model['last_modified'])
56 self.set_header('Last-Modified', model['last_modified'])
55 self.finish(json.dumps(model, default=date_default))
57 self.finish(json.dumps(model, default=date_default))
56
58
57 @web.authenticated
59 @web.authenticated
58 @json_errors
60 @json_errors
59 def get(self, path='', name=None):
61 def get(self, path='', name=None):
60 """Return a Notebook or list of notebooks.
62 """Return a Notebook or list of notebooks.
61
63
62 * GET with path and no notebook name lists notebooks in a directory
64 * GET with path and no notebook name lists notebooks in a directory
63 * GET with path and notebook name returns notebook JSON
65 * GET with path and notebook name returns notebook JSON
64 """
66 """
65 nbm = self.notebook_manager
67 nbm = self.notebook_manager
66 # Check to see if a notebook name was given
68 # Check to see if a notebook name was given
67 if name is None:
69 if name is None:
68 # List notebooks in 'path'
70 # List notebooks in 'path'
69 notebooks = nbm.list_notebooks(path)
71 notebooks = nbm.list_notebooks(path)
70 self.finish(json.dumps(notebooks, default=date_default))
72 self.finish(json.dumps(notebooks, default=date_default))
71 return
73 return
72 # get and return notebook representation
74 # get and return notebook representation
73 model = nbm.get_notebook_model(name, path)
75 model = nbm.get_notebook_model(name, path)
74 self._finish_model(model, location=False)
76 self._finish_model(model, location=False)
75
77
76 @web.authenticated
78 @web.authenticated
77 @json_errors
79 @json_errors
78 def patch(self, path='', name=None):
80 def patch(self, path='', name=None):
79 """PATCH renames a notebook without re-uploading content."""
81 """PATCH renames a notebook without re-uploading content."""
80 nbm = self.notebook_manager
82 nbm = self.notebook_manager
81 if name is None:
83 if name is None:
82 raise web.HTTPError(400, u'Notebook name missing')
84 raise web.HTTPError(400, u'Notebook name missing')
83 model = self.get_json_body()
85 model = self.get_json_body()
84 if model is None:
86 if model is None:
85 raise web.HTTPError(400, u'JSON body missing')
87 raise web.HTTPError(400, u'JSON body missing')
86 model = nbm.update_notebook_model(model, name, path)
88 model = nbm.update_notebook_model(model, name, path)
87 self._finish_model(model)
89 self._finish_model(model)
88
90
89 def _copy_notebook(self, copy_from, path, copy_to=None):
91 def _copy_notebook(self, copy_from, path, copy_to=None):
90 """Copy a notebook in path, optionally specifying the new name.
92 """Copy a notebook in path, optionally specifying the new name.
91
93
92 Only support copying within the same directory.
94 Only support copying within the same directory.
93 """
95 """
94 self.log.info(u"Copying notebook from %s/%s to %s/%s",
96 self.log.info(u"Copying notebook from %s/%s to %s/%s",
95 path, copy_from,
97 path, copy_from,
96 path, copy_to or '',
98 path, copy_to or '',
97 )
99 )
98 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
100 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
99 self.set_status(201)
101 self.set_status(201)
100 self._finish_model(model)
102 self._finish_model(model)
101
103
102 def _upload_notebook(self, model, path, name=None):
104 def _upload_notebook(self, model, path, name=None):
103 """Upload a notebook
105 """Upload a notebook
104
106
105 If name specified, create it in path/name.
107 If name specified, create it in path/name.
106 """
108 """
107 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
109 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
108 if name:
110 if name:
109 model['name'] = name
111 model['name'] = name
110
112
111 model = self.notebook_manager.create_notebook_model(model, path)
113 model = self.notebook_manager.create_notebook_model(model, path)
112 self.set_status(201)
114 self.set_status(201)
113 self._finish_model(model)
115 self._finish_model(model)
114
116
115 def _create_empty_notebook(self, path, name=None):
117 def _create_empty_notebook(self, path, name=None):
116 """Create an empty notebook in path
118 """Create an empty notebook in path
117
119
118 If name specified, create it in path/name.
120 If name specified, create it in path/name.
119 """
121 """
120 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
122 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
121 model = {}
123 model = {}
122 if name:
124 if name:
123 model['name'] = name
125 model['name'] = name
124 model = self.notebook_manager.create_notebook_model(model, path=path)
126 model = self.notebook_manager.create_notebook_model(model, path=path)
125 self.set_status(201)
127 self.set_status(201)
126 self._finish_model(model)
128 self._finish_model(model)
127
129
128 def _save_notebook(self, model, path, name):
130 def _save_notebook(self, model, path, name):
129 """Save an existing notebook."""
131 """Save an existing notebook."""
130 self.log.info(u"Saving notebook at %s/%s", path, name)
132 self.log.info(u"Saving notebook at %s/%s", path, name)
131 model = self.notebook_manager.save_notebook_model(model, name, path)
133 model = self.notebook_manager.save_notebook_model(model, name, path)
132 if model['path'] != path.strip('/') or model['name'] != name:
134 if model['path'] != path.strip('/') or model['name'] != name:
133 # a rename happened, set Location header
135 # a rename happened, set Location header
134 location = True
136 location = True
135 else:
137 else:
136 location = False
138 location = False
137 self._finish_model(model, location)
139 self._finish_model(model, location)
138
140
139 @web.authenticated
141 @web.authenticated
140 @json_errors
142 @json_errors
141 def post(self, path='', name=None):
143 def post(self, path='', name=None):
142 """Create a new notebook in the specified path.
144 """Create a new notebook in the specified path.
143
145
144 POST creates new notebooks. The server always decides on the notebook name.
146 POST creates new notebooks. The server always decides on the notebook name.
145
147
146 POST /api/notebooks/path : new untitled notebook in path
148 POST /api/notebooks/path : new untitled notebook in path
147 If content specified, upload a notebook, otherwise start empty.
149 If content specified, upload a notebook, otherwise start empty.
148 POST /api/notebooks/path?copy=OtherNotebook.ipynb : new copy of OtherNotebook in path
150 POST /api/notebooks/path?copy=OtherNotebook.ipynb : new copy of OtherNotebook in path
149 """
151 """
150
152
151 model = self.get_json_body()
153 model = self.get_json_body()
152 copy = self.get_argument("copy", default="")
154 copy = self.get_argument("copy", default="")
153 if name is not None:
155 if name is not None:
154 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names")
156 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names")
155
157
156 if copy:
158 if copy:
157 self._copy_notebook(copy, path)
159 self._copy_notebook(copy, path)
158 elif model:
160 elif model:
159 self._upload_notebook(model, path)
161 self._upload_notebook(model, path)
160 else:
162 else:
161 self._create_empty_notebook(path)
163 self._create_empty_notebook(path)
162
164
163 @web.authenticated
165 @web.authenticated
164 @json_errors
166 @json_errors
165 def put(self, path='', name=None):
167 def put(self, path='', name=None):
166 """Saves the notebook in the location specified by name and path.
168 """Saves the notebook in the location specified by name and path.
167
169
168 PUT /api/notebooks/path/Name.ipynb : Save notebook at path/Name.ipynb
170 PUT /api/notebooks/path/Name.ipynb : Save notebook at path/Name.ipynb
169 Notebook structure is specified in `content` key of JSON request body.
171 Notebook structure is specified in `content` key of JSON request body.
170 If content is not specified, create a new empty notebook.
172 If content is not specified, create a new empty notebook.
171 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb : copy OtherNotebook to Name
173 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb : copy OtherNotebook to Name
172
174
173 POST and PUT are basically the same. The only difference:
175 POST and PUT are basically the same. The only difference:
174
176
175 - with POST, server always picks the name, with PUT the requester does
177 - with POST, server always picks the name, with PUT the requester does
176 """
178 """
177 if name is None:
179 if name is None:
178 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
180 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
179 model = self.get_json_body()
181 model = self.get_json_body()
180 copy = self.get_argument("copy", default="")
182 copy = self.get_argument("copy", default="")
181 if copy:
183 if copy:
182 if model is not None:
184 if model is not None:
183 raise web.HTTPError(400)
185 raise web.HTTPError(400)
184 self._copy_notebook(copy, path, name)
186 self._copy_notebook(copy, path, name)
185 elif model:
187 elif model:
186 if self.notebook_manager.notebook_exists(name, path):
188 if self.notebook_manager.notebook_exists(name, path):
187 self._save_notebook(model, path, name)
189 self._save_notebook(model, path, name)
188 else:
190 else:
189 self._upload_notebook(model, path, name)
191 self._upload_notebook(model, path, name)
190 else:
192 else:
191 self._create_empty_notebook(path, name)
193 self._create_empty_notebook(path, name)
192
194
193 @web.authenticated
195 @web.authenticated
194 @json_errors
196 @json_errors
195 def delete(self, path='', name=None):
197 def delete(self, path='', name=None):
196 """delete the notebook in the given notebook path"""
198 """delete the notebook in the given notebook path"""
197 nbm = self.notebook_manager
199 nbm = self.notebook_manager
198 nbm.delete_notebook_model(name, path)
200 nbm.delete_notebook_model(name, path)
199 self.set_status(204)
201 self.set_status(204)
200 self.finish()
202 self.finish()
201
203
202
204
203 class NotebookCheckpointsHandler(IPythonHandler):
205 class NotebookCheckpointsHandler(IPythonHandler):
204
206
205 SUPPORTED_METHODS = ('GET', 'POST')
207 SUPPORTED_METHODS = ('GET', 'POST')
206
208
207 @web.authenticated
209 @web.authenticated
208 @json_errors
210 @json_errors
209 def get(self, path='', name=None):
211 def get(self, path='', name=None):
210 """get lists checkpoints for a notebook"""
212 """get lists checkpoints for a notebook"""
211 nbm = self.notebook_manager
213 nbm = self.notebook_manager
212 checkpoints = nbm.list_checkpoints(name, path)
214 checkpoints = nbm.list_checkpoints(name, path)
213 data = json.dumps(checkpoints, default=date_default)
215 data = json.dumps(checkpoints, default=date_default)
214 self.finish(data)
216 self.finish(data)
215
217
216 @web.authenticated
218 @web.authenticated
217 @json_errors
219 @json_errors
218 def post(self, path='', name=None):
220 def post(self, path='', name=None):
219 """post creates a new checkpoint"""
221 """post creates a new checkpoint"""
220 nbm = self.notebook_manager
222 nbm = self.notebook_manager
221 checkpoint = nbm.create_checkpoint(name, path)
223 checkpoint = nbm.create_checkpoint(name, path)
222 data = json.dumps(checkpoint, default=date_default)
224 data = json.dumps(checkpoint, default=date_default)
223 location = url_path_join(self.base_project_url, 'api/notebooks',
225 location = url_path_join(self.base_project_url, 'api/notebooks',
224 path, name, 'checkpoints', checkpoint['id'])
226 path, name, 'checkpoints', checkpoint['id'])
225 self.set_header('Location', location)
227 self.set_header('Location', url_escape(location))
226 self.set_status(201)
228 self.set_status(201)
227 self.finish(data)
229 self.finish(data)
228
230
229
231
230 class ModifyNotebookCheckpointsHandler(IPythonHandler):
232 class ModifyNotebookCheckpointsHandler(IPythonHandler):
231
233
232 SUPPORTED_METHODS = ('POST', 'DELETE')
234 SUPPORTED_METHODS = ('POST', 'DELETE')
233
235
234 @web.authenticated
236 @web.authenticated
235 @json_errors
237 @json_errors
236 def post(self, path, name, checkpoint_id):
238 def post(self, path, name, checkpoint_id):
237 """post restores a notebook from a checkpoint"""
239 """post restores a notebook from a checkpoint"""
238 nbm = self.notebook_manager
240 nbm = self.notebook_manager
239 nbm.restore_checkpoint(checkpoint_id, name, path)
241 nbm.restore_checkpoint(checkpoint_id, name, path)
240 self.set_status(204)
242 self.set_status(204)
241 self.finish()
243 self.finish()
242
244
243 @web.authenticated
245 @web.authenticated
244 @json_errors
246 @json_errors
245 def delete(self, path, name, checkpoint_id):
247 def delete(self, path, name, checkpoint_id):
246 """delete clears a checkpoint for a given notebook"""
248 """delete clears a checkpoint for a given notebook"""
247 nbm = self.notebook_manager
249 nbm = self.notebook_manager
248 nbm.delete_checkpoint(checkpoint_id, name, path)
250 nbm.delete_checkpoint(checkpoint_id, name, path)
249 self.set_status(204)
251 self.set_status(204)
250 self.finish()
252 self.finish()
251
253
252 #-----------------------------------------------------------------------------
254 #-----------------------------------------------------------------------------
253 # URL to handler mappings
255 # URL to handler mappings
254 #-----------------------------------------------------------------------------
256 #-----------------------------------------------------------------------------
255
257
256
258
257 _path_regex = r"(?P<path>(?:/.*)*)"
259 _path_regex = r"(?P<path>(?:/.*)*)"
258 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
260 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
259 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
261 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
260 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
262 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
261
263
262 default_handlers = [
264 default_handlers = [
263 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
265 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
264 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
266 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
265 ModifyNotebookCheckpointsHandler),
267 ModifyNotebookCheckpointsHandler),
266 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
268 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
267 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
269 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
268 ]
270 ]
269
271
270
272
271
273
@@ -1,295 +1,295 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Test the notebooks webservice API."""
2 """Test the notebooks webservice API."""
3
3
4 import io
4 import io
5 import os
5 import os
6 import shutil
6 import shutil
7 from unicodedata import normalize
7 from unicodedata import normalize
8
8
9 from zmq.utils import jsonapi
9 from zmq.utils import jsonapi
10
10
11 pjoin = os.path.join
11 pjoin = os.path.join
12
12
13 import requests
13 import requests
14
14
15 from IPython.html.utils import url_path_join
15 from IPython.html.utils import url_path_join, url_escape
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
17 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 new_heading_cell, to_notebook_json)
18 new_heading_cell, to_notebook_json)
19 from IPython.utils import py3compat
19 from IPython.utils import py3compat
20 from IPython.utils.data import uniq_stable
20 from IPython.utils.data import uniq_stable
21
21
22
22
23 class NBAPI(object):
23 class NBAPI(object):
24 """Wrapper for notebook API calls."""
24 """Wrapper for notebook API calls."""
25 def __init__(self, base_url):
25 def __init__(self, base_url):
26 self.base_url = base_url
26 self.base_url = base_url
27
27
28 def _req(self, verb, path, body=None, params=None):
28 def _req(self, verb, path, body=None, params=None):
29 response = requests.request(verb,
29 response = requests.request(verb,
30 url_path_join(self.base_url, 'api/notebooks', path),
30 url_path_join(self.base_url, 'api/notebooks', path),
31 data=body,
31 data=body,
32 params=params,
32 params=params,
33 )
33 )
34 response.raise_for_status()
34 response.raise_for_status()
35 return response
35 return response
36
36
37 def list(self, path='/'):
37 def list(self, path='/'):
38 return self._req('GET', path)
38 return self._req('GET', path)
39
39
40 def read(self, name, path='/'):
40 def read(self, name, path='/'):
41 return self._req('GET', url_path_join(path, name))
41 return self._req('GET', url_path_join(path, name))
42
42
43 def create_untitled(self, path='/'):
43 def create_untitled(self, path='/'):
44 return self._req('POST', path)
44 return self._req('POST', path)
45
45
46 def upload_untitled(self, body, path='/'):
46 def upload_untitled(self, body, path='/'):
47 return self._req('POST', path, body)
47 return self._req('POST', path, body)
48
48
49 def copy_untitled(self, copy_from, path='/'):
49 def copy_untitled(self, copy_from, path='/'):
50 return self._req('POST', path, params={'copy':copy_from})
50 return self._req('POST', path, params={'copy':copy_from})
51
51
52 def create(self, name, path='/'):
52 def create(self, name, path='/'):
53 return self._req('PUT', url_path_join(path, name))
53 return self._req('PUT', url_path_join(path, name))
54
54
55 def upload(self, name, body, path='/'):
55 def upload(self, name, body, path='/'):
56 return self._req('PUT', url_path_join(path, name), body)
56 return self._req('PUT', url_path_join(path, name), body)
57
57
58 def copy(self, copy_from, copy_to, path='/'):
58 def copy(self, copy_from, copy_to, path='/'):
59 return self._req('PUT', url_path_join(path, copy_to), params={'copy':copy_from})
59 return self._req('PUT', url_path_join(path, copy_to), params={'copy':copy_from})
60
60
61 def save(self, name, body, path='/'):
61 def save(self, name, body, path='/'):
62 return self._req('PUT', url_path_join(path, name), body)
62 return self._req('PUT', url_path_join(path, name), body)
63
63
64 def delete(self, name, path='/'):
64 def delete(self, name, path='/'):
65 return self._req('DELETE', url_path_join(path, name))
65 return self._req('DELETE', url_path_join(path, name))
66
66
67 def rename(self, name, path, new_name):
67 def rename(self, name, path, new_name):
68 body = jsonapi.dumps({'name': new_name})
68 body = jsonapi.dumps({'name': new_name})
69 return self._req('PATCH', url_path_join(path, name), body)
69 return self._req('PATCH', url_path_join(path, name), body)
70
70
71 def get_checkpoints(self, name, path):
71 def get_checkpoints(self, name, path):
72 return self._req('GET', url_path_join(path, name, 'checkpoints'))
72 return self._req('GET', url_path_join(path, name, 'checkpoints'))
73
73
74 def new_checkpoint(self, name, path):
74 def new_checkpoint(self, name, path):
75 return self._req('POST', url_path_join(path, name, 'checkpoints'))
75 return self._req('POST', url_path_join(path, name, 'checkpoints'))
76
76
77 def restore_checkpoint(self, name, path, checkpoint_id):
77 def restore_checkpoint(self, name, path, checkpoint_id):
78 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
78 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
79
79
80 def delete_checkpoint(self, name, path, checkpoint_id):
80 def delete_checkpoint(self, name, path, checkpoint_id):
81 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
81 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
82
82
83 class APITest(NotebookTestBase):
83 class APITest(NotebookTestBase):
84 """Test the kernels web service API"""
84 """Test the kernels web service API"""
85 dirs_nbs = [('', 'inroot'),
85 dirs_nbs = [('', 'inroot'),
86 ('Directory with spaces in', 'inspace'),
86 ('Directory with spaces in', 'inspace'),
87 (u'unicodΓ©', 'innonascii'),
87 (u'unicodΓ©', 'innonascii'),
88 ('foo', 'a'),
88 ('foo', 'a'),
89 ('foo', 'b'),
89 ('foo', 'b'),
90 ('foo', 'name with spaces'),
90 ('foo', 'name with spaces'),
91 ('foo', u'unicodΓ©'),
91 ('foo', u'unicodΓ©'),
92 ('foo/bar', 'baz'),
92 ('foo/bar', 'baz'),
93 (u'Γ₯ b', u'Γ§ d')
93 (u'Γ₯ b', u'Γ§ d')
94 ]
94 ]
95
95
96 dirs = uniq_stable([d for (d,n) in dirs_nbs])
96 dirs = uniq_stable([d for (d,n) in dirs_nbs])
97 del dirs[0] # remove ''
97 del dirs[0] # remove ''
98
98
99 def setUp(self):
99 def setUp(self):
100 nbdir = self.notebook_dir.name
100 nbdir = self.notebook_dir.name
101
101
102 for d in self.dirs:
102 for d in self.dirs:
103 if not os.path.isdir(pjoin(nbdir, d)):
103 if not os.path.isdir(pjoin(nbdir, d)):
104 os.mkdir(pjoin(nbdir, d))
104 os.mkdir(pjoin(nbdir, d))
105
105
106 for d, name in self.dirs_nbs:
106 for d, name in self.dirs_nbs:
107 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
107 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w') as f:
108 nb = new_notebook(name=name)
108 nb = new_notebook(name=name)
109 write(nb, f, format='ipynb')
109 write(nb, f, format='ipynb')
110
110
111 self.nb_api = NBAPI(self.base_url())
111 self.nb_api = NBAPI(self.base_url())
112
112
113 def tearDown(self):
113 def tearDown(self):
114 nbdir = self.notebook_dir.name
114 nbdir = self.notebook_dir.name
115
115
116 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©', u'Γ₯ b']:
116 for dname in ['foo', 'Directory with spaces in', u'unicodΓ©', u'Γ₯ b']:
117 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
117 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
118
118
119 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
119 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
120 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
120 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
121
121
122 def test_list_notebooks(self):
122 def test_list_notebooks(self):
123 nbs = self.nb_api.list().json()
123 nbs = self.nb_api.list().json()
124 self.assertEqual(len(nbs), 1)
124 self.assertEqual(len(nbs), 1)
125 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
125 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
126
126
127 nbs = self.nb_api.list('/Directory with spaces in/').json()
127 nbs = self.nb_api.list('/Directory with spaces in/').json()
128 self.assertEqual(len(nbs), 1)
128 self.assertEqual(len(nbs), 1)
129 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
129 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
130
130
131 nbs = self.nb_api.list(u'/unicodΓ©/').json()
131 nbs = self.nb_api.list(u'/unicodΓ©/').json()
132 self.assertEqual(len(nbs), 1)
132 self.assertEqual(len(nbs), 1)
133 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
133 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
134 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
134 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
135
135
136 nbs = self.nb_api.list('/foo/bar/').json()
136 nbs = self.nb_api.list('/foo/bar/').json()
137 self.assertEqual(len(nbs), 1)
137 self.assertEqual(len(nbs), 1)
138 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
138 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
139 self.assertEqual(nbs[0]['path'], 'foo/bar')
139 self.assertEqual(nbs[0]['path'], 'foo/bar')
140
140
141 nbs = self.nb_api.list('foo').json()
141 nbs = self.nb_api.list('foo').json()
142 self.assertEqual(len(nbs), 4)
142 self.assertEqual(len(nbs), 4)
143 nbnames = { normalize('NFC', n['name']) for n in nbs }
143 nbnames = { normalize('NFC', n['name']) for n in nbs }
144 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
144 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
145 expected = { normalize('NFC', name) for name in expected }
145 expected = { normalize('NFC', name) for name in expected }
146 self.assertEqual(nbnames, expected)
146 self.assertEqual(nbnames, expected)
147
147
148 def test_list_nonexistant_dir(self):
148 def test_list_nonexistant_dir(self):
149 with assert_http_error(404):
149 with assert_http_error(404):
150 self.nb_api.list('nonexistant')
150 self.nb_api.list('nonexistant')
151
151
152 def test_get_contents(self):
152 def test_get_contents(self):
153 for d, name in self.dirs_nbs:
153 for d, name in self.dirs_nbs:
154 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
154 nb = self.nb_api.read('%s.ipynb' % name, d+'/').json()
155 self.assertEqual(nb['name'], u'%s.ipynb' % name)
155 self.assertEqual(nb['name'], u'%s.ipynb' % name)
156 self.assertIn('content', nb)
156 self.assertIn('content', nb)
157 self.assertIn('metadata', nb['content'])
157 self.assertIn('metadata', nb['content'])
158 self.assertIsInstance(nb['content']['metadata'], dict)
158 self.assertIsInstance(nb['content']['metadata'], dict)
159
159
160 # Name that doesn't exist - should be a 404
160 # Name that doesn't exist - should be a 404
161 with assert_http_error(404):
161 with assert_http_error(404):
162 self.nb_api.read('q.ipynb', 'foo')
162 self.nb_api.read('q.ipynb', 'foo')
163
163
164 def _check_nb_created(self, resp, name, path):
164 def _check_nb_created(self, resp, name, path):
165 self.assertEqual(resp.status_code, 201)
165 self.assertEqual(resp.status_code, 201)
166 location_header = py3compat.str_to_unicode(resp.headers['Location'])
166 location_header = py3compat.str_to_unicode(resp.headers['Location'])
167 self.assertEqual(location_header.split('/')[-1], name)
167 self.assertEqual(location_header, url_escape(url_path_join(u'/api/notebooks', path, name)))
168 self.assertEqual(resp.json()['name'], name)
168 self.assertEqual(resp.json()['name'], name)
169 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
169 assert os.path.isfile(pjoin(self.notebook_dir.name, path, name))
170
170
171 def test_create_untitled(self):
171 def test_create_untitled(self):
172 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
172 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
173 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
173 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
174
174
175 # Second time
175 # Second time
176 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
176 resp = self.nb_api.create_untitled(path=u'Γ₯ b')
177 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
177 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
178
178
179 # And two directories down
179 # And two directories down
180 resp = self.nb_api.create_untitled(path='foo/bar')
180 resp = self.nb_api.create_untitled(path='foo/bar')
181 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
181 self._check_nb_created(resp, 'Untitled0.ipynb', pjoin('foo', 'bar'))
182
182
183 def test_upload_untitled(self):
183 def test_upload_untitled(self):
184 nb = new_notebook(name='Upload test')
184 nb = new_notebook(name='Upload test')
185 nbmodel = {'content': nb}
185 nbmodel = {'content': nb}
186 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
186 resp = self.nb_api.upload_untitled(path=u'Γ₯ b',
187 body=jsonapi.dumps(nbmodel))
187 body=jsonapi.dumps(nbmodel))
188 self._check_nb_created(resp, 'Untitled0.ipynb', 'Γ₯ b')
188 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
189
189
190 def test_upload(self):
190 def test_upload(self):
191 nb = new_notebook(name=u'ignored')
191 nb = new_notebook(name=u'ignored')
192 nbmodel = {'content': nb}
192 nbmodel = {'content': nb}
193 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
193 resp = self.nb_api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
194 body=jsonapi.dumps(nbmodel))
194 body=jsonapi.dumps(nbmodel))
195 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
195 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
196
196
197 def test_copy_untitled(self):
197 def test_copy_untitled(self):
198 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
198 resp = self.nb_api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
199 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
199 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
200
200
201 def test_copy(self):
201 def test_copy(self):
202 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
202 resp = self.nb_api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
203 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
203 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
204
204
205 def test_delete(self):
205 def test_delete(self):
206 for d, name in self.dirs_nbs:
206 for d, name in self.dirs_nbs:
207 resp = self.nb_api.delete('%s.ipynb' % name, d)
207 resp = self.nb_api.delete('%s.ipynb' % name, d)
208 self.assertEqual(resp.status_code, 204)
208 self.assertEqual(resp.status_code, 204)
209
209
210 for d in self.dirs + ['/']:
210 for d in self.dirs + ['/']:
211 nbs = self.nb_api.list(d).json()
211 nbs = self.nb_api.list(d).json()
212 self.assertEqual(len(nbs), 0)
212 self.assertEqual(len(nbs), 0)
213
213
214 def test_rename(self):
214 def test_rename(self):
215 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
215 resp = self.nb_api.rename('a.ipynb', 'foo', 'z.ipynb')
216 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
216 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
217 self.assertEqual(resp.json()['name'], 'z.ipynb')
217 self.assertEqual(resp.json()['name'], 'z.ipynb')
218 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
218 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
219
219
220 nbs = self.nb_api.list('foo').json()
220 nbs = self.nb_api.list('foo').json()
221 nbnames = set(n['name'] for n in nbs)
221 nbnames = set(n['name'] for n in nbs)
222 self.assertIn('z.ipynb', nbnames)
222 self.assertIn('z.ipynb', nbnames)
223 self.assertNotIn('a.ipynb', nbnames)
223 self.assertNotIn('a.ipynb', nbnames)
224
224
225 def test_save(self):
225 def test_save(self):
226 resp = self.nb_api.read('a.ipynb', 'foo')
226 resp = self.nb_api.read('a.ipynb', 'foo')
227 nbcontent = jsonapi.loads(resp.text)['content']
227 nbcontent = jsonapi.loads(resp.text)['content']
228 nb = to_notebook_json(nbcontent)
228 nb = to_notebook_json(nbcontent)
229 ws = new_worksheet()
229 ws = new_worksheet()
230 nb.worksheets = [ws]
230 nb.worksheets = [ws]
231 ws.cells.append(new_heading_cell(u'Created by test Β³'))
231 ws.cells.append(new_heading_cell(u'Created by test Β³'))
232
232
233 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
233 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
234 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
234 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
235
235
236 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
236 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
237 with io.open(nbfile, 'r', encoding='utf-8') as f:
237 with io.open(nbfile, 'r', encoding='utf-8') as f:
238 newnb = read(f, format='ipynb')
238 newnb = read(f, format='ipynb')
239 self.assertEqual(newnb.worksheets[0].cells[0].source,
239 self.assertEqual(newnb.worksheets[0].cells[0].source,
240 u'Created by test Β³')
240 u'Created by test Β³')
241 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
241 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
242 newnb = to_notebook_json(nbcontent)
242 newnb = to_notebook_json(nbcontent)
243 self.assertEqual(newnb.worksheets[0].cells[0].source,
243 self.assertEqual(newnb.worksheets[0].cells[0].source,
244 u'Created by test Β³')
244 u'Created by test Β³')
245
245
246 # Save and rename
246 # Save and rename
247 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
247 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
248 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
248 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
249 saved = resp.json()
249 saved = resp.json()
250 self.assertEqual(saved['name'], 'a2.ipynb')
250 self.assertEqual(saved['name'], 'a2.ipynb')
251 self.assertEqual(saved['path'], 'foo/bar')
251 self.assertEqual(saved['path'], 'foo/bar')
252 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
252 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
253 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
253 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
254 with assert_http_error(404):
254 with assert_http_error(404):
255 self.nb_api.read('a.ipynb', 'foo')
255 self.nb_api.read('a.ipynb', 'foo')
256
256
257 def test_checkpoints(self):
257 def test_checkpoints(self):
258 resp = self.nb_api.read('a.ipynb', 'foo')
258 resp = self.nb_api.read('a.ipynb', 'foo')
259 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
259 r = self.nb_api.new_checkpoint('a.ipynb', 'foo')
260 self.assertEqual(r.status_code, 201)
260 self.assertEqual(r.status_code, 201)
261 cp1 = r.json()
261 cp1 = r.json()
262 self.assertEqual(set(cp1), {'id', 'last_modified'})
262 self.assertEqual(set(cp1), {'id', 'last_modified'})
263 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
263 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
264
264
265 # Modify it
265 # Modify it
266 nbcontent = jsonapi.loads(resp.text)['content']
266 nbcontent = jsonapi.loads(resp.text)['content']
267 nb = to_notebook_json(nbcontent)
267 nb = to_notebook_json(nbcontent)
268 ws = new_worksheet()
268 ws = new_worksheet()
269 nb.worksheets = [ws]
269 nb.worksheets = [ws]
270 hcell = new_heading_cell('Created by test')
270 hcell = new_heading_cell('Created by test')
271 ws.cells.append(hcell)
271 ws.cells.append(hcell)
272 # Save
272 # Save
273 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
273 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
274 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
274 resp = self.nb_api.save('a.ipynb', path='foo', body=jsonapi.dumps(nbmodel))
275
275
276 # List checkpoints
276 # List checkpoints
277 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
277 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
278 self.assertEqual(cps, [cp1])
278 self.assertEqual(cps, [cp1])
279
279
280 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
280 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
281 nb = to_notebook_json(nbcontent)
281 nb = to_notebook_json(nbcontent)
282 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
282 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
283
283
284 # Restore cp1
284 # Restore cp1
285 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
285 r = self.nb_api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
286 self.assertEqual(r.status_code, 204)
286 self.assertEqual(r.status_code, 204)
287 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
287 nbcontent = self.nb_api.read('a.ipynb', 'foo').json()['content']
288 nb = to_notebook_json(nbcontent)
288 nb = to_notebook_json(nbcontent)
289 self.assertEqual(nb.worksheets, [])
289 self.assertEqual(nb.worksheets, [])
290
290
291 # Delete cp1
291 # Delete cp1
292 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
292 r = self.nb_api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
293 self.assertEqual(r.status_code, 204)
293 self.assertEqual(r.status_code, 204)
294 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
294 cps = self.nb_api.get_checkpoints('a.ipynb', 'foo').json()
295 self.assertEqual(cps, [])
295 self.assertEqual(cps, [])
@@ -1,127 +1,127 b''
1 """Tornado handlers for the sessions web service.
1 """Tornado handlers for the sessions web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Zach Sailer
5 * Zach Sailer
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from ...base.handlers import IPythonHandler, json_errors
23 from ...base.handlers import IPythonHandler, json_errors
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join
25 from IPython.html.utils import url_path_join, url_escape
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Session web service handlers
28 # Session web service handlers
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31
31
32 class SessionRootHandler(IPythonHandler):
32 class SessionRootHandler(IPythonHandler):
33
33
34 @web.authenticated
34 @web.authenticated
35 @json_errors
35 @json_errors
36 def get(self):
36 def get(self):
37 # Return a list of running sessions
37 # Return a list of running sessions
38 sm = self.session_manager
38 sm = self.session_manager
39 sessions = sm.list_sessions()
39 sessions = sm.list_sessions()
40 self.finish(json.dumps(sessions, default=date_default))
40 self.finish(json.dumps(sessions, default=date_default))
41
41
42 @web.authenticated
42 @web.authenticated
43 @json_errors
43 @json_errors
44 def post(self):
44 def post(self):
45 # Creates a new session
45 # Creates a new session
46 #(unless a session already exists for the named nb)
46 #(unless a session already exists for the named nb)
47 sm = self.session_manager
47 sm = self.session_manager
48 nbm = self.notebook_manager
48 nbm = self.notebook_manager
49 km = self.kernel_manager
49 km = self.kernel_manager
50 model = self.get_json_body()
50 model = self.get_json_body()
51 if model is None:
51 if model is None:
52 raise web.HTTPError(400, "No JSON data provided")
52 raise web.HTTPError(400, "No JSON data provided")
53 try:
53 try:
54 name = model['notebook']['name']
54 name = model['notebook']['name']
55 except KeyError:
55 except KeyError:
56 raise web.HTTPError(400, "Missing field in JSON data: name")
56 raise web.HTTPError(400, "Missing field in JSON data: name")
57 try:
57 try:
58 path = model['notebook']['path']
58 path = model['notebook']['path']
59 except KeyError:
59 except KeyError:
60 raise web.HTTPError(400, "Missing field in JSON data: path")
60 raise web.HTTPError(400, "Missing field in JSON data: path")
61 # Check to see if session exists
61 # Check to see if session exists
62 if sm.session_exists(name=name, path=path):
62 if sm.session_exists(name=name, path=path):
63 model = sm.get_session(name=name, path=path)
63 model = sm.get_session(name=name, path=path)
64 else:
64 else:
65 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
65 kernel_id = km.start_kernel(cwd=nbm.notebook_dir)
66 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
66 model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url)
67 location = url_path_join(self.base_kernel_url, 'api', 'sessions', model['id'])
67 location = url_path_join(self.base_kernel_url, 'api', 'sessions', model['id'])
68 self.set_header('Location', location)
68 self.set_header('Location', url_escape(location))
69 self.set_status(201)
69 self.set_status(201)
70 self.finish(json.dumps(model, default=date_default))
70 self.finish(json.dumps(model, default=date_default))
71
71
72 class SessionHandler(IPythonHandler):
72 class SessionHandler(IPythonHandler):
73
73
74 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
74 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
75
75
76 @web.authenticated
76 @web.authenticated
77 @json_errors
77 @json_errors
78 def get(self, session_id):
78 def get(self, session_id):
79 # Returns the JSON model for a single session
79 # Returns the JSON model for a single session
80 sm = self.session_manager
80 sm = self.session_manager
81 model = sm.get_session(session_id=session_id)
81 model = sm.get_session(session_id=session_id)
82 self.finish(json.dumps(model, default=date_default))
82 self.finish(json.dumps(model, default=date_default))
83
83
84 @web.authenticated
84 @web.authenticated
85 @json_errors
85 @json_errors
86 def patch(self, session_id):
86 def patch(self, session_id):
87 # Currently, this handler is strictly for renaming notebooks
87 # Currently, this handler is strictly for renaming notebooks
88 sm = self.session_manager
88 sm = self.session_manager
89 model = self.get_json_body()
89 model = self.get_json_body()
90 if model is None:
90 if model is None:
91 raise web.HTTPError(400, "No JSON data provided")
91 raise web.HTTPError(400, "No JSON data provided")
92 changes = {}
92 changes = {}
93 if 'notebook' in model:
93 if 'notebook' in model:
94 notebook = model['notebook']
94 notebook = model['notebook']
95 if 'name' in notebook:
95 if 'name' in notebook:
96 changes['name'] = notebook['name']
96 changes['name'] = notebook['name']
97 if 'path' in notebook:
97 if 'path' in notebook:
98 changes['path'] = notebook['path']
98 changes['path'] = notebook['path']
99
99
100 sm.update_session(session_id, **changes)
100 sm.update_session(session_id, **changes)
101 model = sm.get_session(session_id=session_id)
101 model = sm.get_session(session_id=session_id)
102 self.finish(json.dumps(model, default=date_default))
102 self.finish(json.dumps(model, default=date_default))
103
103
104 @web.authenticated
104 @web.authenticated
105 @json_errors
105 @json_errors
106 def delete(self, session_id):
106 def delete(self, session_id):
107 # Deletes the session with given session_id
107 # Deletes the session with given session_id
108 sm = self.session_manager
108 sm = self.session_manager
109 km = self.kernel_manager
109 km = self.kernel_manager
110 session = sm.get_session(session_id=session_id)
110 session = sm.get_session(session_id=session_id)
111 sm.delete_session(session_id)
111 sm.delete_session(session_id)
112 km.shutdown_kernel(session['kernel']['id'])
112 km.shutdown_kernel(session['kernel']['id'])
113 self.set_status(204)
113 self.set_status(204)
114 self.finish()
114 self.finish()
115
115
116
116
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118 # URL to handler mappings
118 # URL to handler mappings
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120
120
121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
122
122
123 default_handlers = [
123 default_handlers = [
124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
125 (r"/api/sessions", SessionRootHandler)
125 (r"/api/sessions", SessionRootHandler)
126 ]
126 ]
127
127
General Comments 0
You need to be logged in to leave comments. Login now