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