Skip to content

Commit

Permalink
Fix for googleapis#161
Browse files Browse the repository at this point in the history
Calendar `get_events` method now includes a new param 'include_recurring' to include all recurring events. Internally will request a calendarView if 'include_recurring' is True (this is the default behaviour).
Schedule.get_events now calls Calendar.get_events with the default calendar.
Added `remove_filter` method to the Query class
Changed the internal representation of the '_filters' list on the Query class to allow gather filter values
Bumped version to 1.0.4 to release on pypi
  • Loading branch information
Jandro committed Jan 10, 2019
1 parent 133e1b8 commit d3bd00a
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 59 deletions.
98 changes: 54 additions & 44 deletions O365/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1490,7 +1490,10 @@ class Calendar(ApiComponent, HandleRecipientsMixin):
_endpoints = {
'calendar': '/calendars/{id}',
'get_events': '/calendars/{id}/events',
'get_event': '/calendars/{id}/events/{ide}'
'default_events': '/calendar/events',
'events_view': '/calendars/{id}/calendarView',
'default_events_view': '/calendar/calendarView',
'get_event': '/calendars/{id}/events/{ide}',
}
event_constructor = Event

Expand Down Expand Up @@ -1592,8 +1595,8 @@ def delete(self):
return True

def get_events(self, limit=25, *, query=None, order_by=None, batch=None,
download_attachments=False):
""" Get events from the default Calendar
download_attachments=False, include_recurring=True):
""" Get events from the this Calendar
:param int limit: max no. of events to get. Over 999 uses batch.
:param query: applies a OData filter to the request
Expand All @@ -1603,12 +1606,24 @@ def get_events(self, limit=25, *, query=None, order_by=None, batch=None,
:param int batch: batch size, retrieves items in
batches allowing to retrieve more items than the limit.
:param download_attachments: downloads event attachments
:param bool include_recurring: whether to include recurring events or not
:return: list of events in this calendar
:rtype: list[Event] or Pagination
"""

url = self.build_url(
self._endpoints.get('get_events').format(id=self.calendar_id))
if self.calendar_id is None:
# I'm the default calendar
if include_recurring:
url = self.build_url(self._endpoints.get('default_events_view'))
else:
url = self.build_url(self._endpoints.get('default_events'))
else:
if include_recurring:
url = self.build_url(
self._endpoints.get('events_view').format(id=self.calendar_id))
else:
url = self.build_url(
self._endpoints.get('get_events').format(id=self.calendar_id))

if limit is None or limit > self.protocol.max_top_value:
batch = self.protocol.max_top_value
Expand All @@ -1618,6 +1633,33 @@ def get_events(self, limit=25, *, query=None, order_by=None, batch=None,

params = {'$top': batch if batch else limit}

if include_recurring:
start = None
end = None
if query and not isinstance(query, str):
# extract start and end from query because
# those are required by a calendarView
for query_data in query._filters:
if not isinstance(query_data, tuple):
continue
attribute = query_data[0]
# the 2nd position contains the filter data
# and the 3rd position in filter_data contains the value
word = query_data[2][3]

if attribute.startswith('start/'):
start = word.replace("'", '') # remove the quotes
query.remove_filter('start')
if attribute.startswith('end/'):
end = word.replace("'", '') # remove the quotes
query.remove_filter('end')

if start is None or end is None:
raise ValueError("When 'include_recurring' is True you must provide a 'start' and 'end' datetimes inside a Query instance.")

params[self._cc('startDateTime')] = start
params[self._cc('endDateTime')] = end

if order_by:
params['$orderby'] = order_by

Expand Down Expand Up @@ -1699,7 +1741,6 @@ class Schedule(ApiComponent):
'root_calendars': '/calendars',
'get_calendar': '/calendars/{id}',
'default_calendar': '/calendar',
'events': '/calendar/events'
}

calendar_constructor = Calendar
Expand Down Expand Up @@ -1855,7 +1896,7 @@ def get_default_calendar(self):
**{self._cloud_data_key: data})

def get_events(self, limit=25, *, query=None, order_by=None, batch=None,
download_attachments=False):
download_attachments=False, include_recurring=True):
""" Get events from the default Calendar
:param int limit: max no. of events to get. Over 999 uses batch.
Expand All @@ -1866,48 +1907,17 @@ def get_events(self, limit=25, *, query=None, order_by=None, batch=None,
:param int batch: batch size, retrieves items in
batches allowing to retrieve more items than the limit.
:param bool download_attachments: downloads event attachments
:param bool include_recurring: whether to include recurring events or not
:return: list of items in this folder
:rtype: list[Event] or Pagination
"""
url = self.build_url(self._endpoints.get('events'))

if limit is None or limit > self.protocol.max_top_value:
batch = self.protocol.max_top_value

if batch:
download_attachments = False

params = {'$top': batch if batch else limit}

if order_by:
params['$orderby'] = order_by

if query:
if isinstance(query, str):
params['$filter'] = query
else:
params.update(query.as_params())

response = self.con.get(url, params=params,
headers={'Prefer': 'outlook.timezone="UTC"'})
if not response:
return []
default_calendar = self.calendar_constructor(parent=self)

data = response.json()

# Everything received from cloud must be passed as self._cloud_data_key
events = [self.event_constructor(parent=self,
download_attachments
=download_attachments,
**{self._cloud_data_key: event})
for event in data.get('value', [])]
next_link = data.get(NEXT_LINK_KEYWORD, None)
if batch and next_link:
return Pagination(parent=self, data=events,
constructor=self.event_constructor,
next_link=next_link, limit=limit)
else:
return events
return default_calendar.get_events(limit=limit, query=query,
order_by=order_by, batch=batch,
download_attachments=download_attachments,
include_recurring=include_recurring)

def new_event(self, subject=None):
""" Returns a new (unsaved) Event object in the default calendar
Expand Down
53 changes: 39 additions & 14 deletions O365/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ def __init__(self, attribute=None, *, protocol):
self._chain = None
self.new(attribute)
self._negation = False
self._filters = []
self._filters = [] # store all the filters
self._order_by = OrderedDict()
self._selects = set()

Expand Down Expand Up @@ -475,12 +475,33 @@ def on_attribute(self, attribute):
self._attribute = self._get_mapping(attribute)
return self

def _add_filter(self, filter_str):
def remove_filter(self, filter_attr):
""" Removes a filter given the attribute name """
filter_attr = self._get_mapping(filter_attr)
new_filters = []
remove_chain = False

for flt in self._filters:
if isinstance(flt, tuple):
if flt[0] == filter_attr:
remove_chain = True
else:
new_filters.append(flt)
else:
# this is a ChainOperator
if remove_chain is False:
new_filters.append(flt)
else:
remove_chain = False

self._filters = new_filters

def _add_filter(self, *filter_data):
if self._attribute:
if self._filters and not isinstance(self._filters[-1],
ChainOperator):
self._filters.append(self._chain)
self._filters.append((self._attribute, filter_str))
self._filters.append((self._attribute, filter_data[0], filter_data[1]))
else:
raise ValueError(
'Attribute property needed. call on_attribute(attribute) '
Expand All @@ -501,11 +522,11 @@ def _parse_filter_word(self, word):
pytz.utc) # transform local datetime to utc
if '/' in self._attribute:
# TODO: this is a fix for the case when the parameter
# filtered is a string instead a dateTimeOffset
# filtered is a string instead a dateTimeOffset
# but checking the '/' is not correct, but it will
# differentiate for now the case on events:
# differentiate for now the case on events:
# start/dateTime (date is a string here) from
# the case on other dates such as
# the case on other dates such as
# receivedDateTime (date is a dateTimeOffset)
word = "'{}'".format(
word.isoformat()) # convert datetime to isoformat.
Expand All @@ -518,8 +539,9 @@ def _parse_filter_word(self, word):

@staticmethod
def _prepare_sentence(attribute, operation, word, negation=False):
return '{} {} {} {}'.format('not' if negation else '', attribute,
operation, word).strip()
negation = 'not' if negation else ''
attrs = (negation, attribute, operation, word)
return '{} {} {} {}'.format(negation, attribute, operation, word).strip(), attrs

@fluent
def logical_operator(self, operation, word):
Expand All @@ -532,7 +554,7 @@ def logical_operator(self, operation, word):
"""
word = self._parse_filter_word(word)
self._add_filter(
self._prepare_sentence(self._attribute, operation, word,
*self._prepare_sentence(self._attribute, operation, word,
self._negation))
return self

Expand Down Expand Up @@ -592,8 +614,9 @@ def less_equal(self, word):

@staticmethod
def _prepare_function(function_name, attribute, word, negation=False):
return "{} {}({}, {})".format('not' if negation else '', function_name,
attribute, word).strip()
negation = 'not' if negation else ''
attrs = (negation, attribute, function_name, word)
return "{} {}({}, {})".format(negation, function_name, attribute, word).strip(), attrs

@fluent
def function(self, function_name, word):
Expand All @@ -606,7 +629,7 @@ def function(self, function_name, word):
word = self._parse_filter_word(word)

self._add_filter(
self._prepare_function(function_name, self._attribute, word,
*self._prepare_function(function_name, self._attribute, word,
self._negation))
return self

Expand Down Expand Up @@ -679,8 +702,10 @@ def iterable(self, iterable_name, *, collection, attribute, word, func=None,
else:
sentence = self._prepare_sentence(attribute, operation, word)

self._add_filter(
'{}/{}(a:a/{})'.format(collection, iterable_name, sentence))
filter_str, attrs = sentence

filter_data = '{}/{}(a:a/{})'.format(collection, iterable_name, filter_str), attrs
self._add_filter(*filter_data)

self._attribute = current_att

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from setuptools import setup, find_packages


VERSION = '1.0.3'
VERSION = '1.0.4'

# Available classifiers: https://pypi.org/pypi?%3Aaction=list_classifiers
CLASSIFIERS = [
Expand Down

0 comments on commit d3bd00a

Please sign in to comment.