├── .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 | " type | \n",
80 | " sourceName | \n",
81 | " sourceVersion | \n",
82 | " unit | \n",
83 | " creationDate | \n",
84 | " startDate | \n",
85 | " endDate | \n",
86 | " value | \n",
87 | " device | \n",
88 | "
\n",
89 | " \n",
90 | " \n",
91 | " \n",
92 | " 743765 | \n",
93 | " HeartRateVariabilitySDNN | \n",
94 | " Apple Watch von Patrick | \n",
95 | " 7.6.1 | \n",
96 | " ms | \n",
97 | " 2021-08-20 21:44:34+02:00 | \n",
98 | " 2021-08-20 21:43:28+02:00 | \n",
99 | " 2021-08-20 21:44:34+02:00 | \n",
100 | " 106.2270 | \n",
101 | " <<HKDevice: 0x2811a01e0>, name:Apple Watch, ma... | \n",
102 | "
\n",
103 | " \n",
104 | " 743766 | \n",
105 | " HeartRateVariabilitySDNN | \n",
106 | " Apple Watch von Patrick | \n",
107 | " 7.6.1 | \n",
108 | " ms | \n",
109 | " 2021-08-20 21:48:35+02:00 | \n",
110 | " 2021-08-20 21:47:30+02:00 | \n",
111 | " 2021-08-20 21:48:35+02:00 | \n",
112 | " 60.9539 | \n",
113 | " <<HKDevice: 0x2811a01e0>, name:Apple Watch, ma... | \n",
114 | "
\n",
115 | " \n",
116 | " 743767 | \n",
117 | " HeartRateVariabilitySDNN | \n",
118 | " Apple Watch von Patrick | \n",
119 | " 7.6.1 | \n",
120 | " ms | \n",
121 | " 2021-08-22 08:19:29+02:00 | \n",
122 | " 2021-08-22 08:18:23+02:00 | \n",
123 | " 2021-08-22 08:19:29+02:00 | \n",
124 | " 71.2487 | \n",
125 | " <<HKDevice: 0x2811a01e0>, name:Apple Watch, ma... | \n",
126 | "
\n",
127 | " \n",
128 | " 743768 | \n",
129 | " HeartRateVariabilitySDNN | \n",
130 | " Apple Watch von Patrick | \n",
131 | " 7.6.1 | \n",
132 | " ms | \n",
133 | " 2021-08-22 09:52:59+02:00 | \n",
134 | " 2021-08-22 09:51:54+02:00 | \n",
135 | " 2021-08-22 09:52:59+02:00 | \n",
136 | " 87.2668 | \n",
137 | " <<HKDevice: 0x2811a01e0>, name:Apple Watch, ma... | \n",
138 | "
\n",
139 | " \n",
140 | " 743769 | \n",
141 | " HeartRateVariabilitySDNN | \n",
142 | " Apple Watch von Patrick | \n",
143 | " 7.6.1 | \n",
144 | " ms | \n",
145 | " 2021-08-22 14:00:49+02:00 | \n",
146 | " 2021-08-22 13:59:45+02:00 | \n",
147 | " 2021-08-22 14:00:49+02:00 | \n",
148 | " 42.2700 | \n",
149 | " <<HKDevice: 0x2811a01e0>, name:Apple Watch, ma... | \n",
150 | "
\n",
151 | " \n",
152 | "
\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 | " Type | \n",
255 | " duration | \n",
256 | " durationUnit | \n",
257 | " totalDistance | \n",
258 | " totalDistanceUnit | \n",
259 | " totalEnergyBurned | \n",
260 | " totalEnergyBurnedUnit | \n",
261 | " sourceName | \n",
262 | " sourceVersion | \n",
263 | " device | \n",
264 | " creationDate | \n",
265 | " startDate | \n",
266 | " endDate | \n",
267 | "
\n",
268 | " \n",
269 | " \n",
270 | " \n",
271 | " 180 | \n",
272 | " Running | \n",
273 | " 18.876555 | \n",
274 | " min | \n",
275 | " 3.026288 | \n",
276 | " km | \n",
277 | " 202.358105 | \n",
278 | " kcal | \n",
279 | " Apple Watch von Patrick | \n",
280 | " 7.2 | \n",
281 | " <<HKDevice: 0x2811cf160>, name:Apple Watch, ma... | \n",
282 | " 2021-08-18 08:08:20+02:00 | \n",
283 | " 2021-08-18 07:49:27+02:00 | \n",
284 | " 2021-08-18 08:08:19+02:00 | \n",
285 | "
\n",
286 | " \n",
287 | " 181 | \n",
288 | " Running | \n",
289 | " 41.686440 | \n",
290 | " min | \n",
291 | " 7.154993 | \n",
292 | " km | \n",
293 | " 481.677116 | \n",
294 | " kcal | \n",
295 | " Apple Watch von Patrick | \n",
296 | " 7.2 | \n",
297 | " <<HKDevice: 0x2811cf160>, name:Apple Watch, ma... | \n",
298 | " 2021-08-19 09:49:37+02:00 | \n",
299 | " 2021-08-19 09:07:54+02:00 | \n",
300 | " 2021-08-19 09:49:36+02:00 | \n",
301 | "
\n",
302 | " \n",
303 | " 182 | \n",
304 | " Running | \n",
305 | " 19.186911 | \n",
306 | " min | \n",
307 | " 3.028681 | \n",
308 | " km | \n",
309 | " 201.441305 | \n",
310 | " kcal | \n",
311 | " Apple Watch von Patrick | \n",
312 | " 7.6.1 | \n",
313 | " <<HKDevice: 0x2811da490>, name:Apple Watch, ma... | \n",
314 | " 2021-08-20 19:03:11+02:00 | \n",
315 | " 2021-08-20 18:43:58+02:00 | \n",
316 | " 2021-08-20 19:03:09+02:00 | \n",
317 | "
\n",
318 | " \n",
319 | " 183 | \n",
320 | " Running | \n",
321 | " 20.134952 | \n",
322 | " min | \n",
323 | " 3.035656 | \n",
324 | " km | \n",
325 | " 199.521000 | \n",
326 | " kcal | \n",
327 | " Apple Watch von Patrick | \n",
328 | " 7.6.1 | \n",
329 | " <<HKDevice: 0x2811da490>, name:Apple Watch, ma... | \n",
330 | " 2021-08-21 17:29:21+02:00 | \n",
331 | " 2021-08-21 17:09:11+02:00 | \n",
332 | " 2021-08-21 17:29:19+02:00 | \n",
333 | "
\n",
334 | " \n",
335 | " 184 | \n",
336 | " Yoga | \n",
337 | " 22.080947 | \n",
338 | " min | \n",
339 | " 0.000000 | \n",
340 | " km | \n",
341 | " 50.902514 | \n",
342 | " kcal | \n",
343 | " Apple Watch von Patrick | \n",
344 | " 7.6.1 | \n",
345 | " <<HKDevice: 0x2811da490>, name:Apple Watch, ma... | \n",
346 | " 2021-08-22 08:47:27+02:00 | \n",
347 | " 2021-08-22 08:25:21+02:00 | \n",
348 | " 2021-08-22 08:47:26+02:00 | \n",
349 | "
\n",
350 | " \n",
351 | "
\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 | " Type | \n",
441 | " duration | \n",
442 | " durationUnit | \n",
443 | " totalDistance | \n",
444 | " totalDistanceUnit | \n",
445 | " totalEnergyBurned | \n",
446 | " totalEnergyBurnedUnit | \n",
447 | " sourceName | \n",
448 | " sourceVersion | \n",
449 | " device | \n",
450 | " creationDate | \n",
451 | " startDate | \n",
452 | " endDate | \n",
453 | "
\n",
454 | " \n",
455 | " \n",
456 | " \n",
457 | " 183 | \n",
458 | " Running | \n",
459 | " 20.134952 | \n",
460 | " min | \n",
461 | " 3.035656 | \n",
462 | " km | \n",
463 | " 199.521 | \n",
464 | " kcal | \n",
465 | " Apple Watch von Patrick | \n",
466 | " 7.6.1 | \n",
467 | " <<HKDevice: 0x2811da490>, name:Apple Watch, ma... | \n",
468 | " 2021-08-21 17:29:21+02:00 | \n",
469 | " 2021-08-21 17:09:11+02:00 | \n",
470 | " 2021-08-21 17:29:19+02:00 | \n",
471 | "
\n",
472 | " \n",
473 | "
\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 | " duration | \n",
768 | " totalDistance | \n",
769 | " totalEnergyBurned | \n",
770 | " creationDate | \n",
771 | "
\n",
772 | " \n",
773 | " \n",
774 | " \n",
775 | " 150 | \n",
776 | " 24.017083 | \n",
777 | " 4.021084 | \n",
778 | " 263.423771 | \n",
779 | " 2021-07-20 17:48:58+02:00 | \n",
780 | "
\n",
781 | " \n",
782 | " 151 | \n",
783 | " 23.304460 | \n",
784 | " 4.025121 | \n",
785 | " 261.674000 | \n",
786 | " 2021-07-21 17:35:13+02:00 | \n",
787 | "
\n",
788 | " \n",
789 | " 152 | \n",
790 | " 22.919020 | \n",
791 | " 4.027181 | \n",
792 | " 268.220008 | \n",
793 | " 2021-07-22 17:04:27+02:00 | \n",
794 | "
\n",
795 | " \n",
796 | " 153 | \n",
797 | " 23.090579 | \n",
798 | " 4.281389 | \n",
799 | " 276.979934 | \n",
800 | " 2021-07-23 17:49:31+02:00 | \n",
801 | "
\n",
802 | " \n",
803 | " 154 | \n",
804 | " 24.015762 | \n",
805 | " 4.119525 | \n",
806 | " 269.036218 | \n",
807 | " 2021-07-24 17:57:31+02:00 | \n",
808 | "
\n",
809 | " \n",
810 | "
\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 | 
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 |
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 |
16 |
17 |
25 |
26 |
27 |
28 | {% for todo in todo_list %}
29 |
30 |
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 |
16 |
17 |
24 |
25 |
26 |
27 | {% for todo in todo_list %}
28 |
29 |
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 |
16 |
17 |
24 |
25 |
26 |
27 | {% for todo in todo_list %}
28 |
29 |
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 |
--------------------------------------------------------------------------------