8 logger = logging.getLogger('gooswapper')
9 logger.setLevel(logging.INFO)
10 consolelog = logging.StreamHandler()
11 consolelog.setLevel(logging.INFO)
12 logformatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
13 consolelog.setFormatter(logformatter)
14 logger.addHandler(consolelog)
15 #We can't use this, because that way all the libraries' logs spam us
16 #logging.basicConfig(level=logging.INFO)
18 #Exchange-related library
19 sys.path.append("/upstreams/exchangelib")
22 #Google calendar-api libraries
24 import apiclient.discovery
26 import oauth2client.file
27 import oauth2client.client
29 #Not sure what the distribution approach here is...
30 gcal_client_id = '805127902516-ptbbtgpq9o8pjr6r3k6hsm60j589o85u.apps.googleusercontent.com'
31 gcal_client_secret = '8hpdxV3MauorryTDoZ1YK8JO'
33 #scope URL for r/w calendar access
34 scope = 'https://www.googleapis.com/auth/calendar'
35 #flow object, for doing OAuth2.0 stuff
36 flow = oauth2client.client.OAuth2WebServerFlow(gcal_client_id,
41 gcal_authpath=".gooswap_gcal_creds.dat"
43 cachepath=".gooswapcache"
45 exchange_credential = None
47 class ex_gcal_link(exchangelib.ExtendedProperty):
48 distinguished_property_set_id = 'PublicStrings'
49 #property_set_id = '12345678-1234-1234-1234-123456781234'
50 # property_set_id = "gooswapper"
51 property_name = "google calendar event id"
52 property_type = 'String'
54 exchangelib.CalendarItem.register('gcal_link',ex_gcal_link)
56 #see docs for exchangelib.UID for why this is needed
57 class GlobalObjectId(exchangelib.ExtendedProperty):
58 distinguished_property_set_id = 'Meeting'
60 property_type = 'Binary'
62 exchangelib.CalendarItem.register('global_object_id', GlobalObjectId)
64 def get_ex_event_by_uid(calendar,uid):
65 return calendar.get(global_object_id=GlobalObjectId(exchangelib.UID(uid)))
67 def get_ex_event_by_id_and_changekey(acct,itemid,changekey):
68 l=list(acct.fetch([(itemid,changekey)]))
69 return list(acct.fetch([(itemid,changekey)]))[0]
71 def get_ex_cred(username="SANGER\mv3",password=None):
73 password = getpass.getpass(prompt="Password for user %s: " % username)
74 return exchangelib.ServiceAccount(username,password)
75 # return exchangelib.Credentials(username,password)
77 def ex_login(emailaddr,autodiscover=True):
78 global exchange_credential
79 if exchange_credential is None:
80 exchange_credential = get_ex_cred()
81 return exchangelib.Account(emailaddr,
82 credentials = exchange_credential,
83 autodiscover = autodiscover)
85 def get_ex_events(calendar):
88 for event in calendar.all().only('uid','changekey','item_id','gcal_link'):
89 if event.gcal_link is not None:
93 logger.warning("Event uid %s was duplicated!" % event.uid)
94 ans[event.uid] = event.changekey
95 itemids[event.uid] = event.item_id
96 logger.info("%d events found" % len(ans))
99 def ex_event_changes(old,new):
100 olds = set(old.keys())
101 news = set(new.keys())
102 added = list(news-olds)
103 deleted = list(olds-news)
105 #intersection - i.e. common to both sets
106 for event in olds & news:
107 if old[event] != new[event]:
108 changed.append(event)
109 logger.info("%d events updated, %d added, %d deleted" % (len(changed),
112 return added, deleted, changed
114 #XXX doesn't work - cf https://github.com/ecederstrand/exchangelib/issues/492
115 def add_ex_to_gcal_needs_ev_id(ex_cal,events):
118 event = get_ex_event_by_uid(ex_cal,ev_id)
119 event.gcal_link = "Testing"
122 def add_ex_to_gcal(ex_acct,
123 gcal_acct,gcal_tz,events,
127 event = get_ex_event_by_id_and_changekey(ex_acct,
128 itemids[ev_id],events[ev_id])
131 gevent["summary"]=event.subject
132 gevent["end"]={"date": str(event.end.astimezone(gcal_tz).date())}
133 gevent["start"]={"date": str(event.start.astimezone(gcal_tz).date())}
134 if event.text_body.strip() != '':
135 gevent["description"] = event.text_body
136 if event.location is not None:
137 gevent["location"] = event.location
138 gevent["extended_properties"]={"shared": {"ex_id": event.item_id}}
139 gevent=gcal_acct.events().insert(calendarId=gcal_id, body=gevent).execute()
140 event.gcal_link = gevent.get("id")
142 events[event.uid] = event.changekey
144 logger.warning("only all-day events supported")
147 #each such file can only store a single credential
148 storage = oauth2client.file.Storage(gcal_authpath)
149 gcal_credential = storage.get()
150 #if no credential found, or they're invalid (e.g. expired),
151 #then get a new one; pass --noauth_local_webserver on the command line
152 #if you don't want it to spawn a browser
153 if gcal_credential is None or gcal_credential.invalid:
154 gcal_credential = oauth2client.tools.run_flow(flow,
156 oauth2client.tools.argparser.parse_args())
157 return gcal_credential
160 gcal_credential = get_gcal_cred()
161 # Object to handle http requests; could add proxy details
162 http = httplib2.Http()
163 http = gcal_credential.authorize(http)
164 return apiclient.discovery.build('calendar', 'v3', http=http)
166 def get_gcal_timezone(gcal_account,calendarid="primary"):
167 gcal = gcal_account.calendars().get(calendarId=calendarid).execute()
168 return exchangelib.EWSTimeZone.timezone(gcal['timeZone'])
172 with open(cachepath,"rb") as f:
173 cache = pickle.load(f)
174 except FileNotFoundError:
177 ex_account = ex_login("mv3@sanger.ac.uk")
178 current,itemids = get_ex_events(ex_account.calendar)
180 gcal_account = gcal_login()
181 gcal_tz = get_gcal_timezone(gcal_account)
183 if cache is not None:
184 added,deleted,changed = ex_event_changes(cache,current)
185 add_ex_to_gcal(ex_account,gcal_account,gcal_tz,current,
188 with open(cachepath,"wb") as f:
189 pickle.dump(current,f)
191 if __name__ == "__main__":