notebookmanager.py
286 lines
| 11.0 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r4609 | """A notebook manager that uses the local file system for storage. | ||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
Brian E. Granger
|
r4484 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r4609 | # Copyright (C) 2008-2011 The IPython Development Team | ||
Brian E. Granger
|
r4484 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
Brian E. Granger
|
r4609 | # the file COPYING, distributed as part of this software. | ||
Brian E. Granger
|
r4484 | #----------------------------------------------------------------------------- | ||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
import datetime | ||||
Thomas Kluyver
|
r6030 | import io | ||
Brian E. Granger
|
r4484 | import os | ||
import uuid | ||||
Stefan van der Walt
|
r4624 | import glob | ||
Brian E. Granger
|
r4484 | |||
from tornado import web | ||||
Brian E. Granger
|
r4494 | from IPython.config.configurable import LoggingConfigurable | ||
Brian E. Granger
|
r4484 | from IPython.nbformat import current | ||
MinRK
|
r7623 | from IPython.utils.traitlets import Unicode, List, Dict, Bool, TraitError | ||
Brian E. Granger
|
r4484 | |||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r5758 | # Classes | ||
#----------------------------------------------------------------------------- | ||||
Brian E. Granger
|
r4484 | |||
Brian E. Granger
|
r4494 | class NotebookManager(LoggingConfigurable): | ||
Brian E. Granger
|
r4484 | |||
MinRK
|
r6322 | notebook_dir = Unicode(os.getcwdu(), config=True, help=""" | ||
Brian E. Granger
|
r4515 | The directory to use for notebooks. | ||
""") | ||||
MinRK
|
r7623 | def _notebook_dir_changed(self, name, old, new): | ||
"""do a bit of validation of the notebook dir""" | ||||
if os.path.exists(new) and not os.path.isdir(new): | ||||
raise TraitError("notebook dir %r is not a directory" % new) | ||||
if not os.path.exists(new): | ||||
self.log.info("Creating notebook dir %s", new) | ||||
try: | ||||
os.mkdir(new) | ||||
except: | ||||
raise TraitError("Couldn't create notebook dir %r" % new) | ||||
MinRK
|
r5653 | |||
save_script = Bool(False, config=True, | ||||
Fernando Perez
|
r5760 | help="""Automatically create a Python script when saving the notebook. | ||
MinRK
|
r5653 | |||
Matthias BUSSONNIER
|
r6765 | For easier use of import, %run and %load across notebooks, a | ||
Fernando Perez
|
r5758 | <notebook-name>.py script will be created next to any | ||
<notebook-name>.ipynb on each save. This can also be set with the | ||||
short `--script` flag. | ||||
MinRK
|
r5653 | """ | ||
) | ||||
Brian E. Granger
|
r4484 | filename_ext = Unicode(u'.ipynb') | ||
Brian E. Granger
|
r4633 | allowed_formats = List([u'json',u'py']) | ||
Brian E. Granger
|
r4484 | |||
# 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) | ||||
""" | ||||
Stefan van der Walt
|
r4623 | names = glob.glob(os.path.join(self.notebook_dir, | ||
'*' + self.filename_ext)) | ||||
names = [os.path.splitext(os.path.basename(name))[0] | ||||
for name in names] | ||||
Brian E. Granger
|
r4484 | 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)) | ||||
Brian E. Granger
|
r4488 | data = sorted(data, key=lambda item: item['name']) | ||
Brian E. Granger
|
r4484 | return data | ||
def new_notebook_id(self, name): | ||||
"""Generate a new notebook_id for a name and store its mappings.""" | ||||
Fernando Perez
|
r4678 | # TODO: the following will give stable urls for notebooks, but unless | ||
# the notebooks are immediately redirected to their new urls when their | ||||
# filemname changes, nasty inconsistencies result. So for now it's | ||||
# disabled and instead we use a random uuid4() call. But we leave the | ||||
# logic here so that we can later reactivate it, whhen the necessary | ||||
# url redirection code is written. | ||||
#notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL, | ||||
# 'file://'+self.get_path_by_name(name).encode('utf-8'))) | ||||
Brian E. Granger
|
r4674 | notebook_id = unicode(uuid.uuid4()) | ||
Fernando Perez
|
r4678 | |||
Brian E. Granger
|
r4484 | 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]) | ||||
Brian E. Granger
|
r4609 | return os.path.isfile(path) | ||
Brian E. Granger
|
r4484 | |||
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: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Brian E. Granger
|
r4484 | 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: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(415, u'Invalid notebook format: %s' % format) | ||
Brian E. Granger
|
r4484 | last_modified, nb = self.get_notebook_object(notebook_id) | ||
MinRK
|
r5278 | kwargs = {} | ||
if format == 'json': | ||||
# don't split lines for sending over the wire, because it | ||||
# should match the Python in-memory format. | ||||
kwargs['split_lines'] = False | ||||
data = current.writes(nb, format, **kwargs) | ||||
Brian E. Granger
|
r4484 | 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): | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Brian E. Granger
|
r4484 | info = os.stat(path) | ||
last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime) | ||||
Brian E. Granger
|
r4633 | with open(path,'r') as f: | ||
s = f.read() | ||||
try: | ||||
# v1 and v2 and json in the .ipynb files. | ||||
nb = current.reads(s, u'json') | ||||
except: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(500, u'Unreadable JSON notebook.') | ||
Brian Granger
|
r7231 | # Always use the filename as the notebook name. | ||
Kent Inverarity
|
r7615 | nb.metadata.name = os.path.splitext(os.path.basename(path))[0] | ||
Brian E. Granger
|
r4484 | return last_modified, nb | ||
Brian E. Granger
|
r4491 | def save_new_notebook(self, data, name=None, format=u'json'): | ||
"""Save a new notebook and return its notebook_id. | ||||
If a name is passed in, it overrides any values in the notebook data | ||||
and the value in the data is updated to use that value. | ||||
""" | ||||
Brian E. Granger
|
r4484 | if format not in self.allowed_formats: | ||
Brian E. Granger
|
r4676 | raise web.HTTPError(415, u'Invalid notebook format: %s' % format) | ||
Brian E. Granger
|
r4491 | |||
Brian E. Granger
|
r4484 | try: | ||
Thomas Kluyver
|
r4869 | nb = current.reads(data.decode('utf-8'), format) | ||
Brian E. Granger
|
r4484 | except: | ||
Brian E. Granger
|
r4676 | raise web.HTTPError(400, u'Invalid JSON data') | ||
Brian E. Granger
|
r4491 | |||
if name is None: | ||||
try: | ||||
Brian E. Granger
|
r4637 | name = nb.metadata.name | ||
Brian E. Granger
|
r4491 | except AttributeError: | ||
Brian E. Granger
|
r4676 | raise web.HTTPError(400, u'Missing notebook name') | ||
Brian E. Granger
|
r4637 | nb.metadata.name = name | ||
Brian E. Granger
|
r4491 | |||
Brian E. Granger
|
r4484 | notebook_id = self.new_notebook_id(name) | ||
self.save_notebook_object(notebook_id, nb) | ||||
return notebook_id | ||||
Brian E. Granger
|
r4491 | def save_notebook(self, notebook_id, data, name=None, format=u'json'): | ||
Brian E. Granger
|
r4484 | """Save an existing notebook by notebook_id.""" | ||
if format not in self.allowed_formats: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(415, u'Invalid notebook format: %s' % format) | ||
Brian E. Granger
|
r4491 | |||
Brian E. Granger
|
r4484 | try: | ||
Thomas Kluyver
|
r4869 | nb = current.reads(data.decode('utf-8'), format) | ||
Brian E. Granger
|
r4484 | except: | ||
Brian E. Granger
|
r4676 | raise web.HTTPError(400, u'Invalid JSON data') | ||
Brian E. Granger
|
r4491 | |||
if name is not None: | ||||
Brian E. Granger
|
r4637 | nb.metadata.name = name | ||
Brian E. Granger
|
r4484 | 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: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Brian E. Granger
|
r4484 | old_name = self.mapping[notebook_id] | ||
try: | ||||
Brian E. Granger
|
r4637 | new_name = nb.metadata.name | ||
Brian E. Granger
|
r4484 | except AttributeError: | ||
Brian E. Granger
|
r4676 | raise web.HTTPError(400, u'Missing notebook name') | ||
Brian E. Granger
|
r4484 | path = self.get_path_by_name(new_name) | ||
try: | ||||
with open(path,'w') as f: | ||||
Brian E. Granger
|
r4633 | current.write(nb, f, u'json') | ||
MinRK
|
r5709 | except Exception as e: | ||
raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e) | ||||
MinRK
|
r5653 | # save .py script as well | ||
if self.save_script: | ||||
pypath = os.path.splitext(path)[0] + '.py' | ||||
try: | ||||
Thomas Kluyver
|
r6031 | with io.open(pypath,'w', encoding='utf-8') as f: | ||
MinRK
|
r5653 | current.write(nb, f, u'py') | ||
MinRK
|
r5709 | except Exception as e: | ||
raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e) | ||||
MinRK
|
r5653 | |||
Brian E. Granger
|
r4484 | if old_name != new_name: | ||
old_path = self.get_path_by_name(old_name) | ||||
if os.path.isfile(old_path): | ||||
os.unlink(old_path) | ||||
MinRK
|
r5653 | if self.save_script: | ||
old_pypath = os.path.splitext(old_path)[0] + '.py' | ||||
if os.path.isfile(old_pypath): | ||||
os.unlink(old_pypath) | ||||
Brian E. Granger
|
r4484 | self.mapping[notebook_id] = new_name | ||
self.rev_mapping[new_name] = notebook_id | ||||
Takafumi Arakaki
|
r7359 | del self.rev_mapping[old_name] | ||
Brian E. Granger
|
r4484 | |||
def delete_notebook(self, notebook_id): | ||||
"""Delete notebook by notebook_id.""" | ||||
path = self.find_path(notebook_id) | ||||
if not os.path.isfile(path): | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Brian E. Granger
|
r4484 | os.unlink(path) | ||
self.delete_notebook_id(notebook_id) | ||||
Brian Granger
|
r5861 | def increment_filename(self, basename): | ||
Brian Granger
|
r5877 | """Return a non-used filename of the form basename<int>. | ||
This searches through the filenames (basename0, basename1, ...) | ||||
until is find one that is not already being used. It is used to | ||||
create Untitled and Copy names that are unique. | ||||
""" | ||||
Brian E. Granger
|
r4484 | i = 0 | ||
while True: | ||||
Brian Granger
|
r5861 | name = u'%s%i' % (basename,i) | ||
Brian E. Granger
|
r4484 | path = self.get_path_by_name(name) | ||
if not os.path.isfile(path): | ||||
break | ||||
else: | ||||
i = i+1 | ||||
Brian Granger
|
r5861 | return path, name | ||
def new_notebook(self): | ||||
"""Create a new notebook and return its notebook_id.""" | ||||
path, name = self.increment_filename('Untitled') | ||||
Brian E. Granger
|
r4484 | notebook_id = self.new_notebook_id(name) | ||
Brian E. Granger
|
r4641 | metadata = current.new_metadata(name=name) | ||
nb = current.new_notebook(metadata=metadata) | ||||
Brian E. Granger
|
r4484 | with open(path,'w') as f: | ||
Brian E. Granger
|
r4633 | current.write(nb, f, u'json') | ||
Brian E. Granger
|
r4484 | return notebook_id | ||
Brian Granger
|
r5860 | def copy_notebook(self, notebook_id): | ||
Brian Granger
|
r5861 | """Copy an existing notebook and return its notebook_id.""" | ||
Brian Granger
|
r5860 | last_mod, nb = self.get_notebook_object(notebook_id) | ||
name = nb.metadata.name + '-Copy' | ||||
Brian Granger
|
r5861 | path, name = self.increment_filename(name) | ||
Brian Granger
|
r5860 | nb.metadata.name = name | ||
notebook_id = self.new_notebook_id(name) | ||||
self.save_notebook_object(notebook_id, nb) | ||||
return notebook_id | ||||