# HG changeset patch # User Paul Boddie # Date 1508883504 -7200 # Node ID b4544a1a80c1cf277763c46fc1fa242039d03aac # Parent f8b6e1ae40f429d568c2967b372942842971a5e9 Moved period collection abstractions into the period module. diff -r f8b6e1ae40f4 -r b4544a1a80c1 imiptools/data.py --- a/imiptools/data.py Wed Oct 25 00:09:19 2017 +0200 +++ b/imiptools/data.py Wed Oct 25 00:18:24 2017 +0200 @@ -19,7 +19,6 @@ this program. If not, see . """ -from bisect import bisect_left, insort_left from datetime import date, datetime, timedelta from email.mime.text import MIMEText from imiptools.dates import format_datetime, get_datetime, \ @@ -30,10 +29,11 @@ get_time, get_timestamp, get_tzid, to_datetime, \ to_timezone, to_utc_datetime from imiptools.freebusy import FreeBusyPeriod -from imiptools.period import Period, RecurringPeriod +from imiptools.period import Period, RecurringPeriod, \ + MergingIterator, RulePeriodCollection from itertools import ifilter from vCalendar import iterwrite, parse, ParseError, to_dict, to_node -from vRecurrence import get_parameters, get_rule +from vRecurrence import get_parameters import email.utils try: @@ -1077,154 +1077,6 @@ return delegators -def rule_has_end(rrule): - - "Return whether 'rrule' defines an end." - - parameters = rrule and get_parameters(rrule) - return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT") - -def make_rule_period(start, duration, attr, tzid): - - """ - Make a period for the rule period starting at 'start' with the given - 'duration' employing the given datetime 'attr' and 'tzid'. - """ - - # Determine the resolution of the period. - - create = len(start) == 3 and date or datetime - start = to_timezone(create(*start), tzid) - end = start + duration - - # Create the period with accompanying metadata based on the main - # period and event details. - - return RecurringPeriod(start, end, tzid, "RRULE", attr) - -class RulePeriodCollection: - - "A collection of rule periods." - - def __init__(self, rrule, main_period, tzid, end, inclusive=False): - - """ - Initialise a period collection for the given 'rrule', employing the - 'main_period' and 'tzid'. - - The specified 'end' datetime indicates the end of the window for which - periods shall be computed. - - If 'inclusive' is set to a true value, any period occurring at the 'end' - will be included. - """ - - self.rrule = rrule - self.main_period = main_period - self.tzid = tzid - - parameters = rrule and get_parameters(rrule) - until = parameters.get("UNTIL") - - # Any UNTIL qualifier changes the nature of the end of the collection. - - if until: - attr = main_period.get_start_attr() - until_dt = to_timezone(get_datetime(until, attr), tzid) - self.end = end and min(until_dt, end) or until_dt - self.inclusive = True - else: - self.end = end - self.inclusive = inclusive - - def __iter__(self): - - """ - Obtain period instances, starting from the main period. Since counting - must start from the first period, filtering from a start date must be - done after the instances have been obtained. - """ - - start = self.main_period.get_start() - selector = get_rule(start, self.rrule) - - return RulePeriodIterator(self.main_period, self.tzid, - selector.select(start, self.end, self.inclusive)) - -class RulePeriodIterator: - - "An iterator over rule periods." - - def __init__(self, main_period, tzid, iterator): - self.attr = main_period.get_start_attr() - self.duration = main_period.get_duration() - self.tzid = tzid - self.iterator = iterator - - def next(self): - recurrence_start = self.iterator.next() - return make_rule_period(recurrence_start, self.duration, self.attr, self.tzid) - -class MergingIterator: - - "An iterator merging ordered collections." - - def __init__(self, iterators): - - "Initialise an iterator merging 'iterators'." - - self.current = [] - - # Populate an ordered collection of (value, iterator) pairs by obtaining - # the first value from each iterator. - - for iterator in iterators: - t = self.get_next(iterator) - if t: - self.current.append(t) - - self.current.sort() - - def __iter__(self): - return self - - def get_next(self, iterator): - - """ - Return a (value, iterator) pair for 'iterator' or None if the iterator - has been exhausted. - """ - - try: - return (iterator.next(), iterator) - except StopIteration: - return None - - def next(self): - - """ - Return the next value in an ordered sequence, choosing it from one of - the available iterators. - """ - - if not self.current: - raise StopIteration - - # Obtain the current value and remove the (value, iterator) pair, - # pending insertion of a new pair for the iterator. - - current, iterator = self.current[0] - del self.current[0] - - # Get the next value, if any and insert the value and iterator into the - # ordered collection. - - t = self.get_next(iterator) - if t: - insort_left(self.current, t) - - return current - def get_periods(obj, tzid, start=None, end=None, inclusive=False): """ @@ -1321,6 +1173,13 @@ return to_timezone(start or datetime.now(), tzid) + timedelta(days) +def rule_has_end(rrule): + + "Return whether 'rrule' defines an end." + + parameters = rrule and get_parameters(rrule) + return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT") + def update_attendees_with_delegates(stored_attendees, attendees): """ diff -r f8b6e1ae40f4 -r b4544a1a80c1 imiptools/period.py --- a/imiptools/period.py Wed Oct 25 00:09:19 2017 +0200 +++ b/imiptools/period.py Wed Oct 25 00:18:24 2017 +0200 @@ -28,6 +28,7 @@ get_start_of_day, \ get_tzid, \ to_timezone, to_utc_datetime +from vRecurrence import get_parameters, get_rule def ifnone(x, y): if x is None: return y @@ -381,6 +382,147 @@ def make_corrected(self, start, end): return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr()) +def make_rule_period(start, duration, attr, tzid): + + """ + Make a period for the rule period starting at 'start' with the given + 'duration' employing the given datetime 'attr' and 'tzid'. + """ + + # Determine the resolution of the period. + + create = len(start) == 3 and date or datetime + start = to_timezone(create(*start), tzid) + end = start + duration + + # Create the period with accompanying metadata based on the main + # period and event details. + + return RecurringPeriod(start, end, tzid, "RRULE", attr) + +class RulePeriodCollection: + + "A collection of rule periods." + + def __init__(self, rrule, main_period, tzid, end, inclusive=False): + + """ + Initialise a period collection for the given 'rrule', employing the + 'main_period' and 'tzid'. + + The specified 'end' datetime indicates the end of the window for which + periods shall be computed. + + If 'inclusive' is set to a true value, any period occurring at the 'end' + will be included. + """ + + self.rrule = rrule + self.main_period = main_period + self.tzid = tzid + + parameters = rrule and get_parameters(rrule) + until = parameters.get("UNTIL") + + # Any UNTIL qualifier changes the nature of the end of the collection. + + if until: + attr = main_period.get_start_attr() + until_dt = to_timezone(get_datetime(until, attr), tzid) + self.end = end and min(until_dt, end) or until_dt + self.inclusive = True + else: + self.end = end + self.inclusive = inclusive + + def __iter__(self): + + """ + Obtain period instances, starting from the main period. Since counting + must start from the first period, filtering from a start date must be + done after the instances have been obtained. + """ + + start = self.main_period.get_start() + selector = get_rule(start, self.rrule) + + return RulePeriodIterator(self.main_period, self.tzid, + selector.select(start, self.end, self.inclusive)) + +class RulePeriodIterator: + + "An iterator over rule periods." + + def __init__(self, main_period, tzid, iterator): + self.attr = main_period.get_start_attr() + self.duration = main_period.get_duration() + self.tzid = tzid + self.iterator = iterator + + def next(self): + recurrence_start = self.iterator.next() + return make_rule_period(recurrence_start, self.duration, self.attr, self.tzid) + +class MergingIterator: + + "An iterator merging ordered collections." + + def __init__(self, iterators): + + "Initialise an iterator merging 'iterators'." + + self.current = [] + + # Populate an ordered collection of (value, iterator) pairs by obtaining + # the first value from each iterator. + + for iterator in iterators: + t = self.get_next(iterator) + if t: + self.current.append(t) + + self.current.sort() + + def __iter__(self): + return self + + def get_next(self, iterator): + + """ + Return a (value, iterator) pair for 'iterator' or None if the iterator + has been exhausted. + """ + + try: + return (iterator.next(), iterator) + except StopIteration: + return None + + def next(self): + + """ + Return the next value in an ordered sequence, choosing it from one of + the available iterators. + """ + + if not self.current: + raise StopIteration + + # Obtain the current value and remove the (value, iterator) pair, + # pending insertion of a new pair for the iterator. + + current, iterator = self.current[0] + del self.current[0] + + # Get the next value, if any and insert the value and iterator into the + # ordered collection. + + t = self.get_next(iterator) + if t: + insort_left(self.current, t) + + return current + def get_overlapping(first, second): """