Transferring my TAFE QLD timetable into Google Calendar with Python

6 minute read

TAFE Queensland has a website to access the class timetables. However, it is text-only. Let’s make it an ical file using python (to import it to Google Calendar ).

This project sort of grew larger than I thought it would. So I made a github repository with all the files and effort.

The website text output looks like this:

website screenshot

And when we copy the text from the page, we get output (into the xed text editor - the default on Linux Mint) like this:

copied text

So the format is:

DATE1
\n
CLASS1
    TIMES
    'Room'
    ROOM LOCATION
    'Unit(s)'
    UNIT TEXT 1
    UNIT TEXT 2
\n
CLASS2
...

DATE2
...

Parsing the text

So we can separate out the key variables with lines as something like:

  • Date: Does it match the WEEKDAY, MONTH MONTHDAY, YEAR format?
  • TIMES: Contains "AM" or "PM".
  • Class N: TIMES line to a new line (\n).
  • ROOM LOCATION: Line following "Room".
  • UNITS: Line text after "Units(s)" until new line (\n).

So firstly, we split text into an array of strings for each DATE. Then split each date string into a CLASS string. Then we build up each CLASS object with date, time, room and units.

Making it an ics file

Then we need some calendar parsing libraries. From this tutorial, the icalendar seems well support with the features we need.

Make sure to install this with pip install icalendar. If you are running vanilla Linux Mint 20.3 as I was, run:

sudo apt install python3-pip

Writing the code

We have explained enough of the logic and the libraries. Show me the code.

#!/usr/bin/python3

from icalendar import Calendar, Event
import pytz
from datetime import datetime
from dateutil.parser import parse

# Set the timezone
tz_string = "Australia/Brisbane"
timezone = pytz.timezone(tz_string)

# Get the lines as a list
with open('timetable-text-4week.txt','r') as f:
    lines = f.readlines()
    lines = [line.strip('\n') for line in lines] #Strip the '\n'

def is_date(date_string):
    try:
        parse(date_string)
        return True

    except ValueError:
        return False

# Test the isdate function
assert(is_date("Friday, January 21, 2022")==True)
assert(is_date("Thursday, February 3, 2022\n")==True)
assert(is_date("this is not a date\n")==False)

def close_lowest(value, num_list):
    # Finds the number in num_list which is less than or equal to its value
    lowdiff = max(num_list)
    for x in range(0, len(num_list)):
        diff = value - num_list[x]
        if (diff < lowdiff) and (diff >= 0):
            lowdiff = diff
            close_low = num_list[x]
    try:  
        return close_low
    except NameError:
        return 'Argument 1 lower than all entries in list'

# Test the close_lowest function
assert(close_lowest(5, [1,3,5,7,9]) == 5)
assert(close_lowest(4, [1,3,5,7,9]) == 3)
assert(close_lowest(-10, [1,3,5,7,9]) == 'Argument 1 lower than all entries in list')

dates = []
times = []
for count, string in enumerate(lines):

    # Each date uniquely defines a day
    if is_date(string):
        dates += [count]

    # Each time uniquely defines an event
    if (("AM" in string) or ("PM" in string)) and ("-" in string):
        times += [count]


events = [] # list of dictionaries with key data 

# Assign each event to a date (all dates are above times)
for x in range(0, len(times)):

    date_equiv = close_lowest(times[x], dates)
    # Assign event lines their own list
    if x < len(times)-1:
        event_lines = lines[times[x] : times[x+1]]
    else:
        event_lines = lines[times[x] : -1]

    room_num = event_lines.index('Room')
    room_txt = event_lines[room_num+1] #Assign line below "Room" to be Location

    try:
        unit_num = event_lines.index('Unit(s)')
        summary = str(event_lines[unit_num + 1]) # Make title the subject name
    except ValueError:
        unit_txt = ''
        summary = 'TAFE QLD CLASS' #default title 

    # Extract times
    date_string = lines[date_equiv]
    time_string = lines[times[x]]

    start = time_string.split('-')[0].strip(' ')
    end = time_string.split('-')[1].strip(' ')

    dt_start = parse(date_string + ' ' + start)
    dt_end = parse(date_string + ' ' + end)
    # Add in the timezones
    dt_start = timezone.localize(dt_start)
    dt_end = timezone.localize(dt_end)
    
    # Make description the entire event string with the date for error checking
    raw_event_string = date_string + '\n'
    for s in event_lines:
        raw_event_string += s + '\n'

    # Make and append the dictionary
    event_dict = {'summary': summary, 'description': raw_event_string, 'start': dt_start, 'end': dt_end, 'location': room_txt}
    events.append(event_dict)

# Make the events in the calendar
# Thanks to https://www.tutorialsbuddy.com/create-ics-calendar-file-in-python for code I have adapted here

cal = Calendar()
cal.add('version', '2.0')

for e in events:
    event = Event()
    event.add('summary', e['summary'])
    event.add('description', e['description'])
    event.add('location', e['location'])
    event.add('dtstart', e['start'])
    event.add('dtend', e['end'])
    event.add('dtstamp', datetime.now(pytz.timezone(tz_string)) ) # Time event was created.
 
    cal.add_component(event) # Adding events to calendar
# Write out the ical file to disk
filename = 'tafe-qld-timetable.ics'
with open(filename, 'wb') as ics_file:
    ics_file.write(cal.to_ical())

print('{} Events found. Processed and .ics file exported called {}'.format(len(events), filename))

Importing to Google Calendar

From google support here:

import google calendar

Here are the results of the output if you decide to replicate this for yourself.

The terminal output:

terminal output

The Google Calendar view: calendar view

Another Google Calendar event view (you can see the correct parsing of the dates into the event): date-parsing-verify