├── .gitignore ├── LICENSE ├── README.md ├── apple-health-analytics ├── README.md └── applehealth.ipynb ├── chatbot-gui ├── app.py ├── chat.py ├── intents.json ├── model.py ├── nltk_utils.py └── train.py ├── file-explorer └── file-explorer.py ├── file-organizing ├── file_organizing.py └── organize-desktop.py ├── googleimagedownloader └── main.py ├── image-viewer └── image-viewer.py ├── moviepicker └── main.py ├── note-take └── note-take.py ├── notetaking-speech-rec ├── README.md ├── main.py └── notion.py ├── paint └── paint.py ├── photo-restoration ├── .env ├── README.md ├── main.py ├── photo_restorer.py ├── screenshot.png ├── static │ └── images │ │ └── example.jpeg └── templates │ └── index.html ├── snake-game └── snake.py ├── snake-pygame ├── arial.ttf └── snake_game.py ├── stockprediction └── main.py ├── stopwatch └── stopwatch.py ├── text-editor └── text-editor.py ├── to-do └── to-do.py ├── todocli-tutorial ├── database.py ├── model.py └── todocli.py └── webapps ├── django ├── README.md └── todoapp │ ├── manage.py │ ├── templates │ └── base.html │ ├── todoapp │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ └── todolist │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── fastapi ├── README.md ├── app.py ├── database.py ├── models.py └── templates │ └── base.html └── flask ├── README.md ├── app.py └── templates └── base.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.mo 2 | *.vo 3 | *.pyc 4 | venv 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Patrick Loeber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Fun and useful projects with Python 2 | 3 | You can find the corresponding tutorials on my channel: [https://www.youtube.com/c/PythonEngineer](https://www.youtube.com/c/PythonEngineer) 4 | -------------------------------------------------------------------------------- /apple-health-analytics/README.md: -------------------------------------------------------------------------------- 1 | ## Analyze Apple Health data 2 | 3 | Dependencies: numpy, pandas, matplotlib 4 | 5 | ## Export health data 6 | 7 | On iPhone go to Health App -> Profile -> Export data -> Send to your computer 8 | 9 | -------------------------------------------------------------------------------- /apple-health-analytics/applehealth.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 63, 6 | "id": "b8d3b21b", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import xml.etree.ElementTree as ET\n", 11 | "#from lxml import etree\n", 12 | "import pandas as pd\n", 13 | "import time\n", 14 | "import numpy as np\n", 15 | "import datetime as dt\n", 16 | "# dt.datetime.strptime" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 64, 22 | "id": "87e15e18", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# create element tree object\n", 27 | "\n", 28 | "tree = ET.parse('data/export.xml') \n", 29 | "# for every health record, extract the attributes into a dictionary (columns). Then create a list (rows)\n", 30 | "root = tree.getroot()\n", 31 | "record_list = [x.attrib for x in root.iter('Record')]\n", 32 | "\n", 33 | "# create DataFrame from a list (rows) of dictionaries (columns)\n", 34 | "record_data = pd.DataFrame(record_list)\n", 35 | "\n", 36 | "# proper type to dates\n", 37 | "for col in ['creationDate', 'startDate', 'endDate']:\n", 38 | " record_data[col] = pd.to_datetime(record_data[col])\n", 39 | "\n", 40 | "# value is numeric, NaN if fails\n", 41 | "record_data['value'] = pd.to_numeric(record_data['value'], errors='coerce')\n", 42 | "\n", 43 | "# some records do not measure anything, just count occurences\n", 44 | "# filling with 1.0 (= one time) makes it easier to aggregate\n", 45 | "record_data['value'] = record_data['value'].fillna(1.0)\n", 46 | "\n", 47 | "# shorter observation names: use vectorized replace function\n", 48 | "record_data['type'] = record_data['type'].str.replace('HKQuantityTypeIdentifier', '')\n", 49 | "record_data['type'] = record_data['type'].str.replace('HKCategoryTypeIdentifier', '')" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 65, 55 | "id": "5d40e382", 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "data": { 60 | "text/html": [ 61 | "
\n", 62 | "\n", 75 | "\n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | "
typesourceNamesourceVersionunitcreationDatestartDateendDatevaluedevice
743765HeartRateVariabilitySDNNApple Watch von Patrick7.6.1ms2021-08-20 21:44:34+02:002021-08-20 21:43:28+02:002021-08-20 21:44:34+02:00106.2270<<HKDevice: 0x2811a01e0>, name:Apple Watch, ma...
743766HeartRateVariabilitySDNNApple Watch von Patrick7.6.1ms2021-08-20 21:48:35+02:002021-08-20 21:47:30+02:002021-08-20 21:48:35+02:0060.9539<<HKDevice: 0x2811a01e0>, name:Apple Watch, ma...
743767HeartRateVariabilitySDNNApple Watch von Patrick7.6.1ms2021-08-22 08:19:29+02:002021-08-22 08:18:23+02:002021-08-22 08:19:29+02:0071.2487<<HKDevice: 0x2811a01e0>, name:Apple Watch, ma...
743768HeartRateVariabilitySDNNApple Watch von Patrick7.6.1ms2021-08-22 09:52:59+02:002021-08-22 09:51:54+02:002021-08-22 09:52:59+02:0087.2668<<HKDevice: 0x2811a01e0>, name:Apple Watch, ma...
743769HeartRateVariabilitySDNNApple Watch von Patrick7.6.1ms2021-08-22 14:00:49+02:002021-08-22 13:59:45+02:002021-08-22 14:00:49+02:0042.2700<<HKDevice: 0x2811a01e0>, name:Apple Watch, ma...
\n", 153 | "
" 154 | ], 155 | "text/plain": [ 156 | " type sourceName sourceVersion unit \\\n", 157 | "743765 HeartRateVariabilitySDNN Apple Watch von Patrick 7.6.1 ms \n", 158 | "743766 HeartRateVariabilitySDNN Apple Watch von Patrick 7.6.1 ms \n", 159 | "743767 HeartRateVariabilitySDNN Apple Watch von Patrick 7.6.1 ms \n", 160 | "743768 HeartRateVariabilitySDNN Apple Watch von Patrick 7.6.1 ms \n", 161 | "743769 HeartRateVariabilitySDNN Apple Watch von Patrick 7.6.1 ms \n", 162 | "\n", 163 | " creationDate startDate \\\n", 164 | "743765 2021-08-20 21:44:34+02:00 2021-08-20 21:43:28+02:00 \n", 165 | "743766 2021-08-20 21:48:35+02:00 2021-08-20 21:47:30+02:00 \n", 166 | "743767 2021-08-22 08:19:29+02:00 2021-08-22 08:18:23+02:00 \n", 167 | "743768 2021-08-22 09:52:59+02:00 2021-08-22 09:51:54+02:00 \n", 168 | "743769 2021-08-22 14:00:49+02:00 2021-08-22 13:59:45+02:00 \n", 169 | "\n", 170 | " endDate value \\\n", 171 | "743765 2021-08-20 21:44:34+02:00 106.2270 \n", 172 | "743766 2021-08-20 21:48:35+02:00 60.9539 \n", 173 | "743767 2021-08-22 08:19:29+02:00 71.2487 \n", 174 | "743768 2021-08-22 09:52:59+02:00 87.2668 \n", 175 | "743769 2021-08-22 14:00:49+02:00 42.2700 \n", 176 | "\n", 177 | " device \n", 178 | "743765 <, name:Apple Watch, ma... \n", 179 | "743766 <, name:Apple Watch, ma... \n", 180 | "743767 <, name:Apple Watch, ma... \n", 181 | "743768 <, name:Apple Watch, ma... \n", 182 | "743769 <, name:Apple Watch, ma... " 183 | ] 184 | }, 185 | "execution_count": 65, 186 | "metadata": {}, 187 | "output_type": "execute_result" 188 | } 189 | ], 190 | "source": [ 191 | "record_data.tail()" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 66, 197 | "id": "b25f460b", 198 | "metadata": {}, 199 | "outputs": [ 200 | { 201 | "name": "stdout", 202 | "output_type": "stream", 203 | "text": [ 204 | "\n", 205 | "RangeIndex: 743770 entries, 0 to 743769\n", 206 | "Data columns (total 9 columns):\n", 207 | " # Column Non-Null Count Dtype \n", 208 | "--- ------ -------------- ----- \n", 209 | " 0 type 743770 non-null object \n", 210 | " 1 sourceName 743770 non-null object \n", 211 | " 2 sourceVersion 738284 non-null object \n", 212 | " 3 unit 739683 non-null object \n", 213 | " 4 creationDate 743770 non-null datetime64[ns, pytz.FixedOffset(120)]\n", 214 | " 5 startDate 743770 non-null datetime64[ns, pytz.FixedOffset(120)]\n", 215 | " 6 endDate 743770 non-null datetime64[ns, pytz.FixedOffset(120)]\n", 216 | " 7 value 743770 non-null float64 \n", 217 | " 8 device 697925 non-null object \n", 218 | "dtypes: datetime64[ns, pytz.FixedOffset(120)](3), float64(1), object(5)\n", 219 | "memory usage: 51.1+ MB\n" 220 | ] 221 | } 222 | ], 223 | "source": [ 224 | "record_data.info()" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": 67, 230 | "id": "8b0198ea", 231 | "metadata": {}, 232 | "outputs": [ 233 | { 234 | "data": { 235 | "text/html": [ 236 | "
\n", 237 | "\n", 250 | "\n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | " \n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | " \n", 306 | " \n", 307 | " \n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | " \n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | "
TypedurationdurationUnittotalDistancetotalDistanceUnittotalEnergyBurnedtotalEnergyBurnedUnitsourceNamesourceVersiondevicecreationDatestartDateendDate
180Running18.876555min3.026288km202.358105kcalApple Watch von Patrick7.2<<HKDevice: 0x2811cf160>, name:Apple Watch, ma...2021-08-18 08:08:20+02:002021-08-18 07:49:27+02:002021-08-18 08:08:19+02:00
181Running41.686440min7.154993km481.677116kcalApple Watch von Patrick7.2<<HKDevice: 0x2811cf160>, name:Apple Watch, ma...2021-08-19 09:49:37+02:002021-08-19 09:07:54+02:002021-08-19 09:49:36+02:00
182Running19.186911min3.028681km201.441305kcalApple Watch von Patrick7.6.1<<HKDevice: 0x2811da490>, name:Apple Watch, ma...2021-08-20 19:03:11+02:002021-08-20 18:43:58+02:002021-08-20 19:03:09+02:00
183Running20.134952min3.035656km199.521000kcalApple Watch von Patrick7.6.1<<HKDevice: 0x2811da490>, name:Apple Watch, ma...2021-08-21 17:29:21+02:002021-08-21 17:09:11+02:002021-08-21 17:29:19+02:00
184Yoga22.080947min0.000000km50.902514kcalApple Watch von Patrick7.6.1<<HKDevice: 0x2811da490>, name:Apple Watch, ma...2021-08-22 08:47:27+02:002021-08-22 08:25:21+02:002021-08-22 08:47:26+02:00
\n", 352 | "
" 353 | ], 354 | "text/plain": [ 355 | " Type duration durationUnit totalDistance totalDistanceUnit \\\n", 356 | "180 Running 18.876555 min 3.026288 km \n", 357 | "181 Running 41.686440 min 7.154993 km \n", 358 | "182 Running 19.186911 min 3.028681 km \n", 359 | "183 Running 20.134952 min 3.035656 km \n", 360 | "184 Yoga 22.080947 min 0.000000 km \n", 361 | "\n", 362 | " totalEnergyBurned totalEnergyBurnedUnit sourceName \\\n", 363 | "180 202.358105 kcal Apple Watch von Patrick \n", 364 | "181 481.677116 kcal Apple Watch von Patrick \n", 365 | "182 201.441305 kcal Apple Watch von Patrick \n", 366 | "183 199.521000 kcal Apple Watch von Patrick \n", 367 | "184 50.902514 kcal Apple Watch von Patrick \n", 368 | "\n", 369 | " sourceVersion device \\\n", 370 | "180 7.2 <, name:Apple Watch, ma... \n", 371 | "181 7.2 <, name:Apple Watch, ma... \n", 372 | "182 7.6.1 <, name:Apple Watch, ma... \n", 373 | "183 7.6.1 <, name:Apple Watch, ma... \n", 374 | "184 7.6.1 <, name:Apple Watch, ma... \n", 375 | "\n", 376 | " creationDate startDate \\\n", 377 | "180 2021-08-18 08:08:20+02:00 2021-08-18 07:49:27+02:00 \n", 378 | "181 2021-08-19 09:49:37+02:00 2021-08-19 09:07:54+02:00 \n", 379 | "182 2021-08-20 19:03:11+02:00 2021-08-20 18:43:58+02:00 \n", 380 | "183 2021-08-21 17:29:21+02:00 2021-08-21 17:09:11+02:00 \n", 381 | "184 2021-08-22 08:47:27+02:00 2021-08-22 08:25:21+02:00 \n", 382 | "\n", 383 | " endDate \n", 384 | "180 2021-08-18 08:08:19+02:00 \n", 385 | "181 2021-08-19 09:49:36+02:00 \n", 386 | "182 2021-08-20 19:03:09+02:00 \n", 387 | "183 2021-08-21 17:29:19+02:00 \n", 388 | "184 2021-08-22 08:47:26+02:00 " 389 | ] 390 | }, 391 | "execution_count": 67, 392 | "metadata": {}, 393 | "output_type": "execute_result" 394 | } 395 | ], 396 | "source": [ 397 | "workout_list = [x.attrib for x in root.iter('Workout')]\n", 398 | "\n", 399 | "# create DataFrame from a list (rows) of dictionaries (columns)\n", 400 | "workout_data = pd.DataFrame(workout_list)\n", 401 | "workout_data['workoutActivityType'] = workout_data['workoutActivityType'].str.replace('HKWorkoutActivityType', '')\n", 402 | "workout_data = workout_data.rename({\"workoutActivityType\": \"Type\"}, axis=1)\n", 403 | "# proper type to dates\n", 404 | "for col in ['creationDate', 'startDate', 'endDate']:\n", 405 | " workout_data[col] = pd.to_datetime(workout_data[col])\n", 406 | " \n", 407 | "workout_data['duration'] = pd.to_numeric(workout_data['duration'])\n", 408 | "workout_data['totalEnergyBurned'] = pd.to_numeric(workout_data['totalEnergyBurned'])\n", 409 | "workout_data['totalDistance'] = pd.to_numeric(workout_data['totalDistance'])\n", 410 | "workout_data.tail()" 411 | ] 412 | }, 413 | { 414 | "cell_type": "code", 415 | "execution_count": 68, 416 | "id": "a9c05a3f", 417 | "metadata": {}, 418 | "outputs": [ 419 | { 420 | "data": { 421 | "text/html": [ 422 | "
\n", 423 | "\n", 436 | "\n", 437 | " \n", 438 | " \n", 439 | " \n", 440 | " \n", 441 | " \n", 442 | " \n", 443 | " \n", 444 | " \n", 445 | " \n", 446 | " \n", 447 | " \n", 448 | " \n", 449 | " \n", 450 | " \n", 451 | " \n", 452 | " \n", 453 | " \n", 454 | " \n", 455 | " \n", 456 | " \n", 457 | " \n", 458 | " \n", 459 | " \n", 460 | " \n", 461 | " \n", 462 | " \n", 463 | " \n", 464 | " \n", 465 | " \n", 466 | " \n", 467 | " \n", 468 | " \n", 469 | " \n", 470 | " \n", 471 | " \n", 472 | " \n", 473 | "
TypedurationdurationUnittotalDistancetotalDistanceUnittotalEnergyBurnedtotalEnergyBurnedUnitsourceNamesourceVersiondevicecreationDatestartDateendDate
183Running20.134952min3.035656km199.521kcalApple Watch von Patrick7.6.1<<HKDevice: 0x2811da490>, name:Apple Watch, ma...2021-08-21 17:29:21+02:002021-08-21 17:09:11+02:002021-08-21 17:29:19+02:00
\n", 474 | "
" 475 | ], 476 | "text/plain": [ 477 | " Type duration durationUnit totalDistance totalDistanceUnit \\\n", 478 | "183 Running 20.134952 min 3.035656 km \n", 479 | "\n", 480 | " totalEnergyBurned totalEnergyBurnedUnit sourceName \\\n", 481 | "183 199.521 kcal Apple Watch von Patrick \n", 482 | "\n", 483 | " sourceVersion device \\\n", 484 | "183 7.6.1 <, name:Apple Watch, ma... \n", 485 | "\n", 486 | " creationDate startDate \\\n", 487 | "183 2021-08-21 17:29:21+02:00 2021-08-21 17:09:11+02:00 \n", 488 | "\n", 489 | " endDate \n", 490 | "183 2021-08-21 17:29:19+02:00 " 491 | ] 492 | }, 493 | "execution_count": 68, 494 | "metadata": {}, 495 | "output_type": "execute_result" 496 | } 497 | ], 498 | "source": [ 499 | "last_run = workout_data.iloc[[-2]]\n", 500 | "last_run" 501 | ] 502 | }, 503 | { 504 | "cell_type": "code", 505 | "execution_count": 69, 506 | "id": "29bfeadd", 507 | "metadata": {}, 508 | "outputs": [], 509 | "source": [ 510 | "def get_heartrate_for_date(start, end, heartrate):\n", 511 | " heartrate = heartrate[heartrate[\"startDate\"] >= start]\n", 512 | " heartrate = heartrate[heartrate[\"endDate\"] <= end]\n", 513 | " return heartrate\n", 514 | "\n", 515 | "def get_heartrate_for_workout(workout, heartrate):\n", 516 | " return get_heartrate_for_date(workout[\"startDate\"].item(), workout[\"endDate\"].item(), heartrate)" 517 | ] 518 | }, 519 | { 520 | "cell_type": "code", 521 | "execution_count": 70, 522 | "id": "fa4b7796", 523 | "metadata": {}, 524 | "outputs": [ 525 | { 526 | "data": { 527 | "text/plain": [ 528 | "(73.0, 136.0, 125.71784232365145)" 529 | ] 530 | }, 531 | "execution_count": 70, 532 | "metadata": {}, 533 | "output_type": "execute_result" 534 | } 535 | ], 536 | "source": [ 537 | "heartrate = record_data[record_data[\"type\"] == \"HeartRate\"]\n", 538 | "\n", 539 | "heartrate = get_heartrate_for_workout(last_run, heartrate)\n", 540 | "minh = heartrate[\"value\"].min()\n", 541 | "maxh = heartrate[\"value\"].max()\n", 542 | "meanh = heartrate[\"value\"].mean()\n", 543 | "minh, maxh, meanh" 544 | ] 545 | }, 546 | { 547 | "cell_type": "code", 548 | "execution_count": 71, 549 | "id": "68be0af3", 550 | "metadata": {}, 551 | "outputs": [ 552 | { 553 | "data": { 554 | "text/plain": [ 555 | "" 556 | ] 557 | }, 558 | "execution_count": 71, 559 | "metadata": {}, 560 | "output_type": "execute_result" 561 | }, 562 | { 563 | "data": { 564 | "image/png": "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\n", 565 | "text/plain": [ 566 | "
" 567 | ] 568 | }, 569 | "metadata": { 570 | "needs_background": "light" 571 | }, 572 | "output_type": "display_data" 573 | } 574 | ], 575 | "source": [ 576 | "heartrate.plot(x='endDate', y='value', style='r|', markersize=8.5, figsize=(12, 6))" 577 | ] 578 | }, 579 | { 580 | "cell_type": "code", 581 | "execution_count": 72, 582 | "id": "a15cb5d6", 583 | "metadata": {}, 584 | "outputs": [], 585 | "source": [ 586 | "today = dt.date.today()\n", 587 | "\n", 588 | "xdaysago = today - dt.timedelta(days=7)\n", 589 | "first_of_month = today - dt.timedelta(days=today.day - 1)" 590 | ] 591 | }, 592 | { 593 | "cell_type": "code", 594 | "execution_count": 73, 595 | "id": "868b3b70", 596 | "metadata": {}, 597 | "outputs": [ 598 | { 599 | "data": { 600 | "text/plain": [ 601 | "datetime.date(2021, 8, 22)" 602 | ] 603 | }, 604 | "execution_count": 73, 605 | "metadata": {}, 606 | "output_type": "execute_result" 607 | } 608 | ], 609 | "source": [ 610 | "today" 611 | ] 612 | }, 613 | { 614 | "cell_type": "code", 615 | "execution_count": 74, 616 | "id": "0718242f", 617 | "metadata": {}, 618 | "outputs": [ 619 | { 620 | "data": { 621 | "text/plain": [ 622 | "datetime.date(2021, 8, 15)" 623 | ] 624 | }, 625 | "execution_count": 74, 626 | "metadata": {}, 627 | "output_type": "execute_result" 628 | } 629 | ], 630 | "source": [ 631 | "xdaysago" 632 | ] 633 | }, 634 | { 635 | "cell_type": "code", 636 | "execution_count": 75, 637 | "id": "ca6b2329", 638 | "metadata": {}, 639 | "outputs": [ 640 | { 641 | "data": { 642 | "text/plain": [ 643 | "datetime.date(2021, 8, 1)" 644 | ] 645 | }, 646 | "execution_count": 75, 647 | "metadata": {}, 648 | "output_type": "execute_result" 649 | } 650 | ], 651 | "source": [ 652 | "first_of_month" 653 | ] 654 | }, 655 | { 656 | "cell_type": "code", 657 | "execution_count": 76, 658 | "id": "d4f6ffad", 659 | "metadata": {}, 660 | "outputs": [], 661 | "source": [ 662 | "xdaysago = today - dt.timedelta(days=33)\n", 663 | "\n", 664 | "time_to_check = pd.to_datetime(first_of_month, utc=True)\n", 665 | "time_to_check = pd.to_datetime(xdaysago, utc=True)\n", 666 | "runs_last_month = workout_data[workout_data[\"creationDate\"] >= time_to_check]\n", 667 | "runs_last_month = runs_last_month[runs_last_month[\"Type\"] == \"Running\"]\n", 668 | "runs_last_month = runs_last_month.drop(columns=[\"device\", \"sourceVersion\"])" 669 | ] 670 | }, 671 | { 672 | "cell_type": "code", 673 | "execution_count": 77, 674 | "id": "8c7310fc", 675 | "metadata": {}, 676 | "outputs": [ 677 | { 678 | "data": { 679 | "text/plain": [ 680 | "(755.4174678126972, 129.0223653750448, 8521.206610642746)" 681 | ] 682 | }, 683 | "execution_count": 77, 684 | "metadata": {}, 685 | "output_type": "execute_result" 686 | } 687 | ], 688 | "source": [ 689 | "duration_sum = runs_last_month[\"duration\"].sum()\n", 690 | "distance_sum = runs_last_month[\"totalDistance\"].sum()\n", 691 | "energy_sum = runs_last_month[\"totalEnergyBurned\"].sum()\n", 692 | "duration_sum, distance_sum, energy_sum" 693 | ] 694 | }, 695 | { 696 | "cell_type": "code", 697 | "execution_count": 78, 698 | "id": "f5e4c561", 699 | "metadata": {}, 700 | "outputs": [ 701 | { 702 | "data": { 703 | "text/plain": [ 704 | "(22.89143841856658, 3.909768647728631, 258.2183821406893)" 705 | ] 706 | }, 707 | "execution_count": 78, 708 | "metadata": {}, 709 | "output_type": "execute_result" 710 | } 711 | ], 712 | "source": [ 713 | "duration_avg = runs_last_month[\"duration\"].mean()\n", 714 | "distance_avg = runs_last_month[\"totalDistance\"].mean()\n", 715 | "energy_avg = runs_last_month[\"totalEnergyBurned\"].mean()\n", 716 | "duration_avg, distance_avg, energy_avg" 717 | ] 718 | }, 719 | { 720 | "cell_type": "code", 721 | "execution_count": 79, 722 | "id": "330f1284", 723 | "metadata": {}, 724 | "outputs": [], 725 | "source": [ 726 | "def get_heartrate_for_workout(workout, heartrate):\n", 727 | " return get_heartrate_for_date(workout[\"startDate\"], workout[\"endDate\"], heartrate)\n", 728 | "\n", 729 | "def convert_to_minute_proportion(number):\n", 730 | " return int(number) + ((number % 1) / 100 * 60)\n", 731 | "heartrate = record_data[record_data[\"type\"] == \"HeartRate\"]\n", 732 | "runs_last_month[\"heartrate\"] = runs_last_month.apply(lambda row: get_heartrate_for_workout(row, heartrate), axis=1)\n", 733 | "runs_last_month[\"hr_mean\"] = runs_last_month.apply(lambda row: row['heartrate'][\"value\"].mean(), axis=1)\n", 734 | "pace = runs_last_month[\"duration\"] / runs_last_month[\"totalDistance\"]\n", 735 | "# convert decimals to minute percentage, pace=min/km\n", 736 | "pace = pace.apply(lambda row: convert_to_minute_proportion(row))\n", 737 | "runs_last_month[\"pace\"] = pace" 738 | ] 739 | }, 740 | { 741 | "cell_type": "code", 742 | "execution_count": 80, 743 | "id": "3d4a4300", 744 | "metadata": {}, 745 | "outputs": [ 746 | { 747 | "data": { 748 | "text/html": [ 749 | "
\n", 750 | "\n", 763 | "\n", 764 | " \n", 765 | " \n", 766 | " \n", 767 | " \n", 768 | " \n", 769 | " \n", 770 | " \n", 771 | " \n", 772 | " \n", 773 | " \n", 774 | " \n", 775 | " \n", 776 | " \n", 777 | " \n", 778 | " \n", 779 | " \n", 780 | " \n", 781 | " \n", 782 | " \n", 783 | " \n", 784 | " \n", 785 | " \n", 786 | " \n", 787 | " \n", 788 | " \n", 789 | " \n", 790 | " \n", 791 | " \n", 792 | " \n", 793 | " \n", 794 | " \n", 795 | " \n", 796 | " \n", 797 | " \n", 798 | " \n", 799 | " \n", 800 | " \n", 801 | " \n", 802 | " \n", 803 | " \n", 804 | " \n", 805 | " \n", 806 | " \n", 807 | " \n", 808 | " \n", 809 | " \n", 810 | "
durationtotalDistancetotalEnergyBurnedcreationDate
15024.0170834.021084263.4237712021-07-20 17:48:58+02:00
15123.3044604.025121261.6740002021-07-21 17:35:13+02:00
15222.9190204.027181268.2200082021-07-22 17:04:27+02:00
15323.0905794.281389276.9799342021-07-23 17:49:31+02:00
15424.0157624.119525269.0362182021-07-24 17:57:31+02:00
\n", 811 | "
" 812 | ], 813 | "text/plain": [ 814 | " duration totalDistance totalEnergyBurned creationDate\n", 815 | "150 24.017083 4.021084 263.423771 2021-07-20 17:48:58+02:00\n", 816 | "151 23.304460 4.025121 261.674000 2021-07-21 17:35:13+02:00\n", 817 | "152 22.919020 4.027181 268.220008 2021-07-22 17:04:27+02:00\n", 818 | "153 23.090579 4.281389 276.979934 2021-07-23 17:49:31+02:00\n", 819 | "154 24.015762 4.119525 269.036218 2021-07-24 17:57:31+02:00" 820 | ] 821 | }, 822 | "execution_count": 80, 823 | "metadata": {}, 824 | "output_type": "execute_result" 825 | } 826 | ], 827 | "source": [ 828 | "import matplotlib.pyplot as plt\n", 829 | "\n", 830 | "plot_data = runs_last_month[['duration', 'totalDistance', 'totalEnergyBurned', 'creationDate']].copy()\n", 831 | "plot_data.head()" 832 | ] 833 | }, 834 | { 835 | "cell_type": "code", 836 | "execution_count": 81, 837 | "id": "15860703", 838 | "metadata": {}, 839 | "outputs": [ 840 | { 841 | "data": { 842 | "image/png": "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6Hqcikqp7CAiIkoFZ7CIiCjQZFVKbYKUA8ZCmlp87LpuLIBflFKPAFgM4LLr8i0AYpVSmwFMBvAZpLPgRqWUAnAKQEdrwiciIn/GJhdEREREREQGYYkgERERERGRQZhgERERERERGYQJFhERERERkUGYYBERERERERmECRYREREREZFBmGAREREREREZhAkWERERERGRQZhgERERERERGYQJFhERERERkUGYYBERERERERmECRYREREREZFBQu0OID0KFCigS5cubXcYXrl8+TKyZ89udxhBgWNtHY51QhwPa3G8rcOxtg7H2joca2sF4nhv2LDhtNa6YOLL/TrBKl26NNavX293GF5ZsmQJwsPD7Q4jKHCsrcOxTojjYS2Ot3U41tbhWFuHY22tQBxvpdS/SV3OEkEiIiIiIiKDMMEiIiIiIiIyCBMsIiIiIiIigzDBIiIiIiIiMggTLCIioiBx8CCweDGgtd2REBEFLiZYREREQeD0aSA8HGjZEujQAfg3yd5XRESUXkywiIiIAlxcHNClC3D8OPDKK0BkJFC1KjByJHDzpt3RERElLzYW+OQT4Omn7Y7Ee0ywiIiIAtybbwIREcDYscCoUcA//wCtWgGvvQbUqQNERdkdIRHR7aKigLp1gQEDgBMngGvX7I7IO0ywiIiIAtivvwIffAD07Ak884xcVrIkMHu2fF24ADRtCjz7LHD2rJ2REhGJU6dkxqppU/lc+vVX4PffgSxZ7I7MO0ywiIiIAtTOnUCPHkCDBsDnn99+fYcOwI4dUjb47bdA5crA99+zCQYR2SM+HvjqK6BSJfksevVVmXHv1AlQyu7ovMcEi4iIKABdvCgHJVmzAr/8AmTOnPTtcuSQssENG4By5YDu3aURxs6d1sZLRMFt40agcWPgueeAmjWBTZuADz+Uzyh/wwSLiIgowGgNPPkksGcP8NNPQPHiqd/njjtkvcP48XJgU7Mm8NZbwNWrZkdLRMHs/Hmgb1+gfn3gwAGZuVq8GKhWze7IfMcEi4iIKMB8+CEwa5Z0CQwP9/5+ISFy9njnTuDRR4Hhw4EaNYB16/KaFisRBSetgWnTpDR57FjghReAXbuAbt38qxwwKUywiIiIAsjChcAbbwCPPQa89JJvjxEWBkydKp0HQ0KAV1+9478270RE6bVjh5Qid+sGlCoFrF0LfPEFkCeP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\n", 843 | "text/plain": [ 844 | "
" 845 | ] 846 | }, 847 | "metadata": { 848 | "needs_background": "light" 849 | }, 850 | "output_type": "display_data" 851 | } 852 | ], 853 | "source": [ 854 | "# plot hr, pace, distance\n", 855 | "fig, axs = plt.subplots(3, 1, figsize=(12, 8))\n", 856 | "\n", 857 | "x = runs_last_month['creationDate']\n", 858 | "axs[0].plot(x, runs_last_month[\"hr_mean\"], color=\"red\")\n", 859 | "axs[0].set_xlabel('Date')\n", 860 | "axs[0].set_ylabel('HR mean')\n", 861 | "axs[0].grid(True)\n", 862 | "\n", 863 | "axs[1].plot(x, runs_last_month[\"pace\"], color=\"blue\")\n", 864 | "axs[1].set_ylabel('Pace')\n", 865 | "axs[1].grid(True)\n", 866 | "\n", 867 | "axs[2].plot(x, runs_last_month[\"totalDistance\"], color=\"green\")\n", 868 | "axs[2].set_ylabel('Distance')\n", 869 | "axs[2].grid(True)\n", 870 | "\n", 871 | "fig.tight_layout()\n", 872 | "plt.show()" 873 | ] 874 | }, 875 | { 876 | "cell_type": "code", 877 | "execution_count": 82, 878 | "id": "35e3cf85", 879 | "metadata": {}, 880 | "outputs": [ 881 | { 882 | "data": { 883 | "text/plain": [ 884 | "(129.0223653750448,\n", 885 | " 12.354174678126972,\n", 886 | " 8521.206610642746,\n", 887 | " 143.81744261405984,\n", 888 | " 5.534279971791522)" 889 | ] 890 | }, 891 | "execution_count": 82, 892 | "metadata": {}, 893 | "output_type": "execute_result" 894 | } 895 | ], 896 | "source": [ 897 | "# Get stats\n", 898 | "total_dist = runs_last_month[\"totalDistance\"].sum()\n", 899 | "total_time = runs_last_month[\"duration\"].sum() / 60\n", 900 | "total_time = convert_to_minute_proportion(total_time)\n", 901 | "total_kcal = runs_last_month[\"totalEnergyBurned\"].sum()\n", 902 | "hr_mean = runs_last_month[\"hr_mean\"].mean()\n", 903 | "pace_mean = (runs_last_month[\"duration\"] / runs_last_month[\"totalDistance\"]).mean()\n", 904 | "pace_mean = convert_to_minute_proportion(pace_mean)\n", 905 | "total_dist, total_time, total_kcal, hr_mean, pace_mean" 906 | ] 907 | }, 908 | { 909 | "cell_type": "code", 910 | "execution_count": null, 911 | "id": "eeecc857", 912 | "metadata": {}, 913 | "outputs": [], 914 | "source": [] 915 | } 916 | ], 917 | "metadata": { 918 | "kernelspec": { 919 | "display_name": "ml", 920 | "language": "python", 921 | "name": "ml" 922 | }, 923 | "language_info": { 924 | "codemirror_mode": { 925 | "name": "ipython", 926 | "version": 3 927 | }, 928 | "file_extension": ".py", 929 | "mimetype": "text/x-python", 930 | "name": "python", 931 | "nbconvert_exporter": "python", 932 | "pygments_lexer": "ipython3", 933 | "version": "3.9.2" 934 | } 935 | }, 936 | "nbformat": 4, 937 | "nbformat_minor": 5 938 | } 939 | -------------------------------------------------------------------------------- /chatbot-gui/app.py: -------------------------------------------------------------------------------- 1 | from tkinter import * 2 | from chat import get_response, bot_name 3 | 4 | BG_GRAY = "#ABB2B9" 5 | BG_COLOR = "#17202A" 6 | TEXT_COLOR = "#EAECEE" 7 | 8 | FONT = "Helvetica 14" 9 | FONT_BOLD = "Helvetica 13 bold" 10 | 11 | class ChatApplication: 12 | 13 | def __init__(self): 14 | self.window = Tk() 15 | self._setup_main_window() 16 | 17 | def run(self): 18 | self.window.mainloop() 19 | 20 | def _setup_main_window(self): 21 | self.window.title("Chat") 22 | self.window.resizable(width=False, height=False) 23 | self.window.configure(width=470, height=550, bg=BG_COLOR) 24 | 25 | # head label 26 | head_label = Label(self.window, bg=BG_COLOR, fg=TEXT_COLOR, 27 | text="Welcome", font=FONT_BOLD, pady=10) 28 | head_label.place(relwidth=1) 29 | 30 | # tiny divider 31 | line = Label(self.window, width=450, bg=BG_GRAY) 32 | line.place(relwidth=1, rely=0.07, relheight=0.012) 33 | 34 | # text widget 35 | self.text_widget = Text(self.window, width=20, height=2, bg=BG_COLOR, fg=TEXT_COLOR, 36 | font=FONT, padx=5, pady=5) 37 | self.text_widget.place(relheight=0.745, relwidth=1, rely=0.08) 38 | self.text_widget.configure(cursor="arrow", state=DISABLED) 39 | 40 | # scroll bar 41 | scrollbar = Scrollbar(self.text_widget) 42 | scrollbar.place(relheight=1, relx=0.974) 43 | scrollbar.configure(command=self.text_widget.yview) 44 | 45 | # bottom label 46 | bottom_label = Label(self.window, bg=BG_GRAY, height=80) 47 | bottom_label.place(relwidth=1, rely=0.825) 48 | 49 | # message entry box 50 | self.msg_entry = Entry(bottom_label, bg="#2C3E50", fg=TEXT_COLOR, font=FONT) 51 | self.msg_entry.place(relwidth=0.74, relheight=0.06, rely=0.008, relx=0.011) 52 | self.msg_entry.focus() 53 | self.msg_entry.bind("", self._on_enter_pressed) 54 | 55 | # send button 56 | send_button = Button(bottom_label, text="Send", font=FONT_BOLD, width=20, bg=BG_GRAY, 57 | command=lambda: self._on_enter_pressed(None)) 58 | send_button.place(relx=0.77, rely=0.008, relheight=0.06, relwidth=0.22) 59 | 60 | def _on_enter_pressed(self, event): 61 | msg = self.msg_entry.get() 62 | self._insert_message(msg, "You") 63 | 64 | def _insert_message(self, msg, sender): 65 | if not msg: 66 | return 67 | 68 | self.msg_entry.delete(0, END) 69 | msg1 = f"{sender}: {msg}\n\n" 70 | self.text_widget.configure(state=NORMAL) 71 | self.text_widget.insert(END, msg1) 72 | self.text_widget.configure(state=DISABLED) 73 | 74 | msg2 = f"{bot_name}: {get_response(msg)}\n\n" 75 | self.text_widget.configure(state=NORMAL) 76 | self.text_widget.insert(END, msg2) 77 | self.text_widget.configure(state=DISABLED) 78 | 79 | self.text_widget.see(END) 80 | 81 | 82 | if __name__ == "__main__": 83 | app = ChatApplication() 84 | app.run() -------------------------------------------------------------------------------- /chatbot-gui/chat.py: -------------------------------------------------------------------------------- 1 | import random 2 | import json 3 | 4 | import torch 5 | 6 | from model import NeuralNet 7 | from nltk_utils import bag_of_words, tokenize 8 | 9 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 10 | 11 | with open('intents.json', 'r') as json_data: 12 | intents = json.load(json_data) 13 | 14 | FILE = "data.pth" 15 | data = torch.load(FILE) 16 | 17 | input_size = data["input_size"] 18 | hidden_size = data["hidden_size"] 19 | output_size = data["output_size"] 20 | all_words = data['all_words'] 21 | tags = data['tags'] 22 | model_state = data["model_state"] 23 | 24 | model = NeuralNet(input_size, hidden_size, output_size).to(device) 25 | model.load_state_dict(model_state) 26 | model.eval() 27 | 28 | bot_name = "Sam" 29 | 30 | def get_response(msg): 31 | sentence = tokenize(msg) 32 | X = bag_of_words(sentence, all_words) 33 | X = X.reshape(1, X.shape[0]) 34 | X = torch.from_numpy(X).to(device) 35 | 36 | output = model(X) 37 | _, predicted = torch.max(output, dim=1) 38 | 39 | tag = tags[predicted.item()] 40 | 41 | probs = torch.softmax(output, dim=1) 42 | prob = probs[0][predicted.item()] 43 | if prob.item() > 0.75: 44 | for intent in intents['intents']: 45 | if tag == intent["tag"]: 46 | return random.choice(intent['responses']) 47 | 48 | return "I do not understand..." 49 | 50 | -------------------------------------------------------------------------------- /chatbot-gui/intents.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "tag": "greeting", 5 | "patterns": [ 6 | "Hi", 7 | "Hey", 8 | "How are you", 9 | "Is anyone there?", 10 | "Hello", 11 | "Good day" 12 | ], 13 | "responses": [ 14 | "Hey :-)", 15 | "Hello, thanks for visiting", 16 | "Hi there, what can I do for you?", 17 | "Hi there, how can I help?" 18 | ] 19 | }, 20 | { 21 | "tag": "goodbye", 22 | "patterns": ["Bye", "See you later", "Goodbye"], 23 | "responses": [ 24 | "See you later, thanks for visiting", 25 | "Have a nice day", 26 | "Bye! Come back again soon." 27 | ] 28 | }, 29 | { 30 | "tag": "thanks", 31 | "patterns": ["Thanks", "Thank you", "That's helpful", "Thank's a lot!"], 32 | "responses": ["Happy to help!", "Any time!", "My pleasure"] 33 | }, 34 | { 35 | "tag": "items", 36 | "patterns": [ 37 | "Which items do you have?", 38 | "What kinds of items are there?", 39 | "What do you sell?" 40 | ], 41 | "responses": [ 42 | "We sell coffee and tea", 43 | "We have coffee and tea" 44 | ] 45 | }, 46 | { 47 | "tag": "payments", 48 | "patterns": [ 49 | "Do you take credit cards?", 50 | "Do you accept Mastercard?", 51 | "Can I pay with Paypal?", 52 | "Are you cash only?" 53 | ], 54 | "responses": [ 55 | "We accept VISA, Mastercard and Paypal", 56 | "We accept most major credit cards, and Paypal" 57 | ] 58 | }, 59 | { 60 | "tag": "delivery", 61 | "patterns": [ 62 | "How long does delivery take?", 63 | "How long does shipping take?", 64 | "When do I get my delivery?" 65 | ], 66 | "responses": [ 67 | "Delivery takes 2-4 days", 68 | "Shipping takes 2-4 days" 69 | ] 70 | }, 71 | { 72 | "tag": "funny", 73 | "patterns": [ 74 | "Tell me a joke!", 75 | "Tell me something funny!", 76 | "Do you know a joke?" 77 | ], 78 | "responses": [ 79 | "Why did the hipster burn his mouth? He drank the coffee before it was cool.", 80 | "What did the buffalo say when his son left for college? Bison." 81 | ] 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /chatbot-gui/model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | 4 | 5 | class NeuralNet(nn.Module): 6 | def __init__(self, input_size, hidden_size, num_classes): 7 | super(NeuralNet, self).__init__() 8 | self.l1 = nn.Linear(input_size, hidden_size) 9 | self.l2 = nn.Linear(hidden_size, hidden_size) 10 | self.l3 = nn.Linear(hidden_size, num_classes) 11 | self.relu = nn.ReLU() 12 | 13 | def forward(self, x): 14 | out = self.l1(x) 15 | out = self.relu(out) 16 | out = self.l2(out) 17 | out = self.relu(out) 18 | out = self.l3(out) 19 | # no activation and no softmax at the end 20 | return out -------------------------------------------------------------------------------- /chatbot-gui/nltk_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import nltk 3 | # nltk.download('punkt') 4 | from nltk.stem.porter import PorterStemmer 5 | stemmer = PorterStemmer() 6 | 7 | def tokenize(sentence): 8 | """ 9 | split sentence into array of words/tokens 10 | a token can be a word or punctuation character, or number 11 | """ 12 | return nltk.word_tokenize(sentence) 13 | 14 | 15 | def stem(word): 16 | """ 17 | stemming = find the root form of the word 18 | examples: 19 | words = ["organize", "organizes", "organizing"] 20 | words = [stem(w) for w in words] 21 | -> ["organ", "organ", "organ"] 22 | """ 23 | return stemmer.stem(word.lower()) 24 | 25 | 26 | def bag_of_words(tokenized_sentence, words): 27 | """ 28 | return bag of words array: 29 | 1 for each known word that exists in the sentence, 0 otherwise 30 | example: 31 | sentence = ["hello", "how", "are", "you"] 32 | words = ["hi", "hello", "I", "you", "bye", "thank", "cool"] 33 | bog = [ 0 , 1 , 0 , 1 , 0 , 0 , 0] 34 | """ 35 | # stem each word 36 | sentence_words = [stem(word) for word in tokenized_sentence] 37 | # initialize bag with 0 for each word 38 | bag = np.zeros(len(words), dtype=np.float32) 39 | for idx, w in enumerate(words): 40 | if w in sentence_words: 41 | bag[idx] = 1 42 | 43 | return bag -------------------------------------------------------------------------------- /chatbot-gui/train.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | import json 4 | 5 | import torch 6 | import torch.nn as nn 7 | from torch.utils.data import Dataset, DataLoader 8 | 9 | from nltk_utils import bag_of_words, tokenize, stem 10 | from model import NeuralNet 11 | 12 | with open('intents.json', 'r') as f: 13 | intents = json.load(f) 14 | 15 | all_words = [] 16 | tags = [] 17 | xy = [] 18 | # loop through each sentence in our intents patterns 19 | for intent in intents['intents']: 20 | tag = intent['tag'] 21 | # add to tag list 22 | tags.append(tag) 23 | for pattern in intent['patterns']: 24 | # tokenize each word in the sentence 25 | w = tokenize(pattern) 26 | # add to our words list 27 | all_words.extend(w) 28 | # add to xy pair 29 | xy.append((w, tag)) 30 | 31 | # stem and lower each word 32 | ignore_words = ['?', '.', '!'] 33 | all_words = [stem(w) for w in all_words if w not in ignore_words] 34 | # remove duplicates and sort 35 | all_words = sorted(set(all_words)) 36 | tags = sorted(set(tags)) 37 | 38 | print(len(xy), "patterns") 39 | print(len(tags), "tags:", tags) 40 | print(len(all_words), "unique stemmed words:", all_words) 41 | 42 | # create training data 43 | X_train = [] 44 | y_train = [] 45 | for (pattern_sentence, tag) in xy: 46 | # X: bag of words for each pattern_sentence 47 | bag = bag_of_words(pattern_sentence, all_words) 48 | X_train.append(bag) 49 | # y: PyTorch CrossEntropyLoss needs only class labels, not one-hot 50 | label = tags.index(tag) 51 | y_train.append(label) 52 | 53 | X_train = np.array(X_train) 54 | y_train = np.array(y_train) 55 | 56 | # Hyper-parameters 57 | num_epochs = 1000 58 | batch_size = 8 59 | learning_rate = 0.001 60 | input_size = len(X_train[0]) 61 | hidden_size = 8 62 | output_size = len(tags) 63 | print(input_size, output_size) 64 | 65 | class ChatDataset(Dataset): 66 | 67 | def __init__(self): 68 | self.n_samples = len(X_train) 69 | self.x_data = X_train 70 | self.y_data = y_train 71 | 72 | # support indexing such that dataset[i] can be used to get i-th sample 73 | def __getitem__(self, index): 74 | return self.x_data[index], self.y_data[index] 75 | 76 | # we can call len(dataset) to return the size 77 | def __len__(self): 78 | return self.n_samples 79 | 80 | dataset = ChatDataset() 81 | train_loader = DataLoader(dataset=dataset, 82 | batch_size=batch_size, 83 | shuffle=True, 84 | num_workers=0) 85 | 86 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 87 | 88 | model = NeuralNet(input_size, hidden_size, output_size).to(device) 89 | 90 | # Loss and optimizer 91 | criterion = nn.CrossEntropyLoss() 92 | optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) 93 | 94 | # Train the model 95 | for epoch in range(num_epochs): 96 | for (words, labels) in train_loader: 97 | words = words.to(device) 98 | labels = labels.to(dtype=torch.long).to(device) 99 | 100 | # Forward pass 101 | outputs = model(words) 102 | # if y would be one-hot, we must apply 103 | # labels = torch.max(labels, 1)[1] 104 | loss = criterion(outputs, labels) 105 | 106 | # Backward and optimize 107 | optimizer.zero_grad() 108 | loss.backward() 109 | optimizer.step() 110 | 111 | if (epoch+1) % 100 == 0: 112 | print (f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}') 113 | 114 | 115 | print(f'final loss: {loss.item():.4f}') 116 | 117 | data = { 118 | "model_state": model.state_dict(), 119 | "input_size": input_size, 120 | "hidden_size": hidden_size, 121 | "output_size": output_size, 122 | "all_words": all_words, 123 | "tags": tags 124 | } 125 | 126 | FILE = "data.pth" 127 | torch.save(data, FILE) 128 | 129 | print(f'training complete. file saved to {FILE}') 130 | -------------------------------------------------------------------------------- /file-explorer/file-explorer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog 3 | 4 | def browse_file(): 5 | file_path = filedialog.askopenfilename() 6 | if file_path: 7 | selected_file.set(file_path) 8 | 9 | def browse_directory(): 10 | directory_path = filedialog.askdirectory() 11 | if directory_path: 12 | selected_directory.set(directory_path) 13 | 14 | # Create the main window 15 | root = tk.Tk() 16 | root.title("Cool File Explorer") 17 | 18 | # Create a label to display the selected file 19 | selected_file = tk.StringVar() 20 | file_label = tk.Label(root, textvariable=selected_file, font=('Helvetica', 12)) 21 | file_label.pack(pady=10) 22 | 23 | # Create a button to browse for a file 24 | file_button = tk.Button(root, text="Browse File", font=('Helvetica', 12), command=browse_file) 25 | file_button.pack() 26 | 27 | # Create a label to display the selected directory 28 | selected_directory = tk.StringVar() 29 | directory_label = tk.Label(root, textvariable=selected_directory, font=('Helvetica', 12)) 30 | directory_label.pack(pady=10) 31 | 32 | # Create a button to browse for a directory 33 | directory_button = tk.Button(root, text="Browse Directory", font=('Helvetica', 12), command=browse_directory) 34 | directory_button.pack() 35 | 36 | # Start the GUI main loop 37 | root.mainloop() 38 | -------------------------------------------------------------------------------- /file-organizing/file_organizing.py: -------------------------------------------------------------------------------- 1 | # file handling: navigate, rename, move, copy, remove 2 | import os 3 | import shutil 4 | from pathlib import Path 5 | 6 | # change working directory 7 | print(os.getcwd()) 8 | 9 | os.chdir("/Users/patrick/Desktop/video-files") 10 | print(os.getcwd()) 11 | 12 | # rename files 13 | for file in os.listdir(): 14 | # This example changes filenames from 15 | # 'dictionary - python-course-3.mov' 16 | # to --> 17 | # '03-python-course-dictionary.mov' 18 | name, ext = os.path.splitext(file) 19 | 20 | splitted = name.split("-") 21 | splitted = [s.strip() for s in splitted] 22 | new_name = f"{splitted[3].zfill(2)}-{splitted[1]}-{splitted[2]}-{splitted[0]}{ext}" 23 | 24 | os.rename(file, new_name) 25 | 26 | # or 27 | # f = Path(file) 28 | # name, ext = f.stem, f.suffix 29 | # f.rename(new_name) 30 | 31 | # create directory 32 | Path("data").mkdir(exist_ok=True) 33 | 34 | # or 35 | if not os.path.exists("data"): 36 | os.mkdir("data") 37 | 38 | # move file and folder 39 | shutil.move('f', 'd') # works for file and folder 40 | 41 | # copy file and folder 42 | shutil.copy("src", "dest") 43 | shutil.copy2("src", "dest") 44 | 45 | # remove file and folder 46 | os.remove("filename") # error if not found 47 | os.rmdir("folder") # error if not empty, or not found 48 | shutil.rmtree("folder") # works for non empty directories 49 | 50 | -------------------------------------------------------------------------------- /file-organizing/organize-desktop.py: -------------------------------------------------------------------------------- 1 | # organize the desktop 2 | # moves images, videos, screenshots, and audio files 3 | # into corresponding folders 4 | import os 5 | import shutil 6 | 7 | 8 | audio = (".3ga", ".aac", ".ac3", ".aif", ".aiff", 9 | ".alac", ".amr", ".ape", ".au", ".dss", 10 | ".flac", ".flv", ".m4a", ".m4b", ".m4p", 11 | ".mp3", ".mpga", ".ogg", ".oga", ".mogg", 12 | ".opus", ".qcp", ".tta", ".voc", ".wav", 13 | ".wma", ".wv") 14 | 15 | video = (".webm", ".MTS", ".M2TS", ".TS", ".mov", 16 | ".mp4", ".m4p", ".m4v", ".mxf") 17 | 18 | img = (".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".png", 19 | ".gif", ".webp", ".svg", ".apng", ".avif") 20 | 21 | def is_audio(file): 22 | return os.path.splitext(file)[1] in audio 23 | 24 | def is_video(file): 25 | return os.path.splitext(file)[1] in video 26 | 27 | def is_image(file): 28 | return os.path.splitext(file)[1] in img 29 | 30 | def is_screenshot(file): 31 | name, ext = os.path.splitext(file) 32 | return (ext in img) and "screenshot" in name.lower() 33 | 34 | os.chdir("/Users/patrick/Desktop") 35 | 36 | for file in os.listdir(): 37 | if is_audio(file): 38 | shutil.move(file, "Users/patrick/Documents/audio") 39 | elif is_video(file): 40 | shutil.move(file, "Users/patrick/Documents/video") 41 | elif is_image(file): 42 | if is_screenshot(file): 43 | shutil.move(file, "Users/patrick/Documents/screenshots") 44 | else: 45 | shutil.move(file, "Users/patrick/Documents/images") 46 | else: 47 | shutil.move(file, "Users/patrick/Documents") 48 | -------------------------------------------------------------------------------- /googleimagedownloader/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests # to sent GET requests 4 | from bs4 import BeautifulSoup # to parse HTML 5 | 6 | # user can input a topic and a number 7 | # download first n images from google image search 8 | 9 | GOOGLE_IMAGE = \ 10 | 'https://www.google.com/search?site=&tbm=isch&source=hp&biw=1873&bih=990&' 11 | 12 | # The User-Agent request header contains a characteristic string 13 | # that allows the network protocol peers to identify the application type, 14 | # operating system, and software version of the requesting software user agent. 15 | # needed for google search 16 | usr_agent = { 17 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11', 18 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 19 | 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', 20 | 'Accept-Encoding': 'none', 21 | 'Accept-Language': 'en-US,en;q=0.8', 22 | 'Connection': 'keep-alive', 23 | } 24 | 25 | SAVE_FOLDER = 'images' 26 | 27 | def main(): 28 | if not os.path.exists(SAVE_FOLDER): 29 | os.mkdir(SAVE_FOLDER) 30 | download_images() 31 | 32 | def download_images(): 33 | # ask for user input 34 | data = input('What are you looking for? ') 35 | n_images = int(input('How many images do you want? ')) 36 | 37 | print('Start searching...') 38 | 39 | # get url query string 40 | searchurl = GOOGLE_IMAGE + 'q=' + data 41 | print(searchurl) 42 | 43 | # request url, without usr_agent the permission gets denied 44 | response = requests.get(searchurl, headers=usr_agent) 45 | html = response.text 46 | 47 | # find all divs where class='rg_meta' 48 | soup = BeautifulSoup(html, 'html.parser') 49 | results = soup.findAll('div', {'class': 'rg_meta'}, limit=n_images) 50 | 51 | # extract the link from the div tag 52 | imagelinks= [] 53 | for re in results: 54 | text = re.text # this is a valid json string 55 | text_dict= json.loads(text) # deserialize json to a Python dict 56 | link = text_dict['ou'] 57 | # image_type = text_dict['ity'] 58 | imagelinks.append(link) 59 | 60 | print(f'found {len(imagelinks)} images') 61 | print('Start downloading...') 62 | 63 | for i, imagelink in enumerate(imagelinks): 64 | # open image link and save as file 65 | response = requests.get(imagelink) 66 | 67 | imagename = SAVE_FOLDER + '/' + data + str(i+1) + '.jpg' 68 | with open(imagename, 'wb') as file: 69 | file.write(response.content) 70 | 71 | print('Done') 72 | 73 | 74 | if __name__ == '__main__': 75 | main() -------------------------------------------------------------------------------- /image-viewer/image-viewer.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog 3 | from PIL import Image, ImageTk 4 | 5 | def open_image(): 6 | file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png;*.jpg;*.jpeg;*.gif;*.bmp")]) 7 | if file_path: 8 | image = Image.open(file_path) 9 | photo = ImageTk.PhotoImage(image) 10 | label.config(image=photo) 11 | label.image = photo 12 | 13 | # Create the main window 14 | root = tk.Tk() 15 | root.title("Cool Image Viewer") 16 | 17 | # Create a label to display the image 18 | label = tk.Label(root) 19 | label.pack() 20 | 21 | # Create a button to open an image 22 | open_button = tk.Button(root, text="Open Image", font=('Helvetica', 14), command=open_image) 23 | open_button.pack() 24 | 25 | # Start the GUI main loop 26 | root.mainloop() 27 | -------------------------------------------------------------------------------- /moviepicker/main.py: -------------------------------------------------------------------------------- 1 | import random 2 | import requests 3 | from bs4 import BeautifulSoup 4 | 5 | # crawl IMDB Top 250 and randomly select a movie 6 | 7 | URL = 'http://www.imdb.com/chart/top' 8 | 9 | def main(): 10 | response = requests.get(URL) 11 | 12 | soup = BeautifulSoup(response.text, 'html.parser') 13 | #soup = BeautifulSoup(response.text, 'lxml') # faster 14 | 15 | # print(soup.prettify()) 16 | 17 | movietags = soup.select('td.titleColumn') 18 | inner_movietags = soup.select('td.titleColumn a') 19 | ratingtags = soup.select('td.posterColumn span[name=ir]') 20 | 21 | def get_year(movie_tag): 22 | moviesplit = movie_tag.text.split() 23 | year = moviesplit[-1] # last item 24 | return year 25 | 26 | years = [get_year(tag) for tag in movietags] 27 | actors_list =[tag['title'] for tag in inner_movietags] # access attribute 'title' 28 | titles = [tag.text for tag in inner_movietags] 29 | ratings = [float(tag['data-value']) for tag in ratingtags] # access attribute 'data-value' 30 | 31 | n_movies = len(titles) 32 | 33 | while(True): 34 | idx = random.randrange(0, n_movies) 35 | 36 | print(f'{titles[idx]} {years[idx]}, Rating: {ratings[idx]:.1f}, Starring: {actors_list[idx]}') 37 | 38 | user_input = input('Do you want another movie (y/[n])? ') 39 | if user_input != 'y': 40 | break 41 | 42 | 43 | if __name__ == '__main__': 44 | main() -------------------------------------------------------------------------------- /note-take/note-take.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import scrolledtext 3 | from tkinter import filedialog 4 | 5 | def save_note(): 6 | note = text_widget.get("1.0", "end-1c") # Get text from the text widget 7 | file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")]) 8 | 9 | if file_path: 10 | with open(file_path, 'w') as file: 11 | file.write(note) 12 | 13 | def open_note(): 14 | file_path = filedialog.askopenfilename(filetypes=[("Text files", "*.txt")]) 15 | 16 | if file_path: 17 | with open(file_path, 'r') as file: 18 | note = file.read() 19 | text_widget.delete("1.0", "end") # Clear existing text 20 | text_widget.insert("1.0", note) # Insert the loaded note 21 | 22 | # Create the main window 23 | root = tk.Tk() 24 | root.title("Cool Note Taker") 25 | 26 | # Create a scrolled text widget 27 | text_widget = scrolledtext.ScrolledText(root, font=('Helvetica', 14)) 28 | text_widget.pack(expand=True, fill='both') 29 | 30 | # Create "Save" and "Open" buttons 31 | save_button = tk.Button(root, text="Save Note", font=('Helvetica', 12), command=save_note) 32 | save_button.pack(side="left", padx=10, pady=10) 33 | 34 | open_button = tk.Button(root, text="Open Note", font=('Helvetica', 12), command=open_note) 35 | open_button.pack(side="right", padx=10, pady=10) 36 | 37 | # Start the GUI main loop 38 | root.mainloop() 39 | -------------------------------------------------------------------------------- /notetaking-speech-rec/README.md: -------------------------------------------------------------------------------- 1 | # Create a Notetaking App with Speech Recognition 2 | 3 | ## Installation 4 | 5 | On Mac you also need: 6 | ``` 7 | $ brew install portaudio 8 | $ pip install pyobjc 9 | 10 | ``` 11 | 12 | Then use: 13 | ``` 14 | # pip install pyaudio 15 | # pip install speechrecognition 16 | # pip install requests gtts playsound 17 | ``` 18 | 19 | Note: On a M1 Mac you may need to use this command to install pyaudio 20 | ``` 21 | # python -m pip install --global-option='build_ext' --global-option='-I/opt/homebrew/Cellar/portaudio/19.7.0/include' --global-option='-L/opt/homebrew/Cellar/portaudio/19.7.0/lib' pyaudio 22 | ``` 23 | 24 | For more setup instructions also have a look here: 25 | - [Pyaudio Installation](http://people.csail.mit.edu/hubert/pyaudio/) 26 | - [Speech Recognition](https://github.com/Uberi/speech_recognition) 27 | - [Notion API setup](https://developers.notion.com/docs/getting-started) -------------------------------------------------------------------------------- /notetaking-speech-rec/main.py: -------------------------------------------------------------------------------- 1 | import speech_recognition as sr 2 | import gtts 3 | from playsound import playsound 4 | import os 5 | from datetime import datetime 6 | from notion import NotionClient 7 | 8 | r = sr.Recognizer() 9 | 10 | token = "YOUR NOTION TOKEN HERE" 11 | database_id = "YOUR NOTION DATABASE_ID HERE" 12 | 13 | client = NotionClient(token, database_id) 14 | 15 | ACTIVATION_COMMAND = "hey sam" 16 | 17 | def get_audio(): 18 | with sr.Microphone() as source: 19 | print("Say something") 20 | audio = r.listen(source) 21 | return audio 22 | 23 | def audio_to_text(audio): 24 | text = "" 25 | try: 26 | text = r.recognize_google(audio) 27 | except sr.UnknownValueError: 28 | print("Speech recognition could not understand audio") 29 | except sr.RequestError: 30 | print("could not request results from API") 31 | return text 32 | 33 | def play_sound(text): 34 | try: 35 | tts = gtts.gTTS(text) 36 | tempfile = "./temp.mp3" 37 | tts.save(tempfile) 38 | playsound(tempfile) 39 | os.remove(tempfile) 40 | except AssertionError: 41 | print("could not play sound") 42 | 43 | 44 | 45 | if __name__ == "__main__": 46 | 47 | while True: 48 | a = get_audio() 49 | command = audio_to_text(a) 50 | 51 | if ACTIVATION_COMMAND in command.lower(): 52 | print("activate") 53 | play_sound("What can I do for you?") 54 | 55 | note = get_audio() 56 | note = audio_to_text(note) 57 | 58 | if note: 59 | play_sound(note) 60 | 61 | now = datetime.now().astimezone().isoformat() 62 | res = client.create_page(note, now, status="Active") 63 | if res.status_code == 200: 64 | play_sound("Stored new item") 65 | 66 | -------------------------------------------------------------------------------- /notetaking-speech-rec/notion.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | 5 | class NotionClient: 6 | 7 | def __init__(self, token, database_id) -> None: 8 | self.database_id = database_id 9 | 10 | self.headers = { 11 | "Authorization": "Bearer " + token, 12 | "Content-Type": "application/json", 13 | "Notion-Version": "2021-08-16" 14 | } 15 | 16 | # read, update 17 | def create_page(self, description, date, status): 18 | create_url = 'https://api.notion.com/v1/pages' 19 | 20 | data = { 21 | "parent": { "database_id": self.database_id }, 22 | "properties": { 23 | "Description": { 24 | "title": [ 25 | { 26 | "text": { 27 | "content": description 28 | } 29 | } 30 | ] 31 | }, 32 | "Date": { 33 | "date": { 34 | "start": date, 35 | "end": None 36 | } 37 | }, 38 | "Status": { 39 | "rich_text": [ 40 | { 41 | "text": { 42 | "content": status 43 | } 44 | } 45 | ] 46 | } 47 | }} 48 | 49 | data = json.dumps(data) 50 | res = requests.post(create_url, headers=self.headers, data=data) 51 | print(res.status_code) 52 | return res 53 | -------------------------------------------------------------------------------- /paint/paint.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | class PaintApp: 4 | def __init__(self, root): 5 | self.root = root 6 | self.root.title("Cool Paint App") 7 | 8 | self.canvas = tk.Canvas(root, bg="white") 9 | self.canvas.pack(fill=tk.BOTH, expand=True) 10 | 11 | self.button_clear = tk.Button(root, text="Clear", command=self.clear_canvas) 12 | self.button_clear.pack() 13 | 14 | self.canvas.bind("", self.start_paint) 15 | self.canvas.bind("", self.paint) 16 | 17 | self.old_x = None 18 | self.old_y = None 19 | 20 | def start_paint(self, event): 21 | self.old_x = event.x 22 | self.old_y = event.y 23 | 24 | def paint(self, event): 25 | new_x = event.x 26 | new_y = event.y 27 | if self.old_x and self.old_y: 28 | self.canvas.create_line(self.old_x, self.old_y, new_x, new_y, fill="black", width=2) 29 | self.old_x = new_x 30 | self.old_y = new_y 31 | 32 | def clear_canvas(self): 33 | self.canvas.delete("all") 34 | 35 | if __name__ == "__main__": 36 | root = tk.Tk() 37 | app = PaintApp(root) 38 | root.mainloop() 39 | -------------------------------------------------------------------------------- /photo-restoration/.env: -------------------------------------------------------------------------------- 1 | REPLICATE_API_TOKEN=YOUR_TOKEN_HERE -------------------------------------------------------------------------------- /photo-restoration/README.md: -------------------------------------------------------------------------------- 1 | # Flask app to restore photos 2 | 3 | Simple Flask app to restore old photos with AI. It uses the [GFPGAN](https://replicate.com/tencentarc/gfpgan) model on [Replicate](https://replicate.com/). 4 | 5 | ![Screenshot](screenshot.png) 6 | ## Setup 7 | ```bash 8 | pip install flask replicate python-dotenv 9 | ``` 10 | 11 | You need a [Replicate](https://replicate.com/) API Token (You can get started for free). Put the token in the `.env` file. 12 | 13 | Then start the app, upload a photo, and have fun! 14 | 15 | ```bash 16 | python main.py 17 | ``` 18 | 19 | ## Resources 20 | 21 | - Inspired by [restorephotos.io](https://www.restorephotos.io/) 22 | - [https://github.com/TencentARC/GFPGAN](https://github.com/TencentARC/GFPGAN) -------------------------------------------------------------------------------- /photo-restoration/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect, url_for, render_template 2 | from werkzeug.utils import secure_filename 3 | from photo_restorer import predict_image 4 | 5 | UPLOAD_FOLDER = '/static/images/' 6 | ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'} 7 | 8 | app = Flask(__name__) 9 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 10 | app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 11 | 12 | 13 | def allowed_file(filename): 14 | return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 15 | 16 | @app.route('/') 17 | def home(): 18 | return render_template('index.html') 19 | 20 | 21 | @app.route('/', methods=['POST']) 22 | def upload_image(): 23 | if 'file' not in request.files: 24 | return redirect(request.url) 25 | 26 | file = request.files['file'] 27 | if file.filename == '': 28 | return redirect(request.url) 29 | 30 | if file and allowed_file(file.filename): 31 | filename = secure_filename(file.filename) 32 | full_filepath = "." + url_for('static', filename='images/' + filename) 33 | 34 | file.save(full_filepath) 35 | restored_img_url = predict_image(full_filepath) 36 | return render_template('index.html', filename=filename, restored_img_url=restored_img_url) 37 | else: 38 | return redirect(request.url) 39 | 40 | 41 | if __name__ == "__main__": 42 | app.run(debug=True) -------------------------------------------------------------------------------- /photo-restoration/photo_restorer.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | load_dotenv() 3 | 4 | import replicate 5 | 6 | model = replicate.models.get("tencentarc/gfpgan") 7 | version = model.versions.get("9283608cc6b7be6b65a8e44983db012355fde4132009bf99d976b2f0896856a3") 8 | 9 | def predict_image(filename): 10 | # https://replicate.com/tencentarc/gfpgan/versions/9283608cc6b7be6b65a8e44983db012355fde4132009bf99d976b2f0896856a3#input 11 | inputs = { 12 | # Input 13 | 'img': open(filename, "rb"), 14 | 15 | # GFPGAN version. v1.3: better quality. v1.4: more details and better 16 | # identity. 17 | 'version': "v1.4", 18 | 19 | # Rescaling factor 20 | 'scale': 2, 21 | } 22 | print(inputs) 23 | 24 | # https://replicate.com/tencentarc/gfpgan/versions/9283608cc6b7be6b65a8e44983db012355fde4132009bf99d976b2f0896856a3#output-schema 25 | output = version.predict(**inputs) 26 | print(output) 27 | return output -------------------------------------------------------------------------------- /photo-restoration/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickloeber/python-fun/96dd56da96ad6b421bfaca13d842ad2326e5395d/photo-restoration/screenshot.png -------------------------------------------------------------------------------- /photo-restoration/static/images/example.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickloeber/python-fun/96dd56da96ad6b421bfaca13d842ad2326e5395d/photo-restoration/static/images/example.jpeg -------------------------------------------------------------------------------- /photo-restoration/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

Photo Restoration

11 | 12 | {% if filename %} 13 |
14 |

Original Image:

15 | 16 |
17 | {% endif %} 18 | {% if restored_img_url %} 19 |
20 |

Restored Image:

21 | 22 | 23 |
24 | {% endif %} 25 | 26 |
27 |

28 | 29 |

30 |

31 | 32 |

33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /snake-game/snake.py: -------------------------------------------------------------------------------- 1 | import curses 2 | from random import randint 3 | 4 | #constants 5 | 6 | WINDOW_WIDTH = 60 # number of columns of window box 7 | WINDOW_HEIGHT = 20 # number of rows of window box 8 | ''' 9 | Number of blocks in window per line = WINDOW_WIDTH -2. 10 | Block x index ranges from 1 to WINDOW_WIDTH -2. 11 | Number of blocks in window per column = WINDOW_HEIGHT -2. 12 | Block y index ranges from 1 to WINDOW_HEIGHT -2. 13 | ''' 14 | 15 | # setup window 16 | curses.initscr() 17 | win = curses.newwin(WINDOW_HEIGHT, WINDOW_WIDTH, 0, 0) # rows, columns 18 | win.keypad(1) 19 | curses.noecho() 20 | curses.curs_set(0) 21 | win.border(0) 22 | win.nodelay(1) # -1 23 | 24 | # snake and food 25 | snake = [(4, 4), (4, 3), (4, 2)] 26 | food = (6, 6) 27 | 28 | win.addch(food[0], food[1], '#') 29 | # game logic 30 | score = 0 31 | 32 | ESC = 27 33 | key = curses.KEY_RIGHT 34 | 35 | while key != ESC: 36 | win.addstr(0, 2, 'Score ' + str(score) + ' ') 37 | win.timeout(150 - (len(snake)) // 5 + len(snake)//10 % 120) # increase speed 38 | 39 | prev_key = key 40 | event = win.getch() 41 | key = event if event != -1 else prev_key 42 | 43 | if key not in [curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_UP, curses.KEY_DOWN, ESC]: 44 | key = prev_key 45 | 46 | # calculate the next coordinates 47 | y = snake[0][0] 48 | x = snake[0][1] 49 | if key == curses.KEY_DOWN: 50 | y += 1 51 | if key == curses.KEY_UP: 52 | y -= 1 53 | if key == curses.KEY_LEFT: 54 | x -= 1 55 | if key == curses.KEY_RIGHT: 56 | x += 1 57 | 58 | snake.insert(0, (y, x)) # append O(n) 59 | 60 | # check if we hit the border 61 | if y == 0: break 62 | if y == WINDOW_HEIGHT-1: break 63 | if x == 0: break 64 | if x == WINDOW_WIDTH -1: break 65 | 66 | # if snake runs over itself 67 | if snake[0] in snake[1:]: break 68 | 69 | if snake[0] == food: 70 | # eat the food 71 | score += 1 72 | food = () 73 | while food == (): 74 | food = (randint(1,WINDOW_HEIGHT-2), randint(1,WINDOW_WIDTH -2)) 75 | if food in snake: 76 | food = () 77 | win.addch(food[0], food[1], '#') 78 | else: 79 | # move snake 80 | last = snake.pop() 81 | win.addch(last[0], last[1], ' ') 82 | 83 | win.addch(snake[0][0], snake[0][1], '*') 84 | 85 | curses.endwin() 86 | print(f"Final score = {score}") -------------------------------------------------------------------------------- /snake-pygame/arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickloeber/python-fun/96dd56da96ad6b421bfaca13d842ad2326e5395d/snake-pygame/arial.ttf -------------------------------------------------------------------------------- /snake-pygame/snake_game.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import random 3 | from enum import Enum 4 | from collections import namedtuple 5 | 6 | pygame.init() 7 | font = pygame.font.Font('arial.ttf', 25) 8 | #font = pygame.font.SysFont('arial', 25) 9 | 10 | class Direction(Enum): 11 | RIGHT = 1 12 | LEFT = 2 13 | UP = 3 14 | DOWN = 4 15 | 16 | Point = namedtuple('Point', 'x, y') 17 | 18 | # rgb colors 19 | WHITE = (255, 255, 255) 20 | RED = (200,0,0) 21 | BLUE1 = (0, 0, 255) 22 | BLUE2 = (0, 100, 255) 23 | BLACK = (0,0,0) 24 | 25 | BLOCK_SIZE = 20 26 | SPEED = 20 27 | 28 | class SnakeGame: 29 | 30 | def __init__(self, w=640, h=480): 31 | self.w = w 32 | self.h = h 33 | # init display 34 | self.display = pygame.display.set_mode((self.w, self.h)) 35 | pygame.display.set_caption('Snake') 36 | self.clock = pygame.time.Clock() 37 | 38 | # init game state 39 | self.direction = Direction.RIGHT 40 | 41 | self.head = Point(self.w/2, self.h/2) 42 | self.snake = [self.head, 43 | Point(self.head.x-BLOCK_SIZE, self.head.y), 44 | Point(self.head.x-(2*BLOCK_SIZE), self.head.y)] 45 | 46 | self.score = 0 47 | self.food = None 48 | self._place_food() 49 | 50 | def _place_food(self): 51 | x = random.randint(0, (self.w-BLOCK_SIZE )//BLOCK_SIZE )*BLOCK_SIZE 52 | y = random.randint(0, (self.h-BLOCK_SIZE )//BLOCK_SIZE )*BLOCK_SIZE 53 | self.food = Point(x, y) 54 | if self.food in self.snake: 55 | self._place_food() 56 | 57 | def play_step(self): 58 | # 1. collect user input 59 | for event in pygame.event.get(): 60 | if event.type == pygame.QUIT: 61 | pygame.quit() 62 | quit() 63 | if event.type == pygame.KEYDOWN: 64 | if event.key == pygame.K_LEFT: 65 | self.direction = Direction.LEFT 66 | elif event.key == pygame.K_RIGHT: 67 | self.direction = Direction.RIGHT 68 | elif event.key == pygame.K_UP: 69 | self.direction = Direction.UP 70 | elif event.key == pygame.K_DOWN: 71 | self.direction = Direction.DOWN 72 | 73 | # 2. move 74 | self._move(self.direction) # update the head 75 | self.snake.insert(0, self.head) 76 | 77 | # 3. check if game over 78 | game_over = False 79 | if self._is_collision(): 80 | game_over = True 81 | return game_over, self.score 82 | 83 | # 4. place new food or just move 84 | if self.head == self.food: 85 | self.score += 1 86 | self._place_food() 87 | else: 88 | self.snake.pop() 89 | 90 | # 5. update ui and clock 91 | self._update_ui() 92 | self.clock.tick(SPEED) 93 | # 6. return game over and score 94 | return game_over, self.score 95 | 96 | def _is_collision(self): 97 | # hits boundary 98 | if self.head.x > self.w - BLOCK_SIZE or self.head.x < 0 or self.head.y > self.h - BLOCK_SIZE or self.head.y < 0: 99 | return True 100 | # hits itself 101 | if self.head in self.snake[1:]: 102 | return True 103 | 104 | return False 105 | 106 | def _update_ui(self): 107 | self.display.fill(BLACK) 108 | 109 | for pt in self.snake: 110 | pygame.draw.rect(self.display, BLUE1, pygame.Rect(pt.x, pt.y, BLOCK_SIZE, BLOCK_SIZE)) 111 | pygame.draw.rect(self.display, BLUE2, pygame.Rect(pt.x+4, pt.y+4, 12, 12)) 112 | 113 | pygame.draw.rect(self.display, RED, pygame.Rect(self.food.x, self.food.y, BLOCK_SIZE, BLOCK_SIZE)) 114 | 115 | text = font.render("Score: " + str(self.score), True, WHITE) 116 | self.display.blit(text, [0, 0]) 117 | pygame.display.flip() 118 | 119 | def _move(self, direction): 120 | x = self.head.x 121 | y = self.head.y 122 | if direction == Direction.RIGHT: 123 | x += BLOCK_SIZE 124 | elif direction == Direction.LEFT: 125 | x -= BLOCK_SIZE 126 | elif direction == Direction.DOWN: 127 | y += BLOCK_SIZE 128 | elif direction == Direction.UP: 129 | y -= BLOCK_SIZE 130 | 131 | self.head = Point(x, y) 132 | 133 | 134 | if __name__ == '__main__': 135 | game = SnakeGame() 136 | 137 | # game loop 138 | while True: 139 | game_over, score = game.play_step() 140 | 141 | if game_over == True: 142 | break 143 | 144 | print('Final Score', score) 145 | 146 | 147 | pygame.quit() -------------------------------------------------------------------------------- /stockprediction/main.py: -------------------------------------------------------------------------------- 1 | # pip install streamlit fbprophet yfinance plotly 2 | import streamlit as st 3 | from datetime import date 4 | 5 | import yfinance as yf 6 | from fbprophet import Prophet 7 | from fbprophet.plot import plot_plotly 8 | from plotly import graph_objs as go 9 | 10 | START = "2015-01-01" 11 | TODAY = date.today().strftime("%Y-%m-%d") 12 | 13 | st.title('Stock Forecast App') 14 | 15 | stocks = ('GOOG', 'AAPL', 'MSFT', 'GME') 16 | selected_stock = st.selectbox('Select dataset for prediction', stocks) 17 | 18 | n_years = st.slider('Years of prediction:', 1, 4) 19 | period = n_years * 365 20 | 21 | 22 | @st.cache 23 | def load_data(ticker): 24 | data = yf.download(ticker, START, TODAY) 25 | data.reset_index(inplace=True) 26 | return data 27 | 28 | 29 | data_load_state = st.text('Loading data...') 30 | data = load_data(selected_stock) 31 | data_load_state.text('Loading data... done!') 32 | 33 | st.subheader('Raw data') 34 | st.write(data.tail()) 35 | 36 | # Plot raw data 37 | def plot_raw_data(): 38 | fig = go.Figure() 39 | fig.add_trace(go.Scatter(x=data['Date'], y=data['Open'], name="stock_open")) 40 | fig.add_trace(go.Scatter(x=data['Date'], y=data['Close'], name="stock_close")) 41 | fig.layout.update(title_text='Time Series data with Rangeslider', xaxis_rangeslider_visible=True) 42 | st.plotly_chart(fig) 43 | 44 | plot_raw_data() 45 | 46 | # Predict forecast with Prophet. 47 | df_train = data[['Date','Close']] 48 | df_train = df_train.rename(columns={"Date": "ds", "Close": "y"}) 49 | 50 | m = Prophet() 51 | m.fit(df_train) 52 | future = m.make_future_dataframe(periods=period) 53 | forecast = m.predict(future) 54 | 55 | # Show and plot forecast 56 | st.subheader('Forecast data') 57 | st.write(forecast.tail()) 58 | 59 | st.write(f'Forecast plot for {n_years} years') 60 | fig1 = plot_plotly(m, forecast) 61 | st.plotly_chart(fig1) 62 | 63 | st.write("Forecast components") 64 | fig2 = m.plot_components(forecast) 65 | st.write(fig2) -------------------------------------------------------------------------------- /stopwatch/stopwatch.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import time 3 | 4 | def start(): 5 | global running 6 | running = True 7 | start_button['state'] = 'disabled' 8 | stop_button['state'] = 'active' 9 | update() 10 | 11 | def stop(): 12 | global running 13 | running = False 14 | start_button['state'] = 'active' 15 | stop_button['state'] = 'disabled' 16 | 17 | def reset(): 18 | global running, elapsed_time 19 | running = False 20 | elapsed_time = 0 21 | time_label.config(text="00:00:00") 22 | start_button['state'] = 'active' 23 | stop_button['state'] = 'disabled' 24 | 25 | def update(): 26 | if running: 27 | global elapsed_time 28 | elapsed_time += 1 29 | hours, remainder = divmod(elapsed_time, 3600) 30 | minutes, seconds = divmod(remainder, 60) 31 | time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}" 32 | time_label.config(text=time_str) 33 | time_label.after(1000, update) 34 | 35 | running = False 36 | elapsed_time = 0 37 | 38 | root = tk.Tk() 39 | root.title("Cool Stopwatch") 40 | 41 | time_label = tk.Label(root, text="00:00:00", font=('Helvetica', 48)) 42 | time_label.pack(padx=20, pady=20) 43 | 44 | start_button = tk.Button(root, text="Start", font=('Helvetica', 16), command=start) 45 | start_button.pack(side="left", padx=10) 46 | stop_button = tk.Button(root, text="Stop", font=('Helvetica', 16), command=stop) 47 | stop_button.pack(side="left", padx=10) 48 | reset_button = tk.Button(root, text="Reset", font=('Helvetica', 16), command=reset) 49 | reset_button.pack(side="left", padx=10) 50 | 51 | stop_button['state'] = 'disabled' 52 | 53 | root.mainloop() 54 | -------------------------------------------------------------------------------- /text-editor/text-editor.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import filedialog, scrolledtext 3 | 4 | def open_file(): 5 | file_path = filedialog.askopenfilename(filetypes=[("Text files", "*.txt")]) 6 | if file_path: 7 | with open(file_path, 'r') as file: 8 | text.delete(1.0, tk.END) 9 | text.insert(tk.END, file.read()) 10 | root.title(f"Cool Text Editor - {file_path}") 11 | 12 | def save_file(): 13 | file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")]) 14 | if file_path: 15 | with open(file_path, 'w') as file: 16 | file.write(text.get(1.0, tk.END)) 17 | root.title(f"Cool Text Editor - {file_path}") 18 | 19 | # Create the main window 20 | root = tk.Tk() 21 | root.title("Cool Text Editor") 22 | 23 | # Create a scrolled text widget 24 | text = scrolledtext.ScrolledText(root, font=('Helvetica', 14)) 25 | text.pack(expand=True, fill='both') 26 | 27 | # Create the menu bar 28 | menu_bar = tk.Menu(root) 29 | root.config(menu=menu_bar) 30 | 31 | # File menu 32 | file_menu = tk.Menu(menu_bar) 33 | menu_bar.add_cascade(label="File", menu=file_menu) 34 | file_menu.add_command(label="Open", command=open_file) 35 | file_menu.add_command(label="Save", command=save_file) 36 | file_menu.add_separator() 37 | file_menu.add_command(label="Exit", command=root.quit) 38 | 39 | # Start the GUI main loop 40 | root.mainloop() 41 | -------------------------------------------------------------------------------- /to-do/to-do.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | def add_task(): 4 | task = entry.get() 5 | if task: 6 | listbox.insert(tk.END, task) 7 | entry.delete(0, tk.END) 8 | 9 | def remove_task(): 10 | selected_task = listbox.curselection() 11 | if selected_task: 12 | listbox.delete(selected_task) 13 | 14 | # Create the main window 15 | root = tk.Tk() 16 | root.title("Cool To-Do List") 17 | 18 | # Entry widget for adding tasks 19 | entry = tk.Entry(root, font=('Helvetica', 18)) 20 | entry.grid(row=0, column=0, columnspan=2) 21 | 22 | # Button to add tasks 23 | add_button = tk.Button(root, text="Add", font=('Helvetica', 14), command=add_task) 24 | add_button.grid(row=0, column=2) 25 | 26 | # Button to remove tasks 27 | remove_button = tk.Button(root, text="Remove", font=('Helvetica', 14), command=remove_task) 28 | remove_button.grid(row=0, column=3) 29 | 30 | # Listbox to display tasks 31 | listbox = tk.Listbox(root, font=('Helvetica', 18), selectmode=tk.SINGLE, selectbackground="#a5a5a5") 32 | listbox.grid(row=1, column=0, columnspan=4) 33 | 34 | # Start the GUI main loop 35 | root.mainloop() 36 | -------------------------------------------------------------------------------- /todocli-tutorial/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from typing import List 3 | import datetime 4 | from model import Todo 5 | 6 | conn = sqlite3.connect('todos.db') 7 | c = conn.cursor() 8 | 9 | 10 | def create_table(): 11 | c.execute("""CREATE TABLE IF NOT EXISTS todos ( 12 | task text, 13 | category text, 14 | date_added text, 15 | date_completed text, 16 | status integer, 17 | position integer 18 | )""") 19 | 20 | 21 | create_table() 22 | 23 | 24 | def insert_todo(todo: Todo): 25 | c.execute('select count(*) FROM todos') 26 | count = c.fetchone()[0] 27 | todo.position = count if count else 0 28 | with conn: 29 | c.execute('INSERT INTO todos VALUES (:task, :category, :date_added, :date_completed, :status, :position)', 30 | {'task': todo.task, 'category': todo.category, 'date_added': todo.date_added, 31 | 'date_completed': todo.date_completed, 'status': todo.status, 'position': todo.position }) 32 | 33 | 34 | def get_all_todos() -> List[Todo]: 35 | c.execute('select * from todos') 36 | results = c.fetchall() 37 | todos = [] 38 | for result in results: 39 | todos.append(Todo(*result)) 40 | return todos 41 | 42 | 43 | def delete_todo(position): 44 | c.execute('select count(*) from todos') 45 | count = c.fetchone()[0] 46 | 47 | with conn: 48 | c.execute("DELETE from todos WHERE position=:position", {"position": position}) 49 | for pos in range(position+1, count): 50 | change_position(pos, pos-1, False) 51 | 52 | 53 | def change_position(old_position: int, new_position: int, commit=True): 54 | c.execute('UPDATE todos SET position = :position_new WHERE position = :position_old', 55 | {'position_old': old_position, 'position_new': new_position}) 56 | if commit: 57 | conn.commit() 58 | 59 | 60 | def update_todo(position: int, task: str, category: str): 61 | with conn: 62 | if task is not None and category is not None: 63 | c.execute('UPDATE todos SET task = :task, category = :category WHERE position = :position', 64 | {'position': position, 'task': task, 'category': category}) 65 | elif task is not None: 66 | c.execute('UPDATE todos SET task = :task WHERE position = :position', 67 | {'position': position, 'task': task}) 68 | elif category is not None: 69 | c.execute('UPDATE todos SET category = :category WHERE position = :position', 70 | {'position': position, 'category': category}) 71 | 72 | 73 | def complete_todo(position: int): 74 | with conn: 75 | c.execute('UPDATE todos SET status = 2, date_completed = :date_completed WHERE position = :position', 76 | {'position': position, 'date_completed': datetime.datetime.now().isoformat()}) 77 | -------------------------------------------------------------------------------- /todocli-tutorial/model.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class Todo: 5 | def __init__(self, task, category, 6 | date_added=None, date_completed=None, 7 | status=None, position=None): 8 | self.task = task 9 | self.category = category 10 | self.date_added = date_added if date_added is not None else datetime.datetime.now().isoformat() 11 | self.date_completed = date_completed if date_completed is not None else None 12 | self.status = status if status is not None else 1 # 1 = open, 2 = completed 13 | self.position = position if position is not None else None 14 | 15 | def __repr__(self) -> str: 16 | return f"({self.task}, {self.category}, {self.date_added}, {self.date_completed}, {self.status}, {self.position})" -------------------------------------------------------------------------------- /todocli-tutorial/todocli.py: -------------------------------------------------------------------------------- 1 | import typer 2 | from rich.console import Console 3 | from rich.table import Table 4 | from model import Todo 5 | from database import get_all_todos, delete_todo, insert_todo, complete_todo, update_todo 6 | 7 | console = Console() 8 | 9 | app = typer.Typer() 10 | 11 | 12 | @app.command(short_help='adds an item') 13 | def add(task: str, category: str): 14 | typer.echo(f"adding {task}, {category}") 15 | todo = Todo(task, category) 16 | insert_todo(todo) 17 | show() 18 | 19 | @app.command() 20 | def delete(position: int): 21 | typer.echo(f"deleting {position}") 22 | # indices in UI begin at 1, but in database at 0 23 | delete_todo(position-1) 24 | show() 25 | 26 | @app.command() 27 | def update(position: int, task: str = None, category: str = None): 28 | typer.echo(f"updating {position}") 29 | update_todo(position-1, task, category) 30 | show() 31 | 32 | @app.command() 33 | def complete(position: int): 34 | typer.echo(f"complete {position}") 35 | complete_todo(position-1) 36 | show() 37 | 38 | @app.command() 39 | def show(): 40 | tasks = get_all_todos() 41 | console.print("[bold magenta]Todos[/bold magenta]!", "💻") 42 | 43 | table = Table(show_header=True, header_style="bold blue") 44 | table.add_column("#", style="dim", width=6) 45 | table.add_column("Todo", min_width=20) 46 | table.add_column("Category", min_width=12, justify="right") 47 | table.add_column("Done", min_width=12, justify="right") 48 | 49 | def get_category_color(category): 50 | COLORS = {'Learn': 'cyan', 'YouTube': 'red', 'Sports': 'cyan', 'Study': 'green'} 51 | if category in COLORS: 52 | return COLORS[category] 53 | return 'white' 54 | 55 | for idx, task in enumerate(tasks, start=1): 56 | c = get_category_color(task.category) 57 | is_done_str = '✅' if task.status == 2 else '❌' 58 | table.add_row(str(idx), task.task, f'[{c}]{task.category}[/{c}]', is_done_str) 59 | console.print(table) 60 | 61 | 62 | if __name__ == "__main__": 63 | app() -------------------------------------------------------------------------------- /webapps/django/README.md: -------------------------------------------------------------------------------- 1 | ## Steps: 2 | 3 | ### Installation 4 | 5 | ```console 6 | pip install Django 7 | django-admin startproject todoapp 8 | ``` 9 | 10 | ### Start 11 | 12 | ```console 13 | python manage.py migrate 14 | python manage.py runserver 15 | python manage.py startapp todolist 16 | ``` 17 | 18 | - add 'todolist' to INSTALLED_APPS 19 | 20 | ### Add views 21 | - implement todolist.views.py and create todolist.urls.py 22 | - add urls to todoapp.urls.py 23 | 24 | ### Add templates 25 | - add templates folder and file 26 | - add "templates" to DIR in settings.py 27 | - modify view: return render... 28 | 29 | ### Add models 30 | - implement todolist.models.py 31 | 32 | ### Put together 33 | ```console 34 | manage.py makemigrations 35 | python manage.py migrate 36 | python manage.py createsuperuser 37 | ``` 38 | 39 | - Adding models to the administration site: 40 | - todolist.admin.py: admin.site.register(Todo) 41 | - login to admin 42 | 43 | ### add template 44 | - add {% csrf_token %} to template 45 | 46 | ### CRUD 47 | - implement views -------------------------------------------------------------------------------- /webapps/django/todoapp/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todoapp.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /webapps/django/todoapp/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Todo App 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

To Do App

16 | 17 |
18 | {% csrf_token %} 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 | {% for todo in todo_list %} 29 |
30 |

{{ todo.id }} | {{ todo.title }}

31 | 32 | {% if todo.complete == False %} 33 | Not Complete 34 | {% else %} 35 | Completed 36 | {% endif %} 37 | 38 | Update 39 | Delete 40 |
41 | {% endfor %} 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todoapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickloeber/python-fun/96dd56da96ad6b421bfaca13d842ad2326e5395d/webapps/django/todoapp/todoapp/__init__.py -------------------------------------------------------------------------------- /webapps/django/todoapp/todoapp/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for todoapp project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todoapp.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todoapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for todoapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-8up6eur7!aw%w+h_w5!i)=)k1#!lclxbq6@u!5x5z$gwju-$*1' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'todolist' 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'todoapp.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': ["templates"], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'todoapp.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 118 | 119 | STATIC_URL = 'static/' 120 | 121 | # Default primary key field type 122 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 123 | 124 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 125 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todoapp/urls.py: -------------------------------------------------------------------------------- 1 | """todoapp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('', include('todolist.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todoapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for todoapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todoapp.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todolist/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickloeber/python-fun/96dd56da96ad6b421bfaca13d842ad2326e5395d/webapps/django/todoapp/todolist/__init__.py -------------------------------------------------------------------------------- /webapps/django/todoapp/todolist/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Todo 3 | 4 | # Register your models here. 5 | admin.site.register(Todo) -------------------------------------------------------------------------------- /webapps/django/todoapp/todolist/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodolistConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'todolist' 7 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todolist/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | class Todo(models.Model): 5 | title=models.CharField(max_length=350) 6 | complete=models.BooleanField(default=False) 7 | 8 | def __str__(self): 9 | return self.title 10 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todolist/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /webapps/django/todoapp/todolist/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name="index"), 7 | path('add', views.add, name="add"), 8 | path('delete/', views.delete, name="delete"), 9 | path('update/', views.update, name="update"), 10 | ] -------------------------------------------------------------------------------- /webapps/django/todoapp/todolist/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.views.decorators.http import require_http_methods 3 | 4 | from .models import Todo 5 | 6 | # Create your views here. 7 | def index(request): 8 | todos = Todo.objects.all() 9 | return render(request, "base.html", {"todo_list": todos}) 10 | # return HttpResponse("Hello World!!") 11 | 12 | 13 | @require_http_methods(["POST"]) 14 | def add(request): 15 | # if request.method == "POST": 16 | title = request.POST["title"] 17 | todo = Todo(title=title) 18 | todo.save() 19 | return redirect("index") 20 | 21 | 22 | def update(request, todo_id): 23 | todo = Todo.objects.get(id=todo_id) 24 | todo.complete = not todo.complete 25 | todo.save() 26 | return redirect("index") 27 | 28 | 29 | def delete(request, todo_id): 30 | todo = Todo.objects.get(id=todo_id) 31 | todo.delete() 32 | return redirect("index") 33 | 34 | -------------------------------------------------------------------------------- /webapps/fastapi/README.md: -------------------------------------------------------------------------------- 1 | ```console 2 | pip install fastapi 3 | pip install "uvicorn[standard]" 4 | pip install python-multipart sqlalchemy jinja2 5 | 6 | uvicorn app:app --reload 7 | ``` 8 | -------------------------------------------------------------------------------- /webapps/fastapi/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Depends, Request, Form, status 2 | 3 | from starlette.responses import RedirectResponse 4 | from starlette.templating import Jinja2Templates 5 | 6 | from sqlalchemy.orm import Session 7 | 8 | import models 9 | from database import SessionLocal, engine 10 | 11 | models.Base.metadata.create_all(bind=engine) 12 | 13 | templates = Jinja2Templates(directory="templates") 14 | 15 | app = FastAPI() 16 | 17 | 18 | # Dependency 19 | def get_db(): 20 | db = SessionLocal() 21 | try: 22 | yield db 23 | finally: 24 | db.close() 25 | 26 | 27 | @app.get("/") 28 | def home(request: Request, db: Session = Depends(get_db)): 29 | todos = db.query(models.Todo).all() 30 | return templates.TemplateResponse("base.html", 31 | {"request": request, "todo_list": todos}) 32 | 33 | @app.post("/add") 34 | def add(request: Request, title: str = Form(...), db: Session = Depends(get_db)): 35 | new_todo = models.Todo(title=title) 36 | db.add(new_todo) 37 | db.commit() 38 | 39 | url = app.url_path_for("home") 40 | return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER) 41 | 42 | 43 | @app.get("/update/{todo_id}") 44 | def update(request: Request, todo_id: int, db: Session = Depends(get_db)): 45 | todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first() 46 | todo.complete = not todo.complete 47 | db.commit() 48 | 49 | url = app.url_path_for("home") 50 | return RedirectResponse(url=url, status_code=status.HTTP_302_FOUND) 51 | 52 | 53 | @app.get("/delete/{todo_id}") 54 | def delete(request: Request, todo_id: int, db: Session = Depends(get_db)): 55 | todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first() 56 | db.delete(todo) 57 | db.commit() 58 | 59 | url = app.url_path_for("home") 60 | return RedirectResponse(url=url, status_code=status.HTTP_302_FOUND) 61 | -------------------------------------------------------------------------------- /webapps/fastapi/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | SQLALCHEMY_DATABASE_URL = "sqlite:///./db.sqlite" 6 | # SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" 7 | 8 | engine = create_engine( 9 | SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} 10 | ) 11 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 12 | 13 | Base = declarative_base() -------------------------------------------------------------------------------- /webapps/fastapi/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String 2 | 3 | from database import Base 4 | 5 | 6 | class Todo(Base): 7 | __tablename__ = "todos" 8 | 9 | id = Column(Integer, primary_key=True, index=True) 10 | title = Column(String) 11 | complete = Column(Boolean, default=False) 12 | 13 | 14 | # schemas? -------------------------------------------------------------------------------- /webapps/fastapi/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Todo App 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

To Do App

16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 | {% for todo in todo_list %} 28 |
29 |

{{todo.id }} | {{ todo.title }}

30 | 31 | {% if todo.complete == False %} 32 | Not Complete 33 | {% else %} 34 | Completed 35 | {% endif %} 36 | 37 | Update 38 | Delete 39 |
40 | {% endfor %} 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /webapps/flask/README.md: -------------------------------------------------------------------------------- 1 | ```console 2 | python3 -m venv venv 3 | . venv/bin/activate 4 | 5 | pip install Flask 6 | pip install Flask-SQLAlchemy 7 | 8 | export FLASK_APP=app.py 9 | export FLASK_ENV=development 10 | flask run 11 | ``` 12 | -------------------------------------------------------------------------------- /webapps/flask/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, url_for 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | app = Flask(__name__) 5 | 6 | # /// = relative path, //// = absolute path 7 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' 8 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 9 | db = SQLAlchemy(app) 10 | 11 | 12 | class Todo(db.Model): 13 | id = db.Column(db.Integer, primary_key=True) 14 | title = db.Column(db.String(100)) 15 | complete = db.Column(db.Boolean) 16 | 17 | 18 | db.create_all() 19 | 20 | 21 | @app.get("/") 22 | def home(): 23 | # todo_list = Todo.query.all() 24 | todo_list = db.session.query(Todo).all() 25 | # return "Hello, World!" 26 | return render_template("base.html", todo_list=todo_list) 27 | 28 | 29 | # @app.route("/add", methods=["POST"]) 30 | @app.post("/add") 31 | def add(): 32 | title = request.form.get("title") 33 | new_todo = Todo(title=title, complete=False) 34 | db.session.add(new_todo) 35 | db.session.commit() 36 | return redirect(url_for("home")) 37 | 38 | 39 | @app.get("/update/") 40 | def update(todo_id): 41 | # todo = Todo.query.filter_by(id=todo_id).first() 42 | todo = db.session.query(Todo).filter(Todo.id == todo_id).first() 43 | todo.complete = not todo.complete 44 | db.session.commit() 45 | return redirect(url_for("home")) 46 | 47 | 48 | @app.get("/delete/") 49 | def delete(todo_id): 50 | # todo = Todo.query.filter_by(id=todo_id).first() 51 | todo = db.session.query(Todo).filter(Todo.id == todo_id).first() 52 | db.session.delete(todo) 53 | db.session.commit() 54 | return redirect(url_for("home")) 55 | -------------------------------------------------------------------------------- /webapps/flask/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Todo App 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

To Do App

16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 | {% for todo in todo_list %} 28 |
29 |

{{todo.id }} | {{ todo.title }}

30 | 31 | {% if todo.complete == False %} 32 | Not Complete 33 | {% else %} 34 | Completed 35 | {% endif %} 36 | 37 | Update 38 | Delete 39 |
40 | {% endfor %} 41 |
42 | 43 | 44 | --------------------------------------------------------------------------------