summaryrefslogtreecommitdiff
path: root/notebooks
diff options
context:
space:
mode:
Diffstat (limited to 'notebooks')
-rw-r--r--notebooks/python/cryptographic_primitives.ipynb261
-rw-r--r--notebooks/python/python_3_8_to_3_11.ipynb2
-rw-r--r--notebooks/python/python_intro.ipynb120
-rw-r--r--notebooks/python/python_oo.ipynb151
4 files changed, 451 insertions, 83 deletions
diff --git a/notebooks/python/cryptographic_primitives.ipynb b/notebooks/python/cryptographic_primitives.ipynb
new file mode 100644
index 0000000..4d96549
--- /dev/null
+++ b/notebooks/python/cryptographic_primitives.ipynb
@@ -0,0 +1,261 @@
1{
2 "cells": [
3 {
4 "cell_type": "code",
5 "execution_count": 12,
6 "id": "fedd7d1a-36a9-4255-8305-a720dfb7b500",
7 "metadata": {},
8 "outputs": [
9 {
10 "name": "stdout",
11 "output_type": "stream",
12 "text": [
13 "b\"{\\x8ea\\xf8wu!\\xeb\\xc4\\xc9\\xcb|\\\\\\xf1K\\xd5t\\x80i\\xbcD\\xb6!\\xfc\\xee\\x06lQ'V0\\xc6\"\n",
14 "7b8e61f8777521ebc4c9cb7c5cf14bd5748069bc44b621fcee066c51275630c6\n",
15 "b'e45h+Hd1IevEyct8XPFL1XSAabxEtiH87gZsUSdWMMY='\n"
16 ]
17 }
18 ],
19 "source": [
20 "# hashes\n",
21 "\n",
22 "import base64\n",
23 "import hashlib\n",
24 "\n",
25 "hashed_result = hashlib.sha256(b\"This is a tesu\")\n",
26 "print(hashed_result.digest())\n",
27 "print(hashed_result.hexdigest())\n",
28 "print(base64.b64encode(hashed_result.digest()))\n"
29 ]
30 },
31 {
32 "cell_type": "code",
33 "execution_count": 13,
34 "id": "9082c388-1c30-46dc-8897-15bf5db255c8",
35 "metadata": {},
36 "outputs": [
37 {
38 "name": "stdout",
39 "output_type": "stream",
40 "text": [
41 "password_hash1 = 'pbkdf2_sha256$100000$j2jYEb1TvX/wh5JEAYGag6h+hRNTGe3rjyv5pegVBu4=$7cV7MljnJJYuGndge3jozwFP28aP8Gfj+QFGvv5bpfo='\n",
42 "password_hash2 = 'pbkdf2_sha256$100000$8+PsThJmJXNAbQ/zaR5AgRA/CuNv8L1uPUXRNuWHc9k=$Kf07CSCVQOTQrvWX5l/ZBdA7zsaEFH247QjUaewpxaU='\n"
43 ]
44 }
45 ],
46 "source": [
47 "# PBKDF2\n",
48 "\n",
49 "import base64\n",
50 "import hashlib\n",
51 "import os\n",
52 "\n",
53 "\n",
54 "def get_password_hash(plaintext_password: str) -> str:\n",
55 " \"\"\"\n",
56 " Returns a complete password hash based on `plaintext_password`\n",
57 "\n",
58 " :return: The hashed version of `plaintext_password`\n",
59 " :rtype: str\n",
60 " \"\"\"\n",
61 " # parameters typycally sent as arguments or fetched from config\n",
62 " hash_algo = \"sha256\" # the hash algorithm to use for HMAC\n",
63 " iterations = 100000 # amount of iterations\n",
64 "\n",
65 " salt = os.urandom(32) # 32 bytes of random data\n",
66 "\n",
67 " key = hashlib.pbkdf2_hmac(hash_algo, plaintext_password.encode(), salt, iterations)\n",
68 "\n",
69 " return (\n",
70 " f\"pbkdf2_{hash_algo}${iterations}$\"\n",
71 " f\"{base64.b64encode(salt).decode(\"ascii\")}$\"\n",
72 " f\"{base64.b64encode(key).decode(\"ascii\")}\"\n",
73 " )\n",
74 "\n",
75 "\n",
76 "def password_matches(plaintext_password: str, password_hash: str) -> bool:\n",
77 " \"\"\"Returns True if `plaintext_password` matches `password_hash`, False otherwise\"\"\"\n",
78 " hash_tokens = password_hash.split(\"$\")\n",
79 " hash_algo = hash_tokens[0].split(\"_\")[1]\n",
80 " iterations = int(hash_tokens[1])\n",
81 " salt = base64.b64decode(hash_tokens[2])\n",
82 " key = hashlib.pbkdf2_hmac(hash_algo, plaintext_password.encode(), salt, iterations)\n",
83 " return key == base64.b64decode(hash_tokens[3])\n",
84 "\n",
85 "\n",
86 "password_hash1 = get_password_hash(\"didn't expect a Spanish Inquisition!\")\n",
87 "password_hash2 = get_password_hash(\"didn't expect a Spanish Inquisition!\")\n",
88 "print(f\"{password_hash1 = }\")\n",
89 "print(f\"{password_hash2 = }\")\n",
90 "\n",
91 "# print(password_matches(\"didn't expect a Spanish Inquisition!\", 'pbkdf2_sha256$100000$JfwrS72mFimypbwhJ/NCyds2dkXtknvfWM5AM3+Z5GQ=$k8YA8csSJZhuYW0um7utq3+lSX5pTAQPd9dukObWrPo='))"
92 ]
93 },
94 {
95 "cell_type": "code",
96 "execution_count": 14,
97 "id": "1274325e-f670-4af8-a0bc-2ff7f668f769",
98 "metadata": {},
99 "outputs": [
100 {
101 "name": "stdout",
102 "output_type": "stream",
103 "text": [
104 "('gxk9W0XFgbzbjRWB7zCPjbWs3', '00000bf5db2c3c6484904bf5e8314aeb123f5594af1f283fb62e14c7d196e84c')\n"
105 ]
106 }
107 ],
108 "source": [
109 "# Proof of work\n",
110 "\n",
111 "import hashlib\n",
112 "import random\n",
113 "import string\n",
114 "\n",
115 "VALID_CHARS = string.ascii_letters + string.digits # valid characters for response\n",
116 "\n",
117 "\n",
118 "def pow_solve_rnd(challenge: str, starting_chars: str = \"00000\", answer_size: int = 25) -> tuple:\n",
119 " \"\"\"\n",
120 " Solves a POW challenge for SHA-256 using `random.choice`\n",
121 "\n",
122 " :return: (response, SHA-256 digest) tuple that solves the challenge\n",
123 " :rtype: tuple\n",
124 " \"\"\"\n",
125 " while True:\n",
126 " attempt = ''.join(\n",
127 " [random.choice(VALID_CHARS) for x in range(answer_size)])\n",
128 " dig = hashlib.sha256(('{0}:{1}'.format(challenge, attempt)).encode()).hexdigest()\n",
129 " if dig.startswith(starting_chars):\n",
130 " return attempt, dig\n",
131 "\n",
132 "print(f\"{pow_solve_rnd('2024-08-07')}\")"
133 ]
134 },
135 {
136 "cell_type": "code",
137 "execution_count": 25,
138 "id": "6752c055-7e49-46c4-858b-599d069a7e77",
139 "metadata": {},
140 "outputs": [
141 {
142 "name": "stdout",
143 "output_type": "stream",
144 "text": [
145 "enc-val$2$6ppVDmiJi0P9BnHuzwFUPA+vV/Gsb4INe2O9E/Ma2uI=$6879D8rw8645nbQ/NQtY3Q+AwKTWpd1P6Q==\n",
146 "Invalid tag when decrypting enc-val$2$tmuH3MTDQ9vMbxJ1pZUb0rQlF43Lgr/AYfkA+vSOZs0=$5CVt3VrxhuuOOVt1vCXeGpbhNh/20IcFNg==\n",
147 "None\n"
148 ]
149 }
150 ],
151 "source": [
152 "# Encryption / decryption using AES-256-GCM\n",
153 "\n",
154 "import hashlib\n",
155 "import os\n",
156 "\n",
157 "from cryptography.exceptions import InvalidTag\n",
158 "from cryptography.hazmat.primitives.ciphers.aead import AESGCM\n",
159 "\n",
160 "\n",
161 "def encrypt(password: str, data: str) -> str:\n",
162 " \"\"\"\n",
163 " Encrypts `data` using `password` and AES-256-GCM.\n",
164 "\n",
165 " The output string is in the following format:\n",
166 " enc-val$`version-num`$`bas64-salt`$`base64-encrypted_data`\n",
167 "\n",
168 " :param password: The password to generate the key with\n",
169 " :type password: str\n",
170 "\n",
171 " :param data: The data to be encrypted\n",
172 " :type data: str\n",
173 "\n",
174 " :return: The output string\n",
175 " :rtype: str\n",
176 " \"\"\"\n",
177 " data_bytes = data.encode()\n",
178 " salt = os.urandom(32)\n",
179 " aesgcm = AESGCM(\n",
180 " hashlib.scrypt(\n",
181 " password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32\n",
182 " )\n",
183 " )\n",
184 " nonce = salt[:12]\n",
185 " # padding_length_bytes = b\"-1\" # no padding used 2 bytes \"sign\"\n",
186 " edata = aesgcm.encrypt(\n",
187 " nonce, data_bytes, salt\n",
188 " )\n",
189 " return (\n",
190 " f\"enc-val$2${base64.b64encode(salt).decode()}${base64.b64encode(edata).decode()}\"\n",
191 " )\n",
192 "\n",
193 "\n",
194 "def decrypt(password: str, edata: str) -> str:\n",
195 " \"\"\"\n",
196 " Decrypts `edata` using `password`.\n",
197 "\n",
198 " `edata` is in the following format:\n",
199 " enc-val$`version-num`$`bas64-salt`$`base64-encrypted_data`\n",
200 "\n",
201 " :param password: The password to generate the key with\n",
202 " :type password: str\n",
203 "\n",
204 " :param edata: The data to be decrypted\n",
205 " :type edata: str\n",
206 "\n",
207 " :return: The output string / decrypted data\n",
208 " :rtype: str\n",
209 " \"\"\"\n",
210 " # check for supported versions first...\n",
211 "\n",
212 " try:\n",
213 " salt, data = (base64.b64decode(t) for t in edata[10:].split('$'))\n",
214 " nonce = salt[:12]\n",
215 " aesgcm = AESGCM(\n",
216 " hashlib.scrypt(\n",
217 " password.encode(), salt=salt, n=2**14, r=8, p=1, dklen=32\n",
218 " )\n",
219 " )\n",
220 "\n",
221 " # decrypt\n",
222 " data = aesgcm.decrypt(nonce, data, salt)\n",
223 " return data.decode()\n",
224 " except InvalidTag:\n",
225 " print(f\"Invalid tag when decrypting {edata}\")\n",
226 "\n",
227 "print(f\"{encrypt('my passphrase', 'my secret')}\")\n",
228 "print(f\"{decrypt('my passphrase', 'enc-val$2$tmuH3MTDQ9vMbxJ1pZUb0rQlF43Lgr/AYfkA+vSOZs0=$5CVt3VrxhuuOOVt1vCXeGpbhNh/20IcFNg==')}\")"
229 ]
230 },
231 {
232 "cell_type": "code",
233 "execution_count": null,
234 "id": "8ccfb453-d3fd-4383-a01d-f81b19f2cb82",
235 "metadata": {},
236 "outputs": [],
237 "source": []
238 }
239 ],
240 "metadata": {
241 "kernelspec": {
242 "display_name": "Python 3 (ipykernel)",
243 "language": "python",
244 "name": "python3"
245 },
246 "language_info": {
247 "codemirror_mode": {
248 "name": "ipython",
249 "version": 3
250 },
251 "file_extension": ".py",
252 "mimetype": "text/x-python",
253 "name": "python",
254 "nbconvert_exporter": "python",
255 "pygments_lexer": "ipython3",
256 "version": "3.13.9"
257 }
258 },
259 "nbformat": 4,
260 "nbformat_minor": 5
261}
diff --git a/notebooks/python/python_3_8_to_3_11.ipynb b/notebooks/python/python_3_8_to_3_11.ipynb
index b8724e8..016c108 100644
--- a/notebooks/python/python_3_8_to_3_11.ipynb
+++ b/notebooks/python/python_3_8_to_3_11.ipynb
@@ -834,7 +834,7 @@
834 "name": "python", 834 "name": "python",
835 "nbconvert_exporter": "python", 835 "nbconvert_exporter": "python",
836 "pygments_lexer": "ipython3", 836 "pygments_lexer": "ipython3",
837 "version": "3.11.7" 837 "version": "3.12.3"
838 } 838 }
839 }, 839 },
840 "nbformat": 4, 840 "nbformat": 4,
diff --git a/notebooks/python/python_intro.ipynb b/notebooks/python/python_intro.ipynb
index f129ffe..f101400 100644
--- a/notebooks/python/python_intro.ipynb
+++ b/notebooks/python/python_intro.ipynb
@@ -50,6 +50,19 @@
50 }, 50 },
51 { 51 {
52 "cell_type": "markdown", 52 "cell_type": "markdown",
53 "id": "779bac39-4646-4b80-9970-343a3daef1e7",
54 "metadata": {},
55 "source": [
56 "## Important PEPs\n",
57 "\n",
58 "- [PEP0](https://peps.python.org/) - Index of Python Enhancement Proposals\n",
59 "- [PEP8](https://peps.python.org/pep-0008/) - Style Guide for Python Code\n",
60 "- [PEP257](https://peps.python.org/pep-0257/) - Docstring Conventions\n",
61 "- [PEP484](https://peps.python.org/pep-0484/) - Type Hints"
62 ]
63 },
64 {
65 "cell_type": "markdown",
53 "id": "6d1edcfc-b001-4393-9c24-72adc9b5fccf", 66 "id": "6d1edcfc-b001-4393-9c24-72adc9b5fccf",
54 "metadata": {}, 67 "metadata": {},
55 "source": [ 68 "source": [
@@ -72,7 +85,7 @@
72 }, 85 },
73 { 86 {
74 "cell_type": "markdown", 87 "cell_type": "markdown",
75 "id": "4f144f73-51c1-419d-b71e-ee513f68f41c", 88 "id": "81573ed5-b76e-4726-9f44-7dc017fd0896",
76 "metadata": {}, 89 "metadata": {},
77 "source": [ 90 "source": [
78 "## Built-in functions\n", 91 "## Built-in functions\n",
@@ -81,8 +94,6 @@
81 "\n", 94 "\n",
82 "[https://docs.python.org/3/library/functions.html](https://docs.python.org/3/library/functions.html)\n", 95 "[https://docs.python.org/3/library/functions.html](https://docs.python.org/3/library/functions.html)\n",
83 "\n", 96 "\n",
84 "- `dir([obj])` - returns a list of valid attributes for that object\n",
85 "\n",
86 "- `id(obj)` - returns the \"identity\" of an object - an integer which is guaranteed to be unique\n", 97 "- `id(obj)` - returns the \"identity\" of an object - an integer which is guaranteed to be unique\n",
87 "\n", 98 "\n",
88 "- `print(...)` - prints objects to a text stream\n", 99 "- `print(...)` - prints objects to a text stream\n",
@@ -94,6 +105,12 @@
94 }, 105 },
95 { 106 {
96 "cell_type": "markdown", 107 "cell_type": "markdown",
108 "id": "bc180243-fe11-405c-beb9-83db4f793a05",
109 "metadata": {},
110 "source": []
111 },
112 {
113 "cell_type": "markdown",
97 "id": "a0c57535-e285-47af-80f9-1d4a16de5f25", 114 "id": "a0c57535-e285-47af-80f9-1d4a16de5f25",
98 "metadata": {}, 115 "metadata": {},
99 "source": [ 116 "source": [
@@ -116,26 +133,28 @@
116 "source": [ 133 "source": [
117 "# Common built-in types:\n", 134 "# Common built-in types:\n",
118 "\n", 135 "\n",
119 "s = 'foo' # this is a string / str, same as str('foo'), may be encoded, immutable (s[0] = 'r' is NOT possible)\n", 136 "my_string = 'foo' # a string / str, same as str('foo'), may be encoded\n",
137 " # immutable (s[0] = 'r' is NOT possible)\n",
120 "\n", 138 "\n",
121 "b = b'foo' # bytes, same as bytes('foo', 'utf-8'), may be decoded, immutable\n", 139 "my_bytes = b'foo' # bytes, same as bytes('foo', 'utf-8'), may be decoded, immutable\n",
122 "\n", 140 "\n",
123 "i = 6 # int, same as int('6'), immutable\n", 141 "my_int = 6 # int, same as int('6'), immutable\n",
124 "\n", 142 "\n",
125 "f = 0.1 # float, same as float('0.1'), immutable, Note!!: Floats have a fixed size,\n", 143 "my_float = 0.1 # float, same as float('0.1'), immutable, Note!!: Floats have a fixed size,\n",
126 "# hence they don't necessarily behave they way we expect from math class.\n", 144 " # hence they don't necessarily behave they way we expect from math class.\n",
145 "# 24732847234232342892343428.3 == 24732847234232342892343428.1 # True\n",
127 "\n", 146 "\n",
128 "b = False # bool, same as bool(0), bool(''), bool(None)... immutable / constant\n", 147 "my_bool = False # bool, same as bool(0), bool(''), bool(None)... immutable / constant\n",
129 "\n", 148 "\n",
130 "n = None # NoneType, similar to 'null' in other languages, immutable / constant\n", 149 "my_none = None # NoneType, similar to 'null' in other languages, immutable / constant\n",
131 "\n", 150 "\n",
132 "l = [1, False, 'foo'] # list, same as list((1, False, 'foo'))\n", 151 "my_list = [1, False, 'foo'] # list, same as list((1, False, 'foo'))\n",
133 "\n", 152 "\n",
134 "t = (1, False, 'foo') # tuple, same as tuple([1, False, 'foo']), immutable\n", 153 "my_tuple = (1, False, 'foo') # tuple, same as tuple([1, False, 'foo']), immutable\n",
135 "\n", 154 "\n",
136 "d = {'foo': 1, 'bar': 8} # dict, same as dict(foo=1, bar=8), similar to hash in other languages\n", 155 "my_dict = {'foo': 1, 'bar': 8} # dict, same as dict(foo=1, bar=8), similar to hash in other languages\n",
137 "\n", 156 "\n",
138 "s = {'foo', 'bar', 1, 1, 4} # set, same as set(['foo', 'bar', 1, 1, 4]), removes duplicates" 157 "my_set = {'foo', 'bar', 1, 1, 4} # set, same as set(['foo', 'bar', 1, 1, 4]), removes duplicates"
139 ] 158 ]
140 }, 159 },
141 { 160 {
@@ -203,7 +222,7 @@
203 "s3[0] # Out: S\n", 222 "s3[0] # Out: S\n",
204 "s3[-1] # Out: t\n", 223 "s3[-1] # Out: t\n",
205 "s3[1:] # Out: tatnett\n", 224 "s3[1:] # Out: tatnett\n",
206 "s3[1:-1] # Out: tatnett\n", 225 "s3[1:-1] # Out: tatnet\n",
207 "s3[-3:] # Out: ett\n", 226 "s3[-3:] # Out: ett\n",
208 "s3[1:-1:2] # Out: tte\n", 227 "s3[1:-1:2] # Out: tte\n",
209 "\n", 228 "\n",
@@ -223,6 +242,29 @@
223 }, 242 },
224 { 243 {
225 "cell_type": "markdown", 244 "cell_type": "markdown",
245 "id": "25d0f9dc-d783-4a23-a3ee-c0b6cee3d4ab",
246 "metadata": {},
247 "source": [
248 "## Lists vs. tuples\n",
249 "\n",
250 "Tuples are not sumply \"read-only\" lists.\n",
251 "\n",
252 "\"Though tuples may seem similar to lists, they are often used in different situations and for different purposes.\n",
253 "Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking ... or indexing.\n",
254 "Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\n",
255 "\n",
256 "```python\n",
257 "my_tuple = (1,2)\n",
258 "my_list = [1,2] \n",
259 "\n",
260 "# tuples are immutable and hashable, hence they can be used as keys in mapping objects as dicts\n",
261 "d = {my_tuple: 1} # OK\n",
262 "d = {my_list: 1} # Error\n",
263 "```"
264 ]
265 },
266 {
267 "cell_type": "markdown",
226 "id": "6b2bc8fa-a5dc-4a58-92b2-abfb19bfe2ed", 268 "id": "6b2bc8fa-a5dc-4a58-92b2-abfb19bfe2ed",
227 "metadata": {}, 269 "metadata": {},
228 "source": [ 270 "source": [
@@ -302,7 +344,7 @@
302 "id": "40db50e4-fb7a-4a77-a074-bc4413f27212", 344 "id": "40db50e4-fb7a-4a77-a074-bc4413f27212",
303 "metadata": {}, 345 "metadata": {},
304 "source": [ 346 "source": [
305 "## Creating and maintaining a Python environment (cont...)\n", 347 "## Creating and maintaining a Python environment - goals\n",
306 "\n", 348 "\n",
307 "Desired qualities for a flexible Python environment:\n", 349 "Desired qualities for a flexible Python environment:\n",
308 "\n", 350 "\n",
@@ -324,10 +366,11 @@
324 "id": "c661bf56-add0-402c-a629-a857f81d0758", 366 "id": "c661bf56-add0-402c-a629-a857f81d0758",
325 "metadata": {}, 367 "metadata": {},
326 "source": [ 368 "source": [
327 "## Creating and maintaining a Python environment (cont...)\n", 369 "## Creating and maintaining a Python environment - using devbox / WSL / UNIX systems directly\n",
328 "\n", 370 "\n",
329 "Exploting the operating system can be done by:\n", 371 "Exploting the operating system can be done by:\n",
330 "\n", 372 "\n",
373 "- using the official Python packages that are maintained by the OS (Ubuntu packages in the case of WSL @ Statnett or a Red Hat enterprise VM @ Statnett) is often good enough\n",
331 "- (re)defining `PYTHONPATH`\n", 374 "- (re)defining `PYTHONPATH`\n",
332 "- using symlinks to point at packages placed at different locations" 375 "- using symlinks to point at packages placed at different locations"
333 ] 376 ]
@@ -337,7 +380,7 @@
337 "id": "db8952aa-0944-4644-9626-7460786fd72a", 380 "id": "db8952aa-0944-4644-9626-7460786fd72a",
338 "metadata": {}, 381 "metadata": {},
339 "source": [ 382 "source": [
340 "## Creating and maintaining a Python environment (cont...)\n", 383 "## Creating and maintaining a Python environment - using the Python virtual environment - *venv*\n",
341 "\n", 384 "\n",
342 "Using venv can be done by directly invoking python:\n", 385 "Using venv can be done by directly invoking python:\n",
343 "\n", 386 "\n",
@@ -365,10 +408,13 @@
365 "id": "299641e6-3d87-4a86-a124-eb49edb75b4b", 408 "id": "299641e6-3d87-4a86-a124-eb49edb75b4b",
366 "metadata": {}, 409 "metadata": {},
367 "source": [ 410 "source": [
368 "## Creating and maintaining a Python environment (cont...)\n", 411 "## Creating and maintaining a Python environment - using Poetry\n",
369 "\n", 412 "\n",
370 "Poetry [https://python-poetry.org](https://python-poetry.org) is the prefered environment and dependency management tool at Statnett.\n", 413 "Poetry [https://python-poetry.org](https://python-poetry.org) is the prefered environment and dependency management tool at Statnett.\n",
371 "\n", 414 "\n",
415 "Although Poetry creates and uses vierual environment(s) behind the scenes,\n",
416 "it is centered around the concept of \"projects\" and not the virtual environment itself\n",
417 "\n",
372 "```bash\n", 418 "```bash\n",
373 "# create project and a virtual environment from scratch\n", 419 "# create project and a virtual environment from scratch\n",
374 "poetry new my-project\n", 420 "poetry new my-project\n",
@@ -411,7 +457,7 @@
411 { 457 {
412 "data": { 458 "data": {
413 "text/plain": [ 459 "text/plain": [
414 "-42990858669078524" 460 "-3158428850515418558"
415 ] 461 ]
416 }, 462 },
417 "execution_count": 15, 463 "execution_count": 15,
@@ -435,8 +481,8 @@
435 "hash(t) # returns f.i. -6333845781340707986\n", 481 "hash(t) # returns f.i. -6333845781340707986\n",
436 "# hash(l) # TypeError: unhashable type: 'list'\n", 482 "# hash(l) # TypeError: unhashable type: 'list'\n",
437 "\n", 483 "\n",
438 "s = 'the long and winding road'\n", 484 "s = \"the long and winding road\"\n",
439 "s2 = 'the long and winding road'\n", 485 "s2 = \"the long and winding road\"\n",
440 "\n", 486 "\n",
441 "# check if s and s2 are the same object:\n", 487 "# check if s and s2 are the same object:\n",
442 "id(s) # Out: 139858905258704\n", 488 "id(s) # Out: 139858905258704\n",
@@ -816,7 +862,7 @@
816 "# the function will behave differently depending on what parameters were\n", 862 "# the function will behave differently depending on what parameters were\n",
817 "# used when calling its enclosing function (factory function)\n", 863 "# used when calling its enclosing function (factory function)\n",
818 "\n", 864 "\n",
819 "def get_multiplier_of(base: int) -> str:\n", 865 "def get_multiplier_of(base: int):\n",
820 " \"\"\"the function enclosing its nested functions\"\"\"\n", 866 " \"\"\"the function enclosing its nested functions\"\"\"\n",
821 " # this function is the enclosing function of the function `multiplier_function`\n", 867 " # this function is the enclosing function of the function `multiplier_function`\n",
822 "\n", 868 "\n",
@@ -958,19 +1004,7 @@
958 "execution_count": 22, 1004 "execution_count": 22,
959 "id": "4346260d-76af-437e-9c10-1c1d0c478695", 1005 "id": "4346260d-76af-437e-9c10-1c1d0c478695",
960 "metadata": {}, 1006 "metadata": {},
961 "outputs": [ 1007 "outputs": [],
962 {
963 "ename": "NameError",
964 "evalue": "name 'is_admin' is not defined",
965 "output_type": "error",
966 "traceback": [
967 "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
968 "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
969 "Cell \u001b[0;32mIn[22], line 25\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m decorated\n\u001b[1;32m 21\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m api_access_decorator\n\u001b[1;32m 24\u001b[0m \u001b[38;5;129m@requires_access\u001b[39m(access_secret\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mb28cfeaa65b73cf\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m---> 25\u001b[0m \u001b[38;5;129m@is_admin\u001b[39m\n\u001b[1;32m 26\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21msensitive_function\u001b[39m(data, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 27\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"very sensitive function\"\"\"\u001b[39;00m\n\u001b[1;32m 28\u001b[0m db\u001b[38;5;241m.\u001b[39msave(data)\n",
970 "\u001b[0;31mNameError\u001b[0m: name 'is_admin' is not defined"
971 ]
972 }
973 ],
974 "source": [ 1008 "source": [
975 "# Decorators (cont ...) - a complete example\n", 1009 "# Decorators (cont ...) - a complete example\n",
976 "\n", 1010 "\n",
@@ -996,7 +1030,7 @@
996 "\n", 1030 "\n",
997 "\n", 1031 "\n",
998 "@requires_access(access_secret='b28cfeaa65b73cf')\n", 1032 "@requires_access(access_secret='b28cfeaa65b73cf')\n",
999 "@is_admin\n", 1033 "# @is_admin - decorators can be \"chained\"\n",
1000 "def sensitive_function(data, **kwargs):\n", 1034 "def sensitive_function(data, **kwargs):\n",
1001 " \"\"\"very sensitive function\"\"\"\n", 1035 " \"\"\"very sensitive function\"\"\"\n",
1002 " db.save(data)" 1036 " db.save(data)"
@@ -1030,11 +1064,19 @@
1030 "\n", 1064 "\n",
1031 "\n", 1065 "\n",
1032 "# modern Python >= 3.6 f-strings\n", 1066 "# modern Python >= 3.6 f-strings\n",
1033 "f'{s} - {i} - {f:5.2f}' # Out: 'another string - 27 - 6.58'\n", 1067 "f\"{s} - {i} - {f:5.2f}\" # Out: 'another string - 27 - 6.58'\n",
1034 "``` \n", 1068 "``` \n",
1035 "\n", 1069 "\n",
1036 "See https://docs.python.org/3/library/string.html#formatspec for the complete format specification" 1070 "See https://docs.python.org/3/library/string.html#formatspec for the complete format specification"
1037 ] 1071 ]
1072 },
1073 {
1074 "cell_type": "code",
1075 "execution_count": null,
1076 "id": "23972592-e838-4fa7-81f1-ca3ca129e757",
1077 "metadata": {},
1078 "outputs": [],
1079 "source": []
1038 } 1080 }
1039 ], 1081 ],
1040 "metadata": { 1082 "metadata": {
@@ -1053,7 +1095,7 @@
1053 "name": "python", 1095 "name": "python",
1054 "nbconvert_exporter": "python", 1096 "nbconvert_exporter": "python",
1055 "pygments_lexer": "ipython3", 1097 "pygments_lexer": "ipython3",
1056 "version": "3.11.7" 1098 "version": "3.13.9"
1057 } 1099 }
1058 }, 1100 },
1059 "nbformat": 4, 1101 "nbformat": 4,
diff --git a/notebooks/python/python_oo.ipynb b/notebooks/python/python_oo.ipynb
index e38d7f3..1ef2a72 100644
--- a/notebooks/python/python_oo.ipynb
+++ b/notebooks/python/python_oo.ipynb
@@ -66,7 +66,55 @@
66 "\n", 66 "\n",
67 "- improving modularity\n", 67 "- improving modularity\n",
68 "\n", 68 "\n",
69 "- providing foundation for a more intuitive design" 69 "- providing foundation for a more intuitive design\n",
70 "\n",
71 "A pseudo code example:\n",
72 "\n",
73 "```c\n",
74 "/* crating and drawing a (pseudo) widget in C */\n",
75 "my_widget = mylibrary_widget_init(); /* a struct of the type widget */\n",
76 "mylibrary_widget_draw(my_widget);\n",
77 "```\n",
78 "\n",
79 "```python\n",
80 "# creating and drawing a (pseudo) widget in Python\n",
81 "import mylibrary\n",
82 "\n",
83 "my_widget = mylibrary.Widget()\n",
84 "my_widget.draw()\n",
85 "```"
86 ]
87 },
88 {
89 "cell_type": "markdown",
90 "id": "878cb805-a2d7-46a3-a34a-0507495420f7",
91 "metadata": {},
92 "source": [
93 "# Do I need to know / understand OOP when I am programming in Python?\n",
94 "\n",
95 "Most definitely - **yes**.\n",
96 "\n",
97 "While using programming language that is not object-oriented (like *C* and / or *Rust*) is always possible,\n",
98 "when using Python there are no *sane* alternatives that have ever been demonstrated:\n",
99 "\n",
100 "- Understanding OO behaviour and syntax is a must in order to understand Python code in general\n",
101 "- OOP is at the core of Python's design - everything in Python is an object\n",
102 "- Good programming and design is not only about solving a particular task. It is about reusing code and making your code reusable in turn\n",
103 "- Following the priciples described above is more challenging when not using OO in general."
104 ]
105 },
106 {
107 "cell_type": "markdown",
108 "id": "b83dc815-ed51-4e5a-a30e-fa0252217194",
109 "metadata": {},
110 "source": [
111 "# I am doing only simple things with Python. Do I still need to learn OOP?\n",
112 "\n",
113 "Most definitely - **yes**.\n",
114 "\n",
115 "- A simple task provides a great opportunity to learn and practice OOP\n",
116 "- Using OOP **does not mean overcomplicating** your code. When applied correctly - it means the opposite\n",
117 "- Doing \"simple things\" exclusively is not very ambitious approach to programming and learning in general"
70 ] 118 ]
71 }, 119 },
72 { 120 {
@@ -109,13 +157,20 @@
109 "\n", 157 "\n",
110 "- object - an instance of a class that may contain its own attributes as well as references to its class' attributes\n", 158 "- object - an instance of a class that may contain its own attributes as well as references to its class' attributes\n",
111 "\n", 159 "\n",
112 "- attribute - variable, property, function defined in the class and present in its instances\n", 160 "- attribute - variable, property, function defined in the class and present in its instances - in Python: any name following a dot. Attributes may be *read-only* or *writable*. In the latter case, assignment to attributes is possible.\n",
113 "\n", 161 "\n",
114 "- class variable - attribute of which a single copy exists, regardless of how many instances of the class exist\n", 162 "- class variable - attribute of which a single copy exists, regardless of how many instances of the class exist\n",
115 "\n", 163 "\n",
116 "- object / instance variable, object / instance attribute - attribute for which each instantiated object of the class has a separate copy, or instance\n", 164 "- object / instance variable, object / instance attribute - attribute for which each instantiated object of the class has a separate copy, or instance\n",
117 "\n", 165 "\n",
118 "- method - member function - function that is an attribute" 166 "- method - member function - function that is an attribute\n",
167 "\n",
168 "```python\n",
169 "# my_str_obj is an object (instance) of the class str\n",
170 "my_str_object = str(\"Test\") # or my_str_object = \"Test\"\n",
171 "my_str_object.lower # attribute that is a function -> method\n",
172 "my_str_object.lower() # calling the method \"lower\"\n",
173 "```"
119 ] 174 ]
120 }, 175 },
121 { 176 {
@@ -218,7 +273,7 @@
218 }, 273 },
219 { 274 {
220 "cell_type": "code", 275 "cell_type": "code",
221 "execution_count": 1, 276 "execution_count": 41,
222 "id": "a9c30a5d-aa86-45ab-b897-f051df80f1d8", 277 "id": "a9c30a5d-aa86-45ab-b897-f051df80f1d8",
223 "metadata": {}, 278 "metadata": {},
224 "outputs": [ 279 "outputs": [
@@ -226,17 +281,17 @@
226 "name": "stdout", 281 "name": "stdout",
227 "output_type": "stream", 282 "output_type": "stream",
228 "text": [ 283 "text": [
229 "car1.get_obj_info_str() = \"I am <__main__.Car object at 0x7f70e643d590> with id 140122876269968 from <class '__main__.Car'> with id 93964809433504\"\n", 284 "car1.get_obj_info_str() = \"I am <__main__.Car object at 0x7fd00029ff80> with id 140531332677504 from <class '__main__.Car'> with id 93887620597168\"\n",
230 "car2.get_obj_info_str() = \"I am <__main__.Car object at 0x7f70e63ddf10> with id 140122875879184 from <class '__main__.Car'> with id 93964809433504\"\n", 285 "car2.get_obj_info_str() = \"I am <__main__.Car object at 0x7fd00029ea50> with id 140531332672080 from <class '__main__.Car'> with id 93887620597168\"\n",
231 "car1.model = 'BMW', car1.reg_nr = 'EC76183', car1.extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140122875882432, id(car1.get_obj_info_str) = 140122875878080\n", 286 "car1.model = 'BMW', car1.reg_nr = 'EC76183', car1.extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140531337557440, id(car1.get_obj_info_str) = 140531251735488\n",
232 "car2.model = 'Scoda', car2.reg_nr = 'BD77655', car2.extras = ['GPSnav'], id(car2.cls_extras) = 140122875882432, id(car2.get_obj_info_str) = 140122875880192\n", 287 "car2.model = 'Scoda', car2.reg_nr = 'BD77655', car2.extras = ['GPSnav'], id(car2.cls_extras) = 140531337557440, id(car2.get_obj_info_str) = 140531251728064\n",
233 "id(Car.cls_extras) = 140122875882432, id(Car.get_obj_info_str) = 140122875964416\n", 288 "id(Car.cls_extras) = 140531337557440, id(Car.get_obj_info_str) = 140531252010400\n",
234 "car1.cls_extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140122875882432\n", 289 "car1.cls_extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140531337557440\n",
235 "car2.cls_extras = ['GPSnav', 'Sound system'], id(car2.cls_extras) = 140122875882432\n", 290 "car2.cls_extras = ['GPSnav', 'Sound system'], id(car2.cls_extras) = 140531337557440\n",
236 "True\n", 291 "True\n",
237 "hasattr(car1, 'import_tax_paid') = True\n", 292 "hasattr(car1, 'import_tax_paid') = True\n",
238 "hasattr(car2, 'import_tax_paid') = False\n", 293 "hasattr(car2, 'import_tax_paid') = False\n",
239 "id(car1.__class__) = 93964809433504, id(car2.__class__) = 93964809433504, id(Car) = 93964809433504\n" 294 "id(car1.__class__) = 93887620597168, id(car2.__class__) = 93887620597168, id(Car) = 93887620597168\n"
240 ] 295 ]
241 } 296 }
242 ], 297 ],
@@ -288,7 +343,7 @@
288 "# - checks if 'attr' is an instance attribute\n", 343 "# - checks if 'attr' is an instance attribute\n",
289 "# - checks if 'attr' is a class attribute (through the method resolution order - MRO)\n", 344 "# - checks if 'attr' is a class attribute (through the method resolution order - MRO)\n",
290 "# - raises AttributeError\n", 345 "# - raises AttributeError\n",
291 "# setter:\n", 346 "# setter (making the attribute writable):\n",
292 "# - (re)defines an instance attribute\n", 347 "# - (re)defines an instance attribute\n",
293 "\n", 348 "\n",
294 "# Details not covered in this course:\n", 349 "# Details not covered in this course:\n",
@@ -323,7 +378,7 @@
323 }, 378 },
324 { 379 {
325 "cell_type": "code", 380 "cell_type": "code",
326 "execution_count": 2, 381 "execution_count": 42,
327 "id": "391e816e-c53b-454b-9e9f-3587234f1fea", 382 "id": "391e816e-c53b-454b-9e9f-3587234f1fea",
328 "metadata": {}, 383 "metadata": {},
329 "outputs": [ 384 "outputs": [
@@ -413,7 +468,7 @@
413 }, 468 },
414 { 469 {
415 "cell_type": "code", 470 "cell_type": "code",
416 "execution_count": 3, 471 "execution_count": 43,
417 "id": "e82feefb-025d-4cbb-a8ac-ddc3cc01269c", 472 "id": "e82feefb-025d-4cbb-a8ac-ddc3cc01269c",
418 "metadata": {}, 473 "metadata": {},
419 "outputs": [ 474 "outputs": [
@@ -495,7 +550,7 @@
495 }, 550 },
496 { 551 {
497 "cell_type": "code", 552 "cell_type": "code",
498 "execution_count": 4, 553 "execution_count": 44,
499 "id": "71a90ec6-3cc0-441e-a866-c2c2b9984559", 554 "id": "71a90ec6-3cc0-441e-a866-c2c2b9984559",
500 "metadata": {}, 555 "metadata": {},
501 "outputs": [ 556 "outputs": [
@@ -613,7 +668,7 @@
613 }, 668 },
614 { 669 {
615 "cell_type": "code", 670 "cell_type": "code",
616 "execution_count": 5, 671 "execution_count": 45,
617 "id": "a4d2735d-d167-4f43-ad55-cb3d13ece2dc", 672 "id": "a4d2735d-d167-4f43-ad55-cb3d13ece2dc",
618 "metadata": {}, 673 "metadata": {},
619 "outputs": [ 674 "outputs": [
@@ -656,7 +711,7 @@
656 }, 711 },
657 { 712 {
658 "cell_type": "code", 713 "cell_type": "code",
659 "execution_count": 6, 714 "execution_count": 46,
660 "id": "a6ead5e6-e987-4bda-bf48-30f91877278d", 715 "id": "a6ead5e6-e987-4bda-bf48-30f91877278d",
661 "metadata": {}, 716 "metadata": {},
662 "outputs": [ 717 "outputs": [
@@ -715,7 +770,7 @@
715 }, 770 },
716 { 771 {
717 "cell_type": "code", 772 "cell_type": "code",
718 "execution_count": 7, 773 "execution_count": 47,
719 "id": "11f6a3fb-64d7-40a9-a130-27548ec4b802", 774 "id": "11f6a3fb-64d7-40a9-a130-27548ec4b802",
720 "metadata": {}, 775 "metadata": {},
721 "outputs": [ 776 "outputs": [
@@ -797,7 +852,7 @@
797 }, 852 },
798 { 853 {
799 "cell_type": "code", 854 "cell_type": "code",
800 "execution_count": 8, 855 "execution_count": 48,
801 "id": "c345a69f-02da-40e7-bb37-c0511b6af096", 856 "id": "c345a69f-02da-40e7-bb37-c0511b6af096",
802 "metadata": {}, 857 "metadata": {},
803 "outputs": [ 858 "outputs": [
@@ -806,6 +861,9 @@
806 "output_type": "stream", 861 "output_type": "stream",
807 "text": [ 862 "text": [
808 "9\n", 863 "9\n",
864 "9\n",
865 "140531251904896\n",
866 "140531251904896\n",
809 "Vector.from_str('1:1:5:6') = Vector(start=Point(x=1, y=1), end=Point(x=5, y=6))\n" 867 "Vector.from_str('1:1:5:6') = Vector(start=Point(x=1, y=1), end=Point(x=5, y=6))\n"
810 ] 868 ]
811 } 869 }
@@ -837,7 +895,14 @@
837 " Point(coordinates[2], coordinates[3]),\n", 895 " Point(coordinates[2], coordinates[3]),\n",
838 " )\n", 896 " )\n",
839 "\n", 897 "\n",
840 "print(Point.get_manhattan_distance(Point(1, 1), Point(5, 6)))\n", 898 "point1 = Point(1, 1)\n",
899 "point2 = Point(5, 6)\n",
900 "print(f\"{point1.get_manhattan_distance(point1, point2)}\") # works, but should not be used in that way\n",
901 "print(f\"{Point.get_manhattan_distance(point1, point2)}\")\n",
902 "print(f\"{id(point1.get_manhattan_distance)}\")\n",
903 "print(f\"{id(point2.get_manhattan_distance)}\")\n",
904 "# print(f\"{id(point1.from_str)}\") <-- not available as an instance attribute\n",
905 "\n",
841 "print(f\"{Vector.from_str('1:1:5:6') = }\")" 906 "print(f\"{Vector.from_str('1:1:5:6') = }\")"
842 ] 907 ]
843 }, 908 },
@@ -890,7 +955,7 @@
890 }, 955 },
891 { 956 {
892 "cell_type": "code", 957 "cell_type": "code",
893 "execution_count": 9, 958 "execution_count": 49,
894 "id": "44d47e1c-1a06-4577-8db6-6ec78ce5cef8", 959 "id": "44d47e1c-1a06-4577-8db6-6ec78ce5cef8",
895 "metadata": {}, 960 "metadata": {},
896 "outputs": [ 961 "outputs": [
@@ -903,32 +968,32 @@
903 "\n", 968 "\n",
904 "class TigerShark(Fish)\n", 969 "class TigerShark(Fish)\n",
905 " | TigerShark(weight: int, alive: bool = True, **kwargs)\n", 970 " | TigerShark(weight: int, alive: bool = True, **kwargs)\n",
906 " | \n", 971 " |\n",
907 " | Base class for all tiger sharks\n", 972 " | Base class for all tiger sharks\n",
908 " | \n", 973 " |\n",
909 " | Method resolution order:\n", 974 " | Method resolution order:\n",
910 " | TigerShark\n", 975 " | TigerShark\n",
911 " | Fish\n", 976 " | Fish\n",
912 " | Animal\n", 977 " | Animal\n",
913 " | builtins.object\n", 978 " | builtins.object\n",
914 " | \n", 979 " |\n",
915 " | Methods defined here:\n", 980 " | Methods defined here:\n",
916 " | \n", 981 " |\n",
917 " | __init__(self, weight: int, alive: bool = True, **kwargs)\n", 982 " | __init__(self, weight: int, alive: bool = True, **kwargs)\n",
918 " | Initialize self. See help(type(self)) for accurate signature.\n", 983 " | Initialize self. See help(type(self)) for accurate signature.\n",
919 " | \n", 984 " |\n",
920 " | ----------------------------------------------------------------------\n", 985 " | ----------------------------------------------------------------------\n",
921 " | Data descriptors inherited from Animal:\n", 986 " | Data descriptors inherited from Animal:\n",
922 " | \n", 987 " |\n",
923 " | __dict__\n", 988 " | __dict__\n",
924 " | dictionary for instance variables (if defined)\n", 989 " | dictionary for instance variables\n",
925 " | \n", 990 " |\n",
926 " | __weakref__\n", 991 " | __weakref__\n",
927 " | list of weak references to the object (if defined)\n", 992 " | list of weak references to the object\n",
928 " | \n", 993 " |\n",
929 " | alive\n", 994 " | alive\n",
930 " | getter property alive\n", 995 " | getter property alive\n",
931 " | \n", 996 " |\n",
932 " | weight\n", 997 " | weight\n",
933 " | getter property weight\n", 998 " | getter property weight\n",
934 "\n", 999 "\n",
@@ -1064,7 +1129,7 @@
1064 }, 1129 },
1065 { 1130 {
1066 "cell_type": "code", 1131 "cell_type": "code",
1067 "execution_count": 10, 1132 "execution_count": 50,
1068 "id": "e6076925-75f0-456c-aed5-7db0a24a67a1", 1133 "id": "e6076925-75f0-456c-aed5-7db0a24a67a1",
1069 "metadata": {}, 1134 "metadata": {},
1070 "outputs": [ 1135 "outputs": [
@@ -1169,7 +1234,7 @@
1169 }, 1234 },
1170 { 1235 {
1171 "cell_type": "code", 1236 "cell_type": "code",
1172 "execution_count": 11, 1237 "execution_count": 51,
1173 "id": "cf79617a-95d2-48f2-be66-fa1fa34e4e04", 1238 "id": "cf79617a-95d2-48f2-be66-fa1fa34e4e04",
1174 "metadata": {}, 1239 "metadata": {},
1175 "outputs": [ 1240 "outputs": [
@@ -1192,14 +1257,14 @@
1192 " | Adam\n", 1257 " | Adam\n",
1193 " | Eve\n", 1258 " | Eve\n",
1194 " | builtins.object\n", 1259 " | builtins.object\n",
1195 " | \n", 1260 " |\n",
1196 " | Data descriptors inherited from Adam:\n", 1261 " | Data descriptors inherited from Adam:\n",
1197 " | \n", 1262 " |\n",
1198 " | __dict__\n", 1263 " | __dict__\n",
1199 " | dictionary for instance variables (if defined)\n", 1264 " | dictionary for instance variables\n",
1200 " | \n", 1265 " |\n",
1201 " | __weakref__\n", 1266 " | __weakref__\n",
1202 " | list of weak references to the object (if defined)\n", 1267 " | list of weak references to the object\n",
1203 "\n" 1268 "\n"
1204 ] 1269 ]
1205 } 1270 }
@@ -1256,7 +1321,7 @@
1256 }, 1321 },
1257 { 1322 {
1258 "cell_type": "code", 1323 "cell_type": "code",
1259 "execution_count": 12, 1324 "execution_count": 52,
1260 "id": "95a1d0cc-0006-4ddc-b08d-d0b8b02acfba", 1325 "id": "95a1d0cc-0006-4ddc-b08d-d0b8b02acfba",
1261 "metadata": {}, 1326 "metadata": {},
1262 "outputs": [ 1327 "outputs": [
@@ -1324,7 +1389,7 @@
1324 }, 1389 },
1325 { 1390 {
1326 "cell_type": "code", 1391 "cell_type": "code",
1327 "execution_count": 13, 1392 "execution_count": 53,
1328 "id": "38066107-0190-4383-ba14-a4e9cd454a9c", 1393 "id": "38066107-0190-4383-ba14-a4e9cd454a9c",
1329 "metadata": {}, 1394 "metadata": {},
1330 "outputs": [ 1395 "outputs": [
@@ -1418,7 +1483,7 @@
1418 "name": "python", 1483 "name": "python",
1419 "nbconvert_exporter": "python", 1484 "nbconvert_exporter": "python",
1420 "pygments_lexer": "ipython3", 1485 "pygments_lexer": "ipython3",
1421 "version": "3.11.7" 1486 "version": "3.13.9"
1422 } 1487 }
1423 }, 1488 },
1424 "nbformat": 4, 1489 "nbformat": 4,