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" % |
|
|
89 |
(r"/notebooks%s" % |
|
|
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 |
|
|
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" % |
|
|
274 |
(r"/api/notebooks%s/checkpoints/%s" % ( |
|
|
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" % |
|
|
277 |
(r"/api/notebooks%s" % |
|
|
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", " |
|
|
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 |
|
|
|
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 & 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, |
|
|
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= |
|
|
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" % |
|
|
74 |
(r"/tree%s" % |
|
|
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. |
|
|
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 |
' |
|
|
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": " |
|
|
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 |
|
|
67 |
for out_type in self. |
|
|
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', |
|
|
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', |
|
|
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