Python Automation

One easy way to learn more about python is the book Automate the Boring Stuff with Python by Al Sweigart, which is also available in a lecture series on Udemy.

The first part of the course is helpful with learning some of the syntax of Python such as if/while/for, functions, try/except, lists, dictionaries, and strings. The second part of the course goes into files, debugging, and using 3rd party modules for web scraping, excel, word and PDF documents, email, and GUI automation.

Lecture Notes

Python Lecture Notes
1 - Python Basics
https://automatetheboringstuff.com/chapter1/
---
1 - Intro
Type all of the examples, such as print('msg')
Try to find answers on web, like stack overflow
    - Explain trying to do not did, error message location/type, copy/paste to website
    - OS, python ver, what already did to solve

2 - Basic terminology and using IDLE
Interactive shell - idle >>>
2 + 2           //expression, go to single value
//Can do +, -, *, /, ** (exponent), % (mod/remainder); Parentheses matter...
Error msg - shows SyntaxError
DataType - each value belongs to category: ints, floats, 'string'
    - String concat -> use 'a' + 'b'
    - String duplicate -> use 'a' * 2
var = 42        //store value, can overwrite
Evaluates to single val = expression, value + operator; doesn't evaluate = statement
Ctrl+enter gets new line on console

3 - writing 1_intro.py
# is a comment
print(' ') or print(' ', end=''), var = input()
functions - many built in, func(argument/value)
input() - waits for enter, is a string value
len(string) - int of chars in string
str()/int()/float() - convert to data type

==========================================================================================

2 - Flow Control
https://automatetheboringstuff.com/chapter2/
---
4 - Flow charts
Decision making - yes/no questions, go to end
    - Boolean vals (True/False), comparison operators (==,!=,<,>,<=,>=), boolean optr
        - str != int, int can == float; = assigns, == compares
    - and,or,not instead of &&,||,!

5 - If, Else, Elif
condition == expression, if condition true - go into if/else
indentation - tells block for if/flow, block == clause -> ends line with :
else - run if condition false
elif - enters first block with true statement, not any others
false -> string - blank string == false, int - 0 == false, bool(value) == 1 if not 0

6 - While
Keeps looping while true, goes to start if condition true
    - could be used to validate names
Infinite loops - condition never met, quit with ctrl+c
Break - stops loop, use in if, does not recheck condition
Continue - goes back to loop, rechecks condition

7 - For Loop
Iterates a specific number of times, range(#) goes from 0 to (#-1)
    - range(start, end)->goes start to end; range(start, end, step)->change increase val
    - ex range(5, -1, -1) - goes from 5 to 0 going down by -1
Can use break/continue
Can use a while loop instead of for loop

==========================================================================================

3 - Functions
https://automatetheboringstuff.com/chapter3/
---
8 - Built in functions
print, input, len
Standard library - group of functions (math, random, sys, os, etc.)
need to import with 'import lib, lib2', use module.function to use
    - or 'from lib import *' allows function to run without module.funciton, less readable
quit - use 'sys.exit()'
pip - install 3rd party module
    pip install pyperclip - copy to system clipboard, .copy() or .paste(); also need xclip

9 - Creating functions
Runs code, multiple times, only runs when called
    - avoid duplication -> less bugs
example: def function_name(arguments):
    - arguments = values passed to function, parameter = variable inside function
    - can specify return value with return 'value'
None type, lack of value; if no return value, function returns None
Keyword arguments - optional arguments, ex (arg, arg2='value')
    ex print('t1', 't2', end='\n', sep=' ') - is new line, seperate each t# with space

10 - Global/local
Variable - in function = local, in file = global; can be same name
    - destroyed when function (local) or program (global) ends
Global - can not use local, local destroyed after end
Local - can access global, can't access other local
    - if duplicate local and global, only if assigned in function uses local, else global
    - change global variable -> need to add global variable to top of function
    - seperate code from global variable - reduce area to check
    - functions as black boxes -> return values matter only

==========================================================================================

4 - try/except
---
11 - Try/Except statements
Can't run instruction (like divide by zero) -> crash program
try: ... except <error>:
Used for input validation - like ValueError
Error in try -> exception -> handle/error message, keep running program

==========================================================================================

5 - First program
---
12 - Guess a number program
 - see program, use if/else, for loop, rand, input

==========================================================================================

6 - Lists
https://automatetheboringstuff.com/chapter4/
---
13 - List data type
List - values/items, orders in list, like array[number], can be 2D or 3D
    - example: [['a', 'b'], [10, 20, 30]]
    - Negative integer -> go from end to front, -1 = last; still has a range
Index - goes to one value, like array[1]
    - Can replace values, just assign value in range that exists, array[1] = new_val
Slice - start and end index, array[1:3], goes from 1 to 2 (not including 3)
    - can do with slices -> can also add to array range
    - Can leave out number on either side of colon -> uses whole range
Delete - use del array[number], unassign
String-like functions
    Length - use len(array) for range
    Concat - can combine with '+'
    Replication - can multiply, like concat multiple times
list function - convert each value (like string chars) to a list
In/not in operator - check if value is in/not in list, is a expression
    - 'ex' in ['ex', '1'] -> is true as 'ex' is in the list, not reverses output

14 - For loops w/ lists, multiple assign, augmented operators
for i in [0, 1, 2, 3]: instead of range(4), i goes through list
    - ex list(range(number)) -> integers in a list
    - range(len(aList)) -> goes through list, could be string, access with aList[i]
Multiple assignment - assigns values from array, ex. var1, var2, var3 = list
    - can have multiple values for lists, ex. var1, var2, var3 = val1, val2, val3
    - Used for swap, ex. var1, var2 = var2, var1
Augmented Assignment Operator - ex. var += 1 to add, +=,-=,*=,/=,%=; is shortcuts

15 - List methods - index(), append(), insert(), remove(), sort()
Method - function, called on a value with list.func(), no duplication
    - index(val) - finds first index of value, returns index, error if not in list
    - append(val) - adds to end of list, returns None
    - insert(num, val) - inserts at index, returns None type
    - remove(val) - removes first value in array, error if not in list
        - use del array[num] to remove at a index
    - sort() - ints, str (alphabetize, use sort(reverse=True) for reverse alphabet)
        - can not do list with both int and strings...
        - uses ASCII order sort(key=str.lower) -> converts string to lower case

16 - List vs String
String - can use sort, slicing, indexing, not/in, for loop (for i in str:)
    - immutable, can't change letter using index, can't be modified only replaced w/ new
        - instead use slices with parts removed - ex str[0:3] + 'char' + str[4:7]
List
    - mutable, index can be changed, is a reference/pointer, easier to copy
        - Pass to function -> function changes saved
    - for full copy, use import copy; copy.deepcopy(val)
line continuation - lists can span multiple lines, only ends with ]
    - or use \ for a new line, for string concat, etc; indentation doesn't matter

==========================================================================================

7 - Dictionaries
https://automatetheboringstuff.com/chapter7/
---
17 - Dictionary Data Type
Like a list, index does not need to be integer, use key instead (key value pair)
    ex. dict = {'key':'value', ...}; use dict['key'] for value
    unordered, but can have values in different order be the same, uses references
    not exist - get KeyError msg
    Check is in dict, use 'key' in dict
