diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index 11d8bde..cd4e8d4 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -28,6 +28,7 @@ import os import stat import sys import threading +import traceback from tornado import web from tornado import websocket @@ -248,6 +249,17 @@ class IPythonHandler(AuthenticatedHandler): use_less=self.use_less, ) + def get_json_body(self): + """Return the body of the request as JSON data.""" + if not self.request.body: + return None + # Do we need to call body.decode('utf-8') here? + body = self.request.body.strip().decode(u'utf-8') + try: + model = json.loads(body) + except: + raise web.HTTPError(400, u'Invalid JSON in body of request') + return model class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler): """static files should only be accessible when logged in""" @@ -282,7 +294,8 @@ def json_errors(method): status = 400 message = u"Unknown server error" self.set_status(status) - reply = dict(message=message) + tb_text = ''.join(traceback.format_exception(t, value, tb)) + reply = dict(message=message, traceback=tb_text) self.finish(json.dumps(reply, default=date_default)) else: return result @@ -381,6 +394,12 @@ class FileFindHandler(web.StaticFileHandler): if if_since >= modified: self.set_status(304) return + + if os.path.splitext(path)[1] == '.ipynb': + raise HTTPError(404, 'HAHA') + name = os.path.splitext(os.path.split(path))[0] + self.set_header('Content-Type', 'application/json') + self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name) with open(abspath, "rb") as file: data = file.read() diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index 68d93f7..4db1004 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -16,12 +16,11 @@ Authors: # Imports #----------------------------------------------------------------------------- -from tornado import web - -from zmq.utils import jsonapi +import json +from tornado import web from IPython.utils.jsonutil import date_default -from ...base.handlers import IPythonHandler +from ...base.handlers import IPythonHandler, json_errors #----------------------------------------------------------------------------- # Session web service handlers @@ -31,63 +30,76 @@ from ...base.handlers import IPythonHandler class SessionRootHandler(IPythonHandler): @web.authenticated + @json_errors def get(self): # Return a list of running sessions sm = self.session_manager - nbm = self.notebook_manager - km = self.kernel_manager sessions = sm.list_sessions() - self.finish(jsonapi.dumps(sessions)) + self.finish(json.dumps(sessions, default=date_default)) @web.authenticated + @json_errors def post(self): # Creates a new session #(unless a session already exists for the named nb) sm = self.session_manager nbm = self.notebook_manager km = self.kernel_manager - notebook_path = self.get_argument('notebook_path', default=None) - name, path = nbm.named_notebook_path(notebook_path) + model = self.get_json_body() + if model is None: + raise HTTPError(400, "No JSON data provided") + try: + name = model['notebook']['name'] + except KeyError: + raise HTTPError(400, "Missing field in JSON data: name") + try: + path = model['notebook']['path'] + except KeyError: + raise HTTPError(400, "Missing field in JSON data: path") # Check to see if session exists if sm.session_exists(name=name, path=path): model = sm.get_session(name=name, path=path) - kernel_id = model['kernel']['id'] - km.start_kernel(kernel_id, cwd=nbm.notebook_dir) else: - session_id = sm.get_session_id() - sm.save_session(session_id=session_id, name=name, path=path) kernel_id = km.start_kernel(cwd=nbm.notebook_dir) - kernel = km.kernel_model(kernel_id, self.ws_url) - sm.update_session(session_id, kernel=kernel_id) - model = sm.get_session(id=session_id) - self.set_header('Location', '{0}kernels/{1}'.format(self.base_kernel_url, kernel_id)) - self.finish(jsonapi.dumps(model)) + model = sm.create_session(name=name, path=path, kernel_id=kernel_id, ws_url=self.ws_url) + self.set_header('Location', '{0}/api/sessions/{1}'.format(self.base_project_url, model['id'])) + self.finish(json.dumps(model, default=date_default)) class SessionHandler(IPythonHandler): SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE') @web.authenticated + @json_errors def get(self, session_id): # Returns the JSON model for a single session sm = self.session_manager model = sm.get_session(id=session_id) - self.finish(jsonapi.dumps(model)) + self.finish(json.dumps(model, default=date_default)) @web.authenticated + @json_errors def patch(self, session_id): # Currently, this handler is strictly for renaming notebooks sm = self.session_manager nbm = self.notebook_manager km = self.kernel_manager - data = self.request.body - data = jsonapi.loads(self.request.body) - name, path = nbm.named_notebook_path(data['notebook_path']) - sm.update_session(session_id, name=name) + model = self.get_json_body() + if model is None: + raise HTTPError(400, "No JSON data provided") + changes = {} + if 'notebook' in model: + notebook = model['notebook'] + if 'name' in notebook: + changes['name'] = notebook['name'] + if 'path' in notebook: + changes['path'] = notebook['path'] + sm.update_session(session_id, **changes) model = sm.get_session(id=session_id) - self.finish(jsonapi.dumps(model)) + self.finish(json.dumps(model, default=date_default)) @web.authenticated + @json_errors def delete(self, session_id): # Deletes the session with given session_id sm = self.session_manager @@ -107,9 +119,7 @@ class SessionHandler(IPythonHandler): _session_id_regex = r"(?P\w+-\w+-\w+-\w+-\w+)" default_handlers = [ - (r"api/sessions/%s/" % _session_id_regex, SessionHandler), (r"api/sessions/%s" % _session_id_regex, SessionHandler), - (r"api/sessions/", SessionRootHandler), (r"api/sessions", SessionRootHandler) ] diff --git a/IPython/html/services/sessions/sessionmanager.py b/IPython/html/services/sessions/sessionmanager.py index 2f7a3e1..b85f0a7 100644 --- a/IPython/html/services/sessions/sessionmanager.py +++ b/IPython/html/services/sessions/sessionmanager.py @@ -42,7 +42,7 @@ class SessionManager(LoggingConfigurable): if self._cursor is None: self._cursor = self.connection.cursor() self._cursor.execute("""CREATE TABLE session - (id, name, path, kernel)""") + (id, name, path, kernel_id, ws_url)""") return self._cursor @property @@ -70,8 +70,15 @@ class SessionManager(LoggingConfigurable): "Create a uuid for a new session" return unicode(uuid.uuid4()) - def save_session(self, session_id, name=None, path=None, kernel=None): - """ Given a session_id (and any other of the arguments), this method + def create_session(self, name=None, path=None, kernel_id=None, ws_url=None): + """Creates a session and returns its model""" + session_id = self.get_session_id() + return self.save_session(session_id, name=name, path=path, kernel_id=kernel_id, ws_url=ws_url) + + def save_session(self, session_id, name=None, path=None, kernel_id=None, ws_url=None): + """Saves the items for the session with the given session_id + + Given a session_id (and any other of the arguments), this method creates a row in the sqlite session database that holds the information for a session. @@ -83,22 +90,32 @@ class SessionManager(LoggingConfigurable): the .ipynb notebook name that started the session path : str the path to the named notebook - kernel : str + kernel_id : str a uuid for the kernel associated with this session + ws_url : str + the websocket url + + Returns + ------- + model : dict + a dictionary of the session model """ self.cursor.execute("""INSERT INTO session VALUES - (?,?,?,?)""", (session_id, name, path, kernel)) + (?,?,?,?,?)""", (session_id, name, path, kernel_id, ws_url)) self.connection.commit() + return self.get_session(id=session_id) def get_session(self, **kwargs): - """ Takes a keyword argument and searches for the value in the session + """Returns the model for a particular session. + + Takes a keyword argument and searches for the value in the session database, then returns the rest of the session's info. Parameters ---------- **kwargs : keyword argument must be given one of the keywords and values from the session database - (i.e. session_id, name, path, kernel) + (i.e. session_id, name, path, kernel_id, ws_url) Returns ------- @@ -120,7 +137,9 @@ class SessionManager(LoggingConfigurable): return model def update_session(self, session_id, **kwargs): - """Updates the values in the session with the given session_id + """Updates the values in the session database. + + Changes the values of the session with the given session_id with the values from the keyword arguments. Parameters @@ -132,20 +151,20 @@ class SessionManager(LoggingConfigurable): and the value replaces the current value in the session with session_id. """ - column = kwargs.keys()[0] # uses only the first kwarg that is entered - value = kwargs.values()[0] - try: - self.cursor.execute("UPDATE session SET %s=? WHERE id=?" %column, (value, session_id)) - self.connection.commit() - except sqlite3.OperationalError: - raise TraitError("No session exists with ID: %s" %session_id) + column = kwargs.keys() # uses only the first kwarg that is entered + value = kwargs.values() + for kwarg in kwargs: + try: + self.cursor.execute("UPDATE session SET %s=? WHERE id=?" %kwarg, (kwargs[kwarg], session_id)) + self.connection.commit() + except sqlite3.OperationalError: + raise TraitError("No session exists with ID: %s" %session_id) def reply_to_dictionary_model(self, reply): """Takes sqlite database session row and turns it into a dictionary""" model = {'id': reply['id'], - 'name' : reply['name'], - 'path' : reply['path'], - 'kernel' : {'id':reply['kernel'], 'ws_url': ''}} + 'notebook': {'name': reply['name'], 'path': reply['path']}, + 'kernel': {'id': reply['kernel_id'], 'ws_url': reply['ws_url']}} return model def list_sessions(self): diff --git a/IPython/html/services/sessions/tests/test_sessionmanager.py b/IPython/html/services/sessions/tests/test_sessionmanager.py index 005d26a..56bb628 100644 --- a/IPython/html/services/sessions/tests/test_sessionmanager.py +++ b/IPython/html/services/sessions/tests/test_sessionmanager.py @@ -15,16 +15,16 @@ class TestSessionManager(TestCase): def test_get_session(self): sm = SessionManager() session_id = sm.get_session_id() - sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678') + sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url') model = sm.get_session(id=session_id) - expected = {'id':session_id, 'name':u'test.ipynb', 'path': u'/path/to/', 'kernel':{'id':u'5678', 'ws_url': u''}} + expected = {'id':session_id, 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678', 'ws_url':u'ws_url'}} self.assertEqual(model, expected) def test_bad_get_session(self): # Should raise error if a bad key is passed to the database. sm = SessionManager() session_id = sm.get_session_id() - sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678') + sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url') self.assertRaises(TraitError, sm.get_session, bad_id=session_id) # Bad keyword def test_list_sessions(self): @@ -32,33 +32,33 @@ class TestSessionManager(TestCase): session_id1 = sm.get_session_id() session_id2 = sm.get_session_id() session_id3 = sm.get_session_id() - sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel='5678') - sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel='5678') - sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel='5678') + sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678', ws_url='ws_url') + sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678', ws_url='ws_url') + sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678', ws_url='ws_url') sessions = sm.list_sessions() - expected = [{'id':session_id1, 'name':u'test1.ipynb', - 'path': u'/path/to/1/', 'kernel':{'id':u'5678', 'ws_url': u''}}, - {'id':session_id2, 'name':u'test2.ipynb', - 'path': u'/path/to/2/', 'kernel':{'id':u'5678', 'ws_url': u''}}, - {'id':session_id3, 'name':u'test3.ipynb', - 'path': u'/path/to/3/', 'kernel':{'id':u'5678', 'ws_url': u''}}] + expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb', + 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}, + {'id':session_id2, 'notebook': {'name':u'test2.ipynb', + 'path': u'/path/to/2/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}, + {'id':session_id3, 'notebook':{'name':u'test3.ipynb', + 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}] self.assertEqual(sessions, expected) def test_update_session(self): sm = SessionManager() session_id = sm.get_session_id() - sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel=None) - sm.update_session(session_id, kernel='5678') + sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id=None, ws_url='ws_url') + sm.update_session(session_id, kernel_id='5678') sm.update_session(session_id, name='new_name.ipynb') model = sm.get_session(id=session_id) - expected = {'id':session_id, 'name':u'new_name.ipynb', 'path': u'/path/to/', 'kernel':{'id':u'5678', 'ws_url': u''}} + expected = {'id':session_id, 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}} self.assertEqual(model, expected) def test_bad_update_session(self): # try to update a session with a bad keyword ~ raise error sm = SessionManager() session_id = sm.get_session_id() - sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678') + sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url') self.assertRaises(TraitError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword def test_delete_session(self): @@ -66,21 +66,21 @@ class TestSessionManager(TestCase): session_id1 = sm.get_session_id() session_id2 = sm.get_session_id() session_id3 = sm.get_session_id() - sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel='5678') - sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel='5678') - sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel='5678') + sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678', ws_url='ws_url') + sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678', ws_url='ws_url') + sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678', ws_url='ws_url') sm.delete_session(session_id2) sessions = sm.list_sessions() - expected = [{'id':session_id1, 'name':u'test1.ipynb', - 'path': u'/path/to/1/', 'kernel':{'id':u'5678', 'ws_url': u''}}, - {'id':session_id3, 'name':u'test3.ipynb', - 'path': u'/path/to/3/', 'kernel':{'id':u'5678', 'ws_url': u''}}] + expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb', + 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}, + {'id':session_id3, 'notebook':{'name':u'test3.ipynb', + 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678', 'ws_url': 'ws_url'}}] self.assertEqual(sessions, expected) def test_bad_delete_session(self): # try to delete a session that doesn't exist ~ raise error sm = SessionManager() session_id = sm.get_session_id() - sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel='5678') + sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678', ws_url='ws_url') self.assertRaises(TraitError, sm.delete_session, session_id='23424') # Bad keyword diff --git a/IPython/html/services/sessions/tests/test_sessions_api.py b/IPython/html/services/sessions/tests/test_sessions_api.py index e195919..3b88be2 100644 --- a/IPython/html/services/sessions/tests/test_sessions_api.py +++ b/IPython/html/services/sessions/tests/test_sessions_api.py @@ -4,10 +4,9 @@ import os import sys import json -from zmq.utils import jsonapi - import requests +from IPython.utils.jsonutil import date_default from IPython.html.utils import url_path_join from IPython.html.tests.launchnotebook import NotebookTestBase @@ -39,12 +38,12 @@ class SessionAPITest(NotebookTestBase): # POST a session url, nb = self.mknb() notebook = nb.json() - param = {'notebook_path': notebook['path'] + notebook['name']} - r = requests.post(self.session_url(), params=param) + model = {'notebook': {'name':notebook['name'], 'path': notebook['path']}} + r = requests.post(self.session_url(), data=json.dumps(model, default=date_default)) data = r.json() assert isinstance(data, dict) - self.assertIn('name', data) - self.assertEqual(data['name'], notebook['name']) + self.assertIn('name', data['notebook']) + self.assertEqual(data['notebook']['name'], notebook['name']) # GET sessions r = requests.get(self.session_url()) @@ -62,8 +61,8 @@ class SessionAPITest(NotebookTestBase): # Create a session url, nb = self.mknb() notebook = nb.json() - param = {'notebook_path': notebook['path'] + notebook['name']} - r = requests.post(self.session_url(), params=param) + model = {'notebook': {'name':notebook['name'], 'path': notebook['path']}} + r = requests.post(self.session_url(), data=json.dumps(model, default=date_default)) session = r.json() # GET a session @@ -73,15 +72,17 @@ class SessionAPITest(NotebookTestBase): self.assertEqual(r.json(), session) # PATCH a session - data = {'notebook_path': 'test.ipynb'} - r = requests.patch(sess_url, data=jsonapi.dumps(data)) + model = {'notebook': {'name':'test.ipynb', 'path': '/'}} + r = requests.patch(sess_url, data=json.dumps(model, default=date_default)) + # Patching the notebook webservice too (just for consistency) requests.patch(self.notebook_url() + '/Untitled0.ipynb', - data=jsonapi.dumps({'name':'test.ipynb'})) + data=json.dumps({'name':'test.ipynb'})) + print r.json() assert isinstance(r.json(), dict) - self.assertIn('name', r.json()) + self.assertIn('name', r.json()['notebook']) self.assertIn('id', r.json()) - self.assertEqual(r.json()['name'], 'test.ipynb') + self.assertEqual(r.json()['notebook']['name'], 'test.ipynb') self.assertEqual(r.json()['id'], session['id']) # DELETE a session diff --git a/IPython/html/static/services/sessions/js/session.js b/IPython/html/static/services/sessions/js/session.js index 328d702..8485719 100644 --- a/IPython/html/static/services/sessions/js/session.js +++ b/IPython/html/static/services/sessions/js/session.js @@ -11,33 +11,39 @@ var IPython = (function (IPython) { - var Session = function(notebook_path, Notebook){ + var Session = function(notebook_name, notebook_path, Notebook){ this.kernel = null; this.kernel_id = null; this.session_id = null; + this.notebook_name = notebook_name; this.notebook_path = notebook_path; this.notebook = Notebook; this._baseProjectUrl = Notebook.baseProjectUrl() }; - Session.prototype.start = function(){ + Session.prototype.start = function() { var that = this - var qs = $.param({notebook_path:this.notebook_path}); - var url = '/api/sessions' + '?' + qs; - $.post(url, - $.proxy(this.start_kernel, that), - 'json' - ); + var notebook = {'notebook':{'name': this.notebook_name, 'path': this.notebook_path}} + var settings = { + processData : false, + cache : false, + type : "POST", + data: JSON.stringify(notebook), + dataType : "json", + }; + var url = this._baseProjectUrl + 'api/sessions'; + $.ajax(url, settings); }; - Session.prototype.notebook_rename = function (notebook_path) { - this.notebook_path = notebook_path; - var name = {'notebook_path': notebook_path} + Session.prototype.notebook_rename = function (name, path) { + this.notebook_name = name; + this.notebook_path = path; + var notebook = {'notebook':{'name':name, 'path': path}}; var settings = { processData : false, cache : false, type : "PATCH", - data: JSON.stringify(name), + data: JSON.stringify(notebook), dataType : "json", }; var url = this._baseProjectUrl + 'api/sessions/' + this.session_id;