##// END OF EJS Templates
Merge pull request #6962 from takluyver/nb-dir-and-file-to-run...
Thomas Kluyver -
r19004:2fda8e1a merge
parent child Browse files
Show More
@@ -1,993 +1,994 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import errno
10 import errno
11 import io
11 import io
12 import json
12 import json
13 import logging
13 import logging
14 import os
14 import os
15 import random
15 import random
16 import re
16 import re
17 import select
17 import select
18 import signal
18 import signal
19 import socket
19 import socket
20 import sys
20 import sys
21 import threading
21 import threading
22 import time
22 import time
23 import webbrowser
23 import webbrowser
24
24
25
25
26 # check for pyzmq 2.1.11
26 # check for pyzmq 2.1.11
27 from IPython.utils.zmqrelated import check_for_zmq
27 from IPython.utils.zmqrelated import check_for_zmq
28 check_for_zmq('2.1.11', 'IPython.html')
28 check_for_zmq('2.1.11', 'IPython.html')
29
29
30 from jinja2 import Environment, FileSystemLoader
30 from jinja2 import Environment, FileSystemLoader
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 ioloop.install()
35 ioloop.install()
36
36
37 # check for tornado 3.1.0
37 # check for tornado 3.1.0
38 msg = "The IPython Notebook requires tornado >= 4.0"
38 msg = "The IPython Notebook requires tornado >= 4.0"
39 try:
39 try:
40 import tornado
40 import tornado
41 except ImportError:
41 except ImportError:
42 raise ImportError(msg)
42 raise ImportError(msg)
43 try:
43 try:
44 version_info = tornado.version_info
44 version_info = tornado.version_info
45 except AttributeError:
45 except AttributeError:
46 raise ImportError(msg + ", but you have < 1.1.0")
46 raise ImportError(msg + ", but you have < 1.1.0")
47 if version_info < (4,0):
47 if version_info < (4,0):
48 raise ImportError(msg + ", but you have %s" % tornado.version)
48 raise ImportError(msg + ", but you have %s" % tornado.version)
49
49
50 from tornado import httpserver
50 from tornado import httpserver
51 from tornado import web
51 from tornado import web
52 from tornado.log import LogFormatter, app_log, access_log, gen_log
52 from tornado.log import LogFormatter, app_log, access_log, gen_log
53
53
54 from IPython.html import (
54 from IPython.html import (
55 DEFAULT_STATIC_FILES_PATH,
55 DEFAULT_STATIC_FILES_PATH,
56 DEFAULT_TEMPLATE_PATH_LIST,
56 DEFAULT_TEMPLATE_PATH_LIST,
57 )
57 )
58 from .base.handlers import Template404
58 from .base.handlers import Template404
59 from .log import log_request
59 from .log import log_request
60 from .services.kernels.kernelmanager import MappingKernelManager
60 from .services.kernels.kernelmanager import MappingKernelManager
61 from .services.contents.manager import ContentsManager
61 from .services.contents.manager import ContentsManager
62 from .services.contents.filemanager import FileContentsManager
62 from .services.contents.filemanager import FileContentsManager
63 from .services.clusters.clustermanager import ClusterManager
63 from .services.clusters.clustermanager import ClusterManager
64 from .services.sessions.sessionmanager import SessionManager
64 from .services.sessions.sessionmanager import SessionManager
65
65
66 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
66 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
67
67
68 from IPython.config import Config
68 from IPython.config import Config
69 from IPython.config.application import catch_config_error, boolean_flag
69 from IPython.config.application import catch_config_error, boolean_flag
70 from IPython.core.application import (
70 from IPython.core.application import (
71 BaseIPythonApplication, base_flags, base_aliases,
71 BaseIPythonApplication, base_flags, base_aliases,
72 )
72 )
73 from IPython.core.profiledir import ProfileDir
73 from IPython.core.profiledir import ProfileDir
74 from IPython.kernel import KernelManager
74 from IPython.kernel import KernelManager
75 from IPython.kernel.kernelspec import KernelSpecManager
75 from IPython.kernel.kernelspec import KernelSpecManager
76 from IPython.kernel.zmq.session import default_secure, Session
76 from IPython.kernel.zmq.session import default_secure, Session
77 from IPython.nbformat.sign import NotebookNotary
77 from IPython.nbformat.sign import NotebookNotary
78 from IPython.utils.importstring import import_item
78 from IPython.utils.importstring import import_item
79 from IPython.utils import submodule
79 from IPython.utils import submodule
80 from IPython.utils.process import check_pid
80 from IPython.utils.process import check_pid
81 from IPython.utils.traitlets import (
81 from IPython.utils.traitlets import (
82 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
82 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
83 DottedObjectName, TraitError,
83 DottedObjectName, TraitError,
84 )
84 )
85 from IPython.utils import py3compat
85 from IPython.utils import py3compat
86 from IPython.utils.path import filefind, get_ipython_dir
86 from IPython.utils.path import filefind, get_ipython_dir
87
87
88 from .utils import url_path_join
88 from .utils import url_path_join
89
89
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91 # Module globals
91 # Module globals
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93
93
94 _examples = """
94 _examples = """
95 ipython notebook # start the notebook
95 ipython notebook # start the notebook
96 ipython notebook --profile=sympy # use the sympy profile
96 ipython notebook --profile=sympy # use the sympy profile
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 """
98 """
99
99
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101 # Helper functions
101 # Helper functions
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103
103
104 def random_ports(port, n):
104 def random_ports(port, n):
105 """Generate a list of n random ports near the given port.
105 """Generate a list of n random ports near the given port.
106
106
107 The first 5 ports will be sequential, and the remaining n-5 will be
107 The first 5 ports will be sequential, and the remaining n-5 will be
108 randomly selected in the range [port-2*n, port+2*n].
108 randomly selected in the range [port-2*n, port+2*n].
109 """
109 """
110 for i in range(min(5, n)):
110 for i in range(min(5, n)):
111 yield port + i
111 yield port + i
112 for i in range(n-5):
112 for i in range(n-5):
113 yield max(1, port + random.randint(-2*n, 2*n))
113 yield max(1, port + random.randint(-2*n, 2*n))
114
114
115 def load_handlers(name):
115 def load_handlers(name):
116 """Load the (URL pattern, handler) tuples for each component."""
116 """Load the (URL pattern, handler) tuples for each component."""
117 name = 'IPython.html.' + name
117 name = 'IPython.html.' + name
118 mod = __import__(name, fromlist=['default_handlers'])
118 mod = __import__(name, fromlist=['default_handlers'])
119 return mod.default_handlers
119 return mod.default_handlers
120
120
121 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
122 # The Tornado web application
122 # The Tornado web application
123 #-----------------------------------------------------------------------------
123 #-----------------------------------------------------------------------------
124
124
125 class NotebookWebApplication(web.Application):
125 class NotebookWebApplication(web.Application):
126
126
127 def __init__(self, ipython_app, kernel_manager, contents_manager,
127 def __init__(self, ipython_app, kernel_manager, contents_manager,
128 cluster_manager, session_manager, kernel_spec_manager, log,
128 cluster_manager, session_manager, kernel_spec_manager, log,
129 base_url, default_url, settings_overrides, jinja_env_options):
129 base_url, default_url, settings_overrides, jinja_env_options):
130
130
131 settings = self.init_settings(
131 settings = self.init_settings(
132 ipython_app, kernel_manager, contents_manager, cluster_manager,
132 ipython_app, kernel_manager, contents_manager, cluster_manager,
133 session_manager, kernel_spec_manager, log, base_url, default_url,
133 session_manager, kernel_spec_manager, log, base_url, default_url,
134 settings_overrides, jinja_env_options)
134 settings_overrides, jinja_env_options)
135 handlers = self.init_handlers(settings)
135 handlers = self.init_handlers(settings)
136
136
137 super(NotebookWebApplication, self).__init__(handlers, **settings)
137 super(NotebookWebApplication, self).__init__(handlers, **settings)
138
138
139 def init_settings(self, ipython_app, kernel_manager, contents_manager,
139 def init_settings(self, ipython_app, kernel_manager, contents_manager,
140 cluster_manager, session_manager, kernel_spec_manager,
140 cluster_manager, session_manager, kernel_spec_manager,
141 log, base_url, default_url, settings_overrides,
141 log, base_url, default_url, settings_overrides,
142 jinja_env_options=None):
142 jinja_env_options=None):
143
143
144 _template_path = settings_overrides.get(
144 _template_path = settings_overrides.get(
145 "template_path",
145 "template_path",
146 ipython_app.template_file_path,
146 ipython_app.template_file_path,
147 )
147 )
148 if isinstance(_template_path, str):
148 if isinstance(_template_path, str):
149 _template_path = (_template_path,)
149 _template_path = (_template_path,)
150 template_path = [os.path.expanduser(path) for path in _template_path]
150 template_path = [os.path.expanduser(path) for path in _template_path]
151
151
152 jenv_opt = jinja_env_options if jinja_env_options else {}
152 jenv_opt = jinja_env_options if jinja_env_options else {}
153 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
153 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
154 settings = dict(
154 settings = dict(
155 # basics
155 # basics
156 log_function=log_request,
156 log_function=log_request,
157 base_url=base_url,
157 base_url=base_url,
158 default_url=default_url,
158 default_url=default_url,
159 template_path=template_path,
159 template_path=template_path,
160 static_path=ipython_app.static_file_path,
160 static_path=ipython_app.static_file_path,
161 static_handler_class = FileFindHandler,
161 static_handler_class = FileFindHandler,
162 static_url_prefix = url_path_join(base_url,'/static/'),
162 static_url_prefix = url_path_join(base_url,'/static/'),
163
163
164 # authentication
164 # authentication
165 cookie_secret=ipython_app.cookie_secret,
165 cookie_secret=ipython_app.cookie_secret,
166 login_url=url_path_join(base_url,'/login'),
166 login_url=url_path_join(base_url,'/login'),
167 password=ipython_app.password,
167 password=ipython_app.password,
168
168
169 # managers
169 # managers
170 kernel_manager=kernel_manager,
170 kernel_manager=kernel_manager,
171 contents_manager=contents_manager,
171 contents_manager=contents_manager,
172 cluster_manager=cluster_manager,
172 cluster_manager=cluster_manager,
173 session_manager=session_manager,
173 session_manager=session_manager,
174 kernel_spec_manager=kernel_spec_manager,
174 kernel_spec_manager=kernel_spec_manager,
175
175
176 # IPython stuff
176 # IPython stuff
177 nbextensions_path = ipython_app.nbextensions_path,
177 nbextensions_path = ipython_app.nbextensions_path,
178 websocket_url=ipython_app.websocket_url,
178 websocket_url=ipython_app.websocket_url,
179 mathjax_url=ipython_app.mathjax_url,
179 mathjax_url=ipython_app.mathjax_url,
180 config=ipython_app.config,
180 config=ipython_app.config,
181 jinja2_env=env,
181 jinja2_env=env,
182 terminals_available=False, # Set later if terminals are available
182 terminals_available=False, # Set later if terminals are available
183 profile_dir = ipython_app.profile_dir.location,
183 profile_dir = ipython_app.profile_dir.location,
184 )
184 )
185
185
186 # allow custom overrides for the tornado web app.
186 # allow custom overrides for the tornado web app.
187 settings.update(settings_overrides)
187 settings.update(settings_overrides)
188 return settings
188 return settings
189
189
190 def init_handlers(self, settings):
190 def init_handlers(self, settings):
191 """Load the (URL pattern, handler) tuples for each component."""
191 """Load the (URL pattern, handler) tuples for each component."""
192
192
193 # Order matters. The first handler to match the URL will handle the request.
193 # Order matters. The first handler to match the URL will handle the request.
194 handlers = []
194 handlers = []
195 handlers.extend(load_handlers('tree.handlers'))
195 handlers.extend(load_handlers('tree.handlers'))
196 handlers.extend(load_handlers('auth.login'))
196 handlers.extend(load_handlers('auth.login'))
197 handlers.extend(load_handlers('auth.logout'))
197 handlers.extend(load_handlers('auth.logout'))
198 handlers.extend(load_handlers('files.handlers'))
198 handlers.extend(load_handlers('files.handlers'))
199 handlers.extend(load_handlers('notebook.handlers'))
199 handlers.extend(load_handlers('notebook.handlers'))
200 handlers.extend(load_handlers('nbconvert.handlers'))
200 handlers.extend(load_handlers('nbconvert.handlers'))
201 handlers.extend(load_handlers('kernelspecs.handlers'))
201 handlers.extend(load_handlers('kernelspecs.handlers'))
202 handlers.extend(load_handlers('services.config.handlers'))
202 handlers.extend(load_handlers('services.config.handlers'))
203 handlers.extend(load_handlers('services.kernels.handlers'))
203 handlers.extend(load_handlers('services.kernels.handlers'))
204 handlers.extend(load_handlers('services.contents.handlers'))
204 handlers.extend(load_handlers('services.contents.handlers'))
205 handlers.extend(load_handlers('services.clusters.handlers'))
205 handlers.extend(load_handlers('services.clusters.handlers'))
206 handlers.extend(load_handlers('services.sessions.handlers'))
206 handlers.extend(load_handlers('services.sessions.handlers'))
207 handlers.extend(load_handlers('services.nbconvert.handlers'))
207 handlers.extend(load_handlers('services.nbconvert.handlers'))
208 handlers.extend(load_handlers('services.kernelspecs.handlers'))
208 handlers.extend(load_handlers('services.kernelspecs.handlers'))
209 handlers.append(
209 handlers.append(
210 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
210 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
211 )
211 )
212 # register base handlers last
212 # register base handlers last
213 handlers.extend(load_handlers('base.handlers'))
213 handlers.extend(load_handlers('base.handlers'))
214 # set the URL that will be redirected from `/`
214 # set the URL that will be redirected from `/`
215 handlers.append(
215 handlers.append(
216 (r'/?', web.RedirectHandler, {
216 (r'/?', web.RedirectHandler, {
217 'url' : url_path_join(settings['base_url'], settings['default_url']),
217 'url' : url_path_join(settings['base_url'], settings['default_url']),
218 'permanent': False, # want 302, not 301
218 'permanent': False, # want 302, not 301
219 })
219 })
220 )
220 )
221 # prepend base_url onto the patterns that we match
221 # prepend base_url onto the patterns that we match
222 new_handlers = []
222 new_handlers = []
223 for handler in handlers:
223 for handler in handlers:
224 pattern = url_path_join(settings['base_url'], handler[0])
224 pattern = url_path_join(settings['base_url'], handler[0])
225 new_handler = tuple([pattern] + list(handler[1:]))
225 new_handler = tuple([pattern] + list(handler[1:]))
226 new_handlers.append(new_handler)
226 new_handlers.append(new_handler)
227 # add 404 on the end, which will catch everything that falls through
227 # add 404 on the end, which will catch everything that falls through
228 new_handlers.append((r'(.*)', Template404))
228 new_handlers.append((r'(.*)', Template404))
229 return new_handlers
229 return new_handlers
230
230
231
231
232 class NbserverListApp(BaseIPythonApplication):
232 class NbserverListApp(BaseIPythonApplication):
233
233
234 description="List currently running notebook servers in this profile."
234 description="List currently running notebook servers in this profile."
235
235
236 flags = dict(
236 flags = dict(
237 json=({'NbserverListApp': {'json': True}},
237 json=({'NbserverListApp': {'json': True}},
238 "Produce machine-readable JSON output."),
238 "Produce machine-readable JSON output."),
239 )
239 )
240
240
241 json = Bool(False, config=True,
241 json = Bool(False, config=True,
242 help="If True, each line of output will be a JSON object with the "
242 help="If True, each line of output will be a JSON object with the "
243 "details from the server info file.")
243 "details from the server info file.")
244
244
245 def start(self):
245 def start(self):
246 if not self.json:
246 if not self.json:
247 print("Currently running servers:")
247 print("Currently running servers:")
248 for serverinfo in list_running_servers(self.profile):
248 for serverinfo in list_running_servers(self.profile):
249 if self.json:
249 if self.json:
250 print(json.dumps(serverinfo))
250 print(json.dumps(serverinfo))
251 else:
251 else:
252 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
252 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
253
253
254 #-----------------------------------------------------------------------------
254 #-----------------------------------------------------------------------------
255 # Aliases and Flags
255 # Aliases and Flags
256 #-----------------------------------------------------------------------------
256 #-----------------------------------------------------------------------------
257
257
258 flags = dict(base_flags)
258 flags = dict(base_flags)
259 flags['no-browser']=(
259 flags['no-browser']=(
260 {'NotebookApp' : {'open_browser' : False}},
260 {'NotebookApp' : {'open_browser' : False}},
261 "Don't open the notebook in a browser after startup."
261 "Don't open the notebook in a browser after startup."
262 )
262 )
263 flags['pylab']=(
263 flags['pylab']=(
264 {'NotebookApp' : {'pylab' : 'warn'}},
264 {'NotebookApp' : {'pylab' : 'warn'}},
265 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
265 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
266 )
266 )
267 flags['no-mathjax']=(
267 flags['no-mathjax']=(
268 {'NotebookApp' : {'enable_mathjax' : False}},
268 {'NotebookApp' : {'enable_mathjax' : False}},
269 """Disable MathJax
269 """Disable MathJax
270
270
271 MathJax is the javascript library IPython uses to render math/LaTeX. It is
271 MathJax is the javascript library IPython uses to render math/LaTeX. It is
272 very large, so you may want to disable it if you have a slow internet
272 very large, so you may want to disable it if you have a slow internet
273 connection, or for offline use of the notebook.
273 connection, or for offline use of the notebook.
274
274
275 When disabled, equations etc. will appear as their untransformed TeX source.
275 When disabled, equations etc. will appear as their untransformed TeX source.
276 """
276 """
277 )
277 )
278
278
279 # Add notebook manager flags
279 # Add notebook manager flags
280 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
280 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
281 'DEPRECATED, IGNORED',
281 'DEPRECATED, IGNORED',
282 'DEPRECATED, IGNORED'))
282 'DEPRECATED, IGNORED'))
283
283
284 aliases = dict(base_aliases)
284 aliases = dict(base_aliases)
285
285
286 aliases.update({
286 aliases.update({
287 'ip': 'NotebookApp.ip',
287 'ip': 'NotebookApp.ip',
288 'port': 'NotebookApp.port',
288 'port': 'NotebookApp.port',
289 'port-retries': 'NotebookApp.port_retries',
289 'port-retries': 'NotebookApp.port_retries',
290 'transport': 'KernelManager.transport',
290 'transport': 'KernelManager.transport',
291 'keyfile': 'NotebookApp.keyfile',
291 'keyfile': 'NotebookApp.keyfile',
292 'certfile': 'NotebookApp.certfile',
292 'certfile': 'NotebookApp.certfile',
293 'notebook-dir': 'NotebookApp.notebook_dir',
293 'notebook-dir': 'NotebookApp.notebook_dir',
294 'browser': 'NotebookApp.browser',
294 'browser': 'NotebookApp.browser',
295 'pylab': 'NotebookApp.pylab',
295 'pylab': 'NotebookApp.pylab',
296 })
296 })
297
297
298 #-----------------------------------------------------------------------------
298 #-----------------------------------------------------------------------------
299 # NotebookApp
299 # NotebookApp
300 #-----------------------------------------------------------------------------
300 #-----------------------------------------------------------------------------
301
301
302 class NotebookApp(BaseIPythonApplication):
302 class NotebookApp(BaseIPythonApplication):
303
303
304 name = 'ipython-notebook'
304 name = 'ipython-notebook'
305
305
306 description = """
306 description = """
307 The IPython HTML Notebook.
307 The IPython HTML Notebook.
308
308
309 This launches a Tornado based HTML Notebook Server that serves up an
309 This launches a Tornado based HTML Notebook Server that serves up an
310 HTML5/Javascript Notebook client.
310 HTML5/Javascript Notebook client.
311 """
311 """
312 examples = _examples
312 examples = _examples
313 aliases = aliases
313 aliases = aliases
314 flags = flags
314 flags = flags
315
315
316 classes = [
316 classes = [
317 KernelManager, ProfileDir, Session, MappingKernelManager,
317 KernelManager, ProfileDir, Session, MappingKernelManager,
318 ContentsManager, FileContentsManager, NotebookNotary,
318 ContentsManager, FileContentsManager, NotebookNotary,
319 ]
319 ]
320 flags = Dict(flags)
320 flags = Dict(flags)
321 aliases = Dict(aliases)
321 aliases = Dict(aliases)
322
322
323 subcommands = dict(
323 subcommands = dict(
324 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
324 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
325 )
325 )
326
326
327 ipython_kernel_argv = List(Unicode)
327 ipython_kernel_argv = List(Unicode)
328
328
329 _log_formatter_cls = LogFormatter
329 _log_formatter_cls = LogFormatter
330
330
331 def _log_level_default(self):
331 def _log_level_default(self):
332 return logging.INFO
332 return logging.INFO
333
333
334 def _log_datefmt_default(self):
334 def _log_datefmt_default(self):
335 """Exclude date from default date format"""
335 """Exclude date from default date format"""
336 return "%H:%M:%S"
336 return "%H:%M:%S"
337
337
338 def _log_format_default(self):
338 def _log_format_default(self):
339 """override default log format to include time"""
339 """override default log format to include time"""
340 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
340 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
341
341
342 # create requested profiles by default, if they don't exist:
342 # create requested profiles by default, if they don't exist:
343 auto_create = Bool(True)
343 auto_create = Bool(True)
344
344
345 # file to be opened in the notebook server
345 # file to be opened in the notebook server
346 file_to_run = Unicode('', config=True)
346 file_to_run = Unicode('', config=True)
347 def _file_to_run_changed(self, name, old, new):
348 path, base = os.path.split(new)
349 if path:
350 self.file_to_run = base
351 self.notebook_dir = path
352
347
353 # Network related information
348 # Network related information
354
349
355 allow_origin = Unicode('', config=True,
350 allow_origin = Unicode('', config=True,
356 help="""Set the Access-Control-Allow-Origin header
351 help="""Set the Access-Control-Allow-Origin header
357
352
358 Use '*' to allow any origin to access your server.
353 Use '*' to allow any origin to access your server.
359
354
360 Takes precedence over allow_origin_pat.
355 Takes precedence over allow_origin_pat.
361 """
356 """
362 )
357 )
363
358
364 allow_origin_pat = Unicode('', config=True,
359 allow_origin_pat = Unicode('', config=True,
365 help="""Use a regular expression for the Access-Control-Allow-Origin header
360 help="""Use a regular expression for the Access-Control-Allow-Origin header
366
361
367 Requests from an origin matching the expression will get replies with:
362 Requests from an origin matching the expression will get replies with:
368
363
369 Access-Control-Allow-Origin: origin
364 Access-Control-Allow-Origin: origin
370
365
371 where `origin` is the origin of the request.
366 where `origin` is the origin of the request.
372
367
373 Ignored if allow_origin is set.
368 Ignored if allow_origin is set.
374 """
369 """
375 )
370 )
376
371
377 allow_credentials = Bool(False, config=True,
372 allow_credentials = Bool(False, config=True,
378 help="Set the Access-Control-Allow-Credentials: true header"
373 help="Set the Access-Control-Allow-Credentials: true header"
379 )
374 )
380
375
381 default_url = Unicode('/tree', config=True,
376 default_url = Unicode('/tree', config=True,
382 help="The default URL to redirect to from `/`"
377 help="The default URL to redirect to from `/`"
383 )
378 )
384
379
385 ip = Unicode('localhost', config=True,
380 ip = Unicode('localhost', config=True,
386 help="The IP address the notebook server will listen on."
381 help="The IP address the notebook server will listen on."
387 )
382 )
388
383
389 def _ip_changed(self, name, old, new):
384 def _ip_changed(self, name, old, new):
390 if new == u'*': self.ip = u''
385 if new == u'*': self.ip = u''
391
386
392 port = Integer(8888, config=True,
387 port = Integer(8888, config=True,
393 help="The port the notebook server will listen on."
388 help="The port the notebook server will listen on."
394 )
389 )
395 port_retries = Integer(50, config=True,
390 port_retries = Integer(50, config=True,
396 help="The number of additional ports to try if the specified port is not available."
391 help="The number of additional ports to try if the specified port is not available."
397 )
392 )
398
393
399 certfile = Unicode(u'', config=True,
394 certfile = Unicode(u'', config=True,
400 help="""The full path to an SSL/TLS certificate file."""
395 help="""The full path to an SSL/TLS certificate file."""
401 )
396 )
402
397
403 keyfile = Unicode(u'', config=True,
398 keyfile = Unicode(u'', config=True,
404 help="""The full path to a private key file for usage with SSL/TLS."""
399 help="""The full path to a private key file for usage with SSL/TLS."""
405 )
400 )
406
401
407 cookie_secret_file = Unicode(config=True,
402 cookie_secret_file = Unicode(config=True,
408 help="""The file where the cookie secret is stored."""
403 help="""The file where the cookie secret is stored."""
409 )
404 )
410 def _cookie_secret_file_default(self):
405 def _cookie_secret_file_default(self):
411 if self.profile_dir is None:
406 if self.profile_dir is None:
412 return ''
407 return ''
413 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
408 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
414
409
415 cookie_secret = Bytes(b'', config=True,
410 cookie_secret = Bytes(b'', config=True,
416 help="""The random bytes used to secure cookies.
411 help="""The random bytes used to secure cookies.
417 By default this is a new random number every time you start the Notebook.
412 By default this is a new random number every time you start the Notebook.
418 Set it to a value in a config file to enable logins to persist across server sessions.
413 Set it to a value in a config file to enable logins to persist across server sessions.
419
414
420 Note: Cookie secrets should be kept private, do not share config files with
415 Note: Cookie secrets should be kept private, do not share config files with
421 cookie_secret stored in plaintext (you can read the value from a file).
416 cookie_secret stored in plaintext (you can read the value from a file).
422 """
417 """
423 )
418 )
424 def _cookie_secret_default(self):
419 def _cookie_secret_default(self):
425 if os.path.exists(self.cookie_secret_file):
420 if os.path.exists(self.cookie_secret_file):
426 with io.open(self.cookie_secret_file, 'rb') as f:
421 with io.open(self.cookie_secret_file, 'rb') as f:
427 return f.read()
422 return f.read()
428 else:
423 else:
429 secret = base64.encodestring(os.urandom(1024))
424 secret = base64.encodestring(os.urandom(1024))
430 self._write_cookie_secret_file(secret)
425 self._write_cookie_secret_file(secret)
431 return secret
426 return secret
432
427
433 def _write_cookie_secret_file(self, secret):
428 def _write_cookie_secret_file(self, secret):
434 """write my secret to my secret_file"""
429 """write my secret to my secret_file"""
435 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
430 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
436 with io.open(self.cookie_secret_file, 'wb') as f:
431 with io.open(self.cookie_secret_file, 'wb') as f:
437 f.write(secret)
432 f.write(secret)
438 try:
433 try:
439 os.chmod(self.cookie_secret_file, 0o600)
434 os.chmod(self.cookie_secret_file, 0o600)
440 except OSError:
435 except OSError:
441 self.log.warn(
436 self.log.warn(
442 "Could not set permissions on %s",
437 "Could not set permissions on %s",
443 self.cookie_secret_file
438 self.cookie_secret_file
444 )
439 )
445
440
446 password = Unicode(u'', config=True,
441 password = Unicode(u'', config=True,
447 help="""Hashed password to use for web authentication.
442 help="""Hashed password to use for web authentication.
448
443
449 To generate, type in a python/IPython shell:
444 To generate, type in a python/IPython shell:
450
445
451 from IPython.lib import passwd; passwd()
446 from IPython.lib import passwd; passwd()
452
447
453 The string should be of the form type:salt:hashed-password.
448 The string should be of the form type:salt:hashed-password.
454 """
449 """
455 )
450 )
456
451
457 open_browser = Bool(True, config=True,
452 open_browser = Bool(True, config=True,
458 help="""Whether to open in a browser after starting.
453 help="""Whether to open in a browser after starting.
459 The specific browser used is platform dependent and
454 The specific browser used is platform dependent and
460 determined by the python standard library `webbrowser`
455 determined by the python standard library `webbrowser`
461 module, unless it is overridden using the --browser
456 module, unless it is overridden using the --browser
462 (NotebookApp.browser) configuration option.
457 (NotebookApp.browser) configuration option.
463 """)
458 """)
464
459
465 browser = Unicode(u'', config=True,
460 browser = Unicode(u'', config=True,
466 help="""Specify what command to use to invoke a web
461 help="""Specify what command to use to invoke a web
467 browser when opening the notebook. If not specified, the
462 browser when opening the notebook. If not specified, the
468 default browser will be determined by the `webbrowser`
463 default browser will be determined by the `webbrowser`
469 standard library module, which allows setting of the
464 standard library module, which allows setting of the
470 BROWSER environment variable to override it.
465 BROWSER environment variable to override it.
471 """)
466 """)
472
467
473 webapp_settings = Dict(config=True,
468 webapp_settings = Dict(config=True,
474 help="DEPRECATED, use tornado_settings"
469 help="DEPRECATED, use tornado_settings"
475 )
470 )
476 def _webapp_settings_changed(self, name, old, new):
471 def _webapp_settings_changed(self, name, old, new):
477 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
472 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
478 self.tornado_settings = new
473 self.tornado_settings = new
479
474
480 tornado_settings = Dict(config=True,
475 tornado_settings = Dict(config=True,
481 help="Supply overrides for the tornado.web.Application that the "
476 help="Supply overrides for the tornado.web.Application that the "
482 "IPython notebook uses.")
477 "IPython notebook uses.")
483
478
484 jinja_environment_options = Dict(config=True,
479 jinja_environment_options = Dict(config=True,
485 help="Supply extra arguments that will be passed to Jinja environment.")
480 help="Supply extra arguments that will be passed to Jinja environment.")
486
481
487
482
488 enable_mathjax = Bool(True, config=True,
483 enable_mathjax = Bool(True, config=True,
489 help="""Whether to enable MathJax for typesetting math/TeX
484 help="""Whether to enable MathJax for typesetting math/TeX
490
485
491 MathJax is the javascript library IPython uses to render math/LaTeX. It is
486 MathJax is the javascript library IPython uses to render math/LaTeX. It is
492 very large, so you may want to disable it if you have a slow internet
487 very large, so you may want to disable it if you have a slow internet
493 connection, or for offline use of the notebook.
488 connection, or for offline use of the notebook.
494
489
495 When disabled, equations etc. will appear as their untransformed TeX source.
490 When disabled, equations etc. will appear as their untransformed TeX source.
496 """
491 """
497 )
492 )
498 def _enable_mathjax_changed(self, name, old, new):
493 def _enable_mathjax_changed(self, name, old, new):
499 """set mathjax url to empty if mathjax is disabled"""
494 """set mathjax url to empty if mathjax is disabled"""
500 if not new:
495 if not new:
501 self.mathjax_url = u''
496 self.mathjax_url = u''
502
497
503 base_url = Unicode('/', config=True,
498 base_url = Unicode('/', config=True,
504 help='''The base URL for the notebook server.
499 help='''The base URL for the notebook server.
505
500
506 Leading and trailing slashes can be omitted,
501 Leading and trailing slashes can be omitted,
507 and will automatically be added.
502 and will automatically be added.
508 ''')
503 ''')
509 def _base_url_changed(self, name, old, new):
504 def _base_url_changed(self, name, old, new):
510 if not new.startswith('/'):
505 if not new.startswith('/'):
511 self.base_url = '/'+new
506 self.base_url = '/'+new
512 elif not new.endswith('/'):
507 elif not new.endswith('/'):
513 self.base_url = new+'/'
508 self.base_url = new+'/'
514
509
515 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
510 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
516 def _base_project_url_changed(self, name, old, new):
511 def _base_project_url_changed(self, name, old, new):
517 self.log.warn("base_project_url is deprecated, use base_url")
512 self.log.warn("base_project_url is deprecated, use base_url")
518 self.base_url = new
513 self.base_url = new
519
514
520 extra_static_paths = List(Unicode, config=True,
515 extra_static_paths = List(Unicode, config=True,
521 help="""Extra paths to search for serving static files.
516 help="""Extra paths to search for serving static files.
522
517
523 This allows adding javascript/css to be available from the notebook server machine,
518 This allows adding javascript/css to be available from the notebook server machine,
524 or overriding individual files in the IPython"""
519 or overriding individual files in the IPython"""
525 )
520 )
526 def _extra_static_paths_default(self):
521 def _extra_static_paths_default(self):
527 return [os.path.join(self.profile_dir.location, 'static')]
522 return [os.path.join(self.profile_dir.location, 'static')]
528
523
529 @property
524 @property
530 def static_file_path(self):
525 def static_file_path(self):
531 """return extra paths + the default location"""
526 """return extra paths + the default location"""
532 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
527 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
533
528
534 extra_template_paths = List(Unicode, config=True,
529 extra_template_paths = List(Unicode, config=True,
535 help="""Extra paths to search for serving jinja templates.
530 help="""Extra paths to search for serving jinja templates.
536
531
537 Can be used to override templates from IPython.html.templates."""
532 Can be used to override templates from IPython.html.templates."""
538 )
533 )
539 def _extra_template_paths_default(self):
534 def _extra_template_paths_default(self):
540 return []
535 return []
541
536
542 @property
537 @property
543 def template_file_path(self):
538 def template_file_path(self):
544 """return extra paths + the default locations"""
539 """return extra paths + the default locations"""
545 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
540 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
546
541
547 nbextensions_path = List(Unicode, config=True,
542 nbextensions_path = List(Unicode, config=True,
548 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
543 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
549 )
544 )
550 def _nbextensions_path_default(self):
545 def _nbextensions_path_default(self):
551 return [os.path.join(get_ipython_dir(), 'nbextensions')]
546 return [os.path.join(get_ipython_dir(), 'nbextensions')]
552
547
553 websocket_url = Unicode("", config=True,
548 websocket_url = Unicode("", config=True,
554 help="""The base URL for websockets,
549 help="""The base URL for websockets,
555 if it differs from the HTTP server (hint: it almost certainly doesn't).
550 if it differs from the HTTP server (hint: it almost certainly doesn't).
556
551
557 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
552 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
558 """
553 """
559 )
554 )
560 mathjax_url = Unicode("", config=True,
555 mathjax_url = Unicode("", config=True,
561 help="""The url for MathJax.js."""
556 help="""The url for MathJax.js."""
562 )
557 )
563 def _mathjax_url_default(self):
558 def _mathjax_url_default(self):
564 if not self.enable_mathjax:
559 if not self.enable_mathjax:
565 return u''
560 return u''
566 static_url_prefix = self.tornado_settings.get("static_url_prefix",
561 static_url_prefix = self.tornado_settings.get("static_url_prefix",
567 url_path_join(self.base_url, "static")
562 url_path_join(self.base_url, "static")
568 )
563 )
569
564
570 # try local mathjax, either in nbextensions/mathjax or static/mathjax
565 # try local mathjax, either in nbextensions/mathjax or static/mathjax
571 for (url_prefix, search_path) in [
566 for (url_prefix, search_path) in [
572 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
567 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
573 (static_url_prefix, self.static_file_path),
568 (static_url_prefix, self.static_file_path),
574 ]:
569 ]:
575 self.log.debug("searching for local mathjax in %s", search_path)
570 self.log.debug("searching for local mathjax in %s", search_path)
576 try:
571 try:
577 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
572 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
578 except IOError:
573 except IOError:
579 continue
574 continue
580 else:
575 else:
581 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
576 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
582 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
577 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
583 return url
578 return url
584
579
585 # no local mathjax, serve from CDN
580 # no local mathjax, serve from CDN
586 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
581 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
587 self.log.info("Using MathJax from CDN: %s", url)
582 self.log.info("Using MathJax from CDN: %s", url)
588 return url
583 return url
589
584
590 def _mathjax_url_changed(self, name, old, new):
585 def _mathjax_url_changed(self, name, old, new):
591 if new and not self.enable_mathjax:
586 if new and not self.enable_mathjax:
592 # enable_mathjax=False overrides mathjax_url
587 # enable_mathjax=False overrides mathjax_url
593 self.mathjax_url = u''
588 self.mathjax_url = u''
594 else:
589 else:
595 self.log.info("Using MathJax: %s", new)
590 self.log.info("Using MathJax: %s", new)
596
591
597 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
592 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
598 config=True,
593 config=True,
599 help='The notebook manager class to use.'
594 help='The notebook manager class to use.'
600 )
595 )
601 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
596 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
602 config=True,
597 config=True,
603 help='The kernel manager class to use.'
598 help='The kernel manager class to use.'
604 )
599 )
605 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
600 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
606 config=True,
601 config=True,
607 help='The session manager class to use.'
602 help='The session manager class to use.'
608 )
603 )
609 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
604 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
610 config=True,
605 config=True,
611 help='The cluster manager class to use.'
606 help='The cluster manager class to use.'
612 )
607 )
613
608
614 kernel_spec_manager = Instance(KernelSpecManager)
609 kernel_spec_manager = Instance(KernelSpecManager)
615
610
616 def _kernel_spec_manager_default(self):
611 def _kernel_spec_manager_default(self):
617 return KernelSpecManager(ipython_dir=self.ipython_dir)
612 return KernelSpecManager(ipython_dir=self.ipython_dir)
618
613
619 trust_xheaders = Bool(False, config=True,
614 trust_xheaders = Bool(False, config=True,
620 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
615 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
621 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
616 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
622 )
617 )
623
618
624 info_file = Unicode()
619 info_file = Unicode()
625
620
626 def _info_file_default(self):
621 def _info_file_default(self):
627 info_file = "nbserver-%s.json"%os.getpid()
622 info_file = "nbserver-%s.json"%os.getpid()
628 return os.path.join(self.profile_dir.security_dir, info_file)
623 return os.path.join(self.profile_dir.security_dir, info_file)
629
624
630 notebook_dir = Unicode(py3compat.getcwd(), config=True,
631 help="The directory to use for notebooks and kernels."
632 )
633
634 pylab = Unicode('disabled', config=True,
625 pylab = Unicode('disabled', config=True,
635 help="""
626 help="""
636 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
627 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
637 """
628 """
638 )
629 )
639 def _pylab_changed(self, name, old, new):
630 def _pylab_changed(self, name, old, new):
640 """when --pylab is specified, display a warning and exit"""
631 """when --pylab is specified, display a warning and exit"""
641 if new != 'warn':
632 if new != 'warn':
642 backend = ' %s' % new
633 backend = ' %s' % new
643 else:
634 else:
644 backend = ''
635 backend = ''
645 self.log.error("Support for specifying --pylab on the command line has been removed.")
636 self.log.error("Support for specifying --pylab on the command line has been removed.")
646 self.log.error(
637 self.log.error(
647 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
638 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
648 )
639 )
649 self.exit(1)
640 self.exit(1)
650
641
642 notebook_dir = Unicode(config=True,
643 help="The directory to use for notebooks and kernels."
644 )
645
646 def _notebook_dir_default(self):
647 if self.file_to_run:
648 return os.path.dirname(os.path.abspath(self.file_to_run))
649 else:
650 return py3compat.getcwd()
651
651 def _notebook_dir_changed(self, name, old, new):
652 def _notebook_dir_changed(self, name, old, new):
652 """Do a bit of validation of the notebook dir."""
653 """Do a bit of validation of the notebook dir."""
653 if not os.path.isabs(new):
654 if not os.path.isabs(new):
654 # If we receive a non-absolute path, make it absolute.
655 # If we receive a non-absolute path, make it absolute.
655 self.notebook_dir = os.path.abspath(new)
656 self.notebook_dir = os.path.abspath(new)
656 return
657 return
657 if not os.path.isdir(new):
658 if not os.path.isdir(new):
658 raise TraitError("No such notebook dir: %r" % new)
659 raise TraitError("No such notebook dir: %r" % new)
659
660
660 # setting App.notebook_dir implies setting notebook and kernel dirs as well
661 # setting App.notebook_dir implies setting notebook and kernel dirs as well
661 self.config.FileContentsManager.root_dir = new
662 self.config.FileContentsManager.root_dir = new
662 self.config.MappingKernelManager.root_dir = new
663 self.config.MappingKernelManager.root_dir = new
663
664
664
665
665 def parse_command_line(self, argv=None):
666 def parse_command_line(self, argv=None):
666 super(NotebookApp, self).parse_command_line(argv)
667 super(NotebookApp, self).parse_command_line(argv)
667
668
668 if self.extra_args:
669 if self.extra_args:
669 arg0 = self.extra_args[0]
670 arg0 = self.extra_args[0]
670 f = os.path.abspath(arg0)
671 f = os.path.abspath(arg0)
671 self.argv.remove(arg0)
672 self.argv.remove(arg0)
672 if not os.path.exists(f):
673 if not os.path.exists(f):
673 self.log.critical("No such file or directory: %s", f)
674 self.log.critical("No such file or directory: %s", f)
674 self.exit(1)
675 self.exit(1)
675
676
676 # Use config here, to ensure that it takes higher priority than
677 # Use config here, to ensure that it takes higher priority than
677 # anything that comes from the profile.
678 # anything that comes from the profile.
678 c = Config()
679 c = Config()
679 if os.path.isdir(f):
680 if os.path.isdir(f):
680 c.NotebookApp.notebook_dir = f
681 c.NotebookApp.notebook_dir = f
681 elif os.path.isfile(f):
682 elif os.path.isfile(f):
682 c.NotebookApp.file_to_run = f
683 c.NotebookApp.file_to_run = f
683 self.update_config(c)
684 self.update_config(c)
684
685
685 def init_kernel_argv(self):
686 def init_kernel_argv(self):
686 """add the profile-dir to arguments to be passed to IPython kernels"""
687 """add the profile-dir to arguments to be passed to IPython kernels"""
687 # FIXME: remove special treatment of IPython kernels
688 # FIXME: remove special treatment of IPython kernels
688 # Kernel should get *absolute* path to profile directory
689 # Kernel should get *absolute* path to profile directory
689 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
690 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
690
691
691 def init_configurables(self):
692 def init_configurables(self):
692 # force Session default to be secure
693 # force Session default to be secure
693 default_secure(self.config)
694 default_secure(self.config)
694 kls = import_item(self.kernel_manager_class)
695 kls = import_item(self.kernel_manager_class)
695 self.kernel_manager = kls(
696 self.kernel_manager = kls(
696 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
697 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
697 connection_dir = self.profile_dir.security_dir,
698 connection_dir = self.profile_dir.security_dir,
698 )
699 )
699 kls = import_item(self.contents_manager_class)
700 kls = import_item(self.contents_manager_class)
700 self.contents_manager = kls(parent=self, log=self.log)
701 self.contents_manager = kls(parent=self, log=self.log)
701 kls = import_item(self.session_manager_class)
702 kls = import_item(self.session_manager_class)
702 self.session_manager = kls(parent=self, log=self.log,
703 self.session_manager = kls(parent=self, log=self.log,
703 kernel_manager=self.kernel_manager,
704 kernel_manager=self.kernel_manager,
704 contents_manager=self.contents_manager)
705 contents_manager=self.contents_manager)
705 kls = import_item(self.cluster_manager_class)
706 kls = import_item(self.cluster_manager_class)
706 self.cluster_manager = kls(parent=self, log=self.log)
707 self.cluster_manager = kls(parent=self, log=self.log)
707 self.cluster_manager.update_profiles()
708 self.cluster_manager.update_profiles()
708
709
709 def init_logging(self):
710 def init_logging(self):
710 # This prevents double log messages because tornado use a root logger that
711 # This prevents double log messages because tornado use a root logger that
711 # self.log is a child of. The logging module dipatches log messages to a log
712 # self.log is a child of. The logging module dipatches log messages to a log
712 # and all of its ancenstors until propagate is set to False.
713 # and all of its ancenstors until propagate is set to False.
713 self.log.propagate = False
714 self.log.propagate = False
714
715
715 for log in app_log, access_log, gen_log:
716 for log in app_log, access_log, gen_log:
716 # consistent log output name (NotebookApp instead of tornado.access, etc.)
717 # consistent log output name (NotebookApp instead of tornado.access, etc.)
717 log.name = self.log.name
718 log.name = self.log.name
718 # hook up tornado 3's loggers to our app handlers
719 # hook up tornado 3's loggers to our app handlers
719 logger = logging.getLogger('tornado')
720 logger = logging.getLogger('tornado')
720 logger.propagate = True
721 logger.propagate = True
721 logger.parent = self.log
722 logger.parent = self.log
722 logger.setLevel(self.log.level)
723 logger.setLevel(self.log.level)
723
724
724 def init_webapp(self):
725 def init_webapp(self):
725 """initialize tornado webapp and httpserver"""
726 """initialize tornado webapp and httpserver"""
726 self.tornado_settings['allow_origin'] = self.allow_origin
727 self.tornado_settings['allow_origin'] = self.allow_origin
727 if self.allow_origin_pat:
728 if self.allow_origin_pat:
728 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
729 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
729 self.tornado_settings['allow_credentials'] = self.allow_credentials
730 self.tornado_settings['allow_credentials'] = self.allow_credentials
730
731
731 self.web_app = NotebookWebApplication(
732 self.web_app = NotebookWebApplication(
732 self, self.kernel_manager, self.contents_manager,
733 self, self.kernel_manager, self.contents_manager,
733 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
734 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
734 self.log, self.base_url, self.default_url, self.tornado_settings,
735 self.log, self.base_url, self.default_url, self.tornado_settings,
735 self.jinja_environment_options
736 self.jinja_environment_options
736 )
737 )
737 if self.certfile:
738 if self.certfile:
738 ssl_options = dict(certfile=self.certfile)
739 ssl_options = dict(certfile=self.certfile)
739 if self.keyfile:
740 if self.keyfile:
740 ssl_options['keyfile'] = self.keyfile
741 ssl_options['keyfile'] = self.keyfile
741 else:
742 else:
742 ssl_options = None
743 ssl_options = None
743 self.web_app.password = self.password
744 self.web_app.password = self.password
744 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
745 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
745 xheaders=self.trust_xheaders)
746 xheaders=self.trust_xheaders)
746 if not self.ip:
747 if not self.ip:
747 warning = "WARNING: The notebook server is listening on all IP addresses"
748 warning = "WARNING: The notebook server is listening on all IP addresses"
748 if ssl_options is None:
749 if ssl_options is None:
749 self.log.critical(warning + " and not using encryption. This "
750 self.log.critical(warning + " and not using encryption. This "
750 "is not recommended.")
751 "is not recommended.")
751 if not self.password:
752 if not self.password:
752 self.log.critical(warning + " and not using authentication. "
753 self.log.critical(warning + " and not using authentication. "
753 "This is highly insecure and not recommended.")
754 "This is highly insecure and not recommended.")
754 success = None
755 success = None
755 for port in random_ports(self.port, self.port_retries+1):
756 for port in random_ports(self.port, self.port_retries+1):
756 try:
757 try:
757 self.http_server.listen(port, self.ip)
758 self.http_server.listen(port, self.ip)
758 except socket.error as e:
759 except socket.error as e:
759 if e.errno == errno.EADDRINUSE:
760 if e.errno == errno.EADDRINUSE:
760 self.log.info('The port %i is already in use, trying another random port.' % port)
761 self.log.info('The port %i is already in use, trying another random port.' % port)
761 continue
762 continue
762 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
763 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
763 self.log.warn("Permission to listen on port %i denied" % port)
764 self.log.warn("Permission to listen on port %i denied" % port)
764 continue
765 continue
765 else:
766 else:
766 raise
767 raise
767 else:
768 else:
768 self.port = port
769 self.port = port
769 success = True
770 success = True
770 break
771 break
771 if not success:
772 if not success:
772 self.log.critical('ERROR: the notebook server could not be started because '
773 self.log.critical('ERROR: the notebook server could not be started because '
773 'no available port could be found.')
774 'no available port could be found.')
774 self.exit(1)
775 self.exit(1)
775
776
776 @property
777 @property
777 def display_url(self):
778 def display_url(self):
778 ip = self.ip if self.ip else '[all ip addresses on your system]'
779 ip = self.ip if self.ip else '[all ip addresses on your system]'
779 return self._url(ip)
780 return self._url(ip)
780
781
781 @property
782 @property
782 def connection_url(self):
783 def connection_url(self):
783 ip = self.ip if self.ip else 'localhost'
784 ip = self.ip if self.ip else 'localhost'
784 return self._url(ip)
785 return self._url(ip)
785
786
786 def _url(self, ip):
787 def _url(self, ip):
787 proto = 'https' if self.certfile else 'http'
788 proto = 'https' if self.certfile else 'http'
788 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
789 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
789
790
790 def init_terminals(self):
791 def init_terminals(self):
791 try:
792 try:
792 from .terminal import initialize
793 from .terminal import initialize
793 initialize(self.web_app)
794 initialize(self.web_app)
794 self.web_app.settings['terminals_available'] = True
795 self.web_app.settings['terminals_available'] = True
795 except ImportError as e:
796 except ImportError as e:
796 self.log.info("Terminals not available (error was %s)", e)
797 self.log.info("Terminals not available (error was %s)", e)
797
798
798 def init_signal(self):
799 def init_signal(self):
799 if not sys.platform.startswith('win'):
800 if not sys.platform.startswith('win'):
800 signal.signal(signal.SIGINT, self._handle_sigint)
801 signal.signal(signal.SIGINT, self._handle_sigint)
801 signal.signal(signal.SIGTERM, self._signal_stop)
802 signal.signal(signal.SIGTERM, self._signal_stop)
802 if hasattr(signal, 'SIGUSR1'):
803 if hasattr(signal, 'SIGUSR1'):
803 # Windows doesn't support SIGUSR1
804 # Windows doesn't support SIGUSR1
804 signal.signal(signal.SIGUSR1, self._signal_info)
805 signal.signal(signal.SIGUSR1, self._signal_info)
805 if hasattr(signal, 'SIGINFO'):
806 if hasattr(signal, 'SIGINFO'):
806 # only on BSD-based systems
807 # only on BSD-based systems
807 signal.signal(signal.SIGINFO, self._signal_info)
808 signal.signal(signal.SIGINFO, self._signal_info)
808
809
809 def _handle_sigint(self, sig, frame):
810 def _handle_sigint(self, sig, frame):
810 """SIGINT handler spawns confirmation dialog"""
811 """SIGINT handler spawns confirmation dialog"""
811 # register more forceful signal handler for ^C^C case
812 # register more forceful signal handler for ^C^C case
812 signal.signal(signal.SIGINT, self._signal_stop)
813 signal.signal(signal.SIGINT, self._signal_stop)
813 # request confirmation dialog in bg thread, to avoid
814 # request confirmation dialog in bg thread, to avoid
814 # blocking the App
815 # blocking the App
815 thread = threading.Thread(target=self._confirm_exit)
816 thread = threading.Thread(target=self._confirm_exit)
816 thread.daemon = True
817 thread.daemon = True
817 thread.start()
818 thread.start()
818
819
819 def _restore_sigint_handler(self):
820 def _restore_sigint_handler(self):
820 """callback for restoring original SIGINT handler"""
821 """callback for restoring original SIGINT handler"""
821 signal.signal(signal.SIGINT, self._handle_sigint)
822 signal.signal(signal.SIGINT, self._handle_sigint)
822
823
823 def _confirm_exit(self):
824 def _confirm_exit(self):
824 """confirm shutdown on ^C
825 """confirm shutdown on ^C
825
826
826 A second ^C, or answering 'y' within 5s will cause shutdown,
827 A second ^C, or answering 'y' within 5s will cause shutdown,
827 otherwise original SIGINT handler will be restored.
828 otherwise original SIGINT handler will be restored.
828
829
829 This doesn't work on Windows.
830 This doesn't work on Windows.
830 """
831 """
831 info = self.log.info
832 info = self.log.info
832 info('interrupted')
833 info('interrupted')
833 print(self.notebook_info())
834 print(self.notebook_info())
834 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
835 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
835 sys.stdout.flush()
836 sys.stdout.flush()
836 r,w,x = select.select([sys.stdin], [], [], 5)
837 r,w,x = select.select([sys.stdin], [], [], 5)
837 if r:
838 if r:
838 line = sys.stdin.readline()
839 line = sys.stdin.readline()
839 if line.lower().startswith('y') and 'n' not in line.lower():
840 if line.lower().startswith('y') and 'n' not in line.lower():
840 self.log.critical("Shutdown confirmed")
841 self.log.critical("Shutdown confirmed")
841 ioloop.IOLoop.instance().stop()
842 ioloop.IOLoop.instance().stop()
842 return
843 return
843 else:
844 else:
844 print("No answer for 5s:", end=' ')
845 print("No answer for 5s:", end=' ')
845 print("resuming operation...")
846 print("resuming operation...")
846 # no answer, or answer is no:
847 # no answer, or answer is no:
847 # set it back to original SIGINT handler
848 # set it back to original SIGINT handler
848 # use IOLoop.add_callback because signal.signal must be called
849 # use IOLoop.add_callback because signal.signal must be called
849 # from main thread
850 # from main thread
850 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
851 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
851
852
852 def _signal_stop(self, sig, frame):
853 def _signal_stop(self, sig, frame):
853 self.log.critical("received signal %s, stopping", sig)
854 self.log.critical("received signal %s, stopping", sig)
854 ioloop.IOLoop.instance().stop()
855 ioloop.IOLoop.instance().stop()
855
856
856 def _signal_info(self, sig, frame):
857 def _signal_info(self, sig, frame):
857 print(self.notebook_info())
858 print(self.notebook_info())
858
859
859 def init_components(self):
860 def init_components(self):
860 """Check the components submodule, and warn if it's unclean"""
861 """Check the components submodule, and warn if it's unclean"""
861 status = submodule.check_submodule_status()
862 status = submodule.check_submodule_status()
862 if status == 'missing':
863 if status == 'missing':
863 self.log.warn("components submodule missing, running `git submodule update`")
864 self.log.warn("components submodule missing, running `git submodule update`")
864 submodule.update_submodules(submodule.ipython_parent())
865 submodule.update_submodules(submodule.ipython_parent())
865 elif status == 'unclean':
866 elif status == 'unclean':
866 self.log.warn("components submodule unclean, you may see 404s on static/components")
867 self.log.warn("components submodule unclean, you may see 404s on static/components")
867 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
868 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
868
869
869 @catch_config_error
870 @catch_config_error
870 def initialize(self, argv=None):
871 def initialize(self, argv=None):
871 super(NotebookApp, self).initialize(argv)
872 super(NotebookApp, self).initialize(argv)
872 self.init_logging()
873 self.init_logging()
873 self.init_kernel_argv()
874 self.init_kernel_argv()
874 self.init_configurables()
875 self.init_configurables()
875 self.init_components()
876 self.init_components()
876 self.init_webapp()
877 self.init_webapp()
877 self.init_terminals()
878 self.init_terminals()
878 self.init_signal()
879 self.init_signal()
879
880
880 def cleanup_kernels(self):
881 def cleanup_kernels(self):
881 """Shutdown all kernels.
882 """Shutdown all kernels.
882
883
883 The kernels will shutdown themselves when this process no longer exists,
884 The kernels will shutdown themselves when this process no longer exists,
884 but explicit shutdown allows the KernelManagers to cleanup the connection files.
885 but explicit shutdown allows the KernelManagers to cleanup the connection files.
885 """
886 """
886 self.log.info('Shutting down kernels')
887 self.log.info('Shutting down kernels')
887 self.kernel_manager.shutdown_all()
888 self.kernel_manager.shutdown_all()
888
889
889 def notebook_info(self):
890 def notebook_info(self):
890 "Return the current working directory and the server url information"
891 "Return the current working directory and the server url information"
891 info = self.contents_manager.info_string() + "\n"
892 info = self.contents_manager.info_string() + "\n"
892 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
893 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
893 return info + "The IPython Notebook is running at: %s" % self.display_url
894 return info + "The IPython Notebook is running at: %s" % self.display_url
894
895
895 def server_info(self):
896 def server_info(self):
896 """Return a JSONable dict of information about this server."""
897 """Return a JSONable dict of information about this server."""
897 return {'url': self.connection_url,
898 return {'url': self.connection_url,
898 'hostname': self.ip if self.ip else 'localhost',
899 'hostname': self.ip if self.ip else 'localhost',
899 'port': self.port,
900 'port': self.port,
900 'secure': bool(self.certfile),
901 'secure': bool(self.certfile),
901 'base_url': self.base_url,
902 'base_url': self.base_url,
902 'notebook_dir': os.path.abspath(self.notebook_dir),
903 'notebook_dir': os.path.abspath(self.notebook_dir),
903 'pid': os.getpid()
904 'pid': os.getpid()
904 }
905 }
905
906
906 def write_server_info_file(self):
907 def write_server_info_file(self):
907 """Write the result of server_info() to the JSON file info_file."""
908 """Write the result of server_info() to the JSON file info_file."""
908 with open(self.info_file, 'w') as f:
909 with open(self.info_file, 'w') as f:
909 json.dump(self.server_info(), f, indent=2)
910 json.dump(self.server_info(), f, indent=2)
910
911
911 def remove_server_info_file(self):
912 def remove_server_info_file(self):
912 """Remove the nbserver-<pid>.json file created for this server.
913 """Remove the nbserver-<pid>.json file created for this server.
913
914
914 Ignores the error raised when the file has already been removed.
915 Ignores the error raised when the file has already been removed.
915 """
916 """
916 try:
917 try:
917 os.unlink(self.info_file)
918 os.unlink(self.info_file)
918 except OSError as e:
919 except OSError as e:
919 if e.errno != errno.ENOENT:
920 if e.errno != errno.ENOENT:
920 raise
921 raise
921
922
922 def start(self):
923 def start(self):
923 """ Start the IPython Notebook server app, after initialization
924 """ Start the IPython Notebook server app, after initialization
924
925
925 This method takes no arguments so all configuration and initialization
926 This method takes no arguments so all configuration and initialization
926 must be done prior to calling this method."""
927 must be done prior to calling this method."""
927 if self.subapp is not None:
928 if self.subapp is not None:
928 return self.subapp.start()
929 return self.subapp.start()
929
930
930 info = self.log.info
931 info = self.log.info
931 for line in self.notebook_info().split("\n"):
932 for line in self.notebook_info().split("\n"):
932 info(line)
933 info(line)
933 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
934 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
934
935
935 self.write_server_info_file()
936 self.write_server_info_file()
936
937
937 if self.open_browser or self.file_to_run:
938 if self.open_browser or self.file_to_run:
938 try:
939 try:
939 browser = webbrowser.get(self.browser or None)
940 browser = webbrowser.get(self.browser or None)
940 except webbrowser.Error as e:
941 except webbrowser.Error as e:
941 self.log.warn('No web browser found: %s.' % e)
942 self.log.warn('No web browser found: %s.' % e)
942 browser = None
943 browser = None
943
944
944 if self.file_to_run:
945 if self.file_to_run:
945 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
946 if not os.path.exists(self.file_to_run):
946 if not os.path.exists(fullpath):
947 self.log.critical("%s does not exist" % self.file_to_run)
947 self.log.critical("%s does not exist" % fullpath)
948 self.exit(1)
948 self.exit(1)
949
949
950 uri = url_path_join('notebooks', self.file_to_run)
950 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
951 uri = url_path_join('notebooks', *relpath.split(os.sep))
951 else:
952 else:
952 uri = 'tree'
953 uri = 'tree'
953 if browser:
954 if browser:
954 b = lambda : browser.open(url_path_join(self.connection_url, uri),
955 b = lambda : browser.open(url_path_join(self.connection_url, uri),
955 new=2)
956 new=2)
956 threading.Thread(target=b).start()
957 threading.Thread(target=b).start()
957 try:
958 try:
958 ioloop.IOLoop.instance().start()
959 ioloop.IOLoop.instance().start()
959 except KeyboardInterrupt:
960 except KeyboardInterrupt:
960 info("Interrupted...")
961 info("Interrupted...")
961 finally:
962 finally:
962 self.cleanup_kernels()
963 self.cleanup_kernels()
963 self.remove_server_info_file()
964 self.remove_server_info_file()
964
965
965
966
966 def list_running_servers(profile='default'):
967 def list_running_servers(profile='default'):
967 """Iterate over the server info files of running notebook servers.
968 """Iterate over the server info files of running notebook servers.
968
969
969 Given a profile name, find nbserver-* files in the security directory of
970 Given a profile name, find nbserver-* files in the security directory of
970 that profile, and yield dicts of their information, each one pertaining to
971 that profile, and yield dicts of their information, each one pertaining to
971 a currently running notebook server instance.
972 a currently running notebook server instance.
972 """
973 """
973 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
974 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
974 for file in os.listdir(pd.security_dir):
975 for file in os.listdir(pd.security_dir):
975 if file.startswith('nbserver-'):
976 if file.startswith('nbserver-'):
976 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
977 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
977 info = json.load(f)
978 info = json.load(f)
978
979
979 # Simple check whether that process is really still running
980 # Simple check whether that process is really still running
980 if check_pid(info['pid']):
981 if check_pid(info['pid']):
981 yield info
982 yield info
982 else:
983 else:
983 # If the process has died, try to delete its info file
984 # If the process has died, try to delete its info file
984 try:
985 try:
985 os.unlink(file)
986 os.unlink(file)
986 except OSError:
987 except OSError:
987 pass # TODO: This should warn or log or something
988 pass # TODO: This should warn or log or something
988 #-----------------------------------------------------------------------------
989 #-----------------------------------------------------------------------------
989 # Main entry point
990 # Main entry point
990 #-----------------------------------------------------------------------------
991 #-----------------------------------------------------------------------------
991
992
992 launch_new_instance = NotebookApp.launch_instance
993 launch_new_instance = NotebookApp.launch_instance
993
994
@@ -1,559 +1,565 b''
1 """A contents manager that uses the local file system for storage."""
1 """A contents manager that uses the local file system for storage."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 import io
7 import io
8 import os
8 import os
9 import glob
9 import glob
10 import shutil
10 import shutil
11
11
12 from tornado import web
12 from tornado import web
13
13
14 from .manager import ContentsManager
14 from .manager import ContentsManager
15 from IPython import nbformat
15 from IPython import nbformat
16 from IPython.utils.io import atomic_writing
16 from IPython.utils.io import atomic_writing
17 from IPython.utils.path import ensure_dir_exists
17 from IPython.utils.path import ensure_dir_exists
18 from IPython.utils.traitlets import Unicode, Bool, TraitError
18 from IPython.utils.traitlets import Unicode, Bool, TraitError
19 from IPython.utils.py3compat import getcwd
19 from IPython.utils.py3compat import getcwd
20 from IPython.utils import tz
20 from IPython.utils import tz
21 from IPython.html.utils import is_hidden, to_os_path, url_path_join
21 from IPython.html.utils import is_hidden, to_os_path, url_path_join
22
22
23
23
24 class FileContentsManager(ContentsManager):
24 class FileContentsManager(ContentsManager):
25
25
26 root_dir = Unicode(getcwd(), config=True)
26 root_dir = Unicode(config=True)
27
28 def _root_dir_default(self):
29 try:
30 return self.parent.notebook_dir
31 except AttributeError:
32 return getcwd()
27
33
28 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
34 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
29 def _save_script_changed(self):
35 def _save_script_changed(self):
30 self.log.warn("""
36 self.log.warn("""
31 Automatically saving notebooks as scripts has been removed.
37 Automatically saving notebooks as scripts has been removed.
32 Use `ipython nbconvert --to python [notebook]` instead.
38 Use `ipython nbconvert --to python [notebook]` instead.
33 """)
39 """)
34
40
35 def _root_dir_changed(self, name, old, new):
41 def _root_dir_changed(self, name, old, new):
36 """Do a bit of validation of the root_dir."""
42 """Do a bit of validation of the root_dir."""
37 if not os.path.isabs(new):
43 if not os.path.isabs(new):
38 # If we receive a non-absolute path, make it absolute.
44 # If we receive a non-absolute path, make it absolute.
39 self.root_dir = os.path.abspath(new)
45 self.root_dir = os.path.abspath(new)
40 return
46 return
41 if not os.path.isdir(new):
47 if not os.path.isdir(new):
42 raise TraitError("%r is not a directory" % new)
48 raise TraitError("%r is not a directory" % new)
43
49
44 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
50 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
45 help="""The directory name in which to keep file checkpoints
51 help="""The directory name in which to keep file checkpoints
46
52
47 This is a path relative to the file's own directory.
53 This is a path relative to the file's own directory.
48
54
49 By default, it is .ipynb_checkpoints
55 By default, it is .ipynb_checkpoints
50 """
56 """
51 )
57 )
52
58
53 def _copy(self, src, dest):
59 def _copy(self, src, dest):
54 """copy src to dest
60 """copy src to dest
55
61
56 like shutil.copy2, but log errors in copystat
62 like shutil.copy2, but log errors in copystat
57 """
63 """
58 shutil.copyfile(src, dest)
64 shutil.copyfile(src, dest)
59 try:
65 try:
60 shutil.copystat(src, dest)
66 shutil.copystat(src, dest)
61 except OSError as e:
67 except OSError as e:
62 self.log.debug("copystat on %s failed", dest, exc_info=True)
68 self.log.debug("copystat on %s failed", dest, exc_info=True)
63
69
64 def _get_os_path(self, path):
70 def _get_os_path(self, path):
65 """Given an API path, return its file system path.
71 """Given an API path, return its file system path.
66
72
67 Parameters
73 Parameters
68 ----------
74 ----------
69 path : string
75 path : string
70 The relative API path to the named file.
76 The relative API path to the named file.
71
77
72 Returns
78 Returns
73 -------
79 -------
74 path : string
80 path : string
75 Native, absolute OS path to for a file.
81 Native, absolute OS path to for a file.
76 """
82 """
77 return to_os_path(path, self.root_dir)
83 return to_os_path(path, self.root_dir)
78
84
79 def dir_exists(self, path):
85 def dir_exists(self, path):
80 """Does the API-style path refer to an extant directory?
86 """Does the API-style path refer to an extant directory?
81
87
82 API-style wrapper for os.path.isdir
88 API-style wrapper for os.path.isdir
83
89
84 Parameters
90 Parameters
85 ----------
91 ----------
86 path : string
92 path : string
87 The path to check. This is an API path (`/` separated,
93 The path to check. This is an API path (`/` separated,
88 relative to root_dir).
94 relative to root_dir).
89
95
90 Returns
96 Returns
91 -------
97 -------
92 exists : bool
98 exists : bool
93 Whether the path is indeed a directory.
99 Whether the path is indeed a directory.
94 """
100 """
95 path = path.strip('/')
101 path = path.strip('/')
96 os_path = self._get_os_path(path=path)
102 os_path = self._get_os_path(path=path)
97 return os.path.isdir(os_path)
103 return os.path.isdir(os_path)
98
104
99 def is_hidden(self, path):
105 def is_hidden(self, path):
100 """Does the API style path correspond to a hidden directory or file?
106 """Does the API style path correspond to a hidden directory or file?
101
107
102 Parameters
108 Parameters
103 ----------
109 ----------
104 path : string
110 path : string
105 The path to check. This is an API path (`/` separated,
111 The path to check. This is an API path (`/` separated,
106 relative to root_dir).
112 relative to root_dir).
107
113
108 Returns
114 Returns
109 -------
115 -------
110 hidden : bool
116 hidden : bool
111 Whether the path exists and is hidden.
117 Whether the path exists and is hidden.
112 """
118 """
113 path = path.strip('/')
119 path = path.strip('/')
114 os_path = self._get_os_path(path=path)
120 os_path = self._get_os_path(path=path)
115 return is_hidden(os_path, self.root_dir)
121 return is_hidden(os_path, self.root_dir)
116
122
117 def file_exists(self, path):
123 def file_exists(self, path):
118 """Returns True if the file exists, else returns False.
124 """Returns True if the file exists, else returns False.
119
125
120 API-style wrapper for os.path.isfile
126 API-style wrapper for os.path.isfile
121
127
122 Parameters
128 Parameters
123 ----------
129 ----------
124 path : string
130 path : string
125 The relative path to the file (with '/' as separator)
131 The relative path to the file (with '/' as separator)
126
132
127 Returns
133 Returns
128 -------
134 -------
129 exists : bool
135 exists : bool
130 Whether the file exists.
136 Whether the file exists.
131 """
137 """
132 path = path.strip('/')
138 path = path.strip('/')
133 os_path = self._get_os_path(path)
139 os_path = self._get_os_path(path)
134 return os.path.isfile(os_path)
140 return os.path.isfile(os_path)
135
141
136 def exists(self, path):
142 def exists(self, path):
137 """Returns True if the path exists, else returns False.
143 """Returns True if the path exists, else returns False.
138
144
139 API-style wrapper for os.path.exists
145 API-style wrapper for os.path.exists
140
146
141 Parameters
147 Parameters
142 ----------
148 ----------
143 path : string
149 path : string
144 The API path to the file (with '/' as separator)
150 The API path to the file (with '/' as separator)
145
151
146 Returns
152 Returns
147 -------
153 -------
148 exists : bool
154 exists : bool
149 Whether the target exists.
155 Whether the target exists.
150 """
156 """
151 path = path.strip('/')
157 path = path.strip('/')
152 os_path = self._get_os_path(path=path)
158 os_path = self._get_os_path(path=path)
153 return os.path.exists(os_path)
159 return os.path.exists(os_path)
154
160
155 def _base_model(self, path):
161 def _base_model(self, path):
156 """Build the common base of a contents model"""
162 """Build the common base of a contents model"""
157 os_path = self._get_os_path(path)
163 os_path = self._get_os_path(path)
158 info = os.stat(os_path)
164 info = os.stat(os_path)
159 last_modified = tz.utcfromtimestamp(info.st_mtime)
165 last_modified = tz.utcfromtimestamp(info.st_mtime)
160 created = tz.utcfromtimestamp(info.st_ctime)
166 created = tz.utcfromtimestamp(info.st_ctime)
161 # Create the base model.
167 # Create the base model.
162 model = {}
168 model = {}
163 model['name'] = path.rsplit('/', 1)[-1]
169 model['name'] = path.rsplit('/', 1)[-1]
164 model['path'] = path
170 model['path'] = path
165 model['last_modified'] = last_modified
171 model['last_modified'] = last_modified
166 model['created'] = created
172 model['created'] = created
167 model['content'] = None
173 model['content'] = None
168 model['format'] = None
174 model['format'] = None
169 return model
175 return model
170
176
171 def _dir_model(self, path, content=True):
177 def _dir_model(self, path, content=True):
172 """Build a model for a directory
178 """Build a model for a directory
173
179
174 if content is requested, will include a listing of the directory
180 if content is requested, will include a listing of the directory
175 """
181 """
176 os_path = self._get_os_path(path)
182 os_path = self._get_os_path(path)
177
183
178 four_o_four = u'directory does not exist: %r' % os_path
184 four_o_four = u'directory does not exist: %r' % os_path
179
185
180 if not os.path.isdir(os_path):
186 if not os.path.isdir(os_path):
181 raise web.HTTPError(404, four_o_four)
187 raise web.HTTPError(404, four_o_four)
182 elif is_hidden(os_path, self.root_dir):
188 elif is_hidden(os_path, self.root_dir):
183 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
189 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
184 os_path
190 os_path
185 )
191 )
186 raise web.HTTPError(404, four_o_four)
192 raise web.HTTPError(404, four_o_four)
187
193
188 model = self._base_model(path)
194 model = self._base_model(path)
189 model['type'] = 'directory'
195 model['type'] = 'directory'
190 if content:
196 if content:
191 model['content'] = contents = []
197 model['content'] = contents = []
192 os_dir = self._get_os_path(path)
198 os_dir = self._get_os_path(path)
193 for name in os.listdir(os_dir):
199 for name in os.listdir(os_dir):
194 os_path = os.path.join(os_dir, name)
200 os_path = os.path.join(os_dir, name)
195 # skip over broken symlinks in listing
201 # skip over broken symlinks in listing
196 if not os.path.exists(os_path):
202 if not os.path.exists(os_path):
197 self.log.warn("%s doesn't exist", os_path)
203 self.log.warn("%s doesn't exist", os_path)
198 continue
204 continue
199 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
205 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
200 self.log.debug("%s not a regular file", os_path)
206 self.log.debug("%s not a regular file", os_path)
201 continue
207 continue
202 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
208 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
203 contents.append(self.get(
209 contents.append(self.get(
204 path='%s/%s' % (path, name),
210 path='%s/%s' % (path, name),
205 content=False)
211 content=False)
206 )
212 )
207
213
208 model['format'] = 'json'
214 model['format'] = 'json'
209
215
210 return model
216 return model
211
217
212 def _file_model(self, path, content=True, format=None):
218 def _file_model(self, path, content=True, format=None):
213 """Build a model for a file
219 """Build a model for a file
214
220
215 if content is requested, include the file contents.
221 if content is requested, include the file contents.
216
222
217 format:
223 format:
218 If 'text', the contents will be decoded as UTF-8.
224 If 'text', the contents will be decoded as UTF-8.
219 If 'base64', the raw bytes contents will be encoded as base64.
225 If 'base64', the raw bytes contents will be encoded as base64.
220 If not specified, try to decode as UTF-8, and fall back to base64
226 If not specified, try to decode as UTF-8, and fall back to base64
221 """
227 """
222 model = self._base_model(path)
228 model = self._base_model(path)
223 model['type'] = 'file'
229 model['type'] = 'file'
224 if content:
230 if content:
225 os_path = self._get_os_path(path)
231 os_path = self._get_os_path(path)
226 if not os.path.isfile(os_path):
232 if not os.path.isfile(os_path):
227 # could be FIFO
233 # could be FIFO
228 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
234 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
229 with io.open(os_path, 'rb') as f:
235 with io.open(os_path, 'rb') as f:
230 bcontent = f.read()
236 bcontent = f.read()
231
237
232 if format != 'base64':
238 if format != 'base64':
233 try:
239 try:
234 model['content'] = bcontent.decode('utf8')
240 model['content'] = bcontent.decode('utf8')
235 except UnicodeError as e:
241 except UnicodeError as e:
236 if format == 'text':
242 if format == 'text':
237 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
243 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
238 else:
244 else:
239 model['format'] = 'text'
245 model['format'] = 'text'
240
246
241 if model['content'] is None:
247 if model['content'] is None:
242 model['content'] = base64.encodestring(bcontent).decode('ascii')
248 model['content'] = base64.encodestring(bcontent).decode('ascii')
243 model['format'] = 'base64'
249 model['format'] = 'base64'
244
250
245 return model
251 return model
246
252
247
253
248 def _notebook_model(self, path, content=True):
254 def _notebook_model(self, path, content=True):
249 """Build a notebook model
255 """Build a notebook model
250
256
251 if content is requested, the notebook content will be populated
257 if content is requested, the notebook content will be populated
252 as a JSON structure (not double-serialized)
258 as a JSON structure (not double-serialized)
253 """
259 """
254 model = self._base_model(path)
260 model = self._base_model(path)
255 model['type'] = 'notebook'
261 model['type'] = 'notebook'
256 if content:
262 if content:
257 os_path = self._get_os_path(path)
263 os_path = self._get_os_path(path)
258 with io.open(os_path, 'r', encoding='utf-8') as f:
264 with io.open(os_path, 'r', encoding='utf-8') as f:
259 try:
265 try:
260 nb = nbformat.read(f, as_version=4)
266 nb = nbformat.read(f, as_version=4)
261 except Exception as e:
267 except Exception as e:
262 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
268 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
263 self.mark_trusted_cells(nb, path)
269 self.mark_trusted_cells(nb, path)
264 model['content'] = nb
270 model['content'] = nb
265 model['format'] = 'json'
271 model['format'] = 'json'
266 self.validate_notebook_model(model)
272 self.validate_notebook_model(model)
267 return model
273 return model
268
274
269 def get(self, path, content=True, type_=None, format=None):
275 def get(self, path, content=True, type_=None, format=None):
270 """ Takes a path for an entity and returns its model
276 """ Takes a path for an entity and returns its model
271
277
272 Parameters
278 Parameters
273 ----------
279 ----------
274 path : str
280 path : str
275 the API path that describes the relative path for the target
281 the API path that describes the relative path for the target
276 content : bool
282 content : bool
277 Whether to include the contents in the reply
283 Whether to include the contents in the reply
278 type_ : str, optional
284 type_ : str, optional
279 The requested type - 'file', 'notebook', or 'directory'.
285 The requested type - 'file', 'notebook', or 'directory'.
280 Will raise HTTPError 400 if the content doesn't match.
286 Will raise HTTPError 400 if the content doesn't match.
281 format : str, optional
287 format : str, optional
282 The requested format for file contents. 'text' or 'base64'.
288 The requested format for file contents. 'text' or 'base64'.
283 Ignored if this returns a notebook or directory model.
289 Ignored if this returns a notebook or directory model.
284
290
285 Returns
291 Returns
286 -------
292 -------
287 model : dict
293 model : dict
288 the contents model. If content=True, returns the contents
294 the contents model. If content=True, returns the contents
289 of the file or directory as well.
295 of the file or directory as well.
290 """
296 """
291 path = path.strip('/')
297 path = path.strip('/')
292
298
293 if not self.exists(path):
299 if not self.exists(path):
294 raise web.HTTPError(404, u'No such file or directory: %s' % path)
300 raise web.HTTPError(404, u'No such file or directory: %s' % path)
295
301
296 os_path = self._get_os_path(path)
302 os_path = self._get_os_path(path)
297 if os.path.isdir(os_path):
303 if os.path.isdir(os_path):
298 if type_ not in (None, 'directory'):
304 if type_ not in (None, 'directory'):
299 raise web.HTTPError(400,
305 raise web.HTTPError(400,
300 u'%s is a directory, not a %s' % (path, type_))
306 u'%s is a directory, not a %s' % (path, type_))
301 model = self._dir_model(path, content=content)
307 model = self._dir_model(path, content=content)
302 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
308 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
303 model = self._notebook_model(path, content=content)
309 model = self._notebook_model(path, content=content)
304 else:
310 else:
305 if type_ == 'directory':
311 if type_ == 'directory':
306 raise web.HTTPError(400,
312 raise web.HTTPError(400,
307 u'%s is not a directory')
313 u'%s is not a directory')
308 model = self._file_model(path, content=content, format=format)
314 model = self._file_model(path, content=content, format=format)
309 return model
315 return model
310
316
311 def _save_notebook(self, os_path, model, path=''):
317 def _save_notebook(self, os_path, model, path=''):
312 """save a notebook file"""
318 """save a notebook file"""
313 # Save the notebook file
319 # Save the notebook file
314 nb = nbformat.from_dict(model['content'])
320 nb = nbformat.from_dict(model['content'])
315
321
316 self.check_and_sign(nb, path)
322 self.check_and_sign(nb, path)
317
323
318 with atomic_writing(os_path, encoding='utf-8') as f:
324 with atomic_writing(os_path, encoding='utf-8') as f:
319 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
325 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
320
326
321 def _save_file(self, os_path, model, path=''):
327 def _save_file(self, os_path, model, path=''):
322 """save a non-notebook file"""
328 """save a non-notebook file"""
323 fmt = model.get('format', None)
329 fmt = model.get('format', None)
324 if fmt not in {'text', 'base64'}:
330 if fmt not in {'text', 'base64'}:
325 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
331 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
326 try:
332 try:
327 content = model['content']
333 content = model['content']
328 if fmt == 'text':
334 if fmt == 'text':
329 bcontent = content.encode('utf8')
335 bcontent = content.encode('utf8')
330 else:
336 else:
331 b64_bytes = content.encode('ascii')
337 b64_bytes = content.encode('ascii')
332 bcontent = base64.decodestring(b64_bytes)
338 bcontent = base64.decodestring(b64_bytes)
333 except Exception as e:
339 except Exception as e:
334 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
340 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
335 with atomic_writing(os_path, text=False) as f:
341 with atomic_writing(os_path, text=False) as f:
336 f.write(bcontent)
342 f.write(bcontent)
337
343
338 def _save_directory(self, os_path, model, path=''):
344 def _save_directory(self, os_path, model, path=''):
339 """create a directory"""
345 """create a directory"""
340 if is_hidden(os_path, self.root_dir):
346 if is_hidden(os_path, self.root_dir):
341 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
347 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
342 if not os.path.exists(os_path):
348 if not os.path.exists(os_path):
343 os.mkdir(os_path)
349 os.mkdir(os_path)
344 elif not os.path.isdir(os_path):
350 elif not os.path.isdir(os_path):
345 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
351 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
346 else:
352 else:
347 self.log.debug("Directory %r already exists", os_path)
353 self.log.debug("Directory %r already exists", os_path)
348
354
349 def save(self, model, path=''):
355 def save(self, model, path=''):
350 """Save the file model and return the model with no content."""
356 """Save the file model and return the model with no content."""
351 path = path.strip('/')
357 path = path.strip('/')
352
358
353 if 'type' not in model:
359 if 'type' not in model:
354 raise web.HTTPError(400, u'No file type provided')
360 raise web.HTTPError(400, u'No file type provided')
355 if 'content' not in model and model['type'] != 'directory':
361 if 'content' not in model and model['type'] != 'directory':
356 raise web.HTTPError(400, u'No file content provided')
362 raise web.HTTPError(400, u'No file content provided')
357
363
358 # One checkpoint should always exist
364 # One checkpoint should always exist
359 if self.file_exists(path) and not self.list_checkpoints(path):
365 if self.file_exists(path) and not self.list_checkpoints(path):
360 self.create_checkpoint(path)
366 self.create_checkpoint(path)
361
367
362 os_path = self._get_os_path(path)
368 os_path = self._get_os_path(path)
363 self.log.debug("Saving %s", os_path)
369 self.log.debug("Saving %s", os_path)
364 try:
370 try:
365 if model['type'] == 'notebook':
371 if model['type'] == 'notebook':
366 self._save_notebook(os_path, model, path)
372 self._save_notebook(os_path, model, path)
367 elif model['type'] == 'file':
373 elif model['type'] == 'file':
368 self._save_file(os_path, model, path)
374 self._save_file(os_path, model, path)
369 elif model['type'] == 'directory':
375 elif model['type'] == 'directory':
370 self._save_directory(os_path, model, path)
376 self._save_directory(os_path, model, path)
371 else:
377 else:
372 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
378 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
373 except web.HTTPError:
379 except web.HTTPError:
374 raise
380 raise
375 except Exception as e:
381 except Exception as e:
376 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
382 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
377
383
378 validation_message = None
384 validation_message = None
379 if model['type'] == 'notebook':
385 if model['type'] == 'notebook':
380 self.validate_notebook_model(model)
386 self.validate_notebook_model(model)
381 validation_message = model.get('message', None)
387 validation_message = model.get('message', None)
382
388
383 model = self.get(path, content=False)
389 model = self.get(path, content=False)
384 if validation_message:
390 if validation_message:
385 model['message'] = validation_message
391 model['message'] = validation_message
386 return model
392 return model
387
393
388 def update(self, model, path):
394 def update(self, model, path):
389 """Update the file's path
395 """Update the file's path
390
396
391 For use in PATCH requests, to enable renaming a file without
397 For use in PATCH requests, to enable renaming a file without
392 re-uploading its contents. Only used for renaming at the moment.
398 re-uploading its contents. Only used for renaming at the moment.
393 """
399 """
394 path = path.strip('/')
400 path = path.strip('/')
395 new_path = model.get('path', path).strip('/')
401 new_path = model.get('path', path).strip('/')
396 if path != new_path:
402 if path != new_path:
397 self.rename(path, new_path)
403 self.rename(path, new_path)
398 model = self.get(new_path, content=False)
404 model = self.get(new_path, content=False)
399 return model
405 return model
400
406
401 def delete(self, path):
407 def delete(self, path):
402 """Delete file at path."""
408 """Delete file at path."""
403 path = path.strip('/')
409 path = path.strip('/')
404 os_path = self._get_os_path(path)
410 os_path = self._get_os_path(path)
405 rm = os.unlink
411 rm = os.unlink
406 if os.path.isdir(os_path):
412 if os.path.isdir(os_path):
407 listing = os.listdir(os_path)
413 listing = os.listdir(os_path)
408 # don't delete non-empty directories (checkpoints dir doesn't count)
414 # don't delete non-empty directories (checkpoints dir doesn't count)
409 if listing and listing != [self.checkpoint_dir]:
415 if listing and listing != [self.checkpoint_dir]:
410 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
416 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
411 elif not os.path.isfile(os_path):
417 elif not os.path.isfile(os_path):
412 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
418 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
413
419
414 # clear checkpoints
420 # clear checkpoints
415 for checkpoint in self.list_checkpoints(path):
421 for checkpoint in self.list_checkpoints(path):
416 checkpoint_id = checkpoint['id']
422 checkpoint_id = checkpoint['id']
417 cp_path = self.get_checkpoint_path(checkpoint_id, path)
423 cp_path = self.get_checkpoint_path(checkpoint_id, path)
418 if os.path.isfile(cp_path):
424 if os.path.isfile(cp_path):
419 self.log.debug("Unlinking checkpoint %s", cp_path)
425 self.log.debug("Unlinking checkpoint %s", cp_path)
420 os.unlink(cp_path)
426 os.unlink(cp_path)
421
427
422 if os.path.isdir(os_path):
428 if os.path.isdir(os_path):
423 self.log.debug("Removing directory %s", os_path)
429 self.log.debug("Removing directory %s", os_path)
424 shutil.rmtree(os_path)
430 shutil.rmtree(os_path)
425 else:
431 else:
426 self.log.debug("Unlinking file %s", os_path)
432 self.log.debug("Unlinking file %s", os_path)
427 rm(os_path)
433 rm(os_path)
428
434
429 def rename(self, old_path, new_path):
435 def rename(self, old_path, new_path):
430 """Rename a file."""
436 """Rename a file."""
431 old_path = old_path.strip('/')
437 old_path = old_path.strip('/')
432 new_path = new_path.strip('/')
438 new_path = new_path.strip('/')
433 if new_path == old_path:
439 if new_path == old_path:
434 return
440 return
435
441
436 new_os_path = self._get_os_path(new_path)
442 new_os_path = self._get_os_path(new_path)
437 old_os_path = self._get_os_path(old_path)
443 old_os_path = self._get_os_path(old_path)
438
444
439 # Should we proceed with the move?
445 # Should we proceed with the move?
440 if os.path.exists(new_os_path):
446 if os.path.exists(new_os_path):
441 raise web.HTTPError(409, u'File already exists: %s' % new_path)
447 raise web.HTTPError(409, u'File already exists: %s' % new_path)
442
448
443 # Move the file
449 # Move the file
444 try:
450 try:
445 shutil.move(old_os_path, new_os_path)
451 shutil.move(old_os_path, new_os_path)
446 except Exception as e:
452 except Exception as e:
447 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
453 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
448
454
449 # Move the checkpoints
455 # Move the checkpoints
450 old_checkpoints = self.list_checkpoints(old_path)
456 old_checkpoints = self.list_checkpoints(old_path)
451 for cp in old_checkpoints:
457 for cp in old_checkpoints:
452 checkpoint_id = cp['id']
458 checkpoint_id = cp['id']
453 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
459 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
454 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
460 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
455 if os.path.isfile(old_cp_path):
461 if os.path.isfile(old_cp_path):
456 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
462 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
457 shutil.move(old_cp_path, new_cp_path)
463 shutil.move(old_cp_path, new_cp_path)
458
464
459 # Checkpoint-related utilities
465 # Checkpoint-related utilities
460
466
461 def get_checkpoint_path(self, checkpoint_id, path):
467 def get_checkpoint_path(self, checkpoint_id, path):
462 """find the path to a checkpoint"""
468 """find the path to a checkpoint"""
463 path = path.strip('/')
469 path = path.strip('/')
464 parent, name = ('/' + path).rsplit('/', 1)
470 parent, name = ('/' + path).rsplit('/', 1)
465 parent = parent.strip('/')
471 parent = parent.strip('/')
466 basename, ext = os.path.splitext(name)
472 basename, ext = os.path.splitext(name)
467 filename = u"{name}-{checkpoint_id}{ext}".format(
473 filename = u"{name}-{checkpoint_id}{ext}".format(
468 name=basename,
474 name=basename,
469 checkpoint_id=checkpoint_id,
475 checkpoint_id=checkpoint_id,
470 ext=ext,
476 ext=ext,
471 )
477 )
472 os_path = self._get_os_path(path=parent)
478 os_path = self._get_os_path(path=parent)
473 cp_dir = os.path.join(os_path, self.checkpoint_dir)
479 cp_dir = os.path.join(os_path, self.checkpoint_dir)
474 ensure_dir_exists(cp_dir)
480 ensure_dir_exists(cp_dir)
475 cp_path = os.path.join(cp_dir, filename)
481 cp_path = os.path.join(cp_dir, filename)
476 return cp_path
482 return cp_path
477
483
478 def get_checkpoint_model(self, checkpoint_id, path):
484 def get_checkpoint_model(self, checkpoint_id, path):
479 """construct the info dict for a given checkpoint"""
485 """construct the info dict for a given checkpoint"""
480 path = path.strip('/')
486 path = path.strip('/')
481 cp_path = self.get_checkpoint_path(checkpoint_id, path)
487 cp_path = self.get_checkpoint_path(checkpoint_id, path)
482 stats = os.stat(cp_path)
488 stats = os.stat(cp_path)
483 last_modified = tz.utcfromtimestamp(stats.st_mtime)
489 last_modified = tz.utcfromtimestamp(stats.st_mtime)
484 info = dict(
490 info = dict(
485 id = checkpoint_id,
491 id = checkpoint_id,
486 last_modified = last_modified,
492 last_modified = last_modified,
487 )
493 )
488 return info
494 return info
489
495
490 # public checkpoint API
496 # public checkpoint API
491
497
492 def create_checkpoint(self, path):
498 def create_checkpoint(self, path):
493 """Create a checkpoint from the current state of a file"""
499 """Create a checkpoint from the current state of a file"""
494 path = path.strip('/')
500 path = path.strip('/')
495 if not self.file_exists(path):
501 if not self.file_exists(path):
496 raise web.HTTPError(404)
502 raise web.HTTPError(404)
497 src_path = self._get_os_path(path)
503 src_path = self._get_os_path(path)
498 # only the one checkpoint ID:
504 # only the one checkpoint ID:
499 checkpoint_id = u"checkpoint"
505 checkpoint_id = u"checkpoint"
500 cp_path = self.get_checkpoint_path(checkpoint_id, path)
506 cp_path = self.get_checkpoint_path(checkpoint_id, path)
501 self.log.debug("creating checkpoint for %s", path)
507 self.log.debug("creating checkpoint for %s", path)
502 self._copy(src_path, cp_path)
508 self._copy(src_path, cp_path)
503
509
504 # return the checkpoint info
510 # return the checkpoint info
505 return self.get_checkpoint_model(checkpoint_id, path)
511 return self.get_checkpoint_model(checkpoint_id, path)
506
512
507 def list_checkpoints(self, path):
513 def list_checkpoints(self, path):
508 """list the checkpoints for a given file
514 """list the checkpoints for a given file
509
515
510 This contents manager currently only supports one checkpoint per file.
516 This contents manager currently only supports one checkpoint per file.
511 """
517 """
512 path = path.strip('/')
518 path = path.strip('/')
513 checkpoint_id = "checkpoint"
519 checkpoint_id = "checkpoint"
514 os_path = self.get_checkpoint_path(checkpoint_id, path)
520 os_path = self.get_checkpoint_path(checkpoint_id, path)
515 if not os.path.exists(os_path):
521 if not os.path.exists(os_path):
516 return []
522 return []
517 else:
523 else:
518 return [self.get_checkpoint_model(checkpoint_id, path)]
524 return [self.get_checkpoint_model(checkpoint_id, path)]
519
525
520
526
521 def restore_checkpoint(self, checkpoint_id, path):
527 def restore_checkpoint(self, checkpoint_id, path):
522 """restore a file to a checkpointed state"""
528 """restore a file to a checkpointed state"""
523 path = path.strip('/')
529 path = path.strip('/')
524 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
530 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
525 nb_path = self._get_os_path(path)
531 nb_path = self._get_os_path(path)
526 cp_path = self.get_checkpoint_path(checkpoint_id, path)
532 cp_path = self.get_checkpoint_path(checkpoint_id, path)
527 if not os.path.isfile(cp_path):
533 if not os.path.isfile(cp_path):
528 self.log.debug("checkpoint file does not exist: %s", cp_path)
534 self.log.debug("checkpoint file does not exist: %s", cp_path)
529 raise web.HTTPError(404,
535 raise web.HTTPError(404,
530 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
536 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
531 )
537 )
532 # ensure notebook is readable (never restore from an unreadable notebook)
538 # ensure notebook is readable (never restore from an unreadable notebook)
533 if cp_path.endswith('.ipynb'):
539 if cp_path.endswith('.ipynb'):
534 with io.open(cp_path, 'r', encoding='utf-8') as f:
540 with io.open(cp_path, 'r', encoding='utf-8') as f:
535 nbformat.read(f, as_version=4)
541 nbformat.read(f, as_version=4)
536 self._copy(cp_path, nb_path)
542 self._copy(cp_path, nb_path)
537 self.log.debug("copying %s -> %s", cp_path, nb_path)
543 self.log.debug("copying %s -> %s", cp_path, nb_path)
538
544
539 def delete_checkpoint(self, checkpoint_id, path):
545 def delete_checkpoint(self, checkpoint_id, path):
540 """delete a file's checkpoint"""
546 """delete a file's checkpoint"""
541 path = path.strip('/')
547 path = path.strip('/')
542 cp_path = self.get_checkpoint_path(checkpoint_id, path)
548 cp_path = self.get_checkpoint_path(checkpoint_id, path)
543 if not os.path.isfile(cp_path):
549 if not os.path.isfile(cp_path):
544 raise web.HTTPError(404,
550 raise web.HTTPError(404,
545 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
551 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
546 )
552 )
547 self.log.debug("unlinking %s", cp_path)
553 self.log.debug("unlinking %s", cp_path)
548 os.unlink(cp_path)
554 os.unlink(cp_path)
549
555
550 def info_string(self):
556 def info_string(self):
551 return "Serving notebooks from local directory: %s" % self.root_dir
557 return "Serving notebooks from local directory: %s" % self.root_dir
552
558
553 def get_kernel_path(self, path, model=None):
559 def get_kernel_path(self, path, model=None):
554 """Return the initial working dir a kernel associated with a given notebook"""
560 """Return the initial working dir a kernel associated with a given notebook"""
555 if '/' in path:
561 if '/' in path:
556 parent_dir = path.rsplit('/', 1)[0]
562 parent_dir = path.rsplit('/', 1)[0]
557 else:
563 else:
558 parent_dir = ''
564 parent_dir = ''
559 return self._get_os_path(parent_dir)
565 return self._get_os_path(parent_dir)
@@ -1,119 +1,127 b''
1 """A MultiKernelManager for use in the notebook webserver
1 """A MultiKernelManager for use in the notebook webserver
2
2
3 - raises HTTPErrors
3 - raises HTTPErrors
4 - creates REST API models
4 - creates REST API models
5 """
5 """
6
6
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10 import os
10 import os
11
11
12 from tornado import web
12 from tornado import web
13
13
14 from IPython.kernel.multikernelmanager import MultiKernelManager
14 from IPython.kernel.multikernelmanager import MultiKernelManager
15 from IPython.utils.traitlets import Unicode, TraitError
15 from IPython.utils.traitlets import List, Unicode, TraitError
16
16
17 from IPython.html.utils import to_os_path
17 from IPython.html.utils import to_os_path
18 from IPython.utils.py3compat import getcwd
18 from IPython.utils.py3compat import getcwd
19
19
20
20
21 class MappingKernelManager(MultiKernelManager):
21 class MappingKernelManager(MultiKernelManager):
22 """A KernelManager that handles notebook mapping and HTTP error handling"""
22 """A KernelManager that handles notebook mapping and HTTP error handling"""
23
23
24 def _kernel_manager_class_default(self):
24 def _kernel_manager_class_default(self):
25 return "IPython.kernel.ioloop.IOLoopKernelManager"
25 return "IPython.kernel.ioloop.IOLoopKernelManager"
26
26
27 root_dir = Unicode(getcwd(), config=True)
27 kernel_argv = List(Unicode)
28
29 root_dir = Unicode(config=True)
30
31 def _root_dir_default(self):
32 try:
33 return self.parent.notebook_dir
34 except AttributeError:
35 return getcwd()
28
36
29 def _root_dir_changed(self, name, old, new):
37 def _root_dir_changed(self, name, old, new):
30 """Do a bit of validation of the root dir."""
38 """Do a bit of validation of the root dir."""
31 if not os.path.isabs(new):
39 if not os.path.isabs(new):
32 # If we receive a non-absolute path, make it absolute.
40 # If we receive a non-absolute path, make it absolute.
33 self.root_dir = os.path.abspath(new)
41 self.root_dir = os.path.abspath(new)
34 return
42 return
35 if not os.path.exists(new) or not os.path.isdir(new):
43 if not os.path.exists(new) or not os.path.isdir(new):
36 raise TraitError("kernel root dir %r is not a directory" % new)
44 raise TraitError("kernel root dir %r is not a directory" % new)
37
45
38 #-------------------------------------------------------------------------
46 #-------------------------------------------------------------------------
39 # Methods for managing kernels and sessions
47 # Methods for managing kernels and sessions
40 #-------------------------------------------------------------------------
48 #-------------------------------------------------------------------------
41
49
42 def _handle_kernel_died(self, kernel_id):
50 def _handle_kernel_died(self, kernel_id):
43 """notice that a kernel died"""
51 """notice that a kernel died"""
44 self.log.warn("Kernel %s died, removing from map.", kernel_id)
52 self.log.warn("Kernel %s died, removing from map.", kernel_id)
45 self.remove_kernel(kernel_id)
53 self.remove_kernel(kernel_id)
46
54
47 def cwd_for_path(self, path):
55 def cwd_for_path(self, path):
48 """Turn API path into absolute OS path."""
56 """Turn API path into absolute OS path."""
49 # short circuit for NotebookManagers that pass in absolute paths
57 # short circuit for NotebookManagers that pass in absolute paths
50 if os.path.exists(path):
58 if os.path.exists(path):
51 return path
59 return path
52
60
53 os_path = to_os_path(path, self.root_dir)
61 os_path = to_os_path(path, self.root_dir)
54 # in the case of notebooks and kernels not being on the same filesystem,
62 # in the case of notebooks and kernels not being on the same filesystem,
55 # walk up to root_dir if the paths don't exist
63 # walk up to root_dir if the paths don't exist
56 while not os.path.exists(os_path) and os_path != self.root_dir:
64 while not os.path.exists(os_path) and os_path != self.root_dir:
57 os_path = os.path.dirname(os_path)
65 os_path = os.path.dirname(os_path)
58 return os_path
66 return os_path
59
67
60 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
68 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
61 """Start a kernel for a session and return its kernel_id.
69 """Start a kernel for a session and return its kernel_id.
62
70
63 Parameters
71 Parameters
64 ----------
72 ----------
65 kernel_id : uuid
73 kernel_id : uuid
66 The uuid to associate the new kernel with. If this
74 The uuid to associate the new kernel with. If this
67 is not None, this kernel will be persistent whenever it is
75 is not None, this kernel will be persistent whenever it is
68 requested.
76 requested.
69 path : API path
77 path : API path
70 The API path (unicode, '/' delimited) for the cwd.
78 The API path (unicode, '/' delimited) for the cwd.
71 Will be transformed to an OS path relative to root_dir.
79 Will be transformed to an OS path relative to root_dir.
72 kernel_name : str
80 kernel_name : str
73 The name identifying which kernel spec to launch. This is ignored if
81 The name identifying which kernel spec to launch. This is ignored if
74 an existing kernel is returned, but it may be checked in the future.
82 an existing kernel is returned, but it may be checked in the future.
75 """
83 """
76 if kernel_id is None:
84 if kernel_id is None:
77 if path is not None:
85 if path is not None:
78 kwargs['cwd'] = self.cwd_for_path(path)
86 kwargs['cwd'] = self.cwd_for_path(path)
79 kernel_id = super(MappingKernelManager, self).start_kernel(
87 kernel_id = super(MappingKernelManager, self).start_kernel(
80 kernel_name=kernel_name, **kwargs)
88 kernel_name=kernel_name, **kwargs)
81 self.log.info("Kernel started: %s" % kernel_id)
89 self.log.info("Kernel started: %s" % kernel_id)
82 self.log.debug("Kernel args: %r" % kwargs)
90 self.log.debug("Kernel args: %r" % kwargs)
83 # register callback for failed auto-restart
91 # register callback for failed auto-restart
84 self.add_restart_callback(kernel_id,
92 self.add_restart_callback(kernel_id,
85 lambda : self._handle_kernel_died(kernel_id),
93 lambda : self._handle_kernel_died(kernel_id),
86 'dead',
94 'dead',
87 )
95 )
88 else:
96 else:
89 self._check_kernel_id(kernel_id)
97 self._check_kernel_id(kernel_id)
90 self.log.info("Using existing kernel: %s" % kernel_id)
98 self.log.info("Using existing kernel: %s" % kernel_id)
91 return kernel_id
99 return kernel_id
92
100
93 def shutdown_kernel(self, kernel_id, now=False):
101 def shutdown_kernel(self, kernel_id, now=False):
94 """Shutdown a kernel by kernel_id"""
102 """Shutdown a kernel by kernel_id"""
95 self._check_kernel_id(kernel_id)
103 self._check_kernel_id(kernel_id)
96 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
104 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
97
105
98 def kernel_model(self, kernel_id):
106 def kernel_model(self, kernel_id):
99 """Return a dictionary of kernel information described in the
107 """Return a dictionary of kernel information described in the
100 JSON standard model."""
108 JSON standard model."""
101 self._check_kernel_id(kernel_id)
109 self._check_kernel_id(kernel_id)
102 model = {"id":kernel_id,
110 model = {"id":kernel_id,
103 "name": self._kernels[kernel_id].kernel_name}
111 "name": self._kernels[kernel_id].kernel_name}
104 return model
112 return model
105
113
106 def list_kernels(self):
114 def list_kernels(self):
107 """Returns a list of kernel_id's of kernels running."""
115 """Returns a list of kernel_id's of kernels running."""
108 kernels = []
116 kernels = []
109 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
117 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
110 for kernel_id in kernel_ids:
118 for kernel_id in kernel_ids:
111 model = self.kernel_model(kernel_id)
119 model = self.kernel_model(kernel_id)
112 kernels.append(model)
120 kernels.append(model)
113 return kernels
121 return kernels
114
122
115 # override _check_kernel_id to raise 404 instead of KeyError
123 # override _check_kernel_id to raise 404 instead of KeyError
116 def _check_kernel_id(self, kernel_id):
124 def _check_kernel_id(self, kernel_id):
117 """Check a that a kernel_id exists and raise 404 if not."""
125 """Check a that a kernel_id exists and raise 404 if not."""
118 if kernel_id not in self:
126 if kernel_id not in self:
119 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
127 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
General Comments 0
You need to be logged in to leave comments. Login now