Methods - return list like data types, need to pass with list(dict.fun())
    - dict.key() - shows all keys
    - dict.values() - shows all values
    - dict.items() - returns tuple of key value pairs
        - tuple = list-like, immutable, use parentheses
    ex. for k, v in dict.items() - tuple loop, k,v has values
    Check with 'val' in dict.values()
    - dict.get(key,fallback) - fallback is default value if key DNE
    - dict.setdefault('key', 'val') - sets val if not already in dict, if exists do nothing
pprint - pretty print dictionary, use pprint.pprint() for direct, .pformat() to string

18 - Data Structures
ex. data structure of lists of dictionaries, can be arbitrary -> easy to remember
Type function - shows data type, ex type(val)

==========================================================================================

8 - String Manipulation
https://automatetheboringstuff.com/chapter6/
---
19 - Advanced String Syntax
String - Common form of data, 'format' or "format", use \ to escape
    - \t tab, \n newline, \\ backslash, \', \"
    - Raw string, r'string', displays backslashes literally
    - Multiline string, start/end with ''' or """
    - Use index and slices for string
        - 'string' in string, is case sensitive

20 - String Methods
Return new string, can't modify in place, are immutable
    - str.lower()/string.upper() - convert all chars to lower or upper
        - Useful for comparisons, if string.lower() == 'true':
    - str.islower()/string.isupper() - checks if any 1+ char is lower/upper
        - false if blank screen, needs at least one char
        - isalpha() (letter only), isalnum() (letter/num only), isdecimal() (num only)
          isspace() (whitespace only), istitle() (Titlecase Only, Words Upper For 1st Char)
    - str.startswith('str')/.endswith('str') - t/f if string starts/ends with 'str'
    - str1.join(list of str), combines strings together with str1 being seperation mark
    - str.split('char'), seperate string to list, seperate with char, default/blank = space
    - str.ljust(num)/rjust(num,'char'), left/right justify text w/ space, 'char' optional
        -str.center(num) - centers text with number size
    - str.strip('char') - default: remove whitespace from string, or char for other chars
        - .lstrip()/.rstrip() - left/right strip
    - str.replace('old','new') - replace all old string with new
    - pyperclip.copy()/.paste() - use system clipboard

21 - String Formatting (string interpretation)
'str %s %s' % {str1, str2}
    - %s is conversion specifier

==========================================================================================

9 - Running programs
https://automatetheboringstuff.com/appendixb/
---
22 - Launch programs outside IDLE
1st line of all programs - shebang line #!/usr/bin/python3
    - requires python to use python3 to run
    - can run program without specifying program (on linux)
Windows - #! python3
    - run with py.exe C:\path\to\program.py, run dialog Win+R
        - pyw is python without command line window
    Batch file: (%* forwards command line args to program, is all args)
        @py c:\users\path\program.py %*
        @pause
    path - windows shortcut
        Control panel -> settings -> advanced system settings -> environment vars
            -> path variable -> add path to folder, seperate with :
Linux - use chmod +x program.py, then run with ./program.py
Command line arguments/options, strings in sys.argv

==========================================================================================

10 - Regular Expressions
https://automatetheboringstuff.com/chapter7/
---
23 - Regex
Pattern matching and regular expressions -> text patterns, ex 123-456-7890
use re module, use re.compile(r'str', flags=) -> use raw string, flags optional
    - match = regex.search('str') to match option, finds first match, result with match.group()
    - use regex.findall('str') to return list of all matches
\d = digit

24 - Regex Groups and Pipes
Use (...) for groups, place regex inside (), use regex.group(num), num is which group in regex
    - for () in text, escape with \( and \)
    - group(0) returns full match string, group(1) returns match string of group 1, ...
| = pipe, match several patterns in regex, ex r'str(a|b|c)'
No match -> return NoneType

25 - Repetition in Regex, Greedy/Nongreedy Matching
For specific number of repetitions, could be a range
    - ? match preceding group 0 or 1 times, optional, appear once or not at all
        - ex r'Bat(wo)?man' either appears or not
        - escape with \?
    - */astrisk, match 0 or more times, escape \*
    - +/plus, match 1 or more, escape \+
    - {x}/exactly x, specific number of times for repetition
    - {x,y}/range x to y, including, can leave off number, goes unbounded like slices
        - default greedy match, longest possible string for pattern
        - non-greedy, use {x,y}? for smallest possible string

26 - Regex Char Classes, findall() method
.findall() - find all text
    - if 0 or 1 groups, return list of strings -> text matching pattern
    - if 2+, returns list tuples strings of matches, some combinations of groups if overlap
Char class - uppercase is inverse
    - \d = digit 0-9, \w = letter, digit, underscore, \s = space, tab, newline
    - \D = char not digit, \W = not letter/digit/_, \S = char not space, tab, newline
Create a char class [] + chars in class, can use range like [a-z] or [a-zA-Z]
    - For chars, need both upper and lower case, exact is [a-z]{2} -> 2 in a row
    - ^ is negative, is an opposite, [^a-z], for no spaces [^a-z\s]

27 - Regex .* and ^$
Requires ^ if starting with, $ if ending with, ^both$ = entire text
. = any char but newline
.* = any character, 0 or more -> any pattern
    - ex can do r'text: (.*) text2: (.*)' for tuples of data
    - .* is greedy as much text as possible, .*? is non-greedy, 1st match
    - use with newline, in regex.compile(r'...', re.DOTALL) add re.DOTALL
        - case insensitive -> use re.IGNORECASE (same as re.I)
Options - 2+, use re.IGNORECASE | re.DOTALL -> bitwise or operator

28 - Regex sub() and Verbose
Substitution - use regex.sub('to replace', 'string')
    - \num -> replace in group, group (\w) -> sub with r'text \1' -> get group 1
Verbose\re.VERBOSE - add newlines to string, need to use ''' text ''', can add <space*2>#comment

29 - Example Program
Use regex to get phone number/email -> output to clipboard

==========================================================================================

11 - Files
https://automatetheboringstuff.com/chapter8/
https://automatetheboringstuff.com/chapter9/
---
30 - Filenames, absolute/relative
File - single long string, file path + file + file extension -> location on file system
    - Root folder: C:\ (Windows, need to use \\ to escape), / (Linux/Mac)
import os -> os.path.join('path','to','file.ext') converts to os file path, os.sep=seperator
    - os.getcwd() - get current work dir, if opening file w/o path - assume cur work dir
    - os.chdir() - change directory
    Absolute - always begin with root folder, relative - to current working dir
        - . = this folder, .. = parent folder
    - os.path.abspath('file.ext') - relative to absolute folder path, given file ref
    - os.path.isabs('path') - true if is absolute path
    - os.path.relpath('path1', 'path2') - gives relative path of current path (path2)
    - os.path.dirname() - directory path of file
    - os.path.basename() - file.ext or last file part of path
    - os.path.exists(path) - true/false if path exists
        - os.path.isfile() / os.path.isdir() - check if file/dir exists
    - os.path.getsize(path) - gets file size in bytes (int)
    - os.listdir(folder) - list of folders/files inside a folder
    - os.makedirs(path) - create folders if not exist, can do folders in folders

31 - Read/Write plaintext
Plaintext - no font/size/color, .txt files, open w/ text editor; binary - image/doc/misc
file = open('filename') - open in read mode, default mode, can't write
    - open('filename', 'w') - write mode, use 'a' for append, if not exist -> new file
        - returns bytes written, need to state '\n' for newlines
    - file.read() - read file string
        - file.readlines() - return list of all lines of a file
    - file.close() - close file
    - file.seek(bytes) - go to file location, use 0 for start of file
Save variables to binary shelve module -> import shelve
    - like dictionary with key/value, save non-text data
    - use file = shelve.open('file') to create, file['var'] =['var1','var2'] to save
        - access with file['var'] after open, close with file.close()
        - file.keys() - get keys, file.values() - get values, use list(file...) to list

32 - Copy/Move Files/Folders
Organize files - copy, rename, etc. -> import shutil (shell utils)
    - shutil.copy(src file, dest) copies, rename -> shutil.copy(file, dest/rename)
    - shutil.copytree(src folder, dest folder) - copy folders
    - shutil.move(src file, dest) moves, also renames like copy could have same dest

33 - Deleting Files
Permanent delete files - be careful
    - os.unlink('file') - deletes a single file
    - os.rmdir(directory) - delete a directory, no files/folders inside it
    - shutil.rmtree(directory) - delete directory + contents
dry run - comment code for delete code, instead print filename
module send2trash -> use pip
    - send2trash.send2trash(folder or file) - moves to trash

34 - Walking a Directory Tree
    - os.walk(absolute path), return string foldername, list subfolders, and list filenames
        - walks though path, go in iteration

Zip files - use import zip
file = zipfile.ZipFile('fileToCreate.zip') - opens a zipfile
    - file = zipfile.ZipFile('new.zip', 'w') - creates
    - file.write(file, compress_type=zipfile.ZIP_DEFLATED) - adds to file
    - file.namelist() - lists file in zip
    - file.getinfo('file') - gets information on file (file_size, compress_size)
    - file.extractall() - extracts files, file.extract('file') extracts single file
    - file.close() - close zip

==========================================================================================

12 - Debugging
https://automatetheboringstuff.com/chapter10/
---
35 - Raise/assert
raise Exception('error str') - custom message, if no try/catch -> crash
traceback - import traceback, use traceback.format_exc()
Assert - sanity check, assert error raise if not true, for logic errors
    - assert conditional statement, 'error msg'

36 - Logging
import logging, logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
    - basicConfig(...,datefmt='%Y-%m-%d %H:%M:%S)
    - logging.debug('msg %s' % (str)), at lowest level
        - log levels (low) debug, info, warning, error, critical (high)
        - turn off msg w/ logging.disable(logging.CRITICAL) #place after .basicConfig
    - text file - logging.basicConfig(filename='file.txt', level=...)
Print - harder to remove after done debugging

37 - Debugger
Use IDLE, debug -> debugger, select stack,local, source, globals
    - or use python3 -m pdb program.py, n = next, s = step, w = stack trace (where), p var
Breakpoints - IDLE - right click line -> set breakpoint

==========================================================================================

13 - Web Scraping
https://automatetheboringstuff.com/chapter11/
---
38 - Webbrowser Module
import webbrowser, can .open('url') in default browser

39 - Downloading with Requests Module
requests - avoid network errors, compression, etc, use pip install
    - response = requests.get('url of file')
        - returns response.status_code, 200 = OK, 404 = file not found
        - response.text - gets text outout
        - response.raise_for_status() - is nothing if status succeeded, exception if not
    - save with open(file, 'wb') -> write binary mode, for unicode
    - response.iter_content(num) - iterates over content in num size chunks
https://nedbatchelder.com/text/unipain.html
    - str use b'' for bytes, default is unicode

40 - Parse HTML with Beautiful Soup Module (web scrape)
Web browser -> view source with ctrl+u; dev tools -> hover over elements, or highlight + inspect
pip install beautifulsoup4, import bs4
    - requests.get(url, headers=header_file) -> if needing user agent
    - output = bs4.BeautifulSoup(response.text, 'html.parser;') parses
    - elems = soup.select('css path')
        - access with elems[0].text, use strip to remove \n and space
        #css selector to find, highlight + right click + Inspect -> right click -> copy -> copy selector (css path)

41 - Control Browser with Selenium Module
pip install selenium -> use javascript, fill out forms, click buttons; launches web browser
**Need to download drivers if using, firefox not included anymore**
    - from selenium import webdriver
    - browser = webdriver.Firefox() - launch firefox GUI
        - browser.get(URL)
        - browser.back(), .forward(), .refresh(), .quit()
    - elem = browser.find_element_by_css_selector(CSS) gets one element
        - elem.click() clicks on elt, elem.send_keys('text') sends text
        - elem.submit() auto submits, elem.text = text for section
    - elems = browser.find_elements_by_css_selector('p') gets all paragraph elts
        - or by class_name, id, link_text/partial_link_text, name, tag_name
        - all text -> use 'html' element gets all text

==========================================================================================

14 - Excel, Word, PDF Documents
https://automatetheboringstuff.com/chapter12/
https://automatetheboringstuff.com/chapter13/
---
42 - Reading Excel Spreadsheets
pip install openpyxl, workbook -> sheet -> col/row -> cell
    - wkbk = openpyxl.load_workbook('file.xlsx') - opens file
        - wkbk.sheetnames - returns list of sheet names
            - wkbk.get_sheet_names() - depreciated
    - sheet = wkbk['Sheet1'] - open sheet from workbook
        - wkbk.get_sheet_by_name('Sheet1') - depreciated
    - sheet['A1'].value - get cell A1 value from object, uses Excel's data type
        - sheet.cell(row=1,column=1) - get with row/column, start at 1

43 - Editing Excel Spreadsheets
    - wb = openpyxl.Workbook() - creates workbook
        - wb.save('filename.xlsx') - saves file
    - sheet['A1'] = <val> - assigns value to cell
    - sheet2 = wb.create_sheet() - creates a new sheet
        - wb.create_sheet(index=0, title='title'), creates w/ title and location
        - sheet2.title = 'new title' - changes title

44 - Reading/Editing PDFs
import PyPDF2, PDFs are binary file-> font, color, pictures, text
    - sometimes can not open/work with some PDF files, can't extract images/media
    - pdf = open('file.pdf', 'rb') - use binary read mode
    - reader = PyPDF2.PdfFileReader(pdf) - opens file
        - reader.numPages - number of pages, start at 0
    - page = reader.getPage(0) - gets page
        - page.extractText() - gets text as a string, can have errors
    - writer = PyPDF2.PdfFileWriter()
        - writer.addPage(reader.getPage(num)) - adds page to different pdf, for reordering
    - output = open('file','wb'); writer.write(output) - saves file
**Don't forget to close after done**

45 - Reading/Editing Word Docs
pip python-docx; import docx; lots of structure doc object -> paragraph -> run objects
    - doc = docx.Document('file') - opens
        - doc.paragraphs - list of paragraph objects
        - par = doc.paragraphs[num].text - text of each paragraph
        - par.runs - list of run objects, change in style, par.run[num].text = text insidie
            - p.runs[num].{bold,italic,underline} - show if true or false (None), can change
        - par.styles = 'Title' -> changes to title style
    - doc.save('file2') - saves file
    - doc2 = docx.Document() - new file
        - doc2.add_paragraph('str') - adds to paragraph
        - par.add_run - adds run to paragraph
Can not move text, should copy to new document only needed

~~~
Chapter 14 - CSV/JSON
https://automatetheboringstuff.com/chapter14/
---
CSV - import csv
file = open('file.csv'); reader = csv.reader(file); data = list(reader) -> is a list
    - data[row][col] - access data
outfile = open('file.csv', 'w', newline=''); outwrite = csv.writer(outfile)
    - outwrite.writerow(list) - writes a list
    - csv.writer(outfile, delimiter='\t',lineterminator='\n') - changes format

JSON {"str": "var", "str2": "var2", ...} - import json
jsonData = json.loads(str) - loads str into dict
jsonStr = json.dumps(json) - outputs json to string

~~~
Chapter 15 - Time, Tasks, Launching
https://automatetheboringstuff.com/chapter15/
---
Time - import time, from UTC
time.time() - current system time, can save start & end, use end - start for diff
    - round(time.time()) - in seconds, round(time, digits)
import datetime - date format
dt = datetime.datetime.now(); dt.year/month/day/hour/minute/second
datetime.datetime.fromtimestamp(time_t) - gets value from timestamp
delta = datetime.timedelta(days=#, hours=#, minutes=#, seconds=#) - total rep in time
    - get delta.days/seconds/microseconds/total_seconds
datetime.datetime.strptime('time str','format of %') - parse time

Multithread - import threading
threadObj = threading.Thread(target=func); threadObj.start() - starts function
    - threading.Thread(target=func, args=['arg1',...],kwargs={...}) - ex. for print
**Concurrency issues...**

Launch - import subprocess
proc = subprocess.Popen('path/to/prgm')
    - has proc.poll() - if finished, proc.wait() - wait until done
    - subprocess.Popen(['path/prgm', 'args'])
    - subprocess.Popen(['program', 'args']) - open file with program
time.sleep(num) - sleep shell

~~~
Chapter 17 - Images
https://automatetheboringstuff.com/chapter17/
---
Use pillow (requires python3-tk, python3-dev)
from PIL import ImageColor
    - ImageColor.getcolor('color', 'RGBA') - gets format
from PIL import Image
    - img = Image.open('img.png'), img.save('filename.jpg') - open/save diff format
        - width, height = img.size  - stores in vars
        - has .filename, .format, .format_description
    - img = Image.new('RGBA', (w,h), 'color') - creates image, if no color=transparent
    - img.crop((left, upper, right, lower)) - crop coord
    - img.paste(imgHandler, (w,h)) - paste onto image
    - img.resize((width,height)) - resize img, can also stretch if ratio wrong
    - img.rotate(deg) - rotates degree, rotate(deg, expand=True) - increase canvas
    - img.putpixel((w,h), (rgb color))
from PIL open ImageDraw
    draw = ImageDraw.Draw(img) - draw on iamge
    - has point, lines, rectangels, ellipses, polygons, text
==========================================================================================

15 - Email
https://automatetheboringstuff.com/chapter16/
---
46 - Sending Email
Use SMTP - Simple Mail Transfer Protocol, import smtplib
conn = smtplib.SMTP('smtp.gmail.com', 587) - example smtp + port
    - conn.ehlo() - start connection, tuple(code, bytes)
    - conn.starttls(), conn.login(email,password) - encrypt & login to account
    - conn.sendmail('from', 'to', 'Subject: ...\n\ntext\n\ntext') - \n\n seperates
    - conn.quit() - exit smtp server

47 - Checking Inbox
IMAP - Internet Message Access Protocol - how to retreive emails
pip install imapclient, pyzmail (not imaplib)
conn = imapclient.IMAPClient('imap.gmail.com', ssl=True), returns success
    - conn.select_folder('INBOX', readonly=True) - no deletion
        - conn.list_folders() - list folders, tuple
        - conn.delete_mssages([num, num1, ...]) - delete emails
    -UIDs = conn.search(['SINCE $Date']) - email ids since date
        - or all, before, on, since, subject, body, text, seen, unseen, answered, etc
    - rawMsg = conn.fetch([num], ['BODY[]', 'FLAGS'])
    - msg = pyzmail.PyzMessage.factory(rawMsg[num][b'BODY[]']) - msg object
        - msg.get_subject(),.get_addresses('from') (or to/bcc)
        - msg.text_part/.html_part - if None, then did not use
    - msg.text_part.get_payload().decode('UTF-8')

==========================================================================================

16 - GUI Automation
https://automatetheboringstuff.com/chapter18/
---
48 - Mouse
pip pyautogui - last resort to control program
    - 0,0 = top left corner, x/y increase going across
    - pyautogui.size() - screen size; puautogui.position() - current position
    - pyautogui.moveTo(x, y) - goes to location
        - pyautogui.moveTo(x, y, duration=#) - duration in seconds
    - pyautogui.moveRel(xOffset, yOffset) - move from current loc (+/-), also has duration
    - pyautogui.click(x, y), or .doubleClick(x,y), .rightClick(x,y), .middleClick(x,y)
        - if no x/y click current loc; can also take tuple value (x,y)
    - pyautogui.dragRel(x, y) - go to x,y hold left click
    - if at 0,0 -> exception, pyautogui has .1 sec pause after function to check
    - pyautogui.displayMousePosition() - show x, y coord, and RGB value of mouse pixel
        - requires use in terminal, not IDLE

49 - Keyboard
    - pyautogui.typewrite('text') - first click to focus window
        - pyautogui.typewrite('text', interval=0.1) - interval to slow typing
        - pyautogui.typewrite(['a', 'left', 'X']) - print character or command (cursor left)
            - pyautogui.KEYBOARD_KEYS - list all key codes that can be used
            - pyautogui.press('key') - press a single key
        - pyautogui.hotkey('ctrl','o') - press keyboard shortcut

50 - Screenshots and Image Recognition
Use pyautogui/pillow (for images)
    - pyautogui.screenshot('filename') - take screenshot, save image to filesystem
    - pyautogui.locateOnScreen('filename.png') - locate png, x,y coord of where to find
        - return x, y, width, height of image
    - pyautogui.locateCenterOnScreen('filename.png') - locate center of image
Note: can take up to 1 second, image needs to be exact same to locate

51 - End

==========================================================================================
 

Example Programs

Python Programs
#01_intro.py
#!/usr/bin/python3
# Says hello, asks for name
print('Hello')

myStr = input('Enter text: ')
print('text is: ' + myStr)
print('length:', end='')
print(len(myStr))

print('Type in a number: ', end='')
inputNum = input()
print('Num++ is ' + str(int(inputNum)+1) + '!')

#==========================================================================================
#02_for_while.py
#flow
#!/usr/bin/python3

name = 'Abc'
if name == 'Abc':
    print('sup')
else:
    print(':(')
print('done')

quit()

#str
#!/usr/bin/python3
print('Enter text')
name = input()
if name:
    print('entered text')
else:
    print(':(')
print('done')

quit()

#while
#!/usr/bin/python3
spam = 0
while spam < 5:
    print('text')
    spam = spam + 1

quit()

#whiletext
#!/usr/bin/python3
text = ''
while text != 'text':
    print('type text to quit')
    text = input()
    #or can do:
    #if text == 'text'
    #   break
print('done')
quit()

#whilecontinue
#!/usr/bin/python3
spam = 0
while spam < 5:
    spam = spam + 1
    if spam == 3:
        continue    # goes back to start, does not print
    print('text')

quit()

#for
#!/usr/bin/python3
print('text')
for i in range(5):
    print('loop '+ str(i) )
quit()

#forsum
#!/usr/bin/python3
total = 0
for i in range(100 + 1):
    total = total + i
print(total)
quit()

#==========================================================================================
#03_rand_func_var_exit_cp.py
#rand
#!/usr/bin/python3
#!/usr/bin/python3
import random
print(str(random.randint(1,10)))
quit()

#exit
#!/usr/bin/python3
import sys
print('here')
sys.exit()
print('never here')

#copypaste
#!/usr/bin/python3
#need to install xclip and python3-pyqt5...
import pyperclip
pyperclip.copy('test')
print('clipboard is:' + pyperclip.paste())
quit()

#func
#!/usr/bin/python3

def printtest(str):
    print('func called with string: ' + str)

printtest('abc')
printtest('')       #cannot be blank
printtest('test')

quit()

#one
#!/usr/bin/python3

def plusone(num):
    return num + 1

newNumber = plusone(5)
print(newNumber)

quit()

#global local
#!/usr/bin/python3

def spam():
    global var  #needed or else variable doesn't change
    print('in function')
    var = 'test'

var = 'abc'
print(var)
spam()
print(var)

quit()

#==========================================================================================
#04_try_except.py
#try
#!/usr/bin/python3

def div42by(divideBy):
    try:
        return 42 / divideBy
    except ZeroDivisionError:
        print('Err: no div 0')

print(div42by(2))
print(div42by(12))
print(div42by(0))
print(div42by(1))

quit()

#except
#!/usr/bin/python3

print('Enter a number')
num = input()
try:
    if int(num) >= 4:
        print('is > 4')
    elif int(num) > 0:
        print('not a lot')
    else:
        print('is neg')
except ValueError:
    print('not a number')

quit()

#==========================================================================================
#05_rand_num.py
#!/usr/bin/python3
#This is a guess the number game
import random   #for randint function

print('Enter a name:')
name = input()
print(name +', guesss a number between 1 and 20 (including)')
secretNumber = random.randint(1,20)

#print('Debug: num is ' + str(secretNumber))

for guessesTaken in range(1,7): #6 iterations
    print('Take a guess')
    guess = int(input()) #convert to a integer

    if guess < secretNumber:
        print('too low')
    elif guess > secretNumber:
        print('too high')
    else: #guess correct
        break

if guess == secretNumber:
    print('found number in ' + str(guessesTaken) + ' guesses')
else:
    print('The number was ' + str(secretNumber))

quit()

#==========================================================================================
#06_list.py
#for list
#!/usr/bin/python3

array = ['a', 'b', 'c', 'd']

for i in range(len(array)):
    print('Index ' + str(i) + ' is ' + array[i])

quit()

#list mutable
#!/usr/bin/python3

def eggs(cheese):
    cheese.append('hello')

spam = [1, 2, 3]
print(spam)
eggs(spam)
print(spam)

quit()

#==========================================================================================
#07_char_count.py
#!/usr/bin/python3
import pprint #pretty printing

def count(msg):
    count = {} #count number of characters exist

    for character in msg.upper():
        count.setdefault(character,0) #set character to 0, if not exist
        count[character] = count[character] + 1

    #print(count)
    #pprint.pprint(count) # in order, single column
    rjtext = pprint.pformat(count) # in order, single column
    print(rjtext)

#msg = 'It was a bright cold day in April, and the clocks were stricking thirteen.'

#multiline quote
# from https://automatetheboringstuff.com/files/rj.txt
msg = '''
The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare

