##// END OF EJS Templates
Add kernel name to sessions REST API...
Thomas Kluyver -
Show More
@@ -1,928 +1,930 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import errno
11 11 import io
12 12 import json
13 13 import logging
14 14 import os
15 15 import random
16 16 import re
17 17 import select
18 18 import signal
19 19 import socket
20 20 import sys
21 21 import threading
22 22 import time
23 23 import webbrowser
24 24
25 25
26 26 # check for pyzmq 2.1.11
27 27 from IPython.utils.zmqrelated import check_for_zmq
28 28 check_for_zmq('2.1.11', 'IPython.html')
29 29
30 30 from jinja2 import Environment, FileSystemLoader
31 31
32 32 # Install the pyzmq ioloop. This has to be done before anything else from
33 33 # tornado is imported.
34 34 from zmq.eventloop import ioloop
35 35 ioloop.install()
36 36
37 37 # check for tornado 3.1.0
38 38 msg = "The IPython Notebook requires tornado >= 3.1.0"
39 39 try:
40 40 import tornado
41 41 except ImportError:
42 42 raise ImportError(msg)
43 43 try:
44 44 version_info = tornado.version_info
45 45 except AttributeError:
46 46 raise ImportError(msg + ", but you have < 1.1.0")
47 47 if version_info < (3,1,0):
48 48 raise ImportError(msg + ", but you have %s" % tornado.version)
49 49
50 50 from tornado import httpserver
51 51 from tornado import web
52 52 from tornado.log import LogFormatter
53 53
54 54 from IPython.html import DEFAULT_STATIC_FILES_PATH
55 55 from .base.handlers import Template404
56 56 from .log import log_request
57 57 from .services.kernels.kernelmanager import MappingKernelManager
58 58 from .services.notebooks.nbmanager import NotebookManager
59 59 from .services.notebooks.filenbmanager import FileNotebookManager
60 60 from .services.clusters.clustermanager import ClusterManager
61 61 from .services.sessions.sessionmanager import SessionManager
62 62
63 63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
64 64
65 65 from IPython.config import Config
66 66 from IPython.config.application import catch_config_error, boolean_flag
67 67 from IPython.core.application import (
68 68 BaseIPythonApplication, base_flags, base_aliases,
69 69 )
70 70 from IPython.core.profiledir import ProfileDir
71 71 from IPython.kernel import KernelManager
72 72 from IPython.kernel.kernelspec import KernelSpecManager
73 73 from IPython.kernel.zmq.session import default_secure, Session
74 74 from IPython.nbformat.sign import NotebookNotary
75 75 from IPython.utils.importstring import import_item
76 76 from IPython.utils import submodule
77 77 from IPython.utils.traitlets import (
78 78 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
79 79 DottedObjectName, TraitError,
80 80 )
81 81 from IPython.utils import py3compat
82 82 from IPython.utils.path import filefind, get_ipython_dir
83 83
84 84 from .utils import url_path_join
85 85
86 86 #-----------------------------------------------------------------------------
87 87 # Module globals
88 88 #-----------------------------------------------------------------------------
89 89
90 90 _examples = """
91 91 ipython notebook # start the notebook
92 92 ipython notebook --profile=sympy # use the sympy profile
93 93 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
94 94 """
95 95
96 96 #-----------------------------------------------------------------------------
97 97 # Helper functions
98 98 #-----------------------------------------------------------------------------
99 99
100 100 def random_ports(port, n):
101 101 """Generate a list of n random ports near the given port.
102 102
103 103 The first 5 ports will be sequential, and the remaining n-5 will be
104 104 randomly selected in the range [port-2*n, port+2*n].
105 105 """
106 106 for i in range(min(5, n)):
107 107 yield port + i
108 108 for i in range(n-5):
109 109 yield max(1, port + random.randint(-2*n, 2*n))
110 110
111 111 def load_handlers(name):
112 112 """Load the (URL pattern, handler) tuples for each component."""
113 113 name = 'IPython.html.' + name
114 114 mod = __import__(name, fromlist=['default_handlers'])
115 115 return mod.default_handlers
116 116
117 117 #-----------------------------------------------------------------------------
118 118 # The Tornado web application
119 119 #-----------------------------------------------------------------------------
120 120
121 121 class NotebookWebApplication(web.Application):
122 122
123 123 def __init__(self, ipython_app, kernel_manager, notebook_manager,
124 124 cluster_manager, session_manager, kernel_spec_manager, log,
125 125 base_url, settings_overrides, jinja_env_options):
126 126
127 127 settings = self.init_settings(
128 128 ipython_app, kernel_manager, notebook_manager, cluster_manager,
129 129 session_manager, kernel_spec_manager, log, base_url,
130 130 settings_overrides, jinja_env_options)
131 131 handlers = self.init_handlers(settings)
132 132
133 133 super(NotebookWebApplication, self).__init__(handlers, **settings)
134 134
135 135 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
136 136 cluster_manager, session_manager, kernel_spec_manager,
137 137 log, base_url, settings_overrides,
138 138 jinja_env_options=None):
139 139 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
140 140 # base_url will always be unicode, which will in turn
141 141 # make the patterns unicode, and ultimately result in unicode
142 142 # keys in kwargs to handler._execute(**kwargs) in tornado.
143 143 # This enforces that base_url be ascii in that situation.
144 144 #
145 145 # Note that the URLs these patterns check against are escaped,
146 146 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
147 147 base_url = py3compat.unicode_to_str(base_url, 'ascii')
148 148 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
149 149 jenv_opt = jinja_env_options if jinja_env_options else {}
150 150 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
151 151 settings = dict(
152 152 # basics
153 153 log_function=log_request,
154 154 base_url=base_url,
155 155 template_path=template_path,
156 156 static_path=ipython_app.static_file_path,
157 157 static_handler_class = FileFindHandler,
158 158 static_url_prefix = url_path_join(base_url,'/static/'),
159 159
160 160 # authentication
161 161 cookie_secret=ipython_app.cookie_secret,
162 162 login_url=url_path_join(base_url,'/login'),
163 163 password=ipython_app.password,
164 164
165 165 # managers
166 166 kernel_manager=kernel_manager,
167 167 notebook_manager=notebook_manager,
168 168 cluster_manager=cluster_manager,
169 169 session_manager=session_manager,
170 170 kernel_spec_manager=kernel_spec_manager,
171 171
172 172 # IPython stuff
173 173 nbextensions_path = ipython_app.nbextensions_path,
174 174 mathjax_url=ipython_app.mathjax_url,
175 175 config=ipython_app.config,
176 176 jinja2_env=env,
177 177 )
178 178
179 179 # allow custom overrides for the tornado web app.
180 180 settings.update(settings_overrides)
181 181 return settings
182 182
183 183 def init_handlers(self, settings):
184 184 # Load the (URL pattern, handler) tuples for each component.
185 185 handlers = []
186 186 handlers.extend(load_handlers('base.handlers'))
187 187 handlers.extend(load_handlers('tree.handlers'))
188 188 handlers.extend(load_handlers('auth.login'))
189 189 handlers.extend(load_handlers('auth.logout'))
190 190 handlers.extend(load_handlers('notebook.handlers'))
191 191 handlers.extend(load_handlers('nbconvert.handlers'))
192 192 handlers.extend(load_handlers('kernelspecs.handlers'))
193 193 handlers.extend(load_handlers('services.kernels.handlers'))
194 194 handlers.extend(load_handlers('services.notebooks.handlers'))
195 195 handlers.extend(load_handlers('services.clusters.handlers'))
196 196 handlers.extend(load_handlers('services.sessions.handlers'))
197 197 handlers.extend(load_handlers('services.nbconvert.handlers'))
198 198 handlers.extend(load_handlers('services.kernelspecs.handlers'))
199 199 # FIXME: /files/ should be handled by the Contents service when it exists
200 200 nbm = settings['notebook_manager']
201 201 if hasattr(nbm, 'notebook_dir'):
202 202 handlers.extend([
203 203 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
204 204 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
205 205 ])
206 206 # prepend base_url onto the patterns that we match
207 207 new_handlers = []
208 208 for handler in handlers:
209 209 pattern = url_path_join(settings['base_url'], handler[0])
210 210 new_handler = tuple([pattern] + list(handler[1:]))
211 211 new_handlers.append(new_handler)
212 212 # add 404 on the end, which will catch everything that falls through
213 213 new_handlers.append((r'(.*)', Template404))
214 214 return new_handlers
215 215
216 216
217 217 class NbserverListApp(BaseIPythonApplication):
218 218
219 219 description="List currently running notebook servers in this profile."
220 220
221 221 flags = dict(
222 222 json=({'NbserverListApp': {'json': True}},
223 223 "Produce machine-readable JSON output."),
224 224 )
225 225
226 226 json = Bool(False, config=True,
227 227 help="If True, each line of output will be a JSON object with the "
228 228 "details from the server info file.")
229 229
230 230 def start(self):
231 231 if not self.json:
232 232 print("Currently running servers:")
233 233 for serverinfo in list_running_servers(self.profile):
234 234 if self.json:
235 235 print(json.dumps(serverinfo))
236 236 else:
237 237 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
238 238
239 239 #-----------------------------------------------------------------------------
240 240 # Aliases and Flags
241 241 #-----------------------------------------------------------------------------
242 242
243 243 flags = dict(base_flags)
244 244 flags['no-browser']=(
245 245 {'NotebookApp' : {'open_browser' : False}},
246 246 "Don't open the notebook in a browser after startup."
247 247 )
248 248 flags['pylab']=(
249 249 {'NotebookApp' : {'pylab' : 'warn'}},
250 250 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
251 251 )
252 252 flags['no-mathjax']=(
253 253 {'NotebookApp' : {'enable_mathjax' : False}},
254 254 """Disable MathJax
255 255
256 256 MathJax is the javascript library IPython uses to render math/LaTeX. It is
257 257 very large, so you may want to disable it if you have a slow internet
258 258 connection, or for offline use of the notebook.
259 259
260 260 When disabled, equations etc. will appear as their untransformed TeX source.
261 261 """
262 262 )
263 263
264 264 # Add notebook manager flags
265 265 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
266 266 'Auto-save a .py script everytime the .ipynb notebook is saved',
267 267 'Do not auto-save .py scripts for every notebook'))
268 268
269 269 aliases = dict(base_aliases)
270 270
271 271 aliases.update({
272 272 'ip': 'NotebookApp.ip',
273 273 'port': 'NotebookApp.port',
274 274 'port-retries': 'NotebookApp.port_retries',
275 275 'transport': 'KernelManager.transport',
276 276 'keyfile': 'NotebookApp.keyfile',
277 277 'certfile': 'NotebookApp.certfile',
278 278 'notebook-dir': 'NotebookApp.notebook_dir',
279 279 'browser': 'NotebookApp.browser',
280 280 'pylab': 'NotebookApp.pylab',
281 281 })
282 282
283 283 #-----------------------------------------------------------------------------
284 284 # NotebookApp
285 285 #-----------------------------------------------------------------------------
286 286
287 287 class NotebookApp(BaseIPythonApplication):
288 288
289 289 name = 'ipython-notebook'
290 290
291 291 description = """
292 292 The IPython HTML Notebook.
293 293
294 294 This launches a Tornado based HTML Notebook Server that serves up an
295 295 HTML5/Javascript Notebook client.
296 296 """
297 297 examples = _examples
298 298 aliases = aliases
299 299 flags = flags
300 300
301 301 classes = [
302 302 KernelManager, ProfileDir, Session, MappingKernelManager,
303 303 NotebookManager, FileNotebookManager, NotebookNotary,
304 304 ]
305 305 flags = Dict(flags)
306 306 aliases = Dict(aliases)
307 307
308 308 subcommands = dict(
309 309 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
310 310 )
311 311
312 312 kernel_argv = List(Unicode)
313 313
314 314 _log_formatter_cls = LogFormatter
315 315
316 316 def _log_level_default(self):
317 317 return logging.INFO
318 318
319 319 def _log_datefmt_default(self):
320 320 """Exclude date from default date format"""
321 321 return "%H:%M:%S"
322 322
323 323 def _log_format_default(self):
324 324 """override default log format to include time"""
325 325 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
326 326
327 327 # create requested profiles by default, if they don't exist:
328 328 auto_create = Bool(True)
329 329
330 330 # file to be opened in the notebook server
331 331 file_to_run = Unicode('', config=True)
332 332 def _file_to_run_changed(self, name, old, new):
333 333 path, base = os.path.split(new)
334 334 if path:
335 335 self.file_to_run = base
336 336 self.notebook_dir = path
337 337
338 338 # Network related information
339 339
340 340 allow_origin = Unicode('', config=True,
341 341 help="""Set the Access-Control-Allow-Origin header
342 342
343 343 Use '*' to allow any origin to access your server.
344 344
345 345 Takes precedence over allow_origin_pat.
346 346 """
347 347 )
348 348
349 349 allow_origin_pat = Unicode('', config=True,
350 350 help="""Use a regular expression for the Access-Control-Allow-Origin header
351 351
352 352 Requests from an origin matching the expression will get replies with:
353 353
354 354 Access-Control-Allow-Origin: origin
355 355
356 356 where `origin` is the origin of the request.
357 357
358 358 Ignored if allow_origin is set.
359 359 """
360 360 )
361 361
362 362 allow_credentials = Bool(False, config=True,
363 363 help="Set the Access-Control-Allow-Credentials: true header"
364 364 )
365 365
366 366 ip = Unicode('localhost', config=True,
367 367 help="The IP address the notebook server will listen on."
368 368 )
369 369
370 370 def _ip_changed(self, name, old, new):
371 371 if new == u'*': self.ip = u''
372 372
373 373 port = Integer(8888, config=True,
374 374 help="The port the notebook server will listen on."
375 375 )
376 376 port_retries = Integer(50, config=True,
377 377 help="The number of additional ports to try if the specified port is not available."
378 378 )
379 379
380 380 certfile = Unicode(u'', config=True,
381 381 help="""The full path to an SSL/TLS certificate file."""
382 382 )
383 383
384 384 keyfile = Unicode(u'', config=True,
385 385 help="""The full path to a private key file for usage with SSL/TLS."""
386 386 )
387 387
388 388 cookie_secret_file = Unicode(config=True,
389 389 help="""The file where the cookie secret is stored."""
390 390 )
391 391 def _cookie_secret_file_default(self):
392 392 if self.profile_dir is None:
393 393 return ''
394 394 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
395 395
396 396 cookie_secret = Bytes(b'', config=True,
397 397 help="""The random bytes used to secure cookies.
398 398 By default this is a new random number every time you start the Notebook.
399 399 Set it to a value in a config file to enable logins to persist across server sessions.
400 400
401 401 Note: Cookie secrets should be kept private, do not share config files with
402 402 cookie_secret stored in plaintext (you can read the value from a file).
403 403 """
404 404 )
405 405 def _cookie_secret_default(self):
406 406 if os.path.exists(self.cookie_secret_file):
407 407 with io.open(self.cookie_secret_file, 'rb') as f:
408 408 return f.read()
409 409 else:
410 410 secret = base64.encodestring(os.urandom(1024))
411 411 self._write_cookie_secret_file(secret)
412 412 return secret
413 413
414 414 def _write_cookie_secret_file(self, secret):
415 415 """write my secret to my secret_file"""
416 416 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
417 417 with io.open(self.cookie_secret_file, 'wb') as f:
418 418 f.write(secret)
419 419 try:
420 420 os.chmod(self.cookie_secret_file, 0o600)
421 421 except OSError:
422 422 self.log.warn(
423 423 "Could not set permissions on %s",
424 424 self.cookie_secret_file
425 425 )
426 426
427 427 password = Unicode(u'', config=True,
428 428 help="""Hashed password to use for web authentication.
429 429
430 430 To generate, type in a python/IPython shell:
431 431
432 432 from IPython.lib import passwd; passwd()
433 433
434 434 The string should be of the form type:salt:hashed-password.
435 435 """
436 436 )
437 437
438 438 open_browser = Bool(True, config=True,
439 439 help="""Whether to open in a browser after starting.
440 440 The specific browser used is platform dependent and
441 441 determined by the python standard library `webbrowser`
442 442 module, unless it is overridden using the --browser
443 443 (NotebookApp.browser) configuration option.
444 444 """)
445 445
446 446 browser = Unicode(u'', config=True,
447 447 help="""Specify what command to use to invoke a web
448 448 browser when opening the notebook. If not specified, the
449 449 default browser will be determined by the `webbrowser`
450 450 standard library module, which allows setting of the
451 451 BROWSER environment variable to override it.
452 452 """)
453 453
454 454 webapp_settings = Dict(config=True,
455 455 help="Supply overrides for the tornado.web.Application that the "
456 456 "IPython notebook uses.")
457 457
458 458 jinja_environment_options = Dict(config=True,
459 459 help="Supply extra arguments that will be passed to Jinja environment.")
460 460
461 461
462 462 enable_mathjax = Bool(True, config=True,
463 463 help="""Whether to enable MathJax for typesetting math/TeX
464 464
465 465 MathJax is the javascript library IPython uses to render math/LaTeX. It is
466 466 very large, so you may want to disable it if you have a slow internet
467 467 connection, or for offline use of the notebook.
468 468
469 469 When disabled, equations etc. will appear as their untransformed TeX source.
470 470 """
471 471 )
472 472 def _enable_mathjax_changed(self, name, old, new):
473 473 """set mathjax url to empty if mathjax is disabled"""
474 474 if not new:
475 475 self.mathjax_url = u''
476 476
477 477 base_url = Unicode('/', config=True,
478 478 help='''The base URL for the notebook server.
479 479
480 480 Leading and trailing slashes can be omitted,
481 481 and will automatically be added.
482 482 ''')
483 483 def _base_url_changed(self, name, old, new):
484 484 if not new.startswith('/'):
485 485 self.base_url = '/'+new
486 486 elif not new.endswith('/'):
487 487 self.base_url = new+'/'
488 488
489 489 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
490 490 def _base_project_url_changed(self, name, old, new):
491 491 self.log.warn("base_project_url is deprecated, use base_url")
492 492 self.base_url = new
493 493
494 494 extra_static_paths = List(Unicode, config=True,
495 495 help="""Extra paths to search for serving static files.
496 496
497 497 This allows adding javascript/css to be available from the notebook server machine,
498 498 or overriding individual files in the IPython"""
499 499 )
500 500 def _extra_static_paths_default(self):
501 501 return [os.path.join(self.profile_dir.location, 'static')]
502 502
503 503 @property
504 504 def static_file_path(self):
505 505 """return extra paths + the default location"""
506 506 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
507 507
508 508 nbextensions_path = List(Unicode, config=True,
509 509 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
510 510 )
511 511 def _nbextensions_path_default(self):
512 512 return [os.path.join(get_ipython_dir(), 'nbextensions')]
513 513
514 514 mathjax_url = Unicode("", config=True,
515 515 help="""The url for MathJax.js."""
516 516 )
517 517 def _mathjax_url_default(self):
518 518 if not self.enable_mathjax:
519 519 return u''
520 520 static_url_prefix = self.webapp_settings.get("static_url_prefix",
521 521 url_path_join(self.base_url, "static")
522 522 )
523 523
524 524 # try local mathjax, either in nbextensions/mathjax or static/mathjax
525 525 for (url_prefix, search_path) in [
526 526 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
527 527 (static_url_prefix, self.static_file_path),
528 528 ]:
529 529 self.log.debug("searching for local mathjax in %s", search_path)
530 530 try:
531 531 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
532 532 except IOError:
533 533 continue
534 534 else:
535 535 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
536 536 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
537 537 return url
538 538
539 539 # no local mathjax, serve from CDN
540 540 if self.certfile:
541 541 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
542 542 host = u"https://c328740.ssl.cf1.rackcdn.com"
543 543 else:
544 544 host = u"http://cdn.mathjax.org"
545 545
546 546 url = host + u"/mathjax/latest/MathJax.js"
547 547 self.log.info("Using MathJax from CDN: %s", url)
548 548 return url
549 549
550 550 def _mathjax_url_changed(self, name, old, new):
551 551 if new and not self.enable_mathjax:
552 552 # enable_mathjax=False overrides mathjax_url
553 553 self.mathjax_url = u''
554 554 else:
555 555 self.log.info("Using MathJax: %s", new)
556 556
557 557 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
558 558 config=True,
559 559 help='The notebook manager class to use.'
560 560 )
561 561 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
562 562 config=True,
563 563 help='The kernel manager class to use.'
564 564 )
565 565 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
566 566 config=True,
567 567 help='The session manager class to use.'
568 568 )
569 569 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
570 570 config=True,
571 571 help='The cluster manager class to use.'
572 572 )
573 573
574 574 kernel_spec_manager = Instance(KernelSpecManager)
575 575
576 576 def _kernel_spec_manager_default(self):
577 577 return KernelSpecManager(ipython_dir=self.ipython_dir)
578 578
579 579 trust_xheaders = Bool(False, config=True,
580 580 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
581 581 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
582 582 )
583 583
584 584 info_file = Unicode()
585 585
586 586 def _info_file_default(self):
587 587 info_file = "nbserver-%s.json"%os.getpid()
588 588 return os.path.join(self.profile_dir.security_dir, info_file)
589 589
590 590 notebook_dir = Unicode(py3compat.getcwd(), config=True,
591 591 help="The directory to use for notebooks and kernels."
592 592 )
593 593
594 594 pylab = Unicode('disabled', config=True,
595 595 help="""
596 596 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
597 597 """
598 598 )
599 599 def _pylab_changed(self, name, old, new):
600 600 """when --pylab is specified, display a warning and exit"""
601 601 if new != 'warn':
602 602 backend = ' %s' % new
603 603 else:
604 604 backend = ''
605 605 self.log.error("Support for specifying --pylab on the command line has been removed.")
606 606 self.log.error(
607 607 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
608 608 )
609 609 self.exit(1)
610 610
611 611 def _notebook_dir_changed(self, name, old, new):
612 612 """Do a bit of validation of the notebook dir."""
613 613 if not os.path.isabs(new):
614 614 # If we receive a non-absolute path, make it absolute.
615 615 self.notebook_dir = os.path.abspath(new)
616 616 return
617 617 if not os.path.isdir(new):
618 618 raise TraitError("No such notebook dir: %r" % new)
619 619
620 620 # setting App.notebook_dir implies setting notebook and kernel dirs as well
621 621 self.config.FileNotebookManager.notebook_dir = new
622 622 self.config.MappingKernelManager.root_dir = new
623 623
624 624
625 625 def parse_command_line(self, argv=None):
626 626 super(NotebookApp, self).parse_command_line(argv)
627 627
628 628 if self.extra_args:
629 629 arg0 = self.extra_args[0]
630 630 f = os.path.abspath(arg0)
631 631 self.argv.remove(arg0)
632 632 if not os.path.exists(f):
633 633 self.log.critical("No such file or directory: %s", f)
634 634 self.exit(1)
635 635
636 636 # Use config here, to ensure that it takes higher priority than
637 637 # anything that comes from the profile.
638 638 c = Config()
639 639 if os.path.isdir(f):
640 640 c.NotebookApp.notebook_dir = f
641 641 elif os.path.isfile(f):
642 642 c.NotebookApp.file_to_run = f
643 643 self.update_config(c)
644 644
645 645 def init_kernel_argv(self):
646 646 """construct the kernel arguments"""
647 647 # Kernel should get *absolute* path to profile directory
648 648 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
649 649
650 650 def init_configurables(self):
651 651 # force Session default to be secure
652 652 default_secure(self.config)
653 653 kls = import_item(self.kernel_manager_class)
654 654 self.kernel_manager = kls(
655 655 parent=self, log=self.log, kernel_argv=self.kernel_argv,
656 656 connection_dir = self.profile_dir.security_dir,
657 657 )
658 658 kls = import_item(self.notebook_manager_class)
659 659 self.notebook_manager = kls(parent=self, log=self.log)
660 660 kls = import_item(self.session_manager_class)
661 self.session_manager = kls(parent=self, log=self.log)
661 self.session_manager = kls(parent=self, log=self.log,
662 kernel_manager=self.kernel_manager,
663 notebook_manager=self.notebook_manager)
662 664 kls = import_item(self.cluster_manager_class)
663 665 self.cluster_manager = kls(parent=self, log=self.log)
664 666 self.cluster_manager.update_profiles()
665 667
666 668 def init_logging(self):
667 669 # This prevents double log messages because tornado use a root logger that
668 670 # self.log is a child of. The logging module dipatches log messages to a log
669 671 # and all of its ancenstors until propagate is set to False.
670 672 self.log.propagate = False
671 673
672 674 # hook up tornado 3's loggers to our app handlers
673 675 logger = logging.getLogger('tornado')
674 676 logger.propagate = True
675 677 logger.parent = self.log
676 678 logger.setLevel(self.log.level)
677 679
678 680 def init_webapp(self):
679 681 """initialize tornado webapp and httpserver"""
680 682 self.webapp_settings['allow_origin'] = self.allow_origin
681 683 if self.allow_origin_pat:
682 684 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
683 685 self.webapp_settings['allow_credentials'] = self.allow_credentials
684 686
685 687 self.web_app = NotebookWebApplication(
686 688 self, self.kernel_manager, self.notebook_manager,
687 689 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
688 690 self.log, self.base_url, self.webapp_settings,
689 691 self.jinja_environment_options
690 692 )
691 693 if self.certfile:
692 694 ssl_options = dict(certfile=self.certfile)
693 695 if self.keyfile:
694 696 ssl_options['keyfile'] = self.keyfile
695 697 else:
696 698 ssl_options = None
697 699 self.web_app.password = self.password
698 700 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
699 701 xheaders=self.trust_xheaders)
700 702 if not self.ip:
701 703 warning = "WARNING: The notebook server is listening on all IP addresses"
702 704 if ssl_options is None:
703 705 self.log.critical(warning + " and not using encryption. This "
704 706 "is not recommended.")
705 707 if not self.password:
706 708 self.log.critical(warning + " and not using authentication. "
707 709 "This is highly insecure and not recommended.")
708 710 success = None
709 711 for port in random_ports(self.port, self.port_retries+1):
710 712 try:
711 713 self.http_server.listen(port, self.ip)
712 714 except socket.error as e:
713 715 if e.errno == errno.EADDRINUSE:
714 716 self.log.info('The port %i is already in use, trying another random port.' % port)
715 717 continue
716 718 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
717 719 self.log.warn("Permission to listen on port %i denied" % port)
718 720 continue
719 721 else:
720 722 raise
721 723 else:
722 724 self.port = port
723 725 success = True
724 726 break
725 727 if not success:
726 728 self.log.critical('ERROR: the notebook server could not be started because '
727 729 'no available port could be found.')
728 730 self.exit(1)
729 731
730 732 @property
731 733 def display_url(self):
732 734 ip = self.ip if self.ip else '[all ip addresses on your system]'
733 735 return self._url(ip)
734 736
735 737 @property
736 738 def connection_url(self):
737 739 ip = self.ip if self.ip else 'localhost'
738 740 return self._url(ip)
739 741
740 742 def _url(self, ip):
741 743 proto = 'https' if self.certfile else 'http'
742 744 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
743 745
744 746 def init_signal(self):
745 747 if not sys.platform.startswith('win'):
746 748 signal.signal(signal.SIGINT, self._handle_sigint)
747 749 signal.signal(signal.SIGTERM, self._signal_stop)
748 750 if hasattr(signal, 'SIGUSR1'):
749 751 # Windows doesn't support SIGUSR1
750 752 signal.signal(signal.SIGUSR1, self._signal_info)
751 753 if hasattr(signal, 'SIGINFO'):
752 754 # only on BSD-based systems
753 755 signal.signal(signal.SIGINFO, self._signal_info)
754 756
755 757 def _handle_sigint(self, sig, frame):
756 758 """SIGINT handler spawns confirmation dialog"""
757 759 # register more forceful signal handler for ^C^C case
758 760 signal.signal(signal.SIGINT, self._signal_stop)
759 761 # request confirmation dialog in bg thread, to avoid
760 762 # blocking the App
761 763 thread = threading.Thread(target=self._confirm_exit)
762 764 thread.daemon = True
763 765 thread.start()
764 766
765 767 def _restore_sigint_handler(self):
766 768 """callback for restoring original SIGINT handler"""
767 769 signal.signal(signal.SIGINT, self._handle_sigint)
768 770
769 771 def _confirm_exit(self):
770 772 """confirm shutdown on ^C
771 773
772 774 A second ^C, or answering 'y' within 5s will cause shutdown,
773 775 otherwise original SIGINT handler will be restored.
774 776
775 777 This doesn't work on Windows.
776 778 """
777 779 info = self.log.info
778 780 info('interrupted')
779 781 print(self.notebook_info())
780 782 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
781 783 sys.stdout.flush()
782 784 r,w,x = select.select([sys.stdin], [], [], 5)
783 785 if r:
784 786 line = sys.stdin.readline()
785 787 if line.lower().startswith('y') and 'n' not in line.lower():
786 788 self.log.critical("Shutdown confirmed")
787 789 ioloop.IOLoop.instance().stop()
788 790 return
789 791 else:
790 792 print("No answer for 5s:", end=' ')
791 793 print("resuming operation...")
792 794 # no answer, or answer is no:
793 795 # set it back to original SIGINT handler
794 796 # use IOLoop.add_callback because signal.signal must be called
795 797 # from main thread
796 798 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
797 799
798 800 def _signal_stop(self, sig, frame):
799 801 self.log.critical("received signal %s, stopping", sig)
800 802 ioloop.IOLoop.instance().stop()
801 803
802 804 def _signal_info(self, sig, frame):
803 805 print(self.notebook_info())
804 806
805 807 def init_components(self):
806 808 """Check the components submodule, and warn if it's unclean"""
807 809 status = submodule.check_submodule_status()
808 810 if status == 'missing':
809 811 self.log.warn("components submodule missing, running `git submodule update`")
810 812 submodule.update_submodules(submodule.ipython_parent())
811 813 elif status == 'unclean':
812 814 self.log.warn("components submodule unclean, you may see 404s on static/components")
813 815 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
814 816
815 817 @catch_config_error
816 818 def initialize(self, argv=None):
817 819 super(NotebookApp, self).initialize(argv)
818 820 self.init_logging()
819 821 self.init_kernel_argv()
820 822 self.init_configurables()
821 823 self.init_components()
822 824 self.init_webapp()
823 825 self.init_signal()
824 826
825 827 def cleanup_kernels(self):
826 828 """Shutdown all kernels.
827 829
828 830 The kernels will shutdown themselves when this process no longer exists,
829 831 but explicit shutdown allows the KernelManagers to cleanup the connection files.
830 832 """
831 833 self.log.info('Shutting down kernels')
832 834 self.kernel_manager.shutdown_all()
833 835
834 836 def notebook_info(self):
835 837 "Return the current working directory and the server url information"
836 838 info = self.notebook_manager.info_string() + "\n"
837 839 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
838 840 return info + "The IPython Notebook is running at: %s" % self.display_url
839 841
840 842 def server_info(self):
841 843 """Return a JSONable dict of information about this server."""
842 844 return {'url': self.connection_url,
843 845 'hostname': self.ip if self.ip else 'localhost',
844 846 'port': self.port,
845 847 'secure': bool(self.certfile),
846 848 'base_url': self.base_url,
847 849 'notebook_dir': os.path.abspath(self.notebook_dir),
848 850 }
849 851
850 852 def write_server_info_file(self):
851 853 """Write the result of server_info() to the JSON file info_file."""
852 854 with open(self.info_file, 'w') as f:
853 855 json.dump(self.server_info(), f, indent=2)
854 856
855 857 def remove_server_info_file(self):
856 858 """Remove the nbserver-<pid>.json file created for this server.
857 859
858 860 Ignores the error raised when the file has already been removed.
859 861 """
860 862 try:
861 863 os.unlink(self.info_file)
862 864 except OSError as e:
863 865 if e.errno != errno.ENOENT:
864 866 raise
865 867
866 868 def start(self):
867 869 """ Start the IPython Notebook server app, after initialization
868 870
869 871 This method takes no arguments so all configuration and initialization
870 872 must be done prior to calling this method."""
871 873 if self.subapp is not None:
872 874 return self.subapp.start()
873 875
874 876 info = self.log.info
875 877 for line in self.notebook_info().split("\n"):
876 878 info(line)
877 879 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
878 880
879 881 self.write_server_info_file()
880 882
881 883 if self.open_browser or self.file_to_run:
882 884 try:
883 885 browser = webbrowser.get(self.browser or None)
884 886 except webbrowser.Error as e:
885 887 self.log.warn('No web browser found: %s.' % e)
886 888 browser = None
887 889
888 890 if self.file_to_run:
889 891 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
890 892 if not os.path.exists(fullpath):
891 893 self.log.critical("%s does not exist" % fullpath)
892 894 self.exit(1)
893 895
894 896 uri = url_path_join('notebooks', self.file_to_run)
895 897 else:
896 898 uri = 'tree'
897 899 if browser:
898 900 b = lambda : browser.open(url_path_join(self.connection_url, uri),
899 901 new=2)
900 902 threading.Thread(target=b).start()
901 903 try:
902 904 ioloop.IOLoop.instance().start()
903 905 except KeyboardInterrupt:
904 906 info("Interrupted...")
905 907 finally:
906 908 self.cleanup_kernels()
907 909 self.remove_server_info_file()
908 910
909 911
910 912 def list_running_servers(profile='default'):
911 913 """Iterate over the server info files of running notebook servers.
912 914
913 915 Given a profile name, find nbserver-* files in the security directory of
914 916 that profile, and yield dicts of their information, each one pertaining to
915 917 a currently running notebook server instance.
916 918 """
917 919 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
918 920 for file in os.listdir(pd.security_dir):
919 921 if file.startswith('nbserver-'):
920 922 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
921 923 yield json.load(f)
922 924
923 925 #-----------------------------------------------------------------------------
924 926 # Main entry point
925 927 #-----------------------------------------------------------------------------
926 928
927 929 launch_new_instance = NotebookApp.launch_instance
928 930
@@ -1,129 +1,127 b''
1 1 """Tornado handlers for the sessions web service.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 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 json
20 20
21 21 from tornado import web
22 22
23 23 from ...base.handlers import IPythonHandler, json_errors
24 24 from IPython.utils.jsonutil import date_default
25 25 from IPython.html.utils import url_path_join, url_escape
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Session web service handlers
29 29 #-----------------------------------------------------------------------------
30 30
31 31
32 32 class SessionRootHandler(IPythonHandler):
33 33
34 34 @web.authenticated
35 35 @json_errors
36 36 def get(self):
37 37 # Return a list of running sessions
38 38 sm = self.session_manager
39 39 sessions = sm.list_sessions()
40 40 self.finish(json.dumps(sessions, default=date_default))
41 41
42 42 @web.authenticated
43 43 @json_errors
44 44 def post(self):
45 45 # Creates a new session
46 46 #(unless a session already exists for the named nb)
47 47 sm = self.session_manager
48 nbm = self.notebook_manager
49 km = self.kernel_manager
48
50 49 model = self.get_json_body()
51 50 if model is None:
52 51 raise web.HTTPError(400, "No JSON data provided")
53 52 try:
54 53 name = model['notebook']['name']
55 54 except KeyError:
56 raise web.HTTPError(400, "Missing field in JSON data: name")
55 raise web.HTTPError(400, "Missing field in JSON data: notebook.name")
57 56 try:
58 57 path = model['notebook']['path']
59 58 except KeyError:
60 raise web.HTTPError(400, "Missing field in JSON data: path")
59 raise web.HTTPError(400, "Missing field in JSON data: notebook.path")
60 try:
61 kernel_name = model['kernel']['name']
62 except KeyError:
63 raise web.HTTPError(400, "Missing field in JSON data: kernel.name")
64
61 65 # Check to see if session exists
62 66 if sm.session_exists(name=name, path=path):
63 67 model = sm.get_session(name=name, path=path)
64 68 else:
65 # allow nbm to specify kernels cwd
66 kernel_path = nbm.get_kernel_path(name=name, path=path)
67 kernel_id = km.start_kernel(path=kernel_path)
68 model = sm.create_session(name=name, path=path, kernel_id=kernel_id)
69 model = sm.create_session(name=name, path=path, kernel_name=kernel_name)
69 70 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
70 71 self.set_header('Location', url_escape(location))
71 72 self.set_status(201)
72 73 self.finish(json.dumps(model, default=date_default))
73 74
74 75 class SessionHandler(IPythonHandler):
75 76
76 77 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
77 78
78 79 @web.authenticated
79 80 @json_errors
80 81 def get(self, session_id):
81 82 # Returns the JSON model for a single session
82 83 sm = self.session_manager
83 84 model = sm.get_session(session_id=session_id)
84 85 self.finish(json.dumps(model, default=date_default))
85 86
86 87 @web.authenticated
87 88 @json_errors
88 89 def patch(self, session_id):
89 90 # Currently, this handler is strictly for renaming notebooks
90 91 sm = self.session_manager
91 92 model = self.get_json_body()
92 93 if model is None:
93 94 raise web.HTTPError(400, "No JSON data provided")
94 95 changes = {}
95 96 if 'notebook' in model:
96 97 notebook = model['notebook']
97 98 if 'name' in notebook:
98 99 changes['name'] = notebook['name']
99 100 if 'path' in notebook:
100 101 changes['path'] = notebook['path']
101 102
102 103 sm.update_session(session_id, **changes)
103 104 model = sm.get_session(session_id=session_id)
104 105 self.finish(json.dumps(model, default=date_default))
105 106
106 107 @web.authenticated
107 108 @json_errors
108 109 def delete(self, session_id):
109 110 # Deletes the session with given session_id
110 111 sm = self.session_manager
111 km = self.kernel_manager
112 session = sm.get_session(session_id=session_id)
113 112 sm.delete_session(session_id)
114 km.shutdown_kernel(session['kernel']['id'])
115 113 self.set_status(204)
116 114 self.finish()
117 115
118 116
119 117 #-----------------------------------------------------------------------------
120 118 # URL to handler mappings
121 119 #-----------------------------------------------------------------------------
122 120
123 121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
124 122
125 123 default_handlers = [
126 124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
127 125 (r"/api/sessions", SessionRootHandler)
128 126 ]
129 127
@@ -1,199 +1,206 b''
1 1 """A base class session manager.
2 2
3 3 Authors:
4 4
5 5 * Zach Sailer
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 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 uuid
20 20 import sqlite3
21 21
22 22 from tornado import web
23 23
24 24 from IPython.config.configurable import LoggingConfigurable
25 25 from IPython.utils.py3compat import unicode_type
26 from IPython.utils.traitlets import Instance
26 27
27 28 #-----------------------------------------------------------------------------
28 29 # Classes
29 30 #-----------------------------------------------------------------------------
30 31
31 32 class SessionManager(LoggingConfigurable):
33
34 kernel_manager = Instance('IPython.html.services.kernels.kernelmanager.MappingKernelManager')
35 notebook_manager = Instance('IPython.html.services.notebooks.nbmanager.NotebookManager', args=())
32 36
33 37 # Session database initialized below
34 38 _cursor = None
35 39 _connection = None
36 40 _columns = {'session_id', 'name', 'path', 'kernel_id'}
37 41
38 42 @property
39 43 def cursor(self):
40 44 """Start a cursor and create a database called 'session'"""
41 45 if self._cursor is None:
42 46 self._cursor = self.connection.cursor()
43 47 self._cursor.execute("""CREATE TABLE session
44 48 (session_id, name, path, kernel_id)""")
45 49 return self._cursor
46 50
47 51 @property
48 52 def connection(self):
49 53 """Start a database connection"""
50 54 if self._connection is None:
51 55 self._connection = sqlite3.connect(':memory:')
52 56 self._connection.row_factory = self.row_factory
53 57 return self._connection
54 58
55 59 def __del__(self):
56 60 """Close connection once SessionManager closes"""
57 61 self.cursor.close()
58 62
59 63 def session_exists(self, name, path):
60 64 """Check to see if the session for a given notebook exists"""
61 65 self.cursor.execute("SELECT * FROM session WHERE name=? AND path=?", (name, path))
62 66 reply = self.cursor.fetchone()
63 67 if reply is None:
64 68 return False
65 69 else:
66 70 return True
67 71
68 72 def new_session_id(self):
69 73 "Create a uuid for a new session"
70 74 return unicode_type(uuid.uuid4())
71 75
72 def create_session(self, name=None, path=None, kernel_id=None):
76 def create_session(self, name=None, path=None, kernel_name='python'):
73 77 """Creates a session and returns its model"""
74 78 session_id = self.new_session_id()
75 return self.save_session(session_id, name=name, path=path, kernel_id=kernel_id)
79 # allow nbm to specify kernels cwd
80 kernel_path = self.notebook_manager.get_kernel_path(name=name, path=path)
81 kernel_id = self.kernel_manager.start_kernel(path=kernel_path,
82 kernel_name=kernel_name)
83 return self.save_session(session_id, name=name, path=path,
84 kernel_id=kernel_id)
76 85
77 86 def save_session(self, session_id, name=None, path=None, kernel_id=None):
78 87 """Saves the items for the session with the given session_id
79 88
80 89 Given a session_id (and any other of the arguments), this method
81 90 creates a row in the sqlite session database that holds the information
82 91 for a session.
83 92
84 93 Parameters
85 94 ----------
86 95 session_id : str
87 96 uuid for the session; this method must be given a session_id
88 97 name : str
89 98 the .ipynb notebook name that started the session
90 99 path : str
91 100 the path to the named notebook
92 101 kernel_id : str
93 102 a uuid for the kernel associated with this session
94 103
95 104 Returns
96 105 -------
97 106 model : dict
98 107 a dictionary of the session model
99 108 """
100 109 self.cursor.execute("INSERT INTO session VALUES (?,?,?,?)",
101 110 (session_id, name, path, kernel_id)
102 111 )
103 112 return self.get_session(session_id=session_id)
104 113
105 114 def get_session(self, **kwargs):
106 115 """Returns the model for a particular session.
107 116
108 117 Takes a keyword argument and searches for the value in the session
109 118 database, then returns the rest of the session's info.
110 119
111 120 Parameters
112 121 ----------
113 122 **kwargs : keyword argument
114 123 must be given one of the keywords and values from the session database
115 124 (i.e. session_id, name, path, kernel_id)
116 125
117 126 Returns
118 127 -------
119 128 model : dict
120 129 returns a dictionary that includes all the information from the
121 130 session described by the kwarg.
122 131 """
123 132 if not kwargs:
124 133 raise TypeError("must specify a column to query")
125 134
126 135 conditions = []
127 136 for column in kwargs.keys():
128 137 if column not in self._columns:
129 138 raise TypeError("No such column: %r", column)
130 139 conditions.append("%s=?" % column)
131 140
132 141 query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions))
133 142
134 143 self.cursor.execute(query, list(kwargs.values()))
135 144 model = self.cursor.fetchone()
136 145 if model is None:
137 146 q = []
138 147 for key, value in kwargs.items():
139 148 q.append("%s=%r" % (key, value))
140 149
141 150 raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q)))
142 151 return model
143 152
144 153 def update_session(self, session_id, **kwargs):
145 154 """Updates the values in the session database.
146 155
147 156 Changes the values of the session with the given session_id
148 157 with the values from the keyword arguments.
149 158
150 159 Parameters
151 160 ----------
152 161 session_id : str
153 162 a uuid that identifies a session in the sqlite3 database
154 163 **kwargs : str
155 164 the key must correspond to a column title in session database,
156 165 and the value replaces the current value in the session
157 166 with session_id.
158 167 """
159 168 self.get_session(session_id=session_id)
160 169
161 170 if not kwargs:
162 171 # no changes
163 172 return
164 173
165 174 sets = []
166 175 for column in kwargs.keys():
167 176 if column not in self._columns:
168 177 raise TypeError("No such column: %r" % column)
169 178 sets.append("%s=?" % column)
170 179 query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets))
171 180 self.cursor.execute(query, list(kwargs.values()) + [session_id])
172 181
173 @staticmethod
174 def row_factory(cursor, row):
182 def row_factory(self, cursor, row):
175 183 """Takes sqlite database session row and turns it into a dictionary"""
176 184 row = sqlite3.Row(cursor, row)
177 185 model = {
178 186 'id': row['session_id'],
179 187 'notebook': {
180 188 'name': row['name'],
181 189 'path': row['path']
182 190 },
183 'kernel': {
184 'id': row['kernel_id'],
185 }
191 'kernel': self.kernel_manager.kernel_model(row['kernel_id'])
186 192 }
187 193 return model
188 194
189 195 def list_sessions(self):
190 196 """Returns a list of dictionaries containing all the information from
191 197 the session database"""
192 198 c = self.cursor.execute("SELECT * FROM session")
193 199 return list(c.fetchall())
194 200
195 201 def delete_session(self, session_id):
196 202 """Deletes the row in the session database with given session_id"""
197 203 # Check that session exists before deleting
198 self.get_session(session_id=session_id)
204 session = self.get_session(session_id=session_id)
205 self.kernel_manager.shutdown_kernel(session['kernel']['id'])
199 206 self.cursor.execute("DELETE FROM session WHERE session_id=?", (session_id,))
@@ -1,83 +1,105 b''
1 1 """Tests for the session manager."""
2 2
3 3 from unittest import TestCase
4 4
5 5 from tornado import web
6 6
7 7 from ..sessionmanager import SessionManager
8 from IPython.html.services.kernels.kernelmanager import MappingKernelManager
9
10 class DummyKernel(object):
11 def __init__(self, kernel_name='python'):
12 self.kernel_name = kernel_name
13
14 class DummyMKM(MappingKernelManager):
15 """MappingKernelManager interface that doesn't start kernels, for testing"""
16 def __init__(self, *args, **kwargs):
17 super(DummyMKM, self).__init__(*args, **kwargs)
18 self.id_letters = iter(u'ABCDEFGHIJK')
19
20 def _new_id(self):
21 return next(self.id_letters)
22
23 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
24 kernel_id = kernel_id or self._new_id()
25 self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name)
26 return kernel_id
27
28 def shutdown_kernel(self, kernel_id, now=False):
29 del self._kernels[kernel_id]
8 30
9 31 class TestSessionManager(TestCase):
10 32
11 33 def test_get_session(self):
12 sm = SessionManager()
13 session_id = sm.new_session_id()
14 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
34 sm = SessionManager(kernel_manager=DummyMKM())
35 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
36 kernel_name='bar')['id']
15 37 model = sm.get_session(session_id=session_id)
16 expected = {'id':session_id, 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678'}}
38 expected = {'id':session_id,
39 'notebook':{'name':u'test.ipynb', 'path': u'/path/to/'},
40 'kernel': {'id':u'A', 'name': 'bar'}}
17 41 self.assertEqual(model, expected)
18 42
19 43 def test_bad_get_session(self):
20 44 # Should raise error if a bad key is passed to the database.
21 sm = SessionManager()
22 session_id = sm.new_session_id()
23 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
45 sm = SessionManager(kernel_manager=DummyMKM())
46 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
47 kernel_name='foo')['id']
24 48 self.assertRaises(TypeError, sm.get_session, bad_id=session_id) # Bad keyword
25 49
26 50 def test_list_sessions(self):
27 sm = SessionManager()
28 session_id1 = sm.new_session_id()
29 session_id2 = sm.new_session_id()
30 session_id3 = sm.new_session_id()
31 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678')
32 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678')
33 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678')
51 sm = SessionManager(kernel_manager=DummyMKM())
52 sessions = [
53 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
54 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
55 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
56 ]
34 57 sessions = sm.list_sessions()
35 expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
36 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678'}},
37 {'id':session_id2, 'notebook': {'name':u'test2.ipynb',
38 'path': u'/path/to/2/'}, 'kernel':{'id':u'5678'}},
39 {'id':session_id3, 'notebook':{'name':u'test3.ipynb',
40 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678'}}]
58 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
59 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
60 {'id':sessions[1]['id'], 'notebook': {'name':u'test2.ipynb',
61 'path': u'/path/to/2/'}, 'kernel':{'id':u'B', 'name':'python'}},
62 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
63 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
41 64 self.assertEqual(sessions, expected)
42 65
43 66 def test_update_session(self):
44 sm = SessionManager()
45 session_id = sm.new_session_id()
46 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id=None)
47 sm.update_session(session_id, kernel_id='5678')
67 sm = SessionManager(kernel_manager=DummyMKM())
68 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
69 kernel_name='julia')['id']
48 70 sm.update_session(session_id, name='new_name.ipynb')
49 71 model = sm.get_session(session_id=session_id)
50 expected = {'id':session_id, 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'}, 'kernel':{'id':u'5678'}}
72 expected = {'id':session_id,
73 'notebook':{'name':u'new_name.ipynb', 'path': u'/path/to/'},
74 'kernel':{'id':u'A', 'name':'julia'}}
51 75 self.assertEqual(model, expected)
52 76
53 77 def test_bad_update_session(self):
54 78 # try to update a session with a bad keyword ~ raise error
55 sm = SessionManager()
56 session_id = sm.new_session_id()
57 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
79 sm = SessionManager(kernel_manager=DummyMKM())
80 session_id = sm.create_session(name='test.ipynb', path='/path/to/',
81 kernel_name='ir')['id']
58 82 self.assertRaises(TypeError, sm.update_session, session_id=session_id, bad_kw='test.ipynb') # Bad keyword
59 83
60 84 def test_delete_session(self):
61 sm = SessionManager()
62 session_id1 = sm.new_session_id()
63 session_id2 = sm.new_session_id()
64 session_id3 = sm.new_session_id()
65 sm.save_session(session_id=session_id1, name='test1.ipynb', path='/path/to/1/', kernel_id='5678')
66 sm.save_session(session_id=session_id2, name='test2.ipynb', path='/path/to/2/', kernel_id='5678')
67 sm.save_session(session_id=session_id3, name='test3.ipynb', path='/path/to/3/', kernel_id='5678')
68 sm.delete_session(session_id2)
69 sessions = sm.list_sessions()
70 expected = [{'id':session_id1, 'notebook':{'name':u'test1.ipynb',
71 'path': u'/path/to/1/'}, 'kernel':{'id':u'5678'}},
72 {'id':session_id3, 'notebook':{'name':u'test3.ipynb',
73 'path': u'/path/to/3/'}, 'kernel':{'id':u'5678'}}]
74 self.assertEqual(sessions, expected)
85 sm = SessionManager(kernel_manager=DummyMKM())
86 sessions = [
87 sm.create_session(name='test1.ipynb', path='/path/to/1/', kernel_name='python'),
88 sm.create_session(name='test2.ipynb', path='/path/to/2/', kernel_name='python'),
89 sm.create_session(name='test3.ipynb', path='/path/to/3/', kernel_name='python'),
90 ]
91 sm.delete_session(sessions[1]['id'])
92 new_sessions = sm.list_sessions()
93 expected = [{'id':sessions[0]['id'], 'notebook':{'name':u'test1.ipynb',
94 'path': u'/path/to/1/'}, 'kernel':{'id':u'A', 'name':'python'}},
95 {'id':sessions[2]['id'], 'notebook':{'name':u'test3.ipynb',
96 'path': u'/path/to/3/'}, 'kernel':{'id':u'C', 'name':'python'}}]
97 self.assertEqual(new_sessions, expected)
75 98
76 99 def test_bad_delete_session(self):
77 100 # try to delete a session that doesn't exist ~ raise error
78 sm = SessionManager()
79 session_id = sm.new_session_id()
80 sm.save_session(session_id=session_id, name='test.ipynb', path='/path/to/', kernel_id='5678')
101 sm = SessionManager(kernel_manager=DummyMKM())
102 sm.create_session(name='test.ipynb', path='/path/to/', kernel_name='python')
81 103 self.assertRaises(TypeError, sm.delete_session, bad_kwarg='23424') # Bad keyword
82 104 self.assertRaises(web.HTTPError, sm.delete_session, session_id='23424') # nonexistant
83 105
@@ -1,115 +1,116 b''
1 1 """Test the sessions web service API."""
2 2
3 3 import errno
4 4 import io
5 5 import os
6 6 import json
7 7 import requests
8 8 import shutil
9 9
10 10 pjoin = os.path.join
11 11
12 12 from IPython.html.utils import url_path_join
13 13 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
14 14 from IPython.nbformat.current import new_notebook, write
15 15
16 16 class SessionAPI(object):
17 17 """Wrapper for notebook API calls."""
18 18 def __init__(self, base_url):
19 19 self.base_url = base_url
20 20
21 21 def _req(self, verb, path, body=None):
22 22 response = requests.request(verb,
23 23 url_path_join(self.base_url, 'api/sessions', path), data=body)
24 24
25 25 if 400 <= response.status_code < 600:
26 26 try:
27 27 response.reason = response.json()['message']
28 28 except:
29 29 pass
30 30 response.raise_for_status()
31 31
32 32 return response
33 33
34 34 def list(self):
35 35 return self._req('GET', '')
36 36
37 37 def get(self, id):
38 38 return self._req('GET', id)
39 39
40 def create(self, name, path):
41 body = json.dumps({'notebook': {'name':name, 'path':path}})
40 def create(self, name, path, kernel_name='python'):
41 body = json.dumps({'notebook': {'name':name, 'path':path},
42 'kernel': {'name': kernel_name}})
42 43 return self._req('POST', '', body)
43 44
44 45 def modify(self, id, name, path):
45 46 body = json.dumps({'notebook': {'name':name, 'path':path}})
46 47 return self._req('PATCH', id, body)
47 48
48 49 def delete(self, id):
49 50 return self._req('DELETE', id)
50 51
51 52 class SessionAPITest(NotebookTestBase):
52 53 """Test the sessions web service API"""
53 54 def setUp(self):
54 55 nbdir = self.notebook_dir.name
55 56 try:
56 57 os.mkdir(pjoin(nbdir, 'foo'))
57 58 except OSError as e:
58 59 # Deleting the folder in an earlier test may have failed
59 60 if e.errno != errno.EEXIST:
60 61 raise
61 62
62 63 with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
63 64 encoding='utf-8') as f:
64 65 nb = new_notebook(name='nb1')
65 66 write(nb, f, format='ipynb')
66 67
67 68 self.sess_api = SessionAPI(self.base_url())
68 69
69 70 def tearDown(self):
70 71 for session in self.sess_api.list().json():
71 72 self.sess_api.delete(session['id'])
72 73 shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'),
73 74 ignore_errors=True)
74 75
75 76 def test_create(self):
76 77 sessions = self.sess_api.list().json()
77 78 self.assertEqual(len(sessions), 0)
78 79
79 80 resp = self.sess_api.create('nb1.ipynb', 'foo')
80 81 self.assertEqual(resp.status_code, 201)
81 82 newsession = resp.json()
82 83 self.assertIn('id', newsession)
83 84 self.assertEqual(newsession['notebook']['name'], 'nb1.ipynb')
84 85 self.assertEqual(newsession['notebook']['path'], 'foo')
85 86 self.assertEqual(resp.headers['Location'], '/api/sessions/{0}'.format(newsession['id']))
86 87
87 88 sessions = self.sess_api.list().json()
88 89 self.assertEqual(sessions, [newsession])
89 90
90 91 # Retrieve it
91 92 sid = newsession['id']
92 93 got = self.sess_api.get(sid).json()
93 94 self.assertEqual(got, newsession)
94 95
95 96 def test_delete(self):
96 97 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
97 98 sid = newsession['id']
98 99
99 100 resp = self.sess_api.delete(sid)
100 101 self.assertEqual(resp.status_code, 204)
101 102
102 103 sessions = self.sess_api.list().json()
103 104 self.assertEqual(sessions, [])
104 105
105 106 with assert_http_error(404):
106 107 self.sess_api.get(sid)
107 108
108 109 def test_modify(self):
109 110 newsession = self.sess_api.create('nb1.ipynb', 'foo').json()
110 111 sid = newsession['id']
111 112
112 113 changed = self.sess_api.modify(sid, 'nb2.ipynb', '').json()
113 114 self.assertEqual(changed['id'], sid)
114 115 self.assertEqual(changed['notebook']['name'], 'nb2.ipynb')
115 116 self.assertEqual(changed['notebook']['path'], '')
General Comments 0
You need to be logged in to leave comments. Login now