##// END OF EJS Templates
Merge pull request #7446 from bollwyvl/nbconvert-pdf-title...
Min RK -
r19924:7de902aa merge
parent child Browse files
Show More
@@ -1,151 +1,161 b''
1 1 """Tornado handlers for nbconvert."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import io
7 7 import os
8 8 import zipfile
9 9
10 10 from tornado import web
11 11
12 12 from ..base.handlers import (
13 13 IPythonHandler, FilesRedirectHandler,
14 14 path_regex,
15 15 )
16 16 from IPython.nbformat import from_dict
17 17
18 18 from IPython.utils.py3compat import cast_bytes
19 from IPython.utils import text
19 20
20 21 def find_resource_files(output_files_dir):
21 22 files = []
22 23 for dirpath, dirnames, filenames in os.walk(output_files_dir):
23 24 files.extend([os.path.join(dirpath, f) for f in filenames])
24 25 return files
25 26
26 27 def respond_zip(handler, name, output, resources):
27 28 """Zip up the output and resource files and respond with the zip file.
28 29
29 30 Returns True if it has served a zip file, False if there are no resource
30 31 files, in which case we serve the plain output file.
31 32 """
32 33 # Check if we have resource files we need to zip
33 34 output_files = resources.get('outputs', None)
34 35 if not output_files:
35 36 return False
36 37
37 38 # Headers
38 39 zip_filename = os.path.splitext(name)[0] + '.zip'
39 40 handler.set_header('Content-Disposition',
40 41 'attachment; filename="%s"' % zip_filename)
41 42 handler.set_header('Content-Type', 'application/zip')
42 43
43 44 # Prepare the zip file
44 45 buffer = io.BytesIO()
45 46 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
46 47 output_filename = os.path.splitext(name)[0] + resources['output_extension']
47 48 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
48 49 for filename, data in output_files.items():
49 50 zipf.writestr(os.path.basename(filename), data)
50 51 zipf.close()
51 52
52 53 handler.finish(buffer.getvalue())
53 54 return True
54 55
55 56 def get_exporter(format, **kwargs):
56 57 """get an exporter, raising appropriate errors"""
57 58 # if this fails, will raise 500
58 59 try:
59 60 from IPython.nbconvert.exporters.export import exporter_map
60 61 except ImportError as e:
61 62 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
62 63
63 64 try:
64 65 Exporter = exporter_map[format]
65 66 except KeyError:
66 67 # should this be 400?
67 68 raise web.HTTPError(404, u"No exporter for format: %s" % format)
68 69
69 70 try:
70 71 return Exporter(**kwargs)
71 72 except Exception as e:
72 73 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
73 74
74 75 class NbconvertFileHandler(IPythonHandler):
75 76
76 77 SUPPORTED_METHODS = ('GET',)
77 78
78 79 @web.authenticated
79 80 def get(self, format, path):
80 81
81 82 exporter = get_exporter(format, config=self.config, log=self.log)
82 83
83 84 path = path.strip('/')
84 85 model = self.contents_manager.get(path=path)
85 86 name = model['name']
86 87 if model['type'] != 'notebook':
87 88 raise web.HTTPError(400, "Not a notebook: %s" % path)
88 89
89 90 self.set_header('Last-Modified', model['last_modified'])
90
91
91 92 try:
92 output, resources = exporter.from_notebook_node(model['content'])
93 output, resources = exporter.from_notebook_node(
94 model['content'],
95 resources={
96 "metadata": {
97 "name": name[:name.rfind('.')],
98 "modified_date": (model['last_modified']
99 .strftime(text.date_format))
100 }
101 }
102 )
93 103 except Exception as e:
94 104 raise web.HTTPError(500, "nbconvert failed: %s" % e)
95 105
96 106 if respond_zip(self, name, output, resources):
97 107 return
98 108
99 109 # Force download if requested
100 110 if self.get_argument('download', 'false').lower() == 'true':
101 111 filename = os.path.splitext(name)[0] + resources['output_extension']
102 112 self.set_header('Content-Disposition',
103 113 'attachment; filename="%s"' % filename)
104 114
105 115 # MIME type
106 116 if exporter.output_mimetype:
107 117 self.set_header('Content-Type',
108 118 '%s; charset=utf-8' % exporter.output_mimetype)
109 119
110 120 self.finish(output)
111 121
112 122 class NbconvertPostHandler(IPythonHandler):
113 123 SUPPORTED_METHODS = ('POST',)
114 124
115 125 @web.authenticated
116 126 def post(self, format):
117 127 exporter = get_exporter(format, config=self.config)
118 128
119 129 model = self.get_json_body()
120 130 name = model.get('name', 'notebook.ipynb')
121 131 nbnode = from_dict(model['content'])
122 132
123 133 try:
124 134 output, resources = exporter.from_notebook_node(nbnode)
125 135 except Exception as e:
126 136 raise web.HTTPError(500, "nbconvert failed: %s" % e)
127 137
128 138 if respond_zip(self, name, output, resources):
129 139 return
130 140
131 141 # MIME type
132 142 if exporter.output_mimetype:
133 143 self.set_header('Content-Type',
134 144 '%s; charset=utf-8' % exporter.output_mimetype)
135 145
136 146 self.finish(output)
137 147
138 148
139 149 #-----------------------------------------------------------------------------
140 150 # URL to handler mappings
141 151 #-----------------------------------------------------------------------------
142 152
143 153 _format_regex = r"(?P<format>\w+)"
144 154
145 155
146 156 default_handlers = [
147 157 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
148 158 (r"/nbconvert/%s%s" % (_format_regex, path_regex),
149 159 NbconvertFileHandler),
150 160 (r"/nbconvert/html%s" % path_regex, FilesRedirectHandler),
151 161 ]
@@ -1,277 +1,279 b''
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 from __future__ import print_function, absolute_import
7 7
8 8 import io
9 9 import os
10 10 import copy
11 11 import collections
12 12 import datetime
13 13
14 14 from IPython.config.configurable import LoggingConfigurable
15 15 from IPython.config import Config
16 16 from IPython import nbformat
17 17 from IPython.utils.traitlets import MetaHasTraits, Unicode, List, TraitError
18 18 from IPython.utils.importstring import import_item
19 19 from IPython.utils import text, py3compat
20 20
21 21
22 22 class ResourcesDict(collections.defaultdict):
23 23 def __missing__(self, key):
24 24 return ''
25 25
26 26
27 27 class FilenameExtension(Unicode):
28 28 """A trait for filename extensions."""
29 29
30 30 default_value = u''
31 31 info_text = 'a filename extension, beginning with a dot'
32 32
33 33 def validate(self, obj, value):
34 34 # cast to proper unicode
35 35 value = super(FilenameExtension, self).validate(obj, value)
36 36
37 37 # check that it starts with a dot
38 38 if value and not value.startswith('.'):
39 39 msg = "FileExtension trait '{}' does not begin with a dot: {!r}"
40 40 raise TraitError(msg.format(self.name, value))
41 41
42 42 return value
43 43
44 44
45 45 class Exporter(LoggingConfigurable):
46 46 """
47 47 Class containing methods that sequentially run a list of preprocessors on a
48 48 NotebookNode object and then return the modified NotebookNode object and
49 49 accompanying resources dict.
50 50 """
51 51
52 52 file_extension = FilenameExtension(
53 53 '.txt', config=True,
54 54 help="Extension of the file that should be written to disk"
55 55 )
56 56
57 57 # MIME type of the result file, for HTTP response headers.
58 58 # This is *not* a traitlet, because we want to be able to access it from
59 59 # the class, not just on instances.
60 60 output_mimetype = ''
61 61
62 62 #Configurability, allows the user to easily add filters and preprocessors.
63 63 preprocessors = List(config=True,
64 64 help="""List of preprocessors, by name or namespace, to enable.""")
65 65
66 66 _preprocessors = List()
67 67
68 68 default_preprocessors = List(['IPython.nbconvert.preprocessors.coalesce_streams',
69 69 'IPython.nbconvert.preprocessors.SVG2PDFPreprocessor',
70 70 'IPython.nbconvert.preprocessors.ExtractOutputPreprocessor',
71 71 'IPython.nbconvert.preprocessors.CSSHTMLHeaderPreprocessor',
72 72 'IPython.nbconvert.preprocessors.RevealHelpPreprocessor',
73 73 'IPython.nbconvert.preprocessors.LatexPreprocessor',
74 74 'IPython.nbconvert.preprocessors.ClearOutputPreprocessor',
75 75 'IPython.nbconvert.preprocessors.ExecutePreprocessor',
76 76 'IPython.nbconvert.preprocessors.HighlightMagicsPreprocessor'],
77 77 config=True,
78 78 help="""List of preprocessors available by default, by name, namespace,
79 79 instance, or type.""")
80 80
81 81
82 82 def __init__(self, config=None, **kw):
83 83 """
84 84 Public constructor
85 85
86 86 Parameters
87 87 ----------
88 88 config : config
89 89 User configuration instance.
90 90 """
91 91 with_default_config = self.default_config
92 92 if config:
93 93 with_default_config.merge(config)
94 94
95 95 super(Exporter, self).__init__(config=with_default_config, **kw)
96 96
97 97 self._init_preprocessors()
98 98
99 99
100 100 @property
101 101 def default_config(self):
102 102 return Config()
103 103
104 104 def from_notebook_node(self, nb, resources=None, **kw):
105 105 """
106 106 Convert a notebook from a notebook node instance.
107 107
108 108 Parameters
109 109 ----------
110 110 nb : :class:`~IPython.nbformat.NotebookNode`
111 111 Notebook node (dict-like with attr-access)
112 112 resources : dict
113 113 Additional resources that can be accessed read/write by
114 114 preprocessors and filters.
115 115 **kw
116 116 Ignored (?)
117 117 """
118 118 nb_copy = copy.deepcopy(nb)
119 119 resources = self._init_resources(resources)
120 120
121 121 if 'language' in nb['metadata']:
122 122 resources['language'] = nb['metadata']['language'].lower()
123 123
124 124 # Preprocess
125 125 nb_copy, resources = self._preprocess(nb_copy, resources)
126 126
127 127 return nb_copy, resources
128 128
129 129
130 130 def from_filename(self, filename, resources=None, **kw):
131 131 """
132 132 Convert a notebook from a notebook file.
133 133
134 134 Parameters
135 135 ----------
136 136 filename : str
137 137 Full filename of the notebook file to open and convert.
138 138 """
139 139
140 140 # Pull the metadata from the filesystem.
141 141 if resources is None:
142 142 resources = ResourcesDict()
143 143 if not 'metadata' in resources or resources['metadata'] == '':
144 144 resources['metadata'] = ResourcesDict()
145 145 basename = os.path.basename(filename)
146 146 notebook_name = basename[:basename.rfind('.')]
147 147 resources['metadata']['name'] = notebook_name
148 148
149 149 modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename))
150 150 resources['metadata']['modified_date'] = modified_date.strftime(text.date_format)
151 151
152 152 with io.open(filename, encoding='utf-8') as f:
153 153 return self.from_notebook_node(nbformat.read(f, as_version=4), resources=resources, **kw)
154 154
155 155
156 156 def from_file(self, file_stream, resources=None, **kw):
157 157 """
158 158 Convert a notebook from a notebook file.
159 159
160 160 Parameters
161 161 ----------
162 162 file_stream : file-like object
163 163 Notebook file-like object to convert.
164 164 """
165 165 return self.from_notebook_node(nbformat.read(file_stream, as_version=4), resources=resources, **kw)
166 166
167 167
168 168 def register_preprocessor(self, preprocessor, enabled=False):
169 169 """
170 170 Register a preprocessor.
171 171 Preprocessors are classes that act upon the notebook before it is
172 172 passed into the Jinja templating engine. preprocessors are also
173 173 capable of passing additional information to the Jinja
174 174 templating engine.
175 175
176 176 Parameters
177 177 ----------
178 178 preprocessor : preprocessor
179 179 """
180 180 if preprocessor is None:
181 181 raise TypeError('preprocessor')
182 182 isclass = isinstance(preprocessor, type)
183 183 constructed = not isclass
184 184
185 185 # Handle preprocessor's registration based on it's type
186 186 if constructed and isinstance(preprocessor, py3compat.string_types):
187 187 # Preprocessor is a string, import the namespace and recursively call
188 188 # this register_preprocessor method
189 189 preprocessor_cls = import_item(preprocessor)
190 190 return self.register_preprocessor(preprocessor_cls, enabled)
191 191
192 192 if constructed and hasattr(preprocessor, '__call__'):
193 193 # Preprocessor is a function, no need to construct it.
194 194 # Register and return the preprocessor.
195 195 if enabled:
196 196 preprocessor.enabled = True
197 197 self._preprocessors.append(preprocessor)
198 198 return preprocessor
199 199
200 200 elif isclass and isinstance(preprocessor, MetaHasTraits):
201 201 # Preprocessor is configurable. Make sure to pass in new default for
202 202 # the enabled flag if one was specified.
203 203 self.register_preprocessor(preprocessor(parent=self), enabled)
204 204
205 205 elif isclass:
206 206 # Preprocessor is not configurable, construct it
207 207 self.register_preprocessor(preprocessor(), enabled)
208 208
209 209 else:
210 210 # Preprocessor is an instance of something without a __call__
211 211 # attribute.
212 212 raise TypeError('preprocessor')
213 213
214 214
215 215 def _init_preprocessors(self):
216 216 """
217 217 Register all of the preprocessors needed for this exporter, disabled
218 218 unless specified explicitly.
219 219 """
220 220 self._preprocessors = []
221 221
222 222 # Load default preprocessors (not necessarly enabled by default).
223 223 for preprocessor in self.default_preprocessors:
224 224 self.register_preprocessor(preprocessor)
225 225
226 226 # Load user-specified preprocessors. Enable by default.
227 227 for preprocessor in self.preprocessors:
228 228 self.register_preprocessor(preprocessor, enabled=True)
229 229
230 230
231 231 def _init_resources(self, resources):
232 232
233 233 #Make sure the resources dict is of ResourcesDict type.
234 234 if resources is None:
235 235 resources = ResourcesDict()
236 236 if not isinstance(resources, ResourcesDict):
237 237 new_resources = ResourcesDict()
238 238 new_resources.update(resources)
239 239 resources = new_resources
240 240
241 241 #Make sure the metadata extension exists in resources
242 242 if 'metadata' in resources:
243 243 if not isinstance(resources['metadata'], ResourcesDict):
244 resources['metadata'] = ResourcesDict(resources['metadata'])
244 new_metadata = ResourcesDict()
245 new_metadata.update(resources['metadata'])
246 resources['metadata'] = new_metadata
245 247 else:
246 248 resources['metadata'] = ResourcesDict()
247 249 if not resources['metadata']['name']:
248 250 resources['metadata']['name'] = 'Notebook'
249 251
250 252 #Set the output extension
251 253 resources['output_extension'] = self.file_extension
252 254 return resources
253 255
254 256
255 257 def _preprocess(self, nb, resources):
256 258 """
257 259 Preprocess the notebook before passing it into the Jinja engine.
258 260 To preprocess the notebook is to apply all of the
259 261
260 262 Parameters
261 263 ----------
262 264 nb : notebook node
263 265 notebook that is being exported.
264 266 resources : a dict of additional resources that
265 267 can be accessed read/write by preprocessors
266 268 """
267 269
268 270 # Do a copy.deepcopy first,
269 271 # we are never safe enough with what the preprocessors could do.
270 272 nbc = copy.deepcopy(nb)
271 273 resc = copy.deepcopy(resources)
272 274
273 275 #Run each preprocessor on the notebook. Carry the output along
274 276 #to each preprocessor
275 277 for preprocessor in self._preprocessors:
276 278 nbc, resc = preprocessor(nbc, resc)
277 279 return nbc, resc
General Comments 0
You need to be logged in to leave comments. Login now