Show More
@@ -3,6 +3,7 | |||||
3 | from __future__ import print_function |
|
3 | from __future__ import print_function | |
4 |
|
4 | |||
5 | import os |
|
5 | import os | |
|
6 | import time | |||
6 |
|
7 | |||
7 | from tornado.web import HTTPError |
|
8 | from tornado.web import HTTPError | |
8 | from unittest import TestCase |
|
9 | from unittest import TestCase | |
@@ -156,6 +157,7 class TestContentsManager(TestCase): | |||||
156 |
|
157 | |||
157 | full_model = cm.get(path) |
|
158 | full_model = cm.get(path) | |
158 | nb = full_model['content'] |
|
159 | nb = full_model['content'] | |
|
160 | nb['metadata']['counter'] = int(1e6 * time.time()) | |||
159 | self.add_code_cell(nb) |
|
161 | self.add_code_cell(nb) | |
160 |
|
162 | |||
161 | cm.save(full_model, path) |
|
163 | cm.save(full_model, path) |
@@ -1,18 +1,27 | |||||
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 | |||
@@ -93,6 +102,48 class NotebookNotary(LoggingConfigurable): | |||||
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 | 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 | algorithm = Enum(algorithms, default_value='sha256', config=True, |
|
147 | algorithm = Enum(algorithms, default_value='sha256', config=True, | |
97 | help="""The hashing algorithm used to sign notebooks.""" |
|
148 | help="""The hashing algorithm used to sign notebooks.""" | |
98 | ) |
|
149 | ) | |
@@ -168,28 +219,72 class NotebookNotary(LoggingConfigurable): | |||||
168 | """ |
|
219 | """ | |
169 | if nb.nbformat < 3: |
|
220 | if nb.nbformat < 3: | |
170 | return False |
|
221 | return False | |
171 | stored_signature = nb['metadata'].get('signature', None) |
|
222 | 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 |
|
223 | return False | |
176 | stored_algo, sig = stored_signature.split(':', 1) |
|
224 | signature = self.compute_signature(nb) | |
177 | if self.algorithm != stored_algo: |
|
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 | return False |
|
230 | return False | |
179 | my_signature = self.compute_signature(nb) |
|
231 | self.db.execute("""UPDATE nbsignatures SET last_seen = ? WHERE | |
180 | return my_signature == sig |
|
232 | algorithm = ? AND | |
|
233 | signature = ?; | |||
|
234 | """, | |||
|
235 | (datetime.utcnow(), self.algorithm, signature), | |||
|
236 | ) | |||
|
237 | self.db.commit() | |||
|
238 | return True | |||
181 |
|
239 | |||
182 | def sign(self, nb): |
|
240 | def sign(self, nb): | |
183 | """Sign a notebook, indicating that its output is trusted |
|
241 | """Sign a notebook, indicating that its output is trusted on this machine | |
184 |
|
242 | |||
185 | stores 'algo:hmac-hexdigest' in notebook.metadata.signature |
|
243 | Stores hash algorithm and hmac digest in a local database of trusted notebooks. | |
186 |
|
||||
187 | e.g. 'sha256:deadbeef123...' |
|
|||
188 | """ |
|
244 | """ | |
189 | if nb.nbformat < 3: |
|
245 | if nb.nbformat < 3: | |
190 | return |
|
246 | return | |
191 | signature = self.compute_signature(nb) |
|
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 | def mark_cells(self, nb, trusted): |
|
289 | def mark_cells(self, nb, trusted): | |
195 | """Mark cells as trusted if the notebook's signature can be verified |
|
290 | """Mark cells as trusted if the notebook's signature can be verified | |
@@ -222,11 +317,9 class NotebookNotary(LoggingConfigurable): | |||||
222 |
|
317 | |||
223 | # explicitly safe output |
|
318 | # explicitly safe output | |
224 | if nbformat_version >= 4: |
|
319 | if nbformat_version >= 4: | |
225 | safe = {'text/plain', 'image/png', 'image/jpeg'} |
|
|||
226 | unsafe_output_types = ['execute_result', 'display_data'] |
|
320 | unsafe_output_types = ['execute_result', 'display_data'] | |
227 | safe_keys = {"output_type", "execution_count", "metadata"} |
|
321 | safe_keys = {"output_type", "execution_count", "metadata"} | |
228 | else: # v3 |
|
322 | else: # v3 | |
229 | safe = {'text', 'png', 'jpeg'} |
|
|||
230 | unsafe_output_types = ['pyout', 'display_data'] |
|
323 | unsafe_output_types = ['pyout', 'display_data'] | |
231 | safe_keys = {"output_type", "prompt_number", "metadata"} |
|
324 | safe_keys = {"output_type", "prompt_number", "metadata"} | |
232 |
|
325 | |||
@@ -261,7 +354,7 class NotebookNotary(LoggingConfigurable): | |||||
261 | trust_flags = { |
|
354 | trust_flags = { | |
262 | 'reset' : ( |
|
355 | 'reset' : ( | |
263 | {'TrustNotebookApp' : { 'reset' : True}}, |
|
356 | {'TrustNotebookApp' : { 'reset' : True}}, | |
264 |
""" |
|
357 | """Delete the trusted notebook cache. | |
265 | All previously signed notebooks will become untrusted. |
|
358 | All previously signed notebooks will become untrusted. | |
266 | """ |
|
359 | """ | |
267 | ), |
|
360 | ), | |
@@ -290,7 +383,7 class TrustNotebookApp(BaseIPythonApplication): | |||||
290 | flags = trust_flags |
|
383 | flags = trust_flags | |
291 |
|
384 | |||
292 | reset = Bool(False, config=True, |
|
385 | reset = Bool(False, config=True, | |
293 |
help="""If True, |
|
386 | help="""If True, delete the trusted signature cache. | |
294 | After reset, all previously signed notebooks will become untrusted. |
|
387 | After reset, all previously signed notebooks will become untrusted. | |
295 | """ |
|
388 | """ | |
296 | ) |
|
389 | ) | |
@@ -320,6 +413,9 class TrustNotebookApp(BaseIPythonApplication): | |||||
320 |
|
413 | |||
321 | def start(self): |
|
414 | def start(self): | |
322 | if self.reset: |
|
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 | self.generate_new_key() |
|
419 | self.generate_new_key() | |
324 | return |
|
420 | return | |
325 | if not self.extra_args: |
|
421 | if not self.extra_args: |
@@ -3,6 +3,9 | |||||
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 | |
@@ -13,8 +16,9 class TestNotary(TestsBase): | |||||
13 |
|
16 | |||
14 | def setUp(self): |
|
17 | def setUp(self): | |
15 | self.notary = sign.NotebookNotary( |
|
18 | self.notary = sign.NotebookNotary( | |
|
19 | db_file=':memory:', | |||
16 | secret=b'secret', |
|
20 | secret=b'secret', | |
17 | profile_dir=get_ipython().profile_dir |
|
21 | profile_dir=get_ipython().profile_dir, | |
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) | |
@@ -25,10 +29,7 class TestNotary(TestsBase): | |||||
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 | |||
@@ -46,9 +47,49 class TestNotary(TestsBase): | |||||
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(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 | def test_check_signature(self): |
|
94 | def test_check_signature(self): | |
54 | nb = self.nb |
|
95 | nb = self.nb |
@@ -56,6 +56,7 def upgrade(nb, from_version=3, from_minor=0): | |||||
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 |
@@ -55,10 +55,6 | |||||
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", |
@@ -64,6 +64,7 def strip_transient(nb): | |||||
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 |
@@ -95,9 +95,7 | |||||
95 | ] |
|
95 | ] | |
96 | } |
|
96 | } | |
97 | ], |
|
97 | ], | |
98 | "metadata": { |
|
98 | "metadata": {}, | |
99 | "signature": "sha256:de8cb1aff3da9097ba3fc7afab4327fc589f2bb1c154feeeb5ed97c87e37486f" |
|
|||
100 | }, |
|
|||
101 | "nbformat": 4, |
|
99 | "nbformat": 4, | |
102 | "nbformat_minor": 0 |
|
100 | "nbformat_minor": 0 | |
103 | } No newline at end of file |
|
101 | } |
@@ -186,9 +186,7 | |||||
186 | ] |
|
186 | ] | |
187 | } |
|
187 | } | |
188 | ], |
|
188 | ], | |
189 | "metadata": { |
|
189 | "metadata": {}, | |
190 | "signature": "sha256:627cdf03b8de558c9344f9d1e8f0beeb2448e37e492d676e6db7b07d33251a2b" |
|
|||
191 | }, |
|
|||
192 | "nbformat": 4, |
|
190 | "nbformat": 4, | |
193 | "nbformat_minor": 0 |
|
191 | "nbformat_minor": 0 | |
194 | } No newline at end of file |
|
192 | } |
@@ -1799,9 +1799,7 | |||||
1799 | ] |
|
1799 | ] | |
1800 | } |
|
1800 | } | |
1801 | ], |
|
1801 | ], | |
1802 | "metadata": { |
|
1802 | "metadata": {}, | |
1803 | "signature": "sha256:31071a05d0ecd75ed72fe3f0de0ad447a6f85cffe382c26efa5e68db1fee54ee" |
|
|||
1804 | }, |
|
|||
1805 | "nbformat": 4, |
|
1803 | "nbformat": 4, | |
1806 | "nbformat_minor": 0 |
|
1804 | "nbformat_minor": 0 | |
1807 | } No newline at end of file |
|
1805 | } |
@@ -481,9 +481,7 | |||||
481 | ] |
|
481 | ] | |
482 | } |
|
482 | } | |
483 | ], |
|
483 | ], | |
484 | "metadata": { |
|
484 | "metadata": {}, | |
485 | "signature": "sha256:df6354daf203e842bc040989d149760382d8ceec769160e4efe8cde9dfcb9107" |
|
|||
486 | }, |
|
|||
487 | "nbformat": 4, |
|
485 | "nbformat": 4, | |
488 | "nbformat_minor": 0 |
|
486 | "nbformat_minor": 0 | |
489 | } No newline at end of file |
|
487 | } |
@@ -1317,9 +1317,7 | |||||
1317 | "source": [] |
|
1317 | "source": [] | |
1318 | } |
|
1318 | } | |
1319 | ], |
|
1319 | ], | |
1320 | "metadata": { |
|
1320 | "metadata": {}, | |
1321 | "signature": "sha256:86c779d5798c4a68bda7e71c8ef320cb7ba9d7e3d0f1bc4b828ee65f617a5ae3" |
|
|||
1322 | }, |
|
|||
1323 | "nbformat": 4, |
|
1321 | "nbformat": 4, | |
1324 | "nbformat_minor": 0 |
|
1322 | "nbformat_minor": 0 | |
1325 | } No newline at end of file |
|
1323 | } |
@@ -160,9 +160,7 | |||||
160 | ] |
|
160 | ] | |
161 | } |
|
161 | } | |
162 | ], |
|
162 | ], | |
163 | "metadata": { |
|
163 | "metadata": {}, | |
164 | "signature": "sha256:ee769d05a7e195e4b8546ef9a866ef03e59bff2f0fcba499d168c06b516aa79a" |
|
|||
165 | }, |
|
|||
166 | "nbformat": 4, |
|
164 | "nbformat": 4, | |
167 | "nbformat_minor": 0 |
|
165 | "nbformat_minor": 0 | |
168 | } No newline at end of file |
|
166 | } |
@@ -732,9 +732,7 | |||||
732 | ] |
|
732 | ] | |
733 | } |
|
733 | } | |
734 | ], |
|
734 | ], | |
735 | "metadata": { |
|
735 | "metadata": {}, | |
736 | "signature": "sha256:74dbf5caa25c937be70dfe2ab509783a01f4a2044850d7044e729300a8c3644d" |
|
|||
737 | }, |
|
|||
738 | "nbformat": 4, |
|
736 | "nbformat": 4, | |
739 | "nbformat_minor": 0 |
|
737 | "nbformat_minor": 0 | |
740 | } No newline at end of file |
|
738 | } |
@@ -150,9 +150,7 | |||||
150 | ] |
|
150 | ] | |
151 | } |
|
151 | } | |
152 | ], |
|
152 | ], | |
153 | "metadata": { |
|
153 | "metadata": {}, | |
154 | "signature": "sha256:ac5c21534f3dd013c78d4d201527f3ed4dea5b6fad4116b8d23c67ba107e48c3" |
|
|||
155 | }, |
|
|||
156 | "nbformat": 4, |
|
154 | "nbformat": 4, | |
157 | "nbformat_minor": 0 |
|
155 | "nbformat_minor": 0 | |
158 | } No newline at end of file |
|
156 | } |
@@ -3051,9 +3051,7 | |||||
3051 | ] |
|
3051 | ] | |
3052 | } |
|
3052 | } | |
3053 | ], |
|
3053 | ], | |
3054 | "metadata": { |
|
3054 | "metadata": {}, | |
3055 | "signature": "sha256:cf83dc9e6288480ac94c44a5983b4ee421f0ade792a9fac64bc00719263386c0" |
|
|||
3056 | }, |
|
|||
3057 | "nbformat": 4, |
|
3055 | "nbformat": 4, | |
3058 | "nbformat_minor": 0 |
|
3056 | "nbformat_minor": 0 | |
3059 | } No newline at end of file |
|
3057 | } |
@@ -1647,9 +1647,7 | |||||
1647 | ] |
|
1647 | ] | |
1648 | } |
|
1648 | } | |
1649 | ], |
|
1649 | ], | |
1650 | "metadata": { |
|
1650 | "metadata": {}, | |
1651 | "signature": "sha256:a0f4cda82587c5b8e7a4502192d1210abedac9084077604eb60aea15f262a344" |
|
|||
1652 | }, |
|
|||
1653 | "nbformat": 4, |
|
1651 | "nbformat": 4, | |
1654 | "nbformat_minor": 0 |
|
1652 | "nbformat_minor": 0 | |
1655 | } No newline at end of file |
|
1653 | } |
@@ -261,9 +261,7 | |||||
261 | ] |
|
261 | ] | |
262 | } |
|
262 | } | |
263 | ], |
|
263 | ], | |
264 | "metadata": { |
|
264 | "metadata": {}, | |
265 | "signature": "sha256:993106eecfd7abe1920e1dbe670c4518189c26e7b29dcc541835f7dcf6fffbb2" |
|
|||
266 | }, |
|
|||
267 | "nbformat": 4, |
|
265 | "nbformat": 4, | |
268 | "nbformat_minor": 0 |
|
266 | "nbformat_minor": 0 | |
269 | } No newline at end of file |
|
267 | } |
@@ -517,9 +517,7 | |||||
517 | ] |
|
517 | ] | |
518 | } |
|
518 | } | |
519 | ], |
|
519 | ], | |
520 | "metadata": { |
|
520 | "metadata": {}, | |
521 | "signature": "sha256:123d82ef0551f78e5dca94db6e00f1e10ae07d930467cf44709ccc6a9216776a" |
|
|||
522 | }, |
|
|||
523 | "nbformat": 4, |
|
521 | "nbformat": 4, | |
524 | "nbformat_minor": 0 |
|
522 | "nbformat_minor": 0 | |
525 | } No newline at end of file |
|
523 | } |
@@ -374,9 +374,7 | |||||
374 | ] |
|
374 | ] | |
375 | } |
|
375 | } | |
376 | ], |
|
376 | ], | |
377 | "metadata": { |
|
377 | "metadata": {}, | |
378 | "signature": "sha256:cbf49a9ceac0258773ae0a652e9ac7c13e0d042debbf0115cdc081862b5f7308" |
|
|||
379 | }, |
|
|||
380 | "nbformat": 4, |
|
378 | "nbformat": 4, | |
381 | "nbformat_minor": 0 |
|
379 | "nbformat_minor": 0 | |
382 | } No newline at end of file |
|
380 | } |
@@ -333,9 +333,7 | |||||
333 | ] |
|
333 | ] | |
334 | } |
|
334 | } | |
335 | ], |
|
335 | ], | |
336 | "metadata": { |
|
336 | "metadata": {}, | |
337 | "signature": "sha256:4352d4e1c693d919ce40b29ecf5a536917160df68b19a85caccedb1ea7ad06e1" |
|
|||
338 | }, |
|
|||
339 | "nbformat": 4, |
|
337 | "nbformat": 4, | |
340 | "nbformat_minor": 0 |
|
338 | "nbformat_minor": 0 | |
341 | } No newline at end of file |
|
339 | } |
@@ -41,9 +41,7 | |||||
41 | ] |
|
41 | ] | |
42 | } |
|
42 | } | |
43 | ], |
|
43 | ], | |
44 | "metadata": { |
|
44 | "metadata": {}, | |
45 | "signature": "sha256:0b4b631419772e40e0f4893f5a0f0fe089a39e46c8862af0256164628394302d" |
|
|||
46 | }, |
|
|||
47 | "nbformat": 4, |
|
45 | "nbformat": 4, | |
48 | "nbformat_minor": 0 |
|
46 | "nbformat_minor": 0 | |
49 | } No newline at end of file |
|
47 | } |
@@ -591,9 +591,7 | |||||
591 | ] |
|
591 | ] | |
592 | } |
|
592 | } | |
593 | ], |
|
593 | ], | |
594 | "metadata": { |
|
594 | "metadata": {}, | |
595 | "signature": "sha256:da6a3d73881e321e5ef406aea3e7d706c9dd405c94675318afde957d292f9cb9" |
|
|||
596 | }, |
|
|||
597 | "nbformat": 4, |
|
595 | "nbformat": 4, | |
598 | "nbformat_minor": 0 |
|
596 | "nbformat_minor": 0 | |
599 | } No newline at end of file |
|
597 | } |
@@ -830,9 +830,7 | |||||
830 | ] |
|
830 | ] | |
831 | } |
|
831 | } | |
832 | ], |
|
832 | ], | |
833 | "metadata": { |
|
833 | "metadata": {}, | |
834 | "signature": "sha256:61f24f38b76cb12d46c178eadc5d85d61bd0fc27f959236ea756ef8e1adc2693" |
|
|||
835 | }, |
|
|||
836 | "nbformat": 4, |
|
834 | "nbformat": 4, | |
837 | "nbformat_minor": 0 |
|
835 | "nbformat_minor": 0 | |
838 | } No newline at end of file |
|
836 | } |
@@ -131,9 +131,7 | |||||
131 | "source": [] |
|
131 | "source": [] | |
132 | } |
|
132 | } | |
133 | ], |
|
133 | ], | |
134 | "metadata": { |
|
134 | "metadata": {}, | |
135 | "signature": "sha256:5f38c57d9570e4b6f6edf98c38a7bff81cd16365baa11fd40265f1504bfc008c" |
|
|||
136 | }, |
|
|||
137 | "nbformat": 4, |
|
135 | "nbformat": 4, | |
138 | "nbformat_minor": 0 |
|
136 | "nbformat_minor": 0 | |
139 | } No newline at end of file |
|
137 | } |
@@ -297,9 +297,7 | |||||
297 | ] |
|
297 | ] | |
298 | } |
|
298 | } | |
299 | ], |
|
299 | ], | |
300 | "metadata": { |
|
300 | "metadata": {}, | |
301 | "signature": "sha256:4e9f4ce8fa9be2e33ffdae399daec11bf3ef63a6f0065b1d59739310ebfe8008" |
|
|||
302 | }, |
|
|||
303 | "nbformat": 4, |
|
301 | "nbformat": 4, | |
304 | "nbformat_minor": 0 |
|
302 | "nbformat_minor": 0 | |
305 | } No newline at end of file |
|
303 | } |
@@ -26762,9 +26762,7 | |||||
26762 | ] |
|
26762 | ] | |
26763 | } |
|
26763 | } | |
26764 | ], |
|
26764 | ], | |
26765 | "metadata": { |
|
26765 | "metadata": {}, | |
26766 | "signature": "sha256:d75ab1c53fa3389eeac78ecf8e89beb52871950f296aad25776699b6d6125037" |
|
|||
26767 | }, |
|
|||
26768 | "nbformat": 4, |
|
26766 | "nbformat": 4, | |
26769 | "nbformat_minor": 0 |
|
26767 | "nbformat_minor": 0 | |
26770 | } No newline at end of file |
|
26768 | } |
@@ -13428,9 +13428,7 | |||||
13428 | ] |
|
13428 | ] | |
13429 | } |
|
13429 | } | |
13430 | ], |
|
13430 | ], | |
13431 | "metadata": { |
|
13431 | "metadata": {}, | |
13432 | "signature": "sha256:c6ccfb14927d633933da8b5c0f776a8da756d833654b5a3f8b6121b390ca6424" |
|
|||
13433 | }, |
|
|||
13434 | "nbformat": 4, |
|
13432 | "nbformat": 4, | |
13435 | "nbformat_minor": 0 |
|
13433 | "nbformat_minor": 0 | |
13436 | } No newline at end of file |
|
13434 | } |
@@ -131,9 +131,7 | |||||
131 | ] |
|
131 | ] | |
132 | } |
|
132 | } | |
133 | ], |
|
133 | ], | |
134 | "metadata": { |
|
134 | "metadata": {}, | |
135 | "signature": "sha256:9a1dd30de04270174c09ef33ca214e5b15ca3721547420087c1563ad557d78e3" |
|
|||
136 | }, |
|
|||
137 | "nbformat": 4, |
|
135 | "nbformat": 4, | |
138 | "nbformat_minor": 0 |
|
136 | "nbformat_minor": 0 | |
139 | } No newline at end of file |
|
137 | } |
@@ -68,9 +68,7 | |||||
68 | ] |
|
68 | ] | |
69 | } |
|
69 | } | |
70 | ], |
|
70 | ], | |
71 | "metadata": { |
|
71 | "metadata": {}, | |
72 | "signature": "sha256:5a3c5ebae9154e13957e7c9d28bd3b7697c4b14f965482feb288d2cdf078983e" |
|
|||
73 | }, |
|
|||
74 | "nbformat": 4, |
|
72 | "nbformat": 4, | |
75 | "nbformat_minor": 0 |
|
73 | "nbformat_minor": 0 | |
76 | } No newline at end of file |
|
74 | } |
@@ -1,8 +1,6 | |||||
1 | { |
|
1 | { | |
2 | "cells": [], |
|
2 | "cells": [], | |
3 | "metadata": { |
|
3 | "metadata": {}, | |
4 | "signature": "sha256:0abf067a20ebda26a671db997ac954770350d292dff7b7d6a4ace8808f70aca1" |
|
|||
5 | }, |
|
|||
6 | "nbformat": 4, |
|
4 | "nbformat": 4, | |
7 | "nbformat_minor": 0 |
|
5 | "nbformat_minor": 0 | |
8 | } No newline at end of file |
|
6 | } |
@@ -355,9 +355,7 | |||||
355 | ] |
|
355 | ] | |
356 | } |
|
356 | } | |
357 | ], |
|
357 | ], | |
358 | "metadata": { |
|
358 | "metadata": {}, | |
359 | "signature": "sha256:ee4b22b4c949fe21b3e5cda24f0916ba59d8c09443f4a897d98b96d4a73ac335" |
|
|||
360 | }, |
|
|||
361 | "nbformat": 4, |
|
359 | "nbformat": 4, | |
362 | "nbformat_minor": 0 |
|
360 | "nbformat_minor": 0 | |
363 | } No newline at end of file |
|
361 | } |
@@ -296,9 +296,7 | |||||
296 | ] |
|
296 | ] | |
297 | } |
|
297 | } | |
298 | ], |
|
298 | ], | |
299 | "metadata": { |
|
299 | "metadata": {}, | |
300 | "signature": "sha256:3b7cae0c0936f25e6ccb7acafe310c08a4162a1a7fd66fa9874a52cffa0f64f9" |
|
|||
301 | }, |
|
|||
302 | "nbformat": 4, |
|
300 | "nbformat": 4, | |
303 | "nbformat_minor": 0 |
|
301 | "nbformat_minor": 0 | |
304 | } No newline at end of file |
|
302 | } |
@@ -346,9 +346,7 | |||||
346 | ] |
|
346 | ] | |
347 | } |
|
347 | } | |
348 | ], |
|
348 | ], | |
349 | "metadata": { |
|
349 | "metadata": {}, | |
350 | "signature": "sha256:1e9336d35cc07875300c5b876df6ce1f1971c2ee94870788c6ea32bbb789c42b" |
|
|||
351 | }, |
|
|||
352 | "nbformat": 4, |
|
350 | "nbformat": 4, | |
353 | "nbformat_minor": 0 |
|
351 | "nbformat_minor": 0 | |
354 | } No newline at end of file |
|
352 | } |
@@ -2533,9 +2533,7 | |||||
2533 | ] |
|
2533 | ] | |
2534 | } |
|
2534 | } | |
2535 | ], |
|
2535 | ], | |
2536 | "metadata": { |
|
2536 | "metadata": {}, | |
2537 | "signature": "sha256:1b19dedc6473d4e886e549020c6710f2d14c17296168a02e7e7fa9673912b893" |
|
|||
2538 | }, |
|
|||
2539 | "nbformat": 4, |
|
2537 | "nbformat": 4, | |
2540 | "nbformat_minor": 0 |
|
2538 | "nbformat_minor": 0 | |
2541 | } No newline at end of file |
|
2539 | } |
@@ -107,9 +107,7 | |||||
107 | "source": [] |
|
107 | "source": [] | |
108 | } |
|
108 | } | |
109 | ], |
|
109 | ], | |
110 | "metadata": { |
|
110 | "metadata": {}, | |
111 | "signature": "sha256:8781781d8835d77bf0f71b535b72ee2718405543c48e87ad0408242119cdb3cc" |
|
|||
112 | }, |
|
|||
113 | "nbformat": 4, |
|
111 | "nbformat": 4, | |
114 | "nbformat_minor": 0 |
|
112 | "nbformat_minor": 0 | |
115 | } No newline at end of file |
|
113 | } |
General Comments 0
You need to be logged in to leave comments.
Login now