diff --git a/IPython/frontend/html/notebook/azurenbmanager.py b/IPython/frontend/html/notebook/azurenbmanager.py
new file mode 100644
index 0000000..bc0d6ee
--- /dev/null
+++ b/IPython/frontend/html/notebook/azurenbmanager.py
@@ -0,0 +1,143 @@
+"""A notebook manager that uses Azure blob storage.
+
+Authors:
+
+* Brian Granger
+"""
+
+#-----------------------------------------------------------------------------
+# Copyright (C) 2012 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 datetime
+
+import azure
+from azure.storage import BlobService
+
+from tornado import web
+
+from .basenbmanager import BaseNotebookManager
+from IPython.nbformat import current
+from IPython.utils.traitlets import Unicode, Instance
+
+
+#-----------------------------------------------------------------------------
+# Classes
+#-----------------------------------------------------------------------------
+
+class AzureNotebookManager(BaseNotebookManager):
+
+ account_name = Unicode('', config=True, help='Azure storage account name.')
+ account_key = Unicode('', config=True, help='Azure storage account key.')
+ container = Unicode('', config=True, help='Container name for notebooks.')
+
+ blob_service_host_base = Unicode('.blob.core.windows.net', config=True,
+ help='The basename for the blob service URL. If running on the preview site this '
+ 'will be .blob.core.azure-preview.com.')
+ def _blob_service_host_base_changed(self, new):
+ self._update_service_host_base(new)
+
+ blob_service = Instance('azure.storage.BlobService')
+ def _blob_service_default(self):
+ return BlobService(account_name=self.account_name, account_key=self.account_key)
+
+ def __init__(self, **kwargs):
+ super(AzureNotebookManager, self).__init__(**kwargs)
+ self._update_service_host_base(self.blob_service_host_base)
+ self._create_container()
+
+ def _update_service_host_base(self, shb):
+ azure.BLOB_SERVICE_HOST_BASE = shb
+
+ def _create_container(self):
+ self.blob_service.create_container(self.container)
+
+ def load_notebook_names(self):
+ """On startup load the notebook ids and names from Azure.
+
+ The blob names are the notebook ids and the notebook names are stored
+ as blob metadata.
+ """
+ self.mapping = {}
+ blobs = self.blob_service.list_blobs(self.container)
+ ids = [blob.name for blob in blobs]
+
+ for id in ids:
+ md = self.blob_service.get_blob_metadata(self.container, id)
+ name = md['x-ms-meta-nbname']
+ self.mapping[id] = name
+
+ def list_notebooks(self):
+ """List all notebooks in the container.
+
+ This version uses `self.mapping` as the authoritative notebook list.
+ """
+ data = [dict(notebook_id=id,name=name) for id, name in self.mapping.items()]
+ data = sorted(data, key=lambda item: item['name'])
+ return data
+
+ def read_notebook_object(self, notebook_id):
+ """Get the object representation of a notebook by notebook_id."""
+ if not self.notebook_exists(notebook_id):
+ raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+ try:
+ s = self.blob_service.get_blob(self.container, notebook_id)
+ except:
+ raise web.HTTPError(500, u'Notebook cannot be read.')
+ try:
+ # v1 and v2 and json in the .ipynb files.
+ nb = current.reads(s, u'json')
+ except:
+ raise web.HTTPError(500, u'Unreadable JSON notebook.')
+ # Todo: The last modified should actually be saved in the notebook document.
+ # We are just using the current datetime until that is implemented.
+ last_modified = datetime.datetime.utcnow()
+ return last_modified, nb
+
+ def write_notebook_object(self, nb, notebook_id=None):
+ """Save an existing notebook object by notebook_id."""
+ try:
+ new_name = nb.metadata.name
+ except AttributeError:
+ raise web.HTTPError(400, u'Missing notebook name')
+
+ if notebook_id is None:
+ notebook_id = self.new_notebook_id(new_name)
+
+ if notebook_id not in self.mapping:
+ raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+
+ try:
+ data = current.writes(nb, u'json')
+ except Exception as e:
+ raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
+
+ metadata = {'nbname': new_name}
+ try:
+ self.blob_service.put_blob(self.container, notebook_id, data, 'BlockBlob', x_ms_meta_name_values=metadata)
+ except Exception as e:
+ raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
+
+ self.mapping[notebook_id] = new_name
+ return notebook_id
+
+ def delete_notebook(self, notebook_id):
+ """Delete notebook by notebook_id."""
+ if not self.notebook_exists(notebook_id):
+ raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+ try:
+ self.blob_service.delete_blob(self.container, notebook_id)
+ except Exception as e:
+ raise web.HTTPError(400, u'Unexpected error while deleting notebook: %s' % e)
+ else:
+ self.delete_notebook_id(notebook_id)
+
+ def log_info(self):
+ self.log.info("Serving notebooks from Azure storage: %s, %s", self.account_name, self.container)
diff --git a/IPython/frontend/html/notebook/basenbmanager.py b/IPython/frontend/html/notebook/basenbmanager.py
new file mode 100644
index 0000000..b7e9297
--- /dev/null
+++ b/IPython/frontend/html/notebook/basenbmanager.py
@@ -0,0 +1,205 @@
+"""A base class notebook manager.
+
+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 os
+import uuid
+
+from tornado import web
+
+from IPython.config.configurable import LoggingConfigurable
+from IPython.nbformat import current
+from IPython.utils.traitlets import List, Dict, Unicode, TraitError
+
+#-----------------------------------------------------------------------------
+# Classes
+#-----------------------------------------------------------------------------
+
+class BaseNotebookManager(LoggingConfigurable):
+
+ # Todo:
+ # The notebook_dir attribute is used to mean a couple of different things:
+ # 1. Where the notebooks are stored if FileNotebookManager is used.
+ # 2. The cwd of the kernel for a project.
+ # Right now we use this attribute in a number of different places and
+ # we are going to have to disentagle all of this.
+ notebook_dir = Unicode(os.getcwdu(), config=True, help="""
+ The directory to use for notebooks.
+ """)
+ 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)
+
+ allowed_formats = List([u'json',u'py'])
+
+ # Map notebook_ids to notebook names
+ mapping = Dict()
+
+ def load_notebook_names(self):
+ """Load the notebook names into memory.
+
+ This should be called once immediately after the notebook manager
+ is created to load the existing notebooks into the mapping in
+ memory.
+ """
+ self.list_notebooks()
+
+ def list_notebooks(self):
+ """List all notebooks.
+
+ 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 new_notebook_id(self, name):
+ """Generate a new notebook_id for a name and store its mapping."""
+ # 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')))
+
+ notebook_id = unicode(uuid.uuid4())
+ self.mapping[notebook_id] = name
+ return notebook_id
+
+ def delete_notebook_id(self, notebook_id):
+ """Delete a notebook's id in the mapping.
+
+ This doesn't delete the actual notebook, only its entry in the mapping.
+ """
+ del self.mapping[notebook_id]
+
+ def notebook_exists(self, notebook_id):
+ """Does a notebook exist?"""
+ return notebook_id in self.mapping
+
+ def get_notebook(self, notebook_id, format=u'json'):
+ """Get the representation of a notebook in format by notebook_id."""
+ format = unicode(format)
+ if format not in self.allowed_formats:
+ raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
+ last_modified, nb = self.read_notebook_object(notebook_id)
+ 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)
+ name = nb.get('name','notebook')
+ return last_modified, name, data
+
+ def read_notebook_object(self, notebook_id):
+ """Get the object representation of a notebook by notebook_id."""
+ raise NotImplementedError('must be implemented in a subclass')
+
+ 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.
+ """
+ if format not in self.allowed_formats:
+ raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
+
+ try:
+ nb = current.reads(data.decode('utf-8'), format)
+ except:
+ raise web.HTTPError(400, u'Invalid JSON data')
+
+ if name is None:
+ try:
+ name = nb.metadata.name
+ except AttributeError:
+ raise web.HTTPError(400, u'Missing notebook name')
+ nb.metadata.name = name
+
+ notebook_id = self.write_notebook_object(nb)
+ return notebook_id
+
+ def save_notebook(self, notebook_id, data, name=None, format=u'json'):
+ """Save an existing notebook by notebook_id."""
+ if format not in self.allowed_formats:
+ raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
+
+ try:
+ nb = current.reads(data.decode('utf-8'), format)
+ except:
+ raise web.HTTPError(400, u'Invalid JSON data')
+
+ if name is not None:
+ nb.metadata.name = name
+ self.write_notebook_object(nb, notebook_id)
+
+ def write_notebook_object(self, nb, notebook_id=None):
+ """Write a notebook object and return its notebook_id.
+
+ If notebook_id is None, this method should create a new notebook_id.
+ If notebook_id is not None, this method should check to make sure it
+ exists and is valid.
+ """
+ raise NotImplementedError('must be implemented in a subclass')
+
+ def delete_notebook(self, notebook_id):
+ """Delete notebook by notebook_id."""
+ raise NotImplementedError('must be implemented in a subclass')
+
+ def increment_filename(self, name):
+ """Increment a filename to make it unique.
+
+ This exists for notebook stores that must have unique names. When a notebook
+ is created or copied this method constructs a unique filename, typically
+ by appending an integer to the name.
+ """
+ return name
+
+ def new_notebook(self):
+ """Create a new notebook and return its notebook_id."""
+ name = self.increment_filename('Untitled')
+ metadata = current.new_metadata(name=name)
+ nb = current.new_notebook(metadata=metadata)
+ notebook_id = self.write_notebook_object(nb)
+ return notebook_id
+
+ def copy_notebook(self, notebook_id):
+ """Copy an existing notebook and return its notebook_id."""
+ last_mod, nb = self.read_notebook_object(notebook_id)
+ name = nb.metadata.name + '-Copy'
+ name = self.increment_filename(name)
+ nb.metadata.name = name
+ notebook_id = self.write_notebook_object(nb)
+ return notebook_id
+
+ def log_info(self):
+ self.log.info("Serving notebooks")
\ No newline at end of file
diff --git a/IPython/frontend/html/notebook/notebookmanager.py b/IPython/frontend/html/notebook/filenbmanager.py
similarity index 55%
rename from IPython/frontend/html/notebook/notebookmanager.py
rename to IPython/frontend/html/notebook/filenbmanager.py
index 279b692..34d7d26 100644
--- a/IPython/frontend/html/notebook/notebookmanager.py
+++ b/IPython/frontend/html/notebook/filenbmanager.py
@@ -6,7 +6,7 @@ Authors:
"""
#-----------------------------------------------------------------------------
-# Copyright (C) 2008-2011 The IPython Development Team
+# 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.
@@ -19,34 +19,19 @@ Authors:
import datetime
import io
import os
-import uuid
import glob
from tornado import web
-from IPython.config.configurable import LoggingConfigurable
+from .basenbmanager import BaseNotebookManager
from IPython.nbformat import current
-from IPython.utils.traitlets import Unicode, List, Dict, Bool, TraitError
+from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
-class NotebookManager(LoggingConfigurable):
-
- notebook_dir = Unicode(os.getcwdu(), config=True, help="""
- The directory to use for notebooks.
- """)
- 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)
+class FileNotebookManager(BaseNotebookManager):
save_script = Bool(False, config=True,
help="""Automatically create a Python script when saving the notebook.
@@ -59,24 +44,21 @@ class NotebookManager(LoggingConfigurable):
)
filename_ext = Unicode(u'.ipynb')
- allowed_formats = List([u'json',u'py'])
- # Map notebook_ids to notebook names
- mapping = Dict()
# Map notebook names to notebook_ids
rev_mapping = Dict()
- def list_notebooks(self):
- """List all notebooks in the notebook dir.
-
- This returns a list of dicts of the form::
-
- dict(notebook_id=notebook,name=name)
- """
+ def get_notebook_names(self):
+ """List all notebook names in the notebook dir."""
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]
+ return names
+
+ def list_notebooks(self):
+ """List all notebooks in the notebook dir."""
+ names = self.get_notebook_names()
data = []
for name in names:
@@ -90,30 +72,20 @@ class NotebookManager(LoggingConfigurable):
def new_notebook_id(self, name):
"""Generate a new notebook_id for a name and store its mappings."""
- # 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')))
-
- notebook_id = unicode(uuid.uuid4())
-
- self.mapping[notebook_id] = name
+ notebook_id = super(FileNotebookManager, self).new_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."""
+ """Delete a notebook's id in the mapping."""
name = self.mapping[notebook_id]
- del self.mapping[notebook_id]
+ super(FileNotebookManager, self).delete_notebook_id(notebook_id)
del self.rev_mapping[name]
def notebook_exists(self, notebook_id):
"""Does a notebook exist?"""
- if notebook_id not in self.mapping:
+ exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
+ if not exists:
return False
path = self.get_path_by_name(self.mapping[notebook_id])
return os.path.isfile(path)
@@ -132,22 +104,7 @@ class NotebookManager(LoggingConfigurable):
path = os.path.join(self.notebook_dir, filename)
return path
- def get_notebook(self, notebook_id, format=u'json'):
- """Get the representation of a notebook in format by notebook_id."""
- format = unicode(format)
- if format not in self.allowed_formats:
- raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
- last_modified, nb = self.get_notebook_object(notebook_id)
- 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)
- name = nb.get('name','notebook')
- return last_modified, name, data
-
- def get_notebook_object(self, notebook_id):
+ def read_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):
@@ -165,60 +122,27 @@ class NotebookManager(LoggingConfigurable):
nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
return last_modified, nb
- 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.
- """
- if format not in self.allowed_formats:
- raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
-
- try:
- nb = current.reads(data.decode('utf-8'), format)
- except:
- raise web.HTTPError(400, u'Invalid JSON data')
-
- if name is None:
- try:
- name = nb.metadata.name
- except AttributeError:
- raise web.HTTPError(400, u'Missing notebook name')
- nb.metadata.name = name
-
- notebook_id = self.new_notebook_id(name)
- self.save_notebook_object(notebook_id, nb)
- return notebook_id
-
- def save_notebook(self, notebook_id, data, name=None, format=u'json'):
- """Save an existing notebook by notebook_id."""
- if format not in self.allowed_formats:
- raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
-
+ def write_notebook_object(self, nb, notebook_id=None):
+ """Save an existing notebook object by notebook_id."""
try:
- nb = current.reads(data.decode('utf-8'), format)
- except:
- raise web.HTTPError(400, u'Invalid JSON data')
+ new_name = nb.metadata.name
+ except AttributeError:
+ raise web.HTTPError(400, u'Missing notebook name')
- if name is not None:
- nb.metadata.name = name
- self.save_notebook_object(notebook_id, nb)
+ if notebook_id is None:
+ notebook_id = self.new_notebook_id(new_name)
- def save_notebook_object(self, notebook_id, nb):
- """Save an existing notebook object by notebook_id."""
if notebook_id not in self.mapping:
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+
old_name = self.mapping[notebook_id]
- try:
- new_name = nb.metadata.name
- except AttributeError:
- raise web.HTTPError(400, u'Missing notebook name')
path = self.get_path_by_name(new_name)
try:
with open(path,'w') as f:
current.write(nb, f, u'json')
except Exception as e:
raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
+
# save .py script as well
if self.save_script:
pypath = os.path.splitext(path)[0] + '.py'
@@ -228,6 +152,7 @@ class NotebookManager(LoggingConfigurable):
except Exception as e:
raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
+ # remove old files if the name changed
if old_name != new_name:
old_path = self.get_path_by_name(old_name)
if os.path.isfile(old_path):
@@ -239,6 +164,8 @@ class NotebookManager(LoggingConfigurable):
self.mapping[notebook_id] = new_name
self.rev_mapping[new_name] = notebook_id
del self.rev_mapping[old_name]
+
+ return notebook_id
def delete_notebook(self, notebook_id):
"""Delete notebook by notebook_id."""
@@ -263,24 +190,7 @@ class NotebookManager(LoggingConfigurable):
break
else:
i = i+1
- return path, name
-
- def new_notebook(self):
- """Create a new notebook and return its notebook_id."""
- path, name = self.increment_filename('Untitled')
- notebook_id = self.new_notebook_id(name)
- metadata = current.new_metadata(name=name)
- nb = current.new_notebook(metadata=metadata)
- with open(path,'w') as f:
- current.write(nb, f, u'json')
- return notebook_id
+ return name
- def copy_notebook(self, notebook_id):
- """Copy an existing notebook and return its notebook_id."""
- last_mod, nb = self.get_notebook_object(notebook_id)
- name = nb.metadata.name + '-Copy'
- path, name = self.increment_filename(name)
- nb.metadata.name = name
- notebook_id = self.new_notebook_id(name)
- self.save_notebook_object(notebook_id, nb)
- return notebook_id
+ def log_info(self):
+ self.log.info("Serving notebooks from local directory: %s", self.notebook_dir)
diff --git a/IPython/frontend/html/notebook/notebookapp.py b/IPython/frontend/html/notebook/notebookapp.py
index 13f13c9..de80b94 100644
--- a/IPython/frontend/html/notebook/notebookapp.py
+++ b/IPython/frontend/html/notebook/notebookapp.py
@@ -51,7 +51,8 @@ from .handlers import (LoginHandler, LogoutHandler,
MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
FileFindHandler,
)
-from .notebookmanager import NotebookManager
+from .basenbmanager import BaseNotebookManager
+from .filenbmanager import FileNotebookManager
from .clustermanager import ClusterManager
from IPython.config.application import catch_config_error, boolean_flag
@@ -66,7 +67,11 @@ from IPython.zmq.ipkernel import (
aliases as ipkernel_aliases,
IPKernelApp
)
-from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
+from IPython.utils.importstring import import_item
+from IPython.utils.traitlets import (
+ Dict, Unicode, Integer, List, Enum, Bool,
+ DottedObjectName
+)
from IPython.utils import py3compat
from IPython.utils.path import filefind
@@ -215,7 +220,7 @@ flags['read-only'] = (
)
# Add notebook manager flags
-flags.update(boolean_flag('script', 'NotebookManager.save_script',
+flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
'Auto-save a .py script everytime the .ipynb notebook is saved',
'Do not auto-save .py scripts for every notebook'))
@@ -232,7 +237,7 @@ aliases.update({
'port-retries': 'NotebookApp.port_retries',
'keyfile': 'NotebookApp.keyfile',
'certfile': 'NotebookApp.certfile',
- 'notebook-dir': 'NotebookManager.notebook_dir',
+ 'notebook-dir': 'BaseNotebookManager.notebook_dir',
'browser': 'NotebookApp.browser',
})
@@ -260,7 +265,8 @@ class NotebookApp(BaseIPythonApplication):
"""
examples = _examples
- classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
+ classes = IPythonConsoleApp.classes + [MappingKernelManager, BaseNotebookManager,
+ FileNotebookManager]
flags = Dict(flags)
aliases = Dict(aliases)
@@ -404,6 +410,10 @@ class NotebookApp(BaseIPythonApplication):
else:
self.log.info("Using MathJax: %s", new)
+ notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
+ config=True,
+ help='The notebook manager class to use.')
+
def parse_command_line(self, argv=None):
super(NotebookApp, self).parse_command_line(argv)
if argv is None:
@@ -421,7 +431,7 @@ class NotebookApp(BaseIPythonApplication):
else:
self.file_to_run = f
nbdir = os.path.dirname(f)
- self.config.NotebookManager.notebook_dir = nbdir
+ self.config.BaseNotebookManager.notebook_dir = nbdir
def init_configurables(self):
# force Session default to be secure
@@ -430,9 +440,10 @@ class NotebookApp(BaseIPythonApplication):
config=self.config, log=self.log, kernel_argv=self.kernel_argv,
connection_dir = self.profile_dir.security_dir,
)
- self.notebook_manager = NotebookManager(config=self.config, log=self.log)
- self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
- self.notebook_manager.list_notebooks()
+ kls = import_item(self.notebook_manager_class)
+ self.notebook_manager = kls(config=self.config, log=self.log)
+ self.notebook_manager.log_info()
+ self.notebook_manager.load_notebook_names()
self.cluster_manager = ClusterManager(config=self.config, log=self.log)
self.cluster_manager.update_profiles()
diff --git a/IPython/frontend/html/notebook/tests/test_nbmanager.py b/IPython/frontend/html/notebook/tests/test_nbmanager.py
index 41290d9..d151386 100644
--- a/IPython/frontend/html/notebook/tests/test_nbmanager.py
+++ b/IPython/frontend/html/notebook/tests/test_nbmanager.py
@@ -7,28 +7,28 @@ from tempfile import NamedTemporaryFile
from IPython.utils.tempdir import TemporaryDirectory
from IPython.utils.traitlets import TraitError
-from IPython.frontend.html.notebook.notebookmanager import NotebookManager
+from IPython.frontend.html.notebook.filenbmanager import FileNotebookManager
class TestNotebookManager(TestCase):
def test_nb_dir(self):
with TemporaryDirectory() as td:
- km = NotebookManager(notebook_dir=td)
- self.assertEqual(km.notebook_dir, td)
+ km = FileNotebookManager(notebook_dir=td)
+ self.assertEquals(km.notebook_dir, td)
def test_create_nb_dir(self):
with TemporaryDirectory() as td:
nbdir = os.path.join(td, 'notebooks')
- km = NotebookManager(notebook_dir=nbdir)
- self.assertEqual(km.notebook_dir, nbdir)
+ km = FileNotebookManager(notebook_dir=nbdir)
+ self.assertEquals(km.notebook_dir, nbdir)
def test_missing_nb_dir(self):
with TemporaryDirectory() as td:
nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
- self.assertRaises(TraitError, NotebookManager, notebook_dir=nbdir)
+ self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
def test_invalid_nb_dir(self):
with NamedTemporaryFile() as tf:
- self.assertRaises(TraitError, NotebookManager, notebook_dir=tf.name)
+ self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
diff --git a/IPython/testing/iptest.py b/IPython/testing/iptest.py
index fd305cc..9e37416 100644
--- a/IPython/testing/iptest.py
+++ b/IPython/testing/iptest.py
@@ -164,6 +164,7 @@ have['oct2py'] = test_for('oct2py')
have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
have['wx'] = test_for('wx')
have['wx.aui'] = test_for('wx.aui')
+have['azure'] = test_for('azure')
if os.name == 'nt':
min_zmq = (2,1,7)
@@ -303,6 +304,9 @@ def make_exclude():
exclusions.append(ipjoin('extensions', 'rmagic'))
exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
+ if not have['azure']:
+ exclusions.append(ipjoin('frontend', 'html', 'notebook', 'azurenbmanager'))
+
# This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
if sys.platform == 'win32':
exclusions = [s.replace('\\','\\\\') for s in exclusions]
diff --git a/docs/source/interactive/htmlnotebook.txt b/docs/source/interactive/htmlnotebook.txt
index 1bd9192..602d400 100644
--- a/docs/source/interactive/htmlnotebook.txt
+++ b/docs/source/interactive/htmlnotebook.txt
@@ -243,7 +243,6 @@ and then on any cell that you need to protect, use::
if script:
# rest of the cell...
-
Keyboard use
------------
@@ -333,9 +332,11 @@ notebook server over ``https://``, not over plain ``http://``. The startup
message from the server prints this, but it's easy to overlook and think the
server is for some reason non-responsive.
+Quick how to's
+==============
-Quick Howto: running a public notebook server
-=============================================
+Running a public notebook server
+--------------------------------
If you want to access your notebook server remotely with just a web browser,
here is a quick set of instructions. Start by creating a certificate file and
@@ -365,7 +366,7 @@ You can then start the notebook and access it later by pointing your browser to
``https://your.host.com:9999`` with ``ipython notebook --profile=nbserver``.
Running with a different URL prefix
-===================================
+-----------------------------------
The notebook dashboard (i.e. the default landing page with an overview
of all your notebooks) typically lives at a URL path of
@@ -379,6 +380,27 @@ modifying ``ipython_notebook_config.py``)::
c.NotebookApp.base_kernel_url = '/ipython/'
c.NotebookApp.webapp_settings = {'static_url_prefix':'/ipython/static/'}
+Using a different notebook store
+--------------------------------
+
+By default the notebook server stores notebooks as files in the working
+directory of the notebook server, also known as the ``notebook_dir``. This
+logic is implemented in the :class:`FileNotebookManager` class. However, the
+server can be configured to use a different notebook manager class, which can
+store the notebooks in a different format. Currently, we ship a
+:class:`AzureNotebookManager` class that stores notebooks in Azure blob
+storage. This can be used by adding the following lines to your
+``ipython_notebook_config.py`` file::
+
+ c.NotebookApp.notebook_manager_class = 'IPython.frontend.html.notebook.azurenbmanager.AzureNotebookManager'
+ c.AzureNotebookManager.account_name = u'paste_your_account_name_here'
+ c.AzureNotebookManager.account_key = u'paste_your_account_key_here'
+ c.AzureNotebookManager.container = u'notebooks'
+
+In addition to providing your Azure Blob Storage account name and key, you will
+have to provide a container name; you can use multiple containers to organize
+your Notebooks.
+
.. _notebook_format:
The notebook format
@@ -423,7 +445,7 @@ cell, when exported to python format::
print "hello IPython"
-Known Issues
+Known issues
============
When behind a proxy, especially if your system or browser is set to autodetect