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