|
import os |
|
import selenium |
|
from selenium import webdriver |
|
from selenium.webdriver.chrome.options import Options |
|
from selenium.webdriver.common.by import By |
|
from selenium.webdriver.common.keys import Keys |
|
from selenium.webdriver.support.ui import WebDriverWait |
|
from selenium.webdriver.support.expected_conditions import presence_of_element_located, element_to_be_clickable |
|
from selenium.common.exceptions import ElementClickInterceptedException |
|
import time |
|
from PIL import Image |
|
import io |
|
import requests |
|
import chromedriver_binary |
|
import datetime |
|
import oyaml as yaml |
|
import calendar |
|
from pushbullet import PushBullet |
|
from pywebio.input import * |
|
from pywebio.output import * |
|
from pywebio.session import * |
|
|
|
class bcolors: |
|
HEADER = '\033[95m' |
|
OKBLUE = '\033[94m' |
|
OKCYAN = '\033[96m' |
|
OKGREEN = '\033[92m' |
|
WARNING = '\033[93m' |
|
FAIL = '\033[91m' |
|
ENDC = '\033[0m' |
|
BOLD = '\033[1m' |
|
UNDERLINE = '\033[4m' |
|
|
|
# Remember to install the chromedriver for your OS, download the version that matches your installed Google Chrome version |
|
# https://chromedriver.chromium.org/downloads |
|
|
|
# Global Variables |
|
driver = None |
|
wait = WebDriverWait(driver, 20) |
|
data = None |
|
pb = None |
|
log_file = None |
|
|
|
""" |
|
Just setting up Selenium with the driver |
|
""" |
|
def read_config(): |
|
global data |
|
global pb |
|
data = yaml.full_load(open("config.yml")) |
|
if "pushbullet_token" in data: |
|
pb = PushBullet(data["pushbullet_token"]) |
|
|
|
|
|
def init_chrome_driver(): |
|
global driver |
|
global wait |
|
global options |
|
options = Options() |
|
options.headless = False # When True it will show a chrome window so you can see what's happening |
|
options.add_argument("--window-size=400, 300") |
|
# Initialize the Chrome Driver with the options we set up |
|
driver = webdriver.Chrome(data["chrome_driver_path"], options=options) |
|
# This defines a default wait object, the waiting time is 10 seconds |
|
# Whenever you call a wait, it will wait until the even you specify happens |
|
# If it does not happen in the time frame you passed, it will throw a TimeOutException |
|
wait = WebDriverWait(driver, data["wait_element_time"]) |
|
|
|
def log_print(log_type="i", txt="", with_time=True, end="\n", start="\n", pb_notif=True, to_file=True): |
|
global log_file |
|
global pb |
|
prefix = "" |
|
if with_time: |
|
prefix = f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\t" |
|
color = "" |
|
send_to_pb = False |
|
log_type = log_type.lower() |
|
if log_type in ["i","info","nfo","log"]: |
|
color = bcolors.OKCYAN |
|
elif log_type in ["w","f"]: |
|
color = bcolors.WARNING |
|
# send_to_pb = True |
|
elif log_type in ["e","f", "n", "error", "fail", "no"]: |
|
color = bcolors.FAIL |
|
send_to_pb = True |
|
elif log_type in ["s","success", "ok"]: |
|
color = bcolors.OKGREEN |
|
send_to_pb = True |
|
|
|
print(start+prefix+color+txt, end=end) |
|
if to_file and log_file is not None: |
|
try: |
|
log_file.write(start+prefix+txt+end+"\n") |
|
log_file.flush() |
|
except Exception as e: |
|
pass |
|
if pb is not None and pb_notif and send_to_pb: |
|
try: |
|
pb.push_note("Bupa Refresh", f"{prefix}\n{txt}") |
|
except Exception as e: |
|
pass |
|
|
|
|
|
def wait_print(seconds, waiting_msg="", end_msg="", log_type="i", pb_notif=False): |
|
while seconds > 0: |
|
log_print(log_type, f"{waiting_msg} {seconds}s... ", end = "\r", start="", pb_notif=pb_notif, to_file=False) |
|
time.sleep(1) |
|
seconds = seconds - 1 |
|
log_print(log_type, f"{waiting_msg} {seconds}s... {end_msg}", end = "\n", start="", pb_notif=pb_notif, to_file=True) |
|
|
|
def loop_refresh(): |
|
with open('dates_log.csv', 'w') as f: |
|
best_date = 0 |
|
cnt = 0 |
|
# Loop forever to keep finding closer and closer appointments |
|
while True: |
|
try: |
|
if cnt > 0: |
|
# This is the power of the wait: I'm waiting until the Calendar input field is clickable |
|
calendar_date = wait.until(element_to_be_clickable((By.ID, "ContentPlaceHolder1_SelectTime1_txtAppDate"))) |
|
else: |
|
raise Exception("First time running!") |
|
except Exception as e: |
|
try: |
|
# If the calendar input field is not found and the timeout gets triggered then it means we are either in the wrong page |
|
# Or we have been kicked/logged out so we'll restart the login process again |
|
# This will also get triggered the first time you run the script as there is nothing on the page so after 10 seconds |
|
# This exception will be thrown and the login starts |
|
login() |
|
# Gotta wait until the "modify/edit date and location" button is visible |
|
edit_btn = wait.until(element_to_be_clickable((By.ID, "ContentPlaceHolder1_repAppointments_lnkChangeAppointment_0"))) |
|
edit_btn.click() |
|
# Now need to wait for the next button to be clickable |
|
next_btn = wait.until(element_to_be_clickable((By.ID, "ContentPlaceHolder1_btnCont"))) |
|
next_btn.click() |
|
# Once we are logged in and moved to the edit booking page we should be able to find the calendar input field |
|
calendar_date = wait.until(element_to_be_clickable((By.ID, "ContentPlaceHolder1_SelectTime1_txtAppDate"))) |
|
except Exception as e: |
|
log_print("e", "Error while logging in! {e}", end="") |
|
|
|
|
|
# Now I'm just getting the new available date from the calendar in the page and comparing it |
|
# With the current appointment date |
|
calendar_date.click() |
|
new_date = calendar_date.get_attribute("value") |
|
new_appt_obj = datetime.datetime.strptime(new_date, '%d/%m/%Y') |
|
try: |
|
f.write(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')},{new_appt_obj}\n") |
|
f.flush() |
|
except Exception as e: |
|
pass |
|
# If we found a closer date than the current one |
|
curr_appt_date = datetime.datetime.strptime(data["appt_date"], '%d/%m/%Y') # Always need to re-read it in case it has changed! |
|
if check_date(new_appt_obj, curr_appt_date): |
|
book_appointment(driver, data, new_date) |
|
else: |
|
# If this date does not work, there might be other dates in the calendar that work! |
|
# Let's extract them and see |
|
try: |
|
calendar_month = driver.find_element(By.CLASS_NAME, "ui-datepicker-month").text |
|
calendar_year = driver.find_element(By.CLASS_NAME, "ui-datepicker-year").text |
|
days = driver.find_elements(By.CSS_SELECTOR, ".Highlighted:not(.ui-datepicker-current-day)") |
|
for available_day in days: |
|
day = available_day.text |
|
log_print("i", f"Checking date from calendar {day}/{calendar_month}/{calendar_year}") |
|
new_date = f"{day}/{calendar_month}/{calendar_year}" |
|
new_appt_obj = datetime.datetime.strptime(f"{day}/{calendar_month}/{calendar_year}", '%d/%B/%Y') |
|
if check_date(new_appt_obj, curr_appt_date): |
|
available_day.click() |
|
book_appointment(driver, data, new_date) |
|
except Exception as e: |
|
log_print("e", f"Error getting other dates from calendar! {e}", end="") |
|
|
|
print() |
|
wait_print(data["time_to_refresh"], waiting_msg=f"No new date found or not a good option! Waiting {data['time_to_refresh']}s before refresh...", |
|
end_msg="Refreshing the page!", log_type="w")# take a pause |
|
driver.refresh() # Refresh the page and restart the loop which will look for the calendar |
|
time.sleep(2) # take a pause |
|
cnt += 1 |
|
|
|
def book_appointment(driver, data, new_date): |
|
time_results = wait.until(element_to_be_clickable((By.CSS_SELECTOR, '#ContentPlaceHolder1_SelectTime1_rblResults input'))) |
|
time_results_label = driver.find_elements_by_css_selector('#ContentPlaceHolder1_SelectTime1_rblResults label') |
|
# Get the radio buttons and their labels for the available times |
|
log_print("i", f"New date and time found! {new_date} {time_results_label[-1].text}") |
|
# And because I'm lazy, I'll always click the last radio button in the list (so the latest time of the day available) |
|
time_results[-1].click() |
|
# After clicking we need to wait to be able to click next |
|
next_btn_edit = wait.until(element_to_be_clickable((By.ID, 'ContentPlaceHolder1_btnCont'))) |
|
next_btn_edit.click() |
|
|
|
# After clicking next we need to wait until the confirmation page shows up |
|
# Because the Save Changes button has no class or id I used XPATH to get the button with that specific text in it |
|
save_btn = wait.until(element_to_be_clickable((By.XPATH, '//button[text()="Save changes"]'))) |
|
log_print("i", f"Found Save button! {save_btn.get_attribute('onclick')}") |
|
save_btn.click() # Click Save! |
|
|
|
# Update the date in the yaml file so at the next loop it won't keep booking this same one and get stuck in a loop |
|
data["appt_date"] = new_date |
|
yaml.dump(data, open("config.yml", "w")) |
|
|
|
|
|
def check_date(date_to_check, curr_appt_date): |
|
log_print("i", f"Checking: \t{curr_appt_date} - {date_to_check}", end="\t") |
|
if date_to_check > curr_appt_date: |
|
log_print("w", f"New date is AFTER the current booked date", end="") |
|
return False |
|
|
|
dates_range=data.get("dates_range") |
|
from_date = None |
|
to_date = None |
|
if dates_range is not None: |
|
from_date= dates_range.get("from") |
|
to_date= dates_range.get("to") |
|
if from_date is not None and date_to_check < datetime.datetime.strptime(from_date, '%d/%m/%Y'): |
|
log_print("w", f"New date {date_to_check} is OUTSIDE the RANGE (BEFORE the FROM date {from_date})", end="") |
|
return False |
|
if to_date is not None and date_to_check > datetime.datetime.strptime(to_date, '%d/%m/%Y'): |
|
log_print("w", f"New date {date_to_check} is OUTSIDE the RANGE (AFTER the TO date {to_date})", end="") |
|
return False |
|
|
|
weekdays_excluded = data.get("dates_weekdays_excluded") |
|
if weekdays_excluded is not None: |
|
for weekday_str in weekdays_excluded: |
|
new_weekday = calendar.day_name[date_to_check.weekday()].lower() |
|
weekday_ex = weekday_str.lower() |
|
# log_print(f"{new_weekday} - {weekday_str}", end="\t") |
|
if new_weekday == weekday_ex: |
|
log_print("w", f"New date {date_to_check} is a {weekday_str.upper()}, which is EXCLUDED", end="") |
|
return False |
|
|
|
dates_excluded = data.get("dates_excluded") |
|
if dates_excluded is not None: |
|
for date_ex_str in dates_excluded: |
|
date_ex = datetime.datetime.strptime(date_ex_str, '%d/%m/%Y') |
|
if date_ex == date_to_check: |
|
log_print("w", f"New date {date_to_check} is in the EXCLUDED list", end="") |
|
return False |
|
log_print("ok", f"New date {date_to_check} WORKS! BOOKING!", end="") |
|
return True |
|
|
|
def test_dates(dates_to_check): |
|
for date in dates_to_check: |
|
check_date(datetime.datetime.strptime(date, '%d/%m/%Y')) |
|
|
|
|
|
|
|
""" |
|
Function to login by inputting the data and moving to the edit booking page |
|
""" |
|
def login(): |
|
driver.get(data["bupa_base_url"]) # Navigate to the BUPA modify booking page |
|
# Get all the field and send the input for each one |
|
# driver.maximize_window() |
|
time.sleep(2) |
|
search_btn = wait.until(element_to_be_clickable((By.ID, "ContentPlaceHolder1_btnSearch"))) |
|
hapid = driver.find_element(By.ID, 'ContentPlaceHolder1_txtHAPID') |
|
hapid.send_keys(data["HAPID_data"]) |
|
firstname = driver.find_element(By.ID, 'ContentPlaceHolder1_txtFirstName') |
|
firstname.send_keys(data["first_name_data"]) |
|
surname = driver.find_element(By.ID, 'ContentPlaceHolder1_txtSurname') |
|
surname.send_keys(data["surname_data"]) |
|
dob = driver.find_element(By.ID, 'ContentPlaceHolder1_txtDOB') |
|
dob.send_keys(data["dob_data"]) |
|
# Finally click search to get to our booking |
|
search_btn.click() |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
log_file = open(f"log_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", "w") |
|
read_config() |
|
# test_dates(["01/01/2021", "29/01/2022", "30/01/2022", "31/12/2021", "01/01/2022", "01/03/2022", "10/02/2022", "01/02/2022", "15/01/2022", "12/12/2022", "01/01/2023"]) |
|
init_chrome_driver() |
|
try: |
|
loop_refresh() |
|
except Exception as e: |
|
log_print("e", "Critical ERROR!! {e}", end="") |
|
close(log_file) |