##// END OF EJS Templates
fixes #7360 by setting resources.metadata
Nicholas Bollweg -
Show More
@@ -1,151 +1,162 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 import collections
9 10
10 11 from tornado import web
11 12
12 13 from ..base.handlers import (
13 14 IPythonHandler, FilesRedirectHandler,
14 15 path_regex,
15 16 )
16 17 from IPython.nbformat import from_dict
17 18
18 19 from IPython.utils.py3compat import cast_bytes
20 from IPython.utils import text
19 21
20 22 def find_resource_files(output_files_dir):
21 23 files = []
22 24 for dirpath, dirnames, filenames in os.walk(output_files_dir):
23 25 files.extend([os.path.join(dirpath, f) for f in filenames])
24 26 return files
25 27
26 28 def respond_zip(handler, name, output, resources):
27 29 """Zip up the output and resource files and respond with the zip file.
28 30
29 31 Returns True if it has served a zip file, False if there are no resource
30 32 files, in which case we serve the plain output file.
31 33 """
32 34 # Check if we have resource files we need to zip
33 35 output_files = resources.get('outputs', None)
34 36 if not output_files:
35 37 return False
36 38
37 39 # Headers
38 40 zip_filename = os.path.splitext(name)[0] + '.zip'
39 41 handler.set_header('Content-Disposition',
40 42 'attachment; filename="%s"' % zip_filename)
41 43 handler.set_header('Content-Type', 'application/zip')
42 44
43 45 # Prepare the zip file
44 46 buffer = io.BytesIO()
45 47 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
46 48 output_filename = os.path.splitext(name)[0] + resources['output_extension']
47 49 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
48 50 for filename, data in output_files.items():
49 51 zipf.writestr(os.path.basename(filename), data)
50 52 zipf.close()
51 53
52 54 handler.finish(buffer.getvalue())
53 55 return True
54 56
55 57 def get_exporter(format, **kwargs):
56 58 """get an exporter, raising appropriate errors"""
57 59 # if this fails, will raise 500
58 60 try:
59 61 from IPython.nbconvert.exporters.export import exporter_map
60 62 except ImportError as e:
61 63 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
62 64
63 65 try:
64 66 Exporter = exporter_map[format]
65 67 except KeyError:
66 68 # should this be 400?
67 69 raise web.HTTPError(404, u"No exporter for format: %s" % format)
68 70
69 71 try:
70 72 return Exporter(**kwargs)
71 73 except Exception as e:
72 74 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
73 75
74 76 class NbconvertFileHandler(IPythonHandler):
75 77
76 78 SUPPORTED_METHODS = ('GET',)
77 79
78 80 @web.authenticated
79 81 def get(self, format, path):
80 82
81 83 exporter = get_exporter(format, config=self.config, log=self.log)
82 84
83 85 path = path.strip('/')
84 86 model = self.contents_manager.get(path=path)
85 87 name = model['name']
86 88 if model['type'] != 'notebook':
87 89 raise web.HTTPError(400, "Not a notebook: %s" % path)
88 90
89 91 self.set_header('Last-Modified', model['last_modified'])
90 92
91 93 try:
92 output, resources = exporter.from_notebook_node(model['content'])
94 output, resources = exporter.from_notebook_node(
95 model['content'],
96 resources={
97 "metadata": {
98 "name": name[:name.rfind('.')],
99 "modified_date": (model['last_modified']
100 .strftime(text.date_format))
101 }
102 }
103 )
93 104 except Exception as e:
94 105 raise web.HTTPError(500, "nbconvert failed: %s" % e)
95 106
96 107 if respond_zip(self, name, output, resources):
97 108 return
98 109
99 110 # Force download if requested
100 111 if self.get_argument('download', 'false').lower() == 'true':
101 112 filename = os.path.splitext(name)[0] + resources['output_extension']
102 113 self.set_header('Content-Disposition',
103 114 'attachment; filename="%s"' % filename)
104 115
105 116 # MIME type
106 117 if exporter.output_mimetype:
107 118 self.set_header('Content-Type',
108 119 '%s; charset=utf-8' % exporter.output_mimetype)
109 120
110 121 self.finish(output)
111 122
112 123 class NbconvertPostHandler(IPythonHandler):
113 124 SUPPORTED_METHODS = ('POST',)
114 125
115 126 @web.authenticated
116 127 def post(self, format):
117 128 exporter = get_exporter(format, config=self.config)
118 129
119 130 model = self.get_json_body()
120 131 name = model.get('name', 'notebook.ipynb')
121 132 nbnode = from_dict(model['content'])
122 133
123 134 try:
124 135 output, resources = exporter.from_notebook_node(nbnode)
125 136 except Exception as e:
126 137 raise web.HTTPError(500, "nbconvert failed: %s" % e)
127 138
128 139 if respond_zip(self, name, output, resources):
129 140 return
130 141
131 142 # MIME type
132 143 if exporter.output_mimetype:
133 144 self.set_header('Content-Type',
134 145 '%s; charset=utf-8' % exporter.output_mimetype)
135 146
136 147 self.finish(output)
137 148
138 149
139 150 #-----------------------------------------------------------------------------
140 151 # URL to handler mappings
141 152 #-----------------------------------------------------------------------------
142 153
143 154 _format_regex = r"(?P<format>\w+)"
144 155
145 156
146 157 default_handlers = [
147 158 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
148 159 (r"/nbconvert/%s%s" % (_format_regex, path_regex),
149 160 NbconvertFileHandler),
150 161 (r"/nbconvert/html%s" % path_regex, FilesRedirectHandler),
151 162 ]
@@ -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