##// END OF EJS Templates
Merge pull request #5116 from minrk/os_path...
Brian E. Granger -
r15428:6b11b356 merge
parent child Browse files
Show More
@@ -1,143 +1,140
1 import io
1 import io
2 import os
2 import os
3 import zipfile
3 import zipfile
4
4
5 from tornado import web
5 from tornado import web
6
6
7 from ..base.handlers import IPythonHandler, notebook_path_regex
7 from ..base.handlers import IPythonHandler, notebook_path_regex
8 from IPython.nbformat.current import to_notebook_json
8 from IPython.nbformat.current import to_notebook_json
9
9
10 from IPython.utils import tz
10 from IPython.utils import tz
11 from IPython.utils.py3compat import cast_bytes
11 from IPython.utils.py3compat import cast_bytes
12
12
13 import sys
13 import sys
14
14
15 def find_resource_files(output_files_dir):
15 def find_resource_files(output_files_dir):
16 files = []
16 files = []
17 for dirpath, dirnames, filenames in os.walk(output_files_dir):
17 for dirpath, dirnames, filenames in os.walk(output_files_dir):
18 files.extend([os.path.join(dirpath, f) for f in filenames])
18 files.extend([os.path.join(dirpath, f) for f in filenames])
19 return files
19 return files
20
20
21 def respond_zip(handler, name, output, resources):
21 def respond_zip(handler, name, output, resources):
22 """Zip up the output and resource files and respond with the zip file.
22 """Zip up the output and resource files and respond with the zip file.
23
23
24 Returns True if it has served a zip file, False if there are no resource
24 Returns True if it has served a zip file, False if there are no resource
25 files, in which case we serve the plain output file.
25 files, in which case we serve the plain output file.
26 """
26 """
27 # Check if we have resource files we need to zip
27 # Check if we have resource files we need to zip
28 output_files = resources.get('outputs', None)
28 output_files = resources.get('outputs', None)
29 if not output_files:
29 if not output_files:
30 return False
30 return False
31
31
32 # Headers
32 # Headers
33 zip_filename = os.path.splitext(name)[0] + '.zip'
33 zip_filename = os.path.splitext(name)[0] + '.zip'
34 handler.set_header('Content-Disposition',
34 handler.set_header('Content-Disposition',
35 'attachment; filename="%s"' % zip_filename)
35 'attachment; filename="%s"' % zip_filename)
36 handler.set_header('Content-Type', 'application/zip')
36 handler.set_header('Content-Type', 'application/zip')
37
37
38 # Prepare the zip file
38 # Prepare the zip file
39 buffer = io.BytesIO()
39 buffer = io.BytesIO()
40 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
40 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
41 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
41 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
42 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
42 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
43 for filename, data in output_files.items():
43 for filename, data in output_files.items():
44 zipf.writestr(os.path.basename(filename), data)
44 zipf.writestr(os.path.basename(filename), data)
45 zipf.close()
45 zipf.close()
46
46
47 handler.finish(buffer.getvalue())
47 handler.finish(buffer.getvalue())
48 return True
48 return True
49
49
50 def get_exporter(format, **kwargs):
50 def get_exporter(format, **kwargs):
51 """get an exporter, raising appropriate errors"""
51 """get an exporter, raising appropriate errors"""
52 # if this fails, will raise 500
52 # if this fails, will raise 500
53 try:
53 try:
54 from IPython.nbconvert.exporters.export import exporter_map
54 from IPython.nbconvert.exporters.export import exporter_map
55 except ImportError as e:
55 except ImportError as e:
56 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
56 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
57
57
58 try:
58 try:
59 Exporter = exporter_map[format]
59 Exporter = exporter_map[format]
60 except KeyError:
60 except KeyError:
61 # should this be 400?
61 # should this be 400?
62 raise web.HTTPError(404, u"No exporter for format: %s" % format)
62 raise web.HTTPError(404, u"No exporter for format: %s" % format)
63
63
64 try:
64 try:
65 return Exporter(**kwargs)
65 return Exporter(**kwargs)
66 except Exception as e:
66 except Exception as e:
67 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
67 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
68
68
69 class NbconvertFileHandler(IPythonHandler):
69 class NbconvertFileHandler(IPythonHandler):
70
70
71 SUPPORTED_METHODS = ('GET',)
71 SUPPORTED_METHODS = ('GET',)
72
72
73 @web.authenticated
73 @web.authenticated
74 def get(self, format, path='', name=None):
74 def get(self, format, path='', name=None):
75
75
76 exporter = get_exporter(format, config=self.config)
76 exporter = get_exporter(format, config=self.config)
77
77
78 path = path.strip('/')
78 path = path.strip('/')
79 os_path = self.notebook_manager.get_os_path(name, path)
79 model = self.notebook_manager.get_notebook(name=name, path=path)
80 if not os.path.isfile(os_path):
81 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
82
80
83 info = os.stat(os_path)
81 self.set_header('Last-Modified', model['last_modified'])
84 self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime))
85
82
86 try:
83 try:
87 output, resources = exporter.from_filename(os_path)
84 output, resources = exporter.from_notebook_node(model['content'])
88 except Exception as e:
85 except Exception as e:
89 raise web.HTTPError(500, "nbconvert failed: %s" % e)
86 raise web.HTTPError(500, "nbconvert failed: %s" % e)
90
87
91 if respond_zip(self, name, output, resources):
88 if respond_zip(self, name, output, resources):
92 return
89 return
93
90
94 # Force download if requested
91 # Force download if requested
95 if self.get_argument('download', 'false').lower() == 'true':
92 if self.get_argument('download', 'false').lower() == 'true':
96 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
93 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
97 self.set_header('Content-Disposition',
94 self.set_header('Content-Disposition',
98 'attachment; filename="%s"' % filename)
95 'attachment; filename="%s"' % filename)
99
96
100 # MIME type
97 # MIME type
101 if exporter.output_mimetype:
98 if exporter.output_mimetype:
102 self.set_header('Content-Type',
99 self.set_header('Content-Type',
103 '%s; charset=utf-8' % exporter.output_mimetype)
100 '%s; charset=utf-8' % exporter.output_mimetype)
104
101
105 self.finish(output)
102 self.finish(output)
106
103
107 class NbconvertPostHandler(IPythonHandler):
104 class NbconvertPostHandler(IPythonHandler):
108 SUPPORTED_METHODS = ('POST',)
105 SUPPORTED_METHODS = ('POST',)
109
106
110 @web.authenticated
107 @web.authenticated
111 def post(self, format):
108 def post(self, format):
112 exporter = get_exporter(format, config=self.config)
109 exporter = get_exporter(format, config=self.config)
113
110
114 model = self.get_json_body()
111 model = self.get_json_body()
115 nbnode = to_notebook_json(model['content'])
112 nbnode = to_notebook_json(model['content'])
116
113
117 try:
114 try:
118 output, resources = exporter.from_notebook_node(nbnode)
115 output, resources = exporter.from_notebook_node(nbnode)
119 except Exception as e:
116 except Exception as e:
120 raise web.HTTPError(500, "nbconvert failed: %s" % e)
117 raise web.HTTPError(500, "nbconvert failed: %s" % e)
121
118
122 if respond_zip(self, nbnode.metadata.name, output, resources):
119 if respond_zip(self, nbnode.metadata.name, output, resources):
123 return
120 return
124
121
125 # MIME type
122 # MIME type
126 if exporter.output_mimetype:
123 if exporter.output_mimetype:
127 self.set_header('Content-Type',
124 self.set_header('Content-Type',
128 '%s; charset=utf-8' % exporter.output_mimetype)
125 '%s; charset=utf-8' % exporter.output_mimetype)
129
126
130 self.finish(output)
127 self.finish(output)
131
128
132 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
133 # URL to handler mappings
130 # URL to handler mappings
134 #-----------------------------------------------------------------------------
131 #-----------------------------------------------------------------------------
135
132
136 _format_regex = r"(?P<format>\w+)"
133 _format_regex = r"(?P<format>\w+)"
137
134
138
135
139 default_handlers = [
136 default_handlers = [
140 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
137 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
141 NbconvertFileHandler),
138 NbconvertFileHandler),
142 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
139 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
143 ] No newline at end of file
140 ]
@@ -1,821 +1,842
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 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 from __future__ import print_function
8 from __future__ import print_function
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2013 The IPython Development Team
10 # Copyright (C) 2013 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 # stdlib
20 # stdlib
21 import errno
21 import errno
22 import io
22 import io
23 import json
23 import json
24 import logging
24 import logging
25 import os
25 import os
26 import random
26 import random
27 import select
27 import select
28 import signal
28 import signal
29 import socket
29 import socket
30 import sys
30 import sys
31 import threading
31 import threading
32 import time
32 import time
33 import webbrowser
33 import webbrowser
34
34
35
35
36 # Third party
36 # Third party
37 # check for pyzmq 2.1.11
37 # check for pyzmq 2.1.11
38 from IPython.utils.zmqrelated import check_for_zmq
38 from IPython.utils.zmqrelated import check_for_zmq
39 check_for_zmq('2.1.11', 'IPython.html')
39 check_for_zmq('2.1.11', 'IPython.html')
40
40
41 from jinja2 import Environment, FileSystemLoader
41 from jinja2 import Environment, FileSystemLoader
42
42
43 # Install the pyzmq ioloop. This has to be done before anything else from
43 # Install the pyzmq ioloop. This has to be done before anything else from
44 # tornado is imported.
44 # tornado is imported.
45 from zmq.eventloop import ioloop
45 from zmq.eventloop import ioloop
46 ioloop.install()
46 ioloop.install()
47
47
48 # check for tornado 3.1.0
48 # check for tornado 3.1.0
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 try:
50 try:
51 import tornado
51 import tornado
52 except ImportError:
52 except ImportError:
53 raise ImportError(msg)
53 raise ImportError(msg)
54 try:
54 try:
55 version_info = tornado.version_info
55 version_info = tornado.version_info
56 except AttributeError:
56 except AttributeError:
57 raise ImportError(msg + ", but you have < 1.1.0")
57 raise ImportError(msg + ", but you have < 1.1.0")
58 if version_info < (3,1,0):
58 if version_info < (3,1,0):
59 raise ImportError(msg + ", but you have %s" % tornado.version)
59 raise ImportError(msg + ", but you have %s" % tornado.version)
60
60
61 from tornado import httpserver
61 from tornado import httpserver
62 from tornado import web
62 from tornado import web
63
63
64 # Our own libraries
64 # Our own libraries
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 from .base.handlers import Template404
66 from .base.handlers import Template404
67 from .log import log_request
67 from .log import log_request
68 from .services.kernels.kernelmanager import MappingKernelManager
68 from .services.kernels.kernelmanager import MappingKernelManager
69 from .services.notebooks.nbmanager import NotebookManager
69 from .services.notebooks.nbmanager import NotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
71 from .services.clusters.clustermanager import ClusterManager
71 from .services.clusters.clustermanager import ClusterManager
72 from .services.sessions.sessionmanager import SessionManager
72 from .services.sessions.sessionmanager import SessionManager
73
73
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75
75
76 from IPython.config.application import catch_config_error, boolean_flag
76 from IPython.config.application import catch_config_error, boolean_flag
77 from IPython.core.application import BaseIPythonApplication
77 from IPython.core.application import BaseIPythonApplication
78 from IPython.core.profiledir import ProfileDir
78 from IPython.core.profiledir import ProfileDir
79 from IPython.consoleapp import IPythonConsoleApp
79 from IPython.consoleapp import IPythonConsoleApp
80 from IPython.kernel import swallow_argv
80 from IPython.kernel import swallow_argv
81 from IPython.kernel.zmq.session import default_secure
81 from IPython.kernel.zmq.session import default_secure
82 from IPython.kernel.zmq.kernelapp import (
82 from IPython.kernel.zmq.kernelapp import (
83 kernel_flags,
83 kernel_flags,
84 kernel_aliases,
84 kernel_aliases,
85 )
85 )
86 from IPython.utils.importstring import import_item
86 from IPython.utils.importstring import import_item
87 from IPython.utils.localinterfaces import localhost
87 from IPython.utils.localinterfaces import localhost
88 from IPython.utils import submodule
88 from IPython.utils import submodule
89 from IPython.utils.traitlets import (
89 from IPython.utils.traitlets import (
90 Dict, Unicode, Integer, List, Bool, Bytes,
90 Dict, Unicode, Integer, List, Bool, Bytes,
91 DottedObjectName
91 DottedObjectName, TraitError,
92 )
92 )
93 from IPython.utils import py3compat
93 from IPython.utils import py3compat
94 from IPython.utils.path import filefind, get_ipython_dir
94 from IPython.utils.path import filefind, get_ipython_dir
95
95
96 from .utils import url_path_join
96 from .utils import url_path_join
97
97
98 #-----------------------------------------------------------------------------
98 #-----------------------------------------------------------------------------
99 # Module globals
99 # Module globals
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101
101
102 _examples = """
102 _examples = """
103 ipython notebook # start the notebook
103 ipython notebook # start the notebook
104 ipython notebook --profile=sympy # use the sympy profile
104 ipython notebook --profile=sympy # use the sympy profile
105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 """
106 """
107
107
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109 # Helper functions
109 # Helper functions
110 #-----------------------------------------------------------------------------
110 #-----------------------------------------------------------------------------
111
111
112 def random_ports(port, n):
112 def random_ports(port, n):
113 """Generate a list of n random ports near the given port.
113 """Generate a list of n random ports near the given port.
114
114
115 The first 5 ports will be sequential, and the remaining n-5 will be
115 The first 5 ports will be sequential, and the remaining n-5 will be
116 randomly selected in the range [port-2*n, port+2*n].
116 randomly selected in the range [port-2*n, port+2*n].
117 """
117 """
118 for i in range(min(5, n)):
118 for i in range(min(5, n)):
119 yield port + i
119 yield port + i
120 for i in range(n-5):
120 for i in range(n-5):
121 yield max(1, port + random.randint(-2*n, 2*n))
121 yield max(1, port + random.randint(-2*n, 2*n))
122
122
123 def load_handlers(name):
123 def load_handlers(name):
124 """Load the (URL pattern, handler) tuples for each component."""
124 """Load the (URL pattern, handler) tuples for each component."""
125 name = 'IPython.html.' + name
125 name = 'IPython.html.' + name
126 mod = __import__(name, fromlist=['default_handlers'])
126 mod = __import__(name, fromlist=['default_handlers'])
127 return mod.default_handlers
127 return mod.default_handlers
128
128
129 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
130 # The Tornado web application
130 # The Tornado web application
131 #-----------------------------------------------------------------------------
131 #-----------------------------------------------------------------------------
132
132
133 class NotebookWebApplication(web.Application):
133 class NotebookWebApplication(web.Application):
134
134
135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 cluster_manager, session_manager, log, base_url,
136 cluster_manager, session_manager, log, base_url,
137 settings_overrides):
137 settings_overrides):
138
138
139 settings = self.init_settings(
139 settings = self.init_settings(
140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 session_manager, log, base_url, settings_overrides)
141 session_manager, log, base_url, settings_overrides)
142 handlers = self.init_handlers(settings)
142 handlers = self.init_handlers(settings)
143
143
144 super(NotebookWebApplication, self).__init__(handlers, **settings)
144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145
145
146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 cluster_manager, session_manager, log, base_url,
147 cluster_manager, session_manager, log, base_url,
148 settings_overrides):
148 settings_overrides):
149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # base_url will always be unicode, which will in turn
150 # base_url will always be unicode, which will in turn
151 # make the patterns unicode, and ultimately result in unicode
151 # make the patterns unicode, and ultimately result in unicode
152 # keys in kwargs to handler._execute(**kwargs) in tornado.
152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # This enforces that base_url be ascii in that situation.
153 # This enforces that base_url be ascii in that situation.
154 #
154 #
155 # Note that the URLs these patterns check against are escaped,
155 # Note that the URLs these patterns check against are escaped,
156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 base_url = py3compat.unicode_to_str(base_url, 'ascii')
157 base_url = py3compat.unicode_to_str(base_url, 'ascii')
158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 settings = dict(
159 settings = dict(
160 # basics
160 # basics
161 log_function=log_request,
161 log_function=log_request,
162 base_url=base_url,
162 base_url=base_url,
163 template_path=template_path,
163 template_path=template_path,
164 static_path=ipython_app.static_file_path,
164 static_path=ipython_app.static_file_path,
165 static_handler_class = FileFindHandler,
165 static_handler_class = FileFindHandler,
166 static_url_prefix = url_path_join(base_url,'/static/'),
166 static_url_prefix = url_path_join(base_url,'/static/'),
167
167
168 # authentication
168 # authentication
169 cookie_secret=ipython_app.cookie_secret,
169 cookie_secret=ipython_app.cookie_secret,
170 login_url=url_path_join(base_url,'/login'),
170 login_url=url_path_join(base_url,'/login'),
171 password=ipython_app.password,
171 password=ipython_app.password,
172
172
173 # managers
173 # managers
174 kernel_manager=kernel_manager,
174 kernel_manager=kernel_manager,
175 notebook_manager=notebook_manager,
175 notebook_manager=notebook_manager,
176 cluster_manager=cluster_manager,
176 cluster_manager=cluster_manager,
177 session_manager=session_manager,
177 session_manager=session_manager,
178
178
179 # IPython stuff
179 # IPython stuff
180 nbextensions_path = ipython_app.nbextensions_path,
180 nbextensions_path = ipython_app.nbextensions_path,
181 mathjax_url=ipython_app.mathjax_url,
181 mathjax_url=ipython_app.mathjax_url,
182 config=ipython_app.config,
182 config=ipython_app.config,
183 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
183 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
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 handlers = []
192 handlers = []
193 handlers.extend(load_handlers('base.handlers'))
193 handlers.extend(load_handlers('base.handlers'))
194 handlers.extend(load_handlers('tree.handlers'))
194 handlers.extend(load_handlers('tree.handlers'))
195 handlers.extend(load_handlers('auth.login'))
195 handlers.extend(load_handlers('auth.login'))
196 handlers.extend(load_handlers('auth.logout'))
196 handlers.extend(load_handlers('auth.logout'))
197 handlers.extend(load_handlers('notebook.handlers'))
197 handlers.extend(load_handlers('notebook.handlers'))
198 handlers.extend(load_handlers('nbconvert.handlers'))
198 handlers.extend(load_handlers('nbconvert.handlers'))
199 handlers.extend(load_handlers('services.kernels.handlers'))
199 handlers.extend(load_handlers('services.kernels.handlers'))
200 handlers.extend(load_handlers('services.notebooks.handlers'))
200 handlers.extend(load_handlers('services.notebooks.handlers'))
201 handlers.extend(load_handlers('services.clusters.handlers'))
201 handlers.extend(load_handlers('services.clusters.handlers'))
202 handlers.extend(load_handlers('services.sessions.handlers'))
202 handlers.extend(load_handlers('services.sessions.handlers'))
203 handlers.extend(load_handlers('services.nbconvert.handlers'))
203 handlers.extend(load_handlers('services.nbconvert.handlers'))
204 handlers.extend([
204 # FIXME: /files/ should be handled by the Contents service when it exists
205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
205 nbm = settings['notebook_manager']
206 if hasattr(nbm, 'notebook_dir'):
207 handlers.extend([
208 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
206 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
209 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
207 ])
210 ])
208 # prepend base_url onto the patterns that we match
211 # prepend base_url onto the patterns that we match
209 new_handlers = []
212 new_handlers = []
210 for handler in handlers:
213 for handler in handlers:
211 pattern = url_path_join(settings['base_url'], handler[0])
214 pattern = url_path_join(settings['base_url'], handler[0])
212 new_handler = tuple([pattern] + list(handler[1:]))
215 new_handler = tuple([pattern] + list(handler[1:]))
213 new_handlers.append(new_handler)
216 new_handlers.append(new_handler)
214 # add 404 on the end, which will catch everything that falls through
217 # add 404 on the end, which will catch everything that falls through
215 new_handlers.append((r'(.*)', Template404))
218 new_handlers.append((r'(.*)', Template404))
216 return new_handlers
219 return new_handlers
217
220
218
221
219 class NbserverListApp(BaseIPythonApplication):
222 class NbserverListApp(BaseIPythonApplication):
220
223
221 description="List currently running notebook servers in this profile."
224 description="List currently running notebook servers in this profile."
222
225
223 flags = dict(
226 flags = dict(
224 json=({'NbserverListApp': {'json': True}},
227 json=({'NbserverListApp': {'json': True}},
225 "Produce machine-readable JSON output."),
228 "Produce machine-readable JSON output."),
226 )
229 )
227
230
228 json = Bool(False, config=True,
231 json = Bool(False, config=True,
229 help="If True, each line of output will be a JSON object with the "
232 help="If True, each line of output will be a JSON object with the "
230 "details from the server info file.")
233 "details from the server info file.")
231
234
232 def start(self):
235 def start(self):
233 if not self.json:
236 if not self.json:
234 print("Currently running servers:")
237 print("Currently running servers:")
235 for serverinfo in list_running_servers(self.profile):
238 for serverinfo in list_running_servers(self.profile):
236 if self.json:
239 if self.json:
237 print(json.dumps(serverinfo))
240 print(json.dumps(serverinfo))
238 else:
241 else:
239 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
242 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
240
243
241 #-----------------------------------------------------------------------------
244 #-----------------------------------------------------------------------------
242 # Aliases and Flags
245 # Aliases and Flags
243 #-----------------------------------------------------------------------------
246 #-----------------------------------------------------------------------------
244
247
245 flags = dict(kernel_flags)
248 flags = dict(kernel_flags)
246 flags['no-browser']=(
249 flags['no-browser']=(
247 {'NotebookApp' : {'open_browser' : False}},
250 {'NotebookApp' : {'open_browser' : False}},
248 "Don't open the notebook in a browser after startup."
251 "Don't open the notebook in a browser after startup."
249 )
252 )
250 flags['no-mathjax']=(
253 flags['no-mathjax']=(
251 {'NotebookApp' : {'enable_mathjax' : False}},
254 {'NotebookApp' : {'enable_mathjax' : False}},
252 """Disable MathJax
255 """Disable MathJax
253
256
254 MathJax is the javascript library IPython uses to render math/LaTeX. It is
257 MathJax is the javascript library IPython uses to render math/LaTeX. It is
255 very large, so you may want to disable it if you have a slow internet
258 very large, so you may want to disable it if you have a slow internet
256 connection, or for offline use of the notebook.
259 connection, or for offline use of the notebook.
257
260
258 When disabled, equations etc. will appear as their untransformed TeX source.
261 When disabled, equations etc. will appear as their untransformed TeX source.
259 """
262 """
260 )
263 )
261
264
262 # Add notebook manager flags
265 # Add notebook manager flags
263 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
266 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
264 'Auto-save a .py script everytime the .ipynb notebook is saved',
267 'Auto-save a .py script everytime the .ipynb notebook is saved',
265 'Do not auto-save .py scripts for every notebook'))
268 'Do not auto-save .py scripts for every notebook'))
266
269
267 # the flags that are specific to the frontend
270 # the flags that are specific to the frontend
268 # these must be scrubbed before being passed to the kernel,
271 # these must be scrubbed before being passed to the kernel,
269 # or it will raise an error on unrecognized flags
272 # or it will raise an error on unrecognized flags
270 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
273 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
271
274
272 aliases = dict(kernel_aliases)
275 aliases = dict(kernel_aliases)
273
276
274 aliases.update({
277 aliases.update({
275 'ip': 'NotebookApp.ip',
278 'ip': 'NotebookApp.ip',
276 'port': 'NotebookApp.port',
279 'port': 'NotebookApp.port',
277 'port-retries': 'NotebookApp.port_retries',
280 'port-retries': 'NotebookApp.port_retries',
278 'transport': 'KernelManager.transport',
281 'transport': 'KernelManager.transport',
279 'keyfile': 'NotebookApp.keyfile',
282 'keyfile': 'NotebookApp.keyfile',
280 'certfile': 'NotebookApp.certfile',
283 'certfile': 'NotebookApp.certfile',
281 'notebook-dir': 'NotebookManager.notebook_dir',
284 'notebook-dir': 'NotebookApp.notebook_dir',
282 'browser': 'NotebookApp.browser',
285 'browser': 'NotebookApp.browser',
283 })
286 })
284
287
285 # remove ipkernel flags that are singletons, and don't make sense in
288 # remove ipkernel flags that are singletons, and don't make sense in
286 # multi-kernel evironment:
289 # multi-kernel evironment:
287 aliases.pop('f', None)
290 aliases.pop('f', None)
288
291
289 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
292 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
290 u'notebook-dir', u'profile', u'profile-dir']
293 u'notebook-dir', u'profile', u'profile-dir']
291
294
292 #-----------------------------------------------------------------------------
295 #-----------------------------------------------------------------------------
293 # NotebookApp
296 # NotebookApp
294 #-----------------------------------------------------------------------------
297 #-----------------------------------------------------------------------------
295
298
296 class NotebookApp(BaseIPythonApplication):
299 class NotebookApp(BaseIPythonApplication):
297
300
298 name = 'ipython-notebook'
301 name = 'ipython-notebook'
299
302
300 description = """
303 description = """
301 The IPython HTML Notebook.
304 The IPython HTML Notebook.
302
305
303 This launches a Tornado based HTML Notebook Server that serves up an
306 This launches a Tornado based HTML Notebook Server that serves up an
304 HTML5/Javascript Notebook client.
307 HTML5/Javascript Notebook client.
305 """
308 """
306 examples = _examples
309 examples = _examples
307
310
308 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
311 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
309 FileNotebookManager]
312 FileNotebookManager]
310 flags = Dict(flags)
313 flags = Dict(flags)
311 aliases = Dict(aliases)
314 aliases = Dict(aliases)
312
315
313 subcommands = dict(
316 subcommands = dict(
314 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
317 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
315 )
318 )
316
319
317 kernel_argv = List(Unicode)
320 kernel_argv = List(Unicode)
318
321
319 def _log_level_default(self):
322 def _log_level_default(self):
320 return logging.INFO
323 return logging.INFO
321
324
322 def _log_format_default(self):
325 def _log_format_default(self):
323 """override default log format to include time"""
326 """override default log format to include time"""
324 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
327 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
325
328
326 # create requested profiles by default, if they don't exist:
329 # create requested profiles by default, if they don't exist:
327 auto_create = Bool(True)
330 auto_create = Bool(True)
328
331
329 # file to be opened in the notebook server
332 # file to be opened in the notebook server
330 file_to_run = Unicode('')
333 file_to_run = Unicode('')
331
334
332 # Network related information.
335 # Network related information.
333
336
334 ip = Unicode(config=True,
337 ip = Unicode(config=True,
335 help="The IP address the notebook server will listen on."
338 help="The IP address the notebook server will listen on."
336 )
339 )
337 def _ip_default(self):
340 def _ip_default(self):
338 return localhost()
341 return localhost()
339
342
340 def _ip_changed(self, name, old, new):
343 def _ip_changed(self, name, old, new):
341 if new == u'*': self.ip = u''
344 if new == u'*': self.ip = u''
342
345
343 port = Integer(8888, config=True,
346 port = Integer(8888, config=True,
344 help="The port the notebook server will listen on."
347 help="The port the notebook server will listen on."
345 )
348 )
346 port_retries = Integer(50, config=True,
349 port_retries = Integer(50, config=True,
347 help="The number of additional ports to try if the specified port is not available."
350 help="The number of additional ports to try if the specified port is not available."
348 )
351 )
349
352
350 certfile = Unicode(u'', config=True,
353 certfile = Unicode(u'', config=True,
351 help="""The full path to an SSL/TLS certificate file."""
354 help="""The full path to an SSL/TLS certificate file."""
352 )
355 )
353
356
354 keyfile = Unicode(u'', config=True,
357 keyfile = Unicode(u'', config=True,
355 help="""The full path to a private key file for usage with SSL/TLS."""
358 help="""The full path to a private key file for usage with SSL/TLS."""
356 )
359 )
357
360
358 cookie_secret = Bytes(b'', config=True,
361 cookie_secret = Bytes(b'', config=True,
359 help="""The random bytes used to secure cookies.
362 help="""The random bytes used to secure cookies.
360 By default this is a new random number every time you start the Notebook.
363 By default this is a new random number every time you start the Notebook.
361 Set it to a value in a config file to enable logins to persist across server sessions.
364 Set it to a value in a config file to enable logins to persist across server sessions.
362
365
363 Note: Cookie secrets should be kept private, do not share config files with
366 Note: Cookie secrets should be kept private, do not share config files with
364 cookie_secret stored in plaintext (you can read the value from a file).
367 cookie_secret stored in plaintext (you can read the value from a file).
365 """
368 """
366 )
369 )
367 def _cookie_secret_default(self):
370 def _cookie_secret_default(self):
368 return os.urandom(1024)
371 return os.urandom(1024)
369
372
370 password = Unicode(u'', config=True,
373 password = Unicode(u'', config=True,
371 help="""Hashed password to use for web authentication.
374 help="""Hashed password to use for web authentication.
372
375
373 To generate, type in a python/IPython shell:
376 To generate, type in a python/IPython shell:
374
377
375 from IPython.lib import passwd; passwd()
378 from IPython.lib import passwd; passwd()
376
379
377 The string should be of the form type:salt:hashed-password.
380 The string should be of the form type:salt:hashed-password.
378 """
381 """
379 )
382 )
380
383
381 open_browser = Bool(True, config=True,
384 open_browser = Bool(True, config=True,
382 help="""Whether to open in a browser after starting.
385 help="""Whether to open in a browser after starting.
383 The specific browser used is platform dependent and
386 The specific browser used is platform dependent and
384 determined by the python standard library `webbrowser`
387 determined by the python standard library `webbrowser`
385 module, unless it is overridden using the --browser
388 module, unless it is overridden using the --browser
386 (NotebookApp.browser) configuration option.
389 (NotebookApp.browser) configuration option.
387 """)
390 """)
388
391
389 browser = Unicode(u'', config=True,
392 browser = Unicode(u'', config=True,
390 help="""Specify what command to use to invoke a web
393 help="""Specify what command to use to invoke a web
391 browser when opening the notebook. If not specified, the
394 browser when opening the notebook. If not specified, the
392 default browser will be determined by the `webbrowser`
395 default browser will be determined by the `webbrowser`
393 standard library module, which allows setting of the
396 standard library module, which allows setting of the
394 BROWSER environment variable to override it.
397 BROWSER environment variable to override it.
395 """)
398 """)
396
399
397 webapp_settings = Dict(config=True,
400 webapp_settings = Dict(config=True,
398 help="Supply overrides for the tornado.web.Application that the "
401 help="Supply overrides for the tornado.web.Application that the "
399 "IPython notebook uses.")
402 "IPython notebook uses.")
400
403
401 enable_mathjax = Bool(True, config=True,
404 enable_mathjax = Bool(True, config=True,
402 help="""Whether to enable MathJax for typesetting math/TeX
405 help="""Whether to enable MathJax for typesetting math/TeX
403
406
404 MathJax is the javascript library IPython uses to render math/LaTeX. It is
407 MathJax is the javascript library IPython uses to render math/LaTeX. It is
405 very large, so you may want to disable it if you have a slow internet
408 very large, so you may want to disable it if you have a slow internet
406 connection, or for offline use of the notebook.
409 connection, or for offline use of the notebook.
407
410
408 When disabled, equations etc. will appear as their untransformed TeX source.
411 When disabled, equations etc. will appear as their untransformed TeX source.
409 """
412 """
410 )
413 )
411 def _enable_mathjax_changed(self, name, old, new):
414 def _enable_mathjax_changed(self, name, old, new):
412 """set mathjax url to empty if mathjax is disabled"""
415 """set mathjax url to empty if mathjax is disabled"""
413 if not new:
416 if not new:
414 self.mathjax_url = u''
417 self.mathjax_url = u''
415
418
416 base_url = Unicode('/', config=True,
419 base_url = Unicode('/', config=True,
417 help='''The base URL for the notebook server.
420 help='''The base URL for the notebook server.
418
421
419 Leading and trailing slashes can be omitted,
422 Leading and trailing slashes can be omitted,
420 and will automatically be added.
423 and will automatically be added.
421 ''')
424 ''')
422 def _base_url_changed(self, name, old, new):
425 def _base_url_changed(self, name, old, new):
423 if not new.startswith('/'):
426 if not new.startswith('/'):
424 self.base_url = '/'+new
427 self.base_url = '/'+new
425 elif not new.endswith('/'):
428 elif not new.endswith('/'):
426 self.base_url = new+'/'
429 self.base_url = new+'/'
427
430
428 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
431 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
429 def _base_project_url_changed(self, name, old, new):
432 def _base_project_url_changed(self, name, old, new):
430 self.log.warn("base_project_url is deprecated, use base_url")
433 self.log.warn("base_project_url is deprecated, use base_url")
431 self.base_url = new
434 self.base_url = new
432
435
433 extra_static_paths = List(Unicode, config=True,
436 extra_static_paths = List(Unicode, config=True,
434 help="""Extra paths to search for serving static files.
437 help="""Extra paths to search for serving static files.
435
438
436 This allows adding javascript/css to be available from the notebook server machine,
439 This allows adding javascript/css to be available from the notebook server machine,
437 or overriding individual files in the IPython"""
440 or overriding individual files in the IPython"""
438 )
441 )
439 def _extra_static_paths_default(self):
442 def _extra_static_paths_default(self):
440 return [os.path.join(self.profile_dir.location, 'static')]
443 return [os.path.join(self.profile_dir.location, 'static')]
441
444
442 @property
445 @property
443 def static_file_path(self):
446 def static_file_path(self):
444 """return extra paths + the default location"""
447 """return extra paths + the default location"""
445 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
448 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
446
449
447 nbextensions_path = List(Unicode, config=True,
450 nbextensions_path = List(Unicode, config=True,
448 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
451 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
449 )
452 )
450 def _nbextensions_path_default(self):
453 def _nbextensions_path_default(self):
451 return [os.path.join(get_ipython_dir(), 'nbextensions')]
454 return [os.path.join(get_ipython_dir(), 'nbextensions')]
452
455
453 mathjax_url = Unicode("", config=True,
456 mathjax_url = Unicode("", config=True,
454 help="""The url for MathJax.js."""
457 help="""The url for MathJax.js."""
455 )
458 )
456 def _mathjax_url_default(self):
459 def _mathjax_url_default(self):
457 if not self.enable_mathjax:
460 if not self.enable_mathjax:
458 return u''
461 return u''
459 static_url_prefix = self.webapp_settings.get("static_url_prefix",
462 static_url_prefix = self.webapp_settings.get("static_url_prefix",
460 url_path_join(self.base_url, "static")
463 url_path_join(self.base_url, "static")
461 )
464 )
462
465
463 # try local mathjax, either in nbextensions/mathjax or static/mathjax
466 # try local mathjax, either in nbextensions/mathjax or static/mathjax
464 for (url_prefix, search_path) in [
467 for (url_prefix, search_path) in [
465 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
468 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
466 (static_url_prefix, self.static_file_path),
469 (static_url_prefix, self.static_file_path),
467 ]:
470 ]:
468 self.log.debug("searching for local mathjax in %s", search_path)
471 self.log.debug("searching for local mathjax in %s", search_path)
469 try:
472 try:
470 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
473 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
471 except IOError:
474 except IOError:
472 continue
475 continue
473 else:
476 else:
474 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
477 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
475 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
478 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
476 return url
479 return url
477
480
478 # no local mathjax, serve from CDN
481 # no local mathjax, serve from CDN
479 if self.certfile:
482 if self.certfile:
480 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
483 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
481 host = u"https://c328740.ssl.cf1.rackcdn.com"
484 host = u"https://c328740.ssl.cf1.rackcdn.com"
482 else:
485 else:
483 host = u"http://cdn.mathjax.org"
486 host = u"http://cdn.mathjax.org"
484
487
485 url = host + u"/mathjax/latest/MathJax.js"
488 url = host + u"/mathjax/latest/MathJax.js"
486 self.log.info("Using MathJax from CDN: %s", url)
489 self.log.info("Using MathJax from CDN: %s", url)
487 return url
490 return url
488
491
489 def _mathjax_url_changed(self, name, old, new):
492 def _mathjax_url_changed(self, name, old, new):
490 if new and not self.enable_mathjax:
493 if new and not self.enable_mathjax:
491 # enable_mathjax=False overrides mathjax_url
494 # enable_mathjax=False overrides mathjax_url
492 self.mathjax_url = u''
495 self.mathjax_url = u''
493 else:
496 else:
494 self.log.info("Using MathJax: %s", new)
497 self.log.info("Using MathJax: %s", new)
495
498
496 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
499 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
497 config=True,
500 config=True,
498 help='The notebook manager class to use.')
501 help='The notebook manager class to use.')
499
502
500 trust_xheaders = Bool(False, config=True,
503 trust_xheaders = Bool(False, config=True,
501 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
504 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
502 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
505 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
503 )
506 )
504
507
505 info_file = Unicode()
508 info_file = Unicode()
506
509
507 def _info_file_default(self):
510 def _info_file_default(self):
508 info_file = "nbserver-%s.json"%os.getpid()
511 info_file = "nbserver-%s.json"%os.getpid()
509 return os.path.join(self.profile_dir.security_dir, info_file)
512 return os.path.join(self.profile_dir.security_dir, info_file)
513
514 notebook_dir = Unicode(py3compat.getcwd(), config=True,
515 help="The directory to use for notebooks and kernels."
516 )
517
518 def _notebook_dir_changed(self, name, old, new):
519 """Do a bit of validation of the notebook dir."""
520 if not os.path.isabs(new):
521 # If we receive a non-absolute path, make it absolute.
522 self.notebook_dir = os.path.abspath(new)
523 return
524 if not os.path.isdir(new):
525 raise TraitError("No such notebook dir: %r" % new)
526
527 # setting App.notebook_dir implies setting notebook and kernel dirs as well
528 self.config.FileNotebookManager.notebook_dir = new
529 self.config.MappingKernelManager.root_dir = new
530
510
531
511 def parse_command_line(self, argv=None):
532 def parse_command_line(self, argv=None):
512 super(NotebookApp, self).parse_command_line(argv)
533 super(NotebookApp, self).parse_command_line(argv)
513
534
514 if self.extra_args:
535 if self.extra_args:
515 arg0 = self.extra_args[0]
536 arg0 = self.extra_args[0]
516 f = os.path.abspath(arg0)
537 f = os.path.abspath(arg0)
517 self.argv.remove(arg0)
538 self.argv.remove(arg0)
518 if not os.path.exists(f):
539 if not os.path.exists(f):
519 self.log.critical("No such file or directory: %s", f)
540 self.log.critical("No such file or directory: %s", f)
520 self.exit(1)
541 self.exit(1)
521 if os.path.isdir(f):
542 if os.path.isdir(f):
522 self.config.FileNotebookManager.notebook_dir = f
543 self.notebook_dir = f
523 elif os.path.isfile(f):
544 elif os.path.isfile(f):
524 self.file_to_run = f
545 self.file_to_run = f
525
546
526 def init_kernel_argv(self):
547 def init_kernel_argv(self):
527 """construct the kernel arguments"""
548 """construct the kernel arguments"""
528 # Scrub frontend-specific flags
549 # Scrub frontend-specific flags
529 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
550 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
530 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
551 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
531 self.log.warn('\n '.join([
552 self.log.warn('\n '.join([
532 "Starting all kernels in pylab mode is not recommended,",
553 "Starting all kernels in pylab mode is not recommended,",
533 "and will be disabled in a future release.",
554 "and will be disabled in a future release.",
534 "Please use the %matplotlib magic to enable matplotlib instead.",
555 "Please use the %matplotlib magic to enable matplotlib instead.",
535 "pylab implies many imports, which can have confusing side effects",
556 "pylab implies many imports, which can have confusing side effects",
536 "and harm the reproducibility of your notebooks.",
557 "and harm the reproducibility of your notebooks.",
537 ]))
558 ]))
538 # Kernel should inherit default config file from frontend
559 # Kernel should inherit default config file from frontend
539 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
560 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
540 # Kernel should get *absolute* path to profile directory
561 # Kernel should get *absolute* path to profile directory
541 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
562 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
542
563
543 def init_configurables(self):
564 def init_configurables(self):
544 # force Session default to be secure
565 # force Session default to be secure
545 default_secure(self.config)
566 default_secure(self.config)
546 self.kernel_manager = MappingKernelManager(
567 self.kernel_manager = MappingKernelManager(
547 parent=self, log=self.log, kernel_argv=self.kernel_argv,
568 parent=self, log=self.log, kernel_argv=self.kernel_argv,
548 connection_dir = self.profile_dir.security_dir,
569 connection_dir = self.profile_dir.security_dir,
549 )
570 )
550 kls = import_item(self.notebook_manager_class)
571 kls = import_item(self.notebook_manager_class)
551 self.notebook_manager = kls(parent=self, log=self.log)
572 self.notebook_manager = kls(parent=self, log=self.log)
552 self.session_manager = SessionManager(parent=self, log=self.log)
573 self.session_manager = SessionManager(parent=self, log=self.log)
553 self.cluster_manager = ClusterManager(parent=self, log=self.log)
574 self.cluster_manager = ClusterManager(parent=self, log=self.log)
554 self.cluster_manager.update_profiles()
575 self.cluster_manager.update_profiles()
555
576
556 def init_logging(self):
577 def init_logging(self):
557 # This prevents double log messages because tornado use a root logger that
578 # This prevents double log messages because tornado use a root logger that
558 # self.log is a child of. The logging module dipatches log messages to a log
579 # self.log is a child of. The logging module dipatches log messages to a log
559 # and all of its ancenstors until propagate is set to False.
580 # and all of its ancenstors until propagate is set to False.
560 self.log.propagate = False
581 self.log.propagate = False
561
582
562 # hook up tornado 3's loggers to our app handlers
583 # hook up tornado 3's loggers to our app handlers
563 for name in ('access', 'application', 'general'):
584 for name in ('access', 'application', 'general'):
564 logger = logging.getLogger('tornado.%s' % name)
585 logger = logging.getLogger('tornado.%s' % name)
565 logger.parent = self.log
586 logger.parent = self.log
566 logger.setLevel(self.log.level)
587 logger.setLevel(self.log.level)
567
588
568 def init_webapp(self):
589 def init_webapp(self):
569 """initialize tornado webapp and httpserver"""
590 """initialize tornado webapp and httpserver"""
570 self.web_app = NotebookWebApplication(
591 self.web_app = NotebookWebApplication(
571 self, self.kernel_manager, self.notebook_manager,
592 self, self.kernel_manager, self.notebook_manager,
572 self.cluster_manager, self.session_manager,
593 self.cluster_manager, self.session_manager,
573 self.log, self.base_url, self.webapp_settings
594 self.log, self.base_url, self.webapp_settings
574 )
595 )
575 if self.certfile:
596 if self.certfile:
576 ssl_options = dict(certfile=self.certfile)
597 ssl_options = dict(certfile=self.certfile)
577 if self.keyfile:
598 if self.keyfile:
578 ssl_options['keyfile'] = self.keyfile
599 ssl_options['keyfile'] = self.keyfile
579 else:
600 else:
580 ssl_options = None
601 ssl_options = None
581 self.web_app.password = self.password
602 self.web_app.password = self.password
582 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
603 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
583 xheaders=self.trust_xheaders)
604 xheaders=self.trust_xheaders)
584 if not self.ip:
605 if not self.ip:
585 warning = "WARNING: The notebook server is listening on all IP addresses"
606 warning = "WARNING: The notebook server is listening on all IP addresses"
586 if ssl_options is None:
607 if ssl_options is None:
587 self.log.critical(warning + " and not using encryption. This "
608 self.log.critical(warning + " and not using encryption. This "
588 "is not recommended.")
609 "is not recommended.")
589 if not self.password:
610 if not self.password:
590 self.log.critical(warning + " and not using authentication. "
611 self.log.critical(warning + " and not using authentication. "
591 "This is highly insecure and not recommended.")
612 "This is highly insecure and not recommended.")
592 success = None
613 success = None
593 for port in random_ports(self.port, self.port_retries+1):
614 for port in random_ports(self.port, self.port_retries+1):
594 try:
615 try:
595 self.http_server.listen(port, self.ip)
616 self.http_server.listen(port, self.ip)
596 except socket.error as e:
617 except socket.error as e:
597 if e.errno == errno.EADDRINUSE:
618 if e.errno == errno.EADDRINUSE:
598 self.log.info('The port %i is already in use, trying another random port.' % port)
619 self.log.info('The port %i is already in use, trying another random port.' % port)
599 continue
620 continue
600 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
621 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
601 self.log.warn("Permission to listen on port %i denied" % port)
622 self.log.warn("Permission to listen on port %i denied" % port)
602 continue
623 continue
603 else:
624 else:
604 raise
625 raise
605 else:
626 else:
606 self.port = port
627 self.port = port
607 success = True
628 success = True
608 break
629 break
609 if not success:
630 if not success:
610 self.log.critical('ERROR: the notebook server could not be started because '
631 self.log.critical('ERROR: the notebook server could not be started because '
611 'no available port could be found.')
632 'no available port could be found.')
612 self.exit(1)
633 self.exit(1)
613
634
614 @property
635 @property
615 def display_url(self):
636 def display_url(self):
616 ip = self.ip if self.ip else '[all ip addresses on your system]'
637 ip = self.ip if self.ip else '[all ip addresses on your system]'
617 return self._url(ip)
638 return self._url(ip)
618
639
619 @property
640 @property
620 def connection_url(self):
641 def connection_url(self):
621 ip = self.ip if self.ip else localhost()
642 ip = self.ip if self.ip else localhost()
622 return self._url(ip)
643 return self._url(ip)
623
644
624 def _url(self, ip):
645 def _url(self, ip):
625 proto = 'https' if self.certfile else 'http'
646 proto = 'https' if self.certfile else 'http'
626 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
647 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
627
648
628 def init_signal(self):
649 def init_signal(self):
629 if not sys.platform.startswith('win'):
650 if not sys.platform.startswith('win'):
630 signal.signal(signal.SIGINT, self._handle_sigint)
651 signal.signal(signal.SIGINT, self._handle_sigint)
631 signal.signal(signal.SIGTERM, self._signal_stop)
652 signal.signal(signal.SIGTERM, self._signal_stop)
632 if hasattr(signal, 'SIGUSR1'):
653 if hasattr(signal, 'SIGUSR1'):
633 # Windows doesn't support SIGUSR1
654 # Windows doesn't support SIGUSR1
634 signal.signal(signal.SIGUSR1, self._signal_info)
655 signal.signal(signal.SIGUSR1, self._signal_info)
635 if hasattr(signal, 'SIGINFO'):
656 if hasattr(signal, 'SIGINFO'):
636 # only on BSD-based systems
657 # only on BSD-based systems
637 signal.signal(signal.SIGINFO, self._signal_info)
658 signal.signal(signal.SIGINFO, self._signal_info)
638
659
639 def _handle_sigint(self, sig, frame):
660 def _handle_sigint(self, sig, frame):
640 """SIGINT handler spawns confirmation dialog"""
661 """SIGINT handler spawns confirmation dialog"""
641 # register more forceful signal handler for ^C^C case
662 # register more forceful signal handler for ^C^C case
642 signal.signal(signal.SIGINT, self._signal_stop)
663 signal.signal(signal.SIGINT, self._signal_stop)
643 # request confirmation dialog in bg thread, to avoid
664 # request confirmation dialog in bg thread, to avoid
644 # blocking the App
665 # blocking the App
645 thread = threading.Thread(target=self._confirm_exit)
666 thread = threading.Thread(target=self._confirm_exit)
646 thread.daemon = True
667 thread.daemon = True
647 thread.start()
668 thread.start()
648
669
649 def _restore_sigint_handler(self):
670 def _restore_sigint_handler(self):
650 """callback for restoring original SIGINT handler"""
671 """callback for restoring original SIGINT handler"""
651 signal.signal(signal.SIGINT, self._handle_sigint)
672 signal.signal(signal.SIGINT, self._handle_sigint)
652
673
653 def _confirm_exit(self):
674 def _confirm_exit(self):
654 """confirm shutdown on ^C
675 """confirm shutdown on ^C
655
676
656 A second ^C, or answering 'y' within 5s will cause shutdown,
677 A second ^C, or answering 'y' within 5s will cause shutdown,
657 otherwise original SIGINT handler will be restored.
678 otherwise original SIGINT handler will be restored.
658
679
659 This doesn't work on Windows.
680 This doesn't work on Windows.
660 """
681 """
661 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
682 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
662 time.sleep(0.1)
683 time.sleep(0.1)
663 info = self.log.info
684 info = self.log.info
664 info('interrupted')
685 info('interrupted')
665 print(self.notebook_info())
686 print(self.notebook_info())
666 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
687 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
667 sys.stdout.flush()
688 sys.stdout.flush()
668 r,w,x = select.select([sys.stdin], [], [], 5)
689 r,w,x = select.select([sys.stdin], [], [], 5)
669 if r:
690 if r:
670 line = sys.stdin.readline()
691 line = sys.stdin.readline()
671 if line.lower().startswith('y'):
692 if line.lower().startswith('y'):
672 self.log.critical("Shutdown confirmed")
693 self.log.critical("Shutdown confirmed")
673 ioloop.IOLoop.instance().stop()
694 ioloop.IOLoop.instance().stop()
674 return
695 return
675 else:
696 else:
676 print("No answer for 5s:", end=' ')
697 print("No answer for 5s:", end=' ')
677 print("resuming operation...")
698 print("resuming operation...")
678 # no answer, or answer is no:
699 # no answer, or answer is no:
679 # set it back to original SIGINT handler
700 # set it back to original SIGINT handler
680 # use IOLoop.add_callback because signal.signal must be called
701 # use IOLoop.add_callback because signal.signal must be called
681 # from main thread
702 # from main thread
682 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
703 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
683
704
684 def _signal_stop(self, sig, frame):
705 def _signal_stop(self, sig, frame):
685 self.log.critical("received signal %s, stopping", sig)
706 self.log.critical("received signal %s, stopping", sig)
686 ioloop.IOLoop.instance().stop()
707 ioloop.IOLoop.instance().stop()
687
708
688 def _signal_info(self, sig, frame):
709 def _signal_info(self, sig, frame):
689 print(self.notebook_info())
710 print(self.notebook_info())
690
711
691 def init_components(self):
712 def init_components(self):
692 """Check the components submodule, and warn if it's unclean"""
713 """Check the components submodule, and warn if it's unclean"""
693 status = submodule.check_submodule_status()
714 status = submodule.check_submodule_status()
694 if status == 'missing':
715 if status == 'missing':
695 self.log.warn("components submodule missing, running `git submodule update`")
716 self.log.warn("components submodule missing, running `git submodule update`")
696 submodule.update_submodules(submodule.ipython_parent())
717 submodule.update_submodules(submodule.ipython_parent())
697 elif status == 'unclean':
718 elif status == 'unclean':
698 self.log.warn("components submodule unclean, you may see 404s on static/components")
719 self.log.warn("components submodule unclean, you may see 404s on static/components")
699 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
720 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
700
721
701 @catch_config_error
722 @catch_config_error
702 def initialize(self, argv=None):
723 def initialize(self, argv=None):
703 super(NotebookApp, self).initialize(argv)
724 super(NotebookApp, self).initialize(argv)
704 self.init_logging()
725 self.init_logging()
705 self.init_kernel_argv()
726 self.init_kernel_argv()
706 self.init_configurables()
727 self.init_configurables()
707 self.init_components()
728 self.init_components()
708 self.init_webapp()
729 self.init_webapp()
709 self.init_signal()
730 self.init_signal()
710
731
711 def cleanup_kernels(self):
732 def cleanup_kernels(self):
712 """Shutdown all kernels.
733 """Shutdown all kernels.
713
734
714 The kernels will shutdown themselves when this process no longer exists,
735 The kernels will shutdown themselves when this process no longer exists,
715 but explicit shutdown allows the KernelManagers to cleanup the connection files.
736 but explicit shutdown allows the KernelManagers to cleanup the connection files.
716 """
737 """
717 self.log.info('Shutting down kernels')
738 self.log.info('Shutting down kernels')
718 self.kernel_manager.shutdown_all()
739 self.kernel_manager.shutdown_all()
719
740
720 def notebook_info(self):
741 def notebook_info(self):
721 "Return the current working directory and the server url information"
742 "Return the current working directory and the server url information"
722 info = self.notebook_manager.info_string() + "\n"
743 info = self.notebook_manager.info_string() + "\n"
723 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
744 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
724 return info + "The IPython Notebook is running at: %s" % self.display_url
745 return info + "The IPython Notebook is running at: %s" % self.display_url
725
746
726 def server_info(self):
747 def server_info(self):
727 """Return a JSONable dict of information about this server."""
748 """Return a JSONable dict of information about this server."""
728 return {'url': self.connection_url,
749 return {'url': self.connection_url,
729 'hostname': self.ip if self.ip else 'localhost',
750 'hostname': self.ip if self.ip else 'localhost',
730 'port': self.port,
751 'port': self.port,
731 'secure': bool(self.certfile),
752 'secure': bool(self.certfile),
732 'base_url': self.base_url,
753 'base_url': self.base_url,
733 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
754 'notebook_dir': os.path.abspath(self.notebook_dir),
734 }
755 }
735
756
736 def write_server_info_file(self):
757 def write_server_info_file(self):
737 """Write the result of server_info() to the JSON file info_file."""
758 """Write the result of server_info() to the JSON file info_file."""
738 with open(self.info_file, 'w') as f:
759 with open(self.info_file, 'w') as f:
739 json.dump(self.server_info(), f, indent=2)
760 json.dump(self.server_info(), f, indent=2)
740
761
741 def remove_server_info_file(self):
762 def remove_server_info_file(self):
742 """Remove the nbserver-<pid>.json file created for this server.
763 """Remove the nbserver-<pid>.json file created for this server.
743
764
744 Ignores the error raised when the file has already been removed.
765 Ignores the error raised when the file has already been removed.
745 """
766 """
746 try:
767 try:
747 os.unlink(self.info_file)
768 os.unlink(self.info_file)
748 except OSError as e:
769 except OSError as e:
749 if e.errno != errno.ENOENT:
770 if e.errno != errno.ENOENT:
750 raise
771 raise
751
772
752 def start(self):
773 def start(self):
753 """ Start the IPython Notebook server app, after initialization
774 """ Start the IPython Notebook server app, after initialization
754
775
755 This method takes no arguments so all configuration and initialization
776 This method takes no arguments so all configuration and initialization
756 must be done prior to calling this method."""
777 must be done prior to calling this method."""
757 if self.subapp is not None:
778 if self.subapp is not None:
758 return self.subapp.start()
779 return self.subapp.start()
759
780
760 info = self.log.info
781 info = self.log.info
761 for line in self.notebook_info().split("\n"):
782 for line in self.notebook_info().split("\n"):
762 info(line)
783 info(line)
763 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
784 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
764
785
765 self.write_server_info_file()
786 self.write_server_info_file()
766
787
767 if self.open_browser or self.file_to_run:
788 if self.open_browser or self.file_to_run:
768 try:
789 try:
769 browser = webbrowser.get(self.browser or None)
790 browser = webbrowser.get(self.browser or None)
770 except webbrowser.Error as e:
791 except webbrowser.Error as e:
771 self.log.warn('No web browser found: %s.' % e)
792 self.log.warn('No web browser found: %s.' % e)
772 browser = None
793 browser = None
773
794
774 f = self.file_to_run
795 f = self.file_to_run
775 if f:
796 if f:
776 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
797 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
777 if f.startswith(nbdir):
798 if f.startswith(nbdir):
778 f = f[len(nbdir):]
799 f = f[len(nbdir):]
779 else:
800 else:
780 self.log.warn(
801 self.log.warn(
781 "Probably won't be able to open notebook %s "
802 "Probably won't be able to open notebook %s "
782 "because it is not in notebook_dir %s",
803 "because it is not in notebook_dir %s",
783 f, nbdir,
804 f, nbdir,
784 )
805 )
785
806
786 if os.path.isfile(self.file_to_run):
807 if os.path.isfile(self.file_to_run):
787 url = url_path_join('notebooks', f)
808 url = url_path_join('notebooks', f)
788 else:
809 else:
789 url = url_path_join('tree', f)
810 url = url_path_join('tree', f)
790 if browser:
811 if browser:
791 b = lambda : browser.open("%s%s" % (self.connection_url, url),
812 b = lambda : browser.open("%s%s" % (self.connection_url, url),
792 new=2)
813 new=2)
793 threading.Thread(target=b).start()
814 threading.Thread(target=b).start()
794 try:
815 try:
795 ioloop.IOLoop.instance().start()
816 ioloop.IOLoop.instance().start()
796 except KeyboardInterrupt:
817 except KeyboardInterrupt:
797 info("Interrupted...")
818 info("Interrupted...")
798 finally:
819 finally:
799 self.cleanup_kernels()
820 self.cleanup_kernels()
800 self.remove_server_info_file()
821 self.remove_server_info_file()
801
822
802
823
803 def list_running_servers(profile='default'):
824 def list_running_servers(profile='default'):
804 """Iterate over the server info files of running notebook servers.
825 """Iterate over the server info files of running notebook servers.
805
826
806 Given a profile name, find nbserver-* files in the security directory of
827 Given a profile name, find nbserver-* files in the security directory of
807 that profile, and yield dicts of their information, each one pertaining to
828 that profile, and yield dicts of their information, each one pertaining to
808 a currently running notebook server instance.
829 a currently running notebook server instance.
809 """
830 """
810 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
831 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
811 for file in os.listdir(pd.security_dir):
832 for file in os.listdir(pd.security_dir):
812 if file.startswith('nbserver-'):
833 if file.startswith('nbserver-'):
813 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
834 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
814 yield json.load(f)
835 yield json.load(f)
815
836
816 #-----------------------------------------------------------------------------
837 #-----------------------------------------------------------------------------
817 # Main entry point
838 # Main entry point
818 #-----------------------------------------------------------------------------
839 #-----------------------------------------------------------------------------
819
840
820 launch_new_instance = NotebookApp.launch_instance
841 launch_new_instance = NotebookApp.launch_instance
821
842
@@ -1,98 +1,128
1 """A kernel manager relating notebooks and kernels
1 """A kernel manager relating notebooks and kernels
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
20
19 from tornado import web
21 from tornado import web
20
22
21 from IPython.kernel.multikernelmanager import MultiKernelManager
23 from IPython.kernel.multikernelmanager import MultiKernelManager
22 from IPython.utils.traitlets import (
24 from IPython.utils.traitlets import (
23 Dict, List, Unicode,
25 Dict, List, Unicode,
24 )
26 )
25
27
28 from IPython.html.utils import to_os_path
29 from IPython.utils.py3compat import getcwd
30
26 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
27 # Classes
32 # Classes
28 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
29
34
30
35
31 class MappingKernelManager(MultiKernelManager):
36 class MappingKernelManager(MultiKernelManager):
32 """A KernelManager that handles notebook mapping and HTTP error handling"""
37 """A KernelManager that handles notebook mapping and HTTP error handling"""
33
38
34 def _kernel_manager_class_default(self):
39 def _kernel_manager_class_default(self):
35 return "IPython.kernel.ioloop.IOLoopKernelManager"
40 return "IPython.kernel.ioloop.IOLoopKernelManager"
36
41
37 kernel_argv = List(Unicode)
42 kernel_argv = List(Unicode)
43
44 root_dir = Unicode(getcwd(), config=True)
45
46 def _root_dir_changed(self, name, old, new):
47 """Do a bit of validation of the root dir."""
48 if not os.path.isabs(new):
49 # If we receive a non-absolute path, make it absolute.
50 self.root_dir = os.path.abspath(new)
51 return
52 if not os.path.exists(new) or not os.path.isdir(new):
53 raise TraitError("kernel root dir %r is not a directory" % new)
38
54
39 #-------------------------------------------------------------------------
55 #-------------------------------------------------------------------------
40 # Methods for managing kernels and sessions
56 # Methods for managing kernels and sessions
41 #-------------------------------------------------------------------------
57 #-------------------------------------------------------------------------
42
58
43 def _handle_kernel_died(self, kernel_id):
59 def _handle_kernel_died(self, kernel_id):
44 """notice that a kernel died"""
60 """notice that a kernel died"""
45 self.log.warn("Kernel %s died, removing from map.", kernel_id)
61 self.log.warn("Kernel %s died, removing from map.", kernel_id)
46 self.remove_kernel(kernel_id)
62 self.remove_kernel(kernel_id)
47
63
48 def start_kernel(self, kernel_id=None, **kwargs):
64 def cwd_for_path(self, path):
65 """Turn API path into absolute OS path."""
66 os_path = to_os_path(path, self.root_dir)
67 # in the case of notebooks and kernels not being on the same filesystem,
68 # walk up to root_dir if the paths don't exist
69 while not os.path.exists(os_path) and os_path != self.root_dir:
70 os_path = os.path.dirname(os_path)
71 return os_path
72
73 def start_kernel(self, kernel_id=None, path=None, **kwargs):
49 """Start a kernel for a session an return its kernel_id.
74 """Start a kernel for a session an return its kernel_id.
50
75
51 Parameters
76 Parameters
52 ----------
77 ----------
53 kernel_id : uuid
78 kernel_id : uuid
54 The uuid to associate the new kernel with. If this
79 The uuid to associate the new kernel with. If this
55 is not None, this kernel will be persistent whenever it is
80 is not None, this kernel will be persistent whenever it is
56 requested.
81 requested.
82 path : API path
83 The API path (unicode, '/' delimited) for the cwd.
84 Will be transformed to an OS path relative to root_dir.
57 """
85 """
58 if kernel_id is None:
86 if kernel_id is None:
59 kwargs['extra_arguments'] = self.kernel_argv
87 kwargs['extra_arguments'] = self.kernel_argv
88 if path is not None:
89 kwargs['cwd'] = self.cwd_for_path(path)
60 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
90 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
61 self.log.info("Kernel started: %s" % kernel_id)
91 self.log.info("Kernel started: %s" % kernel_id)
62 self.log.debug("Kernel args: %r" % kwargs)
92 self.log.debug("Kernel args: %r" % kwargs)
63 # register callback for failed auto-restart
93 # register callback for failed auto-restart
64 self.add_restart_callback(kernel_id,
94 self.add_restart_callback(kernel_id,
65 lambda : self._handle_kernel_died(kernel_id),
95 lambda : self._handle_kernel_died(kernel_id),
66 'dead',
96 'dead',
67 )
97 )
68 else:
98 else:
69 self._check_kernel_id(kernel_id)
99 self._check_kernel_id(kernel_id)
70 self.log.info("Using existing kernel: %s" % kernel_id)
100 self.log.info("Using existing kernel: %s" % kernel_id)
71 return kernel_id
101 return kernel_id
72
102
73 def shutdown_kernel(self, kernel_id, now=False):
103 def shutdown_kernel(self, kernel_id, now=False):
74 """Shutdown a kernel by kernel_id"""
104 """Shutdown a kernel by kernel_id"""
75 self._check_kernel_id(kernel_id)
105 self._check_kernel_id(kernel_id)
76 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
106 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
77
107
78 def kernel_model(self, kernel_id):
108 def kernel_model(self, kernel_id):
79 """Return a dictionary of kernel information described in the
109 """Return a dictionary of kernel information described in the
80 JSON standard model."""
110 JSON standard model."""
81 self._check_kernel_id(kernel_id)
111 self._check_kernel_id(kernel_id)
82 model = {"id":kernel_id}
112 model = {"id":kernel_id}
83 return model
113 return model
84
114
85 def list_kernels(self):
115 def list_kernels(self):
86 """Returns a list of kernel_id's of kernels running."""
116 """Returns a list of kernel_id's of kernels running."""
87 kernels = []
117 kernels = []
88 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
118 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
89 for kernel_id in kernel_ids:
119 for kernel_id in kernel_ids:
90 model = self.kernel_model(kernel_id)
120 model = self.kernel_model(kernel_id)
91 kernels.append(model)
121 kernels.append(model)
92 return kernels
122 return kernels
93
123
94 # override _check_kernel_id to raise 404 instead of KeyError
124 # override _check_kernel_id to raise 404 instead of KeyError
95 def _check_kernel_id(self, kernel_id):
125 def _check_kernel_id(self, kernel_id):
96 """Check a that a kernel_id exists and raise 404 if not."""
126 """Check a that a kernel_id exists and raise 404 if not."""
97 if kernel_id not in self:
127 if kernel_id not in self:
98 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
128 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,467 +1,474
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import io
20 import io
21 import itertools
22 import os
21 import os
23 import glob
22 import glob
24 import shutil
23 import shutil
25
24
26 from tornado import web
25 from tornado import web
27
26
28 from .nbmanager import NotebookManager
27 from .nbmanager import NotebookManager
29 from IPython.nbformat import current
28 from IPython.nbformat import current
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
29 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 from IPython.utils.py3compat import getcwd
31 from IPython.utils import tz
31 from IPython.utils import tz
32 from IPython.html.utils import is_hidden
32 from IPython.html.utils import is_hidden, to_os_path
33
33
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Classes
35 # Classes
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class FileNotebookManager(NotebookManager):
38 class FileNotebookManager(NotebookManager):
39
39
40 save_script = Bool(False, config=True,
40 save_script = Bool(False, config=True,
41 help="""Automatically create a Python script when saving the notebook.
41 help="""Automatically create a Python script when saving the notebook.
42
42
43 For easier use of import, %run and %load across notebooks, a
43 For easier use of import, %run and %load across notebooks, a
44 <notebook-name>.py script will be created next to any
44 <notebook-name>.py script will be created next to any
45 <notebook-name>.ipynb on each save. This can also be set with the
45 <notebook-name>.ipynb on each save. This can also be set with the
46 short `--script` flag.
46 short `--script` flag.
47 """
47 """
48 )
48 )
49 notebook_dir = Unicode(getcwd(), config=True)
49
50
51 def _notebook_dir_changed(self, name, old, new):
52 """Do a bit of validation of the notebook dir."""
53 if not os.path.isabs(new):
54 # If we receive a non-absolute path, make it absolute.
55 self.notebook_dir = os.path.abspath(new)
56 return
57 if not os.path.exists(new) or not os.path.isdir(new):
58 raise TraitError("notebook dir %r is not a directory" % new)
59
50 checkpoint_dir = Unicode(config=True,
60 checkpoint_dir = Unicode(config=True,
51 help="""The location in which to keep notebook checkpoints
61 help="""The location in which to keep notebook checkpoints
52
62
53 By default, it is notebook-dir/.ipynb_checkpoints
63 By default, it is notebook-dir/.ipynb_checkpoints
54 """
64 """
55 )
65 )
56 def _checkpoint_dir_default(self):
66 def _checkpoint_dir_default(self):
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
67 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58
68
59 def _checkpoint_dir_changed(self, name, old, new):
69 def _checkpoint_dir_changed(self, name, old, new):
60 """do a bit of validation of the checkpoint dir"""
70 """do a bit of validation of the checkpoint dir"""
61 if not os.path.isabs(new):
71 if not os.path.isabs(new):
62 # If we receive a non-absolute path, make it absolute.
72 # If we receive a non-absolute path, make it absolute.
63 abs_new = os.path.abspath(new)
73 abs_new = os.path.abspath(new)
64 self.checkpoint_dir = abs_new
74 self.checkpoint_dir = abs_new
65 return
75 return
66 if os.path.exists(new) and not os.path.isdir(new):
76 if os.path.exists(new) and not os.path.isdir(new):
67 raise TraitError("checkpoint dir %r is not a directory" % new)
77 raise TraitError("checkpoint dir %r is not a directory" % new)
68 if not os.path.exists(new):
78 if not os.path.exists(new):
69 self.log.info("Creating checkpoint dir %s", new)
79 self.log.info("Creating checkpoint dir %s", new)
70 try:
80 try:
71 os.mkdir(new)
81 os.mkdir(new)
72 except:
82 except:
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
83 raise TraitError("Couldn't create checkpoint dir %r" % new)
74
84
75 def get_notebook_names(self, path=''):
85 def get_notebook_names(self, path=''):
76 """List all notebook names in the notebook dir and path."""
86 """List all notebook names in the notebook dir and path."""
77 path = path.strip('/')
87 path = path.strip('/')
78 if not os.path.isdir(self.get_os_path(path=path)):
88 if not os.path.isdir(self._get_os_path(path=path)):
79 raise web.HTTPError(404, 'Directory not found: ' + path)
89 raise web.HTTPError(404, 'Directory not found: ' + path)
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
90 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
81 names = [os.path.basename(name)
91 names = [os.path.basename(name)
82 for name in names]
92 for name in names]
83 return names
93 return names
84
94
85 def path_exists(self, path):
95 def path_exists(self, path):
86 """Does the API-style path (directory) actually exist?
96 """Does the API-style path (directory) actually exist?
87
97
88 Parameters
98 Parameters
89 ----------
99 ----------
90 path : string
100 path : string
91 The path to check. This is an API path (`/` separated,
101 The path to check. This is an API path (`/` separated,
92 relative to base notebook-dir).
102 relative to base notebook-dir).
93
103
94 Returns
104 Returns
95 -------
105 -------
96 exists : bool
106 exists : bool
97 Whether the path is indeed a directory.
107 Whether the path is indeed a directory.
98 """
108 """
99 path = path.strip('/')
109 path = path.strip('/')
100 os_path = self.get_os_path(path=path)
110 os_path = self._get_os_path(path=path)
101 return os.path.isdir(os_path)
111 return os.path.isdir(os_path)
102
112
103 def is_hidden(self, path):
113 def is_hidden(self, path):
104 """Does the API style path correspond to a hidden directory or file?
114 """Does the API style path correspond to a hidden directory or file?
105
115
106 Parameters
116 Parameters
107 ----------
117 ----------
108 path : string
118 path : string
109 The path to check. This is an API path (`/` separated,
119 The path to check. This is an API path (`/` separated,
110 relative to base notebook-dir).
120 relative to base notebook-dir).
111
121
112 Returns
122 Returns
113 -------
123 -------
114 exists : bool
124 exists : bool
115 Whether the path is hidden.
125 Whether the path is hidden.
116
126
117 """
127 """
118 path = path.strip('/')
128 path = path.strip('/')
119 os_path = self.get_os_path(path=path)
129 os_path = self._get_os_path(path=path)
120 return is_hidden(os_path, self.notebook_dir)
130 return is_hidden(os_path, self.notebook_dir)
121
131
122 def get_os_path(self, name=None, path=''):
132 def _get_os_path(self, name=None, path=''):
123 """Given a notebook name and a URL path, return its file system
133 """Given a notebook name and a URL path, return its file system
124 path.
134 path.
125
135
126 Parameters
136 Parameters
127 ----------
137 ----------
128 name : string
138 name : string
129 The name of a notebook file with the .ipynb extension
139 The name of a notebook file with the .ipynb extension
130 path : string
140 path : string
131 The relative URL path (with '/' as separator) to the named
141 The relative URL path (with '/' as separator) to the named
132 notebook.
142 notebook.
133
143
134 Returns
144 Returns
135 -------
145 -------
136 path : string
146 path : string
137 A file system path that combines notebook_dir (location where
147 A file system path that combines notebook_dir (location where
138 server started), the relative path, and the filename with the
148 server started), the relative path, and the filename with the
139 current operating system's url.
149 current operating system's url.
140 """
150 """
141 parts = path.strip('/').split('/')
142 parts = [p for p in parts if p != ''] # remove duplicate splits
143 if name is not None:
151 if name is not None:
144 parts.append(name)
152 path = path + '/' + name
145 path = os.path.join(self.notebook_dir, *parts)
153 return to_os_path(path, self.notebook_dir)
146 return path
147
154
148 def notebook_exists(self, name, path=''):
155 def notebook_exists(self, name, path=''):
149 """Returns a True if the notebook exists. Else, returns False.
156 """Returns a True if the notebook exists. Else, returns False.
150
157
151 Parameters
158 Parameters
152 ----------
159 ----------
153 name : string
160 name : string
154 The name of the notebook you are checking.
161 The name of the notebook you are checking.
155 path : string
162 path : string
156 The relative path to the notebook (with '/' as separator)
163 The relative path to the notebook (with '/' as separator)
157
164
158 Returns
165 Returns
159 -------
166 -------
160 bool
167 bool
161 """
168 """
162 path = path.strip('/')
169 path = path.strip('/')
163 nbpath = self.get_os_path(name, path=path)
170 nbpath = self._get_os_path(name, path=path)
164 return os.path.isfile(nbpath)
171 return os.path.isfile(nbpath)
165
172
166 # TODO: Remove this after we create the contents web service and directories are
173 # TODO: Remove this after we create the contents web service and directories are
167 # no longer listed by the notebook web service.
174 # no longer listed by the notebook web service.
168 def list_dirs(self, path):
175 def list_dirs(self, path):
169 """List the directories for a given API style path."""
176 """List the directories for a given API style path."""
170 path = path.strip('/')
177 path = path.strip('/')
171 os_path = self.get_os_path('', path)
178 os_path = self._get_os_path('', path)
172 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
179 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
173 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
180 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
174 dir_names = os.listdir(os_path)
181 dir_names = os.listdir(os_path)
175 dirs = []
182 dirs = []
176 for name in dir_names:
183 for name in dir_names:
177 os_path = self.get_os_path(name, path)
184 os_path = self._get_os_path(name, path)
178 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
185 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
179 try:
186 try:
180 model = self.get_dir_model(name, path)
187 model = self.get_dir_model(name, path)
181 except IOError:
188 except IOError:
182 pass
189 pass
183 dirs.append(model)
190 dirs.append(model)
184 dirs = sorted(dirs, key=lambda item: item['name'])
191 dirs = sorted(dirs, key=lambda item: item['name'])
185 return dirs
192 return dirs
186
193
187 # TODO: Remove this after we create the contents web service and directories are
194 # TODO: Remove this after we create the contents web service and directories are
188 # no longer listed by the notebook web service.
195 # no longer listed by the notebook web service.
189 def get_dir_model(self, name, path=''):
196 def get_dir_model(self, name, path=''):
190 """Get the directory model given a directory name and its API style path"""
197 """Get the directory model given a directory name and its API style path"""
191 path = path.strip('/')
198 path = path.strip('/')
192 os_path = self.get_os_path(name, path)
199 os_path = self._get_os_path(name, path)
193 if not os.path.isdir(os_path):
200 if not os.path.isdir(os_path):
194 raise IOError('directory does not exist: %r' % os_path)
201 raise IOError('directory does not exist: %r' % os_path)
195 info = os.stat(os_path)
202 info = os.stat(os_path)
196 last_modified = tz.utcfromtimestamp(info.st_mtime)
203 last_modified = tz.utcfromtimestamp(info.st_mtime)
197 created = tz.utcfromtimestamp(info.st_ctime)
204 created = tz.utcfromtimestamp(info.st_ctime)
198 # Create the notebook model.
205 # Create the notebook model.
199 model ={}
206 model ={}
200 model['name'] = name
207 model['name'] = name
201 model['path'] = path
208 model['path'] = path
202 model['last_modified'] = last_modified
209 model['last_modified'] = last_modified
203 model['created'] = created
210 model['created'] = created
204 model['type'] = 'directory'
211 model['type'] = 'directory'
205 return model
212 return model
206
213
207 def list_notebooks(self, path):
214 def list_notebooks(self, path):
208 """Returns a list of dictionaries that are the standard model
215 """Returns a list of dictionaries that are the standard model
209 for all notebooks in the relative 'path'.
216 for all notebooks in the relative 'path'.
210
217
211 Parameters
218 Parameters
212 ----------
219 ----------
213 path : str
220 path : str
214 the URL path that describes the relative path for the
221 the URL path that describes the relative path for the
215 listed notebooks
222 listed notebooks
216
223
217 Returns
224 Returns
218 -------
225 -------
219 notebooks : list of dicts
226 notebooks : list of dicts
220 a list of the notebook models without 'content'
227 a list of the notebook models without 'content'
221 """
228 """
222 path = path.strip('/')
229 path = path.strip('/')
223 notebook_names = self.get_notebook_names(path)
230 notebook_names = self.get_notebook_names(path)
224 notebooks = [self.get_notebook(name, path, content=False) for name in notebook_names]
231 notebooks = [self.get_notebook(name, path, content=False) for name in notebook_names]
225 notebooks = sorted(notebooks, key=lambda item: item['name'])
232 notebooks = sorted(notebooks, key=lambda item: item['name'])
226 return notebooks
233 return notebooks
227
234
228 def get_notebook(self, name, path='', content=True):
235 def get_notebook(self, name, path='', content=True):
229 """ Takes a path and name for a notebook and returns its model
236 """ Takes a path and name for a notebook and returns its model
230
237
231 Parameters
238 Parameters
232 ----------
239 ----------
233 name : str
240 name : str
234 the name of the notebook
241 the name of the notebook
235 path : str
242 path : str
236 the URL path that describes the relative path for
243 the URL path that describes the relative path for
237 the notebook
244 the notebook
238
245
239 Returns
246 Returns
240 -------
247 -------
241 model : dict
248 model : dict
242 the notebook model. If contents=True, returns the 'contents'
249 the notebook model. If contents=True, returns the 'contents'
243 dict in the model as well.
250 dict in the model as well.
244 """
251 """
245 path = path.strip('/')
252 path = path.strip('/')
246 if not self.notebook_exists(name=name, path=path):
253 if not self.notebook_exists(name=name, path=path):
247 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
254 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
248 os_path = self.get_os_path(name, path)
255 os_path = self._get_os_path(name, path)
249 info = os.stat(os_path)
256 info = os.stat(os_path)
250 last_modified = tz.utcfromtimestamp(info.st_mtime)
257 last_modified = tz.utcfromtimestamp(info.st_mtime)
251 created = tz.utcfromtimestamp(info.st_ctime)
258 created = tz.utcfromtimestamp(info.st_ctime)
252 # Create the notebook model.
259 # Create the notebook model.
253 model ={}
260 model ={}
254 model['name'] = name
261 model['name'] = name
255 model['path'] = path
262 model['path'] = path
256 model['last_modified'] = last_modified
263 model['last_modified'] = last_modified
257 model['created'] = created
264 model['created'] = created
258 model['type'] = 'notebook'
265 model['type'] = 'notebook'
259 if content:
266 if content:
260 with io.open(os_path, 'r', encoding='utf-8') as f:
267 with io.open(os_path, 'r', encoding='utf-8') as f:
261 try:
268 try:
262 nb = current.read(f, u'json')
269 nb = current.read(f, u'json')
263 except Exception as e:
270 except Exception as e:
264 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
271 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
265 self.mark_trusted_cells(nb, path, name)
272 self.mark_trusted_cells(nb, path, name)
266 model['content'] = nb
273 model['content'] = nb
267 return model
274 return model
268
275
269 def save_notebook(self, model, name='', path=''):
276 def save_notebook(self, model, name='', path=''):
270 """Save the notebook model and return the model with no content."""
277 """Save the notebook model and return the model with no content."""
271 path = path.strip('/')
278 path = path.strip('/')
272
279
273 if 'content' not in model:
280 if 'content' not in model:
274 raise web.HTTPError(400, u'No notebook JSON data provided')
281 raise web.HTTPError(400, u'No notebook JSON data provided')
275
282
276 # One checkpoint should always exist
283 # One checkpoint should always exist
277 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
284 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
278 self.create_checkpoint(name, path)
285 self.create_checkpoint(name, path)
279
286
280 new_path = model.get('path', path).strip('/')
287 new_path = model.get('path', path).strip('/')
281 new_name = model.get('name', name)
288 new_name = model.get('name', name)
282
289
283 if path != new_path or name != new_name:
290 if path != new_path or name != new_name:
284 self.rename_notebook(name, path, new_name, new_path)
291 self.rename_notebook(name, path, new_name, new_path)
285
292
286 # Save the notebook file
293 # Save the notebook file
287 os_path = self.get_os_path(new_name, new_path)
294 os_path = self._get_os_path(new_name, new_path)
288 nb = current.to_notebook_json(model['content'])
295 nb = current.to_notebook_json(model['content'])
289
296
290 self.check_and_sign(nb, new_path, new_name)
297 self.check_and_sign(nb, new_path, new_name)
291
298
292 if 'name' in nb['metadata']:
299 if 'name' in nb['metadata']:
293 nb['metadata']['name'] = u''
300 nb['metadata']['name'] = u''
294 try:
301 try:
295 self.log.debug("Autosaving notebook %s", os_path)
302 self.log.debug("Autosaving notebook %s", os_path)
296 with io.open(os_path, 'w', encoding='utf-8') as f:
303 with io.open(os_path, 'w', encoding='utf-8') as f:
297 current.write(nb, f, u'json')
304 current.write(nb, f, u'json')
298 except Exception as e:
305 except Exception as e:
299 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
306 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
300
307
301 # Save .py script as well
308 # Save .py script as well
302 if self.save_script:
309 if self.save_script:
303 py_path = os.path.splitext(os_path)[0] + '.py'
310 py_path = os.path.splitext(os_path)[0] + '.py'
304 self.log.debug("Writing script %s", py_path)
311 self.log.debug("Writing script %s", py_path)
305 try:
312 try:
306 with io.open(py_path, 'w', encoding='utf-8') as f:
313 with io.open(py_path, 'w', encoding='utf-8') as f:
307 current.write(nb, f, u'py')
314 current.write(nb, f, u'py')
308 except Exception as e:
315 except Exception as e:
309 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
316 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
310
317
311 model = self.get_notebook(new_name, new_path, content=False)
318 model = self.get_notebook(new_name, new_path, content=False)
312 return model
319 return model
313
320
314 def update_notebook(self, model, name, path=''):
321 def update_notebook(self, model, name, path=''):
315 """Update the notebook's path and/or name"""
322 """Update the notebook's path and/or name"""
316 path = path.strip('/')
323 path = path.strip('/')
317 new_name = model.get('name', name)
324 new_name = model.get('name', name)
318 new_path = model.get('path', path).strip('/')
325 new_path = model.get('path', path).strip('/')
319 if path != new_path or name != new_name:
326 if path != new_path or name != new_name:
320 self.rename_notebook(name, path, new_name, new_path)
327 self.rename_notebook(name, path, new_name, new_path)
321 model = self.get_notebook(new_name, new_path, content=False)
328 model = self.get_notebook(new_name, new_path, content=False)
322 return model
329 return model
323
330
324 def delete_notebook(self, name, path=''):
331 def delete_notebook(self, name, path=''):
325 """Delete notebook by name and path."""
332 """Delete notebook by name and path."""
326 path = path.strip('/')
333 path = path.strip('/')
327 os_path = self.get_os_path(name, path)
334 os_path = self._get_os_path(name, path)
328 if not os.path.isfile(os_path):
335 if not os.path.isfile(os_path):
329 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
336 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
330
337
331 # clear checkpoints
338 # clear checkpoints
332 for checkpoint in self.list_checkpoints(name, path):
339 for checkpoint in self.list_checkpoints(name, path):
333 checkpoint_id = checkpoint['id']
340 checkpoint_id = checkpoint['id']
334 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
341 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
335 if os.path.isfile(cp_path):
342 if os.path.isfile(cp_path):
336 self.log.debug("Unlinking checkpoint %s", cp_path)
343 self.log.debug("Unlinking checkpoint %s", cp_path)
337 os.unlink(cp_path)
344 os.unlink(cp_path)
338
345
339 self.log.debug("Unlinking notebook %s", os_path)
346 self.log.debug("Unlinking notebook %s", os_path)
340 os.unlink(os_path)
347 os.unlink(os_path)
341
348
342 def rename_notebook(self, old_name, old_path, new_name, new_path):
349 def rename_notebook(self, old_name, old_path, new_name, new_path):
343 """Rename a notebook."""
350 """Rename a notebook."""
344 old_path = old_path.strip('/')
351 old_path = old_path.strip('/')
345 new_path = new_path.strip('/')
352 new_path = new_path.strip('/')
346 if new_name == old_name and new_path == old_path:
353 if new_name == old_name and new_path == old_path:
347 return
354 return
348
355
349 new_os_path = self.get_os_path(new_name, new_path)
356 new_os_path = self._get_os_path(new_name, new_path)
350 old_os_path = self.get_os_path(old_name, old_path)
357 old_os_path = self._get_os_path(old_name, old_path)
351
358
352 # Should we proceed with the move?
359 # Should we proceed with the move?
353 if os.path.isfile(new_os_path):
360 if os.path.isfile(new_os_path):
354 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
361 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
355 if self.save_script:
362 if self.save_script:
356 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
363 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
357 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
364 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
358 if os.path.isfile(new_py_path):
365 if os.path.isfile(new_py_path):
359 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
366 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
360
367
361 # Move the notebook file
368 # Move the notebook file
362 try:
369 try:
363 os.rename(old_os_path, new_os_path)
370 os.rename(old_os_path, new_os_path)
364 except Exception as e:
371 except Exception as e:
365 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
372 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
366
373
367 # Move the checkpoints
374 # Move the checkpoints
368 old_checkpoints = self.list_checkpoints(old_name, old_path)
375 old_checkpoints = self.list_checkpoints(old_name, old_path)
369 for cp in old_checkpoints:
376 for cp in old_checkpoints:
370 checkpoint_id = cp['id']
377 checkpoint_id = cp['id']
371 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
378 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
372 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
379 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
373 if os.path.isfile(old_cp_path):
380 if os.path.isfile(old_cp_path):
374 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
381 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
375 os.rename(old_cp_path, new_cp_path)
382 os.rename(old_cp_path, new_cp_path)
376
383
377 # Move the .py script
384 # Move the .py script
378 if self.save_script:
385 if self.save_script:
379 os.rename(old_py_path, new_py_path)
386 os.rename(old_py_path, new_py_path)
380
387
381 # Checkpoint-related utilities
388 # Checkpoint-related utilities
382
389
383 def get_checkpoint_path(self, checkpoint_id, name, path=''):
390 def get_checkpoint_path(self, checkpoint_id, name, path=''):
384 """find the path to a checkpoint"""
391 """find the path to a checkpoint"""
385 path = path.strip('/')
392 path = path.strip('/')
386 basename, _ = os.path.splitext(name)
393 basename, _ = os.path.splitext(name)
387 filename = u"{name}-{checkpoint_id}{ext}".format(
394 filename = u"{name}-{checkpoint_id}{ext}".format(
388 name=basename,
395 name=basename,
389 checkpoint_id=checkpoint_id,
396 checkpoint_id=checkpoint_id,
390 ext=self.filename_ext,
397 ext=self.filename_ext,
391 )
398 )
392 cp_path = os.path.join(path, self.checkpoint_dir, filename)
399 cp_path = os.path.join(path, self.checkpoint_dir, filename)
393 return cp_path
400 return cp_path
394
401
395 def get_checkpoint_model(self, checkpoint_id, name, path=''):
402 def get_checkpoint_model(self, checkpoint_id, name, path=''):
396 """construct the info dict for a given checkpoint"""
403 """construct the info dict for a given checkpoint"""
397 path = path.strip('/')
404 path = path.strip('/')
398 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
399 stats = os.stat(cp_path)
406 stats = os.stat(cp_path)
400 last_modified = tz.utcfromtimestamp(stats.st_mtime)
407 last_modified = tz.utcfromtimestamp(stats.st_mtime)
401 info = dict(
408 info = dict(
402 id = checkpoint_id,
409 id = checkpoint_id,
403 last_modified = last_modified,
410 last_modified = last_modified,
404 )
411 )
405 return info
412 return info
406
413
407 # public checkpoint API
414 # public checkpoint API
408
415
409 def create_checkpoint(self, name, path=''):
416 def create_checkpoint(self, name, path=''):
410 """Create a checkpoint from the current state of a notebook"""
417 """Create a checkpoint from the current state of a notebook"""
411 path = path.strip('/')
418 path = path.strip('/')
412 nb_path = self.get_os_path(name, path)
419 nb_path = self._get_os_path(name, path)
413 # only the one checkpoint ID:
420 # only the one checkpoint ID:
414 checkpoint_id = u"checkpoint"
421 checkpoint_id = u"checkpoint"
415 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
422 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
416 self.log.debug("creating checkpoint for notebook %s", name)
423 self.log.debug("creating checkpoint for notebook %s", name)
417 if not os.path.exists(self.checkpoint_dir):
424 if not os.path.exists(self.checkpoint_dir):
418 os.mkdir(self.checkpoint_dir)
425 os.mkdir(self.checkpoint_dir)
419 shutil.copy2(nb_path, cp_path)
426 shutil.copy2(nb_path, cp_path)
420
427
421 # return the checkpoint info
428 # return the checkpoint info
422 return self.get_checkpoint_model(checkpoint_id, name, path)
429 return self.get_checkpoint_model(checkpoint_id, name, path)
423
430
424 def list_checkpoints(self, name, path=''):
431 def list_checkpoints(self, name, path=''):
425 """list the checkpoints for a given notebook
432 """list the checkpoints for a given notebook
426
433
427 This notebook manager currently only supports one checkpoint per notebook.
434 This notebook manager currently only supports one checkpoint per notebook.
428 """
435 """
429 path = path.strip('/')
436 path = path.strip('/')
430 checkpoint_id = "checkpoint"
437 checkpoint_id = "checkpoint"
431 path = self.get_checkpoint_path(checkpoint_id, name, path)
438 path = self.get_checkpoint_path(checkpoint_id, name, path)
432 if not os.path.exists(path):
439 if not os.path.exists(path):
433 return []
440 return []
434 else:
441 else:
435 return [self.get_checkpoint_model(checkpoint_id, name, path)]
442 return [self.get_checkpoint_model(checkpoint_id, name, path)]
436
443
437
444
438 def restore_checkpoint(self, checkpoint_id, name, path=''):
445 def restore_checkpoint(self, checkpoint_id, name, path=''):
439 """restore a notebook to a checkpointed state"""
446 """restore a notebook to a checkpointed state"""
440 path = path.strip('/')
447 path = path.strip('/')
441 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
448 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
442 nb_path = self.get_os_path(name, path)
449 nb_path = self._get_os_path(name, path)
443 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
450 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
444 if not os.path.isfile(cp_path):
451 if not os.path.isfile(cp_path):
445 self.log.debug("checkpoint file does not exist: %s", cp_path)
452 self.log.debug("checkpoint file does not exist: %s", cp_path)
446 raise web.HTTPError(404,
453 raise web.HTTPError(404,
447 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
454 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
448 )
455 )
449 # ensure notebook is readable (never restore from an unreadable notebook)
456 # ensure notebook is readable (never restore from an unreadable notebook)
450 with io.open(cp_path, 'r', encoding='utf-8') as f:
457 with io.open(cp_path, 'r', encoding='utf-8') as f:
451 nb = current.read(f, u'json')
458 nb = current.read(f, u'json')
452 shutil.copy2(cp_path, nb_path)
459 shutil.copy2(cp_path, nb_path)
453 self.log.debug("copying %s -> %s", cp_path, nb_path)
460 self.log.debug("copying %s -> %s", cp_path, nb_path)
454
461
455 def delete_checkpoint(self, checkpoint_id, name, path=''):
462 def delete_checkpoint(self, checkpoint_id, name, path=''):
456 """delete a notebook's checkpoint"""
463 """delete a notebook's checkpoint"""
457 path = path.strip('/')
464 path = path.strip('/')
458 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
465 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
459 if not os.path.isfile(cp_path):
466 if not os.path.isfile(cp_path):
460 raise web.HTTPError(404,
467 raise web.HTTPError(404,
461 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
468 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
462 )
469 )
463 self.log.debug("unlinking %s", cp_path)
470 self.log.debug("unlinking %s", cp_path)
464 os.unlink(cp_path)
471 os.unlink(cp_path)
465
472
466 def info_string(self):
473 def info_string(self):
467 return "Serving notebooks from local directory: %s" % self.notebook_dir
474 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,269 +1,243
1 """A base class notebook manager.
1 """A base class notebook manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import itertools
20 import itertools
21 import os
21 import os
22
22
23 from IPython.config.configurable import LoggingConfigurable
23 from IPython.config.configurable import LoggingConfigurable
24 from IPython.nbformat import current, sign
24 from IPython.nbformat import current, sign
25 from IPython.utils import py3compat
25 from IPython.utils.traitlets import Instance, Unicode
26 from IPython.utils.traitlets import Instance, Unicode, TraitError
27
26
28 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
29 # Classes
28 # Classes
30 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
31
30
32 class NotebookManager(LoggingConfigurable):
31 class NotebookManager(LoggingConfigurable):
33
32
34 # Todo:
35 # The notebook_dir attribute is used to mean a couple of different things:
36 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 # 2. The cwd of the kernel for a project.
38 # Right now we use this attribute in a number of different places and
39 # we are going to have to disentangle all of this.
40 notebook_dir = Unicode(py3compat.getcwd(), config=True, help="""
41 The directory to use for notebooks.
42 """)
43
44 filename_ext = Unicode(u'.ipynb')
33 filename_ext = Unicode(u'.ipynb')
45
34
46 notary = Instance(sign.NotebookNotary)
35 notary = Instance(sign.NotebookNotary)
47 def _notary_default(self):
36 def _notary_default(self):
48 return sign.NotebookNotary(parent=self)
37 return sign.NotebookNotary(parent=self)
49
38
50 # NotebookManager API part 1: methods that must be
39 # NotebookManager API part 1: methods that must be
51 # implemented in subclasses.
40 # implemented in subclasses.
52
41
53 def path_exists(self, path):
42 def path_exists(self, path):
54 """Does the API-style path (directory) actually exist?
43 """Does the API-style path (directory) actually exist?
55
44
56 Override this method in subclasses.
45 Override this method in subclasses.
57
46
58 Parameters
47 Parameters
59 ----------
48 ----------
60 path : string
49 path : string
61 The
50 The
62
51
63 Returns
52 Returns
64 -------
53 -------
65 exists : bool
54 exists : bool
66 Whether the path does indeed exist.
55 Whether the path does indeed exist.
67 """
56 """
68 raise NotImplementedError
57 raise NotImplementedError
69
58
70 def is_hidden(self, path):
59 def is_hidden(self, path):
71 """Does the API style path correspond to a hidden directory or file?
60 """Does the API style path correspond to a hidden directory or file?
72
61
73 Parameters
62 Parameters
74 ----------
63 ----------
75 path : string
64 path : string
76 The path to check. This is an API path (`/` separated,
65 The path to check. This is an API path (`/` separated,
77 relative to base notebook-dir).
66 relative to base notebook-dir).
78
67
79 Returns
68 Returns
80 -------
69 -------
81 exists : bool
70 exists : bool
82 Whether the path is hidden.
71 Whether the path is hidden.
83
72
84 """
73 """
85 raise NotImplementedError
74 raise NotImplementedError
86
75
87 def notebook_exists(self, name, path=''):
76 def notebook_exists(self, name, path=''):
88 """Returns a True if the notebook exists. Else, returns False.
77 """Returns a True if the notebook exists. Else, returns False.
89
78
90 Parameters
79 Parameters
91 ----------
80 ----------
92 name : string
81 name : string
93 The name of the notebook you are checking.
82 The name of the notebook you are checking.
94 path : string
83 path : string
95 The relative path to the notebook (with '/' as separator)
84 The relative path to the notebook (with '/' as separator)
96
85
97 Returns
86 Returns
98 -------
87 -------
99 bool
88 bool
100 """
89 """
101 raise NotImplementedError('must be implemented in a subclass')
90 raise NotImplementedError('must be implemented in a subclass')
102
91
103 # TODO: Remove this after we create the contents web service and directories are
92 # TODO: Remove this after we create the contents web service and directories are
104 # no longer listed by the notebook web service.
93 # no longer listed by the notebook web service.
105 def list_dirs(self, path):
94 def list_dirs(self, path):
106 """List the directory models for a given API style path."""
95 """List the directory models for a given API style path."""
107 raise NotImplementedError('must be implemented in a subclass')
96 raise NotImplementedError('must be implemented in a subclass')
108
97
109 # TODO: Remove this after we create the contents web service and directories are
98 # TODO: Remove this after we create the contents web service and directories are
110 # no longer listed by the notebook web service.
99 # no longer listed by the notebook web service.
111 def get_dir_model(self, name, path=''):
100 def get_dir_model(self, name, path=''):
112 """Get the directory model given a directory name and its API style path.
101 """Get the directory model given a directory name and its API style path.
113
102
114 The keys in the model should be:
103 The keys in the model should be:
115 * name
104 * name
116 * path
105 * path
117 * last_modified
106 * last_modified
118 * created
107 * created
119 * type='directory'
108 * type='directory'
120 """
109 """
121 raise NotImplementedError('must be implemented in a subclass')
110 raise NotImplementedError('must be implemented in a subclass')
122
111
123 def list_notebooks(self, path=''):
112 def list_notebooks(self, path=''):
124 """Return a list of notebook dicts without content.
113 """Return a list of notebook dicts without content.
125
114
126 This returns a list of dicts, each of the form::
115 This returns a list of dicts, each of the form::
127
116
128 dict(notebook_id=notebook,name=name)
117 dict(notebook_id=notebook,name=name)
129
118
130 This list of dicts should be sorted by name::
119 This list of dicts should be sorted by name::
131
120
132 data = sorted(data, key=lambda item: item['name'])
121 data = sorted(data, key=lambda item: item['name'])
133 """
122 """
134 raise NotImplementedError('must be implemented in a subclass')
123 raise NotImplementedError('must be implemented in a subclass')
135
124
136 def get_notebook(self, name, path='', content=True):
125 def get_notebook(self, name, path='', content=True):
137 """Get the notebook model with or without content."""
126 """Get the notebook model with or without content."""
138 raise NotImplementedError('must be implemented in a subclass')
127 raise NotImplementedError('must be implemented in a subclass')
139
128
140 def save_notebook(self, model, name, path=''):
129 def save_notebook(self, model, name, path=''):
141 """Save the notebook and return the model with no content."""
130 """Save the notebook and return the model with no content."""
142 raise NotImplementedError('must be implemented in a subclass')
131 raise NotImplementedError('must be implemented in a subclass')
143
132
144 def update_notebook(self, model, name, path=''):
133 def update_notebook(self, model, name, path=''):
145 """Update the notebook and return the model with no content."""
134 """Update the notebook and return the model with no content."""
146 raise NotImplementedError('must be implemented in a subclass')
135 raise NotImplementedError('must be implemented in a subclass')
147
136
148 def delete_notebook(self, name, path=''):
137 def delete_notebook(self, name, path=''):
149 """Delete notebook by name and path."""
138 """Delete notebook by name and path."""
150 raise NotImplementedError('must be implemented in a subclass')
139 raise NotImplementedError('must be implemented in a subclass')
151
140
152 def create_checkpoint(self, name, path=''):
141 def create_checkpoint(self, name, path=''):
153 """Create a checkpoint of the current state of a notebook
142 """Create a checkpoint of the current state of a notebook
154
143
155 Returns a checkpoint_id for the new checkpoint.
144 Returns a checkpoint_id for the new checkpoint.
156 """
145 """
157 raise NotImplementedError("must be implemented in a subclass")
146 raise NotImplementedError("must be implemented in a subclass")
158
147
159 def list_checkpoints(self, name, path=''):
148 def list_checkpoints(self, name, path=''):
160 """Return a list of checkpoints for a given notebook"""
149 """Return a list of checkpoints for a given notebook"""
161 return []
150 return []
162
151
163 def restore_checkpoint(self, checkpoint_id, name, path=''):
152 def restore_checkpoint(self, checkpoint_id, name, path=''):
164 """Restore a notebook from one of its checkpoints"""
153 """Restore a notebook from one of its checkpoints"""
165 raise NotImplementedError("must be implemented in a subclass")
154 raise NotImplementedError("must be implemented in a subclass")
166
155
167 def delete_checkpoint(self, checkpoint_id, name, path=''):
156 def delete_checkpoint(self, checkpoint_id, name, path=''):
168 """delete a checkpoint for a notebook"""
157 """delete a checkpoint for a notebook"""
169 raise NotImplementedError("must be implemented in a subclass")
158 raise NotImplementedError("must be implemented in a subclass")
170
159
171 def info_string(self):
160 def info_string(self):
172 return "Serving notebooks"
161 return "Serving notebooks"
173
162
174 # NotebookManager API part 2: methods that have useable default
163 # NotebookManager API part 2: methods that have useable default
175 # implementations, but can be overridden in subclasses.
164 # implementations, but can be overridden in subclasses.
176
165
177 def increment_filename(self, basename, path=''):
166 def increment_filename(self, basename, path=''):
178 """Increment a notebook filename without the .ipynb to make it unique.
167 """Increment a notebook filename without the .ipynb to make it unique.
179
168
180 Parameters
169 Parameters
181 ----------
170 ----------
182 basename : unicode
171 basename : unicode
183 The name of a notebook without the ``.ipynb`` file extension.
172 The name of a notebook without the ``.ipynb`` file extension.
184 path : unicode
173 path : unicode
185 The URL path of the notebooks directory
174 The URL path of the notebooks directory
186
175
187 Returns
176 Returns
188 -------
177 -------
189 name : unicode
178 name : unicode
190 A notebook name (with the .ipynb extension) that starts
179 A notebook name (with the .ipynb extension) that starts
191 with basename and does not refer to any existing notebook.
180 with basename and does not refer to any existing notebook.
192 """
181 """
193 path = path.strip('/')
182 path = path.strip('/')
194 for i in itertools.count():
183 for i in itertools.count():
195 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
184 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
196 ext=self.filename_ext)
185 ext=self.filename_ext)
197 if not self.notebook_exists(name, path):
186 if not self.notebook_exists(name, path):
198 break
187 break
199 return name
188 return name
200
189
201 def create_notebook(self, model=None, path=''):
190 def create_notebook(self, model=None, path=''):
202 """Create a new notebook and return its model with no content."""
191 """Create a new notebook and return its model with no content."""
203 path = path.strip('/')
192 path = path.strip('/')
204 if model is None:
193 if model is None:
205 model = {}
194 model = {}
206 if 'content' not in model:
195 if 'content' not in model:
207 metadata = current.new_metadata(name=u'')
196 metadata = current.new_metadata(name=u'')
208 model['content'] = current.new_notebook(metadata=metadata)
197 model['content'] = current.new_notebook(metadata=metadata)
209 if 'name' not in model:
198 if 'name' not in model:
210 model['name'] = self.increment_filename('Untitled', path)
199 model['name'] = self.increment_filename('Untitled', path)
211
200
212 model['path'] = path
201 model['path'] = path
213 model = self.save_notebook(model, model['name'], model['path'])
202 model = self.save_notebook(model, model['name'], model['path'])
214 return model
203 return model
215
204
216 def copy_notebook(self, from_name, to_name=None, path=''):
205 def copy_notebook(self, from_name, to_name=None, path=''):
217 """Copy an existing notebook and return its new model.
206 """Copy an existing notebook and return its new model.
218
207
219 If to_name not specified, increment `from_name-Copy#.ipynb`.
208 If to_name not specified, increment `from_name-Copy#.ipynb`.
220 """
209 """
221 path = path.strip('/')
210 path = path.strip('/')
222 model = self.get_notebook(from_name, path)
211 model = self.get_notebook(from_name, path)
223 if not to_name:
212 if not to_name:
224 base = os.path.splitext(from_name)[0] + '-Copy'
213 base = os.path.splitext(from_name)[0] + '-Copy'
225 to_name = self.increment_filename(base, path)
214 to_name = self.increment_filename(base, path)
226 model['name'] = to_name
215 model['name'] = to_name
227 model = self.save_notebook(model, to_name, path)
216 model = self.save_notebook(model, to_name, path)
228 return model
217 return model
229
218
230 def log_info(self):
219 def log_info(self):
231 self.log.info(self.info_string())
220 self.log.info(self.info_string())
232
221
233 # NotebookManager methods provided for use in subclasses.
222 # NotebookManager methods provided for use in subclasses.
234
223
235 def check_and_sign(self, nb, path, name):
224 def check_and_sign(self, nb, path, name):
236 """Check for trusted cells, and sign the notebook.
225 """Check for trusted cells, and sign the notebook.
237
226
238 Called as a part of saving notebooks.
227 Called as a part of saving notebooks.
239 """
228 """
240 if self.notary.check_cells(nb):
229 if self.notary.check_cells(nb):
241 self.notary.sign(nb)
230 self.notary.sign(nb)
242 else:
231 else:
243 self.log.warn("Saving untrusted notebook %s/%s", path, name)
232 self.log.warn("Saving untrusted notebook %s/%s", path, name)
244
233
245 def mark_trusted_cells(self, nb, path, name):
234 def mark_trusted_cells(self, nb, path, name):
246 """Mark cells as trusted if the notebook signature matches.
235 """Mark cells as trusted if the notebook signature matches.
247
236
248 Called as a part of loading notebooks.
237 Called as a part of loading notebooks.
249 """
238 """
250 trusted = self.notary.check_signature(nb)
239 trusted = self.notary.check_signature(nb)
251 if not trusted:
240 if not trusted:
252 self.log.warn("Notebook %s/%s is not trusted", path, name)
241 self.log.warn("Notebook %s/%s is not trusted", path, name)
253 self.notary.mark_cells(nb, trusted)
242 self.notary.mark_cells(nb, trusted)
254
255 def _notebook_dir_changed(self, name, old, new):
256 """Do a bit of validation of the notebook dir."""
257 if not os.path.isabs(new):
258 # If we receive a non-absolute path, make it absolute.
259 self.notebook_dir = os.path.abspath(new)
260 return
261 if os.path.exists(new) and not os.path.isdir(new):
262 raise TraitError("notebook dir %r is not a directory" % new)
263 if not os.path.exists(new):
264 self.log.info("Creating notebook dir %s", new)
265 try:
266 os.mkdir(new)
267 except:
268 raise TraitError("Couldn't create notebook dir %r" % new)
269
243
@@ -1,251 +1,245
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import os
5 import os
6
6
7 from tornado.web import HTTPError
7 from tornado.web import HTTPError
8 from unittest import TestCase
8 from unittest import TestCase
9 from tempfile import NamedTemporaryFile
9 from tempfile import NamedTemporaryFile
10
10
11 from IPython.utils.tempdir import TemporaryDirectory
11 from IPython.utils.tempdir import TemporaryDirectory
12 from IPython.utils.traitlets import TraitError
12 from IPython.utils.traitlets import TraitError
13 from IPython.html.utils import url_path_join
13 from IPython.html.utils import url_path_join
14
14
15 from ..filenbmanager import FileNotebookManager
15 from ..filenbmanager import FileNotebookManager
16 from ..nbmanager import NotebookManager
16 from ..nbmanager import NotebookManager
17
17
18
18
19 class TestFileNotebookManager(TestCase):
19 class TestFileNotebookManager(TestCase):
20
20
21 def test_nb_dir(self):
21 def test_nb_dir(self):
22 with TemporaryDirectory() as td:
22 with TemporaryDirectory() as td:
23 fm = FileNotebookManager(notebook_dir=td)
23 fm = FileNotebookManager(notebook_dir=td)
24 self.assertEqual(fm.notebook_dir, td)
24 self.assertEqual(fm.notebook_dir, td)
25
25
26 def test_create_nb_dir(self):
27 with TemporaryDirectory() as td:
28 nbdir = os.path.join(td, 'notebooks')
29 fm = FileNotebookManager(notebook_dir=nbdir)
30 self.assertEqual(fm.notebook_dir, nbdir)
31
32 def test_missing_nb_dir(self):
26 def test_missing_nb_dir(self):
33 with TemporaryDirectory() as td:
27 with TemporaryDirectory() as td:
34 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
28 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
35 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
29 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
36
30
37 def test_invalid_nb_dir(self):
31 def test_invalid_nb_dir(self):
38 with NamedTemporaryFile() as tf:
32 with NamedTemporaryFile() as tf:
39 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
33 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
40
34
41 def test_get_os_path(self):
35 def test_get_os_path(self):
42 # full filesystem path should be returned with correct operating system
36 # full filesystem path should be returned with correct operating system
43 # separators.
37 # separators.
44 with TemporaryDirectory() as td:
38 with TemporaryDirectory() as td:
45 nbdir = os.path.join(td, 'notebooks')
39 nbdir = td
46 fm = FileNotebookManager(notebook_dir=nbdir)
40 fm = FileNotebookManager(notebook_dir=nbdir)
47 path = fm.get_os_path('test.ipynb', '/path/to/notebook/')
41 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
48 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
42 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
49 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
43 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
50 self.assertEqual(path, fs_path)
44 self.assertEqual(path, fs_path)
51
45
52 fm = FileNotebookManager(notebook_dir=nbdir)
46 fm = FileNotebookManager(notebook_dir=nbdir)
53 path = fm.get_os_path('test.ipynb')
47 path = fm._get_os_path('test.ipynb')
54 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
48 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
55 self.assertEqual(path, fs_path)
49 self.assertEqual(path, fs_path)
56
50
57 fm = FileNotebookManager(notebook_dir=nbdir)
51 fm = FileNotebookManager(notebook_dir=nbdir)
58 path = fm.get_os_path('test.ipynb', '////')
52 path = fm._get_os_path('test.ipynb', '////')
59 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
53 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
60 self.assertEqual(path, fs_path)
54 self.assertEqual(path, fs_path)
61
55
62 class TestNotebookManager(TestCase):
56 class TestNotebookManager(TestCase):
63
57
64 def make_dir(self, abs_path, rel_path):
58 def make_dir(self, abs_path, rel_path):
65 """make subdirectory, rel_path is the relative path
59 """make subdirectory, rel_path is the relative path
66 to that directory from the location where the server started"""
60 to that directory from the location where the server started"""
67 os_path = os.path.join(abs_path, rel_path)
61 os_path = os.path.join(abs_path, rel_path)
68 try:
62 try:
69 os.makedirs(os_path)
63 os.makedirs(os_path)
70 except OSError:
64 except OSError:
71 print("Directory already exists: %r" % os_path)
65 print("Directory already exists: %r" % os_path)
72
66
73 def test_create_notebook(self):
67 def test_create_notebook(self):
74 with TemporaryDirectory() as td:
68 with TemporaryDirectory() as td:
75 # Test in root directory
69 # Test in root directory
76 nm = FileNotebookManager(notebook_dir=td)
70 nm = FileNotebookManager(notebook_dir=td)
77 model = nm.create_notebook()
71 model = nm.create_notebook()
78 assert isinstance(model, dict)
72 assert isinstance(model, dict)
79 self.assertIn('name', model)
73 self.assertIn('name', model)
80 self.assertIn('path', model)
74 self.assertIn('path', model)
81 self.assertEqual(model['name'], 'Untitled0.ipynb')
75 self.assertEqual(model['name'], 'Untitled0.ipynb')
82 self.assertEqual(model['path'], '')
76 self.assertEqual(model['path'], '')
83
77
84 # Test in sub-directory
78 # Test in sub-directory
85 sub_dir = '/foo/'
79 sub_dir = '/foo/'
86 self.make_dir(nm.notebook_dir, 'foo')
80 self.make_dir(nm.notebook_dir, 'foo')
87 model = nm.create_notebook(None, sub_dir)
81 model = nm.create_notebook(None, sub_dir)
88 assert isinstance(model, dict)
82 assert isinstance(model, dict)
89 self.assertIn('name', model)
83 self.assertIn('name', model)
90 self.assertIn('path', model)
84 self.assertIn('path', model)
91 self.assertEqual(model['name'], 'Untitled0.ipynb')
85 self.assertEqual(model['name'], 'Untitled0.ipynb')
92 self.assertEqual(model['path'], sub_dir.strip('/'))
86 self.assertEqual(model['path'], sub_dir.strip('/'))
93
87
94 def test_get_notebook(self):
88 def test_get_notebook(self):
95 with TemporaryDirectory() as td:
89 with TemporaryDirectory() as td:
96 # Test in root directory
90 # Test in root directory
97 # Create a notebook
91 # Create a notebook
98 nm = FileNotebookManager(notebook_dir=td)
92 nm = FileNotebookManager(notebook_dir=td)
99 model = nm.create_notebook()
93 model = nm.create_notebook()
100 name = model['name']
94 name = model['name']
101 path = model['path']
95 path = model['path']
102
96
103 # Check that we 'get' on the notebook we just created
97 # Check that we 'get' on the notebook we just created
104 model2 = nm.get_notebook(name, path)
98 model2 = nm.get_notebook(name, path)
105 assert isinstance(model2, dict)
99 assert isinstance(model2, dict)
106 self.assertIn('name', model2)
100 self.assertIn('name', model2)
107 self.assertIn('path', model2)
101 self.assertIn('path', model2)
108 self.assertEqual(model['name'], name)
102 self.assertEqual(model['name'], name)
109 self.assertEqual(model['path'], path)
103 self.assertEqual(model['path'], path)
110
104
111 # Test in sub-directory
105 # Test in sub-directory
112 sub_dir = '/foo/'
106 sub_dir = '/foo/'
113 self.make_dir(nm.notebook_dir, 'foo')
107 self.make_dir(nm.notebook_dir, 'foo')
114 model = nm.create_notebook(None, sub_dir)
108 model = nm.create_notebook(None, sub_dir)
115 model2 = nm.get_notebook(name, sub_dir)
109 model2 = nm.get_notebook(name, sub_dir)
116 assert isinstance(model2, dict)
110 assert isinstance(model2, dict)
117 self.assertIn('name', model2)
111 self.assertIn('name', model2)
118 self.assertIn('path', model2)
112 self.assertIn('path', model2)
119 self.assertIn('content', model2)
113 self.assertIn('content', model2)
120 self.assertEqual(model2['name'], 'Untitled0.ipynb')
114 self.assertEqual(model2['name'], 'Untitled0.ipynb')
121 self.assertEqual(model2['path'], sub_dir.strip('/'))
115 self.assertEqual(model2['path'], sub_dir.strip('/'))
122
116
123 def test_update_notebook(self):
117 def test_update_notebook(self):
124 with TemporaryDirectory() as td:
118 with TemporaryDirectory() as td:
125 # Test in root directory
119 # Test in root directory
126 # Create a notebook
120 # Create a notebook
127 nm = FileNotebookManager(notebook_dir=td)
121 nm = FileNotebookManager(notebook_dir=td)
128 model = nm.create_notebook()
122 model = nm.create_notebook()
129 name = model['name']
123 name = model['name']
130 path = model['path']
124 path = model['path']
131
125
132 # Change the name in the model for rename
126 # Change the name in the model for rename
133 model['name'] = 'test.ipynb'
127 model['name'] = 'test.ipynb'
134 model = nm.update_notebook(model, name, path)
128 model = nm.update_notebook(model, name, path)
135 assert isinstance(model, dict)
129 assert isinstance(model, dict)
136 self.assertIn('name', model)
130 self.assertIn('name', model)
137 self.assertIn('path', model)
131 self.assertIn('path', model)
138 self.assertEqual(model['name'], 'test.ipynb')
132 self.assertEqual(model['name'], 'test.ipynb')
139
133
140 # Make sure the old name is gone
134 # Make sure the old name is gone
141 self.assertRaises(HTTPError, nm.get_notebook, name, path)
135 self.assertRaises(HTTPError, nm.get_notebook, name, path)
142
136
143 # Test in sub-directory
137 # Test in sub-directory
144 # Create a directory and notebook in that directory
138 # Create a directory and notebook in that directory
145 sub_dir = '/foo/'
139 sub_dir = '/foo/'
146 self.make_dir(nm.notebook_dir, 'foo')
140 self.make_dir(nm.notebook_dir, 'foo')
147 model = nm.create_notebook(None, sub_dir)
141 model = nm.create_notebook(None, sub_dir)
148 name = model['name']
142 name = model['name']
149 path = model['path']
143 path = model['path']
150
144
151 # Change the name in the model for rename
145 # Change the name in the model for rename
152 model['name'] = 'test_in_sub.ipynb'
146 model['name'] = 'test_in_sub.ipynb'
153 model = nm.update_notebook(model, name, path)
147 model = nm.update_notebook(model, name, path)
154 assert isinstance(model, dict)
148 assert isinstance(model, dict)
155 self.assertIn('name', model)
149 self.assertIn('name', model)
156 self.assertIn('path', model)
150 self.assertIn('path', model)
157 self.assertEqual(model['name'], 'test_in_sub.ipynb')
151 self.assertEqual(model['name'], 'test_in_sub.ipynb')
158 self.assertEqual(model['path'], sub_dir.strip('/'))
152 self.assertEqual(model['path'], sub_dir.strip('/'))
159
153
160 # Make sure the old name is gone
154 # Make sure the old name is gone
161 self.assertRaises(HTTPError, nm.get_notebook, name, path)
155 self.assertRaises(HTTPError, nm.get_notebook, name, path)
162
156
163 def test_save_notebook(self):
157 def test_save_notebook(self):
164 with TemporaryDirectory() as td:
158 with TemporaryDirectory() as td:
165 # Test in the root directory
159 # Test in the root directory
166 # Create a notebook
160 # Create a notebook
167 nm = FileNotebookManager(notebook_dir=td)
161 nm = FileNotebookManager(notebook_dir=td)
168 model = nm.create_notebook()
162 model = nm.create_notebook()
169 name = model['name']
163 name = model['name']
170 path = model['path']
164 path = model['path']
171
165
172 # Get the model with 'content'
166 # Get the model with 'content'
173 full_model = nm.get_notebook(name, path)
167 full_model = nm.get_notebook(name, path)
174
168
175 # Save the notebook
169 # Save the notebook
176 model = nm.save_notebook(full_model, name, path)
170 model = nm.save_notebook(full_model, name, path)
177 assert isinstance(model, dict)
171 assert isinstance(model, dict)
178 self.assertIn('name', model)
172 self.assertIn('name', model)
179 self.assertIn('path', model)
173 self.assertIn('path', model)
180 self.assertEqual(model['name'], name)
174 self.assertEqual(model['name'], name)
181 self.assertEqual(model['path'], path)
175 self.assertEqual(model['path'], path)
182
176
183 # Test in sub-directory
177 # Test in sub-directory
184 # Create a directory and notebook in that directory
178 # Create a directory and notebook in that directory
185 sub_dir = '/foo/'
179 sub_dir = '/foo/'
186 self.make_dir(nm.notebook_dir, 'foo')
180 self.make_dir(nm.notebook_dir, 'foo')
187 model = nm.create_notebook(None, sub_dir)
181 model = nm.create_notebook(None, sub_dir)
188 name = model['name']
182 name = model['name']
189 path = model['path']
183 path = model['path']
190 model = nm.get_notebook(name, path)
184 model = nm.get_notebook(name, path)
191
185
192 # Change the name in the model for rename
186 # Change the name in the model for rename
193 model = nm.save_notebook(model, name, path)
187 model = nm.save_notebook(model, name, path)
194 assert isinstance(model, dict)
188 assert isinstance(model, dict)
195 self.assertIn('name', model)
189 self.assertIn('name', model)
196 self.assertIn('path', model)
190 self.assertIn('path', model)
197 self.assertEqual(model['name'], 'Untitled0.ipynb')
191 self.assertEqual(model['name'], 'Untitled0.ipynb')
198 self.assertEqual(model['path'], sub_dir.strip('/'))
192 self.assertEqual(model['path'], sub_dir.strip('/'))
199
193
200 def test_save_notebook_with_script(self):
194 def test_save_notebook_with_script(self):
201 with TemporaryDirectory() as td:
195 with TemporaryDirectory() as td:
202 # Create a notebook
196 # Create a notebook
203 nm = FileNotebookManager(notebook_dir=td)
197 nm = FileNotebookManager(notebook_dir=td)
204 nm.save_script = True
198 nm.save_script = True
205 model = nm.create_notebook()
199 model = nm.create_notebook()
206 name = model['name']
200 name = model['name']
207 path = model['path']
201 path = model['path']
208
202
209 # Get the model with 'content'
203 # Get the model with 'content'
210 full_model = nm.get_notebook(name, path)
204 full_model = nm.get_notebook(name, path)
211
205
212 # Save the notebook
206 # Save the notebook
213 model = nm.save_notebook(full_model, name, path)
207 model = nm.save_notebook(full_model, name, path)
214
208
215 # Check that the script was created
209 # Check that the script was created
216 py_path = os.path.join(td, os.path.splitext(name)[0]+'.py')
210 py_path = os.path.join(td, os.path.splitext(name)[0]+'.py')
217 assert os.path.exists(py_path), py_path
211 assert os.path.exists(py_path), py_path
218
212
219 def test_delete_notebook(self):
213 def test_delete_notebook(self):
220 with TemporaryDirectory() as td:
214 with TemporaryDirectory() as td:
221 # Test in the root directory
215 # Test in the root directory
222 # Create a notebook
216 # Create a notebook
223 nm = FileNotebookManager(notebook_dir=td)
217 nm = FileNotebookManager(notebook_dir=td)
224 model = nm.create_notebook()
218 model = nm.create_notebook()
225 name = model['name']
219 name = model['name']
226 path = model['path']
220 path = model['path']
227
221
228 # Delete the notebook
222 # Delete the notebook
229 nm.delete_notebook(name, path)
223 nm.delete_notebook(name, path)
230
224
231 # Check that a 'get' on the deleted notebook raises and error
225 # Check that a 'get' on the deleted notebook raises and error
232 self.assertRaises(HTTPError, nm.get_notebook, name, path)
226 self.assertRaises(HTTPError, nm.get_notebook, name, path)
233
227
234 def test_copy_notebook(self):
228 def test_copy_notebook(self):
235 with TemporaryDirectory() as td:
229 with TemporaryDirectory() as td:
236 # Test in the root directory
230 # Test in the root directory
237 # Create a notebook
231 # Create a notebook
238 nm = FileNotebookManager(notebook_dir=td)
232 nm = FileNotebookManager(notebook_dir=td)
239 path = u'Γ₯ b'
233 path = u'Γ₯ b'
240 name = u'nb √.ipynb'
234 name = u'nb √.ipynb'
241 os.mkdir(os.path.join(td, path))
235 os.mkdir(os.path.join(td, path))
242 orig = nm.create_notebook({'name' : name}, path=path)
236 orig = nm.create_notebook({'name' : name}, path=path)
243
237
244 # copy with unspecified name
238 # copy with unspecified name
245 copy = nm.copy_notebook(name, path=path)
239 copy = nm.copy_notebook(name, path=path)
246 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
240 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
247
241
248 # copy with specified name
242 # copy with specified name
249 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
243 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
250 self.assertEqual(copy2['name'], u'copy 2.ipynb')
244 self.assertEqual(copy2['name'], u'copy 2.ipynb')
251
245
@@ -1,127 +1,127
1 """Tornado handlers for the sessions web service.
1 """Tornado handlers for the sessions web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Zach Sailer
5 * Zach Sailer
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from ...base.handlers import IPythonHandler, json_errors
23 from ...base.handlers import IPythonHandler, json_errors
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join, url_escape
25 from IPython.html.utils import url_path_join, url_escape
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Session web service handlers
28 # Session web service handlers
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31
31
32 class SessionRootHandler(IPythonHandler):
32 class SessionRootHandler(IPythonHandler):
33
33
34 @web.authenticated
34 @web.authenticated
35 @json_errors
35 @json_errors
36 def get(self):
36 def get(self):
37 # Return a list of running sessions
37 # Return a list of running sessions
38 sm = self.session_manager
38 sm = self.session_manager
39 sessions = sm.list_sessions()
39 sessions = sm.list_sessions()
40 self.finish(json.dumps(sessions, default=date_default))
40 self.finish(json.dumps(sessions, default=date_default))
41
41
42 @web.authenticated
42 @web.authenticated
43 @json_errors
43 @json_errors
44 def post(self):
44 def post(self):
45 # Creates a new session
45 # Creates a new session
46 #(unless a session already exists for the named nb)
46 #(unless a session already exists for the named nb)
47 sm = self.session_manager
47 sm = self.session_manager
48 nbm = self.notebook_manager
48 nbm = self.notebook_manager
49 km = self.kernel_manager
49 km = self.kernel_manager
50 model = self.get_json_body()
50 model = self.get_json_body()
51 if model is None:
51 if model is None:
52 raise web.HTTPError(400, "No JSON data provided")
52 raise web.HTTPError(400, "No JSON data provided")
53 try:
53 try:
54 name = model['notebook']['name']
54 name = model['notebook']['name']
55 except KeyError:
55 except KeyError:
56 raise web.HTTPError(400, "Missing field in JSON data: name")
56 raise web.HTTPError(400, "Missing field in JSON data: name")
57 try:
57 try:
58 path = model['notebook']['path']
58 path = model['notebook']['path']
59 except KeyError:
59 except KeyError:
60 raise web.HTTPError(400, "Missing field in JSON data: path")
60 raise web.HTTPError(400, "Missing field in JSON data: path")
61 # Check to see if session exists
61 # Check to see if session exists
62 if sm.session_exists(name=name, path=path):
62 if sm.session_exists(name=name, path=path):
63 model = sm.get_session(name=name, path=path)
63 model = sm.get_session(name=name, path=path)
64 else:
64 else:
65 kernel_id = km.start_kernel(cwd=nbm.get_os_path(path))
65 kernel_id = km.start_kernel(path=path)
66 model = sm.create_session(name=name, path=path, kernel_id=kernel_id)
66 model = sm.create_session(name=name, path=path, kernel_id=kernel_id)
67 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
67 location = url_path_join(self.base_url, 'api', 'sessions', model['id'])
68 self.set_header('Location', url_escape(location))
68 self.set_header('Location', url_escape(location))
69 self.set_status(201)
69 self.set_status(201)
70 self.finish(json.dumps(model, default=date_default))
70 self.finish(json.dumps(model, default=date_default))
71
71
72 class SessionHandler(IPythonHandler):
72 class SessionHandler(IPythonHandler):
73
73
74 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
74 SUPPORTED_METHODS = ('GET', 'PATCH', 'DELETE')
75
75
76 @web.authenticated
76 @web.authenticated
77 @json_errors
77 @json_errors
78 def get(self, session_id):
78 def get(self, session_id):
79 # Returns the JSON model for a single session
79 # Returns the JSON model for a single session
80 sm = self.session_manager
80 sm = self.session_manager
81 model = sm.get_session(session_id=session_id)
81 model = sm.get_session(session_id=session_id)
82 self.finish(json.dumps(model, default=date_default))
82 self.finish(json.dumps(model, default=date_default))
83
83
84 @web.authenticated
84 @web.authenticated
85 @json_errors
85 @json_errors
86 def patch(self, session_id):
86 def patch(self, session_id):
87 # Currently, this handler is strictly for renaming notebooks
87 # Currently, this handler is strictly for renaming notebooks
88 sm = self.session_manager
88 sm = self.session_manager
89 model = self.get_json_body()
89 model = self.get_json_body()
90 if model is None:
90 if model is None:
91 raise web.HTTPError(400, "No JSON data provided")
91 raise web.HTTPError(400, "No JSON data provided")
92 changes = {}
92 changes = {}
93 if 'notebook' in model:
93 if 'notebook' in model:
94 notebook = model['notebook']
94 notebook = model['notebook']
95 if 'name' in notebook:
95 if 'name' in notebook:
96 changes['name'] = notebook['name']
96 changes['name'] = notebook['name']
97 if 'path' in notebook:
97 if 'path' in notebook:
98 changes['path'] = notebook['path']
98 changes['path'] = notebook['path']
99
99
100 sm.update_session(session_id, **changes)
100 sm.update_session(session_id, **changes)
101 model = sm.get_session(session_id=session_id)
101 model = sm.get_session(session_id=session_id)
102 self.finish(json.dumps(model, default=date_default))
102 self.finish(json.dumps(model, default=date_default))
103
103
104 @web.authenticated
104 @web.authenticated
105 @json_errors
105 @json_errors
106 def delete(self, session_id):
106 def delete(self, session_id):
107 # Deletes the session with given session_id
107 # Deletes the session with given session_id
108 sm = self.session_manager
108 sm = self.session_manager
109 km = self.kernel_manager
109 km = self.kernel_manager
110 session = sm.get_session(session_id=session_id)
110 session = sm.get_session(session_id=session_id)
111 sm.delete_session(session_id)
111 sm.delete_session(session_id)
112 km.shutdown_kernel(session['kernel']['id'])
112 km.shutdown_kernel(session['kernel']['id'])
113 self.set_status(204)
113 self.set_status(204)
114 self.finish()
114 self.finish()
115
115
116
116
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118 # URL to handler mappings
118 # URL to handler mappings
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120
120
121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
121 _session_id_regex = r"(?P<session_id>\w+-\w+-\w+-\w+-\w+)"
122
122
123 default_handlers = [
123 default_handlers = [
124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
124 (r"/api/sessions/%s" % _session_id_regex, SessionHandler),
125 (r"/api/sessions", SessionRootHandler)
125 (r"/api/sessions", SessionRootHandler)
126 ]
126 ]
127
127
@@ -1,41 +1,73
1 """Test NotebookApp"""
1 """Test NotebookApp"""
2
2
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2013 The IPython Development Team
4 # Copyright (C) 2013 The IPython Development Team
5 #
5 #
6 # Distributed under the terms of the BSD License. The full license is in
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 import os
15 from tempfile import NamedTemporaryFile
16
14 import nose.tools as nt
17 import nose.tools as nt
15
18
19 from IPython.utils.tempdir import TemporaryDirectory
20 from IPython.utils.traitlets import TraitError
16 import IPython.testing.tools as tt
21 import IPython.testing.tools as tt
17 from IPython.html import notebookapp
22 from IPython.html import notebookapp
23 NotebookApp = notebookapp.NotebookApp
18
24
19 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
20 # Test functions
26 # Test functions
21 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
22
28
23 def test_help_output():
29 def test_help_output():
24 """ipython notebook --help-all works"""
30 """ipython notebook --help-all works"""
25 tt.help_all_output_test('notebook')
31 tt.help_all_output_test('notebook')
26
32
27 def test_server_info_file():
33 def test_server_info_file():
28 nbapp = notebookapp.NotebookApp(profile='nbserver_file_test')
34 nbapp = NotebookApp(profile='nbserver_file_test')
29 def get_servers():
35 def get_servers():
30 return list(notebookapp.list_running_servers(profile='nbserver_file_test'))
36 return list(notebookapp.list_running_servers(profile='nbserver_file_test'))
31 nbapp.initialize(argv=[])
37 nbapp.initialize(argv=[])
32 nbapp.write_server_info_file()
38 nbapp.write_server_info_file()
33 servers = get_servers()
39 servers = get_servers()
34 nt.assert_equal(len(servers), 1)
40 nt.assert_equal(len(servers), 1)
35 nt.assert_equal(servers[0]['port'], nbapp.port)
41 nt.assert_equal(servers[0]['port'], nbapp.port)
36 nt.assert_equal(servers[0]['url'], nbapp.connection_url)
42 nt.assert_equal(servers[0]['url'], nbapp.connection_url)
37 nbapp.remove_server_info_file()
43 nbapp.remove_server_info_file()
38 nt.assert_equal(get_servers(), [])
44 nt.assert_equal(get_servers(), [])
39
45
40 # The ENOENT error should be silenced.
46 # The ENOENT error should be silenced.
41 nbapp.remove_server_info_file() No newline at end of file
47 nbapp.remove_server_info_file()
48
49 def test_nb_dir():
50 with TemporaryDirectory() as td:
51 app = NotebookApp(notebook_dir=td)
52 nt.assert_equal(app.notebook_dir, td)
53
54 def test_no_create_nb_dir():
55 with TemporaryDirectory() as td:
56 nbdir = os.path.join(td, 'notebooks')
57 app = NotebookApp()
58 with nt.assert_raises(TraitError):
59 app.notebook_dir = nbdir
60
61 def test_missing_nb_dir():
62 with TemporaryDirectory() as td:
63 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
64 app = NotebookApp()
65 with nt.assert_raises(TraitError):
66 app.notebook_dir = nbdir
67
68 def test_invalid_nb_dir():
69 with NamedTemporaryFile() as tf:
70 app = NotebookApp()
71 with nt.assert_raises(TraitError):
72 app.notebook_dir = tf
73
@@ -1,114 +1,125
1 """Notebook related utilities
1 """Notebook related utilities
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 from __future__ import print_function
15 from __future__ import print_function
16
16
17 import os
17 import os
18 import stat
18 import stat
19
19
20 try:
20 try:
21 from urllib.parse import quote, unquote
21 from urllib.parse import quote, unquote
22 except ImportError:
22 except ImportError:
23 from urllib import quote, unquote
23 from urllib import quote, unquote
24
24
25 from IPython.utils import py3compat
25 from IPython.utils import py3compat
26
26
27 # UF_HIDDEN is a stat flag not defined in the stat module.
27 # UF_HIDDEN is a stat flag not defined in the stat module.
28 # It is used by BSD to indicate hidden files.
28 # It is used by BSD to indicate hidden files.
29 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
29 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
30
30
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32 # Imports
32 # Imports
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34
34
35 def url_path_join(*pieces):
35 def url_path_join(*pieces):
36 """Join components of url into a relative url
36 """Join components of url into a relative url
37
37
38 Use to prevent double slash when joining subpath. This will leave the
38 Use to prevent double slash when joining subpath. This will leave the
39 initial and final / in place
39 initial and final / in place
40 """
40 """
41 initial = pieces[0].startswith('/')
41 initial = pieces[0].startswith('/')
42 final = pieces[-1].endswith('/')
42 final = pieces[-1].endswith('/')
43 stripped = [s.strip('/') for s in pieces]
43 stripped = [s.strip('/') for s in pieces]
44 result = '/'.join(s for s in stripped if s)
44 result = '/'.join(s for s in stripped if s)
45 if initial: result = '/' + result
45 if initial: result = '/' + result
46 if final: result = result + '/'
46 if final: result = result + '/'
47 if result == '//': result = '/'
47 if result == '//': result = '/'
48 return result
48 return result
49
49
50 def path2url(path):
50 def path2url(path):
51 """Convert a local file path to a URL"""
51 """Convert a local file path to a URL"""
52 pieces = [ quote(p) for p in path.split(os.sep) ]
52 pieces = [ quote(p) for p in path.split(os.sep) ]
53 # preserve trailing /
53 # preserve trailing /
54 if pieces[-1] == '':
54 if pieces[-1] == '':
55 pieces[-1] = '/'
55 pieces[-1] = '/'
56 url = url_path_join(*pieces)
56 url = url_path_join(*pieces)
57 return url
57 return url
58
58
59 def url2path(url):
59 def url2path(url):
60 """Convert a URL to a local file path"""
60 """Convert a URL to a local file path"""
61 pieces = [ unquote(p) for p in url.split('/') ]
61 pieces = [ unquote(p) for p in url.split('/') ]
62 path = os.path.join(*pieces)
62 path = os.path.join(*pieces)
63 return path
63 return path
64
64
65 def url_escape(path):
65 def url_escape(path):
66 """Escape special characters in a URL path
66 """Escape special characters in a URL path
67
67
68 Turns '/foo bar/' into '/foo%20bar/'
68 Turns '/foo bar/' into '/foo%20bar/'
69 """
69 """
70 parts = py3compat.unicode_to_str(path).split('/')
70 parts = py3compat.unicode_to_str(path).split('/')
71 return u'/'.join([quote(p) for p in parts])
71 return u'/'.join([quote(p) for p in parts])
72
72
73 def url_unescape(path):
73 def url_unescape(path):
74 """Unescape special characters in a URL path
74 """Unescape special characters in a URL path
75
75
76 Turns '/foo%20bar/' into '/foo bar/'
76 Turns '/foo%20bar/' into '/foo bar/'
77 """
77 """
78 return u'/'.join([
78 return u'/'.join([
79 py3compat.str_to_unicode(unquote(p))
79 py3compat.str_to_unicode(unquote(p))
80 for p in py3compat.unicode_to_str(path).split('/')
80 for p in py3compat.unicode_to_str(path).split('/')
81 ])
81 ])
82
82
83 def is_hidden(abs_path, abs_root=''):
83 def is_hidden(abs_path, abs_root=''):
84 """Is a file is hidden or contained in a hidden directory.
84 """Is a file hidden or contained in a hidden directory?
85
85
86 This will start with the rightmost path element and work backwards to the
86 This will start with the rightmost path element and work backwards to the
87 given root to see if a path is hidden or in a hidden directory. Hidden is
87 given root to see if a path is hidden or in a hidden directory. Hidden is
88 determined by either name starting with '.' or the UF_HIDDEN flag as
88 determined by either name starting with '.' or the UF_HIDDEN flag as
89 reported by stat.
89 reported by stat.
90
90
91 Parameters
91 Parameters
92 ----------
92 ----------
93 abs_path : unicode
93 abs_path : unicode
94 The absolute path to check for hidden directories.
94 The absolute path to check for hidden directories.
95 abs_root : unicode
95 abs_root : unicode
96 The absolute path of the root directory in which hidden directories
96 The absolute path of the root directory in which hidden directories
97 should be checked for.
97 should be checked for.
98 """
98 """
99 if not abs_root:
99 if not abs_root:
100 abs_root = abs_path.split(os.sep, 1)[0] + os.sep
100 abs_root = abs_path.split(os.sep, 1)[0] + os.sep
101 inside_root = abs_path[len(abs_root):]
101 inside_root = abs_path[len(abs_root):]
102 if any(part.startswith('.') for part in inside_root.split(os.sep)):
102 if any(part.startswith('.') for part in inside_root.split(os.sep)):
103 return True
103 return True
104
104
105 # check UF_HIDDEN on any location up to root
105 # check UF_HIDDEN on any location up to root
106 path = abs_path
106 path = abs_path
107 while path and path.startswith(abs_root) and path != abs_root:
107 while path and path.startswith(abs_root) and path != abs_root:
108 st = os.stat(path)
108 st = os.stat(path)
109 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
109 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
110 return True
110 return True
111 path = os.path.dirname(path)
111 path = os.path.dirname(path)
112
112
113 return False
113 return False
114
114
115 def to_os_path(path, root=''):
116 """Convert an API path to a filesystem path
117
118 If given, root will be prepended to the path.
119 root must be a filesystem path already.
120 """
121 parts = path.strip('/').split('/')
122 parts = [p for p in parts if p != ''] # remove duplicate splits
123 path = os.path.join(root, *parts)
124 return path
125
General Comments 0
You need to be logged in to leave comments. Login now