imip-agent

Changeset

1370:b4544a1a80c1
2017-10-25 Paul Boddie raw files shortlog changelog graph Moved period collection abstractions into the period module.
imiptools/data.py (file) imiptools/period.py (file)
     1.1 --- a/imiptools/data.py	Wed Oct 25 00:09:19 2017 +0200
     1.2 +++ b/imiptools/data.py	Wed Oct 25 00:18:24 2017 +0200
     1.3 @@ -19,7 +19,6 @@
     1.4  this program.  If not, see <http://www.gnu.org/licenses/>.
     1.5  """
     1.6  
     1.7 -from bisect import bisect_left, insort_left
     1.8  from datetime import date, datetime, timedelta
     1.9  from email.mime.text import MIMEText
    1.10  from imiptools.dates import format_datetime, get_datetime, \
    1.11 @@ -30,10 +29,11 @@
    1.12                              get_time, get_timestamp, get_tzid, to_datetime, \
    1.13                              to_timezone, to_utc_datetime
    1.14  from imiptools.freebusy import FreeBusyPeriod
    1.15 -from imiptools.period import Period, RecurringPeriod
    1.16 +from imiptools.period import Period, RecurringPeriod, \
    1.17 +                             MergingIterator, RulePeriodCollection
    1.18  from itertools import ifilter
    1.19  from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
    1.20 -from vRecurrence import get_parameters, get_rule
    1.21 +from vRecurrence import get_parameters
    1.22  import email.utils
    1.23  
    1.24  try:
    1.25 @@ -1077,154 +1077,6 @@
    1.26  
    1.27      return delegators
    1.28  
    1.29 -def rule_has_end(rrule):
    1.30 -
    1.31 -    "Return whether 'rrule' defines an end."
    1.32 -
    1.33 -    parameters = rrule and get_parameters(rrule)
    1.34 -    return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT")
    1.35 -
    1.36 -def make_rule_period(start, duration, attr, tzid):
    1.37 -
    1.38 -    """
    1.39 -    Make a period for the rule period starting at 'start' with the given
    1.40 -    'duration' employing the given datetime 'attr' and 'tzid'.
    1.41 -    """
    1.42 -
    1.43 -    # Determine the resolution of the period.
    1.44 -
    1.45 -    create = len(start) == 3 and date or datetime
    1.46 -    start = to_timezone(create(*start), tzid)
    1.47 -    end = start + duration
    1.48 -
    1.49 -    # Create the period with accompanying metadata based on the main
    1.50 -    # period and event details.
    1.51 -
    1.52 -    return RecurringPeriod(start, end, tzid, "RRULE", attr)
    1.53 -
    1.54 -class RulePeriodCollection:
    1.55 -
    1.56 -    "A collection of rule periods."
    1.57 -
    1.58 -    def __init__(self, rrule, main_period, tzid, end, inclusive=False):
    1.59 -
    1.60 -        """
    1.61 -        Initialise a period collection for the given 'rrule', employing the
    1.62 -        'main_period' and 'tzid'.
    1.63 -
    1.64 -        The specified 'end' datetime indicates the end of the window for which
    1.65 -        periods shall be computed.
    1.66 -
    1.67 -        If 'inclusive' is set to a true value, any period occurring at the 'end'
    1.68 -        will be included.
    1.69 -        """
    1.70 -
    1.71 -        self.rrule = rrule
    1.72 -        self.main_period = main_period
    1.73 -        self.tzid = tzid
    1.74 -
    1.75 -        parameters = rrule and get_parameters(rrule)
    1.76 -        until = parameters.get("UNTIL")
    1.77 -
    1.78 -        # Any UNTIL qualifier changes the nature of the end of the collection.
    1.79 -
    1.80 -        if until:
    1.81 -            attr = main_period.get_start_attr()
    1.82 -            until_dt = to_timezone(get_datetime(until, attr), tzid)
    1.83 -            self.end = end and min(until_dt, end) or until_dt
    1.84 -            self.inclusive = True
    1.85 -        else:
    1.86 -            self.end = end
    1.87 -            self.inclusive = inclusive
    1.88 -
    1.89 -    def __iter__(self):
    1.90 -
    1.91 -        """
    1.92 -        Obtain period instances, starting from the main period. Since counting
    1.93 -        must start from the first period, filtering from a start date must be
    1.94 -        done after the instances have been obtained.
    1.95 -        """
    1.96 -
    1.97 -        start = self.main_period.get_start()
    1.98 -        selector = get_rule(start, self.rrule)
    1.99 -
   1.100 -        return RulePeriodIterator(self.main_period, self.tzid,
   1.101 -                               selector.select(start, self.end, self.inclusive))
   1.102 -
   1.103 -class RulePeriodIterator:
   1.104 -
   1.105 -    "An iterator over rule periods."
   1.106 -
   1.107 -    def __init__(self, main_period, tzid, iterator):
   1.108 -        self.attr = main_period.get_start_attr()
   1.109 -        self.duration = main_period.get_duration()
   1.110 -        self.tzid = tzid
   1.111 -        self.iterator = iterator
   1.112 -
   1.113 -    def next(self):
   1.114 -        recurrence_start = self.iterator.next()
   1.115 -        return make_rule_period(recurrence_start, self.duration, self.attr, self.tzid)
   1.116 -
   1.117 -class MergingIterator:
   1.118 -
   1.119 -    "An iterator merging ordered collections."
   1.120 -
   1.121 -    def __init__(self, iterators):
   1.122 -
   1.123 -        "Initialise an iterator merging 'iterators'."
   1.124 -
   1.125 -        self.current = []
   1.126 -
   1.127 -        # Populate an ordered collection of (value, iterator) pairs by obtaining
   1.128 -        # the first value from each iterator.
   1.129 -
   1.130 -        for iterator in iterators:
   1.131 -            t = self.get_next(iterator)
   1.132 -            if t:
   1.133 -                self.current.append(t)
   1.134 -
   1.135 -        self.current.sort()
   1.136 -
   1.137 -    def __iter__(self):
   1.138 -        return self
   1.139 -
   1.140 -    def get_next(self, iterator):
   1.141 -
   1.142 -        """
   1.143 -        Return a (value, iterator) pair for 'iterator' or None if the iterator
   1.144 -        has been exhausted.
   1.145 -        """
   1.146 -
   1.147 -        try:
   1.148 -            return (iterator.next(), iterator)
   1.149 -        except StopIteration:
   1.150 -            return None
   1.151 -
   1.152 -    def next(self):
   1.153 -
   1.154 -        """
   1.155 -        Return the next value in an ordered sequence, choosing it from one of
   1.156 -        the available iterators.
   1.157 -        """
   1.158 -
   1.159 -        if not self.current:
   1.160 -            raise StopIteration
   1.161 -
   1.162 -        # Obtain the current value and remove the (value, iterator) pair,
   1.163 -        # pending insertion of a new pair for the iterator.
   1.164 -
   1.165 -        current, iterator = self.current[0]
   1.166 -        del self.current[0]
   1.167 -
   1.168 -        # Get the next value, if any and insert the value and iterator into the
   1.169 -        # ordered collection.
   1.170 -
   1.171 -        t = self.get_next(iterator)
   1.172 -        if t:
   1.173 -            insort_left(self.current, t)
   1.174 -
   1.175 -        return current
   1.176 -
   1.177  def get_periods(obj, tzid, start=None, end=None, inclusive=False):
   1.178  
   1.179      """
   1.180 @@ -1321,6 +1173,13 @@
   1.181  
   1.182      return to_timezone(start or datetime.now(), tzid) + timedelta(days)
   1.183  
   1.184 +def rule_has_end(rrule):
   1.185 +
   1.186 +    "Return whether 'rrule' defines an end."
   1.187 +
   1.188 +    parameters = rrule and get_parameters(rrule)
   1.189 +    return parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT")
   1.190 +
   1.191  def update_attendees_with_delegates(stored_attendees, attendees):
   1.192  
   1.193      """
     2.1 --- a/imiptools/period.py	Wed Oct 25 00:09:19 2017 +0200
     2.2 +++ b/imiptools/period.py	Wed Oct 25 00:18:24 2017 +0200
     2.3 @@ -28,6 +28,7 @@
     2.4                              get_start_of_day, \
     2.5                              get_tzid, \
     2.6                              to_timezone, to_utc_datetime
     2.7 +from vRecurrence import get_parameters, get_rule
     2.8  
     2.9  def ifnone(x, y):
    2.10      if x is None: return y
    2.11 @@ -381,6 +382,147 @@
    2.12      def make_corrected(self, start, end):
    2.13          return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
    2.14  
    2.15 +def make_rule_period(start, duration, attr, tzid):
    2.16 +
    2.17 +    """
    2.18 +    Make a period for the rule period starting at 'start' with the given
    2.19 +    'duration' employing the given datetime 'attr' and 'tzid'.
    2.20 +    """
    2.21 +
    2.22 +    # Determine the resolution of the period.
    2.23 +
    2.24 +    create = len(start) == 3 and date or datetime
    2.25 +    start = to_timezone(create(*start), tzid)
    2.26 +    end = start + duration
    2.27 +
    2.28 +    # Create the period with accompanying metadata based on the main
    2.29 +    # period and event details.
    2.30 +
    2.31 +    return RecurringPeriod(start, end, tzid, "RRULE", attr)
    2.32 +
    2.33 +class RulePeriodCollection:
    2.34 +
    2.35 +    "A collection of rule periods."
    2.36 +
    2.37 +    def __init__(self, rrule, main_period, tzid, end, inclusive=False):
    2.38 +
    2.39 +        """
    2.40 +        Initialise a period collection for the given 'rrule', employing the
    2.41 +        'main_period' and 'tzid'.
    2.42 +
    2.43 +        The specified 'end' datetime indicates the end of the window for which
    2.44 +        periods shall be computed.
    2.45 +
    2.46 +        If 'inclusive' is set to a true value, any period occurring at the 'end'
    2.47 +        will be included.
    2.48 +        """
    2.49 +
    2.50 +        self.rrule = rrule
    2.51 +        self.main_period = main_period
    2.52 +        self.tzid = tzid
    2.53 +
    2.54 +        parameters = rrule and get_parameters(rrule)
    2.55 +        until = parameters.get("UNTIL")
    2.56 +
    2.57 +        # Any UNTIL qualifier changes the nature of the end of the collection.
    2.58 +
    2.59 +        if until:
    2.60 +            attr = main_period.get_start_attr()
    2.61 +            until_dt = to_timezone(get_datetime(until, attr), tzid)
    2.62 +            self.end = end and min(until_dt, end) or until_dt
    2.63 +            self.inclusive = True
    2.64 +        else:
    2.65 +            self.end = end
    2.66 +            self.inclusive = inclusive
    2.67 +
    2.68 +    def __iter__(self):
    2.69 +
    2.70 +        """
    2.71 +        Obtain period instances, starting from the main period. Since counting
    2.72 +        must start from the first period, filtering from a start date must be
    2.73 +        done after the instances have been obtained.
    2.74 +        """
    2.75 +
    2.76 +        start = self.main_period.get_start()
    2.77 +        selector = get_rule(start, self.rrule)
    2.78 +
    2.79 +        return RulePeriodIterator(self.main_period, self.tzid,
    2.80 +                               selector.select(start, self.end, self.inclusive))
    2.81 +
    2.82 +class RulePeriodIterator:
    2.83 +
    2.84 +    "An iterator over rule periods."
    2.85 +
    2.86 +    def __init__(self, main_period, tzid, iterator):
    2.87 +        self.attr = main_period.get_start_attr()
    2.88 +        self.duration = main_period.get_duration()
    2.89 +        self.tzid = tzid
    2.90 +        self.iterator = iterator
    2.91 +
    2.92 +    def next(self):
    2.93 +        recurrence_start = self.iterator.next()
    2.94 +        return make_rule_period(recurrence_start, self.duration, self.attr, self.tzid)
    2.95 +
    2.96 +class MergingIterator:
    2.97 +
    2.98 +    "An iterator merging ordered collections."
    2.99 +
   2.100 +    def __init__(self, iterators):
   2.101 +
   2.102 +        "Initialise an iterator merging 'iterators'."
   2.103 +
   2.104 +        self.current = []
   2.105 +
   2.106 +        # Populate an ordered collection of (value, iterator) pairs by obtaining
   2.107 +        # the first value from each iterator.
   2.108 +
   2.109 +        for iterator in iterators:
   2.110 +            t = self.get_next(iterator)
   2.111 +            if t:
   2.112 +                self.current.append(t)
   2.113 +
   2.114 +        self.current.sort()
   2.115 +
   2.116 +    def __iter__(self):
   2.117 +        return self
   2.118 +
   2.119 +    def get_next(self, iterator):
   2.120 +
   2.121 +        """
   2.122 +        Return a (value, iterator) pair for 'iterator' or None if the iterator
   2.123 +        has been exhausted.
   2.124 +        """
   2.125 +
   2.126 +        try:
   2.127 +            return (iterator.next(), iterator)
   2.128 +        except StopIteration:
   2.129 +            return None
   2.130 +
   2.131 +    def next(self):
   2.132 +
   2.133 +        """
   2.134 +        Return the next value in an ordered sequence, choosing it from one of
   2.135 +        the available iterators.
   2.136 +        """
   2.137 +
   2.138 +        if not self.current:
   2.139 +            raise StopIteration
   2.140 +
   2.141 +        # Obtain the current value and remove the (value, iterator) pair,
   2.142 +        # pending insertion of a new pair for the iterator.
   2.143 +
   2.144 +        current, iterator = self.current[0]
   2.145 +        del self.current[0]
   2.146 +
   2.147 +        # Get the next value, if any and insert the value and iterator into the
   2.148 +        # ordered collection.
   2.149 +
   2.150 +        t = self.get_next(iterator)
   2.151 +        if t:
   2.152 +            insort_left(self.current, t)
   2.153 +
   2.154 +        return current
   2.155 +
   2.156  def get_overlapping(first, second):
   2.157  
   2.158      """