en fr

Periodically import an ical calendar into owncloud

Posted on 2015-01-09 in Programmation Last modified on: 2015-09-21

I always thought that owncloud should be able to periodically import calendars found on the web. In my case, I have a schedule as an ical feed I would like synchronize with my owncloud. I finally wrote a script to do that.

Initially, I intended to do it in Bash and to use curl to do requests to owncloud. However, an ical feed contains line breaks. I had a lot of problems to manage them correctly. So I wrote my script in python the excellent requests library.

This script :

  • reads in ~/.owncloud the address and the ids of the owncloud server,
  • fetches the events described in the ical feed I ask it to synchronize. Theses events are stored in an instance of Vevent. I had the possibility to filter out some events,
  • deletes the events present in my owncloud but not in the ical feeds,
  • put (create or update) the events in owncloud.

I think this script is simple and clear enough to be easily read. In case of troubles, you can post a comment.

You can also download this script and see it on my mercurial on bitbucket.

  1 #!/usr/bin/python3
  2 
  3 import requests
  4 from requests.auth import HTTPBasicAuth
  5 import re
  6 import xml.etree.ElementTree as ET
  7 
  8 
  9 class Vevent:
 10     UID_REGEXP = re.compile('^UID:.*')
 11     SUMMARY_REGEXP = re.compile('^SUMMARY:.*')
 12 
 13     def __init__(self, lines):
 14         self._lines = lines
 15         self._uid = self._get_line_from_regexp(self.UID_REGEXP)
 16         self._summary = self._get_line_from_regexp(self.SUMMARY_REGEXP)
 17 
 18     def _get_line_from_regexp(self, regexp):
 19         for line in self._lines:
 20             if regexp.match(line):
 21                 index = self._lines.index(line)
 22                 return line + self._lines[index + 1]
 23 
 24     def get_vevent_for_put(self):
 25         return '\r\n'.join(self._lines)
 26 
 27     def get_as_vcal_for_put(self):
 28         str_vevent = 'BEGIN:VCALENDAR\r\n' + self.get_vevent_for_put() \
 29                      + '\r\nEND:VCALENDAR'
 30         # If you experience problem with the line below, try
 31         # return strvevent
 32         # instead
 33         return str_vevent.encode('utf-8')
 34 
 35     @property
 36     def uid(self):
 37         return self._uid
 38 
 39     @property
 40     def summary(self):
 41         return self._summary
 42 
 43     def __repr__(self):
 44         return '{}\n{}'.format(self._uid, self._summary)
 45 
 46 
 47 def get_login():
 48     owncloud_omis_url = ''
 49     login = ''
 50     password = ''
 51     with open('/home/jenselme/.owncloud') as owncloud:
 52         owncloud_omis_url, login, password = [line for line in
 53                                               owncloud.read().split('\n') if line]
 54     return owncloud_omis_url, login, password
 55 
 56 def fetch_all_vevents(urls, filter_dict):
 57     vevents = []
 58     for name, url in urls.items():
 59         current_vevents = get_vevents(url)
 60         if name in filter_dict:
 61             current_vevents = [vevent for vevent in current_vevents
 62                                if filter_dict[name].match(vevent.summary)]
 63         vevents.extend(current_vevents)
 64     return vevents
 65 
 66 def get_vevents(get_url):
 67     calendar = requests.get(get_url).content.decode('utf-8')
 68     calendar_lines = [line for line in calendar.split('\r\n') if line]
 69     # Remove VCALENDAR lines
 70     del calendar_lines[0]
 71     del calendar_lines[-1]
 72 
 73     vevent_lines = []
 74     vevents = []
 75     for line in calendar_lines:
 76         vevent_lines.append(line)
 77         if VEVENT_END.match(line) and vevent_lines:
 78             vevents.append(Vevent(vevent_lines))
 79             vevent_lines = []
 80     return vevents
 81 
 82 def delete_all_removed_vevents(fetch_vevents, destination_url, request_params):
 83     destination_uids = get_destination_calendar_uids(destination_url, request_params)
 84     source_uids = [vevent.uid for vevent in fetch_vevents]
 85     removed_from_source_uids = [uid for uid in destination_uids if uid not in source_uids]
 86     delete_all(removed_from_source_uids, destination_url, request_params)
 87 
 88 def get_destination_calendar_uids(destination_url, request_params):
 89     resp = requests.request('PROPFIND', destination_url, **request_params)
 90     xml_str = resp.content.decode('utf-8')
 91     root = ET.fromstring(xml_str)
 92     hrefs_uid = []
 93     for response in root:
 94         for child in response:
 95             if child.tag == '{DAV:}href':
 96                 hrefs_uid.append(child.text)
 97     uids = []
 98     for href in hrefs_uid:
 99         uid = href.split('/')[-1]
100         if uid:
101             uids.append(uid)
102     return uids
103 
104 def delete_all(uids, url, request_params):
105     for uid in uids:
106         delete_url = '{}/{}'.format(url, uid)
107         requests.delete(delete_url, **request_params)
108 
109 def put_all_vevents_as_vcalendars(vevents, calendar_put_url, request_params):
110     for vevent in vevents:
111         put_url = '{}/{}.ics'.format(calendar_put_url, vevent.uid)
112         data = vevent.get_as_vcal_for_put()
113         requests.put(put_url, data=data, **request_params)
114 
115 
116 if __name__ == '__main__':
117     # Global variables
118     VEVENT_END = re.compile('^END:VEVENT$')
119     SOCIOLOGIE_ORGANISATION = re.compile('.*Sociologie des Organisations.*')
120     EDT_FETCH_URLS = {'omis-org': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=804&projectId=15&calType=ical&firstDate=2015-01-06&lastDate=2015-06-30',
121     'tc-electif': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=1043&projectId=15&calType=ical&firstDate=2014-11-24&lastDate=2015-04-30',
122     'ENT': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=208&projectId=15&calType=ical&firstDate=2014-10-20&lastDate=2015-05-31',
123     'omis-gB': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=507&projectId=15&calType=ical&firstDate=2014-09-15&lastDate=2015-05-31',
124     'tc2-td3': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=194&projectId=15&calType=ical&firstDate=2014-09-01&lastDate=2015-06-30',
125     'omis-i': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=814&projectId=15&calType=ical&firstDate=2014-09-01&lastDate=2015-05-31'}
126 
127     owncloud_omis_url, login, password = get_login()
128 
129     request_params = {'verify': False, 'auth': HTTPBasicAuth(login, password)}
130     fetched_vevents = fetch_all_vevents(EDT_FETCH_URLS, {'omis-org': SOCIOLOGIE_ORGANISATION})
131     delete_all_removed_vevents(fetched_vevents, owncloud_omis_url, request_params)
132     put_all_vevents_as_vcalendars(fetched_vevents, owncloud_omis_url, request_params)