Python Quick Reference Library
What is the Python Quick Reference Library?
While learning to create Python solutions, we thought it wise to pause and document our progress, what we have learned, and what solutions worked for us. This will act as a guide for future developers within our office and beyond.
#C:Files.1 #C:Files.1exe #QUARTO_R: “C:/Program Files/R/R-4.4.1/bin/R.exe” setx QUARTO_R: “C:/Program Files/R/R-4.4.1/bin/R.exe”
Installs
To actually knitt/stitch this document you will need to have Python, R, and Quarto installed on your machine through Visual Studio Code (or RStudio).
Ensure R and Python Interoperability with reticulate
(this allows a .rmd or .qmd document to work)
The following code chunk will ensure that the reticulate
package is installed, which is necessary for Python and R to work together in this document. When running this, you may get a pop-up asking you to select a Secure CRAN mirror
, being in the Pacific Northwest (PNW) we typically use USA (OR)[https]
, then select OK
.
# Function to install a package if it is not already installed
<- function(package) {
install_if_missing if (!requireNamespace(package, quietly = TRUE)) {
install.packages(package)
}
}
# Install and load the 'reticulate' package
install_if_missing("reticulate")
library(reticulate)
Python Basics
In Python, there isn’t a single “base structure” like SQL’s SELECT * FROM Table
that is universally used to introduce beginners to the language. However, there are a few fundamental concepts and structures that are often used to start teaching Python, much like how SELECT * FROM Table
is a common starting point in SQL.
1. The Basic print() Function
- The print() function is often the very first thing taught in Python. It’s a simple way to display output, much like how SELECT * retrieves data in SQL.
- This introduces the user to the syntax of Python and demonstrates how to run a basic command.
print("Hello, World!")
Hello, World!
2. Basic Variables and Data Types
- After introducing print(), the next step is usually to show how to create and use variables.
- This teaches the concept of data types (str, int, bool, etc.) and how to store and use data in Python.
Note: in Python, variables do not need to be explicity called out as their data type as they do in SQL.
# Compared to a SQL Example:
# DECLARE name VARCHAR = 'Alice';
# Assigning values to variables in Python
# only requires the variable name you will call later,
# and the `=`
# and the variable.
# Python auto recognizes the data type.
# Also note, that strings require quotes, int and booleans do not
= "Alice"
name = 30
age = True
is_student
# Printing the variables
print(name)
Alice
print(age)
30
print(is_student)
True
3. Basic Data Structures
- The next step is often to introduce basic data structures like lists and dictionaries, which are central to Python programming.
- This gives users a sense of how Python can store collections of data.
# List
= ["apple", "banana", "cherry"]
fruits print(fruits)
['apple', 'banana', 'cherry']
# Dictionary
= {"name": "Alice", "age": 30, "is_student": True}
person print(person)
{'name': 'Alice', 'age': 30, 'is_student': True}
4. Control Flow Statements
(aka If/Else logic and Loops)
- Once variables and data types are understood, introducing control flow with if, for, and while statements is a logical next step.
- This introduces users to decision-making and iteration in Python.
- This is how you would use database connections to pull in dataframes and loop through them, more on that later.
# Conditional statement
if age > 18:
print("Adult")
else:
print("Minor")
Adult
# Loop
for fruit in fruits:
print(fruit)
apple
banana
cherry
5. Functions
- Finally, teaching how to define and call functions is often a good way to encapsulate everything learned so far.
- Functions show how to organize code into reusable blocks, similar to how queries and procedures are organized in SQL.
def greet(name):
return f"Hello, {name}!"
print(greet("Alice"))
Hello, Alice!
6. Syntax
- Most code must be written in lower case for Python.
- Ex: Print ()vs PRINT() vs print()
- Only print() will actually run.
7. Notes/Comments
- Notes or Comments are important for leaving behind instructions or thought processess for future developers/coders to understand your script and then fix it or re-purpose it. This is especially true in an officer with other users coming behind you to use your tools.
- In Python, there are essentially two ways to comment.
- Single line comments:
# This is a comment
- Multi line comments:
""" This is comment """
- Single line comments:
Note: In Python, when you include a multi-line string that’s not assigned or used in an expression, it’s technically treated as an object and not just a comment, especially in the context of an interactive or execution environment like Quarto. You can either use several single line comments or use the if False:
to keep it from rendering as an object. Outside of Quarto (.qmd) in a normal code, the triple quotes will keep it from being read as code, but this could still be good practice to keep.
# Single Line Comment
if False:
"""
This
Is A
Multi
Line Comment
"""
print("Comment Examples")
Comment Examples
8. Indent
Unlike SQL, spaces/indenting matters for Python. The script flows such that a function that is not indented to run in the right way will break the cascading logic.
In the following example the function ``
# Example function with indenting:
def click_image(image_path, confidence=0.8):
try:
if not os.path.exists(image_path):
f"Image file not found: {image_path}")
logging.error(return False
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image
= pyautogui.screenshot()
screenshot = cv2.cvtColor(np.array(screenshot), cv2.COLOR_BGR2GRAY)
screenshot
= cv2.matchTemplate(screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
min_val, max_val, min_loc, max_loc
if max_val >= confidence:
= max_loc
top_left = ref_image.shape
h, w = top_left[0] + w // 2, top_left[1] + h // 2
center_x, center_y
pyautogui.moveTo(center_x, center_y)
pyautogui.click()f"Clicked on the image: {image_path}")
logging.info(return True
else:
f"Could not locate the image: {image_path} with confidence {confidence}")
logging.warning(return False
except Exception as e:
f"Failed to click the image: {image_path}")
logging.error(f"Exception: {e}")
logging.error(return False
Python Code Structuring
While the exact structure a script takes can vary wildy, I have found that the frist tool I built worked best with a cascading flow of content as follows. This worked to both help the script ensure things were caling on items above them, and help the human reader understand what was happening.
Include comments at each stage explaning what is happening…
1. Libraries
Libraries required for the script to function.
- Import Libraries
2. Global Logic
Functions that ensure the entire document flows and works
- Logging: A logging function to ensure all activities are logged for debugging
- Kill Script: A kill script function to ensure you can stop the script, calling this at appropriate times in the script to ensure it works, at the beggining of each loop.
- Connections:
- Connection to database
- Connection to stored images, image paths
- Glocal Logic (like ensure caps lock is off)
- Establishing any GUI (graphic user interface) for the end user
3. Specific Functions
Specific functions required to make the script run the tasks assigned - Screen capture OCR logic - Click image logic - Specific logic for script
4. Specific Commands and loops
- Establish/start the loop(s)
- Specific commands, which may or may not call in earlier functions
- End loop(s)
- End connection(s)
Breaking it down
The following will break down specific examples and use cases of those code structures.
1. Python Libraries
The following libraries have been found useful for the building of automation tools for the Kootenai County Assessor’s Office.
Python Libraries we’ve used or found helpful
import pyautogui # For automating GUI interactions like mouse movements and clicks
import pyodbc # For establishing database connections and executing SQL queries
import time # For adding delays and managing time-related functions
import numpy as np # For numerical operations and handling arrays/matrices
import keyboard # For detecting and handling keyboard key presses
import threading # For running background tasks and creating concurrent threads
import tkinter as tk # For creating basic GUI elements in Python applications
from tkinter import ttk, messagebox, scrolledtext # For advanced Tkinter widgets, dialog boxes, and scrolled text areas
import pytesseract # For OCR (Optical Character Recognition) to read text from images
from PIL import Image # For working with image data
import cv2 # For image processing and computer vision tasks (OpenCV library)
import ctypes # For interacting with C data types and Windows API functions
from tkcalendar import Calendar, DateEntry # For adding calendar widgets to Tkinter GUIs
import logging # For logging events, errors, and information during script execution
import configparser # For handling configuration files (.ini)
import os # For interacting with the operating system, e.g., file and directory management
from datetime import datetime, date # For handling date and time operations
import psutil # For retrieving information on running processes and system utilization
import pygetwindow as gw # For getting and manipulating window information
from pywinauto import Application # For interacting with Windows applications programmatically
Python Libraries Used
pyautogui
- Purpose: Automates GUI (Graphical User Interface) interactions such as mouse clicks, keystrokes, and screen captures.
- Usage: To control mouse and keyboard actions, such as clicking buttons, typing text, and capturing screenshots of the screen.
pyodbc
- Purpose: Enables connections to databases using ODBC (Open Database Connectivity).
- Usage: To connect to SQL Server databases, execute queries, and fetch results.
pywinauto.Application
- Purpose: Facilitates interaction with Windows GUI applications.
- Usage: To automate tasks within Windows applications, such as focusing on specific windows.
time
- Purpose: Provides time-related functions.
- Usage: To introduce delays (e.g.,
time.sleep()
) between automated actions to ensure proper execution.
numpy
- Purpose: Supports large, multi-dimensional arrays and matrices, along with a large collection of mathematical functions.
- Usage: To perform numerical operations, particularly in image processing.
keyboard
- Purpose: Detects and handles keyboard events.
- Usage: To detect specific key presses, such as the ‘esc’ key to stop the script.
threading
- Purpose: Allows for running multiple threads (tasks) concurrently.
- Usage: To run background tasks like monitoring for a kill key press.
tkinter
- Purpose: Provides tools for creating graphical user interfaces (GUIs).
- Usage: To create input forms for collecting user inputs through a pop-up window.
tkinter.ttk
, tkinter.messagebox
- Purpose:
ttk
offers advanced widgets for Tkinter, andmessagebox
provides standard dialogs for showing messages. - Usage: To enhance the GUI with better-looking widgets and display error messages or other notifications.
pytesseract
- Purpose: Optical Character Recognition (OCR) tool for extracting text from images.
- Usage: To recognize and read text from screenshots or other images captured during the script’s execution.
PIL.ImageGrab
- Purpose: Provides functionality to capture the screen or parts of it.
- Usage: To capture specific regions of the screen for further processing with OCR.
cv2
(OpenCV)
- Purpose: A library for computer vision tasks, including image processing.
- Usage: To process images, match templates, and recognize UI elements on the screen.
os
- Purpose: Provides functions to interact with the operating system, including file handling.
- Usage: To check if image files exist and interact with the file system.
datetime
- Purpose: Supplies classes for manipulating dates and times.
- Usage: To generate timestamps, work with dates, and format date strings.
ctypes
- Purpose: Provides C compatible data types and allows calling functions in DLLs or shared libraries.
- Usage: To interact with Windows system calls, such as checking if the CAPS LOCK key is on.
tkcalendar.DateEntry
- Purpose: A widget to select dates from a calendar.
- Usage: To provide an easy way for the user to select dates in the GUI.
logging
- Purpose: Provides a way to report status, error, and informational messages to a file or console.
- Usage: To record logs of the script’s execution, including errors, warnings, and information for debugging.
pygetwindow
- Purpose: A simple cross-platform module to find and manipulate open windows on your computer.
- Usage: To retrieve window handles, bring specific windows into focus, or manipulate window states (e.g., minimize, maximize).
configparser
- Purpose: Provides functionality for handling configuration files in the
.ini
format. - Usage: To read, write, and modify configuration settings in a structured manner, often used for setting up initial parameters or options for scripts.
psutil
- Purpose: A library for retrieving information on running processes and system utilization (CPU, memory, disks, network, sensors) in Python.
- Usage: To monitor system resources, manage processes, and gather statistics, often used to check if specific processes are running or to ensure enough system resources are available before executing tasks.
Python Library Installation
This section will ensure that all necessary Python libraries are installed and ready to use. If a library is missing, it will be installed automatically. You can alter this in your script to match the libraries you choose. You can also comment it out after the install if it causes run problems. These libraries work together to automate and manage the input process, ensuring a smooth and accurate transfer of data into the ProVal system.
Installing
import subprocess
import pkg_resources
import sys
def install_if_missing(package):
try:
pkg_resources.get_distribution(package)except pkg_resources.DistributionNotFound:
"-m", "pip", "install", package])
subprocess.check_call([sys.executable,
# List of packages you want to ensure are installed
= [
packages "pyautogui", # For automating GUI interactions
"pyodbc", # For database connections
"numpy", # For numerical operations
"keyboard", # For detecting key presses
"pytesseract", # For OCR (Optical Character Recognition)
"Pillow", # For image processing related to Image
"opencv-python", # For image processing (cv2)
"tkcalendar" # For calendar widget in Tkinter
]
# Apply the install_if_missing function to each package
for package in packages:
install_if_missing(package)
# After ensuring all packages are installed, you can import them as needed in your script
import pyautogui # For automating GUI interactions like mouse movements and clicks
import pyodbc # For establishing database connections and executing SQL queries
import time # For adding delays and managing time-related functions
import numpy as np # For numerical operations and handling arrays/matrices
import keyboard # For detecting and handling keyboard key presses
import threading # For running background tasks and creating concurrent threads
import tkinter as tk # For creating basic GUI elements in Python applications
from tkinter import ttk, messagebox # For advanced Tkinter widgets and displaying dialog boxes
import pytesseract # For OCR (Optical Character Recognition) to read text from images
from PIL import Image # For working with image data
import cv2 # For image processing and computer vision tasks (OpenCV library)
import ctypes # For interacting with C data types and Windows API functions
from tkcalendar import DateEntry # For adding a calendar widget to Tkinter GUIs
import logging # For logging events, errors, and information during script execution
# Basic logging setup
=logging.INFO) logging.basicConfig(level
Installing - commented out
"""
import subprocess
import pkg_resources
import sys
def install_if_missing(package):
try:
pkg_resources.get_distribution(package)
except pkg_resources.DistributionNotFound:
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
# List of packages you want to ensure are installed
packages = [
"pyautogui", # For automating GUI interactions
"pyodbc", # For database connections
"numpy", # For numerical operations
"keyboard", # For detecting key presses
"pytesseract", # For OCR (Optical Character Recognition)
"Pillow", # For image processing related to Image
"opencv-python", # For image processing (cv2)
"tkcalendar" # For calendar widget in Tkinter
]
# Apply the install_if_missing function to each package
for package in packages:
install_if_missing(package)
"""
# After ensuring all packages are installed, you can import them as needed in your script
import pyautogui # For automating GUI interactions like mouse movements and clicks
import pyodbc # For establishing database connections and executing SQL queries
import time # For adding delays and managing time-related functions
import numpy as np # For numerical operations and handling arrays/matrices
import keyboard # For detecting and handling keyboard key presses
import threading # For running background tasks and creating concurrent threads
import tkinter as tk # For creating basic GUI elements in Python applications
from tkinter import ttk, messagebox # For advanced Tkinter widgets and displaying dialog boxes
import pytesseract # For OCR (Optical Character Recognition) to read text from images
from PIL import Image # For working with image data
import cv2 # For image processing and computer vision tasks (OpenCV library)
import ctypes # For interacting with C data types and Windows API functions
from tkcalendar import DateEntry # For adding a calendar widget to Tkinter GUIs
import logging # For logging events, errors, and information during script execution
# Basic logging setup
=logging.INFO) logging.basicConfig(level
2. GLOBAL LOGICS - CONNECTIONS
Logging
# THIS IS THE FUNCTION
# Change the path and file name to ensure it is unique to this script
# filename='S:/Common/Comptroller Tech/Reports/Python/finalfolderinpath/filename.log',
logging.basicConfig(='S:/Common/Comptroller Tech/Reports/Python/Auto_Mapping_Packet/MappingPacketsAutomation.log',
filename=logging.DEBUG,
levelformat='%(asctime)s - %(levelname)s - %(message)s'
)= logging.StreamHandler()
console_handler
console_handler.setLevel(logging.DEBUG)'%(asctime)s - %(levelname)s - %(message)s'))
console_handler.setFormatter(logging.Formatter(
logging.getLogger().addHandler(console_handler)
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
# It is used much like print("Attempting to..."), but includes the printout in the log created by the function.
"Attempting to locate and click on the 'Land' tab...") logging.info(
Kill Script
# THIS IS THE FUNCTION
# Global flag to indicate if the script should be stopped
= False
stop_script
def monitor_kill_key():
global stop_script
"Kill key monitor started. Press 'esc' to stop the script.")
logging.info('esc') # Set 'esc' as the kill key
keyboard.wait(= True
stop_script "Kill key pressed. Stopping the script...")
logging.info(
# Start the kill key monitoring in a separate thread
= threading.Thread(target=monitor_kill_key)
kill_key_thread = True
kill_key_thread.daemon
kill_key_thread.start()
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
# Below in the actual code, include the following at various breakpoints,
# Typically at the start of loops.
# Ensure indentation matches the loop point
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Example of use and indentation
for row in rows:
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
= row
DBAIN, DBACRE
ensure_capslock_off()
# Process each AIN individually
'ProVal', DBAIN)
set_focus_and_type(1) time.sleep(
Connections: Database
# THIS IS THE FUNCTION
# Configuration for database connection
= (
db_connection_string "Driver={SQL Server};"
"Server=astxdbprod;"
"Database=GRM_Main;"
"Trusted_Connection=yes;"
)
# Function to connect to the database
def connect_to_database(connection_string):
return pyodbc.connect(connection_string)
# Function to execute a SQL query and fetch data
def execute_query(cursor, query):
cursor.execute(query)return cursor.fetchall()
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
"""
Connect to the database,
pull a simple SQL query with two columns,
then the for row in rows assigns those columns as variables
"""
= connect_to_database(db_connection_string)
conn = conn.cursor()
cursor
# The query should accommodate multiple AINs in a list
# Note: in-between the quotes is a SQL script that uses the user input field function as a condition.
# In this use-case, the user-input gui pop-up function would be between the database function and this call, but theoretically you could call the connection later when you are ready to use it.
= f"SELECT TRIM(pm.AIN), pm.LegalAcres FROM TSBv_Parcelmaster AS pm WHERE pm.AIN IN ({','.join(AINLIST)})"
query = execute_query(cursor, query) rows
Graphic User Interface (GUI) Logic - START
### Graphic User Interface (GUI) Logic - START
# Initialize variables to avoid 'NameError', will call them into the final product after variable selections
= ""
MemoTXT = ""
PDESC
def on_submit():
ensure_capslock_off()
global AINLIST, AINFROM, AINTO, PDESC, PFILE, PNUMBER, TREVIEW, MappingPacketType, Initials, MemoTXT, ForYear
# Collect inputs for AINFROM and AINTO, and split by commas
= [ain.strip() for ain in entry_ainfrom.get().strip().upper().split(",")]
AINFROM = [ain.strip() for ain in entry_ainto.get().strip().upper().split(",")]
AINTO
# Combine the AINFROM and AINTO lists, removing duplicates
= list(set(AINFROM + AINTO))
combined_ain_list
# Replace AINLIST with the combined list
= combined_ain_list
AINLIST
= entry_pfile.get().strip().upper()
PFILE = entry_pnumber.get().strip().upper()
PNUMBER = entry_treview.get().strip().upper()
TREVIEW = combobox_mappingpackettype.get().strip().upper()
MappingPacketType = entry_initials.get().strip().upper()
Initials = for_year_combobox.get().strip() # Get the selected year
ForYear
= datetime.now().month
the_month = datetime.now().day
the_day = datetime.now().year
the_year
#the_month = datetime.datetime.now().month
#the_day = datetime.datetime.now().day
#the_year = datetime.datetime.now().year
= ', '.join(AINFROM)
AINFROM_str = ', '.join(AINTO)
AINTO_str = f"{Initials}-{the_month}/{str(the_year)[-2:]} {MappingPacketType} from {AINFROM_str} into {AINTO_str} for {ForYear}"
MemoTXT f"Generated MemoTXT: {MemoTXT}")
logging.info(
= f"{MappingPacketType} for {ForYear}"
PDESC
if not AINLIST or not PFILE or not PNUMBER or not TREVIEW or not MappingPacketType or not Initials or not MemoTXT or not PDESC:
"Input Error", "All input fields are required.")
messagebox.showerror(return
# Close the GUI
root.destroy()
def setup_gui():
= tk.Tk()
root "User Input Form")
root.title(
setup_widgets(root)return root
def validate_initials(action, value_if_allowed):
# Allow only alphabetic characters and limit to 3 characters
if action == '1': # 1 means an insertion operation
if len(value_if_allowed) > 3:
return False
return value_if_allowed.isalpha()
return True
def setup_widgets(root):
global entry_ainfrom, entry_ainto, entry_pfile, entry_pnumber, entry_treview, combobox_mappingpackettype, entry_initials, for_year_combobox
# Get the current and next year
= datetime.now().year
current_year = current_year + 1
next_year
="Mapping packet FOR what year?:").grid(column=0, row=1, padx=10, pady=5)
ttk.Label(root, text= ttk.Combobox(root, values=[current_year, next_year], width=47)
for_year_combobox =1, row=1, padx=10, pady=5)
for_year_combobox.grid(column0) # Set default selection to the current year
for_year_combobox.current(
# AINFROM input
="List AINs FROM (separated by comma):").grid(column=0, row=0, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_ainfrom =1, row=0, padx=10, pady=5)
entry_ainfrom.grid(column
# AINTO input
="List AINs TO (separated by comma):").grid(column=0, row=1, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_ainto =1, row=1, padx=10, pady=5)
entry_ainto.grid(column
# Existing fields continue below...
="Mapping packet FOR what year?:").grid(column=0, row=2, padx=10, pady=5)
ttk.Label(root, text= ttk.Combobox(root, values=[current_year, next_year], width=47)
for_year_combobox =1, row=2, padx=10, pady=5)
for_year_combobox.grid(column0) # Set default selection to the current year
for_year_combobox.current(
="Filing Date (Top Date):").grid(column=0, row=3, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_pfile =1, row=3, padx=10, pady=5)
entry_pfile.grid(column
="Permit Number (Bottom Date):").grid(column=0, row=4, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_pnumber =1, row=4, padx=10, pady=5)
entry_pnumber.grid(column
="Timber or AG review? Y/N:").grid(column=0, row=5, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_treview =1, row=5, padx=10, pady=5)
entry_treview.grid(column
="Select Mapping Packet Type:").grid(column=0, row=6, padx=10, pady=5)
ttk.Label(root, text
= [
mapping_packet_types "MERGE", "SPLIT", "BLA", "LLA", "RW VACATION", "RW SPLIT", "REDESCRIBE",
"RW AUDIT", "RW Cat19", "AIRPORT LEASE NEW PARCEL", "PLAT VACATION",
"PARCEL DELETED", "ACERAGE AUDIT", "NEW PLAT"
]= ttk.Combobox(root, values=mapping_packet_types, width=47)
combobox_mappingpackettype =1, row=6, padx=10, pady=5)
combobox_mappingpackettype.grid(column0) # Set default selection to the first item
combobox_mappingpackettype.current(
# Validation for the Initials Entry
= (root.register(validate_initials), '%d', '%P')
vcmd
="Enter (3) Initials:").grid(column=0, row=7, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50, validate='key', validatecommand=vcmd)
entry_initials =1, row=7, padx=10, pady=5)
entry_initials.grid(column0, "DGW")
entry_initials.insert(
= ttk.Button(root, text="Submit", command=on_submit)
submit_button =0, row=8, columnspan=2, pady=20)
submit_button.grid(column
= setup_gui()
root
### Graphic User Interface (GUI) Logic - END
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
# EXAMPLES OF USE CASES
if not AINLIST or not MemoTXT or not PDESC or not PFILE or not PNUMBER or not TREVIEW:
"All input fields are required.")
logging.error(
exit()
= connect_to_database(db_connection_string)
conn = conn.cursor()
cursor
# The query should accommodate multiple AINs in a list
= f"SELECT TRIM(pm.AIN), pm.LegalAcres FROM TSBv_Parcelmaster AS pm WHERE pm.AIN IN ({','.join(AINLIST)})"
query = execute_query(cursor, query)
rows
str(DBAIN))
pyautogui.typewrite(f"Sent AIN {DBAIN}.")
logging.info(
pyautogui.typewrite(MemoTXT)
pyautogui.typewrite(PNUMBER)
3. GLOBAL LOGICS - LOGIC FUNCTIONS
CAPS LOCK
# THIS IS THE FUNCTION
# CAPS Lock Logic
def is_capslock_on():
# This will return 1 if CAPS LOCK is on, 0 if it's off
= ctypes.WinDLL("User32.dll")
hllDll = 0x14
VK_CAPITAL return hllDll.GetKeyState(VK_CAPITAL) & 1
def ensure_capslock_off():
if is_capslock_on():
'capslock')
pyautogui.press("CAPS LOCK was on. It has been turned off.")
logging.info(else:
"CAPS LOCK is already off.")
logging.info(
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
ensure_capslock_off()
pyautogui.typewrite(MemoTXT)1) time.sleep(
SET FOCUS
# THIS IS THE FUNCTION
"""
SET FOCUS LOGIC - can be called to set focus to different screens at different points in the process
"""
def set_focus_and_type(window_title, keys):
= pyautogui.getWindowsWithTitle(window_title)
window if window:
0].activate()
window[
pyautogui.typewrite(keys)
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
'ProVal', DBAIN)
set_focus_and_type(1)
time.sleep("set_focus_and_type") logging.info(
PRESS & CLICK KEY LOGIC
# THIS IS THE FUNCTION
"""
PRESS & CLICK LOGIC -- sets logic for various kinds of click tasks
"""
def press_key_with_modifier_multiple_times(modifier, key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.hotkey(modifier, key)
def press_key_multiple_times(key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.press(key)
def triple_click(x, y):
=3)
pyautogui.click(x, y, clicks
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
'shift', 'tab', 6)
press_key_with_modifier_multiple_times(1)
time.sleep(
'up', 12)
press_key_multiple_times("press_key_multiple_times") logging.info(
4. GLOBAL LOGICS - SCREEN HANDLING FUNCTIONS
Connections: OCR and Image Paths
OCR
OCR stands for Optical Character Recognition. It is a technology used to convert different types of documents, such as scanned paper documents, PDF files, or images captured by a digital camera, into editable and searchable data. OCR software analyzes the shapes and patterns of the characters in the image and translates them into machine-readable text.
In our use-case, OCR can be used to read ProVal and Aumentum screens, match a stored image to that screenshot, find the location of that image on the current screen, and then click that image.
OCR requires downloading and installing the tesseract
OCR tool from the GitHub repo.
"""
# GLOBAL LOGICS - SCREEN HANDLING FUNCTIONS
"""
### Connections: OCR and Image Paths
#### OCR
# This OCR program is required to work with this script, it is available on GitHub
# Set the tesseract executable path if not in the system path
# Update this path as necessary by user you will need to download and install tesseract from GitHub
# Link https://github.com/tesseract-ocr/tesseract
# Link https://github.com/UB-Mannheim/tesseract/wiki
= r'C:\Users\dwolfe\AppData\Local\Programs\Tesseract-OCR\tesseract.exe'
pytesseract.pytesseract.tesseract_cmd
#### Image Paths - Active and Inactive
= [
land_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_tab_active.PNG'
]= [
land_base_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_base_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_base_tab_active.PNG'
]= [
permits_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_tab_active.PNG'
]
= [
permits_add_permit_button r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_permit_button.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_permit_button_active.PNG'
]
#### Image Paths - Single Images Only
= r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_memo_duplicate.PNG'
duplicate_memo_image_path = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_fieldvisit_button.PNG'
add_field_visit_image_path = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_aggregate_land_type_add_button.PNG'
aggregate_land_type_add_button = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_farm_total_acres.PNG'
farm_total_acres_image = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permit_description.PNG' permit_description
1 CAPTURE SCREEN IN GREYSCALE & 2 CLICK USING A REFERENCE GREYSCALE SCREENSHOT TO A STORED GREYSCALE IMAGE INCLUDES ABILITY TO CLICK RELATIVE POSITION
capture_and_convert_screenshot
, captures the screen in greyscaleclick_on_image
, pulls the captured screen fromcapture_and_convert_screenshot
and determines location
# 1 CAPTURE SCREEN IN GREYSCALE
def capture_and_convert_screenshot():
# Capture the screenshot using pyautogui
= pyautogui.screenshot()
screenshot
# Convert the screenshot to a numpy array, then to BGR, and finally to greyscale
= np.array(screenshot)
screenshot_np = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
screenshot_np = cv2.cvtColor(screenshot_np, cv2.COLOR_BGR2GRAY)
grey_screenshot
return grey_screenshot
# 2 CLICK USING A REFERENCE GREYSCALE SCREENSHOT TO A STORED GREYSCALE IMAGE INCLUDES ABILITY TO CLICK RELATIVE POSITION
def click_on_image(image_path, direction='center', offset=10, inset=7, confidence=0.8):
= capture_and_convert_screenshot()
grey_screenshot
# Load the reference image in greyscale
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image if ref_image is None:
f"Failed to load reference image from {image_path}")
logging.error(return False
# Perform template matching
= cv2.matchTemplate(grey_screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
_, max_val, _, max_loc
if max_val >= confidence:
= max_loc
top_left = ref_image.shape
h, w = top_left[0] + w
right = top_left[1] + h
bottom
# Calculate click position based on direction and inset/offset
= {
click_positions 'right': (right + offset, top_left[1] + h // 2),
'left': (top_left[0] - offset, top_left[1] + h // 2),
'above': (top_left[0] + w // 2, top_left[1] - offset),
'below': (top_left[0] + w // 2, bottom + offset),
'bottom_right_corner': (right - inset, bottom - inset),
'bottom_left_corner': (top_left[0] + inset, bottom - inset),
'top_right_corner': (right - inset, top_left[1] + inset),
'top_left_corner': (top_left[0] + inset, top_left[1] + inset),
'bottom_center': (top_left[0] + w // 2, bottom - inset),
'top_center': (top_left[0] + w // 2, top_left[1] + inset),
'center': (top_left[0] + w // 2, top_left[1] + h // 2)
}= click_positions[direction]
click_x, click_y
# Perform the click
pyautogui.click(click_x, click_y)f"Clicked {direction} of the image at ({click_x}, {click_y})")
logging.info(return True
else:
f"No good match found at the confidence level of {confidence}.")
logging.warning(return False
"""
# Ref Function click_on_image in the following functions 3, 4, 5, etc...
# These reference functions can be called in the final automation script
"""
3 USING click_on_image FUNCTION
Both version of the following click_images functions will call an image and click on it. However, in some cases, the image may look slightly different if it is already selected vs if it is not. In those cases, having more than one optoin to click is helpful. Therefore: - click_images_multiple
, will cycle through image options (selected vs not-selected, or, active vs inactive) - click_image_single
, will click the single stored image
# 3 USING click_on_image FUNCTION
#Specific Click Functions Here, See click_on_image for directionals, and image pathes for images
def click_images_multiple(paths, direction='center', offset=50, inset=7, confidence=0.75):
for image_path in paths:
f"Trying to click {direction} on image: {image_path}")
logging.info(if click_on_image(image_path, direction=direction, offset=offset, inset=inset, confidence=confidence):
f"Successfully clicked {direction} of {image_path}.")
logging.info(return True
else:
f"Failed to click {direction} of {image_path}.")
logging.warning(return False
def click_image_single(image_path, direction='center', offset=50, inset=7, confidence=0.75):
f"Trying to click {direction} on image: {image_path}")
logging.info(if click_on_image(image_path, direction=direction, offset=offset, inset=inset, confidence=confidence):
f"Successfully clicked {direction} of {image_path}.")
logging.info(return True
else:
f"Failed to click {direction} of {image_path}.")
logging.warning(return False
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
# How to use these click_images_multiple & click_image_single functions in script
# Click below all specified images
if click_images_multiple(multiple_image_path_name_here, direction='below', offset=100, confidence=0.8):
"Clicked successfully.")
logging.info(
# Click at the center of a single image
if click_image_single(single_image_path_name_here, direction='center', confidence=0.8):
"Clicked successfully.")
logging.info(
# Click at the bottom right corner of a single image
if click_image_single(single_image_path_name_here, direction='bottom_right_corner', inset=10, confidence=0.8):
"Clicked successfully.")
logging.info(
# Click to right of permit_description, by calling offset=5 it was just barely below the image, which is what I wanted
if click_image_single(permit_description, direction='below', offset=5, confidence=0.8):
"Clicked successfully permit_description.")
logging.info(1) time.sleep(
4 CHECKING IF IMAGE IS PRESENT
is_image_found
, performs a simple logical test to see if the image is present, which can then be used to create branched senarios.
# 4 CHECKING IF IMAGE IS PRESENT
def is_image_found(image_path, confidence=0.8):
"""
Check if an image is present on the screen with a specified confidence level.
:param image_path: Path to the image file to be checked.
:param confidence: The confidence level for the image matching.
:return: bool - True if image is found, False otherwise.
"""
# Use the existing function to capture and convert the screenshot
= capture_and_convert_screenshot()
grey_screenshot
# Load the reference image in greyscale
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image if ref_image is None:
f"Failed to load reference image from {image_path}")
logging.error(return False
# Perform template matching
= cv2.matchTemplate(grey_screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
_, max_val, _, _
= max_val >= confidence
found if found:
f"Image found with confidence {max_val}: {image_path}")
logging.info(else:
f"Image not found with sufficient confidence {confidence}: {image_path}")
logging.info(
return found
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
# How to use the is_image_found function below in script:
# Check if the image is found and decide based on that
if is_image_found(image_path_name_here, confidence=0.8):
"Image was found - executing related tasks.")
logging.info(# Perform tasks related to the image being found
else:
"Image was not found - executing alternative tasks.")
logging.info(# Perform alternative tasks
# Example:
# Check if the image is found and decide based on that
if is_image_found(farm_total_acres_image, confidence=0.8):
"Image was found - executing related tasks.")
logging.info(# Perform tasks related to the image being found
# Click to the right of Farm Acres
if click_image_single(farm_total_acres_image, direction='right', offset=15, confidence=0.8):
"Clicked successfully farm_total_acres_image.")
logging.info(1)
time.sleep(
# Delete the contents and send DBACRE
'delete')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
str(DBACRE))
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
else:
"farm_total_acres_image Image was not found - executing alternative tasks.")
logging.info(# Perform alternative tasks
# Click to the right of Farm Acres
if click_image_single(aggregate_land_type_add_button, direction='bottom_right_corner', inset=10, confidence=0.8):
"Clicked successfully aggregate_land_type_add_button.")
logging.info(1)
time.sleep(
# Send the DBACRE value after clicking the fallback button
'f')
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
str(DBACRE))
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
5 READ TEXT FROM SCREEN
check_for_text_on_screen
, performs a check to see if particular text is found on the screen at this stage in the script, then branches based on the outcome.
# 5 READ TEXT FROM SCREEN
def check_for_text_on_screen(target_text):
"""
Captures the screen, converts it to greyscale, performs OCR, and checks for the specified text.
:param target_text: Text to search for in the OCR results.
:return: True if the text is found, False otherwise.
"""
= capture_and_convert_screenshot()
grey_screenshot = Image.fromarray(grey_screenshot) # Convert numpy array back to a PIL Image
grey_screenshot_pil = pytesseract.image_to_string(grey_screenshot_pil)
screen_text return target_text in screen_text
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
# How to use the check_for_text_on_screen function below in script:
# Define the specific text you're looking for
= "text_you_want_to_check_here"
specific_text
# Use the variable in your function call and print statements
if check_for_text_on_screen(specific_text):
f"Found '{specific_text}' on the screen.")
logging.info(else:
f"Did not find '{specific_text}' on the screen.")
logging.info(
# Example:
# Process: Enter Land Memos
# CHECK IF LAND
# Define the specific text you're looking for
= "LAND"
specific_text # Use the variable in your function call and print statements
if check_for_text_on_screen(specific_text):
f"Found '{specific_text}' on the screen.")
logging.info(
'l')
pyautogui.press('enter')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
pyautogui.typewrite(MemoTXT)1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(else:
f"Did not find '{specific_text}' on the screen.")
logging.info('enter')
pyautogui.press(1)
time.sleep(
'l')
pyautogui.press(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
pyautogui.typewrite(MemoTXT)1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
KEYBOARD AUTOMATION – USING pyautogui
TO AUTOMATE VARIOUS KEYBOARD ACTIONS
pyautogui automations
The pyautogui
library can be used either directly in the script or as part of a larger function, but examples below.
pyautogui
As stand-alone
- pyautogui.press()
- pyautogui.typewrite()
- pyautogui.hotkey()
pyautogui
As part of a function
press_key_multiple_times
press_key_with_modifier_multiple_times
pyautogui.click() > This one is only used once in my script, as part of a function, and then that function is called many times in various ways.
#### `pyautogui` As stand-alone
# pyautogui.typewrite()
str(DBACRE))
pyautogui.typewrite(str(DBAIN))
pyautogui.typewrite(
pyautogui.typewrite(MemoTXT)
pyautogui.typewrite(PNUMBER)'f')
pyautogui.typewrite(f"04/01/{ForYear}")
pyautogui.typewrite(f"{PDESC} FOR TIMBER REVIEW")
pyautogui.typewrite('p')
pyautogui.typewrite(
# pyautogui.hotkey()
'ctrl', 'o')
pyautogui.hotkey('ctrl', 'shift', 'm')
pyautogui.hotkey(
# pyautogui.press()
'tab'])
pyautogui.press(['delete'])
pyautogui.press(['enter')
pyautogui.press('l')
pyautogui.press('space')
pyautogui.press('right')
pyautogui.press(
#### `pyautogui` As part of a function
def press_key_multiple_times(key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.press(key)
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
'up', 12)
press_key_multiple_times('down', 4)
press_key_multiple_times('tab'], 3)
press_key_multiple_times([
def press_key_with_modifier_multiple_times(modifier, key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.hotkey(modifier, key)
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT
'shift', 'tab', 6)
press_key_with_modifier_multiple_times(
# pyautogui.click()
# 2 CLICK USING A REFERENCE GREYSCALE SCREENSHOT TO A STORED GREYSCALE IMAGE INCLUDES ABILITY TO CLICK RELATIVE POSITION
def click_on_image(image_path, direction='center', offset=10, inset=7, confidence=0.8):
= capture_and_convert_screenshot()
grey_screenshot
# Load the reference image in greyscale
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image if ref_image is None:
f"Failed to load reference image from {image_path}")
logging.error(return False
# Perform template matching
= cv2.matchTemplate(grey_screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
_, max_val, _, max_loc
if max_val >= confidence:
= max_loc
top_left = ref_image.shape
h, w = top_left[0] + w
right = top_left[1] + h
bottom
# Calculate click position based on direction and inset/offset
= {
click_positions 'right': (right + offset, top_left[1] + h // 2),
'left': (top_left[0] - offset, top_left[1] + h // 2),
'above': (top_left[0] + w // 2, top_left[1] - offset),
'below': (top_left[0] + w // 2, bottom + offset),
'bottom_right_corner': (right - inset, bottom - inset),
'bottom_left_corner': (top_left[0] + inset, bottom - inset),
'top_right_corner': (right - inset, top_left[1] + inset),
'top_left_corner': (top_left[0] + inset, top_left[1] + inset),
'bottom_center': (top_left[0] + w // 2, bottom - inset),
'top_center': (top_left[0] + w // 2, top_left[1] + inset),
'center': (top_left[0] + w // 2, top_left[1] + h // 2)
}= click_positions[direction]
click_x, click_y
# pyautogui.click()
# Perform the click
pyautogui.click(click_x, click_y)
f"Clicked {direction} of the image at ({click_x}, {click_y})")
logging.info(return True
else:
f"No good match found at the confidence level of {confidence}.")
logging.warning(return False
PUTTING IT ALL TOGETHER INTO A FINAL SCRIPT
EXAMPLE: MappingPacketsAutomation_FINAL.py
The previous example portions were taken from the MappingPacketsAutomation_FINAL.py
script, here is the final script in it’s final form.
import subprocess
import pkg_resources
import sys
def install_if_missing(package):
try:
pkg_resources.get_distribution(package)except pkg_resources.DistributionNotFound:
"-m", "pip", "install", package])
subprocess.check_call([sys.executable,
# List of packages you want to ensure are installed
= [
packages "pyautogui", # For automating GUI interactions
"pyodbc", # For database connections
"numpy", # For numerical operations
"keyboard", # For detecting key presses
"pytesseract", # For OCR (Optical Character Recognition)
"Pillow", # For image processing related to Image
"opencv-python", # For image processing (cv2)
"tkcalendar" # For calendar widget in Tkinter
]
# Apply the install_if_missing function to each package
for package in packages:
install_if_missing(package)
# After ensuring all packages are installed, you can import them as needed in your script
import pyautogui # For automating GUI interactions like mouse movements and clicks
import pyodbc # For establishing database connections and executing SQL queries
import time # For adding delays and managing time-related functions
import numpy as np # For numerical operations and handling arrays/matrices
import keyboard # For detecting and handling keyboard key presses
import threading # For running background tasks and creating concurrent threads
import tkinter as tk # For creating basic GUI elements in Python applications
from tkinter import ttk, messagebox # For advanced Tkinter widgets and displaying dialog boxes
import pytesseract # For OCR (Optical Character Recognition) to read text from images
from PIL import Image # For working with image data
import cv2 # For image processing and computer vision tasks (OpenCV library)
import ctypes # For interacting with C data types and Windows API functions
from tkcalendar import DateEntry # For adding a calendar widget to Tkinter GUIs
import logging # For logging events, errors, and information during script execution
"""
# GLOBAL LOGICS - CONNECTIONS
"""
### Logging
logging.basicConfig(='S:/Common/Comptroller Tech/Reports/Python/Auto_Mapping_Packet/MappingPacketsAutomation.log',
filename=logging.DEBUG,
levelformat='%(asctime)s - %(levelname)s - %(message)s'
)= logging.StreamHandler()
console_handler
console_handler.setLevel(logging.DEBUG)'%(asctime)s - %(levelname)s - %(message)s'))
console_handler.setFormatter(logging.Formatter(
logging.getLogger().addHandler(console_handler)
### Kill Script
# Global flag to indicate if the script should be stopped
= False
stop_script
def monitor_kill_key():
global stop_script
"Kill key monitor started. Press 'esc' to stop the script.")
logging.info('esc') # Set 'esc' as the kill key
keyboard.wait(= True
stop_script "Kill key pressed. Stopping the script...")
logging.info(
# Start the kill key monitoring in a separate thread
= threading.Thread(target=monitor_kill_key)
kill_key_thread = True
kill_key_thread.daemon
kill_key_thread.start()
### Connections: Database
# Configuration for database connection
= (
db_connection_string "Driver={SQL Server};"
"Server=astxdbprod;"
"Database=GRM_Main;"
"Trusted_Connection=yes;"
)
# Function to connect to the database
def connect_to_database(connection_string):
return pyodbc.connect(connection_string)
# Function to execute a SQL query and fetch data
def execute_query(cursor, query):
cursor.execute(query)return cursor.fetchall()
### Graphic User Interface (GUI) Logic - START
# Initialize variables to avoid 'NameError', will call them into the final product after variable selections
= ""
MemoTXT = ""
PDESC
def on_submit():
ensure_capslock_off()
global AINLIST, AINFROM, AINTO, PDESC, PFILE, PNUMBER, TREVIEW, MappingPacketType, Initials, MemoTXT, ForYear
# Collect inputs for AINFROM and AINTO, and split by commas
= [ain.strip() for ain in entry_ainfrom.get().strip().upper().split(",")]
AINFROM = [ain.strip() for ain in entry_ainto.get().strip().upper().split(",")]
AINTO
# Combine the AINFROM and AINTO lists, removing duplicates
= list(set(AINFROM + AINTO))
combined_ain_list
# Replace AINLIST with the combined list
= combined_ain_list
AINLIST
= entry_pfile.get().strip().upper()
PFILE = entry_pnumber.get().strip().upper()
PNUMBER = entry_treview.get().strip().upper()
TREVIEW = combobox_mappingpackettype.get().strip().upper()
MappingPacketType = entry_initials.get().strip().upper()
Initials = for_year_combobox.get().strip() # Get the selected year
ForYear
= datetime.now().month
the_month = datetime.now().day
the_day = datetime.now().year
the_year
#the_month = datetime.datetime.now().month
#the_day = datetime.datetime.now().day
#the_year = datetime.datetime.now().year
= ', '.join(AINFROM)
AINFROM_str = ', '.join(AINTO)
AINTO_str = f"{Initials}-{the_month}/{str(the_year)[-2:]} {MappingPacketType} from {AINFROM_str} into {AINTO_str} for {ForYear}"
MemoTXT f"Generated MemoTXT: {MemoTXT}")
logging.info(
= f"{MappingPacketType} for {ForYear}"
PDESC
if not AINLIST or not PFILE or not PNUMBER or not TREVIEW or not MappingPacketType or not Initials or not MemoTXT or not PDESC:
"Input Error", "All input fields are required.")
messagebox.showerror(return
# Close the GUI
root.destroy()
def setup_gui():
= tk.Tk()
root "User Input Form")
root.title(
setup_widgets(root)return root
def validate_initials(action, value_if_allowed):
# Allow only alphabetic characters and limit to 3 characters
if action == '1': # 1 means an insertion operation
if len(value_if_allowed) > 3:
return False
return value_if_allowed.isalpha()
return True
def setup_widgets(root):
global entry_ainfrom, entry_ainto, entry_pfile, entry_pnumber, entry_treview, combobox_mappingpackettype, entry_initials, for_year_combobox
# Get the current and next year
= datetime.now().year
current_year = current_year + 1
next_year
="Mapping packet FOR what year?:").grid(column=0, row=1, padx=10, pady=5)
ttk.Label(root, text= ttk.Combobox(root, values=[current_year, next_year], width=47)
for_year_combobox =1, row=1, padx=10, pady=5)
for_year_combobox.grid(column0) # Set default selection to the current year
for_year_combobox.current(
# AINFROM input
="List AINs FROM (separated by comma):").grid(column=0, row=0, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_ainfrom =1, row=0, padx=10, pady=5)
entry_ainfrom.grid(column
# AINTO input
="List AINs TO (separated by comma):").grid(column=0, row=1, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_ainto =1, row=1, padx=10, pady=5)
entry_ainto.grid(column
# Existing fields continue below...
="Mapping packet FOR what year?:").grid(column=0, row=2, padx=10, pady=5)
ttk.Label(root, text= ttk.Combobox(root, values=[current_year, next_year], width=47)
for_year_combobox =1, row=2, padx=10, pady=5)
for_year_combobox.grid(column0) # Set default selection to the current year
for_year_combobox.current(
="Filing Date (Top Date):").grid(column=0, row=3, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_pfile =1, row=3, padx=10, pady=5)
entry_pfile.grid(column
="Permit Number (Bottom Date):").grid(column=0, row=4, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_pnumber =1, row=4, padx=10, pady=5)
entry_pnumber.grid(column
="Timber or AG review? Y/N:").grid(column=0, row=5, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_treview =1, row=5, padx=10, pady=5)
entry_treview.grid(column
="Select Mapping Packet Type:").grid(column=0, row=6, padx=10, pady=5)
ttk.Label(root, text
= [
mapping_packet_types "MERGE", "SPLIT", "BLA", "LLA", "RW VACATION", "RW SPLIT", "REDESCRIBE",
"RW AUDIT", "RW Cat19", "AIRPORT LEASE NEW PARCEL", "PLAT VACATION",
"PARCEL DELETED", "ACERAGE AUDIT", "NEW PLAT"
]= ttk.Combobox(root, values=mapping_packet_types, width=47)
combobox_mappingpackettype =1, row=6, padx=10, pady=5)
combobox_mappingpackettype.grid(column0) # Set default selection to the first item
combobox_mappingpackettype.current(
# Validation for the Initials Entry
= (root.register(validate_initials), '%d', '%P')
vcmd
="Enter (3) Initials:").grid(column=0, row=7, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50, validate='key', validatecommand=vcmd)
entry_initials =1, row=7, padx=10, pady=5)
entry_initials.grid(column0, "DGW")
entry_initials.insert(
= ttk.Button(root, text="Submit", command=on_submit)
submit_button =0, row=8, columnspan=2, pady=20)
submit_button.grid(column
= setup_gui()
root
### Graphic User Interface (GUI) Logic - END
"""
# GLOBAL LOGICS - LOGIC FUNCTIONS
"""
#### CAPS LOCK
def is_capslock_on():
# This will return 1 if CAPS LOCK is on, 0 if it's off
= ctypes.WinDLL("User32.dll")
hllDll = 0x14
VK_CAPITAL return hllDll.GetKeyState(VK_CAPITAL) & 1
def ensure_capslock_off():
if is_capslock_on():
'capslock')
pyautogui.press("CAPS LOCK was on. It has been turned off.")
logging.info(else:
"CAPS LOCK is already off.")
logging.info(
#### SET FOCUS
def set_focus_and_type(window_title, keys):
= pyautogui.getWindowsWithTitle(window_title)
window if window:
0].activate()
window[
pyautogui.typewrite(keys)
#### PRESS & CLICK KEY LOGIC
def press_key_with_modifier_multiple_times(modifier, key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.hotkey(modifier, key)
def press_key_multiple_times(key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.press(key)
def triple_click(x, y):
=3)
pyautogui.click(x, y, clicks
"""
# GLOBAL LOGICS - SCREEN HANDLING FUNCTIONS
"""
### Connections: OCR and Image Paths
#### OCR
# This OCR program is required to work with this script, it is available on GitHub
# Set the tesseract executable path if not in the system path
# Update this path as necessary by user you will need to download and install tesseract from GitHub
# Link https://github.com/tesseract-ocr/tesseract
# Link https://github.com/UB-Mannheim/tesseract/wiki
= r'C:\Users\dwolfe\AppData\Local\Programs\Tesseract-OCR\tesseract.exe'
pytesseract.pytesseract.tesseract_cmd
#### Image Paths - Active and Inactive
= [
land_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_tab_active.PNG'
]= [
land_base_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_base_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_base_tab_active.PNG'
]= [
permits_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_tab_active.PNG'
]
= [
permits_add_permit_button r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_permit_button.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_permit_button_active.PNG'
]
#### Image Paths - Single Images Only
= r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_memo_duplicate.PNG'
duplicate_memo_image_path = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_fieldvisit_button.PNG'
add_field_visit_image_path = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_aggregate_land_type_add_button.PNG'
aggregate_land_type_add_button = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_farm_total_acres.PNG'
farm_total_acres_image = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permit_description.PNG'
permit_description
# 1 CAPTURE SCREEN IN GREYSCALE
def capture_and_convert_screenshot():
# Capture the screenshot using pyautogui
= pyautogui.screenshot()
screenshot
# Convert the screenshot to a numpy array, then to BGR, and finally to greyscale
= np.array(screenshot)
screenshot_np = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
screenshot_np = cv2.cvtColor(screenshot_np, cv2.COLOR_BGR2GRAY)
grey_screenshot
return grey_screenshot
# 2 CLICK USING A REFERENCE GREYSCALE SCREENSHOT TO A STORED GREYSCALE IMAGE INCLUDES ABILITY TO CLICK RELATIVE POSITION
def click_on_image(image_path, direction='center', offset=10, inset=7, confidence=0.8):
= capture_and_convert_screenshot()
grey_screenshot
# Load the reference image in greyscale
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image if ref_image is None:
f"Failed to load reference image from {image_path}")
logging.error(return False
# Perform template matching
= cv2.matchTemplate(grey_screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
_, max_val, _, max_loc
if max_val >= confidence:
= max_loc
top_left = ref_image.shape
h, w = top_left[0] + w
right = top_left[1] + h
bottom
# Calculate click position based on direction and inset/offset
= {
click_positions 'right': (right + offset, top_left[1] + h // 2),
'left': (top_left[0] - offset, top_left[1] + h // 2),
'above': (top_left[0] + w // 2, top_left[1] - offset),
'below': (top_left[0] + w // 2, bottom + offset),
'bottom_right_corner': (right - inset, bottom - inset),
'bottom_left_corner': (top_left[0] + inset, bottom - inset),
'top_right_corner': (right - inset, top_left[1] + inset),
'top_left_corner': (top_left[0] + inset, top_left[1] + inset),
'bottom_center': (top_left[0] + w // 2, bottom - inset),
'top_center': (top_left[0] + w // 2, top_left[1] + inset),
'center': (top_left[0] + w // 2, top_left[1] + h // 2)
}= click_positions[direction]
click_x, click_y
# Perform the click
pyautogui.click(click_x, click_y)f"Clicked {direction} of the image at ({click_x}, {click_y})")
logging.info(return True
else:
f"No good match found at the confidence level of {confidence}.")
logging.warning(return False
"""
# Ref Function click_on_image in the following functions 3, 4, 5, etc...
# These reference functions can be called in the final automation script
"""
# 3 USING click_on_image FUNCTION
#Specific Click Functions Here, See click_on_image for directionals, and image pathes for images
def click_images_multiple(paths, direction='center', offset=50, inset=7, confidence=0.75):
for image_path in paths:
f"Trying to click {direction} on image: {image_path}")
logging.info(if click_on_image(image_path, direction=direction, offset=offset, inset=inset, confidence=confidence):
f"Successfully clicked {direction} of {image_path}.")
logging.info(return True
else:
f"Failed to click {direction} of {image_path}.")
logging.warning(return False
def click_image_single(image_path, direction='center', offset=50, inset=7, confidence=0.75):
f"Trying to click {direction} on image: {image_path}")
logging.info(if click_on_image(image_path, direction=direction, offset=offset, inset=inset, confidence=confidence):
f"Successfully clicked {direction} of {image_path}.")
logging.info(return True
else:
f"Failed to click {direction} of {image_path}.")
logging.warning(return False
"""
# How to use these click_images_multiple & click_image_single functions in script
# Click below all specified images
if click_images_multiple(multiple_image_path_name_here, direction='below', offset=100, confidence=0.8):
logging.info("Clicked successfully.")
# Click at the center of a single image
if click_image_single(single_image_path_name_here, direction='center', confidence=0.8):
logging.info("Clicked successfully.")
# Click at the bottom right corner of a single image
if click_image_single(single_image_path_name_here, direction='bottom_right_corner', inset=10, confidence=0.8):
logging.info("Clicked successfully.")
# Click to right of permit_description, by calling offset=5 it was just barely below the image, which is what I wanted
if click_image_single(permit_description, direction='below', offset=5, confidence=0.8):
logging.info("Clicked successfully permit_description.")
time.sleep(1)
"""
# 4 CHECKING IF IMAGE IS PRESENT
def is_image_found(image_path, confidence=0.8):
"""
Check if an image is present on the screen with a specified confidence level.
:param image_path: Path to the image file to be checked.
:param confidence: The confidence level for the image matching.
:return: bool - True if image is found, False otherwise.
"""
# Use the existing function to capture and convert the screenshot
= capture_and_convert_screenshot()
grey_screenshot
# Load the reference image in greyscale
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image if ref_image is None:
f"Failed to load reference image from {image_path}")
logging.error(return False
# Perform template matching
= cv2.matchTemplate(grey_screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
_, max_val, _, _
= max_val >= confidence
found if found:
f"Image found with confidence {max_val}: {image_path}")
logging.info(else:
f"Image not found with sufficient confidence {confidence}: {image_path}")
logging.info(
return found
"""
# How to use the is_image_found function below in script:
# Check if the image is found and decide based on that
if is_image_found(image_path_name_here, confidence=0.8):
logging.info("Image was found - executing related tasks.")
# Perform tasks related to the image being found
else:
logging.info("Image was not found - executing alternative tasks.")
# Perform alternative tasks
"""
# 5 READ TEXT FROM SCREEN
def check_for_text_on_screen(target_text):
"""
Captures the screen, converts it to greyscale, performs OCR, and checks for the specified text.
:param target_text: Text to search for in the OCR results.
:return: True if the text is found, False otherwise.
"""
= capture_and_convert_screenshot()
grey_screenshot = Image.fromarray(grey_screenshot) # Convert numpy array back to a PIL Image
grey_screenshot_pil = pytesseract.image_to_string(grey_screenshot_pil)
screen_text return target_text in screen_text
"""
# How to use the check_for_text_on_screen function below in script:
# Define the specific text you're looking for
specific_text = "text_you_want_to_check_here"
# Use the variable in your function call and print statements
if check_for_text_on_screen(specific_text):
logging.info(f"Found '{specific_text}' on the screen.")
else:
logging.info(f"Did not find '{specific_text}' on the screen.")
"""
"""
# Start the GUI event loop
"""
root.mainloop()
if not AINLIST or not MemoTXT or not PDESC or not PFILE or not PNUMBER or not TREVIEW:
"All input fields are required.")
logging.error(
exit()
"""
Connect to the database,
pull a simple SQL query with two columns,
then the for row in rows assigns those columns as variables
"""
= connect_to_database(db_connection_string)
conn = conn.cursor()
cursor
# The query should accommodate multiple AINs in a list
= f"SELECT TRIM(pm.AIN), pm.LegalAcres FROM TSBv_Parcelmaster AS pm WHERE pm.AIN IN ({','.join(AINLIST)})"
query = execute_query(cursor, query)
rows
# Iterate through each row in the results
for row in rows:
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
= row
DBAIN, DBACRE
ensure_capslock_off()
# Process each AIN individually
'ProVal', DBAIN)
set_focus_and_type(1)
time.sleep("set_focus_and_type")
logging.info(
"""
Officially begins the automation and screen navigation
"""
# Process: Open an AIN in ProVal
'ProVal', DBAIN)
set_focus_and_type(1)
time.sleep("set_focus_and_type")
logging.info(
'ctrl', 'o')
pyautogui.hotkey(1)
time.sleep("hotkey")
logging.info(
'up', 12)
press_key_multiple_times("press_key_multiple_times")
logging.info(
'down', 4)
press_key_multiple_times("press_key_multiple_times")
logging.info(
'tab'])
pyautogui.press(["press")
logging.info(
'delete'])
pyautogui.press(["press")
logging.info(
str(DBAIN))
pyautogui.typewrite(f"Sent AIN {DBAIN}.")
logging.info(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep("xxxxx")
logging.info(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
"""
## NOW BEGIN AUTOMATION STEPS FOR THIS TOOL
"""
# Process: Open Memos
'ctrl', 'shift', 'm')
pyautogui.hotkey(1)
time.sleep(
# Process: Enter Land Memos
# CHECK IF LAND
# Define the specific text you're looking for
= "LAND"
specific_text # Use the variable in your function call and print statements
if check_for_text_on_screen(specific_text):
f"Found '{specific_text}' on the screen.")
logging.info(
'l')
pyautogui.press('enter')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
pyautogui.typewrite(MemoTXT)1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(else:
f"Did not find '{specific_text}' on the screen.")
logging.info('enter')
pyautogui.press(1)
time.sleep(
'l')
pyautogui.press(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
pyautogui.typewrite(MemoTXT)1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Process: Enter Land Farm Acres
# Click Land_Tab
if click_images_multiple(land_tab_images, direction='center', offset=100, confidence=0.8):
"Clicked successfully land_tab_images.")
logging.info(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Click Land_Base_Tab
if click_images_multiple(land_base_tab_images, direction='center', confidence=0.8):
"Clicked successfully land_base_tab_images.")
logging.info(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Check if the image is found and decide based on that
if is_image_found(farm_total_acres_image, confidence=0.8):
"Image was found - executing related tasks.")
logging.info(# Perform tasks related to the image being found
# Click to the right of Farm Acres
if click_image_single(farm_total_acres_image, direction='right', offset=15, confidence=0.8):
"Clicked successfully farm_total_acres_image.")
logging.info(1)
time.sleep(
# Delete the contents and send DBACRE
'delete')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
str(DBACRE))
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
else:
"farm_total_acres_image Image was not found - executing alternative tasks.")
logging.info(# Perform alternative tasks
# Click to the right of Farm Acres
if click_image_single(aggregate_land_type_add_button, direction='bottom_right_corner', inset=10, confidence=0.8):
"Clicked successfully aggregate_land_type_add_button.")
logging.info(1)
time.sleep(
# Send the DBACRE value after clicking the fallback button
'f')
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
ensure_capslock_off()
str(DBACRE))
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Process: Enter Permit 1/2
# Click Permits_Tab
if click_images_multiple(permits_tab_images, direction='center', inset=10, confidence=0.8):
"Clicked successfully permits_tab_images.")
logging.info(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Click Permits_Add_Button
if click_images_multiple(permits_add_permit_button, direction='center', offset=100, confidence=0.8):
"Clicked successfully permits_add_permit_button.")
logging.info(1)
time.sleep(
ensure_capslock_off()
# Send Permit Number
pyautogui.typewrite(PNUMBER)1)
time.sleep(
'tab'])
pyautogui.press([1)
time.sleep(
#Different down to Timber 2 vs Mandatory 11.
'down', 11)
press_key_multiple_times(1)
time.sleep(
'tab'], 3)
press_key_multiple_times([1)
time.sleep(
ensure_capslock_off()
# Send Permit Filing Date
pyautogui.typewrite(PFILE)1)
time.sleep(
'tab'], 3)
press_key_multiple_times([1)
time.sleep(
# Close Add Permit Pop-Up Box
'space')
pyautogui.press("Closing Add Permit pop-up, then waiting to send description")
logging.info(3)
time.sleep(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Click to right of permit_description
if click_image_single(permit_description, direction='below', offset=5, confidence=0.8):
"Clicked successfully permit_description.")
logging.info(1)
time.sleep(
ensure_capslock_off()
# Send Permit Description
pyautogui.typewrite(PDESC)1)
time.sleep("Send description")
logging.info(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Process: Enter Permit 2/2
# Click FieldVisit_Add_Button
if click_image_single(add_field_visit_image_path, direction='center', inset=10, confidence=0.8):
"Clicked successfully add_field_visit_image_path.")
logging.info(
# If found, complete adding Field Visit process
'shift', 'tab', 6)
press_key_with_modifier_multiple_times(1)
time.sleep(
'space')
pyautogui.press(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'p')
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'space')
pyautogui.press(1)
time.sleep(
'right')
pyautogui.press(1)
time.sleep(
# Permit Due Date
f"04/01/{ForYear}")
pyautogui.typewrite(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Process: CHECK FOR TIMBER
# Timber Review Logic
if TREVIEW in ["Yes", "YES", "Y", "y"]:
"Timber YES.")
logging.info("""
# Send Appraiser Permit for TIMBER
"""
# Same as permit process except for two changes. Different down to Timber vs Mandatory. Add to Permit Description.
# Process: Enter Permit 1/2
# Click Permits_Tab
if click_images_multiple(permits_tab_images, direction='center', inset=10, confidence=0.8):
"Clicked successfully permits_tab_images.")
logging.info(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Click Permits_Add_Button
if click_images_multiple(permits_add_permit_button, direction='center', offset=100, confidence=0.8):
"Clicked successfully permits_add_permit_button.")
logging.info(1)
time.sleep(
ensure_capslock_off()
# Send Permit Number
pyautogui.typewrite(PNUMBER)1)
time.sleep(
'tab'])
pyautogui.press([1)
time.sleep(
#Different down to Timber 2 vs Mandatory 11.
'down', 2)
press_key_multiple_times(1)
time.sleep(
'tab'], 3)
press_key_multiple_times([1)
time.sleep(
ensure_capslock_off()
# Send Permit Filing Date
pyautogui.typewrite(PFILE)1)
time.sleep(
'tab'], 3)
press_key_multiple_times([1)
time.sleep(
# Close Add Permit Pop-Up Box
'space')
pyautogui.press("Closing Add Permit pop-up, then waiting to send description")
logging.info(3)
time.sleep(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Click to right of permit_description
if click_image_single(permit_description, direction='below', offset=5, confidence=0.8):
"Clicked successfully permit_description.")
logging.info(1)
time.sleep(
ensure_capslock_off()
# Send Permit Description -- Add to Permit Description.
f"{PDESC} FOR TIMBER REVIEW")
pyautogui.typewrite(1)
time.sleep("Send description")
logging.info(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Process: Enter Permit 2/2
# Click FieldVisit_Add_Button
if click_image_single(add_field_visit_image_path, direction='center', inset=10, confidence=0.8):
"Clicked successfully add_field_visit_image_path.")
logging.info(
# If found, complete adding Field Visit process
'shift', 'tab', 6)
press_key_with_modifier_multiple_times(1)
time.sleep(
'space')
pyautogui.press(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'p')
pyautogui.typewrite(1)
time.sleep(
'tab')
pyautogui.press(1)
time.sleep(
'space')
pyautogui.press(1)
time.sleep(
'right')
pyautogui.press(1)
time.sleep(
# Permit Due Date
f"04/01/{ForYear}")
pyautogui.typewrite(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
else:
"Timber review not required, skipping this step.")
logging.info(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# Save Account
'ctrl', 's')
pyautogui.hotkey("Save.")
logging.info(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# END ALL PROCESSESS
"THE END...")
logging.info(1)
time.sleep(
# Close the database connection
conn.close()"Database Connection Closed")
logging.info("""
Notes:
if AIN:
press_key_multiple_times('down', 4)
elif PIN:
press_key_multiple_times('down', 0)
## For testing purposes,
# PIN: KC-DGW = AIN: 345134
# PIN: KC-DAS = AIN: 348586
345134,348586
DGW-08/24 SEG ...
SEG PACKET
01/01/2024
01/01/2025
### LEFT OFF HERE NEEDS MORE DIRECTION
time.sleep(1000)
# Version History
# NEW v1 07/26/2024
# 07/30/2024, I give up for now, I'll circle back to this when I'm done training Pat.
# 08/07/2024, successfully used the OCR to read and nagivate several screens in ProVal.
# One huge key was to shift the screenshot reading to Greyscale and then actually change the stored images to Greyscale as well.
#08/13/2024, updated to include the capacity to click around images found, to the left of, right of, bottom corner of, etc.
# Added multiple functionalities
# Possibly ready for testing?
# Added logging and tweaks to inputs
# After running through one live mapping packet, realized the MemoTXT was including brackets and literals
# Updated to remove these and make the MemoTXT clean
# Should be ready to test on another nmapping packet tomorrow.
#08/20/2024, entirely new v3, streamlined, simplified, built in re-usable blocks for future templates
# After much review and many ChatGPT conversations and many tests... v3 is ready to be used as a template for other automations
# Next ...
# Write documenation and instruction manual
# Save Template Version
# Write a Plat version
"""
TEMPLATE - AUTOMATION TEMPLATE
The following template can be used to auomate your tasks.
# NOTE_StartHERE: Copy, Paste, Rename before making changes to this template
# Generally, you should be able to skip over most functions and start building from START_HERE below
# Possibly, you may need to take new images and store those image paths, make sure to greyscale the final image on the shared image location
"""
import subprocess
import pkg_resources
import sys
def install_if_missing(package):
try:
pkg_resources.get_distribution(package)
except pkg_resources.DistributionNotFound:
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
# List of packages you want to ensure are installed
packages = [
"pyautogui", # For automating GUI interactions
"pyodbc", # For database connections
"numpy", # For numerical operations
"keyboard", # For detecting key presses
"pytesseract", # For OCR (Optical Character Recognition)
"Pillow", # For image processing related to Image
"opencv-python", # For image processing (cv2)
"tkcalendar" # For calendar widget in Tkinter
]
# Apply the install_if_missing function to each package
for package in packages:
install_if_missing(package)
"""
# After ensuring all packages are installed, you can import them as needed in your script
import pyautogui # For automating GUI interactions like mouse movements and clicks
import pyodbc # For establishing database connections and executing SQL queries
import time # For adding delays and managing time-related functions
import numpy as np # For numerical operations and handling arrays/matrices
import keyboard # For detecting and handling keyboard key presses
import threading # For running background tasks and creating concurrent threads
import tkinter as tk # For creating basic GUI elements in Python applications
from tkinter import ttk, messagebox # For advanced Tkinter widgets and displaying dialog boxes
import pytesseract # For OCR (Optical Character Recognition) to read text from images
from PIL import Image # For working with image data
import cv2 # For image processing and computer vision tasks (OpenCV library)
import ctypes # For interacting with C data types and Windows API functions
from tkcalendar import DateEntry # For adding a calendar widget to Tkinter GUIs
import logging # For logging events, errors, and information during script execution
"""
# GLOBAL LOGICS - CONNECTIONS
"""
### Logging
logging.basicConfig(='S:/Common/Comptroller Tech/Reports/Python/Auto_Mapping_Packet/MappingPacketsAutomation.log',
filename=logging.DEBUG,
levelformat='%(asctime)s - %(levelname)s - %(message)s'
)= logging.StreamHandler()
console_handler
console_handler.setLevel(logging.DEBUG)'%(asctime)s - %(levelname)s - %(message)s'))
console_handler.setFormatter(logging.Formatter(
logging.getLogger().addHandler(console_handler)
### Kill Script
# Global flag to indicate if the script should be stopped
= False
stop_script
def monitor_kill_key():
global stop_script
"Kill key monitor started. Press 'esc' to stop the script.")
logging.info('esc') # Set 'esc' as the kill key
keyboard.wait(= True
stop_script "Kill key pressed. Stopping the script...")
logging.info(
# Start the kill key monitoring in a separate thread
= threading.Thread(target=monitor_kill_key)
kill_key_thread = True
kill_key_thread.daemon
kill_key_thread.start()
### Connections: Database
# Configuration for database connection
= (
db_connection_string "Driver={SQL Server};"
"Server=astxdbprod;"
"Database=GRM_Main;"
"Trusted_Connection=yes;"
)
# Function to connect to the database
def connect_to_database(connection_string):
return pyodbc.connect(connection_string)
# Function to execute a SQL query and fetch data
def execute_query(cursor, query):
cursor.execute(query)return cursor.fetchall()
# VARIABLES MAY BE UPDATED HERE
### Graphic User Interface (GUI) Logic - START
# Initialize variables to avoid 'NameError', will call them into the final product after variable selections
= ""
MemoTXT = ""
PDESC
def on_submit():
ensure_capslock_off()
global AINLIST, AINFROM, AINTO, PDESC, PFILE, PNUMBER, TREVIEW, MappingPacketType, Initials, MemoTXT, ForYear
# Collect inputs for AINFROM and AINTO, and split by commas
= [ain.strip() for ain in entry_ainfrom.get().strip().upper().split(",")]
AINFROM = [ain.strip() for ain in entry_ainto.get().strip().upper().split(",")]
AINTO
# Combine the AINFROM and AINTO lists, removing duplicates
= list(set(AINFROM + AINTO))
combined_ain_list
# Replace AINLIST with the combined list
= combined_ain_list
AINLIST
= entry_pfile.get().strip().upper()
PFILE = entry_pnumber.get().strip().upper()
PNUMBER = entry_treview.get().strip().upper()
TREVIEW = combobox_mappingpackettype.get().strip().upper()
MappingPacketType = entry_initials.get().strip().upper()
Initials = for_year_combobox.get().strip() # Get the selected year
ForYear
= datetime.now().month
the_month = datetime.now().day
the_day = datetime.now().year
the_year
#the_month = datetime.datetime.now().month
#the_day = datetime.datetime.now().day
#the_year = datetime.datetime.now().year
= ', '.join(AINFROM)
AINFROM_str = ', '.join(AINTO)
AINTO_str = f"{Initials}-{the_month}/{str(the_year)[-2:]} {MappingPacketType} from {AINFROM_str} into {AINTO_str} for {ForYear}"
MemoTXT f"Generated MemoTXT: {MemoTXT}")
logging.info(
= f"{MappingPacketType} for {ForYear}"
PDESC
if not AINLIST or not PFILE or not PNUMBER or not TREVIEW or not MappingPacketType or not Initials or not MemoTXT or not PDESC:
"Input Error", "All input fields are required.")
messagebox.showerror(return
# Close the GUI
root.destroy()
def setup_gui():
= tk.Tk()
root "User Input Form")
root.title(
setup_widgets(root)return root
def validate_initials(action, value_if_allowed):
# Allow only alphabetic characters and limit to 3 characters
if action == '1': # 1 means an insertion operation
if len(value_if_allowed) > 3:
return False
return value_if_allowed.isalpha()
return True
def setup_widgets(root):
global entry_ainfrom, entry_ainto, entry_pfile, entry_pnumber, entry_treview, combobox_mappingpackettype, entry_initials, for_year_combobox
# Get the current and next year
= datetime.now().year
current_year = current_year + 1
next_year
="Mapping packet FOR what year?:").grid(column=0, row=1, padx=10, pady=5)
ttk.Label(root, text= ttk.Combobox(root, values=[current_year, next_year], width=47)
for_year_combobox =1, row=1, padx=10, pady=5)
for_year_combobox.grid(column0) # Set default selection to the current year
for_year_combobox.current(
# AINFROM input
="List AINs FROM (separated by comma):").grid(column=0, row=0, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_ainfrom =1, row=0, padx=10, pady=5)
entry_ainfrom.grid(column
# AINTO input
="List AINs TO (separated by comma):").grid(column=0, row=1, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_ainto =1, row=1, padx=10, pady=5)
entry_ainto.grid(column
# Existing fields continue below...
="Mapping packet FOR what year?:").grid(column=0, row=2, padx=10, pady=5)
ttk.Label(root, text= ttk.Combobox(root, values=[current_year, next_year], width=47)
for_year_combobox =1, row=2, padx=10, pady=5)
for_year_combobox.grid(column0) # Set default selection to the current year
for_year_combobox.current(
="Filing Date (Top Date):").grid(column=0, row=3, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_pfile =1, row=3, padx=10, pady=5)
entry_pfile.grid(column
="Permit Number (Bottom Date):").grid(column=0, row=4, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_pnumber =1, row=4, padx=10, pady=5)
entry_pnumber.grid(column
="Timber or AG review? Y/N:").grid(column=0, row=5, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50)
entry_treview =1, row=5, padx=10, pady=5)
entry_treview.grid(column
="Select Mapping Packet Type:").grid(column=0, row=6, padx=10, pady=5)
ttk.Label(root, text
= [
mapping_packet_types "MERGE", "SPLIT", "BLA", "LLA", "RW VACATION", "RW SPLIT", "REDESCRIBE",
"RW AUDIT", "RW Cat19", "AIRPORT LEASE NEW PARCEL", "PLAT VACATION",
"PARCEL DELETED", "ACERAGE AUDIT", "NEW PLAT"
]= ttk.Combobox(root, values=mapping_packet_types, width=47)
combobox_mappingpackettype =1, row=6, padx=10, pady=5)
combobox_mappingpackettype.grid(column0) # Set default selection to the first item
combobox_mappingpackettype.current(
# Validation for the Initials Entry
= (root.register(validate_initials), '%d', '%P')
vcmd
="Enter (3) Initials:").grid(column=0, row=7, padx=10, pady=5)
ttk.Label(root, text= ttk.Entry(root, width=50, validate='key', validatecommand=vcmd)
entry_initials =1, row=7, padx=10, pady=5)
entry_initials.grid(column0, "DGW")
entry_initials.insert(
= ttk.Button(root, text="Submit", command=on_submit)
submit_button =0, row=8, columnspan=2, pady=20)
submit_button.grid(column
= setup_gui()
root
### Graphic User Interface (GUI) Logic - END
"""
# GLOBAL LOGICS - LOGIC FUNCTIONS
"""
#### CAPS LOCK
def is_capslock_on():
# This will return 1 if CAPS LOCK is on, 0 if it's off
= ctypes.WinDLL("User32.dll")
hllDll = 0x14
VK_CAPITAL return hllDll.GetKeyState(VK_CAPITAL) & 1
def ensure_capslock_off():
if is_capslock_on():
'capslock')
pyautogui.press("CAPS LOCK was on. It has been turned off.")
logging.info(else:
"CAPS LOCK is already off.")
logging.info(
#### SET FOCUS
def set_focus_and_type(window_title, keys):
= pyautogui.getWindowsWithTitle(window_title)
window if window:
0].activate()
window[
pyautogui.typewrite(keys)
#### PRESS & CLICK KEY LOGIC
def press_key_with_modifier_multiple_times(modifier, key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.hotkey(modifier, key)
def press_key_multiple_times(key, times):
for _ in range(times):
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
pyautogui.press(key)
"""
# GLOBAL LOGICS - SCREEN HANDLING FUNCTIONS
"""
### Connections: OCR and Image Paths
#### OCR
# This OCR program is required to work with this script, it is available on GitHub
# Set the tesseract executable path if not in the system path
# Update this path as necessary by user you will need to download and install tesseract from GitHub
# Link https://github.com/tesseract-ocr/tesseract
# Link https://github.com/UB-Mannheim/tesseract/wiki
= r'C:\Users\dwolfe\AppData\Local\Programs\Tesseract-OCR\tesseract.exe'
pytesseract.pytesseract.tesseract_cmd
# IMAGES MAY BE UPDATED HERE
#### Image Paths - Active and Inactive
= [
land_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_tab_active.PNG'
]= [
land_base_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_base_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_land_base_tab_active.PNG'
]= [
permits_tab_images r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_tab.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_tab_active.PNG'
]
= [
permits_add_permit_button r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_permit_button.PNG',
r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_permit_button_active.PNG'
]
#### Image Paths - Single Images Only
= r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_memo_duplicate.PNG'
duplicate_memo_image_path = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permits_add_fieldvisit_button.PNG'
add_field_visit_image_path = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_aggregate_land_type_add_button.PNG'
aggregate_land_type_add_button = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_farm_total_acres.PNG'
farm_total_acres_image = r'S:\Common\Comptroller Tech\Reports\Python\py_images\Proval_permit_description.PNG'
permit_description
# 1 CAPTURE SCREEN IN GREYSCALE
def capture_and_convert_screenshot():
# Capture the screenshot using pyautogui
= pyautogui.screenshot()
screenshot
# Convert the screenshot to a numpy array, then to BGR, and finally to greyscale
= np.array(screenshot)
screenshot_np = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
screenshot_np = cv2.cvtColor(screenshot_np, cv2.COLOR_BGR2GRAY)
grey_screenshot
return grey_screenshot
# 2 CLICK USING A REFERENCE GREYSCALE SCREENSHOT TO A STORED GREYSCALE IMAGE INCLUDES ABILITY TO CLICK RELATIVE POSITION
def click_on_image(image_path, direction='center', offset=10, inset=7, confidence=0.8):
= capture_and_convert_screenshot()
grey_screenshot
# Load the reference image in greyscale
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image if ref_image is None:
f"Failed to load reference image from {image_path}")
logging.error(return False
# Perform template matching
= cv2.matchTemplate(grey_screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
_, max_val, _, max_loc
if max_val >= confidence:
= max_loc
top_left = ref_image.shape
h, w = top_left[0] + w
right = top_left[1] + h
bottom
# Calculate click position based on direction and inset/offset
= {
click_positions 'right': (right + offset, top_left[1] + h // 2),
'left': (top_left[0] - offset, top_left[1] + h // 2),
'above': (top_left[0] + w // 2, top_left[1] - offset),
'below': (top_left[0] + w // 2, bottom + offset),
'bottom_right_corner': (right - inset, bottom - inset),
'bottom_left_corner': (top_left[0] + inset, bottom - inset),
'top_right_corner': (right - inset, top_left[1] + inset),
'top_left_corner': (top_left[0] + inset, top_left[1] + inset),
'bottom_center': (top_left[0] + w // 2, bottom - inset),
'top_center': (top_left[0] + w // 2, top_left[1] + inset),
'center': (top_left[0] + w // 2, top_left[1] + h // 2)
}= click_positions[direction]
click_x, click_y
# Perform the click
pyautogui.click(click_x, click_y)f"Clicked {direction} of the image at ({click_x}, {click_y})")
logging.info(return True
else:
f"No good match found at the confidence level of {confidence}.")
logging.warning(return False
"""
# Ref Function click_on_image in the following functions 3, 4, 5, etc...
# These reference functions can be called in the final automation script
"""
# 3 USING click_on_image FUNCTION
#Specific Click Functions Here, See click_on_image for directionals, and image pathes for images
def click_images_multiple(paths, direction='center', offset=50, inset=7, confidence=0.75):
for image_path in paths:
f"Trying to click {direction} on image: {image_path}")
logging.info(if click_on_image(image_path, direction=direction, offset=offset, inset=inset, confidence=confidence):
f"Successfully clicked {direction} of {image_path}.")
logging.info(return True
else:
f"Failed to click {direction} of {image_path}.")
logging.warning(return False
def click_image_single(image_path, direction='center', offset=50, inset=7, confidence=0.75):
f"Trying to click {direction} on image: {image_path}")
logging.info(if click_on_image(image_path, direction=direction, offset=offset, inset=inset, confidence=confidence):
f"Successfully clicked {direction} of {image_path}.")
logging.info(return True
else:
f"Failed to click {direction} of {image_path}.")
logging.warning(return False
"""
# How to use these click_images_multiple & click_image_single functions in script
# Click below all specified images
if click_images_multiple(multiple_image_path_name_here, direction='below', offset=100, confidence=0.8):
logging.info("Clicked successfully.")
# Click at the center of a single image
if click_image_single(single_image_path_name_here, direction='center', confidence=0.8):
logging.info("Clicked successfully.")
# Click at the bottom right corner of a single image
if click_image_single(single_image_path_name_here, direction='bottom_right_corner', inset=10, confidence=0.8):
logging.info("Clicked successfully.")
# Click to right of permit_description, by calling offset=5 it was just barely below the image, which is what I wanted
if click_image_single(permit_description, direction='below', offset=5, confidence=0.8):
logging.info("Clicked successfully permit_description.")
time.sleep(1)
"""
# 4 CHECKING IF IMAGE IS PRESENT
def is_image_found(image_path, confidence=0.8):
"""
Check if an image is present on the screen with a specified confidence level.
:param image_path: Path to the image file to be checked.
:param confidence: The confidence level for the image matching.
:return: bool - True if image is found, False otherwise.
"""
# Use the existing function to capture and convert the screenshot
= capture_and_convert_screenshot()
grey_screenshot
# Load the reference image in greyscale
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image if ref_image is None:
f"Failed to load reference image from {image_path}")
logging.error(return False
# Perform template matching
= cv2.matchTemplate(grey_screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
_, max_val, _, _
= max_val >= confidence
found if found:
f"Image found with confidence {max_val}: {image_path}")
logging.info(else:
f"Image not found with sufficient confidence {confidence}: {image_path}")
logging.info(
return found
"""
# How to use the is_image_found function below in script:
# Check if the image is found and decide based on that
if is_image_found(image_path_name_here, confidence=0.8):
logging.info("Image was found - executing related tasks.")
# Perform tasks related to the image being found
else:
logging.info("Image was not found - executing alternative tasks.")
# Perform alternative tasks
"""
# 5 READ TEXT FROM SCREEN
def check_for_text_on_screen(target_text):
"""
Captures the screen, converts it to greyscale, performs OCR, and checks for the specified text.
:param target_text: Text to search for in the OCR results.
:return: True if the text is found, False otherwise.
"""
= capture_and_convert_screenshot()
grey_screenshot = Image.fromarray(grey_screenshot) # Convert numpy array back to a PIL Image
grey_screenshot_pil = pytesseract.image_to_string(grey_screenshot_pil)
screen_text return target_text in screen_text
"""
# How to use the check_for_text_on_screen function below in script:
# Define the specific text you're looking for
specific_text = "text_you_want_to_check_here"
# Use the variable in your function call and print statements
if check_for_text_on_screen(specific_text):
logging.info(f"Found '{specific_text}' on the screen.")
else:
logging.info(f"Did not find '{specific_text}' on the screen.")
"""
"""
# Start the GUI event loop
"""
root.mainloop()
if not AINLIST or not MemoTXT or not PDESC or not PFILE or not PNUMBER or not TREVIEW:
"All input fields are required.")
logging.error(
exit()
"""
Connect to the database,
pull a simple SQL query with two columns,
then the for row in rows assigns those columns as variables
"""
= connect_to_database(db_connection_string)
conn = conn.cursor()
cursor
# The query should accommodate multiple AINs in a list
= f"SELECT TRIM(pm.AIN), pm.LegalAcres FROM TSBv_Parcelmaster AS pm WHERE pm.AIN IN ({','.join(AINLIST)})"
query = execute_query(cursor, query)
rows
# Iterate through each row in the results
for row in rows:
if stop_script:
"Script stopping due to kill key press.")
logging.info(break
= row
DBAIN, DBACRE
ensure_capslock_off()
# Process each AIN individually
'ProVal', DBAIN)
set_focus_and_type(1)
time.sleep("set_focus_and_type")
logging.info(
"""
Officially begins the automation and screen navigation
"""
# Process: Open an AIN in ProVal
'ProVal', DBAIN)
set_focus_and_type(1)
time.sleep("set_focus_and_type")
logging.info(
'ctrl', 'o')
pyautogui.hotkey(1)
time.sleep("hotkey")
logging.info(
'up', 12)
press_key_multiple_times("press_key_multiple_times")
logging.info(
'down', 4)
press_key_multiple_times("press_key_multiple_times")
logging.info(
'tab'])
pyautogui.press(["press")
logging.info(
'delete'])
pyautogui.press(["press")
logging.info(
str(DBAIN))
pyautogui.typewrite(f"Sent AIN {DBAIN}.")
logging.info(1)
time.sleep(
'enter')
pyautogui.press(1)
time.sleep("Close Pop-Up, Open the {DBAIN}}")
logging.info(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
"""
## NOW BEGIN AUTOMATION STEPS FOR THIS TOOL
"""
"""
## START EXAMPLE EXECUTIONS TO BE REPLACE BY YOUR ACTUAL SCRIPT
"""
# is_image_found
# How to use the is_image_found function below in script:
# Check if the image is found and decide based on that
if is_image_found(image_path_name_here, confidence=0.8):
"Image was found - executing related tasks.")
logging.info(# Perform tasks related to the image being found
else:
"Image was not found - executing alternative tasks.")
logging.info(# Perform alternative tasks
# check_for_text_on_screen
# How to use the check_for_text_on_screen function below in script:
# Define the specific text you're looking for
= "text_you_want_to_check_here"
specific_text # Use the variable in your function call and print statements
if check_for_text_on_screen(specific_text):
f"Found '{specific_text}' on the screen.")
logging.info(else:
f"Did not find '{specific_text}' on the screen.")
logging.info(
# click_images_multiple
# How to use these click_images_multiple & click_image_single functions in script
# Click below all specified images
if click_images_multiple(multiple_image_path_name_here, direction='below', offset=100, confidence=0.8):
"Clicked successfully.")
logging.info(
# Click at the center of a single image
if click_image_single(single_image_path_name_here, direction='center', confidence=0.8):
"Clicked successfully.")
logging.info(
# Click at the bottom right corner of a single image
if click_image_single(single_image_path_name_here, direction='bottom_right_corner', inset=10, confidence=0.8):
"Clicked successfully.")
logging.info(
# Click to right of permit_description, by calling offset=5 it was just barely below the image, which is what I wanted
if click_image_single(permit_description, direction='below', offset=5, confidence=0.8):
"Clicked successfully permit_description.")
logging.info(1)
time.sleep(
# pyautogui.typewrite()
str(DBACRE))
pyautogui.typewrite(str(DBAIN))
pyautogui.typewrite(
pyautogui.typewrite(MemoTXT)
pyautogui.typewrite(PNUMBER)'f')
pyautogui.typewrite(f"04/01/{ForYear}")
pyautogui.typewrite(f"{PDESC} FOR TIMBER REVIEW")
pyautogui.typewrite('p')
pyautogui.typewrite(
# pyautogui.hotkey()
'ctrl', 'o')
pyautogui.hotkey('ctrl', 'shift', 'm')
pyautogui.hotkey(
# pyautogui.press()
'tab'])
pyautogui.press(['delete'])
pyautogui.press(['enter')
pyautogui.press('l')
pyautogui.press('space')
pyautogui.press('right')
pyautogui.press(
# press_key_multiple_times
'up', 12)
press_key_multiple_times('down', 4)
press_key_multiple_times('tab'], 3)
press_key_multiple_times([
# press_key_with_modifier_multiple_times
'shift', 'tab', 6)
press_key_with_modifier_multiple_times(
"""
## END EXAMPLE EXECUTIONS TO BE REPLACE BY YOUR ACTUAL SCRIPT
"""
# Save Account
'ctrl', 's')
pyautogui.hotkey("Save.")
logging.info(1)
time.sleep(if stop_script:
"Script stopping due to kill key press.")
logging.info(break
# END ALL PROCESSESS
"THE END...")
logging.info(1)
time.sleep(
# Close the database connection
conn.close()"Database Connection Closed")
logging.info(
"""
TEMPLATE 08/20/2024
Notes:
if AIN:
press_key_multiple_times('down', 4)
elif PIN:
press_key_multiple_times('down', 0)
## For testing purposes,
# PIN: KC-DGW = AIN: 345134
# PIN: KC-DAS = AIN: 348586
345134,348586
"""
ADDITIONAL INFORMAITON
Arguments (Args)
The term “Args” in the documentation of a Python function is short for “arguments.” It refers to the parameters that you can pass to the function when you call it. These arguments are essentially inputs that the function uses to perform its operations or calculations. Here’s a quick rundown of what each part of the function documentation typically includes:
- Args (or Parameters): This section lists all the parameters that the function expects, along with a brief description of each one. It often specifies the type of data each parameter should be (like string, integer, list, etc.) and a short explanation of what it’s used for in the function.
In the example I provided for the handle_image_detection function, the “Args” section looks like this:
Args:str): Path to the image file to look for.
image_path (callable, optional): Function to execute if the image is found. It should accept no arguments.
on_found (callable, optional): Function to execute if the image is not found. It should accept no arguments.
on_not_found (float): Confidence level for image detection. confidence (
Each line in this section describes one of the function’s parameters:
- image_path: A string that specifies the path to the image file the function needs to search for.
- on_found: A callable (like a function) that is executed if the image is found. It’s optional because you might not always need to perform an action if the image is found.
- on_not_found: A callable that is executed if the image is not found, which is also optional.
- confidence: A floating-point number that sets the threshold for deciding whether the detected image matches the target image.
This part of the documentation helps users understand how to use the function properly by specifying what kind of data to provide and what each piece of data represents.
EXAMPLE: Breaking down a function into blocks of code chunks
ChatGPT Explains: Let’s break down the code snippet you’ve provided, which encapsulates a sequence of operations for image detection and subsequent actions in an automation script using Python. This will help ensure you understand each part before integrating it into your projects:
1. Try-Except Block
This block is used to catch and handle any exceptions that might occur during the execution of the code within the try block. It prevents the entire script from crashing and allows for logging specific errors.
try:
# Code that might throw an exception
except Exception as e:
# Handling the exception
2. Taking a Screenshot
The function pyautogui.screenshot() captures the current screen, which is then used for image detection. This screenshot is the basis for all subsequent operations.
= pyautogui.screenshot() screenshot
3. Converting Screenshot to Grayscale
The screenshot is converted to grayscale using OpenCV (cv2). Grayscale conversion is common in image processing tasks like template matching because it simplifies the image, reducing the computational complexity.
= cv2.cvtColor(np.array(screenshot), cv2.COLOR_BGR2GRAY) screenshot
4. Reading the Reference Image
The reference image (ref_image) is loaded in grayscale. This is the image you are looking for on the screen. The path to this image is specified by image_path.
= cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) ref_image
5. Template Matching
The cv2.matchTemplate function is used to find the reference image within the screenshot. It slides the reference image over the screenshot and calculates match scores based on the similarity between the reference image and the part of the screenshot it covers.
- TM_CCOEFF_NORMED: A method for template matching which returns a coefficient that normalizes the matching process, giving a score between -1 and 1 where 1 indicates a perfect match.
= cv2.matchTemplate(screenshot, ref_image, cv2.TM_CCOEFF_NORMED) result
6. Finding the Maximum Match Value
cv2.minMaxLoc is used to find the location of the best match in the result matrix returned by matchTemplate. It returns the minimum and maximum values and their locations within the result matrix.
= cv2.minMaxLoc(result) min_val, max_val, min_loc, max_loc
7. Evaluating the Match
The maximum value (max_val) is compared against a threshold (confidence). If max_val is greater than or equal to confidence, it is assumed that the image has been found.
- If Found: If the image is found (max_val >= confidence), log that the image is found and call the on_found function if it is provided.
- If Not Found: If the image is not found, log that the image is not found and call the on_not_found function if it is provided.
if max_val >= confidence:
f"Image found: {image_path}")
logging.info(if on_found:
on_found()else:
f"Image not found: {image_path}")
logging.info(if on_not_found:
on_not_found()
8. Exception Handling
If any errors occur during the execution of the image detection code (like file not found, or an error in the OpenCV operations), they are caught in the except block. The error is logged, providing details that can help in debugging.
except Exception as e:
f"Failed to check image: {image_path}")
logging.error(f"Exception: {e}") logging.error(
This comprehensive breakdown clarifies the sequence of operations involved in image detection using Python with pyautogui and OpenCV, providing robust error handling and flexible response mechanisms through callbacks (on_found and on_not_found).
9. User defined confidence vs default confidence
Understanding cv2.minMaxLoc
The function cv2.minMaxLoc() finds the minimum and maximum values and their positions in a given array (in your case, the result of the template matching operation). It returns four values:
- minVal: The minimum value in the array.
- maxVal: The maximum value in the array.
- minLoc: The position of the minimum value.
- maxLoc: The position of the maximum value.
Explanation of the Line
In the line , max_val, , _ = cv2.minMaxLoc(result), here’s what each part does:
- The first _ is used to ignore the minVal.
- max_val captures the maxVal, which is what you’re interested in for checking the confidence of the match.
- The second and third _ ignore the minLoc and maxLoc respectively.
This usage of underscores is helpful for keeping your code cleaner and more focused on the variables you actually need, avoiding the clutter of unused variables. It’s particularly useful in cases like image processing where a function may return multiple pieces of information, but only some of them are relevant to your specific task.
The max_val obtained is then compared against a user-set confidence threshold to decide whether the match is strong enough to be considered valid. For example:
# Example try/except
try:
# Check if the image is on the screen
= pyautogui.screenshot()
screenshot = cv2.cvtColor(np.array(screenshot), cv2.COLOR_BGR2GRAY)
screenshot = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
ref_image = cv2.matchTemplate(screenshot, ref_image, cv2.TM_CCOEFF_NORMED)
result = cv2.minMaxLoc(result)
min_val, max_val, min_loc, max_loc # _, max_val, _, _ = cv2.minMaxLoc(result)
if max_val >= confidence:
f"Image found: {image_path}")
logging.info(if on_found:
on_found()else:
f"Image not found: {image_path}")
logging.info(if on_not_found:
on_not_found()except Exception as e:
f"Failed to check image: {image_path}")
logging.error(f"Exception: {e}")
logging.error(
# default confidence level
= cv2.minMaxLoc(result)
min_val, max_val, min_loc, max_loc
handle_image_detection(=r'S:\Common\Comptroller Tech\Reports\Python\py_images\Settings_icon.PNG',
image_path=access_settings,
on_found=settings_icon_missing
on_not_found
)
# VERSUS
# user defined confidence level
= cv2.minMaxLoc(result)
_, max_val, _, _
# High confidence needed for accessing settings
handle_image_detection(=r'S:\Common\Comptroller Tech\Reports\Python\py_images\Settings_icon.PNG',
image_path=access_settings,
on_found=settings_icon_missing,
on_not_found=0.95 # High confidence for critical settings adjustment
confidence
)
# Lower confidence for less critical actions
handle_image_detection(=r'S:\Common\Comptroller Tech\Reports\Python\py_images\Optional_feature_icon.PNG',
image_path=enable_optional_feature,
on_found=optional_feature_missing,
on_not_found=0.7 # Lower confidence might be acceptable here
confidence
)
# Alternatively
= 0.8 # User-set confidence
confidence_threshold if max_val >= confidence_threshold:
# Proceed with the action because the image match is strong enough
else:
# Handle the case where the image match is too weak
Sample Section Header For Future Additions To This Ref Guide
SampleSubHeader
- List
- List
- List indented
- List indented
This is a code chuck
# THIS IS THE FUNCTION
# THIS IS CALLING THE FUNCTION BELOW IN SCRIPT