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