##// END OF EJS Templates
When a notebook is written to file, name the metadata name u''.
Brian E. Granger -
Show More
@@ -1,347 +1,357 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) 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 glob
23 23 import shutil
24 24 from unicodedata import normalize
25 25
26 26 from tornado import web
27 27
28 28 from .nbmanager import NotebookManager
29 29 from IPython.nbformat import current
30 30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Classes
34 34 #-----------------------------------------------------------------------------
35 35
36 36 class FileNotebookManager(NotebookManager):
37 37
38 38 save_script = Bool(False, config=True,
39 39 help="""Automatically create a Python script when saving the notebook.
40 40
41 41 For easier use of import, %run and %load across notebooks, a
42 42 <notebook-name>.py script will be created next to any
43 43 <notebook-name>.ipynb on each save. This can also be set with the
44 44 short `--script` flag.
45 45 """
46 46 )
47 47
48 48 checkpoint_dir = Unicode(config=True,
49 49 help="""The location in which to keep notebook checkpoints
50 50
51 51 By default, it is notebook-dir/.ipynb_checkpoints
52 52 """
53 53 )
54 54 def _checkpoint_dir_default(self):
55 55 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
56 56
57 57 def _checkpoint_dir_changed(self, name, old, new):
58 58 """do a bit of validation of the checkpoint dir"""
59 59 if not os.path.isabs(new):
60 60 # If we receive a non-absolute path, make it absolute.
61 61 abs_new = os.path.abspath(new)
62 62 self.checkpoint_dir = abs_new
63 63 return
64 64 if os.path.exists(new) and not os.path.isdir(new):
65 65 raise TraitError("checkpoint dir %r is not a directory" % new)
66 66 if not os.path.exists(new):
67 67 self.log.info("Creating checkpoint dir %s", new)
68 68 try:
69 69 os.mkdir(new)
70 70 except:
71 71 raise TraitError("Couldn't create checkpoint dir %r" % new)
72 72
73 73 filename_ext = Unicode(u'.ipynb')
74 74
75 75 # Map notebook names to notebook_ids
76 76 rev_mapping = Dict()
77 77
78 78 def get_notebook_names(self):
79 79 """List all notebook names in the notebook dir."""
80 80 names = glob.glob(os.path.join(self.notebook_dir,
81 81 '*' + self.filename_ext))
82 82 names = [normalize('NFC', os.path.splitext(os.path.basename(name))[0])
83 83 for name in names]
84 84 return names
85 85
86 86 def list_notebooks(self):
87 87 """List all notebooks in the notebook dir."""
88 88 names = self.get_notebook_names()
89 89
90 90 data = []
91 91 for name in names:
92 92 if name not in self.rev_mapping:
93 93 notebook_id = self.new_notebook_id(name)
94 94 else:
95 95 notebook_id = self.rev_mapping[name]
96 96 data.append(dict(notebook_id=notebook_id,name=name))
97 97 data = sorted(data, key=lambda item: item['name'])
98 98 return data
99 99
100 100 def new_notebook_id(self, name):
101 101 """Generate a new notebook_id for a name and store its mappings."""
102 102 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
103 103 self.rev_mapping[name] = notebook_id
104 104 return notebook_id
105 105
106 106 def delete_notebook_id(self, notebook_id):
107 107 """Delete a notebook's id in the mapping."""
108 108 name = self.mapping[notebook_id]
109 109 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
110 110 del self.rev_mapping[name]
111 111
112 112 def notebook_exists(self, notebook_id):
113 113 """Does a notebook exist?"""
114 114 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
115 115 if not exists:
116 116 return False
117 117 path = self.get_path_by_name(self.mapping[notebook_id])
118 118 return os.path.isfile(path)
119 119
120 120 def get_name(self, notebook_id):
121 121 """get a notebook name, raising 404 if not found"""
122 122 try:
123 123 name = self.mapping[notebook_id]
124 124 except KeyError:
125 125 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
126 126 return name
127 127
128 128 def get_path(self, notebook_id):
129 129 """Return a full path to a notebook given its notebook_id."""
130 130 name = self.get_name(notebook_id)
131 131 return self.get_path_by_name(name)
132 132
133 133 def get_path_by_name(self, name):
134 134 """Return a full path to a notebook given its name."""
135 135 filename = name + self.filename_ext
136 136 path = os.path.join(self.notebook_dir, filename)
137 137 return path
138 138
139 139 def read_notebook_object_from_path(self, path):
140 140 """read a notebook object from a path"""
141 141 info = os.stat(path)
142 142 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
143 143 with open(path,'r') as f:
144 144 s = f.read()
145 145 try:
146 146 # v1 and v2 and json in the .ipynb files.
147 147 nb = current.reads(s, u'json')
148 148 except Exception as e:
149 149 raise web.HTTPError(500, u'Unreadable JSON notebook: %s' % e)
150 150 return last_modified, nb
151 151
152 152 def read_notebook_object(self, notebook_id):
153 153 """Get the Notebook representation of a notebook by notebook_id."""
154 154 path = self.get_path(notebook_id)
155 155 if not os.path.isfile(path):
156 156 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
157 157 last_modified, nb = self.read_notebook_object_from_path(path)
158 158 # Always use the filename as the notebook name.
159 # Eventually we will get rid of the notebook name in the metadata
160 # but for now, that name is just an empty string. Until the notebooks
161 # web service knows about names in URLs we still pass the name
162 # back to the web app using the metadata though.
159 163 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
160 164 return last_modified, nb
161 165
162 166 def write_notebook_object(self, nb, notebook_id=None):
163 167 """Save an existing notebook object by notebook_id."""
164 168 try:
165 169 new_name = normalize('NFC', nb.metadata.name)
166 170 except AttributeError:
167 171 raise web.HTTPError(400, u'Missing notebook name')
168 172
169 173 if notebook_id is None:
170 174 notebook_id = self.new_notebook_id(new_name)
171 175
172 176 if notebook_id not in self.mapping:
173 177 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
174 178
175 179 old_name = self.mapping[notebook_id]
176 old_checkpoints = self.list_checkpoints(notebook_id)
177
180 old_checkpoints = self.list_checkpoints(notebook_id)
178 181 path = self.get_path_by_name(new_name)
182
183 # Right before we save the notebook, we write an empty string as the
184 # notebook name in the metadata. This is to prepare for removing
185 # this attribute entirely post 1.0. The web app still uses the metadata
186 # name for now.
187 nb.metadata.name = u''
188
179 189 try:
180 190 self.log.debug("Autosaving notebook %s", path)
181 191 with open(path,'w') as f:
182 192 current.write(nb, f, u'json')
183 193 except Exception as e:
184 194 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
185 195
186 196 # save .py script as well
187 197 if self.save_script:
188 198 pypath = os.path.splitext(path)[0] + '.py'
189 199 self.log.debug("Writing script %s", pypath)
190 200 try:
191 201 with io.open(pypath,'w', encoding='utf-8') as f:
192 202 current.write(nb, f, u'py')
193 203 except Exception as e:
194 204 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
195 205
196 206 # remove old files if the name changed
197 207 if old_name != new_name:
198 208 # update mapping
199 209 self.mapping[notebook_id] = new_name
200 210 self.rev_mapping[new_name] = notebook_id
201 211 del self.rev_mapping[old_name]
202 212
203 213 # remove renamed original, if it exists
204 214 old_path = self.get_path_by_name(old_name)
205 215 if os.path.isfile(old_path):
206 216 self.log.debug("unlinking notebook %s", old_path)
207 217 os.unlink(old_path)
208 218
209 219 # cleanup old script, if it exists
210 220 if self.save_script:
211 221 old_pypath = os.path.splitext(old_path)[0] + '.py'
212 222 if os.path.isfile(old_pypath):
213 223 self.log.debug("unlinking script %s", old_pypath)
214 224 os.unlink(old_pypath)
215 225
216 226 # rename checkpoints to follow file
217 227 for cp in old_checkpoints:
218 228 checkpoint_id = cp['checkpoint_id']
219 229 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
220 230 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
221 231 if os.path.isfile(old_cp_path):
222 232 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
223 233 os.rename(old_cp_path, new_cp_path)
224 234
225 235 return notebook_id
226 236
227 237 def delete_notebook(self, notebook_id):
228 238 """Delete notebook by notebook_id."""
229 239 nb_path = self.get_path(notebook_id)
230 240 if not os.path.isfile(nb_path):
231 241 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
232 242
233 243 # clear checkpoints
234 244 for checkpoint in self.list_checkpoints(notebook_id):
235 245 checkpoint_id = checkpoint['checkpoint_id']
236 246 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
237 247 self.log.debug(path)
238 248 if os.path.isfile(path):
239 249 self.log.debug("unlinking checkpoint %s", path)
240 250 os.unlink(path)
241 251
242 252 self.log.debug("unlinking notebook %s", nb_path)
243 253 os.unlink(nb_path)
244 254 self.delete_notebook_id(notebook_id)
245 255
246 256 def increment_filename(self, basename):
247 257 """Return a non-used filename of the form basename<int>.
248 258
249 259 This searches through the filenames (basename0, basename1, ...)
250 260 until is find one that is not already being used. It is used to
251 261 create Untitled and Copy names that are unique.
252 262 """
253 263 i = 0
254 264 while True:
255 265 name = u'%s%i' % (basename,i)
256 266 path = self.get_path_by_name(name)
257 267 if not os.path.isfile(path):
258 268 break
259 269 else:
260 270 i = i+1
261 271 return name
262 272
263 273 # Checkpoint-related utilities
264 274
265 275 def get_checkpoint_path_by_name(self, name, checkpoint_id):
266 276 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
267 277 filename = u"{name}-{checkpoint_id}{ext}".format(
268 278 name=name,
269 279 checkpoint_id=checkpoint_id,
270 280 ext=self.filename_ext,
271 281 )
272 282 path = os.path.join(self.checkpoint_dir, filename)
273 283 return path
274 284
275 285 def get_checkpoint_path(self, notebook_id, checkpoint_id):
276 286 """find the path to a checkpoint"""
277 287 name = self.get_name(notebook_id)
278 288 return self.get_checkpoint_path_by_name(name, checkpoint_id)
279 289
280 290 def get_checkpoint_info(self, notebook_id, checkpoint_id):
281 291 """construct the info dict for a given checkpoint"""
282 292 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
283 293 stats = os.stat(path)
284 294 last_modified = datetime.datetime.utcfromtimestamp(stats.st_mtime)
285 295 info = dict(
286 296 checkpoint_id = checkpoint_id,
287 297 last_modified = last_modified,
288 298 )
289 299
290 300 return info
291 301
292 302 # public checkpoint API
293 303
294 304 def create_checkpoint(self, notebook_id):
295 305 """Create a checkpoint from the current state of a notebook"""
296 306 nb_path = self.get_path(notebook_id)
297 307 # only the one checkpoint ID:
298 308 checkpoint_id = u"checkpoint"
299 309 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
300 310 self.log.debug("creating checkpoint for notebook %s", notebook_id)
301 311 if not os.path.exists(self.checkpoint_dir):
302 312 os.mkdir(self.checkpoint_dir)
303 313 shutil.copy2(nb_path, cp_path)
304 314
305 315 # return the checkpoint info
306 316 return self.get_checkpoint_info(notebook_id, checkpoint_id)
307 317
308 318 def list_checkpoints(self, notebook_id):
309 319 """list the checkpoints for a given notebook
310 320
311 321 This notebook manager currently only supports one checkpoint per notebook.
312 322 """
313 323 checkpoint_id = u"checkpoint"
314 324 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
315 325 if not os.path.exists(path):
316 326 return []
317 327 else:
318 328 return [self.get_checkpoint_info(notebook_id, checkpoint_id)]
319 329
320 330
321 331 def restore_checkpoint(self, notebook_id, checkpoint_id):
322 332 """restore a notebook to a checkpointed state"""
323 333 self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
324 334 nb_path = self.get_path(notebook_id)
325 335 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
326 336 if not os.path.isfile(cp_path):
327 337 self.log.debug("checkpoint file does not exist: %s", cp_path)
328 338 raise web.HTTPError(404,
329 339 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
330 340 )
331 341 # ensure notebook is readable (never restore from an unreadable notebook)
332 342 last_modified, nb = self.read_notebook_object_from_path(cp_path)
333 343 shutil.copy2(cp_path, nb_path)
334 344 self.log.debug("copying %s -> %s", cp_path, nb_path)
335 345
336 346 def delete_checkpoint(self, notebook_id, checkpoint_id):
337 347 """delete a notebook's checkpoint"""
338 348 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
339 349 if not os.path.isfile(path):
340 350 raise web.HTTPError(404,
341 351 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
342 352 )
343 353 self.log.debug("unlinking %s", path)
344 354 os.unlink(path)
345 355
346 356 def info_string(self):
347 357 return "Serving notebooks from local directory: %s" % self.notebook_dir
General Comments 0
You need to be logged in to leave comments. Login now