From ded6ceb8cc7018c7079263ffa257669410d1bfbc Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 16 Jun 2025 00:09:20 +0200 Subject: [PATCH 1/5] fix for https://github.com/python-caldav/caldav/issues/119 - some documentation on how to connect to Google --- docs/source/about.rst | 2 +- docs/source/examples.rst | 17 ++++++ examples/google-django.py | 56 +++++++++++++++++++ examples/google-flask.py | 115 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 docs/source/examples.rst create mode 100644 examples/google-django.py create mode 100644 examples/google-flask.py diff --git a/docs/source/about.rst b/docs/source/about.rst index 87459e61..3b7b2109 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -229,7 +229,7 @@ CalDAV URLs can be quite confusing, some software requires the URL to the calend namespace. * Google - new api: see https://developers.google.com/calendar/caldav/v2/guide. - Please comment in https://github.com/python-caldav/caldav/issues/119 if you manage to connect to Google calendar. + There is some information in https://github.com/python-caldav/caldav/issues/119 on how to connect to Google, and there are two contributed `examples `_ on how to obtain a bearer token and use it in the caldav lbirary. * DAViCal: The caldav URL typically seems to be on the format ``https://your.server.example.com/caldav.php/``, though it depends on how the web server is configured. The primary calendars have URLs like ``https://your.server.example.com/caldav.php/donald/calendar`` and other calendars have names like ``https://your.server.example.com/caldav.php/donald/golfing_calendar``. diff --git a/docs/source/examples.rst b/docs/source/examples.rst new file mode 100644 index 00000000..6ee00647 --- /dev/null +++ b/docs/source/examples.rst @@ -0,0 +1,17 @@ + +========== + Examples +========== + +There is an example directory in the source of the project, some of the examples is written by the maintainer, others are written by other contributors - with or without a bit of brush-up from the maintainer to make things a bit more in line with "the best current practices". I'm striving to make the examples redundant, the documentation should contain all you need to know - but it may be worth having a look into actual code if you're stuck. + +View the examples on `github `_ + +Files currently there: + +* ``basic_usage_examples.py`` - written by the maintainer - contains all you need to know to do basic calendar interactions. Code is regularly tested towards the Radicale server in the unit tests. +* ``get_events_example.py`` - written by Krylov Alexandr, with lots of comments from the maintainer. Code is regularly tested towards the Radicale server in the unit tests. +* ``scheduling_examples.py`` - This is NOT tested, it may or may not work! (I should look into this soon) +* ``sync_examples.py`` - this is "pseudo-code", not intended to work, and hence not tested. I'm also planning on making a HOWTO on how to backup your calendar locally. +* ``google-flask.py`` some flask application reading content from a Google calendar. Contributed by @seidnerj, not tested by the caldav maintainer. +* ``google-django`` - some python code utilizing django allauth library to authenticate towards a Google calendar. Contributed by Abe Hanoka. diff --git a/examples/google-django.py b/examples/google-django.py new file mode 100644 index 00000000..0754c285 --- /dev/null +++ b/examples/google-django.py @@ -0,0 +1,56 @@ +""" +Contributed by Abe Hanoka in https://github.com/python-caldav/caldav/issues/119#issuecomment-2652650972 + +> I got this working using django-allauth. Here's a minimal working example that demonstrates how to connect to Google Calendar via CalDAV using OAuth tokens from django-allauth" + +> Make sure your Google OAuth configuration includes the CalDAV scope: https://www.googleapis.com/auth/calendar + +> This approach lets you leverage django-allauth's token management while using caldav for actual calendar operations. The calendar URL format is https://apidata.googleusercontent.com/caldav/v2/{calendar_id}/events. + +> For the allauth setup, I followed this guide: https://stackoverflow.com/questions/51575127/use-google-api-with-a-token-django-allauth + +This code is not tested by the caldav library maintainer. +""" + +from caldav import DAVClient +from caldav.requests import HTTPBearerAuth +from allauth.socialaccount.models import SocialToken, SocialApp +from google.oauth2.credentials import Credentials + +def get_google_credentials(user): + """Get Google OAuth2 credentials from django-allauth""" + token = SocialToken.objects.filter( + account__user=user, + account__provider="google", + ).first() + + if not token: + raise Exception("No Google account connected") + + google = SocialApp.objects.get(provider="google") + return Credentials( + token=token.token, + refresh_token=token.token_secret, + token_uri="https://oauth2.googleapis.com/token", + client_id=google.client_id, + client_secret=google.secret, + ) + +def sync_calendar(user, calendar_id): + """Sync with Google Calendar using CalDAV""" + # Get credentials from django-allauth + credentials = get_google_credentials(user) + + # Set up CalDAV client with OAuth token + client = DAVClient( + url=f"https://apidata.googleusercontent.com/caldav/v2/{calendar_id}/events", + auth=HTTPBearerAuth(credentials.token) + ) + + # Access calendar + principal = client.principal() + calendar = principal.calendars()[0] + + # Now you can work with events + events = calendar.events() + # ...etc diff --git a/examples/google-flask.py b/examples/google-flask.py new file mode 100644 index 00000000..6d7ceed7 --- /dev/null +++ b/examples/google-flask.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +""" +Code contributed by github user seidnerj in +https://github.com/python-caldav/caldav/issues/119#issuecomment-2561980368 + +This code has not been tested by the maintainer of the caldav library. +""" + +import os + +from flask import Flask, jsonify, Response +from google.oauth2.credentials import Credentials +from caldav import DAVClient +from caldav.requests import HTTPBearerAuth + + +app = Flask(__name__) + +# Constants +CREDENTIALS_FILE = 'credentials.json' +TOKEN_FILE = 'token.json' +CALDAV_URL_TEMPLATE = 'https://apidata.googleusercontent.com/caldav/v2/{calendar_id}/user' + +# Cache to store calendar summaries and IDs +CALENDAR_CACHE = {} + + +def get_google_credentials(): + """ + Load or refresh Google OAuth2 credentials. + """ + + if os.path.exists(TOKEN_FILE): + creds = Credentials.from_authorized_user_file(TOKEN_FILE) + else: + from google_auth_oauthlib.flow import InstalledAppFlow + flow = InstalledAppFlow.from_client_secrets_file( + CREDENTIALS_FILE, scopes=['https://www.googleapis.com/auth/calendar']) + creds = flow.run_local_server(port=0) + + # save credentials for future use + with open(TOKEN_FILE, 'w') as token: + token.write(creds.to_json()) + + return creds + + +def get_calendar_list(): + """ + Fetch the list of calendars and store them in the cache. + """ + + creds = get_google_credentials() + from googleapiclient.discovery import build + service = build('calendar', 'v3', credentials=creds) + + calendars = service.calendarList().list().execute() + for calendar in calendars.get('items', []): + CALENDAR_CACHE[calendar['summary']] = calendar['id'] + + +@app.route('/calendars', methods=['GET']) +def list_calendars(): + """ + Endpoint to list all available calendars. + """ + if not CALENDAR_CACHE: + get_calendar_list() + + return jsonify({'calendars': list(CALENDAR_CACHE.keys())}) + + +@app.route('/calendar/.ics', methods=['GET']) +def serve_calendar_ics(calendar_name): + """ + Endpoint to serve .ics data for a specific calendar. + """ + if calendar_name not in CALENDAR_CACHE: + return jsonify({'error': 'Calendar not found'}), 404 + + calendar_id = CALENDAR_CACHE[calendar_name] + creds = get_google_credentials() + access_token = creds.token + + try: + calendar_url = CALDAV_URL_TEMPLATE.format(calendar_id=calendar_id) + + # connect to the calendar using CalDAV + client = DAVClient( + url=calendar_url, + auth=HTTPBearerAuth(access_token) + ) + principal = client.principal() + calendars = principal.calendars() + + # fetch events from the first calendar (usually the only one) + calendar = calendars[0] + ics_data = '' + for event in calendar.events(): + ics_data += event.data + + # serve the calendar as an ICS file + return Response(ics_data, mimetype='text/calendar') + except Exception as ex: + return jsonify({'error': str(ex)}), 500 + + +# requirements: flask, caldav, google-auth, google-auth-oauthlib, google-api-python-client +if __name__ == '__main__': + + # preload calendar list on server start + get_calendar_list() + app.run(host='0.0.0.0', port=5000) + From 3400a8c758949880af7258da849755274ee48774 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 16 Jun 2025 00:11:18 +0200 Subject: [PATCH 2/5] style --- examples/google-django.py | 17 ++++++++++------- examples/google-flask.py | 7 ++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/google-django.py b/examples/google-django.py index 0754c285..48d76861 100644 --- a/examples/google-django.py +++ b/examples/google-django.py @@ -11,11 +11,13 @@ This code is not tested by the caldav library maintainer. """ +from allauth.socialaccount.models import SocialApp +from allauth.socialaccount.models import SocialToken +from google.oauth2.credentials import Credentials from caldav import DAVClient from caldav.requests import HTTPBearerAuth -from allauth.socialaccount.models import SocialToken, SocialApp -from google.oauth2.credentials import Credentials + def get_google_credentials(user): """Get Google OAuth2 credentials from django-allauth""" @@ -23,7 +25,7 @@ def get_google_credentials(user): account__user=user, account__provider="google", ).first() - + if not token: raise Exception("No Google account connected") @@ -36,21 +38,22 @@ def get_google_credentials(user): client_secret=google.secret, ) + def sync_calendar(user, calendar_id): """Sync with Google Calendar using CalDAV""" # Get credentials from django-allauth credentials = get_google_credentials(user) - + # Set up CalDAV client with OAuth token client = DAVClient( url=f"https://apidata.googleusercontent.com/caldav/v2/{calendar_id}/events", - auth=HTTPBearerAuth(credentials.token) + auth=HTTPBearerAuth(credentials.token), ) - + # Access calendar principal = client.principal() calendar = principal.calendars()[0] - + # Now you can work with events events = calendar.events() # ...etc diff --git a/examples/google-flask.py b/examples/google-flask.py index 6d7ceed7..517d7aca 100644 --- a/examples/google-flask.py +++ b/examples/google-flask.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 - """ Code contributed by github user seidnerj in https://github.com/python-caldav/caldav/issues/119#issuecomment-2561980368 This code has not been tested by the maintainer of the caldav library. """ - import os -from flask import Flask, jsonify, Response +from flask import Flask +from flask import jsonify +from flask import Response from google.oauth2.credentials import Credentials + from caldav import DAVClient from caldav.requests import HTTPBearerAuth From 0118c799531eca5985998f9762504fabdc7b512a Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 16 Jun 2025 00:16:07 +0200 Subject: [PATCH 3/5] foo --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8656dbb..90a9f685 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ### Added +* Documentation has been through a major overhaul. * `event.component` is now an alias for `event.icalendar_component`. * `get_davclient` (earlier called `auto_conn`) is more complete now - https://github.com/python-caldav/caldav/pull/502 - https://github.com/python-caldav/caldav/issues/485 - https://github.com/python-caldav/caldav/pull/507 * It can read from environment (including environment variable for reading from test config and for locating the config file). From cf7842d318c15c495c15461686ccac039afc79f0 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 16 Jun 2025 00:20:53 +0200 Subject: [PATCH 4/5] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a9f685..204a33c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project should adhere to [Semantic Versioning](https://semver.org/spec/v2.0 ### Added * Documentation has been through a major overhaul. +* Added some information on how to connect to Google in the doc and examples. * `event.component` is now an alias for `event.icalendar_component`. * `get_davclient` (earlier called `auto_conn`) is more complete now - https://github.com/python-caldav/caldav/pull/502 - https://github.com/python-caldav/caldav/issues/485 - https://github.com/python-caldav/caldav/pull/507 * It can read from environment (including environment variable for reading from test config and for locating the config file). From 9d5e45a54d5d59d88b7fd8f63b56562391a27122 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Mon, 16 Jun 2025 00:22:01 +0200 Subject: [PATCH 5/5] style --- examples/google-flask.py | 46 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/google-flask.py b/examples/google-flask.py index 517d7aca..8640493b 100644 --- a/examples/google-flask.py +++ b/examples/google-flask.py @@ -19,9 +19,11 @@ app = Flask(__name__) # Constants -CREDENTIALS_FILE = 'credentials.json' -TOKEN_FILE = 'token.json' -CALDAV_URL_TEMPLATE = 'https://apidata.googleusercontent.com/caldav/v2/{calendar_id}/user' +CREDENTIALS_FILE = "credentials.json" +TOKEN_FILE = "token.json" +CALDAV_URL_TEMPLATE = ( + "https://apidata.googleusercontent.com/caldav/v2/{calendar_id}/user" +) # Cache to store calendar summaries and IDs CALENDAR_CACHE = {} @@ -36,12 +38,14 @@ def get_google_credentials(): creds = Credentials.from_authorized_user_file(TOKEN_FILE) else: from google_auth_oauthlib.flow import InstalledAppFlow + flow = InstalledAppFlow.from_client_secrets_file( - CREDENTIALS_FILE, scopes=['https://www.googleapis.com/auth/calendar']) + CREDENTIALS_FILE, scopes=["https://www.googleapis.com/auth/calendar"] + ) creds = flow.run_local_server(port=0) # save credentials for future use - with open(TOKEN_FILE, 'w') as token: + with open(TOKEN_FILE, "w") as token: token.write(creds.to_json()) return creds @@ -54,14 +58,15 @@ def get_calendar_list(): creds = get_google_credentials() from googleapiclient.discovery import build - service = build('calendar', 'v3', credentials=creds) + + service = build("calendar", "v3", credentials=creds) calendars = service.calendarList().list().execute() - for calendar in calendars.get('items', []): - CALENDAR_CACHE[calendar['summary']] = calendar['id'] + for calendar in calendars.get("items", []): + CALENDAR_CACHE[calendar["summary"]] = calendar["id"] -@app.route('/calendars', methods=['GET']) +@app.route("/calendars", methods=["GET"]) def list_calendars(): """ Endpoint to list all available calendars. @@ -69,16 +74,16 @@ def list_calendars(): if not CALENDAR_CACHE: get_calendar_list() - return jsonify({'calendars': list(CALENDAR_CACHE.keys())}) + return jsonify({"calendars": list(CALENDAR_CACHE.keys())}) -@app.route('/calendar/.ics', methods=['GET']) +@app.route("/calendar/.ics", methods=["GET"]) def serve_calendar_ics(calendar_name): """ Endpoint to serve .ics data for a specific calendar. """ if calendar_name not in CALENDAR_CACHE: - return jsonify({'error': 'Calendar not found'}), 404 + return jsonify({"error": "Calendar not found"}), 404 calendar_id = CALENDAR_CACHE[calendar_name] creds = get_google_credentials() @@ -88,29 +93,24 @@ def serve_calendar_ics(calendar_name): calendar_url = CALDAV_URL_TEMPLATE.format(calendar_id=calendar_id) # connect to the calendar using CalDAV - client = DAVClient( - url=calendar_url, - auth=HTTPBearerAuth(access_token) - ) + client = DAVClient(url=calendar_url, auth=HTTPBearerAuth(access_token)) principal = client.principal() calendars = principal.calendars() # fetch events from the first calendar (usually the only one) calendar = calendars[0] - ics_data = '' + ics_data = "" for event in calendar.events(): ics_data += event.data # serve the calendar as an ICS file - return Response(ics_data, mimetype='text/calendar') + return Response(ics_data, mimetype="text/calendar") except Exception as ex: - return jsonify({'error': str(ex)}), 500 + return jsonify({"error": str(ex)}), 500 # requirements: flask, caldav, google-auth, google-auth-oauthlib, google-api-python-client -if __name__ == '__main__': - +if __name__ == "__main__": # preload calendar list on server start get_calendar_list() - app.run(host='0.0.0.0', port=5000) - + app.run(host="0.0.0.0", port=5000)