prof-rossetti / intro-to-python

An Introduction to Programming in Python
Other
97 stars 244 forks source link

Google Calendar Integration #108

Open s2t2 opened 2 years ago

s2t2 commented 2 years ago

Google Calendar API

Using the google-api-python-client Package

Reference

Google Calendar API Docs:

Installation

pip install google-api-python-client
pip install oauth2client==4.1.3

NOTE: apparently oauth2client is going away, and in order for this code to work, you have to use python 3.7 (not a more recent version). So ideally these docs will be updated to use the google-auth package instead. Contributions welcome!

Setup

After creating a new Google Cloud project via the Google Cloud APIs console, and enabling the Google Calendar API...

Create service account credentials for this project, and download the resulting JSON key file into the root directory of this repo, for example named "google-credentials.json".

If you want to use the environment variable approach, from the root directory of this repo, set the credentials as an environment variable:

export GOOGLE_API_CREDENTIALS="$(< google-credentials.json)"
echo $GOOGLE_API_CREDENTIALS

Usage


# adapted from / reference:
# ... https://developers.google.com/calendar/quickstart/python
# ... https://github.com/googleworkspace/python-samples/blob/master/calendar/quickstart/quickstart.py
# ... https://bitbucket.org/kingmray/django-google-calendar/src/master/calendar_api/calendar_api.py

import os
import json
from pprint import pprint
#from datetime import datetime
#import datetime
#import pytz

from googleapiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials

GOOGLE_API_CREDENTIALS = os.getenv("GOOGLE_API_CREDENTIALS")
CREDS_JSON = json.loads(GOOGLE_API_CREDENTIALS)

#CREDS_FILEPATH = os.path.join(os.path.dirname(__file__), "auth", "google-credentials.json")
CREDS_FILEPATH = os.path.join(os.path.dirname(__file__), "..", "google-credentials.json")

# see: # https://developers.google.com/identity/protocols/oauth2/scopes#calendar
AUTH_SCOPES = [
    "https://www.googleapis.com/auth/calendar", # read/write access to Calendars
    #"https://www.googleapis.com/auth/calendar.readonly", # read-only access to Calendars
    #"https://www.googleapis.com/auth/calendar.events", # read/write access to Events
    #"https://www.googleapis.com/auth/calendar.events.readonly", # read-only access to Events
    #"https://www.googleapis.com/auth/calendar.settings.readonly", # read-only access to Settings
    #"https://www.googleapis.com/auth/calendar.addons.execute", # run as a Calendar add-on

    #"https://www.googleapis.com/auth/gmail.send",
    #"https://www.googleapis.com/auth/admin.directory.resource.calendar"
]

if __name__ == "__main__":

    #credentials = ServiceAccountCredentials._from_parsed_json_keyfile(CREDS_JSON, AUTH_SCOPES)
    credentials = ServiceAccountCredentials.from_json_keyfile_name(CREDS_FILEPATH, AUTH_SCOPES)

    client = build("calendar", "v3", credentials=credentials)

    print("------------")
    print("CALENDARS:")
    calendars = client.calendarList().list().execute()
    pprint(calendars)

    print("------------")
    if any(calendars):
        calendar_id = input("PLEASE CHOOSE A CALENDAR ID: ") or calendars["items"][0]["id"]
        # https://developers.google.com/calendar/v3/reference/calendars/get
        calendar = client.calendars().get(calendarId=calendar_id).execute() #> dict
        pprint(calendar)

    else:
        calendar_info = {
            "summary": "Example Calendar",
            "description": "Used for development and testing purposes",
            "timeZone": "US/Eastern",
        }
        print(calendar_info)
        # https://developers.google.com/calendar/v3/reference/calendars/insert#python
        # https://developers.google.com/calendar/v3/reference/calendars#resource
        calendar = client.calendars().insert(body=calendar_info).execute() #> dict
        breakpoint()
s2t2 commented 2 years ago

User Provided Credentials

If you are using a flask app with google login routes like these...

1) you'll need to update the google login route to store the token in the session, so we can use it later. do this around line 57:

session["bearer_token"] = token

2) add some new routes, like these, called "gcal_routes.py" in the "routes" directory:


import datetime
from dateutil.parser import parse as to_dt # converts string to datetime object
#from pprint import pprint

from flask import Blueprint, render_template, session, flash, redirect, current_app, request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

from web_app.routes.wrappers import authenticated_route

gcal_routes = Blueprint("gcal_routes", __name__)

def format_timestamp(dt):
    """Formats datetime object for storing in Google Calendar."""
    return dt.strftime("%Y-%m-%dT%H:%M:%S")

