Show More
@@ -1,4 +1,6 b'' | |||||
|
1 | import io | |||
1 | import os |
|
2 | import os | |
|
3 | import zipfile | |||
2 |
|
4 | |||
3 | from tornado import web |
|
5 | from tornado import web | |
4 |
|
6 | |||
@@ -6,12 +8,44 b' from ..base.handlers import IPythonHandler, notebook_path_regex' | |||||
6 | from IPython.nbformat.current import to_notebook_json |
|
8 | from IPython.nbformat.current import to_notebook_json | |
7 | from IPython.nbconvert.exporters.export import exporter_map |
|
9 | from IPython.nbconvert.exporters.export import exporter_map | |
8 | from IPython.utils import tz |
|
10 | from IPython.utils import tz | |
9 |
|
11 | from IPython.utils.py3compat import cast_bytes | ||
10 |
|
12 | |||
11 | def has_resource_files(resources): |
|
13 | import sys | |
12 | output_files_dir = resources.get('output_files_dir', "") |
|
14 | ||
13 | return bool(os.path.isdir(output_files_dir) and \ |
|
15 | def find_resource_files(output_files_dir): | |
14 | os.listdir(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 | |||
15 |
|
49 | |||
16 | class NbconvertFileHandler(IPythonHandler): |
|
50 | class NbconvertFileHandler(IPythonHandler): | |
17 |
|
51 | |||
@@ -29,9 +63,14 b' class NbconvertFileHandler(IPythonHandler):' | |||||
29 | info = os.stat(os_path) |
|
63 | info = os.stat(os_path) | |
30 | self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime)) |
|
64 | self.set_header('Last-Modified', tz.utcfromtimestamp(info.st_mtime)) | |
31 |
|
65 | |||
|
66 | output, resources = exporter.from_filename(os_path) | |||
|
67 | ||||
|
68 | if respond_zip(self, name, output, resources): | |||
|
69 | return | |||
|
70 | ||||
32 | # Force download if requested |
|
71 | # Force download if requested | |
33 | if self.get_argument('download', 'false').lower() == 'true': |
|
72 | if self.get_argument('download', 'false').lower() == 'true': | |
34 |
filename = os.path.splitext(name)[0] + '.' + |
|
73 | filename = os.path.splitext(name)[0] + '.' + resources['output_extension'] | |
35 | self.set_header('Content-Disposition', |
|
74 | self.set_header('Content-Disposition', | |
36 | 'attachment; filename="%s"' % filename) |
|
75 | 'attachment; filename="%s"' % filename) | |
37 |
|
76 | |||
@@ -40,11 +79,6 b' class NbconvertFileHandler(IPythonHandler):' | |||||
40 | self.set_header('Content-Type', |
|
79 | self.set_header('Content-Type', | |
41 | '%s; charset=utf-8' % exporter.output_mimetype) |
|
80 | '%s; charset=utf-8' % exporter.output_mimetype) | |
42 |
|
81 | |||
43 | output, resources = exporter.from_filename(os_path) |
|
|||
44 |
|
||||
45 | # TODO: If there are resources, combine them into a zip file |
|
|||
46 | assert not has_resource_files(resources) |
|
|||
47 |
|
||||
48 | self.finish(output) |
|
82 | self.finish(output) | |
49 |
|
83 | |||
50 | class NbconvertPostHandler(IPythonHandler): |
|
84 | class NbconvertPostHandler(IPythonHandler): | |
@@ -57,16 +91,16 b' class NbconvertPostHandler(IPythonHandler):' | |||||
57 | model = self.get_json_body() |
|
91 | model = self.get_json_body() | |
58 | nbnode = to_notebook_json(model['content']) |
|
92 | nbnode = to_notebook_json(model['content']) | |
59 |
|
93 | |||
|
94 | output, resources = exporter.from_notebook_node(nbnode) | |||
|
95 | ||||
|
96 | if respond_zip(self, nbnode.metadata.name, output, resources): | |||
|
97 | return | |||
|
98 | ||||
60 | # MIME type |
|
99 | # MIME type | |
61 | if exporter.output_mimetype: |
|
100 | if exporter.output_mimetype: | |
62 | self.set_header('Content-Type', |
|
101 | self.set_header('Content-Type', | |
63 | '%s; charset=utf-8' % exporter.output_mimetype) |
|
102 | '%s; charset=utf-8' % exporter.output_mimetype) | |
64 |
|
103 | |||
65 | output, resources = exporter.from_notebook_node(nbnode) |
|
|||
66 |
|
||||
67 | # TODO: If there are resources, combine them into a zip file |
|
|||
68 | assert not has_resource_files(resources) |
|
|||
69 |
|
||||
70 | self.finish(output) |
|
104 | self.finish(output) | |
71 |
|
105 | |||
72 | #----------------------------------------------------------------------------- |
|
106 | #----------------------------------------------------------------------------- |
@@ -10,7 +10,8 b' import requests' | |||||
10 | from IPython.html.utils import url_path_join |
|
10 | from IPython.html.utils import url_path_join | |
11 | from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error |
|
11 | from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error | |
12 | from IPython.nbformat.current import (new_notebook, write, new_worksheet, |
|
12 | from IPython.nbformat.current import (new_notebook, write, new_worksheet, | |
13 |
new_heading_cell, new_code_cell |
|
13 | new_heading_cell, new_code_cell, | |
|
14 | new_output) | |||
14 |
|
15 | |||
15 | class NbconvertAPI(object): |
|
16 | class NbconvertAPI(object): | |
16 | """Wrapper for nbconvert API calls.""" |
|
17 | """Wrapper for nbconvert API calls.""" | |
@@ -48,7 +49,9 b' class APITest(NotebookTestBase):' | |||||
48 | ws = new_worksheet() |
|
49 | ws = new_worksheet() | |
49 | nb.worksheets = [ws] |
|
50 | nb.worksheets = [ws] | |
50 | ws.cells.append(new_heading_cell(u'Created by test ³')) |
|
51 | ws.cells.append(new_heading_cell(u'Created by test ³')) | |
51 |
|
|
52 | cc1 = new_code_cell(input=u'print(2*6)') | |
|
53 | cc1.outputs.append(new_output(output_text=u'12')) | |||
|
54 | ws.cells.append(cc1) | |||
52 |
|
55 | |||
53 | with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w', |
|
56 | with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w', | |
54 | encoding='utf-8') as f: |
|
57 | encoding='utf-8') as f: | |
@@ -83,6 +86,11 b' class APITest(NotebookTestBase):' | |||||
83 | self.assertIn('attachment', content_disposition) |
|
86 | self.assertIn('attachment', content_disposition) | |
84 | self.assertIn('testnb.py', content_disposition) |
|
87 | self.assertIn('testnb.py', content_disposition) | |
85 |
|
88 | |||
|
89 | def test_from_file_zip(self): | |||
|
90 | r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True) | |||
|
91 | self.assertIn(u'application/zip', r.headers['Content-Type']) | |||
|
92 | self.assertIn(u'.zip', r.headers['Content-Disposition']) | |||
|
93 | ||||
86 | def test_from_post(self): |
|
94 | def test_from_post(self): | |
87 | nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb') |
|
95 | nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb') | |
88 | nbmodel = requests.get(nbmodel_url).json() |
|
96 | nbmodel = requests.get(nbmodel_url).json() | |
@@ -96,3 +104,11 b' class APITest(NotebookTestBase):' | |||||
96 | r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel) |
|
104 | r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel) | |
97 | self.assertIn(u'text/x-python', r.headers['Content-Type']) |
|
105 | self.assertIn(u'text/x-python', r.headers['Content-Type']) | |
98 | self.assertIn(u'print(2*6)', r.text) |
|
106 | self.assertIn(u'print(2*6)', r.text) | |
|
107 | ||||
|
108 | def test_from_post_zip(self): | |||
|
109 | nbmodel_url = url_path_join(self.base_url(), 'api/notebooks/foo/testnb.ipynb') | |||
|
110 | nbmodel = requests.get(nbmodel_url).json() | |||
|
111 | ||||
|
112 | r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel) | |||
|
113 | self.assertIn(u'application/zip', r.headers['Content-Type']) | |||
|
114 | self.assertIn(u'.zip', r.headers['Content-Disposition']) |
@@ -55,7 +55,8 b' def new_output(output_type=None, output_text=None, output_png=None,' | |||||
55 | output_html=None, output_svg=None, output_latex=None, output_json=None, |
|
55 | output_html=None, output_svg=None, output_latex=None, output_json=None, | |
56 | output_javascript=None, output_jpeg=None, prompt_number=None, |
|
56 | output_javascript=None, output_jpeg=None, prompt_number=None, | |
57 | ename=None, evalue=None, traceback=None, stream=None, metadata=None): |
|
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 | output = NotebookNode() |
|
60 | output = NotebookNode() | |
60 | if output_type is not None: |
|
61 | if output_type is not None: | |
61 | output.output_type = unicode_type(output_type) |
|
62 | output.output_type = unicode_type(output_type) |
General Comments 0
You need to be logged in to leave comments.
Login now