##// END OF EJS Templates
Refactoring notebook managers and adding Azure backed storage....
Brian Granger -
Show More
@@ -0,0 +1,140 b''
1 """A notebook manager that uses Azure blob storage.
2
3 Authors:
4
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2012 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import datetime
20
21 import azure
22 from azure.storage import BlobService
23
24 from tornado import web
25
26 from .basenbmanager import BaseNotebookManager
27 from IPython.nbformat import current
28 from IPython.utils.traitlets import Unicode, Instance
29
30
31 #-----------------------------------------------------------------------------
32 # Classes
33 #-----------------------------------------------------------------------------
34
35 class AzureNotebookManager(BaseNotebookManager):
36
37 account_name = Unicode('', config=True, help='Azure storage account name.')
38 account_key = Unicode('', config=True, help='Azure storage account key.')
39 container = Unicode('', config=True, help='Container name for notebooks.')
40
41 blob_service_host_base = Unicode('.blob.core.windows.net', config=True,
42 help='The basename for the blob service URL. If running on the preview site this '
43 'will be .blob.core.azure-preview.com.')
44 def _blob_service_host_base_changed(self, new):
45 self._update_service_host_base(new)
46
47 blob_service = Instance('azure.storage.BlobService')
48 def _blob_service_default(self):
49 return BlobService(account_name=self.account_name, account_key=self.account_key)
50
51 def __init__(self, **kwargs):
52 super(BaseNotebookManager,self).__init__(**kwargs)
53 self._update_service_host_base(self.blob_service_host_base)
54 self._create_container()
55
56 def _update_service_host_base(self, shb):
57 azure.BLOB_SERVICE_HOST_BASE = shb
58
59 def _create_container(self):
60 self.blob_service.create_container(self.container)
61
62 def load_notebook_names(self):
63 """On startup load the notebook ids and names from Azure.
64
65 The blob names are the notebook ids and the notebook names are stored
66 as blob metadata.
67 """
68 self.mapping = {}
69 blobs = self.blob_service.list_blobs(self.container)
70 ids = [blob.name for blob in blobs]
71
72 for id in ids:
73 md = self.blob_service.get_blob_metadata(self.container, id)
74 name = md['x-ms-meta-nbname']
75 self.mapping[id] = name
76
77 def list_notebooks(self):
78 """List all notebooks in the container.
79
80 This version uses `self.mapping` as the authoritative notebook list.
81 """
82 data = [dict(notebook_id=id,name=name) for id, name in self.mapping.items()]
83 data = sorted(data, key=lambda item: item['name'])
84 return data
85
86 def read_notebook_object(self, notebook_id):
87 """Get the object representation of a notebook by notebook_id."""
88 if not self.notebook_exists(notebook_id):
89 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
90 try:
91 s = self.blob_service.get_blob(self.container, notebook_id)
92 except:
93 raise web.HTTPError(500, u'Notebook cannot be read.')
94 try:
95 # v1 and v2 and json in the .ipynb files.
96 nb = current.reads(s, u'json')
97 except:
98 raise web.HTTPError(500, u'Unreadable JSON notebook.')
99 # Todo: The last modified should actually be saved in the notebook document.
100 # We are just using the current datetime until that is implemented.
101 last_modified = datetime.datetime.utcnow()
102 return last_modified, nb
103
104 def write_notebook_object(self, nb, notebook_id=None):
105 """Save an existing notebook object by notebook_id."""
106 try:
107 new_name = nb.metadata.name
108 except AttributeError:
109 raise web.HTTPError(400, u'Missing notebook name')
110
111 if notebook_id is None:
112 notebook_id = self.new_notebook_id(new_name)
113
114 if notebook_id not in self.mapping:
115 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
116
117 try:
118 data = current.writes(nb, u'json')
119 except Exception as e:
120 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
121
122 metadata = {'nbname': new_name}
123 try:
124 self.blob_service.put_blob(self.container, notebook_id, data, 'BlockBlob', x_ms_meta_name_values=metadata)
125 except Exception as e:
126 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
127
128 self.mapping[notebook_id] = new_name
129 return notebook_id
130
131 def delete_notebook(self, notebook_id):
132 """Delete notebook by notebook_id."""
133 if not self.notebook_exists(notebook_id):
134 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
135 try:
136 self.blob_service.delete_blob(self.container, notebook_id)
137 except Exception as e:
138 raise web.HTTPError(400, u'Unexpected error while deleting notebook: %s' % e)
139 else:
140 self.delete_notebook_id(notebook_id)
@@ -0,0 +1,181 b''
1 """A base class notebook manager.
2
3 Authors:
4
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import uuid
20
21 from tornado import web
22
23 from IPython.config.configurable import LoggingConfigurable
24 from IPython.nbformat import current
25 from IPython.utils.traitlets import List, Dict
26
27 #-----------------------------------------------------------------------------
28 # Classes
29 #-----------------------------------------------------------------------------
30
31 class BaseNotebookManager(LoggingConfigurable):
32
33 allowed_formats = List([u'json',u'py'])
34
35 # Map notebook_ids to notebook names
36 mapping = Dict()
37
38 def load_notebook_names(self):
39 """Load the notebook names into memory.
40
41 This should be called once immediately after the notebook manager
42 is created to load the existing notebooks into the mapping in
43 memory.
44 """
45 self.list_notebooks()
46
47 def list_notebooks(self):
48 """List all notebooks.
49
50 This returns a list of dicts, each of the form::
51
52 dict(notebook_id=notebook,name=name)
53
54 This list of dicts should be sorted by name::
55
56 data = sorted(data, key=lambda item: item['name'])
57 """
58 raise NotImplementedError('must be implemented in a subclass')
59
60
61 def new_notebook_id(self, name):
62 """Generate a new notebook_id for a name and store its mapping."""
63 # TODO: the following will give stable urls for notebooks, but unless
64 # the notebooks are immediately redirected to their new urls when their
65 # filemname changes, nasty inconsistencies result. So for now it's
66 # disabled and instead we use a random uuid4() call. But we leave the
67 # logic here so that we can later reactivate it, whhen the necessary
68 # url redirection code is written.
69 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
70 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
71
72 notebook_id = unicode(uuid.uuid4())
73 self.mapping[notebook_id] = name
74 return notebook_id
75
76 def delete_notebook_id(self, notebook_id):
77 """Delete a notebook's id in the mapping.
78
79 This doesn't delete the actual notebook, only its entry in the mapping.
80 """
81 del self.mapping[notebook_id]
82
83 def notebook_exists(self, notebook_id):
84 """Does a notebook exist?"""
85 return notebook_id in self.mapping
86
87 def get_notebook(self, notebook_id, format=u'json'):
88 """Get the representation of a notebook in format by notebook_id."""
89 format = unicode(format)
90 if format not in self.allowed_formats:
91 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
92 last_modified, nb = self.read_notebook_object(notebook_id)
93 kwargs = {}
94 if format == 'json':
95 # don't split lines for sending over the wire, because it
96 # should match the Python in-memory format.
97 kwargs['split_lines'] = False
98 data = current.writes(nb, format, **kwargs)
99 name = nb.get('name','notebook')
100 return last_modified, name, data
101
102 def read_notebook_object(self, notebook_id):
103 """Get the object representation of a notebook by notebook_id."""
104 raise NotImplementedError('must be implemented in a subclass')
105
106 def save_new_notebook(self, data, name=None, format=u'json'):
107 """Save a new notebook and return its notebook_id.
108
109 If a name is passed in, it overrides any values in the notebook data
110 and the value in the data is updated to use that value.
111 """
112 if format not in self.allowed_formats:
113 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
114
115 try:
116 nb = current.reads(data.decode('utf-8'), format)
117 except:
118 raise web.HTTPError(400, u'Invalid JSON data')
119
120 if name is None:
121 try:
122 name = nb.metadata.name
123 except AttributeError:
124 raise web.HTTPError(400, u'Missing notebook name')
125 nb.metadata.name = name
126
127 notebook_id = self.write_notebook_object(nb)
128 return notebook_id
129
130 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
131 """Save an existing notebook by notebook_id."""
132 if format not in self.allowed_formats:
133 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
134
135 try:
136 nb = current.reads(data.decode('utf-8'), format)
137 except:
138 raise web.HTTPError(400, u'Invalid JSON data')
139
140 if name is not None:
141 nb.metadata.name = name
142 self.write_notebook_object(nb, notebook_id)
143
144 def write_notebook_object(self, nb, notebook_id=None):
145 """Write a notebook object and return its notebook_id.
146
147 If notebook_id is None, this method should create a new notebook_id.
148 If notebook_id is not None, this method should check to make sure it
149 exists and is valid.
150 """
151 raise NotImplementedError('must be implemented in a subclass')
152
153 def delete_notebook(self, notebook_id):
154 """Delete notebook by notebook_id."""
155 raise NotImplementedError('must be implemented in a subclass')
156
157 def increment_filename(self, name):
158 """Increment a filename to make it unique.
159
160 This exists for notebook stores that must have unique names. When a notebook
161 is created or copied this method constructs a unique filename, typically
162 by appending an integer to the name.
163 """
164 return name
165
166 def new_notebook(self):
167 """Create a new notebook and return its notebook_id."""
168 name = self.increment_filename('Untitled')
169 metadata = current.new_metadata(name=name)
170 nb = current.new_notebook(metadata=metadata)
171 notebook_id = self.write_notebook_object(nb)
172 return notebook_id
173
174 def copy_notebook(self, notebook_id):
175 """Copy an existing notebook and return its notebook_id."""
176 last_mod, nb = self.read_notebook_object(notebook_id)
177 name = nb.metadata.name + '-Copy'
178 name = self.increment_filename(name)
179 nb.metadata.name = name
180 notebook_id = self.write_notebook_object(nb)
181 return notebook_id
@@ -6,7 +6,7 b' Authors:'
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
@@ -19,20 +19,19 b' Authors:'
19 19 import datetime
20 20 import io
21 21 import os
22 import uuid
23 22 import glob
24 23
25 24 from tornado import web
26 25
27 from IPython.config.configurable import LoggingConfigurable
26 from .basenbmanager import BaseNotebookManager
28 27 from IPython.nbformat import current
29 from IPython.utils.traitlets import Unicode, List, Dict, Bool, TraitError
28 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 29
31 30 #-----------------------------------------------------------------------------
32 31 # Classes
33 32 #-----------------------------------------------------------------------------
34 33
35 class NotebookManager(LoggingConfigurable):
34 class FileNotebookManager(BaseNotebookManager):
36 35
37 36 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
38 37 The directory to use for notebooks.
@@ -59,24 +58,21 b' class NotebookManager(LoggingConfigurable):'
59 58 )
60 59
61 60 filename_ext = Unicode(u'.ipynb')
62 allowed_formats = List([u'json',u'py'])
63 61
64 # Map notebook_ids to notebook names
65 mapping = Dict()
66 62 # Map notebook names to notebook_ids
67 63 rev_mapping = Dict()
68 64
69 def list_notebooks(self):
70 """List all notebooks in the notebook dir.
71
72 This returns a list of dicts of the form::
73
74 dict(notebook_id=notebook,name=name)
75 """
65 def get_notebook_names(self):
66 """List all notebook names in the notebook dir."""
76 67 names = glob.glob(os.path.join(self.notebook_dir,
77 68 '*' + self.filename_ext))
78 69 names = [os.path.splitext(os.path.basename(name))[0]
79 70 for name in names]
71 return names
72
73 def list_notebooks(self):
74 """List all notebooks in the notebook dir."""
75 names = self.get_notebook_names()
80 76
81 77 data = []
82 78 for name in names:
@@ -90,30 +86,20 b' class NotebookManager(LoggingConfigurable):'
90 86
91 87 def new_notebook_id(self, name):
92 88 """Generate a new notebook_id for a name and store its mappings."""
93 # TODO: the following will give stable urls for notebooks, but unless
94 # the notebooks are immediately redirected to their new urls when their
95 # filemname changes, nasty inconsistencies result. So for now it's
96 # disabled and instead we use a random uuid4() call. But we leave the
97 # logic here so that we can later reactivate it, whhen the necessary
98 # url redirection code is written.
99 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
100 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
101
102 notebook_id = unicode(uuid.uuid4())
103
104 self.mapping[notebook_id] = name
89 notebook_id = super(BaseNotebookManager, self).new_notebook_id(name)
105 90 self.rev_mapping[name] = notebook_id
106 91 return notebook_id
107 92
108 93 def delete_notebook_id(self, notebook_id):
109 """Delete a notebook's id only. This doesn't delete the actual notebook."""
94 """Delete a notebook's id in the mapping."""
95 super(BaseNotebookManager, self).delete_notebook_id(notebook_id)
110 96 name = self.mapping[notebook_id]
111 del self.mapping[notebook_id]
112 97 del self.rev_mapping[name]
113 98
114 99 def notebook_exists(self, notebook_id):
115 100 """Does a notebook exist?"""
116 if notebook_id not in self.mapping:
101 exists = super(BaseNotebookManager, self).notebook_exists(notebook_id)
102 if not exists:
117 103 return False
118 104 path = self.get_path_by_name(self.mapping[notebook_id])
119 105 return os.path.isfile(path)
@@ -132,22 +118,7 b' class NotebookManager(LoggingConfigurable):'
132 118 path = os.path.join(self.notebook_dir, filename)
133 119 return path
134 120
135 def get_notebook(self, notebook_id, format=u'json'):
136 """Get the representation of a notebook in format by notebook_id."""
137 format = unicode(format)
138 if format not in self.allowed_formats:
139 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
140 last_modified, nb = self.get_notebook_object(notebook_id)
141 kwargs = {}
142 if format == 'json':
143 # don't split lines for sending over the wire, because it
144 # should match the Python in-memory format.
145 kwargs['split_lines'] = False
146 data = current.writes(nb, format, **kwargs)
147 name = nb.get('name','notebook')
148 return last_modified, name, data
149
150 def get_notebook_object(self, notebook_id):
121 def read_notebook_object(self, notebook_id):
151 122 """Get the NotebookNode representation of a notebook by notebook_id."""
152 123 path = self.find_path(notebook_id)
153 124 if not os.path.isfile(path):
@@ -165,60 +136,27 b' class NotebookManager(LoggingConfigurable):'
165 136 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
166 137 return last_modified, nb
167 138
168 def save_new_notebook(self, data, name=None, format=u'json'):
169 """Save a new notebook and return its notebook_id.
170
171 If a name is passed in, it overrides any values in the notebook data
172 and the value in the data is updated to use that value.
173 """
174 if format not in self.allowed_formats:
175 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
176
177 try:
178 nb = current.reads(data.decode('utf-8'), format)
179 except:
180 raise web.HTTPError(400, u'Invalid JSON data')
181
182 if name is None:
183 try:
184 name = nb.metadata.name
185 except AttributeError:
186 raise web.HTTPError(400, u'Missing notebook name')
187 nb.metadata.name = name
188
189 notebook_id = self.new_notebook_id(name)
190 self.save_notebook_object(notebook_id, nb)
191 return notebook_id
192
193 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
194 """Save an existing notebook by notebook_id."""
195 if format not in self.allowed_formats:
196 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
197
139 def write_notebook_object(self, nb, notebook_id=None):
140 """Save an existing notebook object by notebook_id."""
198 141 try:
199 nb = current.reads(data.decode('utf-8'), format)
200 except:
201 raise web.HTTPError(400, u'Invalid JSON data')
142 new_name = nb.metadata.name
143 except AttributeError:
144 raise web.HTTPError(400, u'Missing notebook name')
202 145
203 if name is not None:
204 nb.metadata.name = name
205 self.save_notebook_object(notebook_id, nb)
146 if notebook_id is None:
147 notebook_id = self.new_notebook_id(new_name)
206 148
207 def save_notebook_object(self, notebook_id, nb):
208 """Save an existing notebook object by notebook_id."""
209 149 if notebook_id not in self.mapping:
210 150 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
151
211 152 old_name = self.mapping[notebook_id]
212 try:
213 new_name = nb.metadata.name
214 except AttributeError:
215 raise web.HTTPError(400, u'Missing notebook name')
216 153 path = self.get_path_by_name(new_name)
217 154 try:
218 155 with open(path,'w') as f:
219 156 current.write(nb, f, u'json')
220 157 except Exception as e:
221 158 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
159
222 160 # save .py script as well
223 161 if self.save_script:
224 162 pypath = os.path.splitext(path)[0] + '.py'
@@ -228,6 +166,7 b' class NotebookManager(LoggingConfigurable):'
228 166 except Exception as e:
229 167 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
230 168
169 # remove old files if the name changed
231 170 if old_name != new_name:
232 171 old_path = self.get_path_by_name(old_name)
233 172 if os.path.isfile(old_path):
@@ -239,6 +178,8 b' class NotebookManager(LoggingConfigurable):'
239 178 self.mapping[notebook_id] = new_name
240 179 self.rev_mapping[new_name] = notebook_id
241 180 del self.rev_mapping[old_name]
181
182 return notebook_id
242 183
243 184 def delete_notebook(self, notebook_id):
244 185 """Delete notebook by notebook_id."""
@@ -263,24 +204,4 b' class NotebookManager(LoggingConfigurable):'
263 204 break
264 205 else:
265 206 i = i+1
266 return path, name
267
268 def new_notebook(self):
269 """Create a new notebook and return its notebook_id."""
270 path, name = self.increment_filename('Untitled')
271 notebook_id = self.new_notebook_id(name)
272 metadata = current.new_metadata(name=name)
273 nb = current.new_notebook(metadata=metadata)
274 with open(path,'w') as f:
275 current.write(nb, f, u'json')
276 return notebook_id
277
278 def copy_notebook(self, notebook_id):
279 """Copy an existing notebook and return its notebook_id."""
280 last_mod, nb = self.get_notebook_object(notebook_id)
281 name = nb.metadata.name + '-Copy'
282 path, name = self.increment_filename(name)
283 nb.metadata.name = name
284 notebook_id = self.new_notebook_id(name)
285 self.save_notebook_object(notebook_id, nb)
286 return notebook_id
207 return name
@@ -66,7 +66,11 b' from IPython.zmq.ipkernel import ('
66 66 aliases as ipkernel_aliases,
67 67 IPKernelApp
68 68 )
69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils.importstring import import_item
70 from IPython.utils.traitlets import (
71 Dict, Unicode, Integer, List, Enum, Bool,
72 DottedObjectName
73 )
70 74 from IPython.utils import py3compat
71 75 from IPython.utils.path import filefind
72 76
@@ -404,6 +408,10 b' class NotebookApp(BaseIPythonApplication):'
404 408 else:
405 409 self.log.info("Using MathJax: %s", new)
406 410
411 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.notebookmanager.NotebookManager',
412 config=True,
413 help='The notebook manager class to use.')
414
407 415 def parse_command_line(self, argv=None):
408 416 super(NotebookApp, self).parse_command_line(argv)
409 417 if argv is None:
@@ -430,9 +438,10 b' class NotebookApp(BaseIPythonApplication):'
430 438 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
431 439 connection_dir = self.profile_dir.security_dir,
432 440 )
433 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
441 kls = import_item(self.notebook_manager_class)
442 self.notebook_manager = kls(config=self.config, log=self.log)
434 443 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
435 self.notebook_manager.list_notebooks()
444 self.notebook_manager.load_notebook_names()
436 445 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
437 446 self.cluster_manager.update_profiles()
438 447
@@ -7,28 +7,28 b' from tempfile import NamedTemporaryFile'
7 7 from IPython.utils.tempdir import TemporaryDirectory
8 8 from IPython.utils.traitlets import TraitError
9 9
10 from IPython.frontend.html.notebook.notebookmanager import NotebookManager
10 from IPython.frontend.html.notebook.filenbmanager import FileNotebookManager
11 11
12 12 class TestNotebookManager(TestCase):
13 13
14 14 def test_nb_dir(self):
15 15 with TemporaryDirectory() as td:
16 km = NotebookManager(notebook_dir=td)
17 self.assertEqual(km.notebook_dir, td)
16 km = FileNotebookManager(notebook_dir=td)
17 self.assertEquals(km.notebook_dir, td)
18 18
19 19 def test_create_nb_dir(self):
20 20 with TemporaryDirectory() as td:
21 21 nbdir = os.path.join(td, 'notebooks')
22 km = NotebookManager(notebook_dir=nbdir)
23 self.assertEqual(km.notebook_dir, nbdir)
22 km = FileNotebookManager(notebook_dir=nbdir)
23 self.assertEquals(km.notebook_dir, nbdir)
24 24
25 25 def test_missing_nb_dir(self):
26 26 with TemporaryDirectory() as td:
27 27 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
28 self.assertRaises(TraitError, NotebookManager, notebook_dir=nbdir)
28 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
29 29
30 30 def test_invalid_nb_dir(self):
31 31 with NamedTemporaryFile() as tf:
32 self.assertRaises(TraitError, NotebookManager, notebook_dir=tf.name)
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
33 33
34 34
General Comments 0
You need to be logged in to leave comments. Login now