Coding own application for Gnome-shell Calendar / Evolution calendar

I'm looking to write a calendar application in python that will interact with gnome-shell-calendar. I've asked around, and I was told that it uses evolution-data-sever

To get its information, I found out that there is a python-evolution python module that allows you to interact with the evolution server. But, that module has now been depreciated. Is-there another way to interact with the sever?

I've also noticed a process called gnome-shell-calendar-server. What's The difference between that and the evolution one?


Solution 1:

Evolution Data Server 3.6 can be accessed with Python using gobject introspection. For this, gir1.2-edataserver-1.2 and gir1-2-ecalendar-1.2 also need to be installed.

For example, the following script will list all the events in all calendars in evolution-data-server.

#! /usr/bin/python
# -*- coding: utf-8 -*-

from gi.repository import ECalendar
from gi.repository import EDataServer

# Open a registry and get a list of all the calendars in EDS
registry = EDataServer.SourceRegistry.new_sync(None)
sources = EDataServer.SourceRegistry.list_sources(registry, EDataServer.SOURCE_EXTENSION_CALENDAR)

# Open each calendar containing events and get a list of all objects in them
for source in sources:
    client = ECalendar.CalClient.new(source, ECalendar.CalSourceType.EVENT)
    client.open_sync(False, None)

    # ret is true or false depending if events are found or not
    # values is a list of events
    ret, values = client.get_object_list_as_comps_sync("#t", None)
    if ret:
        for value in values:
            event = value.get_as_string()
            print event

Solution 2:

Mark's answer was probably good in 2012, but it's now a bit outdated. I wanted to play with this and managed to do it after digging on my own.

Packages

This was tested on Ubuntu 20.04 with the following packages installed:

sudo apt update
sudo apt install -y \
    gir1.2-ecalendar-1.2 \
    gir1.2-ecal-2.0

Conda environment setup (optional)

If you use your system's python, you'll only need pgi pip package. Here's how to get it working with Conda:

  1. I used conda to create my virtual env so I installed pgi pip package
  2. I installed libgirepository and pygobject using conda-forge.
  3. I symlinked required typelib files from gir1.2-ecalendar-1.2 and gir1.2-ecal-2.0 to my conda env's girepository's folder exactly like this:
#!/usr/bin/env bash

set -ex

girepository_source=/usr/lib/x86_64-linux-gnu/girepository-1.0
conda_env_name=evolution-calendar-playground
conda_girespository_destination=${CONDA_PREFIX}/envs/${conda_env_name}/lib/girepository-1.0/

# setup conda environment
conda activate ${conda_env_name}
pip install pgi
conda install -c conda-forge libgirepository -y
conda install -c conda-forge pygobject -y

# compare the girepository folders
diff -rq "${girepository_source}" "${conda_girespository_destination}"

# link only required typelib files
ln -s "${girepository_source}/EDataServer-1.2.typelib" "${conda_girespository_destination}"
ln -s "${girepository_source}/Soup-2.4.typelib" "${conda_girespository_destination}"
ln -s "${girepository_source}/Camel-1.2.typelib" "${conda_girespository_destination}"
ln -s "${girepository_source}/GData-0.0.typelib" "${conda_girespository_destination}"
ln -s "${girepository_source}/Json-1.0.typelib" "${conda_girespository_destination}"
ln -s "${girepository_source}/Goa-1.0.typelib" "${conda_girespository_destination}"
ln -s "${girepository_source}/ECal-2.0.typelib" "${conda_girespository_destination}"
ln -s "${girepository_source}/ICalGLib-3.0.typelib" "${conda_girespository_destination}"

# alternative version of the above, link all files that are missing
typelibs_not_in_conda_env=$(
    diff -rq "${girepository_source}" "${conda_girespository_destination}" \
    | grep "Only in ${girepository_source}" \
    | cut -d " " -f 4-
    | tr "\n" " "
)

for typelib in $(echo ${typelibs_not_in_conda_env} | tr " " "\n"); do
    ln -s "${girepository_source}/${typelib}" "${conda_girespository_destination}"
done

# compare the girepository folders (again)
diff -rq "${girepository_source}" "${conda_girespository_destination}"

Updated python example to retrieve all events from all calendars in Evolution Data Server

Here's an updated python script that uses gobject introspection (it's highly based on Mark's example). It does the same as before, but written differently and updated to use gir1.2-ecal-2.0 instead of gir1.2-ecalendar-1.2.

I've linked some documentation to make it easier to know what's going on and added a bunch of print statements since these libraries are dynamically loaded and it's a bit harder to get intellicense on them. Feel free to cleanup everything once you have what you need.

Also, according to the docs, I think some of the connections I open should be cleaned and I'm not doing it, but you should :)

import gi

gi.require_version('EDataServer', '1.2')
from gi.repository import EDataServer

gi.require_version('ECal', '2.0')
from gi.repository import ECal

from gi.repository import Gio

# https://lazka.github.io/pgi-docs/Gio-2.0/classes/Cancellable.html#Gio.Cancellable
GIO_CANCELLABLE = Gio.Cancellable.new()


class EvolutionCalendarWrapper:
    @staticmethod
    def _get_gnome_calendars():
        # https://lazka.github.io/pgi-docs/EDataServer-1.2/classes/SourceRegistry.html#EDataServer.SourceRegistry.new_sync
        registry = EDataServer.SourceRegistry.new_sync(GIO_CANCELLABLE)
        return EDataServer.SourceRegistry.list_sources(registry, EDataServer.SOURCE_EXTENSION_CALENDAR)

    def _get_gnome_events_from_calendar_source(self, source: EDataServer.Source):
        print(source.get_display_name())

        # https://lazka.github.io/pgi-docs/ECal-2.0/classes/Client.html#ECal.Client
        client = ECal.Client()

        # https://lazka.github.io/pgi-docs/ECal-2.0/classes/Client.html#ECal.Client.connect_sync
        new_client = client.connect_sync(
            source=source,
            source_type=ECal.ClientSourceType.EVENTS,
            wait_for_connected_seconds=1,  # this should probably be configured
            cancellable=GIO_CANCELLABLE,
        )

        if new_client:
            events = []
            # https://lazka.github.io/pgi-docs/ECal-2.0/classes/Client.html#ECal.Client.get_object_list_as_comps_sync
            ret, values = new_client.get_object_list_as_comps_sync(sexp="#t", cancellable=GIO_CANCELLABLE)
            if ret:
                for value in values:
                    print(value.get_status())
                    print(value.get_location())
                    print(value.get_descriptions())
                    print("due timestamp:", value.get_due().get_value().as_timet())
                    start_time_value = value.get_dtstart().get_value()
                    print("start_date", start_time_value.get_date())
                    print("start_time", start_time_value.get_time())
                    print("timestamp:", start_time_value.as_timet())
                    print("as_timet_with_zone:", start_time_value.as_timet_with_zone())
                    print("timezone:", start_time_value.get_timezone())
                    # event_str = value.get_as_string()
                    # print(event_str)
                    event = value
                    events.append(event)
        return events

    def get_all_events(self):
        calendars = self._get_gnome_calendars()
        events = []
        for source in calendars:
            events += self._get_gnome_events_from_calendar_source(source)
        return events


def main():
    evolutionCalendar = EvolutionCalendarWrapper()
    events = evolutionCalendar.get_all_events()
    for event in events:
        print(event)


if __name__ == "__main__":
    main()

Now extra caution with what you do with the events out there as you can also edit them, don't break your calendar ;)