python-caldav / caldav

Apache License 2.0
314 stars 94 forks source link

Google calendar - make authentication simpler and document it #311

Open tobixen opened 1 year ago

tobixen commented 1 year ago

@flozz wrote a blog post on https://blog.lasall.dev/post/tell-me-why-google-and-caldav/ on how to connect to Google - I (or someone) should pick up on it and try to make it more convenient to use the caldav library.

The google_*-python libraries should be an optional dependency, they should not be added to the requirement list, but I think it's OK to load them when needed.

flozz commented 1 year ago

Note: I am not the author of this blog post. My message in #310 is probably not clear enough 😅️

But I may write a post on this subject one day on my own blog 😁️

blopker commented 1 year ago

For reference in case the blog goes down:

#!/usr/bin/env python3
import pickle
import os
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from requests.auth import AuthBase
import caldav

class OAuth(AuthBase):
    def __init__(self, credentials):
        self.credentials = credentials

    def __call__(self, r):
        self.credentials.apply(r.headers)
        return r

SCOPES = ['https://www.googleapis.com/auth/calendar']

creds = None
# The file token.pickle 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.pickle'):
    with open('token.pickle', 'rb') as token:
        creds = pickle.load(token)
# 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.pickle', 'wb') as token:
        pickle.dump(creds, token)

calid = "addressbook%23contacts@group.v.calendar.google.com"
url = "https://apidata.googleusercontent.com/caldav/v2/" + calid + "/events"
client = caldav.DAVClient(url, auth=OAuth(creds))

for calendar in client.principal().calendars():
    events = calendar.events()
    for event in events:
        e = event.instance.vevent
        eventTime = e.dtstart.value.strftime("%c")
        eventSummary = e.summary.value
        print("==================================")
        print(f"Event:    {eventSummary}")
        print(f"Time:     {eventTime}")

Additionally, this is how you use a service account instead of going through the user oauth flow (which I needed):

from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2 import service_account
from requests.auth import AuthBase
import caldav
import json

SERVICE_ACCOUNT_FILE = "service.json"

class OAuth(AuthBase):
    def __init__(self, credentials):
        self.credentials = credentials

    def __call__(self, r):
        self.credentials.apply(r.headers)
        return r

SCOPES = ["https://www.googleapis.com/auth/calendar"]

creds = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
creds.refresh(Request())

calid = "{ID}@group.calendar.google.com"
url = "https://apidata.googleusercontent.com/caldav/v2/" + calid + "/events"

client = caldav.DAVClient(url, auth=OAuth(creds))

for calendar in client.principal().calendars():
    events = calendar.events()
    for event in events:
        e = event.instance.vevent
        eventTime = e.dtstart.value.strftime("%c")
        eventSummary = e.summary.value
        print("==================================")
        print(f"Event:    {eventSummary}")
        print(f"Time:     {eventTime}")

The key here is that you get the oauth token (creds.token) by calling refresh on creds before you make the caldav client. You can store the result of the refresh to make the process a bit faster.

creds = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
creds.refresh(Request())