##// END OF EJS Templates
Merge pull request #7244 from minrk/rm-signature...
Min RK -
r19680:d4983156 merge
parent child Browse files
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 """Functions for signing notebooks"""
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 string_types, unicode_type, cast_bytes
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 """Generate a new key for notebook signature.
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, generate a new key for notebook signature.
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