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