diff --git a/IPython/html/services/contents/tests/__init__.py b/IPython/html/services/contents/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/html/services/contents/tests/__init__.py diff --git a/IPython/html/services/contents/tests/test_contentmanger.py b/IPython/html/services/contents/tests/test_contentmanger.py new file mode 100644 index 0000000..d92bc25 --- /dev/null +++ b/IPython/html/services/contents/tests/test_contentmanger.py @@ -0,0 +1,12 @@ +"""Tests for the content manager.""" + +import os +from unittest import TestCase +from tempfile import NamedTemporaryFile + +from IPython.utils.tempdir import TemporaryDirectory +from IPython.utils.traitlets import TraitError + +from ..contentmanager import ContentManager + +#class TestContentManager(TestCase): diff --git a/IPython/html/services/kernels/kernelmanager.py b/IPython/html/services/kernels/kernelmanager.py index aa668bd..dbb3453 100644 --- a/IPython/html/services/kernels/kernelmanager.py +++ b/IPython/html/services/kernels/kernelmanager.py @@ -46,17 +46,16 @@ class MappingKernelManager(MultiKernelManager): self.log.warn("Kernel %s died, removing from map.", kernel_id) self.remove_kernel(kernel_id) - def start_kernel(self, **kwargs): + def start_kernel(self, kernel_id=None, **kwargs): """Start a kernel for a session an return its kernel_id. Parameters ---------- - session_id : uuid - The uuid of the session to associate the new kernel with. If this - is not None, this kernel will be persistent whenever the session - requests a kernel. + kernel_id : uuid + The uuid to associate the new kernel with. If this + is not None, this kernel will be persistent whenever it is + requested. """ - kernel_id = None if kernel_id is None: kwargs['extra_arguments'] = self.kernel_argv kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs) diff --git a/IPython/html/services/sessions/handlers.py b/IPython/html/services/sessions/handlers.py index c1e925d..006166b 100644 --- a/IPython/html/services/sessions/handlers.py +++ b/IPython/html/services/sessions/handlers.py @@ -28,7 +28,6 @@ from ...base.handlers import IPythonHandler #----------------------------------------------------------------------------- - class SessionRootHandler(IPythonHandler): @web.authenticated @@ -45,12 +44,19 @@ class SessionRootHandler(IPythonHandler): nbm = self.notebook_manager km = self.kernel_manager notebook_path = self.get_argument('notebook_path', default=None) - notebook_name, path = nbm.named_notebook_path(notebook_path) - session_id, model = sm.get_session(notebook_name, path) - if model == None: - kernel_id = km.start_kernel() + name, path = nbm.named_notebook_path(notebook_path) + 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) - model = sm.session_model(session_id, notebook_name, path, kernel) + 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)) class SessionHandler(IPythonHandler): @@ -60,20 +66,19 @@ class SessionHandler(IPythonHandler): @web.authenticated def get(self, session_id): sm = self.session_manager - model = sm.get_session_from_id(session_id) + model = sm.get_session(id=session_id) self.finish(jsonapi.dumps(model)) @web.authenticated 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 notebook_path = self.request.body - notebook_name, path = nbm.named_notebook_path(notebook_path) - kernel_id = sm.get_kernel_from_session(session_id) - kernel = km.kernel_model(kernel_id, self.ws_url) - sm.delete_mapping_for_session(session_id) - model = sm.session_model(session_id, notebook_name, path, kernel) + name, path = nbm.named_notebook_path(notebook_path) + sm.update_session(id=session_id, name=name) + model = sm.get_session(id=session_id) self.finish(jsonapi.dumps(model)) @web.authenticated @@ -81,9 +86,9 @@ class SessionHandler(IPythonHandler): sm = self.session_manager nbm = self.notebook_manager km = self.kernel_manager - kernel_id = sm.get_kernel_from_session(session_id) - km.shutdown_kernel(kernel_id) - sm.delete_mapping_for_session(session_id) + session = sm.get_session(id=session_id) + sm.delete_session(session_id) + km.shutdown_kernel(session['kernel']['id']) self.set_status(204) self.finish() @@ -99,6 +104,3 @@ default_handlers = [ (r"api/sessions", SessionRootHandler) ] - - - diff --git a/IPython/html/services/sessions/sessionmanager.py b/IPython/html/services/sessions/sessionmanager.py index 21cad3e..2f7a3e1 100644 --- a/IPython/html/services/sessions/sessionmanager.py +++ b/IPython/html/services/sessions/sessionmanager.py @@ -18,6 +18,7 @@ Authors: import os import uuid +import sqlite3 from tornado import web @@ -31,67 +32,139 @@ from IPython.utils.traitlets import List, Dict, Unicode, TraitError class SessionManager(LoggingConfigurable): - # Use session_ids to map notebook names to kernel_ids - sessions = List() + # Session database initialized below + _cursor = None + _connection = None - def get_session(self, nb_name, nb_path=None): - """Get an existing session or create a new one""" - model = None - for session in self.sessions: - if session['name'] == nb_name and session['path'] == nb_path: - session_id = session['id'] - model = session - if model != None: - return session_id, model + @property + def cursor(self): + """Start a cursor and create a database called 'session'""" + if self._cursor is None: + self._cursor = self.connection.cursor() + self._cursor.execute("""CREATE TABLE session + (id, name, path, kernel)""") + return self._cursor + + @property + def connection(self): + """Start a database connection""" + if self._connection is None: + self._connection = sqlite3.connect(':memory:') + self._connection.row_factory = sqlite3.Row + return self._connection + + def __del__(self): + """Close connection once SessionManager closes""" + self.cursor.close() + + def session_exists(self, name, path): + """Check to see if the session for the given notebook exists""" + self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name,path)) + reply = self.cursor.fetchone() + if reply is None: + return False else: - session_id = unicode(uuid.uuid4()) - return session_id, model - - def session_model(self, session_id, notebook_name=None, notebook_path=None, kernel=None): - """ Create a session that links notebooks with kernels """ - model = dict(id=session_id, - name=notebook_name, - path=notebook_path, - kernel=kernel) - if notebook_path == None: - model['path']="" - self.sessions.append(model) - return model - - def list_sessions(self): - """List all sessions and their information""" - return self.sessions - - def set_kernel_for_sessions(self, session_id, kernel_id): - """Maps the kernel_ids to the session_id in session_mapping""" - for session in self.sessions: - if session['id'] == session_id: - session['kernel']['id'] = kernel_id - return self.sessions + return True + + def get_session_id(self): + "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 + creates a row in the sqlite session database that holds the information + for a session. - def delete_mapping_for_session(self, session_id): - """Delete the session from session_mapping with the given session_id""" - i = 0 - for session in self.sessions: - if session['id'] == session_id: - del self.sessions[i] - i = i + 1 - return self.sessions + Parameters + ---------- + session_id : str + uuid for the session; this method must be given a session_id + name : str + the .ipynb notebook name that started the session + path : str + the path to the named notebook + kernel : str + a uuid for the kernel associated with this session + """ + self.cursor.execute("""INSERT INTO session VALUES + (?,?,?,?)""", (session_id, name, path, kernel)) + self.connection.commit() + + def get_session(self, **kwargs): + """ 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) + + Returns + ------- + model : dict + returns a dictionary that includes all the information from the + session described by the kwarg. + """ + column = kwargs.keys()[0] # uses only the first kwarg that is entered + value = kwargs.values()[0] + try: + self.cursor.execute("SELECT * FROM session WHERE %s=?" %column, (value,)) + except sqlite3.OperationalError: + raise TraitError("The session database has no column: %s" %column) + reply = self.cursor.fetchone() + if reply is not None: + model = self.reply_to_dictionary_model(reply) + else: + model = None + return model + + def update_session(self, session_id, **kwargs): + """Updates the values in the session with the given session_id + with the values from the keyword arguments. - def get_session_from_id(self, session_id): - for session in self.sessions: - if session['id'] == session_id: - return session - - def get_notebook_from_session(self, session_id): - """Returns the notebook_path for the given session_id""" - for session in self.sessions: - if session['id'] == session_id: - return session['name'] - - def get_kernel_from_session(self, session_id): - """Returns the kernel_id for the given session_id""" - for session in self.sessions: - if session['id'] == session_id: - return session['kernel']['id'] + Parameters + ---------- + session_id : str + a uuid that identifies a session in the sqlite3 database + **kwargs : str + the key must correspond to a column title in session database, + 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) + + 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': ''}} + return model + def list_sessions(self): + """Returns a list of dictionaries containing all the information from + the session database""" + session_list=[] + self.cursor.execute("SELECT * FROM session") + sessions = self.cursor.fetchall() + for session in sessions: + model = self.reply_to_dictionary_model(session) + session_list.append(model) + return session_list + + def delete_session(self, session_id): + """Deletes the row in the session database with given session_id""" + # Check that session exists before deleting + model = self.get_session(id=session_id) + if model is None: + raise TraitError("The session does not exist: %s" %session_id) + else: + self.cursor.execute("DELETE FROM session WHERE id=?", (session_id,)) + self.connection.commit() \ No newline at end of file diff --git a/IPython/html/services/sessions/tests/__init__.py b/IPython/html/services/sessions/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/html/services/sessions/tests/__init__.py diff --git a/IPython/html/services/sessions/tests/test_sessionmanager.py b/IPython/html/services/sessions/tests/test_sessionmanager.py new file mode 100644 index 0000000..005d26a --- /dev/null +++ b/IPython/html/services/sessions/tests/test_sessionmanager.py @@ -0,0 +1,86 @@ +"""Tests for the session manager.""" + +import os + +from unittest import TestCase +from tempfile import NamedTemporaryFile + +from IPython.utils.tempdir import TemporaryDirectory +from IPython.utils.traitlets import TraitError + +from ..sessionmanager import SessionManager + +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') + 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''}} + 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') + self.assertRaises(TraitError, sm.get_session, bad_id=session_id) # Bad keyword + + def test_list_sessions(self): + sm = SessionManager() + 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') + 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''}}] + 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.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''}} + 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') + self.assertRaises(TraitError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword + + def test_delete_session(self): + sm = SessionManager() + 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.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''}}] + 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') + self.assertRaises(TraitError, sm.delete_session, session_id='23424') # Bad keyword + diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index 16b8344..82d5f45 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -24,16 +24,15 @@ var IPython = (function (IPython) { * A Kernel Class to communicate with the Python kernel * @Class Kernel */ - var Kernel = function (base_url, session_id) { + var Kernel = function (base_url) { this.kernel_id = null; - this.session_id = session_id this.shell_channel = null; this.iopub_channel = null; this.stdin_channel = null; this.base_url = base_url; this.running = false; - this.username = "username"; - this.base_session_id = utils.uuid(); + this.username= "username"; + this.session_id = utils.uuid(); this._msg_callbacks = {}; if (typeof(WebSocket) !== 'undefined') { @@ -52,7 +51,7 @@ var IPython = (function (IPython) { header : { msg_id : utils.uuid(), username : this.username, - session : this.base_session_id, + session : this.session_id, msg_type : msg_type }, metadata : {}, @@ -76,7 +75,6 @@ var IPython = (function (IPython) { Kernel.prototype.start = function (params) { var that = this; params = params || {}; - params.session = this.session_id; if (!this.running) { var qs = $.param(params); var url = this.base_url + '?' + qs; diff --git a/IPython/html/static/services/sessions/js/session.js b/IPython/html/static/services/sessions/js/session.js index 0635fc5..50fffe2 100644 --- a/IPython/html/static/services/sessions/js/session.js +++ b/IPython/html/static/services/sessions/js/session.js @@ -66,7 +66,6 @@ var IPython = (function (IPython) { this.kernel_content = json.kernel; var base_url = $('body').data('baseKernelUrl') + "api/kernels"; this.kernel = new IPython.Kernel(base_url, this.session_id); - // Now that the kernel has been created, tell the CodeCells about it. this.kernel._kernel_started(this.kernel_content); };