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)
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'])}