Mercurial > hg > gcmultimerge
annotate multimerge.py @ 57:1c2cf6170219
Comments.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Tue, 05 Jul 2016 23:46:18 +0300 |
parents | 597875ef885b |
children | 87fda54f935c |
rev | line source |
---|---|
1 | 1 #!/usr/bin/python |
3 | 2 # coding=utf-8 |
2 | 3 ### |
36 | 4 ### Google Calendar MultiMerge |
2 | 5 ### (C) 2016 Matti 'ccr' Hamalainen <ccr@tnsp.org> |
6 ### | |
7 ### Python 2.7 <= x < 3 required! Please refer to | |
8 ### README.txt for information on other depencies. | |
9 ### | |
1 | 10 import os |
11 import sys | |
12 import signal | |
13 import re | |
45
035be8a9e982
Force reading of configuration in Unicode UTF-8.
Matti Hamalainen <ccr@tnsp.org>
parents:
44
diff
changeset
|
14 import codecs |
29
50711871fd1e
Comment out currently unnecessary imports.
Matti Hamalainen <ccr@tnsp.org>
parents:
28
diff
changeset
|
15 #import time |
50711871fd1e
Comment out currently unnecessary imports.
Matti Hamalainen <ccr@tnsp.org>
parents:
28
diff
changeset
|
16 #import datetime |
14
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
17 |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
18 import smtplib |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
19 from email.mime.text import MIMEText |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
20 |
1 | 21 import httplib2 |
22 import ConfigParser | |
20 | 23 |
1 | 24 import oauth2client |
25 from oauth2client import client | |
26 from oauth2client import tools | |
4 | 27 from oauth2client import file |
1 | 28 from googleapiclient import discovery |
29 | |
30 | |
31 ### | |
32 ### Misc. helper functions | |
33 ### | |
34 | |
35 ## Wrapper for print() that does not break when redirecting stdin/out | |
36 ## because of piped output not having a defined encoding. We default | |
37 ## to UTF-8 encoding in output here. | |
38 def gcm_print(smsg): | |
47 | 39 gcm_msgbuf.append(smsg) |
1 | 40 if sys.stdout.encoding != None: |
41 print(smsg.encode(sys.stdout.encoding)) | |
42 else: | |
43 print(smsg.encode("UTF-8")) | |
44 | |
45 | |
46 ## Fatal errors | |
47 def gcm_fatal(smsg): | |
48 gcm_print(u"ERROR: "+ smsg) | |
14
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
49 if cfg.email_ok and cfg.email: |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
50 ## If e-mail is set, send e-mail |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
51 msg = MIMEText(("\n".join(gcm_msgbuf)).encode("UTF-8"), "plain") |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
52 msg.set_charset("UTF-8") |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
53 msg["Subject"] = cfg.email_subject |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
54 msg["From"] = cfg.email_sender |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
55 msg["To"] = ",".join(cfg.email_to) |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
56 try: |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
57 smtpH = smtplib.SMTP('localhost') |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
58 smtpH.sendmail(cfg.email_sender, cfg.email_to, msg.as_string()) |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
59 smtpH.quit() |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
60 except: |
54 | 61 gcm_print(u"FATAL: Oh crap, e-mail sending failed.") |
1 | 62 sys.exit(1) |
63 | |
64 | |
65 ## Debug messages | |
66 def gcm_debug(smsg): | |
67 if cfg.debug: | |
68 gcm_print(u"DBG: "+ smsg) | |
69 else: | |
47 | 70 gcm_msgbuf.append(u"DBG: {0}".format(smsg)) |
1 | 71 |
72 | |
24 | 73 ## Handler for SIGINT signals |
1 | 74 def gcm_signal_handler(signal, frame): |
54 | 75 gcm_print(u"\nQuitting due to SIGINT / Ctrl+C!") |
1 | 76 sys.exit(0) |
77 | |
78 | |
24 | 79 ## Function for handling Google API credentials |
1 | 80 def gcm_get_credentials(mcfg): |
42 | 81 try: |
82 store = oauth2client.file.Storage(mcfg.credential_file) | |
83 except Exception as e: | |
84 gcm_fatal("Failed to read credential file:\n{0}\n\nERROR: {1}\n".format(mcfg.credential_file, str(e))) | |
85 | |
1 | 86 credentials = store.get() |
87 if not credentials or credentials.invalid: | |
42 | 88 try: |
89 flow = client.flow_from_clientsecrets(mcfg.secret_file, mcfg.scope) | |
90 except Exception as e: | |
91 gcm_fatal("Failed to fetch client secret:\n{0}\n\nERROR: {1}\n".format(mcfg.secret_file, str(e))) | |
92 | |
1 | 93 flow.user_agent = mcfg.app_name |
94 credentials = tools.run_flow(flow, store, mcfg) | |
95 if not credentials or credentials.invalid: | |
96 gcm_fatal("Failed to authenticate / invalid credentials.") | |
97 return credentials | |
98 | |
99 | |
100 def gcm_dump_events(events): | |
101 for event in events: | |
102 ev_start = event["start"].get("dateTime", event["start"].get("date")) | |
103 ev_end = event["end"].get("dateTime", event["end"].get("date")) | |
104 gcm_print(u"{0:25} - {1:25} : {2}".format(ev_start, ev_end, event["summary"])) | |
105 | |
56 | 106 |
107 ## Generate gcm IDs for given list of events | |
26
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
108 def gcm_generate_ids(events, calendar_id): |
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
109 if not events: |
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
110 return events |
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
111 |
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
112 for ev in events: |
28 | 113 ev["gcm_cal_id"] = calendar_id |
30 | 114 ev["gcm_id"] = re.sub("[^a-v0-9]", "0", calendar_id.lower()) + ev["id"] |
26
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
115 |
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
116 return events |
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
117 |
1 | 118 |
53
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
119 ## Find event by its gcm_id from given list or return None if not found |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
120 def gcm_get_event_by_gcm_id(list, id): |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
121 for event in list: |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
122 if event["gcm_id"] == id: |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
123 return event |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
124 return None |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
125 |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
126 |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
127 gcm_no_compare_fields = [u"id", u"iCalUID"] |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
128 |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
129 def gcm_compare_events(ev1, ev2): |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
130 for field in ev1: |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
131 if not field in gcm_no_compare_fields and ev1[field] != ev2[field]: |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
132 return False |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
133 return True |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
134 |
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
135 |
24 | 136 ## |
137 ## Class for handling configuration / settings | |
138 ## | |
1 | 139 class GCMSettings(dict): |
140 def __init__(self): | |
141 self.m_data = {} | |
142 self.m_saveable = {} | |
5
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
143 self.m_validate = {} |
1 | 144 self.m_translate = {} |
145 | |
146 def __getattr__(self, name): | |
147 if name in self.m_data: | |
148 return self.m_data[name] | |
149 else: | |
150 gcm_fatal("GCMSettings.__getattr__(): No such attribute '"+ name +"'.") | |
151 | |
5
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
152 def mvalidate(self, name, value): |
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
153 if name in self.m_validate and self.m_validate[name]: |
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
154 if not self.m_validate[name](value): |
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
155 gcm_fatal("GCMSettings.mvalidate(): Invalid value for attribute '{0}': {1}".format(name, value)) |
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
156 |
1 | 157 def mtranslate(self, name, value): |
158 if name in self.m_translate and self.m_translate[name]: | |
159 return self.m_translate[name](value) | |
160 else: | |
161 return value | |
162 | |
163 def mdef(self, name, saveable, validate, translate, value): | |
5
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
164 self.mvalidate(name, value) |
1 | 165 self.m_saveable[name] = saveable |
5
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
166 self.m_validate[name] = validate |
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
167 self.m_translate[name] = translate |
1 | 168 self.m_data[name] = self.mtranslate(name, value) |
169 | |
170 def mset(self, name, value): | |
5
9d4152f32223
Add some code for settings validation.
Matti Hamalainen <ccr@tnsp.org>
parents:
4
diff
changeset
|
171 self.mvalidate(name, value) |
1 | 172 if name in self.m_data: |
173 self.m_data[name] = self.mtranslate(name, value) | |
174 else: | |
43 | 175 gcm_fatal(u"GCMSettings.mset(): No such attribute '"+ name +"'.") |
1 | 176 |
177 def mget(self, name): | |
178 if name in self.m_data: | |
179 return self.m_data[name] | |
180 else: | |
181 return None | |
182 | |
6
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
183 def mread(self, cfgparser, sect): |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
184 for name in self.m_saveable: |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
185 if cfgparser.has_option(sect, name): |
46
51c87abe5a1e
This Unicode conversion here is probably redundant.
Matti Hamalainen <ccr@tnsp.org>
parents:
45
diff
changeset
|
186 value = cfgparser.get(sect, name) |
6
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
187 self.mset(name, value) |
43 | 188 gcm_debug(u"{0} -> '{1}' == {2}".format(name, value, self.mget(name))) |
6
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
189 |
23 | 190 def is_str(self, mvalue): |
191 return isinstance(mvalue, basestring) | |
192 | |
193 def is_string(self, mvalue): | |
194 return mvalue == None or self.is_str(mvalue) | |
195 | |
196 def is_log_level(self, mvalue): | |
197 if not self.is_str(mvalue): | |
198 return False | |
199 else: | |
200 return mvalue.upper() in ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] | |
201 | |
202 def trans_log_level(self, mvalue): | |
203 return mvalue.upper() | |
204 | |
205 def is_filename(self, mvalue): | |
206 if not self.is_str(mvalue): | |
207 return False | |
208 else: | |
209 return re.match("^[a-z0-9][a-z0-9\.\_\-]+$", mvalue, flags=re.IGNORECASE) | |
210 | |
211 def trans_bool(self, mvalue): | |
212 if self.is_str(mvalue): | |
213 if re.match("^\s*(true|1|on|yes)\s*$", mvalue, re.IGNORECASE): | |
214 mvalue = True | |
215 elif re.match("^\s*(false|0|off|no)\s*$", mvalue, re.IGNORECASE): | |
216 mvalue = False | |
217 else: | |
218 return None | |
219 return mvalue | |
220 | |
221 def is_bool(self, mvalue): | |
222 mval = self.trans_bool(mvalue) | |
223 if not isinstance(mval, bool): | |
224 gcm_fatal("GCMSettings.is_bool(): Invalid boolean value '{0}', should be true|false|1|0|on|off|yes|no.".format(mvalue)) | |
225 else: | |
226 return True | |
227 | |
228 def trans_list(self, mvalue): | |
229 morig = mvalue | |
230 if self.is_str(mvalue): | |
231 mvalue = re.split("\s*,\s*", mvalue, flags=re.IGNORECASE) | |
232 if not isinstance(mvalue, list): | |
233 gcm_fatal("GCMSettings.trans_list(): Could not parse list '{0}'.".format(mvalue)) | |
234 elif not isinstance(mvalue, list): | |
235 gcm_fatal("GCMSettings.trans_list(): Invalid value '{0}'.".format(mvalue)) | |
236 return mvalue | |
237 | |
238 def is_list(self, mvalue): | |
239 return self.trans_list(mvalue) | |
240 | |
241 def is_email(self, mvalue): | |
242 if not self.is_string(mvalue): | |
243 return False | |
244 else: | |
245 return re.match("^.*?\s+<[a-z0-9]+[a-z0-9\.\+\-]*\@[a-z0-9]+[a-z0-9\.\-]+>\s*$|[a-z0-9]+[a-z0-9\.\+\-]*\@[a-z0-9]+[a-z0-9\.\-]+", mvalue, flags=re.IGNORECASE) | |
246 | |
247 def trans_email_list(self, mvalue): | |
248 if mvalue == None: | |
249 return mvalue | |
250 else: | |
251 return self.trans_list(mvalue.strip()) | |
252 | |
253 def is_email_list(self, mvalue): | |
254 mvalue = self.trans_email_list(mvalue) | |
255 if mvalue != None: | |
256 for email in mvalue: | |
257 if not self.is_email(email): | |
258 gcm_fatal("Invalid e-mail address '{0}' in list {1}.".format(email, ", ".join(mvalue))) | |
259 return True | |
260 | |
1 | 261 |
262 ### | |
263 ### Main program starts | |
264 ### | |
265 gcm_msgbuf = [] | |
266 signal.signal(signal.SIGINT, gcm_signal_handler) | |
267 | |
268 | |
269 ## Settings | |
270 cfg = GCMSettings() | |
271 | |
23 | 272 cfg.mdef("debug", True, cfg.is_bool, cfg.trans_bool, False) |
1 | 273 |
14
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
274 cfg.mdef("email_ok", False, None, None, False) |
23 | 275 cfg.mdef("email", True, cfg.is_bool, cfg.trans_bool, False) |
276 cfg.mdef("email_to", True, cfg.is_email_list, cfg.trans_email_list, None) | |
277 cfg.mdef("email_sender", True, cfg.is_email, None, None) | |
49 | 278 cfg.mdef("email_subject", True, cfg.is_string, None, u"Google Calendar MultiMerge status") |
14
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
279 |
49 | 280 cfg.mdef("source_regex", True, cfg.is_string, None, u"^R:\s*(.*?)\s*\(\s*(.+?)\s*\)\s*$") |
23 | 281 cfg.mdef("source_regmap", False, cfg.is_list, cfg.trans_list, [1, 2]) |
1 | 282 cfg.mdef("source_regmap_len", False, None, None, len(cfg.source_regmap)) |
283 | |
23 | 284 cfg.mdef("dest_name", True, cfg.is_string, None, u"Raahen kansainvälisyystoiminta") |
285 cfg.mdef("dest_id", True, cfg.is_string, None, None) | |
1 | 286 |
287 cfg.mdef("noauth_local_webserver", False, None, None, True) | |
288 #cfg.mdef("auth_host_name", False, None, None, "localhost") | |
289 #cfg.mdef("auth_host_port", False, None, None, [8080, 8090]) | |
23 | 290 cfg.mdef("logging_level", True, cfg.is_log_level, cfg.trans_log_level, "ERROR") |
1 | 291 |
292 # No need to touch these | |
293 cfg.mdef("app_name", False, None, None, "Google Calendar MultiMerge") | |
294 cfg.mdef("scope", False, None, None, "https://www.googleapis.com/auth/calendar") | |
295 #cfg.mdef("scope", False, None, None, "https://www.googleapis.com/auth/calendar.readonly") | |
23 | 296 cfg.mdef("secret_file", True, cfg.is_filename, None, "client_secret.json") |
297 cfg.mdef("credential_file", True, cfg.is_filename, None, "client_credentials.json") | |
1 | 298 |
6
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
299 |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
300 ## Read, parse and validate configuration file |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
301 if len(sys.argv) > 1: |
54 | 302 gcm_debug(u"Reading configuration from '{0}'.".format(sys.argv[1])) |
6
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
303 try: |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
304 cfgparser = ConfigParser.RawConfigParser() |
45
035be8a9e982
Force reading of configuration in Unicode UTF-8.
Matti Hamalainen <ccr@tnsp.org>
parents:
44
diff
changeset
|
305 cfgparser.readfp(codecs.open(sys.argv[1], "r", "UTF-8")) |
6
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
306 except Exception as e: |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
307 gcm_fatal("Failed to read configuration file '{0}': {1}".format(sys.argv[1], str(e))) |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
308 |
7
f2ecfb3e04ee
Check that the required section exists in configuration.
Matti Hamalainen <ccr@tnsp.org>
parents:
6
diff
changeset
|
309 # Check that the required section exists |
f2ecfb3e04ee
Check that the required section exists in configuration.
Matti Hamalainen <ccr@tnsp.org>
parents:
6
diff
changeset
|
310 section = "gcm" |
f2ecfb3e04ee
Check that the required section exists in configuration.
Matti Hamalainen <ccr@tnsp.org>
parents:
6
diff
changeset
|
311 if not cfgparser.has_section(section): |
f2ecfb3e04ee
Check that the required section exists in configuration.
Matti Hamalainen <ccr@tnsp.org>
parents:
6
diff
changeset
|
312 gcm_fatal("Invalid configuration, missing '{0}' section.".format(section)) |
f2ecfb3e04ee
Check that the required section exists in configuration.
Matti Hamalainen <ccr@tnsp.org>
parents:
6
diff
changeset
|
313 |
10
b237b96602ad
We need to handle "debug" setting before other settings, so we need a
Matti Hamalainen <ccr@tnsp.org>
parents:
9
diff
changeset
|
314 # Debug setting is a special case, we need to get it |
b237b96602ad
We need to handle "debug" setting before other settings, so we need a
Matti Hamalainen <ccr@tnsp.org>
parents:
9
diff
changeset
|
315 # set before everything else, so do it here .. |
b237b96602ad
We need to handle "debug" setting before other settings, so we need a
Matti Hamalainen <ccr@tnsp.org>
parents:
9
diff
changeset
|
316 if cfgparser.has_option(section, "debug"): |
b237b96602ad
We need to handle "debug" setting before other settings, so we need a
Matti Hamalainen <ccr@tnsp.org>
parents:
9
diff
changeset
|
317 cfg.mset("debug", cfgparser.get(section, "debug")) |
b237b96602ad
We need to handle "debug" setting before other settings, so we need a
Matti Hamalainen <ccr@tnsp.org>
parents:
9
diff
changeset
|
318 |
6
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
319 # Parse the settings and validate |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
320 cfg.mread(cfgparser, section) |
ee6bf617f839
Implement configuration file reading.
Matti Hamalainen <ccr@tnsp.org>
parents:
5
diff
changeset
|
321 |
8 | 322 |
323 ## Validate settings | |
14
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
324 if cfg.email: |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
325 if cfg.email_subject == None or len(cfg.email_subject) == 0: |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
326 gcm_fatal("E-mail enabled but email_subject not set.") |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
327 elif cfg.email_sender == None: |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
328 gcm_fatal("E-mail enabled but email_sender not set.") |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
329 elif cfg.email_to == None: |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
330 gcm_fatal("E-mail enabled but email_to not set.") |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
331 else: |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
332 cfg.mset("email_ok", True) |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
333 |
8262efacf3fb
Initial implementation of sending e-mail in fatal error cases.
Matti Hamalainen <ccr@tnsp.org>
parents:
13
diff
changeset
|
334 |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
335 if len(cfg.source_regmap) != cfg.source_regmap_len: |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
336 gcm_fatal("Setting source_regmap list must be {0} items.".format(cfg.source_regmap_len)) |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
337 else: |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
338 # Force to integers |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
339 try: |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
340 cfg.source_regmap = map(lambda x: int(x), cfg.source_regmap) |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
341 except Exception as e: |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
342 gcm_fatal("Invalid source_regmap: {0}".format(str(e))) |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
343 |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
344 |
8 | 345 if not cfg.dest_name and not cfg.dest_id: |
346 gcm_fatal("Target calendar ID or name required, but not set.") | |
347 | |
348 | |
349 if cfg.dest_name: | |
350 cfg.mset("dest_name", cfg.mget("dest_name").strip()) | |
351 | |
352 | |
1 | 353 ## Initialize and authorize API connection |
354 credentials = gcm_get_credentials(cfg) | |
355 http = credentials.authorize(httplib2.Http()) | |
356 service = discovery.build("calendar", "v3", http=http) | |
357 | |
358 | |
359 ## Fetch complete calendar list | |
54 | 360 gcm_debug(u"Fetching available calendars ..") |
1 | 361 calendars = [] |
19 | 362 cal_token = None |
1 | 363 while True: |
364 # We want everything except deleted and hidden calendars | |
17 | 365 result = service.calendarList().list( |
1 | 366 showHidden=False, |
367 showDeleted=False, | |
19 | 368 pageToken=cal_token |
1 | 369 ).execute() |
370 | |
17 | 371 calendars.extend(result.get("items", [])) |
19 | 372 cal_token = result.get("nextPageToken") |
373 if not cal_token: | |
1 | 374 break |
375 | |
376 if len(calendars) == 0: | |
377 gcm_fatal("No calendars found?") | |
378 | |
50
f8618bae162a
Improve debug messages and comments.
Matti Hamalainen <ccr@tnsp.org>
parents:
49
diff
changeset
|
379 gcm_debug(u"{0} calendars total found.".format(len(calendars))) |
f8618bae162a
Improve debug messages and comments.
Matti Hamalainen <ccr@tnsp.org>
parents:
49
diff
changeset
|
380 |
1 | 381 |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
382 ## Filter desired SOURCE calendars based on specified regexp |
49 | 383 src_re = re.compile(cfg.source_regex, re.UNICODE) |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
384 src_calendars = [] |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
385 for calendar in calendars: |
51
54644b29a9a3
Match also summaryOverride attribute against the source calendar regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
50
diff
changeset
|
386 if u"summary" in calendar: |
50
f8618bae162a
Improve debug messages and comments.
Matti Hamalainen <ccr@tnsp.org>
parents:
49
diff
changeset
|
387 # Find destination calendar ID if not set |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
388 if not cfg.dest_id and cfg.dest_name == calendar["summary"].strip(): |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
389 cfg.mset("dest_id", calendar["id"]) |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
390 |
51
54644b29a9a3
Match also summaryOverride attribute against the source calendar regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
50
diff
changeset
|
391 # If summary or summaryOverride match the regexp, add calendar |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
392 mre = src_re.match(calendar["summary"]) |
52 | 393 if not mre and u"summaryOverride" in calendar: |
394 mre = src_re.match(calendar[u"summaryOverride"]) | |
395 | |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
396 if mre: |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
397 calendar["gcm_title"] = mre.group(cfg.source_regmap[0]) |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
398 calendar["gcm_id"] = mre.group(cfg.source_regmap[1]) |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
399 src_calendars.append(calendar) |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
400 |
50
f8618bae162a
Improve debug messages and comments.
Matti Hamalainen <ccr@tnsp.org>
parents:
49
diff
changeset
|
401 gcm_debug(u"{0} source calendars found.".format(len(src_calendars))) |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
402 |
50
f8618bae162a
Improve debug messages and comments.
Matti Hamalainen <ccr@tnsp.org>
parents:
49
diff
changeset
|
403 |
f8618bae162a
Improve debug messages and comments.
Matti Hamalainen <ccr@tnsp.org>
parents:
49
diff
changeset
|
404 ## Check if we have destination calendar ID |
9
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
405 if not cfg.dest_id: |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
406 gcm_fatal(u"Could not find target/destination calendar ID for '"+ cfg.dest_name +"'.") |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
407 |
01c933dba120
Filter source calendars based on regexp.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
408 |
11
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
409 ## Now, we fetch and collect events |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
410 gcm_debug(u"Fetching calendar events .. ") |
19 | 411 src_events = [] |
31
6becdaa5c45f
Set event colors (just sequentially now) based on which source calendar they belong to.
Matti Hamalainen <ccr@tnsp.org>
parents:
30
diff
changeset
|
412 color_id = 0 |
11
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
413 for calendar in src_calendars: |
31
6becdaa5c45f
Set event colors (just sequentially now) based on which source calendar they belong to.
Matti Hamalainen <ccr@tnsp.org>
parents:
30
diff
changeset
|
414 color_id = color_id + 1 |
54 | 415 gcm_debug(u"- "+calendar["id"]) |
17 | 416 result = service.events().list( |
11
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
417 timeZone="EEST", |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
418 calendarId=calendar["id"], |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
419 singleEvents=True, |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
420 showDeleted=False, |
17 | 421 # orderBy="startTime", |
422 ).execute() | |
11
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
423 |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
424 # Add events, if any, to main list |
26
1267d61f6224
Add function for generating unique internal IDs.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
425 events = gcm_generate_ids(result.get("items", []), calendar["id"]) |
11
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
426 if events: |
31
6becdaa5c45f
Set event colors (just sequentially now) based on which source calendar they belong to.
Matti Hamalainen <ccr@tnsp.org>
parents:
30
diff
changeset
|
427 for event in events: |
6becdaa5c45f
Set event colors (just sequentially now) based on which source calendar they belong to.
Matti Hamalainen <ccr@tnsp.org>
parents:
30
diff
changeset
|
428 event["colorId"] = color_id |
35
be1e798cc60a
Change event's summary to contain source calendar name-id in target calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
34
diff
changeset
|
429 event["summary"] = u"{0} [{1}]".format(event["summary"], calendar["gcm_id"]) |
21
f392d495c5b6
We need to extend a list, not append to it.
Matti Hamalainen <ccr@tnsp.org>
parents:
20
diff
changeset
|
430 src_events.extend(events) |
11
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
431 if cfg.debug: |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
432 gcm_dump_events(events) |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
433 |
fcdee7c04ed8
Implement fetching of source events.
Matti Hamalainen <ccr@tnsp.org>
parents:
10
diff
changeset
|
434 |
13
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
435 ## Get current events |
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
436 gcm_debug(u"Fetching current target calendar events {0}".format(cfg.dest_id)) |
17 | 437 result = service.events().list( |
13
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
438 calendarId=cfg.dest_id, |
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
439 singleEvents=True, |
32 | 440 showDeleted=True, |
441 ).execute() | |
13
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
442 |
33
d58a0a1f23fa
We should not doubly add destination calendar ID here ..
Matti Hamalainen <ccr@tnsp.org>
parents:
32
diff
changeset
|
443 dst_events = gcm_generate_ids(result.get("items", []), "") |
22 | 444 if dst_events: |
445 gcm_debug(u"Found {0} event(s).".format(len(dst_events))) | |
13
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
446 else: |
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
447 gcm_debug(u"No current events.") |
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
448 |
dd240a7ad913
Fetch current events in destination calendar.
Matti Hamalainen <ccr@tnsp.org>
parents:
11
diff
changeset
|
449 |
27 | 450 ## Start merging events .. |
54 | 451 gcm_debug(u"Re-merging events to target calendar ..") |
55 | 452 dst_ids = frozenset(map(lambda x: x["gcm_id"], dst_events)) |
453 src_ids = frozenset(map(lambda x: x["gcm_id"], src_events)) | |
27 | 454 |
455 for event in src_events: | |
456 # Does the event exist already in the target? | |
55 | 457 if event["gcm_id"] in dst_ids: |
39
693db3f8cbe5
Begin importing event comparision stuff.
Matti Hamalainen <ccr@tnsp.org>
parents:
38
diff
changeset
|
458 # Check if event NEEDS updating .. aka compare data |
56 | 459 gcm_debug(u"Event {0} exists, checking ..".format(event["gcm_id"])) |
53
ea62e0ed05ae
Initial implementation of event comparision amd generally more clever about
Matti Hamalainen <ccr@tnsp.org>
parents:
52
diff
changeset
|
460 d_event = gcm_get_event_by_gcm_id(dst_events, event["gcm_id"]) |
55 | 461 if not gcm_compare_events(event, d_event): |
57 | 462 # Seems we need to update |
56 | 463 gcm_debug(u"Updating event {0} ..".format(event["gcm_id"])) |
39
693db3f8cbe5
Begin importing event comparision stuff.
Matti Hamalainen <ccr@tnsp.org>
parents:
38
diff
changeset
|
464 try: |
55 | 465 event.pop("sequence", None) |
39
693db3f8cbe5
Begin importing event comparision stuff.
Matti Hamalainen <ccr@tnsp.org>
parents:
38
diff
changeset
|
466 new_event = service.events().update(calendarId=cfg.dest_id, eventId=event["id"], body=event).execute() |
693db3f8cbe5
Begin importing event comparision stuff.
Matti Hamalainen <ccr@tnsp.org>
parents:
38
diff
changeset
|
467 except Exception as e: |
693db3f8cbe5
Begin importing event comparision stuff.
Matti Hamalainen <ccr@tnsp.org>
parents:
38
diff
changeset
|
468 gcm_fatal("Failed to update event:\n{0}\n\nERROR: {1}\n".format(event, str(e))) |
693db3f8cbe5
Begin importing event comparision stuff.
Matti Hamalainen <ccr@tnsp.org>
parents:
38
diff
changeset
|
469 else: |
54 | 470 gcm_debug(u"No need to update event {0}.".format(event["gcm_id"])) |
27 | 471 else: |
34 | 472 ## Event does not seem to exist. Insert new event. |
54 | 473 gcm_debug(u"Inserting new event {0}".format(event["gcm_id"])) |
34 | 474 event.pop("iCalUID", None) # Remove the iCalUID, having it conflicts with event ID |
475 event["id"] = event["gcm_id"] # Replace Google generated ID with our own | |
476 try: | |
477 new_event = service.events().insert(calendarId=cfg.dest_id, body=event).execute() | |
478 except Exception as e: | |
479 gcm_fatal("Failed to insert new event:\n{0}\n\nERROR: {1}\n".format(event, str(e))) | |
480 | |
481 ## Remove "stale" events | |
54 | 482 gcm_debug(u"Purging stale events ..") |
38
54405de302d0
Attempt to delete stale events. Still needs a check for already deleted
Matti Hamalainen <ccr@tnsp.org>
parents:
37
diff
changeset
|
483 for event in dst_events: |
56 | 484 gcm_debug(u"Checking event {0}".format(event["gcm_id"])) |
55 | 485 if not event["gcm_id"] in src_ids and event["status"] != u"cancelled": |
486 gcm_debug(u"Deleting event {0}".format(event["gcm_id"])) | |
487 # try: | |
488 # service.events().delete(calendarId=cfg.dest_id, eventId=event["id"]).execute() | |
489 # except Exception as e: | |
490 # gcm_fatal("Failed to delete stale event:\n{0}\n\nERROR: {1}\n".format(event, str(e))) | |
38
54405de302d0
Attempt to delete stale events. Still needs a check for already deleted
Matti Hamalainen <ccr@tnsp.org>
parents:
37
diff
changeset
|
491 |
27 | 492 |
54 | 493 gcm_debug(u"Finished.") |