vote.py 2020-06-11

Github Repository for vote.py

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.

Scheduling

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.