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