Skip to content

Instantly share code, notes, and snippets.

@blink1073
Last active February 23, 2017 11:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blink1073/fe1e8505c2caf084593cffa611bfb90d to your computer and use it in GitHub Desktop.
Save blink1073/fe1e8505c2caf084593cffa611bfb90d to your computer and use it in GitHub Desktop.
JupyterLab models
interface ISignal<T, U> { };
interface ITextSelection { };
interface JSONValue { };
interface JSONObject { };
interface IMap<T> extends Map<string, T> { };
interface JSONMap extends IMap<JSONValue> { };
// Editor
interface IEditorModel {
readonly changed: ISignal<this, EditorChangedArgs>;
readonly uuid: string;
source: string;
mimetype: string;
selections: IMap<ITextSelection[]>;
fromJSON(value: JSONObject): void;
toJSON(): JSONObject;
}
interface ISelectionChanged {
name: 'uuid';
value: string;
}
interface IEditorValueChanged {
name: keyof IEditorModel;
}
type EditorChangedArgs = ISelectionChanged | IEditorValueChanged;
// Notebook
interface INotebookModel {
changed: ISignal<this, NotebookChangedArgs>;
readonly cells: IMap<ICellModel>;
cellOrder: ReadonlyArray<string>;
readonly metadata: JSONMap;
readonly nbformat: number;
readonly nbformatMinor: number;
fromJSON(value: JSONObject): void;
toJSON(): JSONObject;
}
interface INotebookMapChanged {
name: 'cells' | 'metadata';
key: string;
}
interface INotebookValueChanged {
name: keyof INotebookModel;
}
type NotebookChangedArgs = INotebookMapChanged | INotebookValueChanged;
// Cell
interface ICellModel extends IEditorModel {
changed: ISignal<this, CellChangedArgs>;
readonly cellType: 'code' | 'raw' | 'markdown';
readonly metadata: JSONMap;
trusted: boolean;
}
interface ICellMapChanged {
name: 'metadata' | 'outputs';
key: string;
}
interface ICellValueChanged {
name: keyof ICellModel;
}
type CellChangedArgs = ICellMapChanged | ICellValueChanged;
// Code Cell
interface ICodeCellModel extends ICellModel {
readonly changed: ISignal<this, CodeCellChangedArgs>;
readonly cellType: 'code';
readonly outputs: IMap<IOutputModel>;
outputOrder: ReadonlyArray<string>;
}
interface ICodeCellValueChanged {
name: keyof ICodeCellModel;
}
type CodeCellChangedArgs = ICellMapChanged | ICodeCellValueChanged;
// Output
interface IOutputModel {
readonly changed: ISignal<this, IOutputChangedArgs>;
readonly uuid: string;
readonly outputType: string;
readonly executionCount: number | null;
readonly data: JSONMap
readonly metadata: JSONMap;
toJSON(): JSONObject;
}
interface IOutputChangedArgs {
name: 'data' | 'metadata';
key: string;
}
// Demo change args
var a: IOutputChangedArgs = {
name: 'data',
key: 'foo'
};
console.log(b.key);
var b: IOutputChangedArgs = {
name: 'metadata',
key: 'bar'
};
console.log(c.key);
// Renderer Model
interface IRenderModel {
changed: ISignal<this, IRendererChangedArgs>;
readonly trusted: boolean;
readonly data: JSONMap;
readonly metadata: JSONMap;
}
interface IRendererChangedArgs {
name: 'data' | 'metadata';
key: string;
}
// For the metadata editor, we pass:
interface IOptions {
source: JSONMap;
}
// Then call update() when the source changes
interface ISignal<T, U> { };
interface ITextSelection { };
interface JSONValue { };
interface JSONObject { };
interface IMap<T> extends Map<string, T> { };
interface JSONMap extends IMap<JSONValue> { };
// Follows IPEP 27
// File
interface IFile {
changed: ISignal<this, keyof IFile>;
readonly path: string;
mimetype: string;
readonly writable: boolean;
readonly format: 'text' | 'base64' | 'json';
readonly type: 'file' | 'notebook' | 'directory';
}
interface IMutableFile {
canUndo(): boolean;
undo(): void;
canRedo(): boolean;
redo(): void;
beginCompoundOperation(): void;
endCompoundOperation(): void;
}
// Text file
interface ITextFile extends IFile, IMutableFile {
changed: ISignal<this, keyof ITextFile>;
readonly format: 'text';
readonly type: 'file';
readonly selections: IMap<ITextSelection[]>;
source: string;
}
// Base 64
interface IBase64File extends IFile {
changed: ISignal<this, keyof IBase64File>;
readonly format: 'base64';
readonly type: 'file';
source: string;
}
// Notebook
interface INotebookFile extends IFile, IMutableFile {
readonly format: 'json';
readonly type: 'notebook';
readonly changed: ISignal<this, keyof INotebookFile>;
getCursor(path: string[]): ICursor;
setIn(path: string[], value: JSONValue);
getIn(path: string[]): JSONValue;
fromJSON(value: JSONValue): void;
toJSON(): JSONValue;
}
interface ICursor {
readonly changed: ISignal<this, void>;
readonly path: ReadonlyArray<string>;
value: JSONValue;
}
// Directory
interface IDirectory extends IFile {
readonly changed: ISignal<this, keyof IDirectory>;
readonly format: 'json';
readonly type: 'directory';
readonly files: ReadonlyArray<string>;
}

