Show More
@@ -0,0 +1,155 b'' | |||
|
1 | { | |
|
2 | "metadata": { | |
|
3 | "cell_tags": ["<None>", null], | |
|
4 | "name": "", | |
|
5 | "kernel_info": { | |
|
6 | "name": "python", | |
|
7 | "language": "python" | |
|
8 | } | |
|
9 | }, | |
|
10 | "nbformat": 4, | |
|
11 | "nbformat_minor": 0, | |
|
12 | "cells": [ | |
|
13 | { | |
|
14 | "cell_type": "heading", | |
|
15 | "level": 1, | |
|
16 | "metadata": {}, | |
|
17 | "source": [ | |
|
18 | "nbconvert latex test" | |
|
19 | ] | |
|
20 | }, | |
|
21 | { | |
|
22 | "cell_type": "markdown", | |
|
23 | "metadata": {}, | |
|
24 | "source": [ | |
|
25 | "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus bibendum felis dictum sodales. Ut suscipit, orci ut interdum imperdiet, purus ligula mollis *justo*, non malesuada nisl augue eget lorem. Donec bibendum, erat sit amet porttitor aliquam, urna lorem ornare libero, in vehicula diam diam ut ante. Nam non urna rhoncus, accumsan elit sit amet, mollis tellus. Vestibulum nec tellus metus. Vestibulum tempor, ligula et vehicula rhoncus, sapien turpis faucibus lorem, id dapibus turpis mauris ac orci. Sed volutpat vestibulum venenatis." | |
|
26 | ] | |
|
27 | }, | |
|
28 | { | |
|
29 | "cell_type": "heading", | |
|
30 | "level": 2, | |
|
31 | "metadata": {}, | |
|
32 | "source": [ | |
|
33 | "Printed Using Python" | |
|
34 | ] | |
|
35 | }, | |
|
36 | { | |
|
37 | "cell_type": "code", | |
|
38 | "source": [ | |
|
39 | "print(\"hello\")" | |
|
40 | ], | |
|
41 | "metadata": { | |
|
42 | "collapsed": false, | |
|
43 | "autoscroll": false | |
|
44 | }, | |
|
45 | "outputs": [ | |
|
46 | { | |
|
47 | "output_type": "stream", | |
|
48 | "metadata": {}, | |
|
49 | "name": "stdout", | |
|
50 | "text": [ | |
|
51 | "hello\n" | |
|
52 | ] | |
|
53 | } | |
|
54 | ], | |
|
55 | "prompt_number": 1 | |
|
56 | }, | |
|
57 | { | |
|
58 | "cell_type": "heading", | |
|
59 | "level": 2, | |
|
60 | "metadata": {}, | |
|
61 | "source": [ | |
|
62 | "Pyout" | |
|
63 | ] | |
|
64 | }, | |
|
65 | { | |
|
66 | "cell_type": "code", | |
|
67 | "source": [ | |
|
68 | "from IPython.display import HTML\n", | |
|
69 | "HTML(\"\"\"\n", | |
|
70 | "<script>\n", | |
|
71 | "console.log(\"hello\");\n", | |
|
72 | "</script>\n", | |
|
73 | "<b>HTML</b>\n", | |
|
74 | "\"\"\")" | |
|
75 | ], | |
|
76 | "metadata": { | |
|
77 | "collapsed": false, | |
|
78 | "autoscroll": false | |
|
79 | }, | |
|
80 | "outputs": [ | |
|
81 | { | |
|
82 | "text/html": [ | |
|
83 | "\n", | |
|
84 | "<script>\n", | |
|
85 | "console.log(\"hello\");\n", | |
|
86 | "</script>\n", | |
|
87 | "<b>HTML</b>\n" | |
|
88 | ], | |
|
89 | "metadata": {}, | |
|
90 | "output_type": "execute_result", | |
|
91 | "prompt_number": 3, | |
|
92 | "text/plain": [ | |
|
93 | "<IPython.core.display.HTML at 0x1112757d0>" | |
|
94 | ] | |
|
95 | } | |
|
96 | ], | |
|
97 | "prompt_number": 3 | |
|
98 | }, | |
|
99 | { | |
|
100 | "cell_type": "code", | |
|
101 | "source": [ | |
|
102 | "%%javascript\n", | |
|
103 | "console.log(\"hi\");" | |
|
104 | ], | |
|
105 | "metadata": { | |
|
106 | "collapsed": false, | |
|
107 | "autoscroll": false | |
|
108 | }, | |
|
109 | "outputs": [ | |
|
110 | { | |
|
111 | "text/javascript": [ | |
|
112 | "console.log(\"hi\");" | |
|
113 | ], | |
|
114 | "metadata": {}, | |
|
115 | "output_type": "display_data", | |
|
116 | "text/plain": [ | |
|
117 | "<IPython.core.display.Javascript at 0x1112b4b50>" | |
|
118 | ] | |
|
119 | } | |
|
120 | ], | |
|
121 | "prompt_number": 7 | |
|
122 | }, | |
|
123 | { | |
|
124 | "cell_type": "heading", | |
|
125 | "level": 3, | |
|
126 | "metadata": {}, | |
|
127 | "source": [ | |
|
128 | "Image" | |
|
129 | ] | |
|
130 | }, | |
|
131 | { | |
|
132 | "cell_type": "code", | |
|
133 | "source": [ | |
|
134 | "from IPython.display import Image\n", | |
|
135 | "Image(\"http://ipython.org/_static/IPy_header.png\")" | |
|
136 | ], | |
|
137 | "metadata": { | |
|
138 | "collapsed": false, | |
|
139 | "autoscroll": false | |
|
140 | }, | |
|
141 | "outputs": [ | |
|
142 | { | |
|
143 | "metadata": {}, | |
|
144 | "output_type": "execute_result", | |
|
145 | "image/png": "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\n", | |
|
146 | "prompt_number": 6, | |
|
147 | "text/plain": [ | |
|
148 | "<IPython.core.display.Image at 0x111275490>" | |
|
149 | ] | |
|
150 | } | |
|
151 | ], | |
|
152 | "prompt_number": 6 | |
|
153 | } | |
|
154 | ] | |
|
155 | } |
@@ -1,70 +1,54 b'' | |||
|
1 | """API for converting notebooks between versions. | |
|
1 | """API for converting notebooks between versions.""" | |
|
2 | 2 | |
|
3 | Authors: | |
|
4 | ||
|
5 | * Jonathan Frederic | |
|
6 | """ | |
|
7 | ||
|
8 | #----------------------------------------------------------------------------- | |
|
9 | # Copyright (C) 2013 The IPython Development Team | |
|
10 | # | |
|
11 | # Distributed under the terms of the BSD License. The full license is in | |
|
12 | # the file COPYING, distributed as part of this software. | |
|
13 | #----------------------------------------------------------------------------- | |
|
14 | ||
|
15 | #----------------------------------------------------------------------------- | |
|
16 | # Imports | |
|
17 | #----------------------------------------------------------------------------- | |
|
3 | # Copyright (c) IPython Development Team. | |
|
4 | # Distributed under the terms of the Modified BSD License. | |
|
18 | 5 | |
|
19 | 6 | from .reader import get_version, versions |
|
20 | 7 | |
|
21 | #----------------------------------------------------------------------------- | |
|
22 | # Functions | |
|
23 | #----------------------------------------------------------------------------- | |
|
24 | 8 | |
|
25 | 9 | def convert(nb, to_version): |
|
26 | 10 | """Convert a notebook node object to a specific version. Assumes that |
|
27 | 11 | all the versions starting from 1 to the latest major X are implemented. |
|
28 | 12 | In other words, there should never be a case where v1 v2 v3 v5 exist without |
|
29 | 13 | a v4. Also assumes that all conversions can be made in one step increments |
|
30 | 14 | between major versions and ignores minor revisions. |
|
31 | 15 | |
|
32 | 16 | Parameters |
|
33 | 17 | ---------- |
|
34 | 18 | nb : NotebookNode |
|
35 | 19 | to_version : int |
|
36 | 20 | Major revision to convert the notebook to. Can either be an upgrade or |
|
37 | 21 | a downgrade. |
|
38 | 22 | """ |
|
39 | 23 | |
|
40 | 24 | # Get input notebook version. |
|
41 | 25 | (version, version_minor) = get_version(nb) |
|
42 | 26 | |
|
43 | 27 | # Check if destination is current version, if so return contents |
|
44 | 28 | if version == to_version: |
|
45 | 29 | return nb |
|
46 | 30 | |
|
47 | 31 | # If the version exist, try to convert to it one step at a time. |
|
48 | 32 | elif to_version in versions: |
|
49 | 33 | |
|
50 | 34 | # Get the the version that this recursion will convert to as a step |
|
51 | 35 | # closer to the final revision. Make sure the newer of the conversion |
|
52 | 36 | # functions is used to perform the conversion. |
|
53 | 37 | if to_version > version: |
|
54 | 38 | step_version = version + 1 |
|
55 | 39 | convert_function = versions[step_version].upgrade |
|
56 | 40 | else: |
|
57 | 41 | step_version = version - 1 |
|
58 | 42 | convert_function = versions[version].downgrade |
|
59 | 43 | |
|
60 | 44 | # Convert and make sure version changed during conversion. |
|
61 | 45 | converted = convert_function(nb) |
|
62 | 46 | if converted.get('nbformat', 1) == version: |
|
63 |
raise |
|
|
47 | raise ValueError("Cannot convert notebook from v%d to v%d. Operation" \ | |
|
64 | 48 | "failed silently." % (version, step_version)) |
|
65 | 49 | |
|
66 | 50 | # Recursively convert until target version is reached. |
|
67 | 51 | return convert(converted, to_version) |
|
68 | 52 | else: |
|
69 |
raise |
|
|
53 | raise ValueError("Cannot convert notebook to v%d because that " \ | |
|
70 | 54 | "version doesn't exist" % (to_version)) |
@@ -1,109 +1,95 b'' | |||
|
1 |
"""API for reading notebooks |
|
|
1 | """API for reading notebooks of different versions""" | |
|
2 | 2 | |
|
3 | Authors: | |
|
4 | ||
|
5 | * Jonathan Frederic | |
|
6 | """ | |
|
7 | ||
|
8 | #----------------------------------------------------------------------------- | |
|
9 | # Copyright (C) 2013 The IPython Development Team | |
|
10 | # | |
|
11 | # Distributed under the terms of the BSD License. The full license is in | |
|
12 | # the file COPYING, distributed as part of this software. | |
|
13 | #----------------------------------------------------------------------------- | |
|
14 | ||
|
15 | #----------------------------------------------------------------------------- | |
|
16 | # Imports | |
|
17 | #----------------------------------------------------------------------------- | |
|
3 | # Copyright (c) IPython Development Team. | |
|
4 | # Distributed under the terms of the Modified BSD License. | |
|
18 | 5 | |
|
19 | 6 | import json |
|
20 | 7 | |
|
21 | 8 | from . import v1 |
|
22 | 9 | from . import v2 |
|
23 | 10 | from . import v3 |
|
11 | from . import v4 | |
|
24 | 12 | |
|
25 | 13 | versions = { |
|
26 | 14 | 1: v1, |
|
27 | 15 | 2: v2, |
|
28 | 16 | 3: v3, |
|
17 | 4: v4, | |
|
29 | 18 | } |
|
30 | 19 | |
|
31 | #----------------------------------------------------------------------------- | |
|
32 | # Code | |
|
33 | #----------------------------------------------------------------------------- | |
|
34 | 20 | |
|
35 | 21 | class NotJSONError(ValueError): |
|
36 | 22 | pass |
|
37 | 23 | |
|
38 | 24 | def parse_json(s, **kwargs): |
|
39 | 25 | """Parse a JSON string into a dict.""" |
|
40 | 26 | try: |
|
41 | 27 | nb_dict = json.loads(s, **kwargs) |
|
42 | 28 | except ValueError: |
|
43 | 29 | # Limit the error message to 80 characters. Display whatever JSON will fit. |
|
44 | 30 | raise NotJSONError(("Notebook does not appear to be JSON: %r" % s)[:77] + "...") |
|
45 | 31 | return nb_dict |
|
46 | 32 | |
|
47 | 33 | # High level API |
|
48 | 34 | |
|
49 | 35 | def get_version(nb): |
|
50 | 36 | """Get the version of a notebook. |
|
51 | 37 | |
|
52 | 38 | Parameters |
|
53 | 39 | ---------- |
|
54 | 40 | nb : dict |
|
55 | 41 | NotebookNode or dict containing notebook data. |
|
56 | 42 | |
|
57 | 43 | Returns |
|
58 | 44 | ------- |
|
59 | 45 | Tuple containing major (int) and minor (int) version numbers |
|
60 | 46 | """ |
|
61 | 47 | major = nb.get('nbformat', 1) |
|
62 | 48 | minor = nb.get('nbformat_minor', 0) |
|
63 | 49 | return (major, minor) |
|
64 | 50 | |
|
65 | 51 | |
|
66 | 52 | def reads(s, **kwargs): |
|
67 | 53 | """Read a notebook from a json string and return the |
|
68 | 54 | NotebookNode object. |
|
69 | 55 | |
|
70 | 56 | This function properly reads notebooks of any version. No version |
|
71 | 57 | conversion is performed. |
|
72 | 58 | |
|
73 | 59 | Parameters |
|
74 | 60 | ---------- |
|
75 | 61 | s : unicode |
|
76 | 62 | The raw unicode string to read the notebook from. |
|
77 | 63 | |
|
78 | 64 | Returns |
|
79 | 65 | ------- |
|
80 | 66 | nb : NotebookNode |
|
81 | 67 | The notebook that was read. |
|
82 | 68 | """ |
|
83 | 69 | from .current import NBFormatError |
|
84 | 70 | |
|
85 | 71 | nb_dict = parse_json(s, **kwargs) |
|
86 | 72 | (major, minor) = get_version(nb_dict) |
|
87 | 73 | if major in versions: |
|
88 | 74 | return versions[major].to_notebook_json(nb_dict, minor=minor) |
|
89 | 75 | else: |
|
90 | 76 | raise NBFormatError('Unsupported nbformat version %s' % major) |
|
91 | 77 | |
|
92 | 78 | |
|
93 | 79 | def read(fp, **kwargs): |
|
94 | 80 | """Read a notebook from a file and return the NotebookNode object. |
|
95 | 81 | |
|
96 | 82 | This function properly reads notebooks of any version. No version |
|
97 | 83 | conversion is performed. |
|
98 | 84 | |
|
99 | 85 | Parameters |
|
100 | 86 | ---------- |
|
101 | 87 | fp : file |
|
102 | 88 | Any file-like object with a read method. |
|
103 | 89 | |
|
104 | 90 | Returns |
|
105 | 91 | ------- |
|
106 | 92 | nb : NotebookNode |
|
107 | 93 | The notebook that was read. |
|
108 | 94 | """ |
|
109 | 95 | return reads(fp.read(), **kwargs) |
@@ -1,312 +1,310 b'' | |||
|
1 | 1 | """Functions for signing notebooks""" |
|
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 base64 |
|
7 | 7 | from contextlib import contextmanager |
|
8 | 8 | import hashlib |
|
9 | 9 | from hmac import HMAC |
|
10 | 10 | import io |
|
11 | 11 | import os |
|
12 | 12 | |
|
13 | 13 | from IPython.utils.py3compat import string_types, unicode_type, cast_bytes |
|
14 | 14 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool |
|
15 | 15 | from IPython.config import LoggingConfigurable, MultipleInstanceError |
|
16 | 16 | from IPython.core.application import BaseIPythonApplication, base_flags |
|
17 | 17 | |
|
18 | 18 | from .current import read, write |
|
19 | 19 | |
|
20 | ||
|
21 | 20 | try: |
|
22 | 21 | # Python 3 |
|
23 | 22 | algorithms = hashlib.algorithms_guaranteed |
|
24 | 23 | except AttributeError: |
|
25 | 24 | algorithms = hashlib.algorithms |
|
26 | 25 | |
|
27 | 26 | |
|
28 | 27 | def yield_everything(obj): |
|
29 | 28 | """Yield every item in a container as bytes |
|
30 | 29 | |
|
31 | 30 | Allows any JSONable object to be passed to an HMAC digester |
|
32 | 31 | without having to serialize the whole thing. |
|
33 | 32 | """ |
|
34 | 33 | if isinstance(obj, dict): |
|
35 | 34 | for key in sorted(obj): |
|
36 | 35 | value = obj[key] |
|
37 | 36 | yield cast_bytes(key) |
|
38 | 37 | for b in yield_everything(value): |
|
39 | 38 | yield b |
|
40 | 39 | elif isinstance(obj, (list, tuple)): |
|
41 | 40 | for element in obj: |
|
42 | 41 | for b in yield_everything(element): |
|
43 | 42 | yield b |
|
44 | 43 | elif isinstance(obj, unicode_type): |
|
45 | 44 | yield obj.encode('utf8') |
|
46 | 45 | else: |
|
47 | 46 | yield unicode_type(obj).encode('utf8') |
|
48 | 47 | |
|
49 | 48 | |
|
50 | 49 | @contextmanager |
|
51 | 50 | def signature_removed(nb): |
|
52 | 51 | """Context manager for operating on a notebook with its signature removed |
|
53 | 52 | |
|
54 | 53 | Used for excluding the previous signature when computing a notebook's signature. |
|
55 | 54 | """ |
|
56 | 55 | save_signature = nb['metadata'].pop('signature', None) |
|
57 | 56 | try: |
|
58 | 57 | yield |
|
59 | 58 | finally: |
|
60 | 59 | if save_signature is not None: |
|
61 | 60 | nb['metadata']['signature'] = save_signature |
|
62 | 61 | |
|
63 | 62 | |
|
64 | 63 | class NotebookNotary(LoggingConfigurable): |
|
65 | 64 | """A class for computing and verifying notebook signatures.""" |
|
66 | 65 | |
|
67 | 66 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") |
|
68 | 67 | def _profile_dir_default(self): |
|
69 | 68 | from IPython.core.application import BaseIPythonApplication |
|
70 | 69 | app = None |
|
71 | 70 | try: |
|
72 | 71 | if BaseIPythonApplication.initialized(): |
|
73 | 72 | app = BaseIPythonApplication.instance() |
|
74 | 73 | except MultipleInstanceError: |
|
75 | 74 | pass |
|
76 | 75 | if app is None: |
|
77 | 76 | # create an app, without the global instance |
|
78 | 77 | app = BaseIPythonApplication() |
|
79 | 78 | app.initialize(argv=[]) |
|
80 | 79 | return app.profile_dir |
|
81 | 80 | |
|
82 | 81 | algorithm = Enum(algorithms, default_value='sha256', config=True, |
|
83 | 82 | help="""The hashing algorithm used to sign notebooks.""" |
|
84 | 83 | ) |
|
85 | 84 | def _algorithm_changed(self, name, old, new): |
|
86 | 85 | self.digestmod = getattr(hashlib, self.algorithm) |
|
87 | 86 | |
|
88 | 87 | digestmod = Any() |
|
89 | 88 | def _digestmod_default(self): |
|
90 | 89 | return getattr(hashlib, self.algorithm) |
|
91 | 90 | |
|
92 | 91 | secret_file = Unicode(config=True, |
|
93 | 92 | help="""The file where the secret key is stored.""" |
|
94 | 93 | ) |
|
95 | 94 | def _secret_file_default(self): |
|
96 | 95 | if self.profile_dir is None: |
|
97 | 96 | return '' |
|
98 | 97 | return os.path.join(self.profile_dir.security_dir, 'notebook_secret') |
|
99 | 98 | |
|
100 | 99 | secret = Bytes(config=True, |
|
101 | 100 | help="""The secret key with which notebooks are signed.""" |
|
102 | 101 | ) |
|
103 | 102 | def _secret_default(self): |
|
104 | 103 | # note : this assumes an Application is running |
|
105 | 104 | if os.path.exists(self.secret_file): |
|
106 | 105 | with io.open(self.secret_file, 'rb') as f: |
|
107 | 106 | return f.read() |
|
108 | 107 | else: |
|
109 | 108 | secret = base64.encodestring(os.urandom(1024)) |
|
110 | 109 | self._write_secret_file(secret) |
|
111 | 110 | return secret |
|
112 | 111 | |
|
113 | 112 | def _write_secret_file(self, secret): |
|
114 | 113 | """write my secret to my secret_file""" |
|
115 | 114 | self.log.info("Writing notebook-signing key to %s", self.secret_file) |
|
116 | 115 | with io.open(self.secret_file, 'wb') as f: |
|
117 | 116 | f.write(secret) |
|
118 | 117 | try: |
|
119 | 118 | os.chmod(self.secret_file, 0o600) |
|
120 | 119 | except OSError: |
|
121 | 120 | self.log.warn( |
|
122 | 121 | "Could not set permissions on %s", |
|
123 | 122 | self.secret_file |
|
124 | 123 | ) |
|
125 | 124 | return secret |
|
126 | 125 | |
|
127 | 126 | def compute_signature(self, nb): |
|
128 | 127 | """Compute a notebook's signature |
|
129 | 128 | |
|
130 | 129 | by hashing the entire contents of the notebook via HMAC digest. |
|
131 | 130 | """ |
|
132 | 131 | hmac = HMAC(self.secret, digestmod=self.digestmod) |
|
133 | 132 | # don't include the previous hash in the content to hash |
|
134 | 133 | with signature_removed(nb): |
|
135 | 134 | # sign the whole thing |
|
136 | 135 | for b in yield_everything(nb): |
|
137 | 136 | hmac.update(b) |
|
138 | 137 | |
|
139 | 138 | return hmac.hexdigest() |
|
140 | 139 | |
|
141 | 140 | def check_signature(self, nb): |
|
142 | 141 | """Check a notebook's stored signature |
|
143 | 142 | |
|
144 | 143 | If a signature is stored in the notebook's metadata, |
|
145 | 144 | a new signature is computed and compared with the stored value. |
|
146 | 145 | |
|
147 | 146 | Returns True if the signature is found and matches, False otherwise. |
|
148 | 147 | |
|
149 | 148 | The following conditions must all be met for a notebook to be trusted: |
|
150 | 149 | - a signature is stored in the form 'scheme:hexdigest' |
|
151 | 150 | - the stored scheme matches the requested scheme |
|
152 | 151 | - the requested scheme is available from hashlib |
|
153 | 152 | - the computed hash from notebook_signature matches the stored hash |
|
154 | 153 | """ |
|
155 | 154 | stored_signature = nb['metadata'].get('signature', None) |
|
156 | 155 | if not stored_signature \ |
|
157 | 156 | or not isinstance(stored_signature, string_types) \ |
|
158 | 157 | or ':' not in stored_signature: |
|
159 | 158 | return False |
|
160 | 159 | stored_algo, sig = stored_signature.split(':', 1) |
|
161 | 160 | if self.algorithm != stored_algo: |
|
162 | 161 | return False |
|
163 | 162 | my_signature = self.compute_signature(nb) |
|
164 | 163 | return my_signature == sig |
|
165 | 164 | |
|
166 | 165 | def sign(self, nb): |
|
167 | 166 | """Sign a notebook, indicating that its output is trusted |
|
168 | 167 | |
|
169 | 168 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature |
|
170 | 169 | |
|
171 | 170 | e.g. 'sha256:deadbeef123...' |
|
172 | 171 | """ |
|
173 | 172 | signature = self.compute_signature(nb) |
|
174 | 173 | nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature) |
|
175 | 174 | |
|
176 | 175 | def mark_cells(self, nb, trusted): |
|
177 | 176 | """Mark cells as trusted if the notebook's signature can be verified |
|
178 | 177 | |
|
179 | 178 | Sets ``cell.metadata.trusted = True | False`` on all code cells, |
|
180 | 179 | depending on whether the stored signature can be verified. |
|
181 | 180 | |
|
182 | 181 | This function is the inverse of check_cells |
|
183 | 182 | """ |
|
184 |
if not nb[' |
|
|
183 | if not nb['cells']: | |
|
185 | 184 | # nothing to mark if there are no cells |
|
186 | 185 | return |
|
187 |
for cell in nb[' |
|
|
186 | for cell in nb['cells']: | |
|
188 | 187 | if cell['cell_type'] == 'code': |
|
189 | 188 | cell['metadata']['trusted'] = trusted |
|
190 | 189 | |
|
191 | 190 | def _check_cell(self, cell): |
|
192 | 191 | """Do we trust an individual cell? |
|
193 | 192 | |
|
194 | 193 | Return True if: |
|
195 | 194 | |
|
196 | 195 | - cell is explicitly trusted |
|
197 | 196 | - cell has no potentially unsafe rich output |
|
198 | 197 | |
|
199 | 198 | If a cell has no output, or only simple print statements, |
|
200 | 199 | it will always be trusted. |
|
201 | 200 | """ |
|
202 | 201 | # explicitly trusted |
|
203 | 202 | if cell['metadata'].pop("trusted", False): |
|
204 | 203 | return True |
|
205 | 204 | |
|
206 | 205 | # explicitly safe output |
|
207 | 206 | safe = { |
|
208 | 207 | 'text/plain', 'image/png', 'image/jpeg', |
|
209 | 'text', 'png', 'jpg', # v3-style short keys | |
|
210 | 208 | } |
|
211 | 209 | |
|
212 | 210 | for output in cell['outputs']: |
|
213 | 211 | output_type = output['output_type'] |
|
214 |
if output_type in |
|
|
212 | if output_type in {'execute_result', 'display_data'}: | |
|
215 | 213 | # if there are any data keys not in the safe whitelist |
|
216 | 214 | output_keys = set(output).difference({"output_type", "prompt_number", "metadata"}) |
|
217 | 215 | if output_keys.difference(safe): |
|
218 | 216 | return False |
|
219 | 217 | |
|
220 | 218 | return True |
|
221 | 219 | |
|
222 | 220 | def check_cells(self, nb): |
|
223 | 221 | """Return whether all code cells are trusted |
|
224 | 222 | |
|
225 | 223 | If there are no code cells, return True. |
|
226 | 224 | |
|
227 | 225 | This function is the inverse of mark_cells. |
|
228 | 226 | """ |
|
229 |
if not nb[' |
|
|
227 | if not nb['cells']: | |
|
230 | 228 | return True |
|
231 | 229 | trusted = True |
|
232 |
for cell in nb[' |
|
|
230 | for cell in nb['cells']: | |
|
233 | 231 | if cell['cell_type'] != 'code': |
|
234 | 232 | continue |
|
235 | 233 | # only distrust a cell if it actually has some output to distrust |
|
236 | 234 | if not self._check_cell(cell): |
|
237 | 235 | trusted = False |
|
238 | 236 | |
|
239 | 237 | return trusted |
|
240 | 238 | |
|
241 | 239 | |
|
242 | 240 | trust_flags = { |
|
243 | 241 | 'reset' : ( |
|
244 | 242 | {'TrustNotebookApp' : { 'reset' : True}}, |
|
245 | 243 | """Generate a new key for notebook signature. |
|
246 | 244 | All previously signed notebooks will become untrusted. |
|
247 | 245 | """ |
|
248 | 246 | ), |
|
249 | 247 | } |
|
250 | 248 | trust_flags.update(base_flags) |
|
251 | 249 | trust_flags.pop('init') |
|
252 | 250 | |
|
253 | 251 | |
|
254 | 252 | class TrustNotebookApp(BaseIPythonApplication): |
|
255 | 253 | |
|
256 | 254 | description="""Sign one or more IPython notebooks with your key, |
|
257 | 255 | to trust their dynamic (HTML, Javascript) output. |
|
258 | 256 | |
|
259 | 257 | Trusting a notebook only applies to the current IPython profile. |
|
260 | 258 | To trust a notebook for use with a profile other than default, |
|
261 | 259 | add `--profile [profile name]`. |
|
262 | 260 | |
|
263 | 261 | Otherwise, you will have to re-execute the notebook to see output. |
|
264 | 262 | """ |
|
265 | 263 | |
|
266 | 264 | examples = """ |
|
267 | 265 | ipython trust mynotebook.ipynb and_this_one.ipynb |
|
268 | 266 | ipython trust --profile myprofile mynotebook.ipynb |
|
269 | 267 | """ |
|
270 | 268 | |
|
271 | 269 | flags = trust_flags |
|
272 | 270 | |
|
273 | 271 | reset = Bool(False, config=True, |
|
274 | 272 | help="""If True, generate a new key for notebook signature. |
|
275 | 273 | After reset, all previously signed notebooks will become untrusted. |
|
276 | 274 | """ |
|
277 | 275 | ) |
|
278 | 276 | |
|
279 | 277 | notary = Instance(NotebookNotary) |
|
280 | 278 | def _notary_default(self): |
|
281 | 279 | return NotebookNotary(parent=self, profile_dir=self.profile_dir) |
|
282 | 280 | |
|
283 | 281 | def sign_notebook(self, notebook_path): |
|
284 | 282 | if not os.path.exists(notebook_path): |
|
285 | 283 | self.log.error("Notebook missing: %s" % notebook_path) |
|
286 | 284 | self.exit(1) |
|
287 | 285 | with io.open(notebook_path, encoding='utf8') as f: |
|
288 | 286 | nb = read(f, 'json') |
|
289 | 287 | if self.notary.check_signature(nb): |
|
290 | 288 | print("Notebook already signed: %s" % notebook_path) |
|
291 | 289 | else: |
|
292 | 290 | print("Signing notebook: %s" % notebook_path) |
|
293 | 291 | self.notary.sign(nb) |
|
294 | 292 | with io.open(notebook_path, 'w', encoding='utf8') as f: |
|
295 | 293 | write(nb, f, 'json') |
|
296 | 294 | |
|
297 | 295 | def generate_new_key(self): |
|
298 | 296 | """Generate a new notebook signature key""" |
|
299 | 297 | print("Generating new notebook key: %s" % self.notary.secret_file) |
|
300 | 298 | self.notary._write_secret_file(os.urandom(1024)) |
|
301 | 299 | |
|
302 | 300 | def start(self): |
|
303 | 301 | if self.reset: |
|
304 | 302 | self.generate_new_key() |
|
305 | 303 | return |
|
306 | 304 | if not self.extra_args: |
|
307 | 305 | self.log.critical("Specify at least one notebook to sign.") |
|
308 | 306 | self.exit(1) |
|
309 | 307 | |
|
310 | 308 | for notebook_path in self.extra_args: |
|
311 | 309 | self.sign_notebook(notebook_path) |
|
312 | 310 |
@@ -1,69 +1,57 b'' | |||
|
1 | """ | |
|
2 | Contains tests class for convert.py | |
|
3 | """ | |
|
4 | #----------------------------------------------------------------------------- | |
|
5 | # Copyright (C) 2013 The IPython Development Team | |
|
6 | # | |
|
7 | # Distributed under the terms of the BSD License. The full license is in | |
|
8 | # the file COPYING, distributed as part of this software. | |
|
9 | #----------------------------------------------------------------------------- | |
|
10 | ||
|
11 | #----------------------------------------------------------------------------- | |
|
12 | # Imports | |
|
13 | #----------------------------------------------------------------------------- | |
|
1 | """Tests for nbformat.convert""" | |
|
2 | ||
|
3 | # Copyright (c) IPython Development Team. | |
|
4 | # Distributed under the terms of the Modified BSD License. | |
|
14 | 5 | |
|
15 | 6 | from .base import TestsBase |
|
16 | 7 | |
|
17 | 8 | from ..convert import convert |
|
18 | 9 | from ..reader import read, get_version |
|
19 | 10 | from ..current import current_nbformat |
|
20 | 11 | |
|
21 | #----------------------------------------------------------------------------- | |
|
22 | # Classes and functions | |
|
23 | #----------------------------------------------------------------------------- | |
|
24 | 12 | |
|
25 | 13 | class TestConvert(TestsBase): |
|
26 | 14 | |
|
27 | def test_downgrade(self): | |
|
15 | def test_downgrade_3_2(self): | |
|
28 | 16 | """Do notebook downgrades work?""" |
|
29 | 17 | |
|
30 | 18 | # Open a version 3 notebook and attempt to downgrade it to version 2. |
|
31 | 19 | with self.fopen(u'test3.ipynb', u'r') as f: |
|
32 | 20 | nb = read(f) |
|
33 | 21 | nb = convert(nb, 2) |
|
34 | 22 | |
|
35 | 23 | # Check if downgrade was successful. |
|
36 | 24 | (major, minor) = get_version(nb) |
|
37 | 25 | self.assertEqual(major, 2) |
|
38 | 26 | |
|
39 | 27 | |
|
40 | def test_upgrade(self): | |
|
28 | def test_upgrade_2_3(self): | |
|
41 | 29 | """Do notebook upgrades work?""" |
|
42 | 30 | |
|
43 | 31 | # Open a version 2 notebook and attempt to upgrade it to version 3. |
|
44 | 32 | with self.fopen(u'test2.ipynb', u'r') as f: |
|
45 | 33 | nb = read(f) |
|
46 | 34 | nb = convert(nb, 3) |
|
47 | 35 | |
|
48 | 36 | # Check if upgrade was successful. |
|
49 | 37 | (major, minor) = get_version(nb) |
|
50 | 38 | self.assertEqual(major, 3) |
|
51 | 39 | |
|
52 | 40 | |
|
53 | 41 | def test_open_current(self): |
|
54 | 42 | """Can an old notebook be opened and converted to the current version |
|
55 | 43 | while remembering the original version of the notebook?""" |
|
56 | 44 | |
|
57 | 45 | # Open a version 2 notebook and attempt to upgrade it to the current version |
|
58 | 46 | # while remembering it's version information. |
|
59 | 47 | with self.fopen(u'test2.ipynb', u'r') as f: |
|
60 | 48 | nb = read(f) |
|
61 | 49 | (original_major, original_minor) = get_version(nb) |
|
62 | 50 | nb = convert(nb, current_nbformat) |
|
63 | 51 | |
|
64 | 52 | # Check if upgrade was successful. |
|
65 | 53 | (major, minor) = get_version(nb) |
|
66 | 54 | self.assertEqual(major, current_nbformat) |
|
67 | 55 | |
|
68 | 56 | # Check if the original major revision was remembered. |
|
69 | 57 | self.assertEqual(original_major, 2) |
@@ -1,122 +1,112 b'' | |||
|
1 | 1 | """Test Notebook signing""" |
|
2 | #----------------------------------------------------------------------------- | |
|
3 | # Copyright (C) 2014, The IPython Development Team | |
|
4 | # | |
|
5 | # Distributed under the terms of the BSD License. The full license is in | |
|
6 | # the file COPYING, distributed as part of this software. | |
|
7 | #----------------------------------------------------------------------------- | |
|
8 | 2 | |
|
9 | #----------------------------------------------------------------------------- | |
|
10 | # Imports | |
|
11 | #----------------------------------------------------------------------------- | |
|
3 | # Copyright (c) IPython Development Team. | |
|
4 | # Distributed under the terms of the Modified BSD License. | |
|
12 | 5 | |
|
13 | 6 | from .. import sign |
|
14 | 7 | from .base import TestsBase |
|
15 | 8 | |
|
16 | 9 | from ..current import read |
|
17 | 10 | from IPython.core.getipython import get_ipython |
|
18 | 11 | |
|
19 | #----------------------------------------------------------------------------- | |
|
20 | # Classes and functions | |
|
21 | #----------------------------------------------------------------------------- | |
|
22 | 12 | |
|
23 | 13 | class TestNotary(TestsBase): |
|
24 | 14 | |
|
25 | 15 | def setUp(self): |
|
26 | 16 | self.notary = sign.NotebookNotary( |
|
27 | 17 | secret=b'secret', |
|
28 | 18 | profile_dir=get_ipython().profile_dir |
|
29 | 19 | ) |
|
30 | 20 | with self.fopen(u'test3.ipynb', u'r') as f: |
|
31 | 21 | self.nb = read(f, u'json') |
|
32 | 22 | |
|
33 | 23 | def test_algorithms(self): |
|
34 | 24 | last_sig = '' |
|
35 | 25 | for algo in sign.algorithms: |
|
36 | 26 | self.notary.algorithm = algo |
|
37 | 27 | self.notary.sign(self.nb) |
|
38 | 28 | sig = self.nb.metadata.signature |
|
39 | 29 | print(sig) |
|
40 | 30 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) |
|
41 | 31 | self.assertNotEqual(last_sig, sig) |
|
42 | 32 | last_sig = sig |
|
43 | 33 | |
|
44 | 34 | def test_sign_same(self): |
|
45 | 35 | """Multiple signatures of the same notebook are the same""" |
|
46 | 36 | sig1 = self.notary.compute_signature(self.nb) |
|
47 | 37 | sig2 = self.notary.compute_signature(self.nb) |
|
48 | 38 | self.assertEqual(sig1, sig2) |
|
49 | 39 | |
|
50 | 40 | def test_change_secret(self): |
|
51 | 41 | """Changing the secret changes the signature""" |
|
52 | 42 | sig1 = self.notary.compute_signature(self.nb) |
|
53 | 43 | self.notary.secret = b'different' |
|
54 | 44 | sig2 = self.notary.compute_signature(self.nb) |
|
55 | 45 | self.assertNotEqual(sig1, sig2) |
|
56 | 46 | |
|
57 | 47 | def test_sign(self): |
|
58 | 48 | self.notary.sign(self.nb) |
|
59 | 49 | sig = self.nb.metadata.signature |
|
60 | 50 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) |
|
61 | 51 | |
|
62 | 52 | def test_check_signature(self): |
|
63 | 53 | nb = self.nb |
|
64 | 54 | md = nb.metadata |
|
65 | 55 | notary = self.notary |
|
66 | 56 | check_signature = notary.check_signature |
|
67 | 57 | # no signature: |
|
68 | 58 | md.pop('signature', None) |
|
69 | 59 | self.assertFalse(check_signature(nb)) |
|
70 | 60 | # hash only, no algo |
|
71 | 61 | md.signature = notary.compute_signature(nb) |
|
72 | 62 | self.assertFalse(check_signature(nb)) |
|
73 | 63 | # proper signature, algo mismatch |
|
74 | 64 | notary.algorithm = 'sha224' |
|
75 | 65 | notary.sign(nb) |
|
76 | 66 | notary.algorithm = 'sha256' |
|
77 | 67 | self.assertFalse(check_signature(nb)) |
|
78 | 68 | # check correctly signed notebook |
|
79 | 69 | notary.sign(nb) |
|
80 | 70 | self.assertTrue(check_signature(nb)) |
|
81 | 71 | |
|
82 | 72 | def test_mark_cells_untrusted(self): |
|
83 |
cells = self.nb. |
|
|
73 | cells = self.nb.cells | |
|
84 | 74 | self.notary.mark_cells(self.nb, False) |
|
85 | 75 | for cell in cells: |
|
86 | 76 | self.assertNotIn('trusted', cell) |
|
87 | 77 | if cell.cell_type == 'code': |
|
88 | 78 | self.assertIn('trusted', cell.metadata) |
|
89 | 79 | self.assertFalse(cell.metadata.trusted) |
|
90 | 80 | else: |
|
91 | 81 | self.assertNotIn('trusted', cell.metadata) |
|
92 | 82 | |
|
93 | 83 | def test_mark_cells_trusted(self): |
|
94 |
cells = self.nb. |
|
|
84 | cells = self.nb.cells | |
|
95 | 85 | self.notary.mark_cells(self.nb, True) |
|
96 | 86 | for cell in cells: |
|
97 | 87 | self.assertNotIn('trusted', cell) |
|
98 | 88 | if cell.cell_type == 'code': |
|
99 | 89 | self.assertIn('trusted', cell.metadata) |
|
100 | 90 | self.assertTrue(cell.metadata.trusted) |
|
101 | 91 | else: |
|
102 | 92 | self.assertNotIn('trusted', cell.metadata) |
|
103 | 93 | |
|
104 | 94 | def test_check_cells(self): |
|
105 | 95 | nb = self.nb |
|
106 | 96 | self.notary.mark_cells(nb, True) |
|
107 | 97 | self.assertTrue(self.notary.check_cells(nb)) |
|
108 |
for cell in nb. |
|
|
98 | for cell in nb.cells: | |
|
109 | 99 | self.assertNotIn('trusted', cell) |
|
110 | 100 | self.notary.mark_cells(nb, False) |
|
111 | 101 | self.assertFalse(self.notary.check_cells(nb)) |
|
112 |
for cell in nb. |
|
|
102 | for cell in nb.cells: | |
|
113 | 103 | self.assertNotIn('trusted', cell) |
|
114 | 104 | |
|
115 | 105 | def test_trust_no_output(self): |
|
116 | 106 | nb = self.nb |
|
117 | 107 | self.notary.mark_cells(nb, False) |
|
118 |
for cell in nb. |
|
|
108 | for cell in nb.cells: | |
|
119 | 109 | if cell.cell_type == 'code': |
|
120 | 110 | cell.outputs = [] |
|
121 | 111 | self.assertTrue(self.notary.check_cells(nb)) |
|
122 | 112 |
@@ -1,54 +1,57 b'' | |||
|
1 | 1 | """Test nbformat.validator""" |
|
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 os |
|
7 | 7 | |
|
8 | 8 | from .base import TestsBase |
|
9 | 9 | from jsonschema import ValidationError |
|
10 | 10 | from ..current import read |
|
11 | 11 | from ..validator import isvalid, validate |
|
12 | 12 | |
|
13 | 13 | |
|
14 | #----------------------------------------------------------------------------- | |
|
15 | # Classes and functions | |
|
16 | #----------------------------------------------------------------------------- | |
|
17 | ||
|
18 | 14 | class TestValidator(TestsBase): |
|
19 | 15 | |
|
20 | 16 | def test_nb2(self): |
|
21 |
"""Test that a v2 notebook converted to |
|
|
17 | """Test that a v2 notebook converted to current passes validation""" | |
|
22 | 18 | with self.fopen(u'test2.ipynb', u'r') as f: |
|
23 | 19 | nb = read(f, u'json') |
|
24 | 20 | validate(nb) |
|
25 | 21 | self.assertEqual(isvalid(nb), True) |
|
26 | 22 | |
|
27 | 23 | def test_nb3(self): |
|
28 | 24 | """Test that a v3 notebook passes validation""" |
|
29 | 25 | with self.fopen(u'test3.ipynb', u'r') as f: |
|
30 | 26 | nb = read(f, u'json') |
|
31 | 27 | validate(nb) |
|
32 | 28 | self.assertEqual(isvalid(nb), True) |
|
33 | 29 | |
|
30 | def test_nb4(self): | |
|
31 | """Test that a v3 notebook passes validation""" | |
|
32 | with self.fopen(u'test4.ipynb', u'r') as f: | |
|
33 | nb = read(f, u'json') | |
|
34 | validate(nb) | |
|
35 | self.assertEqual(isvalid(nb), True) | |
|
36 | ||
|
34 | 37 | def test_invalid(self): |
|
35 | 38 | """Test than an invalid notebook does not pass validation""" |
|
36 | 39 | # this notebook has a few different errors: |
|
37 | 40 | # - the name is an integer, rather than a string |
|
38 | 41 | # - one cell is missing its source |
|
39 | 42 | # - one cell has an invalid level |
|
40 | 43 | with self.fopen(u'invalid.ipynb', u'r') as f: |
|
41 | 44 | nb = read(f, u'json') |
|
42 | 45 | with self.assertRaises(ValidationError): |
|
43 | 46 | validate(nb) |
|
44 | 47 | self.assertEqual(isvalid(nb), False) |
|
45 | 48 | |
|
46 | 49 | def test_future(self): |
|
47 | 50 | """Test than a notebook from the future with extra keys passes validation""" |
|
48 | 51 | with self.fopen(u'test3plus.ipynb', u'r') as f: |
|
49 | 52 | nb = read(f) |
|
50 | 53 | with self.assertRaises(ValidationError): |
|
51 | 54 | validate(nb, version=3) |
|
52 | 55 | |
|
53 | 56 | self.assertEqual(isvalid(nb, version=3), False) |
|
54 | 57 | self.assertEqual(isvalid(nb), True) |
@@ -1,19 +1,19 b'' | |||
|
1 | 1 | """The main API for the v4 notebook format.""" |
|
2 | 2 | |
|
3 | 3 | # Copyright (c) IPython Development Team. |
|
4 | 4 | # Distributed under the terms of the Modified BSD License. |
|
5 | 5 | |
|
6 | 6 | from .nbbase import ( |
|
7 | NotebookNode, | |
|
7 | NotebookNode, from_dict, | |
|
8 | 8 | nbformat, nbformat_minor, nbformat_schema, |
|
9 | 9 | new_code_cell, new_heading_cell, new_markdown_cell, new_notebook, |
|
10 | 10 | new_output, |
|
11 | 11 | ) |
|
12 | 12 | |
|
13 | 13 | from .nbjson import reads as reads_json, writes as writes_json |
|
14 | 14 | from .nbjson import reads as read_json, writes as write_json |
|
15 | 15 | from .nbjson import to_notebook as to_notebook_json |
|
16 | 16 | |
|
17 | 17 | from .convert import downgrade, upgrade |
|
18 | 18 | |
|
19 | 19 |
General Comments 0
You need to be logged in to leave comments.
Login now