diff --git a/CHANGELOG.md b/CHANGELOG.md index a8656dbb..204a33c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ 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). 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..48d76861 --- /dev/null +++ b/examples/google-django.py @@ -0,0 +1,59 @@ +""" +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 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 + + +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..8640493b --- /dev/null +++ b/examples/google-flask.py @@ -0,0 +1,116 @@ +#!/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 +from flask import jsonify +from flask import 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)