Problem Statement

We need a model to represent documents and notebooks that is type-safe and can be backed by a local or remote source.
It should support change notification and modification at any level of nesting. It would ideally support undo/redo for all document changes.

Considerations

  • We have a notebook document model backed by a JSON schema

  • We have an editor model that includes mimeType and selections

  • The live notebook model is a hybrid of the two, where each cell entry is also a sub-document.

  • The rendermime objects need to be able to influence the output data

  • The output area needs to be able to influence the outputs (merging stream data)

  • The cells need to be able to modify everything in at their level and below.

  • We need changes at any of these levels to propagate up the chain to the root model.

  • We need the source of truth of the data to be abstracted - can be local or remote.

  • We need the model to be type-safe.

  • We would like to support undo/redo for the entire document.

Current approach

A model hierarchy with a cascade of signals, where each model is independent until they are serialized. Model "dirtiness" is handled by propagating changes to the root model. The attributes of the models have their own ad-hoc signals for things like "mimeTypeChanged", making it harder to swap out the backend.

Approach with opaque elements

interface IModel<T> {
    changed: ISignal<this, keyof T>;
    set<K extends keyof T>(key: K, value: T[K]): void;
}

interface IMyParams {
   readonly foo: string;
   readonly bar: boolean;
}

interface IMyModel extends IModel<IMyParams>, IMyParams {}

Approach with JSONDb backend

We have an abstract JSON store for each model. We have get/set in the JSON store by path. The models used in the application present type-safe views into the JSON store. Use uuids for cells since they can be reordered, and an array of uuids for the order.

TODO:

  • Figure out the mapping of model path to JSON store - should it be in ContentsManager by url?
  • Investigate rethinkdb handling of array types (DONE)
    • Can only listen to changes to an array as a whole - get an oldval and newval
    • Can make changes to array elements
  • Present an API that can be implemented by both a simple local store and Google Drive - canUndo, undo, etc.

Details

  • There should only be one DocumentManager and one ContentsManager.

  • There should be an app-level DirectoryHandler which is used by the default file browser, launcher, etc.

  • There can be other DirectoryHandlers that also use the DocumentManager and ContentsManager.

  • This means the "cwd" can be any url that the contents manager understands.

  • The ContentsManager fetches a model for a url - either a string or a JSONObject that is backed by a store. The contents manager enforces the final data to and from disk.

  • We can associate other values with the backend store.

Queries:

- uuid/mimeType - string
- uuid/selections - Map
- uuid/text - string

- notebookUuid/cellUuid/mimeType - string
- notebookUuid/cellUuid/selections - Map
- notebookUuid/cellUuid/text - string
- notebookUUid/cellUUid/outputUUid/data/ - Map
interface IModel {
   stateChanged: ISignal<this, void>;
   fromJSON(value: JSONObject): void;
   toJSON(): JSONObject;
}


