Calendee

my own Python-powered Calendly alternative

Open-sourced at:

Availabilities

13 Feb 2023

Goal:
- insert my availabilities as text in an email.
- ability to select timezone of the recipient (method TBC)
- combines availabilities across my 2 pro calendars (both using Google Calendar)

Output should be like:

Germany
- Wed 15 Feb 10:00
- Wed 15 Feb 11:00
- Fri 17 Feb 10:00

with date/time format adapted based on recipient's timezone:

PT
- Wed Feb 15 10am
- Wed Feb 15 11am
- Fri 17 Feb 3pm

so:

{recipient_timezone}
- {date} {time}
- {date} {time}
- {date} {time}

Timezone selection passed as a parameter to the script.

v2:
- "tomorrow" when applicable (instead of full date)

Process

Authorisation works by creating a token file using the Credentials JSON file downloaded from Google.

Steps:

  • Access https://console.cloud.google.com/apis
  • Create a new project
  • Enable the Google Calendar API
  • Create a new OAuth client ID
    calendaree/google-cloud-calendaree-dv-oauth.jpg
  • Download the credentials JSON file
  • save it as credentials.json in the same folder as quickstart.py (next step)
  • Create a quickstart.py with below code, run, authenticate in browser, create the token file.

  • Create a service_key.json at https://console.cloud.google.com/iam-admin/serviceaccounts/create
    In the menu, click on "APIs & Services" > "Credentials."
    Click on "Create credentials" and choose "Service account."
    Enter a name for the service account, and optionally, add a description. Click "Create."
    Optionally, you can grant the service account a role with specific permissions. Click "Continue."
    Click "Done" to create the service account.
    In the "Service Accounts" section, find the service account you just created and click on its name.
    In the "Keys" tab, click on "Add Key" and choose "JSON."
    The JSON key file will be generated and downloaded to your computer. This is your service_key.json file.

# quickstart.py

from __future__ import print_function

import datetime
import os.path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']


def main():
    """Shows basic usage of the Google Calendar API.
    Prints the start and name of the next 10 events on the user's calendar.
    """
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.json', 'w') as token:
            token.write(creds.to_json())

    try:
        service = build('calendar', 'v3', credentials=creds)

        # Call the Calendar API
        now = datetime.datetime.utcnow().isoformat() + 'Z'  # 'Z' indicates UTC time
        print('Getting the upcoming 10 events')
        events_result = service.events().list(calendarId='primary', timeMin=now,
                                              maxResults=10, singleEvents=True,
                                              orderBy='startTime').execute()
        events = events_result.get('items', [])

        if not events:
            print('No upcoming events found.')
            return

        # Prints the start and name of the next 10 events
        for event in events:
            start = event['start'].get('dateTime', event['start'].get('date'))
            print(start, event['summary'])

    except HttpError as error:
        print('An error occurred: %s' % error)


if __name__ == '__main__':
    main()

Initial basic code works as follows:

from datetime import datetime, time, timezone
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials

def get_future_events(calendar_id, token):
    # Set up Google API credentials
    creds = Credentials.from_authorized_user_file(token, ['https://www.googleapis.com/auth/calendar'])

    # Build the Google Calendar API client
    service = build('calendar', 'v3', credentials=creds)

    # Get the current time in UTC
    now = datetime.utcnow().isoformat() + 'Z'  # 'Z' indicates UTC time

    # Call the Google Calendar API to get events
    events_result = service.events().list(
        calendarId=calendar_id,
        timeMin=now,
        singleEvents=True,
        orderBy='startTime'
    ).execute()

    # Get the events from the response
    events = events_result.get('items', [])

    # Filter out events that have already ended
    future_events = [event for event in events if datetime.fromisoformat(event['end'].get('dateTime', event['end'].get('date'))) > datetime.now(timezone.utc)]

    # Return the list of future events
    return future_events

Note: token is the path to the token file created by the quickstart.py script.

16 Feb 2023

Basic logic is working.
Some troubleshooting left re removing available time when events span more than 1 hour + formatting of output, currently coming out as:

- tomorrow 12:00
- tomorrow 17:00
- Mon 20th 12:00
- Tue 21st 17:00
- Wed 22nd 12:00
- Thu 23rd 12:00

working code

See:

variables

are available_days, available_hours, timezones, slot & weekdays_forward:

slot = 30 # minutes

# How many weekdays forward to check for availability
weekdays_forward = 3 

available_days = [ # comment lines below to make unavailable
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri",
]

available_hours = [ # comment lines below to make unavailable
    # "08:00",
    # "09:00",
    # "10:00",
    "11:00",
    # "11:30",
    "12:00",
    # "13:00",
    # "14:00",
    # "15:00",
    "16:00",
    # "16:30",
    "17:00",
    # "18:00",
    # "19:00",
]


timezones = { # This will define the return time & format
    "CET": 0, # default
    "UK": 1, # timezone offset in hours
    "ET": 6,
    "MT": 7,
    "PT": 9,
}

outputs

Standard:

(CET / Germany time)

- Mon  6th 12:00
- Tue  7th 11:00, 12:00 or 17:00
- Wed  8th 11:00, 12:00, 16:00 or 17:00

or see all at https://cal.com/ndeville

UK:

(UK time)

- Mon  6th 11am
- Tue  7th 10am, 11am or 4pm
- Wed  8th 10am, 11am, 3pm or 4pm

or see all at https://cal.com/ndeville

PT:

(PT)

- Mon  6th 3am
- Tue  7th 2am, 3am or 8am
- Wed  8th 2am, 3am, 7am or 8am

or see all at https://cal.com/ndeville

Meetings

Generates list of past meetings objects when external attendees present, for ingestion anywhere (eg. CRM).

Each meeting returned as:

class Meeting:
    def __init__(self):
        self.id = '' # from Google
        self.summary = ''
        self._date = '' # from Google 'start', in YYYY-MM-DD format
        self.htmlLink = ''
        self.attendees = set() # unique emails from both 'attendees' and 'organiser'
        self.description = ''
        self.domain = '' # WARNING: this caters only for 1 domain per meeting

See:

Issues

Token (RESOLVED)

Token needs to be refreshed every day?
Need to find how to configure for perpetual use, or automate.

Seems like using a "Service Account" with "Domain Wide Delegation" is the way to go: https://medium.com/@ArchTaqi/google-calendar-api-in-your-application-without-oauth-consent-screen-4fcc1f8eb380

calendaree/230215-2026-google-admin-domain-wide-delegation.jpg

Solved with:

  • create Service account & download Service key as '.json' file (see Medium article above)
  • apply Domain Wide Delegation to the Service account (see Google Admin article above)
  • use following code:
from google.oauth2 import service_account

SCOPES = ['https://www.googleapis.com/auth/calendar']
credentials = service_account.Credentials.from_service_account_file(token, scopes=SCOPES)
creds = credentials.with_subject(calendar_id)
service = build('calendar', 'v3', credentials=creds)

where token is the path to the Service key file, and calendar_id is the email address of the Google Calendar.

links

social