import oauth2client
import oauth2client.file
import oauth2client.client
+import googleapiclient.errors
#Not sure what the distribution approach here is...
gcal_client_id = '805127902516-ptbbtgpq9o8pjr6r3k6hsm60j589o85u.apps.googleusercontent.com'
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)
password = getpass.getpass(prompt="Password for user %s: " % username)
return exchangelib.ServiceAccount(username,password)
-def ex_login(emailaddr,autodiscover=True):
+def ex_login(emailaddr,ad_cache_path=None):
global exchange_credential
+ autodiscover = True
if exchange_credential is None:
exchange_credential = get_ex_cred()
- return exchangelib.Account(emailaddr,
- credentials = exchange_credential,
- autodiscover = autodiscover)
+ if ad_cache_path is not None:
+ try:
+ with open(ad_cache_path,"rb") as f:
+ url,auth_type = pickle.load(f)
+ autodiscover = False
+ except FileNotFoundError:
+ pass
+
+ if autodiscover:
+ ex_ac = exchangelib.Account(emailaddr,
+ credentials = exchange_credential,
+ autodiscover = autodiscover)
+ if ad_cache_path is not None:
+ cache=(ex_ac.protocol.service_endpoint,
+ ex_ac.protocol.auth_type)
+ with open(ad_cache_path,"wb") as f:
+ pickle.dump(cache,f)
+ else:
+ ex_conf = exchangelib.Configuration(service_endpoint=url,
+ credentials=exchange_credential,
+ auth_type=auth_type)
+ ex_ac = exchangelib.Account(emailaddr,
+ config=ex_conf,
+ autodiscover=False,
+ access_type=exchangelib.DELEGATE)
+
+ return ex_ac
def get_ex_events(calendar):
ans={}
len(deleted)))
return added, deleted, changed
+def rrule_from_ex(event,gcal_tz):
+ if event.type != "RecurringMaster":
+ logger.error("Cannot make recurrence from not-recurring event")
+ return None
+ if event.recurrence is None:
+ logger.error("Empty recurrence structure")
+ return None
+ if isinstance(event.recurrence.pattern,
+ exchangelib.recurrence.DailyPattern):
+ rr = "RRULE:FREQ=DAILY;INTERVAL=%d" % event.recurrence.pattern.interval
+ 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)
+ 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 build_gcal_event_from_ex(event,gcal_tz):
+ gevent={}
+ gevent["summary"]=event.subject
+ if event.is_all_day:
+ gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
+ gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
+ else:
+ gevent["end"]={"dateTime": event.end.astimezone(gcal_tz).isoformat(),
+ "timeZone": str(gcal_tz)}
+ gevent["start"]={"dateTime": event.start.astimezone(gcal_tz).isoformat(),
+ "timeZone": str(gcal_tz)}
+ if event.text_body is not None and event.text_body.strip() != '':
+ gevent["description"] = event.text_body
+ if event.location is not None:
+ gevent["location"] = event.location
+ gevent["extendedProperties"]={"shared": {"ex_id": event.item_id}}
+ return gevent
+
def add_ex_to_gcal(ex_acct,
gcal_acct,gcal_tz,events,
added,
gcal_id="primary"):
for ev_id in added:
event = get_ex_event_by_itemid(ex_acct.calendar,ev_id)
- if event.is_all_day:
- gevent={}
- gevent["summary"]=event.subject
- gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
- gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
- if event.text_body.strip() != '':
- gevent["description"] = event.text_body
- if event.location is not None:
- gevent["location"] = event.location
- gevent["extended_properties"]={"shared": {"ex_id": event.item_id}}
- gevent=gcal_acct.events().insert(calendarId=gcal_id, body=gevent).execute()
- event.gcal_link = gevent.get("id")
- event.save()
- events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey)
- else:
- logger.warning("only all-day events supported")
+ 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
+ print(gevent)
+ else:
+ 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"])
+ 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:
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,
+ events,changed,
+ 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,
+ 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
+ matched = 0
+ skipped = 0
+ for ev_id in events:
+ event = get_ex_event_by_itemid(ex_acct.calendar,ev_id)
+ if event.is_recurring:
+ recur += 1
+ continue
+ elif event.gcal_link is not None:
+ skipped += 1
+ continue
+ matches = gcal_acct.events().list(calendarId=gcal_id,
+ timeMin=event.start.isoformat(),
+ timeMax=event.end.isoformat()).execute()
+ for ge in matches['items']:
+ if ge['summary'].strip()==event.subject.strip():
+ logger.info("Matching '%s' starting at %s" % (event.subject,
+ event.start.isoformat()))
+ event.gcal_link = ge['id']
+ event.save(update_fields=["gcal_link"])
+ events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey,gcal_link=event.gcal_link)
+ gevent = {}
+ gevent["start"] = ge["start"]
+ gevent["end"] = ge["end"]
+ gevent["extendedProperties"]={"shared": {"ex_id": event.item_id}}
+ try:
+ gcal_acct.events().update(calendarId=gcal_id,
+ eventId=event.gcal_link,
+ body=gevent,
+ sendUpdates="none").execute()
+ #this may fail if we don't own the event
+ except googleapiclient.errors.HttpError as err:
+ if err.resp.status == 403:
+ pass
+ matched += 1
+ break
+ logger.info("Matched %d events, skipped %d with existing link, and %d recurring ones" % (matched,skipped,recur))
def get_gcal_cred():
#each such file can only store a single credential
except FileNotFoundError:
cache = None
- ex_account = ex_login("mv3@sanger.ac.uk")
+ ex_account = ex_login("mv3@sanger.ac.uk",".gooswapper_exch_conf.dat")
current = get_ex_events(ex_account.calendar)
gcal_account = gcal_login()
#delete op needs the "cache" set, as that has the link ids in
#for events that are now deleted
del_ex_to_gcal(ex_account,gcal_account,cache,deleted)
+ update_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,changed)
+ else:
+ match_ex_to_gcal(ex_account,gcal_account,gcal_tz,current)
with open(cachepath,"wb") as f:
pickle.dump(current,f)