Browse Source

Merge pull request #2226 from blink1073/server-api-update

Use the Session server API from Notebook 4.3
Steven Silvester 8 years ago
parent
commit
16800fc66c

+ 43 - 20
packages/apputils/src/clientsession.ts

@@ -453,10 +453,10 @@ class ClientSession implements IClientSession {
       return Promise.resolve(void 0);
     }
     this._path = path;
-    this._propertyChanged.emit('path');
     if (this._session) {
-      return this._session.rename(path);
+      return this._session.setPath(path);
     }
+    this._propertyChanged.emit('path');
     return Promise.resolve(void 0);
   }
 
@@ -468,7 +468,9 @@ class ClientSession implements IClientSession {
       return Promise.resolve(void 0);
     }
     this._name = name;
-    // no-op until supported.
+    if (this._session) {
+      return this._session.setName(name);
+    }
     this._propertyChanged.emit('name');
     return Promise.resolve(void 0);
   }
@@ -481,7 +483,9 @@ class ClientSession implements IClientSession {
       return Promise.resolve(void 0);
     }
     this._type = type;
-    // no-op until supported.
+    if (this._session) {
+      return this._session.setType(name);
+    }
     this._propertyChanged.emit('type');
     return Promise.resolve(void 0);
   }
@@ -505,7 +509,7 @@ class ClientSession implements IClientSession {
     let manager = this.manager;
     return manager.ready.then(() => {
       let model = find(manager.running(), item => {
-        return item.notebook.path === this._path;
+        return item.path === this._path;
       });
       if (!model) {
         return;
@@ -596,6 +600,8 @@ class ClientSession implements IClientSession {
     }
     return this.manager.startNew({
       path: this._path,
+      type: this._type,
+      name: this._name,
       kernelName: model ? model.name : null,
       kernelId: model ? model.id : null
     }).then(session => this._handleNewSession(session))
@@ -613,9 +619,21 @@ class ClientSession implements IClientSession {
       this._session.dispose();
     }
     this._session = session;
-    this._onPathChanged(session, session.path);
+    if (session.path !== this._path) {
+      this._path = session.path;
+      this._propertyChanged.emit('path');
+    }
+    if (session.name !== this._name) {
+      this._name = session.name;
+      this._propertyChanged.emit('name');
+    }
+    if (session.type !== this._type) {
+      this._type = session.type;
+      this._propertyChanged.emit('type');
+    }
+
     session.terminated.connect(this._onTerminated, this);
-    session.pathChanged.connect(this._onPathChanged, this);
+    session.propertyChanged.connect(this._onPropertyChanged, this);
     session.kernelChanged.connect(this._onKernelChanged, this);
     session.statusChanged.connect(this._onStatusChanged, this);
     session.iopubMessage.connect(this._onIopubMessage, this);
@@ -656,13 +674,21 @@ class ClientSession implements IClientSession {
   }
 
   /**
-   * Handle a change to a session path.
+   * Handle a change to a session property.
    */
-  private _onPathChanged(sender: Session.ISession, path: string) {
-    if (path !== this._path) {
-      this._path = path;
-      this._propertyChanged.emit('path');
+  private _onPropertyChanged(sender: Session.ISession, property: 'path' | 'name' | 'type') {
+    switch (property) {
+    case 'path':
+      this._path = sender.path;
+      break;
+    case 'name':
+      this._name = sender.name;
+      break;
+    default:
+      this._type = sender.type;
+      break;
     }
+    this._propertyChanged.emit(property);
   }
 
   /**
@@ -1027,7 +1053,7 @@ namespace Private {
 
     if (matchingSessions.length) {
       matchingSessions.sort((a, b) => {
-        return a.notebook.path.localeCompare(b.notebook.path);
+        return a.path.localeCompare(b.path);
       });
 
       each(matchingSessions, session => {
@@ -1043,7 +1069,7 @@ namespace Private {
 
     if (otherSessions.length) {
       otherSessions.sort((a, b) => {
-        return a.notebook.path.localeCompare(b.notebook.path);
+        return a.path.localeCompare(b.path);
       });
 
       each(otherSessions, session => {
@@ -1092,17 +1118,14 @@ namespace Private {
    */
   function optionForSession(session: Session.IModel, displayName: string, maxLength: number): HTMLOptionElement {
     let option = document.createElement('option');
-    let sessionName = session.notebook.path.split('/').pop();
-    const CONSOLE_REGEX = /^console-(\d)+-[0-9a-f]+$/;
-    if (CONSOLE_REGEX.test(sessionName)) {
-      sessionName = `Console ${sessionName.match(CONSOLE_REGEX)[1]}`;
-    }
+    let sessionName = session.name || session.path.split('/').pop();
     if (sessionName.length > maxLength) {
       sessionName = sessionName.slice(0, maxLength - 3) + '...';
     }
     option.text = sessionName;
     option.value = JSON.stringify({ id: session.kernel.id });
-    option.title = `Path: ${session.notebook.path}\n` +
+    option.title = `Path: ${session.path}\n` +
+      `Name: ${sessionName}\n` +
       `Kernel Name: ${displayName}\n` +
       `Kernel Id: ${session.kernel.id}`;
     return option;

+ 1 - 1
packages/console/src/panel.ts

@@ -62,7 +62,7 @@ class ConsolePanel extends Panel {
     } = options;
     let contentFactory = this.contentFactory = (
       options.contentFactory || ConsolePanel.defaultContentFactory
-    )
+    );
     let count = Private.count++;
     if (!path) {
       path = `${basePath || ''}/console-${count}-${uuid()}`;

+ 3 - 3
packages/filebrowser/src/listing.ts

@@ -437,7 +437,7 @@ class DirListing extends Widget {
     const items = this._sortedItems;
     const paths = items.map(item => item.path);
     each(this._model.sessions(), session => {
-      let index = ArrayExt.firstIndexOf(paths, session.notebook.path);
+      let index = ArrayExt.firstIndexOf(paths, session.path);
       if (this._selection[items[index].name]) {
         promises.push(model.manager.services.sessions.shutdown(session.id));
       }
@@ -701,10 +701,10 @@ class DirListing extends Widget {
       this.addClass(SELECTED_CLASS);
     }
 
-    // Handle notebook session statuses.
+    // Handle file session statuses.
     let paths = toArray(map(items, item => item.path));
     each(this._model.sessions(), session => {
-      let index = ArrayExt.firstIndexOf(paths, session.notebook.path);
+      let index = ArrayExt.firstIndexOf(paths, session.path);
       let node = nodes[index];
       node.classList.add(RUNNING_CLASS);
       let name = session.kernel.name;

+ 1 - 1
packages/filebrowser/src/model.ts

@@ -372,7 +372,7 @@ class FileBrowserModel implements IDisposable {
   private _onRunningChanged(sender: Session.IManager, models: IterableOrArrayLike<Session.IModel>): void {
     this._sessions.length = 0;
     each(models, model => {
-      if (this._paths.has(model.notebook.path)) {
+      if (this._paths.has(model.path)) {
         this._sessions.push(model);
       }
     });

+ 3 - 5
packages/running-extension/src/index.ts

@@ -14,7 +14,7 @@ import {
 } from '@jupyterlab/services';
 
 import {
-  RunningSessions, CONSOLE_REGEX
+  RunningSessions
 } from '@jupyterlab/running';
 
 
@@ -49,14 +49,12 @@ function activate(app: JupyterLab, services: IServiceManager, restorer: ILayoutR
   restorer.add(running, 'running-sessions');
 
   running.sessionOpenRequested.connect((sender, model) => {
-    let path = model.notebook.path;
-    let name = path.split('/').pop();
-    if (CONSOLE_REGEX.test(name)) {
+    let path = model.path;
+    if (model.type.toLowerCase() === 'console') {
       app.commands.execute('console:open', { id: model.id });
     } else {
       app.commands.execute('file-operations:open', { path });
     }
-
   });
 
   running.terminalOpenRequested.connect((sender, model) => {

+ 6 - 14
packages/running/src/index.ts

@@ -111,12 +111,6 @@ const FILE_ICON_CLASS = 'jp-mod-file';
  */
 const TERMINAL_ICON_CLASS = 'jp-mod-terminal';
 
-/**
- * A regex for console names.
- */
-export
-const CONSOLE_REGEX = /^console-(\d)+-[0-9a-f]+$/;
-
 
 /**
  * A class that exposes the running terminal and kernel sessions.
@@ -349,8 +343,8 @@ class RunningSessions extends Widget {
     // Strip out non-file backed sessions.
     this._runningSessions = [];
     for (let session of models) {
-      let name = session.notebook.path.split('/').pop();
-      if (name.indexOf('.') !== -1 || CONSOLE_REGEX.test(name)) {
+      let name = session.name || session.path.split('/').pop();
+      if (name.indexOf('.') !== -1 || session.name) {
         this._runningSessions.push(session);
       }
     }
@@ -664,20 +658,18 @@ namespace RunningSessions {
      */
     updateSessionNode(node: HTMLLIElement, model: Session.IModel, kernelName: string): void {
       let icon = DOMUtils.findElement(node, ITEM_ICON_CLASS);
-      let path = model.notebook.path;
-      let name = path.split('/').pop();
+      let name = model.name || model.path.split('/').pop();
       if (name.indexOf('.ipynb') !== -1) {
         icon.className = `${ITEM_ICON_CLASS} ${NOTEBOOK_ICON_CLASS}`;
-      } else if (CONSOLE_REGEX.test(name)) {
+      } else if (model.type.toLowerCase() === 'console') {
         icon.className = `${ITEM_ICON_CLASS} ${CONSOLE_ICON_CLASS}`;
-        path = `Console ${name.match(CONSOLE_REGEX)[1]}`;
       } else {
         icon.className = `${ITEM_ICON_CLASS} ${FILE_ICON_CLASS}`;
       }
       let label = DOMUtils.findElement(node, ITEM_LABEL_CLASS);
-      label.textContent = path;
+      label.textContent = name;
       let title = (
-        `Path: ${model.notebook.path}\n` +
+        `Path: ${model.path}\n` +
         `Kernel: ${kernelName}`
       );
       label.title = title;

+ 2 - 2
packages/services/README.md

@@ -20,7 +20,7 @@ Package Install
 
 ```bash
 npm install --save @jupyterlab/services
-conda install notebook  # notebook 4.2+ required
+conda install notebook  # notebook 4.3+ required
 ```
 
 
@@ -37,7 +37,7 @@ git clone https://github.com/jupyterlab/services.git
 cd services
 npm install
 npm run build
-conda install notebook  # notebook 4.2+ required
+conda install notebook  # notebook 4.3+ required
 ```
 
 **Rebuild**

+ 1 - 1
packages/services/examples/browser/index.ts

@@ -31,7 +31,7 @@ function main() {
     log('Session started');
     session = s;
     // Rename the session.
-    return session.rename('bar.ipynb');
+    return session.setPath('bar.ipynb');
   }).then(() => {
     log(`Session renamed to ${session.path}`);
     // Execute and handle replies on the kernel.

+ 1 - 1
packages/services/examples/node/index.js

@@ -31,7 +31,7 @@ var session;
 services.Session.startNew(options).then(function(s) {
   // Rename the session.
   session = s;
-  return session.rename('bar.ipynb');
+  return session.setPath('bar.ipynb');
 }).then(function() {
   console.log('Session renamed to', session.path);
   // Execute and handle replies on the kernel.

+ 18 - 9
packages/services/src/kernel/default.ts

@@ -222,10 +222,7 @@ class DefaultKernel implements Kernel.IKernel {
       return;
     }
     this._status = 'dead';
-    if (this._ws !== null) {
-      this._ws.close();
-    }
-    this._ws = null;
+    this._clearSocket();
     this._futures.forEach((future, key) => {
       future.dispose();
     });
@@ -353,9 +350,8 @@ class DefaultKernel implements Kernel.IKernel {
       return Promise.reject(new Error('Kernel is dead'));
     }
     this._clearState();
-    return this.ready.then(() => {
-      return Private.shutdownKernel(this.id, this.serverSettings);
-    });
+    this._clearSocket();
+    return Private.shutdownKernel(this.id, this.serverSettings);
   }
 
   /**
@@ -637,6 +633,7 @@ class DefaultKernel implements Kernel.IKernel {
     }
 
     this._connectionPromise = new PromiseDelegate<void>();
+    this._wsStopped = false;
     this._ws = settings.wsFactory(url);
 
     // Ensure incoming binary messages are not Blobs
@@ -668,7 +665,7 @@ class DefaultKernel implements Kernel.IKernel {
    * Handle a websocket message, validating and routing appropriately.
    */
   private _onWSMessage(evt: MessageEvent) {
-    if (this.status === 'dead') {
+    if (this._wsStopped) {
       // If the socket is being closed, ignore any messages
       return;
     }
@@ -717,7 +714,7 @@ class DefaultKernel implements Kernel.IKernel {
    * Handle a websocket close event.
    */
   private _onWSClose(evt: Event) {
-    if (this.status === 'dead') {
+    if (this._wsStopped) {
       return;
     }
     // Clear the websocket event handlers and the socket itself.
@@ -737,6 +734,17 @@ class DefaultKernel implements Kernel.IKernel {
     }
   }
 
+  /**
+   * Clear the socket state.
+   */
+  private _clearSocket(): void {
+    this._wsStopped = true;
+    if (this._ws !== null) {
+      this._ws.close();
+    }
+    this._ws = null;
+  }
+
   /**
    * Handle status iopub messages from the kernel.
    */
@@ -902,6 +910,7 @@ class DefaultKernel implements Kernel.IKernel {
   private _name = '';
   private _status: Kernel.Status = 'unknown';
   private _clientId = '';
+  private _wsStopped = false;
   private _ws: WebSocket = null;
   private _username = '';
   private _reconnectLimit = 7;

+ 81 - 28
packages/services/src/session/default.ts

@@ -48,6 +48,8 @@ class DefaultSession implements Session.ISession {
   constructor(options: Session.IOptions, id: string, kernel: Kernel.IKernel) {
     this._id = id;
     this._path = options.path;
+    this._type = options.type;
+    this._name = options.name;
     this.serverSettings = options.serverSettings || ServerConnection.makeSettings();
     this._uuid = uuid();
     Private.addRunning(this);
@@ -89,10 +91,10 @@ class DefaultSession implements Session.ISession {
   }
 
   /**
-   * A signal emitted when the session path changes.
+   * A signal emitted when a session property changes.
    */
-  get pathChanged(): ISignal<this, string> {
-    return this._pathChanged;
+  get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
+    return this._propertyChanged;
   }
 
   /**
@@ -121,6 +123,20 @@ class DefaultSession implements Session.ISession {
     return this._path;
   }
 
+  /**
+   * Get the session type.
+   */
+  get type(): string {
+    return this._type;
+  }
+
+  /**
+   * Get the session name.
+   */
+  get name(): string {
+    return this._name;
+  }
+
   /**
    * Get the model associated with the session.
    */
@@ -128,9 +144,9 @@ class DefaultSession implements Session.ISession {
     return {
       id: this.id,
       kernel: this.kernel.model,
-      notebook: {
-        path: this.path
-      }
+      path: this._path,
+      type: this._type,
+      name: this._name
     };
   }
 
@@ -176,20 +192,20 @@ class DefaultSession implements Session.ISession {
     if (this._updating) {
       return Promise.resolve(void 0);
     }
-    let oldPath = this._path;
-    let newPath = this._path = model.notebook.path;
+    let oldModel = this.model;
+    this._path = model.path;
+    this._name = model.name;
+    this._type = model.type;
 
     if (this._kernel.isDisposed || model.kernel.id !== this._kernel.id) {
       return Kernel.connectTo(model.kernel.id, this.serverSettings).then(kernel => {
         this.setupKernel(kernel);
         this._kernelChanged.emit(kernel);
-        if (oldPath !== newPath) {
-          this._pathChanged.emit(newPath);
-        }
+        this._handleModelChange(oldModel);
       });
-    } else if (oldPath !== newPath) {
-      this._pathChanged.emit(newPath);
     }
+
+    this._handleModelChange(oldModel);
     return Promise.resolve(void 0);
   }
 
@@ -214,17 +230,39 @@ class DefaultSession implements Session.ISession {
    *
    * @param path - The new session path.
    *
+   * @returns A promise that resolves when the session has renamed.
+   *
    * #### Notes
    * This uses the Jupyter REST API, and the response is validated.
    * The promise is fulfilled on a valid response and rejected otherwise.
    */
-  rename(path: string): Promise<void> {
+  setPath(path: string): Promise<void> {
     if (this.isDisposed) {
       return Promise.reject(new Error('Session is disposed'));
     }
-    let data = JSON.stringify({
-      notebook: { path }
-    });
+    let data = JSON.stringify({ path });
+    return this._patch(data).then(() => { return void 0; });
+  }
+
+  /**
+   * Change the session name.
+   */
+  setName(name: string): Promise<void> {
+    if (this.isDisposed) {
+      return Promise.reject(new Error('Session is disposed'));
+    }
+    let data = JSON.stringify({ name });
+    return this._patch(data).then(() => { return void 0; });
+  }
+
+  /**
+   * Change the session type.
+   */
+  setType(type: string): Promise<void> {
+    if (this.isDisposed) {
+      return Promise.reject(new Error('Session is disposed'));
+    }
+    let data = JSON.stringify({ type });
     return this._patch(data).then(() => { return void 0; });
   }
 
@@ -264,13 +302,6 @@ class DefaultSession implements Session.ISession {
     if (this.isDisposed) {
       return Promise.reject(new Error('Session is disposed'));
     }
-    if (this._kernel) {
-      return this._kernel.ready.then(() => {
-        return Private.shutdownSession(
-          this.id, this.serverSettings
-        );
-      });
-    }
     return Private.shutdownSession(this.id, this.serverSettings);
   }
 
@@ -335,8 +366,25 @@ class DefaultSession implements Session.ISession {
     });
   }
 
+  /**
+   * Handle a change to the model.
+   */
+  private _handleModelChange(oldModel: Session.IModel): void {
+    if (oldModel.name !== this._name) {
+      this._propertyChanged.emit('name');
+    }
+    if (oldModel.type !== this._type) {
+      this._propertyChanged.emit('type');
+    }
+    if (oldModel.path !== this._path) {
+      this._propertyChanged.emit('path');
+    }
+  }
+
   private _id = '';
   private _path = '';
+  private _name = '';
+  private _type = '';
   private _kernel: Kernel.IKernel = null;
   private _uuid = '';
   private _isDisposed = false;
@@ -345,9 +393,10 @@ class DefaultSession implements Session.ISession {
   private _statusChanged = new Signal<this, Kernel.Status>(this);
   private _iopubMessage = new Signal<this, KernelMessage.IMessage>(this);
   private _unhandledMessage = new Signal<this, KernelMessage.IMessage>(this);
-  private _pathChanged = new Signal<this, string>(this);
+  private _propertyChanged = new Signal<this, 'path' | 'name' | 'type'>(this);
 }
 
+
 /**
  * The namespace for `DefaultSession` statics.
  */
@@ -462,7 +511,9 @@ namespace Private {
     let settings = options.serverSettings || ServerConnection.makeSettings();
     return Kernel.connectTo(model.kernel.id, settings).then(kernel => {
       return new DefaultSession({
-        path: model.notebook.path,
+        path: model.path,
+        type: model.type,
+        name: model.name,
         serverSettings: settings
       }, model.id, kernel);
     }).catch(error => {
@@ -502,7 +553,7 @@ namespace Private {
 
     return listRunning(settings).then(models => {
       let model = find(models, value => {
-        return value.notebook.path === path;
+        return value.path === path;
       });
       if (model) {
         return model;
@@ -654,7 +705,9 @@ namespace Private {
     let settings = options.serverSettings || ServerConnection.makeSettings();
     let model = {
       kernel: { name: options.kernelName, id: options.kernelId },
-      notebook: { path: options.path }
+      path: options.path,
+      type: options.type || '',
+      name: options.name || ''
     };
     let request = {
       url: URLExt.join(settings.baseUrl, SESSION_SERVICE_URL),

+ 2 - 2
packages/services/src/session/manager.ts

@@ -175,7 +175,7 @@ class SessionManager implements Session.IManager {
    */
   stopIfNeeded(path: string): Promise<void> {
     return Session.listRunning(this.serverSettings).then(sessions => {
-      const matches = sessions.filter(value => value.notebook.path === path);
+      const matches = sessions.filter(value => value.path === path);
       if (matches.length === 1) {
         const id = matches[0].id;
         return this.shutdown(id).catch(() => { /* no-op */ });
@@ -240,7 +240,7 @@ class SessionManager implements Session.IManager {
     session.terminated.connect(() => {
       this._onTerminated(id);
     });
-    session.pathChanged.connect(() => {
+    session.propertyChanged.connect(() => {
       this._onChanged(session.model);
     });
     session.kernelChanged.connect(() => {

+ 37 - 8
packages/services/src/session/session.ts

@@ -56,9 +56,9 @@ namespace Session {
     statusChanged: ISignal<ISession, Kernel.Status>;
 
     /**
-     * A signal emitted when the session path changes.
+     * A signal emitted when a session property changes.
      */
-    pathChanged: ISignal<ISession, string>;
+    readonly propertyChanged: ISignal<this, 'path' | 'name' | 'type'>;
 
     /**
      * A signal emitted for iopub kernel messages.
@@ -76,10 +76,20 @@ namespace Session {
     readonly id: string;
 
     /**
-     * The path associated with the session.
+     * The current path associated with the sesssion.
      */
     readonly path: string;
 
+    /**
+     * The current name associated with the sesssion.
+     */
+    readonly name: string;
+
+    /**
+     * The type of the session.
+     */
+    readonly type: string;
+
     /**
      * The server settings of the session.
      */
@@ -117,7 +127,17 @@ namespace Session {
      * This uses the Jupyter REST API, and the response is validated.
      * The promise is fulfilled on a valid response and rejected otherwise.
      */
-    rename(path: string): Promise<void>;
+    setPath(path: string): Promise<void>;
+
+    /**
+     * Change the session name.
+     */
+    setName(name: string): Promise<void>;
+
+    /**
+     * Change the session type.
+     */
+    setType(type: string): Promise<void>;
 
     /**
      * Change the kernel.
@@ -286,6 +306,16 @@ namespace Session {
      */
     path?: string;
 
+    /**
+     * The name of the session.
+     */
+    name?: string;
+
+    /**
+     * The type of the session.
+     */
+    type?: string;
+
     /**
      * The type of kernel (e.g. python3).
      */
@@ -456,10 +486,9 @@ namespace Session {
      * The unique identifier for the session client.
      */
     readonly id: string;
-    readonly notebook?: {
-      [ key: string ]: string;
-      path: string;
-    };
+    readonly name: string;
+    readonly path: string;
+    readonly type: string;
     readonly kernel?: Kernel.IModel;
   }
 }

+ 4 - 2
packages/services/src/session/validate.ts

@@ -44,8 +44,10 @@ function validateProperty(object: any, name: string, typeName?: string): void {
 export
 function validateModel(model: Session.IModel): void {
   validateProperty(model, 'id', 'string');
-  validateProperty(model, 'notebook', 'object');
+  validateProperty(model, 'path', 'string');
+  validateProperty(model, 'type', 'string');
+  validateProperty(model, 'name', 'string');
   validateProperty(model, 'kernel', 'object');
   validateKernelModel(model.kernel);
-  validateProperty(model.notebook, 'path', 'string');
+
 }

+ 3 - 3
packages/services/test/src/integration.ts

@@ -159,7 +159,7 @@ describe('jupyter.services - Integration', () => {
       let id = '';
       return Session.startNew(options).then(value => {
         session = value;
-        return session.rename('Untitled2.ipynb');
+        return session.setPath('Untitled2.ipynb');
       }).then(() => {
         expect(session.path).to.be('Untitled2.ipynb');
         // should grab the same session object
@@ -238,10 +238,10 @@ describe('jupyter.services - Integration', () => {
       let newPath = 'Untitled2b.ipynb';
       return Session.startNew(options).then(s => {
         session = s;
-        return session.rename(newPath);
+        return session.setPath(newPath);
       }).then(() => {
         expect(session.path).to.be(newPath);
-        expect(session.model.notebook.path).to.be(newPath);
+        expect(session.model.path).to.be(newPath);
         return session.shutdown();
       });
     });

+ 10 - 4
packages/services/test/src/session/manager.spec.ts

@@ -30,7 +30,9 @@ import {
 function createSessionModel(id = ''): Session.IModel {
   return {
     id: id || uuid(),
-    notebook: { path: uuid() },
+    path: uuid(),
+    type: '',
+    name: '',
     kernel: { id: uuid(), name: uuid() }
   };
 }
@@ -168,7 +170,9 @@ describe('session/manager', () => {
           let model = {
             id: s.id,
             kernel: s.kernel.model,
-            notebook: { path: 'bar' }
+            path: 'bar',
+            type: '',
+            name: ''
           };
           tester.onRequest = () => {
             tester.respond(200, model);
@@ -177,7 +181,7 @@ describe('session/manager', () => {
             manager.dispose();
             done();
           });
-          return s.rename(model.notebook.path);
+          return s.setPath(model.path);
         }).catch(done);
       });
 
@@ -189,7 +193,9 @@ describe('session/manager', () => {
               name: 'foo',
               id: uuid()
             },
-            notebook: { path: 'bar' }
+            path: 'bar',
+            name: '',
+            type: ''
           };
           let name = model.kernel.name;
           tester.onRequest = request => {

+ 133 - 28
packages/services/test/src/session/session.spec.ts

@@ -30,7 +30,9 @@ import {
 function createSessionModel(id?: string): Session.IModel {
   return {
     id: id || uuid(),
-    notebook: { path: uuid() },
+    path: uuid(),
+    name: '',
+    type: '',
     kernel: { id: uuid(), name: uuid() }
   };
 }
@@ -42,7 +44,7 @@ function createSessionModel(id?: string): Session.IModel {
 function createSessionOptions(sessionModel?: Session.IModel): Session.IOptions {
   sessionModel = sessionModel || createSessionModel();
   return {
-    path: sessionModel.notebook.path,
+    path: sessionModel.path,
     kernelName: sessionModel.kernel.name
   };
 }
@@ -91,7 +93,7 @@ describe('session', () => {
     });
 
     it('should throw an error for an invalid model', (done) => {
-      let data = { id: '1234', notebook: { path: 'test' } };
+      let data = { id: '1234', path: 'test' };
       tester.onRequest = () => {
         tester.respond(200, data);
       };
@@ -100,7 +102,7 @@ describe('session', () => {
     });
 
     it('should throw an error for another invalid model', (done) => {
-      let data = [{ id: '1234', kernel: { id: '', name: '' }, notebook: { } }];
+      let data = [{ id: '1234', kernel: { id: '', name: '' }, path: '' }];
       tester.onRequest = () => {
         tester.respond(200, data);
       };
@@ -131,7 +133,9 @@ describe('session', () => {
         tester.onRequest = request => {
           tester.respond(200, [ {
             id: session.model.id,
-            notebook: { path : 'foo/bar.ipynb' },
+            path: 'foo/bar.ipynb',
+            name: '',
+            type: '',
             kernel: newKernel
           } ]);
           tester.onRequest = () => {
@@ -201,7 +205,7 @@ describe('session', () => {
     it('should fail for wrong response model', (done) => {
       let sessionModel = createSessionModel();
       let data = {
-        id: 1, kernel: { name: '', id: '' }, notebook: { path: ''}
+        id: 1, kernel: { name: '', id: '' }, path: '', type: '', name: ''
       };
       tester.onRequest = request => {
         if (request.method === 'POST') {
@@ -236,8 +240,8 @@ describe('session', () => {
     it('should find an existing session by path', (done) => {
       let sessionModel = createSessionModel();
       tester.runningSessions = [sessionModel];
-      Session.findByPath(sessionModel.notebook.path).then(newId => {
-        expect(newId.notebook.path).to.be(sessionModel.notebook.path);
+      Session.findByPath(sessionModel.path).then(newId => {
+        expect(newId.path).to.be(sessionModel.path);
         done();
       }).catch(done);
     });
@@ -416,7 +420,7 @@ describe('session', () => {
       });
     });
 
-    context('#pathChanged', () => {
+    context('#propertyChanged', () => {
 
       it('should be emitted when the session path changes', () => {
         // TODO: reinstate after switching to mock-socket
@@ -425,11 +429,11 @@ describe('session', () => {
         //   tester.respond(200, model);
         // };
         // session.pathChanged.connect((s, path) => {
-        //   // expect(session.path).to.be(model.notebook.path);
-        //   // expect(path).to.be(model.notebook.path);
+        //   // expect(session.path).to.be(model.path);
+        //   // expect(path).to.be(model.path);
         //   done();
         // });
-        // session.rename(model.notebook.path);
+        // session.rename(model.path);
       });
 
     });
@@ -448,12 +452,26 @@ describe('session', () => {
       });
     });
 
+    context('#name', () => {
+
+      it('should be a string', () => {
+        expect(typeof session.name).to.be('string');
+      });
+    });
+
+    context('#type', () => {
+
+      it('should be a string', () => {
+        expect(typeof session.name).to.be('string');
+      });
+    });
+
     context('#model', () => {
 
       it('should be an IModel', () => {
         let model = session.model;
         expect(typeof model.id).to.be('string');
-        expect(typeof model.notebook.path).to.be('string');
+        expect(typeof model.path).to.be('string');
         expect(typeof model.kernel.name).to.be('string');
         expect(typeof model.kernel.id).to.be('string');
       });
@@ -524,23 +542,21 @@ describe('session', () => {
 
     });
 
-    context('#rename()', () => {
+    context('#setPath()', () => {
 
-      it('should rename the session', (done) => {
-        let model = session.model;
-        let path = model.notebook.path = 'foo.ipynb';
+      it('should set the path of the session', () => {
+        let model = { ...session.model, path: 'foo.ipynb' };
         tester.onRequest = () => {
           tester.respond(200, model);
         };
-        session.rename(path).then(() => {
-          expect(session.path).to.be(path);
+        return session.setPath(model.path).then(() => {
+          expect(session.path).to.be(model.path);
           session.dispose();
-          done();
-        }).catch(done);
+        });
       });
 
       it('should fail for improper response status', (done) => {
-        let promise = session.rename('foo');
+        let promise = session.setPath('foo');
         tester.onRequest = () => {
           tester.respond(201, { });
           expectFailure(promise, done);
@@ -548,7 +564,7 @@ describe('session', () => {
       });
 
       it('should fail for error response status', (done) => {
-        let promise = session.rename('foo');
+        let promise = session.setPath('foo');
         tester.onRequest = () => {
           tester.respond(500, { });
           expectFailure(promise, done, '');
@@ -556,7 +572,7 @@ describe('session', () => {
       });
 
       it('should fail for improper model', (done) => {
-        let promise = session.rename('foo');
+        let promise = session.setPath('foo');
         tester.onRequest = () => {
           tester.respond(200, { });
           expectFailure(promise, done);
@@ -565,7 +581,97 @@ describe('session', () => {
 
       it('should fail if the session is disposed', (done) => {
         session.dispose();
-        let promise = session.rename('foo');
+        let promise = session.setPath('foo');
+        expectFailure(promise, done, 'Session is disposed');
+      });
+
+    });
+
+    context('#setType()', () => {
+
+      it('should set the type of the session', () => {
+        let model = { ...session.model, type: 'foo' };
+        tester.onRequest = () => {
+          tester.respond(200, model);
+        };
+        return session.setType(model.type).then(() => {
+          expect(session.type).to.be(model.type);
+          session.dispose();
+        });
+      });
+
+      it('should fail for improper response status', (done) => {
+        let promise = session.setType('foo');
+        tester.onRequest = () => {
+          tester.respond(201, { });
+          expectFailure(promise, done);
+        };
+      });
+
+      it('should fail for error response status', (done) => {
+        let promise = session.setType('foo');
+        tester.onRequest = () => {
+          tester.respond(500, { });
+          expectFailure(promise, done, '');
+        };
+      });
+
+      it('should fail for improper model', (done) => {
+        let promise = session.setType('foo');
+        tester.onRequest = () => {
+          tester.respond(200, { });
+          expectFailure(promise, done);
+        };
+      });
+
+      it('should fail if the session is disposed', (done) => {
+        session.dispose();
+        let promise = session.setPath('foo');
+        expectFailure(promise, done, 'Session is disposed');
+      });
+
+    });
+
+    context('#setName()', () => {
+
+      it('should set the name of the session', () => {
+        let model = { ...session.model, name: 'foo' };
+        tester.onRequest = () => {
+          tester.respond(200, model);
+        };
+        return session.setName(model.name).then(() => {
+          expect(session.name).to.be(model.name);
+          session.dispose();
+        });
+      });
+
+      it('should fail for improper response status', (done) => {
+        let promise = session.setName('foo');
+        tester.onRequest = () => {
+          tester.respond(201, { });
+          expectFailure(promise, done);
+        };
+      });
+
+      it('should fail for error response status', (done) => {
+        let promise = session.setName('foo');
+        tester.onRequest = () => {
+          tester.respond(500, { });
+          expectFailure(promise, done, '');
+        };
+      });
+
+      it('should fail for improper model', (done) => {
+        let promise = session.setName('foo');
+        tester.onRequest = () => {
+          tester.respond(200, { });
+          expectFailure(promise, done);
+        };
+      });
+
+      it('should fail if the session is disposed', (done) => {
+        session.dispose();
+        let promise = session.setPath('foo');
         expectFailure(promise, done, 'Session is disposed');
       });
 
@@ -610,8 +716,7 @@ describe('session', () => {
       });
 
       it('should update the session path if it has changed', () => {
-        let model = session.model;
-        model.notebook.path = 'foo.ipynb';
+        let model = { ...session.model, path: 'foo.ipynb' };
         let name = model.kernel.name;
         let id = model.kernel.id;
         tester.onRequest = request => {
@@ -623,7 +728,7 @@ describe('session', () => {
         };
         return session.changeKernel({ name }).then(kernel => {
           expect(kernel.name).to.be(name);
-          expect(session.path).to.be(model.notebook.path);
+          expect(session.path).to.be(model.path);
           session.dispose();
         });
       });

+ 10 - 6
packages/services/test/src/session/validate.spec.ts

@@ -16,21 +16,25 @@ describe('session/validate', () => {
 
   describe('#validateModel()', () => {
 
-    it('should pass a valid id', () => {
-      let id: Session.IModel = {
+    it('should pass a valid model', () => {
+      let model: Session.IModel = {
         id: 'foo',
         kernel: { name: 'foo', id: '123'},
-        notebook: { path: 'bar' }
+        path: 'bar',
+        name: '',
+        type: ''
       };
-      validateModel(id);
+      validateModel(model);
     });
 
     it('should fail on missing data', () => {
-      let id: Session.IModel = {
+      let model: any = {
         id: 'foo',
         kernel: { name: 'foo', id: '123'},
+        path: 'bar',
+        name: ''
       };
-      expect(() => validateModel(id)).to.throwError();
+      expect(() => validateModel(model)).to.throwError();
     });
 
   });

+ 3 - 3
packages/services/test/src/utils.ts

@@ -275,9 +275,9 @@ class RequestHandler {
         name: 'python',
         id: uuid()
       },
-      notebook: {
-        path: uuid()
-      }
+      path: uuid(),
+      type: '',
+      name: ''
     };
     switch (request.method) {
     case 'PATCH':

+ 1 - 1
setup.py

@@ -106,7 +106,7 @@ setup_args['cmdclass'] = cmdclass
 
 setuptools_args = {}
 install_requires = setuptools_args['install_requires'] = [
-    'notebook>=4.2.0',
+    'notebook>=4.3.1',
     'jupyterlab_launcher>=0.2.8'
 ]