##// END OF EJS Templates
use itertools.count for incrementing notebook filename
MinRK -
Show More
@@ -1,414 +1,409
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 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 import datetime
21 20 import io
21 import itertools
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 from unicodedata import normalize
27
28 26 from tornado import web
29 27
30 28 from .nbmanager import NotebookManager
31 29 from IPython.nbformat import current
32 30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
33 31 from IPython.utils import tz
34 32
35 33 #-----------------------------------------------------------------------------
36 34 # Classes
37 35 #-----------------------------------------------------------------------------
38 36
39 37 class FileNotebookManager(NotebookManager):
40 38
41 39 save_script = Bool(False, config=True,
42 40 help="""Automatically create a Python script when saving the notebook.
43 41
44 42 For easier use of import, %run and %load across notebooks, a
45 43 <notebook-name>.py script will be created next to any
46 44 <notebook-name>.ipynb on each save. This can also be set with the
47 45 short `--script` flag.
48 46 """
49 47 )
50 48
51 49 checkpoint_dir = Unicode(config=True,
52 50 help="""The location in which to keep notebook checkpoints
53 51
54 52 By default, it is notebook-dir/.ipynb_checkpoints
55 53 """
56 54 )
57 55 def _checkpoint_dir_default(self):
58 56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
59 57
60 58 def _checkpoint_dir_changed(self, name, old, new):
61 59 """do a bit of validation of the checkpoint dir"""
62 60 if not os.path.isabs(new):
63 61 # If we receive a non-absolute path, make it absolute.
64 62 abs_new = os.path.abspath(new)
65 63 self.checkpoint_dir = abs_new
66 64 return
67 65 if os.path.exists(new) and not os.path.isdir(new):
68 66 raise TraitError("checkpoint dir %r is not a directory" % new)
69 67 if not os.path.exists(new):
70 68 self.log.info("Creating checkpoint dir %s", new)
71 69 try:
72 70 os.mkdir(new)
73 71 except:
74 72 raise TraitError("Couldn't create checkpoint dir %r" % new)
75 73
76 74 def get_notebook_names(self, path=''):
77 75 """List all notebook names in the notebook dir and path."""
78 76 path = path.strip('/')
79 77 if not os.path.isdir(self.get_os_path(path=path)):
80 78 raise web.HTTPError(404, 'Directory not found: ' + path)
81 79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
82 80 names = [os.path.basename(name)
83 81 for name in names]
84 82 return names
85 83
86 def increment_filename(self, basename, path=''):
84 def increment_filename(self, basename, path='', ext='.ipynb'):
87 85 """Return a non-used filename of the form basename<int>."""
88 86 path = path.strip('/')
89 i = 0
90 while True:
91 name = u'%s%i.ipynb' % (basename,i)
87 for i in itertools.count():
88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
92 89 os_path = self.get_os_path(name, path)
93 90 if not os.path.isfile(os_path):
94 91 break
95 else:
96 i = i+1
97 92 return name
98 93
99 94 def path_exists(self, path):
100 95 """Does the API-style path (directory) actually exist?
101 96
102 97 Parameters
103 98 ----------
104 99 path : string
105 100 The path to check. This is an API path (`/` separated,
106 101 relative to base notebook-dir).
107 102
108 103 Returns
109 104 -------
110 105 exists : bool
111 106 Whether the path is indeed a directory.
112 107 """
113 108 path = path.strip('/')
114 109 os_path = self.get_os_path(path=path)
115 110 return os.path.isdir(os_path)
116 111
117 112 def get_os_path(self, name=None, path=''):
118 113 """Given a notebook name and a URL path, return its file system
119 114 path.
120 115
121 116 Parameters
122 117 ----------
123 118 name : string
124 119 The name of a notebook file with the .ipynb extension
125 120 path : string
126 121 The relative URL path (with '/' as separator) to the named
127 122 notebook.
128 123
129 124 Returns
130 125 -------
131 126 path : string
132 127 A file system path that combines notebook_dir (location where
133 128 server started), the relative path, and the filename with the
134 129 current operating system's url.
135 130 """
136 131 parts = path.strip('/').split('/')
137 132 parts = [p for p in parts if p != ''] # remove duplicate splits
138 133 if name is not None:
139 134 parts.append(name)
140 135 path = os.path.join(self.notebook_dir, *parts)
141 136 return path
142 137
143 138 def notebook_exists(self, name, path=''):
144 139 """Returns a True if the notebook exists. Else, returns False.
145 140
146 141 Parameters
147 142 ----------
148 143 name : string
149 144 The name of the notebook you are checking.
150 145 path : string
151 146 The relative path to the notebook (with '/' as separator)
152 147
153 148 Returns
154 149 -------
155 150 bool
156 151 """
157 152 path = path.strip('/')
158 153 nbpath = self.get_os_path(name, path=path)
159 154 return os.path.isfile(nbpath)
160 155
161 156 def list_notebooks(self, path):
162 157 """Returns a list of dictionaries that are the standard model
163 158 for all notebooks in the relative 'path'.
164 159
165 160 Parameters
166 161 ----------
167 162 path : str
168 163 the URL path that describes the relative path for the
169 164 listed notebooks
170 165
171 166 Returns
172 167 -------
173 168 notebooks : list of dicts
174 169 a list of the notebook models without 'content'
175 170 """
176 171 path = path.strip('/')
177 172 notebook_names = self.get_notebook_names(path)
178 173 notebooks = []
179 174 for name in notebook_names:
180 175 model = self.get_notebook_model(name, path, content=False)
181 176 notebooks.append(model)
182 177 notebooks = sorted(notebooks, key=lambda item: item['name'])
183 178 return notebooks
184 179
185 180 def get_notebook_model(self, name, path='', content=True):
186 181 """ Takes a path and name for a notebook and returns it's model
187 182
188 183 Parameters
189 184 ----------
190 185 name : str
191 186 the name of the notebook
192 187 path : str
193 188 the URL path that describes the relative path for
194 189 the notebook
195 190
196 191 Returns
197 192 -------
198 193 model : dict
199 194 the notebook model. If contents=True, returns the 'contents'
200 195 dict in the model as well.
201 196 """
202 197 path = path.strip('/')
203 198 if not self.notebook_exists(name=name, path=path):
204 199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
205 200 os_path = self.get_os_path(name, path)
206 201 info = os.stat(os_path)
207 202 last_modified = tz.utcfromtimestamp(info.st_mtime)
208 203 created = tz.utcfromtimestamp(info.st_ctime)
209 204 # Create the notebook model.
210 205 model ={}
211 206 model['name'] = name
212 207 model['path'] = path
213 208 model['last_modified'] = last_modified
214 209 model['created'] = created
215 210 if content is True:
216 211 with io.open(os_path, 'r', encoding='utf-8') as f:
217 212 try:
218 213 nb = current.read(f, u'json')
219 214 except Exception as e:
220 215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
221 216 model['content'] = nb
222 217 return model
223 218
224 219 def save_notebook_model(self, model, name='', path=''):
225 220 """Save the notebook model and return the model with no content."""
226 221 path = path.strip('/')
227 222
228 223 if 'content' not in model:
229 224 raise web.HTTPError(400, u'No notebook JSON data provided')
230 225
231 226 new_path = model.get('path', path).strip('/')
232 227 new_name = model.get('name', name)
233 228
234 229 if path != new_path or name != new_name:
235 230 self.rename_notebook(name, path, new_name, new_path)
236 231
237 232 # Save the notebook file
238 233 os_path = self.get_os_path(new_name, new_path)
239 234 nb = current.to_notebook_json(model['content'])
240 235 if 'name' in nb['metadata']:
241 236 nb['metadata']['name'] = u''
242 237 try:
243 238 self.log.debug("Autosaving notebook %s", os_path)
244 239 with io.open(os_path, 'w', encoding='utf-8') as f:
245 240 current.write(nb, f, u'json')
246 241 except Exception as e:
247 242 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
248 243
249 244 # Save .py script as well
250 245 if self.save_script:
251 246 py_path = os.path.splitext(os_path)[0] + '.py'
252 247 self.log.debug("Writing script %s", py_path)
253 248 try:
254 249 with io.open(py_path, 'w', encoding='utf-8') as f:
255 250 current.write(model, f, u'py')
256 251 except Exception as e:
257 252 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
258 253
259 254 model = self.get_notebook_model(new_name, new_path, content=False)
260 255 return model
261 256
262 257 def update_notebook_model(self, model, name, path=''):
263 258 """Update the notebook's path and/or name"""
264 259 path = path.strip('/')
265 260 new_name = model.get('name', name)
266 261 new_path = model.get('path', path).strip('/')
267 262 if path != new_path or name != new_name:
268 263 self.rename_notebook(name, path, new_name, new_path)
269 264 model = self.get_notebook_model(new_name, new_path, content=False)
270 265 return model
271 266
272 267 def delete_notebook_model(self, name, path=''):
273 268 """Delete notebook by name and path."""
274 269 path = path.strip('/')
275 270 os_path = self.get_os_path(name, path)
276 271 if not os.path.isfile(os_path):
277 272 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
278 273
279 274 # clear checkpoints
280 275 for checkpoint in self.list_checkpoints(name, path):
281 276 checkpoint_id = checkpoint['id']
282 277 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
283 278 if os.path.isfile(cp_path):
284 279 self.log.debug("Unlinking checkpoint %s", cp_path)
285 280 os.unlink(cp_path)
286 281
287 282 self.log.debug("Unlinking notebook %s", os_path)
288 283 os.unlink(os_path)
289 284
290 285 def rename_notebook(self, old_name, old_path, new_name, new_path):
291 286 """Rename a notebook."""
292 287 old_path = old_path.strip('/')
293 288 new_path = new_path.strip('/')
294 289 if new_name == old_name and new_path == old_path:
295 290 return
296 291
297 292 new_os_path = self.get_os_path(new_name, new_path)
298 293 old_os_path = self.get_os_path(old_name, old_path)
299 294
300 295 # Should we proceed with the move?
301 296 if os.path.isfile(new_os_path):
302 297 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
303 298 if self.save_script:
304 299 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
305 300 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
306 301 if os.path.isfile(new_py_path):
307 302 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
308 303
309 304 # Move the notebook file
310 305 try:
311 306 os.rename(old_os_path, new_os_path)
312 307 except Exception as e:
313 308 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
314 309
315 310 # Move the checkpoints
316 311 old_checkpoints = self.list_checkpoints(old_name, old_path)
317 312 for cp in old_checkpoints:
318 313 checkpoint_id = cp['id']
319 314 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
320 315 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
321 316 if os.path.isfile(old_cp_path):
322 317 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
323 318 os.rename(old_cp_path, new_cp_path)
324 319
325 320 # Move the .py script
326 321 if self.save_script:
327 322 os.rename(old_py_path, new_py_path)
328 323
329 324 # Checkpoint-related utilities
330 325
331 326 def get_checkpoint_path(self, checkpoint_id, name, path=''):
332 327 """find the path to a checkpoint"""
333 328 path = path.strip('/')
334 329 filename = u"{name}-{checkpoint_id}{ext}".format(
335 330 name=name,
336 331 checkpoint_id=checkpoint_id,
337 332 ext=self.filename_ext,
338 333 )
339 334 cp_path = os.path.join(path, self.checkpoint_dir, filename)
340 335 return cp_path
341 336
342 337 def get_checkpoint_model(self, checkpoint_id, name, path=''):
343 338 """construct the info dict for a given checkpoint"""
344 339 path = path.strip('/')
345 340 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
346 341 stats = os.stat(cp_path)
347 342 last_modified = tz.utcfromtimestamp(stats.st_mtime)
348 343 info = dict(
349 344 id = checkpoint_id,
350 345 last_modified = last_modified,
351 346 )
352 347 return info
353 348
354 349 # public checkpoint API
355 350
356 351 def create_checkpoint(self, name, path=''):
357 352 """Create a checkpoint from the current state of a notebook"""
358 353 path = path.strip('/')
359 354 nb_path = self.get_os_path(name, path)
360 355 # only the one checkpoint ID:
361 356 checkpoint_id = u"checkpoint"
362 357 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
363 358 self.log.debug("creating checkpoint for notebook %s", name)
364 359 if not os.path.exists(self.checkpoint_dir):
365 360 os.mkdir(self.checkpoint_dir)
366 361 shutil.copy2(nb_path, cp_path)
367 362
368 363 # return the checkpoint info
369 364 return self.get_checkpoint_model(checkpoint_id, name, path)
370 365
371 366 def list_checkpoints(self, name, path=''):
372 367 """list the checkpoints for a given notebook
373 368
374 369 This notebook manager currently only supports one checkpoint per notebook.
375 370 """
376 371 path = path.strip('/')
377 372 checkpoint_id = "checkpoint"
378 373 path = self.get_checkpoint_path(checkpoint_id, name, path)
379 374 if not os.path.exists(path):
380 375 return []
381 376 else:
382 377 return [self.get_checkpoint_model(checkpoint_id, name, path)]
383 378
384 379
385 380 def restore_checkpoint(self, checkpoint_id, name, path=''):
386 381 """restore a notebook to a checkpointed state"""
387 382 path = path.strip('/')
388 383 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
389 384 nb_path = self.get_os_path(name, path)
390 385 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
391 386 if not os.path.isfile(cp_path):
392 387 self.log.debug("checkpoint file does not exist: %s", cp_path)
393 388 raise web.HTTPError(404,
394 389 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
395 390 )
396 391 # ensure notebook is readable (never restore from an unreadable notebook)
397 392 with io.open(cp_path, 'r', encoding='utf-8') as f:
398 393 nb = current.read(f, u'json')
399 394 shutil.copy2(cp_path, nb_path)
400 395 self.log.debug("copying %s -> %s", cp_path, nb_path)
401 396
402 397 def delete_checkpoint(self, checkpoint_id, name, path=''):
403 398 """delete a notebook's checkpoint"""
404 399 path = path.strip('/')
405 400 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
406 401 if not os.path.isfile(cp_path):
407 402 raise web.HTTPError(404,
408 403 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
409 404 )
410 405 self.log.debug("unlinking %s", cp_path)
411 406 os.unlink(cp_path)
412 407
413 408 def info_string(self):
414 409 return "Serving notebooks from local directory: %s" % self.notebook_dir
General Comments 0
You need to be logged in to leave comments. Login now