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