##// END OF EJS Templates
API: Allow NotebookManagers to control kernel startup dir. #5468
Dale Jung -
Show More
@@ -1,128 +1,132 b''
1 1 """A kernel manager relating notebooks and kernels
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 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 os
20 20
21 21 from tornado import web
22 22
23 23 from IPython.kernel.multikernelmanager import MultiKernelManager
24 24 from IPython.utils.traitlets import (
25 25 Dict, List, Unicode,
26 26 )
27 27
28 28 from IPython.html.utils import to_os_path
29 29 from IPython.utils.py3compat import getcwd
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes
33 33 #-----------------------------------------------------------------------------
34 34
35 35
36 36 class MappingKernelManager(MultiKernelManager):
37 37 """A KernelManager that handles notebook mapping and HTTP error handling"""
38 38
39 39 def _kernel_manager_class_default(self):
40 40 return "IPython.kernel.ioloop.IOLoopKernelManager"
41 41
42 42 kernel_argv = List(Unicode)
43 43
44 44 root_dir = Unicode(getcwd(), config=True)
45 45
46 46 def _root_dir_changed(self, name, old, new):
47 47 """Do a bit of validation of the root dir."""
48 48 if not os.path.isabs(new):
49 49 # If we receive a non-absolute path, make it absolute.
50 50 self.root_dir = os.path.abspath(new)
51 51 return
52 52 if not os.path.exists(new) or not os.path.isdir(new):
53 53 raise TraitError("kernel root dir %r is not a directory" % new)
54 54
55 55 #-------------------------------------------------------------------------
56 56 # Methods for managing kernels and sessions
57 57 #-------------------------------------------------------------------------
58 58
59 59 def _handle_kernel_died(self, kernel_id):
60 60 """notice that a kernel died"""
61 61 self.log.warn("Kernel %s died, removing from map.", kernel_id)
62 62 self.remove_kernel(kernel_id)
63 63
64 64 def cwd_for_path(self, path):
65 65 """Turn API path into absolute OS path."""
66 # short circuit for NotebookManagers that pass in absolute paths
67 if os.path.exists(path):
68 return path
69
66 70 os_path = to_os_path(path, self.root_dir)
67 71 # in the case of notebooks and kernels not being on the same filesystem,
68 72 # walk up to root_dir if the paths don't exist
69 73 while not os.path.exists(os_path) and os_path != self.root_dir:
70 74 os_path = os.path.dirname(os_path)
71 75 return os_path
72 76
73 77 def start_kernel(self, kernel_id=None, path=None, **kwargs):
74 78 """Start a kernel for a session an return its kernel_id.
75 79
76 80 Parameters
77 81 ----------
78 82 kernel_id : uuid
79 83 The uuid to associate the new kernel with. If this
80 84 is not None, this kernel will be persistent whenever it is
81 85 requested.
82 86 path : API path
83 87 The API path (unicode, '/' delimited) for the cwd.
84 88 Will be transformed to an OS path relative to root_dir.
85 89 """
86 90 if kernel_id is None:
87 91 kwargs['extra_arguments'] = self.kernel_argv
88 92 if path is not None:
89 93 kwargs['cwd'] = self.cwd_for_path(path)
90 94 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
91 95 self.log.info("Kernel started: %s" % kernel_id)
92 96 self.log.debug("Kernel args: %r" % kwargs)
93 97 # register callback for failed auto-restart
94 98 self.add_restart_callback(kernel_id,
95 99 lambda : self._handle_kernel_died(kernel_id),
96 100 'dead',
97 101 )
98 102 else:
99 103 self._check_kernel_id(kernel_id)
100 104 self.log.info("Using existing kernel: %s" % kernel_id)
101 105 return kernel_id
102 106
103 107 def shutdown_kernel(self, kernel_id, now=False):
104 108 """Shutdown a kernel by kernel_id"""
105 109 self._check_kernel_id(kernel_id)
106 110 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
107 111
108 112 def kernel_model(self, kernel_id):
109 113 """Return a dictionary of kernel information described in the
110 114 JSON standard model."""
111 115 self._check_kernel_id(kernel_id)
112 116 model = {"id":kernel_id}
113 117 return model
114 118
115 119 def list_kernels(self):
116 120 """Returns a list of kernel_id's of kernels running."""
117 121 kernels = []
118 122 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
119 123 for kernel_id in kernel_ids:
120 124 model = self.kernel_model(kernel_id)
121 125 kernels.append(model)
122 126 return kernels
123 127
124 128 # override _check_kernel_id to raise 404 instead of KeyError
125 129 def _check_kernel_id(self, kernel_id):
126 130 """Check a that a kernel_id exists and raise 404 if not."""
127 131 if kernel_id not in self:
128 132 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,494 +1,498 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 * 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 20 import io
21 21 import os
22 22 import glob
23 23 import shutil
24 24
25 25 from tornado import web
26 26
27 27 from .nbmanager import NotebookManager
28 28 from IPython.nbformat import current
29 29 from IPython.utils.traitlets import Unicode, Bool, TraitError
30 30 from IPython.utils.py3compat import getcwd
31 31 from IPython.utils import tz
32 32 from IPython.html.utils import is_hidden, to_os_path
33 33
34 34 def sort_key(item):
35 35 """Case-insensitive sorting."""
36 36 return item['name'].lower()
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class FileNotebookManager(NotebookManager):
43 43
44 44 save_script = Bool(False, config=True,
45 45 help="""Automatically create a Python script when saving the notebook.
46 46
47 47 For easier use of import, %run and %load across notebooks, a
48 48 <notebook-name>.py script will be created next to any
49 49 <notebook-name>.ipynb on each save. This can also be set with the
50 50 short `--script` flag.
51 51 """
52 52 )
53 53 notebook_dir = Unicode(getcwd(), config=True)
54 54
55 55 def _notebook_dir_changed(self, name, old, new):
56 56 """Do a bit of validation of the notebook dir."""
57 57 if not os.path.isabs(new):
58 58 # If we receive a non-absolute path, make it absolute.
59 59 self.notebook_dir = os.path.abspath(new)
60 60 return
61 61 if not os.path.exists(new) or not os.path.isdir(new):
62 62 raise TraitError("notebook dir %r is not a directory" % new)
63 63
64 64 checkpoint_dir = Unicode(config=True,
65 65 help="""The location in which to keep notebook checkpoints
66 66
67 67 By default, it is notebook-dir/.ipynb_checkpoints
68 68 """
69 69 )
70 70 def _checkpoint_dir_default(self):
71 71 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
72 72
73 73 def _checkpoint_dir_changed(self, name, old, new):
74 74 """do a bit of validation of the checkpoint dir"""
75 75 if not os.path.isabs(new):
76 76 # If we receive a non-absolute path, make it absolute.
77 77 abs_new = os.path.abspath(new)
78 78 self.checkpoint_dir = abs_new
79 79 return
80 80 if os.path.exists(new) and not os.path.isdir(new):
81 81 raise TraitError("checkpoint dir %r is not a directory" % new)
82 82 if not os.path.exists(new):
83 83 self.log.info("Creating checkpoint dir %s", new)
84 84 try:
85 85 os.mkdir(new)
86 86 except:
87 87 raise TraitError("Couldn't create checkpoint dir %r" % new)
88 88
89 89 def _copy(self, src, dest):
90 90 """copy src to dest
91 91
92 92 like shutil.copy2, but log errors in copystat
93 93 """
94 94 shutil.copyfile(src, dest)
95 95 try:
96 96 shutil.copystat(src, dest)
97 97 except OSError as e:
98 98 self.log.debug("copystat on %s failed", dest, exc_info=True)
99 99
100 100 def get_notebook_names(self, path=''):
101 101 """List all notebook names in the notebook dir and path."""
102 102 path = path.strip('/')
103 103 if not os.path.isdir(self._get_os_path(path=path)):
104 104 raise web.HTTPError(404, 'Directory not found: ' + path)
105 105 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
106 106 names = [os.path.basename(name)
107 107 for name in names]
108 108 return names
109 109
110 110 def path_exists(self, path):
111 111 """Does the API-style path (directory) actually exist?
112 112
113 113 Parameters
114 114 ----------
115 115 path : string
116 116 The path to check. This is an API path (`/` separated,
117 117 relative to base notebook-dir).
118 118
119 119 Returns
120 120 -------
121 121 exists : bool
122 122 Whether the path is indeed a directory.
123 123 """
124 124 path = path.strip('/')
125 125 os_path = self._get_os_path(path=path)
126 126 return os.path.isdir(os_path)
127 127
128 128 def is_hidden(self, path):
129 129 """Does the API style path correspond to a hidden directory or file?
130 130
131 131 Parameters
132 132 ----------
133 133 path : string
134 134 The path to check. This is an API path (`/` separated,
135 135 relative to base notebook-dir).
136 136
137 137 Returns
138 138 -------
139 139 exists : bool
140 140 Whether the path is hidden.
141 141
142 142 """
143 143 path = path.strip('/')
144 144 os_path = self._get_os_path(path=path)
145 145 return is_hidden(os_path, self.notebook_dir)
146 146
147 147 def _get_os_path(self, name=None, path=''):
148 148 """Given a notebook name and a URL path, return its file system
149 149 path.
150 150
151 151 Parameters
152 152 ----------
153 153 name : string
154 154 The name of a notebook file with the .ipynb extension
155 155 path : string
156 156 The relative URL path (with '/' as separator) to the named
157 157 notebook.
158 158
159 159 Returns
160 160 -------
161 161 path : string
162 162 A file system path that combines notebook_dir (location where
163 163 server started), the relative path, and the filename with the
164 164 current operating system's url.
165 165 """
166 166 if name is not None:
167 167 path = path + '/' + name
168 168 return to_os_path(path, self.notebook_dir)
169 169
170 170 def notebook_exists(self, name, path=''):
171 171 """Returns a True if the notebook exists. Else, returns False.
172 172
173 173 Parameters
174 174 ----------
175 175 name : string
176 176 The name of the notebook you are checking.
177 177 path : string
178 178 The relative path to the notebook (with '/' as separator)
179 179
180 180 Returns
181 181 -------
182 182 bool
183 183 """
184 184 path = path.strip('/')
185 185 nbpath = self._get_os_path(name, path=path)
186 186 return os.path.isfile(nbpath)
187 187
188 188 # TODO: Remove this after we create the contents web service and directories are
189 189 # no longer listed by the notebook web service.
190 190 def list_dirs(self, path):
191 191 """List the directories for a given API style path."""
192 192 path = path.strip('/')
193 193 os_path = self._get_os_path('', path)
194 194 if not os.path.isdir(os_path):
195 195 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
196 196 elif is_hidden(os_path, self.notebook_dir):
197 197 self.log.info("Refusing to serve hidden directory, via 404 Error")
198 198 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
199 199 dir_names = os.listdir(os_path)
200 200 dirs = []
201 201 for name in dir_names:
202 202 os_path = self._get_os_path(name, path)
203 203 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
204 204 and self.should_list(name):
205 205 try:
206 206 model = self.get_dir_model(name, path)
207 207 except IOError:
208 208 pass
209 209 dirs.append(model)
210 210 dirs = sorted(dirs, key=sort_key)
211 211 return dirs
212 212
213 213 # TODO: Remove this after we create the contents web service and directories are
214 214 # no longer listed by the notebook web service.
215 215 def get_dir_model(self, name, path=''):
216 216 """Get the directory model given a directory name and its API style path"""
217 217 path = path.strip('/')
218 218 os_path = self._get_os_path(name, path)
219 219 if not os.path.isdir(os_path):
220 220 raise IOError('directory does not exist: %r' % os_path)
221 221 info = os.stat(os_path)
222 222 last_modified = tz.utcfromtimestamp(info.st_mtime)
223 223 created = tz.utcfromtimestamp(info.st_ctime)
224 224 # Create the notebook model.
225 225 model ={}
226 226 model['name'] = name
227 227 model['path'] = path
228 228 model['last_modified'] = last_modified
229 229 model['created'] = created
230 230 model['type'] = 'directory'
231 231 return model
232 232
233 233 def list_notebooks(self, path):
234 234 """Returns a list of dictionaries that are the standard model
235 235 for all notebooks in the relative 'path'.
236 236
237 237 Parameters
238 238 ----------
239 239 path : str
240 240 the URL path that describes the relative path for the
241 241 listed notebooks
242 242
243 243 Returns
244 244 -------
245 245 notebooks : list of dicts
246 246 a list of the notebook models without 'content'
247 247 """
248 248 path = path.strip('/')
249 249 notebook_names = self.get_notebook_names(path)
250 250 notebooks = [self.get_notebook(name, path, content=False)
251 251 for name in notebook_names if self.should_list(name)]
252 252 notebooks = sorted(notebooks, key=sort_key)
253 253 return notebooks
254 254
255 255 def get_notebook(self, name, path='', content=True):
256 256 """ Takes a path and name for a notebook and returns its model
257 257
258 258 Parameters
259 259 ----------
260 260 name : str
261 261 the name of the notebook
262 262 path : str
263 263 the URL path that describes the relative path for
264 264 the notebook
265 265
266 266 Returns
267 267 -------
268 268 model : dict
269 269 the notebook model. If contents=True, returns the 'contents'
270 270 dict in the model as well.
271 271 """
272 272 path = path.strip('/')
273 273 if not self.notebook_exists(name=name, path=path):
274 274 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
275 275 os_path = self._get_os_path(name, path)
276 276 info = os.stat(os_path)
277 277 last_modified = tz.utcfromtimestamp(info.st_mtime)
278 278 created = tz.utcfromtimestamp(info.st_ctime)
279 279 # Create the notebook model.
280 280 model ={}
281 281 model['name'] = name
282 282 model['path'] = path
283 283 model['last_modified'] = last_modified
284 284 model['created'] = created
285 285 model['type'] = 'notebook'
286 286 if content:
287 287 with io.open(os_path, 'r', encoding='utf-8') as f:
288 288 try:
289 289 nb = current.read(f, u'json')
290 290 except Exception as e:
291 291 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
292 292 self.mark_trusted_cells(nb, name, path)
293 293 model['content'] = nb
294 294 return model
295 295
296 296 def save_notebook(self, model, name='', path=''):
297 297 """Save the notebook model and return the model with no content."""
298 298 path = path.strip('/')
299 299
300 300 if 'content' not in model:
301 301 raise web.HTTPError(400, u'No notebook JSON data provided')
302 302
303 303 # One checkpoint should always exist
304 304 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
305 305 self.create_checkpoint(name, path)
306 306
307 307 new_path = model.get('path', path).strip('/')
308 308 new_name = model.get('name', name)
309 309
310 310 if path != new_path or name != new_name:
311 311 self.rename_notebook(name, path, new_name, new_path)
312 312
313 313 # Save the notebook file
314 314 os_path = self._get_os_path(new_name, new_path)
315 315 nb = current.to_notebook_json(model['content'])
316 316
317 317 self.check_and_sign(nb, new_name, new_path)
318 318
319 319 if 'name' in nb['metadata']:
320 320 nb['metadata']['name'] = u''
321 321 try:
322 322 self.log.debug("Autosaving notebook %s", os_path)
323 323 with io.open(os_path, 'w', encoding='utf-8') as f:
324 324 current.write(nb, f, u'json')
325 325 except Exception as e:
326 326 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
327 327
328 328 # Save .py script as well
329 329 if self.save_script:
330 330 py_path = os.path.splitext(os_path)[0] + '.py'
331 331 self.log.debug("Writing script %s", py_path)
332 332 try:
333 333 with io.open(py_path, 'w', encoding='utf-8') as f:
334 334 current.write(nb, f, u'py')
335 335 except Exception as e:
336 336 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
337 337
338 338 model = self.get_notebook(new_name, new_path, content=False)
339 339 return model
340 340
341 341 def update_notebook(self, model, name, path=''):
342 342 """Update the notebook's path and/or name"""
343 343 path = path.strip('/')
344 344 new_name = model.get('name', name)
345 345 new_path = model.get('path', path).strip('/')
346 346 if path != new_path or name != new_name:
347 347 self.rename_notebook(name, path, new_name, new_path)
348 348 model = self.get_notebook(new_name, new_path, content=False)
349 349 return model
350 350
351 351 def delete_notebook(self, name, path=''):
352 352 """Delete notebook by name and path."""
353 353 path = path.strip('/')
354 354 os_path = self._get_os_path(name, path)
355 355 if not os.path.isfile(os_path):
356 356 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
357 357
358 358 # clear checkpoints
359 359 for checkpoint in self.list_checkpoints(name, path):
360 360 checkpoint_id = checkpoint['id']
361 361 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
362 362 if os.path.isfile(cp_path):
363 363 self.log.debug("Unlinking checkpoint %s", cp_path)
364 364 os.unlink(cp_path)
365 365
366 366 self.log.debug("Unlinking notebook %s", os_path)
367 367 os.unlink(os_path)
368 368
369 369 def rename_notebook(self, old_name, old_path, new_name, new_path):
370 370 """Rename a notebook."""
371 371 old_path = old_path.strip('/')
372 372 new_path = new_path.strip('/')
373 373 if new_name == old_name and new_path == old_path:
374 374 return
375 375
376 376 new_os_path = self._get_os_path(new_name, new_path)
377 377 old_os_path = self._get_os_path(old_name, old_path)
378 378
379 379 # Should we proceed with the move?
380 380 if os.path.isfile(new_os_path):
381 381 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
382 382 if self.save_script:
383 383 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
384 384 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
385 385 if os.path.isfile(new_py_path):
386 386 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
387 387
388 388 # Move the notebook file
389 389 try:
390 390 os.rename(old_os_path, new_os_path)
391 391 except Exception as e:
392 392 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
393 393
394 394 # Move the checkpoints
395 395 old_checkpoints = self.list_checkpoints(old_name, old_path)
396 396 for cp in old_checkpoints:
397 397 checkpoint_id = cp['id']
398 398 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
399 399 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
400 400 if os.path.isfile(old_cp_path):
401 401 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
402 402 os.rename(old_cp_path, new_cp_path)
403 403
404 404 # Move the .py script
405 405 if self.save_script:
406 406 os.rename(old_py_path, new_py_path)
407 407
408 408 # Checkpoint-related utilities
409 409
410 410 def get_checkpoint_path(self, checkpoint_id, name, path=''):
411 411 """find the path to a checkpoint"""
412 412 path = path.strip('/')
413 413 basename, _ = os.path.splitext(name)
414 414 filename = u"{name}-{checkpoint_id}{ext}".format(
415 415 name=basename,
416 416 checkpoint_id=checkpoint_id,
417 417 ext=self.filename_ext,
418 418 )
419 419 cp_path = os.path.join(path, self.checkpoint_dir, filename)
420 420 return cp_path
421 421
422 422 def get_checkpoint_model(self, checkpoint_id, name, path=''):
423 423 """construct the info dict for a given checkpoint"""
424 424 path = path.strip('/')
425 425 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
426 426 stats = os.stat(cp_path)
427 427 last_modified = tz.utcfromtimestamp(stats.st_mtime)
428 428 info = dict(
429 429 id = checkpoint_id,
430 430 last_modified = last_modified,
431 431 )
432 432 return info
433 433
434 434 # public checkpoint API
435 435
436 436 def create_checkpoint(self, name, path=''):
437 437 """Create a checkpoint from the current state of a notebook"""
438 438 path = path.strip('/')
439 439 nb_path = self._get_os_path(name, path)
440 440 # only the one checkpoint ID:
441 441 checkpoint_id = u"checkpoint"
442 442 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
443 443 self.log.debug("creating checkpoint for notebook %s", name)
444 444 if not os.path.exists(self.checkpoint_dir):
445 445 os.mkdir(self.checkpoint_dir)
446 446 self._copy(nb_path, cp_path)
447 447
448 448 # return the checkpoint info
449 449 return self.get_checkpoint_model(checkpoint_id, name, path)
450 450
451 451 def list_checkpoints(self, name, path=''):
452 452 """list the checkpoints for a given notebook
453 453
454 454 This notebook manager currently only supports one checkpoint per notebook.
455 455 """
456 456 path = path.strip('/')
457 457 checkpoint_id = "checkpoint"
458 458 path = self.get_checkpoint_path(checkpoint_id, name, path)
459 459 if not os.path.exists(path):
460 460 return []
461 461 else:
462 462 return [self.get_checkpoint_model(checkpoint_id, name, path)]
463 463
464 464
465 465 def restore_checkpoint(self, checkpoint_id, name, path=''):
466 466 """restore a notebook to a checkpointed state"""
467 467 path = path.strip('/')
468 468 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
469 469 nb_path = self._get_os_path(name, path)
470 470 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
471 471 if not os.path.isfile(cp_path):
472 472 self.log.debug("checkpoint file does not exist: %s", cp_path)
473 473 raise web.HTTPError(404,
474 474 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
475 475 )
476 476 # ensure notebook is readable (never restore from an unreadable notebook)
477 477 with io.open(cp_path, 'r', encoding='utf-8') as f:
478 478 current.read(f, u'json')
479 479 self._copy(cp_path, nb_path)
480 480 self.log.debug("copying %s -> %s", cp_path, nb_path)
481 481
482 482 def delete_checkpoint(self, checkpoint_id, name, path=''):
483 483 """delete a notebook's checkpoint"""
484 484 path = path.strip('/')
485 485 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
486 486 if not os.path.isfile(cp_path):
487 487 raise web.HTTPError(404,
488 488 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
489 489 )
490 490 self.log.debug("unlinking %s", cp_path)
491 491 os.unlink(cp_path)
492 492
493 493 def info_string(self):
494 494 return "Serving notebooks from local directory: %s" % self.notebook_dir
495
496 def get_kernel_path(self, name, path='', model=None):
497 """ Return the path to start kernel in """
498 return os.path.join(self.notebook_dir, path)
@@ -1,283 +1,287 b''
1 1 """A base class notebook manager.
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 20 from fnmatch import fnmatch
21 21 import itertools
22 22 import os
23 23
24 24 from IPython.config.configurable import LoggingConfigurable
25 25 from IPython.nbformat import current, sign
26 26 from IPython.utils.traitlets import Instance, Unicode, List
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Classes
30 30 #-----------------------------------------------------------------------------
31 31
32 32 class NotebookManager(LoggingConfigurable):
33 33
34 34 filename_ext = Unicode(u'.ipynb')
35 35
36 36 notary = Instance(sign.NotebookNotary)
37 37 def _notary_default(self):
38 38 return sign.NotebookNotary(parent=self)
39 39
40 40 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
41 41 Glob patterns to hide in file and directory listings.
42 42 """)
43 43
44 44 # NotebookManager API part 1: methods that must be
45 45 # implemented in subclasses.
46 46
47 47 def path_exists(self, path):
48 48 """Does the API-style path (directory) actually exist?
49 49
50 50 Override this method in subclasses.
51 51
52 52 Parameters
53 53 ----------
54 54 path : string
55 55 The path to check
56 56
57 57 Returns
58 58 -------
59 59 exists : bool
60 60 Whether the path does indeed exist.
61 61 """
62 62 raise NotImplementedError
63 63
64 64 def is_hidden(self, path):
65 65 """Does the API style path correspond to a hidden directory or file?
66 66
67 67 Parameters
68 68 ----------
69 69 path : string
70 70 The path to check. This is an API path (`/` separated,
71 71 relative to base notebook-dir).
72 72
73 73 Returns
74 74 -------
75 75 exists : bool
76 76 Whether the path is hidden.
77 77
78 78 """
79 79 raise NotImplementedError
80 80
81 81 def notebook_exists(self, name, path=''):
82 82 """Returns a True if the notebook exists. Else, returns False.
83 83
84 84 Parameters
85 85 ----------
86 86 name : string
87 87 The name of the notebook you are checking.
88 88 path : string
89 89 The relative path to the notebook (with '/' as separator)
90 90
91 91 Returns
92 92 -------
93 93 bool
94 94 """
95 95 raise NotImplementedError('must be implemented in a subclass')
96 96
97 97 # TODO: Remove this after we create the contents web service and directories are
98 98 # no longer listed by the notebook web service.
99 99 def list_dirs(self, path):
100 100 """List the directory models for a given API style path."""
101 101 raise NotImplementedError('must be implemented in a subclass')
102 102
103 103 # TODO: Remove this after we create the contents web service and directories are
104 104 # no longer listed by the notebook web service.
105 105 def get_dir_model(self, name, path=''):
106 106 """Get the directory model given a directory name and its API style path.
107 107
108 108 The keys in the model should be:
109 109 * name
110 110 * path
111 111 * last_modified
112 112 * created
113 113 * type='directory'
114 114 """
115 115 raise NotImplementedError('must be implemented in a subclass')
116 116
117 117 def list_notebooks(self, path=''):
118 118 """Return a list of notebook dicts without content.
119 119
120 120 This returns a list of dicts, each of the form::
121 121
122 122 dict(notebook_id=notebook,name=name)
123 123
124 124 This list of dicts should be sorted by name::
125 125
126 126 data = sorted(data, key=lambda item: item['name'])
127 127 """
128 128 raise NotImplementedError('must be implemented in a subclass')
129 129
130 130 def get_notebook(self, name, path='', content=True):
131 131 """Get the notebook model with or without content."""
132 132 raise NotImplementedError('must be implemented in a subclass')
133 133
134 134 def save_notebook(self, model, name, path=''):
135 135 """Save the notebook and return the model with no content."""
136 136 raise NotImplementedError('must be implemented in a subclass')
137 137
138 138 def update_notebook(self, model, name, path=''):
139 139 """Update the notebook and return the model with no content."""
140 140 raise NotImplementedError('must be implemented in a subclass')
141 141
142 142 def delete_notebook(self, name, path=''):
143 143 """Delete notebook by name and path."""
144 144 raise NotImplementedError('must be implemented in a subclass')
145 145
146 146 def create_checkpoint(self, name, path=''):
147 147 """Create a checkpoint of the current state of a notebook
148 148
149 149 Returns a checkpoint_id for the new checkpoint.
150 150 """
151 151 raise NotImplementedError("must be implemented in a subclass")
152 152
153 153 def list_checkpoints(self, name, path=''):
154 154 """Return a list of checkpoints for a given notebook"""
155 155 return []
156 156
157 157 def restore_checkpoint(self, checkpoint_id, name, path=''):
158 158 """Restore a notebook from one of its checkpoints"""
159 159 raise NotImplementedError("must be implemented in a subclass")
160 160
161 161 def delete_checkpoint(self, checkpoint_id, name, path=''):
162 162 """delete a checkpoint for a notebook"""
163 163 raise NotImplementedError("must be implemented in a subclass")
164 164
165 165 def info_string(self):
166 166 return "Serving notebooks"
167 167
168 168 # NotebookManager API part 2: methods that have useable default
169 169 # implementations, but can be overridden in subclasses.
170 170
171 def get_kernel_path(self, name, path='', model=None):
172 """ Return the path to start kernel in """
173 return path
174
171 175 def increment_filename(self, basename, path=''):
172 176 """Increment a notebook filename without the .ipynb to make it unique.
173 177
174 178 Parameters
175 179 ----------
176 180 basename : unicode
177 181 The name of a notebook without the ``.ipynb`` file extension.
178 182 path : unicode
179 183 The URL path of the notebooks directory
180 184
181 185 Returns
182 186 -------
183 187 name : unicode
184 188 A notebook name (with the .ipynb extension) that starts
185 189 with basename and does not refer to any existing notebook.
186 190 """
187 191 path = path.strip('/')
188 192 for i in itertools.count():
189 193 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
190 194 ext=self.filename_ext)
191 195 if not self.notebook_exists(name, path):
192 196 break
193 197 return name
194 198
195 199 def create_notebook(self, model=None, path=''):
196 200 """Create a new notebook and return its model with no content."""
197 201 path = path.strip('/')
198 202 if model is None:
199 203 model = {}
200 204 if 'content' not in model:
201 205 metadata = current.new_metadata(name=u'')
202 206 model['content'] = current.new_notebook(metadata=metadata)
203 207 if 'name' not in model:
204 208 model['name'] = self.increment_filename('Untitled', path)
205 209
206 210 model['path'] = path
207 211 model = self.save_notebook(model, model['name'], model['path'])
208 212 return model
209 213
210 214 def copy_notebook(self, from_name, to_name=None, path=''):
211 215 """Copy an existing notebook and return its new model.
212 216
213 217 If to_name not specified, increment `from_name-Copy#.ipynb`.
214 218 """
215 219 path = path.strip('/')
216 220 model = self.get_notebook(from_name, path)
217 221 if not to_name:
218 222 base = os.path.splitext(from_name)[0] + '-Copy'
219 223 to_name = self.increment_filename(base, path)
220 224 model['name'] = to_name
221 225 model = self.save_notebook(model, to_name, path)
222 226 return model
223 227
224 228 def log_info(self):
225 229 self.log.info(self.info_string())
226 230
227 231 def trust_notebook(self, name, path=''):
228 232 """Explicitly trust a notebook
229 233
230 234 Parameters
231 235 ----------
232 236 name : string
233 237 The filename of the notebook
234 238 path : string
235 239 The notebook's directory
236 240 """
237 241 model = self.get_notebook(name, path)
238 242 nb = model['content']
239 243 self.log.warn("Trusting notebook %s/%s", path, name)
240 244 self.notary.mark_cells(nb, True)
241 245 self.save_notebook(model, name, path)
242 246
243 247 def check_and_sign(self, nb, name, path=''):
244 248 """Check for trusted cells, and sign the notebook.
245 249
246 250 Called as a part of saving notebooks.
247 251
248 252 Parameters
249 253 ----------
250 254 nb : dict
251 255 The notebook structure
252 256 name : string
253 257 The filename of the notebook
254 258 path : string
255 259 The notebook's directory
256 260 """
257 261 if self.notary.check_cells(nb):
258 262 self.notary.sign(nb)
259 263 else:
260 264 self.log.warn("Saving untrusted notebook %s/%s", path, name)
261 265
262 266 def mark_trusted_cells(self, nb, name, path=''):
263 267 """Mark cells as trusted if the notebook signature matches.
264 268
265 269 Called as a part of loading notebooks.
266 270
267 271 Parameters
268 272 ----------
269 273 nb : dict
270 274 The notebook structure
271 275 name : string
272 276 The filename of the notebook
273 277 path : string
274 278 The notebook's directory
275 279 """
276 280 trusted = self.notary.check_signature(nb)
277 281 if not trusted:
278 282 self.log.warn("Notebook %s/%s is not trusted", path, name)
279 283 self.notary.mark_cells(nb, trusted)
280 284
281 285 def should_list(self, name):
282 286 """Should this file/directory name be displayed in a listing?"""
283 287 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,127 +1,129 b''
1 1 """Tornado handlers for the sessions web service.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 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 json
20 20
21 21 from tornado import web
22 22
23 23 from ...base.handlers import IPythonHandler, json_errors
24 24 from IPython.utils.jsonutil import date_default
25 25 from IPython.html.utils import url_path_join, url_escape
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Session web service handlers
29 29 #-----------------------------------------------------------------------------
30 30
31 31
32 32 class SessionRootHandler(IPythonHandler):
33 33
34 34 @web.authenticated
35 35 @json_errors
36 36 def get(self):
37 37 # Return a list of running sessions
38 38 sm = self.session_manager
39 39 sessions = sm.list_sessions()
40 40 self.finish(json.dumps(sessions, default=date_default))
41 41
42 42 @web.authenticated
43 43 @json_errors
44 44 def post(self):
45 45 # Creates a new session
46 46 #(unless a session already exists for the named nb)
47 47 sm = self.session_manager
48 48 nbm = self.notebook_manager
49 49 km = self.kernel_manager
50 50 model = self.get_json_body()
51 51 if model is None:
52 52 raise web.HTTPError(400, "No JSON data provided")
53 53 try:
54 54 name = model['notebook']['name']
55 55 except KeyError:
56 56 raise web.HTTPError(400, "Missing field in JSON data: name")
57 57 try:
58 58 path = model['notebook']['path']
59 59 except KeyError:
60 60 raise web.HTTPError(400, "Missing field in JSON data: path")
61 61 # Check to see if session exists
62 62 if sm.session_exists(name=name, path=path):
63 63 model = sm.get_session(name=name, path=path)
64 64 else:
65 kernel_id = km.start_kernel(path=path)
65 # allow nbm to specify kernels cwd
66 kernel_path = nbm.get_kernel_path(name=name, path=path)
67 kernel_id = km.start_kernel(path=kernel_path)
66 68 model = sm.create_session(name=name, path=path, kernel_id=kernel_id)
67 69 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
68 70 self.set_header('Location', url_escape(location))
69 71 self.set_status(201)
70 72 self.finish(json.dumps(model, default=date_default))
71 73
72 74 class SessionHandler(IPythonHandler):
73 75
74 76 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
75 77
76 78 @web.authenticated
77 79 @json_errors
78 80 def get(self, session_id):
79 81 # Returns the JSON model for a single session
80 82 sm = self.session_manager
81 83 model = sm.get_session(session_id=session_id)
82 84 self.finish(json.dumps(model, default=date_default))
83 85
84 86 @web.authenticated
85 87 @json_errors
86 88 def patch(self, session_id):
87 89 # Currently, this handler is strictly for renaming notebooks
88 90 sm = self.session_manager
89 91 model = self.get_json_body()
90 92 if model is None:
91 93 raise web.HTTPError(400, "No JSON data provided")
92 94 changes = {}
93 95 if 'notebook' in model:
94 96 notebook = model['notebook']
95 97 if 'name' in notebook:
96 98 changes['name'] = notebook['name']
97 99 if 'path' in notebook:
98 100 changes['path'] = notebook['path']
99 101
100 102 sm.update_session(session_id, **changes)
101 103 model = sm.get_session(session_id=session_id)
102 104 self.finish(json.dumps(model, default=date_default))
103 105
104 106 @web.authenticated
105 107 @json_errors
106 108 def delete(self, session_id):
107 109 # Deletes the session with given session_id
108 110 sm = self.session_manager
109 111 km = self.kernel_manager
110 112 session = sm.get_session(session_id=session_id)
111 113 sm.delete_session(session_id)
112 114 km.shutdown_kernel(session['kernel']['id'])
113 115 self.set_status(204)
114 116 self.finish()
115 117
116 118
117 119 #-----------------------------------------------------------------------------
118 120 # URL to handler mappings
119 121 #-----------------------------------------------------------------------------
120 122
121 123 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
122 124
123 125 default_handlers = [
124 126 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
125 127 (r"/api/sessions", SessionRootHandler)
126 128 ]
127 129
General Comments 0
You need to be logged in to leave comments. Login now