9 logger = logging.getLogger('gooswapper')
10 logger.setLevel(logging.INFO)
11 consolelog = logging.StreamHandler()
12 consolelog.setLevel(logging.INFO)
13 logformatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
14 consolelog.setFormatter(logformatter)
15 logger.addHandler(consolelog)
16 #We can't use this, because that way all the libraries' logs spam us
17 #logging.basicConfig(level=logging.INFO)
19 #Exchange-related library
20 sys.path.append("/upstreams/exchangelib")
23 #Google calendar-api libraries
25 import apiclient.discovery
27 import oauth2client.file
28 import oauth2client.client
30 #Not sure what the distribution approach here is...
31 gcal_client_id = '805127902516-ptbbtgpq9o8pjr6r3k6hsm60j589o85u.apps.googleusercontent.com'
32 gcal_client_secret = '8hpdxV3MauorryTDoZ1YK8JO'
34 #scope URL for r/w calendar access
35 scope = 'https://www.googleapis.com/auth/calendar'
36 #flow object, for doing OAuth2.0 stuff
37 flow = oauth2client.client.OAuth2WebServerFlow(gcal_client_id,
42 gcal_authpath=".gooswap_gcal_creds.dat"
44 cachepath=".gooswapcache"
46 exchange_credential = None
48 CachedExEvent=collections.namedtuple('CachedExEvent',
49 ['changekey','gcal_link'])
51 class ex_gcal_link(exchangelib.ExtendedProperty):
52 distinguished_property_set_id = 'PublicStrings'
53 property_name = "google calendar event id"
54 property_type = 'String'
56 exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
58 def get_ex_event_by_itemid(calendar,itemid):
59 return calendar.get(item_id=itemid)
61 def get_ex_event_by_id_and_changekey(acct,itemid,changekey):
62 l=list(acct.fetch([(itemid,changekey)]))
63 return list(acct.fetch([(itemid,changekey)]))[0]
65 def get_ex_cred(username="SANGER\mv3",password=None):
67 password = getpass.getpass(prompt="Password for user %s: " % username)
68 return exchangelib.ServiceAccount(username,password)
70 def ex_login(emailaddr,autodiscover=True):
71 global exchange_credential
72 if exchange_credential is None:
73 exchange_credential = get_ex_cred()
74 return exchangelib.Account(emailaddr,
75 credentials = exchange_credential,
76 autodiscover = autodiscover)
78 def get_ex_events(calendar):
80 for event in calendar.all().only('changekey','item_id','gcal_link'):
81 if event.item_id in ans:
82 logger.warning("Event item_id %s was duplicated!" % event.item_id)
83 ans[event.item_id] = CachedExEvent(event.changekey,event.gcal_link)
84 logger.info("%d events found" % len(ans))
87 def ex_event_changes(old,new):
88 olds = set(old.keys())
89 news = set(new.keys())
90 added = list(news-olds)
91 deleted = list(olds-news)
93 #intersection - i.e. common to both sets
94 for event in olds & news:
95 if old[event].changekey != new[event].changekey:
97 logger.info("%d events updated, %d added, %d deleted" % (len(changed),
100 return added, deleted, changed
102 def add_ex_to_gcal(ex_acct,
103 gcal_acct,gcal_tz,events,
107 event = get_ex_event_by_itemid(ex_acct.calendar,ev_id)
110 gevent["summary"]=event.subject
111 gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
112 gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
113 if event.text_body.strip() != '':
114 gevent["description"] = event.text_body
115 if event.location is not None:
116 gevent["location"] = event.location
117 gevent["extendedProperties"]={"shared": {"ex_id": event.item_id}}
118 gevent=gcal_acct.events().insert(calendarId=gcal_id, body=gevent).execute()
119 event.gcal_link = gevent.get("id")
121 events[event.item_id] = events[event.item_id]._replace(changekey=event.changekey)
123 logger.warning("only all-day events supported")
125 def del_ex_to_gcal(ex_acct, gcal_acct, events, deleted, gcal_id="primary"):
126 for ev_id in deleted:
127 if events[ev_id].gcal_link is not None:
128 gcal_acct.events().delete(calendarId=gcal_id,
129 eventId=events[ev_id].gcal_link,
130 sendUpdates="none").execute()
133 #each such file can only store a single credential
134 storage = oauth2client.file.Storage(gcal_authpath)
135 gcal_credential = storage.get()
136 #if no credential found, or they're invalid (e.g. expired),
137 #then get a new one; pass --noauth_local_webserver on the command line
138 #if you don't want it to spawn a browser
139 if gcal_credential is None or gcal_credential.invalid:
140 gcal_credential = oauth2client.tools.run_flow(flow,
142 oauth2client.tools.argparser.parse_args())
143 return gcal_credential
146 gcal_credential = get_gcal_cred()
147 # Object to handle http requests; could add proxy details
148 http = httplib2.Http()
149 http = gcal_credential.authorize(http)
150 return apiclient.discovery.build('calendar', 'v3', http=http)
152 def get_gcal_timezone(gcal_account,calendarid="primary"):
153 gcal = gcal_account.calendars().get(calendarId=calendarid).execute()
154 return exchangelib.EWSTimeZone.timezone(gcal['timeZone'])
158 with open(cachepath,"rb") as f:
159 cache = pickle.load(f)
160 except FileNotFoundError:
163 ex_account = ex_login("mv3@sanger.ac.uk")
164 current = get_ex_events(ex_account.calendar)
166 gcal_account = gcal_login()
167 gcal_tz = get_gcal_timezone(gcal_account)
169 if cache is not None:
170 added,deleted,changed = ex_event_changes(cache,current)
171 add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,added)
172 #delete op needs the "cache" set, as that has the link ids in
173 #for events that are now deleted
174 del_ex_to_gcal(ex_account,gcal_account,cache,deleted)
176 with open(cachepath,"wb") as f:
177 pickle.dump(current,f)
179 if __name__ == "__main__":