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