X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~matthewv/git?a=blobdiff_plain;f=gooswapper.py;h=9f4cefa271a7ff60e1ef96d04db36b070e419a98;hb=9d7a5bf70543eee9e58f3c282672cb6c52d73a17;hp=a4029f0971779d625a847e7338f31d363cea1c8e;hpb=34452e21069ff73bd7ad22f94f7adfff74d881bb;p=gooswapper diff --git a/gooswapper.py b/gooswapper.py index a4029f0..9f4cefa 100644 --- a/gooswapper.py +++ b/gooswapper.py @@ -54,7 +54,18 @@ class ex_gcal_link(exchangelib.ExtendedProperty): property_name = "google calendar event id" property_type = 'String' -exchangelib.CalendarItem.register('gcal_link',ex_gcal_link) +try: + exchangelib.CalendarItem.get_field_by_fieldname('gcal_link') +except ValueError: + exchangelib.CalendarItem.register('gcal_link',ex_gcal_link) + +#useful if you want to replay an event +def drop_from_ex_cache(itemid): + with open(cachepath,"rb") as f: + cache = pickle.load(f) + cache.pop(itemid) + with open(cachepath,"wb") as f: + pickle.dump(cache,f) def get_ex_event_by_itemid(calendar,itemid): return calendar.get(item_id=itemid) @@ -63,6 +74,20 @@ def get_ex_event_by_id_and_changekey(acct,itemid,changekey): l=list(acct.fetch([(itemid,changekey)])) return list(acct.fetch([(itemid,changekey)]))[0] +def get_gcal_event_by_eventid(gcal_acct,eventId,gcal_id="primary"): + return gcal_acct.events().get(calendarId=gcal_id,eventId=eventId).execute() + +def get_gcal_recur_instance(gcal_acct,gcal_master,start,gcal_id="primary"): + ans = gcal_acct.events().instances(calendarId=gcal_id, + eventId=gcal_master, + originalStart=start.isoformat(), + showDeleted=True).execute() + if len(ans['items']) != 1: + logger.error("Searching for recurrance instance returned %d events" % \ + len(ans['items'])) + return None + return ans['items'][0] + def get_ex_cred(username="SANGER\mv3",password=None): if password is None: password = getpass.getpass(prompt="Password for user %s: " % username) @@ -125,6 +150,21 @@ def ex_event_changes(old,new): len(deleted))) return added, deleted, changed +#exchangelib gives us days in recurrence patterns as integers, +#RFC5545 wants SU,MO,TU,WE,TH,FR,SA +#it has a utility function to convert to Monday, Tuesday, ... +def rr_daystr_from_int(i): + return exchangelib.recurrence._weekday_to_str(i).upper()[:2] + +#for monthly patterns, we want the week (or -1 for last) combined with each +#day specified +def rr_daystr_monthly(p): + if p.week_number == 5: + wn = "-1" + else: + wn = str(p.week_number) + return ",".join([wn + rr_daystr_from_int(x) for x in p.weekdays]) + def rrule_from_ex(event,gcal_tz): if event.type != "RecurringMaster": logger.error("Cannot make recurrence from not-recurring event") @@ -135,20 +175,75 @@ def rrule_from_ex(event,gcal_tz): if isinstance(event.recurrence.pattern, exchangelib.recurrence.DailyPattern): rr = "RRULE:FREQ=DAILY;INTERVAL=%d" % event.recurrence.pattern.interval + elif isinstance(event.recurrence.pattern, + exchangelib.recurrence.WeeklyPattern): + rr = "RRULE:FREQ=WEEKLY;INTERVAL=%d;BYDAY=%s;WKST=%s" % \ + (event.recurrence.pattern.interval, + ",".join([rr_daystr_from_int(x) for x in event.recurrence.pattern.weekdays]), + rr_daystr_from_int(event.recurrence.pattern.first_day_of_week) ) + elif isinstance(event.recurrence.pattern, + exchangelib.recurrence.RelativeMonthlyPattern): + rr = "RRULE:FREQ=MONTHLY;INTERVAL=%d;BYDAY=%s" % \ + (event.recurrence.pattern.interval, + rr_daystr_monthly(event.recurrence.pattern)) + elif isinstance(event.recurrence.pattern, + exchangelib.recurrence.AbsoluteMonthlyPattern): + rr = "RRULE:FREQ=MONTHLY;INTERVAL=%d;BYMONTHDAY=%d" % \ + (event.recurrence.pattern.interval, + event.recurrence.pattern.day_of_month) + elif isinstance(event.recurrence.pattern, + exchangelib.recurrence.AbsoluteYearlyPattern): + rr = "RRULE:FREQ=YEARLY;BYMONTH=%d;BYMONTHDAY=%d" % \ + (event.recurrence.pattern.month, + event.recurrence.pattern.day_of_month) + elif isinstance(event.recurrence.pattern, + exchangelib.recurrence.RelativeYearlyPattern): + rr = "RRULE:FREQ=YEARLY;BYMONTH=%d;BYDAY=%s" % \ + (event.recurrence.pattern.month, + rr_daystr_monthly(event.recurrence.pattern)) else: logger.error("Recurrence %s not supported" % event.recurrence) return None if isinstance(event.recurrence.boundary, exchangelib.recurrence.EndDatePattern): rr += ";UNTIL={0:%Y}{0:%m}{0:%d}".format(event.recurrence.boundary.end) + elif isinstance(event.recurrence.boundary, + exchangelib.recurrence.NoEndPattern): + pass #no end date to set else: logger.error("Recurrence %s not supported" % event.recurrence) return None - if event.modified_occurrences is not None or \ - event.deleted_occurrences is not None: - logger.warning("Modified/Deleted recurrences not supported") return [rr] +def modify_recurring(ex_acct,gcal_acct,gcal_tz, + events,master,gcal_id="primary"): + if master.modified_occurrences is not None: + for mod in master.modified_occurrences: + instance = get_gcal_recur_instance(gcal_acct,master.gcal_link, + mod.original_start,gcal_id) + if instance is None: #give up after first failure + return + mod_event = get_ex_event_by_itemid(ex_acct.calendar,mod.item_id) + gevent = build_gcal_event_from_ex(mod_event,gcal_tz) + gevent = gcal_acct.events().update(calendarId=gcal_id, + eventId=instance.get('id'), + body=gevent, + sendUpdates="none").execute() + mod_event.gcal_link = gevent.get("id") + mod_event.save(update_fields=["gcal_link"]) + if master.deleted_occurrences is not None: + for d in master.deleted_occurrences: + instance = get_gcal_recur_instance(gcal_acct,master.gcal_link, + d.start,gcal_id) + if instance is None: #give up after any failure + return + if instance["status"] != "cancelled": + instance["status"]="cancelled" + gcal_acct.events().update(calendarId=gcal_id, + eventId=instance.get('id'), + body=instance, + sendUpdates="none").execute() + def build_gcal_event_from_ex(event,gcal_tz): gevent={} gevent["summary"]=event.subject @@ -180,19 +275,30 @@ def add_ex_to_gcal(ex_acct, gevent["recurrence"] = rr print(gevent) else: - logger.warning("Unable to set recurrence") + logger.warning("Unable to set recurrence for %s" % event.item_id) + continue #don't make the gcal event gevent = gcal_acct.events().insert(calendarId=gcal_id, body=gevent).execute() event.gcal_link = gevent.get("id") event.save(update_fields=["gcal_link"]) + if event.type=="RecurringMaster" and (event.deleted_occurrences or \ + event.modified_occurrences): + modify_recurring(ex_acct,gcal_acct,gcal_tz, + events,event,gcal_id) + #changekey is updated by the above + event.refresh() events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link) - + def del_ex_to_gcal(ex_acct, gcal_acct, events, deleted, gcal_id="primary"): for ev_id in deleted: if events[ev_id].gcal_link is not None: - gcal_acct.events().delete(calendarId=gcal_id, - eventId=events[ev_id].gcal_link, - sendUpdates="none").execute() + gevent = get_gcal_event_by_eventid(gcal_acct, + events[ev_id].gcal_link, + gcal_id) + if gevent["status"] != "cancelled": + gcal_acct.events().delete(calendarId=gcal_id, + eventId=events[ev_id].gcal_link, + sendUpdates="none").execute() def update_ex_to_gcal(ex_acct, gcal_acct,gcal_tz, @@ -200,14 +306,24 @@ def update_ex_to_gcal(ex_acct, gcal_id="primary"): for ev_id in changed: event = get_ex_event_by_itemid(ex_acct.calendar,ev_id) - if not event.is_recurring: - gevent = build_gcal_event_from_ex(event,gcal_tz) - gevent = gcal_acct.events().update(calendarId=gcal_id, + gevent = build_gcal_event_from_ex(event,gcal_tz) + if event.type=="RecurringMaster": + rr = rrule_from_ex(event,gcal_tz) + if rr is not None: + gevent["recurrence"] = rr + if event.deleted_occurrences or \ + event.modified_occurrences: + modify_recurring(ex_acct,gcal_acct,gcal_tz, + events,event,gcal_id) + event.refresh() #changekey is updated by the above + events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link) + else: + logger.warning("Unable to set recurrence for %s" % event.item_id) + continue #don't make the gcal event + gevent = gcal_acct.events().update(calendarId=gcal_id, eventId=event.gcal_link, body=gevent, sendUpdates="none").execute() - else: - logger.warning("recurring events not yet supported") def match_ex_to_gcal(ex_acct,gcal_acct,gcal_tz,events,gcal_id="primary"): recur = 0