##// END OF EJS Templates
simplified named_notebook_path implementation...
Paul Ivanov -
Show More
@@ -1,271 +1,263 b''
1 """A base class notebook manager.
1 """A base class notebook manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 import uuid
20 import uuid
21
21
22 from tornado import web
22 from tornado import web
23 from urllib import quote, unquote
23 from urllib import quote, unquote
24
24
25 from IPython.config.configurable import LoggingConfigurable
25 from IPython.config.configurable import LoggingConfigurable
26 from IPython.nbformat import current
26 from IPython.nbformat import current
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Classes
30 # Classes
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 class NotebookManager(LoggingConfigurable):
33 class NotebookManager(LoggingConfigurable):
34
34
35 # Todo:
35 # Todo:
36 # The notebook_dir attribute is used to mean a couple of different things:
36 # The notebook_dir attribute is used to mean a couple of different things:
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 # 1. Where the notebooks are stored if FileNotebookManager is used.
38 # 2. The cwd of the kernel for a project.
38 # 2. The cwd of the kernel for a project.
39 # Right now we use this attribute in a number of different places and
39 # Right now we use this attribute in a number of different places and
40 # we are going to have to disentangle all of this.
40 # we are going to have to disentangle all of this.
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
42 The directory to use for notebooks.
42 The directory to use for notebooks.
43 """)
43 """)
44
44
45 def named_notebook_path(self, notebook_path):
45 def named_notebook_path(self, notebook_path):
46 """Given a notebook_path name, returns a (name, path) tuple, where
46 """Given a notebook_path name, returns a (name, path) tuple, where
47 name is a .ipynb file, and path is the directory for the file.
47 name is a .ipynb file, and path is the directory for the file.
48
48
49 Parameters
49 Parameters
50 ----------
50 ----------
51 notebook_path : string
51 notebook_path : string
52 A path that may be a .ipynb name or a directory
52 A path that may be a .ipynb name or a directory
53
53
54 Returns
54 Returns
55 -------
55 -------
56 name : string or None
56 name : string or None
57 the filename of the notebook, or None if not a .ipynb extesnsion
57 the filename of the notebook, or None if not a .ipynb extension
58 path : string or None
58 path : string
59 the path to the directory which contains the notebook
59 the path to the directory which contains the notebook
60 """
60 """
61 names = notebook_path.split('/')
61 names = notebook_path.split('/')
62 if len(names) > 1:
62
63 name = names[-1]
63 name = names[-1]
64 if name.endswith(".ipynb"):
64 if name.endswith(".ipynb"):
65 name = name
65 name = name
66 path = notebook_path[:-1]+'/'
66 path = "/".join(names[:-1]) + '/'
67 else:
68 name = None
69 path = notebook_path+'/'
70 else:
71 name = names[0]
72 if name.endswith(".ipynb"):
73 name = name
74 path = None
75 else:
67 else:
76 name = None
68 name = None
77 path = notebook_path+'/'
69 path = "/".join(names) + '/'
78 return name, path
70 return name, path
79
71
80 def url_encode(self, path):
72 def url_encode(self, path):
81 parts = path.split('/')
73 parts = path.split('/')
82 return os.path.join(*[quote(p) for p in parts])
74 return os.path.join(*[quote(p) for p in parts])
83
75
84 def url_decode(self, path):
76 def url_decode(self, path):
85 parts = path.split('/')
77 parts = path.split('/')
86 return os.path.join(*[unquote(p) for p in parts])
78 return os.path.join(*[unquote(p) for p in parts])
87
79
88 def _notebook_dir_changed(self, new):
80 def _notebook_dir_changed(self, new):
89 """do a bit of validation of the notebook dir"""
81 """do a bit of validation of the notebook dir"""
90 if not os.path.isabs(new):
82 if not os.path.isabs(new):
91 # If we receive a non-absolute path, make it absolute.
83 # If we receive a non-absolute path, make it absolute.
92 abs_new = os.path.abspath(new)
84 abs_new = os.path.abspath(new)
93 #self.notebook_dir = os.path.dirname(abs_new)
85 #self.notebook_dir = os.path.dirname(abs_new)
94 return
86 return
95 if os.path.exists(new) and not os.path.isdir(new):
87 if os.path.exists(new) and not os.path.isdir(new):
96 raise TraitError("notebook dir %r is not a directory" % new)
88 raise TraitError("notebook dir %r is not a directory" % new)
97 if not os.path.exists(new):
89 if not os.path.exists(new):
98 self.log.info("Creating notebook dir %s", new)
90 self.log.info("Creating notebook dir %s", new)
99 try:
91 try:
100 os.mkdir(new)
92 os.mkdir(new)
101 except:
93 except:
102 raise TraitError("Couldn't create notebook dir %r" % new)
94 raise TraitError("Couldn't create notebook dir %r" % new)
103
95
104 allowed_formats = List([u'json',u'py'])
96 allowed_formats = List([u'json',u'py'])
105
97
106 def add_new_folder(self, path=None):
98 def add_new_folder(self, path=None):
107 new_path = os.path.join(self.notebook_dir, path)
99 new_path = os.path.join(self.notebook_dir, path)
108 if not os.path.exists(new_path):
100 if not os.path.exists(new_path):
109 os.makedirs(new_path)
101 os.makedirs(new_path)
110 else:
102 else:
111 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
103 raise web.HTTPError(409, u'Directory already exists or creation permission not allowed.')
112
104
113 def load_notebook_names(self, path):
105 def load_notebook_names(self, path):
114 """Load the notebook names into memory.
106 """Load the notebook names into memory.
115
107
116 This should be called once immediately after the notebook manager
108 This should be called once immediately after the notebook manager
117 is created to load the existing notebooks into the mapping in
109 is created to load the existing notebooks into the mapping in
118 memory.
110 memory.
119 """
111 """
120 self.list_notebooks(path)
112 self.list_notebooks(path)
121
113
122 def list_notebooks(self):
114 def list_notebooks(self):
123 """List all notebooks.
115 """List all notebooks.
124
116
125 This returns a list of dicts, each of the form::
117 This returns a list of dicts, each of the form::
126
118
127 dict(notebook_id=notebook,name=name)
119 dict(notebook_id=notebook,name=name)
128
120
129 This list of dicts should be sorted by name::
121 This list of dicts should be sorted by name::
130
122
131 data = sorted(data, key=lambda item: item['name'])
123 data = sorted(data, key=lambda item: item['name'])
132 """
124 """
133 raise NotImplementedError('must be implemented in a subclass')
125 raise NotImplementedError('must be implemented in a subclass')
134
126
135
127
136 def notebook_exists(self, notebook_path):
128 def notebook_exists(self, notebook_path):
137 """Does a notebook exist?"""
129 """Does a notebook exist?"""
138
130
139
131
140 def notebook_model(self, notebook_name, notebook_path=None, content=True):
132 def notebook_model(self, notebook_name, notebook_path=None, content=True):
141 """ Creates the standard notebook model """
133 """ Creates the standard notebook model """
142 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
134 last_modified, contents = self.read_notebook_object(notebook_name, notebook_path)
143 model = {"name": notebook_name,
135 model = {"name": notebook_name,
144 "path": notebook_path,
136 "path": notebook_path,
145 "last_modified (UTC)": last_modified.ctime()}
137 "last_modified (UTC)": last_modified.ctime()}
146 if content == True:
138 if content == True:
147 model['content'] = contents
139 model['content'] = contents
148 return model
140 return model
149
141
150 def get_notebook(self, notebook_name, notebook_path=None, format=u'json'):
142 def get_notebook(self, notebook_name, notebook_path=None, format=u'json'):
151 """Get the representation of a notebook in format by notebook_name."""
143 """Get the representation of a notebook in format by notebook_name."""
152 format = unicode(format)
144 format = unicode(format)
153 if format not in self.allowed_formats:
145 if format not in self.allowed_formats:
154 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
146 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
155 kwargs = {}
147 kwargs = {}
156 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
148 last_mod, nb = self.read_notebook_object(notebook_name, notebook_path)
157 if format == 'json':
149 if format == 'json':
158 # don't split lines for sending over the wire, because it
150 # don't split lines for sending over the wire, because it
159 # should match the Python in-memory format.
151 # should match the Python in-memory format.
160 kwargs['split_lines'] = False
152 kwargs['split_lines'] = False
161 representation = current.writes(nb, format, **kwargs)
153 representation = current.writes(nb, format, **kwargs)
162 name = nb.metadata.get('name', 'notebook')
154 name = nb.metadata.get('name', 'notebook')
163 return last_mod, representation, name
155 return last_mod, representation, name
164
156
165 def read_notebook_object(self, notebook_name, notebook_path=None):
157 def read_notebook_object(self, notebook_name, notebook_path=None):
166 """Get the object representation of a notebook by notebook_id."""
158 """Get the object representation of a notebook by notebook_id."""
167 raise NotImplementedError('must be implemented in a subclass')
159 raise NotImplementedError('must be implemented in a subclass')
168
160
169 def save_new_notebook(self, data, notebook_path = None, name=None, format=u'json'):
161 def save_new_notebook(self, data, notebook_path = None, name=None, format=u'json'):
170 """Save a new notebook and return its name.
162 """Save a new notebook and return its name.
171
163
172 If a name is passed in, it overrides any values in the notebook data
164 If a name is passed in, it overrides any values in the notebook data
173 and the value in the data is updated to use that value.
165 and the value in the data is updated to use that value.
174 """
166 """
175 if format not in self.allowed_formats:
167 if format not in self.allowed_formats:
176 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
168 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
177
169
178 try:
170 try:
179 nb = current.reads(data.decode('utf-8'), format)
171 nb = current.reads(data.decode('utf-8'), format)
180 except:
172 except:
181 raise web.HTTPError(400, u'Invalid JSON data')
173 raise web.HTTPError(400, u'Invalid JSON data')
182
174
183 if name is None:
175 if name is None:
184 try:
176 try:
185 name = nb.metadata.name
177 name = nb.metadata.name
186 except AttributeError:
178 except AttributeError:
187 raise web.HTTPError(400, u'Missing notebook name')
179 raise web.HTTPError(400, u'Missing notebook name')
188 nb.metadata.name = name
180 nb.metadata.name = name
189
181
190 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
182 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
191 return notebook_name
183 return notebook_name
192
184
193 def save_notebook(self, data, notebook_path=None, name=None, new_name=None, format=u'json'):
185 def save_notebook(self, data, notebook_path=None, name=None, new_name=None, format=u'json'):
194 """Save an existing notebook by notebook_name."""
186 """Save an existing notebook by notebook_name."""
195 if format not in self.allowed_formats:
187 if format not in self.allowed_formats:
196 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
188 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
197
189
198 try:
190 try:
199 nb = current.reads(data.decode('utf-8'), format)
191 nb = current.reads(data.decode('utf-8'), format)
200 except:
192 except:
201 raise web.HTTPError(400, u'Invalid JSON data')
193 raise web.HTTPError(400, u'Invalid JSON data')
202
194
203 if name is not None:
195 if name is not None:
204 nb.metadata.name = name
196 nb.metadata.name = name
205 self.write_notebook_object(nb, name, notebook_path, new_name)
197 self.write_notebook_object(nb, name, notebook_path, new_name)
206
198
207 def write_notebook_object(self, nb, notebook_name=None, notebook_path=None, new_name=None):
199 def write_notebook_object(self, nb, notebook_name=None, notebook_path=None, new_name=None):
208 """Write a notebook object and return its notebook_name.
200 """Write a notebook object and return its notebook_name.
209
201
210 If notebook_name is None, this method should create a new notebook_name.
202 If notebook_name is None, this method should create a new notebook_name.
211 If notebook_name is not None, this method should check to make sure it
203 If notebook_name is not None, this method should check to make sure it
212 exists and is valid.
204 exists and is valid.
213 """
205 """
214 raise NotImplementedError('must be implemented in a subclass')
206 raise NotImplementedError('must be implemented in a subclass')
215
207
216 def delete_notebook(self, notebook_name, notebook_path):
208 def delete_notebook(self, notebook_name, notebook_path):
217 """Delete notebook by notebook_id."""
209 """Delete notebook by notebook_id."""
218 raise NotImplementedError('must be implemented in a subclass')
210 raise NotImplementedError('must be implemented in a subclass')
219
211
220 def increment_filename(self, name):
212 def increment_filename(self, name):
221 """Increment a filename to make it unique.
213 """Increment a filename to make it unique.
222
214
223 This exists for notebook stores that must have unique names. When a notebook
215 This exists for notebook stores that must have unique names. When a notebook
224 is created or copied this method constructs a unique filename, typically
216 is created or copied this method constructs a unique filename, typically
225 by appending an integer to the name.
217 by appending an integer to the name.
226 """
218 """
227 return name
219 return name
228
220
229 def new_notebook(self, notebook_path=None):
221 def new_notebook(self, notebook_path=None):
230 """Create a new notebook and return its notebook_name."""
222 """Create a new notebook and return its notebook_name."""
231 name = self.increment_filename('Untitled', notebook_path)
223 name = self.increment_filename('Untitled', notebook_path)
232 metadata = current.new_metadata(name=name)
224 metadata = current.new_metadata(name=name)
233 nb = current.new_notebook(metadata=metadata)
225 nb = current.new_notebook(metadata=metadata)
234 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
226 notebook_name = self.write_notebook_object(nb, notebook_path=notebook_path)
235 return notebook_name
227 return notebook_name
236
228
237 def copy_notebook(self, name, path=None):
229 def copy_notebook(self, name, path=None):
238 """Copy an existing notebook and return its new notebook_name."""
230 """Copy an existing notebook and return its new notebook_name."""
239 last_mod, nb = self.read_notebook_object(name, path)
231 last_mod, nb = self.read_notebook_object(name, path)
240 name = nb.metadata.name + '-Copy'
232 name = nb.metadata.name + '-Copy'
241 name = self.increment_filename(name, path)
233 name = self.increment_filename(name, path)
242 nb.metadata.name = name
234 nb.metadata.name = name
243 notebook_name = self.write_notebook_object(nb, notebook_path = path)
235 notebook_name = self.write_notebook_object(nb, notebook_path = path)
244 return notebook_name
236 return notebook_name
245
237
246 # Checkpoint-related
238 # Checkpoint-related
247
239
248 def create_checkpoint(self, notebook_name, notebook_path=None):
240 def create_checkpoint(self, notebook_name, notebook_path=None):
249 """Create a checkpoint of the current state of a notebook
241 """Create a checkpoint of the current state of a notebook
250
242
251 Returns a checkpoint_id for the new checkpoint.
243 Returns a checkpoint_id for the new checkpoint.
252 """
244 """
253 raise NotImplementedError("must be implemented in a subclass")
245 raise NotImplementedError("must be implemented in a subclass")
254
246
255 def list_checkpoints(self, notebook_name, notebook_path=None):
247 def list_checkpoints(self, notebook_name, notebook_path=None):
256 """Return a list of checkpoints for a given notebook"""
248 """Return a list of checkpoints for a given notebook"""
257 return []
249 return []
258
250
259 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
251 def restore_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
260 """Restore a notebook from one of its checkpoints"""
252 """Restore a notebook from one of its checkpoints"""
261 raise NotImplementedError("must be implemented in a subclass")
253 raise NotImplementedError("must be implemented in a subclass")
262
254
263 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
255 def delete_checkpoint(self, notebook_name, checkpoint_id, notebook_path=None):
264 """delete a checkpoint for a notebook"""
256 """delete a checkpoint for a notebook"""
265 raise NotImplementedError("must be implemented in a subclass")
257 raise NotImplementedError("must be implemented in a subclass")
266
258
267 def log_info(self):
259 def log_info(self):
268 self.log.info(self.info_string())
260 self.log.info(self.info_string())
269
261
270 def info_string(self):
262 def info_string(self):
271 return "Serving notebooks"
263 return "Serving notebooks"
@@ -1,52 +1,59 b''
1 """Tests for the notebook manager."""
1 """Tests for the notebook manager."""
2
2
3 import os
3 import os
4 from unittest import TestCase
4 from unittest import TestCase
5 from tempfile import NamedTemporaryFile
5 from tempfile import NamedTemporaryFile
6
6
7 from IPython.utils.tempdir import TemporaryDirectory
7 from IPython.utils.tempdir import TemporaryDirectory
8 from IPython.utils.traitlets import TraitError
8 from IPython.utils.traitlets import TraitError
9
9
10 from ..filenbmanager import FileNotebookManager
10 from ..filenbmanager import FileNotebookManager
11 from ..nbmanager import NotebookManager
11 from ..nbmanager import NotebookManager
12
12
13 class TestFileNotebookManager(TestCase):
13 class TestFileNotebookManager(TestCase):
14
14
15 def test_nb_dir(self):
15 def test_nb_dir(self):
16 with TemporaryDirectory() as td:
16 with TemporaryDirectory() as td:
17 km = FileNotebookManager(notebook_dir=td)
17 km = FileNotebookManager(notebook_dir=td)
18 self.assertEqual(km.notebook_dir, td)
18 self.assertEqual(km.notebook_dir, td)
19
19
20 def test_create_nb_dir(self):
20 def test_create_nb_dir(self):
21 with TemporaryDirectory() as td:
21 with TemporaryDirectory() as td:
22 nbdir = os.path.join(td, 'notebooks')
22 nbdir = os.path.join(td, 'notebooks')
23 km = FileNotebookManager(notebook_dir=nbdir)
23 km = FileNotebookManager(notebook_dir=nbdir)
24 self.assertEqual(km.notebook_dir, nbdir)
24 self.assertEqual(km.notebook_dir, nbdir)
25
25
26 def test_missing_nb_dir(self):
26 def test_missing_nb_dir(self):
27 with TemporaryDirectory() as td:
27 with TemporaryDirectory() as td:
28 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
28 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
29 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
29 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
30
30
31 def test_invalid_nb_dir(self):
31 def test_invalid_nb_dir(self):
32 with NamedTemporaryFile() as tf:
32 with NamedTemporaryFile() as tf:
33 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
33 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
34
34
35 class TestNotebookManager(TestCase):
35 class TestNotebookManager(TestCase):
36 def test_named_notebook_path(self):
36 def test_named_notebook_path(self):
37 nm = NotebookManager()
37 nm = NotebookManager()
38
38
39 # doesn't end with ipynb, should just be path
39 # doesn't end with ipynb, should just be path
40 name, path = nm.named_notebook_path('hello')
40 name, path = nm.named_notebook_path('hello')
41 self.assertEqual(name, None)
41 self.assertEqual(name, None)
42 self.assertEqual(path, 'hello/')
42 self.assertEqual(path, 'hello/')
43
43
44 name, path = nm.named_notebook_path('/')
45 self.assertEqual(name, None)
46
44 name, path = nm.named_notebook_path('hello.ipynb')
47 name, path = nm.named_notebook_path('hello.ipynb')
45 self.assertEqual(name, 'hello.ipynb')
48 self.assertEqual(name, 'hello.ipynb')
46 self.assertEqual(path, None)
49 self.assertEqual(path, '/')
50
51 name, path = nm.named_notebook_path('/hello.ipynb')
52 self.assertEqual(name, 'hello.ipynb')
53 self.assertEqual(path, '/')
47
54
48 name, path = nm.named_notebook_path('/this/is/a/path/hello.ipynb')
55 name, path = nm.named_notebook_path('/this/is/a/path/hello.ipynb')
49 self.assertEqual(name, 'hello.ipynb')
56 self.assertEqual(name, 'hello.ipynb')
50 self.assertEqual(path, '/this/is/a/path/')
57 self.assertEqual(path, '/this/is/a/path/')
51
58
52
59
General Comments 0
You need to be logged in to leave comments. Login now