##// END OF EJS Templates
hook up proper loggers...
MinRK -
Show More
@@ -1,913 +1,927
1 1 """Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import Cookie
20 20 import datetime
21 21 import email.utils
22 22 import hashlib
23 23 import logging
24 24 import mimetypes
25 25 import os
26 26 import stat
27 27 import threading
28 28 import time
29 29 import uuid
30 30
31 31 from tornado.escape import url_escape
32 32 from tornado import web
33 33 from tornado import websocket
34 34
35 try:
36 from tornado.log import app_log
37 except ImportError:
38 app_log = logging.getLogger()
39
35 40 from zmq.eventloop import ioloop
36 41 from zmq.utils import jsonapi
37 42
43 from IPython.config import Application
38 44 from IPython.external.decorator import decorator
39 45 from IPython.kernel.zmq.session import Session
40 46 from IPython.lib.security import passwd_check
41 47 from IPython.utils.jsonutil import date_default
42 48 from IPython.utils.path import filefind
43 49 from IPython.utils.py3compat import PY3
44 50
45 51 try:
46 52 from docutils.core import publish_string
47 53 except ImportError:
48 54 publish_string = None
49 55
50 56 #-----------------------------------------------------------------------------
51 57 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 58 #-----------------------------------------------------------------------------
53 59
54 60 # Google Chrome, as of release 16, changed its websocket protocol number. The
55 61 # parts tornado cares about haven't really changed, so it's OK to continue
56 62 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
57 63 # version as of Oct 30/2011) the version check fails, see the issue report:
58 64
59 65 # https://github.com/facebook/tornado/issues/385
60 66
61 67 # This issue has been fixed in Tornado post 2.1.1:
62 68
63 69 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64 70
65 71 # Here we manually apply the same patch as above so that users of IPython can
66 72 # continue to work with an officially released Tornado. We make the
67 73 # monkeypatch version check as narrow as possible to limit its effects; once
68 74 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69 75
70 76 import tornado
71 77
72 78 if tornado.version_info <= (2,1,1):
73 79
74 80 def _execute(self, transforms, *args, **kwargs):
75 81 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76 82
77 83 self.open_args = args
78 84 self.open_kwargs = kwargs
79 85
80 86 # The difference between version 8 and 13 is that in 8 the
81 87 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 88 # simply "Origin".
83 89 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 90 self.ws_connection = WebSocketProtocol8(self)
85 91 self.ws_connection.accept_connection()
86 92
87 93 elif self.request.headers.get("Sec-WebSocket-Version"):
88 94 self.stream.write(tornado.escape.utf8(
89 95 "HTTP/1.1 426 Upgrade Required\r\n"
90 96 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 97 self.stream.close()
92 98
93 99 else:
94 100 self.ws_connection = WebSocketProtocol76(self)
95 101 self.ws_connection.accept_connection()
96 102
97 103 websocket.WebSocketHandler._execute = _execute
98 104 del _execute
99 105
100 106 #-----------------------------------------------------------------------------
101 107 # Decorator for disabling read-only handlers
102 108 #-----------------------------------------------------------------------------
103 109
104 110 @decorator
105 111 def not_if_readonly(f, self, *args, **kwargs):
106 112 if self.settings.get('read_only', False):
107 113 raise web.HTTPError(403, "Notebook server is read-only")
108 114 else:
109 115 return f(self, *args, **kwargs)
110 116
111 117 @decorator
112 118 def authenticate_unless_readonly(f, self, *args, **kwargs):
113 119 """authenticate this page *unless* readonly view is active.
114 120
115 121 In read-only mode, the notebook list and print view should
116 122 be accessible without authentication.
117 123 """
118 124
119 125 @web.authenticated
120 126 def auth_f(self, *args, **kwargs):
121 127 return f(self, *args, **kwargs)
122 128
123 129 if self.settings.get('read_only', False):
124 130 return f(self, *args, **kwargs)
125 131 else:
126 132 return auth_f(self, *args, **kwargs)
127 133
128 134 def urljoin(*pieces):
129 135 """Join components of url into a relative url
130 136
131 137 Use to prevent double slash when joining subpath
132 138 """
133 139 striped = [s.strip('/') for s in pieces]
134 140 return '/'.join(s for s in striped if s)
135 141
136 142 #-----------------------------------------------------------------------------
137 143 # Top-level handlers
138 144 #-----------------------------------------------------------------------------
139 145
140 146 class RequestHandler(web.RequestHandler):
141 147 """RequestHandler with default variable setting."""
142 148
143 149 def render(*args, **kwargs):
144 150 kwargs.setdefault('message', '')
145 151 return web.RequestHandler.render(*args, **kwargs)
146 152
147 153 class AuthenticatedHandler(RequestHandler):
148 154 """A RequestHandler with an authenticated user."""
149 155
150 156 def clear_login_cookie(self):
151 157 self.clear_cookie(self.cookie_name)
152 158
153 159 def get_current_user(self):
154 160 user_id = self.get_secure_cookie(self.cookie_name)
155 161 # For now the user_id should not return empty, but it could eventually
156 162 if user_id == '':
157 163 user_id = 'anonymous'
158 164 if user_id is None:
159 165 # prevent extra Invalid cookie sig warnings:
160 166 self.clear_login_cookie()
161 167 if not self.read_only and not self.login_available:
162 168 user_id = 'anonymous'
163 169 return user_id
164 170
165 171 @property
166 172 def cookie_name(self):
167 173 return self.settings.get('cookie_name', '')
168 174
169 175 @property
170 176 def password(self):
171 177 """our password"""
172 178 return self.settings.get('password', '')
173 179
174 180 @property
175 181 def logged_in(self):
176 182 """Is a user currently logged in?
177 183
178 184 """
179 185 user = self.get_current_user()
180 186 return (user and not user == 'anonymous')
181 187
182 188 @property
183 189 def login_available(self):
184 190 """May a user proceed to log in?
185 191
186 192 This returns True if login capability is available, irrespective of
187 193 whether the user is already logged in or not.
188 194
189 195 """
190 196 return bool(self.settings.get('password', ''))
191 197
192 198 @property
193 199 def read_only(self):
194 200 """Is the notebook read-only?
195 201
196 202 """
197 203 return self.settings.get('read_only', False)
198 204
199 205
200 206 class IPythonHandler(AuthenticatedHandler):
201 207 """IPython-specific extensions to authenticated handling
202 208
203 209 Mostly property shortcuts to IPython-specific settings.
204 210 """
205 211
206 212 @property
207 213 def config(self):
208 214 return self.settings.get('config', None)
209 215
210 216 @property
217 def log(self):
218 """use the IPython log by default, falling back on tornado's logger"""
219 if Application.initialized():
220 return Application.instance().log
221 else:
222 return app_log
223
224 @property
211 225 def use_less(self):
212 226 """Use less instead of css in templates"""
213 227 return self.settings.get('use_less', False)
214 228
215 229 #---------------------------------------------------------------
216 230 # URLs
217 231 #---------------------------------------------------------------
218 232
219 233 @property
220 234 def ws_url(self):
221 235 """websocket url matching the current request
222 236
223 237 turns http[s]://host[:port] into
224 238 ws[s]://host[:port]
225 239 """
226 240 proto = self.request.protocol.replace('http', 'ws')
227 241 host = self.settings.get('websocket_host', '')
228 242 # default to config value
229 243 if host == '':
230 244 host = self.request.host # get from request
231 245 return "%s://%s" % (proto, host)
232 246
233 247 @property
234 248 def mathjax_url(self):
235 249 return self.settings.get('mathjax_url', '')
236 250
237 251 @property
238 252 def base_project_url(self):
239 253 return self.settings.get('base_project_url', '/')
240 254
241 255 @property
242 256 def base_kernel_url(self):
243 257 return self.settings.get('base_kernel_url', '/')
244 258
245 259 #---------------------------------------------------------------
246 260 # Manager objects
247 261 #---------------------------------------------------------------
248 262
249 263 @property
250 264 def kernel_manager(self):
251 265 return self.settings['kernel_manager']
252 266
253 267 @property
254 268 def notebook_manager(self):
255 269 return self.settings['notebook_manager']
256 270
257 271 @property
258 272 def cluster_manager(self):
259 273 return self.settings['cluster_manager']
260 274
261 275 @property
262 276 def project(self):
263 277 return self.notebook_manager.notebook_dir
264 278
265 279 #---------------------------------------------------------------
266 280 # template rendering
267 281 #---------------------------------------------------------------
268 282
269 283 def get_template(self, name):
270 284 """Return the jinja template object for a given name"""
271 285 return self.settings['jinja2_env'].get_template(name)
272 286
273 287 def render_template(self, name, **ns):
274 288 ns.update(self.template_namespace)
275 289 template = self.get_template(name)
276 290 return template.render(**ns)
277 291
278 292 @property
279 293 def template_namespace(self):
280 294 return dict(
281 295 base_project_url=self.base_project_url,
282 296 base_kernel_url=self.base_kernel_url,
283 297 read_only=self.read_only,
284 298 logged_in=self.logged_in,
285 299 login_available=self.login_available,
286 300 use_less=self.use_less,
287 301 )
288 302
289 303 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
290 304 """static files should only be accessible when logged in"""
291 305
292 306 @authenticate_unless_readonly
293 307 def get(self, path):
294 308 return web.StaticFileHandler.get(self, path)
295 309
296 310
297 311 class ProjectDashboardHandler(IPythonHandler):
298 312
299 313 @authenticate_unless_readonly
300 314 def get(self):
301 315 self.write(self.render_template('projectdashboard.html',
302 316 project=self.project,
303 317 project_component=self.project.split('/'),
304 318 ))
305 319
306 320
307 321 class LoginHandler(IPythonHandler):
308 322
309 323 def _render(self, message=None):
310 324 self.write(self.render_template('login.html',
311 325 next=url_escape(self.get_argument('next', default=self.base_project_url)),
312 326 message=message,
313 327 ))
314 328
315 329 def get(self):
316 330 if self.current_user:
317 331 self.redirect(self.get_argument('next', default=self.base_project_url))
318 332 else:
319 333 self._render()
320 334
321 335 def post(self):
322 336 pwd = self.get_argument('password', default=u'')
323 337 if self.login_available:
324 338 if passwd_check(self.password, pwd):
325 339 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
326 340 else:
327 341 self._render(message={'error': 'Invalid password'})
328 342 return
329 343
330 344 self.redirect(self.get_argument('next', default=self.base_project_url))
331 345
332 346
333 347 class LogoutHandler(IPythonHandler):
334 348
335 349 def get(self):
336 350 self.clear_login_cookie()
337 351 if self.login_available:
338 352 message = {'info': 'Successfully logged out.'}
339 353 else:
340 354 message = {'warning': 'Cannot log out. Notebook authentication '
341 355 'is disabled.'}
342 356 self.write(self.render_template('logout.html',
343 357 message=message))
344 358
345 359
346 360 class NewHandler(IPythonHandler):
347 361
348 362 @web.authenticated
349 363 def get(self):
350 364 notebook_id = self.notebook_manager.new_notebook()
351 365 self.redirect('/' + urljoin(self.base_project_url, notebook_id))
352 366
353 367 class NamedNotebookHandler(IPythonHandler):
354 368
355 369 @authenticate_unless_readonly
356 370 def get(self, notebook_id):
357 371 nbm = self.notebook_manager
358 372 if not nbm.notebook_exists(notebook_id):
359 373 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
360 374 self.write(self.render_template('notebook.html',
361 375 project=self.project,
362 376 notebook_id=notebook_id,
363 377 kill_kernel=False,
364 378 mathjax_url=self.mathjax_url,
365 379 )
366 380 )
367 381
368 382
369 383 class PrintNotebookHandler(IPythonHandler):
370 384
371 385 @authenticate_unless_readonly
372 386 def get(self, notebook_id):
373 387 if not self.notebook_manager.notebook_exists(notebook_id):
374 388 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
375 389 self.write( self.render_template('printnotebook.html',
376 390 project=self.project,
377 391 notebook_id=notebook_id,
378 392 kill_kernel=False,
379 393 mathjax_url=self.mathjax_url,
380 394 ))
381 395
382 396 #-----------------------------------------------------------------------------
383 397 # Kernel handlers
384 398 #-----------------------------------------------------------------------------
385 399
386 400
387 401 class MainKernelHandler(IPythonHandler):
388 402
389 403 @web.authenticated
390 404 def get(self):
391 405 km = self.kernel_manager
392 406 self.finish(jsonapi.dumps(km.list_kernel_ids()))
393 407
394 408 @web.authenticated
395 409 def post(self):
396 410 km = self.kernel_manager
397 411 nbm = self.notebook_manager
398 412 notebook_id = self.get_argument('notebook', default=None)
399 413 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
400 414 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
401 415 self.set_header('Location', '/'+kernel_id)
402 416 self.finish(jsonapi.dumps(data))
403 417
404 418
405 419 class KernelHandler(IPythonHandler):
406 420
407 421 SUPPORTED_METHODS = ('DELETE')
408 422
409 423 @web.authenticated
410 424 def delete(self, kernel_id):
411 425 km = self.kernel_manager
412 426 km.shutdown_kernel(kernel_id)
413 427 self.set_status(204)
414 428 self.finish()
415 429
416 430
417 431 class KernelActionHandler(IPythonHandler):
418 432
419 433 @web.authenticated
420 434 def post(self, kernel_id, action):
421 435 km = self.kernel_manager
422 436 if action == 'interrupt':
423 437 km.interrupt_kernel(kernel_id)
424 438 self.set_status(204)
425 439 if action == 'restart':
426 440 km.restart_kernel(kernel_id)
427 441 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
428 442 self.set_header('Location', '/'+kernel_id)
429 443 self.write(jsonapi.dumps(data))
430 444 self.finish()
431 445
432 446
433 447 class ZMQStreamHandler(websocket.WebSocketHandler):
434 448
435 449 def clear_cookie(self, *args, **kwargs):
436 450 """meaningless for websockets"""
437 451 pass
438 452
439 453 def _reserialize_reply(self, msg_list):
440 454 """Reserialize a reply message using JSON.
441 455
442 456 This takes the msg list from the ZMQ socket, unserializes it using
443 457 self.session and then serializes the result using JSON. This method
444 458 should be used by self._on_zmq_reply to build messages that can
445 459 be sent back to the browser.
446 460 """
447 461 idents, msg_list = self.session.feed_identities(msg_list)
448 462 msg = self.session.unserialize(msg_list)
449 463 try:
450 464 msg['header'].pop('date')
451 465 except KeyError:
452 466 pass
453 467 try:
454 468 msg['parent_header'].pop('date')
455 469 except KeyError:
456 470 pass
457 471 msg.pop('buffers')
458 472 return jsonapi.dumps(msg, default=date_default)
459 473
460 474 def _on_zmq_reply(self, msg_list):
461 475 # Sometimes this gets triggered when the on_close method is scheduled in the
462 476 # eventloop but hasn't been called.
463 477 if self.stream.closed(): return
464 478 try:
465 479 msg = self._reserialize_reply(msg_list)
466 480 except Exception:
467 logging.critical("Malformed message: %r" % msg_list, exc_info=True)
481 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
468 482 else:
469 483 self.write_message(msg)
470 484
471 485 def allow_draft76(self):
472 486 """Allow draft 76, until browsers such as Safari update to RFC 6455.
473 487
474 488 This has been disabled by default in tornado in release 2.2.0, and
475 489 support will be removed in later versions.
476 490 """
477 491 return True
478 492
479 493
480 494 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
481 495
482 496 def open(self, kernel_id):
483 497 self.kernel_id = kernel_id.decode('ascii')
484 498 self.session = Session(config=self.config)
485 499 self.save_on_message = self.on_message
486 500 self.on_message = self.on_first_message
487 501
488 502 def _inject_cookie_message(self, msg):
489 503 """Inject the first message, which is the document cookie,
490 504 for authentication."""
491 505 if not PY3 and isinstance(msg, unicode):
492 506 # Cookie constructor doesn't accept unicode strings
493 507 # under Python 2.x for some reason
494 508 msg = msg.encode('utf8', 'replace')
495 509 try:
496 510 self.request._cookies = Cookie.SimpleCookie(msg)
497 511 except:
498 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
512 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
499 513
500 514 def on_first_message(self, msg):
501 515 self._inject_cookie_message(msg)
502 516 if self.get_current_user() is None:
503 logging.warn("Couldn't authenticate WebSocket connection")
517 self.log.warn("Couldn't authenticate WebSocket connection")
504 518 raise web.HTTPError(403)
505 519 self.on_message = self.save_on_message
506 520
507 521
508 522 class IOPubHandler(AuthenticatedZMQStreamHandler):
509 523
510 524 def initialize(self, *args, **kwargs):
511 525 self.iopub_stream = None
512 526
513 527 def on_first_message(self, msg):
514 528 try:
515 529 super(IOPubHandler, self).on_first_message(msg)
516 530 except web.HTTPError:
517 531 self.close()
518 532 return
519 533 km = self.kernel_manager
520 534 kernel_id = self.kernel_id
521 535 km.add_restart_callback(kernel_id, self.on_kernel_restarted)
522 536 km.add_restart_callback(kernel_id, self.on_restart_failed, 'dead')
523 537 try:
524 538 self.iopub_stream = km.connect_iopub(kernel_id)
525 539 except web.HTTPError:
526 540 # WebSockets don't response to traditional error codes so we
527 541 # close the connection.
528 542 if not self.stream.closed():
529 543 self.stream.close()
530 544 self.close()
531 545 else:
532 546 self.iopub_stream.on_recv(self._on_zmq_reply)
533 547
534 548 def on_message(self, msg):
535 549 pass
536 550
537 551 def _send_status_message(self, status):
538 552 msg = self.session.msg("status",
539 553 {'execution_state': status}
540 554 )
541 555 self.write_message(jsonapi.dumps(msg, default=date_default))
542 556
543 557 def on_kernel_restarted(self):
544 logging.warn("kernel %s restarted", self.kernel_id)
558 self.log.warn("kernel %s restarted", self.kernel_id)
545 559 self._send_status_message('restarting')
546 560
547 561 def on_restart_failed(self):
548 logging.error("kernel %s restarted failed!", self.kernel_id)
562 self.log.error("kernel %s restarted failed!", self.kernel_id)
549 563 self._send_status_message('dead')
550 564
551 565 def on_close(self):
552 566 # This method can be called twice, once by self.kernel_died and once
553 567 # from the WebSocket close event. If the WebSocket connection is
554 568 # closed before the ZMQ streams are setup, they could be None.
555 569 km = self.kernel_manager
556 570 if self.kernel_id in km:
557 571 km.remove_restart_callback(
558 572 self.kernel_id, self.on_kernel_restarted,
559 573 )
560 574 km.remove_restart_callback(
561 575 self.kernel_id, self.on_restart_failed, 'dead',
562 576 )
563 577 if self.iopub_stream is not None and not self.iopub_stream.closed():
564 578 self.iopub_stream.on_recv(None)
565 579 self.iopub_stream.close()
566 580
567 581
568 582 class ShellHandler(AuthenticatedZMQStreamHandler):
569 583
570 584 @property
571 585 def max_msg_size(self):
572 586 return self.settings.get('max_msg_size', 65535)
573 587
574 588 def initialize(self, *args, **kwargs):
575 589 self.shell_stream = None
576 590
577 591 def on_first_message(self, msg):
578 592 try:
579 593 super(ShellHandler, self).on_first_message(msg)
580 594 except web.HTTPError:
581 595 self.close()
582 596 return
583 597 km = self.kernel_manager
584 598 kernel_id = self.kernel_id
585 599 try:
586 600 self.shell_stream = km.connect_shell(kernel_id)
587 601 except web.HTTPError:
588 602 # WebSockets don't response to traditional error codes so we
589 603 # close the connection.
590 604 if not self.stream.closed():
591 605 self.stream.close()
592 606 self.close()
593 607 else:
594 608 self.shell_stream.on_recv(self._on_zmq_reply)
595 609
596 610 def on_message(self, msg):
597 611 if len(msg) < self.max_msg_size:
598 612 msg = jsonapi.loads(msg)
599 613 self.session.send(self.shell_stream, msg)
600 614
601 615 def on_close(self):
602 616 # Make sure the stream exists and is not already closed.
603 617 if self.shell_stream is not None and not self.shell_stream.closed():
604 618 self.shell_stream.close()
605 619
606 620
607 621 #-----------------------------------------------------------------------------
608 622 # Notebook web service handlers
609 623 #-----------------------------------------------------------------------------
610 624
611 625 class NotebookRedirectHandler(IPythonHandler):
612 626
613 627 @authenticate_unless_readonly
614 628 def get(self, notebook_name):
615 629 # strip trailing .ipynb:
616 630 notebook_name = os.path.splitext(notebook_name)[0]
617 631 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
618 632 if notebook_id:
619 633 url = self.settings.get('base_project_url', '/') + notebook_id
620 634 return self.redirect(url)
621 635 else:
622 636 raise HTTPError(404)
623 637
624 638
625 639 class NotebookRootHandler(IPythonHandler):
626 640
627 641 @authenticate_unless_readonly
628 642 def get(self):
629 643 nbm = self.notebook_manager
630 644 km = self.kernel_manager
631 645 files = nbm.list_notebooks()
632 646 for f in files :
633 647 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
634 648 self.finish(jsonapi.dumps(files))
635 649
636 650 @web.authenticated
637 651 def post(self):
638 652 nbm = self.notebook_manager
639 653 body = self.request.body.strip()
640 654 format = self.get_argument('format', default='json')
641 655 name = self.get_argument('name', default=None)
642 656 if body:
643 657 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
644 658 else:
645 659 notebook_id = nbm.new_notebook()
646 660 self.set_header('Location', '/'+notebook_id)
647 661 self.finish(jsonapi.dumps(notebook_id))
648 662
649 663
650 664 class NotebookHandler(IPythonHandler):
651 665
652 666 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
653 667
654 668 @authenticate_unless_readonly
655 669 def get(self, notebook_id):
656 670 nbm = self.notebook_manager
657 671 format = self.get_argument('format', default='json')
658 672 last_mod, name, data = nbm.get_notebook(notebook_id, format)
659 673
660 674 if format == u'json':
661 675 self.set_header('Content-Type', 'application/json')
662 676 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
663 677 elif format == u'py':
664 678 self.set_header('Content-Type', 'application/x-python')
665 679 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
666 680 self.set_header('Last-Modified', last_mod)
667 681 self.finish(data)
668 682
669 683 @web.authenticated
670 684 def put(self, notebook_id):
671 685 nbm = self.notebook_manager
672 686 format = self.get_argument('format', default='json')
673 687 name = self.get_argument('name', default=None)
674 688 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
675 689 self.set_status(204)
676 690 self.finish()
677 691
678 692 @web.authenticated
679 693 def delete(self, notebook_id):
680 694 self.notebook_manager.delete_notebook(notebook_id)
681 695 self.set_status(204)
682 696 self.finish()
683 697
684 698
685 699 class NotebookCopyHandler(IPythonHandler):
686 700
687 701 @web.authenticated
688 702 def get(self, notebook_id):
689 703 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
690 704 self.redirect('/'+urljoin(self.base_project_url, notebook_id))
691 705
692 706
693 707 #-----------------------------------------------------------------------------
694 708 # Cluster handlers
695 709 #-----------------------------------------------------------------------------
696 710
697 711
698 712 class MainClusterHandler(IPythonHandler):
699 713
700 714 @web.authenticated
701 715 def get(self):
702 716 self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
703 717
704 718
705 719 class ClusterProfileHandler(IPythonHandler):
706 720
707 721 @web.authenticated
708 722 def get(self, profile):
709 723 self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
710 724
711 725
712 726 class ClusterActionHandler(IPythonHandler):
713 727
714 728 @web.authenticated
715 729 def post(self, profile, action):
716 730 cm = self.cluster_manager
717 731 if action == 'start':
718 732 n = self.get_argument('n',default=None)
719 733 if n is None:
720 734 data = cm.start_cluster(profile)
721 735 else:
722 736 data = cm.start_cluster(profile, int(n))
723 737 if action == 'stop':
724 738 data = cm.stop_cluster(profile)
725 739 self.finish(jsonapi.dumps(data))
726 740
727 741
728 742 #-----------------------------------------------------------------------------
729 743 # RST web service handlers
730 744 #-----------------------------------------------------------------------------
731 745
732 746
733 747 class RSTHandler(IPythonHandler):
734 748
735 749 @web.authenticated
736 750 def post(self):
737 751 if publish_string is None:
738 752 raise web.HTTPError(503, u'docutils not available')
739 753 body = self.request.body.strip()
740 754 source = body
741 755 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
742 756 defaults = {'file_insertion_enabled': 0,
743 757 'raw_enabled': 0,
744 758 '_disable_config': 1,
745 759 'stylesheet_path': 0
746 760 # 'template': template_path
747 761 }
748 762 try:
749 763 html = publish_string(source, writer_name='html',
750 764 settings_overrides=defaults
751 765 )
752 766 except:
753 767 raise web.HTTPError(400, u'Invalid RST')
754 768 print html
755 769 self.set_header('Content-Type', 'text/html')
756 770 self.finish(html)
757 771
758 772 # to minimize subclass changes:
759 773 HTTPError = web.HTTPError
760 774
761 775 class FileFindHandler(web.StaticFileHandler):
762 776 """subclass of StaticFileHandler for serving files from a search path"""
763 777
764 778 _static_paths = {}
765 779 # _lock is needed for tornado < 2.2.0 compat
766 780 _lock = threading.Lock() # protects _static_hashes
767 781
768 782 def initialize(self, path, default_filename=None):
769 783 if isinstance(path, basestring):
770 784 path = [path]
771 785 self.roots = tuple(
772 786 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
773 787 )
774 788 self.default_filename = default_filename
775 789
776 790 @classmethod
777 791 def locate_file(cls, path, roots):
778 792 """locate a file to serve on our static file search path"""
779 793 with cls._lock:
780 794 if path in cls._static_paths:
781 795 return cls._static_paths[path]
782 796 try:
783 797 abspath = os.path.abspath(filefind(path, roots))
784 798 except IOError:
785 799 # empty string should always give exists=False
786 800 return ''
787 801
788 802 # os.path.abspath strips a trailing /
789 803 # it needs to be temporarily added back for requests to root/
790 804 if not (abspath + os.path.sep).startswith(roots):
791 805 raise HTTPError(403, "%s is not in root static directory", path)
792 806
793 807 cls._static_paths[path] = abspath
794 808 return abspath
795 809
796 810 def get(self, path, include_body=True):
797 811 path = self.parse_url_path(path)
798 812
799 813 # begin subclass override
800 814 abspath = self.locate_file(path, self.roots)
801 815 # end subclass override
802 816
803 817 if os.path.isdir(abspath) and self.default_filename is not None:
804 818 # need to look at the request.path here for when path is empty
805 819 # but there is some prefix to the path that was already
806 820 # trimmed by the routing
807 821 if not self.request.path.endswith("/"):
808 822 self.redirect(self.request.path + "/")
809 823 return
810 824 abspath = os.path.join(abspath, self.default_filename)
811 825 if not os.path.exists(abspath):
812 826 raise HTTPError(404)
813 827 if not os.path.isfile(abspath):
814 828 raise HTTPError(403, "%s is not a file", path)
815 829
816 830 stat_result = os.stat(abspath)
817 831 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
818 832
819 833 self.set_header("Last-Modified", modified)
820 834
821 835 mime_type, encoding = mimetypes.guess_type(abspath)
822 836 if mime_type:
823 837 self.set_header("Content-Type", mime_type)
824 838
825 839 cache_time = self.get_cache_time(path, modified, mime_type)
826 840
827 841 if cache_time > 0:
828 842 self.set_header("Expires", datetime.datetime.utcnow() + \
829 843 datetime.timedelta(seconds=cache_time))
830 844 self.set_header("Cache-Control", "max-age=" + str(cache_time))
831 845 else:
832 846 self.set_header("Cache-Control", "public")
833 847
834 848 self.set_extra_headers(path)
835 849
836 850 # Check the If-Modified-Since, and don't send the result if the
837 851 # content has not been modified
838 852 ims_value = self.request.headers.get("If-Modified-Since")
839 853 if ims_value is not None:
840 854 date_tuple = email.utils.parsedate(ims_value)
841 855 if_since = datetime.datetime(*date_tuple[:6])
842 856 if if_since >= modified:
843 857 self.set_status(304)
844 858 return
845 859
846 860 with open(abspath, "rb") as file:
847 861 data = file.read()
848 862 hasher = hashlib.sha1()
849 863 hasher.update(data)
850 864 self.set_header("Etag", '"%s"' % hasher.hexdigest())
851 865 if include_body:
852 866 self.write(data)
853 867 else:
854 868 assert self.request.method == "HEAD"
855 869 self.set_header("Content-Length", len(data))
856 870
857 871 @classmethod
858 872 def get_version(cls, settings, path):
859 873 """Generate the version string to be used in static URLs.
860 874
861 875 This method may be overridden in subclasses (but note that it
862 876 is a class method rather than a static method). The default
863 877 implementation uses a hash of the file's contents.
864 878
865 879 ``settings`` is the `Application.settings` dictionary and ``path``
866 880 is the relative location of the requested asset on the filesystem.
867 881 The returned value should be a string, or ``None`` if no version
868 882 could be determined.
869 883 """
870 884 # begin subclass override:
871 885 static_paths = settings['static_path']
872 886 if isinstance(static_paths, basestring):
873 887 static_paths = [static_paths]
874 888 roots = tuple(
875 889 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
876 890 )
877 891
878 892 try:
879 893 abs_path = filefind(path, roots)
880 894 except IOError:
881 logging.error("Could not find static file %r", path)
895 app_log.error("Could not find static file %r", path)
882 896 return None
883 897
884 898 # end subclass override
885 899
886 900 with cls._lock:
887 901 hashes = cls._static_hashes
888 902 if abs_path not in hashes:
889 903 try:
890 904 f = open(abs_path, "rb")
891 905 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
892 906 f.close()
893 907 except Exception:
894 logging.error("Could not open static file %r", path)
908 app_log.error("Could not open static file %r", path)
895 909 hashes[abs_path] = None
896 910 hsh = hashes.get(abs_path)
897 911 if hsh:
898 912 return hsh[:5]
899 913 return None
900 914
901 915
902 916 def parse_url_path(self, url_path):
903 917 """Converts a static URL path into a filesystem path.
904 918
905 919 ``url_path`` is the path component of the URL with
906 920 ``static_url_prefix`` removed. The return value should be
907 921 filesystem path relative to ``static_path``.
908 922 """
909 923 if os.path.sep != "/":
910 924 url_path = url_path.replace("/", os.path.sep)
911 925 return url_path
912 926
913 927
General Comments 0
You need to be logged in to leave comments. Login now