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