##// END OF EJS Templates
Refactoring notebook managers and adding Azure backed storage....
Brian Granger -
Show More
@@ -0,0 +1,140 b''
1 """A notebook manager that uses Azure blob storage.
2
3 Authors:
4
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2012 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import datetime
20
21 import azure
22 from azure.storage import BlobService
23
24 from tornado import web
25
26 from .basenbmanager import BaseNotebookManager
27 from IPython.nbformat import current
28 from IPython.utils.traitlets import Unicode, Instance
29
30
31 #-----------------------------------------------------------------------------
32 # Classes
33 #-----------------------------------------------------------------------------
34
35 class AzureNotebookManager(BaseNotebookManager):
36
37 account_name = Unicode('', config=True, help='Azure storage account name.')
38 account_key = Unicode('', config=True, help='Azure storage account key.')
39 container = Unicode('', config=True, help='Container name for notebooks.')
40
41 blob_service_host_base = Unicode('.blob.core.windows.net', config=True,
42 help='The basename for the blob service URL. If running on the preview site this '
43 'will be .blob.core.azure-preview.com.')
44 def _blob_service_host_base_changed(self, new):
45 self._update_service_host_base(new)
46
47 blob_service = Instance('azure.storage.BlobService')
48 def _blob_service_default(self):
49 return BlobService(account_name=self.account_name, account_key=self.account_key)
50
51 def __init__(self, **kwargs):
52 super(BaseNotebookManager,self).__init__(**kwargs)
53 self._update_service_host_base(self.blob_service_host_base)
54 self._create_container()
55
56 def _update_service_host_base(self, shb):
57 azure.BLOB_SERVICE_HOST_BASE = shb
58
59 def _create_container(self):
60 self.blob_service.create_container(self.container)
61
62 def load_notebook_names(self):
63 """On startup load the notebook ids and names from Azure.
64
65 The blob names are the notebook ids and the notebook names are stored
66 as blob metadata.
67 """
68 self.mapping = {}
69 blobs = self.blob_service.list_blobs(self.container)
70 ids = [blob.name for blob in blobs]
71
72 for id in ids:
73 md = self.blob_service.get_blob_metadata(self.container, id)
74 name = md['x-ms-meta-nbname']
75 self.mapping[id] = name
76
77 def list_notebooks(self):
78 """List all notebooks in the container.
79
80 This version uses `self.mapping` as the authoritative notebook list.
81 """
82 data = [dict(notebook_id=id,name=name) for id, name in self.mapping.items()]
83 data = sorted(data, key=lambda item: item['name'])
84 return data
85
86 def read_notebook_object(self, notebook_id):
87 """Get the object representation of a notebook by notebook_id."""
88 if not self.notebook_exists(notebook_id):
89 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
90 try:
91 s = self.blob_service.get_blob(self.container, notebook_id)
92 except:
93 raise web.HTTPError(500, u'Notebook cannot be read.')
94 try:
95 # v1 and v2 and json in the .ipynb files.
96 nb = current.reads(s, u'json')
97 except:
98 raise web.HTTPError(500, u'Unreadable JSON notebook.')
99 # Todo: The last modified should actually be saved in the notebook document.
100 # We are just using the current datetime until that is implemented.
101 last_modified = datetime.datetime.utcnow()
102 return last_modified, nb
103
104 def write_notebook_object(self, nb, notebook_id=None):
105 """Save an existing notebook object by notebook_id."""
106 try:
107 new_name = nb.metadata.name
108 except AttributeError:
109 raise web.HTTPError(400, u'Missing notebook name')
110
111 if notebook_id is None:
112 notebook_id = self.new_notebook_id(new_name)
113
114 if notebook_id not in self.mapping:
115 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
116
117 try:
118 data = current.writes(nb, u'json')
119 except Exception as e:
120 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
121
122 metadata = {'nbname': new_name}
123 try:
124 self.blob_service.put_blob(self.container, notebook_id, data, 'BlockBlob', x_ms_meta_name_values=metadata)
125 except Exception as e:
126 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
127
128 self.mapping[notebook_id] = new_name
129 return notebook_id
130
131 def delete_notebook(self, notebook_id):
132 """Delete notebook by notebook_id."""
133 if not self.notebook_exists(notebook_id):
134 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
135 try:
136 self.blob_service.delete_blob(self.container, notebook_id)
137 except Exception as e:
138 raise web.HTTPError(400, u'Unexpected error while deleting notebook: %s' % e)
139 else:
140 self.delete_notebook_id(notebook_id)
@@ -0,0 +1,181 b''
1 """A base class notebook manager.
2
3 Authors:
4
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import uuid
20
21 from tornado import web
22
23 from IPython.config.configurable import LoggingConfigurable
24 from IPython.nbformat import current
25 from IPython.utils.traitlets import List, Dict
26
27 #-----------------------------------------------------------------------------
28 # Classes
29 #-----------------------------------------------------------------------------
30
31 class BaseNotebookManager(LoggingConfigurable):
32
33 allowed_formats = List([u'json',u'py'])
34
35 # Map notebook_ids to notebook names
36 mapping = Dict()
37
38 def load_notebook_names(self):
39 """Load the notebook names into memory.
40
41 This should be called once immediately after the notebook manager
42 is created to load the existing notebooks into the mapping in
43 memory.
44 """
45 self.list_notebooks()
46
47 def list_notebooks(self):
48 """List all notebooks.
49
50 This returns a list of dicts, each of the form::
51
52 dict(notebook_id=notebook,name=name)
53
54 This list of dicts should be sorted by name::
55
56 data = sorted(data, key=lambda item: item['name'])
57 """
58 raise NotImplementedError('must be implemented in a subclass')
59
60
61 def new_notebook_id(self, name):
62 """Generate a new notebook_id for a name and store its mapping."""
63 # TODO: the following will give stable urls for notebooks, but unless
64 # the notebooks are immediately redirected to their new urls when their
65 # filemname changes, nasty inconsistencies result. So for now it's
66 # disabled and instead we use a random uuid4() call. But we leave the
67 # logic here so that we can later reactivate it, whhen the necessary
68 # url redirection code is written.
69 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
70 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
71
72 notebook_id = unicode(uuid.uuid4())
73 self.mapping[notebook_id] = name
74 return notebook_id
75
76 def delete_notebook_id(self, notebook_id):
77 """Delete a notebook's id in the mapping.
78
79 This doesn't delete the actual notebook, only its entry in the mapping.
80 """
81 del self.mapping[notebook_id]
82
83 def notebook_exists(self, notebook_id):
84 """Does a notebook exist?"""
85 return notebook_id in self.mapping
86
87 def get_notebook(self, notebook_id, format=u'json'):
88 """Get the representation of a notebook in format by notebook_id."""
89 format = unicode(format)
90 if format not in self.allowed_formats:
91 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
92 last_modified, nb = self.read_notebook_object(notebook_id)
93 kwargs = {}
94 if format == 'json':
95 # don't split lines for sending over the wire, because it
96 # should match the Python in-memory format.
97 kwargs['split_lines'] = False
98 data = current.writes(nb, format, **kwargs)
99 name = nb.get('name','notebook')
100 return last_modified, name, data
101
102 def read_notebook_object(self, notebook_id):
103 """Get the object representation of a notebook by notebook_id."""
104 raise NotImplementedError('must be implemented in a subclass')
105
106 def save_new_notebook(self, data, name=None, format=u'json'):
107 """Save a new notebook and return its notebook_id.
108
109 If a name is passed in, it overrides any values in the notebook data
110 and the value in the data is updated to use that value.
111 """
112 if format not in self.allowed_formats:
113 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
114
115 try:
116 nb = current.reads(data.decode('utf-8'), format)
117 except:
118 raise web.HTTPError(400, u'Invalid JSON data')
119
120 if name is None:
121 try:
122 name = nb.metadata.name
123 except AttributeError:
124 raise web.HTTPError(400, u'Missing notebook name')
125 nb.metadata.name = name
126
127 notebook_id = self.write_notebook_object(nb)
128 return notebook_id
129
130 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
131 """Save an existing notebook by notebook_id."""
132 if format not in self.allowed_formats:
133 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
134
135 try:
136 nb = current.reads(data.decode('utf-8'), format)
137 except:
138 raise web.HTTPError(400, u'Invalid JSON data')
139
140 if name is not None:
141 nb.metadata.name = name
142 self.write_notebook_object(nb, notebook_id)
143
144 def write_notebook_object(self, nb, notebook_id=None):
145 """Write a notebook object and return its notebook_id.
146
147 If notebook_id is None, this method should create a new notebook_id.
148 If notebook_id is not None, this method should check to make sure it
149 exists and is valid.
150 """
151 raise NotImplementedError('must be implemented in a subclass')
152
153 def delete_notebook(self, notebook_id):
154 """Delete notebook by notebook_id."""
155 raise NotImplementedError('must be implemented in a subclass')
156
157 def increment_filename(self, name):
158 """Increment a filename to make it unique.
159
160 This exists for notebook stores that must have unique names. When a notebook
161 is created or copied this method constructs a unique filename, typically
162 by appending an integer to the name.
163 """
164 return name
165
166 def new_notebook(self):
167 """Create a new notebook and return its notebook_id."""
168 name = self.increment_filename('Untitled')
169 metadata = current.new_metadata(name=name)
170 nb = current.new_notebook(metadata=metadata)
171 notebook_id = self.write_notebook_object(nb)
172 return notebook_id
173
174 def copy_notebook(self, notebook_id):
175 """Copy an existing notebook and return its notebook_id."""
176 last_mod, nb = self.read_notebook_object(notebook_id)
177 name = nb.metadata.name + '-Copy'
178 name = self.increment_filename(name)
179 nb.metadata.name = name
180 notebook_id = self.write_notebook_object(nb)
181 return notebook_id
@@ -1,286 +1,207 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 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2011 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 datetime
19 import datetime
20 import io
20 import io
21 import os
21 import os
22 import uuid
23 import glob
22 import glob
24
23
25 from tornado import web
24 from tornado import web
26
25
27 from IPython.config.configurable import LoggingConfigurable
26 from .basenbmanager import BaseNotebookManager
28 from IPython.nbformat import current
27 from IPython.nbformat import current
29 from IPython.utils.traitlets import Unicode, List, Dict, Bool, TraitError
28 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30
29
31 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
32 # Classes
31 # Classes
33 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
34
33
35 class NotebookManager(LoggingConfigurable):
34 class FileNotebookManager(BaseNotebookManager):
36
35
37 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
36 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
38 The directory to use for notebooks.
37 The directory to use for notebooks.
39 """)
38 """)
40 def _notebook_dir_changed(self, name, old, new):
39 def _notebook_dir_changed(self, name, old, new):
41 """do a bit of validation of the notebook dir"""
40 """do a bit of validation of the notebook dir"""
42 if os.path.exists(new) and not os.path.isdir(new):
41 if os.path.exists(new) and not os.path.isdir(new):
43 raise TraitError("notebook dir %r is not a directory" % new)
42 raise TraitError("notebook dir %r is not a directory" % new)
44 if not os.path.exists(new):
43 if not os.path.exists(new):
45 self.log.info("Creating notebook dir %s", new)
44 self.log.info("Creating notebook dir %s", new)
46 try:
45 try:
47 os.mkdir(new)
46 os.mkdir(new)
48 except:
47 except:
49 raise TraitError("Couldn't create notebook dir %r" % new)
48 raise TraitError("Couldn't create notebook dir %r" % new)
50
49
51 save_script = Bool(False, config=True,
50 save_script = Bool(False, config=True,
52 help="""Automatically create a Python script when saving the notebook.
51 help="""Automatically create a Python script when saving the notebook.
53
52
54 For easier use of import, %run and %load across notebooks, a
53 For easier use of import, %run and %load across notebooks, a
55 <notebook-name>.py script will be created next to any
54 <notebook-name>.py script will be created next to any
56 <notebook-name>.ipynb on each save. This can also be set with the
55 <notebook-name>.ipynb on each save. This can also be set with the
57 short `--script` flag.
56 short `--script` flag.
58 """
57 """
59 )
58 )
60
59
61 filename_ext = Unicode(u'.ipynb')
60 filename_ext = Unicode(u'.ipynb')
62 allowed_formats = List([u'json',u'py'])
63
61
64 # Map notebook_ids to notebook names
65 mapping = Dict()
66 # Map notebook names to notebook_ids
62 # Map notebook names to notebook_ids
67 rev_mapping = Dict()
63 rev_mapping = Dict()
68
64
69 def list_notebooks(self):
65 def get_notebook_names(self):
70 """List all notebooks in the notebook dir.
66 """List all notebook names in the notebook dir."""
71
72 This returns a list of dicts of the form::
73
74 dict(notebook_id=notebook,name=name)
75 """
76 names = glob.glob(os.path.join(self.notebook_dir,
67 names = glob.glob(os.path.join(self.notebook_dir,
77 '*' + self.filename_ext))
68 '*' + self.filename_ext))
78 names = [os.path.splitext(os.path.basename(name))[0]
69 names = [os.path.splitext(os.path.basename(name))[0]
79 for name in names]
70 for name in names]
71 return names
72
73 def list_notebooks(self):
74 """List all notebooks in the notebook dir."""
75 names = self.get_notebook_names()
80
76
81 data = []
77 data = []
82 for name in names:
78 for name in names:
83 if name not in self.rev_mapping:
79 if name not in self.rev_mapping:
84 notebook_id = self.new_notebook_id(name)
80 notebook_id = self.new_notebook_id(name)
85 else:
81 else:
86 notebook_id = self.rev_mapping[name]
82 notebook_id = self.rev_mapping[name]
87 data.append(dict(notebook_id=notebook_id,name=name))
83 data.append(dict(notebook_id=notebook_id,name=name))
88 data = sorted(data, key=lambda item: item['name'])
84 data = sorted(data, key=lambda item: item['name'])
89 return data
85 return data
90
86
91 def new_notebook_id(self, name):
87 def new_notebook_id(self, name):
92 """Generate a new notebook_id for a name and store its mappings."""
88 """Generate a new notebook_id for a name and store its mappings."""
93 # TODO: the following will give stable urls for notebooks, but unless
89 notebook_id = super(BaseNotebookManager, self).new_notebook_id(name)
94 # the notebooks are immediately redirected to their new urls when their
95 # filemname changes, nasty inconsistencies result. So for now it's
96 # disabled and instead we use a random uuid4() call. But we leave the
97 # logic here so that we can later reactivate it, whhen the necessary
98 # url redirection code is written.
99 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
100 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
101
102 notebook_id = unicode(uuid.uuid4())
103
104 self.mapping[notebook_id] = name
105 self.rev_mapping[name] = notebook_id
90 self.rev_mapping[name] = notebook_id
106 return notebook_id
91 return notebook_id
107
92
108 def delete_notebook_id(self, notebook_id):
93 def delete_notebook_id(self, notebook_id):
109 """Delete a notebook's id only. This doesn't delete the actual notebook."""
94 """Delete a notebook's id in the mapping."""
95 super(BaseNotebookManager, self).delete_notebook_id(notebook_id)
110 name = self.mapping[notebook_id]
96 name = self.mapping[notebook_id]
111 del self.mapping[notebook_id]
112 del self.rev_mapping[name]
97 del self.rev_mapping[name]
113
98
114 def notebook_exists(self, notebook_id):
99 def notebook_exists(self, notebook_id):
115 """Does a notebook exist?"""
100 """Does a notebook exist?"""
116 if notebook_id not in self.mapping:
101 exists = super(BaseNotebookManager, self).notebook_exists(notebook_id)
102 if not exists:
117 return False
103 return False
118 path = self.get_path_by_name(self.mapping[notebook_id])
104 path = self.get_path_by_name(self.mapping[notebook_id])
119 return os.path.isfile(path)
105 return os.path.isfile(path)
120
106
121 def find_path(self, notebook_id):
107 def find_path(self, notebook_id):
122 """Return a full path to a notebook given its notebook_id."""
108 """Return a full path to a notebook given its notebook_id."""
123 try:
109 try:
124 name = self.mapping[notebook_id]
110 name = self.mapping[notebook_id]
125 except KeyError:
111 except KeyError:
126 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
112 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
127 return self.get_path_by_name(name)
113 return self.get_path_by_name(name)
128
114
129 def get_path_by_name(self, name):
115 def get_path_by_name(self, name):
130 """Return a full path to a notebook given its name."""
116 """Return a full path to a notebook given its name."""
131 filename = name + self.filename_ext
117 filename = name + self.filename_ext
132 path = os.path.join(self.notebook_dir, filename)
118 path = os.path.join(self.notebook_dir, filename)
133 return path
119 return path
134
120
135 def get_notebook(self, notebook_id, format=u'json'):
121 def read_notebook_object(self, notebook_id):
136 """Get the representation of a notebook in format by notebook_id."""
137 format = unicode(format)
138 if format not in self.allowed_formats:
139 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
140 last_modified, nb = self.get_notebook_object(notebook_id)
141 kwargs = {}
142 if format == 'json':
143 # don't split lines for sending over the wire, because it
144 # should match the Python in-memory format.
145 kwargs['split_lines'] = False
146 data = current.writes(nb, format, **kwargs)
147 name = nb.get('name','notebook')
148 return last_modified, name, data
149
150 def get_notebook_object(self, notebook_id):
151 """Get the NotebookNode representation of a notebook by notebook_id."""
122 """Get the NotebookNode representation of a notebook by notebook_id."""
152 path = self.find_path(notebook_id)
123 path = self.find_path(notebook_id)
153 if not os.path.isfile(path):
124 if not os.path.isfile(path):
154 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
125 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
155 info = os.stat(path)
126 info = os.stat(path)
156 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
127 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
157 with open(path,'r') as f:
128 with open(path,'r') as f:
158 s = f.read()
129 s = f.read()
159 try:
130 try:
160 # v1 and v2 and json in the .ipynb files.
131 # v1 and v2 and json in the .ipynb files.
161 nb = current.reads(s, u'json')
132 nb = current.reads(s, u'json')
162 except:
133 except:
163 raise web.HTTPError(500, u'Unreadable JSON notebook.')
134 raise web.HTTPError(500, u'Unreadable JSON notebook.')
164 # Always use the filename as the notebook name.
135 # Always use the filename as the notebook name.
165 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
136 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
166 return last_modified, nb
137 return last_modified, nb
167
138
168 def save_new_notebook(self, data, name=None, format=u'json'):
139 def write_notebook_object(self, nb, notebook_id=None):
169 """Save a new notebook and return its notebook_id.
140 """Save an existing notebook object by notebook_id."""
170
171 If a name is passed in, it overrides any values in the notebook data
172 and the value in the data is updated to use that value.
173 """
174 if format not in self.allowed_formats:
175 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
176
177 try:
178 nb = current.reads(data.decode('utf-8'), format)
179 except:
180 raise web.HTTPError(400, u'Invalid JSON data')
181
182 if name is None:
183 try:
184 name = nb.metadata.name
185 except AttributeError:
186 raise web.HTTPError(400, u'Missing notebook name')
187 nb.metadata.name = name
188
189 notebook_id = self.new_notebook_id(name)
190 self.save_notebook_object(notebook_id, nb)
191 return notebook_id
192
193 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
194 """Save an existing notebook by notebook_id."""
195 if format not in self.allowed_formats:
196 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
197
198 try:
141 try:
199 nb = current.reads(data.decode('utf-8'), format)
142 new_name = nb.metadata.name
200 except:
143 except AttributeError:
201 raise web.HTTPError(400, u'Invalid JSON data')
144 raise web.HTTPError(400, u'Missing notebook name')
202
145
203 if name is not None:
146 if notebook_id is None:
204 nb.metadata.name = name
147 notebook_id = self.new_notebook_id(new_name)
205 self.save_notebook_object(notebook_id, nb)
206
148
207 def save_notebook_object(self, notebook_id, nb):
208 """Save an existing notebook object by notebook_id."""
209 if notebook_id not in self.mapping:
149 if notebook_id not in self.mapping:
210 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
150 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
151
211 old_name = self.mapping[notebook_id]
152 old_name = self.mapping[notebook_id]
212 try:
213 new_name = nb.metadata.name
214 except AttributeError:
215 raise web.HTTPError(400, u'Missing notebook name')
216 path = self.get_path_by_name(new_name)
153 path = self.get_path_by_name(new_name)
217 try:
154 try:
218 with open(path,'w') as f:
155 with open(path,'w') as f:
219 current.write(nb, f, u'json')
156 current.write(nb, f, u'json')
220 except Exception as e:
157 except Exception as e:
221 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
158 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
159
222 # save .py script as well
160 # save .py script as well
223 if self.save_script:
161 if self.save_script:
224 pypath = os.path.splitext(path)[0] + '.py'
162 pypath = os.path.splitext(path)[0] + '.py'
225 try:
163 try:
226 with io.open(pypath,'w', encoding='utf-8') as f:
164 with io.open(pypath,'w', encoding='utf-8') as f:
227 current.write(nb, f, u'py')
165 current.write(nb, f, u'py')
228 except Exception as e:
166 except Exception as e:
229 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
167 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
230
168
169 # remove old files if the name changed
231 if old_name != new_name:
170 if old_name != new_name:
232 old_path = self.get_path_by_name(old_name)
171 old_path = self.get_path_by_name(old_name)
233 if os.path.isfile(old_path):
172 if os.path.isfile(old_path):
234 os.unlink(old_path)
173 os.unlink(old_path)
235 if self.save_script:
174 if self.save_script:
236 old_pypath = os.path.splitext(old_path)[0] + '.py'
175 old_pypath = os.path.splitext(old_path)[0] + '.py'
237 if os.path.isfile(old_pypath):
176 if os.path.isfile(old_pypath):
238 os.unlink(old_pypath)
177 os.unlink(old_pypath)
239 self.mapping[notebook_id] = new_name
178 self.mapping[notebook_id] = new_name
240 self.rev_mapping[new_name] = notebook_id
179 self.rev_mapping[new_name] = notebook_id
241 del self.rev_mapping[old_name]
180 del self.rev_mapping[old_name]
181
182 return notebook_id
242
183
243 def delete_notebook(self, notebook_id):
184 def delete_notebook(self, notebook_id):
244 """Delete notebook by notebook_id."""
185 """Delete notebook by notebook_id."""
245 path = self.find_path(notebook_id)
186 path = self.find_path(notebook_id)
246 if not os.path.isfile(path):
187 if not os.path.isfile(path):
247 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
188 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
248 os.unlink(path)
189 os.unlink(path)
249 self.delete_notebook_id(notebook_id)
190 self.delete_notebook_id(notebook_id)
250
191
251 def increment_filename(self, basename):
192 def increment_filename(self, basename):
252 """Return a non-used filename of the form basename<int>.
193 """Return a non-used filename of the form basename<int>.
253
194
254 This searches through the filenames (basename0, basename1, ...)
195 This searches through the filenames (basename0, basename1, ...)
255 until is find one that is not already being used. It is used to
196 until is find one that is not already being used. It is used to
256 create Untitled and Copy names that are unique.
197 create Untitled and Copy names that are unique.
257 """
198 """
258 i = 0
199 i = 0
259 while True:
200 while True:
260 name = u'%s%i' % (basename,i)
201 name = u'%s%i' % (basename,i)
261 path = self.get_path_by_name(name)
202 path = self.get_path_by_name(name)
262 if not os.path.isfile(path):
203 if not os.path.isfile(path):
263 break
204 break
264 else:
205 else:
265 i = i+1
206 i = i+1
266 return path, name
207 return name
267
268 def new_notebook(self):
269 """Create a new notebook and return its notebook_id."""
270 path, name = self.increment_filename('Untitled')
271 notebook_id = self.new_notebook_id(name)
272 metadata = current.new_metadata(name=name)
273 nb = current.new_notebook(metadata=metadata)
274 with open(path,'w') as f:
275 current.write(nb, f, u'json')
276 return notebook_id
277
278 def copy_notebook(self, notebook_id):
279 """Copy an existing notebook and return its notebook_id."""
280 last_mod, nb = self.get_notebook_object(notebook_id)
281 name = nb.metadata.name + '-Copy'
282 path, name = self.increment_filename(name)
283 nb.metadata.name = name
284 notebook_id = self.new_notebook_id(name)
285 self.save_notebook_object(notebook_id, nb)
286 return notebook_id
@@ -1,606 +1,615 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 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 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import webbrowser
31 import webbrowser
32
32
33 # Third party
33 # Third party
34 import zmq
34 import zmq
35
35
36 # Install the pyzmq ioloop. This has to be done before anything else from
36 # Install the pyzmq ioloop. This has to be done before anything else from
37 # tornado is imported.
37 # tornado is imported.
38 from zmq.eventloop import ioloop
38 from zmq.eventloop import ioloop
39 ioloop.install()
39 ioloop.install()
40
40
41 from tornado import httpserver
41 from tornado import httpserver
42 from tornado import web
42 from tornado import web
43
43
44 # Our own libraries
44 # Our own libraries
45 from .kernelmanager import MappingKernelManager
45 from .kernelmanager import MappingKernelManager
46 from .handlers import (LoginHandler, LogoutHandler,
46 from .handlers import (LoginHandler, LogoutHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
52 FileFindHandler,
52 FileFindHandler,
53 )
53 )
54 from .notebookmanager import NotebookManager
54 from .notebookmanager import NotebookManager
55 from .clustermanager import ClusterManager
55 from .clustermanager import ClusterManager
56
56
57 from IPython.config.application import catch_config_error, boolean_flag
57 from IPython.config.application import catch_config_error, boolean_flag
58 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.application import BaseIPythonApplication
59 from IPython.core.profiledir import ProfileDir
59 from IPython.core.profiledir import ProfileDir
60 from IPython.frontend.consoleapp import IPythonConsoleApp
60 from IPython.frontend.consoleapp import IPythonConsoleApp
61 from IPython.lib.kernel import swallow_argv
61 from IPython.lib.kernel import swallow_argv
62 from IPython.zmq.session import Session, default_secure
62 from IPython.zmq.session import Session, default_secure
63 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 from IPython.zmq.zmqshell import ZMQInteractiveShell
64 from IPython.zmq.ipkernel import (
64 from IPython.zmq.ipkernel import (
65 flags as ipkernel_flags,
65 flags as ipkernel_flags,
66 aliases as ipkernel_aliases,
66 aliases as ipkernel_aliases,
67 IPKernelApp
67 IPKernelApp
68 )
68 )
69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils.importstring import import_item
70 from IPython.utils.traitlets import (
71 Dict, Unicode, Integer, List, Enum, Bool,
72 DottedObjectName
73 )
70 from IPython.utils import py3compat
74 from IPython.utils import py3compat
71 from IPython.utils.path import filefind
75 from IPython.utils.path import filefind
72
76
73 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
74 # Module globals
78 # Module globals
75 #-----------------------------------------------------------------------------
79 #-----------------------------------------------------------------------------
76
80
77 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
81 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
78 _kernel_action_regex = r"(?P<action>restart|interrupt)"
82 _kernel_action_regex = r"(?P<action>restart|interrupt)"
79 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
83 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
80 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
84 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
81 _cluster_action_regex = r"(?P<action>start|stop)"
85 _cluster_action_regex = r"(?P<action>start|stop)"
82
86
83
87
84 LOCALHOST = '127.0.0.1'
88 LOCALHOST = '127.0.0.1'
85
89
86 _examples = """
90 _examples = """
87 ipython notebook # start the notebook
91 ipython notebook # start the notebook
88 ipython notebook --profile=sympy # use the sympy profile
92 ipython notebook --profile=sympy # use the sympy profile
89 ipython notebook --pylab=inline # pylab in inline plotting mode
93 ipython notebook --pylab=inline # pylab in inline plotting mode
90 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
91 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
95 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
92 """
96 """
93
97
94 #-----------------------------------------------------------------------------
98 #-----------------------------------------------------------------------------
95 # Helper functions
99 # Helper functions
96 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
97
101
98 def url_path_join(a,b):
102 def url_path_join(a,b):
99 if a.endswith('/') and b.startswith('/'):
103 if a.endswith('/') and b.startswith('/'):
100 return a[:-1]+b
104 return a[:-1]+b
101 else:
105 else:
102 return a+b
106 return a+b
103
107
104 def random_ports(port, n):
108 def random_ports(port, n):
105 """Generate a list of n random ports near the given port.
109 """Generate a list of n random ports near the given port.
106
110
107 The first 5 ports will be sequential, and the remaining n-5 will be
111 The first 5 ports will be sequential, and the remaining n-5 will be
108 randomly selected in the range [port-2*n, port+2*n].
112 randomly selected in the range [port-2*n, port+2*n].
109 """
113 """
110 for i in range(min(5, n)):
114 for i in range(min(5, n)):
111 yield port + i
115 yield port + i
112 for i in range(n-5):
116 for i in range(n-5):
113 yield port + random.randint(-2*n, 2*n)
117 yield port + random.randint(-2*n, 2*n)
114
118
115 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
116 # The Tornado web application
120 # The Tornado web application
117 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
118
122
119 class NotebookWebApplication(web.Application):
123 class NotebookWebApplication(web.Application):
120
124
121 def __init__(self, ipython_app, kernel_manager, notebook_manager,
125 def __init__(self, ipython_app, kernel_manager, notebook_manager,
122 cluster_manager, log,
126 cluster_manager, log,
123 base_project_url, settings_overrides):
127 base_project_url, settings_overrides):
124 handlers = [
128 handlers = [
125 (r"/", ProjectDashboardHandler),
129 (r"/", ProjectDashboardHandler),
126 (r"/login", LoginHandler),
130 (r"/login", LoginHandler),
127 (r"/logout", LogoutHandler),
131 (r"/logout", LogoutHandler),
128 (r"/new", NewHandler),
132 (r"/new", NewHandler),
129 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
133 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
130 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
134 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
131 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
135 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
132 (r"/kernels", MainKernelHandler),
136 (r"/kernels", MainKernelHandler),
133 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
137 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
134 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
138 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
135 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
139 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
136 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
140 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
137 (r"/notebooks", NotebookRootHandler),
141 (r"/notebooks", NotebookRootHandler),
138 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
142 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
139 (r"/rstservice/render", RSTHandler),
143 (r"/rstservice/render", RSTHandler),
140 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
144 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
141 (r"/clusters", MainClusterHandler),
145 (r"/clusters", MainClusterHandler),
142 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
146 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
143 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
147 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
144 ]
148 ]
145
149
146 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
147 # base_project_url will always be unicode, which will in turn
151 # base_project_url will always be unicode, which will in turn
148 # make the patterns unicode, and ultimately result in unicode
152 # make the patterns unicode, and ultimately result in unicode
149 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # keys in kwargs to handler._execute(**kwargs) in tornado.
150 # This enforces that base_project_url be ascii in that situation.
154 # This enforces that base_project_url be ascii in that situation.
151 #
155 #
152 # Note that the URLs these patterns check against are escaped,
156 # Note that the URLs these patterns check against are escaped,
153 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
154 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
158 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
155
159
156 settings = dict(
160 settings = dict(
157 template_path=os.path.join(os.path.dirname(__file__), "templates"),
161 template_path=os.path.join(os.path.dirname(__file__), "templates"),
158 static_path=ipython_app.static_file_path,
162 static_path=ipython_app.static_file_path,
159 static_handler_class = FileFindHandler,
163 static_handler_class = FileFindHandler,
160 cookie_secret=os.urandom(1024),
164 cookie_secret=os.urandom(1024),
161 login_url="%s/login"%(base_project_url.rstrip('/')),
165 login_url="%s/login"%(base_project_url.rstrip('/')),
162 )
166 )
163
167
164 # allow custom overrides for the tornado web app.
168 # allow custom overrides for the tornado web app.
165 settings.update(settings_overrides)
169 settings.update(settings_overrides)
166
170
167 # prepend base_project_url onto the patterns that we match
171 # prepend base_project_url onto the patterns that we match
168 new_handlers = []
172 new_handlers = []
169 for handler in handlers:
173 for handler in handlers:
170 pattern = url_path_join(base_project_url, handler[0])
174 pattern = url_path_join(base_project_url, handler[0])
171 new_handler = tuple([pattern]+list(handler[1:]))
175 new_handler = tuple([pattern]+list(handler[1:]))
172 new_handlers.append( new_handler )
176 new_handlers.append( new_handler )
173
177
174 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
178 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
175
179
176 self.kernel_manager = kernel_manager
180 self.kernel_manager = kernel_manager
177 self.notebook_manager = notebook_manager
181 self.notebook_manager = notebook_manager
178 self.cluster_manager = cluster_manager
182 self.cluster_manager = cluster_manager
179 self.ipython_app = ipython_app
183 self.ipython_app = ipython_app
180 self.read_only = self.ipython_app.read_only
184 self.read_only = self.ipython_app.read_only
181 self.log = log
185 self.log = log
182
186
183
187
184 #-----------------------------------------------------------------------------
188 #-----------------------------------------------------------------------------
185 # Aliases and Flags
189 # Aliases and Flags
186 #-----------------------------------------------------------------------------
190 #-----------------------------------------------------------------------------
187
191
188 flags = dict(ipkernel_flags)
192 flags = dict(ipkernel_flags)
189 flags['no-browser']=(
193 flags['no-browser']=(
190 {'NotebookApp' : {'open_browser' : False}},
194 {'NotebookApp' : {'open_browser' : False}},
191 "Don't open the notebook in a browser after startup."
195 "Don't open the notebook in a browser after startup."
192 )
196 )
193 flags['no-mathjax']=(
197 flags['no-mathjax']=(
194 {'NotebookApp' : {'enable_mathjax' : False}},
198 {'NotebookApp' : {'enable_mathjax' : False}},
195 """Disable MathJax
199 """Disable MathJax
196
200
197 MathJax is the javascript library IPython uses to render math/LaTeX. It is
201 MathJax is the javascript library IPython uses to render math/LaTeX. It is
198 very large, so you may want to disable it if you have a slow internet
202 very large, so you may want to disable it if you have a slow internet
199 connection, or for offline use of the notebook.
203 connection, or for offline use of the notebook.
200
204
201 When disabled, equations etc. will appear as their untransformed TeX source.
205 When disabled, equations etc. will appear as their untransformed TeX source.
202 """
206 """
203 )
207 )
204 flags['read-only'] = (
208 flags['read-only'] = (
205 {'NotebookApp' : {'read_only' : True}},
209 {'NotebookApp' : {'read_only' : True}},
206 """Allow read-only access to notebooks.
210 """Allow read-only access to notebooks.
207
211
208 When using a password to protect the notebook server, this flag
212 When using a password to protect the notebook server, this flag
209 allows unauthenticated clients to view the notebook list, and
213 allows unauthenticated clients to view the notebook list, and
210 individual notebooks, but not edit them, start kernels, or run
214 individual notebooks, but not edit them, start kernels, or run
211 code.
215 code.
212
216
213 If no password is set, the server will be entirely read-only.
217 If no password is set, the server will be entirely read-only.
214 """
218 """
215 )
219 )
216
220
217 # Add notebook manager flags
221 # Add notebook manager flags
218 flags.update(boolean_flag('script', 'NotebookManager.save_script',
222 flags.update(boolean_flag('script', 'NotebookManager.save_script',
219 'Auto-save a .py script everytime the .ipynb notebook is saved',
223 'Auto-save a .py script everytime the .ipynb notebook is saved',
220 'Do not auto-save .py scripts for every notebook'))
224 'Do not auto-save .py scripts for every notebook'))
221
225
222 # the flags that are specific to the frontend
226 # the flags that are specific to the frontend
223 # these must be scrubbed before being passed to the kernel,
227 # these must be scrubbed before being passed to the kernel,
224 # or it will raise an error on unrecognized flags
228 # or it will raise an error on unrecognized flags
225 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
229 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
226
230
227 aliases = dict(ipkernel_aliases)
231 aliases = dict(ipkernel_aliases)
228
232
229 aliases.update({
233 aliases.update({
230 'ip': 'NotebookApp.ip',
234 'ip': 'NotebookApp.ip',
231 'port': 'NotebookApp.port',
235 'port': 'NotebookApp.port',
232 'port-retries': 'NotebookApp.port_retries',
236 'port-retries': 'NotebookApp.port_retries',
233 'keyfile': 'NotebookApp.keyfile',
237 'keyfile': 'NotebookApp.keyfile',
234 'certfile': 'NotebookApp.certfile',
238 'certfile': 'NotebookApp.certfile',
235 'notebook-dir': 'NotebookManager.notebook_dir',
239 'notebook-dir': 'NotebookManager.notebook_dir',
236 'browser': 'NotebookApp.browser',
240 'browser': 'NotebookApp.browser',
237 })
241 })
238
242
239 # remove ipkernel flags that are singletons, and don't make sense in
243 # remove ipkernel flags that are singletons, and don't make sense in
240 # multi-kernel evironment:
244 # multi-kernel evironment:
241 aliases.pop('f', None)
245 aliases.pop('f', None)
242
246
243 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
247 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
244 u'notebook-dir']
248 u'notebook-dir']
245
249
246 #-----------------------------------------------------------------------------
250 #-----------------------------------------------------------------------------
247 # NotebookApp
251 # NotebookApp
248 #-----------------------------------------------------------------------------
252 #-----------------------------------------------------------------------------
249
253
250 class NotebookApp(BaseIPythonApplication):
254 class NotebookApp(BaseIPythonApplication):
251
255
252 name = 'ipython-notebook'
256 name = 'ipython-notebook'
253 default_config_file_name='ipython_notebook_config.py'
257 default_config_file_name='ipython_notebook_config.py'
254
258
255 description = """
259 description = """
256 The IPython HTML Notebook.
260 The IPython HTML Notebook.
257
261
258 This launches a Tornado based HTML Notebook Server that serves up an
262 This launches a Tornado based HTML Notebook Server that serves up an
259 HTML5/Javascript Notebook client.
263 HTML5/Javascript Notebook client.
260 """
264 """
261 examples = _examples
265 examples = _examples
262
266
263 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
267 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
264 flags = Dict(flags)
268 flags = Dict(flags)
265 aliases = Dict(aliases)
269 aliases = Dict(aliases)
266
270
267 kernel_argv = List(Unicode)
271 kernel_argv = List(Unicode)
268
272
269 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
273 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
270 default_value=logging.INFO,
274 default_value=logging.INFO,
271 config=True,
275 config=True,
272 help="Set the log level by value or name.")
276 help="Set the log level by value or name.")
273
277
274 # create requested profiles by default, if they don't exist:
278 # create requested profiles by default, if they don't exist:
275 auto_create = Bool(True)
279 auto_create = Bool(True)
276
280
277 # file to be opened in the notebook server
281 # file to be opened in the notebook server
278 file_to_run = Unicode('')
282 file_to_run = Unicode('')
279
283
280 # Network related information.
284 # Network related information.
281
285
282 ip = Unicode(LOCALHOST, config=True,
286 ip = Unicode(LOCALHOST, config=True,
283 help="The IP address the notebook server will listen on."
287 help="The IP address the notebook server will listen on."
284 )
288 )
285
289
286 def _ip_changed(self, name, old, new):
290 def _ip_changed(self, name, old, new):
287 if new == u'*': self.ip = u''
291 if new == u'*': self.ip = u''
288
292
289 port = Integer(8888, config=True,
293 port = Integer(8888, config=True,
290 help="The port the notebook server will listen on."
294 help="The port the notebook server will listen on."
291 )
295 )
292 port_retries = Integer(50, config=True,
296 port_retries = Integer(50, config=True,
293 help="The number of additional ports to try if the specified port is not available."
297 help="The number of additional ports to try if the specified port is not available."
294 )
298 )
295
299
296 certfile = Unicode(u'', config=True,
300 certfile = Unicode(u'', config=True,
297 help="""The full path to an SSL/TLS certificate file."""
301 help="""The full path to an SSL/TLS certificate file."""
298 )
302 )
299
303
300 keyfile = Unicode(u'', config=True,
304 keyfile = Unicode(u'', config=True,
301 help="""The full path to a private key file for usage with SSL/TLS."""
305 help="""The full path to a private key file for usage with SSL/TLS."""
302 )
306 )
303
307
304 password = Unicode(u'', config=True,
308 password = Unicode(u'', config=True,
305 help="""Hashed password to use for web authentication.
309 help="""Hashed password to use for web authentication.
306
310
307 To generate, type in a python/IPython shell:
311 To generate, type in a python/IPython shell:
308
312
309 from IPython.lib import passwd; passwd()
313 from IPython.lib import passwd; passwd()
310
314
311 The string should be of the form type:salt:hashed-password.
315 The string should be of the form type:salt:hashed-password.
312 """
316 """
313 )
317 )
314
318
315 open_browser = Bool(True, config=True,
319 open_browser = Bool(True, config=True,
316 help="""Whether to open in a browser after starting.
320 help="""Whether to open in a browser after starting.
317 The specific browser used is platform dependent and
321 The specific browser used is platform dependent and
318 determined by the python standard library `webbrowser`
322 determined by the python standard library `webbrowser`
319 module, unless it is overridden using the --browser
323 module, unless it is overridden using the --browser
320 (NotebookApp.browser) configuration option.
324 (NotebookApp.browser) configuration option.
321 """)
325 """)
322
326
323 browser = Unicode(u'', config=True,
327 browser = Unicode(u'', config=True,
324 help="""Specify what command to use to invoke a web
328 help="""Specify what command to use to invoke a web
325 browser when opening the notebook. If not specified, the
329 browser when opening the notebook. If not specified, the
326 default browser will be determined by the `webbrowser`
330 default browser will be determined by the `webbrowser`
327 standard library module, which allows setting of the
331 standard library module, which allows setting of the
328 BROWSER environment variable to override it.
332 BROWSER environment variable to override it.
329 """)
333 """)
330
334
331 read_only = Bool(False, config=True,
335 read_only = Bool(False, config=True,
332 help="Whether to prevent editing/execution of notebooks."
336 help="Whether to prevent editing/execution of notebooks."
333 )
337 )
334
338
335 webapp_settings = Dict(config=True,
339 webapp_settings = Dict(config=True,
336 help="Supply overrides for the tornado.web.Application that the "
340 help="Supply overrides for the tornado.web.Application that the "
337 "IPython notebook uses.")
341 "IPython notebook uses.")
338
342
339 enable_mathjax = Bool(True, config=True,
343 enable_mathjax = Bool(True, config=True,
340 help="""Whether to enable MathJax for typesetting math/TeX
344 help="""Whether to enable MathJax for typesetting math/TeX
341
345
342 MathJax is the javascript library IPython uses to render math/LaTeX. It is
346 MathJax is the javascript library IPython uses to render math/LaTeX. It is
343 very large, so you may want to disable it if you have a slow internet
347 very large, so you may want to disable it if you have a slow internet
344 connection, or for offline use of the notebook.
348 connection, or for offline use of the notebook.
345
349
346 When disabled, equations etc. will appear as their untransformed TeX source.
350 When disabled, equations etc. will appear as their untransformed TeX source.
347 """
351 """
348 )
352 )
349 def _enable_mathjax_changed(self, name, old, new):
353 def _enable_mathjax_changed(self, name, old, new):
350 """set mathjax url to empty if mathjax is disabled"""
354 """set mathjax url to empty if mathjax is disabled"""
351 if not new:
355 if not new:
352 self.mathjax_url = u''
356 self.mathjax_url = u''
353
357
354 base_project_url = Unicode('/', config=True,
358 base_project_url = Unicode('/', config=True,
355 help='''The base URL for the notebook server''')
359 help='''The base URL for the notebook server''')
356 base_kernel_url = Unicode('/', config=True,
360 base_kernel_url = Unicode('/', config=True,
357 help='''The base URL for the kernel server''')
361 help='''The base URL for the kernel server''')
358 websocket_host = Unicode("", config=True,
362 websocket_host = Unicode("", config=True,
359 help="""The hostname for the websocket server."""
363 help="""The hostname for the websocket server."""
360 )
364 )
361
365
362 extra_static_paths = List(Unicode, config=True,
366 extra_static_paths = List(Unicode, config=True,
363 help="""Extra paths to search for serving static files.
367 help="""Extra paths to search for serving static files.
364
368
365 This allows adding javascript/css to be available from the notebook server machine,
369 This allows adding javascript/css to be available from the notebook server machine,
366 or overriding individual files in the IPython"""
370 or overriding individual files in the IPython"""
367 )
371 )
368 def _extra_static_paths_default(self):
372 def _extra_static_paths_default(self):
369 return [os.path.join(self.profile_dir.location, 'static')]
373 return [os.path.join(self.profile_dir.location, 'static')]
370
374
371 @property
375 @property
372 def static_file_path(self):
376 def static_file_path(self):
373 """return extra paths + the default location"""
377 """return extra paths + the default location"""
374 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
378 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
375
379
376 mathjax_url = Unicode("", config=True,
380 mathjax_url = Unicode("", config=True,
377 help="""The url for MathJax.js."""
381 help="""The url for MathJax.js."""
378 )
382 )
379 def _mathjax_url_default(self):
383 def _mathjax_url_default(self):
380 if not self.enable_mathjax:
384 if not self.enable_mathjax:
381 return u''
385 return u''
382 static_url_prefix = self.webapp_settings.get("static_url_prefix",
386 static_url_prefix = self.webapp_settings.get("static_url_prefix",
383 "/static/")
387 "/static/")
384 try:
388 try:
385 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
389 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
386 except IOError:
390 except IOError:
387 if self.certfile:
391 if self.certfile:
388 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
392 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
389 base = u"https://c328740.ssl.cf1.rackcdn.com"
393 base = u"https://c328740.ssl.cf1.rackcdn.com"
390 else:
394 else:
391 base = u"http://cdn.mathjax.org"
395 base = u"http://cdn.mathjax.org"
392
396
393 url = base + u"/mathjax/latest/MathJax.js"
397 url = base + u"/mathjax/latest/MathJax.js"
394 self.log.info("Using MathJax from CDN: %s", url)
398 self.log.info("Using MathJax from CDN: %s", url)
395 return url
399 return url
396 else:
400 else:
397 self.log.info("Using local MathJax from %s" % mathjax)
401 self.log.info("Using local MathJax from %s" % mathjax)
398 return static_url_prefix+u"mathjax/MathJax.js"
402 return static_url_prefix+u"mathjax/MathJax.js"
399
403
400 def _mathjax_url_changed(self, name, old, new):
404 def _mathjax_url_changed(self, name, old, new):
401 if new and not self.enable_mathjax:
405 if new and not self.enable_mathjax:
402 # enable_mathjax=False overrides mathjax_url
406 # enable_mathjax=False overrides mathjax_url
403 self.mathjax_url = u''
407 self.mathjax_url = u''
404 else:
408 else:
405 self.log.info("Using MathJax: %s", new)
409 self.log.info("Using MathJax: %s", new)
406
410
411 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.notebookmanager.NotebookManager',
412 config=True,
413 help='The notebook manager class to use.')
414
407 def parse_command_line(self, argv=None):
415 def parse_command_line(self, argv=None):
408 super(NotebookApp, self).parse_command_line(argv)
416 super(NotebookApp, self).parse_command_line(argv)
409 if argv is None:
417 if argv is None:
410 argv = sys.argv[1:]
418 argv = sys.argv[1:]
411
419
412 # Scrub frontend-specific flags
420 # Scrub frontend-specific flags
413 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
421 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
414 # Kernel should inherit default config file from frontend
422 # Kernel should inherit default config file from frontend
415 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
423 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
416
424
417 if self.extra_args:
425 if self.extra_args:
418 f = os.path.abspath(self.extra_args[0])
426 f = os.path.abspath(self.extra_args[0])
419 if os.path.isdir(f):
427 if os.path.isdir(f):
420 nbdir = f
428 nbdir = f
421 else:
429 else:
422 self.file_to_run = f
430 self.file_to_run = f
423 nbdir = os.path.dirname(f)
431 nbdir = os.path.dirname(f)
424 self.config.NotebookManager.notebook_dir = nbdir
432 self.config.NotebookManager.notebook_dir = nbdir
425
433
426 def init_configurables(self):
434 def init_configurables(self):
427 # force Session default to be secure
435 # force Session default to be secure
428 default_secure(self.config)
436 default_secure(self.config)
429 self.kernel_manager = MappingKernelManager(
437 self.kernel_manager = MappingKernelManager(
430 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
438 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
431 connection_dir = self.profile_dir.security_dir,
439 connection_dir = self.profile_dir.security_dir,
432 )
440 )
433 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
441 kls = import_item(self.notebook_manager_class)
442 self.notebook_manager = kls(config=self.config, log=self.log)
434 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
443 self.log.info("Serving notebooks from %s", self.notebook_manager.notebook_dir)
435 self.notebook_manager.list_notebooks()
444 self.notebook_manager.load_notebook_names()
436 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
445 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
437 self.cluster_manager.update_profiles()
446 self.cluster_manager.update_profiles()
438
447
439 def init_logging(self):
448 def init_logging(self):
440 # This prevents double log messages because tornado use a root logger that
449 # This prevents double log messages because tornado use a root logger that
441 # self.log is a child of. The logging module dipatches log messages to a log
450 # self.log is a child of. The logging module dipatches log messages to a log
442 # and all of its ancenstors until propagate is set to False.
451 # and all of its ancenstors until propagate is set to False.
443 self.log.propagate = False
452 self.log.propagate = False
444
453
445 def init_webapp(self):
454 def init_webapp(self):
446 """initialize tornado webapp and httpserver"""
455 """initialize tornado webapp and httpserver"""
447 self.web_app = NotebookWebApplication(
456 self.web_app = NotebookWebApplication(
448 self, self.kernel_manager, self.notebook_manager,
457 self, self.kernel_manager, self.notebook_manager,
449 self.cluster_manager, self.log,
458 self.cluster_manager, self.log,
450 self.base_project_url, self.webapp_settings
459 self.base_project_url, self.webapp_settings
451 )
460 )
452 if self.certfile:
461 if self.certfile:
453 ssl_options = dict(certfile=self.certfile)
462 ssl_options = dict(certfile=self.certfile)
454 if self.keyfile:
463 if self.keyfile:
455 ssl_options['keyfile'] = self.keyfile
464 ssl_options['keyfile'] = self.keyfile
456 else:
465 else:
457 ssl_options = None
466 ssl_options = None
458 self.web_app.password = self.password
467 self.web_app.password = self.password
459 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
468 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
460 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
469 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
461 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
470 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
462 'but not using any encryption or authentication. This is highly '
471 'but not using any encryption or authentication. This is highly '
463 'insecure and not recommended.')
472 'insecure and not recommended.')
464
473
465 success = None
474 success = None
466 for port in random_ports(self.port, self.port_retries+1):
475 for port in random_ports(self.port, self.port_retries+1):
467 try:
476 try:
468 self.http_server.listen(port, self.ip)
477 self.http_server.listen(port, self.ip)
469 except socket.error as e:
478 except socket.error as e:
470 if e.errno != errno.EADDRINUSE:
479 if e.errno != errno.EADDRINUSE:
471 raise
480 raise
472 self.log.info('The port %i is already in use, trying another random port.' % port)
481 self.log.info('The port %i is already in use, trying another random port.' % port)
473 else:
482 else:
474 self.port = port
483 self.port = port
475 success = True
484 success = True
476 break
485 break
477 if not success:
486 if not success:
478 self.log.critical('ERROR: the notebook server could not be started because '
487 self.log.critical('ERROR: the notebook server could not be started because '
479 'no available port could be found.')
488 'no available port could be found.')
480 self.exit(1)
489 self.exit(1)
481
490
482 def init_signal(self):
491 def init_signal(self):
483 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
492 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
484 # safely extract zmq version info:
493 # safely extract zmq version info:
485 try:
494 try:
486 zmq_v = zmq.pyzmq_version_info()
495 zmq_v = zmq.pyzmq_version_info()
487 except AttributeError:
496 except AttributeError:
488 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
497 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
489 if 'dev' in zmq.__version__:
498 if 'dev' in zmq.__version__:
490 zmq_v.append(999)
499 zmq_v.append(999)
491 zmq_v = tuple(zmq_v)
500 zmq_v = tuple(zmq_v)
492 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
501 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
493 # This won't work with 2.1.7 and
502 # This won't work with 2.1.7 and
494 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
503 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
495 # but it will work
504 # but it will work
496 signal.signal(signal.SIGINT, self._handle_sigint)
505 signal.signal(signal.SIGINT, self._handle_sigint)
497 signal.signal(signal.SIGTERM, self._signal_stop)
506 signal.signal(signal.SIGTERM, self._signal_stop)
498
507
499 def _handle_sigint(self, sig, frame):
508 def _handle_sigint(self, sig, frame):
500 """SIGINT handler spawns confirmation dialog"""
509 """SIGINT handler spawns confirmation dialog"""
501 # register more forceful signal handler for ^C^C case
510 # register more forceful signal handler for ^C^C case
502 signal.signal(signal.SIGINT, self._signal_stop)
511 signal.signal(signal.SIGINT, self._signal_stop)
503 # request confirmation dialog in bg thread, to avoid
512 # request confirmation dialog in bg thread, to avoid
504 # blocking the App
513 # blocking the App
505 thread = threading.Thread(target=self._confirm_exit)
514 thread = threading.Thread(target=self._confirm_exit)
506 thread.daemon = True
515 thread.daemon = True
507 thread.start()
516 thread.start()
508
517
509 def _restore_sigint_handler(self):
518 def _restore_sigint_handler(self):
510 """callback for restoring original SIGINT handler"""
519 """callback for restoring original SIGINT handler"""
511 signal.signal(signal.SIGINT, self._handle_sigint)
520 signal.signal(signal.SIGINT, self._handle_sigint)
512
521
513 def _confirm_exit(self):
522 def _confirm_exit(self):
514 """confirm shutdown on ^C
523 """confirm shutdown on ^C
515
524
516 A second ^C, or answering 'y' within 5s will cause shutdown,
525 A second ^C, or answering 'y' within 5s will cause shutdown,
517 otherwise original SIGINT handler will be restored.
526 otherwise original SIGINT handler will be restored.
518
527
519 This doesn't work on Windows.
528 This doesn't work on Windows.
520 """
529 """
521 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
530 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
522 time.sleep(0.1)
531 time.sleep(0.1)
523 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
532 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
524 sys.stdout.flush()
533 sys.stdout.flush()
525 r,w,x = select.select([sys.stdin], [], [], 5)
534 r,w,x = select.select([sys.stdin], [], [], 5)
526 if r:
535 if r:
527 line = sys.stdin.readline()
536 line = sys.stdin.readline()
528 if line.lower().startswith('y'):
537 if line.lower().startswith('y'):
529 self.log.critical("Shutdown confirmed")
538 self.log.critical("Shutdown confirmed")
530 ioloop.IOLoop.instance().stop()
539 ioloop.IOLoop.instance().stop()
531 return
540 return
532 else:
541 else:
533 print "No answer for 5s:",
542 print "No answer for 5s:",
534 print "resuming operation..."
543 print "resuming operation..."
535 # no answer, or answer is no:
544 # no answer, or answer is no:
536 # set it back to original SIGINT handler
545 # set it back to original SIGINT handler
537 # use IOLoop.add_callback because signal.signal must be called
546 # use IOLoop.add_callback because signal.signal must be called
538 # from main thread
547 # from main thread
539 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
548 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
540
549
541 def _signal_stop(self, sig, frame):
550 def _signal_stop(self, sig, frame):
542 self.log.critical("received signal %s, stopping", sig)
551 self.log.critical("received signal %s, stopping", sig)
543 ioloop.IOLoop.instance().stop()
552 ioloop.IOLoop.instance().stop()
544
553
545 @catch_config_error
554 @catch_config_error
546 def initialize(self, argv=None):
555 def initialize(self, argv=None):
547 self.init_logging()
556 self.init_logging()
548 super(NotebookApp, self).initialize(argv)
557 super(NotebookApp, self).initialize(argv)
549 self.init_configurables()
558 self.init_configurables()
550 self.init_webapp()
559 self.init_webapp()
551 self.init_signal()
560 self.init_signal()
552
561
553 def cleanup_kernels(self):
562 def cleanup_kernels(self):
554 """shutdown all kernels
563 """shutdown all kernels
555
564
556 The kernels will shutdown themselves when this process no longer exists,
565 The kernels will shutdown themselves when this process no longer exists,
557 but explicit shutdown allows the KernelManagers to cleanup the connection files.
566 but explicit shutdown allows the KernelManagers to cleanup the connection files.
558 """
567 """
559 self.log.info('Shutting down kernels')
568 self.log.info('Shutting down kernels')
560 km = self.kernel_manager
569 km = self.kernel_manager
561 # copy list, since shutdown_kernel deletes keys
570 # copy list, since shutdown_kernel deletes keys
562 for kid in list(km.kernel_ids):
571 for kid in list(km.kernel_ids):
563 km.shutdown_kernel(kid)
572 km.shutdown_kernel(kid)
564
573
565 def start(self):
574 def start(self):
566 ip = self.ip if self.ip else '[all ip addresses on your system]'
575 ip = self.ip if self.ip else '[all ip addresses on your system]'
567 proto = 'https' if self.certfile else 'http'
576 proto = 'https' if self.certfile else 'http'
568 info = self.log.info
577 info = self.log.info
569 info("The IPython Notebook is running at: %s://%s:%i%s" %
578 info("The IPython Notebook is running at: %s://%s:%i%s" %
570 (proto, ip, self.port,self.base_project_url) )
579 (proto, ip, self.port,self.base_project_url) )
571 info("Use Control-C to stop this server and shut down all kernels.")
580 info("Use Control-C to stop this server and shut down all kernels.")
572
581
573 if self.open_browser or self.file_to_run:
582 if self.open_browser or self.file_to_run:
574 ip = self.ip or '127.0.0.1'
583 ip = self.ip or '127.0.0.1'
575 try:
584 try:
576 browser = webbrowser.get(self.browser or None)
585 browser = webbrowser.get(self.browser or None)
577 except webbrowser.Error as e:
586 except webbrowser.Error as e:
578 self.log.warn('No web browser found: %s.' % e)
587 self.log.warn('No web browser found: %s.' % e)
579 browser = None
588 browser = None
580
589
581 if self.file_to_run:
590 if self.file_to_run:
582 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
591 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
583 url = self.notebook_manager.rev_mapping.get(name, '')
592 url = self.notebook_manager.rev_mapping.get(name, '')
584 else:
593 else:
585 url = ''
594 url = ''
586 if browser:
595 if browser:
587 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
596 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
588 self.port, self.base_project_url, url), new=2)
597 self.port, self.base_project_url, url), new=2)
589 threading.Thread(target=b).start()
598 threading.Thread(target=b).start()
590 try:
599 try:
591 ioloop.IOLoop.instance().start()
600 ioloop.IOLoop.instance().start()
592 except KeyboardInterrupt:
601 except KeyboardInterrupt:
593 info("Interrupted...")
602 info("Interrupted...")
594 finally:
603 finally:
595 self.cleanup_kernels()
604 self.cleanup_kernels()
596
605
597
606
598 #-----------------------------------------------------------------------------
607 #-----------------------------------------------------------------------------
599 # Main entry point
608 # Main entry point
600 #-----------------------------------------------------------------------------
609 #-----------------------------------------------------------------------------
601
610
602 def launch_new_instance():
611 def launch_new_instance():
603 app = NotebookApp.instance()
612 app = NotebookApp.instance()
604 app.initialize()
613 app.initialize()
605 app.start()
614 app.start()
606
615
@@ -1,34 +1,34 b''
1 """Tests for the notebook manager."""
1 """Tests for the notebook manager."""
2
2
3 import os
3 import os
4 from unittest import TestCase
4 from unittest import TestCase
5 from tempfile import NamedTemporaryFile
5 from tempfile import NamedTemporaryFile
6
6
7 from IPython.utils.tempdir import TemporaryDirectory
7 from IPython.utils.tempdir import TemporaryDirectory
8 from IPython.utils.traitlets import TraitError
8 from IPython.utils.traitlets import TraitError
9
9
10 from IPython.frontend.html.notebook.notebookmanager import NotebookManager
10 from IPython.frontend.html.notebook.filenbmanager import FileNotebookManager
11
11
12 class TestNotebookManager(TestCase):
12 class TestNotebookManager(TestCase):
13
13
14 def test_nb_dir(self):
14 def test_nb_dir(self):
15 with TemporaryDirectory() as td:
15 with TemporaryDirectory() as td:
16 km = NotebookManager(notebook_dir=td)
16 km = FileNotebookManager(notebook_dir=td)
17 self.assertEqual(km.notebook_dir, td)
17 self.assertEquals(km.notebook_dir, td)
18
18
19 def test_create_nb_dir(self):
19 def test_create_nb_dir(self):
20 with TemporaryDirectory() as td:
20 with TemporaryDirectory() as td:
21 nbdir = os.path.join(td, 'notebooks')
21 nbdir = os.path.join(td, 'notebooks')
22 km = NotebookManager(notebook_dir=nbdir)
22 km = FileNotebookManager(notebook_dir=nbdir)
23 self.assertEqual(km.notebook_dir, nbdir)
23 self.assertEquals(km.notebook_dir, nbdir)
24
24
25 def test_missing_nb_dir(self):
25 def test_missing_nb_dir(self):
26 with TemporaryDirectory() as td:
26 with TemporaryDirectory() as td:
27 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
27 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
28 self.assertRaises(TraitError, NotebookManager, notebook_dir=nbdir)
28 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
29
29
30 def test_invalid_nb_dir(self):
30 def test_invalid_nb_dir(self):
31 with NamedTemporaryFile() as tf:
31 with NamedTemporaryFile() as tf:
32 self.assertRaises(TraitError, NotebookManager, notebook_dir=tf.name)
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
33
33
34
34
General Comments 0
You need to be logged in to leave comments. Login now