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