Skip to content

Instantly share code, notes, and snippets.

@samliu
Last active April 14, 2021 19:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save samliu/26cc049a8d222277016bdbd75917288c to your computer and use it in GitHub Desktop.
Save samliu/26cc049a8d222277016bdbd75917288c to your computer and use it in GitHub Desktop.
Find CVS appointments for COVID-19 vaccine, and get them texted to your phone.
"""check_vaccine.py
Checks CVS for vaccine appointment availability and sends text messages if they show up.
Requires paid account on Twilio. You can fund $5 and it'll be plenty, a SMS is less than 1 cent.
Assuming you use a *nix system with Python 3 and want to deploy via crontab, you can do something like this:
1. `which python` Record the output. (It's nice to give crontab the full path of your python binary.)
2. `export VISUAL=vim` (For `crontab -e` to use vim; my personal preference.)
3. `crontab -e` to create a new line in your crontab. Next line is an example:
4. `* * * * * /home/$USER/.virtualenvs/python3.7/bin/python /home/$USER/check_vaccine.py >> /home/$USER/vaccine_check.log`
(Run the script every minute and log the output)
5. `tail -f /home/$USER/vaccine_check.log` (For if you want to just see the messages getting printed to stdout.)
Alternatively just add some logic to do infinite loop + sleep between calls to `find_appointments()`.
If you want the dependencies, write the following into `requirements.txt`:
python>=3.4
pytz>=2019.3
requests>=2.23.0
twilio>=6.53.0
...then `pip install -r requirements.txt`
"""
import requests
import json
import time
import os
import pathlib
import pytz
from datetime import datetime
from datetime import timedelta
from twilio.rest import Client
class bcolors:
"""For printing in color."""
WARNING = '\033[93m'
ENDC = '\033[0m'
class CVSVaccineChecker(object):
def __init__(self, twilio_account_sid, twilio_auth_token,
from_number='+16505551234',
to_numbers=None,
state='NJ',
timezone=pytz.timezone('US/Eastern'), rate_limit_minutes=10,
city_skip_list=None, verbose=True):
"""CVS Vaccine Checker.
twilio_account_id: string, credential from twilio. SMS is cheap, fund $5 and
you should be fine.
twilio_auth_token: string, another credential from twilio.
from_number: string, phone number you purchased from twilio for sending.
Expected format is `+<countrycode><phone number>`.
to_numbers: list of strings. Same format as above, one for each number you
want to notify.
state: string, CVS doesn't support every state so check their website.
timezone: pytz timezone object representing the timezone you want. Default
is US eastern time.
rate_limit_minutes: number of minutes to rate limit SMS notifications. Since
the site's availability info lags, you don't want to keep getting
notifications even after the spots have been booked.
city_skip_list: list of strings representing cities you want to skip / not
notify on.
verbose: bool, whether to print outputs to stdout.
"""
self.client = Client(twilio_account_sid, twilio_auth_token)
self.from_number = from_number
self.to_numbers = []
if self.to_numbers:
self.to_numbers = to_numbers
self.state = state
self.tz = timezone
self.rate_limit_minutes = rate_limit_minutes
self.city_skip_list = []
if city_skip_list:
self.city_skip_list = city_skip_list
self.verbose = verbose
# Create the last updated timestamp file if it doesn't exist.
self.last_updated_file = pathlib.Path("last_updated.ts")
if not self.last_updated_file.exists():
f = open(self.last_updated_file, 'w')
f.write(str(datetime.now()))
f.close()
def __maybe_print(self, s):
if self.verbose:
print(s)
def __send_sms(self, msg, dest_numbers=None):
"""Send an SMS notification using twilio."""
if type(dest_numbers) is not list:
raise Exception('Must specify numbers to send text messages to')
for dest_number in dest_numbers:
message = self.client.messages \
.create(
body=msg,
from_=self.from_number,
to=dest_number,
)
self.__maybe_print(f'Sent message to {message.sid}')
def find_appointments(self):
available_cities = []
try:
# Download json from CVS website.
url = ('https://www.cvs.com//immunizations/covid-19-vaccine.vaccine'
f'-status.{self.state}.json?vaccineinfo')
r = requests.get(
url,
headers={
'referer': 'https://www.cvs.com/immunizations/covid-19-vaccine'})
data = r.content
self.__maybe_print(data)
except Exception as e:
self.__maybe_print(f'fuq it didn\'t work. {str(e)}')
obj = json.loads(data)
del obj["responseMetaData"] # Remove entry to make iteration easier.
# Check availability. Status options are: ["Fully Booked", "Available"].
for _, val in obj.items():
for i in range(0, len(val['data'][self.state])):
# If there is availability anywhere in the state, take some action.
if val['data'][self.state][i]['status'] == 'Available':
city_name = str(val['data'][self.state][i]['city']).title()
if city_name not in self.city_skip_list:
available_cities.append(city_name)
now = datetime.now(self.tz)
strDateTime = now.strftime('%m/%d/%Y %I:%M %p')
self.__maybe_print(f'{len(available_cities)} cities have slots open as of '
f'{strDateTime} {self.tz}')
# Send messages if necessary.
if len(available_cities) > 0:
# Janky rate limit logic using a flat file.
rate_limited = False
with open(self.last_updated_file) as file:
last_updated = datetime.strptime(
file.readlines()[0], '%Y-%m-%d %H:%M:%S.%f')
delta = datetime.now() - last_updated
if delta >= timedelta(minutes=self.rate_limit_minutes):
f = open(self.last_updated_file, 'w')
f.write(str(datetime.now()))
f.close()
else:
# If we get here, it's been less than five minutes since we last sent a
# message, so let's rate limit and skip sending.
rate_limited = True
self.__maybe_print('Skipping send, time delta since last notification'
f'was {str(delta)}')
if not rate_limited:
nums = self.to_numbers
msg = (' We found appointments available in ' +
', '.join(available_cities) +
'! Book now: https://www.cvs.com/vaccine/intake/store/'
'eligibility-screener/eligibility-covid')
self.__send_sms(msg,
dest_numbers=nums)
# Open a browser window too. This is so custom to what browser you use
# and where you installed it that I'm leaving it commented out.
#
# reserve_url = ('https://www.cvs.com/vaccine/intake/store'
# '/eligibility-screener/eligibility-covid')
# import webbrowser
# chrome_path = '/usr/bin/google-chrome %s'
# webbrowser.get(chrome_path).open(reserve_url)
self.__maybe_print(f'{bcolors.WARNING}Availability in these cities:'
f' {", ".join(available_cities)} {bcolors.ENDC}')
self.__maybe_print('--------------------------------\n') # Spacer.
if __name__ == "__main__":
# Example of how to use this.
checker = CVSVaccineChecker(
twilio_account_sid='',
twilio_auth_token='',
from_number='+16505551234',
to_numbers=['+16505554321'],
state='NJ')
# Instead of cron you could make this a long lived process and call this between sleep().
checker.find_appointments()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment