commit
8296f24904
@ -0,0 +1,531 @@
|
||||
"""A contents manager that uses the local file system for storage."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
from tornado import web
|
||||
|
||||
from .manager import ContentsManager
|
||||
from IPython.nbformat import current
|
||||
from IPython.utils.path import ensure_dir_exists
|
||||
from IPython.utils.traitlets import Unicode, Bool, TraitError
|
||||
from IPython.utils.py3compat import getcwd
|
||||
from IPython.utils import tz
|
||||
from IPython.html.utils import is_hidden, to_os_path, url_path_join
|
||||
|
||||
|
||||
class FileContentsManager(ContentsManager):
|
||||
|
||||
root_dir = Unicode(getcwd(), config=True)
|
||||
|
||||
save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
|
||||
def _save_script_changed(self):
|
||||
self.log.warn("""
|
||||
Automatically saving notebooks as scripts has been removed.
|
||||
Use `ipython nbconvert --to python [notebook]` instead.
|
||||
""")
|
||||
|
||||
def _root_dir_changed(self, name, old, new):
|
||||
"""Do a bit of validation of the root_dir."""
|
||||
if not os.path.isabs(new):
|
||||
# If we receive a non-absolute path, make it absolute.
|
||||
self.root_dir = os.path.abspath(new)
|
||||
return
|
||||
if not os.path.isdir(new):
|
||||
raise TraitError("%r is not a directory" % new)
|
||||
|
||||
checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
|
||||
help="""The directory name in which to keep file checkpoints
|
||||
|
||||
This is a path relative to the file's own directory.
|
||||
|
||||
By default, it is .ipynb_checkpoints
|
||||
"""
|
||||
)
|
||||
|
||||
def _copy(self, src, dest):
|
||||
"""copy src to dest
|
||||
|
||||
like shutil.copy2, but log errors in copystat
|
||||
"""
|
||||
shutil.copyfile(src, dest)
|
||||
try:
|
||||
shutil.copystat(src, dest)
|
||||
except OSError as e:
|
||||
self.log.debug("copystat on %s failed", dest, exc_info=True)
|
||||
|
||||
def _get_os_path(self, name=None, path=''):
|
||||
"""Given a filename and API path, return its file system
|
||||
path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
A filename
|
||||
path : string
|
||||
The relative API path to the named file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
path : string
|
||||
API path to be evaluated relative to root_dir.
|
||||
"""
|
||||
if name is not None:
|
||||
path = url_path_join(path, name)
|
||||
return to_os_path(path, self.root_dir)
|
||||
|
||||
def path_exists(self, path):
|
||||
"""Does the API-style path refer to an extant directory?
|
||||
|
||||
API-style wrapper for os.path.isdir
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to root_dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path is indeed a directory.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(path=path)
|
||||
return os.path.isdir(os_path)
|
||||
|
||||
def is_hidden(self, path):
|
||||
"""Does the API style path correspond to a hidden directory or file?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to root_dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path is hidden.
|
||||
|
||||
"""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(path=path)
|
||||
return is_hidden(os_path, self.root_dir)
|
||||
|
||||
def file_exists(self, name, path=''):
|
||||
"""Returns True if the file exists, else returns False.
|
||||
|
||||
API-style wrapper for os.path.isfile
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The name of the file you are checking.
|
||||
path : string
|
||||
The relative path to the file's directory (with '/' as separator)
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the file exists.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
nbpath = self._get_os_path(name, path=path)
|
||||
return os.path.isfile(nbpath)
|
||||
|
||||
def exists(self, name=None, path=''):
|
||||
"""Returns True if the path [and name] exists, else returns False.
|
||||
|
||||
API-style wrapper for os.path.exists
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The name of the file you are checking.
|
||||
path : string
|
||||
The relative path to the file's directory (with '/' as separator)
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the target exists.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(name, path=path)
|
||||
return os.path.exists(os_path)
|
||||
|
||||
def _base_model(self, name, path=''):
|
||||
"""Build the common base of a contents model"""
|
||||
os_path = self._get_os_path(name, path)
|
||||
info = os.stat(os_path)
|
||||
last_modified = tz.utcfromtimestamp(info.st_mtime)
|
||||
created = tz.utcfromtimestamp(info.st_ctime)
|
||||
# Create the base model.
|
||||
model = {}
|
||||
model['name'] = name
|
||||
model['path'] = path
|
||||
model['last_modified'] = last_modified
|
||||
model['created'] = created
|
||||
model['content'] = None
|
||||
model['format'] = None
|
||||
return model
|
||||
|
||||
def _dir_model(self, name, path='', content=True):
|
||||
"""Build a model for a directory
|
||||
|
||||
if content is requested, will include a listing of the directory
|
||||
"""
|
||||
os_path = self._get_os_path(name, path)
|
||||
|
||||
four_o_four = u'directory does not exist: %r' % os_path
|
||||
|
||||
if not os.path.isdir(os_path):
|
||||
raise web.HTTPError(404, four_o_four)
|
||||
elif is_hidden(os_path, self.root_dir):
|
||||
self.log.info("Refusing to serve hidden directory %r, via 404 Error",
|
||||
os_path
|
||||
)
|
||||
raise web.HTTPError(404, four_o_four)
|
||||
|
||||
if name is None:
|
||||
if '/' in path:
|
||||
path, name = path.rsplit('/', 1)
|
||||
else:
|
||||
name = ''
|
||||
model = self._base_model(name, path)
|
||||
model['type'] = 'directory'
|
||||
dir_path = u'{}/{}'.format(path, name)
|
||||
if content:
|
||||
model['content'] = contents = []
|
||||
for os_path in glob.glob(self._get_os_path('*', dir_path)):
|
||||
name = os.path.basename(os_path)
|
||||
if self.should_list(name) and not is_hidden(os_path, self.root_dir):
|
||||
contents.append(self.get_model(name=name, path=dir_path, content=False))
|
||||
|
||||
model['format'] = 'json'
|
||||
|
||||
return model
|
||||
|
||||
def _file_model(self, name, path='', content=True):
|
||||
"""Build a model for a file
|
||||
|
||||
if content is requested, include the file contents.
|
||||
UTF-8 text files will be unicode, binary files will be base64-encoded.
|
||||
"""
|
||||
model = self._base_model(name, path)
|
||||
model['type'] = 'file'
|
||||
if content:
|
||||
os_path = self._get_os_path(name, path)
|
||||
with io.open(os_path, 'rb') as f:
|
||||
bcontent = f.read()
|
||||
try:
|
||||
model['content'] = bcontent.decode('utf8')
|
||||
except UnicodeError as e:
|
||||
model['content'] = base64.encodestring(bcontent).decode('ascii')
|
||||
model['format'] = 'base64'
|
||||
else:
|
||||
model['format'] = 'text'
|
||||
return model
|
||||
|
||||
|
||||
def _notebook_model(self, name, path='', content=True):
|
||||
"""Build a notebook model
|
||||
|
||||
if content is requested, the notebook content will be populated
|
||||
as a JSON structure (not double-serialized)
|
||||
"""
|
||||
model = self._base_model(name, path)
|
||||
model['type'] = 'notebook'
|
||||
if content:
|
||||
os_path = self._get_os_path(name, path)
|
||||
with io.open(os_path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
nb = current.read(f, u'json')
|
||||
except Exception as e:
|
||||
raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
|
||||
self.mark_trusted_cells(nb, name, path)
|
||||
model['content'] = nb
|
||||
model['format'] = 'json'
|
||||
return model
|
||||
|
||||
def get_model(self, name, path='', content=True):
|
||||
""" Takes a path and name for an entity and returns its model
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
the name of the target
|
||||
path : str
|
||||
the API path that describes the relative path for the target
|
||||
|
||||
Returns
|
||||
-------
|
||||
model : dict
|
||||
the contents model. If content=True, returns the contents
|
||||
of the file or directory as well.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
|
||||
if not self.exists(name=name, path=path):
|
||||
raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
|
||||
|
||||
os_path = self._get_os_path(name, path)
|
||||
if os.path.isdir(os_path):
|
||||
model = self._dir_model(name, path, content)
|
||||
elif name.endswith('.ipynb'):
|
||||
model = self._notebook_model(name, path, content)
|
||||
else:
|
||||
model = self._file_model(name, path, content)
|
||||
return model
|
||||
|
||||
def _save_notebook(self, os_path, model, name='', path=''):
|
||||
"""save a notebook file"""
|
||||
# Save the notebook file
|
||||
nb = current.to_notebook_json(model['content'])
|
||||
|
||||
self.check_and_sign(nb, name, path)
|
||||
|
||||
if 'name' in nb['metadata']:
|
||||
nb['metadata']['name'] = u''
|
||||
|
||||
with io.open(os_path, 'w', encoding='utf-8') as f:
|
||||
current.write(nb, f, u'json')
|
||||
|
||||
def _save_file(self, os_path, model, name='', path=''):
|
||||
"""save a non-notebook file"""
|
||||
fmt = model.get('format', None)
|
||||
if fmt not in {'text', 'base64'}:
|
||||
raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
|
||||
try:
|
||||
content = model['content']
|
||||
if fmt == 'text':
|
||||
bcontent = content.encode('utf8')
|
||||
else:
|
||||
b64_bytes = content.encode('ascii')
|
||||
bcontent = base64.decodestring(b64_bytes)
|
||||
except Exception as e:
|
||||
raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
|
||||
with io.open(os_path, 'wb') as f:
|
||||
f.write(bcontent)
|
||||
|
||||
def _save_directory(self, os_path, model, name='', path=''):
|
||||
"""create a directory"""
|
||||
if is_hidden(os_path, self.root_dir):
|
||||
raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
|
||||
if not os.path.exists(os_path):
|
||||
os.mkdir(os_path)
|
||||
elif not os.path.isdir(os_path):
|
||||
raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
|
||||
else:
|
||||
self.log.debug("Directory %r already exists", os_path)
|
||||
|
||||
def save(self, model, name='', path=''):
|
||||
"""Save the file model and return the model with no content."""
|
||||
path = path.strip('/')
|
||||
|
||||
if 'type' not in model:
|
||||
raise web.HTTPError(400, u'No file type provided')
|
||||
if 'content' not in model and model['type'] != 'directory':
|
||||
raise web.HTTPError(400, u'No file content provided')
|
||||
|
||||
# One checkpoint should always exist
|
||||
if self.file_exists(name, path) and not self.list_checkpoints(name, path):
|
||||
self.create_checkpoint(name, path)
|
||||
|
||||
new_path = model.get('path', path).strip('/')
|
||||
new_name = model.get('name', name)
|
||||
|
||||
if path != new_path or name != new_name:
|
||||
self.rename(name, path, new_name, new_path)
|
||||
|
||||
os_path = self._get_os_path(new_name, new_path)
|
||||
self.log.debug("Saving %s", os_path)
|
||||
try:
|
||||
if model['type'] == 'notebook':
|
||||
self._save_notebook(os_path, model, new_name, new_path)
|
||||
elif model['type'] == 'file':
|
||||
self._save_file(os_path, model, new_name, new_path)
|
||||
elif model['type'] == 'directory':
|
||||
self._save_directory(os_path, model, new_name, new_path)
|
||||
else:
|
||||
raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
|
||||
except web.HTTPError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
|
||||
|
||||
model = self.get_model(new_name, new_path, content=False)
|
||||
return model
|
||||
|
||||
def update(self, model, name, path=''):
|
||||
"""Update the file's path and/or name
|
||||
|
||||
For use in PATCH requests, to enable renaming a file without
|
||||
re-uploading its contents. Only used for renaming at the moment.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
new_name = model.get('name', name)
|
||||
new_path = model.get('path', path).strip('/')
|
||||
if path != new_path or name != new_name:
|
||||
self.rename(name, path, new_name, new_path)
|
||||
model = self.get_model(new_name, new_path, content=False)
|
||||
return model
|
||||
|
||||
def delete(self, name, path=''):
|
||||
"""Delete file by name and path."""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(name, path)
|
||||
rm = os.unlink
|
||||
if os.path.isdir(os_path):
|
||||
listing = os.listdir(os_path)
|
||||
# don't delete non-empty directories (checkpoints dir doesn't count)
|
||||
if listing and listing != [self.checkpoint_dir]:
|
||||
raise web.HTTPError(400, u'Directory %s not empty' % os_path)
|
||||
elif not os.path.isfile(os_path):
|
||||
raise web.HTTPError(404, u'File does not exist: %s' % os_path)
|
||||
|
||||
# clear checkpoints
|
||||
for checkpoint in self.list_checkpoints(name, path):
|
||||
checkpoint_id = checkpoint['id']
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if os.path.isfile(cp_path):
|
||||
self.log.debug("Unlinking checkpoint %s", cp_path)
|
||||
os.unlink(cp_path)
|
||||
|
||||
if os.path.isdir(os_path):
|
||||
self.log.debug("Removing directory %s", os_path)
|
||||
shutil.rmtree(os_path)
|
||||
else:
|
||||
self.log.debug("Unlinking file %s", os_path)
|
||||
rm(os_path)
|
||||
|
||||
def rename(self, old_name, old_path, new_name, new_path):
|
||||
"""Rename a file."""
|
||||
old_path = old_path.strip('/')
|
||||
new_path = new_path.strip('/')
|
||||
if new_name == old_name and new_path == old_path:
|
||||
return
|
||||
|
||||
new_os_path = self._get_os_path(new_name, new_path)
|
||||
old_os_path = self._get_os_path(old_name, old_path)
|
||||
|
||||
# Should we proceed with the move?
|
||||
if os.path.isfile(new_os_path):
|
||||
raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
|
||||
|
||||
# Move the file
|
||||
try:
|
||||
shutil.move(old_os_path, new_os_path)
|
||||
except Exception as e:
|
||||
raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
|
||||
|
||||
# Move the checkpoints
|
||||
old_checkpoints = self.list_checkpoints(old_name, old_path)
|
||||
for cp in old_checkpoints:
|
||||
checkpoint_id = cp['id']
|
||||
old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
|
||||
new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
|
||||
if os.path.isfile(old_cp_path):
|
||||
self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
|
||||
shutil.move(old_cp_path, new_cp_path)
|
||||
|
||||
# Checkpoint-related utilities
|
||||
|
||||
def get_checkpoint_path(self, checkpoint_id, name, path=''):
|
||||
"""find the path to a checkpoint"""
|
||||
path = path.strip('/')
|
||||
basename, ext = os.path.splitext(name)
|
||||
filename = u"{name}-{checkpoint_id}{ext}".format(
|
||||
name=basename,
|
||||
checkpoint_id=checkpoint_id,
|
||||
ext=ext,
|
||||
)
|
||||
os_path = self._get_os_path(path=path)
|
||||
cp_dir = os.path.join(os_path, self.checkpoint_dir)
|
||||
ensure_dir_exists(cp_dir)
|
||||
cp_path = os.path.join(cp_dir, filename)
|
||||
return cp_path
|
||||
|
||||
def get_checkpoint_model(self, checkpoint_id, name, path=''):
|
||||
"""construct the info dict for a given checkpoint"""
|
||||
path = path.strip('/')
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
stats = os.stat(cp_path)
|
||||
last_modified = tz.utcfromtimestamp(stats.st_mtime)
|
||||
info = dict(
|
||||
id = checkpoint_id,
|
||||
last_modified = last_modified,
|
||||
)
|
||||
return info
|
||||
|
||||
# public checkpoint API
|
||||
|
||||
def create_checkpoint(self, name, path=''):
|
||||
"""Create a checkpoint from the current state of a file"""
|
||||
path = path.strip('/')
|
||||
src_path = self._get_os_path(name, path)
|
||||
# only the one checkpoint ID:
|
||||
checkpoint_id = u"checkpoint"
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
self.log.debug("creating checkpoint for %s", name)
|
||||
self._copy(src_path, cp_path)
|
||||
|
||||
# return the checkpoint info
|
||||
return self.get_checkpoint_model(checkpoint_id, name, path)
|
||||
|
||||
def list_checkpoints(self, name, path=''):
|
||||
"""list the checkpoints for a given file
|
||||
|
||||
This contents manager currently only supports one checkpoint per file.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
checkpoint_id = "checkpoint"
|
||||
os_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if not os.path.exists(os_path):
|
||||
return []
|
||||
else:
|
||||
return [self.get_checkpoint_model(checkpoint_id, name, path)]
|
||||
|
||||
|
||||
def restore_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""restore a file to a checkpointed state"""
|
||||
path = path.strip('/')
|
||||
self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
|
||||
nb_path = self._get_os_path(name, path)
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if not os.path.isfile(cp_path):
|
||||
self.log.debug("checkpoint file does not exist: %s", cp_path)
|
||||
raise web.HTTPError(404,
|
||||
u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
|
||||
)
|
||||
# ensure notebook is readable (never restore from an unreadable notebook)
|
||||
if cp_path.endswith('.ipynb'):
|
||||
with io.open(cp_path, 'r', encoding='utf-8') as f:
|
||||
current.read(f, u'json')
|
||||
self._copy(cp_path, nb_path)
|
||||
self.log.debug("copying %s -> %s", cp_path, nb_path)
|
||||
|
||||
def delete_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""delete a file's checkpoint"""
|
||||
path = path.strip('/')
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if not os.path.isfile(cp_path):
|
||||
raise web.HTTPError(404,
|
||||
u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
|
||||
)
|
||||
self.log.debug("unlinking %s", cp_path)
|
||||
os.unlink(cp_path)
|
||||
|
||||
def info_string(self):
|
||||
return "Serving notebooks from local directory: %s" % self.root_dir
|
||||
|
||||
def get_kernel_path(self, name, path='', model=None):
|
||||
"""Return the initial working dir a kernel associated with a given notebook"""
|
||||
return os.path.join(self.root_dir, path)
|
||||
@ -0,0 +1,286 @@
|
||||
"""Tornado handlers for the contents web service."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import json
|
||||
|
||||
from tornado import web
|
||||
|
||||
from IPython.html.utils import url_path_join, url_escape
|
||||
from IPython.utils.jsonutil import date_default
|
||||
|
||||
from IPython.html.base.handlers import (IPythonHandler, json_errors,
|
||||
file_path_regex, path_regex,
|
||||
file_name_regex)
|
||||
|
||||
|
||||
def sort_key(model):
|
||||
"""key function for case-insensitive sort by name and type"""
|
||||
iname = model['name'].lower()
|
||||
type_key = {
|
||||
'directory' : '0',
|
||||
'notebook' : '1',
|
||||
'file' : '2',
|
||||
}.get(model['type'], '9')
|
||||
return u'%s%s' % (type_key, iname)
|
||||
|
||||
class ContentsHandler(IPythonHandler):
|
||||
|
||||
SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
|
||||
|
||||
def location_url(self, name, path):
|
||||
"""Return the full URL location of a file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : unicode
|
||||
The base name of the file, such as "foo.ipynb".
|
||||
path : unicode
|
||||
The API path of the file, such as "foo/bar".
|
||||
"""
|
||||
return url_escape(url_path_join(
|
||||
self.base_url, 'api', 'contents', path, name
|
||||
))
|
||||
|
||||
def _finish_model(self, model, location=True):
|
||||
"""Finish a JSON request with a model, setting relevant headers, etc."""
|
||||
if location:
|
||||
location = self.location_url(model['name'], model['path'])
|
||||
self.set_header('Location', location)
|
||||
self.set_header('Last-Modified', model['last_modified'])
|
||||
self.finish(json.dumps(model, default=date_default))
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def get(self, path='', name=None):
|
||||
"""Return a model for a file or directory.
|
||||
|
||||
A directory model contains a list of models (without content)
|
||||
of the files and directories it contains.
|
||||
"""
|
||||
path = path or ''
|
||||
model = self.contents_manager.get_model(name=name, path=path)
|
||||
if model['type'] == 'directory':
|
||||
# group listing by type, then by name (case-insensitive)
|
||||
# FIXME: sorting should be done in the frontends
|
||||
model['content'].sort(key=sort_key)
|
||||
self._finish_model(model, location=False)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def patch(self, path='', name=None):
|
||||
"""PATCH renames a notebook without re-uploading content."""
|
||||
cm = self.contents_manager
|
||||
if name is None:
|
||||
raise web.HTTPError(400, u'Filename missing')
|
||||
model = self.get_json_body()
|
||||
if model is None:
|
||||
raise web.HTTPError(400, u'JSON body missing')
|
||||
model = cm.update(model, name, path)
|
||||
self._finish_model(model)
|
||||
|
||||
def _copy(self, copy_from, path, copy_to=None):
|
||||
"""Copy a file, optionally specifying the new name.
|
||||
"""
|
||||
self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
|
||||
copy_from=copy_from,
|
||||
path=path,
|
||||
copy_to=copy_to or '',
|
||||
))
|
||||
model = self.contents_manager.copy(copy_from, copy_to, path)
|
||||
self.set_status(201)
|
||||
self._finish_model(model)
|
||||
|
||||
def _upload(self, model, path, name=None):
|
||||
"""Handle upload of a new file
|
||||
|
||||
If name specified, create it in path/name,
|
||||
otherwise create a new untitled file in path.
|
||||
"""
|
||||
self.log.info(u"Uploading file to %s/%s", path, name or '')
|
||||
if name:
|
||||
model['name'] = name
|
||||
|
||||
model = self.contents_manager.create_file(model, path)
|
||||
self.set_status(201)
|
||||
self._finish_model(model)
|
||||
|
||||
def _create_empty_file(self, path, name=None, ext='.ipynb'):
|
||||
"""Create an empty file in path
|
||||
|
||||
If name specified, create it in path/name.
|
||||
"""
|
||||
self.log.info(u"Creating new file in %s/%s", path, name or '')
|
||||
model = {}
|
||||
if name:
|
||||
model['name'] = name
|
||||
model = self.contents_manager.create_file(model, path=path, ext=ext)
|
||||
self.set_status(201)
|
||||
self._finish_model(model)
|
||||
|
||||
def _save(self, model, path, name):
|
||||
"""Save an existing file."""
|
||||
self.log.info(u"Saving file at %s/%s", path, name)
|
||||
model = self.contents_manager.save(model, name, path)
|
||||
if model['path'] != path.strip('/') or model['name'] != name:
|
||||
# a rename happened, set Location header
|
||||
location = True
|
||||
else:
|
||||
location = False
|
||||
self._finish_model(model, location)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def post(self, path='', name=None):
|
||||
"""Create a new file or directory in the specified path.
|
||||
|
||||
POST creates new files or directories. The server always decides on the name.
|
||||
|
||||
POST /api/contents/path
|
||||
New untitled notebook in path. If content specified, upload a
|
||||
notebook, otherwise start empty.
|
||||
POST /api/contents/path
|
||||
with body {"copy_from" : "OtherNotebook.ipynb"}
|
||||
New copy of OtherNotebook in path
|
||||
"""
|
||||
|
||||
if name is not None:
|
||||
path = u'{}/{}'.format(path, name)
|
||||
|
||||
cm = self.contents_manager
|
||||
|
||||
if cm.file_exists(path):
|
||||
raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
|
||||
|
||||
if not cm.path_exists(path):
|
||||
raise web.HTTPError(404, "No such directory: %s" % path)
|
||||
|
||||
model = self.get_json_body()
|
||||
|
||||
if model is not None:
|
||||
copy_from = model.get('copy_from')
|
||||
ext = model.get('ext', '.ipynb')
|
||||
if model.get('content') is not None:
|
||||
if copy_from:
|
||||
raise web.HTTPError(400, "Can't upload and copy at the same time.")
|
||||
self._upload(model, path)
|
||||
elif copy_from:
|
||||
self._copy(copy_from, path)
|
||||
else:
|
||||
self._create_empty_file(path, ext=ext)
|
||||
else:
|
||||
self._create_empty_file(path)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def put(self, path='', name=None):
|
||||
"""Saves the file in the location specified by name and path.
|
||||
|
||||
PUT is very similar to POST, but the requester specifies the name,
|
||||
whereas with POST, the server picks the name.
|
||||
|
||||
PUT /api/contents/path/Name.ipynb
|
||||
Save notebook at ``path/Name.ipynb``. Notebook structure is specified
|
||||
in `content` key of JSON request body. If content is not specified,
|
||||
create a new empty notebook.
|
||||
PUT /api/contents/path/Name.ipynb
|
||||
with JSON body::
|
||||
|
||||
{
|
||||
"copy_from" : "[path/to/]OtherNotebook.ipynb"
|
||||
}
|
||||
|
||||
Copy OtherNotebook to Name
|
||||
"""
|
||||
if name is None:
|
||||
raise web.HTTPError(400, "name must be specified with PUT.")
|
||||
|
||||
model = self.get_json_body()
|
||||
if model:
|
||||
copy_from = model.get('copy_from')
|
||||
if copy_from:
|
||||
if model.get('content'):
|
||||
raise web.HTTPError(400, "Can't upload and copy at the same time.")
|
||||
self._copy(copy_from, path, name)
|
||||
elif self.contents_manager.file_exists(name, path):
|
||||
self._save(model, path, name)
|
||||
else:
|
||||
self._upload(model, path, name)
|
||||
else:
|
||||
self._create_empty_file(path, name)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def delete(self, path='', name=None):
|
||||
"""delete a file in the given path"""
|
||||
cm = self.contents_manager
|
||||
self.log.warn('delete %s:%s', path, name)
|
||||
cm.delete(name, path)
|
||||
self.set_status(204)
|
||||
self.finish()
|
||||
|
||||
|
||||
class CheckpointsHandler(IPythonHandler):
|
||||
|
||||
SUPPORTED_METHODS = ('GET', 'POST')
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def get(self, path='', name=None):
|
||||
"""get lists checkpoints for a file"""
|
||||
cm = self.contents_manager
|
||||
checkpoints = cm.list_checkpoints(name, path)
|
||||
data = json.dumps(checkpoints, default=date_default)
|
||||
self.finish(data)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def post(self, path='', name=None):
|
||||
"""post creates a new checkpoint"""
|
||||
cm = self.contents_manager
|
||||
checkpoint = cm.create_checkpoint(name, path)
|
||||
data = json.dumps(checkpoint, default=date_default)
|
||||
location = url_path_join(self.base_url, 'api/contents',
|
||||
path, name, 'checkpoints', checkpoint['id'])
|
||||
self.set_header('Location', url_escape(location))
|
||||
self.set_status(201)
|
||||
self.finish(data)
|
||||
|
||||
|
||||
class ModifyCheckpointsHandler(IPythonHandler):
|
||||
|
||||
SUPPORTED_METHODS = ('POST', 'DELETE')
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def post(self, path, name, checkpoint_id):
|
||||
"""post restores a file from a checkpoint"""
|
||||
cm = self.contents_manager
|
||||
cm.restore_checkpoint(checkpoint_id, name, path)
|
||||
self.set_status(204)
|
||||
self.finish()
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def delete(self, path, name, checkpoint_id):
|
||||
"""delete clears a checkpoint for a given file"""
|
||||
cm = self.contents_manager
|
||||
cm.delete_checkpoint(checkpoint_id, name, path)
|
||||
self.set_status(204)
|
||||
self.finish()
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# URL to handler mappings
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
_checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
|
||||
|
||||
default_handlers = [
|
||||
(r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
|
||||
(r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
|
||||
ModifyCheckpointsHandler),
|
||||
(r"/api/contents%s" % file_path_regex, ContentsHandler),
|
||||
(r"/api/contents%s" % path_regex, ContentsHandler),
|
||||
]
|
||||
@ -0,0 +1,333 @@
|
||||
"""A base class for contents managers."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from fnmatch import fnmatch
|
||||
import itertools
|
||||
import os
|
||||
|
||||
from tornado.web import HTTPError
|
||||
|
||||
from IPython.config.configurable import LoggingConfigurable
|
||||
from IPython.nbformat import current, sign
|
||||
from IPython.utils.traitlets import Instance, Unicode, List
|
||||
|
||||
|
||||
class ContentsManager(LoggingConfigurable):
|
||||
"""Base class for serving files and directories.
|
||||
|
||||
This serves any text or binary file,
|
||||
as well as directories,
|
||||
with special handling for JSON notebook documents.
|
||||
|
||||
Most APIs take a path argument,
|
||||
which is always an API-style unicode path,
|
||||
and always refers to a directory.
|
||||
|
||||
- unicode, not url-escaped
|
||||
- '/'-separated
|
||||
- leading and trailing '/' will be stripped
|
||||
- if unspecified, path defaults to '',
|
||||
indicating the root path.
|
||||
|
||||
name is also unicode, and refers to a specfic target:
|
||||
|
||||
- unicode, not url-escaped
|
||||
- must not contain '/'
|
||||
- It refers to an individual filename
|
||||
- It may refer to a directory name,
|
||||
in the case of listing or creating directories.
|
||||
|
||||
"""
|
||||
|
||||
notary = Instance(sign.NotebookNotary)
|
||||
def _notary_default(self):
|
||||
return sign.NotebookNotary(parent=self)
|
||||
|
||||
hide_globs = List(Unicode, [
|
||||
u'__pycache__', '*.pyc', '*.pyo',
|
||||
'.DS_Store', '*.so', '*.dylib', '*~',
|
||||
], config=True, help="""
|
||||
Glob patterns to hide in file and directory listings.
|
||||
""")
|
||||
|
||||
untitled_notebook = Unicode("Untitled", config=True,
|
||||
help="The base name used when creating untitled notebooks."
|
||||
)
|
||||
|
||||
untitled_file = Unicode("untitled", config=True,
|
||||
help="The base name used when creating untitled files."
|
||||
)
|
||||
|
||||
untitled_directory = Unicode("Untitled Folder", config=True,
|
||||
help="The base name used when creating untitled directories."
|
||||
)
|
||||
|
||||
# ContentsManager API part 1: methods that must be
|
||||
# implemented in subclasses.
|
||||
|
||||
def path_exists(self, path):
|
||||
"""Does the API-style path (directory) actually exist?
|
||||
|
||||
Like os.path.isdir
|
||||
|
||||
Override this method in subclasses.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path does indeed exist.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_hidden(self, path):
|
||||
"""Does the API style path correspond to a hidden directory or file?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to root dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
hidden : bool
|
||||
Whether the path is hidden.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def file_exists(self, name, path=''):
|
||||
"""Does a file exist at the given name and path?
|
||||
|
||||
Like os.path.isfile
|
||||
|
||||
Override this method in subclasses.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The name of the file you are checking.
|
||||
path : string
|
||||
The relative path to the file's directory (with '/' as separator)
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the file exists.
|
||||
"""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def exists(self, name, path=''):
|
||||
"""Does a file or directory exist at the given name and path?
|
||||
|
||||
Like os.path.exists
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The name of the file you are checking.
|
||||
path : string
|
||||
The relative path to the file's directory (with '/' as separator)
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the target exists.
|
||||
"""
|
||||
return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
|
||||
|
||||
def get_model(self, name, path='', content=True):
|
||||
"""Get the model of a file or directory with or without content."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def save(self, model, name, path=''):
|
||||
"""Save the file or directory and return the model with no content."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def update(self, model, name, path=''):
|
||||
"""Update the file or directory and return the model with no content.
|
||||
|
||||
For use in PATCH requests, to enable renaming a file without
|
||||
re-uploading its contents. Only used for renaming at the moment.
|
||||
"""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def delete(self, name, path=''):
|
||||
"""Delete file or directory by name and path."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def create_checkpoint(self, name, path=''):
|
||||
"""Create a checkpoint of the current state of a file
|
||||
|
||||
Returns a checkpoint_id for the new checkpoint.
|
||||
"""
|
||||
raise NotImplementedError("must be implemented in a subclass")
|
||||
|
||||
def list_checkpoints(self, name, path=''):
|
||||
"""Return a list of checkpoints for a given file"""
|
||||
return []
|
||||
|
||||
def restore_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""Restore a file from one of its checkpoints"""
|
||||
raise NotImplementedError("must be implemented in a subclass")
|
||||
|
||||
def delete_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""delete a checkpoint for a file"""
|
||||
raise NotImplementedError("must be implemented in a subclass")
|
||||
|
||||
# ContentsManager API part 2: methods that have useable default
|
||||
# implementations, but can be overridden in subclasses.
|
||||
|
||||
def info_string(self):
|
||||
return "Serving contents"
|
||||
|
||||
def get_kernel_path(self, name, path='', model=None):
|
||||
""" Return the path to start kernel in """
|
||||
return path
|
||||
|
||||
def increment_filename(self, filename, path=''):
|
||||
"""Increment a filename until it is unique.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : unicode
|
||||
The name of a file, including extension
|
||||
path : unicode
|
||||
The API path of the target's directory
|
||||
|
||||
Returns
|
||||
-------
|
||||
name : unicode
|
||||
A filename that is unique, based on the input filename.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
basename, ext = os.path.splitext(filename)
|
||||
for i in itertools.count():
|
||||
name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
|
||||
ext=ext)
|
||||
if not self.file_exists(name, path):
|
||||
break
|
||||
return name
|
||||
|
||||
def create_file(self, model=None, path='', ext='.ipynb'):
|
||||
"""Create a new file or directory and return its model with no content."""
|
||||
path = path.strip('/')
|
||||
if model is None:
|
||||
model = {}
|
||||
if 'content' not in model and model.get('type', None) != 'directory':
|
||||
if ext == '.ipynb':
|
||||
metadata = current.new_metadata(name=u'')
|
||||
model['content'] = current.new_notebook(metadata=metadata)
|
||||
model['type'] = 'notebook'
|
||||
model['format'] = 'json'
|
||||
else:
|
||||
model['content'] = ''
|
||||
model['type'] = 'file'
|
||||
model['format'] = 'text'
|
||||
if 'name' not in model:
|
||||
if model['type'] == 'directory':
|
||||
untitled = self.untitled_directory
|
||||
elif model['type'] == 'notebook':
|
||||
untitled = self.untitled_notebook
|
||||
elif model['type'] == 'file':
|
||||
untitled = self.untitled_file
|
||||
else:
|
||||
raise HTTPError(400, "Unexpected model type: %r" % model['type'])
|
||||
model['name'] = self.increment_filename(untitled + ext, path)
|
||||
|
||||
model['path'] = path
|
||||
model = self.save(model, model['name'], model['path'])
|
||||
return model
|
||||
|
||||
def copy(self, from_name, to_name=None, path=''):
|
||||
"""Copy an existing file and return its new model.
|
||||
|
||||
If to_name not specified, increment `from_name-Copy#.ext`.
|
||||
|
||||
copy_from can be a full path to a file,
|
||||
or just a base name. If a base name, `path` is used.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
if '/' in from_name:
|
||||
from_path, from_name = from_name.rsplit('/', 1)
|
||||
else:
|
||||
from_path = path
|
||||
model = self.get_model(from_name, from_path)
|
||||
if model['type'] == 'directory':
|
||||
raise HTTPError(400, "Can't copy directories")
|
||||
if not to_name:
|
||||
base, ext = os.path.splitext(from_name)
|
||||
copy_name = u'{0}-Copy{1}'.format(base, ext)
|
||||
to_name = self.increment_filename(copy_name, path)
|
||||
model['name'] = to_name
|
||||
model['path'] = path
|
||||
model = self.save(model, to_name, path)
|
||||
return model
|
||||
|
||||
def log_info(self):
|
||||
self.log.info(self.info_string())
|
||||
|
||||
def trust_notebook(self, name, path=''):
|
||||
"""Explicitly trust a notebook
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The filename of the notebook
|
||||
path : string
|
||||
The notebook's directory
|
||||
"""
|
||||
model = self.get_model(name, path)
|
||||
nb = model['content']
|
||||
self.log.warn("Trusting notebook %s/%s", path, name)
|
||||
self.notary.mark_cells(nb, True)
|
||||
self.save(model, name, path)
|
||||
|
||||
def check_and_sign(self, nb, name='', path=''):
|
||||
"""Check for trusted cells, and sign the notebook.
|
||||
|
||||
Called as a part of saving notebooks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nb : dict
|
||||
The notebook object (in nbformat.current format)
|
||||
name : string
|
||||
The filename of the notebook (for logging)
|
||||
path : string
|
||||
The notebook's directory (for logging)
|
||||
"""
|
||||
if self.notary.check_cells(nb):
|
||||
self.notary.sign(nb)
|
||||
else:
|
||||
self.log.warn("Saving untrusted notebook %s/%s", path, name)
|
||||
|
||||
def mark_trusted_cells(self, nb, name='', path=''):
|
||||
"""Mark cells as trusted if the notebook signature matches.
|
||||
|
||||
Called as a part of loading notebooks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nb : dict
|
||||
The notebook object (in nbformat.current format)
|
||||
name : string
|
||||
The filename of the notebook (for logging)
|
||||
path : string
|
||||
The notebook's directory (for logging)
|
||||
"""
|
||||
trusted = self.notary.check_signature(nb)
|
||||
if not trusted:
|
||||
self.log.warn("Notebook %s/%s is not trusted", path, name)
|
||||
self.notary.mark_cells(nb, trusted)
|
||||
|
||||
def should_list(self, name):
|
||||
"""Should this file/directory name be displayed in a listing?"""
|
||||
return not any(fnmatch(name, glob) for glob in self.hide_globs)
|
||||
@ -1,470 +0,0 @@
|
||||
"""A notebook manager that uses the local file system for storage."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import io
|
||||
import os
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
from tornado import web
|
||||
|
||||
from .nbmanager import NotebookManager
|
||||
from IPython.nbformat import current
|
||||
from IPython.utils.path import ensure_dir_exists
|
||||
from IPython.utils.traitlets import Unicode, Bool, TraitError
|
||||
from IPython.utils.py3compat import getcwd
|
||||
from IPython.utils import tz
|
||||
from IPython.html.utils import is_hidden, to_os_path
|
||||
|
||||
def sort_key(item):
|
||||
"""Case-insensitive sorting."""
|
||||
return item['name'].lower()
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Classes
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class FileNotebookManager(NotebookManager):
|
||||
|
||||
save_script = Bool(False, config=True,
|
||||
help="""Automatically create a Python script when saving the notebook.
|
||||
|
||||
For easier use of import, %run and %load across notebooks, a
|
||||
<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.
|
||||
"""
|
||||
)
|
||||
notebook_dir = Unicode(getcwd(), config=True)
|
||||
|
||||
def _notebook_dir_changed(self, name, old, new):
|
||||
"""Do a bit of validation of the notebook dir."""
|
||||
if not os.path.isabs(new):
|
||||
# If we receive a non-absolute path, make it absolute.
|
||||
self.notebook_dir = os.path.abspath(new)
|
||||
return
|
||||
if not os.path.exists(new) or not os.path.isdir(new):
|
||||
raise TraitError("notebook dir %r is not a directory" % new)
|
||||
|
||||
checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
|
||||
help="""The directory name in which to keep notebook checkpoints
|
||||
|
||||
This is a path relative to the notebook's own directory.
|
||||
|
||||
By default, it is .ipynb_checkpoints
|
||||
"""
|
||||
)
|
||||
|
||||
def _copy(self, src, dest):
|
||||
"""copy src to dest
|
||||
|
||||
like shutil.copy2, but log errors in copystat
|
||||
"""
|
||||
shutil.copyfile(src, dest)
|
||||
try:
|
||||
shutil.copystat(src, dest)
|
||||
except OSError as e:
|
||||
self.log.debug("copystat on %s failed", dest, exc_info=True)
|
||||
|
||||
def get_notebook_names(self, path=''):
|
||||
"""List all notebook names in the notebook dir and path."""
|
||||
path = path.strip('/')
|
||||
if not os.path.isdir(self._get_os_path(path=path)):
|
||||
raise web.HTTPError(404, 'Directory not found: ' + path)
|
||||
names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
|
||||
names = [os.path.basename(name)
|
||||
for name in names]
|
||||
return names
|
||||
|
||||
def path_exists(self, path):
|
||||
"""Does the API-style path (directory) actually exist?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to base notebook-dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path is indeed a directory.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(path=path)
|
||||
return os.path.isdir(os_path)
|
||||
|
||||
def is_hidden(self, path):
|
||||
"""Does the API style path correspond to a hidden directory or file?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to base notebook-dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path is hidden.
|
||||
|
||||
"""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(path=path)
|
||||
return is_hidden(os_path, self.notebook_dir)
|
||||
|
||||
def _get_os_path(self, name=None, path=''):
|
||||
"""Given a notebook name and a URL path, return its file system
|
||||
path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The name of a notebook file with the .ipynb extension
|
||||
path : string
|
||||
The relative URL path (with '/' as separator) to the named
|
||||
notebook.
|
||||
|
||||
Returns
|
||||
-------
|
||||
path : string
|
||||
A file system path that combines notebook_dir (location where
|
||||
server started), the relative path, and the filename with the
|
||||
current operating system's url.
|
||||
"""
|
||||
if name is not None:
|
||||
path = path + '/' + name
|
||||
return to_os_path(path, self.notebook_dir)
|
||||
|
||||
def notebook_exists(self, name, path=''):
|
||||
"""Returns a True if the notebook exists. Else, returns False.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The name of the notebook you are checking.
|
||||
path : string
|
||||
The relative path to the notebook (with '/' as separator)
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
"""
|
||||
path = path.strip('/')
|
||||
nbpath = self._get_os_path(name, path=path)
|
||||
return os.path.isfile(nbpath)
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def list_dirs(self, path):
|
||||
"""List the directories for a given API style path."""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path('', path)
|
||||
if not os.path.isdir(os_path):
|
||||
raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
|
||||
elif is_hidden(os_path, self.notebook_dir):
|
||||
self.log.info("Refusing to serve hidden directory, via 404 Error")
|
||||
raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
|
||||
dir_names = os.listdir(os_path)
|
||||
dirs = []
|
||||
for name in dir_names:
|
||||
os_path = self._get_os_path(name, path)
|
||||
if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
|
||||
and self.should_list(name):
|
||||
try:
|
||||
model = self.get_dir_model(name, path)
|
||||
except IOError:
|
||||
pass
|
||||
dirs.append(model)
|
||||
dirs = sorted(dirs, key=sort_key)
|
||||
return dirs
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def get_dir_model(self, name, path=''):
|
||||
"""Get the directory model given a directory name and its API style path"""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(name, path)
|
||||
if not os.path.isdir(os_path):
|
||||
raise IOError('directory does not exist: %r' % os_path)
|
||||
info = os.stat(os_path)
|
||||
last_modified = tz.utcfromtimestamp(info.st_mtime)
|
||||
created = tz.utcfromtimestamp(info.st_ctime)
|
||||
# Create the notebook model.
|
||||
model ={}
|
||||
model['name'] = name
|
||||
model['path'] = path
|
||||
model['last_modified'] = last_modified
|
||||
model['created'] = created
|
||||
model['type'] = 'directory'
|
||||
return model
|
||||
|
||||
def list_notebooks(self, path):
|
||||
"""Returns a list of dictionaries that are the standard model
|
||||
for all notebooks in the relative 'path'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
the URL path that describes the relative path for the
|
||||
listed notebooks
|
||||
|
||||
Returns
|
||||
-------
|
||||
notebooks : list of dicts
|
||||
a list of the notebook models without 'content'
|
||||
"""
|
||||
path = path.strip('/')
|
||||
notebook_names = self.get_notebook_names(path)
|
||||
notebooks = [self.get_notebook(name, path, content=False)
|
||||
for name in notebook_names if self.should_list(name)]
|
||||
notebooks = sorted(notebooks, key=sort_key)
|
||||
return notebooks
|
||||
|
||||
def get_notebook(self, name, path='', content=True):
|
||||
""" Takes a path and name for a notebook and returns its model
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
the name of the notebook
|
||||
path : str
|
||||
the URL path that describes the relative path for
|
||||
the notebook
|
||||
|
||||
Returns
|
||||
-------
|
||||
model : dict
|
||||
the notebook model. If contents=True, returns the 'contents'
|
||||
dict in the model as well.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
if not self.notebook_exists(name=name, path=path):
|
||||
raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
|
||||
os_path = self._get_os_path(name, path)
|
||||
info = os.stat(os_path)
|
||||
last_modified = tz.utcfromtimestamp(info.st_mtime)
|
||||
created = tz.utcfromtimestamp(info.st_ctime)
|
||||
# Create the notebook model.
|
||||
model ={}
|
||||
model['name'] = name
|
||||
model['path'] = path
|
||||
model['last_modified'] = last_modified
|
||||
model['created'] = created
|
||||
model['type'] = 'notebook'
|
||||
if content:
|
||||
with io.open(os_path, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
nb = current.read(f, u'json')
|
||||
except Exception as e:
|
||||
raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
|
||||
self.mark_trusted_cells(nb, name, path)
|
||||
model['content'] = nb
|
||||
return model
|
||||
|
||||
def save_notebook(self, model, name='', path=''):
|
||||
"""Save the notebook model and return the model with no content."""
|
||||
path = path.strip('/')
|
||||
|
||||
if 'content' not in model:
|
||||
raise web.HTTPError(400, u'No notebook JSON data provided')
|
||||
|
||||
# One checkpoint should always exist
|
||||
if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
|
||||
self.create_checkpoint(name, path)
|
||||
|
||||
new_path = model.get('path', path).strip('/')
|
||||
new_name = model.get('name', name)
|
||||
|
||||
if path != new_path or name != new_name:
|
||||
self.rename_notebook(name, path, new_name, new_path)
|
||||
|
||||
# Save the notebook file
|
||||
os_path = self._get_os_path(new_name, new_path)
|
||||
nb = current.to_notebook_json(model['content'])
|
||||
|
||||
self.check_and_sign(nb, new_name, new_path)
|
||||
|
||||
if 'name' in nb['metadata']:
|
||||
nb['metadata']['name'] = u''
|
||||
try:
|
||||
self.log.debug("Autosaving notebook %s", os_path)
|
||||
with io.open(os_path, 'w', encoding='utf-8') as f:
|
||||
current.write(nb, f, u'json')
|
||||
except Exception as e:
|
||||
raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
|
||||
|
||||
# Save .py script as well
|
||||
if self.save_script:
|
||||
py_path = os.path.splitext(os_path)[0] + '.py'
|
||||
self.log.debug("Writing script %s", py_path)
|
||||
try:
|
||||
with io.open(py_path, 'w', encoding='utf-8') as f:
|
||||
current.write(nb, f, u'py')
|
||||
except Exception as e:
|
||||
raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
|
||||
|
||||
model = self.get_notebook(new_name, new_path, content=False)
|
||||
return model
|
||||
|
||||
def update_notebook(self, model, name, path=''):
|
||||
"""Update the notebook's path and/or name"""
|
||||
path = path.strip('/')
|
||||
new_name = model.get('name', name)
|
||||
new_path = model.get('path', path).strip('/')
|
||||
if path != new_path or name != new_name:
|
||||
self.rename_notebook(name, path, new_name, new_path)
|
||||
model = self.get_notebook(new_name, new_path, content=False)
|
||||
return model
|
||||
|
||||
def delete_notebook(self, name, path=''):
|
||||
"""Delete notebook by name and path."""
|
||||
path = path.strip('/')
|
||||
os_path = self._get_os_path(name, path)
|
||||
if not os.path.isfile(os_path):
|
||||
raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
|
||||
|
||||
# clear checkpoints
|
||||
for checkpoint in self.list_checkpoints(name, path):
|
||||
checkpoint_id = checkpoint['id']
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if os.path.isfile(cp_path):
|
||||
self.log.debug("Unlinking checkpoint %s", cp_path)
|
||||
os.unlink(cp_path)
|
||||
|
||||
self.log.debug("Unlinking notebook %s", os_path)
|
||||
os.unlink(os_path)
|
||||
|
||||
def rename_notebook(self, old_name, old_path, new_name, new_path):
|
||||
"""Rename a notebook."""
|
||||
old_path = old_path.strip('/')
|
||||
new_path = new_path.strip('/')
|
||||
if new_name == old_name and new_path == old_path:
|
||||
return
|
||||
|
||||
new_os_path = self._get_os_path(new_name, new_path)
|
||||
old_os_path = self._get_os_path(old_name, old_path)
|
||||
|
||||
# Should we proceed with the move?
|
||||
if os.path.isfile(new_os_path):
|
||||
raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
|
||||
if self.save_script:
|
||||
old_py_path = os.path.splitext(old_os_path)[0] + '.py'
|
||||
new_py_path = os.path.splitext(new_os_path)[0] + '.py'
|
||||
if os.path.isfile(new_py_path):
|
||||
raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
|
||||
|
||||
# Move the notebook file
|
||||
try:
|
||||
shutil.move(old_os_path, new_os_path)
|
||||
except Exception as e:
|
||||
raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
|
||||
|
||||
# Move the checkpoints
|
||||
old_checkpoints = self.list_checkpoints(old_name, old_path)
|
||||
for cp in old_checkpoints:
|
||||
checkpoint_id = cp['id']
|
||||
old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
|
||||
new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
|
||||
if os.path.isfile(old_cp_path):
|
||||
self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
|
||||
shutil.move(old_cp_path, new_cp_path)
|
||||
|
||||
# Move the .py script
|
||||
if self.save_script:
|
||||
shutil.move(old_py_path, new_py_path)
|
||||
|
||||
# Checkpoint-related utilities
|
||||
|
||||
def get_checkpoint_path(self, checkpoint_id, name, path=''):
|
||||
"""find the path to a checkpoint"""
|
||||
path = path.strip('/')
|
||||
basename, _ = os.path.splitext(name)
|
||||
filename = u"{name}-{checkpoint_id}{ext}".format(
|
||||
name=basename,
|
||||
checkpoint_id=checkpoint_id,
|
||||
ext=self.filename_ext,
|
||||
)
|
||||
os_path = self._get_os_path(path=path)
|
||||
cp_dir = os.path.join(os_path, self.checkpoint_dir)
|
||||
ensure_dir_exists(cp_dir)
|
||||
cp_path = os.path.join(cp_dir, filename)
|
||||
return cp_path
|
||||
|
||||
def get_checkpoint_model(self, checkpoint_id, name, path=''):
|
||||
"""construct the info dict for a given checkpoint"""
|
||||
path = path.strip('/')
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
stats = os.stat(cp_path)
|
||||
last_modified = tz.utcfromtimestamp(stats.st_mtime)
|
||||
info = dict(
|
||||
id = checkpoint_id,
|
||||
last_modified = last_modified,
|
||||
)
|
||||
return info
|
||||
|
||||
# public checkpoint API
|
||||
|
||||
def create_checkpoint(self, name, path=''):
|
||||
"""Create a checkpoint from the current state of a notebook"""
|
||||
path = path.strip('/')
|
||||
nb_path = self._get_os_path(name, path)
|
||||
# only the one checkpoint ID:
|
||||
checkpoint_id = u"checkpoint"
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
self.log.debug("creating checkpoint for notebook %s", name)
|
||||
self._copy(nb_path, cp_path)
|
||||
|
||||
# return the checkpoint info
|
||||
return self.get_checkpoint_model(checkpoint_id, name, path)
|
||||
|
||||
def list_checkpoints(self, name, path=''):
|
||||
"""list the checkpoints for a given notebook
|
||||
|
||||
This notebook manager currently only supports one checkpoint per notebook.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
checkpoint_id = "checkpoint"
|
||||
os_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if not os.path.exists(os_path):
|
||||
return []
|
||||
else:
|
||||
return [self.get_checkpoint_model(checkpoint_id, name, path)]
|
||||
|
||||
|
||||
def restore_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""restore a notebook to a checkpointed state"""
|
||||
path = path.strip('/')
|
||||
self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
|
||||
nb_path = self._get_os_path(name, path)
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if not os.path.isfile(cp_path):
|
||||
self.log.debug("checkpoint file does not exist: %s", cp_path)
|
||||
raise web.HTTPError(404,
|
||||
u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
|
||||
)
|
||||
# ensure notebook is readable (never restore from an unreadable notebook)
|
||||
with io.open(cp_path, 'r', encoding='utf-8') as f:
|
||||
current.read(f, u'json')
|
||||
self._copy(cp_path, nb_path)
|
||||
self.log.debug("copying %s -> %s", cp_path, nb_path)
|
||||
|
||||
def delete_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""delete a notebook's checkpoint"""
|
||||
path = path.strip('/')
|
||||
cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
|
||||
if not os.path.isfile(cp_path):
|
||||
raise web.HTTPError(404,
|
||||
u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
|
||||
)
|
||||
self.log.debug("unlinking %s", cp_path)
|
||||
os.unlink(cp_path)
|
||||
|
||||
def info_string(self):
|
||||
return "Serving notebooks from local directory: %s" % self.notebook_dir
|
||||
|
||||
def get_kernel_path(self, name, path='', model=None):
|
||||
""" Return the path to start kernel in """
|
||||
return os.path.join(self.notebook_dir, path)
|
||||
@ -1,288 +0,0 @@
|
||||
"""Tornado handlers for the notebooks web service.
|
||||
|
||||
Authors:
|
||||
|
||||
* Brian Granger
|
||||
"""
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (C) 2011 The IPython Development Team
|
||||
#
|
||||
# Distributed under the terms of the BSD License. The full license is in
|
||||
# the file COPYING, distributed as part of this software.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Imports
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import json
|
||||
|
||||
from tornado import web
|
||||
|
||||
from IPython.html.utils import url_path_join, url_escape
|
||||
from IPython.utils.jsonutil import date_default
|
||||
|
||||
from IPython.html.base.handlers import (IPythonHandler, json_errors,
|
||||
notebook_path_regex, path_regex,
|
||||
notebook_name_regex)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Notebook web service handlers
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class NotebookHandler(IPythonHandler):
|
||||
|
||||
SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
|
||||
|
||||
def notebook_location(self, name, path=''):
|
||||
"""Return the full URL location of a notebook based.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : unicode
|
||||
The base name of the notebook, such as "foo.ipynb".
|
||||
path : unicode
|
||||
The URL path of the notebook.
|
||||
"""
|
||||
return url_escape(url_path_join(
|
||||
self.base_url, 'api', 'notebooks', path, name
|
||||
))
|
||||
|
||||
def _finish_model(self, model, location=True):
|
||||
"""Finish a JSON request with a model, setting relevant headers, etc."""
|
||||
if location:
|
||||
location = self.notebook_location(model['name'], model['path'])
|
||||
self.set_header('Location', location)
|
||||
self.set_header('Last-Modified', model['last_modified'])
|
||||
self.finish(json.dumps(model, default=date_default))
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def get(self, path='', name=None):
|
||||
"""Return a Notebook or list of notebooks.
|
||||
|
||||
* GET with path and no notebook name lists notebooks in a directory
|
||||
* GET with path and notebook name returns notebook JSON
|
||||
"""
|
||||
nbm = self.notebook_manager
|
||||
# Check to see if a notebook name was given
|
||||
if name is None:
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service. This should only handle notebooks
|
||||
# and not directories.
|
||||
dirs = nbm.list_dirs(path)
|
||||
notebooks = []
|
||||
index = []
|
||||
for nb in nbm.list_notebooks(path):
|
||||
if nb['name'].lower() == 'index.ipynb':
|
||||
index.append(nb)
|
||||
else:
|
||||
notebooks.append(nb)
|
||||
notebooks = index + dirs + notebooks
|
||||
self.finish(json.dumps(notebooks, default=date_default))
|
||||
return
|
||||
# get and return notebook representation
|
||||
model = nbm.get_notebook(name, path)
|
||||
self._finish_model(model, location=False)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def patch(self, path='', name=None):
|
||||
"""PATCH renames a notebook without re-uploading content."""
|
||||
nbm = self.notebook_manager
|
||||
if name is None:
|
||||
raise web.HTTPError(400, u'Notebook name missing')
|
||||
model = self.get_json_body()
|
||||
if model is None:
|
||||
raise web.HTTPError(400, u'JSON body missing')
|
||||
model = nbm.update_notebook(model, name, path)
|
||||
self._finish_model(model)
|
||||
|
||||
def _copy_notebook(self, copy_from, path, copy_to=None):
|
||||
"""Copy a notebook in path, optionally specifying the new name.
|
||||
|
||||
Only support copying within the same directory.
|
||||
"""
|
||||
self.log.info(u"Copying notebook from %s/%s to %s/%s",
|
||||
path, copy_from,
|
||||
path, copy_to or '',
|
||||
)
|
||||
model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
|
||||
self.set_status(201)
|
||||
self._finish_model(model)
|
||||
|
||||
def _upload_notebook(self, model, path, name=None):
|
||||
"""Upload a notebook
|
||||
|
||||
If name specified, create it in path/name.
|
||||
"""
|
||||
self.log.info(u"Uploading notebook to %s/%s", path, name or '')
|
||||
if name:
|
||||
model['name'] = name
|
||||
|
||||
model = self.notebook_manager.create_notebook(model, path)
|
||||
self.set_status(201)
|
||||
self._finish_model(model)
|
||||
|
||||
def _create_empty_notebook(self, path, name=None):
|
||||
"""Create an empty notebook in path
|
||||
|
||||
If name specified, create it in path/name.
|
||||
"""
|
||||
self.log.info(u"Creating new notebook in %s/%s", path, name or '')
|
||||
model = {}
|
||||
if name:
|
||||
model['name'] = name
|
||||
model = self.notebook_manager.create_notebook(model, path=path)
|
||||
self.set_status(201)
|
||||
self._finish_model(model)
|
||||
|
||||
def _save_notebook(self, model, path, name):
|
||||
"""Save an existing notebook."""
|
||||
self.log.info(u"Saving notebook at %s/%s", path, name)
|
||||
model = self.notebook_manager.save_notebook(model, name, path)
|
||||
if model['path'] != path.strip('/') or model['name'] != name:
|
||||
# a rename happened, set Location header
|
||||
location = True
|
||||
else:
|
||||
location = False
|
||||
self._finish_model(model, location)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def post(self, path='', name=None):
|
||||
"""Create a new notebook in the specified path.
|
||||
|
||||
POST creates new notebooks. The server always decides on the notebook name.
|
||||
|
||||
POST /api/notebooks/path
|
||||
New untitled notebook in path. If content specified, upload a
|
||||
notebook, otherwise start empty.
|
||||
POST /api/notebooks/path?copy=OtherNotebook.ipynb
|
||||
New copy of OtherNotebook in path
|
||||
"""
|
||||
|
||||
if name is not None:
|
||||
raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
|
||||
|
||||
model = self.get_json_body()
|
||||
|
||||
if model is not None:
|
||||
copy_from = model.get('copy_from')
|
||||
if copy_from:
|
||||
if model.get('content'):
|
||||
raise web.HTTPError(400, "Can't upload and copy at the same time.")
|
||||
self._copy_notebook(copy_from, path)
|
||||
else:
|
||||
self._upload_notebook(model, path)
|
||||
else:
|
||||
self._create_empty_notebook(path)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def put(self, path='', name=None):
|
||||
"""Saves the notebook in the location specified by name and path.
|
||||
|
||||
PUT is very similar to POST, but the requester specifies the name,
|
||||
whereas with POST, the server picks the name.
|
||||
|
||||
PUT /api/notebooks/path/Name.ipynb
|
||||
Save notebook at ``path/Name.ipynb``. Notebook structure is specified
|
||||
in `content` key of JSON request body. If content is not specified,
|
||||
create a new empty notebook.
|
||||
PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
|
||||
Copy OtherNotebook to Name
|
||||
"""
|
||||
if name is None:
|
||||
raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
|
||||
|
||||
model = self.get_json_body()
|
||||
if model:
|
||||
copy_from = model.get('copy_from')
|
||||
if copy_from:
|
||||
if model.get('content'):
|
||||
raise web.HTTPError(400, "Can't upload and copy at the same time.")
|
||||
self._copy_notebook(copy_from, path, name)
|
||||
elif self.notebook_manager.notebook_exists(name, path):
|
||||
self._save_notebook(model, path, name)
|
||||
else:
|
||||
self._upload_notebook(model, path, name)
|
||||
else:
|
||||
self._create_empty_notebook(path, name)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def delete(self, path='', name=None):
|
||||
"""delete the notebook in the given notebook path"""
|
||||
nbm = self.notebook_manager
|
||||
nbm.delete_notebook(name, path)
|
||||
self.set_status(204)
|
||||
self.finish()
|
||||
|
||||
|
||||
class NotebookCheckpointsHandler(IPythonHandler):
|
||||
|
||||
SUPPORTED_METHODS = ('GET', 'POST')
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def get(self, path='', name=None):
|
||||
"""get lists checkpoints for a notebook"""
|
||||
nbm = self.notebook_manager
|
||||
checkpoints = nbm.list_checkpoints(name, path)
|
||||
data = json.dumps(checkpoints, default=date_default)
|
||||
self.finish(data)
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def post(self, path='', name=None):
|
||||
"""post creates a new checkpoint"""
|
||||
nbm = self.notebook_manager
|
||||
checkpoint = nbm.create_checkpoint(name, path)
|
||||
data = json.dumps(checkpoint, default=date_default)
|
||||
location = url_path_join(self.base_url, 'api/notebooks',
|
||||
path, name, 'checkpoints', checkpoint['id'])
|
||||
self.set_header('Location', url_escape(location))
|
||||
self.set_status(201)
|
||||
self.finish(data)
|
||||
|
||||
|
||||
class ModifyNotebookCheckpointsHandler(IPythonHandler):
|
||||
|
||||
SUPPORTED_METHODS = ('POST', 'DELETE')
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def post(self, path, name, checkpoint_id):
|
||||
"""post restores a notebook from a checkpoint"""
|
||||
nbm = self.notebook_manager
|
||||
nbm.restore_checkpoint(checkpoint_id, name, path)
|
||||
self.set_status(204)
|
||||
self.finish()
|
||||
|
||||
@web.authenticated
|
||||
@json_errors
|
||||
def delete(self, path, name, checkpoint_id):
|
||||
"""delete clears a checkpoint for a given notebook"""
|
||||
nbm = self.notebook_manager
|
||||
nbm.delete_checkpoint(checkpoint_id, name, path)
|
||||
self.set_status(204)
|
||||
self.finish()
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# URL to handler mappings
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
_checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
|
||||
|
||||
default_handlers = [
|
||||
(r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
|
||||
(r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
|
||||
ModifyNotebookCheckpointsHandler),
|
||||
(r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
|
||||
(r"/api/notebooks%s" % path_regex, NotebookHandler),
|
||||
]
|
||||
|
||||
@ -1,287 +0,0 @@
|
||||
"""A base class notebook manager.
|
||||
|
||||
Authors:
|
||||
|
||||
* Brian Granger
|
||||
* Zach Sailer
|
||||
"""
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (C) 2011 The IPython Development Team
|
||||
#
|
||||
# Distributed under the terms of the BSD License. The full license is in
|
||||
# the file COPYING, distributed as part of this software.
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Imports
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
from fnmatch import fnmatch
|
||||
import itertools
|
||||
import os
|
||||
|
||||
from IPython.config.configurable import LoggingConfigurable
|
||||
from IPython.nbformat import current, sign
|
||||
from IPython.utils.traitlets import Instance, Unicode, List
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Classes
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class NotebookManager(LoggingConfigurable):
|
||||
|
||||
filename_ext = Unicode(u'.ipynb')
|
||||
|
||||
notary = Instance(sign.NotebookNotary)
|
||||
def _notary_default(self):
|
||||
return sign.NotebookNotary(parent=self)
|
||||
|
||||
hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
|
||||
Glob patterns to hide in file and directory listings.
|
||||
""")
|
||||
|
||||
# NotebookManager API part 1: methods that must be
|
||||
# implemented in subclasses.
|
||||
|
||||
def path_exists(self, path):
|
||||
"""Does the API-style path (directory) actually exist?
|
||||
|
||||
Override this method in subclasses.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path does indeed exist.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_hidden(self, path):
|
||||
"""Does the API style path correspond to a hidden directory or file?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : string
|
||||
The path to check. This is an API path (`/` separated,
|
||||
relative to base notebook-dir).
|
||||
|
||||
Returns
|
||||
-------
|
||||
exists : bool
|
||||
Whether the path is hidden.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def notebook_exists(self, name, path=''):
|
||||
"""Returns a True if the notebook exists. Else, returns False.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The name of the notebook you are checking.
|
||||
path : string
|
||||
The relative path to the notebook (with '/' as separator)
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
"""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def list_dirs(self, path):
|
||||
"""List the directory models for a given API style path."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
# TODO: Remove this after we create the contents web service and directories are
|
||||
# no longer listed by the notebook web service.
|
||||
def get_dir_model(self, name, path=''):
|
||||
"""Get the directory model given a directory name and its API style path.
|
||||
|
||||
The keys in the model should be:
|
||||
* name
|
||||
* path
|
||||
* last_modified
|
||||
* created
|
||||
* type='directory'
|
||||
"""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def list_notebooks(self, path=''):
|
||||
"""Return a list of notebook dicts without content.
|
||||
|
||||
This returns a list of dicts, each of the form::
|
||||
|
||||
dict(notebook_id=notebook,name=name)
|
||||
|
||||
This list of dicts should be sorted by name::
|
||||
|
||||
data = sorted(data, key=lambda item: item['name'])
|
||||
"""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def get_notebook(self, name, path='', content=True):
|
||||
"""Get the notebook model with or without content."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def save_notebook(self, model, name, path=''):
|
||||
"""Save the notebook and return the model with no content."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def update_notebook(self, model, name, path=''):
|
||||
"""Update the notebook and return the model with no content."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def delete_notebook(self, name, path=''):
|
||||
"""Delete notebook by name and path."""
|
||||
raise NotImplementedError('must be implemented in a subclass')
|
||||
|
||||
def create_checkpoint(self, name, path=''):
|
||||
"""Create a checkpoint of the current state of a notebook
|
||||
|
||||
Returns a checkpoint_id for the new checkpoint.
|
||||
"""
|
||||
raise NotImplementedError("must be implemented in a subclass")
|
||||
|
||||
def list_checkpoints(self, name, path=''):
|
||||
"""Return a list of checkpoints for a given notebook"""
|
||||
return []
|
||||
|
||||
def restore_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""Restore a notebook from one of its checkpoints"""
|
||||
raise NotImplementedError("must be implemented in a subclass")
|
||||
|
||||
def delete_checkpoint(self, checkpoint_id, name, path=''):
|
||||
"""delete a checkpoint for a notebook"""
|
||||
raise NotImplementedError("must be implemented in a subclass")
|
||||
|
||||
def info_string(self):
|
||||
return "Serving notebooks"
|
||||
|
||||
# NotebookManager API part 2: methods that have useable default
|
||||
# implementations, but can be overridden in subclasses.
|
||||
|
||||
def get_kernel_path(self, name, path='', model=None):
|
||||
""" Return the path to start kernel in """
|
||||
return path
|
||||
|
||||
def increment_filename(self, basename, path=''):
|
||||
"""Increment a notebook filename without the .ipynb to make it unique.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
basename : unicode
|
||||
The name of a notebook without the ``.ipynb`` file extension.
|
||||
path : unicode
|
||||
The URL path of the notebooks directory
|
||||
|
||||
Returns
|
||||
-------
|
||||
name : unicode
|
||||
A notebook name (with the .ipynb extension) that starts
|
||||
with basename and does not refer to any existing notebook.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
for i in itertools.count():
|
||||
name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
|
||||
ext=self.filename_ext)
|
||||
if not self.notebook_exists(name, path):
|
||||
break
|
||||
return name
|
||||
|
||||
def create_notebook(self, model=None, path=''):
|
||||
"""Create a new notebook and return its model with no content."""
|
||||
path = path.strip('/')
|
||||
if model is None:
|
||||
model = {}
|
||||
if 'content' not in model:
|
||||
metadata = current.new_metadata(name=u'')
|
||||
model['content'] = current.new_notebook(metadata=metadata)
|
||||
if 'name' not in model:
|
||||
model['name'] = self.increment_filename('Untitled', path)
|
||||
|
||||
model['path'] = path
|
||||
model = self.save_notebook(model, model['name'], model['path'])
|
||||
return model
|
||||
|
||||
def copy_notebook(self, from_name, to_name=None, path=''):
|
||||
"""Copy an existing notebook and return its new model.
|
||||
|
||||
If to_name not specified, increment `from_name-Copy#.ipynb`.
|
||||
"""
|
||||
path = path.strip('/')
|
||||
model = self.get_notebook(from_name, path)
|
||||
if not to_name:
|
||||
base = os.path.splitext(from_name)[0] + '-Copy'
|
||||
to_name = self.increment_filename(base, path)
|
||||
model['name'] = to_name
|
||||
model = self.save_notebook(model, to_name, path)
|
||||
return model
|
||||
|
||||
def log_info(self):
|
||||
self.log.info(self.info_string())
|
||||
|
||||
def trust_notebook(self, name, path=''):
|
||||
"""Explicitly trust a notebook
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
The filename of the notebook
|
||||
path : string
|
||||
The notebook's directory
|
||||
"""
|
||||
model = self.get_notebook(name, path)
|
||||
nb = model['content']
|
||||
self.log.warn("Trusting notebook %s/%s", path, name)
|
||||
self.notary.mark_cells(nb, True)
|
||||
self.save_notebook(model, name, path)
|
||||
|
||||
def check_and_sign(self, nb, name, path=''):
|
||||
"""Check for trusted cells, and sign the notebook.
|
||||
|
||||
Called as a part of saving notebooks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nb : dict
|
||||
The notebook structure
|
||||
name : string
|
||||
The filename of the notebook
|
||||
path : string
|
||||
The notebook's directory
|
||||
"""
|
||||
if self.notary.check_cells(nb):
|
||||
self.notary.sign(nb)
|
||||
else:
|
||||
self.log.warn("Saving untrusted notebook %s/%s", path, name)
|
||||
|
||||
def mark_trusted_cells(self, nb, name, path=''):
|
||||
"""Mark cells as trusted if the notebook signature matches.
|
||||
|
||||
Called as a part of loading notebooks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nb : dict
|
||||
The notebook structure
|
||||
name : string
|
||||
The filename of the notebook
|
||||
path : string
|
||||
The notebook's directory
|
||||
"""
|
||||
trusted = self.notary.check_signature(nb)
|
||||
if not trusted:
|
||||
self.log.warn("Notebook %s/%s is not trusted", path, name)
|
||||
self.notary.mark_cells(nb, trusted)
|
||||
|
||||
def should_list(self, name):
|
||||
"""Should this file/directory name be displayed in a listing?"""
|
||||
return not any(fnmatch(name, glob) for glob in self.hide_globs)
|
||||
@ -1,2 +1,7 @@
|
||||
/*!
|
||||
*
|
||||
* IPython auth
|
||||
*
|
||||
*/
|
||||
@import "login.less";
|
||||
@import "logout.less";
|
||||
@ -1,32 +1,24 @@
|
||||
//----------------------------------------------------------------------------
|
||||
// Copyright (C) 2008-2011 The IPython Development Team
|
||||
//
|
||||
// Distributed under the terms of the BSD License. The full license is in
|
||||
// the file COPYING, distributed as part of this software.
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
//============================================================================
|
||||
// Events
|
||||
//============================================================================
|
||||
// Copyright (c) IPython Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
// Give us an object to bind all events to. This object should be created
|
||||
// before all other objects so it exists when others register event handlers.
|
||||
// To trigger an event handler:
|
||||
// $([IPython.events]).trigger('event.Namespace');
|
||||
// To handle it:
|
||||
// $([IPython.events]).on('event.Namespace',function () {});
|
||||
// To register an event handler:
|
||||
//
|
||||
// require(['base/js/events'], function (events) {
|
||||
// events.on("event.Namespace", function () { do_stuff(); });
|
||||
// });
|
||||
|
||||
var IPython = (function (IPython) {
|
||||
define(['base/js/namespace', 'jquery'], function(IPython, $) {
|
||||
"use strict";
|
||||
|
||||
var utils = IPython.utils;
|
||||
|
||||
var Events = function () {};
|
||||
|
||||
|
||||
var events = new Events();
|
||||
|
||||
// Backwards compatability.
|
||||
IPython.Events = Events;
|
||||
IPython.events = new Events();
|
||||
|
||||
return IPython;
|
||||
|
||||
}(IPython));
|
||||
|
||||
IPython.events = events;
|
||||
|
||||
return $([events]);
|
||||
});
|
||||
|
||||
@ -1,34 +1,8 @@
|
||||
//----------------------------------------------------------------------------
|
||||
// Copyright (C) 2011 The IPython Development Team
|
||||
//
|
||||
// Distributed under the terms of the BSD License. The full license is in
|
||||
// the file COPYING, distributed as part of this software.
|
||||
//----------------------------------------------------------------------------
|
||||
// Copyright (c) IPython Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
var IPython = IPython || {};
|
||||
|
||||
IPython.version = "3.0.0-dev";
|
||||
|
||||
IPython.namespace = function (ns_string) {
|
||||
"use strict";
|
||||
|
||||
var parts = ns_string.split('.'),
|
||||
parent = IPython,
|
||||
i;
|
||||
|
||||
// String redundant leading global
|
||||
if (parts[0] === "IPython") {
|
||||
parts = parts.slice(1);
|
||||
}
|
||||
|
||||
for (i=0; i<parts.length; i+=1) {
|
||||
// Create property if it doesn't exist
|
||||
if (typeof parent[parts[i]] === "undefined") {
|
||||
parent[parts[i]] = {};
|
||||
}
|
||||
}
|
||||
return parent;
|
||||
};
|
||||
|
||||
|
||||
|
||||
define([], function(){
|
||||
IPython.version = "3.0.0-dev";
|
||||
return IPython;
|
||||
});
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
//----------------------------------------------------------------------------
|
||||
// Copyright (C) 2008-2011 The IPython Development Team
|
||||
//
|
||||
// Distributed under the terms of the BSD License. The full license is in
|
||||
// the file COPYING, distributed as part of this software.
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
//============================================================================
|
||||
// On document ready
|
||||
//============================================================================
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
"use strict";
|
||||
|
||||
IPython.page = new IPython.Page();
|
||||
IPython.page.show();
|
||||
|
||||
});
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
// Copyright (c) IPython Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
define([
|
||||
'base/js/namespace',
|
||||
'jquery',
|
||||
'base/js/utils',
|
||||
], function(IPython, $, utils) {
|
||||
"use strict";
|
||||
|
||||
var KernelSelector = function(selector, notebook) {
|
||||
this.selector = selector;
|
||||
this.notebook = notebook;
|
||||
this.events = notebook.events;
|
||||
this.current_selection = notebook.default_kernel_name;
|
||||
this.kernelspecs = {};
|
||||
if (this.selector !== undefined) {
|
||||
this.element = $(selector);
|
||||
this.request_kernelspecs();
|
||||
}
|
||||
this.bind_events();
|
||||
// Make the object globally available for user convenience & inspection
|
||||
IPython.kernelselector = this;
|
||||
};
|
||||
|
||||
KernelSelector.prototype.request_kernelspecs = function() {
|
||||
var url = utils.url_join_encode(this.notebook.base_url, 'api/kernelspecs');
|
||||
$.ajax(url, {success: $.proxy(this._got_kernelspecs, this)});
|
||||
};
|
||||
|
||||
KernelSelector.prototype._got_kernelspecs = function(data, status, xhr) {
|
||||
this.kernelspecs = {};
|
||||
var menu = this.element.find("#kernel_selector");
|
||||
var change_kernel_submenu = $("#menu-change-kernel-submenu");
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var ks = data[i];
|
||||
this.kernelspecs[ks.name] = ks;
|
||||
var ksentry = $("<li>").attr("id", "kernel-" +ks.name).append($('<a>')
|
||||
.attr('href', '#')
|
||||
.click($.proxy(this.change_kernel, this, ks.name))
|
||||
.text(ks.display_name));
|
||||
menu.append(ksentry);
|
||||
|
||||
var ks_submenu_entry = $("<li>").attr("id", "kernel-submenu-"+ks.name).append($('<a>')
|
||||
.attr('href', '#')
|
||||
.click($.proxy(this.change_kernel, this, ks.name))
|
||||
.text(ks.display_name));
|
||||
change_kernel_submenu.append(ks_submenu_entry);
|
||||
}
|
||||
};
|
||||
|
||||
KernelSelector.prototype.change_kernel = function(kernel_name) {
|
||||
if (kernel_name === this.current_selection) {
|
||||
return;
|
||||
}
|
||||
var ks = this.kernelspecs[kernel_name];
|
||||
try {
|
||||
this.notebook.start_session(kernel_name);
|
||||
} catch (e) {
|
||||
if (e.name === 'SessionAlreadyStarting') {
|
||||
console.log("Cannot change kernel while waiting for pending session start.");
|
||||
} else {
|
||||
// unhandled error
|
||||
throw e;
|
||||
}
|
||||
// only trigger spec_changed if change was successful
|
||||
return;
|
||||
}
|
||||
this.events.trigger('spec_changed.Kernel', ks);
|
||||
};
|
||||
|
||||
KernelSelector.prototype.bind_events = function() {
|
||||
var that = this;
|
||||
this.events.on('spec_changed.Kernel', function(event, data) {
|
||||
that.current_selection = data.name;
|
||||
that.element.find("#current_kernel_spec").find('.kernel_name').text(data.display_name);
|
||||
});
|
||||
|
||||
this.events.on('started.Session', function(events, session) {
|
||||
if (session.kernel_name !== that.current_selection) {
|
||||
// If we created a 'python' session, we only know if it's Python
|
||||
// 3 or 2 on the server's reply, so we fire the event again to
|
||||
// set things up.
|
||||
var ks = that.kernelspecs[session.kernel_name];
|
||||
that.events.trigger('spec_changed.Kernel', ks);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {'KernelSelector': KernelSelector};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@
|
||||
#kernel_selector_widget {
|
||||
margin-right: 1em;
|
||||
float:right;
|
||||
|
||||
& > button {
|
||||
.btn();
|
||||
.btn-default();
|
||||
.btn-sm();
|
||||
|
||||
& > span.caret {
|
||||
margin-top:0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,40 +1,52 @@
|
||||
//----------------------------------------------------------------------------
|
||||
// Copyright (C) 2014 The IPython Development Team
|
||||
//
|
||||
// Distributed under the terms of the BSD License. The full license is in
|
||||
// the file COPYING, distributed as part of this software.
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
//============================================================================
|
||||
// Running Kernels List
|
||||
//============================================================================
|
||||
|
||||
var IPython = (function (IPython) {
|
||||
// Copyright (c) IPython Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
define([
|
||||
'base/js/namespace',
|
||||
'jquery',
|
||||
'tree/js/notebooklist',
|
||||
], function(IPython, $, notebooklist) {
|
||||
"use strict";
|
||||
|
||||
var utils = IPython.utils;
|
||||
|
||||
var KernelList = function (selector, options) {
|
||||
IPython.NotebookList.call(this, selector, options, 'running');
|
||||
// Constructor
|
||||
//
|
||||
// Parameters:
|
||||
// selector: string
|
||||
// options: dictionary
|
||||
// Dictionary of keyword arguments.
|
||||
// session_list: SessionList instance
|
||||
// base_url: string
|
||||
// notebook_path: string
|
||||
notebooklist.NotebookList.call(this, selector, $.extend({
|
||||
element_name: 'running'},
|
||||
options));
|
||||
};
|
||||
|
||||
KernelList.prototype = Object.create(IPython.NotebookList.prototype);
|
||||
KernelList.prototype = Object.create(notebooklist.NotebookList.prototype);
|
||||
|
||||
KernelList.prototype.sessions_loaded = function (d) {
|
||||
this.sessions = d;
|
||||
this.clear_list();
|
||||
var item;
|
||||
for (var path in d) {
|
||||
item = this.new_notebook_item(-1);
|
||||
this.add_link('', path, item);
|
||||
this.add_shutdown_button(item, this.sessions[path]);
|
||||
var item, path_name;
|
||||
for (path_name in d) {
|
||||
if (!d.hasOwnProperty(path_name)) {
|
||||
// nothing is safe in javascript
|
||||
continue;
|
||||
}
|
||||
item = this.new_item(-1);
|
||||
this.add_link({
|
||||
name: path_name,
|
||||
path: '',
|
||||
type: 'notebook',
|
||||
}, item);
|
||||
this.add_shutdown_button(item, this.sessions[path_name]);
|
||||
}
|
||||
|
||||
$('#running_list_header').toggle($.isEmptyObject(d));
|
||||
}
|
||||
};
|
||||
|
||||
// Backwards compatability.
|
||||
IPython.KernelList = KernelList;
|
||||
|
||||
return IPython;
|
||||
|
||||
}(IPython));
|
||||
return {'KernelList': KernelList};
|
||||
});
|
||||
|
||||
@ -1,2 +1,7 @@
|
||||
/*!
|
||||
*
|
||||
* IPython tree view
|
||||
*
|
||||
*/
|
||||
@import "altuploadform.less";
|
||||
@import "tree.less";
|
||||
@ -1,22 +1,27 @@
|
||||
//----------------------------------------------------------------------------
|
||||
// Copyright (C) 2013 The IPython Development Team
|
||||
//
|
||||
// Distributed under the terms of the BSD License. The full license is in
|
||||
// the file COPYING, distributed as part of this software.
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
//============================================================================
|
||||
// Basic Widgets
|
||||
//============================================================================
|
||||
// Copyright (c) IPython Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
define([
|
||||
"widgets/js/manager",
|
||||
"widgets/js/widget_bool",
|
||||
"widgets/js/widget_button",
|
||||
"widgets/js/widget_container",
|
||||
"widgets/js/widget_box",
|
||||
"widgets/js/widget_float",
|
||||
"widgets/js/widget_image",
|
||||
"widgets/js/widget_int",
|
||||
"widgets/js/widget_selection",
|
||||
"widgets/js/widget_selectioncontainer",
|
||||
"widgets/js/widget_string",
|
||||
], function(){ return true; });
|
||||
], function(widgetmanager) {
|
||||
|
||||
// Register all of the loaded views with the widget manager.
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
for (var target_name in arguments[i]) {
|
||||
if (arguments[i].hasOwnProperty(target_name)) {
|
||||
widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {'WidgetManager': widgetmanager.WidgetManager};
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue