__init__.py

#

Copyright (C) 2010 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Data for GoogleCL's calendar service.

import datetime
import googlecl
import googlecl.base
import logging
import re
import time
from googlecl.calendar.date import DateRangeParser

service_name = __name__.split('.')[-1]
LOGGER_NAME = __name__
SECTION_HEADER = service_name.upper()

LOG = logging.getLogger(LOGGER_NAME)
#

Rename to reduce verbosity

safe_encode = googlecl.safe_encode
#
def condense_recurring_events(events):
  seen_ids = []
  combined_events = []
  for event in events:
    print "looking at event %s" % event.title.text
    if event.original_event.id not in seen_ids:
      seen_ids.append(event.original_event.id)
      combined_events.append(event)
  return combined_events
#

Convert reminder string to minutes integer.

def convert_reminder_string(reminder):
#

Keyword arguments: reminder: String representation of time, e.g. '10' for 10 minutes, '1d' for one day, '3h' for three hours, etc. Returns: Integer of reminder converted to minutes.

Raises: ValueError if conversion failed.

  if not reminder:
    return None
  unit = reminder.lower()[-1]
  value = reminder[:-1]
  if unit == 's':
    return int(value) / 60
  elif unit == 'm':
    return int(value)
  elif unit == 'h':
    return int(value) * 60
  elif unit == 'd':
    return int(value) * 60 * 24
  elif unit == 'w':
    return int(value) * 60 * 24 * 7
  else:
    return int(reminder)
#
def filter_recurring_events(events, recurrences_expanded):
  if recurrences_expanded:
    is_recurring = lambda event: event.original_event
  else:
    is_recurring = lambda event: event.recurrence
  return [e for e in events if not is_recurring(e)]
#
def filter_single_events(events, recurrences_expanded):
  if recurrences_expanded:
    is_single = lambda event: not event.original_event
  else:
    is_single = lambda event: not event.recurrence
  return [e for e in events if not is_single(e)]
#
def filter_all_day_events_outside_range(start_date, end_date, events):
  if start_date:
    if start_date.all_day:
      start_datetime = start_date.local
    else:
      start_datetime = datetime.datetime(year=start_date.local.year,
                                         month=start_date.local.month,
                                         day=start_date.local.day)
  if end_date:
    if end_date.all_day:
      inclusive_end_datetime = end_date.local + datetime.timedelta(hours=24)
    else:
      end_datetime = datetime.datetime(year=end_date.local.year,
                                       month=end_date.local.month,
                                       day=end_date.local.day)
  new_events = []
  for event in events:
    try:
      start = datetime.datetime.strptime(event.when[0].start_time, '%Y-%m-%d')
      end = datetime.datetime.strptime(event.when[0].end_time, '%Y-%m-%d')
    except ValueError, err:
      if str(err).find('unconverted data remains') == -1:
        raise err
      else:
#

Errors that complain of unconverted data are events with duration

        new_events.append(event)
    else:
      if ((not start_date or start >= start_datetime) and
          (not end_date or end <= inclusive_end_datetime)):
        new_events.append(event)
      elif event.recurrence:
#

While writing the below comment, I was 90% sure it was true. Testing this case, however, showed that things worked out just fine -- the events were filtered out. I must have misunderstood the "when" data.

#

The tricky case: an Event that describes a recurring all-day event. In the rare case that: NO recurrences occur in the given range AND AT LEAST ONE recurrence occurs just outside the given range (AND it's an all-day recurrence), we will incorrectly return this event. This is unavoidable unless we a) perform another query or b) incorporate a recurrence parser.

        new_events.append(event)

  return new_events
#
def filter_canceled_events(events, recurrences_expanded):
  AT_LEAST_ONE_EVENT = 'not dead yet!'
  canceled_recurring_events = {}
  ongoing_events = []
  is_canceled = lambda e: e.event_status.value == 'CANCELED' or not e.when

  for event in events:
    print 'looking at event %s' % event.title.text
    if recurrences_expanded:
      if event.original_event:
        print 'event is original: %s' % event.title.text
        try:
          status = canceled_recurring_events[event.original_event.id]
        except KeyError:
          status = None
        if is_canceled(event) and status != AT_LEAST_ONE_EVENT:
          print 'adding event to canceled: %s' % event.title.text
          canceled_recurring_events[event.original_event.id] = event
        if not is_canceled(event):
          print 'at least one more of: %s' % event.title.text
          canceled_recurring_events[event.original_event.id]= AT_LEAST_ONE_EVENT
      ongoing_events.append(event)
#

If recurrences have not been expanded, we can't tell if they were canceled or not.

    if not is_canceled(event):
      ongoing_events.append(event)

  for event in canceled_recurring_events.values():
    if event != AT_LEAST_ONE_EVENT:
      ongoing_events.remove(event)

  return ongoing_events
#

Get datetime objects for the start and end of the event specified by a

def get_datetimes(cal_entry):
#

calendar entry.

Keyword arguments: cal_entry: A CalendarEventEntry.

Returns: (start_time, end_time, freq) where start_time - datetime object of the start of the event. end_time - datetime object of the end of the event. freq - string that tells how often the event repeats (NoneType if the event does not repeat (does not have a gd:recurrence element)).

  if cal_entry.recurrence:
    return parse_recurrence(cal_entry.recurrence.text)
  else:
    freq = None
    when = cal_entry.when[0]
    try:
#

Trim the string data from "when" to only include down to seconds

      start_time_data = time.strptime(when.start_time[:19],
                                      '%Y-%m-%dT%H:%M:%S')
      end_time_data = time.strptime(when.end_time[:19],
                                    '%Y-%m-%dT%H:%M:%S')
    except ValueError:
#

Try to handle date format for all-day events

      start_time_data = time.strptime(when.start_time, '%Y-%m-%d')
      end_time_data = time.strptime(when.end_time, '%Y-%m-%d')
  return (start_time_data, end_time_data, freq)
#

Parse recurrence data found in event entry.

def parse_recurrence(time_string):
#

Keyword arguments: time_string: Value of entry's recurrence.text field.

Returns: Tuple of (start_time, end_time, frequency). All values are in the user's current timezone (I hope). start_time and end_time are datetime objects, and frequency is a dictionary mapping RFC 2445 RRULE parameters to their values. (http://www.ietf.org/rfc/rfc2445.txt, section 4.3.10)

Google calendars uses a pretty limited section of RFC 2445, and I'm abusing that here. This will probably break if Google ever changes how they handle recurrence, or how the recurrence string is built.

  data = time_string.split('\n')
  start_time_string = data[0].split(':')[-1]
  start_time = time.strptime(start_time_string,'%Y%m%dT%H%M%S')

  end_time_string = data[1].split(':')[-1]
  end_time = time.strptime(end_time_string,'%Y%m%dT%H%M%S')

  freq_string = data[2][6:]
  freq_properties = freq_string.split(';')
  freq = {}
  for prop in freq_properties:
    key, value = prop.split('=')
    freq[key] = value
  return (start_time, end_time, freq)
#
class CalendarEntryToStringWrapper(googlecl.base.BaseEntryToStringWrapper):
#

Initialize a CalendarEntry wrapper.

  def __init__(self, entry, config):
#

Args: entry: CalendarEntry to interpret to strings. config: Configuration parser. Needed for some values.

    googlecl.base.BaseEntryToStringWrapper.__init__(self, entry)
    self.config_parser = config
#

When event takes place.

  @property
  def when(self):
#
    start_date, end_date, freq = get_datetimes(self.entry)
    print_format = self.config_parser.lazy_get(SECTION_HEADER,
                                                      'date_print_format')
    start_text = time.strftime(print_format, start_date)
    end_text = time.strftime(print_format, end_date)
    value = start_text + ' - ' + end_text
    if freq:
      if freq.has_key('BYDAY'):
        value += ' (' + freq['BYDAY'].lower() + ')'
      else:
        value += ' (' + freq['FREQ'].lower() + ')'
    return value
#

Where event takes place

  @property
  def where(self):
#
    return self._join(self.entry.where, text_attribute='value_string')
#
def _list(client, options, args):
  cal_user_list = client.get_calendar_user_list(options.cal)
  if not cal_user_list:
    LOG.error('No calendar matches "' + options.cal + '"')
    return
  titles_list = googlecl.build_titles_list(options.title, args)
  parser = DateRangeParser()
  date_range = parser.parse(options.date)
  for cal in cal_user_list:
    print ''
    print safe_encode('[' + unicode(cal) + ']')
    single_events = client.get_events(cal.user,
                                      start_date=date_range.start,
                                      end_date=date_range.end,
                                      titles=titles_list,
                                      query=options.query,
                                      split=False)

    for entry in single_events:
      print googlecl.base.compile_entry_string(
                            CalendarEntryToStringWrapper(entry, client.config),
                            options.fields.split(','),
                            delimiter=options.delimiter)
#
######################################################################### <span id="each-of-the-following-run*-functions-execute-a-particular-task.

keyword-arguments: -client:-client-to-the-service-being-used. -options:-contains-all-attributes-required-to-perform-the-task -args:-additional-arguments-passed-in-on-the-command-line,-may-or-may-not-be -------required" href="each-of-the-following-run*-functions-execute-a-particular-task.

keyword-arguments: -client:-client-to-the-service-being-used. -options:-contains-all-attributes-required-to-perform-the-task -args:-additional-arguments-passed-in-on-the-command-line,-may-or-may-not-be -------required"> Each of the following run* functions execute a particular task.

Keyword arguments: client: Client to the service being used. options: Contains all attributes required to perform the task args: Additional arguments passed in on the command line, may or may not be required

def _run_list(client, options, args):
#

If no other search parameters are mentioned, set date to be today. (Prevent user from retrieving all events ever)

  if not (options.title or args  or options.query or options.date):
    options.date = 'today,'
  _list(client, options, args)
#
def _run_list_today(client, options, args):
  options.date = 'today'
  _list(client, options, args)
#
def _run_add(client, options, args):
  cal_user_list = client.get_calendar_user_list(options.cal)
  if not cal_user_list:
    LOG.error('No calendar matches "' + options.cal + '"')
    return
  reminder_in_minutes = convert_reminder_string(options.reminder)
  events_list = options.src + args
  reminder_results = []
  for cal in cal_user_list:
    if options.date:
      results = client.full_add_event(events_list, cal.user, options.date,
                                      reminder_in_minutes)
    else:
      results = client.quick_add_event(events_list, cal.user)
      if reminder_in_minutes is not None:
        reminder_results = client.add_reminders(cal.user,
                                                results,
                                                reminder_in_minutes)
    if LOG.isEnabledFor(logging.DEBUG):
      for entry in results + reminder_results:
        LOG.debug('ID: %s, status: %s, reason: %s',
                  entry.batch_id.text,
                  entry.batch_status.code,
                  entry.batch_status.reason)
    for entry in results:
      LOG.info('Event created: %s' % entry.GetHtmlLink().href)
#
def _run_delete(client, options, args):
  cal_user_list = client.get_calendar_user_list(options.cal)
  if not cal_user_list:
    LOG.error('No calendar matches "' + options.cal + '"')
    return
  parser = DateRangeParser()
  date_range = parser.parse(options.date)

  titles_list = googlecl.build_titles_list(options.title, args)
  for cal in cal_user_list:
    single_events, recurring_events = client.get_events(cal.user,
                                                    start_date=date_range.start,
                                                    end_date=date_range.end,
                                                    titles=titles_list,
                                                    query=options.query,
                                                    expand_recurrence=True)
    if options.prompt:
      LOG.info(safe_encode('For calendar ' + unicode(cal)))
    if single_events:
      client.DeleteEntryList(single_events, 'event', options.prompt)
    if recurring_events:
      if date_range.specified_as_range:
#

if the user specified a date that was a range...

        client.delete_recurring_events(recurring_events, date_range.start,
                                       date_range.end, cal.user, options.prompt)
      else:
        client.delete_recurring_events(recurring_events, date_range.start,
                                       None, cal.user, options.prompt)
    if not (single_events or recurring_events):
      LOG.warning('No events found that match your options!')


TASKS = {'list': googlecl.base.Task('List events on a calendar',
                                    callback=_run_list,
                                    required=['fields', 'delimiter'],
                                    optional=['title', 'query',
                                              'date', 'cal']),
         'today': googlecl.base.Task('List events for the next 24 hours',
                                     callback=_run_list_today,
                                     required=['fields', 'delimiter'],
                                     optional=['title', 'query', 'cal']),
         'add': googlecl.base.Task('Add event to a calendar',
                                   callback=_run_add,
                                   required='src',
                                   optional='cal'),
         'delete': googlecl.base.Task('Delete event from a calendar',
                                      callback=_run_delete,
                                      required=[['title', 'query']],
                                      optional=['date', 'cal'])}