##// END OF EJS Templates
fix print css less
Bussonnier Matthias -
Show More
@@ -1,898 +1,899 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import Cookie
19 import Cookie
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import hashlib
22 import hashlib
23 import logging
23 import logging
24 import mimetypes
24 import mimetypes
25 import os
25 import os
26 import stat
26 import stat
27 import threading
27 import threading
28 import time
28 import time
29 import uuid
29 import uuid
30
30
31 from tornado.escape import url_escape
31 from tornado.escape import url_escape
32 from tornado import web
32 from tornado import web
33 from tornado import websocket
33 from tornado import websocket
34
34
35 from zmq.eventloop import ioloop
35 from zmq.eventloop import ioloop
36 from zmq.utils import jsonapi
36 from zmq.utils import jsonapi
37
37
38 from IPython.external.decorator import decorator
38 from IPython.external.decorator import decorator
39 from IPython.zmq.session import Session
39 from IPython.zmq.session import Session
40 from IPython.lib.security import passwd_check
40 from IPython.lib.security import passwd_check
41 from IPython.utils.jsonutil import date_default
41 from IPython.utils.jsonutil import date_default
42 from IPython.utils.path import filefind
42 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import PY3
43 from IPython.utils.py3compat import PY3
44
44
45 try:
45 try:
46 from docutils.core import publish_string
46 from docutils.core import publish_string
47 except ImportError:
47 except ImportError:
48 publish_string = None
48 publish_string = None
49
49
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53
53
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
55 # parts tornado cares about haven't really changed, so it's OK to continue
55 # parts tornado cares about haven't really changed, so it's OK to continue
56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
57 # version as of Oct 30/2011) the version check fails, see the issue report:
57 # version as of Oct 30/2011) the version check fails, see the issue report:
58
58
59 # https://github.com/facebook/tornado/issues/385
59 # https://github.com/facebook/tornado/issues/385
60
60
61 # This issue has been fixed in Tornado post 2.1.1:
61 # This issue has been fixed in Tornado post 2.1.1:
62
62
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64
64
65 # Here we manually apply the same patch as above so that users of IPython can
65 # Here we manually apply the same patch as above so that users of IPython can
66 # continue to work with an officially released Tornado. We make the
66 # continue to work with an officially released Tornado. We make the
67 # monkeypatch version check as narrow as possible to limit its effects; once
67 # monkeypatch version check as narrow as possible to limit its effects; once
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69
69
70 import tornado
70 import tornado
71
71
72 if tornado.version_info <= (2,1,1):
72 if tornado.version_info <= (2,1,1):
73
73
74 def _execute(self, transforms, *args, **kwargs):
74 def _execute(self, transforms, *args, **kwargs):
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76
76
77 self.open_args = args
77 self.open_args = args
78 self.open_kwargs = kwargs
78 self.open_kwargs = kwargs
79
79
80 # The difference between version 8 and 13 is that in 8 the
80 # The difference between version 8 and 13 is that in 8 the
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 # simply "Origin".
82 # simply "Origin".
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 self.ws_connection = WebSocketProtocol8(self)
84 self.ws_connection = WebSocketProtocol8(self)
85 self.ws_connection.accept_connection()
85 self.ws_connection.accept_connection()
86
86
87 elif self.request.headers.get("Sec-WebSocket-Version"):
87 elif self.request.headers.get("Sec-WebSocket-Version"):
88 self.stream.write(tornado.escape.utf8(
88 self.stream.write(tornado.escape.utf8(
89 "HTTP/1.1 426 Upgrade Required\r\n"
89 "HTTP/1.1 426 Upgrade Required\r\n"
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 self.stream.close()
91 self.stream.close()
92
92
93 else:
93 else:
94 self.ws_connection = WebSocketProtocol76(self)
94 self.ws_connection = WebSocketProtocol76(self)
95 self.ws_connection.accept_connection()
95 self.ws_connection.accept_connection()
96
96
97 websocket.WebSocketHandler._execute = _execute
97 websocket.WebSocketHandler._execute = _execute
98 del _execute
98 del _execute
99
99
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101 # Decorator for disabling read-only handlers
101 # Decorator for disabling read-only handlers
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103
103
104 @decorator
104 @decorator
105 def not_if_readonly(f, self, *args, **kwargs):
105 def not_if_readonly(f, self, *args, **kwargs):
106 if self.application.read_only:
106 if self.application.read_only:
107 raise web.HTTPError(403, "Notebook server is read-only")
107 raise web.HTTPError(403, "Notebook server is read-only")
108 else:
108 else:
109 return f(self, *args, **kwargs)
109 return f(self, *args, **kwargs)
110
110
111 @decorator
111 @decorator
112 def authenticate_unless_readonly(f, self, *args, **kwargs):
112 def authenticate_unless_readonly(f, self, *args, **kwargs):
113 """authenticate this page *unless* readonly view is active.
113 """authenticate this page *unless* readonly view is active.
114
114
115 In read-only mode, the notebook list and print view should
115 In read-only mode, the notebook list and print view should
116 be accessible without authentication.
116 be accessible without authentication.
117 """
117 """
118
118
119 @web.authenticated
119 @web.authenticated
120 def auth_f(self, *args, **kwargs):
120 def auth_f(self, *args, **kwargs):
121 return f(self, *args, **kwargs)
121 return f(self, *args, **kwargs)
122
122
123 if self.application.read_only:
123 if self.application.read_only:
124 return f(self, *args, **kwargs)
124 return f(self, *args, **kwargs)
125 else:
125 else:
126 return auth_f(self, *args, **kwargs)
126 return auth_f(self, *args, **kwargs)
127
127
128 def urljoin(*pieces):
128 def urljoin(*pieces):
129 """Join componenet of url into a relative url
129 """Join componenet of url into a relative url
130
130
131 Use to prevent double slash when joining subpath
131 Use to prevent double slash when joining subpath
132 """
132 """
133 striped = [s.strip('/') for s in pieces]
133 striped = [s.strip('/') for s in pieces]
134 return '/'.join(s for s in striped if s)
134 return '/'.join(s for s in striped if s)
135
135
136 #-----------------------------------------------------------------------------
136 #-----------------------------------------------------------------------------
137 # Top-level handlers
137 # Top-level handlers
138 #-----------------------------------------------------------------------------
138 #-----------------------------------------------------------------------------
139
139
140 class RequestHandler(web.RequestHandler):
140 class RequestHandler(web.RequestHandler):
141 """RequestHandler with default variable setting."""
141 """RequestHandler with default variable setting."""
142
142
143 def render(*args, **kwargs):
143 def render(*args, **kwargs):
144 kwargs.setdefault('message', '')
144 kwargs.setdefault('message', '')
145 return web.RequestHandler.render(*args, **kwargs)
145 return web.RequestHandler.render(*args, **kwargs)
146
146
147 class AuthenticatedHandler(RequestHandler):
147 class AuthenticatedHandler(RequestHandler):
148 """A RequestHandler with an authenticated user."""
148 """A RequestHandler with an authenticated user."""
149
149
150 def get_current_user(self):
150 def get_current_user(self):
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
152 # For now the user_id should not return empty, but it could eventually
152 # For now the user_id should not return empty, but it could eventually
153 if user_id == '':
153 if user_id == '':
154 user_id = 'anonymous'
154 user_id = 'anonymous'
155 if user_id is None:
155 if user_id is None:
156 # prevent extra Invalid cookie sig warnings:
156 # prevent extra Invalid cookie sig warnings:
157 self.clear_cookie(self.settings['cookie_name'])
157 self.clear_cookie(self.settings['cookie_name'])
158 if not self.application.password and not self.application.read_only:
158 if not self.application.password and not self.application.read_only:
159 user_id = 'anonymous'
159 user_id = 'anonymous'
160 return user_id
160 return user_id
161
161
162 @property
162 @property
163 def logged_in(self):
163 def logged_in(self):
164 """Is a user currently logged in?
164 """Is a user currently logged in?
165
165
166 """
166 """
167 user = self.get_current_user()
167 user = self.get_current_user()
168 return (user and not user == 'anonymous')
168 return (user and not user == 'anonymous')
169
169
170 @property
170 @property
171 def login_available(self):
171 def login_available(self):
172 """May a user proceed to log in?
172 """May a user proceed to log in?
173
173
174 This returns True if login capability is available, irrespective of
174 This returns True if login capability is available, irrespective of
175 whether the user is already logged in or not.
175 whether the user is already logged in or not.
176
176
177 """
177 """
178 return bool(self.application.password)
178 return bool(self.application.password)
179
179
180 @property
180 @property
181 def read_only(self):
181 def read_only(self):
182 """Is the notebook read-only?
182 """Is the notebook read-only?
183
183
184 """
184 """
185 return self.application.read_only
185 return self.application.read_only
186
186
187 @property
187 def use_less(self):
188 def use_less(self):
188 """Use less instead of css in templates"""
189 """Use less instead of css in templates"""
189 return self.application.use_less
190 return self.application.use_less
190
191
191 @property
192 @property
192 def ws_url(self):
193 def ws_url(self):
193 """websocket url matching the current request
194 """websocket url matching the current request
194
195
195 turns http[s]://host[:port] into
196 turns http[s]://host[:port] into
196 ws[s]://host[:port]
197 ws[s]://host[:port]
197 """
198 """
198 proto = self.request.protocol.replace('http', 'ws')
199 proto = self.request.protocol.replace('http', 'ws')
199 host = self.application.ipython_app.websocket_host # default to config value
200 host = self.application.ipython_app.websocket_host # default to config value
200 if host == '':
201 if host == '':
201 host = self.request.host # get from request
202 host = self.request.host # get from request
202 return "%s://%s" % (proto, host)
203 return "%s://%s" % (proto, host)
203
204
204
205
205 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
206 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
206 """static files should only be accessible when logged in"""
207 """static files should only be accessible when logged in"""
207
208
208 @authenticate_unless_readonly
209 @authenticate_unless_readonly
209 def get(self, path):
210 def get(self, path):
210 return web.StaticFileHandler.get(self, path)
211 return web.StaticFileHandler.get(self, path)
211
212
212
213
213 class ProjectDashboardHandler(AuthenticatedHandler):
214 class ProjectDashboardHandler(AuthenticatedHandler):
214
215
215 @authenticate_unless_readonly
216 @authenticate_unless_readonly
216 def get(self):
217 def get(self):
217 nbm = self.application.notebook_manager
218 nbm = self.application.notebook_manager
218 project = nbm.notebook_dir
219 project = nbm.notebook_dir
219 template = self.application.jinja2_env.get_template('projectdashboard.html')
220 template = self.application.jinja2_env.get_template('projectdashboard.html')
220 self.write( template.render(project=project,
221 self.write( template.render(project=project,
221 base_project_url=self.application.ipython_app.base_project_url,
222 base_project_url=self.application.ipython_app.base_project_url,
222 base_kernel_url=self.application.ipython_app.base_kernel_url,
223 base_kernel_url=self.application.ipython_app.base_kernel_url,
223 read_only=self.read_only,
224 read_only=self.read_only,
224 logged_in=self.logged_in,
225 logged_in=self.logged_in,
225 login_available=self.login_available))
226 login_available=self.login_available))
226
227
227
228
228 class LoginHandler(AuthenticatedHandler):
229 class LoginHandler(AuthenticatedHandler):
229
230
230 def _render(self, message=None):
231 def _render(self, message=None):
231 template = self.application.jinja2_env.get_template('login.html')
232 template = self.application.jinja2_env.get_template('login.html')
232 self.write( template.render(
233 self.write( template.render(
233 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
234 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
234 read_only=self.read_only,
235 read_only=self.read_only,
235 logged_in=self.logged_in,
236 logged_in=self.logged_in,
236 login_available=self.login_available,
237 login_available=self.login_available,
237 base_project_url=self.application.ipython_app.base_project_url,
238 base_project_url=self.application.ipython_app.base_project_url,
238 message=message
239 message=message
239 ))
240 ))
240
241
241 def get(self):
242 def get(self):
242 if self.current_user:
243 if self.current_user:
243 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
244 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
244 else:
245 else:
245 self._render()
246 self._render()
246
247
247 def post(self):
248 def post(self):
248 pwd = self.get_argument('password', default=u'')
249 pwd = self.get_argument('password', default=u'')
249 if self.application.password:
250 if self.application.password:
250 if passwd_check(self.application.password, pwd):
251 if passwd_check(self.application.password, pwd):
251 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
252 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
252 else:
253 else:
253 self._render(message={'error': 'Invalid password'})
254 self._render(message={'error': 'Invalid password'})
254 return
255 return
255
256
256 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
257 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
257
258
258
259
259 class LogoutHandler(AuthenticatedHandler):
260 class LogoutHandler(AuthenticatedHandler):
260
261
261 def get(self):
262 def get(self):
262 self.clear_cookie(self.settings['cookie_name'])
263 self.clear_cookie(self.settings['cookie_name'])
263 if self.login_available:
264 if self.login_available:
264 message = {'info': 'Successfully logged out.'}
265 message = {'info': 'Successfully logged out.'}
265 else:
266 else:
266 message = {'warning': 'Cannot log out. Notebook authentication '
267 message = {'warning': 'Cannot log out. Notebook authentication '
267 'is disabled.'}
268 'is disabled.'}
268 template = self.application.jinja2_env.get_template('logout.html')
269 template = self.application.jinja2_env.get_template('logout.html')
269 self.write( template.render(
270 self.write( template.render(
270 read_only=self.read_only,
271 read_only=self.read_only,
271 logged_in=self.logged_in,
272 logged_in=self.logged_in,
272 login_available=self.login_available,
273 login_available=self.login_available,
273 base_project_url=self.application.ipython_app.base_project_url,
274 base_project_url=self.application.ipython_app.base_project_url,
274 message=message))
275 message=message))
275
276
276
277
277 class NewHandler(AuthenticatedHandler):
278 class NewHandler(AuthenticatedHandler):
278
279
279 @web.authenticated
280 @web.authenticated
280 def get(self):
281 def get(self):
281 nbm = self.application.notebook_manager
282 nbm = self.application.notebook_manager
282 project = nbm.notebook_dir
283 project = nbm.notebook_dir
283 notebook_id = nbm.new_notebook()
284 notebook_id = nbm.new_notebook()
284 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
285 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
285
286
286 class NamedNotebookHandler(AuthenticatedHandler):
287 class NamedNotebookHandler(AuthenticatedHandler):
287
288
288 @authenticate_unless_readonly
289 @authenticate_unless_readonly
289 def get(self, notebook_id):
290 def get(self, notebook_id):
290 nbm = self.application.notebook_manager
291 nbm = self.application.notebook_manager
291 project = nbm.notebook_dir
292 project = nbm.notebook_dir
292 if not nbm.notebook_exists(notebook_id):
293 if not nbm.notebook_exists(notebook_id):
293 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
294 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
294 template = self.application.jinja2_env.get_template('notebook.html')
295 template = self.application.jinja2_env.get_template('notebook.html')
295 self.write( template.render(project=project,
296 self.write( template.render(project=project,
296 notebook_id=notebook_id,
297 notebook_id=notebook_id,
297 base_project_url=self.application.ipython_app.base_project_url,
298 base_project_url=self.application.ipython_app.base_project_url,
298 base_kernel_url=self.application.ipython_app.base_kernel_url,
299 base_kernel_url=self.application.ipython_app.base_kernel_url,
299 kill_kernel=False,
300 kill_kernel=False,
300 read_only=self.read_only,
301 read_only=self.read_only,
301 logged_in=self.logged_in,
302 logged_in=self.logged_in,
302 login_available=self.login_available,
303 login_available=self.login_available,
303 mathjax_url=self.application.ipython_app.mathjax_url,
304 mathjax_url=self.application.ipython_app.mathjax_url,
304 use_less=self.use_less,))
305 use_less=self.use_less,))
305
306
306
307
307 class PrintNotebookHandler(AuthenticatedHandler):
308 class PrintNotebookHandler(AuthenticatedHandler):
308
309
309 @authenticate_unless_readonly
310 @authenticate_unless_readonly
310 def get(self, notebook_id):
311 def get(self, notebook_id):
311 nbm = self.application.notebook_manager
312 nbm = self.application.notebook_manager
312 project = nbm.notebook_dir
313 project = nbm.notebook_dir
313 if not nbm.notebook_exists(notebook_id):
314 if not nbm.notebook_exists(notebook_id):
314 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
315 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
315 template = self.application.jinja2_env.get_template('printnotebook.html')
316 template = self.application.jinja2_env.get_template('printnotebook.html')
316 self.write( template.render(
317 self.write( template.render(
317 project=project,
318 project=project,
318 notebook_id=notebook_id,
319 notebook_id=notebook_id,
319 base_project_url=self.application.ipython_app.base_project_url,
320 base_project_url=self.application.ipython_app.base_project_url,
320 base_kernel_url=self.application.ipython_app.base_kernel_url,
321 base_kernel_url=self.application.ipython_app.base_kernel_url,
321 kill_kernel=False,
322 kill_kernel=False,
322 read_only=self.read_only,
323 read_only=self.read_only,
323 logged_in=self.logged_in,
324 logged_in=self.logged_in,
324 login_available=self.login_available,
325 login_available=self.login_available,
325 mathjax_url=self.application.ipython_app.mathjax_url,
326 mathjax_url=self.application.ipython_app.mathjax_url,
326 ))
327 ))
327
328
328 #-----------------------------------------------------------------------------
329 #-----------------------------------------------------------------------------
329 # Kernel handlers
330 # Kernel handlers
330 #-----------------------------------------------------------------------------
331 #-----------------------------------------------------------------------------
331
332
332
333
333 class MainKernelHandler(AuthenticatedHandler):
334 class MainKernelHandler(AuthenticatedHandler):
334
335
335 @web.authenticated
336 @web.authenticated
336 def get(self):
337 def get(self):
337 km = self.application.kernel_manager
338 km = self.application.kernel_manager
338 self.finish(jsonapi.dumps(km.list_kernel_ids()))
339 self.finish(jsonapi.dumps(km.list_kernel_ids()))
339
340
340 @web.authenticated
341 @web.authenticated
341 def post(self):
342 def post(self):
342 km = self.application.kernel_manager
343 km = self.application.kernel_manager
343 nbm = self.application.notebook_manager
344 nbm = self.application.notebook_manager
344 notebook_id = self.get_argument('notebook', default=None)
345 notebook_id = self.get_argument('notebook', default=None)
345 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
346 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
346 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
347 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
347 self.set_header('Location', '/'+kernel_id)
348 self.set_header('Location', '/'+kernel_id)
348 self.finish(jsonapi.dumps(data))
349 self.finish(jsonapi.dumps(data))
349
350
350
351
351 class KernelHandler(AuthenticatedHandler):
352 class KernelHandler(AuthenticatedHandler):
352
353
353 SUPPORTED_METHODS = ('DELETE')
354 SUPPORTED_METHODS = ('DELETE')
354
355
355 @web.authenticated
356 @web.authenticated
356 def delete(self, kernel_id):
357 def delete(self, kernel_id):
357 km = self.application.kernel_manager
358 km = self.application.kernel_manager
358 km.shutdown_kernel(kernel_id)
359 km.shutdown_kernel(kernel_id)
359 self.set_status(204)
360 self.set_status(204)
360 self.finish()
361 self.finish()
361
362
362
363
363 class KernelActionHandler(AuthenticatedHandler):
364 class KernelActionHandler(AuthenticatedHandler):
364
365
365 @web.authenticated
366 @web.authenticated
366 def post(self, kernel_id, action):
367 def post(self, kernel_id, action):
367 km = self.application.kernel_manager
368 km = self.application.kernel_manager
368 if action == 'interrupt':
369 if action == 'interrupt':
369 km.interrupt_kernel(kernel_id)
370 km.interrupt_kernel(kernel_id)
370 self.set_status(204)
371 self.set_status(204)
371 if action == 'restart':
372 if action == 'restart':
372 km.restart_kernel(kernel_id)
373 km.restart_kernel(kernel_id)
373 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
374 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
374 self.set_header('Location', '/'+kernel_id)
375 self.set_header('Location', '/'+kernel_id)
375 self.write(jsonapi.dumps(data))
376 self.write(jsonapi.dumps(data))
376 self.finish()
377 self.finish()
377
378
378
379
379 class ZMQStreamHandler(websocket.WebSocketHandler):
380 class ZMQStreamHandler(websocket.WebSocketHandler):
380
381
381 def _reserialize_reply(self, msg_list):
382 def _reserialize_reply(self, msg_list):
382 """Reserialize a reply message using JSON.
383 """Reserialize a reply message using JSON.
383
384
384 This takes the msg list from the ZMQ socket, unserializes it using
385 This takes the msg list from the ZMQ socket, unserializes it using
385 self.session and then serializes the result using JSON. This method
386 self.session and then serializes the result using JSON. This method
386 should be used by self._on_zmq_reply to build messages that can
387 should be used by self._on_zmq_reply to build messages that can
387 be sent back to the browser.
388 be sent back to the browser.
388 """
389 """
389 idents, msg_list = self.session.feed_identities(msg_list)
390 idents, msg_list = self.session.feed_identities(msg_list)
390 msg = self.session.unserialize(msg_list)
391 msg = self.session.unserialize(msg_list)
391 try:
392 try:
392 msg['header'].pop('date')
393 msg['header'].pop('date')
393 except KeyError:
394 except KeyError:
394 pass
395 pass
395 try:
396 try:
396 msg['parent_header'].pop('date')
397 msg['parent_header'].pop('date')
397 except KeyError:
398 except KeyError:
398 pass
399 pass
399 msg.pop('buffers')
400 msg.pop('buffers')
400 return jsonapi.dumps(msg, default=date_default)
401 return jsonapi.dumps(msg, default=date_default)
401
402
402 def _on_zmq_reply(self, msg_list):
403 def _on_zmq_reply(self, msg_list):
403 try:
404 try:
404 msg = self._reserialize_reply(msg_list)
405 msg = self._reserialize_reply(msg_list)
405 except Exception:
406 except Exception:
406 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
407 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
407 else:
408 else:
408 self.write_message(msg)
409 self.write_message(msg)
409
410
410 def allow_draft76(self):
411 def allow_draft76(self):
411 """Allow draft 76, until browsers such as Safari update to RFC 6455.
412 """Allow draft 76, until browsers such as Safari update to RFC 6455.
412
413
413 This has been disabled by default in tornado in release 2.2.0, and
414 This has been disabled by default in tornado in release 2.2.0, and
414 support will be removed in later versions.
415 support will be removed in later versions.
415 """
416 """
416 return True
417 return True
417
418
418
419
419 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
420 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
420
421
421 def open(self, kernel_id):
422 def open(self, kernel_id):
422 self.kernel_id = kernel_id.decode('ascii')
423 self.kernel_id = kernel_id.decode('ascii')
423 try:
424 try:
424 cfg = self.application.config
425 cfg = self.application.config
425 except AttributeError:
426 except AttributeError:
426 # protect from the case where this is run from something other than
427 # protect from the case where this is run from something other than
427 # the notebook app:
428 # the notebook app:
428 cfg = None
429 cfg = None
429 self.session = Session(config=cfg)
430 self.session = Session(config=cfg)
430 self.save_on_message = self.on_message
431 self.save_on_message = self.on_message
431 self.on_message = self.on_first_message
432 self.on_message = self.on_first_message
432
433
433 def get_current_user(self):
434 def get_current_user(self):
434 user_id = self.get_secure_cookie(self.settings['cookie_name'])
435 user_id = self.get_secure_cookie(self.settings['cookie_name'])
435 if user_id == '' or (user_id is None and not self.application.password):
436 if user_id == '' or (user_id is None and not self.application.password):
436 user_id = 'anonymous'
437 user_id = 'anonymous'
437 return user_id
438 return user_id
438
439
439 def _inject_cookie_message(self, msg):
440 def _inject_cookie_message(self, msg):
440 """Inject the first message, which is the document cookie,
441 """Inject the first message, which is the document cookie,
441 for authentication."""
442 for authentication."""
442 if not PY3 and isinstance(msg, unicode):
443 if not PY3 and isinstance(msg, unicode):
443 # Cookie constructor doesn't accept unicode strings
444 # Cookie constructor doesn't accept unicode strings
444 # under Python 2.x for some reason
445 # under Python 2.x for some reason
445 msg = msg.encode('utf8', 'replace')
446 msg = msg.encode('utf8', 'replace')
446 try:
447 try:
447 self.request._cookies = Cookie.SimpleCookie(msg)
448 self.request._cookies = Cookie.SimpleCookie(msg)
448 except:
449 except:
449 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
450 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
450
451
451 def on_first_message(self, msg):
452 def on_first_message(self, msg):
452 self._inject_cookie_message(msg)
453 self._inject_cookie_message(msg)
453 if self.get_current_user() is None:
454 if self.get_current_user() is None:
454 logging.warn("Couldn't authenticate WebSocket connection")
455 logging.warn("Couldn't authenticate WebSocket connection")
455 raise web.HTTPError(403)
456 raise web.HTTPError(403)
456 self.on_message = self.save_on_message
457 self.on_message = self.save_on_message
457
458
458
459
459 class IOPubHandler(AuthenticatedZMQStreamHandler):
460 class IOPubHandler(AuthenticatedZMQStreamHandler):
460
461
461 def initialize(self, *args, **kwargs):
462 def initialize(self, *args, **kwargs):
462 self._kernel_alive = True
463 self._kernel_alive = True
463 self._beating = False
464 self._beating = False
464 self.iopub_stream = None
465 self.iopub_stream = None
465 self.hb_stream = None
466 self.hb_stream = None
466
467
467 def on_first_message(self, msg):
468 def on_first_message(self, msg):
468 try:
469 try:
469 super(IOPubHandler, self).on_first_message(msg)
470 super(IOPubHandler, self).on_first_message(msg)
470 except web.HTTPError:
471 except web.HTTPError:
471 self.close()
472 self.close()
472 return
473 return
473 km = self.application.kernel_manager
474 km = self.application.kernel_manager
474 self.time_to_dead = km.time_to_dead
475 self.time_to_dead = km.time_to_dead
475 self.first_beat = km.first_beat
476 self.first_beat = km.first_beat
476 kernel_id = self.kernel_id
477 kernel_id = self.kernel_id
477 try:
478 try:
478 self.iopub_stream = km.create_iopub_stream(kernel_id)
479 self.iopub_stream = km.create_iopub_stream(kernel_id)
479 self.hb_stream = km.create_hb_stream(kernel_id)
480 self.hb_stream = km.create_hb_stream(kernel_id)
480 except web.HTTPError:
481 except web.HTTPError:
481 # WebSockets don't response to traditional error codes so we
482 # WebSockets don't response to traditional error codes so we
482 # close the connection.
483 # close the connection.
483 if not self.stream.closed():
484 if not self.stream.closed():
484 self.stream.close()
485 self.stream.close()
485 self.close()
486 self.close()
486 else:
487 else:
487 self.iopub_stream.on_recv(self._on_zmq_reply)
488 self.iopub_stream.on_recv(self._on_zmq_reply)
488 self.start_hb(self.kernel_died)
489 self.start_hb(self.kernel_died)
489
490
490 def on_message(self, msg):
491 def on_message(self, msg):
491 pass
492 pass
492
493
493 def on_close(self):
494 def on_close(self):
494 # This method can be called twice, once by self.kernel_died and once
495 # This method can be called twice, once by self.kernel_died and once
495 # from the WebSocket close event. If the WebSocket connection is
496 # from the WebSocket close event. If the WebSocket connection is
496 # closed before the ZMQ streams are setup, they could be None.
497 # closed before the ZMQ streams are setup, they could be None.
497 self.stop_hb()
498 self.stop_hb()
498 if self.iopub_stream is not None and not self.iopub_stream.closed():
499 if self.iopub_stream is not None and not self.iopub_stream.closed():
499 self.iopub_stream.on_recv(None)
500 self.iopub_stream.on_recv(None)
500 self.iopub_stream.close()
501 self.iopub_stream.close()
501 if self.hb_stream is not None and not self.hb_stream.closed():
502 if self.hb_stream is not None and not self.hb_stream.closed():
502 self.hb_stream.close()
503 self.hb_stream.close()
503
504
504 def start_hb(self, callback):
505 def start_hb(self, callback):
505 """Start the heartbeating and call the callback if the kernel dies."""
506 """Start the heartbeating and call the callback if the kernel dies."""
506 if not self._beating:
507 if not self._beating:
507 self._kernel_alive = True
508 self._kernel_alive = True
508
509
509 def ping_or_dead():
510 def ping_or_dead():
510 self.hb_stream.flush()
511 self.hb_stream.flush()
511 if self._kernel_alive:
512 if self._kernel_alive:
512 self._kernel_alive = False
513 self._kernel_alive = False
513 self.hb_stream.send(b'ping')
514 self.hb_stream.send(b'ping')
514 # flush stream to force immediate socket send
515 # flush stream to force immediate socket send
515 self.hb_stream.flush()
516 self.hb_stream.flush()
516 else:
517 else:
517 try:
518 try:
518 callback()
519 callback()
519 except:
520 except:
520 pass
521 pass
521 finally:
522 finally:
522 self.stop_hb()
523 self.stop_hb()
523
524
524 def beat_received(msg):
525 def beat_received(msg):
525 self._kernel_alive = True
526 self._kernel_alive = True
526
527
527 self.hb_stream.on_recv(beat_received)
528 self.hb_stream.on_recv(beat_received)
528 loop = ioloop.IOLoop.instance()
529 loop = ioloop.IOLoop.instance()
529 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
530 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
530 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
531 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
531 self._beating= True
532 self._beating= True
532
533
533 def _really_start_hb(self):
534 def _really_start_hb(self):
534 """callback for delayed heartbeat start
535 """callback for delayed heartbeat start
535
536
536 Only start the hb loop if we haven't been closed during the wait.
537 Only start the hb loop if we haven't been closed during the wait.
537 """
538 """
538 if self._beating and not self.hb_stream.closed():
539 if self._beating and not self.hb_stream.closed():
539 self._hb_periodic_callback.start()
540 self._hb_periodic_callback.start()
540
541
541 def stop_hb(self):
542 def stop_hb(self):
542 """Stop the heartbeating and cancel all related callbacks."""
543 """Stop the heartbeating and cancel all related callbacks."""
543 if self._beating:
544 if self._beating:
544 self._beating = False
545 self._beating = False
545 self._hb_periodic_callback.stop()
546 self._hb_periodic_callback.stop()
546 if not self.hb_stream.closed():
547 if not self.hb_stream.closed():
547 self.hb_stream.on_recv(None)
548 self.hb_stream.on_recv(None)
548
549
549 def _delete_kernel_data(self):
550 def _delete_kernel_data(self):
550 """Remove the kernel data and notebook mapping."""
551 """Remove the kernel data and notebook mapping."""
551 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
552 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
552
553
553 def kernel_died(self):
554 def kernel_died(self):
554 self._delete_kernel_data()
555 self._delete_kernel_data()
555 self.application.log.error("Kernel died: %s" % self.kernel_id)
556 self.application.log.error("Kernel died: %s" % self.kernel_id)
556 self.write_message(
557 self.write_message(
557 {'header': {'msg_type': 'status'},
558 {'header': {'msg_type': 'status'},
558 'parent_header': {},
559 'parent_header': {},
559 'content': {'execution_state':'dead'}
560 'content': {'execution_state':'dead'}
560 }
561 }
561 )
562 )
562 self.on_close()
563 self.on_close()
563
564
564
565
565 class ShellHandler(AuthenticatedZMQStreamHandler):
566 class ShellHandler(AuthenticatedZMQStreamHandler):
566
567
567 def initialize(self, *args, **kwargs):
568 def initialize(self, *args, **kwargs):
568 self.shell_stream = None
569 self.shell_stream = None
569
570
570 def on_first_message(self, msg):
571 def on_first_message(self, msg):
571 try:
572 try:
572 super(ShellHandler, self).on_first_message(msg)
573 super(ShellHandler, self).on_first_message(msg)
573 except web.HTTPError:
574 except web.HTTPError:
574 self.close()
575 self.close()
575 return
576 return
576 km = self.application.kernel_manager
577 km = self.application.kernel_manager
577 self.max_msg_size = km.max_msg_size
578 self.max_msg_size = km.max_msg_size
578 kernel_id = self.kernel_id
579 kernel_id = self.kernel_id
579 try:
580 try:
580 self.shell_stream = km.create_shell_stream(kernel_id)
581 self.shell_stream = km.create_shell_stream(kernel_id)
581 except web.HTTPError:
582 except web.HTTPError:
582 # WebSockets don't response to traditional error codes so we
583 # WebSockets don't response to traditional error codes so we
583 # close the connection.
584 # close the connection.
584 if not self.stream.closed():
585 if not self.stream.closed():
585 self.stream.close()
586 self.stream.close()
586 self.close()
587 self.close()
587 else:
588 else:
588 self.shell_stream.on_recv(self._on_zmq_reply)
589 self.shell_stream.on_recv(self._on_zmq_reply)
589
590
590 def on_message(self, msg):
591 def on_message(self, msg):
591 if len(msg) < self.max_msg_size:
592 if len(msg) < self.max_msg_size:
592 msg = jsonapi.loads(msg)
593 msg = jsonapi.loads(msg)
593 self.session.send(self.shell_stream, msg)
594 self.session.send(self.shell_stream, msg)
594
595
595 def on_close(self):
596 def on_close(self):
596 # Make sure the stream exists and is not already closed.
597 # Make sure the stream exists and is not already closed.
597 if self.shell_stream is not None and not self.shell_stream.closed():
598 if self.shell_stream is not None and not self.shell_stream.closed():
598 self.shell_stream.close()
599 self.shell_stream.close()
599
600
600
601
601 #-----------------------------------------------------------------------------
602 #-----------------------------------------------------------------------------
602 # Notebook web service handlers
603 # Notebook web service handlers
603 #-----------------------------------------------------------------------------
604 #-----------------------------------------------------------------------------
604
605
605 class NotebookRootHandler(AuthenticatedHandler):
606 class NotebookRootHandler(AuthenticatedHandler):
606
607
607 @authenticate_unless_readonly
608 @authenticate_unless_readonly
608 def get(self):
609 def get(self):
609 nbm = self.application.notebook_manager
610 nbm = self.application.notebook_manager
610 km = self.application.kernel_manager
611 km = self.application.kernel_manager
611 files = nbm.list_notebooks()
612 files = nbm.list_notebooks()
612 for f in files :
613 for f in files :
613 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
614 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
614 self.finish(jsonapi.dumps(files))
615 self.finish(jsonapi.dumps(files))
615
616
616 @web.authenticated
617 @web.authenticated
617 def post(self):
618 def post(self):
618 nbm = self.application.notebook_manager
619 nbm = self.application.notebook_manager
619 body = self.request.body.strip()
620 body = self.request.body.strip()
620 format = self.get_argument('format', default='json')
621 format = self.get_argument('format', default='json')
621 name = self.get_argument('name', default=None)
622 name = self.get_argument('name', default=None)
622 if body:
623 if body:
623 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
624 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
624 else:
625 else:
625 notebook_id = nbm.new_notebook()
626 notebook_id = nbm.new_notebook()
626 self.set_header('Location', '/'+notebook_id)
627 self.set_header('Location', '/'+notebook_id)
627 self.finish(jsonapi.dumps(notebook_id))
628 self.finish(jsonapi.dumps(notebook_id))
628
629
629
630
630 class NotebookHandler(AuthenticatedHandler):
631 class NotebookHandler(AuthenticatedHandler):
631
632
632 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
633 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
633
634
634 @authenticate_unless_readonly
635 @authenticate_unless_readonly
635 def get(self, notebook_id):
636 def get(self, notebook_id):
636 nbm = self.application.notebook_manager
637 nbm = self.application.notebook_manager
637 format = self.get_argument('format', default='json')
638 format = self.get_argument('format', default='json')
638 last_mod, name, data = nbm.get_notebook(notebook_id, format)
639 last_mod, name, data = nbm.get_notebook(notebook_id, format)
639
640
640 if format == u'json':
641 if format == u'json':
641 self.set_header('Content-Type', 'application/json')
642 self.set_header('Content-Type', 'application/json')
642 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
643 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
643 elif format == u'py':
644 elif format == u'py':
644 self.set_header('Content-Type', 'application/x-python')
645 self.set_header('Content-Type', 'application/x-python')
645 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
646 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
646 self.set_header('Last-Modified', last_mod)
647 self.set_header('Last-Modified', last_mod)
647 self.finish(data)
648 self.finish(data)
648
649
649 @web.authenticated
650 @web.authenticated
650 def put(self, notebook_id):
651 def put(self, notebook_id):
651 nbm = self.application.notebook_manager
652 nbm = self.application.notebook_manager
652 format = self.get_argument('format', default='json')
653 format = self.get_argument('format', default='json')
653 name = self.get_argument('name', default=None)
654 name = self.get_argument('name', default=None)
654 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
655 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
655 self.set_status(204)
656 self.set_status(204)
656 self.finish()
657 self.finish()
657
658
658 @web.authenticated
659 @web.authenticated
659 def delete(self, notebook_id):
660 def delete(self, notebook_id):
660 nbm = self.application.notebook_manager
661 nbm = self.application.notebook_manager
661 nbm.delete_notebook(notebook_id)
662 nbm.delete_notebook(notebook_id)
662 self.set_status(204)
663 self.set_status(204)
663 self.finish()
664 self.finish()
664
665
665
666
666 class NotebookCopyHandler(AuthenticatedHandler):
667 class NotebookCopyHandler(AuthenticatedHandler):
667
668
668 @web.authenticated
669 @web.authenticated
669 def get(self, notebook_id):
670 def get(self, notebook_id):
670 nbm = self.application.notebook_manager
671 nbm = self.application.notebook_manager
671 project = nbm.notebook_dir
672 project = nbm.notebook_dir
672 notebook_id = nbm.copy_notebook(notebook_id)
673 notebook_id = nbm.copy_notebook(notebook_id)
673 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
674 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
674
675
675
676
676 #-----------------------------------------------------------------------------
677 #-----------------------------------------------------------------------------
677 # Cluster handlers
678 # Cluster handlers
678 #-----------------------------------------------------------------------------
679 #-----------------------------------------------------------------------------
679
680
680
681
681 class MainClusterHandler(AuthenticatedHandler):
682 class MainClusterHandler(AuthenticatedHandler):
682
683
683 @web.authenticated
684 @web.authenticated
684 def get(self):
685 def get(self):
685 cm = self.application.cluster_manager
686 cm = self.application.cluster_manager
686 self.finish(jsonapi.dumps(cm.list_profiles()))
687 self.finish(jsonapi.dumps(cm.list_profiles()))
687
688
688
689
689 class ClusterProfileHandler(AuthenticatedHandler):
690 class ClusterProfileHandler(AuthenticatedHandler):
690
691
691 @web.authenticated
692 @web.authenticated
692 def get(self, profile):
693 def get(self, profile):
693 cm = self.application.cluster_manager
694 cm = self.application.cluster_manager
694 self.finish(jsonapi.dumps(cm.profile_info(profile)))
695 self.finish(jsonapi.dumps(cm.profile_info(profile)))
695
696
696
697
697 class ClusterActionHandler(AuthenticatedHandler):
698 class ClusterActionHandler(AuthenticatedHandler):
698
699
699 @web.authenticated
700 @web.authenticated
700 def post(self, profile, action):
701 def post(self, profile, action):
701 cm = self.application.cluster_manager
702 cm = self.application.cluster_manager
702 if action == 'start':
703 if action == 'start':
703 n = self.get_argument('n',default=None)
704 n = self.get_argument('n',default=None)
704 if n is None:
705 if n is None:
705 data = cm.start_cluster(profile)
706 data = cm.start_cluster(profile)
706 else:
707 else:
707 data = cm.start_cluster(profile,int(n))
708 data = cm.start_cluster(profile,int(n))
708 if action == 'stop':
709 if action == 'stop':
709 data = cm.stop_cluster(profile)
710 data = cm.stop_cluster(profile)
710 self.finish(jsonapi.dumps(data))
711 self.finish(jsonapi.dumps(data))
711
712
712
713
713 #-----------------------------------------------------------------------------
714 #-----------------------------------------------------------------------------
714 # RST web service handlers
715 # RST web service handlers
715 #-----------------------------------------------------------------------------
716 #-----------------------------------------------------------------------------
716
717
717
718
718 class RSTHandler(AuthenticatedHandler):
719 class RSTHandler(AuthenticatedHandler):
719
720
720 @web.authenticated
721 @web.authenticated
721 def post(self):
722 def post(self):
722 if publish_string is None:
723 if publish_string is None:
723 raise web.HTTPError(503, u'docutils not available')
724 raise web.HTTPError(503, u'docutils not available')
724 body = self.request.body.strip()
725 body = self.request.body.strip()
725 source = body
726 source = body
726 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
727 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
727 defaults = {'file_insertion_enabled': 0,
728 defaults = {'file_insertion_enabled': 0,
728 'raw_enabled': 0,
729 'raw_enabled': 0,
729 '_disable_config': 1,
730 '_disable_config': 1,
730 'stylesheet_path': 0
731 'stylesheet_path': 0
731 # 'template': template_path
732 # 'template': template_path
732 }
733 }
733 try:
734 try:
734 html = publish_string(source, writer_name='html',
735 html = publish_string(source, writer_name='html',
735 settings_overrides=defaults
736 settings_overrides=defaults
736 )
737 )
737 except:
738 except:
738 raise web.HTTPError(400, u'Invalid RST')
739 raise web.HTTPError(400, u'Invalid RST')
739 print html
740 print html
740 self.set_header('Content-Type', 'text/html')
741 self.set_header('Content-Type', 'text/html')
741 self.finish(html)
742 self.finish(html)
742
743
743 # to minimize subclass changes:
744 # to minimize subclass changes:
744 HTTPError = web.HTTPError
745 HTTPError = web.HTTPError
745
746
746 class FileFindHandler(web.StaticFileHandler):
747 class FileFindHandler(web.StaticFileHandler):
747 """subclass of StaticFileHandler for serving files from a search path"""
748 """subclass of StaticFileHandler for serving files from a search path"""
748
749
749 _static_paths = {}
750 _static_paths = {}
750 # _lock is needed for tornado < 2.2.0 compat
751 # _lock is needed for tornado < 2.2.0 compat
751 _lock = threading.Lock() # protects _static_hashes
752 _lock = threading.Lock() # protects _static_hashes
752
753
753 def initialize(self, path, default_filename=None):
754 def initialize(self, path, default_filename=None):
754 if isinstance(path, basestring):
755 if isinstance(path, basestring):
755 path = [path]
756 path = [path]
756 self.roots = tuple(
757 self.roots = tuple(
757 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
758 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
758 )
759 )
759 self.default_filename = default_filename
760 self.default_filename = default_filename
760
761
761 @classmethod
762 @classmethod
762 def locate_file(cls, path, roots):
763 def locate_file(cls, path, roots):
763 """locate a file to serve on our static file search path"""
764 """locate a file to serve on our static file search path"""
764 with cls._lock:
765 with cls._lock:
765 if path in cls._static_paths:
766 if path in cls._static_paths:
766 return cls._static_paths[path]
767 return cls._static_paths[path]
767 try:
768 try:
768 abspath = os.path.abspath(filefind(path, roots))
769 abspath = os.path.abspath(filefind(path, roots))
769 except IOError:
770 except IOError:
770 # empty string should always give exists=False
771 # empty string should always give exists=False
771 return ''
772 return ''
772
773
773 # os.path.abspath strips a trailing /
774 # os.path.abspath strips a trailing /
774 # it needs to be temporarily added back for requests to root/
775 # it needs to be temporarily added back for requests to root/
775 if not (abspath + os.path.sep).startswith(roots):
776 if not (abspath + os.path.sep).startswith(roots):
776 raise HTTPError(403, "%s is not in root static directory", path)
777 raise HTTPError(403, "%s is not in root static directory", path)
777
778
778 cls._static_paths[path] = abspath
779 cls._static_paths[path] = abspath
779 return abspath
780 return abspath
780
781
781 def get(self, path, include_body=True):
782 def get(self, path, include_body=True):
782 path = self.parse_url_path(path)
783 path = self.parse_url_path(path)
783
784
784 # begin subclass override
785 # begin subclass override
785 abspath = self.locate_file(path, self.roots)
786 abspath = self.locate_file(path, self.roots)
786 # end subclass override
787 # end subclass override
787
788
788 if os.path.isdir(abspath) and self.default_filename is not None:
789 if os.path.isdir(abspath) and self.default_filename is not None:
789 # need to look at the request.path here for when path is empty
790 # need to look at the request.path here for when path is empty
790 # but there is some prefix to the path that was already
791 # but there is some prefix to the path that was already
791 # trimmed by the routing
792 # trimmed by the routing
792 if not self.request.path.endswith("/"):
793 if not self.request.path.endswith("/"):
793 self.redirect(self.request.path + "/")
794 self.redirect(self.request.path + "/")
794 return
795 return
795 abspath = os.path.join(abspath, self.default_filename)
796 abspath = os.path.join(abspath, self.default_filename)
796 if not os.path.exists(abspath):
797 if not os.path.exists(abspath):
797 raise HTTPError(404)
798 raise HTTPError(404)
798 if not os.path.isfile(abspath):
799 if not os.path.isfile(abspath):
799 raise HTTPError(403, "%s is not a file", path)
800 raise HTTPError(403, "%s is not a file", path)
800
801
801 stat_result = os.stat(abspath)
802 stat_result = os.stat(abspath)
802 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
803 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
803
804
804 self.set_header("Last-Modified", modified)
805 self.set_header("Last-Modified", modified)
805
806
806 mime_type, encoding = mimetypes.guess_type(abspath)
807 mime_type, encoding = mimetypes.guess_type(abspath)
807 if mime_type:
808 if mime_type:
808 self.set_header("Content-Type", mime_type)
809 self.set_header("Content-Type", mime_type)
809
810
810 cache_time = self.get_cache_time(path, modified, mime_type)
811 cache_time = self.get_cache_time(path, modified, mime_type)
811
812
812 if cache_time > 0:
813 if cache_time > 0:
813 self.set_header("Expires", datetime.datetime.utcnow() + \
814 self.set_header("Expires", datetime.datetime.utcnow() + \
814 datetime.timedelta(seconds=cache_time))
815 datetime.timedelta(seconds=cache_time))
815 self.set_header("Cache-Control", "max-age=" + str(cache_time))
816 self.set_header("Cache-Control", "max-age=" + str(cache_time))
816 else:
817 else:
817 self.set_header("Cache-Control", "public")
818 self.set_header("Cache-Control", "public")
818
819
819 self.set_extra_headers(path)
820 self.set_extra_headers(path)
820
821
821 # Check the If-Modified-Since, and don't send the result if the
822 # Check the If-Modified-Since, and don't send the result if the
822 # content has not been modified
823 # content has not been modified
823 ims_value = self.request.headers.get("If-Modified-Since")
824 ims_value = self.request.headers.get("If-Modified-Since")
824 if ims_value is not None:
825 if ims_value is not None:
825 date_tuple = email.utils.parsedate(ims_value)
826 date_tuple = email.utils.parsedate(ims_value)
826 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
827 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
827 if if_since >= modified:
828 if if_since >= modified:
828 self.set_status(304)
829 self.set_status(304)
829 return
830 return
830
831
831 with open(abspath, "rb") as file:
832 with open(abspath, "rb") as file:
832 data = file.read()
833 data = file.read()
833 hasher = hashlib.sha1()
834 hasher = hashlib.sha1()
834 hasher.update(data)
835 hasher.update(data)
835 self.set_header("Etag", '"%s"' % hasher.hexdigest())
836 self.set_header("Etag", '"%s"' % hasher.hexdigest())
836 if include_body:
837 if include_body:
837 self.write(data)
838 self.write(data)
838 else:
839 else:
839 assert self.request.method == "HEAD"
840 assert self.request.method == "HEAD"
840 self.set_header("Content-Length", len(data))
841 self.set_header("Content-Length", len(data))
841
842
842 @classmethod
843 @classmethod
843 def get_version(cls, settings, path):
844 def get_version(cls, settings, path):
844 """Generate the version string to be used in static URLs.
845 """Generate the version string to be used in static URLs.
845
846
846 This method may be overridden in subclasses (but note that it
847 This method may be overridden in subclasses (but note that it
847 is a class method rather than a static method). The default
848 is a class method rather than a static method). The default
848 implementation uses a hash of the file's contents.
849 implementation uses a hash of the file's contents.
849
850
850 ``settings`` is the `Application.settings` dictionary and ``path``
851 ``settings`` is the `Application.settings` dictionary and ``path``
851 is the relative location of the requested asset on the filesystem.
852 is the relative location of the requested asset on the filesystem.
852 The returned value should be a string, or ``None`` if no version
853 The returned value should be a string, or ``None`` if no version
853 could be determined.
854 could be determined.
854 """
855 """
855 # begin subclass override:
856 # begin subclass override:
856 static_paths = settings['static_path']
857 static_paths = settings['static_path']
857 if isinstance(static_paths, basestring):
858 if isinstance(static_paths, basestring):
858 static_paths = [static_paths]
859 static_paths = [static_paths]
859 roots = tuple(
860 roots = tuple(
860 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
861 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
861 )
862 )
862
863
863 try:
864 try:
864 abs_path = filefind(path, roots)
865 abs_path = filefind(path, roots)
865 except IOError:
866 except IOError:
866 logging.error("Could not find static file %r", path)
867 logging.error("Could not find static file %r", path)
867 return None
868 return None
868
869
869 # end subclass override
870 # end subclass override
870
871
871 with cls._lock:
872 with cls._lock:
872 hashes = cls._static_hashes
873 hashes = cls._static_hashes
873 if abs_path not in hashes:
874 if abs_path not in hashes:
874 try:
875 try:
875 f = open(abs_path, "rb")
876 f = open(abs_path, "rb")
876 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
877 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
877 f.close()
878 f.close()
878 except Exception:
879 except Exception:
879 logging.error("Could not open static file %r", path)
880 logging.error("Could not open static file %r", path)
880 hashes[abs_path] = None
881 hashes[abs_path] = None
881 hsh = hashes.get(abs_path)
882 hsh = hashes.get(abs_path)
882 if hsh:
883 if hsh:
883 return hsh[:5]
884 return hsh[:5]
884 return None
885 return None
885
886
886
887
887 def parse_url_path(self, url_path):
888 def parse_url_path(self, url_path):
888 """Converts a static URL path into a filesystem path.
889 """Converts a static URL path into a filesystem path.
889
890
890 ``url_path`` is the path component of the URL with
891 ``url_path`` is the path component of the URL with
891 ``static_url_prefix`` removed. The return value should be
892 ``static_url_prefix`` removed. The return value should be
892 filesystem path relative to ``static_path``.
893 filesystem path relative to ``static_path``.
893 """
894 """
894 if os.path.sep != "/":
895 if os.path.sep != "/":
895 url_path = url_path.replace("/", os.path.sep)
896 url_path = url_path.replace("/", os.path.sep)
896 return url_path
897 return url_path
897
898
898
899
@@ -1,653 +1,654 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import uuid
31 import uuid
32 import webbrowser
32 import webbrowser
33
33
34 # Third party
34 # Third party
35 import zmq
35 import zmq
36 from jinja2 import Environment, FileSystemLoader
36 from jinja2 import Environment, FileSystemLoader
37
37
38 # Install the pyzmq ioloop. This has to be done before anything else from
38 # Install the pyzmq ioloop. This has to be done before anything else from
39 # tornado is imported.
39 # tornado is imported.
40 from zmq.eventloop import ioloop
40 from zmq.eventloop import ioloop
41 ioloop.install()
41 ioloop.install()
42
42
43 from tornado import httpserver
43 from tornado import httpserver
44 from tornado import web
44 from tornado import web
45
45
46 # Our own libraries
46 # Our own libraries
47 from .kernelmanager import MappingKernelManager
47 from .kernelmanager import MappingKernelManager
48 from .handlers import (LoginHandler, LogoutHandler,
48 from .handlers import (LoginHandler, LogoutHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
54 FileFindHandler,
54 FileFindHandler,
55 )
55 )
56 from .nbmanager import NotebookManager
56 from .nbmanager import NotebookManager
57 from .filenbmanager import FileNotebookManager
57 from .filenbmanager import FileNotebookManager
58 from .clustermanager import ClusterManager
58 from .clustermanager import ClusterManager
59
59
60 from IPython.config.application import catch_config_error, boolean_flag
60 from IPython.config.application import catch_config_error, boolean_flag
61 from IPython.core.application import BaseIPythonApplication
61 from IPython.core.application import BaseIPythonApplication
62 from IPython.core.profiledir import ProfileDir
62 from IPython.core.profiledir import ProfileDir
63 from IPython.frontend.consoleapp import IPythonConsoleApp
63 from IPython.frontend.consoleapp import IPythonConsoleApp
64 from IPython.lib.kernel import swallow_argv
64 from IPython.lib.kernel import swallow_argv
65 from IPython.zmq.session import Session, default_secure
65 from IPython.zmq.session import Session, default_secure
66 from IPython.zmq.zmqshell import ZMQInteractiveShell
66 from IPython.zmq.zmqshell import ZMQInteractiveShell
67 from IPython.zmq.ipkernel import (
67 from IPython.zmq.ipkernel import (
68 flags as ipkernel_flags,
68 flags as ipkernel_flags,
69 aliases as ipkernel_aliases,
69 aliases as ipkernel_aliases,
70 IPKernelApp
70 IPKernelApp
71 )
71 )
72 from IPython.utils.importstring import import_item
72 from IPython.utils.importstring import import_item
73 from IPython.utils.traitlets import (
73 from IPython.utils.traitlets import (
74 Dict, Unicode, Integer, List, Enum, Bool,
74 Dict, Unicode, Integer, List, Enum, Bool,
75 DottedObjectName
75 DottedObjectName
76 )
76 )
77 from IPython.utils import py3compat
77 from IPython.utils import py3compat
78 from IPython.utils.path import filefind
78 from IPython.utils.path import filefind
79
79
80 #-----------------------------------------------------------------------------
80 #-----------------------------------------------------------------------------
81 # Module globals
81 # Module globals
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83
83
84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
84 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
85 _kernel_action_regex = r"(?P<action>restart|interrupt)"
86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
86 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
87 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
88 _cluster_action_regex = r"(?P<action>start|stop)"
88 _cluster_action_regex = r"(?P<action>start|stop)"
89
89
90
90
91 LOCALHOST = '127.0.0.1'
91 LOCALHOST = '127.0.0.1'
92
92
93 _examples = """
93 _examples = """
94 ipython notebook # start the notebook
94 ipython notebook # start the notebook
95 ipython notebook --profile=sympy # use the sympy profile
95 ipython notebook --profile=sympy # use the sympy profile
96 ipython notebook --pylab=inline # pylab in inline plotting mode
96 ipython notebook --pylab=inline # pylab in inline plotting mode
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
99 """
99 """
100
100
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102 # Helper functions
102 # Helper functions
103 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
104
104
105 def url_path_join(a,b):
105 def url_path_join(a,b):
106 if a.endswith('/') and b.startswith('/'):
106 if a.endswith('/') and b.startswith('/'):
107 return a[:-1]+b
107 return a[:-1]+b
108 else:
108 else:
109 return a+b
109 return a+b
110
110
111 def random_ports(port, n):
111 def random_ports(port, n):
112 """Generate a list of n random ports near the given port.
112 """Generate a list of n random ports near the given port.
113
113
114 The first 5 ports will be sequential, and the remaining n-5 will be
114 The first 5 ports will be sequential, and the remaining n-5 will be
115 randomly selected in the range [port-2*n, port+2*n].
115 randomly selected in the range [port-2*n, port+2*n].
116 """
116 """
117 for i in range(min(5, n)):
117 for i in range(min(5, n)):
118 yield port + i
118 yield port + i
119 for i in range(n-5):
119 for i in range(n-5):
120 yield port + random.randint(-2*n, 2*n)
120 yield port + random.randint(-2*n, 2*n)
121
121
122 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
123 # The Tornado web application
123 # The Tornado web application
124 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
125
125
126 class NotebookWebApplication(web.Application):
126 class NotebookWebApplication(web.Application):
127
127
128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
128 def __init__(self, ipython_app, kernel_manager, notebook_manager,
129 cluster_manager, log,
129 cluster_manager, log,
130 base_project_url, settings_overrides):
130 base_project_url, settings_overrides):
131 handlers = [
131 handlers = [
132 (r"/", ProjectDashboardHandler),
132 (r"/", ProjectDashboardHandler),
133 (r"/login", LoginHandler),
133 (r"/login", LoginHandler),
134 (r"/logout", LogoutHandler),
134 (r"/logout", LogoutHandler),
135 (r"/new", NewHandler),
135 (r"/new", NewHandler),
136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
136 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
137 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
138 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
139 (r"/kernels", MainKernelHandler),
139 (r"/kernels", MainKernelHandler),
140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
140 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
141 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
142 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
143 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
144 (r"/notebooks", NotebookRootHandler),
144 (r"/notebooks", NotebookRootHandler),
145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
145 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
146 (r"/rstservice/render", RSTHandler),
146 (r"/rstservice/render", RSTHandler),
147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
147 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
148 (r"/clusters", MainClusterHandler),
148 (r"/clusters", MainClusterHandler),
149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
149 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
150 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
151 ]
151 ]
152
152
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 # base_project_url will always be unicode, which will in turn
154 # base_project_url will always be unicode, which will in turn
155 # make the patterns unicode, and ultimately result in unicode
155 # make the patterns unicode, and ultimately result in unicode
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 # This enforces that base_project_url be ascii in that situation.
157 # This enforces that base_project_url be ascii in that situation.
158 #
158 #
159 # Note that the URLs these patterns check against are escaped,
159 # Note that the URLs these patterns check against are escaped,
160 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
160 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162
162
163 settings = dict(
163 settings = dict(
164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
164 template_path=os.path.join(os.path.dirname(__file__), "templates"),
165 static_path=ipython_app.static_file_path,
165 static_path=ipython_app.static_file_path,
166 static_handler_class = FileFindHandler,
166 static_handler_class = FileFindHandler,
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
168 cookie_secret=os.urandom(1024),
168 cookie_secret=os.urandom(1024),
169 login_url=url_path_join(base_project_url,'/login'),
169 login_url=url_path_join(base_project_url,'/login'),
170 cookie_name='username-%s' % uuid.uuid4(),
170 cookie_name='username-%s' % uuid.uuid4(),
171 )
171 )
172
172
173 # allow custom overrides for the tornado web app.
173 # allow custom overrides for the tornado web app.
174 settings.update(settings_overrides)
174 settings.update(settings_overrides)
175
175
176 # prepend base_project_url onto the patterns that we match
176 # prepend base_project_url onto the patterns that we match
177 new_handlers = []
177 new_handlers = []
178 for handler in handlers:
178 for handler in handlers:
179 pattern = url_path_join(base_project_url, handler[0])
179 pattern = url_path_join(base_project_url, handler[0])
180 new_handler = tuple([pattern]+list(handler[1:]))
180 new_handler = tuple([pattern]+list(handler[1:]))
181 new_handlers.append( new_handler )
181 new_handlers.append( new_handler )
182
182
183 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
183 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
184
184
185 self.kernel_manager = kernel_manager
185 self.kernel_manager = kernel_manager
186 self.notebook_manager = notebook_manager
186 self.notebook_manager = notebook_manager
187 self.cluster_manager = cluster_manager
187 self.cluster_manager = cluster_manager
188 self.ipython_app = ipython_app
188 self.ipython_app = ipython_app
189 self.read_only = self.ipython_app.read_only
189 self.read_only = self.ipython_app.read_only
190 self.config = self.ipython_app.config
190 self.config = self.ipython_app.config
191 self.use_less = self.ipython_app.use_less
191 self.log = log
192 self.log = log
192 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
193 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
193
194
194
195
195
196
196 #-----------------------------------------------------------------------------
197 #-----------------------------------------------------------------------------
197 # Aliases and Flags
198 # Aliases and Flags
198 #-----------------------------------------------------------------------------
199 #-----------------------------------------------------------------------------
199
200
200 flags = dict(ipkernel_flags)
201 flags = dict(ipkernel_flags)
201 flags['no-browser']=(
202 flags['no-browser']=(
202 {'NotebookApp' : {'open_browser' : False}},
203 {'NotebookApp' : {'open_browser' : False}},
203 "Don't open the notebook in a browser after startup."
204 "Don't open the notebook in a browser after startup."
204 )
205 )
205 flags['no-mathjax']=(
206 flags['no-mathjax']=(
206 {'NotebookApp' : {'enable_mathjax' : False}},
207 {'NotebookApp' : {'enable_mathjax' : False}},
207 """Disable MathJax
208 """Disable MathJax
208
209
209 MathJax is the javascript library IPython uses to render math/LaTeX. It is
210 MathJax is the javascript library IPython uses to render math/LaTeX. It is
210 very large, so you may want to disable it if you have a slow internet
211 very large, so you may want to disable it if you have a slow internet
211 connection, or for offline use of the notebook.
212 connection, or for offline use of the notebook.
212
213
213 When disabled, equations etc. will appear as their untransformed TeX source.
214 When disabled, equations etc. will appear as their untransformed TeX source.
214 """
215 """
215 )
216 )
216 flags['read-only'] = (
217 flags['read-only'] = (
217 {'NotebookApp' : {'read_only' : True}},
218 {'NotebookApp' : {'read_only' : True}},
218 """Allow read-only access to notebooks.
219 """Allow read-only access to notebooks.
219
220
220 When using a password to protect the notebook server, this flag
221 When using a password to protect the notebook server, this flag
221 allows unauthenticated clients to view the notebook list, and
222 allows unauthenticated clients to view the notebook list, and
222 individual notebooks, but not edit them, start kernels, or run
223 individual notebooks, but not edit them, start kernels, or run
223 code.
224 code.
224
225
225 If no password is set, the server will be entirely read-only.
226 If no password is set, the server will be entirely read-only.
226 """
227 """
227 )
228 )
228
229
229 # Add notebook manager flags
230 # Add notebook manager flags
230 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
231 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
231 'Auto-save a .py script everytime the .ipynb notebook is saved',
232 'Auto-save a .py script everytime the .ipynb notebook is saved',
232 'Do not auto-save .py scripts for every notebook'))
233 'Do not auto-save .py scripts for every notebook'))
233
234
234 # the flags that are specific to the frontend
235 # the flags that are specific to the frontend
235 # these must be scrubbed before being passed to the kernel,
236 # these must be scrubbed before being passed to the kernel,
236 # or it will raise an error on unrecognized flags
237 # or it will raise an error on unrecognized flags
237 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
238 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
238
239
239 aliases = dict(ipkernel_aliases)
240 aliases = dict(ipkernel_aliases)
240
241
241 aliases.update({
242 aliases.update({
242 'ip': 'NotebookApp.ip',
243 'ip': 'NotebookApp.ip',
243 'port': 'NotebookApp.port',
244 'port': 'NotebookApp.port',
244 'port-retries': 'NotebookApp.port_retries',
245 'port-retries': 'NotebookApp.port_retries',
245 'transport': 'KernelManager.transport',
246 'transport': 'KernelManager.transport',
246 'keyfile': 'NotebookApp.keyfile',
247 'keyfile': 'NotebookApp.keyfile',
247 'certfile': 'NotebookApp.certfile',
248 'certfile': 'NotebookApp.certfile',
248 'notebook-dir': 'NotebookManager.notebook_dir',
249 'notebook-dir': 'NotebookManager.notebook_dir',
249 'browser': 'NotebookApp.browser',
250 'browser': 'NotebookApp.browser',
250 })
251 })
251
252
252 # remove ipkernel flags that are singletons, and don't make sense in
253 # remove ipkernel flags that are singletons, and don't make sense in
253 # multi-kernel evironment:
254 # multi-kernel evironment:
254 aliases.pop('f', None)
255 aliases.pop('f', None)
255
256
256 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
257 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
257 u'notebook-dir']
258 u'notebook-dir']
258
259
259 #-----------------------------------------------------------------------------
260 #-----------------------------------------------------------------------------
260 # NotebookApp
261 # NotebookApp
261 #-----------------------------------------------------------------------------
262 #-----------------------------------------------------------------------------
262
263
263 class NotebookApp(BaseIPythonApplication):
264 class NotebookApp(BaseIPythonApplication):
264
265
265 name = 'ipython-notebook'
266 name = 'ipython-notebook'
266 default_config_file_name='ipython_notebook_config.py'
267 default_config_file_name='ipython_notebook_config.py'
267
268
268 description = """
269 description = """
269 The IPython HTML Notebook.
270 The IPython HTML Notebook.
270
271
271 This launches a Tornado based HTML Notebook Server that serves up an
272 This launches a Tornado based HTML Notebook Server that serves up an
272 HTML5/Javascript Notebook client.
273 HTML5/Javascript Notebook client.
273 """
274 """
274 examples = _examples
275 examples = _examples
275
276
276 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
277 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
277 FileNotebookManager]
278 FileNotebookManager]
278 flags = Dict(flags)
279 flags = Dict(flags)
279 aliases = Dict(aliases)
280 aliases = Dict(aliases)
280
281
281 kernel_argv = List(Unicode)
282 kernel_argv = List(Unicode)
282
283
283 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
284 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
284 default_value=logging.INFO,
285 default_value=logging.INFO,
285 config=True,
286 config=True,
286 help="Set the log level by value or name.")
287 help="Set the log level by value or name.")
287
288
288 # create requested profiles by default, if they don't exist:
289 # create requested profiles by default, if they don't exist:
289 auto_create = Bool(True)
290 auto_create = Bool(True)
290
291
291 # file to be opened in the notebook server
292 # file to be opened in the notebook server
292 file_to_run = Unicode('')
293 file_to_run = Unicode('')
293
294
294 # Network related information.
295 # Network related information.
295
296
296 ip = Unicode(LOCALHOST, config=True,
297 ip = Unicode(LOCALHOST, config=True,
297 help="The IP address the notebook server will listen on."
298 help="The IP address the notebook server will listen on."
298 )
299 )
299
300
300 def _ip_changed(self, name, old, new):
301 def _ip_changed(self, name, old, new):
301 if new == u'*': self.ip = u''
302 if new == u'*': self.ip = u''
302
303
303 port = Integer(8888, config=True,
304 port = Integer(8888, config=True,
304 help="The port the notebook server will listen on."
305 help="The port the notebook server will listen on."
305 )
306 )
306 port_retries = Integer(50, config=True,
307 port_retries = Integer(50, config=True,
307 help="The number of additional ports to try if the specified port is not available."
308 help="The number of additional ports to try if the specified port is not available."
308 )
309 )
309
310
310 certfile = Unicode(u'', config=True,
311 certfile = Unicode(u'', config=True,
311 help="""The full path to an SSL/TLS certificate file."""
312 help="""The full path to an SSL/TLS certificate file."""
312 )
313 )
313
314
314 keyfile = Unicode(u'', config=True,
315 keyfile = Unicode(u'', config=True,
315 help="""The full path to a private key file for usage with SSL/TLS."""
316 help="""The full path to a private key file for usage with SSL/TLS."""
316 )
317 )
317
318
318 password = Unicode(u'', config=True,
319 password = Unicode(u'', config=True,
319 help="""Hashed password to use for web authentication.
320 help="""Hashed password to use for web authentication.
320
321
321 To generate, type in a python/IPython shell:
322 To generate, type in a python/IPython shell:
322
323
323 from IPython.lib import passwd; passwd()
324 from IPython.lib import passwd; passwd()
324
325
325 The string should be of the form type:salt:hashed-password.
326 The string should be of the form type:salt:hashed-password.
326 """
327 """
327 )
328 )
328
329
329 open_browser = Bool(True, config=True,
330 open_browser = Bool(True, config=True,
330 help="""Whether to open in a browser after starting.
331 help="""Whether to open in a browser after starting.
331 The specific browser used is platform dependent and
332 The specific browser used is platform dependent and
332 determined by the python standard library `webbrowser`
333 determined by the python standard library `webbrowser`
333 module, unless it is overridden using the --browser
334 module, unless it is overridden using the --browser
334 (NotebookApp.browser) configuration option.
335 (NotebookApp.browser) configuration option.
335 """)
336 """)
336
337
337 browser = Unicode(u'', config=True,
338 browser = Unicode(u'', config=True,
338 help="""Specify what command to use to invoke a web
339 help="""Specify what command to use to invoke a web
339 browser when opening the notebook. If not specified, the
340 browser when opening the notebook. If not specified, the
340 default browser will be determined by the `webbrowser`
341 default browser will be determined by the `webbrowser`
341 standard library module, which allows setting of the
342 standard library module, which allows setting of the
342 BROWSER environment variable to override it.
343 BROWSER environment variable to override it.
343 """)
344 """)
344
345
345 read_only = Bool(False, config=True,
346 read_only = Bool(False, config=True,
346 help="Whether to prevent editing/execution of notebooks."
347 help="Whether to prevent editing/execution of notebooks."
347 )
348 )
348
349
349 use_less = Bool(False, config=True,
350 use_less = Bool(False, config=True,
350 help="""Wether to use Browser Side less-css parsing
351 help="""Wether to use Browser Side less-css parsing
351 instead of compiled css version in templates that allows
352 instead of compiled css version in templates that allows
352 it. This is mainly convenient when working on the less
353 it. This is mainly convenient when working on the less
353 file to avoid a build step, or if user want to overwrite
354 file to avoid a build step, or if user want to overwrite
354 some of the less variables without having to recompile
355 some of the less variables without having to recompile
355 everything.""")
356 everything.""")
356
357
357 webapp_settings = Dict(config=True,
358 webapp_settings = Dict(config=True,
358 help="Supply overrides for the tornado.web.Application that the "
359 help="Supply overrides for the tornado.web.Application that the "
359 "IPython notebook uses.")
360 "IPython notebook uses.")
360
361
361 enable_mathjax = Bool(True, config=True,
362 enable_mathjax = Bool(True, config=True,
362 help="""Whether to enable MathJax for typesetting math/TeX
363 help="""Whether to enable MathJax for typesetting math/TeX
363
364
364 MathJax is the javascript library IPython uses to render math/LaTeX. It is
365 MathJax is the javascript library IPython uses to render math/LaTeX. It is
365 very large, so you may want to disable it if you have a slow internet
366 very large, so you may want to disable it if you have a slow internet
366 connection, or for offline use of the notebook.
367 connection, or for offline use of the notebook.
367
368
368 When disabled, equations etc. will appear as their untransformed TeX source.
369 When disabled, equations etc. will appear as their untransformed TeX source.
369 """
370 """
370 )
371 )
371 def _enable_mathjax_changed(self, name, old, new):
372 def _enable_mathjax_changed(self, name, old, new):
372 """set mathjax url to empty if mathjax is disabled"""
373 """set mathjax url to empty if mathjax is disabled"""
373 if not new:
374 if not new:
374 self.mathjax_url = u''
375 self.mathjax_url = u''
375
376
376 base_project_url = Unicode('/', config=True,
377 base_project_url = Unicode('/', config=True,
377 help='''The base URL for the notebook server.
378 help='''The base URL for the notebook server.
378
379
379 Leading and trailing slashes can be omitted,
380 Leading and trailing slashes can be omitted,
380 and will automatically be added.
381 and will automatically be added.
381 ''')
382 ''')
382 def _base_project_url_changed(self, name, old, new):
383 def _base_project_url_changed(self, name, old, new):
383 if not new.startswith('/'):
384 if not new.startswith('/'):
384 self.base_project_url = '/'+new
385 self.base_project_url = '/'+new
385 elif not new.endswith('/'):
386 elif not new.endswith('/'):
386 self.base_project_url = new+'/'
387 self.base_project_url = new+'/'
387
388
388 base_kernel_url = Unicode('/', config=True,
389 base_kernel_url = Unicode('/', config=True,
389 help='''The base URL for the kernel server
390 help='''The base URL for the kernel server
390
391
391 Leading and trailing slashes can be omitted,
392 Leading and trailing slashes can be omitted,
392 and will automatically be added.
393 and will automatically be added.
393 ''')
394 ''')
394 def _base_kernel_url_changed(self, name, old, new):
395 def _base_kernel_url_changed(self, name, old, new):
395 if not new.startswith('/'):
396 if not new.startswith('/'):
396 self.base_kernel_url = '/'+new
397 self.base_kernel_url = '/'+new
397 elif not new.endswith('/'):
398 elif not new.endswith('/'):
398 self.base_kernel_url = new+'/'
399 self.base_kernel_url = new+'/'
399
400
400 websocket_host = Unicode("", config=True,
401 websocket_host = Unicode("", config=True,
401 help="""The hostname for the websocket server."""
402 help="""The hostname for the websocket server."""
402 )
403 )
403
404
404 extra_static_paths = List(Unicode, config=True,
405 extra_static_paths = List(Unicode, config=True,
405 help="""Extra paths to search for serving static files.
406 help="""Extra paths to search for serving static files.
406
407
407 This allows adding javascript/css to be available from the notebook server machine,
408 This allows adding javascript/css to be available from the notebook server machine,
408 or overriding individual files in the IPython"""
409 or overriding individual files in the IPython"""
409 )
410 )
410 def _extra_static_paths_default(self):
411 def _extra_static_paths_default(self):
411 return [os.path.join(self.profile_dir.location, 'static')]
412 return [os.path.join(self.profile_dir.location, 'static')]
412
413
413 @property
414 @property
414 def static_file_path(self):
415 def static_file_path(self):
415 """return extra paths + the default location"""
416 """return extra paths + the default location"""
416 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
417 return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
417
418
418 mathjax_url = Unicode("", config=True,
419 mathjax_url = Unicode("", config=True,
419 help="""The url for MathJax.js."""
420 help="""The url for MathJax.js."""
420 )
421 )
421 def _mathjax_url_default(self):
422 def _mathjax_url_default(self):
422 if not self.enable_mathjax:
423 if not self.enable_mathjax:
423 return u''
424 return u''
424 static_url_prefix = self.webapp_settings.get("static_url_prefix",
425 static_url_prefix = self.webapp_settings.get("static_url_prefix",
425 "/static/")
426 "/static/")
426 try:
427 try:
427 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
428 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
428 except IOError:
429 except IOError:
429 if self.certfile:
430 if self.certfile:
430 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
431 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
431 base = u"https://c328740.ssl.cf1.rackcdn.com"
432 base = u"https://c328740.ssl.cf1.rackcdn.com"
432 else:
433 else:
433 base = u"http://cdn.mathjax.org"
434 base = u"http://cdn.mathjax.org"
434
435
435 url = base + u"/mathjax/latest/MathJax.js"
436 url = base + u"/mathjax/latest/MathJax.js"
436 self.log.info("Using MathJax from CDN: %s", url)
437 self.log.info("Using MathJax from CDN: %s", url)
437 return url
438 return url
438 else:
439 else:
439 self.log.info("Using local MathJax from %s" % mathjax)
440 self.log.info("Using local MathJax from %s" % mathjax)
440 return static_url_prefix+u"mathjax/MathJax.js"
441 return static_url_prefix+u"mathjax/MathJax.js"
441
442
442 def _mathjax_url_changed(self, name, old, new):
443 def _mathjax_url_changed(self, name, old, new):
443 if new and not self.enable_mathjax:
444 if new and not self.enable_mathjax:
444 # enable_mathjax=False overrides mathjax_url
445 # enable_mathjax=False overrides mathjax_url
445 self.mathjax_url = u''
446 self.mathjax_url = u''
446 else:
447 else:
447 self.log.info("Using MathJax: %s", new)
448 self.log.info("Using MathJax: %s", new)
448
449
449 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
450 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
450 config=True,
451 config=True,
451 help='The notebook manager class to use.')
452 help='The notebook manager class to use.')
452
453
453 def parse_command_line(self, argv=None):
454 def parse_command_line(self, argv=None):
454 super(NotebookApp, self).parse_command_line(argv)
455 super(NotebookApp, self).parse_command_line(argv)
455 if argv is None:
456 if argv is None:
456 argv = sys.argv[1:]
457 argv = sys.argv[1:]
457
458
458 # Scrub frontend-specific flags
459 # Scrub frontend-specific flags
459 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
460 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
460 # Kernel should inherit default config file from frontend
461 # Kernel should inherit default config file from frontend
461 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
462 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
462
463
463 if self.extra_args:
464 if self.extra_args:
464 f = os.path.abspath(self.extra_args[0])
465 f = os.path.abspath(self.extra_args[0])
465 if os.path.isdir(f):
466 if os.path.isdir(f):
466 nbdir = f
467 nbdir = f
467 else:
468 else:
468 self.file_to_run = f
469 self.file_to_run = f
469 nbdir = os.path.dirname(f)
470 nbdir = os.path.dirname(f)
470 self.config.NotebookManager.notebook_dir = nbdir
471 self.config.NotebookManager.notebook_dir = nbdir
471
472
472 def init_configurables(self):
473 def init_configurables(self):
473 # force Session default to be secure
474 # force Session default to be secure
474 default_secure(self.config)
475 default_secure(self.config)
475 self.kernel_manager = MappingKernelManager(
476 self.kernel_manager = MappingKernelManager(
476 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
477 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
477 connection_dir = self.profile_dir.security_dir,
478 connection_dir = self.profile_dir.security_dir,
478 )
479 )
479 kls = import_item(self.notebook_manager_class)
480 kls = import_item(self.notebook_manager_class)
480 self.notebook_manager = kls(config=self.config, log=self.log)
481 self.notebook_manager = kls(config=self.config, log=self.log)
481 self.notebook_manager.log_info()
482 self.notebook_manager.log_info()
482 self.notebook_manager.load_notebook_names()
483 self.notebook_manager.load_notebook_names()
483 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
484 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
484 self.cluster_manager.update_profiles()
485 self.cluster_manager.update_profiles()
485
486
486 def init_logging(self):
487 def init_logging(self):
487 # This prevents double log messages because tornado use a root logger that
488 # This prevents double log messages because tornado use a root logger that
488 # self.log is a child of. The logging module dipatches log messages to a log
489 # self.log is a child of. The logging module dipatches log messages to a log
489 # and all of its ancenstors until propagate is set to False.
490 # and all of its ancenstors until propagate is set to False.
490 self.log.propagate = False
491 self.log.propagate = False
491
492
492 def init_webapp(self):
493 def init_webapp(self):
493 """initialize tornado webapp and httpserver"""
494 """initialize tornado webapp and httpserver"""
494 self.web_app = NotebookWebApplication(
495 self.web_app = NotebookWebApplication(
495 self, self.kernel_manager, self.notebook_manager,
496 self, self.kernel_manager, self.notebook_manager,
496 self.cluster_manager, self.log,
497 self.cluster_manager, self.log,
497 self.base_project_url, self.webapp_settings
498 self.base_project_url, self.webapp_settings
498 )
499 )
499 if self.certfile:
500 if self.certfile:
500 ssl_options = dict(certfile=self.certfile)
501 ssl_options = dict(certfile=self.certfile)
501 if self.keyfile:
502 if self.keyfile:
502 ssl_options['keyfile'] = self.keyfile
503 ssl_options['keyfile'] = self.keyfile
503 else:
504 else:
504 ssl_options = None
505 ssl_options = None
505 self.web_app.password = self.password
506 self.web_app.password = self.password
506 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
507 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
507 if not self.ip:
508 if not self.ip:
508 warning = "WARNING: The notebook server is listening on all IP addresses"
509 warning = "WARNING: The notebook server is listening on all IP addresses"
509 if ssl_options is None:
510 if ssl_options is None:
510 self.log.critical(warning + " and not using encryption. This"
511 self.log.critical(warning + " and not using encryption. This"
511 "is not recommended.")
512 "is not recommended.")
512 if not self.password and not self.read_only:
513 if not self.password and not self.read_only:
513 self.log.critical(warning + "and not using authentication."
514 self.log.critical(warning + "and not using authentication."
514 "This is highly insecure and not recommended.")
515 "This is highly insecure and not recommended.")
515 success = None
516 success = None
516 for port in random_ports(self.port, self.port_retries+1):
517 for port in random_ports(self.port, self.port_retries+1):
517 try:
518 try:
518 self.http_server.listen(port, self.ip)
519 self.http_server.listen(port, self.ip)
519 except socket.error as e:
520 except socket.error as e:
520 if e.errno != errno.EADDRINUSE:
521 if e.errno != errno.EADDRINUSE:
521 raise
522 raise
522 self.log.info('The port %i is already in use, trying another random port.' % port)
523 self.log.info('The port %i is already in use, trying another random port.' % port)
523 else:
524 else:
524 self.port = port
525 self.port = port
525 success = True
526 success = True
526 break
527 break
527 if not success:
528 if not success:
528 self.log.critical('ERROR: the notebook server could not be started because '
529 self.log.critical('ERROR: the notebook server could not be started because '
529 'no available port could be found.')
530 'no available port could be found.')
530 self.exit(1)
531 self.exit(1)
531
532
532 def init_signal(self):
533 def init_signal(self):
533 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
534 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
534 # safely extract zmq version info:
535 # safely extract zmq version info:
535 try:
536 try:
536 zmq_v = zmq.pyzmq_version_info()
537 zmq_v = zmq.pyzmq_version_info()
537 except AttributeError:
538 except AttributeError:
538 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
539 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
539 if 'dev' in zmq.__version__:
540 if 'dev' in zmq.__version__:
540 zmq_v.append(999)
541 zmq_v.append(999)
541 zmq_v = tuple(zmq_v)
542 zmq_v = tuple(zmq_v)
542 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
543 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
543 # This won't work with 2.1.7 and
544 # This won't work with 2.1.7 and
544 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
545 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
545 # but it will work
546 # but it will work
546 signal.signal(signal.SIGINT, self._handle_sigint)
547 signal.signal(signal.SIGINT, self._handle_sigint)
547 signal.signal(signal.SIGTERM, self._signal_stop)
548 signal.signal(signal.SIGTERM, self._signal_stop)
548
549
549 def _handle_sigint(self, sig, frame):
550 def _handle_sigint(self, sig, frame):
550 """SIGINT handler spawns confirmation dialog"""
551 """SIGINT handler spawns confirmation dialog"""
551 # register more forceful signal handler for ^C^C case
552 # register more forceful signal handler for ^C^C case
552 signal.signal(signal.SIGINT, self._signal_stop)
553 signal.signal(signal.SIGINT, self._signal_stop)
553 # request confirmation dialog in bg thread, to avoid
554 # request confirmation dialog in bg thread, to avoid
554 # blocking the App
555 # blocking the App
555 thread = threading.Thread(target=self._confirm_exit)
556 thread = threading.Thread(target=self._confirm_exit)
556 thread.daemon = True
557 thread.daemon = True
557 thread.start()
558 thread.start()
558
559
559 def _restore_sigint_handler(self):
560 def _restore_sigint_handler(self):
560 """callback for restoring original SIGINT handler"""
561 """callback for restoring original SIGINT handler"""
561 signal.signal(signal.SIGINT, self._handle_sigint)
562 signal.signal(signal.SIGINT, self._handle_sigint)
562
563
563 def _confirm_exit(self):
564 def _confirm_exit(self):
564 """confirm shutdown on ^C
565 """confirm shutdown on ^C
565
566
566 A second ^C, or answering 'y' within 5s will cause shutdown,
567 A second ^C, or answering 'y' within 5s will cause shutdown,
567 otherwise original SIGINT handler will be restored.
568 otherwise original SIGINT handler will be restored.
568
569
569 This doesn't work on Windows.
570 This doesn't work on Windows.
570 """
571 """
571 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
572 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
572 time.sleep(0.1)
573 time.sleep(0.1)
573 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
574 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
574 sys.stdout.flush()
575 sys.stdout.flush()
575 r,w,x = select.select([sys.stdin], [], [], 5)
576 r,w,x = select.select([sys.stdin], [], [], 5)
576 if r:
577 if r:
577 line = sys.stdin.readline()
578 line = sys.stdin.readline()
578 if line.lower().startswith('y'):
579 if line.lower().startswith('y'):
579 self.log.critical("Shutdown confirmed")
580 self.log.critical("Shutdown confirmed")
580 ioloop.IOLoop.instance().stop()
581 ioloop.IOLoop.instance().stop()
581 return
582 return
582 else:
583 else:
583 print "No answer for 5s:",
584 print "No answer for 5s:",
584 print "resuming operation..."
585 print "resuming operation..."
585 # no answer, or answer is no:
586 # no answer, or answer is no:
586 # set it back to original SIGINT handler
587 # set it back to original SIGINT handler
587 # use IOLoop.add_callback because signal.signal must be called
588 # use IOLoop.add_callback because signal.signal must be called
588 # from main thread
589 # from main thread
589 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
590 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
590
591
591 def _signal_stop(self, sig, frame):
592 def _signal_stop(self, sig, frame):
592 self.log.critical("received signal %s, stopping", sig)
593 self.log.critical("received signal %s, stopping", sig)
593 ioloop.IOLoop.instance().stop()
594 ioloop.IOLoop.instance().stop()
594
595
595 @catch_config_error
596 @catch_config_error
596 def initialize(self, argv=None):
597 def initialize(self, argv=None):
597 self.init_logging()
598 self.init_logging()
598 super(NotebookApp, self).initialize(argv)
599 super(NotebookApp, self).initialize(argv)
599 self.init_configurables()
600 self.init_configurables()
600 self.init_webapp()
601 self.init_webapp()
601 self.init_signal()
602 self.init_signal()
602
603
603 def cleanup_kernels(self):
604 def cleanup_kernels(self):
604 """Shutdown all kernels.
605 """Shutdown all kernels.
605
606
606 The kernels will shutdown themselves when this process no longer exists,
607 The kernels will shutdown themselves when this process no longer exists,
607 but explicit shutdown allows the KernelManagers to cleanup the connection files.
608 but explicit shutdown allows the KernelManagers to cleanup the connection files.
608 """
609 """
609 self.log.info('Shutting down kernels')
610 self.log.info('Shutting down kernels')
610 self.kernel_manager.shutdown_all()
611 self.kernel_manager.shutdown_all()
611
612
612 def start(self):
613 def start(self):
613 ip = self.ip if self.ip else '[all ip addresses on your system]'
614 ip = self.ip if self.ip else '[all ip addresses on your system]'
614 proto = 'https' if self.certfile else 'http'
615 proto = 'https' if self.certfile else 'http'
615 info = self.log.info
616 info = self.log.info
616 info("The IPython Notebook is running at: %s://%s:%i%s" %
617 info("The IPython Notebook is running at: %s://%s:%i%s" %
617 (proto, ip, self.port,self.base_project_url) )
618 (proto, ip, self.port,self.base_project_url) )
618 info("Use Control-C to stop this server and shut down all kernels.")
619 info("Use Control-C to stop this server and shut down all kernels.")
619
620
620 if self.open_browser or self.file_to_run:
621 if self.open_browser or self.file_to_run:
621 ip = self.ip or '127.0.0.1'
622 ip = self.ip or '127.0.0.1'
622 try:
623 try:
623 browser = webbrowser.get(self.browser or None)
624 browser = webbrowser.get(self.browser or None)
624 except webbrowser.Error as e:
625 except webbrowser.Error as e:
625 self.log.warn('No web browser found: %s.' % e)
626 self.log.warn('No web browser found: %s.' % e)
626 browser = None
627 browser = None
627
628
628 if self.file_to_run:
629 if self.file_to_run:
629 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
630 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
630 url = self.notebook_manager.rev_mapping.get(name, '')
631 url = self.notebook_manager.rev_mapping.get(name, '')
631 else:
632 else:
632 url = ''
633 url = ''
633 if browser:
634 if browser:
634 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
635 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
635 self.port, self.base_project_url, url), new=2)
636 self.port, self.base_project_url, url), new=2)
636 threading.Thread(target=b).start()
637 threading.Thread(target=b).start()
637 try:
638 try:
638 ioloop.IOLoop.instance().start()
639 ioloop.IOLoop.instance().start()
639 except KeyboardInterrupt:
640 except KeyboardInterrupt:
640 info("Interrupted...")
641 info("Interrupted...")
641 finally:
642 finally:
642 self.cleanup_kernels()
643 self.cleanup_kernels()
643
644
644
645
645 #-----------------------------------------------------------------------------
646 #-----------------------------------------------------------------------------
646 # Main entry point
647 # Main entry point
647 #-----------------------------------------------------------------------------
648 #-----------------------------------------------------------------------------
648
649
649 def launch_new_instance():
650 def launch_new_instance():
650 app = NotebookApp.instance()
651 app = NotebookApp.instance()
651 app.initialize()
652 app.initialize()
652 app.start()
653 app.start()
653
654
@@ -1,111 +1,105 b''
1 .corner-all{border-radius:3px;}
1 .corner-all{border-radius:3px;}
2 body{overflow:hidden;background-color:#ffffff;}
2 body{overflow:hidden;background-color:#ffffff;}
3 span#save_widget{padding:5px;margin:0px 0px 0px 300px;display:inline-block;}
3 span#save_widget{padding:5px;margin:0px 0px 0px 300px;display:inline-block;}
4 span#notebook_name{height:1em;line-height:1em;padding:3px;border:none;font-size:146.5%;}
4 span#notebook_name{height:1em;line-height:1em;padding:3px;border:none;font-size:146.5%;}
5 .ui-menubar-item .ui-button .ui-button-text{padding:0.4em 1.0em;font-size:100%;}
5 .ui-menubar-item .ui-button .ui-button-text{padding:0.4em 1.0em;font-size:100%;}
6 .ui-menu{-moz-box-shadow:0px 6px 10px -1px #adadad;-webkit-box-shadow:0px 6px 10px -1px #adadad;box-shadow:0px 6px 10px -1px #adadad;}
6 .ui-menu{-moz-box-shadow:0px 6px 10px -1px #adadad;-webkit-box-shadow:0px 6px 10px -1px #adadad;box-shadow:0px 6px 10px -1px #adadad;}
7 .ui-menu .ui-menu-item a{border:1px solid transparent;padding:2px 1.6em;}
7 .ui-menu .ui-menu-item a{border:1px solid transparent;padding:2px 1.6em;}
8 .ui-menu .ui-menu-item a.ui-state-focus{margin:0;}
8 .ui-menu .ui-menu-item a.ui-state-focus{margin:0;}
9 .ui-menu hr{margin:0.3em 0;}
9 .ui-menu hr{margin:0.3em 0;}
10 #menubar_container{position:relative;}
10 #menubar_container{position:relative;}
11 #notification_area{position:absolute;right:0px;top:0px;height:25px;padding:3px 0px;padding-right:3px;z-index:10;}
11 #notification_area{position:absolute;right:0px;top:0px;height:25px;padding:3px 0px;padding-right:3px;z-index:10;}
12 .notification_widget{float:right;right:0px;top:1px;height:25px;padding:3px 6px;z-index:10;}
12 .notification_widget{float:right;right:0px;top:1px;height:25px;padding:3px 6px;z-index:10;}
13 .toolbar{padding:3px 15px;border-bottom:1px #e0e0e0 solid;}
13 .toolbar{padding:3px 15px;border-bottom:1px #e0e0e0 solid;}
14 #cell_type{font-size:85%;}
14 #cell_type{font-size:85%;}
15 #ipython-main-app{width:100%;position:relative;}
15 #ipython-main-app{width:100%;position:relative;}
16 span#quick_help_area{position:static;padding:5px 0px;margin:0px 0px 0px 0px;}
16 span#quick_help_area{position:static;padding:5px 0px;margin:0px 0px 0px 0px;}
17 .help_string{float:right;width:170px;padding:0px 5px;text-align:left;font-size:85%;}
17 .help_string{float:right;width:170px;padding:0px 5px;text-align:left;font-size:85%;}
18 .help_string_label{float:right;font-size:85%;}
18 .help_string_label{float:right;font-size:85%;}
19 div#notebook_panel{margin:0px 0px 0px 0px;padding:0px;}
19 div#notebook_panel{margin:0px 0px 0px 0px;padding:0px;}
20 div#notebook{overflow-y:scroll;overflow-x:auto;width:100%;padding:5px 5px 15px 5px;margin:0px;}
20 div#notebook{overflow-y:scroll;overflow-x:auto;width:100%;padding:5px 5px 15px 5px;margin:0px;}
21 div#pager_splitter{height:8px;}
21 div#pager_splitter{height:8px;}
22 #pager_container{position:relative;}
22 #pager_container{position:relative;}
23 div#pager{padding:15px;overflow:auto;display:none;}
23 div#pager{padding:15px;overflow:auto;display:none;}
24 div.ui-widget-content{border:1px solid #e0e0e0;outline:none;}
24 div.ui-widget-content{border:1px solid #e0e0e0;outline:none;}
25 .cell{border:1px solid transparent;}.cell.selected{border-radius:3px;border-radius:3px;border-radius:3px;background-color:#fafafa;border:thin #e0e0e0 solid;}
25 .cell{border:1px solid transparent;}.cell.selected{border-radius:3px;border-radius:3px;border-radius:3px;background-color:#fafafa;border:thin #e0e0e0 solid;}
26 div.cell{width:100%;padding:5px 5px 5px 0px;margin:2px 0px 2px 0px;}
26 div.cell{width:100%;padding:5px 5px 5px 0px;margin:2px 0px 2px 0px;}
27 div.prompt{width:11ex;padding:0.4em;margin:0px;font-family:monospace;text-align:right;}
27 div.prompt{width:11ex;padding:0.4em;margin:0px;font-family:monospace;text-align:right;}
28 div.input{page-break-inside:avoid;}
28 div.input{page-break-inside:avoid;}
29 div.input_area{color:black;border:1px solid #e0e0e0;border-radius:3px;border-radius:3px;border-radius:3px;background:#f7f7f7;}
29 div.input_area{color:black;border:1px solid #e0e0e0;border-radius:3px;border-radius:3px;border-radius:3px;background:#f7f7f7;}
30 div.input_prompt{color:navy;border-top:1px solid transparent;}
30 div.input_prompt{color:navy;border-top:1px solid transparent;}
31 div.output_wrapper{margin-top:5px;margin-left:5px;width:100%;position:relative;}
31 div.output_wrapper{margin-top:5px;margin-left:5px;width:100%;position:relative;}
32 div.output_scroll{height:24em;width:100%;overflow:auto;border-radius:3px;border-radius:3px;border-radius:3px;box-shadow:inset 0 2px 8px rgba(0, 0, 0, 0.8);}
32 div.output_scroll{height:24em;width:100%;overflow:auto;border-radius:3px;border-radius:3px;border-radius:3px;box-shadow:inset 0 2px 8px rgba(0, 0, 0, 0.8);}
33 div.output_collapsed{margin-right:5px;}
33 div.output_collapsed{margin-right:5px;}
34 div.out_prompt_overlay{height:100%;padding:0px;position:absolute;border-radius:3px;border-radius:3px;border-radius:3px;}
34 div.out_prompt_overlay{height:100%;padding:0px;position:absolute;border-radius:3px;border-radius:3px;border-radius:3px;}
35 div.out_prompt_overlay:hover{box-shadow:inset 0 0 1px #000;background:rgba(240, 240, 240, 0.5);}
35 div.out_prompt_overlay:hover{box-shadow:inset 0 0 1px #000;background:rgba(240, 240, 240, 0.5);}
36 div.output_prompt{color:darkred;margin:0 5px 0 -5px;}
36 div.output_prompt{color:darkred;margin:0 5px 0 -5px;}
37 div.output_area{padding:0px;page-break-inside:avoid;}
37 div.output_area{padding:0px;page-break-inside:avoid;}
38 div.output_subarea{padding:0.44em 0.4em 0.4em 1px;}
38 div.output_subarea{padding:0.44em 0.4em 0.4em 1px;}
39 div.output_text{text-align:left;color:black;font-family:monospace;}
39 div.output_text{text-align:left;color:black;font-family:monospace;}
40 div.output_stream{padding-top:0.0em;padding-bottom:0.0em;}
40 div.output_stream{padding-top:0.0em;padding-bottom:0.0em;}
41 div.output_stderr{background:#fdd;}
41 div.output_stderr{background:#fdd;}
42 div.output_latex{text-align:left;color:black;}
42 div.output_latex{text-align:left;color:black;}
43 div.text_cell{padding:5px 5px 5px 5px;}
43 div.text_cell{padding:5px 5px 5px 5px;}
44 div.text_cell_input{color:black;border:1px solid #e0e0e0;border-radius:3px;border-radius:3px;border-radius:3px;background:#f7f7f7;}
44 div.text_cell_input{color:black;border:1px solid #e0e0e0;border-radius:3px;border-radius:3px;border-radius:3px;background:#f7f7f7;}
45 div.text_cell_render{font-family:"Helvetica Neue",Arial,Helvetica,Geneva,sans-serif;outline:none;resize:none;width:inherit;border-style:none;padding:5px;color:black;}
45 div.text_cell_render{font-family:"Helvetica Neue",Arial,Helvetica,Geneva,sans-serif;outline:none;resize:none;width:inherit;border-style:none;padding:5px;color:black;}
46 .CodeMirror{line-height:1.231;}
46 .CodeMirror{line-height:1.231;}
47 .CodeMirror-scroll{height:auto;overflow-y:hidden;overflow-x:auto;}
47 .CodeMirror-scroll{height:auto;overflow-y:hidden;overflow-x:auto;}
48 .ansiblack{color:black;}
48 .ansiblack{color:black;}
49 .ansired{color:darkred;}
49 .ansired{color:darkred;}
50 .ansigreen{color:darkgreen;}
50 .ansigreen{color:darkgreen;}
51 .ansiyellow{color:brown;}
51 .ansiyellow{color:brown;}
52 .ansiblue{color:darkblue;}
52 .ansiblue{color:darkblue;}
53 .ansipurple{color:darkviolet;}
53 .ansipurple{color:darkviolet;}
54 .ansicyan{color:steelblue;}
54 .ansicyan{color:steelblue;}
55 .ansigrey{color:grey;}
55 .ansigrey{color:grey;}
56 .ansibold{font-weight:bold;}
56 .ansibold{font-weight:bold;}
57 .completions{position:absolute;z-index:10;overflow:hidden;border:1px solid #e0e0e0;}
57 .completions{position:absolute;z-index:10;overflow:hidden;border:1px solid #e0e0e0;}
58 .completions select{background:white;outline:none;border:none;padding:0px;margin:0px;overflow:auto;font-family:monospace;}
58 .completions select{background:white;outline:none;border:none;padding:0px;margin:0px;overflow:auto;font-family:monospace;}
59 option.context{background-color:#DEF7FF;}
59 option.context{background-color:#DEF7FF;}
60 option.introspection{background-color:#EBF4EB;}
60 option.introspection{background-color:#EBF4EB;}
61 .completions p b{font-weight:bold;}
61 .completions p b{font-weight:bold;}
62 .completions p{background:#DDF;border-bottom:black solid 1px;padding:1px;font-family:monospace;}
62 .completions p{background:#DDF;border-bottom:black solid 1px;padding:1px;font-family:monospace;}
63 pre.dialog{background-color:#f7f7f7;border:1px solid #ddd;border-radius:3px;border-radius:3px;border-radius:3px;padding:0.4em;padding-left:2em;}
63 pre.dialog{background-color:#f7f7f7;border:1px solid #ddd;border-radius:3px;border-radius:3px;border-radius:3px;padding:0.4em;padding-left:2em;}
64 p.dialog{padding:0.2em;}
64 p.dialog{padding:0.2em;}
65 .shortcut_key{display:inline-block;width:15ex;text-align:right;font-family:monospace;}
65 .shortcut_key{display:inline-block;width:15ex;text-align:right;font-family:monospace;}
66 pre,code,kbd,samp{white-space:pre-wrap;}
66 pre,code,kbd,samp{white-space:pre-wrap;}
67 #fonttest{font-family:monospace;}
67 #fonttest{font-family:monospace;}
68 .js-error{color:darkred;}
68 .js-error{color:darkred;}
69 body{overflow:visible !important;}
69 @media print{body{overflow:visible !important;} div#notebook{overflow:visible !important;} .ui-widget-content{border:0px;} #save_widget{margin:0px !important;} #header,#pager,#pager_splitter,#menubar,#toolbar{display:none !important;} .cell{border:none !important;}}.rendered_html{color:black;}
70 div#notebook{overflow:visible !important;}
71 .ui-widget-content{border:0px;}
72 #save_widget{margin:0px !important;}
73 #header,#pager,#pager_splitter,#menubar,#toolbar{display:none !important;}
74 .cell{border:none !important;}
75 .rendered_html{color:black;}
76 .rendered_html em{font-style:italic;}
70 .rendered_html em{font-style:italic;}
77 .rendered_html strong{font-weight:bold;}
71 .rendered_html strong{font-weight:bold;}
78 .rendered_html u{text-decoration:underline;}
72 .rendered_html u{text-decoration:underline;}
79 .rendered_html :link{text-decoration:underline;}
73 .rendered_html :link{text-decoration:underline;}
80 .rendered_html :visited{text-decoration:underline;}
74 .rendered_html :visited{text-decoration:underline;}
81 .rendered_html h1{font-size:197%;margin:.65em 0;font-weight:bold;}
75 .rendered_html h1{font-size:197%;margin:.65em 0;font-weight:bold;}
82 .rendered_html h2{font-size:153.9%;margin:.75em 0;font-weight:bold;}
76 .rendered_html h2{font-size:153.9%;margin:.75em 0;font-weight:bold;}
83 .rendered_html h3{font-size:123.1%;margin:.85em 0;font-weight:bold;}
77 .rendered_html h3{font-size:123.1%;margin:.85em 0;font-weight:bold;}
84 .rendered_html h4{font-size:100% margin: 0.95em 0;font-weight:bold;}
78 .rendered_html h4{font-size:100% margin: 0.95em 0;font-weight:bold;}
85 .rendered_html h5{font-size:85%;margin:1.5em 0;font-weight:bold;}
79 .rendered_html h5{font-size:85%;margin:1.5em 0;font-weight:bold;}
86 .rendered_html h6{font-size:77%;margin:1.65em 0;font-weight:bold;}
80 .rendered_html h6{font-size:77%;margin:1.65em 0;font-weight:bold;}
87 .rendered_html ul{list-style:disc;margin:1em 2em;}
81 .rendered_html ul{list-style:disc;margin:1em 2em;}
88 .rendered_html ul ul{list-style:square;margin:0em 2em;}
82 .rendered_html ul ul{list-style:square;margin:0em 2em;}
89 .rendered_html ul ul ul{list-style:circle;margin-left:0em 2em;}
83 .rendered_html ul ul ul{list-style:circle;margin-left:0em 2em;}
90 .rendered_html ol{list-style:upper-roman;margin:1em 2em;}
84 .rendered_html ol{list-style:upper-roman;margin:1em 2em;}
91 .rendered_html ol ol{list-style:upper-alpha;margin:0em 2em;}
85 .rendered_html ol ol{list-style:upper-alpha;margin:0em 2em;}
92 .rendered_html ol ol ol{list-style:decimal;margin:0em 2em;}
86 .rendered_html ol ol ol{list-style:decimal;margin:0em 2em;}
93 .rendered_html ol ol ol ol{list-style:lower-alpha;margin:0em 2em;}
87 .rendered_html ol ol ol ol{list-style:lower-alpha;margin:0em 2em;}
94 .rendered_html ol ol ol ol ol{list-style:lower-roman;margin:0em 2em;}
88 .rendered_html ol ol ol ol ol{list-style:lower-roman;margin:0em 2em;}
95 .rendered_html hr{color:black;background-color:black;}
89 .rendered_html hr{color:black;background-color:black;}
96 .rendered_html pre{margin:1em 2em;}
90 .rendered_html pre{margin:1em 2em;}
97 .rendered_html blockquote{margin:1em 2em;}
91 .rendered_html blockquote{margin:1em 2em;}
98 .rendered_html table{border:1px solid black;border-collapse:collapse;margin:1em 2em;}
92 .rendered_html table{border:1px solid black;border-collapse:collapse;margin:1em 2em;}
99 .rendered_html td{border:1px solid black;text-align:left;vertical-align:middle;padding:4px;}
93 .rendered_html td{border:1px solid black;text-align:left;vertical-align:middle;padding:4px;}
100 .rendered_html th{border:1px solid black;text-align:left;vertical-align:middle;padding:4px;font-weight:bold;}
94 .rendered_html th{border:1px solid black;text-align:left;vertical-align:middle;padding:4px;font-weight:bold;}
101 .rendered_html tr{border:1px solid black;}
95 .rendered_html tr{border:1px solid black;}
102 .rendered_html p+p{margin-top:1em;}
96 .rendered_html p+p{margin-top:1em;}
103 .corner-all{border-radius:3px;}
97 .corner-all{border-radius:3px;}
104 @-moz-keyframes fadeOut{from{opacity:1;} to{opacity:0;}}@-webkit-keyframes fadeOut{from{opacity:1;} to{opacity:0;}}@-moz-keyframes fadeIn{from{opacity:0;} to{opacity:1;}}@-webkit-keyframes fadeIn{from{opacity:0;} to{opacity:1;}}.bigtooltip{overflow:auto;height:200px;-webkit-transition-property:height;-webkit-transition-duration:1s;-moz-transition-property:height;-moz-transition-duration:1s;transition-property:height;transition-duration:1s;}
98 @-moz-keyframes fadeOut{from{opacity:1;} to{opacity:0;}}@-webkit-keyframes fadeOut{from{opacity:1;} to{opacity:0;}}@-moz-keyframes fadeIn{from{opacity:0;} to{opacity:1;}}@-webkit-keyframes fadeIn{from{opacity:0;} to{opacity:1;}}.bigtooltip{overflow:auto;height:200px;-webkit-transition-property:height;-webkit-transition-duration:1s;-moz-transition-property:height;-moz-transition-duration:1s;transition-property:height;transition-duration:1s;}
105 .smalltooltip{-webkit-transition-property:height;-webkit-transition-duration:1s;-moz-transition-property:height;-moz-transition-duration:1s;transition-property:height;transition-duration:1s;text-overflow:ellipsis;overflow:hidden;height:80px;}
99 .smalltooltip{-webkit-transition-property:height;-webkit-transition-duration:1s;-moz-transition-property:height;-moz-transition-duration:1s;transition-property:height;transition-duration:1s;text-overflow:ellipsis;overflow:hidden;height:80px;}
106 .tooltipbuttons{position:absolute;padding-right:15px;top:0px;right:0px;}
100 .tooltipbuttons{position:absolute;padding-right:15px;top:0px;right:0px;}
107 .tooltiptext{padding-right:30px;}
101 .tooltiptext{padding-right:30px;}
108 .ipython_tooltip{max-width:700px;-webkit-animation:fadeOut 800ms;-moz-animation:fadeOut 800ms;animation:fadeOut 800ms;-webkit-animation:fadeIn 800ms;-moz-animation:fadeIn 800ms;animation:fadeIn 800ms;vertical-align:middle;background-color:#f7f7f7;overflow:visible;border:#e0e0e0 1px solid;outline:none;padding:3px;margin:0px;padding-left:7px;font-family:monospace;min-height:50px;position:absolute;-moz-box-shadow:0px 6px 10px -1px #adadad;-webkit-box-shadow:0px 6px 10px -1px #adadad;box-shadow:0px 6px 10px -1px #adadad;border-radius:3px;border-radius:3px;border-radius:3px;}.ipython_tooltip a{float:right;}
102 .ipython_tooltip{max-width:700px;-webkit-animation:fadeOut 800ms;-moz-animation:fadeOut 800ms;animation:fadeOut 800ms;-webkit-animation:fadeIn 800ms;-moz-animation:fadeIn 800ms;animation:fadeIn 800ms;vertical-align:middle;background-color:#f7f7f7;overflow:visible;border:#e0e0e0 1px solid;outline:none;padding:3px;margin:0px;padding-left:7px;font-family:monospace;min-height:50px;position:absolute;-moz-box-shadow:0px 6px 10px -1px #adadad;-webkit-box-shadow:0px 6px 10px -1px #adadad;box-shadow:0px 6px 10px -1px #adadad;border-radius:3px;border-radius:3px;border-radius:3px;}.ipython_tooltip a{float:right;}
109 .pretooltiparrow{left:0px;margin:0px;top:-16px;width:40px;height:16px;overflow:hidden;position:absolute;}
103 .pretooltiparrow{left:0px;margin:0px;top:-16px;width:40px;height:16px;overflow:hidden;position:absolute;}
110 .pretooltiparrow:before{background-color:#f7f7f7;border:1px #e0e0e0 solid;z-index:11;content:"";position:absolute;left:15px;top:10px;width:25px;height:25px;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);}
104 .pretooltiparrow:before{background-color:#f7f7f7;border:1px #e0e0e0 solid;z-index:11;content:"";position:absolute;left:15px;top:10px;width:25px;height:25px;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);}
111 .corner-all{border-radius:3px;}
105 .corner-all{border-radius:3px;}
@@ -1,7 +1,10 b''
1 body { overflow: visible !important; }
1 @media print {
2 div#notebook { overflow: visible !important; }
2 body { overflow: visible !important; }
3 .ui-widget-content { border: 0px; }
3 div#notebook { overflow: visible !important; }
4 #save_widget {margin: 0px !important;}
4 .ui-widget-content { border: 0px; }
5 #header,#pager,#pager_splitter,#menubar,#toolbar {display: none !important;}
5 #save_widget {margin: 0px !important;}
6 .cell { border:none !important}
6 #header,#pager,#pager_splitter,#menubar,#toolbar {display: none !important;}
7 .cell { border:none !important}
8
9 }
7
10
General Comments 0
You need to be logged in to leave comments. Login now