├── .gitignore ├── LICENSE ├── README.md ├── auto-unsubscriber └── unsubscriber.py ├── bittorrent-downloader └── download_torrent.py ├── blank-row-inserter └── blankRowInserter.py ├── cell-inverter ├── cellInverter.py └── example.xlsx ├── character-picture-grid └── character-picture-grid.py ├── chore-assignment-emailer └── chore-emailer.py ├── coin-toss └── coin-toss.py ├── comma-code └── comma-code.py ├── command-line-email └── yahoomail-emailer.py ├── custom-invitations ├── customInvitations.py └── guests.txt ├── custom-seating-cards ├── Pacifico.ttf ├── custom_cards.py ├── flower.png └── guests.txt ├── excel-to-csv-converter └── excelToCsv.py ├── fantasy-game-inventory └── game-inventory.py ├── fill-gaps └── fill_gaps.py ├── find-unneeded-files └── find_unneeded.py ├── image-site-downloader └── imgur-downloader.py ├── instant-messenger-bot ├── active_identifier.png └── slack_messenger.py ├── link-verification └── verify_links.py ├── looking-busy └── look_busy.py ├── mad-libs ├── input.txt ├── mad-libs.py └── output.txt ├── multiclipboard ├── mcb.db └── mcb.pyw ├── multiplication-table-maker ├── multiplicationTable.py └── multiplicationTable.xlsx ├── pdf-paranoia └── pdfParanoia.py ├── pdf-password-breaker ├── dictionary.txt └── passwordBreaker.py ├── photo-folder-finder └── photo_folder_finder.py ├── play-2048 └── 2048.py ├── prettified-stopwatch └── stopwatch.py ├── regex-search ├── input1.txt ├── input2.txt └── regex-search.py ├── regex-strip └── regex-strip.py ├── requirements.txt ├── resize-add-logo └── resizeAndAddLogo.py ├── selective-copy ├── nested │ ├── j.jpg │ └── p.png ├── result │ └── p.png ├── selective-copy.py └── t.jpg ├── strong-password-detector └── strong-password.py ├── table-printer └── table-printer.py ├── text-to-spreadsheet ├── text1.txt ├── text2.txt ├── text3.txt └── textToSheet.py ├── the-collatz-sequence └── collatz.py ├── umbrella-reminder └── umbrella_reminder.py ├── web-comic-downloader └── downloader.py └── worksheet-to-text-files ├── sheetToTextFile.py └── worksheet.xlsx /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *geckodriver.log 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kene Udeh 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 | # automate-the-boring-stuff-projects 2 | My project solutions for [automate the boring stuff with python by Al Sweigart](https://automatetheboringstuff.com) 3 | 4 | * Chapter 3 - Functions 5 | * [The Collatz Sequence](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/the-collatz-sequence) 6 | * Chapter 4 - Lists 7 | * [Comma Code](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/comma-code) 8 | * [Character Picture Grid](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/character-picture-grid) 9 | * Chapter 5 – Dictionaries and Structuring Data 10 | * [Fantasy Game Inventory](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/fantasy-game-inventory) 11 | * Chapter 6 – Manipulating Strings 12 | * [Table Printer](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/table-printer) 13 | * Chapter 7 – Pattern Matching with Regular Expressions 14 | * [Strong Pasword Detection](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/strong-password-detector) 15 | * [Regex Version Of Strip](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/regex-strip) 16 | * Chapter 8 – Reading and Writing Files 17 | * [Extending The MultiClipboard](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/multiclipboard) 18 | * [Mad Libs](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/mad-libs) 19 | * [Regex Search](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/regex-search) 20 | * Chapter 9 – Organizing Files 21 | * [Selective Copy](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/selective-copy) 22 | * [Deleting Unneeded Files](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/find-unneeded-files) 23 | * [Filling In The Gaps](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/fill-gaps) 24 | * Chapter 10 – Debugging 25 | * [Debugging Coin Toss](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/coin-toss) 26 | * Chapter 11 – Web Scraping 27 | * [Command Line Emailer](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/command-line-email) 28 | * [Image Site Downloader](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/image-site-downloader) 29 | * [2048](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/play-2048) 30 | * [Link Verification](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/link-verification) 31 | * Chapter 12 – Working with Excel Spreadsheets 32 | * [Multiplication Table Maker](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/multiplication-table-maker) 33 | * [Blank Row Inserter](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/blank-row-inserter) 34 | * [Spreadsheet Cell Inverter](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/cell-inverter) 35 | * [Text Files To Spreadsheet](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/text-to-spreadsheet) 36 | * [Spreadsheet To Text Files](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/worksheet-to-text-files) 37 | * Chapter 13 – Working with PDF and Word Documents 38 | * [PDF Paranoia](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/pdf-paranoia) 39 | * [Custom Invitations As Word Documents](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/custom-invitations) 40 | * [Brute-Force PDF Password Breaker](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/pdf-password-breaker) 41 | * Chapter 14 – Working with CSV Files and JSON Data 42 | * [Excel-To-CSV Converter](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/excel-to-csv-converter) 43 | * Chapter 15 – Keeping Time, Scheduling Tasks, and Launching Programs 44 | * [Prettified Stopwatch](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/prettified-stopwatch) 45 | * [Scheduled Web Comic Downloader](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/web-comic-downloader) 46 | * Chapter 16 – Sending Email and Text Messages 47 | * [Random Chore Assignment Emailer](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/chore-assignment-emailer) 48 | * [Umbrella Reminder](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/umbrella-reminder) 49 | * [Auto Unsubscriber](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/auto-unsubscriber) 50 | * [Controlling Your Computer Through Email](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/bittorrent-downloader) 51 | * Chapter 17 – Manipulating Images 52 | * [Extending And Fixing The Chapter Project Programs](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/resize-add-logo) 53 | * [Identifying Photo Folders On The Hard Drive](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/photo-folder-finder) 54 | * [Custom Seating Cards](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/custom-seating-cards) 55 | * Chapter 18 – Controlling the Keyboard and Mouse with GUI Automation 56 | * [Looking Busy](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/looking-busy) 57 | * [Instant Messenger Bot](https://github.com/kudeh/automate-the-boring-stuff-projects/tree/master/instant-messenger-bot) 58 | -------------------------------------------------------------------------------- /auto-unsubscriber/unsubscriber.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # unsubscriber.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 16 Project 5 | 6 | import imapclient 7 | import imaplib 8 | import bs4 9 | import pyzmail 10 | import webbrowser 11 | 12 | 13 | def unsubscribe(imap_address, email_address, password): 14 | """Checks unsubscribe links within emails and opens link 15 | Args: 16 | imap_address (str): email providers imap address 17 | email_address (str): email address 18 | password (str): password for email 19 | Returns: 20 | None 21 | """ 22 | imaplib._MAXLINE = 10000000 23 | imapObj = imapclient.IMAPClient(imap_address, ssl=True) 24 | # See https://support.google.com/accounts/answer/6010255 if (Login Error) 25 | imapObj.login(email_address, password) 26 | imapObj.select_folder('INBOX', readonly=True) 27 | UIDs = imapObj.search(['ALL']) 28 | 29 | for u in UIDs: 30 | rawMessages = imapObj.fetch([u], ['BODY[]', 'FLAGS']) 31 | message = pyzmail.PyzMessage.factory(rawMessages[u][b'BODY[]']) 32 | 33 | if message.html_part: 34 | html = message.html_part.get_payload().decode(message.html_part.charset) 35 | soup = bs4.BeautifulSoup(html, 'html.parser') 36 | linkElems = soup.select('a') 37 | 38 | for link in linkElems: 39 | 40 | if 'unsubscribe' in link.text.lower(): 41 | url = link.get('href') 42 | print('opening {}: '.format(url)) 43 | webbrowser.open(url) 44 | 45 | imapObj.logout() 46 | 47 | 48 | if __name__ == "__main__": 49 | 50 | email = input('Enter your email: ') 51 | password = input('Enter your email password: ') 52 | 53 | unsubscribe('imap.gmail.com', email, password) -------------------------------------------------------------------------------- /bittorrent-downloader/download_torrent.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # download_torrent.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 16 Project 5 | 6 | import subprocess 7 | import imaplib 8 | import imapclient 9 | import pyzmail 10 | from twilio.rest import Client 11 | 12 | 13 | def check_for_torrents(imap_address, email_address, password, verified_creds): 14 | """Checks email for torrent links from verified accounts 15 | Args: 16 | imap_address (str): email providers imap address 17 | email_address (str): email address 18 | password (str): password for email 19 | verified_creds (dict): dict containing verfied email and password 20 | """ 21 | imaplib._MAXLINE = 10000000 22 | imapObj = imapclient.IMAPClient(imap_address, ssl=True) 23 | # See https://support.google.com/accounts/answer/6010255 if (Login Error) 24 | imapObj.login(email_address, password) 25 | imapObj.select_folder('INBOX', readonly=True) 26 | UIDs = imapObj.search(['FROM ' + verified_creds['email']]) 27 | 28 | links = [] 29 | if UIDs: 30 | for u in UIDs: 31 | rawMessages = imapObj.fetch([u], ['BODY[]', 'FLAGS']) 32 | message = pyzmail.PyzMessage.factory(rawMessages[u][b'BODY[]']) 33 | text = message.text_part.get_payload().decode(message.text_part.charset) 34 | 35 | if verified_creds['password'] in text: 36 | html = message.html_part.get_payload().decode(message.html_part.charset) 37 | links.append(html) 38 | 39 | imapObj.delete_messages(UIDs) 40 | imapObj.expunge() 41 | 42 | imapObj.logout() 43 | 44 | return links 45 | 46 | 47 | def send_reminder(accountSID, authToken, myTwilioNumber, myCellPhone, message): 48 | """Sends a text using twilio 49 | Args: 50 | accountSID (str): twilio acct sid 51 | authToken (str): twilio authentication token 52 | myTwilioNumber (str): twilio number 53 | myCellPhone (str): my cell phone number 54 | Returns: 55 | None 56 | """ 57 | twilioCli = Client(accountSID, authToken) 58 | message = twilioCli.messages.\ 59 | create(body='Rain Alert! Water is (not) wet. Grab an Umbrella bro.',\ 60 | from_=myTwilioNumber, to=myCellPhone) 61 | 62 | 63 | if __name__ == "__main__": 64 | 65 | torrent_client = '' #enter path to qbittorent 66 | email = input('Enter your email: ') 67 | password = input('Enter your email password: ') 68 | 69 | links = check_for_torrents('imap.gmail.com', email, password,\ 70 | {'email': 'verified_email@ex.com', 'password': 'vpass'}) 71 | 72 | for l in links: 73 | tProc = subprocess.Popen(torrent_client + ' ' + l) 74 | tProc.wait() 75 | message = 'Download Finished For: ' + l 76 | send_reminder('A***************', 'A**************',\ 77 | '+1**********', '+1**********', message) -------------------------------------------------------------------------------- /blank-row-inserter/blankRowInserter.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # blankRowInserter.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 12 Project 5 | 6 | import sys 7 | 8 | 9 | import openpyxl 10 | 11 | 12 | def blankRowInserter(index, num_blanks, filename): 13 | """ 14 | Args: 15 | index (int): row in file to start insert 16 | num_blanks (int): number of blank rows to insert 17 | filename (str): filename to insert blanks 18 | Returns: 19 | None 20 | """ 21 | wb = openpyxl.load_workbook(filename) 22 | sheet = wb.active 23 | rows = tuple(sheet.rows) 24 | 25 | 26 | for rowObj in rows[::-1]: 27 | for cellObj in rowObj: 28 | c = cellObj.column 29 | r = cellObj.row 30 | 31 | if r >= index and r < index+num_blanks: 32 | sheet.cell(row=r+num_blanks, column=c).value = cellObj.value 33 | sheet.cell(row=r, column=c).value = '' 34 | elif r >= index+num_blanks: 35 | sheet.cell(row=r+num_blanks, column=c).value = cellObj.value 36 | 37 | wb.save('result_'+filename) 38 | 39 | 40 | if __name__ == "__main__": 41 | 42 | num_args = len(sys.argv) 43 | 44 | if num_args < 4: 45 | print("usage: python blankRowInserter.py 3 2 myProduce.xlsx") 46 | else: 47 | index = int(sys.argv[1]) 48 | num_blanks = int(sys.argv[2]) 49 | filename = sys.argv[3] 50 | 51 | blankRowInserter(index, num_blanks, filename) 52 | 53 | -------------------------------------------------------------------------------- /cell-inverter/cellInverter.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # cellInverter.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 12 Project 5 | 6 | 7 | import openpyxl 8 | 9 | 10 | def invertCells(filename): 11 | """inverts all cells in a workbook 12 | Args: 13 | filename (str): excel file to invert cells in 14 | Returns: 15 | None 16 | """ 17 | wb = openpyxl.load_workbook(filename) 18 | sheet = wb.active 19 | newSheet = wb.create_sheet(index=0, title='inverted') 20 | 21 | for rowObj in sheet.rows: 22 | for cellObj in rowObj: 23 | colIndex = cellObj.column 24 | rowIndex = cellObj.row 25 | 26 | newSheet.cell(row=colIndex, column=rowIndex).value = cellObj.value 27 | 28 | wb.save('result_'+filename) 29 | 30 | 31 | if __name__ == "__main__": 32 | invertCells('example.xlsx') -------------------------------------------------------------------------------- /cell-inverter/example.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/cell-inverter/example.xlsx -------------------------------------------------------------------------------- /character-picture-grid/character-picture-grid.py: -------------------------------------------------------------------------------- 1 | # character-picture-grid.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 4 Project 4 | 5 | def rotate90(grid): 6 | """Rotates a grid 90 degrees 7 | Args: 8 | grid (list): a 2d list representing a grid 9 | Returns: 10 | grid (list): rotated copy of a 2d grid 11 | """ 12 | return list(zip(*grid[::-1])) 13 | 14 | def print2DGrid(grid): 15 | """Prints a 2D grid 16 | Args: 17 | grid (list): 2D grid 18 | Returns: 19 | None 20 | """ 21 | for row in range(len(grid)): 22 | for col in range(len(grid[row])): 23 | print(grid[row][col], end='') 24 | 25 | print() 26 | 27 | if __name__ == "__main__": 28 | 29 | grid = [['.', '.', '.', '.', '.', '.'], 30 | ['.', 'O', 'O', '.', '.', '.'], 31 | ['O', 'O', 'O', 'O', '.', '.'], 32 | ['O', 'O', 'O', 'O', 'O', '.'], 33 | ['.', 'O', 'O', 'O', 'O', 'O'], 34 | ['O', 'O', 'O', 'O', 'O', '.'], 35 | ['O', 'O', 'O', 'O', '.', '.'], 36 | ['.', 'O', 'O', '.', '.', '.'], 37 | ['.', '.', '.', '.', '.', '.']] 38 | 39 | gridRotated = rotate90(grid) 40 | print2DGrid(gridRotated) -------------------------------------------------------------------------------- /chore-assignment-emailer/chore-emailer.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # chore-emailer.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 16 Project 5 | 6 | import random 7 | import smtplib 8 | 9 | def emailer(chores, emails): 10 | """emails random chores to emails 11 | Args: 12 | chores: list of chores 13 | emails: list of emails to send chores 14 | Returns: 15 | None 16 | """ 17 | if not emails: 18 | print('emails list should not be empty') 19 | return 20 | 21 | if not chores: 22 | print('chores list should not be empty') 23 | return 24 | 25 | chores_dict = {} 26 | 27 | f = 0 # front of emails list 28 | 29 | while chores: 30 | 31 | randomChore = random.choice(chores) 32 | chores.remove(randomChore) 33 | email = emails[f] 34 | chores_dict.setdefault(email, []) 35 | chores_dict[email].append(randomChore) 36 | 37 | f = (f+1) % len(emails) # use list circularly 38 | 39 | smtpObj = smtplib.SMTP('smtp.gmail.com', 587) 40 | smtpObj.ehlo() 41 | 42 | 43 | email = input('Enter your email: ') 44 | password = input('Enter your email password: ') 45 | 46 | smtpObj.starttls() 47 | smtpObj.login(email, password) 48 | # See https://support.google.com/accounts/answer/6010255 if (Bad Credentials Error) 49 | 50 | for k, v in chores_dict.items(): 51 | c = ', '.join(v) 52 | print('Sending email to %s...' % k) 53 | sendmailStatus = smtpObj.sendmail(email, k, \ 54 | 'Subject: Your Chores.\nHi There!, {} are your chores'.format(c)) 55 | if sendmailStatus != {}: 56 | print('There was a problem sending email to %s: %s' % (email, 57 | sendmailStatus)) 58 | 59 | smtpObj.quit() 60 | 61 | 62 | 63 | 64 | 65 | if __name__ == "__main__": 66 | emailer(['dishes', 'bathroom', 'vacuum', 'walk dog'], ['example@yahoo.com, example2@yahoo.com']) -------------------------------------------------------------------------------- /coin-toss/coin-toss.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # coin-toss.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 10 Project 5 | 6 | import random 7 | 8 | if __name__ == "__main__": 9 | 10 | guess = '' 11 | options = ['tails', 'heads'] 12 | 13 | while guess not in ('heads', 'tails'): 14 | print('Guess the coin toss! Enter heads or tails:') 15 | guess = input() 16 | 17 | toss = random.randint(0, 1) # 0 is tails, 1 is heads 18 | 19 | if guess == options[toss]: 20 | print('You got it!') 21 | else: 22 | print('Nope! Guess again!') 23 | guess = input() 24 | if guess == options[toss]: 25 | print('You got it!') 26 | else: 27 | print('Nope. You are really bad at this game.') -------------------------------------------------------------------------------- /comma-code/comma-code.py: -------------------------------------------------------------------------------- 1 | # comma-code.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 4 Project 4 | 5 | def comma_code(items): 6 | """ Combines list into a string of the form item1, item2, and item 3 7 | Args: 8 | items (list): List of strings 9 | 10 | Returns: 11 | string: list items combined into a string 12 | """ 13 | item_len = len(items) 14 | 15 | if item_len == 0: 16 | return '' 17 | elif item_len == 1: 18 | return items[0] 19 | 20 | return ', '.join(items[:-1]) + ', and ' + items[-1] 21 | 22 | 23 | if __name__ == "__main__": 24 | spam = ['apples', 'bananas', 'tofu', 'cats'] 25 | print(comma_code(spam)) -------------------------------------------------------------------------------- /command-line-email/yahoomail-emailer.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # yahoomail-emailer.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 11 Project 5 | 6 | import sys 7 | import re 8 | import time 9 | 10 | from selenium import webdriver 11 | from selenium.webdriver.common.keys import Keys 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | from selenium.webdriver.support import expected_conditions as EC 14 | from selenium.webdriver.common.by import By 15 | from selenium.common.exceptions import TimeoutException 16 | 17 | def emailer(sendFrom, password, sendTo, message): 18 | """ Sends message to email address 19 | Args: 20 | sendFrom(str): senders email address 21 | password(str): senders password 22 | sendTo (str): receipient email address 23 | message (str): message to send 24 | Returns: 25 | None 26 | """ 27 | emailRegex = re.compile(r'''( 28 | [a-zA-Z0-9._%+-]+ # username 29 | @ # @ symbol 30 | [a-zA-Z0-9.-]+ # domain name 31 | (\.[a-zA-Z]{2,4}) # dot-something 32 | )''', re.VERBOSE) 33 | 34 | if not emailRegex.match(sendTo): 35 | print("**Invalid Email Address Provided**") 36 | 37 | else: 38 | 39 | delay = 10 # seconds 40 | 41 | # Set up selenium browser 42 | browser = webdriver.Firefox(executable_path='/Users/keneudeh/Downloads/geckodriver') 43 | browser.get('https://login.yahoo.com') 44 | 45 | # Enter yahoo email address 46 | loginUsernameElem = browser.find_element_by_id('login-username') 47 | loginUsernameElem.send_keys(sendFrom) 48 | loginUsernameElem.send_keys(Keys.TAB) 49 | 50 | # Press 'Next' to go to password filed page 51 | nextBtnElem = browser.find_element_by_id('login-signin') 52 | nextBtnElem.send_keys(Keys.ENTER) 53 | 54 | # Wait for 'password' field page to load then enter password, tab to submit btn 55 | loginPwdElem = wait_for_page_load(browser, By.ID, 'login-passwd', delay) 56 | loginPwdElem.send_keys(password) 57 | loginPwdElem.send_keys(Keys.TAB) 58 | 59 | # Select 'sign in' btn, press ENTER 60 | signInBtnElem = browser.find_element_by_id('login-signin') 61 | signInBtnElem.send_keys(Keys.ENTER) 62 | 63 | # Click on link to redirect to mail.yahoo.com 64 | mailLinkElem = wait_for_page_load(browser, By.ID, 'uh-mail-link', delay) 65 | mailLinkElem.send_keys(Keys.ENTER) 66 | 67 | # Click on 'Compose' button link 68 | composeBtnElem = wait_for_page_load(browser, By.LINK_TEXT, 'Compose', delay) 69 | composeBtnElem.send_keys(Keys.ENTER) 70 | 71 | # use 'time.sleep' to wait instead of 'wait_for..', cause of refreshing issue 72 | time.sleep(delay) 73 | # Enter email address 74 | messageToElem = browser.find_element_by_id('message-to-field') 75 | messageToElem.send_keys(sendTo) 76 | # Enter Subject 77 | messageSubjectElem = browser.find_element_by_css_selector('input[placeholder = "Subject"]') 78 | messageSubjectElem.send_keys('FYI') 79 | # Enter email body 80 | messageBodyElem = browser.find_element_by_css_selector('div[data-test-id = "rte"]') 81 | messageBodyElem.send_keys(message) 82 | # Send email 83 | composeSendBtnElem = browser.find_element_by_css_selector('button[data-test-id = "compose-send-button"]') 84 | composeSendBtnElem.send_keys(Keys.ENTER) 85 | 86 | def wait_for_page_load(browser, by, selector, delay): 87 | """ waits for page to load 88 | Args: 89 | browser (webdriver): Selenium web driver 90 | by (By): Selenium By object 91 | selector (str): selector of element, which will be available on page load 92 | delay (int): number of seconds to wait 93 | Returns: 94 | elem (Element): Selenium element 95 | """ 96 | try: 97 | elem = WebDriverWait(browser, delay).until(EC.presence_of_element_located((by, selector))) 98 | print("Page is ready!") 99 | except TimeoutException: 100 | print("Loading took too much time!") 101 | 102 | return elem 103 | 104 | if __name__ == '__main__': 105 | 106 | #$ python yahoomail-emailer.py sendingto@gmail.com message I am sending 107 | 108 | if len(sys.argv) > 2: 109 | sendTo = sys.argv[1] 110 | message = ' '.join(sys.argv[2:]) 111 | 112 | sendFrom = input('Enter your yahoomail email: ') 113 | password = input('Enter your yahoomail password: ') 114 | 115 | emailer(sendFrom, password, sendTo, message) 116 | 117 | elif len(sys.argv) == 2: 118 | print('missing message to send') 119 | 120 | else: 121 | print("Add command line args %s and %s" % ("'email_address'", "'message'")) 122 | -------------------------------------------------------------------------------- /custom-invitations/customInvitations.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # customInvitations.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 13 Project 5 | 6 | import os 7 | 8 | 9 | import docx 10 | from docx.enum.text import WD_ALIGN_PARAGRAPH 11 | from docx.shared import Pt 12 | 13 | 14 | def createInvitations(txtFile, docName): 15 | """Creates invitations based on names in txt file 16 | Args: 17 | txtFile (str): text file to read from 18 | docName (str): doc file to save invitations in 19 | """ 20 | doc = docx.Document() 21 | 22 | intro = 'It would be a pleasure to have the company of' 23 | address = 'at 11101 Memory lane on the evening of' 24 | date = 'April 31st' 25 | time = "at 24 O'Clock" 26 | 27 | with open(txtFile) as guestList: 28 | for guest in guestList: 29 | name = guest[:-1] 30 | p1 = doc.add_paragraph() 31 | p1.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER 32 | f1 = p1.add_run(intro) 33 | f1.font.bold = True 34 | f1.font.italic = True 35 | f1.font.size = Pt(13) 36 | 37 | p2 = doc.add_paragraph() 38 | p2.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER 39 | f2 = p2.add_run(name) 40 | f2.font.bold = True 41 | f2.font.size = Pt(15) 42 | 43 | p3 = doc.add_paragraph() 44 | p3.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER 45 | f3 = p3.add_run(address) 46 | f3.font.bold = True 47 | f3.font.italic = True 48 | f3.font.size = Pt(12) 49 | 50 | p4 = doc.add_paragraph() 51 | p4.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER 52 | f4 = p4.add_run(date) 53 | f4.font.size = Pt(12) 54 | 55 | p5 = doc.add_paragraph() 56 | p5.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER 57 | f5 = p5.add_run(time) 58 | f5.font.bold = True 59 | f5.font.italic = True 60 | f5.font.size = Pt(12) 61 | 62 | doc.add_page_break() 63 | 64 | doc.save(docName) 65 | 66 | 67 | 68 | if __name__ == "__main__": 69 | createInvitations('guests.txt', 'invitations.docx') -------------------------------------------------------------------------------- /custom-invitations/guests.txt: -------------------------------------------------------------------------------- 1 | Prof. Plum 2 | Miss Scarlet 3 | Col. Mustard 4 | Al Sweigart 5 | Robocop -------------------------------------------------------------------------------- /custom-seating-cards/Pacifico.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/custom-seating-cards/Pacifico.ttf -------------------------------------------------------------------------------- /custom-seating-cards/custom_cards.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # custom_cards.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 17 Project 5 | 6 | import os 7 | 8 | 9 | from PIL import Image, ImageDraw, ImageFont 10 | 11 | def make_cards(guestList): 12 | """Makes custom cards for each guest 13 | Args: 14 | guestList (str): Path to file containing guest list 15 | Returns: 16 | None 17 | """ 18 | # make folder to store resulting images 19 | os.makedirs('imageCards', exist_ok=True) 20 | 21 | # load flower image 22 | flowerImg = Image.open('flower.png') 23 | 24 | # read each guest from file 25 | with open(guestList) as file: 26 | for line in file: 27 | guest = line[:-1] 28 | 29 | # create image 30 | card = Image.new('RGBA', (288, 360), 'white') 31 | # add flower image 32 | card.paste(flowerImg, (0, 0)) 33 | 34 | # create border around image 35 | border = Image.new('RGBA', (291, 363), 'black') 36 | border.paste(card, (3,3)) 37 | 38 | # draw guest name 39 | draw_obj = ImageDraw.Draw(border) 40 | card_font = ImageFont.truetype('Pacifico.ttf', 24) 41 | draw_obj.text((120, 100), guest, fill='red', font=card_font) 42 | 43 | # save resulting image 44 | imageName = '{}_card.png'.format(guest) 45 | border.save(os.path.join('imageCards', imageName)) 46 | 47 | 48 | if __name__ == "__main__": 49 | make_cards('guests.txt') -------------------------------------------------------------------------------- /custom-seating-cards/flower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/custom-seating-cards/flower.png -------------------------------------------------------------------------------- /custom-seating-cards/guests.txt: -------------------------------------------------------------------------------- 1 | Prof. Plum 2 | Miss Scarlet 3 | Col. Mustard 4 | Al Sweigart 5 | Robocop -------------------------------------------------------------------------------- /excel-to-csv-converter/excelToCsv.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # excelToCsv.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 14 Project 5 | 6 | import os 7 | import csv 8 | 9 | 10 | import openpyxl 11 | 12 | 13 | def excelToCsv(folder): 14 | """converts sheets in every excel file in a folder to csv 15 | Args: 16 | folder (str): folder containing excel files 17 | Returns: 18 | None 19 | """ 20 | for excelFile in os.listdir(folder): 21 | # Skip non-xlsx files, load the workbook object. 22 | if not excelFile.endswith('xlsx'): 23 | continue 24 | wb = openpyxl.load_workbook(excelFile) 25 | 26 | for sheetName in wb.get_sheet_names(): 27 | # Loop through every sheet in the workbook. 28 | sheet = wb.get_sheet_by_name(sheetName) 29 | 30 | # Create the CSV filename from the Excel filename and sheet title. 31 | csvFilename = excelFile.split('.')[0]+'_'+sheet.title+'.csv' 32 | csvFileObj = open(csvFilename, 'w', newline='') 33 | 34 | # Create the csv.writer object for this CSV file. 35 | csvWriter = csv.writer(csvFileObj) 36 | 37 | # Loop through every row in the sheet. 38 | for rowObj in sheet.rows: 39 | rowData = [] # append each cell to this list 40 | # Loop through each cell in the row. 41 | for cellObj in rowObj: 42 | # Append each cell's data to rowData. 43 | rowData.append(cellObj.value) 44 | # Write the rowData list to the CSV file. 45 | csvWriter.writerow(rowData) 46 | 47 | csvFileObj.close() 48 | 49 | if __name__ == "__main__": 50 | excelToCsv('.') -------------------------------------------------------------------------------- /fantasy-game-inventory/game-inventory.py: -------------------------------------------------------------------------------- 1 | # game-inventory.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 5 Project 4 | 5 | def displayInventory(inventory): 6 | """ Displays how much of what a player has in inventory 7 | 8 | Args: 9 | inventory (dict): Inventory containing items and their counts 10 | 11 | Returns: 12 | None 13 | """ 14 | print("Inventory:") 15 | item_total = 0 16 | 17 | for k, v in inventory.items(): 18 | print(v, ' ', k) 19 | item_total += v 20 | 21 | print("Total number of items: " + str(item_total)) 22 | 23 | def addToInventory(inventory, addedItems): 24 | """ Add Items to inventory 25 | Args: 26 | inventory (dict): Inventory containing items and their counts 27 | addedItems (list): Items to add to inventory 28 | 29 | Returns: 30 | updatedInventory (dict): Inventory containing updated items and their counts 31 | """ 32 | updatedInventory = dict(inventory) 33 | # your code goes here 34 | for item in addedItems: 35 | updatedInventory.setdefault(item, 0) 36 | updatedInventory[item] += 1 37 | 38 | return updatedInventory 39 | 40 | 41 | 42 | if __name__ == "__main__": 43 | 44 | stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} 45 | displayInventory(stuff) 46 | 47 | inv = {'gold coin': 42, 'rope': 1} 48 | dragonLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby'] 49 | inv = addToInventory(inv, dragonLoot) 50 | displayInventory(inv) -------------------------------------------------------------------------------- /fill-gaps/fill_gaps.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # fill_gaps.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 9 Project 5 | 6 | import os 7 | import re 8 | import shutil 9 | 10 | def getFilesWithPrefix(folderPath, prefix): 11 | """get all files with a certain prefix 12 | Args: 13 | folderPath (str): path to folder to search 14 | Returns: 15 | 16 | """ 17 | fileRegex = re.compile(prefix+'(\d{1,})(.\w+)') 18 | fileList = sorted( [file for file in os.listdir(folderPath) if fileRegex.match(file)] ) 19 | return fileList 20 | 21 | def fillGaps(folderPath, prefix): 22 | """fill gaps in numbering of files in folder 23 | Args: 24 | folderPath (str): path to folder to search 25 | prefix (str): prefix of files to fill gap 26 | Returns: 27 | None 28 | """ 29 | fileList = getFilesWithPrefix(folderPath, prefix) # files sorted ascending order 30 | fileRegex = re.compile(prefix+'(\d{1,})(.\w+)') 31 | 32 | start = int(fileRegex.search(fileList[0]).group(1)) # start with the minimum number in list 33 | count = start # count to be incremented during checks for gaps 34 | max_length = len(fileRegex.search(fileList[-1]).group(1)) # max length of largest number, for padding zeros 35 | 36 | for file in fileList: 37 | 38 | mo = fileRegex.search(file) 39 | fileNum = int(mo.group(1)) 40 | 41 | if fileNum != count: 42 | newFileName = prefix + '0'*(max_length-len(str(fileNum))) + str(count) + mo.group(2) 43 | shutil.move(os.path.abspath(file), os.path.abspath(newFileName)) 44 | 45 | count += 1 46 | 47 | def insertGaps(folderPath, prefix, index): 48 | """insert gaps in numbering of files in folder 49 | Args: 50 | folderPath (str): path to folder to search 51 | prefix (str): prefix of files to insert gap 52 | index (int): where to insert the gap 53 | Returns: 54 | None 55 | """ 56 | 57 | fileList = getFilesWithPrefix(folderPath, prefix) # files sorted ascending order 58 | fileRegex = re.compile(prefix+'(\d{1,})(.\w+)') 59 | 60 | max_length = len(fileRegex.search(fileList[-1]).group(1)) # max length of largest number, for padding zeros 61 | 62 | firstIndex = int(fileRegex.search(fileList[0]).group(1)) # smallest number 63 | lastIndex = int(fileRegex.search(fileList[-1]).group(1)) # largest number 64 | 65 | if index >= firstIndex and index <= lastIndex: # if gap index falls in range 66 | 67 | i = 0 68 | currIndex = firstIndex 69 | while currIndex < index: 70 | # loop till the file number is >= gap index 71 | i += 1 72 | currIndex = int(fileRegex.search(fileList[i]).group(1)) 73 | 74 | if currIndex == index: # if gap index is taken, make a gap else already free 75 | 76 | for file in fileList[i:][::-1]: 77 | # loop through reversed file list, to prevent overwriting results and increment file number 78 | 79 | mo = fileRegex.search(file) 80 | newFileNum = int(mo.group(1)) + 1 81 | newFileName = prefix + '0'*(max_length-len(str(newFileNum))) + str(newFileNum) + mo.group(2) 82 | shutil.move(os.path.abspath(file), os.path.abspath(newFileName)) 83 | 84 | 85 | if __name__ == "__main__": 86 | 87 | with open('spam001.txt', 'w') as s1, open('spam003.txt', 'w') as s3: 88 | s1.write('spam001') 89 | s3.write('spam003') 90 | 91 | fillGaps('.', 'spam') 92 | #insertGaps('.', 'spam', 2) 93 | -------------------------------------------------------------------------------- /find-unneeded-files/find_unneeded.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # find_unneeded.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 9 Project 5 | 6 | import os 7 | 8 | def findUnneeded(folderPath, rejectSize): 9 | """walks through a folder tree and searches for exceptionally large files or folders 10 | Args: 11 | folderPath (str): path of folder to walk 12 | rejectSize (int): file size in bytes to possibly delete 13 | Returns: 14 | None 15 | """ 16 | root = os.path.abspath(folderPath) 17 | 18 | for doc in os.listdir(root): 19 | 20 | docPath = os.path.join(root, doc) 21 | 22 | if os.path.isdir(docPath): 23 | size = getDirSize(docPath) 24 | else: 25 | size = os.path.getsize(docPath) 26 | 27 | if size > rejectSize: 28 | print(f'{docPath}: {size}') 29 | 30 | def getDirSize(start_path): 31 | """Finds the total size of a folder and it's contents 32 | Args: 33 | start_path (str): path to folder 34 | Returns: 35 | size (int): folder size in bytes 36 | """ 37 | size = 0 38 | 39 | for folderName, subFolder, filename in os.walk(start_path): 40 | for file in filename: 41 | filePath = os.path.join(folderName, file) 42 | size += os.path.getsize(filePath) 43 | 44 | 45 | return size 46 | 47 | 48 | if __name__ == '__main__': 49 | findUnneeded('..', 1000) -------------------------------------------------------------------------------- /image-site-downloader/imgur-downloader.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # imgur-downloader.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 11 Project 5 | 6 | import os 7 | import requests 8 | import bs4 9 | 10 | def downloader(query, max_save, output_path): 11 | """ 12 | Args: 13 | query (str): search query 14 | max_save (int): max number of images to save to results 15 | Returns: 16 | None 17 | """ 18 | 19 | # create imgur search url 20 | searchUrl = 'https://imgur.com/search' 21 | queryUrl = searchUrl+'?q='+query 22 | 23 | # set up output_path 24 | abs_output_path = os.path.abspath(output_path) 25 | os.makedirs(abs_output_path, exist_ok=True) 26 | 27 | # Make request to imgur with query 28 | res1 = requests.get(queryUrl) 29 | 30 | try: 31 | res1.raise_for_status() 32 | 33 | # parse res.text with bs4 to images 34 | imugurSoup = bs4.BeautifulSoup(res1.text, 'html.parser') 35 | images = imugurSoup.select('.image-list-link img') 36 | 37 | # extract number image urls 38 | num_to_save = min(max_save, len(images)) 39 | download_links = ['https:'+img.get('src') for img in images[:num_to_save]] 40 | 41 | # make requests for extracted url 42 | for link in download_links: 43 | 44 | # request image link from imgur 45 | res2 = requests.get(link) 46 | 47 | try: 48 | res2.raise_for_status() 49 | 50 | # save to file with url base name in folder results 51 | imgFile = open(os.path.join(abs_output_path, os.path.basename(link)), 'wb') 52 | for chunk in res2.iter_content(100000): 53 | imgFile.write(chunk) 54 | imgFile.close() 55 | 56 | except Exception as exc: 57 | print('There was a problem: %s' % (exc)) 58 | 59 | 60 | 61 | except Exception as exc: 62 | print('There was a problem: %s' % (exc)) 63 | 64 | 65 | if __name__ == '__main__': 66 | downloader('messi', 10, 'results') -------------------------------------------------------------------------------- /instant-messenger-bot/active_identifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/instant-messenger-bot/active_identifier.png -------------------------------------------------------------------------------- /instant-messenger-bot/slack_messenger.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # slack_messenger.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 18 Project 5 | 6 | import time 7 | 8 | 9 | import pyautogui 10 | 11 | 12 | def send_message(contact, message): 13 | """Sends message to an active slack contact 14 | Args: 15 | contact (str): contacts name on slack 16 | message (str): message to send to friend 17 | Returns: 18 | None 19 | """ 20 | try: 21 | print('5 seconds to navigate to slack app..') 22 | time.sleep(5) 23 | 24 | # Use JumpTo slack feature 25 | pyautogui.hotkey('command', 'k') 26 | time.sleep(1) 27 | # Enter contact name in search box, click enter 28 | pyautogui.typewrite(contact) 29 | time.sleep(1) 30 | pyautogui.typewrite(['enter']) 31 | time.sleep(1) 32 | 33 | active = pyautogui.locateOnScreen('active_identifier.png') 34 | 35 | if not active: 36 | print(f'{contact} is not active, skipped contact') 37 | return 38 | 39 | print('Contact is active, sending message...') 40 | pyautogui.typewrite(['tab']) 41 | pyautogui.typewrite(message) 42 | pyautogui.typewrite(['enter']) 43 | 44 | except KeyboardInterrupt: 45 | print('Process was cancelled..') 46 | 47 | 48 | if __name__ == "__main__": 49 | 50 | contacts = input('Enter contact list, separated by space >> Messi Onazi John: ').split(' ') 51 | message = input('Enter the message you wish to send out to them: ') 52 | 53 | for c in contacts: 54 | contact = c.strip() 55 | send_message(contact, message) -------------------------------------------------------------------------------- /link-verification/verify_links.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # verify_links.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 11 Project 5 | 6 | import requests 7 | import bs4 8 | 9 | def verify(url): 10 | """verifies all links within a page, prints broken links 11 | Args: 12 | url (str): url of page to check 13 | Returns: 14 | None 15 | """ 16 | 17 | res1 = requests.get(url) 18 | 19 | try: 20 | res1.raise_for_status() 21 | 22 | soup = bs4.BeautifulSoup(res1.text, 'html.parser') 23 | pageLinks = [link.get('href') for link in soup.select('a') if link.get('href')] 24 | 25 | brokenCount = 0 26 | goodCount = 0 27 | 28 | for link in pageLinks: 29 | 30 | if link.startswith('http'): 31 | res2 = requests.get(link) 32 | 33 | try: 34 | 35 | res2.raise_for_status() 36 | print(f'Good: {link}') 37 | goodCount += 1 38 | 39 | except Exception as exc: 40 | print(f'Broken: {link}') 41 | brokenCount += 1 42 | 43 | print(f'{goodCount} Good. {brokenCount} Broken') 44 | 45 | 46 | except Exception as exc: 47 | print('There was a problem: %s' % (exc)) 48 | 49 | 50 | if __name__ == "__main__": 51 | verify('https://automatetheboringstuff.com/chapter11/') -------------------------------------------------------------------------------- /looking-busy/look_busy.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # look_busy.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 18 Project 5 | 6 | import time 7 | 8 | 9 | import pyautogui 10 | 11 | 12 | def make_busy(): 13 | """Moves mouse every 10 seconds to keep apps active 14 | Args: 15 | None 16 | Returns: 17 | None 18 | """ 19 | print('Press CTRL-C to quit.') 20 | try: 21 | while True: 22 | pyautogui.moveRel(5, 0, 0.5) 23 | pyautogui.moveRel(-5, 0, 0.5) 24 | time.sleep(10) 25 | except KeyboardInterrupt: 26 | print('Process has quit...') 27 | 28 | 29 | if __name__ == "__main__": 30 | make_busy() -------------------------------------------------------------------------------- /mad-libs/input.txt: -------------------------------------------------------------------------------- 1 | The ADJECTIVE panda walked to the NOUN and then VERB. 2 | A nearby NOUN was unaffected by these events. -------------------------------------------------------------------------------- /mad-libs/mad-libs.py: -------------------------------------------------------------------------------- 1 | # mad-libs.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 8 Project 4 | 5 | import os 6 | import re 7 | 8 | def madLibs(input_file, output_file): 9 | """lets the user add their own text anywhere the word ADJECTIVE, NOUN, ADVERB, or VERB appears in the text file 10 | Args: 11 | filename (str): name of file to parse 12 | Returns: 13 | None 14 | """ 15 | regex = re.compile(r'(NOUN|ADJECTIVE|ADVERB|VERB)') 16 | 17 | with open(input_file, 'r') as in_file, open(output_file, 'w') as out_file: 18 | 19 | content = in_file.read() 20 | 21 | matches = regex.findall(content) 22 | 23 | for found in matches: 24 | sub = input('Enter a ' + found + ': ') 25 | content = content.replace(found, sub, 1) 26 | 27 | out_file.write(content) 28 | print(content) 29 | 30 | if __name__ == "__main__": 31 | madLibs('input.txt', 'output.txt') 32 | -------------------------------------------------------------------------------- /mad-libs/output.txt: -------------------------------------------------------------------------------- 1 | The a panda walked to the b and then c. 2 | A nearby d was unaffected by these events. -------------------------------------------------------------------------------- /multiclipboard/mcb.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/multiclipboard/mcb.db -------------------------------------------------------------------------------- /multiclipboard/mcb.pyw: -------------------------------------------------------------------------------- 1 | #! python3 2 | # mcb.pyw - Saves and loads pieces of text to the clipboard. 3 | # Usage: py.exe mcb.pyw save - Saves clipboard to keyword. 4 | # py.exe mcb.pyw - Loads keyword to clipboard. 5 | # py.exe mcb.pyw list - Loads all keywords to clipboard. 6 | 7 | import shelve, pyperclip, sys 8 | 9 | mcbShelf = shelve.open('mcb') 10 | 11 | 12 | if len(sys.argv) == 3: 13 | # Save & Delete clipboard content. 14 | 15 | if sys.argv[1].lower() == 'save': 16 | mcbShelf[sys.argv[2]] = pyperclip.paste() 17 | 18 | elif sys.argv[1].lower() == 'delete': 19 | 20 | if sys.argv[2]: # Delete key 21 | del mcbShelf[sys.argv[2]] 22 | pyperclip.copy('') 23 | 24 | 25 | elif len(sys.argv) == 2: 26 | # List, Delete all keywords and load content. 27 | 28 | if sys.argv[1].lower() == 'list': 29 | pyperclip.copy(str(list(mcbShelf.keys()))) 30 | 31 | elif sys.argv[1].lower() == 'delete': 32 | for key in list(mcbShelf.keys()): 33 | del mcbShelf[key] 34 | pyperclip.copy('') 35 | 36 | elif sys.argv[1] in mcbShelf: 37 | pyperclip.copy(mcbShelf[sys.argv[1]]) 38 | 39 | mcbShelf.close() 40 | 41 | if __name__ == "__main__": 42 | pass -------------------------------------------------------------------------------- /multiplication-table-maker/multiplicationTable.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # multiplicationTable.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 12 Project 5 | 6 | import sys 7 | import os 8 | 9 | import openpyxl 10 | from openpyxl.styles import Font 11 | 12 | 13 | def multiplicationTable(n, filename='multiplicationTable.xlsx'): 14 | """ create multiplication table, store in excel file 15 | Args: 16 | n (int): n for nxn multiplication table 17 | filename (str): name of excel file to store table in 18 | Returns: 19 | None 20 | """ 21 | # create excel file 22 | wb = openpyxl.Workbook() 23 | # create worksheet 24 | sheet = wb.active 25 | sheet.title = '{}x{} multiplication table'.format(n, n) 26 | boldFont = Font(bold=True) 27 | 28 | # write row headers 29 | for i in range(1, n+1): 30 | sheet.cell(row=i+1, column=1).value = i 31 | sheet.cell(row=i+1, column=1).font = boldFont 32 | 33 | # write column headers 34 | for i in range(1, n+1): 35 | sheet.cell(row=1, column=i+1).value = i 36 | sheet.cell(row=1, column=i+1).font = boldFont 37 | 38 | # write multiplication table 39 | for row in range(1, n+1): 40 | for col in range(1, n+1): 41 | sheet.cell(row=row+1, column=col+1).value = row*col 42 | 43 | # save table 44 | wb.save(filename) 45 | 46 | 47 | if __name__ == "__main__": 48 | 49 | if len(sys.argv) < 2: 50 | print('enter value for n') 51 | else: 52 | n = sys.argv[1] 53 | multiplicationTable(int(n)) -------------------------------------------------------------------------------- /multiplication-table-maker/multiplicationTable.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/multiplication-table-maker/multiplicationTable.xlsx -------------------------------------------------------------------------------- /pdf-paranoia/pdfParanoia.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # pdfParanoia.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 13 Project 5 | 6 | import os 7 | 8 | 9 | import PyPDF2 10 | 11 | 12 | def encryptPDFs(root, password): 13 | """Encrypts all pdfs folder walk 14 | Args: 15 | root (str): folder path to walk 16 | password (str): password to encrypt pdfs with 17 | Returns: 18 | None 19 | """ 20 | for folder, subfolder, fileList in os.walk(root): 21 | for file in fileList: 22 | if file.endswith('.pdf'): 23 | filepath = os.path.join(os.path.abspath(folder), file) 24 | pdfFileObj = open(filepath, 'rb') 25 | pdfReader = PyPDF2.PdfFileReader(pdfFileObj) 26 | 27 | if not pdfReader.isEncrypted: 28 | pdfWriter = PyPDF2.PdfFileWriter() 29 | for pageNum in range(pdfReader.numPages): 30 | pdfWriter.addPage(pdfReader.getPage(pageNum)) 31 | pdfWriter.encrypt(password) 32 | newPath = os.path.dirname(filepath) + '/untitled folder/' + \ 33 | ('_encrypted.'.join(os.path.basename(filepath).split('.'))) 34 | resultPdf = open(newPath, 'wb') 35 | pdfWriter.write(resultPdf) 36 | resultPdf.close() 37 | 38 | 39 | def decryptPDFs(root, password): 40 | """Decrypts all pdfs folder walk 41 | Args: 42 | root (str): folder path to walk 43 | password (str): password to decrypt pdfs with 44 | Returns: 45 | None 46 | """ 47 | for folder, subfolder, fileList in os.walk(root): 48 | for file in fileList: 49 | if file.endswith('_encrypted.pdf'): 50 | filepath = os.path.join(os.path.abspath(folder), file) 51 | pdfFileObj = open(filepath, 'rb') 52 | pdfReader = PyPDF2.PdfFileReader(pdfFileObj) 53 | 54 | if pdfReader.isEncrypted: 55 | success = pdfReader.decrypt(password) 56 | 57 | if success: 58 | pdfWriter = PyPDF2.PdfFileWriter() 59 | for pageNum in range(pdfReader.numPages): 60 | pdfWriter.addPage(pdfReader.getPage(pageNum)) 61 | newPath = os.path.dirname(filepath) + '/' + \ 62 | ''.join(os.path.basename(filepath).split('_encrypted')) 63 | resultPdf = open(newPath, 'wb') 64 | pdfWriter.write(resultPdf) 65 | resultPdf.close() 66 | else: 67 | print('wrong password provided') 68 | 69 | 70 | if __name__ == "__main__": 71 | 72 | password = input('Enter encryption password: ') 73 | encryptPDFs('.', password) 74 | decryptPDFs('.', password) -------------------------------------------------------------------------------- /pdf-password-breaker/passwordBreaker.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # passwordBreaker.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 13 Project 5 | 6 | import os 7 | 8 | 9 | import PyPDF2 10 | 11 | 12 | def breakPassword(filename): 13 | """Breaks a single word password of a PDF 14 | Args: 15 | filename (str): Filename for encrypted pdf 16 | Returns: 17 | None 18 | """ 19 | encryptedFile = open(filename, 'rb') 20 | pdfReader = PyPDF2.PdfFileReader(encryptedFile) 21 | 22 | with open('dictionary.txt') as words: 23 | wordList = words.read().split('\n') 24 | 25 | for word in wordList: 26 | wordLower = word.lower() 27 | wordCap = word.capitalize() 28 | 29 | if pdfReader.decrypt(word): 30 | return word 31 | elif pdfReader.decrypt(wordCap): 32 | return wordCap 33 | elif pdfReader.decrypt(wordLower): 34 | return wordLower 35 | 36 | return 37 | 38 | 39 | if __name__ == "__main__": 40 | 41 | pdfWriter = PyPDF2.PdfFileWriter() 42 | pdfWriter.encrypt('ASPIRE') 43 | encryptedPdf = open('pdf_encrypted.pdf', 'wb') 44 | pdfWriter.write(encryptedPdf) 45 | encryptedPdf.close() 46 | 47 | print(breakPassword('pdf_encrypted.pdf')) 48 | 49 | -------------------------------------------------------------------------------- /photo-folder-finder/photo_folder_finder.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # photo_folder_finder.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 17 Project 5 | 6 | 7 | import os 8 | 9 | 10 | from PIL import Image 11 | 12 | 13 | def find(root): 14 | """Walks through root directory searches for photo folders 15 | Args: 16 | root: directory on where to start search 17 | Returns: 18 | None 19 | """ 20 | for foldername, subfolders, filenames in os.walk(root): 21 | numPhotoFiles = 0 22 | numNonPhotoFiles = 0 23 | for filename in filenames: 24 | # Check if file extension isn't .png or .jpg. 25 | if not (filename.lower().endswith('.png') or filename.lower().endswith('.jpg')): 26 | numNonPhotoFiles += 1 27 | continue # skip to next filename 28 | 29 | # Open image file using Pillow. 30 | try: 31 | im = Image.open(os.path.join(foldername, filename)) 32 | except OSError: 33 | continue 34 | 35 | width, height = im.size 36 | 37 | # Check if width & height are larger than 500. 38 | if width > 500 and height > 500: 39 | # Image is large enough to be considered a photo. 40 | numPhotoFiles += 1 41 | else: 42 | # Image is too small to be a photo. 43 | numNonPhotoFiles += 1 44 | 45 | # If more than half of files were photos, 46 | # print the absolute path of the folder. 47 | if numPhotoFiles > numNonPhotoFiles: 48 | print(os.path.abspath(foldername)) 49 | 50 | 51 | if __name__ == "__main__": 52 | find('/') -------------------------------------------------------------------------------- /play-2048/2048.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # 2048.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 11 Project 5 | 6 | from selenium import webdriver 7 | from selenium.webdriver.common.keys import Keys 8 | import random 9 | 10 | def play(): 11 | """ 12 | Args: 13 | None 14 | Returns: 15 | None 16 | """ 17 | driver = webdriver.Firefox(executable_path='/Users/keneudeh/Downloads/geckodriver') 18 | driver.get('https://play2048.co/') 19 | 20 | key_select = [Keys.UP, Keys.DOWN, Keys.LEFT, Keys.RIGHT] 21 | 22 | gameStatusElem = driver.find_element_by_css_selector('.game-container p') 23 | htmlElem = driver.find_element_by_css_selector('html') 24 | 25 | 26 | while gameStatusElem.text != 'Game over!': 27 | 28 | htmlElem.send_keys(key_select[random.randint(0, 3)]) 29 | gameStatusElem = driver.find_element_by_css_selector('.game-container p') 30 | 31 | score = driver.find_element_by_css_selector('.score-container').text 32 | 33 | print(f'You scored: {score}') 34 | 35 | 36 | 37 | if __name__ == '__main__': 38 | play() -------------------------------------------------------------------------------- /prettified-stopwatch/stopwatch.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # stopwatch.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 15 Project 5 | 6 | import time 7 | 8 | 9 | import pyperclip 10 | 11 | 12 | def stopwatch(): 13 | """ records time spent per task, prints and stores in clipboard 14 | Args: 15 | None 16 | Returns: 17 | None 18 | """ 19 | # Display the program's instructions. 20 | print('Press ENTER to begin. Afterwards, press ENTER to "click" the stopwatch. Press Ctrl-C to quit.') 21 | input() # press Enter to begin 22 | print('Started.') 23 | clip = '' 24 | startTime = time.time() # get the first lap's start time 25 | lastTime = startTime 26 | lapNum = 1 27 | 28 | # Start tracking the lap times. 29 | try: 30 | while True: 31 | input() 32 | lapTime = round(time.time() - lastTime, 2) 33 | totalTime = round(time.time() - startTime, 2) 34 | info = 'Lap #%s: %s (%s)' % (str(lapNum).rjust(2), str(totalTime).center(7), str(lapTime).rjust(6)) 35 | clip += info + '\n' 36 | print(info, end='') 37 | lapNum += 1 38 | lastTime = time.time() # reset the last lap time 39 | except KeyboardInterrupt: 40 | # Handle the Ctrl-C exception to keep its error message from displaying. 41 | pyperclip.copy(clip) 42 | print('\nDone.') 43 | print('Results available in clipboard') 44 | 45 | 46 | if __name__ == "__main__": 47 | stopwatch() 48 | -------------------------------------------------------------------------------- /regex-search/input1.txt: -------------------------------------------------------------------------------- 1 | tsfd sfe t135 2 | lorem fd tehna 458322s 3 | sfee34 sres sfate -------------------------------------------------------------------------------- /regex-search/input2.txt: -------------------------------------------------------------------------------- 1 | re fad sre 2 | sdeit930nfiaow2 3 | res efe90f asr 4 | res re sete re -------------------------------------------------------------------------------- /regex-search/regex-search.py: -------------------------------------------------------------------------------- 1 | # regex-search.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 8 Project 4 | 5 | import os 6 | import re 7 | 8 | def regexSearch(regexStr, folderPath): 9 | """Searches all txt files in a folder for any line that matches a user-supplied regular expression 10 | Args: 11 | folderPath (str): path of folder to parse 12 | Returns: 13 | None 14 | """ 15 | if not os.path.isdir(folderPath): 16 | return 'Input a directory path' 17 | 18 | userRegex = re.compile(regex) 19 | 20 | for filename in os.listdir(folderPath): 21 | 22 | if filename.endswith('.txt'): 23 | 24 | with open(filename) as file: 25 | 26 | for line in file: 27 | mo = userRegex.search(line) 28 | 29 | if mo: 30 | print(line, end='') 31 | 32 | 33 | if __name__ == "__main__": 34 | 35 | regex = input('enter regex: ') 36 | regexSearch(regex, '.') -------------------------------------------------------------------------------- /regex-strip/regex-strip.py: -------------------------------------------------------------------------------- 1 | # regex-strip.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 7 Project 4 | 5 | import re 6 | 7 | def strip(text): 8 | """ python's str.strip() method implemented using regex 9 | Args: 10 | text (str): text to strip of white space 11 | Returns: 12 | textStripped (str): text stripped of white space 13 | """ 14 | stripStartRegex = re.compile(r'(^\s*)') 15 | stripEndRegex = re.compile(r'(\s*$)') 16 | 17 | textStartStripped = stripStartRegex.sub('', text) 18 | textStripped = stripEndRegex.sub('', textStartStripped) 19 | 20 | return textStripped 21 | 22 | if __name__ == "__main__": 23 | text = ' test ffs ' 24 | print(strip(text)) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.7.1 2 | certifi==2019.3.9 3 | chardet==3.0.4 4 | et-xmlfile==1.0.1 5 | idna==2.8 6 | IMAPClient==2.1.0 7 | jdcal==1.4.1 8 | lxml==4.3.3 9 | openpyxl==2.6.2 10 | Pillow==6.0.0 11 | PyAutoGUI==0.9.42 12 | PyGetWindow==0.0.5 13 | PyJWT==1.7.1 14 | PyMsgBox==1.0.6 15 | pyobjc-core==5.2 16 | pyobjc-framework-Cocoa==5.2 17 | pyobjc-framework-Quartz==5.2 18 | PyPDF2==1.26.0 19 | pyperclip==1.7.0 20 | PyRect==0.1.4 21 | PyScreeze==0.1.21 22 | PySocks==1.7.0 23 | python-docx==0.8.10 24 | PyTweening==1.0.3 25 | pytz==2019.1 26 | pyzmail36==1.0.4 27 | requests==2.22.0 28 | selenium==3.141.0 29 | Send2Trash==1.5.0 30 | six==1.12.0 31 | soupsieve==1.9.1 32 | twilio==6.27.0 33 | urllib3==1.25.2 34 | -------------------------------------------------------------------------------- /resize-add-logo/resizeAndAddLogo.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # resizeAndAddLogo.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 17 Project 5 | 6 | 7 | import os 8 | from PIL import Image 9 | 10 | SQUARE_FIT_SIZE = 300 11 | LOGO_FILENAME = 'catlogo.png' 12 | 13 | logoIm = Image.open(LOGO_FILENAME) 14 | logoWidth, logoHeight = logoIm.size 15 | 16 | os.makedirs('withLogo', exist_ok=True) 17 | # Loop over all files in the working directory. 18 | for filename in os.listdir('.'): 19 | if not (filename.lower().endswith('.png') or filename.lower().endswith('.jpg') 20 | or filename.lower().endswith('.gif') or filename.lower().endswith('bmp')) \ 21 | or filename == LOGO_FILENAME: 22 | continue # skip non-image files and the logo file itself 23 | 24 | im = Image.open(filename) 25 | width, height = im.size 26 | 27 | # Check if image needs to be resized. 28 | if width > SQUARE_FIT_SIZE and height > SQUARE_FIT_SIZE: 29 | # Calculate the new width and height to resize to. 30 | if width > height: 31 | height = int((SQUARE_FIT_SIZE / width) * height) 32 | width = SQUARE_FIT_SIZE 33 | else: 34 | width = int((SQUARE_FIT_SIZE / height) * width) 35 | height = SQUARE_FIT_SIZE 36 | 37 | # Resize the image. 38 | print('Resizing %s...' % (filename)) 39 | im = im.resize((width, height)) 40 | 41 | # Add the logo if it fits properly in image. 42 | imWidth, imHeight = im.size 43 | if imWidth < logoWidth * 2 and imHeight < logoHeight * 2: 44 | print("Logo taking up too much space in image so skipped..") 45 | else: 46 | print('Adding logo to %s...' % (filename)) 47 | im.paste(logoIm, (width - logoWidth, height - logoHeight), logoIm) 48 | 49 | # Save changes. 50 | im.save(os.path.join('withLogo', filename)) -------------------------------------------------------------------------------- /selective-copy/nested/j.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/selective-copy/nested/j.jpg -------------------------------------------------------------------------------- /selective-copy/nested/p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/selective-copy/nested/p.png -------------------------------------------------------------------------------- /selective-copy/result/p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/selective-copy/result/p.png -------------------------------------------------------------------------------- /selective-copy/selective-copy.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # selective-copy.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 9 Project 5 | 6 | import os, shutil 7 | 8 | def selectiveCopy(inputFolder, ext, outputFolder): 9 | """Walks through a folder tree and searches for files with a certain file extension, copies them into new folder 10 | Args: 11 | inputFolder (str): Path of folder to search in 12 | ext (str): file extension to search for 13 | outputFolder (str): Path of folder to copy files into 14 | Returns: 15 | None 16 | """ 17 | 18 | resultFolder = os.path.abspath(outputFolder) 19 | #print(resultFolder) 20 | 21 | for folderName, subFolder, filename in os.walk(inputFolder): 22 | 23 | for file in filename: 24 | 25 | if file.endswith(ext): 26 | 27 | filepath = os.path.join(os.path.abspath(folderName), file) 28 | 29 | if not os.path.exists(resultFolder): 30 | #create result folder if it doesn't exist 31 | os.makedirs(resultFolder) 32 | 33 | 34 | if os.path.dirname(filepath) != resultFolder: 35 | #prevent copying files from result folder 36 | shutil.copy(filepath, resultFolder) 37 | print(f'Copied {filepath} to {resultFolder}') 38 | 39 | 40 | if __name__ == '__main__': 41 | selectiveCopy('.', 'png', 'result') 42 | -------------------------------------------------------------------------------- /selective-copy/t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/selective-copy/t.jpg -------------------------------------------------------------------------------- /strong-password-detector/strong-password.py: -------------------------------------------------------------------------------- 1 | # strong-password.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 7 Project 4 | 5 | import re 6 | 7 | def testPasswordStrength(password): 8 | """ check for at least eight characters long, contains both uppercase and lowercase characters, and has at least one digit 9 | Args: 10 | password (str): password as string 11 | Returns: 12 | strong (bool): True if password is strong else 13 | """ 14 | 15 | eightCharsLongRegex = re.compile(r'[\w\d\s\W\D\S]{8,}') 16 | upperCaseRegex = re.compile(r'[A-Z]+') 17 | lowerCaseRegex = re.compile(r'[a-z]+') 18 | oneOrMoreDigitRegex = re.compile(r'\d+') 19 | 20 | if not eightCharsLongRegex.search(password): 21 | return False 22 | elif not upperCaseRegex.search(password): 23 | return False 24 | elif not lowerCaseRegex.search(password): 25 | return False 26 | elif not oneOrMoreDigitRegex.search(password): 27 | return False 28 | 29 | return True 30 | 31 | 32 | if __name__ == "__main__": 33 | password = 'A&dsas9$_' 34 | print(testPasswordStrength(password)) -------------------------------------------------------------------------------- /table-printer/table-printer.py: -------------------------------------------------------------------------------- 1 | # table-printer.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 6 Project 4 | 5 | def printTable(list2D): 6 | """Prints a table of items right justified 7 | Args: 8 | list2D (list): 2D list to print 9 | Returns: 10 | None 11 | """ 12 | 13 | # get the max length string of each row 14 | row_max_len = [] 15 | 16 | for row in range(len(list2D)): 17 | row_max_len.append(max([len(col) for col in list2D[row]])) 18 | 19 | # print table right Justified 20 | for col in range(len(list2D[0])): 21 | for row in range(len(list2D)): 22 | print(list2D[row][col].rjust(row_max_len[row]), end=' ') 23 | 24 | print() 25 | 26 | 27 | 28 | if __name__ == "__main__": 29 | 30 | tableData = [['apples', 'oranges', 'cherries', 'banana'], 31 | ['Alice', 'Bob', 'Carol', 'David'], 32 | ['dogs', 'cats', 'moose', 'goose']] 33 | 34 | printTable(tableData) -------------------------------------------------------------------------------- /text-to-spreadsheet/text1.txt: -------------------------------------------------------------------------------- 1 | text1 2 | tex1, 1 3 | tex1, 2 4 | text1, 3 5 | text1, 4 -------------------------------------------------------------------------------- /text-to-spreadsheet/text2.txt: -------------------------------------------------------------------------------- 1 | text2 2 | text2, 1 3 | text2, 2 4 | text2, 3 5 | text2, 4 -------------------------------------------------------------------------------- /text-to-spreadsheet/text3.txt: -------------------------------------------------------------------------------- 1 | text3 2 | text3, 1 3 | text3, 2 4 | text3, 3 5 | text3, 4 -------------------------------------------------------------------------------- /text-to-spreadsheet/textToSheet.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # textToSheet.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 12 Project 5 | 6 | import os 7 | 8 | 9 | import openpyxl 10 | 11 | 12 | def textToSheet(directory, filename): 13 | """converts text files to columns in excel worksheet 14 | Args: 15 | directory (str): folder containing text files 16 | filename (str): name of excel file 17 | Returns: 18 | None 19 | """ 20 | wb = openpyxl.Workbook() 21 | wb.create_sheet(index=0, title='result') 22 | sheet = wb.active 23 | 24 | colIndex = 1 25 | 26 | # write text files as columns in worksheet 27 | for file in os.listdir(): 28 | if file.endswith('.txt'): 29 | rowIndex = 1 30 | with open(file) as f: 31 | for line in f: 32 | sheet.cell(row=rowIndex, column=colIndex).value = line 33 | rowIndex += 1 34 | colIndex += 1 35 | 36 | wb.save(filename) 37 | 38 | if __name__ == "__main__": 39 | textToSheet('.', 'text-to-cols.xlsx') -------------------------------------------------------------------------------- /the-collatz-sequence/collatz.py: -------------------------------------------------------------------------------- 1 | # collatz.py 2 | # Author: Kene Udeh 3 | # Source: Automate the Boring stuff with python Ch. 4 Project 4 | 5 | def collatz(number): 6 | """If number is even (number // 2) else (3 * number + 1) 7 | Args: 8 | number (int): number to collatz 9 | 10 | Returns: 11 | int: collatz number 12 | """ 13 | if (number % 2) == 0: 14 | print(number // 2) 15 | return number // 2 16 | 17 | print(3 * number + 1) 18 | return 3 * number + 1 19 | 20 | 21 | if __name__ == '__main__': 22 | 23 | 24 | try: 25 | 26 | num = int(input('Enter a number: ')) 27 | result = collatz(num) 28 | while(result != 1): 29 | result = collatz(result) 30 | 31 | except ValueError: 32 | print('Please Enter a Valid Number') 33 | 34 | 35 | -------------------------------------------------------------------------------- /umbrella-reminder/umbrella_reminder.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # umbrella_reminder.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 16 Project 5 | 6 | import requests 7 | import bs4 8 | from twilio.rest import Client 9 | 10 | 11 | def rain_check(): 12 | """Checks if it's going to rain 13 | Args: 14 | None 15 | Returns: 16 | rain (bool): True if rainy 17 | """ 18 | url = 'https://weather.com/en-CA/weather/today/l/CAON4756:1:CA' 19 | rain = False 20 | 21 | try: 22 | res = requests.get(url) 23 | res.raise_for_status() 24 | 25 | soup = bs4.BeautifulSoup(res.text, 'html.parser') 26 | weather_elem = soup.select('.today_nowcard-phrase') 27 | 28 | if 'rain' in weather_elem[0].text.lower(): 29 | rain = True 30 | 31 | except Exception as exc: 32 | print(exc) 33 | 34 | return rain 35 | 36 | 37 | def send_reminder(accountSID, authToken, myTwilioNumber, myCellPhone): 38 | """Sends a text using twilio 39 | Args: 40 | accountSID (str): twilio acct sid 41 | authToken (str): twilio authentication token 42 | myTwilioNumber (str): twilio number 43 | myCellPhone (str): my cell phone number 44 | Returns: 45 | None 46 | """ 47 | twilioCli = Client(accountSID, authToken) 48 | message = twilioCli.messages.\ 49 | create(body='Rain Alert! Water is (not) wet. Grab an Umbrella bro.',\ 50 | from_=myTwilioNumber, to=myCellPhone) 51 | 52 | 53 | 54 | if __name__ == "__main__": 55 | 56 | if rain_check(): 57 | send_reminder('A***************', 'A**************', '+1********', '+1**********') -------------------------------------------------------------------------------- /web-comic-downloader/downloader.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # downloader.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 15 Project 5 | 6 | import os 7 | import shelve 8 | 9 | import requests 10 | import bs4 11 | 12 | 13 | def download(url): 14 | """ Checks if comic website(buttersafe) updated, and saves comic 15 | Args: 16 | url (str): url of comic site 17 | Returns: 18 | None 19 | """ 20 | # make requests to website 21 | try: 22 | headers = {'user-agent': 'Mozilla/5.0 (Macintosh; \ 23 | Intel Mac OS X 10_12_6) AppleWebKit/537.36 \ 24 | (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36'} 25 | res = requests.get(url, headers=headers) 26 | res.raise_for_status() 27 | except Exception as exc: 28 | print(exc) 29 | 30 | # use bs4 to check contents of date 31 | soup = bs4.BeautifulSoup(res.text, 'html.parser') 32 | dateElem = soup.select('#headernav-date') 33 | currDate = dateElem[0].text.strip() 34 | 35 | # shelf file 36 | shelfFile = shelve.open('prevDate') 37 | 38 | if not shelfFile.keys(): # if shelve is empty, first time function runs 39 | print('----first ever request----') 40 | shelfFile['prev'] = currDate 41 | 42 | else: 43 | print('staring daily check...') 44 | prevDate = shelfFile['prev'] 45 | 46 | # return from function if site hasn't been updated 47 | if prevDate == currDate: 48 | print('no updates available...') 49 | return 50 | 51 | # if update was made, get image url, make request & save 52 | imgUrl = soup.select('#comic > img')[0].get('src') 53 | os.makedirs('comics', exist_ok=True) 54 | try: 55 | print('making comic image request...') 56 | res2 = requests.get(imgUrl, headers=headers) 57 | print('saving comic image...') 58 | with open(os.path.join('comics', os.path.basename(imgUrl)), 'wb') as imgFile: 59 | for chunk in res2.iter_content(100000): 60 | imgFile.write(chunk) 61 | except Exception as exc: 62 | print(exc) 63 | 64 | # update and close shelve file 65 | shelfFile['prev'] = currDate 66 | shelfFile.close() 67 | 68 | 69 | if __name__ == "__main__": 70 | download('http://www.buttersafe.com') -------------------------------------------------------------------------------- /worksheet-to-text-files/sheetToTextFile.py: -------------------------------------------------------------------------------- 1 | #! python3 2 | # sheetToTextFile.py 3 | # Author: Kene Udeh 4 | # Source: Automate the Boring stuff with python Ch. 12 Project 5 | 6 | 7 | import os 8 | 9 | 10 | import openpyxl 11 | 12 | 13 | def toTextFiles(filename): 14 | """writes column data in worksheet into text files 15 | Args: 16 | filename (str): name of worksheet to read from 17 | Returns: 18 | None 19 | """ 20 | wb = openpyxl.load_workbook(filename) 21 | sheet = wb.active 22 | count = 1 23 | 24 | for colObj in sheet.columns: 25 | 26 | with open('text-'+str(count)+'.txt', 'w') as file: 27 | for cellObj in colObj: 28 | file.write(cellObj.value) 29 | 30 | count += 1 31 | 32 | 33 | if __name__ == "__main__": 34 | toTextFiles('worksheet.xlsx') -------------------------------------------------------------------------------- /worksheet-to-text-files/worksheet.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kudeh/automate-the-boring-stuff-projects/6889aa930cb198e52557d4cfe0dd4bbb0da92ef4/worksheet-to-text-files/worksheet.xlsx --------------------------------------------------------------------------------