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