This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever.  You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.org/license


Title: Romeo and Juliet

Author: William Shakespeare

Posting Date: May 25, 2012 [EBook #1112]
Release Date: November, 1997  [Etext #1112]

Language: English
'''

count(msg)

quit()

#==========================================================================================
#08_str.py
#!/usr/bin/python3

str = 'this\nis a test for using \' chars'
str2 = r'literal\n'
print(str)
print(str2)

print(str.lower())
print(str.upper())
str4 = str.replace('\n',' ')
print(str4)

quit()

#==========================================================================================
#09_launch.py
#!/usr/bin/python3
import sys

print('Hello world')

print('args: ', end='')
print(sys.argv)     #command arguments

quit()

#==========================================================================================
#10_regex_pipe.py
#phone
#!/usr/bin/python3

#takes in number, format 123-456-7890
def isPhoneNum(text):
    if len(text) != 12:
        return False #not phone-num size
    for i in range(0,3):
        if not text[i].isdecimal():
            return False #no area code
    if text[3] != '-':
        return False #missing dash
    for i in range(4,7):
        if not text[i].isdecimal():
            return False #no 3 digits
    if text[7] != '-':
        return False #missing dash
    for i in range(8,12):
        if not text[i].isdecimal():
            return False #no 4 digits
    return True

print(isPhoneNum('415-555-1234'))
print(isPhoneNum('15-555-1234'))

#more code if number is in text
def testIPN(message):
    foundNum = False
    for i in range(len(message)):
        chunk = message[i:i+12]     #slice, not worry if going past bounds
        if isPhoneNum(chunk):
            print('Phone number ' + chunk + ' in ' + str(i) + ' loops')
            foundNum = True
    if not foundNum:
        print('Could not find any phone number')

message = 'call me at 415-555-1011 tomorrow, or at 415-555-9999'
print(message)
testIPN(message)
message = 'call me at 415-55-1011 tomorrow, or at 415-555-999'
print(message)
testIPN(message)

quit()

#phone regex
#!/usr/bin/python3
import re

message = 'call me at 415-555-1011 tomorrow, or at 415-555-9999'
# \d = digit
phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
mo = phoneNumRegex.search(message) #return match option, first occurance
print(mo.group()) #finds first match
mo = phoneNumRegex.findall(message) #return list of matches
print(mo) #finds first match

quit()

#phone group
#!/usr/bin/python3
import re

message = 'my number is 415-555-4242'
phoneNumRegex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)') # \d = digit. () = group
mo = phoneNumRegex.search(message) #return match option, first occurance
print(mo.group() + ', 1: ' + mo.group(1) + ' 2: ' + mo.group(2))

message = 'my number is (415)-555-4242'
phoneNumRegex = re.compile(r'\(\d\d\d\)-\d\d\d-\d\d\d\d') # \d = digit. () = group
mo = phoneNumRegex.search(message) #return match option, first occurance
print(mo.group())

quit()

#pipe
#!/usr/bin/python3

import re

batRegex = re.compile(r'Bat(man|mobile|copter|sub)') #group, seperate with pipe
mo = batRegex.search('Batmobile lost a wheel')
print(mo.group() + ' found in group ' + mo.group(1))
mo = batRegex.search('Batmotorcycle lost a wheel')
print(mo.group()) #return none, crash

quit()

#repeat
#!/usr/bin/python3

import re

#question?
batRegex = re.compile(r'Bat(wo)?man') #group appear 0 or 1 times
print(batRegex.search('The Adventures of Batman'))
print(batRegex.search('The Adventures of Batwoman'))

#phone numbs
phoneRegex = re.compile(r'(\d\d\d-)?\d\d\d-\d\d\d\d') #does not require area code
print(phoneRegex.search('my num is 555-1234'))
print(phoneRegex.search('my num is 123-555-1234'))

#asterisk*
batRegex = re.compile(r'Bat(wo)*man') #group appear 0+ times
print(batRegex.search('The Adventures of Batman'))
print(batRegex.search('The Adventures of Batwowoman'))

#add+
batRegex = re.compile(r'Bat(wo)+man') #group appear 1+ times
print(batRegex.search('The Adventures of Batwoman'))

#escape\
regex = re.compile(r'\+\*\?')
print(regex.search('learn about +*? regex'))
regex = re.compile(r'(\+\*\?)+') #appear 1+ times (in order)
print(regex.search('learn about +*?+*?+*? regex'))

#exact match times
haRegex = re.compile(r'(Ha){3}')
print(haRegex.search('he "HaHaHa"'))
haRegex = re.compile(r'(Ha){3,5}')#including, match first 5 only
print(haRegex.search('he "HaHaHaHaHa"'))
#match phone number, optional area code, 3 in a row, optinal ending ,
phRegex = re.compile(r'((\d\d\d-)?\d\d\d-\d\d\d\d(,)?){3}')
print(phRegex.search('nums are 415-555-1234,555-4242,212-555-0000'))

#testing
digitRegex = re.compile(r'(\d){3,5}') #match digits, greedy
print(digitRegex.search('1234567890'))
digitRegex = re.compile(r'(\d){3,5}?') #match digits, non-greedy
print(digitRegex.search('1234567890'))

quit()

#findall
#!/usr/bin/python3
import re
msg = '123-456-7890, 342-234-2315, 556-234-7152, and 234-6151'
phoneRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
print(phoneRegex.search(msg)) #find first
print(phoneRegex.findall(msg)) #list of values

phoneRegex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)') #2 groups
print(phoneRegex.findall(msg)) #list of values

lyrics = '12 drum, 11 piper, 10 lords, 9 dance, 8 milk, 7 swan, 6 geese, ' + \
        '5 rings, 4 birds, 3 hens, 2 doves, 1 tree'
# 1 or more digits, space (or use ' '), 1+ word char
xmas = re.compile(r'\d+\s\w+')
print(xmas.findall(lyrics))

vowelRegex = re.compile(r'[aeiouAEIOU]') #4'(a|e|i|o|u)'
print(vowelRegex.findall('Robocop eats baby food.'))
vowelRegex = re.compile(r'[aeiouAEIOU]{2}') #read 2 vowels
print(vowelRegex.findall('Robocop eats baby food.'))
vowelRegex = re.compile(r'[^aeiouAEIOU]') #opposite
print(vowelRegex.findall('Robocop eats baby food.'))
vowelRegex = re.compile(r'[^aeiouAEIOU\s]') #opposite
print(vowelRegex.findall('Robocop eats baby food.'))

quit()

#dot star caret dollar
#!/usr/bin/python3
import re
beginHello = re.compile(r'^Hello') #begins with hello
print(beginHello.search('Hello there!'))
endWorld = re.compile(r'world!$') #$ends with world
print(endWorld.search('Hello world!'))

allDigit = re.compile(r'^\d+$') #one or more digits, no chars
print(allDigit.search('31241324512431'))

atRegex = re.compile(r'.at')
print(atRegex.findall('The cat in the hat sat on the flat mat'))
atRegex = re.compile(r'.{1,2}at') #at preciding 1 or 2 chars, any
print(atRegex.findall('The cat in the hat sat on the flat mat'))

str = 'First Name: Test Last Name: User'
nameRegex = re.compile(r'First Name: (.*) Last Name: (.*)') #use groups
print(nameRegex.findall(str))

str2 = '<To serve humans> for dinner.>'
nongreedy = re.compile(r'<(.*?)>') #non greedy, 1st match
print(nongreedy.findall(str2))
greedy = re.compile(r'<(.*)>') #non greedy, 1st match
print(greedy.findall(str2))

str3 = 'line one\nline two\nline three'
dotstar = re.compile(r'.*')
print(dotstar.search(str3)) #match until newline
dotstar = re.compile(r'.*', re.DOTALL)
print(dotstar.search(str3)) #match until newline

vowelRegex = re.compile(r'[aeiou]', re.IGNORECASE) #4'(a|e|i|o|u)'
print(vowelRegex.findall('A robocop eats baby food.'))

quit()

#sub verbose
#!/usr/bin/python3
import re
str='Agent Alice gave docs to Agent Bob'
name = re.compile(r'Agent \w+')
print(name.findall(str))
print(name.sub('REDACTED',str))
name = re.compile(r'Agent (\w)\w*') #groups, get first letter of group, 0+ more other chars
print(name.findall(str))
print(name.sub(r'Agent \1****',str)) #replace word in group

verb = re.compile(r'''
(\d\d\d-)|    #area code
(\(\d\d\d\))  #area code with paren, no dash
\d\d\d    #digits
-
\d\d\d\d
\sx\d{2,4}  #extension, like x1234
''', re.VERBOSE)

quit()

#scraper
#!/usr/bin/python3
import re, pyperclip

# regex for phone num
phoneRegex = re.compile(r'''
# types: 415-123-1234, 123-1234, (415) 123-1234, 123-1234 ext 12345, ext. 12345, x12345
(                           #one group, no touples if using findall
((\d\d\d)|(\(\d\d\d\)))?    # area code (optional)
(\s|-)    # first separator
\d\d\d    # 3 digits
-       # separator
\d\d\d\d    # 4 digits
(((ext(\.)?\s)|x)
  (\d{2,5}))?    # extension (optional)
)
''', re.VERBOSE)

# regex for email
emailRegex = re.compile(r'''
#some.+_thing@something.com
[a-zA-Z0-9_.+]+    #name, custom
@                #@
[a-zA-Z0-9_.+]+    #domain
''', re.VERBOSE)

# get text off clipboard
text = pyperclip.paste()

# extract email/phone from text
extractedPhone = phoneRegex.findall(text)
# copy extract email/phone to clipboard
extractedEmail = emailRegex.findall(text)

allPhoneNumbers = [] #empty list
for phoneNum in extractedPhone:
    allPhoneNumbers.append(phoneNum[0])

#print(extractedPhone) #testing
#print(allPhoneNumbers)
#print(extractedEmail)

# copy email/phone to clipboard
#one line, newline seperated
results = '\n'.join(allPhoneNumbers) + '\n' + '\n'.join(extractedEmail)
pyperclip.copy(results);
#print(results)

quit()

#==========================================================================================
#11_file.py
#file
#!/usr/bin/python3
import os

print(os.path.join('f1','f2','f3','f.png'))
print(os.sep)
print(os.getcwd())
strPath = os.getcwd() + os.sep + 'test.ext'
print(strPath)
print(os.path.dirname(strPath))
print(os.path.basename(strPath))

quit()

#filesize
#!/usr/bin/python3
import os

totalSize = 0
filePath = os.getcwd()
for filename in os.listdir(filePath):
    if not os.path.isfile(os.path.join(filePath, filename)):
        continue
    totalSize = totalSize + os.path.getsize(os.path.join(filePath, filename))

print('File sizes are ' + str(totalSize) + ' bytes')

quit()

#rw
#!/usr/bin/python3
import os

filePath = os.getcwd() + os.sep + '11_hello.txt'
if not os.path.isfile(filePath):
    print('File does not exist')
    quit()

helloFile = open(filePath)
print(helloFile.read())
helloFile.seek(0) #go to start of file
print(helloFile.readlines())
helloFile.close()

baconFile = open('bacon.txt', 'w') #overwrite
baconBytes = baconFile.write('Bacon is not a vegetable.')
print('bytes written: ' + str(baconBytes))
baconFile.close()

baconFile = open('bacon.txt', 'a') #append
baconBytes = baconFile.write('\n\nBacon is a thing')
print('bytes written: ' + str(baconBytes))
baconFile.close()

baconFile = open('bacon.txt', 'r') #read
print(baconFile.read())
baconFile.close()

quit()


#vars
#!/usr/bin/python3
import os, shelve
shelfFile = shelve.open('shelvedata')
shelfFile['things'] = ['a','b','c','d']
shelfFile.close()

shelfFile = shelve.open('shelvedata')
print(shelfFile['things']) #key/value
print(list(shelfFile.keys()))
print(list(shelfFile.values()))

shelfFile.close()
quit()

#delete
#!/usr/bin/python3
import os

os.chdir(os.path.expanduser('~') + os.sep + 'Pictures')
print('location: ' + os.getcwd())

for filename in os.listdir():
    if filename.endswith('.jpg'):
        #os.unlink(filenm) #uncomment and change filenm to rm
        print(filename)

quit()

#walk
#!/usr/bin/python3
import os

folderPath = os.getcwd() + os.sep + 'folder'
print('folder: ' + folderPath)

for folderName, subfolders, filenames in os.walk(folderPath):
    print('Folder: ' + folderName)
    print('subfolders in ' + folderName + ':' + str(subfolders))
    print('filenames in ' + folderName + ':' + str(filenames))
    print(===) #newline

    ''' text/comment
    for subfolder in subfolders:
        if 'text' in subfolder:
            #os.rmdir(subfolder)
    for file in filenames:
        if file.endswith('.py'):
            #shutil.copy(os.path.join(folderName, file), \
            #        os.path.join(folderName, file + '.bk')) #rename
    '''

quit()

#==========================================================================================
#12_debug_log_assert.py
#raise
#!/usr/bin/python3

"""

***************
*             *
*             *
*             *
***************

"""

def boxPrint(symbol, width, height):
    if len(symbol) != 1:
        raise Exception('"symbol" needs to be a string of length 1')
    if (width < 2) or (height < 2):
        raise Exception('"width" and "height" must be >= 2')

    print(symbol * width)   #use string replication
    for i in range(height-2):
        print(symbol + (' ' * (width-2)) + symbol)
    print(symbol * width)

boxPrint('*', 15, 5)
boxPrint('o', 5, 7)

#boxPrint('**', 5, 10) #error
#boxPrint('*', 1, 1) #error

quit()

#traceback
#!/usr/bin/python3
import traceback
try:
    raise Exception('error msg')
except:
    errorFile = open('traceback_error_log.txt', 'a') #add to existing file
    errorFile.write(traceback.format_exc())
    errorFile.close()
    print('The traceback info was written to 12_traceback_error_log.txt')

quit()

#assert
#!/usr/bin/python3

market_2nd = {'ns': 'green', 'ew':'red'} #north-south = green, east-west = red

def switchLights(intersection):
    for key in intersection.keys(): #loop over keys
        if intersection[key] == 'green':
            intersection[key] = 'yellow'
        elif intersection[key] == 'yellow':
            intersection[key] = 'red'
        elif intersection[key] == 'red':
            intersection[key] = 'green'
    assert 'red' in intersection.values(), 'neither light is red' + str(intersection)

print(market_2nd)
switchLights(market_2nd)
print(market_2nd)

quit()

#log
#!/usr/bin/python3
#buggy factorial
import logging
#logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',datefmt='%H:%M:%S')
#logging.disable(logging.CRITICAL) #turn off logging
logging.debug('start of program')

def factorial(n):
    logging.debug('start of factorial(%s)' % (n))
    total = 1
    #for i in range (n+1): #is buggy
    for i in range (1, n + 1):
        total *= i
        logging.debug('i is %s, total is %s' % (i, total))

    logging.debug('return value is %s' % (total))
    return total

print(factorial(5))

logging.debug('end of program')
quit()


#debug
#!/usr/bin/python3

print('1st num:',end=' ')
first = input()
print('2nd num:',end=' ')
second = input()
print('3rd num:',end=' ')
third = input()
print('The sum is ' + first + second + third)

quit()

#breakpoint
#!/usr/bin/python3
import random

heads = 0

for i in range(1,1001): #1000 coin flips
    if random.randint(0,1) == 1:
        heads = heads + 1
    if i == 500:
        print('1/2 done')

print('heads: ' + str(heads) + ' times')

quit()

#==========================================================================================
#13_web.py
#webbrowser
#!/usr/bin/python3
#open google maps with clipboard or sysargs
import webbrowser, sys, pyperclip
#webbrowser.open('https://automatetheboringstuff.com') #opens browser

#check if command line args passed
if len(sys.argv) > 1:
    # sys.argv = ['program', 'arg1', 'arg2', ...]]
    address = ' '.join(sys.argv[1:]) #join list, seperate w/ space, avoid [0]
else:
    address = pyperclip.paste()

#https://www.google.com/maps/place/<address>
webbrowser.open('https://www.google.com/maps/place/' + address)

quit()

#request
#!/usr/bin/python3
import requests
res = requests.get('https://automatetheboringstuff.com/files/rj.txt')
print('response: ' + str(res.status_code))
print('len: ' + str(len(res.text)))

'''
playFile = open('rj.txt', 'wb')
for chunk in res.iter_content(100000): #return content in bytes, iterate content len
    playFile.write(chunk)
playFile.close()
'''

badRes = requests.get('https://automatetheboringstuff.com/ljdsaflkjsfdalkj') #badurl
print('response: ' + str(badRes.status_code))
badRes.raise_for_status()

quit()

#parse
#!/usr/bin/python3
import bs4, requests #beautifulsoup4, downloading
#get price info:
def getAmazonPrice(productURL):
	#need to use user-agent for Amazon, due to bot detection
    header = {
        'User-Agent': '...',
    }
    res = requests.get(productURL, headers=header)
    res.raise_for_status() #raise exception if error
    #print('response: ' + str(res.status_code))

    soup = bs4.BeautifulSoup(res.text, 'html.parser')
    #css selector to find, highlight + right click + Inspect -> right click -> copy -> copy selector (css path)
    #elems = soup.select('#mediaNoAccordion > div.a-row > div.a-column.a-span4.a-text-right.a-span-last > span.a-size-medium.a-color-price.header-price') #address of price
    #elems = soup.select('#mediaNoAccordion .header-price')
    elems = soup.select('#content > div:nth-child(2) > div.column.column-block.small-12.medium-3.medium-text-right > div:nth-child(1) > div.column.small-8.medium-12 > p > span > span')
    return elems[0].text.strip()

#has lots of scraper blockers
#getAmazonPrice('https://www.amazon.com/Automate-Boring-Stuff-Python-Programming-ebook/dp/B00WJ049VU')
price = getAmazonPrice('https://camelcamelcamel.com/Automate-Boring-Stuff-Python-Programming/product/1593275994')
print('price: ' + price)

quit()

#selenium
#!/usr/bin/python3
#requires firefox
from selenium import webdriver

browser = webdriver.Firefox() #launch, store in object, opens new window
browser.get('https://automatetheboringstuff.com')
elem = browser.find_element_by_css_selector('.entry-content > ol:nth=child(15) > li:nth-child(1) > a:nth-child(1)')
elem.click() #click on link
elems = browser.find_elements_by_css_selector('p')
len(elems)

#search
searchElem = browser.find_element_by_css_selector('.search-field')
searchElem.send_keys('test')

#read
elem = browser.find_element_by_css_selector('entry-content > p:nth-child(4)')
print(elem.text)

quit()

#==========================================================================================
#14_xls_pdf_doc.py
#excel
#!/usr/bin/python3
import openpyxl, os
#os.chdir('\doc\location')

#read xlsx - from http://autbor.com/example.xlsx
workbook = openpyxl.load_workbook('example.xlsx')
print(type(workbook))
#print(workbook.get_sheet_names()) #depreciated
#sheet = workbook.get_sheet_by_name('Sheet1') #depreciated
print(workbook.sheetnames)
sheet = workbook['Sheet1']
print(type(sheet))
print(str(sheet['A1'].value))
print(str(sheet.cell(row=1,column=2).value))
for i in range(1,8):
    print(i, sheet.cell(row=i, column=2).value, end='; ')
print('')

print('------------')
#write
wb = openpyxl.Workbook() #create
print(wb.sheetnames)
st = wb[wb.sheetnames[0]]
st['A1'] = 42
st['A2'] = 'Hello'
#wb.save('file.xlsx')
st2 = wb.create_sheet()
print(wb.sheetnames)
st2.title = 'new title'
print(wb.sheetnames)
st3 = wb.create_sheet(index=0, title='test2')
print(wb.sheetnames)

quit()

#pdf
#!/usr/bin/python3
import PyPDF2

#read pdf - from http://autbor.com/meetingminutes1.pdf
pdfFile = open('pdf1.pdf', 'rb')
reader = PyPDF2.PdfFileReader(pdfFile)
print('pages: ' + str(reader.numPages))
page = reader.getPage(0)
print(page.extractText()[:10])
#for pageNum in range(reader.numPages):
#   print(reader.getPage(pageNum).extractText())

#modificaiton
pdfFile2 = open('pdf2.pdf', 'rb')
reader2 = PyPDF2.PdfFileReader(pdfFile2)

writer = PyPDF2.PdfFileWriter()

for pageNum in range(reader.numPages):
    page = reader.getPage(pageNum)
    writer.addPage(page)

for pageNum in range(reader2.numPages):
    page = reader2.getPage(pageNum)
    writer.addPage(page)

outputFile = open('pdf3.pdf', 'wb')
writer.write(outputFile)
outputFile.close()
pdfFile.close()
pdfFile.close()

quit()

#doc
#!/usr/bin/python3
import docx

#read docx - from http://autbor.com/demo.docx
doc = docx.Document('demo.docx')
print(doc.paragraphs[0].text)

par = doc.paragraphs[1]
print(par.text)
print(par.runs)
print(par.runs[0].text)
#d.save('test.docx') #saves file

# new doc
doc2 = docx.Document()
doc2.add_paragraph('this is a paragraph, ')
doc2.add_paragraph('this is another paragraph')
par2 = doc2.paragraphs[0]
par2.add_run('this is a new run')
par2.runs[1].bold = True
#d.save('test4.docx')

#get doc as string
def getText(filename):
    doc3 = docx.Document(filename)
    fullText = [] #blank list
    for para in doc3.paragraphs:
        fullText.append(para.text) #append text of each object
    return '\n'.join(fullText) #single string

print(getText('demo.docx'))

quit()

#==========================================================================================
#15_email.py
#email
#!/usr/bin/python3
import smtplib
conn = smtplib.SMTP('smtp.gmail.com', 587) #connection object, port number 587
conn.ehlo() #start connection
conn.starttls() #secure
conn.login('email@gmail.com', 'password')
conn.sendmail('from@email', 'to@email', 'Subject: subject here\n\nEmail goes here\n\ntext')
conn.quit()

quit()

#check email
#!/usr/bin/python3
import imapclient, pyzmail
conn = imapclient.IMAPClient('imap.gmail.com', ssl=True) #depends on email provider
conn.login(email, pswd)
conn.select_folder('INBOX', readonly=True) #not delete email
# all, before, on, since, subject, body, text, etc
conn.search(['SINCE 20-AUG-2019']) #imap syntax
rawMsg = conn.fetch([num], ['BODY[]', 'FLAGS'])
pyzmail.PyzMessage.factory(rawMsg[num][b'BODY[]'])
print(msg.get_subject())
print(msg.get_addresses('from')
if msg.text_part != None:
    print(msg.text_part.get_payload().decode('UTF-8'))
#conn.list_folders()

quit()

#==========================================================================================
#16_mouse.py
#!/usr/bin/python3
import pyautogui

print('Screen size: ' + str(pyautogui.size()))
width, height = pyautogui.size()
print('Current location: ' + str(pyautogui.position()))

pyautogui.moveTo(600,600, duration=0.1) #move to x,y
print('Current location: ' + str(pyautogui.position()))
pyautogui.moveRel(120,120) #move to x,y
print('Current location: ' + str(pyautogui.position()))
#pyautogui.click(x, y)

quit()