interface IEditorModel extends IModel {
    readonly mimeType: IObservableValue<string>;
    readonly text: IObservableValue<string>;
    readonly selections(): IObservableMap<ITextSelection[]>;
}


class EditorModel implements IEditorModel {
    constructor(backend: IJSONBackend, path: string[]) {
        this.mimeType = new ObservableValue<string>(backend, path.concat('mimeType'));
        this.text = new ObservableValue<string>(backend, path.concat('text'))
        this.mimeType.changed.connect(this._onChange, this);
        this.text.changed.connect(this._onChange, this);
        let selectionsPath = path.concat('selections');
        this.selections = new ObservableMap(backend, selectionsPath);
        this.selections.changed.connect(this._onChange, this);
    }

    stateChanged: ISignal<this, void>;

    readonly mimeType: IObservableValue<string>;

    readonly text: IObservableValue<string>;

    readonly selections(): IObservableMap<ITextSelection[]>;

    fromJSON(value: JSONObject): void {
        this.mimeType.setValue(value['mimeType'] || 'text/plain');
        this.text.setValue(value['text'] || '');
        this.selections.fromJSON(value.selections || {});
    }

    toJSON(): JSONObject {
        return {
            "mimeType": this.mimeType.getValue(),
            "text": this.text.getValue(),
            "selections": this.selections.toJSON()
        }
    }

    private _onChange() {
        this.changed.emit());
    }
}

What would we need observablelist for?

Currently: list of cells, list of outputs

We can just make the notebook and output area widget arrange as appropriate on a bulk change.

Notes from Conversation with Chris and Ian on 22 Feb

  • We treat the root of a path as its "drive".

  • The contents manager uses models for files that are backed by the appropriate drive - akin to the Windows Explorer mapping network drives.

  • Drives must present a file-hierarchy structure, even if they are implemented in an exotic fashion.

  • Drives are registered on the contents manager - the base handler is for the '/' drive.

  • The contents model has an api for setting/getting nested values, which could notionally be based on a delimited path.

  • The contents model should also have an api for signaling changes, which could nominally be the path, oldValue, and newValue.

  • The contents model should allow for metadata associated with the file, that is not persisted to disk, such as user selections.

  • The contents model should support an undo interface - even if not implemented - canUndo, canRedo, redo, batchOperation, etc.

  • The models built on top of the content model will present type-safe properties and changed signals that call into the contents model.

Example:

interface ICodeCellModel {
    changed: ISignal<this, ICellChangedArgs>;

    source: string;

    metadata: IObservableJSON;

    outputs: IObservableMap<IOutput>;

    outputOrder: string[];
}

Where ICellChangedArgs is type-safe for the given property name and change. Changes to a simple value would include newValue/oldValue, and to a map would include the key.

@ian-r-rose
Copy link

Some more thoughts/replies:

Problem Statement

We need a model to represent documents and notebooks that is type-safe and can be backed by a
local or remote source.
It should support change notification and modification at any level of nesting.
It would ideally support undo/redo for all document changes.

Considerations

  • We have a notebook document model backed by a JSON schema
  • We have an editor model that includes mimeType and selections
  • The live notebook model is a hybrid of the two, where each cell entry
    is also a sub-document.

This also relates to our discussion here with regards to synchronized-persisted vs synchronized-ephemeral data structures. It is limiting to restrict ourselves to things that are only in the JSON schema.

  • The rendermime objects need to be able to influence the output data
  • The output area needs to be able to influence the outputs (merging
    stream data)
  • The cells need to be able to modify everything in at their level and below.
  • We need changes at any of these levels to propagate up the chain to
    the root model.

Do we need this? That is, should change notifications on the model be deep or shallow?

  • We need the source of truth of the data to be abstracted - can
    be local or remote.
  • We need the model to be type-safe.

Agreed, though this results in some difficulties at the interface with APIs like the Google Realtime API.

  • We would like to support undo/redo for the entire document.

Current approach

A model hierarchy with a cascade of signals, where each model is
independent until they are serialized. Model "dirtiness" is handled
by propagating changes to the root model. The attributes of the models
have their own ad-hoc signals for things like "mimeTypeChanged", making
it harder to swap out the backend.

