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