Massive work on the notebook document format.

* Finished nbformat work and debugged the versioning API.
* Integrated the nbformat with the notebook. Save/New/Open/Export
  are all now working.
Brian E. Granger 15 years ago
parent 4973135bf1
commit b11824ef1b

@ -4,15 +4,14 @@
# Imports
#-----------------------------------------------------------------------------
import datetime
import json
import logging
import os
import urllib
from tornado import web
from tornado import websocket
#-----------------------------------------------------------------------------
# Handlers
#-----------------------------------------------------------------------------
@ -20,7 +19,16 @@ from tornado import websocket
class MainHandler(web.RequestHandler):
def get(self):
self.render('notebook.html')
notebook_id = self.application.notebook_manager.new_notebook()
self.render('notebook.html', notebook_id=notebook_id)
class NamedNotebookHandler(web.RequestHandler):
def get(self, notebook_id):
nbm = self.application.notebook_manager
if not nbm.notebook_exists(notebook_id):
raise web.HTTPError(404)
self.render('notebook.html', notebook_id=notebook_id)
class KernelHandler(web.RequestHandler):
@ -30,6 +38,7 @@ class KernelHandler(web.RequestHandler):
def post(self):
kernel_id = self.application.start_kernel()
self.set_header('Location', '/'+kernel_id)
self.write(json.dumps(kernel_id))
@ -65,52 +74,52 @@ class ZMQStreamHandler(websocket.WebSocketHandler):
class NotebookRootHandler(web.RequestHandler):
def get(self):
files = os.listdir(os.getcwd())
files = [file for file in files if file.endswith(".ipynb")]
nbm = self.application.notebook_manager
files = nbm.list_notebooks()
self.write(json.dumps(files))
def post(self):
nbm = self.application.notebook_manager
body = self.request.body.strip()
format = self.get_argument('format', default='json')
if body:
notebook_id = nbm.save_new_notebook(body, format)
else:
notebook_id = nbm.new_notebook()
self.set_header('Location', '/'+notebook_id)
self.write(json.dumps(notebook_id))
class NotebookHandler(web.RequestHandler):
SUPPORTED_METHODS = ("GET", "DELETE", "PUT")
def find_path(self, filename):
filename = urllib.unquote(filename)
if not filename.endswith('.ipynb'):
raise web.HTTPError(400)
path = os.path.join(os.getcwd(), filename)
return path
class NotebookHandler(web.RequestHandler):
def get(self, filename):
path = self.find_path(filename)
if not os.path.isfile(path):
raise web.HTTPError(404)
info = os.stat(path)
self.set_header("Content-Type", "application/unknown")
self.set_header("Last-Modified", datetime.datetime.utcfromtimestamp(
info.st_mtime))
f = open(path, "r")
try:
self.finish(f.read())
finally:
f.close()
def put(self, filename):
path = self.find_path(filename)
f = open(path, "w")
f.write(self.request.body)
f.close()
SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
def get(self, notebook_id):
nbm = self.application.notebook_manager
format = self.get_argument('format', default='json')
last_mod, name, data = nbm.get_notebook(notebook_id, format)
if format == u'json':
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename=%s.json' % name)
elif format == u'xml':
self.set_header('Content-Type', 'text/xml')
self.set_header('Content-Disposition','attachment; filename=%s.ipynb' % name)
elif format == u'py':
self.set_header('Content-Type', 'text/plain')
self.set_header('Content-Disposition','attachment; filename=%s.py' % name)
self.set_header('Last-Modified', last_mod)
self.finish(data)
def put(self, notebook_id):
nbm = self.application.notebook_manager
format = self.get_argument('format', default='json')
nbm.save_notebook(notebook_id, self.request.body, format)
self.set_status(204)
self.finish()
def delete(self, filename):
path = self.find_path(filename)
if not os.path.isfile(path):
raise web.HTTPError(404)
os.unlink(path)
def delete(self, notebook_id):
nbm = self.application.notebook_manager
nbm.delete_notebook(notebook_id)
self.set_status(204)
self.finish()