I would also add that, because models tend to be objects containing heterogeneous
data types, this makes static typing in the backend a challenge, since it needs to
understand how to handle synchronization of several different types which
may have different semantics.

Approach with opaque elements

interface IModel<T> {
   changed: ISignal<this, keyof T>;
   set<K extends keyof T>(key: K, value: T[K]): void;
}

interface IMyParams {
readonly foo: string;
readonly bar: boolean;
}

interface IMyModel extends IModel, IMyParams {}

Approach with JSONDb backend

We have an abstract JSON store for each model.
We have get/set in the JSON store by path.
The models used in the application present type-safe views into the JSON store.
Use uuids for cells since they can be reordered, and an array of uuids for the order.
I have also had conversations with @Carreau to this effect, where uuids for cells
can be used for other things like alternative layouts for dashboarding. Even if we did this,
it would still probably worthwhile to work through how a list should work with this scheme.

TODO:

  • Figure out the mapping of model path to JSON store - should it be in ContentsManager by url?
  • Investigate rethinkdb handling of array types (DONE)
    • Can only listen to changes to an array as a whole - get an oldval and newval
    • Can make changes to array elements
  • Present an API that can be implemented by both a simple local store and Google Drive - canUndo, undo, etc.

Details

  • There should only be one DocumentManager and one ContentsManager.
    For the whole of lab? This seems kind of limiting. Where would they delegate the logic of using different backends?
  • There should be an app-level DirectoryHandler which is used by the
    default file browser, launcher, etc.
  • There can be other DirectoryHandlers that also use the DocumentManager
    and ContentsManager.
  • This means the "cwd" can be any url that the contents manager understands.
  • The ContentsManager fetches a model for a url - either a string or a JSONObject
    that is backed by a store. The contents manager enforces
    the final data to and from disk.
  • We can associate other values with the backend store.

Queries:

- uuid/mimeType - string
- uuid/selections - Map
- uuid/text - string
  • notebookUuid/cellUuid/mimeType - string
  • notebookUuid/cellUuid/selections - Map
  • notebookUuid/cellUuid/text - string
  • notebookUUid/cellUUid/outputUUid/data/ - Map

interface IModel {
stateChanged: ISignal<this, void>;
fromJSON(value: JSONObject): void;
toJSON(): JSONObject;
}

interface IEditorModel extends IModel {
readonly mimeType: IObservableValue;
readonly text: IObservableValue;
readonly selections(): IObservableMap<ITextSelection[]>;
}

class EditorModel implements IEditorModel {
constructor(backend: IJSONBackend, path: string[]) {
this.mimeType = new ObservableValue(backend, path.concat('mimeType'));
this.text = new ObservableValue(backend, path.concat('text'))
this.mimeType.changed.connect(this._onChange, this);
this.text.changed.connect(this._onChange, this);
let selectionsPath = path.concat('selections');
this.selections = new ObservableMap(backend, selectionsPath);
this.selections.changed.connect(this._onChange, this);
}

stateChanged: ISignal<this, void>;

readonly mimeType: IObservableValue;

readonly text: IObservableValue;

readonly selections(): IObservableMap<ITextSelection[]>;

fromJSON(value: JSONObject): void {
this.mimeType.setValue(value['mimeType'] || 'text/plain');
this.text.setValue(value['text'] || '');
this.selections.fromJSON(value.selections || {});
}

toJSON(): JSONObject {
return {
"mimeType": this.mimeType.getValue(),
"text": this.text.getValue(),
"selections": this.selections.toJSON()
}
}

private _onChange() {
this.changed.emit());
}
}

In any event, I think that an IObservableValue<T> is a good idea.

What would we need observablelist for?

Currently: list of cells, list of outputs

We can just make the notebook and output area widget arrange
as appropriate on a bulk change.

@sccolbert
Copy link

The contents model should also have an api for signaling changes, which could nominally be the path, oldValue, and newValue.

I'm not sure I agree with this. I think the contents model (the JSON db blob thing) should allow you to subscribe to changes based on some query syntax, rather than indiscriminately firing off a signal for something no one cares about.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment