Show More
@@ -3,6 +3,7 b'' | |||
|
3 | 3 | from __future__ import print_function |
|
4 | 4 | |
|
5 | 5 | import os |
|
6 | import time | |
|
6 | 7 | |
|
7 | 8 | from tornado.web import HTTPError |
|
8 | 9 | from unittest import TestCase |
@@ -156,6 +157,7 b' class TestContentsManager(TestCase):' | |||
|
156 | 157 | |
|
157 | 158 | full_model = cm.get(path) |
|
158 | 159 | nb = full_model['content'] |
|
160 | nb['metadata']['counter'] = int(1e6 * time.time()) | |
|
159 | 161 | self.add_code_cell(nb) |
|
160 | 162 | |
|
161 | 163 | cm.save(full_model, path) |
@@ -1,18 +1,27 b'' | |||
|
1 |
""" |
|
|
1 | """Utilities 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 | from datetime import datetime | |
|
8 | 9 | import hashlib |
|
9 | 10 | from hmac import HMAC |
|
10 | 11 | import io |
|
11 | 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 | 22 | from IPython.utils.io import atomic_writing |
|
14 |
from IPython.utils.py3compat import |
|
|
15 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool | |
|
23 | from IPython.utils.py3compat import unicode_type, cast_bytes | |
|
24 | from IPython.utils.traitlets import Instance, Bytes, Enum, Any, Unicode, Bool, Integer | |
|
16 | 25 | from IPython.config import LoggingConfigurable, MultipleInstanceError |
|
17 | 26 | from IPython.core.application import BaseIPythonApplication, base_flags |
|
18 | 27 | |
@@ -93,6 +102,48 b' class NotebookNotary(LoggingConfigurable):' | |||
|
93 | 102 | app.initialize(argv=[]) |
|
94 | 103 | return app.profile_dir |
|
95 | 104 | |
|
105 | db_file = Unicode(config=True, | |
|
106 | help="""The sqlite file in which to store notebook signatures. | |
|
107 | By default, this will be in your IPython profile. | |
|
108 | You can set it to ':memory:' to disable sqlite writing to the filesystem. | |
|
109 | """) | |
|
110 | def _db_file_default(self): | |
|
111 | if self.profile_dir is None: | |
|
112 | return ':memory:' | |
|
113 | return os.path.join(self.profile_dir.security_dir, u'nbsignatures.db') | |
|
114 | ||
|
115 | # 64k entries ~ 12MB | |
|
116 | cache_size = Integer(65535, config=True, | |
|
117 | help="""The number of notebook signatures to cache. | |
|
118 | When the number of signatures exceeds this value, | |
|
119 | the oldest 25% of signatures will be culled. | |
|
120 | """ | |
|
121 | ) | |
|
122 | db = Any() | |
|
123 | def _db_default(self): | |
|
124 | if sqlite3 is None: | |
|
125 | self.log.warn("Missing SQLite3, all notebooks will be untrusted!") | |
|
126 | return | |
|
127 | kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) | |
|
128 | db = sqlite3.connect(self.db_file, **kwargs) | |
|
129 | self.init_db(db) | |
|
130 | return db | |
|
131 | ||
|
132 | def init_db(self, db): | |
|
133 | db.execute(""" | |
|
134 | CREATE TABLE IF NOT EXISTS nbsignatures | |
|
135 | ( | |
|
136 | id integer PRIMARY KEY AUTOINCREMENT, | |
|
137 | algorithm text, | |
|
138 | signature text, | |
|
139 | path text, | |
|
140 | last_seen timestamp | |
|
141 | )""") | |
|
142 | db.execute(""" | |
|
143 | CREATE INDEX IF NOT EXISTS algosig ON nbsignatures(algorithm, signature) | |
|
144 | """) | |
|
145 | db.commit() | |
|
146 | ||
|
96 | 147 | algorithm = Enum(algorithms, default_value='sha256', config=True, |
|
97 | 148 | help="""The hashing algorithm used to sign notebooks.""" |
|
98 | 149 | ) |
@@ -168,28 +219,72 b' class NotebookNotary(LoggingConfigurable):' | |||
|
168 | 219 | """ |
|
169 | 220 | if nb.nbformat < 3: |
|
170 | 221 | return False |
|
171 | stored_signature = nb['metadata'].get('signature', None) | |
|
172 | if not stored_signature \ | |
|
173 | or not isinstance(stored_signature, string_types) \ | |
|
174 | or ':' not in stored_signature: | |
|
222 | if self.db is None: | |
|
175 | 223 | return False |
|
176 | stored_algo, sig = stored_signature.split(':', 1) | |
|
177 | if self.algorithm != stored_algo: | |
|
224 | signature = self.compute_signature(nb) | |
|
225 | r = self.db.execute("""SELECT id FROM nbsignatures WHERE | |
|
226 | algorithm = ? AND | |
|
227 | signature = ?; | |
|
228 | """, (self.algorithm, signature)).fetchone() | |
|
229 | if r is None: | |
|
178 | 230 | return False |
|
179 | my_signature = self.compute_signature(nb) | |
|
180 | return my_signature == sig | |
|
231 | self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE | |
|
232 | algorithm = ? AND | |
|
233 | signature = ?; | |
|
234 | """, | |
|
235 | (datetime.utcnow(), self.algorithm, signature), | |
|
236 | ) | |
|
237 | self.db.commit() | |
|
238 | return True | |
|
181 | 239 | |
|
182 | 240 | def sign(self, nb): |
|
183 | """Sign a notebook, indicating that its output is trusted | |
|
184 | ||
|
185 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature | |
|
241 | """Sign a notebook, indicating that its output is trusted on this machine | |
|
186 | 242 | |
|
187 | e.g. 'sha256:deadbeef123...' | |
|
243 | Stores hash algorithm and hmac digest in a local database of trusted notebooks. | |
|
188 | 244 | """ |
|
189 | 245 | if nb.nbformat < 3: |
|
190 | 246 | return |
|
191 | 247 | signature = self.compute_signature(nb) |
|
192 | nb['metadata']['signature'] = "%s:%s" % (self.algorithm, signature) | |
|
248 | self.store_signature(signature, nb) | |
|
249 | ||
|
250 | def store_signature(self, signature, nb): | |
|
251 | if self.db is None: | |
|
252 | return | |
|
253 | self.db.execute("""INSERT OR IGNORE INTO nbsignatures | |
|
254 | (algorithm, signature, last_seen) VALUES (?, ?, ?)""", | |
|
255 | (self.algorithm, signature, datetime.utcnow()) | |
|
256 | ) | |
|
257 | self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE | |
|
258 | algorithm = ? AND | |
|
259 | signature = ?; | |
|
260 | """, | |
|
261 | (datetime.utcnow(), self.algorithm, signature), | |
|
262 | ) | |
|
263 | self.db.commit() | |
|
264 | n, = self.db.execute("SELECT Count(*) FROM nbsignatures").fetchone() | |
|
265 | if n > self.cache_size: | |
|
266 | self.cull_db() | |
|
267 | ||
|
268 | def unsign(self, nb): | |
|
269 | """Ensure that a notebook is untrusted | |
|
270 | ||
|
271 | by removing its signature from the trusted database, if present. | |
|
272 | """ | |
|
273 | signature = self.compute_signature(nb) | |
|
274 | self.db.execute("""DELETE FROM nbsignatures WHERE | |
|
275 | algorithm = ? AND | |
|
276 | signature = ?; | |
|
277 | """, | |
|
278 | (self.algorithm, signature) | |
|
279 | ) | |
|
280 | self.db.commit() | |
|
281 | ||
|
282 | def cull_db(self): | |
|
283 | """Cull oldest 25% of the trusted signatures when the size limit is reached""" | |
|
284 | self.db.execute("""DELETE FROM nbsignatures WHERE id IN ( | |
|
285 | SELECT id FROM nbsignatures ORDER BY last_seen DESC LIMIT -1 OFFSET ? | |
|
286 | ); | |
|
287 | """, (max(int(0.75 * self.cache_size), 1),)) | |
|
193 | 288 | |
|
194 | 289 | def mark_cells(self, nb, trusted): |
|
195 | 290 | """Mark cells as trusted if the notebook's signature can be verified |
@@ -222,11 +317,9 b' class NotebookNotary(LoggingConfigurable):' | |||
|
222 | 317 | |
|
223 | 318 | # explicitly safe output |
|
224 | 319 | if nbformat_version >= 4: |
|
225 | safe = {'text/plain', 'image/png', 'image/jpeg'} | |
|
226 | 320 | unsafe_output_types = ['execute_result', 'display_data'] |
|
227 | 321 | safe_keys = {"output_type", "execution_count", "metadata"} |
|
228 | 322 | else: # v3 |
|
229 | safe = {'text', 'png', 'jpeg'} | |
|
230 | 323 | unsafe_output_types = ['pyout', 'display_data'] |
|
231 | 324 | safe_keys = {"output_type", "prompt_number", "metadata"} |
|
232 | 325 | |
@@ -261,7 +354,7 b' class NotebookNotary(LoggingConfigurable):' | |||
|
261 | 354 | trust_flags = { |
|
262 | 355 | 'reset' : ( |
|
263 | 356 | {'TrustNotebookApp' : { 'reset' : True}}, |
|
264 |
""" |
|
|
357 | """Delete the trusted notebook cache. | |
|
265 | 358 | All previously signed notebooks will become untrusted. |
|
266 | 359 | """ |
|
267 | 360 | ), |
@@ -290,7 +383,7 b' class TrustNotebookApp(BaseIPythonApplication):' | |||
|
290 | 383 | flags = trust_flags |
|
291 | 384 | |
|
292 | 385 | reset = Bool(False, config=True, |
|
293 |
help="""If True, |
|
|
386 | help="""If True, delete the trusted signature cache. | |
|
294 | 387 | After reset, all previously signed notebooks will become untrusted. |
|
295 | 388 | """ |
|
296 | 389 | ) |
@@ -320,6 +413,9 b' class TrustNotebookApp(BaseIPythonApplication):' | |||
|
320 | 413 | |
|
321 | 414 | def start(self): |
|
322 | 415 | if self.reset: |
|
416 | if os.path.exists(self.notary.db_file): | |
|
417 | print("Removing trusted signature cache: %s" % self.notary.db_file) | |
|
418 | os.remove(self.notary.db_file) | |
|
323 | 419 | self.generate_new_key() |
|
324 | 420 | return |
|
325 | 421 | if not self.extra_args: |
@@ -3,6 +3,9 b'' | |||
|
3 | 3 | # Copyright (c) IPython Development Team. |
|
4 | 4 | # Distributed under the terms of the Modified BSD License. |
|
5 | 5 | |
|
6 | import copy | |
|
7 | import time | |
|
8 | ||
|
6 | 9 | from .base import TestsBase |
|
7 | 10 | |
|
8 | 11 | from IPython.nbformat import read, sign |
@@ -13,8 +16,9 b' class TestNotary(TestsBase):' | |||
|
13 | 16 | |
|
14 | 17 | def setUp(self): |
|
15 | 18 | self.notary = sign.NotebookNotary( |
|
19 | db_file=':memory:', | |
|
16 | 20 | secret=b'secret', |
|
17 | profile_dir=get_ipython().profile_dir | |
|
21 | profile_dir=get_ipython().profile_dir, | |
|
18 | 22 | ) |
|
19 | 23 | with self.fopen(u'test3.ipynb', u'r') as f: |
|
20 | 24 | self.nb = read(f, as_version=4) |
@@ -25,10 +29,7 b' class TestNotary(TestsBase):' | |||
|
25 | 29 | last_sig = '' |
|
26 | 30 | for algo in sign.algorithms: |
|
27 | 31 | self.notary.algorithm = algo |
|
28 | self.notary.sign(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 | sig = self.notary.compute_signature(self.nb) | |
|
32 | 33 | self.assertNotEqual(last_sig, sig) |
|
33 | 34 | last_sig = sig |
|
34 | 35 | |
@@ -46,9 +47,49 b' class TestNotary(TestsBase):' | |||
|
46 | 47 | self.assertNotEqual(sig1, sig2) |
|
47 | 48 | |
|
48 | 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 | 55 | self.notary.sign(self.nb) |
|
50 | sig = self.nb.metadata.signature | |
|
51 | self.assertEqual(sig[:len(self.notary.algorithm)+1], '%s:' % self.notary.algorithm) | |
|
56 | self.assertTrue(self.notary.check_signature(self.nb)) | |
|
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(10) | |
|
68 | ] | |
|
69 | for row in self.notary.db.execute("SELECT * FROM nbsignatures"): | |
|
70 | print(row) | |
|
71 | self.notary.cache_size = 8 | |
|
72 | for i, nb in enumerate(nbs[:8]): | |
|
73 | nb.metadata.dirty = i | |
|
74 | self.notary.sign(nb) | |
|
75 | ||
|
76 | for i, nb in enumerate(nbs[:8]): | |
|
77 | time.sleep(dt) | |
|
78 | self.assertTrue(self.notary.check_signature(nb), 'nb %i is trusted' % i) | |
|
79 | ||
|
80 | # signing the 9th triggers culling of first 3 | |
|
81 | # (75% of 8 = 6, 9 - 6 = 3 culled) | |
|
82 | self.notary.sign(nbs[8]) | |
|
83 | self.assertFalse(self.notary.check_signature(nbs[0])) | |
|
84 | self.assertFalse(self.notary.check_signature(nbs[1])) | |
|
85 | self.assertFalse(self.notary.check_signature(nbs[2])) | |
|
86 | self.assertTrue(self.notary.check_signature(nbs[3])) | |
|
87 | # checking nb3 should keep it from being culled: | |
|
88 | self.notary.sign(nbs[0]) | |
|
89 | self.notary.sign(nbs[1]) | |
|
90 | self.notary.sign(nbs[2]) | |
|
91 | self.assertTrue(self.notary.check_signature(nbs[3])) | |
|
92 | self.assertFalse(self.notary.check_signature(nbs[4])) | |
|
52 | 93 | |
|
53 | 94 | def test_check_signature(self): |
|
54 | 95 | nb = self.nb |
@@ -56,6 +56,7 b' def upgrade(nb, from_version=3, from_minor=0):' | |||
|
56 | 56 | cells.append(upgrade_cell(cell)) |
|
57 | 57 | # upgrade metadata |
|
58 | 58 | nb.metadata.pop('name', '') |
|
59 | nb.metadata.pop('signature', '') | |
|
59 | 60 | # Validate the converted notebook before returning it |
|
60 | 61 | _warn_if_invalid(nb, nbformat) |
|
61 | 62 | return nb |
@@ -55,10 +55,6 b'' | |||
|
55 | 55 | } |
|
56 | 56 | } |
|
57 | 57 | }, |
|
58 | "signature": { | |
|
59 | "description": "Hash of the notebook.", | |
|
60 | "type": "string" | |
|
61 | }, | |
|
62 | 58 | "orig_nbformat": { |
|
63 | 59 | "description": "Original notebook format (major number) before converting the notebook between versions. This should never be written to a file.", |
|
64 | 60 | "type": "integer", |
@@ -64,6 +64,7 b' def strip_transient(nb):' | |||
|
64 | 64 | """ |
|
65 | 65 | nb.metadata.pop('orig_nbformat', None) |
|
66 | 66 | nb.metadata.pop('orig_nbformat_minor', None) |
|
67 | nb.metadata.pop('signature', None) | |
|
67 | 68 | for cell in nb.cells: |
|
68 | 69 | cell.metadata.pop('trusted', None) |
|
69 | 70 | return nb |
@@ -95,9 +95,7 b'' | |||
|
95 | 95 | ] |
|
96 | 96 | } |
|
97 | 97 | ], |
|
98 | "metadata": { | |
|
99 | "signature": "sha256:de8cb1aff3da9097ba3fc7afab4327fc589f2bb1c154feeeb5ed97c87e37486f" | |
|
100 | }, | |
|
98 | "metadata": {}, | |
|
101 | 99 | "nbformat": 4, |
|
102 | 100 | "nbformat_minor": 0 |
|
103 | 101 | } No newline at end of file |
@@ -186,9 +186,7 b'' | |||
|
186 | 186 | ] |
|
187 | 187 | } |
|
188 | 188 | ], |
|
189 | "metadata": { | |
|
190 | "signature": "sha256:627cdf03b8de558c9344f9d1e8f0beeb2448e37e492d676e6db7b07d33251a2b" | |
|
191 | }, | |
|
189 | "metadata": {}, | |
|
192 | 190 | "nbformat": 4, |
|
193 | 191 | "nbformat_minor": 0 |
|
194 | 192 | } No newline at end of file |
@@ -1799,9 +1799,7 b'' | |||
|
1799 | 1799 | ] |
|
1800 | 1800 | } |
|
1801 | 1801 | ], |
|
1802 | "metadata": { | |
|
1803 | "signature": "sha256:31071a05d0ecd75ed72fe3f0de0ad447a6f85cffe382c26efa5e68db1fee54ee" | |
|
1804 | }, | |
|
1802 | "metadata": {}, | |
|
1805 | 1803 | "nbformat": 4, |
|
1806 | 1804 | "nbformat_minor": 0 |
|
1807 | 1805 | } No newline at end of file |
@@ -481,9 +481,7 b'' | |||
|
481 | 481 | ] |
|
482 | 482 | } |
|
483 | 483 | ], |
|
484 | "metadata": { | |
|
485 | "signature": "sha256:df6354daf203e842bc040989d149760382d8ceec769160e4efe8cde9dfcb9107" | |
|
486 | }, | |
|
484 | "metadata": {}, | |
|
487 | 485 | "nbformat": 4, |
|
488 | 486 | "nbformat_minor": 0 |
|
489 | 487 | } No newline at end of file |
@@ -1317,9 +1317,7 b'' | |||
|
1317 | 1317 | "source": [] |
|
1318 | 1318 | } |
|
1319 | 1319 | ], |
|
1320 | "metadata": { | |
|
1321 | "signature": "sha256:86c779d5798c4a68bda7e71c8ef320cb7ba9d7e3d0f1bc4b828ee65f617a5ae3" | |
|
1322 | }, | |
|
1320 | "metadata": {}, | |
|
1323 | 1321 | "nbformat": 4, |
|
1324 | 1322 | "nbformat_minor": 0 |
|
1325 | 1323 | } No newline at end of file |
@@ -160,9 +160,7 b'' | |||
|
160 | 160 | ] |
|
161 | 161 | } |
|
162 | 162 | ], |
|
163 | "metadata": { | |
|
164 | "signature": "sha256:ee769d05a7e195e4b8546ef9a866ef03e59bff2f0fcba499d168c06b516aa79a" | |
|
165 | }, | |
|
163 | "metadata": {}, | |
|
166 | 164 | "nbformat": 4, |
|
167 | 165 | "nbformat_minor": 0 |
|
168 | 166 | } No newline at end of file |
@@ -732,9 +732,7 b'' | |||
|
732 | 732 | ] |
|
733 | 733 | } |
|
734 | 734 | ], |
|
735 | "metadata": { | |
|
736 | "signature": "sha256:74dbf5caa25c937be70dfe2ab509783a01f4a2044850d7044e729300a8c3644d" | |
|
737 | }, | |
|
735 | "metadata": {}, | |
|
738 | 736 | "nbformat": 4, |
|
739 | 737 | "nbformat_minor": 0 |
|
740 | 738 | } No newline at end of file |
@@ -150,9 +150,7 b'' | |||
|
150 | 150 | ] |
|
151 | 151 | } |
|
152 | 152 | ], |
|
153 | "metadata": { | |
|
154 | "signature": "sha256:ac5c21534f3dd013c78d4d201527f3ed4dea5b6fad4116b8d23c67ba107e48c3" | |
|
155 | }, | |
|
153 | "metadata": {}, | |
|
156 | 154 | "nbformat": 4, |
|
157 | 155 | "nbformat_minor": 0 |
|
158 | 156 | } No newline at end of file |
@@ -3051,9 +3051,7 b'' | |||
|
3051 | 3051 | ] |
|
3052 | 3052 | } |
|
3053 | 3053 | ], |
|
3054 | "metadata": { | |
|
3055 | "signature": "sha256:cf83dc9e6288480ac94c44a5983b4ee421f0ade792a9fac64bc00719263386c0" | |
|
3056 | }, | |
|
3054 | "metadata": {}, | |
|
3057 | 3055 | "nbformat": 4, |
|
3058 | 3056 | "nbformat_minor": 0 |
|
3059 | 3057 | } No newline at end of file |
@@ -1647,9 +1647,7 b'' | |||
|
1647 | 1647 | ] |
|
1648 | 1648 | } |
|
1649 | 1649 | ], |
|
1650 | "metadata": { | |
|
1651 | "signature": "sha256:a0f4cda82587c5b8e7a4502192d1210abedac9084077604eb60aea15f262a344" | |
|
1652 | }, | |
|
1650 | "metadata": {}, | |
|
1653 | 1651 | "nbformat": 4, |
|
1654 | 1652 | "nbformat_minor": 0 |
|
1655 | 1653 | } No newline at end of file |
@@ -261,9 +261,7 b'' | |||
|
261 | 261 | ] |
|
262 | 262 | } |
|
263 | 263 | ], |
|
264 | "metadata": { | |
|
265 | "signature": "sha256:993106eecfd7abe1920e1dbe670c4518189c26e7b29dcc541835f7dcf6fffbb2" | |
|
266 | }, | |
|
264 | "metadata": {}, | |
|
267 | 265 | "nbformat": 4, |
|
268 | 266 | "nbformat_minor": 0 |
|
269 | 267 | } No newline at end of file |
@@ -517,9 +517,7 b'' | |||
|
517 | 517 | ] |
|
518 | 518 | } |
|
519 | 519 | ], |
|
520 | "metadata": { | |
|
521 | "signature": "sha256:123d82ef0551f78e5dca94db6e00f1e10ae07d930467cf44709ccc6a9216776a" | |
|
522 | }, | |
|
520 | "metadata": {}, | |
|
523 | 521 | "nbformat": 4, |
|
524 | 522 | "nbformat_minor": 0 |
|
525 | 523 | } No newline at end of file |
@@ -374,9 +374,7 b'' | |||
|
374 | 374 | ] |
|
375 | 375 | } |
|
376 | 376 | ], |
|
377 | "metadata": { | |
|
378 | "signature": "sha256:cbf49a9ceac0258773ae0a652e9ac7c13e0d042debbf0115cdc081862b5f7308" | |
|
379 | }, | |
|
377 | "metadata": {}, | |
|
380 | 378 | "nbformat": 4, |
|
381 | 379 | "nbformat_minor": 0 |
|
382 | 380 | } No newline at end of file |
@@ -333,9 +333,7 b'' | |||
|
333 | 333 | ] |
|
334 | 334 | } |
|
335 | 335 | ], |
|
336 | "metadata": { | |
|
337 | "signature": "sha256:4352d4e1c693d919ce40b29ecf5a536917160df68b19a85caccedb1ea7ad06e1" | |
|
338 | }, | |
|
336 | "metadata": {}, | |
|
339 | 337 | "nbformat": 4, |
|
340 | 338 | "nbformat_minor": 0 |
|
341 | 339 | } No newline at end of file |
@@ -41,9 +41,7 b'' | |||
|
41 | 41 | ] |
|
42 | 42 | } |
|
43 | 43 | ], |
|
44 | "metadata": { | |
|
45 | "signature": "sha256:0b4b631419772e40e0f4893f5a0f0fe089a39e46c8862af0256164628394302d" | |
|
46 | }, | |
|
44 | "metadata": {}, | |
|
47 | 45 | "nbformat": 4, |
|
48 | 46 | "nbformat_minor": 0 |
|
49 | 47 | } No newline at end of file |
@@ -591,9 +591,7 b'' | |||
|
591 | 591 | ] |
|
592 | 592 | } |
|
593 | 593 | ], |
|
594 | "metadata": { | |
|
595 | "signature": "sha256:da6a3d73881e321e5ef406aea3e7d706c9dd405c94675318afde957d292f9cb9" | |
|
596 | }, | |
|
594 | "metadata": {}, | |
|
597 | 595 | "nbformat": 4, |
|
598 | 596 | "nbformat_minor": 0 |
|
599 | 597 | } No newline at end of file |
@@ -830,9 +830,7 b'' | |||
|
830 | 830 | ] |
|
831 | 831 | } |
|
832 | 832 | ], |
|
833 | "metadata": { | |
|
834 | "signature": "sha256:61f24f38b76cb12d46c178eadc5d85d61bd0fc27f959236ea756ef8e1adc2693" | |
|
835 | }, | |
|
833 | "metadata": {}, | |
|
836 | 834 | "nbformat": 4, |
|
837 | 835 | "nbformat_minor": 0 |
|
838 | 836 | } No newline at end of file |
@@ -131,9 +131,7 b'' | |||
|
131 | 131 | "source": [] |
|
132 | 132 | } |
|
133 | 133 | ], |
|
134 | "metadata": { | |
|
135 | "signature": "sha256:5f38c57d9570e4b6f6edf98c38a7bff81cd16365baa11fd40265f1504bfc008c" | |
|
136 | }, | |
|
134 | "metadata": {}, | |
|
137 | 135 | "nbformat": 4, |
|
138 | 136 | "nbformat_minor": 0 |
|
139 | 137 | } No newline at end of file |
@@ -297,9 +297,7 b'' | |||
|
297 | 297 | ] |
|
298 | 298 | } |
|
299 | 299 | ], |
|
300 | "metadata": { | |
|
301 | "signature": "sha256:4e9f4ce8fa9be2e33ffdae399daec11bf3ef63a6f0065b1d59739310ebfe8008" | |
|
302 | }, | |
|
300 | "metadata": {}, | |
|
303 | 301 | "nbformat": 4, |
|
304 | 302 | "nbformat_minor": 0 |
|
305 | 303 | } No newline at end of file |
@@ -26762,9 +26762,7 b'' | |||
|
26762 | 26762 | ] |
|
26763 | 26763 | } |
|
26764 | 26764 | ], |
|
26765 | "metadata": { | |
|
26766 | "signature": "sha256:d75ab1c53fa3389eeac78ecf8e89beb52871950f296aad25776699b6d6125037" | |
|
26767 | }, | |
|
26765 | "metadata": {}, | |
|
26768 | 26766 | "nbformat": 4, |
|
26769 | 26767 | "nbformat_minor": 0 |
|
26770 | 26768 | } No newline at end of file |
@@ -13428,9 +13428,7 b'' | |||
|
13428 | 13428 | ] |
|
13429 | 13429 | } |
|
13430 | 13430 | ], |
|
13431 | "metadata": { | |
|
13432 | "signature": "sha256:c6ccfb14927d633933da8b5c0f776a8da756d833654b5a3f8b6121b390ca6424" | |
|
13433 | }, | |
|
13431 | "metadata": {}, | |
|
13434 | 13432 | "nbformat": 4, |
|
13435 | 13433 | "nbformat_minor": 0 |
|
13436 | 13434 | } No newline at end of file |
@@ -131,9 +131,7 b'' | |||
|
131 | 131 | ] |
|
132 | 132 | } |
|
133 | 133 | ], |
|
134 | "metadata": { | |
|
135 | "signature": "sha256:9a1dd30de04270174c09ef33ca214e5b15ca3721547420087c1563ad557d78e3" | |
|
136 | }, | |
|
134 | "metadata": {}, | |
|
137 | 135 | "nbformat": 4, |
|
138 | 136 | "nbformat_minor": 0 |
|
139 | 137 | } No newline at end of file |
@@ -68,9 +68,7 b'' | |||
|
68 | 68 | ] |
|
69 | 69 | } |
|
70 | 70 | ], |
|
71 | "metadata": { | |
|
72 | "signature": "sha256:5a3c5ebae9154e13957e7c9d28bd3b7697c4b14f965482feb288d2cdf078983e" | |
|
73 | }, | |
|
71 | "metadata": {}, | |
|
74 | 72 | "nbformat": 4, |
|
75 | 73 | "nbformat_minor": 0 |
|
76 | 74 | } No newline at end of file |
@@ -1,8 +1,6 b'' | |||
|
1 | 1 | { |
|
2 | 2 | "cells": [], |
|
3 | "metadata": { | |
|
4 | "signature": "sha256:0abf067a20ebda26a671db997ac954770350d292dff7b7d6a4ace8808f70aca1" | |
|
5 | }, | |
|
3 | "metadata": {}, | |
|
6 | 4 | "nbformat": 4, |
|
7 | 5 | "nbformat_minor": 0 |
|
8 | 6 | } No newline at end of file |
@@ -355,9 +355,7 b'' | |||
|
355 | 355 | ] |
|
356 | 356 | } |
|
357 | 357 | ], |
|
358 | "metadata": { | |
|
359 | "signature": "sha256:ee4b22b4c949fe21b3e5cda24f0916ba59d8c09443f4a897d98b96d4a73ac335" | |
|
360 | }, | |
|
358 | "metadata": {}, | |
|
361 | 359 | "nbformat": 4, |
|
362 | 360 | "nbformat_minor": 0 |
|
363 | 361 | } No newline at end of file |
@@ -296,9 +296,7 b'' | |||
|
296 | 296 | ] |
|
297 | 297 | } |
|
298 | 298 | ], |
|
299 | "metadata": { | |
|
300 | "signature": "sha256:3b7cae0c0936f25e6ccb7acafe310c08a4162a1a7fd66fa9874a52cffa0f64f9" | |
|
301 | }, | |
|
299 | "metadata": {}, | |
|
302 | 300 | "nbformat": 4, |
|
303 | 301 | "nbformat_minor": 0 |
|
304 | 302 | } No newline at end of file |
@@ -346,9 +346,7 b'' | |||
|
346 | 346 | ] |
|
347 | 347 | } |
|
348 | 348 | ], |
|
349 | "metadata": { | |
|
350 | "signature": "sha256:1e9336d35cc07875300c5b876df6ce1f1971c2ee94870788c6ea32bbb789c42b" | |
|
351 | }, | |
|
349 | "metadata": {}, | |
|
352 | 350 | "nbformat": 4, |
|
353 | 351 | "nbformat_minor": 0 |
|
354 | 352 | } No newline at end of file |
General Comments 0
You need to be logged in to leave comments.
Login now