def user_authorized_gcal_client():
    """
        Access user's calendars on their behalf via their login credentials.
        See: https://google-auth.readthedocs.io/en/stable/user-guide.html#user-credentials
    """
    bearer_token = session.get("bearer_token")
    print("BEARER TOKEN:", bearer_token.keys())
    access_token = bearer_token["access_token"] # or user.access_token

    credentials = Credentials(access_token)
    client = build("calendar", "v3", credentials=credentials)
    return client

@gcal_routes.route("/reservations/export/gcal", methods=["POST"])
@authenticated_route
def export_reservation_to_gcal():
    form_data = dict(request.form)
    print("FORM DATA:", form_data)
    redirect_path = form_data["redirect_path"]  # for page navigation
    event_start = form_data["event_start"]  #> '2021-08-10 11:00:00-04:00'
    event_end = form_data["event_end"]      #> '2021-08-10 11:45:00-04:00'
    time_zone = form_data["time_zone"]      #> 'America/New_York'

    try:
        client = user_authorized_gcal_client()
        calendars = client.calendarList().list().execute()
        print("GCALS:", [(cal["id"], cal["summary"], cal["timeZone"]) for cal in calendars])

        return render_template("gcal_calendar_selection_form.html",
            calendars=calendars,
            event_start=event_start,
            event_end=event_end,
            time_zone=time_zone,
            redirect_path=redirect_path,
        )
    except Exception as err:
        print(err)
        flash(f"Oops, something went wrong with your google credentials... {err} Try logging out and back in again.", "danger")
        return redirect(redirect_path)

@gcal_routes.route("/reservations/export/gcal/create", methods=["POST"])
@authenticated_route
def create_gcal_event():
    form_data = dict(request.form)
    print("FORM DATA:", form_data)
    calendar_id = form_data["calendar_id"]
    event_start = form_data["event_start"]  #> '2021-08-10 11:00:00-04:00'
    event_end = form_data["event_end"]      #> '2021-08-10 11:45:00-04:00'
    time_zone = form_data["time_zone"]
    redirect_path = form_data["redirect_path"]

    try:
        event_start_timestamp = format_timestamp(to_dt(event_start))
        event_end_timestamp = format_timestamp(to_dt(event_end))

        event_data = {
            'summary': "Gym Time",
            'description': "This event was generated by Our Gym Calendar App.",
            'start': {'dateTime': event_start_timestamp, 'timeZone': time_zone},
            'end': {'dateTime':  event_end_timestamp, 'timeZone': time_zone},
            #"attendees": [{"email": email}],
            #"source": {"title": source_title, "url": source_url},
            "visibility": "default" # "private"
        }

        client = user_authorized_gcal_client()

        # https://developers.google.com/calendar/api/guides/create-events
        # https://developers.google.com/calendar/v3/reference/events/insert
        # https://developers.google.com/calendar/concepts/sharing
        # https://developers.google.com/calendar/auth#perform-g-suite-domain-wide-delegation-of-authority
        # Service accounts cannot invite attendees without Domain-Wide Delegation of Authority.
        # https://developers.google.com/admin-sdk/directory/v1/guides/delegation
        event = client.events().insert(calendarId=calendar_id, body=event_data).execute()
        print("GCAL EVENT:", event)

        flash("Reservation exported to Google Calendar!", "success")
        return redirect(redirect_path)
    except Exception as err:
        print(err)
        flash(f"Oops, something went wrong... {err}", "danger")
        return redirect(redirect_path)

3) add a new HTML form to send the original POST Request to "/reservations/export/gcal", with inputs named event_start, event_end, and timezone

4) add a new HTML dropdown for the calendar selection:


{% extends "bootstrap_5_layout.html" %}
{% set active_page = "gcal_calendar_selection" %}

{% block content %}

    <h1>Select a Google Calendar</h1>

    <p>Export this reservation to which calendar?</p>

    <form method="POST" action="/reservations/export/gcal/create">
        <input type="hidden" name="event_start" value="{{ event_start }}">
        <input type="hidden" name="event_end" value="{{ event_end }}">
        <input type="hidden" name="time_zone" value="{{ time_zone }}">
        <input type="hidden" name="redirect_path" value="{{ redirect_path }}">

        <!-- see: https://getbootstrap.com/docs/5.0/forms/select/ -->
        <select name="calendar_id" id="google-calendar-selector" class="form-select form-select-lg mb-3">
            {% for calendar in calendars %}
                <option value="{{ calendar['id'] }}"> {{ calendar['summary'] }}</option>
            {% endfor %}
        </select>

        <button type="submit" class="btn btn-outline-success">Create Event</button>
    </form>

{% endblock %}