##// END OF EJS Templates
reorganize who knows what about paths...
MinRK -
Show More
@@ -1,821 +1,844 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 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 # FIXME: /files/ should be handled by the Contents service when it exists
205 nbm = settings['notebook_manager']
206 if hasattr(nbm, 'notebook_dir'):
204 handlers.extend([
207 handlers.extend([
205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
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)
510
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 os.path.exists(new) and not os.path.isdir(new):
525 raise TraitError("notebook dir %r is not a directory" % new)
526 if not os.path.exists(new):
527 self.log.info("Creating notebook dir %s", new)
528 try:
529 os.mkdir(new)
530 except:
531 raise TraitError("Couldn't create notebook dir %r" % new)
532
533
511 def parse_command_line(self, argv=None):
534 def parse_command_line(self, argv=None):
512 super(NotebookApp, self).parse_command_line(argv)
535 super(NotebookApp, self).parse_command_line(argv)
513
536
514 if self.extra_args:
537 if self.extra_args:
515 arg0 = self.extra_args[0]
538 arg0 = self.extra_args[0]
516 f = os.path.abspath(arg0)
539 f = os.path.abspath(arg0)
517 self.argv.remove(arg0)
540 self.argv.remove(arg0)
518 if not os.path.exists(f):
541 if not os.path.exists(f):
519 self.log.critical("No such file or directory: %s", f)
542 self.log.critical("No such file or directory: %s", f)
520 self.exit(1)
543 self.exit(1)
521 if os.path.isdir(f):
544 if os.path.isdir(f):
522 self.config.FileNotebookManager.notebook_dir = f
545 self.notebook_dir = f
523 elif os.path.isfile(f):
546 elif os.path.isfile(f):
524 self.file_to_run = f
547 self.file_to_run = f
525
548
526 def init_kernel_argv(self):
549 def init_kernel_argv(self):
527 """construct the kernel arguments"""
550 """construct the kernel arguments"""
528 # Scrub frontend-specific flags
551 # Scrub frontend-specific flags
529 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
552 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
530 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
553 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
531 self.log.warn('\n '.join([
554 self.log.warn('\n '.join([
532 "Starting all kernels in pylab mode is not recommended,",
555 "Starting all kernels in pylab mode is not recommended,",
533 "and will be disabled in a future release.",
556 "and will be disabled in a future release.",
534 "Please use the %matplotlib magic to enable matplotlib instead.",
557 "Please use the %matplotlib magic to enable matplotlib instead.",
535 "pylab implies many imports, which can have confusing side effects",
558 "pylab implies many imports, which can have confusing side effects",
536 "and harm the reproducibility of your notebooks.",
559 "and harm the reproducibility of your notebooks.",
537 ]))
560 ]))
538 # Kernel should inherit default config file from frontend
561 # Kernel should inherit default config file from frontend
539 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
562 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
540 # Kernel should get *absolute* path to profile directory
563 # Kernel should get *absolute* path to profile directory
541 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
564 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
542
565
543 def init_configurables(self):
566 def init_configurables(self):
544 # force Session default to be secure
567 # force Session default to be secure
545 default_secure(self.config)
568 default_secure(self.config)
546 self.kernel_manager = MappingKernelManager(
569 self.kernel_manager = MappingKernelManager(
547 parent=self, log=self.log, kernel_argv=self.kernel_argv,
570 parent=self, log=self.log, kernel_argv=self.kernel_argv,
548 connection_dir = self.profile_dir.security_dir,
571 connection_dir = self.profile_dir.security_dir,
549 )
572 )
550 kls = import_item(self.notebook_manager_class)
573 kls = import_item(self.notebook_manager_class)
551 self.notebook_manager = kls(parent=self, log=self.log)
574 self.notebook_manager = kls(parent=self, log=self.log)
552 self.session_manager = SessionManager(parent=self, log=self.log)
575 self.session_manager = SessionManager(parent=self, log=self.log)
553 self.cluster_manager = ClusterManager(parent=self, log=self.log)
576 self.cluster_manager = ClusterManager(parent=self, log=self.log)
554 self.cluster_manager.update_profiles()
577 self.cluster_manager.update_profiles()
555
578
556 def init_logging(self):
579 def init_logging(self):
557 # This prevents double log messages because tornado use a root logger that
580 # 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
581 # 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.
582 # and all of its ancenstors until propagate is set to False.
560 self.log.propagate = False
583 self.log.propagate = False
561
584
562 # hook up tornado 3's loggers to our app handlers
585 # hook up tornado 3's loggers to our app handlers
563 for name in ('access', 'application', 'general'):
586 for name in ('access', 'application', 'general'):
564 logger = logging.getLogger('tornado.%s' % name)
587 logger = logging.getLogger('tornado.%s' % name)
565 logger.parent = self.log
588 logger.parent = self.log
566 logger.setLevel(self.log.level)
589 logger.setLevel(self.log.level)
567
590
568 def init_webapp(self):
591 def init_webapp(self):
569 """initialize tornado webapp and httpserver"""
592 """initialize tornado webapp and httpserver"""
570 self.web_app = NotebookWebApplication(
593 self.web_app = NotebookWebApplication(
571 self, self.kernel_manager, self.notebook_manager,
594 self, self.kernel_manager, self.notebook_manager,
572 self.cluster_manager, self.session_manager,
595 self.cluster_manager, self.session_manager,
573 self.log, self.base_url, self.webapp_settings
596 self.log, self.base_url, self.webapp_settings
574 )
597 )
575 if self.certfile:
598 if self.certfile:
576 ssl_options = dict(certfile=self.certfile)
599 ssl_options = dict(certfile=self.certfile)
577 if self.keyfile:
600 if self.keyfile:
578 ssl_options['keyfile'] = self.keyfile
601 ssl_options['keyfile'] = self.keyfile
579 else:
602 else:
580 ssl_options = None
603 ssl_options = None
581 self.web_app.password = self.password
604 self.web_app.password = self.password
582 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
605 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
583 xheaders=self.trust_xheaders)
606 xheaders=self.trust_xheaders)
584 if not self.ip:
607 if not self.ip:
585 warning = "WARNING: The notebook server is listening on all IP addresses"
608 warning = "WARNING: The notebook server is listening on all IP addresses"
586 if ssl_options is None:
609 if ssl_options is None:
587 self.log.critical(warning + " and not using encryption. This "
610 self.log.critical(warning + " and not using encryption. This "
588 "is not recommended.")
611 "is not recommended.")
589 if not self.password:
612 if not self.password:
590 self.log.critical(warning + " and not using authentication. "
613 self.log.critical(warning + " and not using authentication. "
591 "This is highly insecure and not recommended.")
614 "This is highly insecure and not recommended.")
592 success = None
615 success = None
593 for port in random_ports(self.port, self.port_retries+1):
616 for port in random_ports(self.port, self.port_retries+1):
594 try:
617 try:
595 self.http_server.listen(port, self.ip)
618 self.http_server.listen(port, self.ip)
596 except socket.error as e:
619 except socket.error as e:
597 if e.errno == errno.EADDRINUSE:
620 if e.errno == errno.EADDRINUSE:
598 self.log.info('The port %i is already in use, trying another random port.' % port)
621 self.log.info('The port %i is already in use, trying another random port.' % port)
599 continue
622 continue
600 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
623 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
601 self.log.warn("Permission to listen on port %i denied" % port)
624 self.log.warn("Permission to listen on port %i denied" % port)
602 continue
625 continue
603 else:
626 else:
604 raise
627 raise
605 else:
628 else:
606 self.port = port
629 self.port = port
607 success = True
630 success = True
608 break
631 break
609 if not success:
632 if not success:
610 self.log.critical('ERROR: the notebook server could not be started because '
633 self.log.critical('ERROR: the notebook server could not be started because '
611 'no available port could be found.')
634 'no available port could be found.')
612 self.exit(1)
635 self.exit(1)
613
636
614 @property
637 @property
615 def display_url(self):
638 def display_url(self):
616 ip = self.ip if self.ip else '[all ip addresses on your system]'
639 ip = self.ip if self.ip else '[all ip addresses on your system]'
617 return self._url(ip)
640 return self._url(ip)
618
641
619 @property
642 @property
620 def connection_url(self):
643 def connection_url(self):
621 ip = self.ip if self.ip else localhost()
644 ip = self.ip if self.ip else localhost()
622 return self._url(ip)
645 return self._url(ip)
623
646
624 def _url(self, ip):
647 def _url(self, ip):
625 proto = 'https' if self.certfile else 'http'
648 proto = 'https' if self.certfile else 'http'
626 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
649 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
627
650
628 def init_signal(self):
651 def init_signal(self):
629 if not sys.platform.startswith('win'):
652 if not sys.platform.startswith('win'):
630 signal.signal(signal.SIGINT, self._handle_sigint)
653 signal.signal(signal.SIGINT, self._handle_sigint)
631 signal.signal(signal.SIGTERM, self._signal_stop)
654 signal.signal(signal.SIGTERM, self._signal_stop)
632 if hasattr(signal, 'SIGUSR1'):
655 if hasattr(signal, 'SIGUSR1'):
633 # Windows doesn't support SIGUSR1
656 # Windows doesn't support SIGUSR1
634 signal.signal(signal.SIGUSR1, self._signal_info)
657 signal.signal(signal.SIGUSR1, self._signal_info)
635 if hasattr(signal, 'SIGINFO'):
658 if hasattr(signal, 'SIGINFO'):
636 # only on BSD-based systems
659 # only on BSD-based systems
637 signal.signal(signal.SIGINFO, self._signal_info)
660 signal.signal(signal.SIGINFO, self._signal_info)
638
661
639 def _handle_sigint(self, sig, frame):
662 def _handle_sigint(self, sig, frame):
640 """SIGINT handler spawns confirmation dialog"""
663 """SIGINT handler spawns confirmation dialog"""
641 # register more forceful signal handler for ^C^C case
664 # register more forceful signal handler for ^C^C case
642 signal.signal(signal.SIGINT, self._signal_stop)
665 signal.signal(signal.SIGINT, self._signal_stop)
643 # request confirmation dialog in bg thread, to avoid
666 # request confirmation dialog in bg thread, to avoid
644 # blocking the App
667 # blocking the App
645 thread = threading.Thread(target=self._confirm_exit)
668 thread = threading.Thread(target=self._confirm_exit)
646 thread.daemon = True
669 thread.daemon = True
647 thread.start()
670 thread.start()
648
671
649 def _restore_sigint_handler(self):
672 def _restore_sigint_handler(self):
650 """callback for restoring original SIGINT handler"""
673 """callback for restoring original SIGINT handler"""
651 signal.signal(signal.SIGINT, self._handle_sigint)
674 signal.signal(signal.SIGINT, self._handle_sigint)
652
675
653 def _confirm_exit(self):
676 def _confirm_exit(self):
654 """confirm shutdown on ^C
677 """confirm shutdown on ^C
655
678
656 A second ^C, or answering 'y' within 5s will cause shutdown,
679 A second ^C, or answering 'y' within 5s will cause shutdown,
657 otherwise original SIGINT handler will be restored.
680 otherwise original SIGINT handler will be restored.
658
681
659 This doesn't work on Windows.
682 This doesn't work on Windows.
660 """
683 """
661 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
684 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
662 time.sleep(0.1)
685 time.sleep(0.1)
663 info = self.log.info
686 info = self.log.info
664 info('interrupted')
687 info('interrupted')
665 print(self.notebook_info())
688 print(self.notebook_info())
666 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
689 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
667 sys.stdout.flush()
690 sys.stdout.flush()
668 r,w,x = select.select([sys.stdin], [], [], 5)
691 r,w,x = select.select([sys.stdin], [], [], 5)
669 if r:
692 if r:
670 line = sys.stdin.readline()
693 line = sys.stdin.readline()
671 if line.lower().startswith('y'):
694 if line.lower().startswith('y'):
672 self.log.critical("Shutdown confirmed")
695 self.log.critical("Shutdown confirmed")
673 ioloop.IOLoop.instance().stop()
696 ioloop.IOLoop.instance().stop()
674 return
697 return
675 else:
698 else:
676 print("No answer for 5s:", end=' ')
699 print("No answer for 5s:", end=' ')
677 print("resuming operation...")
700 print("resuming operation...")
678 # no answer, or answer is no:
701 # no answer, or answer is no:
679 # set it back to original SIGINT handler
702 # set it back to original SIGINT handler
680 # use IOLoop.add_callback because signal.signal must be called
703 # use IOLoop.add_callback because signal.signal must be called
681 # from main thread
704 # from main thread
682 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
705 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
683
706
684 def _signal_stop(self, sig, frame):
707 def _signal_stop(self, sig, frame):
685 self.log.critical("received signal %s, stopping", sig)
708 self.log.critical("received signal %s, stopping", sig)
686 ioloop.IOLoop.instance().stop()
709 ioloop.IOLoop.instance().stop()
687
710
688 def _signal_info(self, sig, frame):
711 def _signal_info(self, sig, frame):
689 print(self.notebook_info())
712 print(self.notebook_info())
690
713
691 def init_components(self):
714 def init_components(self):
692 """Check the components submodule, and warn if it's unclean"""
715 """Check the components submodule, and warn if it's unclean"""
693 status = submodule.check_submodule_status()
716 status = submodule.check_submodule_status()
694 if status == 'missing':
717 if status == 'missing':
695 self.log.warn("components submodule missing, running `git submodule update`")
718 self.log.warn("components submodule missing, running `git submodule update`")
696 submodule.update_submodules(submodule.ipython_parent())
719 submodule.update_submodules(submodule.ipython_parent())
697 elif status == 'unclean':
720 elif status == 'unclean':
698 self.log.warn("components submodule unclean, you may see 404s on static/components")
721 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")
722 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
700
723
701 @catch_config_error
724 @catch_config_error
702 def initialize(self, argv=None):
725 def initialize(self, argv=None):
703 super(NotebookApp, self).initialize(argv)
726 super(NotebookApp, self).initialize(argv)
704 self.init_logging()
727 self.init_logging()
705 self.init_kernel_argv()
728 self.init_kernel_argv()
706 self.init_configurables()
729 self.init_configurables()
707 self.init_components()
730 self.init_components()
708 self.init_webapp()
731 self.init_webapp()
709 self.init_signal()
732 self.init_signal()
710
733
711 def cleanup_kernels(self):
734 def cleanup_kernels(self):
712 """Shutdown all kernels.
735 """Shutdown all kernels.
713
736
714 The kernels will shutdown themselves when this process no longer exists,
737 The kernels will shutdown themselves when this process no longer exists,
715 but explicit shutdown allows the KernelManagers to cleanup the connection files.
738 but explicit shutdown allows the KernelManagers to cleanup the connection files.
716 """
739 """
717 self.log.info('Shutting down kernels')
740 self.log.info('Shutting down kernels')
718 self.kernel_manager.shutdown_all()
741 self.kernel_manager.shutdown_all()
719
742
720 def notebook_info(self):
743 def notebook_info(self):
721 "Return the current working directory and the server url information"
744 "Return the current working directory and the server url information"
722 info = self.notebook_manager.info_string() + "\n"
745 info = self.notebook_manager.info_string() + "\n"
723 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
746 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
724 return info + "The IPython Notebook is running at: %s" % self.display_url
747 return info + "The IPython Notebook is running at: %s" % self.display_url
725
748
726 def server_info(self):
749 def server_info(self):
727 """Return a JSONable dict of information about this server."""
750 """Return a JSONable dict of information about this server."""
728 return {'url': self.connection_url,
751 return {'url': self.connection_url,
729 'hostname': self.ip if self.ip else 'localhost',
752 'hostname': self.ip if self.ip else 'localhost',
730 'port': self.port,
753 'port': self.port,
731 'secure': bool(self.certfile),
754 'secure': bool(self.certfile),
732 'base_url': self.base_url,
755 'base_url': self.base_url,
733 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
756 'notebook_dir': os.path.abspath(self.notebook_dir),
734 }
757 }
735
758
736 def write_server_info_file(self):
759 def write_server_info_file(self):
737 """Write the result of server_info() to the JSON file info_file."""
760 """Write the result of server_info() to the JSON file info_file."""
738 with open(self.info_file, 'w') as f:
761 with open(self.info_file, 'w') as f:
739 json.dump(self.server_info(), f, indent=2)
762 json.dump(self.server_info(), f, indent=2)
740
763
741 def remove_server_info_file(self):
764 def remove_server_info_file(self):
742 """Remove the nbserver-<pid>.json file created for this server.
765 """Remove the nbserver-<pid>.json file created for this server.
743
766
744 Ignores the error raised when the file has already been removed.
767 Ignores the error raised when the file has already been removed.
745 """
768 """
746 try:
769 try:
747 os.unlink(self.info_file)
770 os.unlink(self.info_file)
748 except OSError as e:
771 except OSError as e:
749 if e.errno != errno.ENOENT:
772 if e.errno != errno.ENOENT:
750 raise
773 raise
751
774
752 def start(self):
775 def start(self):
753 """ Start the IPython Notebook server app, after initialization
776 """ Start the IPython Notebook server app, after initialization
754
777
755 This method takes no arguments so all configuration and initialization
778 This method takes no arguments so all configuration and initialization
756 must be done prior to calling this method."""
779 must be done prior to calling this method."""
757 if self.subapp is not None:
780 if self.subapp is not None:
758 return self.subapp.start()
781 return self.subapp.start()
759
782
760 info = self.log.info
783 info = self.log.info
761 for line in self.notebook_info().split("\n"):
784 for line in self.notebook_info().split("\n"):
762 info(line)
785 info(line)
763 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
786 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
764
787
765 self.write_server_info_file()
788 self.write_server_info_file()
766
789
767 if self.open_browser or self.file_to_run:
790 if self.open_browser or self.file_to_run:
768 try:
791 try:
769 browser = webbrowser.get(self.browser or None)
792 browser = webbrowser.get(self.browser or None)
770 except webbrowser.Error as e:
793 except webbrowser.Error as e:
771 self.log.warn('No web browser found: %s.' % e)
794 self.log.warn('No web browser found: %s.' % e)
772 browser = None
795 browser = None
773
796
774 f = self.file_to_run
797 f = self.file_to_run
775 if f:
798 if f:
776 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
799 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
777 if f.startswith(nbdir):
800 if f.startswith(nbdir):
778 f = f[len(nbdir):]
801 f = f[len(nbdir):]
779 else:
802 else:
780 self.log.warn(
803 self.log.warn(
781 "Probably won't be able to open notebook %s "
804 "Probably won't be able to open notebook %s "
782 "because it is not in notebook_dir %s",
805 "because it is not in notebook_dir %s",
783 f, nbdir,
806 f, nbdir,
784 )
807 )
785
808
786 if os.path.isfile(self.file_to_run):
809 if os.path.isfile(self.file_to_run):
787 url = url_path_join('notebooks', f)
810 url = url_path_join('notebooks', f)
788 else:
811 else:
789 url = url_path_join('tree', f)
812 url = url_path_join('tree', f)
790 if browser:
813 if browser:
791 b = lambda : browser.open("%s%s" % (self.connection_url, url),
814 b = lambda : browser.open("%s%s" % (self.connection_url, url),
792 new=2)
815 new=2)
793 threading.Thread(target=b).start()
816 threading.Thread(target=b).start()
794 try:
817 try:
795 ioloop.IOLoop.instance().start()
818 ioloop.IOLoop.instance().start()
796 except KeyboardInterrupt:
819 except KeyboardInterrupt:
797 info("Interrupted...")
820 info("Interrupted...")
798 finally:
821 finally:
799 self.cleanup_kernels()
822 self.cleanup_kernels()
800 self.remove_server_info_file()
823 self.remove_server_info_file()
801
824
802
825
803 def list_running_servers(profile='default'):
826 def list_running_servers(profile='default'):
804 """Iterate over the server info files of running notebook servers.
827 """Iterate over the server info files of running notebook servers.
805
828
806 Given a profile name, find nbserver-* files in the security directory of
829 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
830 that profile, and yield dicts of their information, each one pertaining to
808 a currently running notebook server instance.
831 a currently running notebook server instance.
809 """
832 """
810 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
833 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
811 for file in os.listdir(pd.security_dir):
834 for file in os.listdir(pd.security_dir):
812 if file.startswith('nbserver-'):
835 if file.startswith('nbserver-'):
813 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
836 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
814 yield json.load(f)
837 yield json.load(f)
815
838
816 #-----------------------------------------------------------------------------
839 #-----------------------------------------------------------------------------
817 # Main entry point
840 # Main entry point
818 #-----------------------------------------------------------------------------
841 #-----------------------------------------------------------------------------
819
842
820 launch_new_instance = NotebookApp.launch_instance
843 launch_new_instance = NotebookApp.launch_instance
821
844
@@ -1,98 +1,140 b''
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)
38
43
44 root_dir = Unicode(getcwd(), config=True)
45 def _root_dir_default(self):
46 from IPython.html.notebookapp import NotebookApp
47 if NotebookApp.initialized():
48 try:
49 app = NotebookApp.instance()
50 except Exception:
51 # can raise MultipleInstanceError, ignore
52 pass
53 else:
54 return app.notebook_dir
55 return app.notebook_dir
56 return getcwd()
57
58 def _root_dir_changed(self, name, old, new):
59 """Do a bit of validation of the root dir."""
60 if not os.path.isabs(new):
61 # If we receive a non-absolute path, make it absolute.
62 self.root_dir = os.path.abspath(new)
63 return
64 if not os.path.exists(new) or not os.path.isdir(new):
65 raise TraitError("kernel root dir %r is not a directory" % new)
66
39 #-------------------------------------------------------------------------
67 #-------------------------------------------------------------------------
40 # Methods for managing kernels and sessions
68 # Methods for managing kernels and sessions
41 #-------------------------------------------------------------------------
69 #-------------------------------------------------------------------------
42
70
43 def _handle_kernel_died(self, kernel_id):
71 def _handle_kernel_died(self, kernel_id):
44 """notice that a kernel died"""
72 """notice that a kernel died"""
45 self.log.warn("Kernel %s died, removing from map.", kernel_id)
73 self.log.warn("Kernel %s died, removing from map.", kernel_id)
46 self.remove_kernel(kernel_id)
74 self.remove_kernel(kernel_id)
47
75
48 def start_kernel(self, kernel_id=None, **kwargs):
76 def cwd_for_path(self, path):
77 """Turn API path into absolute OS path."""
78 os_path = to_os_path(path, self.root_dir)
79 # in the case of notebooks and kernels not being on the same filesystem,
80 # walk up to root_dir if the paths don't exist
81 while not os.path.exists(os_path) and os_path != self.root_dir:
82 os_path = os.path.dirname(os_path)
83 return os_path
84
85 def start_kernel(self, kernel_id=None, path=None, **kwargs):
49 """Start a kernel for a session an return its kernel_id.
86 """Start a kernel for a session an return its kernel_id.
50
87
51 Parameters
88 Parameters
52 ----------
89 ----------
53 kernel_id : uuid
90 kernel_id : uuid
54 The uuid to associate the new kernel with. If this
91 The uuid to associate the new kernel with. If this
55 is not None, this kernel will be persistent whenever it is
92 is not None, this kernel will be persistent whenever it is
56 requested.
93 requested.
94 path : API path
95 The API path (unicode, '/' delimited) for the cwd.
96 Will be transformed to an OS path relative to root_dir.
57 """
97 """
58 if kernel_id is None:
98 if kernel_id is None:
59 kwargs['extra_arguments'] = self.kernel_argv
99 kwargs['extra_arguments'] = self.kernel_argv
100 if path is not None:
101 kwargs['cwd'] = self.cwd_for_path(path)
60 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
102 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
61 self.log.info("Kernel started: %s" % kernel_id)
103 self.log.info("Kernel started: %s" % kernel_id)
62 self.log.debug("Kernel args: %r" % kwargs)
104 self.log.debug("Kernel args: %r" % kwargs)
63 # register callback for failed auto-restart
105 # register callback for failed auto-restart
64 self.add_restart_callback(kernel_id,
106 self.add_restart_callback(kernel_id,
65 lambda : self._handle_kernel_died(kernel_id),
107 lambda : self._handle_kernel_died(kernel_id),
66 'dead',
108 'dead',
67 )
109 )
68 else:
110 else:
69 self._check_kernel_id(kernel_id)
111 self._check_kernel_id(kernel_id)
70 self.log.info("Using existing kernel: %s" % kernel_id)
112 self.log.info("Using existing kernel: %s" % kernel_id)
71 return kernel_id
113 return kernel_id
72
114
73 def shutdown_kernel(self, kernel_id, now=False):
115 def shutdown_kernel(self, kernel_id, now=False):
74 """Shutdown a kernel by kernel_id"""
116 """Shutdown a kernel by kernel_id"""
75 self._check_kernel_id(kernel_id)
117 self._check_kernel_id(kernel_id)
76 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
118 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
77
119
78 def kernel_model(self, kernel_id):
120 def kernel_model(self, kernel_id):
79 """Return a dictionary of kernel information described in the
121 """Return a dictionary of kernel information described in the
80 JSON standard model."""
122 JSON standard model."""
81 self._check_kernel_id(kernel_id)
123 self._check_kernel_id(kernel_id)
82 model = {"id":kernel_id}
124 model = {"id":kernel_id}
83 return model
125 return model
84
126
85 def list_kernels(self):
127 def list_kernels(self):
86 """Returns a list of kernel_id's of kernels running."""
128 """Returns a list of kernel_id's of kernels running."""
87 kernels = []
129 kernels = []
88 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
130 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
89 for kernel_id in kernel_ids:
131 for kernel_id in kernel_ids:
90 model = self.kernel_model(kernel_id)
132 model = self.kernel_model(kernel_id)
91 kernels.append(model)
133 kernels.append(model)
92 return kernels
134 return kernels
93
135
94 # override _check_kernel_id to raise 404 instead of KeyError
136 # override _check_kernel_id to raise 404 instead of KeyError
95 def _check_kernel_id(self, kernel_id):
137 def _check_kernel_id(self, kernel_id):
96 """Check a that a kernel_id exists and raise 404 if not."""
138 """Check a that a kernel_id exists and raise 404 if not."""
97 if kernel_id not in self:
139 if kernel_id not in self:
98 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
140 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
@@ -1,467 +1,485 b''
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)
50 def _notebook_dir_default(self):
51 from IPython.html.notebookapp import NotebookApp
52 if NotebookApp.initialized():
53 try:
54 app = NotebookApp.instance()
55 except Exception:
56 # can raise MultipleInstanceError, ignore
57 pass
58 else:
59 return app.notebook_dir
60 return getcwd()
61
62 def _notebook_dir_changed(self, name, old, new):
63 """Do a bit of validation of the notebook dir."""
64 if not os.path.isabs(new):
65 # If we receive a non-absolute path, make it absolute.
66 self.notebook_dir = os.path.abspath(new)
67 return
68 if not os.path.exists(new) or not os.path.isdir(new):
69 raise TraitError("notebook dir %r is not a directory" % new)
49
70
50 checkpoint_dir = Unicode(config=True,
71 checkpoint_dir = Unicode(config=True,
51 help="""The location in which to keep notebook checkpoints
72 help="""The location in which to keep notebook checkpoints
52
73
53 By default, it is notebook-dir/.ipynb_checkpoints
74 By default, it is notebook-dir/.ipynb_checkpoints
54 """
75 """
55 )
76 )
56 def _checkpoint_dir_default(self):
77 def _checkpoint_dir_default(self):
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
78 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58
79
59 def _checkpoint_dir_changed(self, name, old, new):
80 def _checkpoint_dir_changed(self, name, old, new):
60 """do a bit of validation of the checkpoint dir"""
81 """do a bit of validation of the checkpoint dir"""
61 if not os.path.isabs(new):
82 if not os.path.isabs(new):
62 # If we receive a non-absolute path, make it absolute.
83 # If we receive a non-absolute path, make it absolute.
63 abs_new = os.path.abspath(new)
84 abs_new = os.path.abspath(new)
64 self.checkpoint_dir = abs_new
85 self.checkpoint_dir = abs_new
65 return
86 return
66 if os.path.exists(new) and not os.path.isdir(new):
87 if os.path.exists(new) and not os.path.isdir(new):
67 raise TraitError("checkpoint dir %r is not a directory" % new)
88 raise TraitError("checkpoint dir %r is not a directory" % new)
68 if not os.path.exists(new):
89 if not os.path.exists(new):
69 self.log.info("Creating checkpoint dir %s", new)
90 self.log.info("Creating checkpoint dir %s", new)
70 try:
91 try:
71 os.mkdir(new)
92 os.mkdir(new)
72 except:
93 except:
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
94 raise TraitError("Couldn't create checkpoint dir %r" % new)
74
95
75 def get_notebook_names(self, path=''):
96 def get_notebook_names(self, path=''):
76 """List all notebook names in the notebook dir and path."""
97 """List all notebook names in the notebook dir and path."""
77 path = path.strip('/')
98 path = path.strip('/')
78 if not os.path.isdir(self.get_os_path(path=path)):
99 if not os.path.isdir(self._get_os_path(path=path)):
79 raise web.HTTPError(404, 'Directory not found: ' + path)
100 raise web.HTTPError(404, 'Directory not found: ' + path)
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
101 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
81 names = [os.path.basename(name)
102 names = [os.path.basename(name)
82 for name in names]
103 for name in names]
83 return names
104 return names
84
105
85 def path_exists(self, path):
106 def path_exists(self, path):
86 """Does the API-style path (directory) actually exist?
107 """Does the API-style path (directory) actually exist?
87
108
88 Parameters
109 Parameters
89 ----------
110 ----------
90 path : string
111 path : string
91 The path to check. This is an API path (`/` separated,
112 The path to check. This is an API path (`/` separated,
92 relative to base notebook-dir).
113 relative to base notebook-dir).
93
114
94 Returns
115 Returns
95 -------
116 -------
96 exists : bool
117 exists : bool
97 Whether the path is indeed a directory.
118 Whether the path is indeed a directory.
98 """
119 """
99 path = path.strip('/')
120 path = path.strip('/')
100 os_path = self.get_os_path(path=path)
121 os_path = self._get_os_path(path=path)
101 return os.path.isdir(os_path)
122 return os.path.isdir(os_path)
102
123
103 def is_hidden(self, path):
124 def is_hidden(self, path):
104 """Does the API style path correspond to a hidden directory or file?
125 """Does the API style path correspond to a hidden directory or file?
105
126
106 Parameters
127 Parameters
107 ----------
128 ----------
108 path : string
129 path : string
109 The path to check. This is an API path (`/` separated,
130 The path to check. This is an API path (`/` separated,
110 relative to base notebook-dir).
131 relative to base notebook-dir).
111
132
112 Returns
133 Returns
113 -------
134 -------
114 exists : bool
135 exists : bool
115 Whether the path is hidden.
136 Whether the path is hidden.
116
137
117 """
138 """
118 path = path.strip('/')
139 path = path.strip('/')
119 os_path = self.get_os_path(path=path)
140 os_path = self._get_os_path(path=path)
120 return is_hidden(os_path, self.notebook_dir)
141 return is_hidden(os_path, self.notebook_dir)
121
142
122 def get_os_path(self, name=None, path=''):
143 def _get_os_path(self, name=None, path=''):
123 """Given a notebook name and a URL path, return its file system
144 """Given a notebook name and a URL path, return its file system
124 path.
145 path.
125
146
126 Parameters
147 Parameters
127 ----------
148 ----------
128 name : string
149 name : string
129 The name of a notebook file with the .ipynb extension
150 The name of a notebook file with the .ipynb extension
130 path : string
151 path : string
131 The relative URL path (with '/' as separator) to the named
152 The relative URL path (with '/' as separator) to the named
132 notebook.
153 notebook.
133
154
134 Returns
155 Returns
135 -------
156 -------
136 path : string
157 path : string
137 A file system path that combines notebook_dir (location where
158 A file system path that combines notebook_dir (location where
138 server started), the relative path, and the filename with the
159 server started), the relative path, and the filename with the
139 current operating system's url.
160 current operating system's url.
140 """
161 """
141 parts = path.strip('/').split('/')
142 parts = [p for p in parts if p != ''] # remove duplicate splits
143 if name is not None:
162 if name is not None:
144 parts.append(name)
163 path = path + '/' + name
145 path = os.path.join(self.notebook_dir, *parts)
164 return to_os_path(path, self.notebook_dir)
146 return path
147
165
148 def notebook_exists(self, name, path=''):
166 def notebook_exists(self, name, path=''):
149 """Returns a True if the notebook exists. Else, returns False.
167 """Returns a True if the notebook exists. Else, returns False.
150
168
151 Parameters
169 Parameters
152 ----------
170 ----------
153 name : string
171 name : string
154 The name of the notebook you are checking.
172 The name of the notebook you are checking.
155 path : string
173 path : string
156 The relative path to the notebook (with '/' as separator)
174 The relative path to the notebook (with '/' as separator)
157
175
158 Returns
176 Returns
159 -------
177 -------
160 bool
178 bool
161 """
179 """
162 path = path.strip('/')
180 path = path.strip('/')
163 nbpath = self.get_os_path(name, path=path)
181 nbpath = self._get_os_path(name, path=path)
164 return os.path.isfile(nbpath)
182 return os.path.isfile(nbpath)
165
183
166 # TODO: Remove this after we create the contents web service and directories are
184 # TODO: Remove this after we create the contents web service and directories are
167 # no longer listed by the notebook web service.
185 # no longer listed by the notebook web service.
168 def list_dirs(self, path):
186 def list_dirs(self, path):
169 """List the directories for a given API style path."""
187 """List the directories for a given API style path."""
170 path = path.strip('/')
188 path = path.strip('/')
171 os_path = self.get_os_path('', path)
189 os_path = self._get_os_path('', path)
172 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
190 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)
191 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
174 dir_names = os.listdir(os_path)
192 dir_names = os.listdir(os_path)
175 dirs = []
193 dirs = []
176 for name in dir_names:
194 for name in dir_names:
177 os_path = self.get_os_path(name, path)
195 os_path = self._get_os_path(name, path)
178 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
196 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
179 try:
197 try:
180 model = self.get_dir_model(name, path)
198 model = self.get_dir_model(name, path)
181 except IOError:
199 except IOError:
182 pass
200 pass
183 dirs.append(model)
201 dirs.append(model)
184 dirs = sorted(dirs, key=lambda item: item['name'])
202 dirs = sorted(dirs, key=lambda item: item['name'])
185 return dirs
203 return dirs
186
204
187 # TODO: Remove this after we create the contents web service and directories are
205 # TODO: Remove this after we create the contents web service and directories are
188 # no longer listed by the notebook web service.
206 # no longer listed by the notebook web service.
189 def get_dir_model(self, name, path=''):
207 def get_dir_model(self, name, path=''):
190 """Get the directory model given a directory name and its API style path"""
208 """Get the directory model given a directory name and its API style path"""
191 path = path.strip('/')
209 path = path.strip('/')
192 os_path = self.get_os_path(name, path)
210 os_path = self._get_os_path(name, path)
193 if not os.path.isdir(os_path):
211 if not os.path.isdir(os_path):
194 raise IOError('directory does not exist: %r' % os_path)
212 raise IOError('directory does not exist: %r' % os_path)
195 info = os.stat(os_path)
213 info = os.stat(os_path)
196 last_modified = tz.utcfromtimestamp(info.st_mtime)
214 last_modified = tz.utcfromtimestamp(info.st_mtime)
197 created = tz.utcfromtimestamp(info.st_ctime)
215 created = tz.utcfromtimestamp(info.st_ctime)
198 # Create the notebook model.
216 # Create the notebook model.
199 model ={}
217 model ={}
200 model['name'] = name
218 model['name'] = name
201 model['path'] = path
219 model['path'] = path
202 model['last_modified'] = last_modified
220 model['last_modified'] = last_modified
203 model['created'] = created
221 model['created'] = created
204 model['type'] = 'directory'
222 model['type'] = 'directory'
205 return model
223 return model
206
224
207 def list_notebooks(self, path):
225 def list_notebooks(self, path):
208 """Returns a list of dictionaries that are the standard model
226 """Returns a list of dictionaries that are the standard model
209 for all notebooks in the relative 'path'.
227 for all notebooks in the relative 'path'.
210
228
211 Parameters
229 Parameters
212 ----------
230 ----------
213 path : str
231 path : str
214 the URL path that describes the relative path for the
232 the URL path that describes the relative path for the
215 listed notebooks
233 listed notebooks
216
234
217 Returns
235 Returns
218 -------
236 -------
219 notebooks : list of dicts
237 notebooks : list of dicts
220 a list of the notebook models without 'content'
238 a list of the notebook models without 'content'
221 """
239 """
222 path = path.strip('/')
240 path = path.strip('/')
223 notebook_names = self.get_notebook_names(path)
241 notebook_names = self.get_notebook_names(path)
224 notebooks = [self.get_notebook(name, path, content=False) for name in notebook_names]
242 notebooks = [self.get_notebook(name, path, content=False) for name in notebook_names]
225 notebooks = sorted(notebooks, key=lambda item: item['name'])
243 notebooks = sorted(notebooks, key=lambda item: item['name'])
226 return notebooks
244 return notebooks
227
245
228 def get_notebook(self, name, path='', content=True):
246 def get_notebook(self, name, path='', content=True):
229 """ Takes a path and name for a notebook and returns its model
247 """ Takes a path and name for a notebook and returns its model
230
248
231 Parameters
249 Parameters
232 ----------
250 ----------
233 name : str
251 name : str
234 the name of the notebook
252 the name of the notebook
235 path : str
253 path : str
236 the URL path that describes the relative path for
254 the URL path that describes the relative path for
237 the notebook
255 the notebook
238
256
239 Returns
257 Returns
240 -------
258 -------
241 model : dict
259 model : dict
242 the notebook model. If contents=True, returns the 'contents'
260 the notebook model. If contents=True, returns the 'contents'
243 dict in the model as well.
261 dict in the model as well.
244 """
262 """
245 path = path.strip('/')
263 path = path.strip('/')
246 if not self.notebook_exists(name=name, path=path):
264 if not self.notebook_exists(name=name, path=path):
247 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
265 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
248 os_path = self.get_os_path(name, path)
266 os_path = self._get_os_path(name, path)
249 info = os.stat(os_path)
267 info = os.stat(os_path)
250 last_modified = tz.utcfromtimestamp(info.st_mtime)
268 last_modified = tz.utcfromtimestamp(info.st_mtime)
251 created = tz.utcfromtimestamp(info.st_ctime)
269 created = tz.utcfromtimestamp(info.st_ctime)
252 # Create the notebook model.
270 # Create the notebook model.
253 model ={}
271 model ={}
254 model['name'] = name
272 model['name'] = name
255 model['path'] = path
273 model['path'] = path
256 model['last_modified'] = last_modified
274 model['last_modified'] = last_modified
257 model['created'] = created
275 model['created'] = created
258 model['type'] = 'notebook'
276 model['type'] = 'notebook'
259 if content:
277 if content:
260 with io.open(os_path, 'r', encoding='utf-8') as f:
278 with io.open(os_path, 'r', encoding='utf-8') as f:
261 try:
279 try:
262 nb = current.read(f, u'json')
280 nb = current.read(f, u'json')
263 except Exception as e:
281 except Exception as e:
264 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
282 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
265 self.mark_trusted_cells(nb, path, name)
283 self.mark_trusted_cells(nb, path, name)
266 model['content'] = nb
284 model['content'] = nb
267 return model
285 return model
268
286
269 def save_notebook(self, model, name='', path=''):
287 def save_notebook(self, model, name='', path=''):
270 """Save the notebook model and return the model with no content."""
288 """Save the notebook model and return the model with no content."""
271 path = path.strip('/')
289 path = path.strip('/')
272
290
273 if 'content' not in model:
291 if 'content' not in model:
274 raise web.HTTPError(400, u'No notebook JSON data provided')
292 raise web.HTTPError(400, u'No notebook JSON data provided')
275
293
276 # One checkpoint should always exist
294 # One checkpoint should always exist
277 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
295 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
278 self.create_checkpoint(name, path)
296 self.create_checkpoint(name, path)
279
297
280 new_path = model.get('path', path).strip('/')
298 new_path = model.get('path', path).strip('/')
281 new_name = model.get('name', name)
299 new_name = model.get('name', name)
282
300
283 if path != new_path or name != new_name:
301 if path != new_path or name != new_name:
284 self.rename_notebook(name, path, new_name, new_path)
302 self.rename_notebook(name, path, new_name, new_path)
285
303
286 # Save the notebook file
304 # Save the notebook file
287 os_path = self.get_os_path(new_name, new_path)
305 os_path = self._get_os_path(new_name, new_path)
288 nb = current.to_notebook_json(model['content'])
306 nb = current.to_notebook_json(model['content'])
289
307
290 self.check_and_sign(nb, new_path, new_name)
308 self.check_and_sign(nb, new_path, new_name)
291
309
292 if 'name' in nb['metadata']:
310 if 'name' in nb['metadata']:
293 nb['metadata']['name'] = u''
311 nb['metadata']['name'] = u''
294 try:
312 try:
295 self.log.debug("Autosaving notebook %s", os_path)
313 self.log.debug("Autosaving notebook %s", os_path)
296 with io.open(os_path, 'w', encoding='utf-8') as f:
314 with io.open(os_path, 'w', encoding='utf-8') as f:
297 current.write(nb, f, u'json')
315 current.write(nb, f, u'json')
298 except Exception as e:
316 except Exception as e:
299 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
317 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
300
318
301 # Save .py script as well
319 # Save .py script as well
302 if self.save_script:
320 if self.save_script:
303 py_path = os.path.splitext(os_path)[0] + '.py'
321 py_path = os.path.splitext(os_path)[0] + '.py'
304 self.log.debug("Writing script %s", py_path)
322 self.log.debug("Writing script %s", py_path)
305 try:
323 try:
306 with io.open(py_path, 'w', encoding='utf-8') as f:
324 with io.open(py_path, 'w', encoding='utf-8') as f:
307 current.write(nb, f, u'py')
325 current.write(nb, f, u'py')
308 except Exception as e:
326 except Exception as e:
309 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
327 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
310
328
311 model = self.get_notebook(new_name, new_path, content=False)
329 model = self.get_notebook(new_name, new_path, content=False)
312 return model
330 return model
313
331
314 def update_notebook(self, model, name, path=''):
332 def update_notebook(self, model, name, path=''):
315 """Update the notebook's path and/or name"""
333 """Update the notebook's path and/or name"""
316 path = path.strip('/')
334 path = path.strip('/')
317 new_name = model.get('name', name)
335 new_name = model.get('name', name)
318 new_path = model.get('path', path).strip('/')
336 new_path = model.get('path', path).strip('/')
319 if path != new_path or name != new_name:
337 if path != new_path or name != new_name:
320 self.rename_notebook(name, path, new_name, new_path)
338 self.rename_notebook(name, path, new_name, new_path)
321 model = self.get_notebook(new_name, new_path, content=False)
339 model = self.get_notebook(new_name, new_path, content=False)
322 return model
340 return model
323
341
324 def delete_notebook(self, name, path=''):
342 def delete_notebook(self, name, path=''):
325 """Delete notebook by name and path."""
343 """Delete notebook by name and path."""
326 path = path.strip('/')
344 path = path.strip('/')
327 os_path = self.get_os_path(name, path)
345 os_path = self._get_os_path(name, path)
328 if not os.path.isfile(os_path):
346 if not os.path.isfile(os_path):
329 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
347 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
330
348
331 # clear checkpoints
349 # clear checkpoints
332 for checkpoint in self.list_checkpoints(name, path):
350 for checkpoint in self.list_checkpoints(name, path):
333 checkpoint_id = checkpoint['id']
351 checkpoint_id = checkpoint['id']
334 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
352 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
335 if os.path.isfile(cp_path):
353 if os.path.isfile(cp_path):
336 self.log.debug("Unlinking checkpoint %s", cp_path)
354 self.log.debug("Unlinking checkpoint %s", cp_path)
337 os.unlink(cp_path)
355 os.unlink(cp_path)
338
356
339 self.log.debug("Unlinking notebook %s", os_path)
357 self.log.debug("Unlinking notebook %s", os_path)
340 os.unlink(os_path)
358 os.unlink(os_path)
341
359
342 def rename_notebook(self, old_name, old_path, new_name, new_path):
360 def rename_notebook(self, old_name, old_path, new_name, new_path):
343 """Rename a notebook."""
361 """Rename a notebook."""
344 old_path = old_path.strip('/')
362 old_path = old_path.strip('/')
345 new_path = new_path.strip('/')
363 new_path = new_path.strip('/')
346 if new_name == old_name and new_path == old_path:
364 if new_name == old_name and new_path == old_path:
347 return
365 return
348
366
349 new_os_path = self.get_os_path(new_name, new_path)
367 new_os_path = self._get_os_path(new_name, new_path)
350 old_os_path = self.get_os_path(old_name, old_path)
368 old_os_path = self._get_os_path(old_name, old_path)
351
369
352 # Should we proceed with the move?
370 # Should we proceed with the move?
353 if os.path.isfile(new_os_path):
371 if os.path.isfile(new_os_path):
354 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
372 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
355 if self.save_script:
373 if self.save_script:
356 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
374 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
357 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
375 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
358 if os.path.isfile(new_py_path):
376 if os.path.isfile(new_py_path):
359 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
377 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
360
378
361 # Move the notebook file
379 # Move the notebook file
362 try:
380 try:
363 os.rename(old_os_path, new_os_path)
381 os.rename(old_os_path, new_os_path)
364 except Exception as e:
382 except Exception as e:
365 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
383 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
366
384
367 # Move the checkpoints
385 # Move the checkpoints
368 old_checkpoints = self.list_checkpoints(old_name, old_path)
386 old_checkpoints = self.list_checkpoints(old_name, old_path)
369 for cp in old_checkpoints:
387 for cp in old_checkpoints:
370 checkpoint_id = cp['id']
388 checkpoint_id = cp['id']
371 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
389 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)
390 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
373 if os.path.isfile(old_cp_path):
391 if os.path.isfile(old_cp_path):
374 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
392 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
375 os.rename(old_cp_path, new_cp_path)
393 os.rename(old_cp_path, new_cp_path)
376
394
377 # Move the .py script
395 # Move the .py script
378 if self.save_script:
396 if self.save_script:
379 os.rename(old_py_path, new_py_path)
397 os.rename(old_py_path, new_py_path)
380
398
381 # Checkpoint-related utilities
399 # Checkpoint-related utilities
382
400
383 def get_checkpoint_path(self, checkpoint_id, name, path=''):
401 def get_checkpoint_path(self, checkpoint_id, name, path=''):
384 """find the path to a checkpoint"""
402 """find the path to a checkpoint"""
385 path = path.strip('/')
403 path = path.strip('/')
386 basename, _ = os.path.splitext(name)
404 basename, _ = os.path.splitext(name)
387 filename = u"{name}-{checkpoint_id}{ext}".format(
405 filename = u"{name}-{checkpoint_id}{ext}".format(
388 name=basename,
406 name=basename,
389 checkpoint_id=checkpoint_id,
407 checkpoint_id=checkpoint_id,
390 ext=self.filename_ext,
408 ext=self.filename_ext,
391 )
409 )
392 cp_path = os.path.join(path, self.checkpoint_dir, filename)
410 cp_path = os.path.join(path, self.checkpoint_dir, filename)
393 return cp_path
411 return cp_path
394
412
395 def get_checkpoint_model(self, checkpoint_id, name, path=''):
413 def get_checkpoint_model(self, checkpoint_id, name, path=''):
396 """construct the info dict for a given checkpoint"""
414 """construct the info dict for a given checkpoint"""
397 path = path.strip('/')
415 path = path.strip('/')
398 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
416 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
399 stats = os.stat(cp_path)
417 stats = os.stat(cp_path)
400 last_modified = tz.utcfromtimestamp(stats.st_mtime)
418 last_modified = tz.utcfromtimestamp(stats.st_mtime)
401 info = dict(
419 info = dict(
402 id = checkpoint_id,
420 id = checkpoint_id,
403 last_modified = last_modified,
421 last_modified = last_modified,
404 )
422 )
405 return info
423 return info
406
424
407 # public checkpoint API
425 # public checkpoint API
408
426
409 def create_checkpoint(self, name, path=''):
427 def create_checkpoint(self, name, path=''):
410 """Create a checkpoint from the current state of a notebook"""
428 """Create a checkpoint from the current state of a notebook"""
411 path = path.strip('/')
429 path = path.strip('/')
412 nb_path = self.get_os_path(name, path)
430 nb_path = self._get_os_path(name, path)
413 # only the one checkpoint ID:
431 # only the one checkpoint ID:
414 checkpoint_id = u"checkpoint"
432 checkpoint_id = u"checkpoint"
415 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
433 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
416 self.log.debug("creating checkpoint for notebook %s", name)
434 self.log.debug("creating checkpoint for notebook %s", name)
417 if not os.path.exists(self.checkpoint_dir):
435 if not os.path.exists(self.checkpoint_dir):
418 os.mkdir(self.checkpoint_dir)
436 os.mkdir(self.checkpoint_dir)
419 shutil.copy2(nb_path, cp_path)
437 shutil.copy2(nb_path, cp_path)
420
438
421 # return the checkpoint info
439 # return the checkpoint info
422 return self.get_checkpoint_model(checkpoint_id, name, path)
440 return self.get_checkpoint_model(checkpoint_id, name, path)
423
441
424 def list_checkpoints(self, name, path=''):
442 def list_checkpoints(self, name, path=''):
425 """list the checkpoints for a given notebook
443 """list the checkpoints for a given notebook
426
444
427 This notebook manager currently only supports one checkpoint per notebook.
445 This notebook manager currently only supports one checkpoint per notebook.
428 """
446 """
429 path = path.strip('/')
447 path = path.strip('/')
430 checkpoint_id = "checkpoint"
448 checkpoint_id = "checkpoint"
431 path = self.get_checkpoint_path(checkpoint_id, name, path)
449 path = self.get_checkpoint_path(checkpoint_id, name, path)
432 if not os.path.exists(path):
450 if not os.path.exists(path):
433 return []
451 return []
434 else:
452 else:
435 return [self.get_checkpoint_model(checkpoint_id, name, path)]
453 return [self.get_checkpoint_model(checkpoint_id, name, path)]
436
454
437
455
438 def restore_checkpoint(self, checkpoint_id, name, path=''):
456 def restore_checkpoint(self, checkpoint_id, name, path=''):
439 """restore a notebook to a checkpointed state"""
457 """restore a notebook to a checkpointed state"""
440 path = path.strip('/')
458 path = path.strip('/')
441 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
459 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
442 nb_path = self.get_os_path(name, path)
460 nb_path = self._get_os_path(name, path)
443 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
461 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
444 if not os.path.isfile(cp_path):
462 if not os.path.isfile(cp_path):
445 self.log.debug("checkpoint file does not exist: %s", cp_path)
463 self.log.debug("checkpoint file does not exist: %s", cp_path)
446 raise web.HTTPError(404,
464 raise web.HTTPError(404,
447 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
465 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
448 )
466 )
449 # ensure notebook is readable (never restore from an unreadable notebook)
467 # ensure notebook is readable (never restore from an unreadable notebook)
450 with io.open(cp_path, 'r', encoding='utf-8') as f:
468 with io.open(cp_path, 'r', encoding='utf-8') as f:
451 nb = current.read(f, u'json')
469 nb = current.read(f, u'json')
452 shutil.copy2(cp_path, nb_path)
470 shutil.copy2(cp_path, nb_path)
453 self.log.debug("copying %s -> %s", cp_path, nb_path)
471 self.log.debug("copying %s -> %s", cp_path, nb_path)
454
472
455 def delete_checkpoint(self, checkpoint_id, name, path=''):
473 def delete_checkpoint(self, checkpoint_id, name, path=''):
456 """delete a notebook's checkpoint"""
474 """delete a notebook's checkpoint"""
457 path = path.strip('/')
475 path = path.strip('/')
458 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
476 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
459 if not os.path.isfile(cp_path):
477 if not os.path.isfile(cp_path):
460 raise web.HTTPError(404,
478 raise web.HTTPError(404,
461 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
479 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
462 )
480 )
463 self.log.debug("unlinking %s", cp_path)
481 self.log.debug("unlinking %s", cp_path)
464 os.unlink(cp_path)
482 os.unlink(cp_path)
465
483
466 def info_string(self):
484 def info_string(self):
467 return "Serving notebooks from local directory: %s" % self.notebook_dir
485 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,269 +1,243 b''
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
243
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
@@ -1,251 +1,245 b''
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 b''
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,72 b''
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()
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_create_nb_dir():
55 with TemporaryDirectory() as td:
56 nbdir = os.path.join(td, 'notebooks')
57 app = NotebookApp(notebook_dir=nbdir)
58 nt.assert_equal(app.notebook_dir, nbdir)
59
60 def test_missing_nb_dir():
61 with TemporaryDirectory() as td:
62 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
63 app = NotebookApp()
64 with nt.assert_raises(TraitError):
65 app.notebook_dir = nbdir
66
67 def test_invalid_nb_dir():
68 with NamedTemporaryFile() as tf:
69 app = NotebookApp()
70 with nt.assert_raises(TraitError):
71 app.notebook_dir = tf
72
@@ -1,114 +1,125 b''
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