I have a friend, let's call her Blaine, who is a reliable liberal, but has never voted. She's 28 and has never registered. I pushed and I pushed and I pushed in 2018 but she never did.
Well, this time is going to be different. And I'm seeing to it.
I wrote a script to bug her by sending a text repeatedly, on a tightening schedule, until the deadline for registration to vote in November in Florida.
The code for sending the texts was incredibly easy (though it cost around $20). This might be attributable to the fact that I've done it before. (Some time ago I wrote a script to run in the background of my home desktop querying icanhazip.com periodically, saving the result to a text file, and texting me if it changed.) You can literally just copy the code in the Twilio docs and it will work. Super simple.
The code for getting the scheduler to run was a bit less trivial. It required some thinking, but wasn't as difficult as I was anticipating. I want to talk about the strategy I employ in vote.py here.
My requirements for this were pretty simple: I wanted to text Blaine on a periodic basis, wherein the period shrank exponentially. But, I wanted to avoid texting her at midnight, or outside certain pre-designated working hours.
def runschedule(duedate, to, body, test):
try:
message = sendtext(to, body)
SIDS.append((message, time.time()))
except:
print("Message failed. Trying again next time.")
s = scheduler(time.time, time.sleep)
seconds = getseconds(duedate, test)
s.enter(seconds, 1, runschedule, argument=(duedate, to, body, test))
s.run()
Sched.scheduler
works by entering the number of seconds to sleep until it
triggers a function which is passed through sched.scheduler.enter()
. You can
schedule multiple events with different sleep times, but the strategy for a
continuous repeating event is apparently to use the function to reschedule the
event. Basically, just use it recursively. For me, this is the function
runschedule()
.
I program somewhat iteratively and composably (if that's a word), so my next question was: how do I get the number of seconds until a given date and time?
def getseconds(duedate):
freq = DEFAULTFREQ
timetogo = duedate - dt.datetime.now()
try:
seconds = freq[timetogo.days // 30] # interval of months
except IndexError:
seconds = freq[-1]
seconds = checkho(seconds)
return seconds
This is actually still a fairly simple task. I have a habit of coding something
stupidly first and then fixing it more logically, so I started with a dictionary
keyed on "months until the due date" before I remembered that I could just use a
damn array. I generate the array as an exponential function: 2^x * 1800
.
Then, in getseconds()
, to determine the seconds until next event, I take
the difference between the due date and now using datetime.datetime
(which is
an awesome damn library) and divide the number of days left by 30 (so,
essentially the months left). That gives the index in DEFAULTFREQ
and the
seconds 'til T-Time.
def checkho(seconds):
ho = DEFAULTHO
delta = dt.timedelta(seconds=seconds)
t_time = dt.datetime.now() + delta
if t_time.hour < ho[0]:
t_time = t_time.replace(hour=ho[0])
delta = t_time - dt.datetime.now()
print("Text would be too early, rescheduling...")
elif t_time.hour >= ho[1]:
t_time = t_time.replace(day=t_time.day+1, hour=ho[0])
delta = t_time - dt.datetime.now()
print("Text would be too late, rescheduling...")
return delta.seconds
But, now that I have that, that's not enough to schedule the text, because Now +
Seconds could be outside the acceptable Hours of Operation. So I wrote
checkho()
. Checkho takes the seconds determined in getseconds()
,
determines the date and time that is by adding
datetime.timedelta(seconds=seconds)
to now, and then determing whether the
resulting t_time
is before DEFAULTHO[0]
or after DEFAULTHO[1]
. In either
case, I just reschedule at the next DEFAULTHO[0]
.
So there you have it. Surprisingly simple once it's written, like all code. I've texted Blaine a handful of times already. The next text will go out tonight at 8:08.