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