@ -27,13 +27,15 @@ tornado.ioloop = ioloop
from tornado import httpserver
from tornado import web
from kernelmanager import KernelManager
from sessionmanager import SessionManager
from handlers import (
MainHandler, KernelHandler, KernelActionHandler, ZMQStreamHandler,
from .kernelmanager import KernelManager
from .sessionmanager import SessionManager
from .handlers import (
MainHandler, NamedNotebookHandler,
KernelHandler, KernelActionHandler, ZMQStreamHandler,
NotebookRootHandler, NotebookHandler
)
from routers import IOPubStreamRouter, ShellStreamRouter
from .routers import IOPubStreamRouter, ShellStreamRouter
from .notebookmanager import NotebookManager
from IPython.core.application import BaseIPythonApplication
from IPython.core.profiledir import ProfileDir
@ -53,6 +55,7 @@ from IPython.utils.traitlets import Dict, Unicode, Int, Any, List, Enum
_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
_kernel_action_regex = r"(?P<action>restart|interrupt)"
_notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
LOCALHOST = '127.0.0.1'
@ -65,12 +68,13 @@ class NotebookWebApplication(web.Application):
def __init__(self, kernel_manager, log, kernel_argv, config):
handlers = [
(r"/", MainHandler),
(r"/%s" % _notebook_id_regex, NamedNotebookHandler),
(r"/kernels", KernelHandler),
(r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
(r"/kernels/%s/iopub" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='iopub')),
(r"/kernels/%s/shell" % _kernel_id_regex, ZMQStreamHandler, dict(stream_name='shell')),
(r"/notebooks", NotebookRootHandler),
(r"/notebooks/([^/]+)", NotebookHandler)
(r"/notebooks/%s" % _notebook_id_regex, NotebookHandler)
]
settings = dict(
template_path=os.path.join(os.path.dirname(__file__), "templates"),
@ -84,6 +88,7 @@ class NotebookWebApplication(web.Application):
self.config = config
self._routers = {}
self._session_dict = {}
self.notebook_manager = NotebookManager(config=self.config)
#-------------------------------------------------------------------------
# Methods for managing kernels and sessions

@ -0,0 +1,195 @@
#-----------------------------------------------------------------------------
# Copyright (C) 2011 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING.txt, distributed as part of this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
import datetime
import os
import uuid
from tornado import web
from IPython.config.configurable import Configurable
from IPython.nbformat import current
from IPython.utils.traitlets import Unicode, List, Dict
#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
class NotebookManager(Configurable):
notebook_dir = Unicode(os.getcwd())
filename_ext = Unicode(u'.ipynb')
allowed_formats = List([u'json',u'xml',u'py'])
# Map notebook_ids to notebook names
mapping = Dict()
# Map notebook names to notebook_ids
rev_mapping = Dict()
def list_notebooks(self):
"""List all notebooks in the notebook dir.
This returns a list of dicts of the form::
dict(notebook_id=notebook,name=name)
"""
names = os.listdir(self.notebook_dir)
names = [name.strip(self.filename_ext)\
for name in names if name.endswith(self.filename_ext)]
data = []
for name in names:
if name not in self.rev_mapping:
notebook_id = self.new_notebook_id(name)
else:
notebook_id = self.rev_mapping[name]
data.append(dict(notebook_id=notebook_id,name=name))
return data
def new_notebook_id(self, name):
"""Generate a new notebook_id for a name and store its mappings."""
notebook_id = unicode(uuid.uuid4())
self.mapping[notebook_id] = name
self.rev_mapping[name] = notebook_id
return notebook_id
def delete_notebook_id(self, notebook_id):
"""Delete a notebook's id only. This doesn't delete the actual notebook."""
name = self.mapping[notebook_id]
del self.mapping[notebook_id]
del self.rev_mapping[name]
def notebook_exists(self, notebook_id):
"""Does a notebook exist?"""
if notebook_id not in self.mapping:
return False
path = self.get_path_by_name(self.mapping[notebook_id])
if not os.path.isfile(path):
return False
return True
def find_path(self, notebook_id):
"""Return a full path to a notebook given its notebook_id."""
try:
name = self.mapping[notebook_id]
except KeyError:
raise web.HTTPError(404)
return self.get_path_by_name(name)
def get_path_by_name(self, name):
"""Return a full path to a notebook given its name."""
filename = name + self.filename_ext
path = os.path.join(self.notebook_dir, filename)
return path
def get_notebook(self, notebook_id, format=u'json'):
"""Get the representation of a notebook in format by notebook_id."""
format = unicode(format)
if format not in self.allowed_formats:
raise web.HTTPError(415)
last_modified, nb = self.get_notebook_object(notebook_id)
data = current.writes(nb, format)
name = nb.get('name','notebook')
return last_modified, name, data
def get_notebook_object(self, notebook_id):
"""Get the NotebookNode representation of a notebook by notebook_id."""
path = self.find_path(notebook_id)
if not os.path.isfile(path):
raise web.HTTPError(404)
info = os.stat(path)
last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
try:
with open(path,'r') as f:
s = f.read()
try:
# v2 and later have xml in the .ipynb files
nb = current.reads(s, 'xml')
except:
# v1 had json in the .ipynb files
nb = current.reads(s, 'json')
except:
raise web.HTTPError(404)
return last_modified, nb
def save_new_notebook(self, data, format=u'json'):
"""Save a new notebook and return its notebook_id."""
if format not in self.allowed_formats:
raise web.HTTPError(415)
try:
nb = current.reads(data, format)
except:
raise web.HTTPError(400)
try:
name = nb.name
except AttributeError:
raise web.HTTPError(400)
notebook_id = self.new_notebook_id(name)
self.save_notebook_object(notebook_id, nb)
return notebook_id
def save_notebook(self, notebook_id, data, format=u'json'):
"""Save an existing notebook by notebook_id."""
if format not in self.allowed_formats:
raise web.HTTPError(415)
try:
nb = current.reads(data, format)
except:
raise web.HTTPError(400)
self.save_notebook_object(notebook_id, nb)
def save_notebook_object(self, notebook_id, nb):
"""Save an existing notebook object by notebook_id."""
if notebook_id not in self.mapping:
raise web.HTTPError(404)
old_name = self.mapping[notebook_id]
try:
new_name = nb.name
except AttributeError:
raise web.HTTPError(400)
path = self.get_path_by_name(new_name)
try:
with open(path,'w') as f:
current.write(nb, f, u'xml')
except:
raise web.HTTPError(400)
if old_name != new_name:
old_path = self.get_path_by_name(old_name)
if os.path.isfile(old_path):
os.unlink(old_path)
self.mapping[notebook_id] = new_name
self.rev_mapping[new_name] = notebook_id
def delete_notebook(self, notebook_id):
"""Delete notebook by notebook_id."""
path = self.find_path(notebook_id)
if not os.path.isfile(path):
raise web.HTTPError(404)
os.unlink(path)
self.delete_notebook_id(notebook_id)
def new_notebook(self):
"""Create a new notebook and returns its notebook_id."""
i = 0
while True:
name = u'Untitled%i' % i
path = self.get_path_by_name(name)
if not os.path.isfile(path):
break
else:
i = i+1
notebook_id = self.new_notebook_id(name)
nb = current.new_notebook(name=name, id=notebook_id)
with open(path,'w') as f:
current.write(nb, f, u'xml')
return notebook_id

@ -206,6 +206,13 @@ span.button_label {
font-size: 77%;
}
#download_format {
float: right;
font-size: 85%;
width: 60px;
margin: 1px 5px;
}
div#left_panel_splitter {
width: 8px;
top: 0px;

@ -302,18 +302,28 @@ var IPython = (function (IPython) {
CodeCell.prototype.fromJSON = function (data) {
if (data.cell_type === 'code') {
this.set_code(data.code);
this.set_input_prompt(data.prompt_number);
if (data.input !== undefined) {
this.set_code(data.input);
}
if (data.prompt_number !== undefined) {
this.set_input_prompt(data.prompt_number);
} else {
this.set_input_prompt();
};
};
};
CodeCell.prototype.toJSON = function () {
return {
code : this.get_code(),
cell_type : 'code',
prompt_number : this.input_prompt_number
var data = {}
data.input = this.get_code();
data.cell_type = 'code';
if (this.input_prompt_number !== ' ') {
data.prompt_number = this.input_prompt_number
};
data.outputs = [];
data.language = 'python';
return data;
};
IPython.CodeCell = CodeCell;

@ -14,14 +14,9 @@ var IPython = (function (IPython) {
this.next_prompt_number = 1;
this.kernel = null;
this.msg_cell_map = {};
this.filename = null;
this.notebook_load_re = /%notebook load/
this.notebook_save_re = /%notebook save/
this.notebook_filename_re = /(\w)+.ipynb/
this.style();
this.create_elements();
this.bind_events();
this.start_kernel();
};
@ -473,24 +468,8 @@ var IPython = (function (IPython) {
if (cell instanceof IPython.CodeCell) {
cell.clear_output();
var code = cell.get_code();
if (that.notebook_load_re.test(code)) {
// %notebook load
var code_parts = code.split(' ');
if (code_parts.length === 3) {
that.load_notebook(code_parts[2]);
};
} else if (that.notebook_save_re.test(code)) {
// %notebook save
var code_parts = code.split(' ');
if (code_parts.length === 3) {
that.save_notebook(code_parts[2]);
} else {
that.save_notebook()
};
} else {
var msg_id = that.kernel.execute(cell.get_code());
that.msg_cell_map[msg_id] = cell.cell_id;
};
var msg_id = that.kernel.execute(cell.get_code());
that.msg_cell_map[msg_id] = cell.cell_id;
} else if (cell instanceof IPython.TextCell) {
cell.render();
}
@ -532,18 +511,22 @@ var IPython = (function (IPython) {
// Always delete cell 0 as they get renumbered as they are deleted.
this.delete_cell(0);
};
var new_cells = data.cells;
ncells = new_cells.length;
var cell_data = null;
for (var i=0; i<ncells; i++) {
cell_data = new_cells[i];
if (cell_data.cell_type == 'code') {
this.insert_code_cell_after();
this.selected_cell().fromJSON(cell_data);
} else if (cell_data.cell_type === 'text') {
this.insert_text_cell_after();
this.selected_cell().fromJSON(cell_data);
};
// Only handle 1 worksheet for now.
var worksheet = data.worksheets[0];
if (worksheet !== undefined) {
var new_cells = worksheet.cells;
ncells = new_cells.length;
var cell_data = null;
for (var i=0; i<ncells; i++) {
cell_data = new_cells[i];
if (cell_data.cell_type == 'code') {
this.insert_code_cell_after();
this.selected_cell().fromJSON(cell_data);
} else if (cell_data.cell_type === 'text') {
this.insert_text_cell_after();
this.selected_cell().fromJSON(cell_data);
};
};
};
};
@ -555,67 +538,66 @@ var IPython = (function (IPython) {
for (var i=0; i<ncells; i++) {
cell_array[i] = cells[i].toJSON();
};
json = {
cells : cell_array
data = {
// Only handle 1 worksheet for now.
worksheets : [{cells:cell_array}]
}
return data
};
Notebook.prototype.save_notebook = function () {
if (IPython.save_widget.test_notebook_name()) {
var notebook_id = IPython.save_widget.get_notebook_id();
var nbname = IPython.save_widget.get_notebook_name();
// We may want to move the name/id/nbformat logic inside toJSON?
var data = this.toJSON();
data.name = nbname;
data.nbformat = 2;
data.id = notebook_id
// We do the call with settings so we can set cache to false.
var settings = {
processData : false,
cache : false,
type : "PUT",
data : JSON.stringify(data),
success : $.proxy(this.notebook_saved,this)
};
IPython.save_widget.status_saving();
$.ajax("/notebooks/" + notebook_id, settings);
};
return json
};
Notebook.prototype.test_filename = function (filename) {
if (this.notebook_filename_re.test(filename)) {
return true;
} else {
var bad_filename = $('<div/>');
bad_filename.html(
"The filename you entered (" + filename + ") is not valid. Notebook filenames must have the following form: foo.ipynb"
);
bad_filename.dialog({title: 'Invalid filename', modal: true});
return false;
};
};
Notebook.prototype.save_notebook = function (filename) {
this.filename = filename || this.filename || '';
if (this.filename === '') {
var no_filename = $('<div/>');
no_filename.html(
"This notebook has no filename, please specify a filename of the form: foo.ipynb"
);
no_filename.dialog({title: 'Missing filename', modal: true});
return;
}
if (!this.test_filename(this.filename)) {return;}
var thedata = this.toJSON();
var settings = {
processData : false,
cache : false,
type : "PUT",
data : JSON.stringify(thedata),
success : function (data, status, xhr) {console.log(data);}
};
$.ajax("/notebooks/" + this.filename, settings);
};
Notebook.prototype.notebook_saved = function (data, status, xhr) {
IPython.save_widget.status_save();
}
Notebook.prototype.load_notebook = function (filename) {
if (!this.test_filename(filename)) {return;}
var that = this;
Notebook.prototype.load_notebook = function () {
var notebook_id = IPython.save_widget.get_notebook_id();
// We do the call with settings so we can set cache to false.
var settings = {
processData : false,
cache : false,
type : "GET",
dataType : "json",
success : function (data, status, xhr) {
that.fromJSON(data);
that.filename = filename;
that.kernel.restart();
}
success : $.proxy(this.notebook_loaded,this)
};
$.ajax("/notebooks/" + filename, settings);
IPython.save_widget.status_loading();
$.ajax("/notebooks/" + notebook_id, settings);
}
Notebook.prototype.notebook_loaded = function (data, status, xhr) {
this.fromJSON(data);
if (this.ncells() === 0) {
this.insert_code_cell_after();
};
IPython.save_widget.status_save();
IPython.save_widget.set_notebook_name(data.name);
this.start_kernel();
};
IPython.Notebook = Notebook;
return IPython;

@ -29,15 +29,19 @@ $(document).ready(function () {
IPython.kernel_status_widget = new IPython.KernelStatusWidget('#kernel_status');
IPython.kernel_status_widget.status_idle();
IPython.layout_manager.do_resize();
IPython.notebook.insert_code_cell_after();
IPython.layout_manager.do_resize();
// These have display: none in the css file and are made visible here to prevent FLOUC.
$('div#header').css('display','block');
$('div#notebook_app').css('display','block');
IPython.layout_manager.do_resize();
IPython.pager.collapse();
IPython.layout_manager.do_resize();
IPython.notebook.load_notebook();
// Perform these actions after the notebook has been loaded.
setTimeout(function () {
IPython.save_widget.update_url();
IPython.layout_manager.do_resize();
IPython.pager.collapse();
}, 100);
});

@ -9,6 +9,7 @@ var IPython = (function (IPython) {
var SaveWidget = function (selector) {
this.selector = selector;
this.notebook_name_re = /[^/\\]+/
if (this.selector !== undefined) {
this.element = $(selector);
this.style();
@ -29,7 +30,7 @@ var IPython = (function (IPython) {
SaveWidget.prototype.bind_events = function () {
var that = this;
this.element.find('button#save_notebook').click(function () {
IPython.notebook.save_notebook(that.get_notebook_name());
IPython.notebook.save_notebook();
});
};
@ -39,11 +40,59 @@ var IPython = (function (IPython) {
}
SaveWidget.prototype.set_notebook_name = function (name) {
this.element.find('input#notebook_name').attr('value',name);
SaveWidget.prototype.set_notebook_name = function (nbname) {
this.element.find('input#notebook_name').attr('value',nbname);
}
SaveWidget.prototype.get_notebook_id = function () {
return this.element.find('span#notebook_id').text()
};
SaveWidget.prototype.update_url = function () {
var notebook_id = this.get_notebook_id();
if (notebook_id !== '') {
window.history.replaceState({}, '', notebook_id);
};
};
SaveWidget.prototype.test_notebook_name = function () {
var nbname = this.get_notebook_name();
if (this.notebook_name_re.test(nbname)) {
return true;
} else {
var bad_name = $('<div/>');
bad_name.html(
"The notebook name you entered (" +
nbname +
") is not valid. Notebook names can contain any characters except / and \\"
);
bad_name.dialog({title: 'Invalid name', modal: true});
return false;
};
};
SaveWidget.prototype.status_save = function () {
this.element.find('span.ui-button-text').text('Save');
this.element.find('button#save_notebook').button('enable');
};
SaveWidget.prototype.status_saving = function () {
this.element.find('span.ui-button-text').text('Saving');
this.element.find('button#save_notebook').button('disable');
};
SaveWidget.prototype.status_loading = function () {
this.element.find('span.ui-button-text').text('Loading');
this.element.find('button#save_notebook').button('disable');
};
IPython.SaveWidget = SaveWidget;
return IPython;

@ -129,17 +129,19 @@ var IPython = (function (IPython) {
TextCell.prototype.fromJSON = function (data) {
if (data.cell_type === 'text') {
this.set_text(data.text);
this.grow(this.element.find("textarea.text_cell_input"));
if (data.text !== undefined) {
this.set_text(data.text);
this.grow(this.element.find("textarea.text_cell_input"));
};
};
}
TextCell.prototype.toJSON = function () {
return {
cell_type : 'text',
text : this.get_text(),
};
var data = {}
data.cell_type = 'text';
data.text = this.get_text();
return data;
};
IPython.TextCell = TextCell;

@ -33,6 +33,7 @@
<span id="ipython_notebook"><h1>IPython Notebook</h1></span>
<span id="save_widget">
<input type="text" id="notebook_name" size="20"></textarea>
<span id="notebook_id" style="display:none">{{notebook_id}}</span>
<button id="save_notebook">Save</button>
</span>
<span id="kernel_status">Idle</span>
@ -52,6 +53,18 @@
</span>
<span class="section_row_header">Actions</span>
</div>
<div class="section_row">
<span class="section_row_buttons">
<button id="download_notebook">Export</button>
</span>
<span>
<select id="download_format">
<option value="xml">xml</option>
<option value="json">json</option>
<option value="py">py</option>
</select>
</span>
</div>
</div>
</div>

Loading…
Cancel
Save