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