
Build Sophisticated Sequential Scheduling Features With the Nylas Calendar API
The Nylas Calendar API makes it simple to add sequential scheduling functionality to your app with 100% of calendar providers.
Ben Lloyd Pearson | July 23, 2020
Update: availability is now even easier. Since the publication of this post, we’ve released new Availability and Consecutive Availability endpoints that automatically handle the scenarios outlined here.
Want a PDF of this article?
Share it with a friend or save it for later reading.
x
We'll send the PDF straight to your inbox!
For more information about how this data is used, please view our Privacy Policy
import os import datetime from nylas import APIClient
CLIENT_ID = os.environ['CLIENT_ID'] CLIENT_SECRET = os.environ['CLIENT_SECRET'] ACCESS_TOKEN = os.environ['ACCESS_TOKEN'] nylas = APIClient( CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN, )
# Global Variables direct_reports = ["[email protected]", "[email protected]"] meeting_length = 30 total_time = meeting_length * len(direct_reports) date_range = "6/15/2020 - 6/19/2020" work_start = "10:00am" work_end = "6:00pm" user_email = nylas.account.email_address
# Accepts: an amount of time as an integer representing minutes, # A datetime object for a specific date, # And the email address of the user to check for availability. def find_available_timeslots(total_time, date, email_address): start_time = datetime.datetime.strptime(work_start, "%I:%M%p") end_time = datetime.datetime.strptime(work_end, "%I:%M%p") # Nylas uses epoch time stamps to represent time. start = datetime.datetime.combine(date, start_time.time()).strftime("%s") end = datetime.datetime.combine(date, end_time.time()).strftime("%s") # Use the Free-Busy endpoint to get all of the times the user is busy during the specified time. free_busy = nylas.free_busy(email_address, start, end)[0] available_time_slots = [] start_of_time_slot = int(start) end_of_day = int(end) # If the user has no events, their entire day is free. if len(free_busy['time_slots']) == 0: available_time_slots.append({ 'start_time': start_of_time_slot, 'end_time': end_of_day, }) return available_time_slots for time_slot in free_busy['time_slots']: # If the gap between two busy periods is longer than # the total required time for the sequential events... if time_slot["start_time"] - start_of_time_slot > total_time: # Add this slot to the user's list of availability available_time_slots.append({ 'start_time': start_of_time_slot, 'end_time': time_slot["start_time"], }) start_of_time_slot = time_slot["end_time"] # Check the last free period of the user's day # to see if it is long enough for the sequential events if end_of_day - free_busy['time_slots'][-1]["end_time"] > total_time: available_time_slots.append({ 'start_time': time_slot["end_time"], 'end_time': end_of_day, }) # Return all available time slots long enough for all meetings. return available_time_slots
[ { 'start_time': 1595430000, 'end_time': 1595448000 }, { 'start_time': 1595449800, 'end_time': 1595451600 }, ... ]
# Accepts a list of time slots from find_available_timeslots(), # and a list of email addresses to match availability def eval_timeslots(time_slots, emails): evaluated_timeslots = [] # For each available time slot the user has... for time_slot in time_slots: # Create a key for a percentage representation of availability match between users time_slot["user_match"] = [] time_slot_total = time_slot["end_time"] - time_slot["start_time"] # For each of the email addresses to match availability with... for email in emails: # Use the Nylas Free-Busy endpoint to return availability info free_busy = nylas.free_busy(email, time_slot["start_time"], time_slot["end_time"])[0] # If nothing is returned, it means the user is 100% available if len(free_busy["time_slots"]) == 0: time_slot["user_match"].append({ 'user': email, 'match': 100, }) # Otherwise, calculate the total amount of time the person is busy else: busy_time = 0 for busy_time_slot in free_busy["time_slots"]: busy_time = busy_time_slot['end_time'] - busy_time_slot['start_time'] # Show the availability of the email address as a percent of total time_slot["user_match"].append({ 'user': email, 'match': 100 - int((busy_time / time_slot_total) * 100), }) # Calculate the average availability of all the provided email addresses total = 0 for match in time_slot["user_match"]: total += match["match"] average_match = int(total / len(time_slot["user_match"])) time_slot["average_match"] = average_match evaluated_timeslots.append(time_slot) return evaluated_timeslots
[ { "start_time": 1595430000, "end_time": 1595458800, "user_match": [ { "user": "[email protected]", "match": 100 }, { "user": "[email protected]", "match": 50 } ], "average_match": 75 } ]
# Accepts: A list returned by evaluated_timeslots() def match_availability(evaluated_timeslots): user_availability = {} # For all of the time slots we evaluated earlier... for time_slot in evaluated_timeslots: for user_match in time_slot["user_match"]: if user_match["user"] not in user_availability.keys(): user_availability[user_match["user"]] = [] user_availability[user_match["user"]].append(user_match["match"]) # Return a list of users and their average availability availability = [] for user,matches in user_availability.items(): availability.append({ 'user': user, 'match': sum(matches) / len(matches) }) return availability
[ { "user": "[email protected]", "match": 65 }, { "user": "[email protected]", "match": 22 } ]
# Accepts: A list of time slots that we'll attempt to sequentially schedule the meetings def plan_meetings(time_slots): if len(time_slots) > 0: # Choose the time slot with the highest average availability first # This ensures we start with the most likely possibility. highest_time_slot = {"average_match": 0} for time_slot in time_slots: if time_slot["average_match"] > highest_time_slot["average_match"]: highest_time_slot = time_slot # Sort the users by availability from least to most. # Starting with least available user makes it more likely to find time for everyone sorted_users = sorted(highest_time_slot["user_match"], key=lambda item: item["match"]) # Generate a list of available time slots that are divided into equal lengths available_time_slots = [] for i in range( highest_time_slot["start_time"], highest_time_slot["end_time"] - meeting_length * 60, meeting_length * 60 ): available_time_slots.append({'start_time': i, 'end_time': i + meeting_length * 60}) planned_time_slots = [] planned_users = [] # For each email address in our sorted list... for user in sorted_users: # For each time slot the user is available for time_slot in available_time_slots: # Check the availability for the email address free_busy = nylas.free_busy(user["user"], time_slot["start_time"], time_slot["end_time"])[0] # Schedule the email address in the first available slot if len(free_busy["time_slots"]) == 0: planned_time_slots.append({ 'start_time': time_slot["start_time"], 'end_time': time_slot["end_time"], 'user': user["user"], }) planned_users.append(user["user"]) # Make the slot unavailable after scheduling an event available_time_slots.remove(time_slot) break if user["user"] not in planned_users: print("could not find available time for {}".format(user["user"])) return [] return planned_time_slots else: return []
[ { "start_time": 100000, "end_time": 400000, "user_schedule": [ { "user": "[email protected]", "start_time": 100000, "end_time": 200000 }, { "user": "[email protected]", "start_time": 200000 "end_time": 300000 } ] }, ... ]
# Accepts a list of time slots generated by plan_meetings() def schedule_meetings(planned_time_slots): for planned_time_slot in planned_time_slots: event = nylas.events.create() calendars = nylas.calendars.all() for calendar in calendars: if calendar["name"] == user_email: event.calendar_id = calendar["id"] event.title = "1 on 1 with {}".format(user_email) event.description = "Let's catch up on our current progress" event.busy = True event.participants = [ {'email': user_email}, {'email': planned_time_slot["user"]}, ] event.when = { "start_time": planned_time_slot["start_time"], "end_time": planned_time_slot["end_time"], } event.save() return True
if __name__ == "__main__": start_date,end_date = date_range.split(" - ") start = datetime.datetime.strptime(start_date, "%m/%d/%Y") end = datetime.datetime.strptime(end_date, "%m/%d/%Y") dates = [start + datetime.timedelta(days=x) for x in range(0, (end-start).days + 1)] time_slots = {} planned_time_slots = [] for date in dates: # Find all times the primary user is available available_timeslots = find_available_timeslots(total_time, date, user_email) # Identify the time slots that the provided email addresses are also available evaluated_timeslots = eval_timeslots(available_timeslots, direct_reports) # Identify possible sequential schedules planned_time_slots.append(plan_meetings(evaluated_timeslots)) # Calculate overall email address availability user_availability = match_availability(evaluated_timeslots) # Automatically schedule the meetings for the first available time slot. for planned_time_slot in planned_time_slots: if len(planned_time_slot > 0): schedule_meetings(planned_time_slot) break else: print("Unable to schedule meetings with all email addresses.") # Show availability info so the user can schedule meetings individually print(user_availability)
$ python3 sequential_schedule.py