PageRenderTime 36ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/googlecl/calendar/__init__.py

http://googlecl.googlecode.com/
Python | 393 lines | 380 code | 0 blank | 13 comment | 0 complexity | 4f6b3b70de8e0d54f1fd13a7cc44023a MD5 | raw file
  1. # Copyright (C) 2010 Google Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Data for GoogleCL's calendar service."""
  15. import datetime
  16. import googlecl
  17. import googlecl.base
  18. import logging
  19. import re
  20. import time
  21. from googlecl.calendar.date import DateRangeParser
  22. service_name = __name__.split('.')[-1]
  23. LOGGER_NAME = __name__
  24. SECTION_HEADER = service_name.upper()
  25. LOG = logging.getLogger(LOGGER_NAME)
  26. # Rename to reduce verbosity
  27. safe_encode = googlecl.safe_encode
  28. def condense_recurring_events(events):
  29. seen_ids = []
  30. combined_events = []
  31. for event in events:
  32. print "looking at event %s" % event.title.text
  33. if event.original_event.id not in seen_ids:
  34. seen_ids.append(event.original_event.id)
  35. combined_events.append(event)
  36. return combined_events
  37. def convert_reminder_string(reminder):
  38. """Convert reminder string to minutes integer.
  39. Keyword arguments:
  40. reminder: String representation of time,
  41. e.g. '10' for 10 minutes,
  42. '1d' for one day,
  43. '3h' for three hours, etc.
  44. Returns:
  45. Integer of reminder converted to minutes.
  46. Raises:
  47. ValueError if conversion failed.
  48. """
  49. if not reminder:
  50. return None
  51. unit = reminder.lower()[-1]
  52. value = reminder[:-1]
  53. if unit == 's':
  54. return int(value) / 60
  55. elif unit == 'm':
  56. return int(value)
  57. elif unit == 'h':
  58. return int(value) * 60
  59. elif unit == 'd':
  60. return int(value) * 60 * 24
  61. elif unit == 'w':
  62. return int(value) * 60 * 24 * 7
  63. else:
  64. return int(reminder)
  65. def filter_recurring_events(events, recurrences_expanded):
  66. if recurrences_expanded:
  67. is_recurring = lambda event: event.original_event
  68. else:
  69. is_recurring = lambda event: event.recurrence
  70. return [e for e in events if not is_recurring(e)]
  71. def filter_single_events(events, recurrences_expanded):
  72. if recurrences_expanded:
  73. is_single = lambda event: not event.original_event
  74. else:
  75. is_single = lambda event: not event.recurrence
  76. return [e for e in events if not is_single(e)]
  77. def filter_all_day_events_outside_range(start_date, end_date, events):
  78. if start_date:
  79. if start_date.all_day:
  80. start_datetime = start_date.local
  81. else:
  82. start_datetime = datetime.datetime(year=start_date.local.year,
  83. month=start_date.local.month,
  84. day=start_date.local.day)
  85. if end_date:
  86. if end_date.all_day:
  87. inclusive_end_datetime = end_date.local + datetime.timedelta(hours=24)
  88. else:
  89. end_datetime = datetime.datetime(year=end_date.local.year,
  90. month=end_date.local.month,
  91. day=end_date.local.day)
  92. new_events = []
  93. for event in events:
  94. try:
  95. start = datetime.datetime.strptime(event.when[0].start_time, '%Y-%m-%d')
  96. end = datetime.datetime.strptime(event.when[0].end_time, '%Y-%m-%d')
  97. except ValueError, err:
  98. if str(err).find('unconverted data remains') == -1:
  99. raise err
  100. else:
  101. #Errors that complain of unconverted data are events with duration
  102. new_events.append(event)
  103. else:
  104. if ((not start_date or start >= start_datetime) and
  105. (not end_date or end <= inclusive_end_datetime)):
  106. new_events.append(event)
  107. elif event.recurrence:
  108. # While writing the below comment, I was 90% sure it was true. Testing
  109. # this case, however, showed that things worked out just fine -- the
  110. # events were filtered out. I must have misunderstood the "when" data.
  111. # The tricky case: an Event that describes a recurring all-day event.
  112. # In the rare case that:
  113. # NO recurrences occur in the given range AND AT LEAST ONE recurrence
  114. # occurs just outside the given range (AND it's an all-day recurrence),
  115. # we will incorrectly return this event.
  116. # This is unavoidable unless we a) perform another query or b)
  117. # incorporate a recurrence parser.
  118. new_events.append(event)
  119. return new_events
  120. def filter_canceled_events(events, recurrences_expanded):
  121. AT_LEAST_ONE_EVENT = 'not dead yet!'
  122. canceled_recurring_events = {}
  123. ongoing_events = []
  124. is_canceled = lambda e: e.event_status.value == 'CANCELED' or not e.when
  125. for event in events:
  126. print 'looking at event %s' % event.title.text
  127. if recurrences_expanded:
  128. if event.original_event:
  129. print 'event is original: %s' % event.title.text
  130. try:
  131. status = canceled_recurring_events[event.original_event.id]
  132. except KeyError:
  133. status = None
  134. if is_canceled(event) and status != AT_LEAST_ONE_EVENT:
  135. print 'adding event to canceled: %s' % event.title.text
  136. canceled_recurring_events[event.original_event.id] = event
  137. if not is_canceled(event):
  138. print 'at least one more of: %s' % event.title.text
  139. canceled_recurring_events[event.original_event.id]= AT_LEAST_ONE_EVENT
  140. ongoing_events.append(event)
  141. # If recurrences have not been expanded, we can't tell if they were
  142. # canceled or not.
  143. if not is_canceled(event):
  144. ongoing_events.append(event)
  145. for event in canceled_recurring_events.values():
  146. if event != AT_LEAST_ONE_EVENT:
  147. ongoing_events.remove(event)
  148. return ongoing_events
  149. def get_datetimes(cal_entry):
  150. """Get datetime objects for the start and end of the event specified by a
  151. calendar entry.
  152. Keyword arguments:
  153. cal_entry: A CalendarEventEntry.
  154. Returns:
  155. (start_time, end_time, freq) where
  156. start_time - datetime object of the start of the event.
  157. end_time - datetime object of the end of the event.
  158. freq - string that tells how often the event repeats (NoneType if the
  159. event does not repeat (does not have a gd:recurrence element)).
  160. """
  161. if cal_entry.recurrence:
  162. return parse_recurrence(cal_entry.recurrence.text)
  163. else:
  164. freq = None
  165. when = cal_entry.when[0]
  166. try:
  167. # Trim the string data from "when" to only include down to seconds
  168. start_time_data = time.strptime(when.start_time[:19],
  169. '%Y-%m-%dT%H:%M:%S')
  170. end_time_data = time.strptime(when.end_time[:19],
  171. '%Y-%m-%dT%H:%M:%S')
  172. except ValueError:
  173. # Try to handle date format for all-day events
  174. start_time_data = time.strptime(when.start_time, '%Y-%m-%d')
  175. end_time_data = time.strptime(when.end_time, '%Y-%m-%d')
  176. return (start_time_data, end_time_data, freq)
  177. def parse_recurrence(time_string):
  178. """Parse recurrence data found in event entry.
  179. Keyword arguments:
  180. time_string: Value of entry's recurrence.text field.
  181. Returns:
  182. Tuple of (start_time, end_time, frequency). All values are in the user's
  183. current timezone (I hope). start_time and end_time are datetime objects,
  184. and frequency is a dictionary mapping RFC 2445 RRULE parameters to their
  185. values. (http://www.ietf.org/rfc/rfc2445.txt, section 4.3.10)
  186. """
  187. # Google calendars uses a pretty limited section of RFC 2445, and I'm
  188. # abusing that here. This will probably break if Google ever changes how
  189. # they handle recurrence, or how the recurrence string is built.
  190. data = time_string.split('\n')
  191. start_time_string = data[0].split(':')[-1]
  192. start_time = time.strptime(start_time_string,'%Y%m%dT%H%M%S')
  193. end_time_string = data[1].split(':')[-1]
  194. end_time = time.strptime(end_time_string,'%Y%m%dT%H%M%S')
  195. freq_string = data[2][6:]
  196. freq_properties = freq_string.split(';')
  197. freq = {}
  198. for prop in freq_properties:
  199. key, value = prop.split('=')
  200. freq[key] = value
  201. return (start_time, end_time, freq)
  202. class CalendarEntryToStringWrapper(googlecl.base.BaseEntryToStringWrapper):
  203. def __init__(self, entry, config):
  204. """Initialize a CalendarEntry wrapper.
  205. Args:
  206. entry: CalendarEntry to interpret to strings.
  207. config: Configuration parser. Needed for some values.
  208. """
  209. googlecl.base.BaseEntryToStringWrapper.__init__(self, entry)
  210. self.config_parser = config
  211. @property
  212. def when(self):
  213. """When event takes place."""
  214. start_date, end_date, freq = get_datetimes(self.entry)
  215. print_format = self.config_parser.lazy_get(SECTION_HEADER,
  216. 'date_print_format')
  217. start_text = time.strftime(print_format, start_date)
  218. end_text = time.strftime(print_format, end_date)
  219. value = start_text + ' - ' + end_text
  220. if freq:
  221. if freq.has_key('BYDAY'):
  222. value += ' (' + freq['BYDAY'].lower() + ')'
  223. else:
  224. value += ' (' + freq['FREQ'].lower() + ')'
  225. return value
  226. @property
  227. def where(self):
  228. """Where event takes place"""
  229. return self._join(self.entry.where, text_attribute='value_string')
  230. def _list(client, options, args):
  231. cal_user_list = client.get_calendar_user_list(options.cal)
  232. if not cal_user_list:
  233. LOG.error('No calendar matches "' + options.cal + '"')
  234. return
  235. titles_list = googlecl.build_titles_list(options.title, args)
  236. parser = DateRangeParser()
  237. date_range = parser.parse(options.date)
  238. for cal in cal_user_list:
  239. print ''
  240. print safe_encode('[' + unicode(cal) + ']')
  241. single_events = client.get_events(cal.user,
  242. start_date=date_range.start,
  243. end_date=date_range.end,
  244. titles=titles_list,
  245. query=options.query,
  246. split=False)
  247. for entry in single_events:
  248. print googlecl.base.compile_entry_string(
  249. CalendarEntryToStringWrapper(entry, client.config),
  250. options.fields.split(','),
  251. delimiter=options.delimiter)
  252. #===============================================================================
  253. # Each of the following _run_* functions execute a particular task.
  254. #
  255. # Keyword arguments:
  256. # client: Client to the service being used.
  257. # options: Contains all attributes required to perform the task
  258. # args: Additional arguments passed in on the command line, may or may not be
  259. # required
  260. #===============================================================================
  261. def _run_list(client, options, args):
  262. # If no other search parameters are mentioned, set date to be
  263. # today. (Prevent user from retrieving all events ever)
  264. if not (options.title or args or options.query or options.date):
  265. options.date = 'today,'
  266. _list(client, options, args)
  267. def _run_list_today(client, options, args):
  268. options.date = 'today'
  269. _list(client, options, args)
  270. def _run_add(client, options, args):
  271. cal_user_list = client.get_calendar_user_list(options.cal)
  272. if not cal_user_list:
  273. LOG.error('No calendar matches "' + options.cal + '"')
  274. return
  275. reminder_in_minutes = convert_reminder_string(options.reminder)
  276. events_list = options.src + args
  277. reminder_results = []
  278. for cal in cal_user_list:
  279. if options.date:
  280. results = client.full_add_event(events_list, cal.user, options.date,
  281. reminder_in_minutes)
  282. else:
  283. results = client.quick_add_event(events_list, cal.user)
  284. if reminder_in_minutes is not None:
  285. reminder_results = client.add_reminders(cal.user,
  286. results,
  287. reminder_in_minutes)
  288. if LOG.isEnabledFor(logging.DEBUG):
  289. for entry in results + reminder_results:
  290. LOG.debug('ID: %s, status: %s, reason: %s',
  291. entry.batch_id.text,
  292. entry.batch_status.code,
  293. entry.batch_status.reason)
  294. for entry in results:
  295. LOG.info('Event created: %s' % entry.GetHtmlLink().href)
  296. def _run_delete(client, options, args):
  297. cal_user_list = client.get_calendar_user_list(options.cal)
  298. if not cal_user_list:
  299. LOG.error('No calendar matches "' + options.cal + '"')
  300. return
  301. parser = DateRangeParser()
  302. date_range = parser.parse(options.date)
  303. titles_list = googlecl.build_titles_list(options.title, args)
  304. for cal in cal_user_list:
  305. single_events, recurring_events = client.get_events(cal.user,
  306. start_date=date_range.start,
  307. end_date=date_range.end,
  308. titles=titles_list,
  309. query=options.query,
  310. expand_recurrence=True)
  311. if options.prompt:
  312. LOG.info(safe_encode('For calendar ' + unicode(cal)))
  313. if single_events:
  314. client.DeleteEntryList(single_events, 'event', options.prompt)
  315. if recurring_events:
  316. if date_range.specified_as_range:
  317. # if the user specified a date that was a range...
  318. client.delete_recurring_events(recurring_events, date_range.start,
  319. date_range.end, cal.user, options.prompt)
  320. else:
  321. client.delete_recurring_events(recurring_events, date_range.start,
  322. None, cal.user, options.prompt)
  323. if not (single_events or recurring_events):
  324. LOG.warning('No events found that match your options!')
  325. TASKS = {'list': googlecl.base.Task('List events on a calendar',
  326. callback=_run_list,
  327. required=['fields', 'delimiter'],
  328. optional=['title', 'query',
  329. 'date', 'cal']),
  330. 'today': googlecl.base.Task('List events for the next 24 hours',
  331. callback=_run_list_today,
  332. required=['fields', 'delimiter'],
  333. optional=['title', 'query', 'cal']),
  334. 'add': googlecl.base.Task('Add event to a calendar',
  335. callback=_run_add,
  336. required='src',
  337. optional='cal'),
  338. 'delete': googlecl.base.Task('Delete event from a calendar',
  339. callback=_run_delete,
  340. required=[['title', 'query']],
  341. optional=['date', 'cal'])}