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