Show More
@@ -1,331 +1,411 b'' | |||||
1 |
""" |
|
1 | """Utilities for signing notebooks""" | |
2 |
|
2 | |||
3 | # Copyright (c) IPython Development Team. |
|
3 | # Copyright (c) IPython Development Team. | |
4 | # Distributed under the terms of the Modified BSD License. |
|
4 | # Distributed under the terms of the Modified BSD License. | |
5 |
|
5 | |||
6 | import base64 |
|
6 | import base64 | |
7 | from contextlib import contextmanager |
|
7 | from contextlib import contextmanager | |
|
8 | from datetime import datetime | |||
8 | import hashlib |
|
9 | import hashlib | |
9 | from hmac import HMAC |
|
10 | from hmac import HMAC | |
10 | import io |
|
11 | import io | |
11 | import os |
|
12 | import os | |
12 |
|
13 | |||
|
14 | try: | |||
|
15 | import sqlite3 | |||
|
16 | except ImportError: | |||
|
17 | try: | |||
|
18 | from pysqlite2 import dbapi2 as sqlite3 | |||
|
19 | except ImportError: | |||
|
20 | sqlite3 = None | |||
|
21 | ||||
13 | from IPython.utils.io import atomic_writing |
|
22 | from IPython.utils.io import atomic_writing | |
14 |
from IPython.utils.py3compat import |
|
23 | from IPython.utils.py3compat import unicode_type, cast_bytes | |
15 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool |
|
24 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer | |
16 | from IPython.config import LoggingConfigurable, MultipleInstanceError |
|
25 | from IPython.config import LoggingConfigurable, MultipleInstanceError | |
17 | from IPython.core.application import BaseIPythonApplication, base_flags |
|
26 | from IPython.core.application import BaseIPythonApplication, base_flags | |
18 |
|
27 | |||
19 | from . import read, write, NO_CONVERT |
|
28 | from . import read, write, NO_CONVERT | |
20 |
|
29 | |||
21 | try: |
|
30 | try: | |
22 | # Python 3 |
|
31 | # Python 3 | |
23 | algorithms = hashlib.algorithms_guaranteed |
|
32 | algorithms = hashlib.algorithms_guaranteed | |
24 | except AttributeError: |
|
33 | except AttributeError: | |
25 | algorithms = hashlib.algorithms |
|
34 | algorithms = hashlib.algorithms | |
26 |
|
35 | |||
27 |
|
36 | |||
28 | def yield_everything(obj): |
|
37 | def yield_everything(obj): | |
29 | """Yield every item in a container as bytes |
|
38 | """Yield every item in a container as bytes | |
30 |
|
39 | |||
31 | Allows any JSONable object to be passed to an HMAC digester |
|
40 | Allows any JSONable object to be passed to an HMAC digester | |
32 | without having to serialize the whole thing. |
|
41 | without having to serialize the whole thing. | |
33 | """ |
|
42 | """ | |
34 | if isinstance(obj, dict): |
|
43 | if isinstance(obj, dict): | |
35 | for key in sorted(obj): |
|
44 | for key in sorted(obj): | |
36 | value = obj[key] |
|
45 | value = obj[key] | |
37 | yield cast_bytes(key) |
|
46 | yield cast_bytes(key) | |
38 | for b in yield_everything(value): |
|
47 | for b in yield_everything(value): | |
39 | yield b |
|
48 | yield b | |
40 | elif isinstance(obj, (list, tuple)): |
|
49 | elif isinstance(obj, (list, tuple)): | |
41 | for element in obj: |
|
50 | for element in obj: | |
42 | for b in yield_everything(element): |
|
51 | for b in yield_everything(element): | |
43 | yield b |
|
52 | yield b | |
44 | elif isinstance(obj, unicode_type): |
|
53 | elif isinstance(obj, unicode_type): | |
45 | yield obj.encode('utf8') |
|
54 | yield obj.encode('utf8') | |
46 | else: |
|
55 | else: | |
47 | yield unicode_type(obj).encode('utf8') |
|
56 | yield unicode_type(obj).encode('utf8') | |
48 |
|
57 | |||
49 | def yield_code_cells(nb): |
|
58 | def yield_code_cells(nb): | |
50 | """Iterator that yields all cells in a notebook |
|
59 | """Iterator that yields all cells in a notebook | |
51 |
|
60 | |||
52 | nbformat version independent |
|
61 | nbformat version independent | |
53 | """ |
|
62 | """ | |
54 | if nb.nbformat >= 4: |
|
63 | if nb.nbformat >= 4: | |
55 | for cell in nb['cells']: |
|
64 | for cell in nb['cells']: | |
56 | if cell['cell_type'] == 'code': |
|
65 | if cell['cell_type'] == 'code': | |
57 | yield cell |
|
66 | yield cell | |
58 | elif nb.nbformat == 3: |
|
67 | elif nb.nbformat == 3: | |
59 | for ws in nb['worksheets']: |
|
68 | for ws in nb['worksheets']: | |
60 | for cell in ws['cells']: |
|
69 | for cell in ws['cells']: | |
61 | if cell['cell_type'] == 'code': |
|
70 | if cell['cell_type'] == 'code': | |
62 | yield cell |
|
71 | yield cell | |
63 |
|
72 | |||
64 | @contextmanager |
|
73 | @contextmanager | |
65 | def signature_removed(nb): |
|
74 | def signature_removed(nb): | |
66 | """Context manager for operating on a notebook with its signature removed |
|
75 | """Context manager for operating on a notebook with its signature removed | |
67 |
|
76 | |||
68 | Used for excluding the previous signature when computing a notebook's signature. |
|
77 | Used for excluding the previous signature when computing a notebook's signature. | |
69 | """ |
|
78 | """ | |
70 | save_signature = nb['metadata'].pop('signature', None) |
|
79 | save_signature = nb['metadata'].pop('signature', None) | |
71 | try: |
|
80 | try: | |
72 | yield |
|
81 | yield | |
73 | finally: |
|
82 | finally: | |
74 | if save_signature is not None: |
|
83 | if save_signature is not None: | |
75 | nb['metadata']['signature'] = save_signature |
|
84 | nb['metadata']['signature'] = save_signature | |
76 |
|
85 | |||
77 |
|
86 | |||
78 | class NotebookNotary(LoggingConfigurable): |
|
87 | class NotebookNotary(LoggingConfigurable): | |
79 | """A class for computing and verifying notebook signatures.""" |
|
88 | """A class for computing and verifying notebook signatures.""" | |
80 |
|
89 | |||
81 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") |
|
90 | profile_dir = Instance("IPython.core.profiledir.ProfileDir") | |
82 | def _profile_dir_default(self): |
|
91 | def _profile_dir_default(self): | |
83 | from IPython.core.application import BaseIPythonApplication |
|
92 | from IPython.core.application import BaseIPythonApplication | |
84 | app = None |
|
93 | app = None | |
85 | try: |
|
94 | try: | |
86 | if BaseIPythonApplication.initialized(): |
|
95 | if BaseIPythonApplication.initialized(): | |
87 | app = BaseIPythonApplication.instance() |
|
96 | app = BaseIPythonApplication.instance() | |
88 | except MultipleInstanceError: |
|
97 | except MultipleInstanceError: | |
89 | pass |
|
98 | pass | |
90 | if app is None: |
|
99 | if app is None: | |
91 | # create an app, without the global instance |
|
100 | # create an app, without the global instance | |
92 | app = BaseIPythonApplication() |
|
101 | app = BaseIPythonApplication() | |
93 | app.initialize(argv=[]) |
|
102 | app.initialize(argv=[]) | |
94 | return app.profile_dir |
|
103 | return app.profile_dir | |
95 |
|
104 | |||
|
105 | db_file = Unicode(config=True) | |||
|
106 | def _db_file_default(self): | |||
|
107 | if self.profile_dir is None: | |||
|
108 | return ':memory:' | |||
|
109 | return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db') | |||
|
110 | ||||
|
111 | # 64k entries ~ 12MB | |||
|
112 | db_size_limit = Integer(65535, config=True) | |||
|
113 | db = Any() | |||
|
114 | def _db_default(self): | |||
|
115 | if sqlite3 is None: | |||
|
116 | self.log.warn("Missing SQLite3, all notebooks will be untrusted!") | |||
|
117 | return | |||
|
118 | kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) | |||
|
119 | db = sqlite3.connect(self.db_file, **kwargs) | |||
|
120 | self.init_db(db) | |||
|
121 | return db | |||
|
122 | ||||
|
123 | def init_db(self, db): | |||
|
124 | db.execute(""" | |||
|
125 | CREATE TABLE IF NOT EXISTS nbsignatures | |||
|
126 | ( | |||
|
127 | id integer PRIMARY KEY AUTOINCREMENT, | |||
|
128 | algorithm text, | |||
|
129 | signature text, | |||
|
130 | path text, | |||
|
131 | last_seen timestamp | |||
|
132 | )""") | |||
|
133 | db.execute(""" | |||
|
134 | CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature) | |||
|
135 | """) | |||
|
136 | db.commit() | |||
|
137 | ||||
96 | algorithm = Enum(algorithms, default_value='sha256', config=True, |
|
138 | algorithm = Enum(algorithms, default_value='sha256', config=True, | |
97 | help="""The hashing algorithm used to sign notebooks.""" |
|
139 | help="""The hashing algorithm used to sign notebooks.""" | |
98 | ) |
|
140 | ) | |
99 | def _algorithm_changed(self, name, old, new): |
|
141 | def _algorithm_changed(self, name, old, new): | |
100 | self.digestmod = getattr(hashlib, self.algorithm) |
|
142 | self.digestmod = getattr(hashlib, self.algorithm) | |
101 |
|
143 | |||
102 | digestmod = Any() |
|
144 | digestmod = Any() | |
103 | def _digestmod_default(self): |
|
145 | def _digestmod_default(self): | |
104 | return getattr(hashlib, self.algorithm) |
|
146 | return getattr(hashlib, self.algorithm) | |
105 |
|
147 | |||
106 | secret_file = Unicode(config=True, |
|
148 | secret_file = Unicode(config=True, | |
107 | help="""The file where the secret key is stored.""" |
|
149 | help="""The file where the secret key is stored.""" | |
108 | ) |
|
150 | ) | |
109 | def _secret_file_default(self): |
|
151 | def _secret_file_default(self): | |
110 | if self.profile_dir is None: |
|
152 | if self.profile_dir is None: | |
111 | return '' |
|
153 | return '' | |
112 | return os.path.join(self.profile_dir.security_dir, 'notebook_secret') |
|
154 | return os.path.join(self.profile_dir.security_dir, 'notebook_secret') | |
113 |
|
155 | |||
114 | secret = Bytes(config=True, |
|
156 | secret = Bytes(config=True, | |
115 | help="""The secret key with which notebooks are signed.""" |
|
157 | help="""The secret key with which notebooks are signed.""" | |
116 | ) |
|
158 | ) | |
117 | def _secret_default(self): |
|
159 | def _secret_default(self): | |
118 | # note : this assumes an Application is running |
|
160 | # note : this assumes an Application is running | |
119 | if os.path.exists(self.secret_file): |
|
161 | if os.path.exists(self.secret_file): | |
120 | with io.open(self.secret_file, 'rb') as f: |
|
162 | with io.open(self.secret_file, 'rb') as f: | |
121 | return f.read() |
|
163 | return f.read() | |
122 | else: |
|
164 | else: | |
123 | secret = base64.encodestring(os.urandom(1024)) |
|
165 | secret = base64.encodestring(os.urandom(1024)) | |
124 | self._write_secret_file(secret) |
|
166 | self._write_secret_file(secret) | |
125 | return secret |
|
167 | return secret | |
126 |
|
168 | |||
127 | def _write_secret_file(self, secret): |
|
169 | def _write_secret_file(self, secret): | |
128 | """write my secret to my secret_file""" |
|
170 | """write my secret to my secret_file""" | |
129 | self.log.info("Writing notebook-signing key to %s", self.secret_file) |
|
171 | self.log.info("Writing notebook-signing key to %s", self.secret_file) | |
130 | with io.open(self.secret_file, 'wb') as f: |
|
172 | with io.open(self.secret_file, 'wb') as f: | |
131 | f.write(secret) |
|
173 | f.write(secret) | |
132 | try: |
|
174 | try: | |
133 | os.chmod(self.secret_file, 0o600) |
|
175 | os.chmod(self.secret_file, 0o600) | |
134 | except OSError: |
|
176 | except OSError: | |
135 | self.log.warn( |
|
177 | self.log.warn( | |
136 | "Could not set permissions on %s", |
|
178 | "Could not set permissions on %s", | |
137 | self.secret_file |
|
179 | self.secret_file | |
138 | ) |
|
180 | ) | |
139 | return secret |
|
181 | return secret | |
140 |
|
182 | |||
141 | def compute_signature(self, nb): |
|
183 | def compute_signature(self, nb): | |
142 | """Compute a notebook's signature |
|
184 | """Compute a notebook's signature | |
143 |
|
185 | |||
144 | by hashing the entire contents of the notebook via HMAC digest. |
|
186 | by hashing the entire contents of the notebook via HMAC digest. | |
145 | """ |
|
187 | """ | |
146 | hmac = HMAC(self.secret, digestmod=self.digestmod) |
|
188 | hmac = HMAC(self.secret, digestmod=self.digestmod) | |
147 | # don't include the previous hash in the content to hash |
|
189 | # don't include the previous hash in the content to hash | |
148 | with signature_removed(nb): |
|
190 | with signature_removed(nb): | |
149 | # sign the whole thing |
|
191 | # sign the whole thing | |
150 | for b in yield_everything(nb): |
|
192 | for b in yield_everything(nb): | |
151 | hmac.update(b) |
|
193 | hmac.update(b) | |
152 |
|
194 | |||
153 | return hmac.hexdigest() |
|
195 | return hmac.hexdigest() | |
154 |
|
196 | |||
155 | def check_signature(self, nb): |
|
197 | def check_signature(self, nb): | |
156 | """Check a notebook's stored signature |
|
198 | """Check a notebook's stored signature | |
157 |
|
199 | |||
158 | If a signature is stored in the notebook's metadata, |
|
200 | If a signature is stored in the notebook's metadata, | |
159 | a new signature is computed and compared with the stored value. |
|
201 | a new signature is computed and compared with the stored value. | |
160 |
|
202 | |||
161 | Returns True if the signature is found and matches, False otherwise. |
|
203 | Returns True if the signature is found and matches, False otherwise. | |
162 |
|
204 | |||
163 | The following conditions must all be met for a notebook to be trusted: |
|
205 | The following conditions must all be met for a notebook to be trusted: | |
164 | - a signature is stored in the form 'scheme:hexdigest' |
|
206 | - a signature is stored in the form 'scheme:hexdigest' | |
165 | - the stored scheme matches the requested scheme |
|
207 | - the stored scheme matches the requested scheme | |
166 | - the requested scheme is available from hashlib |
|
208 | - the requested scheme is available from hashlib | |
167 | - the computed hash from notebook_signature matches the stored hash |
|
209 | - the computed hash from notebook_signature matches the stored hash | |
168 | """ |
|
210 | """ | |
169 | if nb.nbformat < 3: |
|
211 | if nb.nbformat < 3: | |
170 | return False |
|
212 | return False | |
171 | stored_signature = nb['metadata'].get('signature', None) |
|
213 | if self.db is None: | |
172 | if not stored_signature \ |
|
|||
173 | or not isinstance(stored_signature, string_types) \ |
|
|||
174 | or ':' not in stored_signature: |
|
|||
175 | return False |
|
214 | return False | |
176 | stored_algo, sig = stored_signature.split(':', 1) |
|
215 | signature = self.compute_signature(nb) | |
177 | if self.algorithm != stored_algo: |
|
216 | r = self.db.execute("""SELECT id FROM nbsignatures WHERE | |
|
217 | algorithm = ? AND | |||
|
218 | signature = ?; | |||
|
219 | """, (self.algorithm, signature)).fetchone() | |||
|
220 | if r is None: | |||
178 | return False |
|
221 | return False | |
179 | my_signature = self.compute_signature(nb) |
|
222 | self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE | |
180 | return my_signature == sig |
|
223 | algorithm = ? AND | |
|
224 | signature = ?; | |||
|
225 | """, | |||
|
226 | (datetime.utcnow(), self.algorithm, signature), | |||
|
227 | ) | |||
|
228 | self.db.commit() | |||
|
229 | return True | |||
181 |
|
230 | |||
182 | def sign(self, nb): |
|
231 | def sign(self, nb): | |
183 | """Sign a notebook, indicating that its output is trusted |
|
232 | """Sign a notebook, indicating that its output is trusted on this machine | |
184 |
|
||||
185 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature |
|
|||
186 |
|
233 | |||
187 | e.g. 'sha256:deadbeef123...' |
|
234 | Stores hash algorithm and hmac digest in a local database of trusted notebooks. | |
188 | """ |
|
235 | """ | |
189 | if nb.nbformat < 3: |
|
236 | if nb.nbformat < 3: | |
190 | return |
|
237 | return | |
191 | signature = self.compute_signature(nb) |
|
238 | signature = self.compute_signature(nb) | |
192 | nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature) |
|
239 | self.store_signature(signature, nb) | |
|
240 | ||||
|
241 | def store_signature(self, signature, nb): | |||
|
242 | if self.db is None: | |||
|
243 | return | |||
|
244 | self.db.execute("""INSERT OR IGNORE INTO nbsignatures | |||
|
245 | (algorithm, signature, last_seen) VALUES (?, ?, ?)""", | |||
|
246 | (self.algorithm, signature, datetime.utcnow()) | |||
|
247 | ) | |||
|
248 | self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE | |||
|
249 | algorithm = ? AND | |||
|
250 | signature = ?; | |||
|
251 | """, | |||
|
252 | (datetime.utcnow(), self.algorithm, signature), | |||
|
253 | ) | |||
|
254 | self.db.commit() | |||
|
255 | ||||
|
256 | def unsign(self, nb): | |||
|
257 | """Ensure that a notebook is untrusted | |||
|
258 | ||||
|
259 | by removing its signature from the trusted database, if present. | |||
|
260 | """ | |||
|
261 | signature = self.compute_signature(nb) | |||
|
262 | self.db.execute("""DELETE FROM nbsignatures WHERE | |||
|
263 | algorithm = ? AND | |||
|
264 | signature = ?; | |||
|
265 | """, | |||
|
266 | (self.algorithm, signature) | |||
|
267 | ) | |||
|
268 | self.db.commit() | |||
|
269 | ||||
|
270 | def cull_db(self): | |||
|
271 | self.db.execute("""DELETE FROM nbsignatures WHERE id IN ( | |||
|
272 | SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ? | |||
|
273 | ); | |||
|
274 | """, (self.db_size_limit,)) | |||
193 |
|
275 | |||
194 | def mark_cells(self, nb, trusted): |
|
276 | def mark_cells(self, nb, trusted): | |
195 | """Mark cells as trusted if the notebook's signature can be verified |
|
277 | """Mark cells as trusted if the notebook's signature can be verified | |
196 |
|
278 | |||
197 | Sets ``cell.metadata.trusted = True | False`` on all code cells, |
|
279 | Sets ``cell.metadata.trusted = True | False`` on all code cells, | |
198 | depending on whether the stored signature can be verified. |
|
280 | depending on whether the stored signature can be verified. | |
199 |
|
281 | |||
200 | This function is the inverse of check_cells |
|
282 | This function is the inverse of check_cells | |
201 | """ |
|
283 | """ | |
202 | if nb.nbformat < 3: |
|
284 | if nb.nbformat < 3: | |
203 | return |
|
285 | return | |
204 |
|
286 | |||
205 | for cell in yield_code_cells(nb): |
|
287 | for cell in yield_code_cells(nb): | |
206 | cell['metadata']['trusted'] = trusted |
|
288 | cell['metadata']['trusted'] = trusted | |
207 |
|
289 | |||
208 | def _check_cell(self, cell, nbformat_version): |
|
290 | def _check_cell(self, cell, nbformat_version): | |
209 | """Do we trust an individual cell? |
|
291 | """Do we trust an individual cell? | |
210 |
|
292 | |||
211 | Return True if: |
|
293 | Return True if: | |
212 |
|
294 | |||
213 | - cell is explicitly trusted |
|
295 | - cell is explicitly trusted | |
214 | - cell has no potentially unsafe rich output |
|
296 | - cell has no potentially unsafe rich output | |
215 |
|
297 | |||
216 | If a cell has no output, or only simple print statements, |
|
298 | If a cell has no output, or only simple print statements, | |
217 | it will always be trusted. |
|
299 | it will always be trusted. | |
218 | """ |
|
300 | """ | |
219 | # explicitly trusted |
|
301 | # explicitly trusted | |
220 | if cell['metadata'].pop("trusted", False): |
|
302 | if cell['metadata'].pop("trusted", False): | |
221 | return True |
|
303 | return True | |
222 |
|
304 | |||
223 | # explicitly safe output |
|
305 | # explicitly safe output | |
224 | if nbformat_version >= 4: |
|
306 | if nbformat_version >= 4: | |
225 | safe = {'text/plain', 'image/png', 'image/jpeg'} |
|
|||
226 | unsafe_output_types = ['execute_result', 'display_data'] |
|
307 | unsafe_output_types = ['execute_result', 'display_data'] | |
227 | safe_keys = {"output_type", "execution_count", "metadata"} |
|
308 | safe_keys = {"output_type", "execution_count", "metadata"} | |
228 | else: # v3 |
|
309 | else: # v3 | |
229 | safe = {'text', 'png', 'jpeg'} |
|
|||
230 | unsafe_output_types = ['pyout', 'display_data'] |
|
310 | unsafe_output_types = ['pyout', 'display_data'] | |
231 | safe_keys = {"output_type", "prompt_number", "metadata"} |
|
311 | safe_keys = {"output_type", "prompt_number", "metadata"} | |
232 |
|
312 | |||
233 | for output in cell['outputs']: |
|
313 | for output in cell['outputs']: | |
234 | output_type = output['output_type'] |
|
314 | output_type = output['output_type'] | |
235 | if output_type in unsafe_output_types: |
|
315 | if output_type in unsafe_output_types: | |
236 | # if there are any data keys not in the safe whitelist |
|
316 | # if there are any data keys not in the safe whitelist | |
237 | output_keys = set(output) |
|
317 | output_keys = set(output) | |
238 | if output_keys.difference(safe_keys): |
|
318 | if output_keys.difference(safe_keys): | |
239 | return False |
|
319 | return False | |
240 |
|
320 | |||
241 | return True |
|
321 | return True | |
242 |
|
322 | |||
243 | def check_cells(self, nb): |
|
323 | def check_cells(self, nb): | |
244 | """Return whether all code cells are trusted |
|
324 | """Return whether all code cells are trusted | |
245 |
|
325 | |||
246 | If there are no code cells, return True. |
|
326 | If there are no code cells, return True. | |
247 |
|
327 | |||
248 | This function is the inverse of mark_cells. |
|
328 | This function is the inverse of mark_cells. | |
249 | """ |
|
329 | """ | |
250 | if nb.nbformat < 3: |
|
330 | if nb.nbformat < 3: | |
251 | return False |
|
331 | return False | |
252 | trusted = True |
|
332 | trusted = True | |
253 | for cell in yield_code_cells(nb): |
|
333 | for cell in yield_code_cells(nb): | |
254 | # only distrust a cell if it actually has some output to distrust |
|
334 | # only distrust a cell if it actually has some output to distrust | |
255 | if not self._check_cell(cell, nb.nbformat): |
|
335 | if not self._check_cell(cell, nb.nbformat): | |
256 | trusted = False |
|
336 | trusted = False | |
257 |
|
337 | |||
258 | return trusted |
|
338 | return trusted | |
259 |
|
339 | |||
260 |
|
340 | |||
261 | trust_flags = { |
|
341 | trust_flags = { | |
262 | 'reset' : ( |
|
342 | 'reset' : ( | |
263 | {'TrustNotebookApp' : { 'reset' : True}}, |
|
343 | {'TrustNotebookApp' : { 'reset' : True}}, | |
264 | """Generate a new key for notebook signature. |
|
344 | """Generate a new key for notebook signature. | |
265 | All previously signed notebooks will become untrusted. |
|
345 | All previously signed notebooks will become untrusted. | |
266 | """ |
|
346 | """ | |
267 | ), |
|
347 | ), | |
268 | } |
|
348 | } | |
269 | trust_flags.update(base_flags) |
|
349 | trust_flags.update(base_flags) | |
270 | trust_flags.pop('init') |
|
350 | trust_flags.pop('init') | |
271 |
|
351 | |||
272 |
|
352 | |||
273 | class TrustNotebookApp(BaseIPythonApplication): |
|
353 | class TrustNotebookApp(BaseIPythonApplication): | |
274 |
|
354 | |||
275 | description="""Sign one or more IPython notebooks with your key, |
|
355 | description="""Sign one or more IPython notebooks with your key, | |
276 | to trust their dynamic (HTML, Javascript) output. |
|
356 | to trust their dynamic (HTML, Javascript) output. | |
277 |
|
357 | |||
278 | Trusting a notebook only applies to the current IPython profile. |
|
358 | Trusting a notebook only applies to the current IPython profile. | |
279 | To trust a notebook for use with a profile other than default, |
|
359 | To trust a notebook for use with a profile other than default, | |
280 | add `--profile [profile name]`. |
|
360 | add `--profile [profile name]`. | |
281 |
|
361 | |||
282 | Otherwise, you will have to re-execute the notebook to see output. |
|
362 | Otherwise, you will have to re-execute the notebook to see output. | |
283 | """ |
|
363 | """ | |
284 |
|
364 | |||
285 | examples = """ |
|
365 | examples = """ | |
286 | ipython trust mynotebook.ipynb and_this_one.ipynb |
|
366 | ipython trust mynotebook.ipynb and_this_one.ipynb | |
287 | ipython trust --profile myprofile mynotebook.ipynb |
|
367 | ipython trust --profile myprofile mynotebook.ipynb | |
288 | """ |
|
368 | """ | |
289 |
|
369 | |||
290 | flags = trust_flags |
|
370 | flags = trust_flags | |
291 |
|
371 | |||
292 | reset = Bool(False, config=True, |
|
372 | reset = Bool(False, config=True, | |
293 | help="""If True, generate a new key for notebook signature. |
|
373 | help="""If True, generate a new key for notebook signature. | |
294 | After reset, all previously signed notebooks will become untrusted. |
|
374 | After reset, all previously signed notebooks will become untrusted. | |
295 | """ |
|
375 | """ | |
296 | ) |
|
376 | ) | |
297 |
|
377 | |||
298 | notary = Instance(NotebookNotary) |
|
378 | notary = Instance(NotebookNotary) | |
299 | def _notary_default(self): |
|
379 | def _notary_default(self): | |
300 | return NotebookNotary(parent=self, profile_dir=self.profile_dir) |
|
380 | return NotebookNotary(parent=self, profile_dir=self.profile_dir) | |
301 |
|
381 | |||
302 | def sign_notebook(self, notebook_path): |
|
382 | def sign_notebook(self, notebook_path): | |
303 | if not os.path.exists(notebook_path): |
|
383 | if not os.path.exists(notebook_path): | |
304 | self.log.error("Notebook missing: %s" % notebook_path) |
|
384 | self.log.error("Notebook missing: %s" % notebook_path) | |
305 | self.exit(1) |
|
385 | self.exit(1) | |
306 | with io.open(notebook_path, encoding='utf8') as f: |
|
386 | with io.open(notebook_path, encoding='utf8') as f: | |
307 | nb = read(f, NO_CONVERT) |
|
387 | nb = read(f, NO_CONVERT) | |
308 | if self.notary.check_signature(nb): |
|
388 | if self.notary.check_signature(nb): | |
309 | print("Notebook already signed: %s" % notebook_path) |
|
389 | print("Notebook already signed: %s" % notebook_path) | |
310 | else: |
|
390 | else: | |
311 | print("Signing notebook: %s" % notebook_path) |
|
391 | print("Signing notebook: %s" % notebook_path) | |
312 | self.notary.sign(nb) |
|
392 | self.notary.sign(nb) | |
313 | with atomic_writing(notebook_path) as f: |
|
393 | with atomic_writing(notebook_path) as f: | |
314 | write(nb, f, NO_CONVERT) |
|
394 | write(nb, f, NO_CONVERT) | |
315 |
|
395 | |||
316 | def generate_new_key(self): |
|
396 | def generate_new_key(self): | |
317 | """Generate a new notebook signature key""" |
|
397 | """Generate a new notebook signature key""" | |
318 | print("Generating new notebook key: %s" % self.notary.secret_file) |
|
398 | print("Generating new notebook key: %s" % self.notary.secret_file) | |
319 | self.notary._write_secret_file(os.urandom(1024)) |
|
399 | self.notary._write_secret_file(os.urandom(1024)) | |
320 |
|
400 | |||
321 | def start(self): |
|
401 | def start(self): | |
322 | if self.reset: |
|
402 | if self.reset: | |
323 | self.generate_new_key() |
|
403 | self.generate_new_key() | |
324 | return |
|
404 | return | |
325 | if not self.extra_args: |
|
405 | if not self.extra_args: | |
326 | self.log.critical("Specify at least one notebook to sign.") |
|
406 | self.log.critical("Specify at least one notebook to sign.") | |
327 | self.exit(1) |
|
407 | self.exit(1) | |
328 |
|
408 | |||
329 | for notebook_path in self.extra_args: |
|
409 | for notebook_path in self.extra_args: | |
330 | self.sign_notebook(notebook_path) |
|
410 | self.sign_notebook(notebook_path) | |
331 |
|
411 |
@@ -1,150 +1,200 b'' | |||||
1 | """Test Notebook signing""" |
|
1 | """Test Notebook signing""" | |
2 |
|
2 | |||
3 | # Copyright (c) IPython Development Team. |
|
3 | # Copyright (c) IPython Development Team. | |
4 | # Distributed under the terms of the Modified BSD License. |
|
4 | # Distributed under the terms of the Modified BSD License. | |
5 |
|
5 | |||
|
6 | import copy | |||
|
7 | import time | |||
|
8 | ||||
6 | from .base import TestsBase |
|
9 | from .base import TestsBase | |
7 |
|
10 | |||
8 | from IPython.nbformat import read, sign |
|
11 | from IPython.nbformat import read, sign | |
9 | from IPython.core.getipython import get_ipython |
|
12 | from IPython.core.getipython import get_ipython | |
10 |
|
13 | |||
11 |
|
14 | |||
12 | class TestNotary(TestsBase): |
|
15 | class TestNotary(TestsBase): | |
13 |
|
16 | |||
14 | def setUp(self): |
|
17 | def setUp(self): | |
15 | self.notary = sign.NotebookNotary( |
|
18 | self.notary = sign.NotebookNotary( | |
16 | secret=b'secret', |
|
19 | secret=b'secret', | |
17 | profile_dir=get_ipython().profile_dir |
|
20 | profile_dir=get_ipython().profile_dir, | |
|
21 | db_url=':memory:' | |||
18 | ) |
|
22 | ) | |
19 | with self.fopen(u'test3.ipynb', u'r') as f: |
|
23 | with self.fopen(u'test3.ipynb', u'r') as f: | |
20 | self.nb = read(f, as_version=4) |
|
24 | self.nb = read(f, as_version=4) | |
21 | with self.fopen(u'test3.ipynb', u'r') as f: |
|
25 | with self.fopen(u'test3.ipynb', u'r') as f: | |
22 | self.nb3 = read(f, as_version=3) |
|
26 | self.nb3 = read(f, as_version=3) | |
23 |
|
27 | |||
24 | def test_algorithms(self): |
|
28 | def test_algorithms(self): | |
25 | last_sig = '' |
|
29 | last_sig = '' | |
26 | for algo in sign.algorithms: |
|
30 | for algo in sign.algorithms: | |
27 | self.notary.algorithm = algo |
|
31 | self.notary.algorithm = algo | |
28 | self.notary.sign(self.nb) |
|
32 | sig = self.notary.compute_signature(self.nb) | |
29 | sig = self.nb.metadata.signature |
|
|||
30 | print(sig) |
|
|||
31 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) |
|
|||
32 | self.assertNotEqual(last_sig, sig) |
|
33 | self.assertNotEqual(last_sig, sig) | |
33 | last_sig = sig |
|
34 | last_sig = sig | |
34 |
|
35 | |||
35 | def test_sign_same(self): |
|
36 | def test_sign_same(self): | |
36 | """Multiple signatures of the same notebook are the same""" |
|
37 | """Multiple signatures of the same notebook are the same""" | |
37 | sig1 = self.notary.compute_signature(self.nb) |
|
38 | sig1 = self.notary.compute_signature(self.nb) | |
38 | sig2 = self.notary.compute_signature(self.nb) |
|
39 | sig2 = self.notary.compute_signature(self.nb) | |
39 | self.assertEqual(sig1, sig2) |
|
40 | self.assertEqual(sig1, sig2) | |
40 |
|
41 | |||
41 | def test_change_secret(self): |
|
42 | def test_change_secret(self): | |
42 | """Changing the secret changes the signature""" |
|
43 | """Changing the secret changes the signature""" | |
43 | sig1 = self.notary.compute_signature(self.nb) |
|
44 | sig1 = self.notary.compute_signature(self.nb) | |
44 | self.notary.secret = b'different' |
|
45 | self.notary.secret = b'different' | |
45 | sig2 = self.notary.compute_signature(self.nb) |
|
46 | sig2 = self.notary.compute_signature(self.nb) | |
46 | self.assertNotEqual(sig1, sig2) |
|
47 | self.assertNotEqual(sig1, sig2) | |
47 |
|
48 | |||
48 | def test_sign(self): |
|
49 | def test_sign(self): | |
|
50 | self.assertFalse(self.notary.check_signature(self.nb)) | |||
|
51 | self.notary.sign(self.nb) | |||
|
52 | self.assertTrue(self.notary.check_signature(self.nb)) | |||
|
53 | ||||
|
54 | def test_unsign(self): | |||
49 | self.notary.sign(self.nb) |
|
55 | self.notary.sign(self.nb) | |
50 | sig = self.nb.metadata.signature |
|
56 | self.assertTrue(self.notary.check_signature(self.nb)) | |
51 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) |
|
57 | self.notary.unsign(self.nb) | |
|
58 | self.assertFalse(self.notary.check_signature(self.nb)) | |||
|
59 | self.notary.unsign(self.nb) | |||
|
60 | self.assertFalse(self.notary.check_signature(self.nb)) | |||
|
61 | ||||
|
62 | def test_cull_db(self): | |||
|
63 | # this test has various sleeps of 2ms | |||
|
64 | # to ensure low resolution timestamps compare as expected | |||
|
65 | dt = 2e-3 | |||
|
66 | nbs = [ | |||
|
67 | copy.deepcopy(self.nb) for i in range(5) | |||
|
68 | ] | |||
|
69 | for i, nb in enumerate(nbs): | |||
|
70 | nb.metadata.dirty = i | |||
|
71 | self.notary.sign(nb) | |||
|
72 | ||||
|
73 | for i, nb in enumerate(nbs): | |||
|
74 | time.sleep(dt) | |||
|
75 | self.assertTrue(self.notary.check_signature(nb), 'nb %i is trusted' % i) | |||
|
76 | ||||
|
77 | self.notary.db_size_limit = 2 | |||
|
78 | self.notary.cull_db() | |||
|
79 | ||||
|
80 | # expect all but last two signatures to be culled | |||
|
81 | self.assertEqual( | |||
|
82 | [self.notary.check_signature(nb) for nb in nbs], | |||
|
83 | [False] * (len(nbs) - 2) + [True] * 2 | |||
|
84 | ) | |||
|
85 | ||||
|
86 | # sign them all again | |||
|
87 | for nb in nbs: | |||
|
88 | time.sleep(dt) | |||
|
89 | self.notary.sign(nb) | |||
|
90 | ||||
|
91 | # checking front two marks them as newest for next cull instead of oldest | |||
|
92 | time.sleep(dt) | |||
|
93 | self.notary.check_signature(nbs[0]) | |||
|
94 | self.notary.check_signature(nbs[1]) | |||
|
95 | self.notary.cull_db() | |||
|
96 | ||||
|
97 | self.assertEqual( | |||
|
98 | [self.notary.check_signature(nb) for nb in nbs], | |||
|
99 | [True] * 2 + [False] * (len(nbs) - 2) | |||
|
100 | ) | |||
|
101 | ||||
52 |
|
102 | |||
53 | def test_check_signature(self): |
|
103 | def test_check_signature(self): | |
54 | nb = self.nb |
|
104 | nb = self.nb | |
55 | md = nb.metadata |
|
105 | md = nb.metadata | |
56 | notary = self.notary |
|
106 | notary = self.notary | |
57 | check_signature = notary.check_signature |
|
107 | check_signature = notary.check_signature | |
58 | # no signature: |
|
108 | # no signature: | |
59 | md.pop('signature', None) |
|
109 | md.pop('signature', None) | |
60 | self.assertFalse(check_signature(nb)) |
|
110 | self.assertFalse(check_signature(nb)) | |
61 | # hash only, no algo |
|
111 | # hash only, no algo | |
62 | md.signature = notary.compute_signature(nb) |
|
112 | md.signature = notary.compute_signature(nb) | |
63 | self.assertFalse(check_signature(nb)) |
|
113 | self.assertFalse(check_signature(nb)) | |
64 | # proper signature, algo mismatch |
|
114 | # proper signature, algo mismatch | |
65 | notary.algorithm = 'sha224' |
|
115 | notary.algorithm = 'sha224' | |
66 | notary.sign(nb) |
|
116 | notary.sign(nb) | |
67 | notary.algorithm = 'sha256' |
|
117 | notary.algorithm = 'sha256' | |
68 | self.assertFalse(check_signature(nb)) |
|
118 | self.assertFalse(check_signature(nb)) | |
69 | # check correctly signed notebook |
|
119 | # check correctly signed notebook | |
70 | notary.sign(nb) |
|
120 | notary.sign(nb) | |
71 | self.assertTrue(check_signature(nb)) |
|
121 | self.assertTrue(check_signature(nb)) | |
72 |
|
122 | |||
73 | def test_mark_cells_untrusted(self): |
|
123 | def test_mark_cells_untrusted(self): | |
74 | cells = self.nb.cells |
|
124 | cells = self.nb.cells | |
75 | self.notary.mark_cells(self.nb, False) |
|
125 | self.notary.mark_cells(self.nb, False) | |
76 | for cell in cells: |
|
126 | for cell in cells: | |
77 | self.assertNotIn('trusted', cell) |
|
127 | self.assertNotIn('trusted', cell) | |
78 | if cell.cell_type == 'code': |
|
128 | if cell.cell_type == 'code': | |
79 | self.assertIn('trusted', cell.metadata) |
|
129 | self.assertIn('trusted', cell.metadata) | |
80 | self.assertFalse(cell.metadata.trusted) |
|
130 | self.assertFalse(cell.metadata.trusted) | |
81 | else: |
|
131 | else: | |
82 | self.assertNotIn('trusted', cell.metadata) |
|
132 | self.assertNotIn('trusted', cell.metadata) | |
83 |
|
133 | |||
84 | def test_mark_cells_trusted(self): |
|
134 | def test_mark_cells_trusted(self): | |
85 | cells = self.nb.cells |
|
135 | cells = self.nb.cells | |
86 | self.notary.mark_cells(self.nb, True) |
|
136 | self.notary.mark_cells(self.nb, True) | |
87 | for cell in cells: |
|
137 | for cell in cells: | |
88 | self.assertNotIn('trusted', cell) |
|
138 | self.assertNotIn('trusted', cell) | |
89 | if cell.cell_type == 'code': |
|
139 | if cell.cell_type == 'code': | |
90 | self.assertIn('trusted', cell.metadata) |
|
140 | self.assertIn('trusted', cell.metadata) | |
91 | self.assertTrue(cell.metadata.trusted) |
|
141 | self.assertTrue(cell.metadata.trusted) | |
92 | else: |
|
142 | else: | |
93 | self.assertNotIn('trusted', cell.metadata) |
|
143 | self.assertNotIn('trusted', cell.metadata) | |
94 |
|
144 | |||
95 | def test_check_cells(self): |
|
145 | def test_check_cells(self): | |
96 | nb = self.nb |
|
146 | nb = self.nb | |
97 | self.notary.mark_cells(nb, True) |
|
147 | self.notary.mark_cells(nb, True) | |
98 | self.assertTrue(self.notary.check_cells(nb)) |
|
148 | self.assertTrue(self.notary.check_cells(nb)) | |
99 | for cell in nb.cells: |
|
149 | for cell in nb.cells: | |
100 | self.assertNotIn('trusted', cell) |
|
150 | self.assertNotIn('trusted', cell) | |
101 | self.notary.mark_cells(nb, False) |
|
151 | self.notary.mark_cells(nb, False) | |
102 | self.assertFalse(self.notary.check_cells(nb)) |
|
152 | self.assertFalse(self.notary.check_cells(nb)) | |
103 | for cell in nb.cells: |
|
153 | for cell in nb.cells: | |
104 | self.assertNotIn('trusted', cell) |
|
154 | self.assertNotIn('trusted', cell) | |
105 |
|
155 | |||
106 | def test_trust_no_output(self): |
|
156 | def test_trust_no_output(self): | |
107 | nb = self.nb |
|
157 | nb = self.nb | |
108 | self.notary.mark_cells(nb, False) |
|
158 | self.notary.mark_cells(nb, False) | |
109 | for cell in nb.cells: |
|
159 | for cell in nb.cells: | |
110 | if cell.cell_type == 'code': |
|
160 | if cell.cell_type == 'code': | |
111 | cell.outputs = [] |
|
161 | cell.outputs = [] | |
112 | self.assertTrue(self.notary.check_cells(nb)) |
|
162 | self.assertTrue(self.notary.check_cells(nb)) | |
113 |
|
163 | |||
114 | def test_mark_cells_untrusted_v3(self): |
|
164 | def test_mark_cells_untrusted_v3(self): | |
115 | nb = self.nb3 |
|
165 | nb = self.nb3 | |
116 | cells = nb.worksheets[0].cells |
|
166 | cells = nb.worksheets[0].cells | |
117 | self.notary.mark_cells(nb, False) |
|
167 | self.notary.mark_cells(nb, False) | |
118 | for cell in cells: |
|
168 | for cell in cells: | |
119 | self.assertNotIn('trusted', cell) |
|
169 | self.assertNotIn('trusted', cell) | |
120 | if cell.cell_type == 'code': |
|
170 | if cell.cell_type == 'code': | |
121 | self.assertIn('trusted', cell.metadata) |
|
171 | self.assertIn('trusted', cell.metadata) | |
122 | self.assertFalse(cell.metadata.trusted) |
|
172 | self.assertFalse(cell.metadata.trusted) | |
123 | else: |
|
173 | else: | |
124 | self.assertNotIn('trusted', cell.metadata) |
|
174 | self.assertNotIn('trusted', cell.metadata) | |
125 |
|
175 | |||
126 | def test_mark_cells_trusted_v3(self): |
|
176 | def test_mark_cells_trusted_v3(self): | |
127 | nb = self.nb3 |
|
177 | nb = self.nb3 | |
128 | cells = nb.worksheets[0].cells |
|
178 | cells = nb.worksheets[0].cells | |
129 | self.notary.mark_cells(nb, True) |
|
179 | self.notary.mark_cells(nb, True) | |
130 | for cell in cells: |
|
180 | for cell in cells: | |
131 | self.assertNotIn('trusted', cell) |
|
181 | self.assertNotIn('trusted', cell) | |
132 | if cell.cell_type == 'code': |
|
182 | if cell.cell_type == 'code': | |
133 | self.assertIn('trusted', cell.metadata) |
|
183 | self.assertIn('trusted', cell.metadata) | |
134 | self.assertTrue(cell.metadata.trusted) |
|
184 | self.assertTrue(cell.metadata.trusted) | |
135 | else: |
|
185 | else: | |
136 | self.assertNotIn('trusted', cell.metadata) |
|
186 | self.assertNotIn('trusted', cell.metadata) | |
137 |
|
187 | |||
138 | def test_check_cells_v3(self): |
|
188 | def test_check_cells_v3(self): | |
139 | nb = self.nb3 |
|
189 | nb = self.nb3 | |
140 | cells = nb.worksheets[0].cells |
|
190 | cells = nb.worksheets[0].cells | |
141 | self.notary.mark_cells(nb, True) |
|
191 | self.notary.mark_cells(nb, True) | |
142 | self.assertTrue(self.notary.check_cells(nb)) |
|
192 | self.assertTrue(self.notary.check_cells(nb)) | |
143 | for cell in cells: |
|
193 | for cell in cells: | |
144 | self.assertNotIn('trusted', cell) |
|
194 | self.assertNotIn('trusted', cell) | |
145 | self.notary.mark_cells(nb, False) |
|
195 | self.notary.mark_cells(nb, False) | |
146 | self.assertFalse(self.notary.check_cells(nb)) |
|
196 | self.assertFalse(self.notary.check_cells(nb)) | |
147 | for cell in cells: |
|
197 | for cell in cells: | |
148 | self.assertNotIn('trusted', cell) |
|
198 | self.assertNotIn('trusted', cell) | |
149 |
|
199 | |||
150 |
|
200 |
@@ -1,252 +1,253 b'' | |||||
1 | """Code for converting notebooks to and from v3.""" |
|
1 | """Code for converting notebooks to and from v3.""" | |
2 |
|
2 | |||
3 | # Copyright (c) IPython Development Team. |
|
3 | # Copyright (c) IPython Development Team. | |
4 | # Distributed under the terms of the Modified BSD License. |
|
4 | # Distributed under the terms of the Modified BSD License. | |
5 |
|
5 | |||
6 | import json |
|
6 | import json | |
7 | import re |
|
7 | import re | |
8 |
|
8 | |||
9 | from .nbbase import ( |
|
9 | from .nbbase import ( | |
10 | nbformat, nbformat_minor, |
|
10 | nbformat, nbformat_minor, | |
11 | NotebookNode, |
|
11 | NotebookNode, | |
12 | ) |
|
12 | ) | |
13 |
|
13 | |||
14 | from IPython.nbformat import v3 |
|
14 | from IPython.nbformat import v3 | |
15 | from IPython.utils.log import get_logger |
|
15 | from IPython.utils.log import get_logger | |
16 |
|
16 | |||
17 | def _warn_if_invalid(nb, version): |
|
17 | def _warn_if_invalid(nb, version): | |
18 | """Log validation errors, if there are any.""" |
|
18 | """Log validation errors, if there are any.""" | |
19 | from IPython.nbformat import validate, ValidationError |
|
19 | from IPython.nbformat import validate, ValidationError | |
20 | try: |
|
20 | try: | |
21 | validate(nb, version=version) |
|
21 | validate(nb, version=version) | |
22 | except ValidationError as e: |
|
22 | except ValidationError as e: | |
23 | get_logger().error("Notebook JSON is not valid v%i: %s", version, e) |
|
23 | get_logger().error("Notebook JSON is not valid v%i: %s", version, e) | |
24 |
|
24 | |||
25 | def upgrade(nb, from_version=3, from_minor=0): |
|
25 | def upgrade(nb, from_version=3, from_minor=0): | |
26 | """Convert a notebook to v4. |
|
26 | """Convert a notebook to v4. | |
27 |
|
27 | |||
28 | Parameters |
|
28 | Parameters | |
29 | ---------- |
|
29 | ---------- | |
30 | nb : NotebookNode |
|
30 | nb : NotebookNode | |
31 | The Python representation of the notebook to convert. |
|
31 | The Python representation of the notebook to convert. | |
32 | from_version : int |
|
32 | from_version : int | |
33 | The original version of the notebook to convert. |
|
33 | The original version of the notebook to convert. | |
34 | from_minor : int |
|
34 | from_minor : int | |
35 | The original minor version of the notebook to convert (only relevant for v >= 3). |
|
35 | The original minor version of the notebook to convert (only relevant for v >= 3). | |
36 | """ |
|
36 | """ | |
37 | if from_version == 3: |
|
37 | if from_version == 3: | |
38 | # Validate the notebook before conversion |
|
38 | # Validate the notebook before conversion | |
39 | _warn_if_invalid(nb, from_version) |
|
39 | _warn_if_invalid(nb, from_version) | |
40 |
|
40 | |||
41 | # Mark the original nbformat so consumers know it has been converted |
|
41 | # Mark the original nbformat so consumers know it has been converted | |
42 | orig_nbformat = nb.pop('orig_nbformat', None) |
|
42 | orig_nbformat = nb.pop('orig_nbformat', None) | |
43 | nb.metadata.orig_nbformat = orig_nbformat or 3 |
|
43 | nb.metadata.orig_nbformat = orig_nbformat or 3 | |
44 |
|
44 | |||
45 | # Mark the new format |
|
45 | # Mark the new format | |
46 | nb.nbformat = nbformat |
|
46 | nb.nbformat = nbformat | |
47 | nb.nbformat_minor = nbformat_minor |
|
47 | nb.nbformat_minor = nbformat_minor | |
48 |
|
48 | |||
49 | # remove worksheet(s) |
|
49 | # remove worksheet(s) | |
50 | nb['cells'] = cells = [] |
|
50 | nb['cells'] = cells = [] | |
51 | # In the unlikely event of multiple worksheets, |
|
51 | # In the unlikely event of multiple worksheets, | |
52 | # they will be flattened |
|
52 | # they will be flattened | |
53 | for ws in nb.pop('worksheets', []): |
|
53 | for ws in nb.pop('worksheets', []): | |
54 | # upgrade each cell |
|
54 | # upgrade each cell | |
55 | for cell in ws['cells']: |
|
55 | for cell in ws['cells']: | |
56 | cells.append(upgrade_cell(cell)) |
|
56 | cells.append(upgrade_cell(cell)) | |
57 | # upgrade metadata |
|
57 | # upgrade metadata | |
58 | nb.metadata.pop('name', '') |
|
58 | nb.metadata.pop('name', '') | |
|
59 | nb.metadata.pop('signature', '') | |||
59 | # Validate the converted notebook before returning it |
|
60 | # Validate the converted notebook before returning it | |
60 | _warn_if_invalid(nb, nbformat) |
|
61 | _warn_if_invalid(nb, nbformat) | |
61 | return nb |
|
62 | return nb | |
62 | elif from_version == 4: |
|
63 | elif from_version == 4: | |
63 | # nothing to do |
|
64 | # nothing to do | |
64 | if from_minor != nbformat_minor: |
|
65 | if from_minor != nbformat_minor: | |
65 | nb.metadata.orig_nbformat_minor = from_minor |
|
66 | nb.metadata.orig_nbformat_minor = from_minor | |
66 | nb.nbformat_minor = nbformat_minor |
|
67 | nb.nbformat_minor = nbformat_minor | |
67 |
|
68 | |||
68 | return nb |
|
69 | return nb | |
69 | else: |
|
70 | else: | |
70 | raise ValueError('Cannot convert a notebook directly from v%s to v4. ' \ |
|
71 | raise ValueError('Cannot convert a notebook directly from v%s to v4. ' \ | |
71 | 'Try using the IPython.nbformat.convert module.' % from_version) |
|
72 | 'Try using the IPython.nbformat.convert module.' % from_version) | |
72 |
|
73 | |||
73 | def upgrade_cell(cell): |
|
74 | def upgrade_cell(cell): | |
74 | """upgrade a cell from v3 to v4 |
|
75 | """upgrade a cell from v3 to v4 | |
75 |
|
76 | |||
76 | heading cell: |
|
77 | heading cell: | |
77 | - -> markdown heading |
|
78 | - -> markdown heading | |
78 | code cell: |
|
79 | code cell: | |
79 | - remove language metadata |
|
80 | - remove language metadata | |
80 | - cell.input -> cell.source |
|
81 | - cell.input -> cell.source | |
81 | - cell.prompt_number -> cell.execution_count |
|
82 | - cell.prompt_number -> cell.execution_count | |
82 | - update outputs |
|
83 | - update outputs | |
83 | """ |
|
84 | """ | |
84 | cell.setdefault('metadata', NotebookNode()) |
|
85 | cell.setdefault('metadata', NotebookNode()) | |
85 | if cell.cell_type == 'code': |
|
86 | if cell.cell_type == 'code': | |
86 | cell.pop('language', '') |
|
87 | cell.pop('language', '') | |
87 | if 'collapsed' in cell: |
|
88 | if 'collapsed' in cell: | |
88 | cell.metadata['collapsed'] = cell.pop('collapsed') |
|
89 | cell.metadata['collapsed'] = cell.pop('collapsed') | |
89 | cell.source = cell.pop('input', '') |
|
90 | cell.source = cell.pop('input', '') | |
90 | cell.execution_count = cell.pop('prompt_number', None) |
|
91 | cell.execution_count = cell.pop('prompt_number', None) | |
91 | cell.outputs = upgrade_outputs(cell.outputs) |
|
92 | cell.outputs = upgrade_outputs(cell.outputs) | |
92 | elif cell.cell_type == 'heading': |
|
93 | elif cell.cell_type == 'heading': | |
93 | cell.cell_type = 'markdown' |
|
94 | cell.cell_type = 'markdown' | |
94 | level = cell.pop('level', 1) |
|
95 | level = cell.pop('level', 1) | |
95 | cell.source = u'{hashes} {single_line}'.format( |
|
96 | cell.source = u'{hashes} {single_line}'.format( | |
96 | hashes='#' * level, |
|
97 | hashes='#' * level, | |
97 | single_line = ' '.join(cell.get('source', '').splitlines()), |
|
98 | single_line = ' '.join(cell.get('source', '').splitlines()), | |
98 | ) |
|
99 | ) | |
99 | elif cell.cell_type == 'html': |
|
100 | elif cell.cell_type == 'html': | |
100 | # Technically, this exists. It will never happen in practice. |
|
101 | # Technically, this exists. It will never happen in practice. | |
101 | cell.cell_type = 'markdown' |
|
102 | cell.cell_type = 'markdown' | |
102 | return cell |
|
103 | return cell | |
103 |
|
104 | |||
104 | def downgrade_cell(cell): |
|
105 | def downgrade_cell(cell): | |
105 | """downgrade a cell from v4 to v3 |
|
106 | """downgrade a cell from v4 to v3 | |
106 |
|
107 | |||
107 | code cell: |
|
108 | code cell: | |
108 | - set cell.language |
|
109 | - set cell.language | |
109 | - cell.input <- cell.source |
|
110 | - cell.input <- cell.source | |
110 | - cell.prompt_number <- cell.execution_count |
|
111 | - cell.prompt_number <- cell.execution_count | |
111 | - update outputs |
|
112 | - update outputs | |
112 | markdown cell: |
|
113 | markdown cell: | |
113 | - single-line heading -> heading cell |
|
114 | - single-line heading -> heading cell | |
114 | """ |
|
115 | """ | |
115 | if cell.cell_type == 'code': |
|
116 | if cell.cell_type == 'code': | |
116 | cell.language = 'python' |
|
117 | cell.language = 'python' | |
117 | cell.input = cell.pop('source', '') |
|
118 | cell.input = cell.pop('source', '') | |
118 | cell.prompt_number = cell.pop('execution_count', None) |
|
119 | cell.prompt_number = cell.pop('execution_count', None) | |
119 | cell.collapsed = cell.metadata.pop('collapsed', False) |
|
120 | cell.collapsed = cell.metadata.pop('collapsed', False) | |
120 | cell.outputs = downgrade_outputs(cell.outputs) |
|
121 | cell.outputs = downgrade_outputs(cell.outputs) | |
121 | elif cell.cell_type == 'markdown': |
|
122 | elif cell.cell_type == 'markdown': | |
122 | source = cell.get('source', '') |
|
123 | source = cell.get('source', '') | |
123 | if '\n' not in source and source.startswith('#'): |
|
124 | if '\n' not in source and source.startswith('#'): | |
124 | prefix, text = re.match(r'(#+)\s*(.*)', source).groups() |
|
125 | prefix, text = re.match(r'(#+)\s*(.*)', source).groups() | |
125 | cell.cell_type = 'heading' |
|
126 | cell.cell_type = 'heading' | |
126 | cell.source = text |
|
127 | cell.source = text | |
127 | cell.level = len(prefix) |
|
128 | cell.level = len(prefix) | |
128 | return cell |
|
129 | return cell | |
129 |
|
130 | |||
130 | _mime_map = { |
|
131 | _mime_map = { | |
131 | "text" : "text/plain", |
|
132 | "text" : "text/plain", | |
132 | "html" : "text/html", |
|
133 | "html" : "text/html", | |
133 | "svg" : "image/svg+xml", |
|
134 | "svg" : "image/svg+xml", | |
134 | "png" : "image/png", |
|
135 | "png" : "image/png", | |
135 | "jpeg" : "image/jpeg", |
|
136 | "jpeg" : "image/jpeg", | |
136 | "latex" : "text/latex", |
|
137 | "latex" : "text/latex", | |
137 | "json" : "application/json", |
|
138 | "json" : "application/json", | |
138 | "javascript" : "application/javascript", |
|
139 | "javascript" : "application/javascript", | |
139 | }; |
|
140 | }; | |
140 |
|
141 | |||
141 | def to_mime_key(d): |
|
142 | def to_mime_key(d): | |
142 | """convert dict with v3 aliases to plain mime-type keys""" |
|
143 | """convert dict with v3 aliases to plain mime-type keys""" | |
143 | for alias, mime in _mime_map.items(): |
|
144 | for alias, mime in _mime_map.items(): | |
144 | if alias in d: |
|
145 | if alias in d: | |
145 | d[mime] = d.pop(alias) |
|
146 | d[mime] = d.pop(alias) | |
146 | return d |
|
147 | return d | |
147 |
|
148 | |||
148 | def from_mime_key(d): |
|
149 | def from_mime_key(d): | |
149 | """convert dict with mime-type keys to v3 aliases""" |
|
150 | """convert dict with mime-type keys to v3 aliases""" | |
150 | for alias, mime in _mime_map.items(): |
|
151 | for alias, mime in _mime_map.items(): | |
151 | if mime in d: |
|
152 | if mime in d: | |
152 | d[alias] = d.pop(mime) |
|
153 | d[alias] = d.pop(mime) | |
153 | return d |
|
154 | return d | |
154 |
|
155 | |||
155 | def upgrade_output(output): |
|
156 | def upgrade_output(output): | |
156 | """upgrade a single code cell output from v3 to v4 |
|
157 | """upgrade a single code cell output from v3 to v4 | |
157 |
|
158 | |||
158 | - pyout -> execute_result |
|
159 | - pyout -> execute_result | |
159 | - pyerr -> error |
|
160 | - pyerr -> error | |
160 | - output.type -> output.data.mime/type |
|
161 | - output.type -> output.data.mime/type | |
161 | - mime-type keys |
|
162 | - mime-type keys | |
162 | - stream.stream -> stream.name |
|
163 | - stream.stream -> stream.name | |
163 | """ |
|
164 | """ | |
164 | if output['output_type'] in {'pyout', 'display_data'}: |
|
165 | if output['output_type'] in {'pyout', 'display_data'}: | |
165 | output.setdefault('metadata', NotebookNode()) |
|
166 | output.setdefault('metadata', NotebookNode()) | |
166 | if output['output_type'] == 'pyout': |
|
167 | if output['output_type'] == 'pyout': | |
167 | output['output_type'] = 'execute_result' |
|
168 | output['output_type'] = 'execute_result' | |
168 | output['execution_count'] = output.pop('prompt_number', None) |
|
169 | output['execution_count'] = output.pop('prompt_number', None) | |
169 |
|
170 | |||
170 | # move output data into data sub-dict |
|
171 | # move output data into data sub-dict | |
171 | data = {} |
|
172 | data = {} | |
172 | for key in list(output): |
|
173 | for key in list(output): | |
173 | if key in {'output_type', 'execution_count', 'metadata'}: |
|
174 | if key in {'output_type', 'execution_count', 'metadata'}: | |
174 | continue |
|
175 | continue | |
175 | data[key] = output.pop(key) |
|
176 | data[key] = output.pop(key) | |
176 | to_mime_key(data) |
|
177 | to_mime_key(data) | |
177 | output['data'] = data |
|
178 | output['data'] = data | |
178 | to_mime_key(output.metadata) |
|
179 | to_mime_key(output.metadata) | |
179 | if 'application/json' in data: |
|
180 | if 'application/json' in data: | |
180 | data['application/json'] = json.loads(data['application/json']) |
|
181 | data['application/json'] = json.loads(data['application/json']) | |
181 | # promote ascii bytes (from v2) to unicode |
|
182 | # promote ascii bytes (from v2) to unicode | |
182 | for key in ('image/png', 'image/jpeg'): |
|
183 | for key in ('image/png', 'image/jpeg'): | |
183 | if key in data and isinstance(data[key], bytes): |
|
184 | if key in data and isinstance(data[key], bytes): | |
184 | data[key] = data[key].decode('ascii') |
|
185 | data[key] = data[key].decode('ascii') | |
185 | elif output['output_type'] == 'pyerr': |
|
186 | elif output['output_type'] == 'pyerr': | |
186 | output['output_type'] = 'error' |
|
187 | output['output_type'] = 'error' | |
187 | elif output['output_type'] == 'stream': |
|
188 | elif output['output_type'] == 'stream': | |
188 | output['name'] = output.pop('stream') |
|
189 | output['name'] = output.pop('stream') | |
189 | return output |
|
190 | return output | |
190 |
|
191 | |||
191 | def downgrade_output(output): |
|
192 | def downgrade_output(output): | |
192 | """downgrade a single code cell output to v3 from v4 |
|
193 | """downgrade a single code cell output to v3 from v4 | |
193 |
|
194 | |||
194 | - pyout <- execute_result |
|
195 | - pyout <- execute_result | |
195 | - pyerr <- error |
|
196 | - pyerr <- error | |
196 | - output.data.mime/type -> output.type |
|
197 | - output.data.mime/type -> output.type | |
197 | - un-mime-type keys |
|
198 | - un-mime-type keys | |
198 | - stream.stream <- stream.name |
|
199 | - stream.stream <- stream.name | |
199 | """ |
|
200 | """ | |
200 | if output['output_type'] in {'execute_result', 'display_data'}: |
|
201 | if output['output_type'] in {'execute_result', 'display_data'}: | |
201 | if output['output_type'] == 'execute_result': |
|
202 | if output['output_type'] == 'execute_result': | |
202 | output['output_type'] = 'pyout' |
|
203 | output['output_type'] = 'pyout' | |
203 | output['prompt_number'] = output.pop('execution_count', None) |
|
204 | output['prompt_number'] = output.pop('execution_count', None) | |
204 |
|
205 | |||
205 | # promote data dict to top-level output namespace |
|
206 | # promote data dict to top-level output namespace | |
206 | data = output.pop('data', {}) |
|
207 | data = output.pop('data', {}) | |
207 | if 'application/json' in data: |
|
208 | if 'application/json' in data: | |
208 | data['application/json'] = json.dumps(data['application/json']) |
|
209 | data['application/json'] = json.dumps(data['application/json']) | |
209 | from_mime_key(data) |
|
210 | from_mime_key(data) | |
210 | output.update(data) |
|
211 | output.update(data) | |
211 | from_mime_key(output.get('metadata', {})) |
|
212 | from_mime_key(output.get('metadata', {})) | |
212 | elif output['output_type'] == 'error': |
|
213 | elif output['output_type'] == 'error': | |
213 | output['output_type'] = 'pyerr' |
|
214 | output['output_type'] = 'pyerr' | |
214 | elif output['output_type'] == 'stream': |
|
215 | elif output['output_type'] == 'stream': | |
215 | output['stream'] = output.pop('name') |
|
216 | output['stream'] = output.pop('name') | |
216 | return output |
|
217 | return output | |
217 |
|
218 | |||
218 | def upgrade_outputs(outputs): |
|
219 | def upgrade_outputs(outputs): | |
219 | """upgrade outputs of a code cell from v3 to v4""" |
|
220 | """upgrade outputs of a code cell from v3 to v4""" | |
220 | return [upgrade_output(op) for op in outputs] |
|
221 | return [upgrade_output(op) for op in outputs] | |
221 |
|
222 | |||
222 | def downgrade_outputs(outputs): |
|
223 | def downgrade_outputs(outputs): | |
223 | """downgrade outputs of a code cell to v3 from v4""" |
|
224 | """downgrade outputs of a code cell to v3 from v4""" | |
224 | return [downgrade_output(op) for op in outputs] |
|
225 | return [downgrade_output(op) for op in outputs] | |
225 |
|
226 | |||
226 | def downgrade(nb): |
|
227 | def downgrade(nb): | |
227 | """Convert a v4 notebook to v3. |
|
228 | """Convert a v4 notebook to v3. | |
228 |
|
229 | |||
229 | Parameters |
|
230 | Parameters | |
230 | ---------- |
|
231 | ---------- | |
231 | nb : NotebookNode |
|
232 | nb : NotebookNode | |
232 | The Python representation of the notebook to convert. |
|
233 | The Python representation of the notebook to convert. | |
233 | """ |
|
234 | """ | |
234 | if nb.nbformat != nbformat: |
|
235 | if nb.nbformat != nbformat: | |
235 | return nb |
|
236 | return nb | |
236 |
|
237 | |||
237 | # Validate the notebook before conversion |
|
238 | # Validate the notebook before conversion | |
238 | _warn_if_invalid(nb, nbformat) |
|
239 | _warn_if_invalid(nb, nbformat) | |
239 |
|
240 | |||
240 | nb.nbformat = v3.nbformat |
|
241 | nb.nbformat = v3.nbformat | |
241 | nb.nbformat_minor = v3.nbformat_minor |
|
242 | nb.nbformat_minor = v3.nbformat_minor | |
242 | cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ] |
|
243 | cells = [ downgrade_cell(cell) for cell in nb.pop('cells') ] | |
243 | nb.worksheets = [v3.new_worksheet(cells=cells)] |
|
244 | nb.worksheets = [v3.new_worksheet(cells=cells)] | |
244 | nb.metadata.setdefault('name', '') |
|
245 | nb.metadata.setdefault('name', '') | |
245 |
|
246 | |||
246 | # Validate the converted notebook before returning it |
|
247 | # Validate the converted notebook before returning it | |
247 | _warn_if_invalid(nb, v3.nbformat) |
|
248 | _warn_if_invalid(nb, v3.nbformat) | |
248 |
|
249 | |||
249 | nb.orig_nbformat = nb.metadata.pop('orig_nbformat', nbformat) |
|
250 | nb.orig_nbformat = nb.metadata.pop('orig_nbformat', nbformat) | |
250 | nb.orig_nbformat_minor = nb.metadata.pop('orig_nbformat_minor', nbformat_minor) |
|
251 | nb.orig_nbformat_minor = nb.metadata.pop('orig_nbformat_minor', nbformat_minor) | |
251 |
|
252 | |||
252 | return nb |
|
253 | return nb |
@@ -1,375 +1,371 b'' | |||||
1 | { |
|
1 | { | |
2 | "$schema": "http://json-schema.org/draft-04/schema#", |
|
2 | "$schema": "http://json-schema.org/draft-04/schema#", | |
3 | "description": "IPython Notebook v4.0 JSON schema.", |
|
3 | "description": "IPython Notebook v4.0 JSON schema.", | |
4 | "type": "object", |
|
4 | "type": "object", | |
5 | "additionalProperties": false, |
|
5 | "additionalProperties": false, | |
6 | "required": ["metadata", "nbformat_minor", "nbformat", "cells"], |
|
6 | "required": ["metadata", "nbformat_minor", "nbformat", "cells"], | |
7 | "properties": { |
|
7 | "properties": { | |
8 | "metadata": { |
|
8 | "metadata": { | |
9 | "description": "Notebook root-level metadata.", |
|
9 | "description": "Notebook root-level metadata.", | |
10 | "type": "object", |
|
10 | "type": "object", | |
11 | "additionalProperties": true, |
|
11 | "additionalProperties": true, | |
12 | "properties": { |
|
12 | "properties": { | |
13 | "kernelspec": { |
|
13 | "kernelspec": { | |
14 | "description": "Kernel information.", |
|
14 | "description": "Kernel information.", | |
15 | "type": "object", |
|
15 | "type": "object", | |
16 | "required": ["name", "display_name"], |
|
16 | "required": ["name", "display_name"], | |
17 | "properties": { |
|
17 | "properties": { | |
18 | "name": { |
|
18 | "name": { | |
19 | "description": "Name of the kernel specification.", |
|
19 | "description": "Name of the kernel specification.", | |
20 | "type": "string" |
|
20 | "type": "string" | |
21 | }, |
|
21 | }, | |
22 | "display_name": { |
|
22 | "display_name": { | |
23 | "description": "Name to display in UI.", |
|
23 | "description": "Name to display in UI.", | |
24 | "type": "string" |
|
24 | "type": "string" | |
25 | } |
|
25 | } | |
26 | } |
|
26 | } | |
27 | }, |
|
27 | }, | |
28 | "language_info": { |
|
28 | "language_info": { | |
29 | "description": "Kernel information.", |
|
29 | "description": "Kernel information.", | |
30 | "type": "object", |
|
30 | "type": "object", | |
31 | "required": ["name"], |
|
31 | "required": ["name"], | |
32 | "properties": { |
|
32 | "properties": { | |
33 | "name": { |
|
33 | "name": { | |
34 | "description": "The programming language which this kernel runs.", |
|
34 | "description": "The programming language which this kernel runs.", | |
35 | "type": "string" |
|
35 | "type": "string" | |
36 | }, |
|
36 | }, | |
37 | "codemirror_mode": { |
|
37 | "codemirror_mode": { | |
38 | "description": "The codemirror mode to use for code in this language.", |
|
38 | "description": "The codemirror mode to use for code in this language.", | |
39 | "oneOf": [ |
|
39 | "oneOf": [ | |
40 | {"type": "string"}, |
|
40 | {"type": "string"}, | |
41 | {"type": "object"} |
|
41 | {"type": "object"} | |
42 | ] |
|
42 | ] | |
43 | }, |
|
43 | }, | |
44 | "file_extension": { |
|
44 | "file_extension": { | |
45 | "description": "The file extension for files in this language.", |
|
45 | "description": "The file extension for files in this language.", | |
46 | "type": "string" |
|
46 | "type": "string" | |
47 | }, |
|
47 | }, | |
48 | "mimetype": { |
|
48 | "mimetype": { | |
49 | "description": "The mimetype corresponding to files in this language.", |
|
49 | "description": "The mimetype corresponding to files in this language.", | |
50 | "type": "string" |
|
50 | "type": "string" | |
51 | }, |
|
51 | }, | |
52 | "pygments_lexer": { |
|
52 | "pygments_lexer": { | |
53 | "description": "The pygments lexer to use for code in this language.", |
|
53 | "description": "The pygments lexer to use for code in this language.", | |
54 | "type": "string" |
|
54 | "type": "string" | |
55 | } |
|
55 | } | |
56 | } |
|
56 | } | |
57 | }, |
|
57 | }, | |
58 | "signature": { |
|
|||
59 | "description": "Hash of the notebook.", |
|
|||
60 | "type": "string" |
|
|||
61 | }, |
|
|||
62 | "orig_nbformat": { |
|
58 | "orig_nbformat": { | |
63 | "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.", |
|
59 | "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.", | |
64 | "type": "integer", |
|
60 | "type": "integer", | |
65 | "minimum": 1 |
|
61 | "minimum": 1 | |
66 | } |
|
62 | } | |
67 | } |
|
63 | } | |
68 | }, |
|
64 | }, | |
69 | "nbformat_minor": { |
|
65 | "nbformat_minor": { | |
70 | "description": "Notebook format (minor number). Incremented for backward compatible changes to the notebook format.", |
|
66 | "description": "Notebook format (minor number). Incremented for backward compatible changes to the notebook format.", | |
71 | "type": "integer", |
|
67 | "type": "integer", | |
72 | "minimum": 0 |
|
68 | "minimum": 0 | |
73 | }, |
|
69 | }, | |
74 | "nbformat": { |
|
70 | "nbformat": { | |
75 | "description": "Notebook format (major number). Incremented between backwards incompatible changes to the notebook format.", |
|
71 | "description": "Notebook format (major number). Incremented between backwards incompatible changes to the notebook format.", | |
76 | "type": "integer", |
|
72 | "type": "integer", | |
77 | "minimum": 4, |
|
73 | "minimum": 4, | |
78 | "maximum": 4 |
|
74 | "maximum": 4 | |
79 | }, |
|
75 | }, | |
80 | "cells": { |
|
76 | "cells": { | |
81 | "description": "Array of cells of the current notebook.", |
|
77 | "description": "Array of cells of the current notebook.", | |
82 | "type": "array", |
|
78 | "type": "array", | |
83 | "items": {"$ref": "#/definitions/cell"} |
|
79 | "items": {"$ref": "#/definitions/cell"} | |
84 | } |
|
80 | } | |
85 | }, |
|
81 | }, | |
86 |
|
82 | |||
87 | "definitions": { |
|
83 | "definitions": { | |
88 | "cell": { |
|
84 | "cell": { | |
89 | "type": "object", |
|
85 | "type": "object", | |
90 | "oneOf": [ |
|
86 | "oneOf": [ | |
91 | {"$ref": "#/definitions/raw_cell"}, |
|
87 | {"$ref": "#/definitions/raw_cell"}, | |
92 | {"$ref": "#/definitions/markdown_cell"}, |
|
88 | {"$ref": "#/definitions/markdown_cell"}, | |
93 | {"$ref": "#/definitions/code_cell"} |
|
89 | {"$ref": "#/definitions/code_cell"} | |
94 | ] |
|
90 | ] | |
95 | }, |
|
91 | }, | |
96 |
|
92 | |||
97 | "raw_cell": { |
|
93 | "raw_cell": { | |
98 | "description": "Notebook raw nbconvert cell.", |
|
94 | "description": "Notebook raw nbconvert cell.", | |
99 | "type": "object", |
|
95 | "type": "object", | |
100 | "additionalProperties": false, |
|
96 | "additionalProperties": false, | |
101 | "required": ["cell_type", "metadata", "source"], |
|
97 | "required": ["cell_type", "metadata", "source"], | |
102 | "properties": { |
|
98 | "properties": { | |
103 | "cell_type": { |
|
99 | "cell_type": { | |
104 | "description": "String identifying the type of cell.", |
|
100 | "description": "String identifying the type of cell.", | |
105 | "enum": ["raw"] |
|
101 | "enum": ["raw"] | |
106 | }, |
|
102 | }, | |
107 | "metadata": { |
|
103 | "metadata": { | |
108 | "description": "Cell-level metadata.", |
|
104 | "description": "Cell-level metadata.", | |
109 | "type": "object", |
|
105 | "type": "object", | |
110 | "additionalProperties": true, |
|
106 | "additionalProperties": true, | |
111 | "properties": { |
|
107 | "properties": { | |
112 | "format": { |
|
108 | "format": { | |
113 | "description": "Raw cell metadata format for nbconvert.", |
|
109 | "description": "Raw cell metadata format for nbconvert.", | |
114 | "type": "string" |
|
110 | "type": "string" | |
115 | }, |
|
111 | }, | |
116 | "name": {"$ref": "#/definitions/misc/metadata_name"}, |
|
112 | "name": {"$ref": "#/definitions/misc/metadata_name"}, | |
117 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} |
|
113 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} | |
118 | } |
|
114 | } | |
119 | }, |
|
115 | }, | |
120 | "source": {"$ref": "#/definitions/misc/source"} |
|
116 | "source": {"$ref": "#/definitions/misc/source"} | |
121 | } |
|
117 | } | |
122 | }, |
|
118 | }, | |
123 |
|
119 | |||
124 | "markdown_cell": { |
|
120 | "markdown_cell": { | |
125 | "description": "Notebook markdown cell.", |
|
121 | "description": "Notebook markdown cell.", | |
126 | "type": "object", |
|
122 | "type": "object", | |
127 | "additionalProperties": false, |
|
123 | "additionalProperties": false, | |
128 | "required": ["cell_type", "metadata", "source"], |
|
124 | "required": ["cell_type", "metadata", "source"], | |
129 | "properties": { |
|
125 | "properties": { | |
130 | "cell_type": { |
|
126 | "cell_type": { | |
131 | "description": "String identifying the type of cell.", |
|
127 | "description": "String identifying the type of cell.", | |
132 | "enum": ["markdown"] |
|
128 | "enum": ["markdown"] | |
133 | }, |
|
129 | }, | |
134 | "metadata": { |
|
130 | "metadata": { | |
135 | "description": "Cell-level metadata.", |
|
131 | "description": "Cell-level metadata.", | |
136 | "type": "object", |
|
132 | "type": "object", | |
137 | "properties": { |
|
133 | "properties": { | |
138 | "name": {"$ref": "#/definitions/misc/metadata_name"}, |
|
134 | "name": {"$ref": "#/definitions/misc/metadata_name"}, | |
139 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} |
|
135 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} | |
140 | }, |
|
136 | }, | |
141 | "additionalProperties": true |
|
137 | "additionalProperties": true | |
142 | }, |
|
138 | }, | |
143 | "source": {"$ref": "#/definitions/misc/source"} |
|
139 | "source": {"$ref": "#/definitions/misc/source"} | |
144 | } |
|
140 | } | |
145 | }, |
|
141 | }, | |
146 |
|
142 | |||
147 | "code_cell": { |
|
143 | "code_cell": { | |
148 | "description": "Notebook code cell.", |
|
144 | "description": "Notebook code cell.", | |
149 | "type": "object", |
|
145 | "type": "object", | |
150 | "additionalProperties": false, |
|
146 | "additionalProperties": false, | |
151 | "required": ["cell_type", "metadata", "source", "outputs", "execution_count"], |
|
147 | "required": ["cell_type", "metadata", "source", "outputs", "execution_count"], | |
152 | "properties": { |
|
148 | "properties": { | |
153 | "cell_type": { |
|
149 | "cell_type": { | |
154 | "description": "String identifying the type of cell.", |
|
150 | "description": "String identifying the type of cell.", | |
155 | "enum": ["code"] |
|
151 | "enum": ["code"] | |
156 | }, |
|
152 | }, | |
157 | "metadata": { |
|
153 | "metadata": { | |
158 | "description": "Cell-level metadata.", |
|
154 | "description": "Cell-level metadata.", | |
159 | "type": "object", |
|
155 | "type": "object", | |
160 | "additionalProperties": true, |
|
156 | "additionalProperties": true, | |
161 | "properties": { |
|
157 | "properties": { | |
162 | "collapsed": { |
|
158 | "collapsed": { | |
163 | "description": "Whether the cell is collapsed/expanded.", |
|
159 | "description": "Whether the cell is collapsed/expanded.", | |
164 | "type": "boolean" |
|
160 | "type": "boolean" | |
165 | }, |
|
161 | }, | |
166 | "autoscroll": { |
|
162 | "autoscroll": { | |
167 | "description": "Whether the cell's output is scrolled, unscrolled, or autoscrolled.", |
|
163 | "description": "Whether the cell's output is scrolled, unscrolled, or autoscrolled.", | |
168 | "enum": [true, false, "auto"] |
|
164 | "enum": [true, false, "auto"] | |
169 | }, |
|
165 | }, | |
170 | "name": {"$ref": "#/definitions/misc/metadata_name"}, |
|
166 | "name": {"$ref": "#/definitions/misc/metadata_name"}, | |
171 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} |
|
167 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} | |
172 | } |
|
168 | } | |
173 | }, |
|
169 | }, | |
174 | "source": {"$ref": "#/definitions/misc/source"}, |
|
170 | "source": {"$ref": "#/definitions/misc/source"}, | |
175 | "outputs": { |
|
171 | "outputs": { | |
176 | "description": "Execution, display, or stream outputs.", |
|
172 | "description": "Execution, display, or stream outputs.", | |
177 | "type": "array", |
|
173 | "type": "array", | |
178 | "items": {"$ref": "#/definitions/output"} |
|
174 | "items": {"$ref": "#/definitions/output"} | |
179 | }, |
|
175 | }, | |
180 | "execution_count": { |
|
176 | "execution_count": { | |
181 | "description": "The code cell's prompt number. Will be null if the cell has not been run.", |
|
177 | "description": "The code cell's prompt number. Will be null if the cell has not been run.", | |
182 | "type": ["integer", "null"], |
|
178 | "type": ["integer", "null"], | |
183 | "minimum": 0 |
|
179 | "minimum": 0 | |
184 | } |
|
180 | } | |
185 | } |
|
181 | } | |
186 | }, |
|
182 | }, | |
187 |
|
183 | |||
188 | "unrecognized_cell": { |
|
184 | "unrecognized_cell": { | |
189 | "description": "Unrecognized cell from a future minor-revision to the notebook format.", |
|
185 | "description": "Unrecognized cell from a future minor-revision to the notebook format.", | |
190 | "type": "object", |
|
186 | "type": "object", | |
191 | "additionalProperties": true, |
|
187 | "additionalProperties": true, | |
192 | "required": ["cell_type", "metadata"], |
|
188 | "required": ["cell_type", "metadata"], | |
193 | "properties": { |
|
189 | "properties": { | |
194 | "cell_type": { |
|
190 | "cell_type": { | |
195 | "description": "String identifying the type of cell.", |
|
191 | "description": "String identifying the type of cell.", | |
196 | "not" : { |
|
192 | "not" : { | |
197 | "enum": ["markdown", "code", "raw"] |
|
193 | "enum": ["markdown", "code", "raw"] | |
198 | } |
|
194 | } | |
199 | }, |
|
195 | }, | |
200 | "metadata": { |
|
196 | "metadata": { | |
201 | "description": "Cell-level metadata.", |
|
197 | "description": "Cell-level metadata.", | |
202 | "type": "object", |
|
198 | "type": "object", | |
203 | "properties": { |
|
199 | "properties": { | |
204 | "name": {"$ref": "#/definitions/misc/metadata_name"}, |
|
200 | "name": {"$ref": "#/definitions/misc/metadata_name"}, | |
205 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} |
|
201 | "tags": {"$ref": "#/definitions/misc/metadata_tags"} | |
206 | }, |
|
202 | }, | |
207 | "additionalProperties": true |
|
203 | "additionalProperties": true | |
208 | } |
|
204 | } | |
209 | } |
|
205 | } | |
210 | }, |
|
206 | }, | |
211 |
|
207 | |||
212 | "output": { |
|
208 | "output": { | |
213 | "type": "object", |
|
209 | "type": "object", | |
214 | "oneOf": [ |
|
210 | "oneOf": [ | |
215 | {"$ref": "#/definitions/execute_result"}, |
|
211 | {"$ref": "#/definitions/execute_result"}, | |
216 | {"$ref": "#/definitions/display_data"}, |
|
212 | {"$ref": "#/definitions/display_data"}, | |
217 | {"$ref": "#/definitions/stream"}, |
|
213 | {"$ref": "#/definitions/stream"}, | |
218 | {"$ref": "#/definitions/error"} |
|
214 | {"$ref": "#/definitions/error"} | |
219 | ] |
|
215 | ] | |
220 | }, |
|
216 | }, | |
221 |
|
217 | |||
222 | "execute_result": { |
|
218 | "execute_result": { | |
223 | "description": "Result of executing a code cell.", |
|
219 | "description": "Result of executing a code cell.", | |
224 | "type": "object", |
|
220 | "type": "object", | |
225 | "additionalProperties": false, |
|
221 | "additionalProperties": false, | |
226 | "required": ["output_type", "data", "metadata", "execution_count"], |
|
222 | "required": ["output_type", "data", "metadata", "execution_count"], | |
227 | "properties": { |
|
223 | "properties": { | |
228 | "output_type": { |
|
224 | "output_type": { | |
229 | "description": "Type of cell output.", |
|
225 | "description": "Type of cell output.", | |
230 | "enum": ["execute_result"] |
|
226 | "enum": ["execute_result"] | |
231 | }, |
|
227 | }, | |
232 | "execution_count": { |
|
228 | "execution_count": { | |
233 | "description": "A result's prompt number.", |
|
229 | "description": "A result's prompt number.", | |
234 | "type": ["integer", "null"], |
|
230 | "type": ["integer", "null"], | |
235 | "minimum": 0 |
|
231 | "minimum": 0 | |
236 | }, |
|
232 | }, | |
237 | "data": {"$ref": "#/definitions/misc/mimebundle"}, |
|
233 | "data": {"$ref": "#/definitions/misc/mimebundle"}, | |
238 | "metadata": {"$ref": "#/definitions/misc/output_metadata"} |
|
234 | "metadata": {"$ref": "#/definitions/misc/output_metadata"} | |
239 | } |
|
235 | } | |
240 | }, |
|
236 | }, | |
241 |
|
237 | |||
242 | "display_data": { |
|
238 | "display_data": { | |
243 | "description": "Data displayed as a result of code cell execution.", |
|
239 | "description": "Data displayed as a result of code cell execution.", | |
244 | "type": "object", |
|
240 | "type": "object", | |
245 | "additionalProperties": false, |
|
241 | "additionalProperties": false, | |
246 | "required": ["output_type", "data", "metadata"], |
|
242 | "required": ["output_type", "data", "metadata"], | |
247 | "properties": { |
|
243 | "properties": { | |
248 | "output_type": { |
|
244 | "output_type": { | |
249 | "description": "Type of cell output.", |
|
245 | "description": "Type of cell output.", | |
250 | "enum": ["display_data"] |
|
246 | "enum": ["display_data"] | |
251 | }, |
|
247 | }, | |
252 | "data": {"$ref": "#/definitions/misc/mimebundle"}, |
|
248 | "data": {"$ref": "#/definitions/misc/mimebundle"}, | |
253 | "metadata": {"$ref": "#/definitions/misc/output_metadata"} |
|
249 | "metadata": {"$ref": "#/definitions/misc/output_metadata"} | |
254 | } |
|
250 | } | |
255 | }, |
|
251 | }, | |
256 |
|
252 | |||
257 | "stream": { |
|
253 | "stream": { | |
258 | "description": "Stream output from a code cell.", |
|
254 | "description": "Stream output from a code cell.", | |
259 | "type": "object", |
|
255 | "type": "object", | |
260 | "additionalProperties": false, |
|
256 | "additionalProperties": false, | |
261 | "required": ["output_type", "name", "text"], |
|
257 | "required": ["output_type", "name", "text"], | |
262 | "properties": { |
|
258 | "properties": { | |
263 | "output_type": { |
|
259 | "output_type": { | |
264 | "description": "Type of cell output.", |
|
260 | "description": "Type of cell output.", | |
265 | "enum": ["stream"] |
|
261 | "enum": ["stream"] | |
266 | }, |
|
262 | }, | |
267 | "name": { |
|
263 | "name": { | |
268 | "description": "The name of the stream (stdout, stderr).", |
|
264 | "description": "The name of the stream (stdout, stderr).", | |
269 | "type": "string" |
|
265 | "type": "string" | |
270 | }, |
|
266 | }, | |
271 | "text": { |
|
267 | "text": { | |
272 | "description": "The stream's text output, represented as an array of strings.", |
|
268 | "description": "The stream's text output, represented as an array of strings.", | |
273 | "$ref": "#/definitions/misc/multiline_string" |
|
269 | "$ref": "#/definitions/misc/multiline_string" | |
274 | } |
|
270 | } | |
275 | } |
|
271 | } | |
276 | }, |
|
272 | }, | |
277 |
|
273 | |||
278 | "error": { |
|
274 | "error": { | |
279 | "description": "Output of an error that occurred during code cell execution.", |
|
275 | "description": "Output of an error that occurred during code cell execution.", | |
280 | "type": "object", |
|
276 | "type": "object", | |
281 | "additionalProperties": false, |
|
277 | "additionalProperties": false, | |
282 | "required": ["output_type", "ename", "evalue", "traceback"], |
|
278 | "required": ["output_type", "ename", "evalue", "traceback"], | |
283 | "properties": { |
|
279 | "properties": { | |
284 | "output_type": { |
|
280 | "output_type": { | |
285 | "description": "Type of cell output.", |
|
281 | "description": "Type of cell output.", | |
286 | "enum": ["error"] |
|
282 | "enum": ["error"] | |
287 | }, |
|
283 | }, | |
288 | "ename": { |
|
284 | "ename": { | |
289 | "description": "The name of the error.", |
|
285 | "description": "The name of the error.", | |
290 | "type": "string" |
|
286 | "type": "string" | |
291 | }, |
|
287 | }, | |
292 | "evalue": { |
|
288 | "evalue": { | |
293 | "description": "The value, or message, of the error.", |
|
289 | "description": "The value, or message, of the error.", | |
294 | "type": "string" |
|
290 | "type": "string" | |
295 | }, |
|
291 | }, | |
296 | "traceback": { |
|
292 | "traceback": { | |
297 | "description": "The error's traceback, represented as an array of strings.", |
|
293 | "description": "The error's traceback, represented as an array of strings.", | |
298 | "type": "array", |
|
294 | "type": "array", | |
299 | "items": {"type": "string"} |
|
295 | "items": {"type": "string"} | |
300 | } |
|
296 | } | |
301 | } |
|
297 | } | |
302 | }, |
|
298 | }, | |
303 |
|
299 | |||
304 | "unrecognized_output": { |
|
300 | "unrecognized_output": { | |
305 | "description": "Unrecognized output from a future minor-revision to the notebook format.", |
|
301 | "description": "Unrecognized output from a future minor-revision to the notebook format.", | |
306 | "type": "object", |
|
302 | "type": "object", | |
307 | "additionalProperties": true, |
|
303 | "additionalProperties": true, | |
308 | "required": ["output_type"], |
|
304 | "required": ["output_type"], | |
309 | "properties": { |
|
305 | "properties": { | |
310 | "output_type": { |
|
306 | "output_type": { | |
311 | "description": "Type of cell output.", |
|
307 | "description": "Type of cell output.", | |
312 | "not": { |
|
308 | "not": { | |
313 | "enum": ["execute_result", "display_data", "stream", "error"] |
|
309 | "enum": ["execute_result", "display_data", "stream", "error"] | |
314 | } |
|
310 | } | |
315 | } |
|
311 | } | |
316 | } |
|
312 | } | |
317 | }, |
|
313 | }, | |
318 |
|
314 | |||
319 | "misc": { |
|
315 | "misc": { | |
320 | "metadata_name": { |
|
316 | "metadata_name": { | |
321 | "description": "The cell's name. If present, must be a non-empty string.", |
|
317 | "description": "The cell's name. If present, must be a non-empty string.", | |
322 | "type": "string", |
|
318 | "type": "string", | |
323 | "pattern": "^.+$" |
|
319 | "pattern": "^.+$" | |
324 | }, |
|
320 | }, | |
325 | "metadata_tags": { |
|
321 | "metadata_tags": { | |
326 | "description": "The cell's tags. Tags must be unique, and must not contain commas.", |
|
322 | "description": "The cell's tags. Tags must be unique, and must not contain commas.", | |
327 | "type": "array", |
|
323 | "type": "array", | |
328 | "uniqueItems": true, |
|
324 | "uniqueItems": true, | |
329 | "items": { |
|
325 | "items": { | |
330 | "type": "string", |
|
326 | "type": "string", | |
331 | "pattern": "^[^,]+$" |
|
327 | "pattern": "^[^,]+$" | |
332 | } |
|
328 | } | |
333 | }, |
|
329 | }, | |
334 | "source": { |
|
330 | "source": { | |
335 | "description": "Contents of the cell, represented as an array of lines.", |
|
331 | "description": "Contents of the cell, represented as an array of lines.", | |
336 | "$ref": "#/definitions/misc/multiline_string" |
|
332 | "$ref": "#/definitions/misc/multiline_string" | |
337 | }, |
|
333 | }, | |
338 | "execution_count": { |
|
334 | "execution_count": { | |
339 | "description": "The code cell's prompt number. Will be null if the cell has not been run.", |
|
335 | "description": "The code cell's prompt number. Will be null if the cell has not been run.", | |
340 | "type": ["integer", "null"], |
|
336 | "type": ["integer", "null"], | |
341 | "minimum": 0 |
|
337 | "minimum": 0 | |
342 | }, |
|
338 | }, | |
343 | "mimebundle": { |
|
339 | "mimebundle": { | |
344 | "description": "A mime-type keyed dictionary of data", |
|
340 | "description": "A mime-type keyed dictionary of data", | |
345 | "type": "object", |
|
341 | "type": "object", | |
346 | "additionalProperties": false, |
|
342 | "additionalProperties": false, | |
347 | "properties": { |
|
343 | "properties": { | |
348 | "application/json": { |
|
344 | "application/json": { | |
349 | "type": "object" |
|
345 | "type": "object" | |
350 | } |
|
346 | } | |
351 | }, |
|
347 | }, | |
352 | "patternProperties": { |
|
348 | "patternProperties": { | |
353 | "^(?!application/json$)[a-zA-Z0-9]+/[a-zA-Z0-9\\-\\+\\.]+$": { |
|
349 | "^(?!application/json$)[a-zA-Z0-9]+/[a-zA-Z0-9\\-\\+\\.]+$": { | |
354 | "description": "mimetype output (e.g. text/plain), represented as either an array of strings or a string.", |
|
350 | "description": "mimetype output (e.g. text/plain), represented as either an array of strings or a string.", | |
355 | "$ref": "#/definitions/misc/multiline_string" |
|
351 | "$ref": "#/definitions/misc/multiline_string" | |
356 | } |
|
352 | } | |
357 | } |
|
353 | } | |
358 | }, |
|
354 | }, | |
359 | "output_metadata": { |
|
355 | "output_metadata": { | |
360 | "description": "Cell output metadata.", |
|
356 | "description": "Cell output metadata.", | |
361 | "type": "object", |
|
357 | "type": "object", | |
362 | "additionalProperties": true |
|
358 | "additionalProperties": true | |
363 | }, |
|
359 | }, | |
364 | "multiline_string": { |
|
360 | "multiline_string": { | |
365 | "oneOf" : [ |
|
361 | "oneOf" : [ | |
366 | {"type": "string"}, |
|
362 | {"type": "string"}, | |
367 | { |
|
363 | { | |
368 | "type": "array", |
|
364 | "type": "array", | |
369 | "items": {"type": "string"} |
|
365 | "items": {"type": "string"} | |
370 | } |
|
366 | } | |
371 | ] |
|
367 | ] | |
372 | } |
|
368 | } | |
373 | } |
|
369 | } | |
374 | } |
|
370 | } | |
375 | } |
|
371 | } |
@@ -1,95 +1,96 b'' | |||||
1 | """Base classes and utilities for readers and writers.""" |
|
1 | """Base classes and utilities for readers and writers.""" | |
2 |
|
2 | |||
3 | # Copyright (c) IPython Development Team. |
|
3 | # Copyright (c) IPython Development Team. | |
4 | # Distributed under the terms of the Modified BSD License. |
|
4 | # Distributed under the terms of the Modified BSD License. | |
5 |
|
5 | |||
6 | from IPython.utils.py3compat import string_types, cast_unicode_py2 |
|
6 | from IPython.utils.py3compat import string_types, cast_unicode_py2 | |
7 |
|
7 | |||
8 |
|
8 | |||
9 | def rejoin_lines(nb): |
|
9 | def rejoin_lines(nb): | |
10 | """rejoin multiline text into strings |
|
10 | """rejoin multiline text into strings | |
11 |
|
11 | |||
12 | For reversing effects of ``split_lines(nb)``. |
|
12 | For reversing effects of ``split_lines(nb)``. | |
13 |
|
13 | |||
14 | This only rejoins lines that have been split, so if text objects were not split |
|
14 | This only rejoins lines that have been split, so if text objects were not split | |
15 | they will pass through unchanged. |
|
15 | they will pass through unchanged. | |
16 |
|
16 | |||
17 | Used when reading JSON files that may have been passed through split_lines. |
|
17 | Used when reading JSON files that may have been passed through split_lines. | |
18 | """ |
|
18 | """ | |
19 | for cell in nb.cells: |
|
19 | for cell in nb.cells: | |
20 | if 'source' in cell and isinstance(cell.source, list): |
|
20 | if 'source' in cell and isinstance(cell.source, list): | |
21 | cell.source = ''.join(cell.source) |
|
21 | cell.source = ''.join(cell.source) | |
22 | if cell.get('cell_type', None) == 'code': |
|
22 | if cell.get('cell_type', None) == 'code': | |
23 | for output in cell.get('outputs', []): |
|
23 | for output in cell.get('outputs', []): | |
24 | output_type = output.get('output_type', '') |
|
24 | output_type = output.get('output_type', '') | |
25 | if output_type in {'execute_result', 'display_data'}: |
|
25 | if output_type in {'execute_result', 'display_data'}: | |
26 | for key, value in output.get('data', {}).items(): |
|
26 | for key, value in output.get('data', {}).items(): | |
27 | if key != 'application/json' and isinstance(value, list): |
|
27 | if key != 'application/json' and isinstance(value, list): | |
28 | output.data[key] = ''.join(value) |
|
28 | output.data[key] = ''.join(value) | |
29 | elif output_type: |
|
29 | elif output_type: | |
30 | if isinstance(output.get('text', ''), list): |
|
30 | if isinstance(output.get('text', ''), list): | |
31 | output.text = ''.join(output.text) |
|
31 | output.text = ''.join(output.text) | |
32 | return nb |
|
32 | return nb | |
33 |
|
33 | |||
34 |
|
34 | |||
35 | def split_lines(nb): |
|
35 | def split_lines(nb): | |
36 | """split likely multiline text into lists of strings |
|
36 | """split likely multiline text into lists of strings | |
37 |
|
37 | |||
38 | For file output more friendly to line-based VCS. ``rejoin_lines(nb)`` will |
|
38 | For file output more friendly to line-based VCS. ``rejoin_lines(nb)`` will | |
39 | reverse the effects of ``split_lines(nb)``. |
|
39 | reverse the effects of ``split_lines(nb)``. | |
40 |
|
40 | |||
41 | Used when writing JSON files. |
|
41 | Used when writing JSON files. | |
42 | """ |
|
42 | """ | |
43 | for cell in nb.cells: |
|
43 | for cell in nb.cells: | |
44 | source = cell.get('source', None) |
|
44 | source = cell.get('source', None) | |
45 | if isinstance(source, string_types): |
|
45 | if isinstance(source, string_types): | |
46 | cell['source'] = source.splitlines(True) |
|
46 | cell['source'] = source.splitlines(True) | |
47 |
|
47 | |||
48 | if cell.cell_type == 'code': |
|
48 | if cell.cell_type == 'code': | |
49 | for output in cell.outputs: |
|
49 | for output in cell.outputs: | |
50 | if output.output_type in {'execute_result', 'display_data'}: |
|
50 | if output.output_type in {'execute_result', 'display_data'}: | |
51 | for key, value in output.data.items(): |
|
51 | for key, value in output.data.items(): | |
52 | if key != 'application/json' and isinstance(value, string_types): |
|
52 | if key != 'application/json' and isinstance(value, string_types): | |
53 | output.data[key] = value.splitlines(True) |
|
53 | output.data[key] = value.splitlines(True) | |
54 | elif output.output_type == 'stream': |
|
54 | elif output.output_type == 'stream': | |
55 | if isinstance(output.text, string_types): |
|
55 | if isinstance(output.text, string_types): | |
56 | output.text = output.text.splitlines(True) |
|
56 | output.text = output.text.splitlines(True) | |
57 | return nb |
|
57 | return nb | |
58 |
|
58 | |||
59 |
|
59 | |||
60 | def strip_transient(nb): |
|
60 | def strip_transient(nb): | |
61 | """Strip transient values that shouldn't be stored in files. |
|
61 | """Strip transient values that shouldn't be stored in files. | |
62 |
|
62 | |||
63 | This should be called in *both* read and write. |
|
63 | This should be called in *both* read and write. | |
64 | """ |
|
64 | """ | |
65 | nb.metadata.pop('orig_nbformat', None) |
|
65 | nb.metadata.pop('orig_nbformat', None) | |
66 | nb.metadata.pop('orig_nbformat_minor', None) |
|
66 | nb.metadata.pop('orig_nbformat_minor', None) | |
|
67 | nb.metadata.pop('signature', None) | |||
67 | for cell in nb.cells: |
|
68 | for cell in nb.cells: | |
68 | cell.metadata.pop('trusted', None) |
|
69 | cell.metadata.pop('trusted', None) | |
69 | return nb |
|
70 | return nb | |
70 |
|
71 | |||
71 |
|
72 | |||
72 | class NotebookReader(object): |
|
73 | class NotebookReader(object): | |
73 | """A class for reading notebooks.""" |
|
74 | """A class for reading notebooks.""" | |
74 |
|
75 | |||
75 | def reads(self, s, **kwargs): |
|
76 | def reads(self, s, **kwargs): | |
76 | """Read a notebook from a string.""" |
|
77 | """Read a notebook from a string.""" | |
77 | raise NotImplementedError("loads must be implemented in a subclass") |
|
78 | raise NotImplementedError("loads must be implemented in a subclass") | |
78 |
|
79 | |||
79 | def read(self, fp, **kwargs): |
|
80 | def read(self, fp, **kwargs): | |
80 | """Read a notebook from a file like object""" |
|
81 | """Read a notebook from a file like object""" | |
81 | nbs = cast_unicode_py2(fp.read()) |
|
82 | nbs = cast_unicode_py2(fp.read()) | |
82 | return self.reads(nbs, **kwargs) |
|
83 | return self.reads(nbs, **kwargs) | |
83 |
|
84 | |||
84 |
|
85 | |||
85 | class NotebookWriter(object): |
|
86 | class NotebookWriter(object): | |
86 | """A class for writing notebooks.""" |
|
87 | """A class for writing notebooks.""" | |
87 |
|
88 | |||
88 | def writes(self, nb, **kwargs): |
|
89 | def writes(self, nb, **kwargs): | |
89 | """Write a notebook to a string.""" |
|
90 | """Write a notebook to a string.""" | |
90 | raise NotImplementedError("loads must be implemented in a subclass") |
|
91 | raise NotImplementedError("loads must be implemented in a subclass") | |
91 |
|
92 | |||
92 | def write(self, nb, fp, **kwargs): |
|
93 | def write(self, nb, fp, **kwargs): | |
93 | """Write a notebook to a file like object""" |
|
94 | """Write a notebook to a file like object""" | |
94 | nbs = cast_unicode_py2(self.writes(nb, **kwargs)) |
|
95 | nbs = cast_unicode_py2(self.writes(nb, **kwargs)) | |
95 | return fp.write(nbs) |
|
96 | return fp.write(nbs) |
General Comments 0
You need to be logged in to leave comments.
Login now