##// END OF EJS Templates
Merge pull request #1886 from tkf/fix-notebook-rename...
Fernando Perez -
r7404:68a629ff merge
parent child Browse files
Show More
@@ -1,275 +1,276 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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 datetime
19 import datetime
20 import io
20 import io
21 import os
21 import os
22 import uuid
22 import uuid
23 import glob
23 import glob
24
24
25 from tornado import web
25 from tornado import web
26
26
27 from IPython.config.configurable import LoggingConfigurable
27 from IPython.config.configurable import LoggingConfigurable
28 from IPython.nbformat import current
28 from IPython.nbformat import current
29 from IPython.utils.traitlets import Unicode, List, Dict, Bool
29 from IPython.utils.traitlets import Unicode, List, Dict, Bool
30
30
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32 # Classes
32 # Classes
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34
34
35 class NotebookManager(LoggingConfigurable):
35 class NotebookManager(LoggingConfigurable):
36
36
37 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
37 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
38 The directory to use for notebooks.
38 The directory to use for notebooks.
39 """)
39 """)
40
40
41 save_script = Bool(False, config=True,
41 save_script = Bool(False, config=True,
42 help="""Automatically create a Python script when saving the notebook.
42 help="""Automatically create a Python script when saving the notebook.
43
43
44 For easier use of import, %run and %load across notebooks, a
44 For easier use of import, %run and %load across notebooks, a
45 <notebook-name>.py script will be created next to any
45 <notebook-name>.py script will be created next to any
46 <notebook-name>.ipynb on each save. This can also be set with the
46 <notebook-name>.ipynb on each save. This can also be set with the
47 short `--script` flag.
47 short `--script` flag.
48 """
48 """
49 )
49 )
50
50
51 filename_ext = Unicode(u'.ipynb')
51 filename_ext = Unicode(u'.ipynb')
52 allowed_formats = List([u'json',u'py'])
52 allowed_formats = List([u'json',u'py'])
53
53
54 # Map notebook_ids to notebook names
54 # Map notebook_ids to notebook names
55 mapping = Dict()
55 mapping = Dict()
56 # Map notebook names to notebook_ids
56 # Map notebook names to notebook_ids
57 rev_mapping = Dict()
57 rev_mapping = Dict()
58
58
59 def list_notebooks(self):
59 def list_notebooks(self):
60 """List all notebooks in the notebook dir.
60 """List all notebooks in the notebook dir.
61
61
62 This returns a list of dicts of the form::
62 This returns a list of dicts of the form::
63
63
64 dict(notebook_id=notebook,name=name)
64 dict(notebook_id=notebook,name=name)
65 """
65 """
66 names = glob.glob(os.path.join(self.notebook_dir,
66 names = glob.glob(os.path.join(self.notebook_dir,
67 '*' + self.filename_ext))
67 '*' + self.filename_ext))
68 names = [os.path.splitext(os.path.basename(name))[0]
68 names = [os.path.splitext(os.path.basename(name))[0]
69 for name in names]
69 for name in names]
70
70
71 data = []
71 data = []
72 for name in names:
72 for name in names:
73 if name not in self.rev_mapping:
73 if name not in self.rev_mapping:
74 notebook_id = self.new_notebook_id(name)
74 notebook_id = self.new_notebook_id(name)
75 else:
75 else:
76 notebook_id = self.rev_mapping[name]
76 notebook_id = self.rev_mapping[name]
77 data.append(dict(notebook_id=notebook_id,name=name))
77 data.append(dict(notebook_id=notebook_id,name=name))
78 data = sorted(data, key=lambda item: item['name'])
78 data = sorted(data, key=lambda item: item['name'])
79 return data
79 return data
80
80
81 def new_notebook_id(self, name):
81 def new_notebook_id(self, name):
82 """Generate a new notebook_id for a name and store its mappings."""
82 """Generate a new notebook_id for a name and store its mappings."""
83 # TODO: the following will give stable urls for notebooks, but unless
83 # TODO: the following will give stable urls for notebooks, but unless
84 # the notebooks are immediately redirected to their new urls when their
84 # the notebooks are immediately redirected to their new urls when their
85 # filemname changes, nasty inconsistencies result. So for now it's
85 # filemname changes, nasty inconsistencies result. So for now it's
86 # disabled and instead we use a random uuid4() call. But we leave the
86 # disabled and instead we use a random uuid4() call. But we leave the
87 # logic here so that we can later reactivate it, whhen the necessary
87 # logic here so that we can later reactivate it, whhen the necessary
88 # url redirection code is written.
88 # url redirection code is written.
89 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
89 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
90 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
90 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
91
91
92 notebook_id = unicode(uuid.uuid4())
92 notebook_id = unicode(uuid.uuid4())
93
93
94 self.mapping[notebook_id] = name
94 self.mapping[notebook_id] = name
95 self.rev_mapping[name] = notebook_id
95 self.rev_mapping[name] = notebook_id
96 return notebook_id
96 return notebook_id
97
97
98 def delete_notebook_id(self, notebook_id):
98 def delete_notebook_id(self, notebook_id):
99 """Delete a notebook's id only. This doesn't delete the actual notebook."""
99 """Delete a notebook's id only. This doesn't delete the actual notebook."""
100 name = self.mapping[notebook_id]
100 name = self.mapping[notebook_id]
101 del self.mapping[notebook_id]
101 del self.mapping[notebook_id]
102 del self.rev_mapping[name]
102 del self.rev_mapping[name]
103
103
104 def notebook_exists(self, notebook_id):
104 def notebook_exists(self, notebook_id):
105 """Does a notebook exist?"""
105 """Does a notebook exist?"""
106 if notebook_id not in self.mapping:
106 if notebook_id not in self.mapping:
107 return False
107 return False
108 path = self.get_path_by_name(self.mapping[notebook_id])
108 path = self.get_path_by_name(self.mapping[notebook_id])
109 return os.path.isfile(path)
109 return os.path.isfile(path)
110
110
111 def find_path(self, notebook_id):
111 def find_path(self, notebook_id):
112 """Return a full path to a notebook given its notebook_id."""
112 """Return a full path to a notebook given its notebook_id."""
113 try:
113 try:
114 name = self.mapping[notebook_id]
114 name = self.mapping[notebook_id]
115 except KeyError:
115 except KeyError:
116 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
116 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
117 return self.get_path_by_name(name)
117 return self.get_path_by_name(name)
118
118
119 def get_path_by_name(self, name):
119 def get_path_by_name(self, name):
120 """Return a full path to a notebook given its name."""
120 """Return a full path to a notebook given its name."""
121 filename = name + self.filename_ext
121 filename = name + self.filename_ext
122 path = os.path.join(self.notebook_dir, filename)
122 path = os.path.join(self.notebook_dir, filename)
123 return path
123 return path
124
124
125 def get_notebook(self, notebook_id, format=u'json'):
125 def get_notebook(self, notebook_id, format=u'json'):
126 """Get the representation of a notebook in format by notebook_id."""
126 """Get the representation of a notebook in format by notebook_id."""
127 format = unicode(format)
127 format = unicode(format)
128 if format not in self.allowed_formats:
128 if format not in self.allowed_formats:
129 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
129 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
130 last_modified, nb = self.get_notebook_object(notebook_id)
130 last_modified, nb = self.get_notebook_object(notebook_id)
131 kwargs = {}
131 kwargs = {}
132 if format == 'json':
132 if format == 'json':
133 # don't split lines for sending over the wire, because it
133 # don't split lines for sending over the wire, because it
134 # should match the Python in-memory format.
134 # should match the Python in-memory format.
135 kwargs['split_lines'] = False
135 kwargs['split_lines'] = False
136 data = current.writes(nb, format, **kwargs)
136 data = current.writes(nb, format, **kwargs)
137 name = nb.get('name','notebook')
137 name = nb.get('name','notebook')
138 return last_modified, name, data
138 return last_modified, name, data
139
139
140 def get_notebook_object(self, notebook_id):
140 def get_notebook_object(self, notebook_id):
141 """Get the NotebookNode representation of a notebook by notebook_id."""
141 """Get the NotebookNode representation of a notebook by notebook_id."""
142 path = self.find_path(notebook_id)
142 path = self.find_path(notebook_id)
143 if not os.path.isfile(path):
143 if not os.path.isfile(path):
144 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
144 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
145 info = os.stat(path)
145 info = os.stat(path)
146 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
146 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
147 with open(path,'r') as f:
147 with open(path,'r') as f:
148 s = f.read()
148 s = f.read()
149 try:
149 try:
150 # v1 and v2 and json in the .ipynb files.
150 # v1 and v2 and json in the .ipynb files.
151 nb = current.reads(s, u'json')
151 nb = current.reads(s, u'json')
152 except:
152 except:
153 raise web.HTTPError(500, u'Unreadable JSON notebook.')
153 raise web.HTTPError(500, u'Unreadable JSON notebook.')
154 # Always use the filename as the notebook name.
154 # Always use the filename as the notebook name.
155 nb.metadata.name = os.path.split(path)[-1].split(u'.')[0]
155 nb.metadata.name = os.path.split(path)[-1].split(u'.')[0]
156 return last_modified, nb
156 return last_modified, nb
157
157
158 def save_new_notebook(self, data, name=None, format=u'json'):
158 def save_new_notebook(self, data, name=None, format=u'json'):
159 """Save a new notebook and return its notebook_id.
159 """Save a new notebook and return its notebook_id.
160
160
161 If a name is passed in, it overrides any values in the notebook data
161 If a name is passed in, it overrides any values in the notebook data
162 and the value in the data is updated to use that value.
162 and the value in the data is updated to use that value.
163 """
163 """
164 if format not in self.allowed_formats:
164 if format not in self.allowed_formats:
165 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
165 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
166
166
167 try:
167 try:
168 nb = current.reads(data.decode('utf-8'), format)
168 nb = current.reads(data.decode('utf-8'), format)
169 except:
169 except:
170 raise web.HTTPError(400, u'Invalid JSON data')
170 raise web.HTTPError(400, u'Invalid JSON data')
171
171
172 if name is None:
172 if name is None:
173 try:
173 try:
174 name = nb.metadata.name
174 name = nb.metadata.name
175 except AttributeError:
175 except AttributeError:
176 raise web.HTTPError(400, u'Missing notebook name')
176 raise web.HTTPError(400, u'Missing notebook name')
177 nb.metadata.name = name
177 nb.metadata.name = name
178
178
179 notebook_id = self.new_notebook_id(name)
179 notebook_id = self.new_notebook_id(name)
180 self.save_notebook_object(notebook_id, nb)
180 self.save_notebook_object(notebook_id, nb)
181 return notebook_id
181 return notebook_id
182
182
183 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
183 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
184 """Save an existing notebook by notebook_id."""
184 """Save an existing notebook by notebook_id."""
185 if format not in self.allowed_formats:
185 if format not in self.allowed_formats:
186 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
186 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
187
187
188 try:
188 try:
189 nb = current.reads(data.decode('utf-8'), format)
189 nb = current.reads(data.decode('utf-8'), format)
190 except:
190 except:
191 raise web.HTTPError(400, u'Invalid JSON data')
191 raise web.HTTPError(400, u'Invalid JSON data')
192
192
193 if name is not None:
193 if name is not None:
194 nb.metadata.name = name
194 nb.metadata.name = name
195 self.save_notebook_object(notebook_id, nb)
195 self.save_notebook_object(notebook_id, nb)
196
196
197 def save_notebook_object(self, notebook_id, nb):
197 def save_notebook_object(self, notebook_id, nb):
198 """Save an existing notebook object by notebook_id."""
198 """Save an existing notebook object by notebook_id."""
199 if notebook_id not in self.mapping:
199 if notebook_id not in self.mapping:
200 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
200 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
201 old_name = self.mapping[notebook_id]
201 old_name = self.mapping[notebook_id]
202 try:
202 try:
203 new_name = nb.metadata.name
203 new_name = nb.metadata.name
204 except AttributeError:
204 except AttributeError:
205 raise web.HTTPError(400, u'Missing notebook name')
205 raise web.HTTPError(400, u'Missing notebook name')
206 path = self.get_path_by_name(new_name)
206 path = self.get_path_by_name(new_name)
207 try:
207 try:
208 with open(path,'w') as f:
208 with open(path,'w') as f:
209 current.write(nb, f, u'json')
209 current.write(nb, f, u'json')
210 except Exception as e:
210 except Exception as e:
211 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
211 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
212 # save .py script as well
212 # save .py script as well
213 if self.save_script:
213 if self.save_script:
214 pypath = os.path.splitext(path)[0] + '.py'
214 pypath = os.path.splitext(path)[0] + '.py'
215 try:
215 try:
216 with io.open(pypath,'w', encoding='utf-8') as f:
216 with io.open(pypath,'w', encoding='utf-8') as f:
217 current.write(nb, f, u'py')
217 current.write(nb, f, u'py')
218 except Exception as e:
218 except Exception as e:
219 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
219 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
220
220
221 if old_name != new_name:
221 if old_name != new_name:
222 old_path = self.get_path_by_name(old_name)
222 old_path = self.get_path_by_name(old_name)
223 if os.path.isfile(old_path):
223 if os.path.isfile(old_path):
224 os.unlink(old_path)
224 os.unlink(old_path)
225 if self.save_script:
225 if self.save_script:
226 old_pypath = os.path.splitext(old_path)[0] + '.py'
226 old_pypath = os.path.splitext(old_path)[0] + '.py'
227 if os.path.isfile(old_pypath):
227 if os.path.isfile(old_pypath):
228 os.unlink(old_pypath)
228 os.unlink(old_pypath)
229 self.mapping[notebook_id] = new_name
229 self.mapping[notebook_id] = new_name
230 self.rev_mapping[new_name] = notebook_id
230 self.rev_mapping[new_name] = notebook_id
231 del self.rev_mapping[old_name]
231
232
232 def delete_notebook(self, notebook_id):
233 def delete_notebook(self, notebook_id):
233 """Delete notebook by notebook_id."""
234 """Delete notebook by notebook_id."""
234 path = self.find_path(notebook_id)
235 path = self.find_path(notebook_id)
235 if not os.path.isfile(path):
236 if not os.path.isfile(path):
236 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
237 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
237 os.unlink(path)
238 os.unlink(path)
238 self.delete_notebook_id(notebook_id)
239 self.delete_notebook_id(notebook_id)
239
240
240 def increment_filename(self, basename):
241 def increment_filename(self, basename):
241 """Return a non-used filename of the form basename<int>.
242 """Return a non-used filename of the form basename<int>.
242
243
243 This searches through the filenames (basename0, basename1, ...)
244 This searches through the filenames (basename0, basename1, ...)
244 until is find one that is not already being used. It is used to
245 until is find one that is not already being used. It is used to
245 create Untitled and Copy names that are unique.
246 create Untitled and Copy names that are unique.
246 """
247 """
247 i = 0
248 i = 0
248 while True:
249 while True:
249 name = u'%s%i' % (basename,i)
250 name = u'%s%i' % (basename,i)
250 path = self.get_path_by_name(name)
251 path = self.get_path_by_name(name)
251 if not os.path.isfile(path):
252 if not os.path.isfile(path):
252 break
253 break
253 else:
254 else:
254 i = i+1
255 i = i+1
255 return path, name
256 return path, name
256
257
257 def new_notebook(self):
258 def new_notebook(self):
258 """Create a new notebook and return its notebook_id."""
259 """Create a new notebook and return its notebook_id."""
259 path, name = self.increment_filename('Untitled')
260 path, name = self.increment_filename('Untitled')
260 notebook_id = self.new_notebook_id(name)
261 notebook_id = self.new_notebook_id(name)
261 metadata = current.new_metadata(name=name)
262 metadata = current.new_metadata(name=name)
262 nb = current.new_notebook(metadata=metadata)
263 nb = current.new_notebook(metadata=metadata)
263 with open(path,'w') as f:
264 with open(path,'w') as f:
264 current.write(nb, f, u'json')
265 current.write(nb, f, u'json')
265 return notebook_id
266 return notebook_id
266
267
267 def copy_notebook(self, notebook_id):
268 def copy_notebook(self, notebook_id):
268 """Copy an existing notebook and return its notebook_id."""
269 """Copy an existing notebook and return its notebook_id."""
269 last_mod, nb = self.get_notebook_object(notebook_id)
270 last_mod, nb = self.get_notebook_object(notebook_id)
270 name = nb.metadata.name + '-Copy'
271 name = nb.metadata.name + '-Copy'
271 path, name = self.increment_filename(name)
272 path, name = self.increment_filename(name)
272 nb.metadata.name = name
273 nb.metadata.name = name
273 notebook_id = self.new_notebook_id(name)
274 notebook_id = self.new_notebook_id(name)
274 self.save_notebook_object(notebook_id, nb)
275 self.save_notebook_object(notebook_id, nb)
275 return notebook_id
276 return notebook_id
General Comments 0
You need to be logged in to leave comments. Login now