Skip to content

Instantly share code, notes, and snippets.

@sketchybear
Last active September 23, 2024 13:42
Show Gist options
  • Save sketchybear/ac07b0bd3b9501e4dfd83364db7b2956 to your computer and use it in GitHub Desktop.
Save sketchybear/ac07b0bd3b9501e4dfd83364db7b2956 to your computer and use it in GitHub Desktop.
This fairly simple script uses Selenium to check a website for stocks of ADHD meds within a postcode of the UK.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select, WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from bs4 import BeautifulSoup
import time
import re
from twilio.rest import Client # Import Twilio client
import sys # Import sys to update the progress bar
from datetime import datetime
import pytz # To handle UK time
# Import Twilio credentials and phone numbers from the partial
from twilio_config import TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER, TARGET_PHONE_NUMBERS
# Set the correct path to ChromeDriver
chrome_driver_path = '/usr/bin/chromedriver'
# Set up Chromium options to run in headless mode (no GUI)
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless') # Run headless
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
# Set up the ChromeDriver service with the correct path
service = Service(chrome_driver_path)
# Initialize Twilio client with credentials from twilio_config.py
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
# Specify the parameters for the search
target_location = "TOWN" # Replace with your desired location
target_postcode = "POSTCODE" # Replace with your desired postcode
target_medicine = "Concerta XL" # Replace with your desired medicine
target_dosage = "18mg" # Replace with your desired dosage
# ANSI escape codes for formatting (for terminal use)
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RESET = "\033[0m"
# Progress bar function with countdown timer
def progress_bar(duration):
total_steps = 50 # Number of steps in the progress bar
for i in range(duration):
time_remaining = duration - i
minutes, seconds = divmod(time_remaining, 60)
progress = int((i / duration) * total_steps)
bar = "[" + "=" * progress + " " * (total_steps - progress) + "]"
sys.stdout.write(f"\r{bar} {minutes:02}:{seconds:02} remaining until next run...")
sys.stdout.flush()
time.sleep(1)
sys.stdout.write("\r" + " " * 80 + "\r") # Clear the progress bar line after it's done
# Check if the current time is between 8 AM and 6 PM UK time, and it's not Sunday
def is_within_operating_hours():
uk_timezone = pytz.timezone('Europe/London')
current_time_uk = datetime.now(uk_timezone)
start_time = current_time_uk.replace(hour=8, minute=0, second=0, microsecond=0)
end_time = current_time_uk.replace(hour=18, minute=0, second=0, microsecond=0)
# Check if it's Sunday (6 is Sunday in Python's weekday() where Monday = 0)
if current_time_uk.weekday() == 6:
return False
# Check if current time is between 8 AM and 6 PM
return start_time <= current_time_uk <= end_time
# Log errors in errors.txt
def log_error(error_message):
with open("errors.txt", "a") as error_file:
error_file.write(f"{datetime.now()}: {error_message}\n")
# Function to perform the check
def check_results():
try:
print(f"Starting stock search for {target_location} with {target_medicine} ({target_dosage})...")
# Initialize the WebDriver for Chromium
driver = webdriver.Chrome(service=service, options=chrome_options)
# Set up explicit wait
wait = WebDriverWait(driver, 10) # Wait for up to 10 seconds
# Open the website
driver.get("https://www.adhd.directory/stock-checker")
print("Website has loaded ...")
# Step 1: Wait for the accordion element to be clickable and click it to reveal the fields
accordion = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".flex.justify-between.items-center")))
accordion.click()
print("Form unfolded, ready for action ...")
# Step 2: Wait for the medication dropdown to be present and select the target medicine
medication_dropdown = wait.until(EC.presence_of_element_located((By.ID, "medication")))
select_medication = Select(medication_dropdown)
select_medication.select_by_visible_text(target_medicine)
print(f"Medication '{target_medicine}' selected ...")
# Step 3: Wait for the target dosage button to be clickable and click it
dosage_button = wait.until(EC.element_to_be_clickable((By.XPATH, f"//button[text()='{target_dosage}']")))
dosage_button.click()
print(f"'{target_dosage}' dosage selected ...")
# Step 4: Wait for the postcode field to be visible, then enter the target postcode
postal_code_input = wait.until(EC.presence_of_element_located((By.ID, "store-search")))
postal_code_input.send_keys(target_postcode)
print(f"Postcode '{target_postcode}' has been entered ...")
# Step 5: Wait for the submit button using its class and click it
submit_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".w-fit.rounded-md.border.border-gray-200.py-1.px-2.transition-colors.bg-indigo-500.text-white.text-xs")))
submit_button.click()
print("Submit button clicked, herding llamas ...")
# Step 6: Wait for results and fetch all divs with class containing "shadow-md"
result_divs = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div[class*='shadow-md']")))
print("Results and llamas gathered ...")
# Step 7: Filter results that contain the target location and remove images or non-text elements
location_results = []
for div in result_divs:
if target_location.lower() in div.text.lower(): # Match target location (case insensitive)
# Use BeautifulSoup to parse the div content and remove images and other non-text elements
soup = BeautifulSoup(div.get_attribute('innerHTML'), 'html.parser')
# Remove all <img>, <svg>, and other non-text elements, especially GIFs
for tag in soup(['img', 'svg', 'video', 'picture']):
tag.decompose()
# Cleaned text
cleaned_text = soup.get_text().strip()
# Remove non-ASCII characters (like emojis) using regex
cleaned_text = re.sub(r'[^\x00-\x7F]+', '', cleaned_text)
# Highlight 'out of stock' in red and 'in stock' in green
cleaned_text = cleaned_text.replace("out of stock", RED + "out of stock" + RESET)
cleaned_text = cleaned_text.replace("in stock", GREEN + "in stock" + RESET)
# Append cleaned and formatted text
location_results.append(cleaned_text)
# Print results in the terminal
print(f"\n--- Search Results for {target_location} ---")
for result in location_results:
print(result)
print(f"--- End of Results for {target_location} ---\n")
# Only send an SMS if there are in-stock items
in_stock_items = [result for result in location_results if "out of stock" not in result.lower() and "in stock" in result.lower()]
if in_stock_items:
message_body = f"In stock at the following locations in {target_location}:\n" + "\n".join(in_stock_items)
# Send SMS to each phone number in the list
for phone_number in TARGET_PHONE_NUMBERS:
client.messages.create(
body=message_body,
from_=TWILIO_PHONE_NUMBER,
to=phone_number
)
print(f"Twilio message sent to {phone_number} for in-stock items.") # Log to terminal
# Close the driver after you're done
driver.quit()
except Exception as e:
# Log any errors to errors.txt and continue running
log_error(str(e))
print(f"An error occurred: {e}. Logged to errors.txt")
# Run the check every hour between 8 AM and 6 PM UK time, excluding Sundays
while True:
if is_within_operating_hours():
print(f"Within operating hours (8 AM - 6 PM, Monday to Saturday). Running the check for {target_location} ...")
check_results()
else:
# Yellow message for outside operating hours or Sunday
print(f"{YELLOW}Outside operating hours or it's Sunday. Waiting for the next valid time slot.{RESET}")
print("Waiting for 30 mins before checking again ...")
progress_bar(1800) # 30 minutes in seconds
# twilio_config.py
TWILIO_ACCOUNT_SID = '###########' # Replace with your Account SID
TWILIO_AUTH_TOKEN = '#############' # Replace with your Auth Token
TWILIO_PHONE_NUMBER = '############' # Replace with your Twilio number
TARGET_PHONE_NUMBERS = ["############"] # UK numbers, you can comma separate multiple numbers
@sketchybear
Copy link
Author

Now updated with:

  • Hours of operation - no need to run this 24/7 - so you can set the window of time you'd like the checker to run, otherwise it'll sit idle and just keep checking the time to see if to can run
  • Doesn't run at all on a Sunday
  • Added a yellow colour for warning messages
  • Twilio params are now in a separate file
  • Old Sim City easter egg for those who are old enough

@sketchybear
Copy link
Author

Added some error handling and moved the town, postcode and meds to parameters to make it easier to manage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment