##// END OF EJS Templates
Merge pull request #4656 from takluyver/nbconvert-service...
Min RK -
r13924:0d046b44 merge
parent child Browse files
Show More
1 NO CONTENT: new file 100644
@@ -0,0 +1,117
1 import io
2 import os
3 import zipfile
4
5 from tornado import web
6
7 from ..base.handlers import IPythonHandler, notebook_path_regex
8 from IPython.nbformat.current import to_notebook_json
9 from IPython.nbconvert.exporters.export import exporter_map
10 from IPython.utils import tz
11 from IPython.utils.py3compat import cast_bytes
12
13 import sys
14
15 def find_resource_files(output_files_dir):
16 files = []
17 for dirpath, dirnames, filenames in os.walk(output_files_dir):
18 files.extend([os.path.join(dirpath, f) for f in filenames])
19 return files
20
21 def respond_zip(handler, name, output, resources):
22 """Zip up the output and resource files and respond with the zip file.
23
24 Returns True if it has served a zip file, False if there are no resource
25 files, in which case we serve the plain output file.
26 """
27 # Check if we have resource files we need to zip
28 output_files = resources.get('outputs', None)
29 if not output_files:
30 return False
31
32 # Headers
33 zip_filename = os.path.splitext(name)[0] + '.zip'
34 handler.set_header('Content-Disposition',
35 'attachment; filename="%s"' % zip_filename)
36 handler.set_header('Content-Type', 'application/zip')
37
38 # Prepare the zip file
39 buffer = io.BytesIO()
40 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
41 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
42 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
43 for filename, data in output_files.items():
44 zipf.writestr(os.path.basename(filename), data)
45 zipf.close()
46
47 handler.finish(buffer.getvalue())
48 return True
49
50 class NbconvertFileHandler(IPythonHandler):
51
52 SUPPORTED_METHODS = ('GET',)
53
54 @web.authenticated
55 def get(self, format, path='', name=None):
56 exporter = exporter_map[format](config=self.config)
57
58 path = path.strip('/')
59 os_path = self.notebook_manager.get_os_path(name, path)
60 if not os.path.isfile(os_path):
61 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
62
63 info = os.stat(os_path)
64 self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime))
65
66 output, resources = exporter.from_filename(os_path)
67
68 if respond_zip(self, name, output, resources):
69 return
70
71 # Force download if requested
72 if self.get_argument('download', 'false').lower() == 'true':
73 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
74 self.set_header('Content-Disposition',
75 'attachment; filename="%s"' % filename)
76
77 # MIME type
78 if exporter.output_mimetype:
79 self.set_header('Content-Type',
80 '%s; charset=utf-8' % exporter.output_mimetype)
81
82 self.finish(output)
83
84 class NbconvertPostHandler(IPythonHandler):
85 SUPPORTED_METHODS = ('POST',)
86
87 @web.authenticated
88 def post(self, format):
89 exporter = exporter_map[format](config=self.config)
90
91 model = self.get_json_body()
92 nbnode = to_notebook_json(model['content'])
93
94 output, resources = exporter.from_notebook_node(nbnode)
95
96 if respond_zip(self, nbnode.metadata.name, output, resources):
97 return
98
99 # MIME type
100 if exporter.output_mimetype:
101 self.set_header('Content-Type',
102 '%s; charset=utf-8' % exporter.output_mimetype)
103
104 self.finish(output)
105
106 #-----------------------------------------------------------------------------
107 # URL to handler mappings
108 #-----------------------------------------------------------------------------
109
110 _format_regex = r"(?P<format>\w+)"
111
112
113 default_handlers = [
114 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
115 NbconvertFileHandler),
116 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
117 ] No newline at end of file
1 NO CONTENT: new file 100644
@@ -0,0 +1,120
1 # coding: utf-8
2 import base64
3 import io
4 import json
5 import os
6 from os.path import join as pjoin
7 import shutil
8
9 import requests
10
11 from IPython.html.utils import url_path_join
12 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
14 new_heading_cell, new_code_cell,
15 new_output)
16
17 class NbconvertAPI(object):
18 """Wrapper for nbconvert API calls."""
19 def __init__(self, base_url):
20 self.base_url = base_url
21
22 def _req(self, verb, path, body=None, params=None):
23 response = requests.request(verb,
24 url_path_join(self.base_url, 'nbconvert', path),
25 data=body, params=params,
26 )
27 response.raise_for_status()
28 return response
29
30 def from_file(self, format, path, name, download=False):
31 return self._req('GET', url_path_join(format, path, name),
32 params={'download':download})
33
34 def from_post(self, format, nbmodel):
35 body = json.dumps(nbmodel)
36 return self._req('POST', format, body)
37
38 def list_formats(self):
39 return self._req('GET', '')
40
41 png_green_pixel = base64.encodestring(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00'
42 b'\x00\x00\x01\x00\x00x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDAT'
43 b'\x08\xd7c\x90\xfb\xcf\x00\x00\x02\\\x01\x1e.~d\x87\x00\x00\x00\x00IEND\xaeB`\x82')
44
45 class APITest(NotebookTestBase):
46 def setUp(self):
47 nbdir = self.notebook_dir.name
48
49 if not os.path.isdir(pjoin(nbdir, 'foo')):
50 os.mkdir(pjoin(nbdir, 'foo'))
51
52 nb = new_notebook(name='testnb')
53
54 ws = new_worksheet()
55 nb.worksheets = [ws]
56 ws.cells.append(new_heading_cell(u'Created by test ³'))
57 cc1 = new_code_cell(input=u'print(2*6)')
58 cc1.outputs.append(new_output(output_text=u'12'))
59 cc1.outputs.append(new_output(output_png=png_green_pixel, output_type='pyout'))
60 ws.cells.append(cc1)
61
62 with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w',
63 encoding='utf-8') as f:
64 write(nb, f, format='ipynb')
65
66 self.nbconvert_api = NbconvertAPI(self.base_url())
67
68 def tearDown(self):
69 nbdir = self.notebook_dir.name
70
71 for dname in ['foo']:
72 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
73
74 def test_from_file(self):
75 r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')
76 self.assertEqual(r.status_code, 200)
77 self.assertIn(u'text/html', r.headers['Content-Type'])
78 self.assertIn(u'Created by test', r.text)
79 self.assertIn(u'print', r.text)
80
81 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb')
82 self.assertIn(u'text/x-python', r.headers['Content-Type'])
83 self.assertIn(u'print(2*6)', r.text)
84
85 def test_from_file_404(self):
86 with assert_http_error(404):
87 self.nbconvert_api.from_file('html', 'foo', 'thisdoesntexist.ipynb')
88
89 def test_from_file_download(self):
90 r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb', download=True)
91 content_disposition = r.headers['Content-Disposition']
92 self.assertIn('attachment', content_disposition)
93 self.assertIn('testnb.py', content_disposition)
94
95 def test_from_file_zip(self):
96 r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True)
97 self.assertIn(u'application/zip', r.headers['Content-Type'])
98 self.assertIn(u'.zip', r.headers['Content-Disposition'])
99
100 def test_from_post(self):
101 nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb')
102 nbmodel = requests.get(nbmodel_url).json()
103
104 r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel)
105 self.assertEqual(r.status_code, 200)
106 self.assertIn(u'text/html', r.headers['Content-Type'])
107 self.assertIn(u'Created by test', r.text)
108 self.assertIn(u'print', r.text)
109
110 r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel)
111 self.assertIn(u'text/x-python', r.headers['Content-Type'])
112 self.assertIn(u'print(2*6)', r.text)
113
114 def test_from_post_zip(self):
115 nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb')
116 nbmodel = requests.get(nbmodel_url).json()
117
118 r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel)
119 self.assertIn(u'application/zip', r.headers['Content-Type'])
120 self.assertIn(u'.zip', r.headers['Content-Disposition'])
1 NO CONTENT: new file 100644
@@ -0,0 +1,23
1 import json
2
3 from tornado import web
4
5 from ...base.handlers import IPythonHandler, json_errors
6 from IPython.nbconvert.exporters.export import exporter_map
7
8 class NbconvertRootHandler(IPythonHandler):
9 SUPPORTED_METHODS = ('GET',)
10
11 @web.authenticated
12 @json_errors
13 def get(self):
14 res = {}
15 for format, exporter in exporter_map.items():
16 res[format] = info = {}
17 info['output_mimetype'] = exporter.output_mimetype
18
19 self.finish(json.dumps(res))
20
21 default_handlers = [
22 (r"/api/nbconvert", NbconvertRootHandler),
23 ] No newline at end of file
1 NO CONTENT: new file 100644
@@ -0,0 +1,31
1 import requests
2
3 from IPython.html.utils import url_path_join
4 from IPython.html.tests.launchnotebook import NotebookTestBase
5
6 class NbconvertAPI(object):
7 """Wrapper for nbconvert API calls."""
8 def __init__(self, base_url):
9 self.base_url = base_url
10
11 def _req(self, verb, path, body=None, params=None):
12 response = requests.request(verb,
13 url_path_join(self.base_url, 'api/nbconvert', path),
14 data=body, params=params,
15 )
16 response.raise_for_status()
17 return response
18
19 def list_formats(self):
20 return self._req('GET', '')
21
22 class APITest(NotebookTestBase):
23 def setUp(self):
24 self.nbconvert_api = NbconvertAPI(self.base_url())
25
26 def test_list_formats(self):
27 formats = self.nbconvert_api.list_formats().json()
28 self.assertIsInstance(formats, dict)
29 self.assertIn('python', formats)
30 self.assertIn('html', formats)
31 self.assertEqual(formats['python']['output_mimetype'], 'text/x-python') No newline at end of file
@@ -0,0 +1,3
1 * Print preview is back in the notebook menus, along with options to
2 download the open notebook in various formats. This is powered by
3 nbconvert.
@@ -1,364 +1,372
1 1 """Base Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19
20 20 import functools
21 21 import json
22 22 import logging
23 23 import os
24 24 import stat
25 25 import sys
26 26 import traceback
27 27
28 28 from tornado import web
29 29
30 30 try:
31 31 from tornado.log import app_log
32 32 except ImportError:
33 33 app_log = logging.getLogger()
34 34
35 35 from IPython.config import Application
36 36 from IPython.utils.path import filefind
37 37 from IPython.utils.py3compat import string_types
38 38
39 39 # UF_HIDDEN is a stat flag not defined in the stat module.
40 40 # It is used by BSD to indicate hidden files.
41 41 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # Top-level handlers
45 45 #-----------------------------------------------------------------------------
46 46
47 47 class RequestHandler(web.RequestHandler):
48 48 """RequestHandler with default variable setting."""
49 49
50 50 def render(*args, **kwargs):
51 51 kwargs.setdefault('message', '')
52 52 return web.RequestHandler.render(*args, **kwargs)
53 53
54 54 class AuthenticatedHandler(RequestHandler):
55 55 """A RequestHandler with an authenticated user."""
56 56
57 57 def clear_login_cookie(self):
58 58 self.clear_cookie(self.cookie_name)
59 59
60 60 def get_current_user(self):
61 61 user_id = self.get_secure_cookie(self.cookie_name)
62 62 # For now the user_id should not return empty, but it could eventually
63 63 if user_id == '':
64 64 user_id = 'anonymous'
65 65 if user_id is None:
66 66 # prevent extra Invalid cookie sig warnings:
67 67 self.clear_login_cookie()
68 68 if not self.login_available:
69 69 user_id = 'anonymous'
70 70 return user_id
71 71
72 72 @property
73 73 def cookie_name(self):
74 74 default_cookie_name = 'username-{host}'.format(
75 75 host=self.request.host,
76 76 ).replace(':', '-')
77 77 return self.settings.get('cookie_name', default_cookie_name)
78 78
79 79 @property
80 80 def password(self):
81 81 """our password"""
82 82 return self.settings.get('password', '')
83 83
84 84 @property
85 85 def logged_in(self):
86 86 """Is a user currently logged in?
87 87
88 88 """
89 89 user = self.get_current_user()
90 90 return (user and not user == 'anonymous')
91 91
92 92 @property
93 93 def login_available(self):
94 94 """May a user proceed to log in?
95 95
96 96 This returns True if login capability is available, irrespective of
97 97 whether the user is already logged in or not.
98 98
99 99 """
100 100 return bool(self.settings.get('password', ''))
101 101
102 102
103 103 class IPythonHandler(AuthenticatedHandler):
104 104 """IPython-specific extensions to authenticated handling
105 105
106 106 Mostly property shortcuts to IPython-specific settings.
107 107 """
108 108
109 109 @property
110 110 def config(self):
111 111 return self.settings.get('config', None)
112 112
113 113 @property
114 114 def log(self):
115 115 """use the IPython log by default, falling back on tornado's logger"""
116 116 if Application.initialized():
117 117 return Application.instance().log
118 118 else:
119 119 return app_log
120 120
121 121 @property
122 122 def use_less(self):
123 123 """Use less instead of css in templates"""
124 124 return self.settings.get('use_less', False)
125 125
126 126 #---------------------------------------------------------------
127 127 # URLs
128 128 #---------------------------------------------------------------
129 129
130 130 @property
131 131 def ws_url(self):
132 132 """websocket url matching the current request
133 133
134 134 By default, this is just `''`, indicating that it should match
135 135 the same host, protocol, port, etc.
136 136 """
137 137 return self.settings.get('websocket_url', '')
138 138
139 139 @property
140 140 def mathjax_url(self):
141 141 return self.settings.get('mathjax_url', '')
142 142
143 143 @property
144 144 def base_project_url(self):
145 145 return self.settings.get('base_project_url', '/')
146 146
147 147 @property
148 148 def base_kernel_url(self):
149 149 return self.settings.get('base_kernel_url', '/')
150 150
151 151 #---------------------------------------------------------------
152 152 # Manager objects
153 153 #---------------------------------------------------------------
154 154
155 155 @property
156 156 def kernel_manager(self):
157 157 return self.settings['kernel_manager']
158 158
159 159 @property
160 160 def notebook_manager(self):
161 161 return self.settings['notebook_manager']
162 162
163 163 @property
164 164 def cluster_manager(self):
165 165 return self.settings['cluster_manager']
166 166
167 167 @property
168 168 def session_manager(self):
169 169 return self.settings['session_manager']
170 170
171 171 @property
172 172 def project_dir(self):
173 173 return self.notebook_manager.notebook_dir
174 174
175 175 #---------------------------------------------------------------
176 176 # template rendering
177 177 #---------------------------------------------------------------
178 178
179 179 def get_template(self, name):
180 180 """Return the jinja template object for a given name"""
181 181 return self.settings['jinja2_env'].get_template(name)
182 182
183 183 def render_template(self, name, **ns):
184 184 ns.update(self.template_namespace)
185 185 template = self.get_template(name)
186 186 return template.render(**ns)
187 187
188 188 @property
189 189 def template_namespace(self):
190 190 return dict(
191 191 base_project_url=self.base_project_url,
192 192 base_kernel_url=self.base_kernel_url,
193 193 logged_in=self.logged_in,
194 194 login_available=self.login_available,
195 195 use_less=self.use_less,
196 196 )
197 197
198 198 def get_json_body(self):
199 199 """Return the body of the request as JSON data."""
200 200 if not self.request.body:
201 201 return None
202 202 # Do we need to call body.decode('utf-8') here?
203 203 body = self.request.body.strip().decode(u'utf-8')
204 204 try:
205 205 model = json.loads(body)
206 206 except Exception:
207 207 self.log.debug("Bad JSON: %r", body)
208 208 self.log.error("Couldn't parse JSON", exc_info=True)
209 209 raise web.HTTPError(400, u'Invalid JSON in body of request')
210 210 return model
211 211
212 212
213 213 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
214 214 """static files should only be accessible when logged in"""
215 215
216 216 @web.authenticated
217 217 def get(self, path):
218 218 if os.path.splitext(path)[1] == '.ipynb':
219 219 name = os.path.basename(path)
220 220 self.set_header('Content-Type', 'application/json')
221 221 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
222 222
223 223 return web.StaticFileHandler.get(self, path)
224 224
225 225 def compute_etag(self):
226 226 return None
227 227
228 228 def validate_absolute_path(self, root, absolute_path):
229 229 """Validate and return the absolute path.
230 230
231 231 Requires tornado 3.1
232 232
233 233 Adding to tornado's own handling, forbids the serving of hidden files.
234 234 """
235 235 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
236 236 abs_root = os.path.abspath(root)
237 237 self.forbid_hidden(abs_root, abs_path)
238 238 return abs_path
239 239
240 240 def forbid_hidden(self, absolute_root, absolute_path):
241 241 """Raise 403 if a file is hidden or contained in a hidden directory.
242 242
243 243 Hidden is determined by either name starting with '.'
244 244 or the UF_HIDDEN flag as reported by stat
245 245 """
246 246 inside_root = absolute_path[len(absolute_root):]
247 247 if any(part.startswith('.') for part in inside_root.split(os.sep)):
248 248 raise web.HTTPError(403)
249 249
250 250 # check UF_HIDDEN on any location up to root
251 251 path = absolute_path
252 252 while path and path.startswith(absolute_root) and path != absolute_root:
253 253 st = os.stat(path)
254 254 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
255 255 raise web.HTTPError(403)
256 256 path = os.path.dirname(path)
257 257
258 258 return absolute_path
259 259
260 260
261 261 def json_errors(method):
262 262 """Decorate methods with this to return GitHub style JSON errors.
263 263
264 264 This should be used on any JSON API on any handler method that can raise HTTPErrors.
265 265
266 266 This will grab the latest HTTPError exception using sys.exc_info
267 267 and then:
268 268
269 269 1. Set the HTTP status code based on the HTTPError
270 270 2. Create and return a JSON body with a message field describing
271 271 the error in a human readable form.
272 272 """
273 273 @functools.wraps(method)
274 274 def wrapper(self, *args, **kwargs):
275 275 try:
276 276 result = method(self, *args, **kwargs)
277 277 except web.HTTPError as e:
278 278 status = e.status_code
279 279 message = e.log_message
280 280 self.set_status(e.status_code)
281 281 self.finish(json.dumps(dict(message=message)))
282 282 except Exception:
283 283 self.log.error("Unhandled error in API request", exc_info=True)
284 284 status = 500
285 285 message = "Unknown server error"
286 286 t, value, tb = sys.exc_info()
287 287 self.set_status(status)
288 288 tb_text = ''.join(traceback.format_exception(t, value, tb))
289 289 reply = dict(message=message, traceback=tb_text)
290 290 self.finish(json.dumps(reply))
291 291 else:
292 292 return result
293 293 return wrapper
294 294
295 295
296 296
297 297 #-----------------------------------------------------------------------------
298 298 # File handler
299 299 #-----------------------------------------------------------------------------
300 300
301 301 # to minimize subclass changes:
302 302 HTTPError = web.HTTPError
303 303
304 304 class FileFindHandler(web.StaticFileHandler):
305 305 """subclass of StaticFileHandler for serving files from a search path"""
306 306
307 307 # cache search results, don't search for files more than once
308 308 _static_paths = {}
309 309
310 310 def initialize(self, path, default_filename=None):
311 311 if isinstance(path, string_types):
312 312 path = [path]
313 313
314 314 self.root = tuple(
315 315 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
316 316 )
317 317 self.default_filename = default_filename
318 318
319 319 def compute_etag(self):
320 320 return None
321 321
322 322 @classmethod
323 323 def get_absolute_path(cls, roots, path):
324 324 """locate a file to serve on our static file search path"""
325 325 with cls._lock:
326 326 if path in cls._static_paths:
327 327 return cls._static_paths[path]
328 328 try:
329 329 abspath = os.path.abspath(filefind(path, roots))
330 330 except IOError:
331 331 # IOError means not found
332 332 raise web.HTTPError(404)
333 333
334 334 cls._static_paths[path] = abspath
335 335 return abspath
336 336
337 337 def validate_absolute_path(self, root, absolute_path):
338 338 """check if the file should be served (raises 404, 403, etc.)"""
339 339 for root in self.root:
340 340 if (absolute_path + os.sep).startswith(root):
341 341 break
342 342
343 343 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
344 344
345 345
346 346 class TrailingSlashHandler(web.RequestHandler):
347 347 """Simple redirect handler that strips trailing slashes
348 348
349 349 This should be the first, highest priority handler.
350 350 """
351 351
352 352 SUPPORTED_METHODS = ['GET']
353 353
354 354 def get(self):
355 355 self.redirect(self.request.uri.rstrip('/'))
356 356
357 357 #-----------------------------------------------------------------------------
358 # URL pattern fragments for re-use
359 #-----------------------------------------------------------------------------
360
361 path_regex = r"(?P<path>(?:/.*)*)"
362 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
363 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
364
365 #-----------------------------------------------------------------------------
358 366 # URL to handler mappings
359 367 #-----------------------------------------------------------------------------
360 368
361 369
362 370 default_handlers = [
363 371 (r".*/", TrailingSlashHandler)
364 372 ]
@@ -1,91 +1,90
1 1 """Tornado handlers for the live notebook view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import os
20 20 from tornado import web
21 21 HTTPError = web.HTTPError
22 22
23 from ..base.handlers import IPythonHandler
24 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
23 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
25 24 from ..utils import url_path_join, url_escape
26 25
27 26 #-----------------------------------------------------------------------------
28 27 # Handlers
29 28 #-----------------------------------------------------------------------------
30 29
31 30
32 31 class NotebookHandler(IPythonHandler):
33 32
34 33 @web.authenticated
35 34 def get(self, path='', name=None):
36 35 """get renders the notebook template if a name is given, or
37 36 redirects to the '/files/' handler if the name is not given."""
38 37 path = path.strip('/')
39 38 nbm = self.notebook_manager
40 39 if name is None:
41 40 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
42 41
43 42 # a .ipynb filename was given
44 43 if not nbm.notebook_exists(name, path):
45 44 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
46 45 name = url_escape(name)
47 46 path = url_escape(path)
48 47 self.write(self.render_template('notebook.html',
49 48 project=self.project_dir,
50 49 notebook_path=path,
51 50 notebook_name=name,
52 51 kill_kernel=False,
53 52 mathjax_url=self.mathjax_url,
54 53 )
55 54 )
56 55
57 56 class NotebookRedirectHandler(IPythonHandler):
58 57 def get(self, path=''):
59 58 nbm = self.notebook_manager
60 59 if nbm.path_exists(path):
61 60 # it's a *directory*, redirect to /tree
62 61 url = url_path_join(self.base_project_url, 'tree', path)
63 62 else:
64 63 # otherwise, redirect to /files
65 64 if '/files/' in path:
66 65 # redirect without files/ iff it would 404
67 66 # this preserves pre-2.0-style 'files/' links
68 67 # FIXME: this is hardcoded based on notebook_path,
69 68 # but so is the files handler itself,
70 69 # so it should work until both are cleaned up.
71 70 parts = path.split('/')
72 71 files_path = os.path.join(nbm.notebook_dir, *parts)
73 72 self.log.warn("filespath: %s", files_path)
74 73 if not os.path.exists(files_path):
75 74 path = path.replace('/files/', '/', 1)
76 75
77 76 url = url_path_join(self.base_project_url, 'files', path)
78 77 url = url_escape(url)
79 78 self.log.debug("Redirecting %s to %s", self.request.path, url)
80 79 self.redirect(url)
81 80
82 81 #-----------------------------------------------------------------------------
83 82 # URL to handler mappings
84 83 #-----------------------------------------------------------------------------
85 84
86 85
87 86 default_handlers = [
88 (r"/notebooks%s" % _notebook_path_regex, NotebookHandler),
89 (r"/notebooks%s" % _path_regex, NotebookRedirectHandler),
87 (r"/notebooks%s" % notebook_path_regex, NotebookHandler),
88 (r"/notebooks%s" % path_regex, NotebookRedirectHandler),
90 89 ]
91 90
@@ -1,749 +1,751
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import logging
23 23 import os
24 24 import random
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
31 31 import webbrowser
32 32
33 33
34 34 # Third party
35 35 # check for pyzmq 2.1.11
36 36 from IPython.utils.zmqrelated import check_for_zmq
37 37 check_for_zmq('2.1.11', 'IPython.html')
38 38
39 39 from jinja2 import Environment, FileSystemLoader
40 40
41 41 # Install the pyzmq ioloop. This has to be done before anything else from
42 42 # tornado is imported.
43 43 from zmq.eventloop import ioloop
44 44 ioloop.install()
45 45
46 46 # check for tornado 3.1.0
47 47 msg = "The IPython Notebook requires tornado >= 3.1.0"
48 48 try:
49 49 import tornado
50 50 except ImportError:
51 51 raise ImportError(msg)
52 52 try:
53 53 version_info = tornado.version_info
54 54 except AttributeError:
55 55 raise ImportError(msg + ", but you have < 1.1.0")
56 56 if version_info < (3,1,0):
57 57 raise ImportError(msg + ", but you have %s" % tornado.version)
58 58
59 59 from tornado import httpserver
60 60 from tornado import web
61 61
62 62 # Our own libraries
63 63 from IPython.html import DEFAULT_STATIC_FILES_PATH
64 64
65 65 from .services.kernels.kernelmanager import MappingKernelManager
66 66 from .services.notebooks.nbmanager import NotebookManager
67 67 from .services.notebooks.filenbmanager import FileNotebookManager
68 68 from .services.clusters.clustermanager import ClusterManager
69 69 from .services.sessions.sessionmanager import SessionManager
70 70
71 71 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
72 72
73 73 from IPython.config.application import catch_config_error, boolean_flag
74 74 from IPython.core.application import BaseIPythonApplication
75 75 from IPython.consoleapp import IPythonConsoleApp
76 76 from IPython.kernel import swallow_argv
77 77 from IPython.kernel.zmq.session import default_secure
78 78 from IPython.kernel.zmq.kernelapp import (
79 79 kernel_flags,
80 80 kernel_aliases,
81 81 )
82 82 from IPython.utils.importstring import import_item
83 83 from IPython.utils.localinterfaces import localhost
84 84 from IPython.utils import submodule
85 85 from IPython.utils.traitlets import (
86 86 Dict, Unicode, Integer, List, Bool, Bytes,
87 87 DottedObjectName
88 88 )
89 89 from IPython.utils import py3compat
90 90 from IPython.utils.path import filefind, get_ipython_dir
91 91
92 92 from .utils import url_path_join
93 93
94 94 #-----------------------------------------------------------------------------
95 95 # Module globals
96 96 #-----------------------------------------------------------------------------
97 97
98 98 _examples = """
99 99 ipython notebook # start the notebook
100 100 ipython notebook --profile=sympy # use the sympy profile
101 101 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
102 102 """
103 103
104 104 #-----------------------------------------------------------------------------
105 105 # Helper functions
106 106 #-----------------------------------------------------------------------------
107 107
108 108 def random_ports(port, n):
109 109 """Generate a list of n random ports near the given port.
110 110
111 111 The first 5 ports will be sequential, and the remaining n-5 will be
112 112 randomly selected in the range [port-2*n, port+2*n].
113 113 """
114 114 for i in range(min(5, n)):
115 115 yield port + i
116 116 for i in range(n-5):
117 117 yield max(1, port + random.randint(-2*n, 2*n))
118 118
119 119 def load_handlers(name):
120 120 """Load the (URL pattern, handler) tuples for each component."""
121 121 name = 'IPython.html.' + name
122 122 mod = __import__(name, fromlist=['default_handlers'])
123 123 return mod.default_handlers
124 124
125 125 #-----------------------------------------------------------------------------
126 126 # The Tornado web application
127 127 #-----------------------------------------------------------------------------
128 128
129 129 class NotebookWebApplication(web.Application):
130 130
131 131 def __init__(self, ipython_app, kernel_manager, notebook_manager,
132 132 cluster_manager, session_manager, log, base_project_url,
133 133 settings_overrides):
134 134
135 135 settings = self.init_settings(
136 136 ipython_app, kernel_manager, notebook_manager, cluster_manager,
137 137 session_manager, log, base_project_url, settings_overrides)
138 138 handlers = self.init_handlers(settings)
139 139
140 140 super(NotebookWebApplication, self).__init__(handlers, **settings)
141 141
142 142 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
143 143 cluster_manager, session_manager, log, base_project_url,
144 144 settings_overrides):
145 145 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
146 146 # base_project_url will always be unicode, which will in turn
147 147 # make the patterns unicode, and ultimately result in unicode
148 148 # keys in kwargs to handler._execute(**kwargs) in tornado.
149 149 # This enforces that base_project_url be ascii in that situation.
150 150 #
151 151 # Note that the URLs these patterns check against are escaped,
152 152 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
153 153 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
154 154 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
155 155 settings = dict(
156 156 # basics
157 157 base_project_url=base_project_url,
158 158 base_kernel_url=ipython_app.base_kernel_url,
159 159 template_path=template_path,
160 160 static_path=ipython_app.static_file_path,
161 161 static_handler_class = FileFindHandler,
162 162 static_url_prefix = url_path_join(base_project_url,'/static/'),
163 163
164 164 # authentication
165 165 cookie_secret=ipython_app.cookie_secret,
166 166 login_url=url_path_join(base_project_url,'/login'),
167 167 password=ipython_app.password,
168 168
169 169 # managers
170 170 kernel_manager=kernel_manager,
171 171 notebook_manager=notebook_manager,
172 172 cluster_manager=cluster_manager,
173 173 session_manager=session_manager,
174 174
175 175 # IPython stuff
176 176 nbextensions_path = ipython_app.nbextensions_path,
177 177 mathjax_url=ipython_app.mathjax_url,
178 178 config=ipython_app.config,
179 179 use_less=ipython_app.use_less,
180 180 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
181 181 )
182 182
183 183 # allow custom overrides for the tornado web app.
184 184 settings.update(settings_overrides)
185 185 return settings
186 186
187 187 def init_handlers(self, settings):
188 188 # Load the (URL pattern, handler) tuples for each component.
189 189 handlers = []
190 190 handlers.extend(load_handlers('base.handlers'))
191 191 handlers.extend(load_handlers('tree.handlers'))
192 192 handlers.extend(load_handlers('auth.login'))
193 193 handlers.extend(load_handlers('auth.logout'))
194 194 handlers.extend(load_handlers('notebook.handlers'))
195 handlers.extend(load_handlers('nbconvert.handlers'))
195 196 handlers.extend(load_handlers('services.kernels.handlers'))
196 197 handlers.extend(load_handlers('services.notebooks.handlers'))
197 198 handlers.extend(load_handlers('services.clusters.handlers'))
198 199 handlers.extend(load_handlers('services.sessions.handlers'))
200 handlers.extend(load_handlers('services.nbconvert.handlers'))
199 201 handlers.extend([
200 202 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
201 203 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
202 204 ])
203 205 # prepend base_project_url onto the patterns that we match
204 206 new_handlers = []
205 207 for handler in handlers:
206 208 pattern = url_path_join(settings['base_project_url'], handler[0])
207 209 new_handler = tuple([pattern] + list(handler[1:]))
208 210 new_handlers.append(new_handler)
209 211 return new_handlers
210 212
211 213
212 214
213 215 #-----------------------------------------------------------------------------
214 216 # Aliases and Flags
215 217 #-----------------------------------------------------------------------------
216 218
217 219 flags = dict(kernel_flags)
218 220 flags['no-browser']=(
219 221 {'NotebookApp' : {'open_browser' : False}},
220 222 "Don't open the notebook in a browser after startup."
221 223 )
222 224 flags['no-mathjax']=(
223 225 {'NotebookApp' : {'enable_mathjax' : False}},
224 226 """Disable MathJax
225 227
226 228 MathJax is the javascript library IPython uses to render math/LaTeX. It is
227 229 very large, so you may want to disable it if you have a slow internet
228 230 connection, or for offline use of the notebook.
229 231
230 232 When disabled, equations etc. will appear as their untransformed TeX source.
231 233 """
232 234 )
233 235
234 236 # Add notebook manager flags
235 237 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
236 238 'Auto-save a .py script everytime the .ipynb notebook is saved',
237 239 'Do not auto-save .py scripts for every notebook'))
238 240
239 241 # the flags that are specific to the frontend
240 242 # these must be scrubbed before being passed to the kernel,
241 243 # or it will raise an error on unrecognized flags
242 244 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
243 245
244 246 aliases = dict(kernel_aliases)
245 247
246 248 aliases.update({
247 249 'ip': 'NotebookApp.ip',
248 250 'port': 'NotebookApp.port',
249 251 'port-retries': 'NotebookApp.port_retries',
250 252 'transport': 'KernelManager.transport',
251 253 'keyfile': 'NotebookApp.keyfile',
252 254 'certfile': 'NotebookApp.certfile',
253 255 'notebook-dir': 'NotebookManager.notebook_dir',
254 256 'browser': 'NotebookApp.browser',
255 257 })
256 258
257 259 # remove ipkernel flags that are singletons, and don't make sense in
258 260 # multi-kernel evironment:
259 261 aliases.pop('f', None)
260 262
261 263 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
262 264 u'notebook-dir', u'profile', u'profile-dir']
263 265
264 266 #-----------------------------------------------------------------------------
265 267 # NotebookApp
266 268 #-----------------------------------------------------------------------------
267 269
268 270 class NotebookApp(BaseIPythonApplication):
269 271
270 272 name = 'ipython-notebook'
271 273
272 274 description = """
273 275 The IPython HTML Notebook.
274 276
275 277 This launches a Tornado based HTML Notebook Server that serves up an
276 278 HTML5/Javascript Notebook client.
277 279 """
278 280 examples = _examples
279 281
280 282 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
281 283 FileNotebookManager]
282 284 flags = Dict(flags)
283 285 aliases = Dict(aliases)
284 286
285 287 kernel_argv = List(Unicode)
286 288
287 289 def _log_level_default(self):
288 290 return logging.INFO
289 291
290 292 def _log_format_default(self):
291 293 """override default log format to include time"""
292 294 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
293 295
294 296 # create requested profiles by default, if they don't exist:
295 297 auto_create = Bool(True)
296 298
297 299 # file to be opened in the notebook server
298 300 file_to_run = Unicode('')
299 301
300 302 # Network related information.
301 303
302 304 ip = Unicode(config=True,
303 305 help="The IP address the notebook server will listen on."
304 306 )
305 307 def _ip_default(self):
306 308 return localhost()
307 309
308 310 def _ip_changed(self, name, old, new):
309 311 if new == u'*': self.ip = u''
310 312
311 313 port = Integer(8888, config=True,
312 314 help="The port the notebook server will listen on."
313 315 )
314 316 port_retries = Integer(50, config=True,
315 317 help="The number of additional ports to try if the specified port is not available."
316 318 )
317 319
318 320 certfile = Unicode(u'', config=True,
319 321 help="""The full path to an SSL/TLS certificate file."""
320 322 )
321 323
322 324 keyfile = Unicode(u'', config=True,
323 325 help="""The full path to a private key file for usage with SSL/TLS."""
324 326 )
325 327
326 328 cookie_secret = Bytes(b'', config=True,
327 329 help="""The random bytes used to secure cookies.
328 330 By default this is a new random number every time you start the Notebook.
329 331 Set it to a value in a config file to enable logins to persist across server sessions.
330 332
331 333 Note: Cookie secrets should be kept private, do not share config files with
332 334 cookie_secret stored in plaintext (you can read the value from a file).
333 335 """
334 336 )
335 337 def _cookie_secret_default(self):
336 338 return os.urandom(1024)
337 339
338 340 password = Unicode(u'', config=True,
339 341 help="""Hashed password to use for web authentication.
340 342
341 343 To generate, type in a python/IPython shell:
342 344
343 345 from IPython.lib import passwd; passwd()
344 346
345 347 The string should be of the form type:salt:hashed-password.
346 348 """
347 349 )
348 350
349 351 open_browser = Bool(True, config=True,
350 352 help="""Whether to open in a browser after starting.
351 353 The specific browser used is platform dependent and
352 354 determined by the python standard library `webbrowser`
353 355 module, unless it is overridden using the --browser
354 356 (NotebookApp.browser) configuration option.
355 357 """)
356 358
357 359 browser = Unicode(u'', config=True,
358 360 help="""Specify what command to use to invoke a web
359 361 browser when opening the notebook. If not specified, the
360 362 default browser will be determined by the `webbrowser`
361 363 standard library module, which allows setting of the
362 364 BROWSER environment variable to override it.
363 365 """)
364 366
365 367 use_less = Bool(False, config=True,
366 368 help="""Wether to use Browser Side less-css parsing
367 369 instead of compiled css version in templates that allows
368 370 it. This is mainly convenient when working on the less
369 371 file to avoid a build step, or if user want to overwrite
370 372 some of the less variables without having to recompile
371 373 everything.
372 374
373 375 You will need to install the less.js component in the static directory
374 376 either in the source tree or in your profile folder.
375 377 """)
376 378
377 379 webapp_settings = Dict(config=True,
378 380 help="Supply overrides for the tornado.web.Application that the "
379 381 "IPython notebook uses.")
380 382
381 383 enable_mathjax = Bool(True, config=True,
382 384 help="""Whether to enable MathJax for typesetting math/TeX
383 385
384 386 MathJax is the javascript library IPython uses to render math/LaTeX. It is
385 387 very large, so you may want to disable it if you have a slow internet
386 388 connection, or for offline use of the notebook.
387 389
388 390 When disabled, equations etc. will appear as their untransformed TeX source.
389 391 """
390 392 )
391 393 def _enable_mathjax_changed(self, name, old, new):
392 394 """set mathjax url to empty if mathjax is disabled"""
393 395 if not new:
394 396 self.mathjax_url = u''
395 397
396 398 base_project_url = Unicode('/', config=True,
397 399 help='''The base URL for the notebook server.
398 400
399 401 Leading and trailing slashes can be omitted,
400 402 and will automatically be added.
401 403 ''')
402 404 def _base_project_url_changed(self, name, old, new):
403 405 if not new.startswith('/'):
404 406 self.base_project_url = '/'+new
405 407 elif not new.endswith('/'):
406 408 self.base_project_url = new+'/'
407 409
408 410 base_kernel_url = Unicode('/', config=True,
409 411 help='''The base URL for the kernel server
410 412
411 413 Leading and trailing slashes can be omitted,
412 414 and will automatically be added.
413 415 ''')
414 416 def _base_kernel_url_changed(self, name, old, new):
415 417 if not new.startswith('/'):
416 418 self.base_kernel_url = '/'+new
417 419 elif not new.endswith('/'):
418 420 self.base_kernel_url = new+'/'
419 421
420 422 websocket_url = Unicode("", config=True,
421 423 help="""The base URL for the websocket server,
422 424 if it differs from the HTTP server (hint: it almost certainly doesn't).
423 425
424 426 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
425 427 """
426 428 )
427 429
428 430 extra_static_paths = List(Unicode, config=True,
429 431 help="""Extra paths to search for serving static files.
430 432
431 433 This allows adding javascript/css to be available from the notebook server machine,
432 434 or overriding individual files in the IPython"""
433 435 )
434 436 def _extra_static_paths_default(self):
435 437 return [os.path.join(self.profile_dir.location, 'static')]
436 438
437 439 @property
438 440 def static_file_path(self):
439 441 """return extra paths + the default location"""
440 442 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
441 443
442 444 nbextensions_path = List(Unicode, config=True,
443 445 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
444 446 )
445 447 def _nbextensions_path_default(self):
446 448 return [os.path.join(get_ipython_dir(), 'nbextensions')]
447 449
448 450 mathjax_url = Unicode("", config=True,
449 451 help="""The url for MathJax.js."""
450 452 )
451 453 def _mathjax_url_default(self):
452 454 if not self.enable_mathjax:
453 455 return u''
454 456 static_url_prefix = self.webapp_settings.get("static_url_prefix",
455 457 url_path_join(self.base_project_url, "static")
456 458 )
457 459
458 460 # try local mathjax, either in nbextensions/mathjax or static/mathjax
459 461 for (url_prefix, search_path) in [
460 462 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
461 463 (static_url_prefix, self.static_file_path),
462 464 ]:
463 465 self.log.debug("searching for local mathjax in %s", search_path)
464 466 try:
465 467 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
466 468 except IOError:
467 469 continue
468 470 else:
469 471 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
470 472 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
471 473 return url
472 474
473 475 # no local mathjax, serve from CDN
474 476 if self.certfile:
475 477 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
476 478 host = u"https://c328740.ssl.cf1.rackcdn.com"
477 479 else:
478 480 host = u"http://cdn.mathjax.org"
479 481
480 482 url = host + u"/mathjax/latest/MathJax.js"
481 483 self.log.info("Using MathJax from CDN: %s", url)
482 484 return url
483 485
484 486 def _mathjax_url_changed(self, name, old, new):
485 487 if new and not self.enable_mathjax:
486 488 # enable_mathjax=False overrides mathjax_url
487 489 self.mathjax_url = u''
488 490 else:
489 491 self.log.info("Using MathJax: %s", new)
490 492
491 493 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
492 494 config=True,
493 495 help='The notebook manager class to use.')
494 496
495 497 trust_xheaders = Bool(False, config=True,
496 498 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
497 499 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
498 500 )
499 501
500 502 def parse_command_line(self, argv=None):
501 503 super(NotebookApp, self).parse_command_line(argv)
502 504
503 505 if self.extra_args:
504 506 arg0 = self.extra_args[0]
505 507 f = os.path.abspath(arg0)
506 508 self.argv.remove(arg0)
507 509 if not os.path.exists(f):
508 510 self.log.critical("No such file or directory: %s", f)
509 511 self.exit(1)
510 512 if os.path.isdir(f):
511 513 self.config.FileNotebookManager.notebook_dir = f
512 514 elif os.path.isfile(f):
513 515 self.file_to_run = f
514 516
515 517 def init_kernel_argv(self):
516 518 """construct the kernel arguments"""
517 519 # Scrub frontend-specific flags
518 520 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
519 521 # Kernel should inherit default config file from frontend
520 522 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
521 523 # Kernel should get *absolute* path to profile directory
522 524 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
523 525
524 526 def init_configurables(self):
525 527 # force Session default to be secure
526 528 default_secure(self.config)
527 529 self.kernel_manager = MappingKernelManager(
528 530 parent=self, log=self.log, kernel_argv=self.kernel_argv,
529 531 connection_dir = self.profile_dir.security_dir,
530 532 )
531 533 kls = import_item(self.notebook_manager_class)
532 534 self.notebook_manager = kls(parent=self, log=self.log)
533 535 self.session_manager = SessionManager(parent=self, log=self.log)
534 536 self.cluster_manager = ClusterManager(parent=self, log=self.log)
535 537 self.cluster_manager.update_profiles()
536 538
537 539 def init_logging(self):
538 540 # This prevents double log messages because tornado use a root logger that
539 541 # self.log is a child of. The logging module dipatches log messages to a log
540 542 # and all of its ancenstors until propagate is set to False.
541 543 self.log.propagate = False
542 544
543 545 # hook up tornado 3's loggers to our app handlers
544 546 for name in ('access', 'application', 'general'):
545 547 logger = logging.getLogger('tornado.%s' % name)
546 548 logger.parent = self.log
547 549 logger.setLevel(self.log.level)
548 550
549 551 def init_webapp(self):
550 552 """initialize tornado webapp and httpserver"""
551 553 self.web_app = NotebookWebApplication(
552 554 self, self.kernel_manager, self.notebook_manager,
553 555 self.cluster_manager, self.session_manager,
554 556 self.log, self.base_project_url, self.webapp_settings
555 557 )
556 558 if self.certfile:
557 559 ssl_options = dict(certfile=self.certfile)
558 560 if self.keyfile:
559 561 ssl_options['keyfile'] = self.keyfile
560 562 else:
561 563 ssl_options = None
562 564 self.web_app.password = self.password
563 565 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
564 566 xheaders=self.trust_xheaders)
565 567 if not self.ip:
566 568 warning = "WARNING: The notebook server is listening on all IP addresses"
567 569 if ssl_options is None:
568 570 self.log.critical(warning + " and not using encryption. This "
569 571 "is not recommended.")
570 572 if not self.password:
571 573 self.log.critical(warning + " and not using authentication. "
572 574 "This is highly insecure and not recommended.")
573 575 success = None
574 576 for port in random_ports(self.port, self.port_retries+1):
575 577 try:
576 578 self.http_server.listen(port, self.ip)
577 579 except socket.error as e:
578 580 if e.errno == errno.EADDRINUSE:
579 581 self.log.info('The port %i is already in use, trying another random port.' % port)
580 582 continue
581 583 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
582 584 self.log.warn("Permission to listen on port %i denied" % port)
583 585 continue
584 586 else:
585 587 raise
586 588 else:
587 589 self.port = port
588 590 success = True
589 591 break
590 592 if not success:
591 593 self.log.critical('ERROR: the notebook server could not be started because '
592 594 'no available port could be found.')
593 595 self.exit(1)
594 596
595 597 def init_signal(self):
596 598 if not sys.platform.startswith('win'):
597 599 signal.signal(signal.SIGINT, self._handle_sigint)
598 600 signal.signal(signal.SIGTERM, self._signal_stop)
599 601 if hasattr(signal, 'SIGUSR1'):
600 602 # Windows doesn't support SIGUSR1
601 603 signal.signal(signal.SIGUSR1, self._signal_info)
602 604 if hasattr(signal, 'SIGINFO'):
603 605 # only on BSD-based systems
604 606 signal.signal(signal.SIGINFO, self._signal_info)
605 607
606 608 def _handle_sigint(self, sig, frame):
607 609 """SIGINT handler spawns confirmation dialog"""
608 610 # register more forceful signal handler for ^C^C case
609 611 signal.signal(signal.SIGINT, self._signal_stop)
610 612 # request confirmation dialog in bg thread, to avoid
611 613 # blocking the App
612 614 thread = threading.Thread(target=self._confirm_exit)
613 615 thread.daemon = True
614 616 thread.start()
615 617
616 618 def _restore_sigint_handler(self):
617 619 """callback for restoring original SIGINT handler"""
618 620 signal.signal(signal.SIGINT, self._handle_sigint)
619 621
620 622 def _confirm_exit(self):
621 623 """confirm shutdown on ^C
622 624
623 625 A second ^C, or answering 'y' within 5s will cause shutdown,
624 626 otherwise original SIGINT handler will be restored.
625 627
626 628 This doesn't work on Windows.
627 629 """
628 630 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
629 631 time.sleep(0.1)
630 632 info = self.log.info
631 633 info('interrupted')
632 634 print(self.notebook_info())
633 635 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
634 636 sys.stdout.flush()
635 637 r,w,x = select.select([sys.stdin], [], [], 5)
636 638 if r:
637 639 line = sys.stdin.readline()
638 640 if line.lower().startswith('y'):
639 641 self.log.critical("Shutdown confirmed")
640 642 ioloop.IOLoop.instance().stop()
641 643 return
642 644 else:
643 645 print("No answer for 5s:", end=' ')
644 646 print("resuming operation...")
645 647 # no answer, or answer is no:
646 648 # set it back to original SIGINT handler
647 649 # use IOLoop.add_callback because signal.signal must be called
648 650 # from main thread
649 651 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
650 652
651 653 def _signal_stop(self, sig, frame):
652 654 self.log.critical("received signal %s, stopping", sig)
653 655 ioloop.IOLoop.instance().stop()
654 656
655 657 def _signal_info(self, sig, frame):
656 658 print(self.notebook_info())
657 659
658 660 def init_components(self):
659 661 """Check the components submodule, and warn if it's unclean"""
660 662 status = submodule.check_submodule_status()
661 663 if status == 'missing':
662 664 self.log.warn("components submodule missing, running `git submodule update`")
663 665 submodule.update_submodules(submodule.ipython_parent())
664 666 elif status == 'unclean':
665 667 self.log.warn("components submodule unclean, you may see 404s on static/components")
666 668 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
667 669
668 670
669 671 @catch_config_error
670 672 def initialize(self, argv=None):
671 673 super(NotebookApp, self).initialize(argv)
672 674 self.init_logging()
673 675 self.init_kernel_argv()
674 676 self.init_configurables()
675 677 self.init_components()
676 678 self.init_webapp()
677 679 self.init_signal()
678 680
679 681 def cleanup_kernels(self):
680 682 """Shutdown all kernels.
681 683
682 684 The kernels will shutdown themselves when this process no longer exists,
683 685 but explicit shutdown allows the KernelManagers to cleanup the connection files.
684 686 """
685 687 self.log.info('Shutting down kernels')
686 688 self.kernel_manager.shutdown_all()
687 689
688 690 def notebook_info(self):
689 691 "Return the current working directory and the server url information"
690 692 info = self.notebook_manager.info_string() + "\n"
691 693 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
692 694 return info + "The IPython Notebook is running at: %s" % self._url
693 695
694 696 def start(self):
695 697 """ Start the IPython Notebook server app, after initialization
696 698
697 699 This method takes no arguments so all configuration and initialization
698 700 must be done prior to calling this method."""
699 701 ip = self.ip if self.ip else '[all ip addresses on your system]'
700 702 proto = 'https' if self.certfile else 'http'
701 703 info = self.log.info
702 704 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
703 705 self.base_project_url)
704 706 for line in self.notebook_info().split("\n"):
705 707 info(line)
706 708 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
707 709
708 710 if self.open_browser or self.file_to_run:
709 711 ip = self.ip or localhost()
710 712 try:
711 713 browser = webbrowser.get(self.browser or None)
712 714 except webbrowser.Error as e:
713 715 self.log.warn('No web browser found: %s.' % e)
714 716 browser = None
715 717
716 718 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
717 719 f = self.file_to_run
718 720 if f:
719 721 if f.startswith(nbdir):
720 722 f = f[len(nbdir):]
721 723 else:
722 724 self.log.warn(
723 725 "Probably won't be able to open notebook %s "
724 726 "because it is not in notebook_dir %s",
725 727 f, nbdir,
726 728 )
727 729
728 730 if os.path.isfile(self.file_to_run):
729 731 url = url_path_join('notebooks', f)
730 732 else:
731 733 url = url_path_join('tree', f)
732 734 if browser:
733 735 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
734 736 self.port, self.base_project_url, url), new=2)
735 737 threading.Thread(target=b).start()
736 738 try:
737 739 ioloop.IOLoop.instance().start()
738 740 except KeyboardInterrupt:
739 741 info("Interrupted...")
740 742 finally:
741 743 self.cleanup_kernels()
742 744
743 745
744 746 #-----------------------------------------------------------------------------
745 747 # Main entry point
746 748 #-----------------------------------------------------------------------------
747 749
748 750 launch_new_instance = NotebookApp.launch_instance
749 751
@@ -1,414 +1,414
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import io
21 21 import itertools
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from tornado import web
27 27
28 28 from .nbmanager import NotebookManager
29 29 from IPython.nbformat import current
30 30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 31 from IPython.utils import tz
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Classes
35 35 #-----------------------------------------------------------------------------
36 36
37 37 class FileNotebookManager(NotebookManager):
38 38
39 39 save_script = Bool(False, config=True,
40 40 help="""Automatically create a Python script when saving the notebook.
41 41
42 42 For easier use of import, %run and %load across notebooks, a
43 43 <notebook-name>.py script will be created next to any
44 44 <notebook-name>.ipynb on each save. This can also be set with the
45 45 short `--script` flag.
46 46 """
47 47 )
48 48
49 49 checkpoint_dir = Unicode(config=True,
50 50 help="""The location in which to keep notebook checkpoints
51 51
52 52 By default, it is notebook-dir/.ipynb_checkpoints
53 53 """
54 54 )
55 55 def _checkpoint_dir_default(self):
56 56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 57
58 58 def _checkpoint_dir_changed(self, name, old, new):
59 59 """do a bit of validation of the checkpoint dir"""
60 60 if not os.path.isabs(new):
61 61 # If we receive a non-absolute path, make it absolute.
62 62 abs_new = os.path.abspath(new)
63 63 self.checkpoint_dir = abs_new
64 64 return
65 65 if os.path.exists(new) and not os.path.isdir(new):
66 66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 67 if not os.path.exists(new):
68 68 self.log.info("Creating checkpoint dir %s", new)
69 69 try:
70 70 os.mkdir(new)
71 71 except:
72 72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 73
74 74 def get_notebook_names(self, path=''):
75 75 """List all notebook names in the notebook dir and path."""
76 76 path = path.strip('/')
77 77 if not os.path.isdir(self.get_os_path(path=path)):
78 78 raise web.HTTPError(404, 'Directory not found: ' + path)
79 79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 80 names = [os.path.basename(name)
81 81 for name in names]
82 82 return names
83 83
84 84 def increment_filename(self, basename, path='', ext='.ipynb'):
85 85 """Return a non-used filename of the form basename<int>."""
86 86 path = path.strip('/')
87 87 for i in itertools.count():
88 88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 89 os_path = self.get_os_path(name, path)
90 90 if not os.path.isfile(os_path):
91 91 break
92 92 return name
93 93
94 94 def path_exists(self, path):
95 95 """Does the API-style path (directory) actually exist?
96 96
97 97 Parameters
98 98 ----------
99 99 path : string
100 100 The path to check. This is an API path (`/` separated,
101 101 relative to base notebook-dir).
102 102
103 103 Returns
104 104 -------
105 105 exists : bool
106 106 Whether the path is indeed a directory.
107 107 """
108 108 path = path.strip('/')
109 109 os_path = self.get_os_path(path=path)
110 110 return os.path.isdir(os_path)
111 111
112 112 def get_os_path(self, name=None, path=''):
113 113 """Given a notebook name and a URL path, return its file system
114 114 path.
115 115
116 116 Parameters
117 117 ----------
118 118 name : string
119 119 The name of a notebook file with the .ipynb extension
120 120 path : string
121 121 The relative URL path (with '/' as separator) to the named
122 122 notebook.
123 123
124 124 Returns
125 125 -------
126 126 path : string
127 127 A file system path that combines notebook_dir (location where
128 128 server started), the relative path, and the filename with the
129 129 current operating system's url.
130 130 """
131 131 parts = path.strip('/').split('/')
132 132 parts = [p for p in parts if p != ''] # remove duplicate splits
133 133 if name is not None:
134 134 parts.append(name)
135 135 path = os.path.join(self.notebook_dir, *parts)
136 136 return path
137 137
138 138 def notebook_exists(self, name, path=''):
139 139 """Returns a True if the notebook exists. Else, returns False.
140 140
141 141 Parameters
142 142 ----------
143 143 name : string
144 144 The name of the notebook you are checking.
145 145 path : string
146 146 The relative path to the notebook (with '/' as separator)
147 147
148 148 Returns
149 149 -------
150 150 bool
151 151 """
152 152 path = path.strip('/')
153 153 nbpath = self.get_os_path(name, path=path)
154 154 return os.path.isfile(nbpath)
155 155
156 156 def list_notebooks(self, path):
157 157 """Returns a list of dictionaries that are the standard model
158 158 for all notebooks in the relative 'path'.
159 159
160 160 Parameters
161 161 ----------
162 162 path : str
163 163 the URL path that describes the relative path for the
164 164 listed notebooks
165 165
166 166 Returns
167 167 -------
168 168 notebooks : list of dicts
169 169 a list of the notebook models without 'content'
170 170 """
171 171 path = path.strip('/')
172 172 notebook_names = self.get_notebook_names(path)
173 173 notebooks = []
174 174 for name in notebook_names:
175 175 model = self.get_notebook_model(name, path, content=False)
176 176 notebooks.append(model)
177 177 notebooks = sorted(notebooks, key=lambda item: item['name'])
178 178 return notebooks
179 179
180 180 def get_notebook_model(self, name, path='', content=True):
181 """ Takes a path and name for a notebook and returns it's model
181 """ Takes a path and name for a notebook and returns its model
182 182
183 183 Parameters
184 184 ----------
185 185 name : str
186 186 the name of the notebook
187 187 path : str
188 188 the URL path that describes the relative path for
189 189 the notebook
190 190
191 191 Returns
192 192 -------
193 193 model : dict
194 194 the notebook model. If contents=True, returns the 'contents'
195 195 dict in the model as well.
196 196 """
197 197 path = path.strip('/')
198 198 if not self.notebook_exists(name=name, path=path):
199 199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
200 200 os_path = self.get_os_path(name, path)
201 201 info = os.stat(os_path)
202 202 last_modified = tz.utcfromtimestamp(info.st_mtime)
203 203 created = tz.utcfromtimestamp(info.st_ctime)
204 204 # Create the notebook model.
205 205 model ={}
206 206 model['name'] = name
207 207 model['path'] = path
208 208 model['last_modified'] = last_modified
209 209 model['created'] = created
210 210 if content is True:
211 211 with io.open(os_path, 'r', encoding='utf-8') as f:
212 212 try:
213 213 nb = current.read(f, u'json')
214 214 except Exception as e:
215 215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
216 216 model['content'] = nb
217 217 return model
218 218
219 219 def save_notebook_model(self, model, name='', path=''):
220 220 """Save the notebook model and return the model with no content."""
221 221 path = path.strip('/')
222 222
223 223 if 'content' not in model:
224 224 raise web.HTTPError(400, u'No notebook JSON data provided')
225 225
226 226 # One checkpoint should always exist
227 227 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
228 228 self.create_checkpoint(name, path)
229 229
230 230 new_path = model.get('path', path).strip('/')
231 231 new_name = model.get('name', name)
232 232
233 233 if path != new_path or name != new_name:
234 234 self.rename_notebook(name, path, new_name, new_path)
235 235
236 236 # Save the notebook file
237 237 os_path = self.get_os_path(new_name, new_path)
238 238 nb = current.to_notebook_json(model['content'])
239 239 if 'name' in nb['metadata']:
240 240 nb['metadata']['name'] = u''
241 241 try:
242 242 self.log.debug("Autosaving notebook %s", os_path)
243 243 with io.open(os_path, 'w', encoding='utf-8') as f:
244 244 current.write(nb, f, u'json')
245 245 except Exception as e:
246 246 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
247 247
248 248 # Save .py script as well
249 249 if self.save_script:
250 250 py_path = os.path.splitext(os_path)[0] + '.py'
251 251 self.log.debug("Writing script %s", py_path)
252 252 try:
253 253 with io.open(py_path, 'w', encoding='utf-8') as f:
254 254 current.write(nb, f, u'py')
255 255 except Exception as e:
256 256 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
257 257
258 258 model = self.get_notebook_model(new_name, new_path, content=False)
259 259 return model
260 260
261 261 def update_notebook_model(self, model, name, path=''):
262 262 """Update the notebook's path and/or name"""
263 263 path = path.strip('/')
264 264 new_name = model.get('name', name)
265 265 new_path = model.get('path', path).strip('/')
266 266 if path != new_path or name != new_name:
267 267 self.rename_notebook(name, path, new_name, new_path)
268 268 model = self.get_notebook_model(new_name, new_path, content=False)
269 269 return model
270 270
271 271 def delete_notebook_model(self, name, path=''):
272 272 """Delete notebook by name and path."""
273 273 path = path.strip('/')
274 274 os_path = self.get_os_path(name, path)
275 275 if not os.path.isfile(os_path):
276 276 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
277 277
278 278 # clear checkpoints
279 279 for checkpoint in self.list_checkpoints(name, path):
280 280 checkpoint_id = checkpoint['id']
281 281 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
282 282 if os.path.isfile(cp_path):
283 283 self.log.debug("Unlinking checkpoint %s", cp_path)
284 284 os.unlink(cp_path)
285 285
286 286 self.log.debug("Unlinking notebook %s", os_path)
287 287 os.unlink(os_path)
288 288
289 289 def rename_notebook(self, old_name, old_path, new_name, new_path):
290 290 """Rename a notebook."""
291 291 old_path = old_path.strip('/')
292 292 new_path = new_path.strip('/')
293 293 if new_name == old_name and new_path == old_path:
294 294 return
295 295
296 296 new_os_path = self.get_os_path(new_name, new_path)
297 297 old_os_path = self.get_os_path(old_name, old_path)
298 298
299 299 # Should we proceed with the move?
300 300 if os.path.isfile(new_os_path):
301 301 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
302 302 if self.save_script:
303 303 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
304 304 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
305 305 if os.path.isfile(new_py_path):
306 306 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
307 307
308 308 # Move the notebook file
309 309 try:
310 310 os.rename(old_os_path, new_os_path)
311 311 except Exception as e:
312 312 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
313 313
314 314 # Move the checkpoints
315 315 old_checkpoints = self.list_checkpoints(old_name, old_path)
316 316 for cp in old_checkpoints:
317 317 checkpoint_id = cp['id']
318 318 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
319 319 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
320 320 if os.path.isfile(old_cp_path):
321 321 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
322 322 os.rename(old_cp_path, new_cp_path)
323 323
324 324 # Move the .py script
325 325 if self.save_script:
326 326 os.rename(old_py_path, new_py_path)
327 327
328 328 # Checkpoint-related utilities
329 329
330 330 def get_checkpoint_path(self, checkpoint_id, name, path=''):
331 331 """find the path to a checkpoint"""
332 332 path = path.strip('/')
333 333 basename, _ = os.path.splitext(name)
334 334 filename = u"{name}-{checkpoint_id}{ext}".format(
335 335 name=basename,
336 336 checkpoint_id=checkpoint_id,
337 337 ext=self.filename_ext,
338 338 )
339 339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
340 340 return cp_path
341 341
342 342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
343 343 """construct the info dict for a given checkpoint"""
344 344 path = path.strip('/')
345 345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
346 346 stats = os.stat(cp_path)
347 347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
348 348 info = dict(
349 349 id = checkpoint_id,
350 350 last_modified = last_modified,
351 351 )
352 352 return info
353 353
354 354 # public checkpoint API
355 355
356 356 def create_checkpoint(self, name, path=''):
357 357 """Create a checkpoint from the current state of a notebook"""
358 358 path = path.strip('/')
359 359 nb_path = self.get_os_path(name, path)
360 360 # only the one checkpoint ID:
361 361 checkpoint_id = u"checkpoint"
362 362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
363 363 self.log.debug("creating checkpoint for notebook %s", name)
364 364 if not os.path.exists(self.checkpoint_dir):
365 365 os.mkdir(self.checkpoint_dir)
366 366 shutil.copy2(nb_path, cp_path)
367 367
368 368 # return the checkpoint info
369 369 return self.get_checkpoint_model(checkpoint_id, name, path)
370 370
371 371 def list_checkpoints(self, name, path=''):
372 372 """list the checkpoints for a given notebook
373 373
374 374 This notebook manager currently only supports one checkpoint per notebook.
375 375 """
376 376 path = path.strip('/')
377 377 checkpoint_id = "checkpoint"
378 378 path = self.get_checkpoint_path(checkpoint_id, name, path)
379 379 if not os.path.exists(path):
380 380 return []
381 381 else:
382 382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
383 383
384 384
385 385 def restore_checkpoint(self, checkpoint_id, name, path=''):
386 386 """restore a notebook to a checkpointed state"""
387 387 path = path.strip('/')
388 388 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
389 389 nb_path = self.get_os_path(name, path)
390 390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
391 391 if not os.path.isfile(cp_path):
392 392 self.log.debug("checkpoint file does not exist: %s", cp_path)
393 393 raise web.HTTPError(404,
394 394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
395 395 )
396 396 # ensure notebook is readable (never restore from an unreadable notebook)
397 397 with io.open(cp_path, 'r', encoding='utf-8') as f:
398 398 nb = current.read(f, u'json')
399 399 shutil.copy2(cp_path, nb_path)
400 400 self.log.debug("copying %s -> %s", cp_path, nb_path)
401 401
402 402 def delete_checkpoint(self, checkpoint_id, name, path=''):
403 403 """delete a notebook's checkpoint"""
404 404 path = path.strip('/')
405 405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
406 406 if not os.path.isfile(cp_path):
407 407 raise web.HTTPError(404,
408 408 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
409 409 )
410 410 self.log.debug("unlinking %s", cp_path)
411 411 os.unlink(cp_path)
412 412
413 413 def info_string(self):
414 414 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,281 +1,280
1 1 """Tornado handlers for the notebooks web service.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import json
20 20
21 21 from tornado import web
22 22
23 23 from IPython.html.utils import url_path_join, url_escape
24 24 from IPython.utils.jsonutil import date_default
25 25
26 from IPython.html.base.handlers import IPythonHandler, json_errors
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 notebook_path_regex, path_regex,
28 notebook_name_regex)
27 29
28 30 #-----------------------------------------------------------------------------
29 31 # Notebook web service handlers
30 32 #-----------------------------------------------------------------------------
31 33
32 34
33 35 class NotebookHandler(IPythonHandler):
34 36
35 37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
36 38
37 39 def notebook_location(self, name, path=''):
38 40 """Return the full URL location of a notebook based.
39 41
40 42 Parameters
41 43 ----------
42 44 name : unicode
43 45 The base name of the notebook, such as "foo.ipynb".
44 46 path : unicode
45 47 The URL path of the notebook.
46 48 """
47 49 return url_escape(url_path_join(
48 50 self.base_project_url, 'api', 'notebooks', path, name
49 51 ))
50 52
51 53 def _finish_model(self, model, location=True):
52 54 """Finish a JSON request with a model, setting relevant headers, etc."""
53 55 if location:
54 56 location = self.notebook_location(model['name'], model['path'])
55 57 self.set_header('Location', location)
56 58 self.set_header('Last-Modified', model['last_modified'])
57 59 self.finish(json.dumps(model, default=date_default))
58 60
59 61 @web.authenticated
60 62 @json_errors
61 63 def get(self, path='', name=None):
62 64 """Return a Notebook or list of notebooks.
63 65
64 66 * GET with path and no notebook name lists notebooks in a directory
65 67 * GET with path and notebook name returns notebook JSON
66 68 """
67 69 nbm = self.notebook_manager
68 70 # Check to see if a notebook name was given
69 71 if name is None:
70 72 # List notebooks in 'path'
71 73 notebooks = nbm.list_notebooks(path)
72 74 self.finish(json.dumps(notebooks, default=date_default))
73 75 return
74 76 # get and return notebook representation
75 77 model = nbm.get_notebook_model(name, path)
76 78 self._finish_model(model, location=False)
77 79
78 80 @web.authenticated
79 81 @json_errors
80 82 def patch(self, path='', name=None):
81 83 """PATCH renames a notebook without re-uploading content."""
82 84 nbm = self.notebook_manager
83 85 if name is None:
84 86 raise web.HTTPError(400, u'Notebook name missing')
85 87 model = self.get_json_body()
86 88 if model is None:
87 89 raise web.HTTPError(400, u'JSON body missing')
88 90 model = nbm.update_notebook_model(model, name, path)
89 91 self._finish_model(model)
90 92
91 93 def _copy_notebook(self, copy_from, path, copy_to=None):
92 94 """Copy a notebook in path, optionally specifying the new name.
93 95
94 96 Only support copying within the same directory.
95 97 """
96 98 self.log.info(u"Copying notebook from %s/%s to %s/%s",
97 99 path, copy_from,
98 100 path, copy_to or '',
99 101 )
100 102 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
101 103 self.set_status(201)
102 104 self._finish_model(model)
103 105
104 106 def _upload_notebook(self, model, path, name=None):
105 107 """Upload a notebook
106 108
107 109 If name specified, create it in path/name.
108 110 """
109 111 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
110 112 if name:
111 113 model['name'] = name
112 114
113 115 model = self.notebook_manager.create_notebook_model(model, path)
114 116 self.set_status(201)
115 117 self._finish_model(model)
116 118
117 119 def _create_empty_notebook(self, path, name=None):
118 120 """Create an empty notebook in path
119 121
120 122 If name specified, create it in path/name.
121 123 """
122 124 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
123 125 model = {}
124 126 if name:
125 127 model['name'] = name
126 128 model = self.notebook_manager.create_notebook_model(model, path=path)
127 129 self.set_status(201)
128 130 self._finish_model(model)
129 131
130 132 def _save_notebook(self, model, path, name):
131 133 """Save an existing notebook."""
132 134 self.log.info(u"Saving notebook at %s/%s", path, name)
133 135 model = self.notebook_manager.save_notebook_model(model, name, path)
134 136 if model['path'] != path.strip('/') or model['name'] != name:
135 137 # a rename happened, set Location header
136 138 location = True
137 139 else:
138 140 location = False
139 141 self._finish_model(model, location)
140 142
141 143 @web.authenticated
142 144 @json_errors
143 145 def post(self, path='', name=None):
144 146 """Create a new notebook in the specified path.
145 147
146 148 POST creates new notebooks. The server always decides on the notebook name.
147 149
148 150 POST /api/notebooks/path
149 151 New untitled notebook in path. If content specified, upload a
150 152 notebook, otherwise start empty.
151 153 POST /api/notebooks/path?copy=OtherNotebook.ipynb
152 154 New copy of OtherNotebook in path
153 155 """
154 156
155 157 if name is not None:
156 158 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
157 159
158 160 model = self.get_json_body()
159 161
160 162 if model is not None:
161 163 copy_from = model.get('copy_from')
162 164 if copy_from:
163 165 if model.get('content'):
164 166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
165 167 self._copy_notebook(copy_from, path)
166 168 else:
167 169 self._upload_notebook(model, path)
168 170 else:
169 171 self._create_empty_notebook(path)
170 172
171 173 @web.authenticated
172 174 @json_errors
173 175 def put(self, path='', name=None):
174 176 """Saves the notebook in the location specified by name and path.
175 177
176 178 PUT is very similar to POST, but the requester specifies the name,
177 179 whereas with POST, the server picks the name.
178 180
179 181 PUT /api/notebooks/path/Name.ipynb
180 182 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
181 183 in `content` key of JSON request body. If content is not specified,
182 184 create a new empty notebook.
183 185 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
184 186 Copy OtherNotebook to Name
185 187 """
186 188 if name is None:
187 189 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
188 190
189 191 model = self.get_json_body()
190 192 if model:
191 193 copy_from = model.get('copy_from')
192 194 if copy_from:
193 195 if model.get('content'):
194 196 raise web.HTTPError(400, "Can't upload and copy at the same time.")
195 197 self._copy_notebook(copy_from, path, name)
196 198 elif self.notebook_manager.notebook_exists(name, path):
197 199 self._save_notebook(model, path, name)
198 200 else:
199 201 self._upload_notebook(model, path, name)
200 202 else:
201 203 self._create_empty_notebook(path, name)
202 204
203 205 @web.authenticated
204 206 @json_errors
205 207 def delete(self, path='', name=None):
206 208 """delete the notebook in the given notebook path"""
207 209 nbm = self.notebook_manager
208 210 nbm.delete_notebook_model(name, path)
209 211 self.set_status(204)
210 212 self.finish()
211 213
212 214
213 215 class NotebookCheckpointsHandler(IPythonHandler):
214 216
215 217 SUPPORTED_METHODS = ('GET', 'POST')
216 218
217 219 @web.authenticated
218 220 @json_errors
219 221 def get(self, path='', name=None):
220 222 """get lists checkpoints for a notebook"""
221 223 nbm = self.notebook_manager
222 224 checkpoints = nbm.list_checkpoints(name, path)
223 225 data = json.dumps(checkpoints, default=date_default)
224 226 self.finish(data)
225 227
226 228 @web.authenticated
227 229 @json_errors
228 230 def post(self, path='', name=None):
229 231 """post creates a new checkpoint"""
230 232 nbm = self.notebook_manager
231 233 checkpoint = nbm.create_checkpoint(name, path)
232 234 data = json.dumps(checkpoint, default=date_default)
233 235 location = url_path_join(self.base_project_url, 'api/notebooks',
234 236 path, name, 'checkpoints', checkpoint['id'])
235 237 self.set_header('Location', url_escape(location))
236 238 self.set_status(201)
237 239 self.finish(data)
238 240
239 241
240 242 class ModifyNotebookCheckpointsHandler(IPythonHandler):
241 243
242 244 SUPPORTED_METHODS = ('POST', 'DELETE')
243 245
244 246 @web.authenticated
245 247 @json_errors
246 248 def post(self, path, name, checkpoint_id):
247 249 """post restores a notebook from a checkpoint"""
248 250 nbm = self.notebook_manager
249 251 nbm.restore_checkpoint(checkpoint_id, name, path)
250 252 self.set_status(204)
251 253 self.finish()
252 254
253 255 @web.authenticated
254 256 @json_errors
255 257 def delete(self, path, name, checkpoint_id):
256 258 """delete clears a checkpoint for a given notebook"""
257 259 nbm = self.notebook_manager
258 260 nbm.delete_checkpoint(checkpoint_id, name, path)
259 261 self.set_status(204)
260 262 self.finish()
261 263
262 264 #-----------------------------------------------------------------------------
263 265 # URL to handler mappings
264 266 #-----------------------------------------------------------------------------
265 267
266 268
267 _path_regex = r"(?P<path>(?:/.*)*)"
268 269 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
269 _notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
270 _notebook_path_regex = "%s/%s" % (_path_regex, _notebook_name_regex)
271 270
272 271 default_handlers = [
273 (r"/api/notebooks%s/checkpoints" % _notebook_path_regex, NotebookCheckpointsHandler),
274 (r"/api/notebooks%s/checkpoints/%s" % (_notebook_path_regex, _checkpoint_id_regex),
272 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
273 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
275 274 ModifyNotebookCheckpointsHandler),
276 (r"/api/notebooks%s" % _notebook_path_regex, NotebookHandler),
277 (r"/api/notebooks%s" % _path_regex, NotebookHandler),
275 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
276 (r"/api/notebooks%s" % path_regex, NotebookHandler),
278 277 ]
279 278
280 279
281 280
@@ -1,90 +1,90
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // CellToolbar Example
10 10 //============================================================================
11 11
12 12 (function(IPython) {
13 13 "use strict";
14 14
15 15 var CellToolbar = IPython.CellToolbar;
16 16 var raw_cell_preset = [];
17 17 var utils = IPython.utils;
18 18
19 19 var select_type = CellToolbar.utils.select_ui_generator([
20 20 ["None", "-"],
21 21 ["LaTeX", "text/latex"],
22 22 ["reST", "text/restructuredtext"],
23 23 ["HTML", "text/html"],
24 24 ["Markdown", "text/markdown"],
25 ["Python", "application/x-python"],
25 ["Python", "text/x-python"],
26 26 ["Custom", "dialog"],
27 27
28 28 ],
29 29 // setter
30 30 function(cell, value) {
31 31 if (value === "-") {
32 32 delete cell.metadata.raw_mimetype;
33 33 } else if (value === 'dialog'){
34 34 var dialog = $('<div/>').append(
35 35 $("<p/>")
36 36 .html("Set the MIME type of the raw cell:")
37 37 ).append(
38 38 $("<br/>")
39 39 ).append(
40 40 $('<input/>').attr('type','text').attr('size','25')
41 41 .val(cell.metadata.raw_mimetype || "-")
42 42 );
43 43 IPython.dialog.modal({
44 44 title: "Raw Cell MIME Type",
45 45 body: dialog,
46 46 buttons : {
47 47 "Cancel": {},
48 48 "OK": {
49 49 class: "btn-primary",
50 50 click: function () {
51 51 console.log(cell);
52 52 cell.metadata.raw_mimetype = $(this).find('input').val();
53 53 console.log(cell.metadata);
54 54 }
55 55 }
56 56 },
57 57 open : function (event, ui) {
58 58 var that = $(this);
59 59 // Upon ENTER, click the OK button.
60 60 that.find('input[type="text"]').keydown(function (event, ui) {
61 61 if (event.which === utils.keycodes.ENTER) {
62 62 that.find('.btn-primary').first().click();
63 63 return false;
64 64 }
65 65 });
66 66 that.find('input[type="text"]').focus().select();
67 67 }
68 68 });
69 69 } else {
70 70 cell.metadata.raw_mimetype = value;
71 71 }
72 72 },
73 73 //getter
74 74 function(cell) {
75 75 return cell.metadata.raw_mimetype || "";
76 76 },
77 77 // name
78 78 "Raw NBConvert Format",
79 79 // cell_types
80 80 ["raw"]
81 81 );
82 82
83 83 CellToolbar.register_callback('raw_cell.select', select_type);
84 84
85 85 raw_cell_preset.push('raw_cell.select');
86 86
87 87 CellToolbar.register_preset('Raw Cell Format', raw_cell_preset);
88 88 console.log('Raw Cell Format toolbar preset loaded.');
89 89
90 90 }(IPython));
@@ -1,309 +1,322
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // MenuBar
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule MenuBar
16 16 */
17 17
18 18
19 19 var IPython = (function (IPython) {
20 20 "use strict";
21 21
22 22 var utils = IPython.utils;
23 23
24 24 /**
25 25 * A MenuBar Class to generate the menubar of IPython notebook
26 26 * @Class MenuBar
27 27 *
28 28 * @constructor
29 29 *
30 30 *
31 31 * @param selector {string} selector for the menubar element in DOM
32 32 * @param {object} [options]
33 33 * @param [options.baseProjectUrl] {String} String to use for the
34 34 * Base Project url, default would be to inspect
35 35 * $('body').data('baseProjectUrl');
36 36 * does not support change for now is set through this option
37 37 */
38 38 var MenuBar = function (selector, options) {
39 39 options = options || {};
40 40 if (options.baseProjectUrl !== undefined) {
41 41 this._baseProjectUrl = options.baseProjectUrl;
42 42 }
43 43 this.selector = selector;
44 44 if (this.selector !== undefined) {
45 45 this.element = $(selector);
46 46 this.style();
47 47 this.bind_events();
48 48 }
49 49 };
50 50
51 51 MenuBar.prototype.baseProjectUrl = function(){
52 52 return this._baseProjectUrl || $('body').data('baseProjectUrl');
53 53 };
54 54
55 55 MenuBar.prototype.notebookPath = function() {
56 56 var path = $('body').data('notebookPath');
57 57 path = decodeURIComponent(path);
58 58 return path;
59 59 };
60 60
61 61 MenuBar.prototype.style = function () {
62 62 this.element.addClass('border-box-sizing');
63 63 this.element.find("li").click(function (event, ui) {
64 64 // The selected cell loses focus when the menu is entered, so we
65 65 // re-select it upon selection.
66 66 var i = IPython.notebook.get_selected_index();
67 67 IPython.notebook.select(i);
68 68 }
69 69 );
70 70 };
71 71
72 MenuBar.prototype._nbconvert = function (format, download) {
73 download = download || false;
74 var notebook_name = IPython.notebook.get_notebook_name();
75 if (IPython.notebook.dirty) {
76 IPython.notebook.save_notebook({async : false});
77 }
78 var url = utils.url_path_join(
79 this.baseProjectUrl(),
80 'nbconvert',
81 format,
82 this.notebookPath(),
83 notebook_name + '.ipynb'
84 ) + "?download=" + download.toString();
85
86 window.open(url);
87 }
72 88
73 89 MenuBar.prototype.bind_events = function () {
74 90 // File
75 91 var that = this;
76 92 this.element.find('#new_notebook').click(function () {
77 93 IPython.notebook.new_notebook();
78 94 });
79 95 this.element.find('#open_notebook').click(function () {
80 96 window.open(utils.url_join_encode(
81 97 that.baseProjectUrl(),
82 98 'tree',
83 99 that.notebookPath()
84 100 ));
85 101 });
86 102 this.element.find('#copy_notebook').click(function () {
87 103 IPython.notebook.copy_notebook();
88 104 return false;
89 105 });
90 106 this.element.find('#download_ipynb').click(function () {
91 107 var notebook_name = IPython.notebook.get_notebook_name();
92 108 if (IPython.notebook.dirty) {
93 109 IPython.notebook.save_notebook({async : false});
94 110 }
95 111
96 112 var url = utils.url_join_encode(
97 113 that.baseProjectUrl(),
98 114 'files',
99 115 that.notebookPath(),
100 116 notebook_name + '.ipynb'
101 117 );
102 118 window.location.assign(url);
103 119 });
104 120
105 /* FIXME: download-as-py doesn't work right now
106 * We will need nbconvert hooked up to get this back
121 this.element.find('#print_preview').click(function () {
122 that._nbconvert('html', false);
123 });
107 124
108 125 this.element.find('#download_py').click(function () {
109 var notebook_name = IPython.notebook.get_notebook_name();
110 if (IPython.notebook.dirty) {
111 IPython.notebook.save_notebook({async : false});
112 }
113 var url = utils.url_path_join(
114 that.baseProjectUrl(),
115 'api/notebooks',
116 that.notebookPath(),
117 notebook_name + '.ipynb?format=py&download=True'
118 );
119 window.location.assign(url);
126 that._nbconvert('python', true);
120 127 });
121 128
122 */
129 this.element.find('#download_html').click(function () {
130 that._nbconvert('html', true);
131 });
132
133 this.element.find('#download_rst').click(function () {
134 that._nbconvert('rst', true);
135 });
123 136
124 137 this.element.find('#rename_notebook').click(function () {
125 138 IPython.save_widget.rename_notebook();
126 139 });
127 140 this.element.find('#save_checkpoint').click(function () {
128 141 IPython.notebook.save_checkpoint();
129 142 });
130 143 this.element.find('#restore_checkpoint').click(function () {
131 144 });
132 145 this.element.find('#kill_and_exit').click(function () {
133 146 IPython.notebook.session.delete();
134 147 setTimeout(function(){
135 148 // allow closing of new tabs in Chromium, impossible in FF
136 149 window.open('', '_self', '');
137 150 window.close();
138 151 }, 500);
139 152 });
140 153 // Edit
141 154 this.element.find('#cut_cell').click(function () {
142 155 IPython.notebook.cut_cell();
143 156 });
144 157 this.element.find('#copy_cell').click(function () {
145 158 IPython.notebook.copy_cell();
146 159 });
147 160 this.element.find('#delete_cell').click(function () {
148 161 IPython.notebook.delete_cell();
149 162 });
150 163 this.element.find('#undelete_cell').click(function () {
151 164 IPython.notebook.undelete();
152 165 });
153 166 this.element.find('#split_cell').click(function () {
154 167 IPython.notebook.split_cell();
155 168 });
156 169 this.element.find('#merge_cell_above').click(function () {
157 170 IPython.notebook.merge_cell_above();
158 171 });
159 172 this.element.find('#merge_cell_below').click(function () {
160 173 IPython.notebook.merge_cell_below();
161 174 });
162 175 this.element.find('#move_cell_up').click(function () {
163 176 IPython.notebook.move_cell_up();
164 177 });
165 178 this.element.find('#move_cell_down').click(function () {
166 179 IPython.notebook.move_cell_down();
167 180 });
168 181 this.element.find('#select_previous').click(function () {
169 182 IPython.notebook.select_prev();
170 183 });
171 184 this.element.find('#select_next').click(function () {
172 185 IPython.notebook.select_next();
173 186 });
174 187 this.element.find('#edit_nb_metadata').click(function () {
175 188 IPython.notebook.edit_metadata();
176 189 });
177 190
178 191 // View
179 192 this.element.find('#toggle_header').click(function () {
180 193 $('div#header').toggle();
181 194 IPython.layout_manager.do_resize();
182 195 });
183 196 this.element.find('#toggle_toolbar').click(function () {
184 197 $('div#maintoolbar').toggle();
185 198 IPython.layout_manager.do_resize();
186 199 });
187 200 // Insert
188 201 this.element.find('#insert_cell_above').click(function () {
189 202 IPython.notebook.insert_cell_above('code');
190 203 });
191 204 this.element.find('#insert_cell_below').click(function () {
192 205 IPython.notebook.insert_cell_below('code');
193 206 });
194 207 // Cell
195 208 this.element.find('#run_cell').click(function () {
196 209 IPython.notebook.execute_selected_cell();
197 210 });
198 211 this.element.find('#run_cell_in_place').click(function () {
199 212 IPython.notebook.execute_selected_cell({terminal:true});
200 213 });
201 214 this.element.find('#run_all_cells').click(function () {
202 215 IPython.notebook.execute_all_cells();
203 216 });
204 217 this.element.find('#run_all_cells_above').click(function () {
205 218 IPython.notebook.execute_cells_above();
206 219 });
207 220 this.element.find('#run_all_cells_below').click(function () {
208 221 IPython.notebook.execute_cells_below();
209 222 });
210 223 this.element.find('#to_code').click(function () {
211 224 IPython.notebook.to_code();
212 225 });
213 226 this.element.find('#to_markdown').click(function () {
214 227 IPython.notebook.to_markdown();
215 228 });
216 229 this.element.find('#to_raw').click(function () {
217 230 IPython.notebook.to_raw();
218 231 });
219 232 this.element.find('#to_heading1').click(function () {
220 233 IPython.notebook.to_heading(undefined, 1);
221 234 });
222 235 this.element.find('#to_heading2').click(function () {
223 236 IPython.notebook.to_heading(undefined, 2);
224 237 });
225 238 this.element.find('#to_heading3').click(function () {
226 239 IPython.notebook.to_heading(undefined, 3);
227 240 });
228 241 this.element.find('#to_heading4').click(function () {
229 242 IPython.notebook.to_heading(undefined, 4);
230 243 });
231 244 this.element.find('#to_heading5').click(function () {
232 245 IPython.notebook.to_heading(undefined, 5);
233 246 });
234 247 this.element.find('#to_heading6').click(function () {
235 248 IPython.notebook.to_heading(undefined, 6);
236 249 });
237 250 this.element.find('#toggle_output').click(function () {
238 251 IPython.notebook.toggle_output();
239 252 });
240 253 this.element.find('#collapse_all_output').click(function () {
241 254 IPython.notebook.collapse_all_output();
242 255 });
243 256 this.element.find('#scroll_all_output').click(function () {
244 257 IPython.notebook.scroll_all_output();
245 258 });
246 259 this.element.find('#expand_all_output').click(function () {
247 260 IPython.notebook.expand_all_output();
248 261 });
249 262 this.element.find('#clear_all_output').click(function () {
250 263 IPython.notebook.clear_all_output();
251 264 });
252 265 // Kernel
253 266 this.element.find('#int_kernel').click(function () {
254 267 IPython.notebook.session.interrupt_kernel();
255 268 });
256 269 this.element.find('#restart_kernel').click(function () {
257 270 IPython.notebook.restart_kernel();
258 271 });
259 272 // Help
260 273 this.element.find('#keyboard_shortcuts').click(function () {
261 274 IPython.quick_help.show_keyboard_shortcuts();
262 275 });
263 276
264 277 this.update_restore_checkpoint(null);
265 278
266 279 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
267 280 that.update_restore_checkpoint(IPython.notebook.checkpoints);
268 281 });
269 282
270 283 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
271 284 that.update_restore_checkpoint(IPython.notebook.checkpoints);
272 285 });
273 286 };
274 287
275 288 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
276 289 var ul = this.element.find("#restore_checkpoint").find("ul");
277 290 ul.empty();
278 291 if (!checkpoints || checkpoints.length === 0) {
279 292 ul.append(
280 293 $("<li/>")
281 294 .addClass("disabled")
282 295 .append(
283 296 $("<a/>")
284 297 .text("No checkpoints")
285 298 )
286 299 );
287 300 return;
288 301 }
289 302
290 303 checkpoints.map(function (checkpoint) {
291 304 var d = new Date(checkpoint.last_modified);
292 305 ul.append(
293 306 $("<li/>").append(
294 307 $("<a/>")
295 308 .attr("href", "#")
296 309 .text(d.format("mmm dd HH:MM:ss"))
297 310 .click(function () {
298 311 IPython.notebook.restore_checkpoint_dialog(checkpoint);
299 312 })
300 313 )
301 314 );
302 315 });
303 316 };
304 317
305 318 IPython.MenuBar = MenuBar;
306 319
307 320 return IPython;
308 321
309 322 }(IPython));
@@ -1,303 +1,306
1 1 {% extends "page.html" %}
2 2
3 3 {% block stylesheet %}
4 4
5 5 {% if mathjax_url %}
6 6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 7 {% endif %}
8 8 <script type="text/javascript">
9 9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 10 // where it will be undefined, and should prompt a dialog later.
11 11 window.mathjax_url = "{{mathjax_url}}";
12 12 </script>
13 13
14 14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
15 15
16 16 {{super()}}
17 17
18 18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
19 19
20 20 {% endblock %}
21 21
22 22 {% block params %}
23 23
24 24 data-project="{{project}}"
25 25 data-base-project-url="{{base_project_url}}"
26 26 data-base-kernel-url="{{base_kernel_url}}"
27 27 data-notebook-name="{{notebook_name}}"
28 28 data-notebook-path="{{notebook_path}}"
29 29 class="notebook_app"
30 30
31 31 {% endblock %}
32 32
33 33
34 34 {% block header %}
35 35
36 36 <span id="save_widget" class="nav pull-left">
37 37 <span id="notebook_name"></span>
38 38 <span id="checkpoint_status"></span>
39 39 <span id="autosave_status"></span>
40 40 </span>
41 41
42 42 {% endblock %}
43 43
44 44
45 45 {% block site %}
46 46
47 47 <div id="menubar-container" class="container">
48 48 <div id="menubar">
49 49 <div class="navbar">
50 50 <div class="navbar-inner">
51 51 <div class="container">
52 52 <ul id="menus" class="nav">
53 53 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
54 54 <ul class="dropdown-menu">
55 55 <li id="new_notebook"
56 56 title="Make a new notebook (Opens a new window)">
57 57 <a href="#">New</a></li>
58 58 <li id="open_notebook"
59 59 title="Opens a new window with the Dashboard view">
60 60 <a href="#">Open...</a></li>
61 61 <!-- <hr/> -->
62 62 <li class="divider"></li>
63 63 <li id="copy_notebook"
64 64 title="Open a copy of this notebook's contents and start a new kernel">
65 65 <a href="#">Make a Copy...</a></li>
66 66 <li id="rename_notebook"><a href="#">Rename...</a></li>
67 67 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
68 68 <!-- <hr/> -->
69 69 <li class="divider"></li>
70 70 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
71 71 <ul class="dropdown-menu">
72 72 <li><a href="#"></a></li>
73 73 <li><a href="#"></a></li>
74 74 <li><a href="#"></a></li>
75 75 <li><a href="#"></a></li>
76 76 <li><a href="#"></a></li>
77 77 </ul>
78 78 </li>
79 79 <li class="divider"></li>
80 <li id="print_preview"><a href="#">Print Preview</a></li>
80 81 <li class="dropdown-submenu"><a href="#">Download as</a>
81 82 <ul class="dropdown-menu">
82 83 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
83 <!-- <li id="download_py"><a href="#">Python (.py)</a></li> -->
84 <li id="download_py"><a href="#">Python (.py)</a></li>
85 <li id="download_html"><a href="#">HTML (.html)</a></li>
86 <li id="download_rst"><a href="#">reST (.rst)</a></li>
84 87 </ul>
85 88 </li>
86 89 <li class="divider"></li>
87 90
88 91 <li id="kill_and_exit"
89 92 title="Shutdown this notebook's kernel, and close this window">
90 93 <a href="#" >Close and halt</a></li>
91 94 </ul>
92 95 </li>
93 96 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
94 97 <ul class="dropdown-menu">
95 98 <li id="cut_cell"><a href="#">Cut Cell</a></li>
96 99 <li id="copy_cell"><a href="#">Copy Cell</a></li>
97 100 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
98 101 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
99 102 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
100 103 <li id="delete_cell"><a href="#">Delete Cell</a></li>
101 104 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
102 105 <li class="divider"></li>
103 106 <li id="split_cell"><a href="#">Split Cell</a></li>
104 107 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
105 108 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
106 109 <li class="divider"></li>
107 110 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
108 111 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
109 112 <li class="divider"></li>
110 113 <li id="select_previous"><a href="#">Select Previous Cell</a></li>
111 114 <li id="select_next"><a href="#">Select Next Cell</a></li>
112 115 <li class="divider"></li>
113 116 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
114 117 </ul>
115 118 </li>
116 119 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
117 120 <ul class="dropdown-menu">
118 121 <li id="toggle_header"
119 122 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
120 123 <a href="#">Toggle Header</a></li>
121 124 <li id="toggle_toolbar"
122 125 title="Show/Hide the action icons (below menu bar)">
123 126 <a href="#">Toggle Toolbar</a></li>
124 127 </ul>
125 128 </li>
126 129 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
127 130 <ul class="dropdown-menu">
128 131 <li id="insert_cell_above"
129 132 title="Insert an empty Code cell above the currently active cell">
130 133 <a href="#">Insert Cell Above</a></li>
131 134 <li id="insert_cell_below"
132 135 title="Insert an empty Code cell below the currently active cell">
133 136 <a href="#">Insert Cell Below</a></li>
134 137 </ul>
135 138 </li>
136 139 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
137 140 <ul class="dropdown-menu">
138 141 <li id="run_cell" title="Run this cell, and move cursor to the next one">
139 142 <a href="#">Run</a></li>
140 143 <li id="run_cell_in_place" title="Run this cell, without moving to the next one">
141 144 <a href="#">Run in Place</a></li>
142 145 <li id="run_all_cells" title="Run all cells in the notebook">
143 146 <a href="#">Run All</a></li>
144 147 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
145 148 <a href="#">Run All Above</a></li>
146 149 <li id="run_all_cells_below" title="Run this cell and all cells below it">
147 150 <a href="#">Run All Below</a></li>
148 151 <li class="divider"></li>
149 152 <li id="change_cell_type" class="dropdown-submenu"
150 153 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
151 154 <a href="#">Cell Type</a>
152 155 <ul class="dropdown-menu">
153 156 <li id="to_code"
154 157 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
155 158 <a href="#">Code</a></li>
156 159 <li id="to_markdown"
157 160 title="Contents will be rendered as HTML and serve as explanatory text">
158 161 <a href="#">Markdown</a></li>
159 162 <li id="to_raw"
160 163 title="Contents will pass through nbconvert unmodified">
161 164 <a href="#">Raw NBConvert</a></li>
162 165 <li id="to_heading1"><a href="#">Heading 1</a></li>
163 166 <li id="to_heading2"><a href="#">Heading 2</a></li>
164 167 <li id="to_heading3"><a href="#">Heading 3</a></li>
165 168 <li id="to_heading4"><a href="#">Heading 4</a></li>
166 169 <li id="to_heading5"><a href="#">Heading 5</a></li>
167 170 <li id="to_heading6"><a href="#">Heading 6</a></li>
168 171 </ul>
169 172 </li>
170 173 <li class="divider"></li>
171 174 <li id="toggle_output"
172 175 title="Show/Hide the output portion of a Code cell">
173 176 <a href="#">Toggle Current Output</a></li>
174 177 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
175 178 <ul class="dropdown-menu">
176 179 <li id="expand_all_output"><a href="#">Expand</a></li>
177 180 <li id="scroll_all_output"><a href="#">Scroll Long</a></li>
178 181 <li id="collapse_all_output"><a href="#">Collapse</a></li>
179 182 <li id="clear_all_output"
180 183 title="Remove the output portion of all Code cells">
181 184 <a href="#">Clear</a></li>
182 185 </ul>
183 186 </li>
184 187 </ul>
185 188 </li>
186 189 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
187 190 <ul class="dropdown-menu">
188 191 <li id="int_kernel"
189 192 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
190 193 <a href="#">Interrupt</a></li>
191 194 <li id="restart_kernel"
192 195 title="Restart the Kernel">
193 196 <a href="#">Restart</a></li>
194 197 </ul>
195 198 </li>
196 199 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
197 200 <ul class="dropdown-menu" title="Opens in a new window">
198 201 <li><a href="http://ipython.org/documentation.html" target="_blank">IPython Help</a></li>
199 202 <li><a href="http://ipython.org/ipython-doc/stable/interactive/notebook.html" target="_blank">Notebook Help</a></li>
200 203 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
201 204 <li><a href="http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html" target="_blank">Editor Shortcuts</a></li>
202 205 <li class="divider"></li>
203 206 <li><a href="http://docs.python.org" target="_blank">Python</a></li>
204 207 <li><a href="http://docs.scipy.org/doc/numpy/reference/" target="_blank">NumPy</a></li>
205 208 <li><a href="http://docs.scipy.org/doc/scipy/reference/" target="_blank">SciPy</a></li>
206 209 <li><a href="http://matplotlib.org/" target="_blank">Matplotlib</a></li>
207 210 <li><a href="http://docs.sympy.org/dev/index.html" target="_blank">SymPy</a></li>
208 211 <li><a href="http://pandas.pydata.org/pandas-docs/stable/" target="_blank">pandas</a></li>
209 212 </ul>
210 213 </li>
211 214 </ul>
212 215 <div id="notification_area"></div>
213 216 </div>
214 217 </div>
215 218 </div>
216 219 </div>
217 220 <div id="maintoolbar" class="navbar">
218 221 <div class="toolbar-inner navbar-inner navbar-nobg">
219 222 <div id="maintoolbar-container" class="container"></div>
220 223 </div>
221 224 </div>
222 225 </div>
223 226
224 227 <div id="ipython-main-app">
225 228
226 229 <div id="notebook_panel">
227 230 <div id="notebook"></div>
228 231 <div id="pager_splitter"></div>
229 232 <div id="pager">
230 233 <div id='pager_button_area'>
231 234 </div>
232 235 <div id="pager-container" class="container"></div>
233 236 </div>
234 237 </div>
235 238
236 239 </div>
237 240 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
238 241
239 242
240 243 {% endblock %}
241 244
242 245
243 246 {% block script %}
244 247
245 248 {{super()}}
246 249
247 250 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
248 251 <script type="text/javascript">
249 252 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js") }}";
250 253 </script>
251 254 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
252 255 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
253 256 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
254 257 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
255 258 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
256 259 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
257 260 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
258 261 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
259 262 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
260 263 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
261 264 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
262 265 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
263 266 <script src="{{ static_url("components/codemirror/mode/python/python.js") }}" charset="utf-8"></script>
264 267 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
265 268
266 269 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
267 270
268 271 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
269 272
270 273 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
271 274 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
272 275 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
273 276 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
274 277 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
275 278 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
276 279 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
277 280 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
278 281 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
279 282 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
280 283 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
281 284 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
282 285 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
283 286 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
284 287 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
285 288 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
286 289 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
287 290 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
288 291 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
289 292 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
290 293 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
291 294 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
292 295 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
293 296 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
294 297 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
295 298 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
296 299
297 300 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
298 301
299 302 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
300 303 <script src="{{ static_url("notebook/js/celltoolbarpresets/rawcell.js") }}" type="text/javascript" charset="utf-8"></script>
301 304 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
302 305
303 306 {% endblock %}
@@ -1,88 +1,88
1 1 """Base class for notebook tests."""
2 2
3 import os
4 3 import sys
5 4 import time
6 5 import requests
7 6 from contextlib import contextmanager
8 from subprocess import Popen, PIPE
7 from subprocess import Popen, STDOUT
9 8 from unittest import TestCase
10 9
10 import nose
11
11 12 from IPython.utils.tempdir import TemporaryDirectory
12 13
13 14 class NotebookTestBase(TestCase):
14 15 """A base class for tests that need a running notebook.
15 16
16 17 This creates an empty profile in a temp ipython_dir
17 18 and then starts the notebook server with a separate temp notebook_dir.
18 19 """
19 20
20 21 port = 12341
21 22
22 23 @classmethod
23 24 def wait_until_alive(cls):
24 25 """Wait for the server to be alive"""
25 26 url = 'http://localhost:%i/api/notebooks' % cls.port
26 27 while True:
27 28 try:
28 29 requests.get(url)
29 30 except requests.exceptions.ConnectionError:
30 31 time.sleep(.1)
31 32 else:
32 33 break
33 34
34 35 @classmethod
35 36 def wait_until_dead(cls):
36 37 """Wait for the server to stop getting requests after shutdown"""
37 38 url = 'http://localhost:%i/api/notebooks' % cls.port
38 39 while True:
39 40 try:
40 41 requests.get(url)
41 42 except requests.exceptions.ConnectionError:
42 43 break
43 44 else:
44 45 time.sleep(.1)
45 46
46 47 @classmethod
47 48 def setup_class(cls):
48 49 cls.ipython_dir = TemporaryDirectory()
49 50 cls.notebook_dir = TemporaryDirectory()
50 51 notebook_args = [
51 52 sys.executable, '-c',
52 53 'from IPython.html.notebookapp import launch_new_instance; launch_new_instance()',
53 54 '--port=%d' % cls.port,
54 55 '--no-browser',
55 56 '--ipython-dir=%s' % cls.ipython_dir.name,
56 57 '--notebook-dir=%s' % cls.notebook_dir.name,
57 58 ]
58 devnull = open(os.devnull, 'w')
59 59 cls.notebook = Popen(notebook_args,
60 stdout=devnull,
61 stderr=devnull,
60 stdout=nose.iptest_stdstreams_fileno(),
61 stderr=STDOUT,
62 62 )
63 63 cls.wait_until_alive()
64 64
65 65 @classmethod
66 66 def teardown_class(cls):
67 67 cls.notebook.terminate()
68 68 cls.ipython_dir.cleanup()
69 69 cls.notebook_dir.cleanup()
70 70 cls.wait_until_dead()
71 71
72 72 @classmethod
73 73 def base_url(cls):
74 74 return 'http://localhost:%i/' % cls.port
75 75
76 76
77 77 @contextmanager
78 78 def assert_http_error(status, msg=None):
79 79 try:
80 80 yield
81 81 except requests.HTTPError as e:
82 82 real_status = e.response.status_code
83 83 assert real_status == status, \
84 84 "Expected status %d, got %d" % (real_status, status)
85 85 if msg:
86 86 assert msg in str(e), e
87 87 else:
88 88 assert False, "Expected HTTP error status" No newline at end of file
@@ -1,77 +1,76
1 1 """Tornado handlers for the tree view.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18 import os
19 19
20 20 from tornado import web
21 from ..base.handlers import IPythonHandler
21 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
22 22 from ..utils import url_path_join, path2url, url2path, url_escape
23 from ..services.notebooks.handlers import _notebook_path_regex, _path_regex
24 23
25 24 #-----------------------------------------------------------------------------
26 25 # Handlers
27 26 #-----------------------------------------------------------------------------
28 27
29 28
30 29 class TreeHandler(IPythonHandler):
31 30 """Render the tree view, listing notebooks, clusters, etc."""
32 31
33 32 @web.authenticated
34 33 def get(self, path='', name=None):
35 34 path = path.strip('/')
36 35 nbm = self.notebook_manager
37 36 if name is not None:
38 37 # is a notebook, redirect to notebook handler
39 38 url = url_escape(url_path_join(
40 39 self.base_project_url, 'notebooks', path, name
41 40 ))
42 41 self.log.debug("Redirecting %s to %s", self.request.path, url)
43 42 self.redirect(url)
44 43 else:
45 44 if not nbm.path_exists(path=path):
46 45 # no such directory, 404
47 46 raise web.HTTPError(404)
48 47 self.write(self.render_template('tree.html',
49 48 project=self.project_dir,
50 49 tree_url_path=path,
51 50 notebook_path=path,
52 51 ))
53 52
54 53
55 54 class TreeRedirectHandler(IPythonHandler):
56 55 """Redirect a request to the corresponding tree URL"""
57 56
58 57 @web.authenticated
59 58 def get(self, path=''):
60 59 url = url_escape(url_path_join(
61 60 self.base_project_url, 'tree', path.strip('/')
62 61 ))
63 62 self.log.debug("Redirecting %s to %s", self.request.path, url)
64 63 self.redirect(url)
65 64
66 65
67 66 #-----------------------------------------------------------------------------
68 67 # URL to handler mappings
69 68 #-----------------------------------------------------------------------------
70 69
71 70
72 71 default_handlers = [
73 (r"/tree%s" % _notebook_path_regex, TreeHandler),
74 (r"/tree%s" % _path_regex, TreeHandler),
72 (r"/tree%s" % notebook_path_regex, TreeHandler),
73 (r"/tree%s" % path_regex, TreeHandler),
75 74 (r"/tree", TreeHandler),
76 75 (r"/", TreeRedirectHandler),
77 76 ]
@@ -1,270 +1,275
1 1 """This module defines a base Exporter class. For Jinja template-based export,
2 2 see templateexporter.py.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 from __future__ import print_function, absolute_import
18 18
19 19 # Stdlib imports
20 20 import io
21 21 import os
22 22 import copy
23 23 import collections
24 24 import datetime
25 25
26 26
27 27 # IPython imports
28 28 from IPython.config.configurable import LoggingConfigurable
29 29 from IPython.config import Config
30 30 from IPython.nbformat import current as nbformat
31 31 from IPython.utils.traitlets import MetaHasTraits, Unicode, List
32 32 from IPython.utils.importstring import import_item
33 33 from IPython.utils import text, py3compat
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Class
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class ResourcesDict(collections.defaultdict):
40 40 def __missing__(self, key):
41 41 return ''
42 42
43 43
44 44 class Exporter(LoggingConfigurable):
45 45 """
46 46 Class containing methods that sequentially run a list of preprocessors on a
47 47 NotebookNode object and then return the modified NotebookNode object and
48 48 accompanying resources dict.
49 49 """
50 50
51 51 file_extension = Unicode(
52 52 'txt', config=True,
53 53 help="Extension of the file that should be written to disk"
54 54 )
55 55
56 # MIME type of the result file, for HTTP response headers.
57 # This is *not* a traitlet, because we want to be able to access it from
58 # the class, not just on instances.
59 output_mimetype = ''
60
56 61 #Configurability, allows the user to easily add filters and preprocessors.
57 62 preprocessors = List(config=True,
58 63 help="""List of preprocessors, by name or namespace, to enable.""")
59 64
60 65 _preprocessors = None
61 66
62 67 default_preprocessors = List(['IPython.nbconvert.preprocessors.coalesce_streams',
63 68 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor',
64 69 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor',
65 70 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor',
66 71 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor',
67 72 'IPython.nbconvert.preprocessors.LatexPreprocessor',
68 73 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor'],
69 74 config=True,
70 75 help="""List of preprocessors available by default, by name, namespace,
71 76 instance, or type.""")
72 77
73 78
74 79 def __init__(self, config=None, **kw):
75 80 """
76 81 Public constructor
77 82
78 83 Parameters
79 84 ----------
80 85 config : config
81 86 User configuration instance.
82 87 """
83 88 with_default_config = self.default_config
84 89 if config:
85 90 with_default_config.merge(config)
86 91
87 92 super(Exporter, self).__init__(config=with_default_config, **kw)
88 93
89 94 self._init_preprocessors()
90 95
91 96
92 97 @property
93 98 def default_config(self):
94 99 return Config()
95 100
96 101 @nbformat.docstring_nbformat_mod
97 102 def from_notebook_node(self, nb, resources=None, **kw):
98 103 """
99 104 Convert a notebook from a notebook node instance.
100 105
101 106 Parameters
102 107 ----------
103 108 nb : :class:`~{nbformat_mod}.nbbase.NotebookNode`
104 109 Notebook node
105 110 resources : dict
106 111 Additional resources that can be accessed read/write by
107 112 preprocessors and filters.
108 113 **kw
109 114 Ignored (?)
110 115 """
111 116 nb_copy = copy.deepcopy(nb)
112 117 resources = self._init_resources(resources)
113 118
114 119 # Preprocess
115 120 nb_copy, resources = self._preprocess(nb_copy, resources)
116 121
117 122 return nb_copy, resources
118 123
119 124
120 125 def from_filename(self, filename, resources=None, **kw):
121 126 """
122 127 Convert a notebook from a notebook file.
123 128
124 129 Parameters
125 130 ----------
126 131 filename : str
127 132 Full filename of the notebook file to open and convert.
128 133 """
129 134
130 135 # Pull the metadata from the filesystem.
131 136 if resources is None:
132 137 resources = ResourcesDict()
133 138 if not 'metadata' in resources or resources['metadata'] == '':
134 139 resources['metadata'] = ResourcesDict()
135 140 basename = os.path.basename(filename)
136 141 notebook_name = basename[:basename.rfind('.')]
137 142 resources['metadata']['name'] = notebook_name
138 143
139 144 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
140 145 resources['metadata']['modified_date'] = modified_date.strftime(text.date_format)
141 146
142 147 with io.open(filename, encoding='utf-8') as f:
143 148 return self.from_notebook_node(nbformat.read(f, 'json'), resources=resources, **kw)
144 149
145 150
146 151 def from_file(self, file_stream, resources=None, **kw):
147 152 """
148 153 Convert a notebook from a notebook file.
149 154
150 155 Parameters
151 156 ----------
152 157 file_stream : file-like object
153 158 Notebook file-like object to convert.
154 159 """
155 160 return self.from_notebook_node(nbformat.read(file_stream, 'json'), resources=resources, **kw)
156 161
157 162
158 163 def register_preprocessor(self, preprocessor, enabled=False):
159 164 """
160 165 Register a preprocessor.
161 166 Preprocessors are classes that act upon the notebook before it is
162 167 passed into the Jinja templating engine. preprocessors are also
163 168 capable of passing additional information to the Jinja
164 169 templating engine.
165 170
166 171 Parameters
167 172 ----------
168 173 preprocessor : preprocessor
169 174 """
170 175 if preprocessor is None:
171 176 raise TypeError('preprocessor')
172 177 isclass = isinstance(preprocessor, type)
173 178 constructed = not isclass
174 179
175 180 # Handle preprocessor's registration based on it's type
176 181 if constructed and isinstance(preprocessor, py3compat.string_types):
177 182 # Preprocessor is a string, import the namespace and recursively call
178 183 # this register_preprocessor method
179 184 preprocessor_cls = import_item(preprocessor)
180 185 return self.register_preprocessor(preprocessor_cls, enabled)
181 186
182 187 if constructed and hasattr(preprocessor, '__call__'):
183 188 # Preprocessor is a function, no need to construct it.
184 189 # Register and return the preprocessor.
185 190 if enabled:
186 191 preprocessor.enabled = True
187 192 self._preprocessors.append(preprocessor)
188 193 return preprocessor
189 194
190 195 elif isclass and isinstance(preprocessor, MetaHasTraits):
191 196 # Preprocessor is configurable. Make sure to pass in new default for
192 197 # the enabled flag if one was specified.
193 198 self.register_preprocessor(preprocessor(parent=self), enabled)
194 199
195 200 elif isclass:
196 201 # Preprocessor is not configurable, construct it
197 202 self.register_preprocessor(preprocessor(), enabled)
198 203
199 204 else:
200 205 # Preprocessor is an instance of something without a __call__
201 206 # attribute.
202 207 raise TypeError('preprocessor')
203 208
204 209
205 210 def _init_preprocessors(self):
206 211 """
207 212 Register all of the preprocessors needed for this exporter, disabled
208 213 unless specified explicitly.
209 214 """
210 215 if self._preprocessors is None:
211 216 self._preprocessors = []
212 217
213 218 #Load default preprocessors (not necessarly enabled by default).
214 219 if self.default_preprocessors:
215 220 for preprocessor in self.default_preprocessors:
216 221 self.register_preprocessor(preprocessor)
217 222
218 223 #Load user preprocessors. Enable by default.
219 224 if self.preprocessors:
220 225 for preprocessor in self.preprocessors:
221 226 self.register_preprocessor(preprocessor, enabled=True)
222 227
223 228
224 229 def _init_resources(self, resources):
225 230
226 231 #Make sure the resources dict is of ResourcesDict type.
227 232 if resources is None:
228 233 resources = ResourcesDict()
229 234 if not isinstance(resources, ResourcesDict):
230 235 new_resources = ResourcesDict()
231 236 new_resources.update(resources)
232 237 resources = new_resources
233 238
234 239 #Make sure the metadata extension exists in resources
235 240 if 'metadata' in resources:
236 241 if not isinstance(resources['metadata'], ResourcesDict):
237 242 resources['metadata'] = ResourcesDict(resources['metadata'])
238 243 else:
239 244 resources['metadata'] = ResourcesDict()
240 245 if not resources['metadata']['name']:
241 246 resources['metadata']['name'] = 'Notebook'
242 247
243 248 #Set the output extension
244 249 resources['output_extension'] = self.file_extension
245 250 return resources
246 251
247 252
248 253 def _preprocess(self, nb, resources):
249 254 """
250 255 Preprocess the notebook before passing it into the Jinja engine.
251 256 To preprocess the notebook is to apply all of the
252 257
253 258 Parameters
254 259 ----------
255 260 nb : notebook node
256 261 notebook that is being exported.
257 262 resources : a dict of additional resources that
258 263 can be accessed read/write by preprocessors
259 264 """
260 265
261 266 # Do a copy.deepcopy first,
262 267 # we are never safe enough with what the preprocessors could do.
263 268 nbc = copy.deepcopy(nb)
264 269 resc = copy.deepcopy(resources)
265 270
266 271 #Run each preprocessor on the notebook. Carry the output along
267 272 #to each preprocessor
268 273 for preprocessor in self._preprocessors:
269 274 nbc, resc = preprocessor(nbc, resc)
270 275 return nbc, resc
@@ -1,56 +1,59
1 1 """HTML Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 from IPython.utils.traitlets import Unicode, List
16 16
17 17 from IPython.nbconvert import preprocessors
18 18 from IPython.config import Config
19 19
20 20 from .templateexporter import TemplateExporter
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Classes
24 24 #-----------------------------------------------------------------------------
25 25
26 26 class HTMLExporter(TemplateExporter):
27 27 """
28 28 Exports a basic HTML document. This exporter assists with the export of
29 29 HTML. Inherit from it if you are writing your own HTML template and need
30 30 custom preprocessors/filters. If you don't need custom preprocessors/
31 31 filters, just change the 'template_file' config option.
32 32 """
33 33
34 34 file_extension = Unicode(
35 35 'html', config=True,
36 36 help="Extension of the file that should be written to disk"
37 37 )
38 38
39 mime_type = Unicode('text/html', config=True,
40 help="MIME type of the result file, for HTTP response headers."
41 )
42
39 43 default_template = Unicode('full', config=True, help="""Flavor of the data
40 44 format to use. I.E. 'full' or 'basic'""")
41 45
42 def _raw_mimetype_default(self):
43 return 'text/html'
46 output_mimetype = 'text/html'
44 47
45 48 @property
46 49 def default_config(self):
47 50 c = Config({
48 51 'CSSHTMLHeaderPreprocessor':{
49 52 'enabled':True
50 53 },
51 54 'HighlightMagicsPreprocessor': {
52 55 'enabled':True
53 56 }
54 57 })
55 58 c.merge(super(HTMLExporter,self).default_config)
56 59 return c
@@ -1,93 +1,92
1 1 """LaTeX Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 # Stdlib imports
16 16 import os
17 17
18 18 # IPython imports
19 19 from IPython.utils.traitlets import Unicode, List
20 20 from IPython.config import Config
21 21
22 22 from IPython.nbconvert import filters, preprocessors
23 23 from .templateexporter import TemplateExporter
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes and functions
27 27 #-----------------------------------------------------------------------------
28 28
29 29 class LatexExporter(TemplateExporter):
30 30 """
31 31 Exports to a Latex template. Inherit from this class if your template is
32 32 LaTeX based and you need custom tranformers/filters. Inherit from it if
33 33 you are writing your own HTML template and need custom tranformers/filters.
34 34 If you don't need custom tranformers/filters, just change the
35 35 'template_file' config option. Place your template in the special "/latex"
36 36 subfolder of the "../templates" folder.
37 37 """
38 38
39 39 file_extension = Unicode(
40 40 'tex', config=True,
41 41 help="Extension of the file that should be written to disk")
42 42
43 43 default_template = Unicode('article', config=True, help="""Template of the
44 44 data format to use. I.E. 'article' or 'report'""")
45 45
46 46 #Latex constants
47 47 default_template_path = Unicode(
48 48 os.path.join("..", "templates", "latex"), config=True,
49 49 help="Path where the template files are located.")
50 50
51 51 template_skeleton_path = Unicode(
52 52 os.path.join("..", "templates", "latex", "skeleton"), config=True,
53 53 help="Path where the template skeleton files are located.")
54 54
55 55 #Special Jinja2 syntax that will not conflict when exporting latex.
56 56 jinja_comment_block_start = Unicode("((=", config=True)
57 57 jinja_comment_block_end = Unicode("=))", config=True)
58 58 jinja_variable_block_start = Unicode("(((", config=True)
59 59 jinja_variable_block_end = Unicode(")))", config=True)
60 60 jinja_logic_block_start = Unicode("((*", config=True)
61 61 jinja_logic_block_end = Unicode("*))", config=True)
62 62
63 63 #Extension that the template files use.
64 64 template_extension = Unicode(".tplx", config=True)
65 65
66 def _raw_mimetype_default(self):
67 return 'text/latex'
66 output_mimetype = 'text/latex'
68 67
69 68
70 69 @property
71 70 def default_config(self):
72 71 c = Config({
73 72 'NbConvertBase': {
74 73 'display_data_priority' : ['latex', 'pdf', 'png', 'jpg', 'svg', 'jpeg', 'text']
75 74 },
76 75 'ExtractOutputPreprocessor': {
77 76 'enabled':True
78 77 },
79 78 'SVG2PDFPreprocessor': {
80 79 'enabled':True
81 80 },
82 81 'LatexPreprocessor': {
83 82 'enabled':True
84 83 },
85 84 'SphinxPreprocessor': {
86 85 'enabled':True
87 86 },
88 87 'HighlightMagicsPreprocessor': {
89 88 'enabled':True
90 89 }
91 90 })
92 91 c.merge(super(LatexExporter,self).default_config)
93 92 return c
@@ -1,43 +1,42
1 1 """Markdown Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 from IPython.config import Config
16 16 from IPython.utils.traitlets import Unicode
17 17
18 18 from .templateexporter import TemplateExporter
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Classes
22 22 #-----------------------------------------------------------------------------
23 23
24 24 class MarkdownExporter(TemplateExporter):
25 25 """
26 26 Exports to a markdown document (.md)
27 27 """
28 28
29 29 file_extension = Unicode(
30 30 'md', config=True,
31 31 help="Extension of the file that should be written to disk")
32 32
33 def _raw_mimetype_default(self):
34 return 'text/markdown'
33 output_mimetype = 'text/markdown'
35 34
36 35 def _raw_mimetypes_default(self):
37 return ['text/markdown', 'text/html']
36 return ['text/markdown', 'text/html', '']
38 37
39 38 @property
40 39 def default_config(self):
41 40 c = Config({'ExtractOutputPreprocessor':{'enabled':True}})
42 41 c.merge(super(MarkdownExporter,self).default_config)
43 42 return c
@@ -1,34 +1,32
1 1 """Python script Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 from IPython.utils.traitlets import Unicode
16 16
17 17 from .templateexporter import TemplateExporter
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Classes
21 21 #-----------------------------------------------------------------------------
22 22
23 23 class PythonExporter(TemplateExporter):
24 24 """
25 25 Exports a Python code file.
26 26 """
27 27
28 28 file_extension = Unicode(
29 29 'py', config=True,
30 30 help="Extension of the file that should be written to disk")
31 31
32 def _raw_mimetype_default(self):
33 return 'application/x-python'
34
32 output_mimetype = 'text/x-python'
@@ -1,40 +1,39
1 1 """restructuredText Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 from IPython.utils.traitlets import Unicode
16 16 from IPython.config import Config
17 17
18 18 from .templateexporter import TemplateExporter
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Classes
22 22 #-----------------------------------------------------------------------------
23 23
24 24 class RSTExporter(TemplateExporter):
25 25 """
26 26 Exports restructured text documents.
27 27 """
28 28
29 29 file_extension = Unicode(
30 30 'rst', config=True,
31 31 help="Extension of the file that should be written to disk")
32 32
33 def _raw_mimetype_default(self):
34 return 'text/restructuredtext'
33 output_mimetype = 'text/restructuredtext'
35 34
36 35 @property
37 36 def default_config(self):
38 37 c = Config({'ExtractOutputPreprocessor':{'enabled':True}})
39 38 c.merge(super(RSTExporter,self).default_config)
40 39 return c
@@ -1,45 +1,47
1 1 """HTML slide show Exporter class"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 from IPython.utils.traitlets import Unicode
16 16
17 17 from IPython.nbconvert import preprocessors
18 18 from IPython.config import Config
19 19
20 20 from .html import HTMLExporter
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Classes
24 24 #-----------------------------------------------------------------------------
25 25
26 26 class SlidesExporter(HTMLExporter):
27 27 """Exports HTML slides with reveal.js"""
28 28
29 29 file_extension = Unicode(
30 30 'slides.html', config=True,
31 31 help="Extension of the file that should be written to disk"
32 32 )
33 33
34 output_mimetype = 'text/html'
35
34 36 default_template = Unicode('reveal', config=True, help="""Template of the
35 37 data format to use. I.E. 'reveal'""")
36 38
37 39 @property
38 40 def default_config(self):
39 41 c = Config({
40 42 'RevealHelpPreprocessor': {
41 43 'enabled': True,
42 44 },
43 45 })
44 46 c.merge(super(SlidesExporter,self).default_config)
45 47 return c
@@ -1,324 +1,322
1 1 """This module defines TemplateExporter, a highly configurable converter
2 2 that uses Jinja2 to export notebook files into different formats.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 from __future__ import print_function, absolute_import
18 18
19 19 # Stdlib imports
20 20 import os
21 21
22 22 # other libs/dependencies
23 23 from jinja2 import Environment, FileSystemLoader, ChoiceLoader, TemplateNotFound
24 24
25 25 # IPython imports
26 26 from IPython.utils.traitlets import MetaHasTraits, Unicode, List, Dict, Any
27 27 from IPython.utils.importstring import import_item
28 28 from IPython.utils import py3compat, text
29 29
30 30 from IPython.nbformat.current import docstring_nbformat_mod
31 31 from IPython.nbconvert import filters
32 32 from .exporter import Exporter
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Globals and constants
36 36 #-----------------------------------------------------------------------------
37 37
38 38 #Jinja2 extensions to load.
39 39 JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols']
40 40
41 41 default_filters = {
42 42 'indent': text.indent,
43 43 'markdown2html': filters.markdown2html,
44 44 'ansi2html': filters.ansi2html,
45 45 'filter_data_type': filters.DataTypeFilter,
46 46 'get_lines': filters.get_lines,
47 47 'highlight2html': filters.Highlight2Html,
48 48 'highlight2latex': filters.Highlight2Latex,
49 49 'ipython2python': filters.ipython2python,
50 50 'posix_path': filters.posix_path,
51 51 'markdown2latex': filters.markdown2latex,
52 52 'markdown2rst': filters.markdown2rst,
53 53 'comment_lines': filters.comment_lines,
54 54 'strip_ansi': filters.strip_ansi,
55 55 'strip_dollars': filters.strip_dollars,
56 56 'strip_files_prefix': filters.strip_files_prefix,
57 57 'html2text' : filters.html2text,
58 58 'add_anchor': filters.add_anchor,
59 59 'ansi2latex': filters.ansi2latex,
60 60 'wrap_text': filters.wrap_text,
61 61 'escape_latex': filters.escape_latex,
62 62 'citation2latex': filters.citation2latex,
63 63 'path2url': filters.path2url,
64 64 'add_prompts': filters.add_prompts,
65 65 }
66 66
67 67 #-----------------------------------------------------------------------------
68 68 # Class
69 69 #-----------------------------------------------------------------------------
70 70
71 71 class TemplateExporter(Exporter):
72 72 """
73 73 Exports notebooks into other file formats. Uses Jinja 2 templating engine
74 74 to output new formats. Inherit from this class if you are creating a new
75 75 template type along with new filters/preprocessors. If the filters/
76 76 preprocessors provided by default suffice, there is no need to inherit from
77 77 this class. Instead, override the template_file and file_extension
78 78 traits via a config file.
79 79
80 80 {filters}
81 81 """
82 82
83 83 # finish the docstring
84 84 __doc__ = __doc__.format(filters = '- '+'\n - '.join(default_filters.keys()))
85 85
86 86
87 87 template_file = Unicode(u'default',
88 88 config=True,
89 89 help="Name of the template file to use")
90 90 def _template_file_changed(self, name, old, new):
91 91 if new == 'default':
92 92 self.template_file = self.default_template
93 93 else:
94 94 self.template_file = new
95 95 self.template = None
96 96 self._load_template()
97 97
98 98 default_template = Unicode(u'')
99 99 template = Any()
100 100 environment = Any()
101 101
102 102 template_path = List(['.'], config=True)
103 103 def _template_path_changed(self, name, old, new):
104 104 self._load_template()
105 105
106 106 default_template_path = Unicode(
107 107 os.path.join("..", "templates"),
108 108 help="Path where the template files are located.")
109 109
110 110 template_skeleton_path = Unicode(
111 111 os.path.join("..", "templates", "skeleton"),
112 112 help="Path where the template skeleton files are located.")
113 113
114 114 #Jinja block definitions
115 115 jinja_comment_block_start = Unicode("", config=True)
116 116 jinja_comment_block_end = Unicode("", config=True)
117 117 jinja_variable_block_start = Unicode("", config=True)
118 118 jinja_variable_block_end = Unicode("", config=True)
119 119 jinja_logic_block_start = Unicode("", config=True)
120 120 jinja_logic_block_end = Unicode("", config=True)
121 121
122 122 #Extension that the template files use.
123 123 template_extension = Unicode(".tpl", config=True)
124 124
125 125 filters = Dict(config=True,
126 126 help="""Dictionary of filters, by name and namespace, to add to the Jinja
127 127 environment.""")
128 128
129 raw_mimetype = Unicode('')
130 129 raw_mimetypes = List(config=True,
131 130 help="""formats of raw cells to be included in this Exporter's output."""
132 131 )
133 132 def _raw_mimetypes_default(self):
134 return [self.raw_mimetype]
133 return [self.output_mimetype, '']
135 134
136 135
137 136 def __init__(self, config=None, extra_loaders=None, **kw):
138 137 """
139 138 Public constructor
140 139
141 140 Parameters
142 141 ----------
143 142 config : config
144 143 User configuration instance.
145 144 extra_loaders : list[of Jinja Loaders]
146 145 ordered list of Jinja loader to find templates. Will be tried in order
147 146 before the default FileSystem ones.
148 147 template : str (optional, kw arg)
149 148 Template to use when exporting.
150 149 """
151 150 super(TemplateExporter, self).__init__(config=config, **kw)
152 151
153 152 #Init
154 153 self._init_template()
155 154 self._init_environment(extra_loaders=extra_loaders)
156 155 self._init_preprocessors()
157 156 self._init_filters()
158 157
159 158
160 159 def _load_template(self):
161 160 """Load the Jinja template object from the template file
162 161
163 162 This is a no-op if the template attribute is already defined,
164 163 or the Jinja environment is not setup yet.
165 164
166 165 This is triggered by various trait changes that would change the template.
167 166 """
168 167 if self.template is not None:
169 168 return
170 169 # called too early, do nothing
171 170 if self.environment is None:
172 171 return
173 172 # Try different template names during conversion. First try to load the
174 173 # template by name with extension added, then try loading the template
175 174 # as if the name is explicitly specified, then try the name as a
176 175 # 'flavor', and lastly just try to load the template by module name.
177 176 module_name = self.__module__.rsplit('.', 1)[-1]
178 177 try_names = []
179 178 if self.template_file:
180 179 try_names.extend([
181 180 self.template_file + self.template_extension,
182 181 self.template_file,
183 182 module_name + '_' + self.template_file + self.template_extension,
184 183 ])
185 184 try_names.append(module_name + self.template_extension)
186 185 for try_name in try_names:
187 186 self.log.debug("Attempting to load template %s", try_name)
188 187 try:
189 188 self.template = self.environment.get_template(try_name)
190 189 except (TemplateNotFound, IOError):
191 190 pass
192 191 except Exception as e:
193 192 self.log.warn("Unexpected exception loading template: %s", try_name, exc_info=True)
194 193 else:
195 194 self.log.info("Loaded template %s", try_name)
196 195 break
197 196
198 197 @docstring_nbformat_mod
199 198 def from_notebook_node(self, nb, resources=None, **kw):
200 199 """
201 200 Convert a notebook from a notebook node instance.
202 201
203 202 Parameters
204 203 ----------
205 204 nb : :class:`~{nbformat_mod}.nbbase.NotebookNode`
206 205 Notebook node
207 206 resources : dict
208 207 Additional resources that can be accessed read/write by
209 208 preprocessors and filters.
210 209 """
211 210 nb_copy, resources = super(TemplateExporter, self).from_notebook_node(nb, resources, **kw)
212 resources.setdefault('raw_mimetype', self.raw_mimetype)
213 211 resources.setdefault('raw_mimetypes', self.raw_mimetypes)
214 212
215 213 self._load_template()
216 214
217 215 if self.template is not None:
218 216 output = self.template.render(nb=nb_copy, resources=resources)
219 217 else:
220 218 raise IOError('template file "%s" could not be found' % self.template_file)
221 219 return output, resources
222 220
223 221
224 222 def register_filter(self, name, jinja_filter):
225 223 """
226 224 Register a filter.
227 225 A filter is a function that accepts and acts on one string.
228 226 The filters are accesible within the Jinja templating engine.
229 227
230 228 Parameters
231 229 ----------
232 230 name : str
233 231 name to give the filter in the Jinja engine
234 232 filter : filter
235 233 """
236 234 if jinja_filter is None:
237 235 raise TypeError('filter')
238 236 isclass = isinstance(jinja_filter, type)
239 237 constructed = not isclass
240 238
241 239 #Handle filter's registration based on it's type
242 240 if constructed and isinstance(jinja_filter, py3compat.string_types):
243 241 #filter is a string, import the namespace and recursively call
244 242 #this register_filter method
245 243 filter_cls = import_item(jinja_filter)
246 244 return self.register_filter(name, filter_cls)
247 245
248 246 if constructed and hasattr(jinja_filter, '__call__'):
249 247 #filter is a function, no need to construct it.
250 248 self.environment.filters[name] = jinja_filter
251 249 return jinja_filter
252 250
253 251 elif isclass and isinstance(jinja_filter, MetaHasTraits):
254 252 #filter is configurable. Make sure to pass in new default for
255 253 #the enabled flag if one was specified.
256 254 filter_instance = jinja_filter(parent=self)
257 255 self.register_filter(name, filter_instance )
258 256
259 257 elif isclass:
260 258 #filter is not configurable, construct it
261 259 filter_instance = jinja_filter()
262 260 self.register_filter(name, filter_instance)
263 261
264 262 else:
265 263 #filter is an instance of something without a __call__
266 264 #attribute.
267 265 raise TypeError('filter')
268 266
269 267
270 268 def _init_template(self):
271 269 """
272 270 Make sure a template name is specified. If one isn't specified, try to
273 271 build one from the information we know.
274 272 """
275 273 self._template_file_changed('template_file', self.template_file, self.template_file)
276 274
277 275
278 276 def _init_environment(self, extra_loaders=None):
279 277 """
280 278 Create the Jinja templating environment.
281 279 """
282 280 here = os.path.dirname(os.path.realpath(__file__))
283 281 loaders = []
284 282 if extra_loaders:
285 283 loaders.extend(extra_loaders)
286 284
287 285 paths = self.template_path
288 286 paths.extend([os.path.join(here, self.default_template_path),
289 287 os.path.join(here, self.template_skeleton_path)])
290 288 loaders.append(FileSystemLoader(paths))
291 289
292 290 self.environment = Environment(
293 291 loader= ChoiceLoader(loaders),
294 292 extensions=JINJA_EXTENSIONS
295 293 )
296 294
297 295 #Set special Jinja2 syntax that will not conflict with latex.
298 296 if self.jinja_logic_block_start:
299 297 self.environment.block_start_string = self.jinja_logic_block_start
300 298 if self.jinja_logic_block_end:
301 299 self.environment.block_end_string = self.jinja_logic_block_end
302 300 if self.jinja_variable_block_start:
303 301 self.environment.variable_start_string = self.jinja_variable_block_start
304 302 if self.jinja_variable_block_end:
305 303 self.environment.variable_end_string = self.jinja_variable_block_end
306 304 if self.jinja_comment_block_start:
307 305 self.environment.comment_start_string = self.jinja_comment_block_start
308 306 if self.jinja_comment_block_end:
309 307 self.environment.comment_end_string = self.jinja_comment_block_end
310 308
311 309
312 310 def _init_filters(self):
313 311 """
314 312 Register all of the filters required for the exporter.
315 313 """
316 314
317 315 #Add default filters to the Jinja2 environment
318 316 for key, value in default_filters.items():
319 317 self.register_filter(key, value)
320 318
321 319 #Load user filters. Overwrite existing filters if need be.
322 320 if self.filters:
323 321 for key, user_filter in self.filters.items():
324 322 self.register_filter(key, user_filter)
@@ -1,54 +1,54
1 1 """Base TestCase class for testing Exporters"""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2013, the IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 import os
16 16
17 17 from IPython.testing.decorators import onlyif_cmds_exist
18 18
19 19 from ...tests.base import TestsBase
20 20
21 21 #-----------------------------------------------------------------------------
22 22 # Class
23 23 #-----------------------------------------------------------------------------
24 24
25 25 all_raw_mimetypes = {
26 'application/x-python',
26 'text/x-python',
27 27 'text/markdown',
28 28 'text/html',
29 29 'text/restructuredtext',
30 30 'text/latex',
31 31 }
32 32
33 33 class ExportersTestsBase(TestsBase):
34 34 """Contains base test functions for exporters"""
35 35
36 36 exporter_class = None
37 37 should_include_raw = None
38 38
39 39 def _get_notebook(self, nb_name='notebook2.ipynb'):
40 40 return os.path.join(self._get_files_path(), nb_name)
41 41
42 42 @onlyif_cmds_exist('pandoc')
43 43 def test_raw_cell_inclusion(self):
44 44 """test raw cell inclusion based on raw_mimetype metadata"""
45 45 if self.should_include_raw is None:
46 46 return
47 47 exporter = self.exporter_class()
48 48 (output, resources) = exporter.from_filename(self._get_notebook('rawtest.ipynb'))
49 49 for inc in self.should_include_raw:
50 50 self.assertIn('raw %s' % inc, output, "should include %s" % inc)
51 51 self.assertIn('no raw_mimetype metadata', output)
52 52 for exc in all_raw_mimetypes.difference(self.should_include_raw):
53 53 self.assertNotIn('raw %s' % exc, output, "should exclude %s" % exc)
54 54 self.assertNotIn('never be included', output)
@@ -1,84 +1,84
1 1 {
2 2 "metadata": {
3 3 "name": ""
4 4 },
5 5 "nbformat": 3,
6 6 "nbformat_minor": 0,
7 7 "worksheets": [
8 8 {
9 9 "cells": [
10 10 {
11 11 "cell_type": "raw",
12 12 "metadata": {
13 13 "raw_mimetype": "text/html"
14 14 },
15 15 "source": [
16 16 "<b>raw html</b>"
17 17 ]
18 18 },
19 19 {
20 20 "cell_type": "raw",
21 21 "metadata": {
22 22 "raw_mimetype": "text/markdown"
23 23 },
24 24 "source": [
25 25 "* raw markdown\n",
26 26 "* bullet\n",
27 27 "* list"
28 28 ]
29 29 },
30 30 {
31 31 "cell_type": "raw",
32 32 "metadata": {
33 33 "raw_mimetype": "text/restructuredtext"
34 34 },
35 35 "source": [
36 36 "``raw rst``\n",
37 37 "\n",
38 38 ".. sourcecode:: python\n",
39 39 "\n",
40 40 " def foo(): pass\n"
41 41 ]
42 42 },
43 43 {
44 44 "cell_type": "raw",
45 45 "metadata": {
46 "raw_mimetype": "application/x-python"
46 "raw_mimetype": "text/x-python"
47 47 },
48 48 "source": [
49 49 "def bar():\n",
50 50 " \"\"\"raw python\"\"\"\n",
51 51 " pass"
52 52 ]
53 53 },
54 54 {
55 55 "cell_type": "raw",
56 56 "metadata": {
57 57 "raw_mimetype": "text/latex"
58 58 },
59 59 "source": [
60 60 "\\LaTeX\n",
61 61 "% raw latex"
62 62 ]
63 63 },
64 64 {
65 65 "cell_type": "raw",
66 66 "metadata": {},
67 67 "source": [
68 68 "# no raw_mimetype metadata, should be included by default"
69 69 ]
70 70 },
71 71 {
72 72 "cell_type": "raw",
73 73 "metadata": {
74 74 "raw_mimetype": "doesnotexist"
75 75 },
76 76 "source": [
77 77 "garbage format defined, should never be included"
78 78 ]
79 79 }
80 80 ],
81 81 "metadata": {}
82 82 }
83 83 ]
84 84 }
@@ -1,102 +1,103
1 1 """Module containing a preprocessor that extracts all of the outputs from the
2 2 notebook file. The extracted outputs are returned in the 'resources' dictionary.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15
16 16 import base64
17 17 import sys
18 18 import os
19 19
20 from IPython.utils.traitlets import Unicode
20 from IPython.utils.traitlets import Unicode, Set
21 21 from .base import Preprocessor
22 22 from IPython.utils import py3compat
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Classes
26 26 #-----------------------------------------------------------------------------
27 27
28 28 class ExtractOutputPreprocessor(Preprocessor):
29 29 """
30 30 Extracts all of the outputs from the notebook file. The extracted
31 31 outputs are returned in the 'resources' dictionary.
32 32 """
33 33
34 34 output_filename_template = Unicode(
35 35 "{unique_key}_{cell_index}_{index}.{extension}", config=True)
36 36
37 extract_output_types = Set({'png', 'jpg', 'svg', 'pdf'}, config=True)
37 38
38 39 def preprocess_cell(self, cell, resources, cell_index):
39 40 """
40 41 Apply a transformation on each cell,
41 42
42 43 Parameters
43 44 ----------
44 45 cell : NotebookNode cell
45 46 Notebook cell being processed
46 47 resources : dictionary
47 48 Additional resources used in the conversion process. Allows
48 49 preprocessors to pass variables into the Jinja engine.
49 50 cell_index : int
50 51 Index of the cell being processed (see base.py)
51 52 """
52 53
53 54 #Get the unique key from the resource dict if it exists. If it does not
54 55 #exist, use 'output' as the default. Also, get files directory if it
55 56 #has been specified
56 57 unique_key = resources.get('unique_key', 'output')
57 58 output_files_dir = resources.get('output_files_dir', None)
58 59
59 60 #Make sure outputs key exists
60 61 if not isinstance(resources['outputs'], dict):
61 62 resources['outputs'] = {}
62 63
63 64 #Loop through all of the outputs in the cell
64 65 for index, out in enumerate(cell.get('outputs', [])):
65 66
66 #Get the output in data formats that the template is interested in.
67 for out_type in self.display_data_priority:
67 #Get the output in data formats that the template needs extracted
68 for out_type in self.extract_output_types:
68 69 if out.hasattr(out_type):
69 70 data = out[out_type]
70 71
71 72 #Binary files are base64-encoded, SVG is already XML
72 73 if out_type in ('png', 'jpg', 'jpeg', 'pdf'):
73 74
74 75 # data is b64-encoded as text (str, unicode)
75 76 # decodestring only accepts bytes
76 77 data = py3compat.cast_bytes(data)
77 78 data = base64.decodestring(data)
78 79 elif sys.platform == 'win32':
79 80 data = data.replace('\n', '\r\n').encode("UTF-8")
80 81 else:
81 82 data = data.encode("UTF-8")
82 83
83 84 #Build an output name
84 85 filename = self.output_filename_template.format(
85 86 unique_key=unique_key,
86 87 cell_index=cell_index,
87 88 index=index,
88 89 extension=out_type)
89 90
90 91 #On the cell, make the figure available via
91 92 # cell.outputs[i].svg_filename ... etc (svg in example)
92 93 # Where
93 94 # cell.outputs[i].svg contains the data
94 95 if output_files_dir is not None:
95 96 filename = os.path.join(output_files_dir, filename)
96 97 out[out_type + '_filename'] = filename
97 98
98 99 #In the resources, make the figure available via
99 100 # resources['outputs']['filename'] = data
100 101 resources['outputs'][filename] = data
101 102
102 103 return cell, resources
@@ -1,64 +1,65
1 1 """
2 2 Module with tests for the extractoutput preprocessor
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 from .base import PreprocessorTestsBase
18 18 from ..extractoutput import ExtractOutputPreprocessor
19 19
20 20
21 21 #-----------------------------------------------------------------------------
22 22 # Class
23 23 #-----------------------------------------------------------------------------
24 24
25 25 class TestExtractOutput(PreprocessorTestsBase):
26 26 """Contains test functions for extractoutput.py"""
27 27
28 28
29 29 def build_preprocessor(self):
30 30 """Make an instance of a preprocessor"""
31 31 preprocessor = ExtractOutputPreprocessor()
32 preprocessor.extract_output_types = {'text', 'png'}
32 33 preprocessor.enabled = True
33 34 return preprocessor
34 35
35 36
36 37 def test_constructor(self):
37 38 """Can a ExtractOutputPreprocessor be constructed?"""
38 39 self.build_preprocessor()
39 40
40 41
41 42 def test_output(self):
42 43 """Test the output of the ExtractOutputPreprocessor"""
43 44 nb = self.build_notebook()
44 45 res = self.build_resources()
45 46 preprocessor = self.build_preprocessor()
46 47 nb, res = preprocessor(nb, res)
47 48
48 49 # Check if text was extracted.
49 50 output = nb.worksheets[0].cells[0].outputs[1]
50 51 assert 'text_filename' in output
51 52 text_filename = output['text_filename']
52 53
53 54 # Check if png was extracted.
54 55 output = nb.worksheets[0].cells[0].outputs[6]
55 56 assert 'png_filename' in output
56 57 png_filename = output['png_filename']
57 58
58 59 # Verify text output
59 60 assert text_filename in res['outputs']
60 61 self.assertEqual(res['outputs'][text_filename], b'b')
61 62
62 63 # Verify png output
63 64 assert png_filename in res['outputs']
64 65 self.assertEqual(res['outputs'][png_filename], b'g')
@@ -1,98 +1,98
1 1 ((= Auto-generated template file, DO NOT edit directly!
2 2 To edit this file, please refer to ../../skeleton/README.md =))
3 3
4 4
5 5 ((=
6 6
7 7 DO NOT USE THIS AS A BASE,
8 8 IF YOU ARE COPY AND PASTING THIS FILE
9 9 YOU ARE PROBABLY DOING THINGS INCORRECTLY.
10 10
11 11 Null template, does nothing except defining a basic structure
12 12 To layout the different blocks of a notebook.
13 13
14 14 Subtemplates can override blocks to define their custom representation.
15 15
16 16 If one of the block you do overwrite is not a leave block, consider
17 17 calling super.
18 18
19 19 ((*- block nonLeaveBlock -*))
20 20 #add stuff at beginning
21 21 ((( super() )))
22 22 #add stuff at end
23 23 ((*- endblock nonLeaveBlock -*))
24 24
25 25 consider calling super even if it is a leave block, we might insert more blocks later.
26 26
27 27 =))
28 28 ((*- block header -*))
29 29 ((*- endblock header -*))
30 30 ((*- block body -*))
31 31 ((*- for worksheet in nb.worksheets -*))
32 32 ((*- for cell in worksheet.cells -*))
33 33 ((*- block any_cell scoped -*))
34 34 ((*- if cell.cell_type in ['code'] -*))
35 35 ((*- block codecell scoped -*))
36 36 ((*- block input_group -*))
37 37 ((*- block in_prompt -*))((*- endblock in_prompt -*))
38 38 ((*- block input -*))((*- endblock input -*))
39 39 ((*- endblock input_group -*))
40 40 ((*- if cell.outputs -*))
41 41 ((*- block output_group -*))
42 42 ((*- block output_prompt -*))((*- endblock output_prompt -*))
43 43 ((*- block outputs scoped -*))
44 44 ((*- for output in cell.outputs -*))
45 45 ((*- block output scoped -*))
46 46 ((*- if output.output_type in ['pyout'] -*))
47 47 ((*- block pyout scoped -*))((*- endblock pyout -*))
48 48 ((*- elif output.output_type in ['stream'] -*))
49 49 ((*- block stream scoped -*))
50 50 ((*- if output.stream in ['stdout'] -*))
51 51 ((*- block stream_stdout scoped -*))
52 52 ((*- endblock stream_stdout -*))
53 53 ((*- elif output.stream in ['stderr'] -*))
54 54 ((*- block stream_stderr scoped -*))
55 55 ((*- endblock stream_stderr -*))
56 56 ((*- endif -*))
57 57 ((*- endblock stream -*))
58 58 ((*- elif output.output_type in ['display_data'] -*))
59 59 ((*- block display_data scoped -*))
60 60 ((*- block data_priority scoped -*))
61 61 ((*- endblock data_priority -*))
62 62 ((*- endblock display_data -*))
63 63 ((*- elif output.output_type in ['pyerr'] -*))
64 64 ((*- block pyerr scoped -*))
65 65 ((*- for line in output.traceback -*))
66 66 ((*- block traceback_line scoped -*))((*- endblock traceback_line -*))
67 67 ((*- endfor -*))
68 68 ((*- endblock pyerr -*))
69 69 ((*- endif -*))
70 70 ((*- endblock output -*))
71 71 ((*- endfor -*))
72 72 ((*- endblock outputs -*))
73 73 ((*- endblock output_group -*))
74 74 ((*- endif -*))
75 75 ((*- endblock codecell -*))
76 76 ((*- elif cell.cell_type in ['markdown'] -*))
77 77 ((*- block markdowncell scoped-*))
78 78 ((*- endblock markdowncell -*))
79 79 ((*- elif cell.cell_type in ['heading'] -*))
80 80 ((*- block headingcell scoped-*))
81 81 ((*- endblock headingcell -*))
82 82 ((*- elif cell.cell_type in ['raw'] -*))
83 83 ((*- block rawcell scoped -*))
84 ((* if cell.metadata.get('raw_mimetype', resources.get('raw_mimetype')) == resources.get('raw_mimetype') *))
84 ((* if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) *))
85 85 ((( cell.source )))
86 86 ((* endif *))
87 87 ((*- endblock rawcell -*))
88 88 ((*- else -*))
89 89 ((*- block unknowncell scoped-*))
90 90 ((*- endblock unknowncell -*))
91 91 ((*- endif -*))
92 92 ((*- endblock any_cell -*))
93 93 ((*- endfor -*))
94 94 ((*- endfor -*))
95 95 ((*- endblock body -*))
96 96
97 97 ((*- block footer -*))
98 98 ((*- endblock footer -*))
@@ -1,51 +1,51
1 1 {%- extends 'null.tpl' -%}
2 2
3 3
4 4 {% block in_prompt %}
5 5 # In[{{ cell.prompt_number if cell.prompt_number else ' ' }}]:
6 6 {% endblock in_prompt %}
7 7
8 8 {% block output_prompt %}
9 9 # Out[{{ cell.prompt_number }}]:
10 10 {% endblock output_prompt %}
11 11
12 12 {% block input %}
13 13 {{ cell.input | ipython2python }}
14 14 {% endblock input %}
15 15
16 16 {# Those Two are for error displaying
17 17 even if the first one seem to do nothing,
18 18 it introduces a new line
19 19 #}
20 20 {% block pyerr %}
21 21 {{ super() }}
22 22 {% endblock pyerr %}
23 23
24 24 {% block traceback_line %}
25 25 {{ line | indent | strip_ansi }}
26 26 {% endblock traceback_line %}
27 27 {# .... #}
28 28
29 29 {% block pyout %}
30 {{ output.text | indent | comment_lines }}
30 {{ output.text or '' | indent | comment_lines }}
31 31 {% endblock pyout %}
32 32
33 33 {% block stream %}
34 34 {{ output.text | indent | comment_lines }}
35 35 {% endblock stream %}
36 36
37 37 {% block display_data scoped %}
38 38 # image file:
39 39 {% endblock display_data %}
40 40
41 41 {% block markdowncell scoped %}
42 42 {{ cell.source | comment_lines }}
43 43 {% endblock markdowncell %}
44 44
45 45 {% block headingcell scoped %}
46 46 {{ '#' * cell.level }}{{ cell.source | replace('\n', ' ') | comment_lines }}
47 47 {% endblock headingcell %}
48 48
49 49 {% block unknowncell scoped %}
50 50 unknown type {{ cell.type }}
51 51 {% endblock unknowncell %}
@@ -1,94 +1,94
1 1 {#
2 2
3 3 DO NOT USE THIS AS A BASE,
4 4 IF YOU ARE COPY AND PASTING THIS FILE
5 5 YOU ARE PROBABLY DOING THINGS INCORRECTLY.
6 6
7 7 Null template, does nothing except defining a basic structure
8 8 To layout the different blocks of a notebook.
9 9
10 10 Subtemplates can override blocks to define their custom representation.
11 11
12 12 If one of the block you do overwrite is not a leave block, consider
13 13 calling super.
14 14
15 15 {%- block nonLeaveBlock -%}
16 16 #add stuff at beginning
17 17 {{ super() }}
18 18 #add stuff at end
19 19 {%- endblock nonLeaveBlock -%}
20 20
21 21 consider calling super even if it is a leave block, we might insert more blocks later.
22 22
23 23 #}
24 24 {%- block header -%}
25 25 {%- endblock header -%}
26 26 {%- block body -%}
27 27 {%- for worksheet in nb.worksheets -%}
28 28 {%- for cell in worksheet.cells -%}
29 29 {%- block any_cell scoped -%}
30 30 {%- if cell.cell_type in ['code'] -%}
31 31 {%- block codecell scoped -%}
32 32 {%- block input_group -%}
33 33 {%- block in_prompt -%}{%- endblock in_prompt -%}
34 34 {%- block input -%}{%- endblock input -%}
35 35 {%- endblock input_group -%}
36 36 {%- if cell.outputs -%}
37 37 {%- block output_group -%}
38 38 {%- block output_prompt -%}{%- endblock output_prompt -%}
39 39 {%- block outputs scoped -%}
40 40 {%- for output in cell.outputs -%}
41 41 {%- block output scoped -%}
42 42 {%- if output.output_type in ['pyout'] -%}
43 43 {%- block pyout scoped -%}{%- endblock pyout -%}
44 44 {%- elif output.output_type in ['stream'] -%}
45 45 {%- block stream scoped -%}
46 46 {%- if output.stream in ['stdout'] -%}
47 47 {%- block stream_stdout scoped -%}
48 48 {%- endblock stream_stdout -%}
49 49 {%- elif output.stream in ['stderr'] -%}
50 50 {%- block stream_stderr scoped -%}
51 51 {%- endblock stream_stderr -%}
52 52 {%- endif -%}
53 53 {%- endblock stream -%}
54 54 {%- elif output.output_type in ['display_data'] -%}
55 55 {%- block display_data scoped -%}
56 56 {%- block data_priority scoped -%}
57 57 {%- endblock data_priority -%}
58 58 {%- endblock display_data -%}
59 59 {%- elif output.output_type in ['pyerr'] -%}
60 60 {%- block pyerr scoped -%}
61 61 {%- for line in output.traceback -%}
62 62 {%- block traceback_line scoped -%}{%- endblock traceback_line -%}
63 63 {%- endfor -%}
64 64 {%- endblock pyerr -%}
65 65 {%- endif -%}
66 66 {%- endblock output -%}
67 67 {%- endfor -%}
68 68 {%- endblock outputs -%}
69 69 {%- endblock output_group -%}
70 70 {%- endif -%}
71 71 {%- endblock codecell -%}
72 72 {%- elif cell.cell_type in ['markdown'] -%}
73 73 {%- block markdowncell scoped-%}
74 74 {%- endblock markdowncell -%}
75 75 {%- elif cell.cell_type in ['heading'] -%}
76 76 {%- block headingcell scoped-%}
77 77 {%- endblock headingcell -%}
78 78 {%- elif cell.cell_type in ['raw'] -%}
79 79 {%- block rawcell scoped -%}
80 {% if cell.metadata.get('raw_mimetype', resources.get('raw_mimetype', '')).lower() in resources.get('raw_mimetypes', ['']) %}
80 {% if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) %}
81 81 {{ cell.source }}
82 82 {% endif %}
83 83 {%- endblock rawcell -%}
84 84 {%- else -%}
85 85 {%- block unknowncell scoped-%}
86 86 {%- endblock unknowncell -%}
87 87 {%- endif -%}
88 88 {%- endblock any_cell -%}
89 89 {%- endfor -%}
90 90 {%- endfor -%}
91 91 {%- endblock body -%}
92 92
93 93 {%- block footer -%}
94 94 {%- endblock footer -%}
@@ -1,216 +1,217
1 1 """The basic dict based notebook format.
2 2
3 3 The Python representation of a notebook is a nested structure of
4 4 dictionary subclasses that support attribute access
5 5 (IPython.utils.ipstruct.Struct). The functions in this module are merely
6 6 helpers to build the structs in the right form.
7 7
8 8 Authors:
9 9
10 10 * Brian Granger
11 11 """
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Copyright (C) 2008-2011 The IPython Development Team
15 15 #
16 16 # Distributed under the terms of the BSD License. The full license is in
17 17 # the file COPYING, distributed as part of this software.
18 18 #-----------------------------------------------------------------------------
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Imports
22 22 #-----------------------------------------------------------------------------
23 23
24 24 import pprint
25 25 import uuid
26 26
27 27 from IPython.utils.ipstruct import Struct
28 28 from IPython.utils.py3compat import cast_unicode, unicode_type
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Code
32 32 #-----------------------------------------------------------------------------
33 33
34 34 # Change this when incrementing the nbformat version
35 35 nbformat = 3
36 36 nbformat_minor = 0
37 37
38 38 class NotebookNode(Struct):
39 39 pass
40 40
41 41
42 42 def from_dict(d):
43 43 if isinstance(d, dict):
44 44 newd = NotebookNode()
45 45 for k,v in d.items():
46 46 newd[k] = from_dict(v)
47 47 return newd
48 48 elif isinstance(d, (tuple, list)):
49 49 return [from_dict(i) for i in d]
50 50 else:
51 51 return d
52 52
53 53
54 54 def new_output(output_type=None, output_text=None, output_png=None,
55 55 output_html=None, output_svg=None, output_latex=None, output_json=None,
56 56 output_javascript=None, output_jpeg=None, prompt_number=None,
57 57 ename=None, evalue=None, traceback=None, stream=None, metadata=None):
58 """Create a new code cell with input and output"""
58 """Create a new output, to go in the ``cell.outputs`` list of a code cell.
59 """
59 60 output = NotebookNode()
60 61 if output_type is not None:
61 62 output.output_type = unicode_type(output_type)
62 63
63 64 if metadata is None:
64 65 metadata = {}
65 66 if not isinstance(metadata, dict):
66 67 raise TypeError("metadata must be dict")
67 68 output.metadata = metadata
68 69
69 70 if output_type != 'pyerr':
70 71 if output_text is not None:
71 72 output.text = cast_unicode(output_text)
72 73 if output_png is not None:
73 74 output.png = cast_unicode(output_png)
74 75 if output_jpeg is not None:
75 76 output.jpeg = cast_unicode(output_jpeg)
76 77 if output_html is not None:
77 78 output.html = cast_unicode(output_html)
78 79 if output_svg is not None:
79 80 output.svg = cast_unicode(output_svg)
80 81 if output_latex is not None:
81 82 output.latex = cast_unicode(output_latex)
82 83 if output_json is not None:
83 84 output.json = cast_unicode(output_json)
84 85 if output_javascript is not None:
85 86 output.javascript = cast_unicode(output_javascript)
86 87
87 88 if output_type == u'pyout':
88 89 if prompt_number is not None:
89 90 output.prompt_number = int(prompt_number)
90 91
91 92 if output_type == u'pyerr':
92 93 if ename is not None:
93 94 output.ename = cast_unicode(ename)
94 95 if evalue is not None:
95 96 output.evalue = cast_unicode(evalue)
96 97 if traceback is not None:
97 98 output.traceback = [cast_unicode(frame) for frame in list(traceback)]
98 99
99 100 if output_type == u'stream':
100 101 output.stream = 'stdout' if stream is None else cast_unicode(stream)
101 102
102 103 return output
103 104
104 105
105 106 def new_code_cell(input=None, prompt_number=None, outputs=None,
106 107 language=u'python', collapsed=False, metadata=None):
107 108 """Create a new code cell with input and output"""
108 109 cell = NotebookNode()
109 110 cell.cell_type = u'code'
110 111 if language is not None:
111 112 cell.language = cast_unicode(language)
112 113 if input is not None:
113 114 cell.input = cast_unicode(input)
114 115 if prompt_number is not None:
115 116 cell.prompt_number = int(prompt_number)
116 117 if outputs is None:
117 118 cell.outputs = []
118 119 else:
119 120 cell.outputs = outputs
120 121 if collapsed is not None:
121 122 cell.collapsed = bool(collapsed)
122 123 cell.metadata = NotebookNode(metadata or {})
123 124
124 125 return cell
125 126
126 127 def new_text_cell(cell_type, source=None, rendered=None, metadata=None):
127 128 """Create a new text cell."""
128 129 cell = NotebookNode()
129 130 # VERSIONHACK: plaintext -> raw
130 131 # handle never-released plaintext name for raw cells
131 132 if cell_type == 'plaintext':
132 133 cell_type = 'raw'
133 134 if source is not None:
134 135 cell.source = cast_unicode(source)
135 136 if rendered is not None:
136 137 cell.rendered = cast_unicode(rendered)
137 138 cell.metadata = NotebookNode(metadata or {})
138 139 cell.cell_type = cell_type
139 140 return cell
140 141
141 142
142 143 def new_heading_cell(source=None, rendered=None, level=1, metadata=None):
143 144 """Create a new section cell with a given integer level."""
144 145 cell = NotebookNode()
145 146 cell.cell_type = u'heading'
146 147 if source is not None:
147 148 cell.source = cast_unicode(source)
148 149 if rendered is not None:
149 150 cell.rendered = cast_unicode(rendered)
150 151 cell.level = int(level)
151 152 cell.metadata = NotebookNode(metadata or {})
152 153 return cell
153 154
154 155
155 156 def new_worksheet(name=None, cells=None, metadata=None):
156 157 """Create a worksheet by name with with a list of cells."""
157 158 ws = NotebookNode()
158 159 if name is not None:
159 160 ws.name = cast_unicode(name)
160 161 if cells is None:
161 162 ws.cells = []
162 163 else:
163 164 ws.cells = list(cells)
164 165 ws.metadata = NotebookNode(metadata or {})
165 166 return ws
166 167
167 168
168 169 def new_notebook(name=None, metadata=None, worksheets=None):
169 170 """Create a notebook by name, id and a list of worksheets."""
170 171 nb = NotebookNode()
171 172 nb.nbformat = nbformat
172 173 nb.nbformat_minor = nbformat_minor
173 174 if worksheets is None:
174 175 nb.worksheets = []
175 176 else:
176 177 nb.worksheets = list(worksheets)
177 178 if metadata is None:
178 179 nb.metadata = new_metadata()
179 180 else:
180 181 nb.metadata = NotebookNode(metadata)
181 182 if name is not None:
182 183 nb.metadata.name = cast_unicode(name)
183 184 return nb
184 185
185 186
186 187 def new_metadata(name=None, authors=None, license=None, created=None,
187 188 modified=None, gistid=None):
188 189 """Create a new metadata node."""
189 190 metadata = NotebookNode()
190 191 if name is not None:
191 192 metadata.name = cast_unicode(name)
192 193 if authors is not None:
193 194 metadata.authors = list(authors)
194 195 if created is not None:
195 196 metadata.created = cast_unicode(created)
196 197 if modified is not None:
197 198 metadata.modified = cast_unicode(modified)
198 199 if license is not None:
199 200 metadata.license = cast_unicode(license)
200 201 if gistid is not None:
201 202 metadata.gistid = cast_unicode(gistid)
202 203 return metadata
203 204
204 205 def new_author(name=None, email=None, affiliation=None, url=None):
205 206 """Create a new author."""
206 207 author = NotebookNode()
207 208 if name is not None:
208 209 author.name = cast_unicode(name)
209 210 if email is not None:
210 211 author.email = cast_unicode(email)
211 212 if affiliation is not None:
212 213 author.affiliation = cast_unicode(affiliation)
213 214 if url is not None:
214 215 author.url = cast_unicode(url)
215 216 return author
216 217
General Comments 0
You need to be logged in to leave comments. Login now