imip-agent

Changeset

1062:2264ab469f6d
2016-03-02 Paul Boddie raw files shortlog changelog graph Introduced a free/busy collection abstraction for potential access and representation efficiency improvements. freebusy-collections
imip_store.py (file) imiptools/client.py (file) imiptools/data.py (file) imiptools/handlers/common.py (file) imiptools/handlers/scheduling/freebusy.py (file) imiptools/period.py (file) imipweb/calendar.py (file) imipweb/event.py (file) tools/make_freebusy.py (file)
     1.1 --- a/imip_store.py	Tue Feb 09 15:57:23 2016 +0100
     1.2 +++ b/imip_store.py	Wed Mar 02 21:17:11 2016 +0100
     1.3 @@ -24,7 +24,7 @@
     1.4  from imiptools.data import make_calendar, parse_object, to_stream
     1.5  from imiptools.dates import format_datetime, get_datetime, to_timezone
     1.6  from imiptools.filesys import fix_permissions, FileBase
     1.7 -from imiptools.period import FreeBusyPeriod
     1.8 +from imiptools.period import FreeBusyPeriod, FreeBusyCollection
     1.9  from imiptools.text import parse_line
    1.10  from os.path import isdir, isfile, join
    1.11  from os import listdir, remove, rmdir
    1.12 @@ -577,23 +577,29 @@
    1.13          "Get free/busy details for the given 'user'."
    1.14  
    1.15          filename = self.get_object_in_store(user, name or "freebusy")
    1.16 +
    1.17          if not filename or not isfile(filename):
    1.18 -            return []
    1.19 +            periods = []
    1.20          else:
    1.21 -            return map(lambda t: FreeBusyPeriod(*t),
    1.22 +            periods = map(lambda t: FreeBusyPeriod(*t),
    1.23                  (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
    1.24  
    1.25 +        return FreeBusyCollection(periods)
    1.26 +
    1.27      def get_freebusy_for_other(self, user, other, get_table=None):
    1.28  
    1.29          "For the given 'user', get free/busy details for the 'other' user."
    1.30  
    1.31          filename = self.get_object_in_store(user, "freebusy-other", other)
    1.32 +
    1.33          if not filename or not isfile(filename):
    1.34 -            return []
    1.35 +            periods = []
    1.36          else:
    1.37 -            return map(lambda t: FreeBusyPeriod(*t),
    1.38 +            periods = map(lambda t: FreeBusyPeriod(*t),
    1.39                  (get_table or self._get_table_atomic)(user, filename, [(4, None)]))
    1.40  
    1.41 +        return FreeBusyCollection(periods)
    1.42 +
    1.43      def set_freebusy(self, user, freebusy, name=None, set_table=None):
    1.44  
    1.45          "For the given 'user', set 'freebusy' details."
    1.46 @@ -603,7 +609,7 @@
    1.47              return False
    1.48  
    1.49          (set_table or self._set_table_atomic)(user, filename,
    1.50 -            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
    1.51 +            map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
    1.52          return True
    1.53  
    1.54      def set_freebusy_for_other(self, user, freebusy, other, set_table=None):
    1.55 @@ -615,7 +621,7 @@
    1.56              return False
    1.57  
    1.58          (set_table or self._set_table_atomic)(user, filename,
    1.59 -            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
    1.60 +            map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
    1.61          return True
    1.62  
    1.63      # Tentative free/busy periods related to countering.
    1.64 @@ -644,7 +650,7 @@
    1.65          finally:
    1.66              self.release_lock(user)
    1.67  
    1.68 -        return offers
    1.69 +        return FreeBusyCollection(offers)
    1.70  
    1.71      def set_freebusy_offers(self, user, freebusy):
    1.72  
    1.73 @@ -1011,11 +1017,14 @@
    1.74          "Get free/busy details for the given 'quota' and 'user'."
    1.75  
    1.76          filename = self.get_object_in_store(quota, "freebusy", user)
    1.77 -        if not filename or not isfile(filename):
    1.78 -            return []
    1.79  
    1.80 -        return map(lambda t: FreeBusyPeriod(*t),
    1.81 -            (get_table or self._get_table_atomic)(quota, filename, [(4, None)]))
    1.82 +        if not filename or not isfile(filename):
    1.83 +            periods = []
    1.84 +        else:
    1.85 +            periods = map(lambda t: FreeBusyPeriod(*t),
    1.86 +                (get_table or self._get_table_atomic)(quota, filename, [(4, None)]))
    1.87 +
    1.88 +        return FreeBusyCollection(periods)
    1.89  
    1.90      def set_freebusy(self, quota, user, freebusy, set_table=None):
    1.91  
    1.92 @@ -1026,7 +1035,7 @@
    1.93              return False
    1.94  
    1.95          (set_table or self._set_table_atomic)(quota, filename,
    1.96 -            map(lambda fb: fb.as_tuple(strings_only=True), freebusy))
    1.97 +            map(lambda fb: fb.as_tuple(strings_only=True), freebusy.periods))
    1.98          return True
    1.99  
   1.100      # Journal entry methods.
   1.101 @@ -1039,11 +1048,14 @@
   1.102          """
   1.103  
   1.104          filename = self.get_object_in_store(quota, "journal", group)
   1.105 -        if not filename or not isfile(filename):
   1.106 -            return []
   1.107  
   1.108 -        return map(lambda t: FreeBusyPeriod(*t),
   1.109 -            self._get_table_atomic(quota, filename, [(4, None)]))
   1.110 +        if not filename or not isfile(filename):
   1.111 +            periods = []
   1.112 +        else:
   1.113 +            periods = map(lambda t: FreeBusyPeriod(*t),
   1.114 +                self._get_table_atomic(quota, filename, [(4, None)]))
   1.115 +
   1.116 +        return FreeBusyCollection(periods)
   1.117  
   1.118      def set_entries(self, quota, group, entries):
   1.119  
   1.120 @@ -1057,7 +1069,7 @@
   1.121              return False
   1.122  
   1.123          self._set_table_atomic(quota, filename,
   1.124 -            map(lambda fb: fb.as_tuple(strings_only=True), entries))
   1.125 +            map(lambda fb: fb.as_tuple(strings_only=True), entries.periods))
   1.126          return True
   1.127  
   1.128  # vim: tabstop=4 expandtab shiftwidth=4
     2.1 --- a/imiptools/client.py	Tue Feb 09 15:57:23 2016 +0100
     2.2 +++ b/imiptools/client.py	Wed Mar 02 21:17:11 2016 +0100
     2.3 @@ -27,9 +27,6 @@
     2.4  from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \
     2.5                              get_duration, get_timestamp
     2.6  from imiptools.i18n import get_translator
     2.7 -from imiptools.period import can_schedule, remove_event_periods, \
     2.8 -                             remove_additional_periods, remove_affected_period, \
     2.9 -                             update_freebusy
    2.10  from imiptools.profile import Preferences
    2.11  import imip_store
    2.12  
    2.13 @@ -288,7 +285,7 @@
    2.14          offer.
    2.15          """
    2.16  
    2.17 -        update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires)
    2.18 +        freebusy.update_freebusy(periods, transp, uid, recurrenceid, summary, organiser, expires)
    2.19  
    2.20      # Preparation of messages communicating the state of events.
    2.21  
    2.22 @@ -862,7 +859,7 @@
    2.23          Indicate whether within 'freebusy' the given 'periods' can be scheduled.
    2.24          """
    2.25  
    2.26 -        return can_schedule(freebusy, periods, self.uid, self.recurrenceid)
    2.27 +        return freebusy.can_schedule(periods, self.uid, self.recurrenceid)
    2.28  
    2.29      def have_new_object(self, strict=True):
    2.30  
    2.31 @@ -993,9 +990,9 @@
    2.32  
    2.33          "Remove this event from the given 'freebusy' collection."
    2.34  
    2.35 -        removed = remove_event_periods(freebusy, self.uid, self.recurrenceid)
    2.36 +        removed = freebusy.remove_event_periods(self.uid, self.recurrenceid)
    2.37          if not removed and self.recurrenceid:
    2.38 -            return remove_affected_period(freebusy, self.uid, self.get_recurrence_start_point(self.recurrenceid))
    2.39 +            return freebusy.remove_affected_period(self.uid, self.get_recurrence_start_point(self.recurrenceid))
    2.40          else:
    2.41              return removed
    2.42  
    2.43 @@ -1011,18 +1008,18 @@
    2.44  
    2.45          if self.recurrenceid:
    2.46              recurrenceid = self.get_recurrence_start_point(self.recurrenceid)
    2.47 -            remove_affected_period(freebusy, self.uid, recurrenceid)
    2.48 +            freebusy.remove_affected_period(self.uid, recurrenceid)
    2.49          else:
    2.50              # Remove obsolete recurrence periods.
    2.51  
    2.52 -            remove_additional_periods(freebusy, self.uid, recurrenceids)
    2.53 +            freebusy.remove_additional_periods(self.uid, recurrenceids)
    2.54  
    2.55              # Remove original periods affected by additional recurrences.
    2.56  
    2.57              if recurrenceids:
    2.58                  for recurrenceid in recurrenceids:
    2.59                      recurrenceid = self.get_recurrence_start_point(recurrenceid)
    2.60 -                    remove_affected_period(freebusy, self.uid, recurrenceid)
    2.61 +                    freebusy.remove_affected_period(self.uid, recurrenceid)
    2.62  
    2.63      def update_freebusy(self, freebusy, user, as_organiser, offer=False):
    2.64  
     3.1 --- a/imiptools/data.py	Tue Feb 09 15:57:23 2016 +0100
     3.2 +++ b/imiptools/data.py	Wed Mar 02 21:17:11 2016 +0100
     3.3 @@ -3,7 +3,7 @@
     3.4  """
     3.5  Interpretation of vCalendar content.
     3.6  
     3.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
     3.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
     3.9  
    3.10  This program is free software; you can redistribute it and/or modify it under
    3.11  the terms of the GNU General Public License as published by the Free Software
    3.12 @@ -29,7 +29,7 @@
    3.13                              get_recurrence_start_point, \
    3.14                              get_time, get_tzid, to_datetime, to_timezone, \
    3.15                              to_utc_datetime
    3.16 -from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod, period_overlaps
    3.17 +from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod
    3.18  from vCalendar import iterwrite, parse, ParseError, to_dict, to_node
    3.19  from vRecurrence import get_parameters, get_rule
    3.20  import email.utils
    3.21 @@ -568,7 +568,7 @@
    3.22          # Get a constrained view if start and end limits are specified.
    3.23  
    3.24          if period:
    3.25 -            periods = period_overlaps(freebusy, period, True)
    3.26 +            periods = freebusy.period_overlaps(period, True)
    3.27          else:
    3.28              periods = freebusy
    3.29  
     4.1 --- a/imiptools/handlers/common.py	Tue Feb 09 15:57:23 2016 +0100
     4.2 +++ b/imiptools/handlers/common.py	Wed Mar 02 21:17:11 2016 +0100
     4.3 @@ -3,7 +3,7 @@
     4.4  """
     4.5  Common handler functionality for different entities.
     4.6  
     4.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
     4.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
     4.9  
    4.10  This program is free software; you can redistribute it and/or modify it under
    4.11  the terms of the GNU General Public License as published by the Free Software
    4.12 @@ -22,7 +22,7 @@
    4.13  from imiptools.data import get_address, get_uri, make_freebusy, to_part, \
    4.14                             uri_dict
    4.15  from imiptools.dates import format_datetime
    4.16 -from imiptools.period import FreeBusyPeriod, Period, replace_overlapping
    4.17 +from imiptools.period import FreeBusyPeriod, Period
    4.18  
    4.19  class CommonFreebusy:
    4.20  
    4.21 @@ -59,7 +59,7 @@
    4.22  
    4.23          for sender, sender_attr in senders:
    4.24              stored_freebusy = self.store.get_freebusy_for_other(self.user, sender)
    4.25 -            replace_overlapping(stored_freebusy, period, freebusy)
    4.26 +            stored_freebusy.replace_overlapping(period, freebusy)
    4.27              self.store.set_freebusy_for_other(self.user, stored_freebusy, sender)
    4.28  
    4.29      def request(self):
     5.1 --- a/imiptools/handlers/scheduling/freebusy.py	Tue Feb 09 15:57:23 2016 +0100
     5.2 +++ b/imiptools/handlers/scheduling/freebusy.py	Wed Mar 02 21:17:11 2016 +0100
     5.3 @@ -21,8 +21,6 @@
     5.4  
     5.5  from imiptools.data import uri_values
     5.6  from imiptools.dates import ValidityError, to_timezone
     5.7 -from imiptools.period import coalesce_freebusy, invert_freebusy, \
     5.8 -                             periods_from, remove_periods
     5.9  
    5.10  def schedule_in_freebusy(handler, args, freebusy=None):
    5.11  
    5.12 @@ -117,17 +115,16 @@
    5.13          if attendee != handler.user:
    5.14              freebusy = handler.get_store().get_freebusy_for_other(handler.user, attendee)
    5.15              if freebusy:
    5.16 -                remove_periods(freebusy, event_periods)
    5.17 +                freebusy.remove_periods(event_periods)
    5.18                  busy += freebusy
    5.19  
    5.20      # Obtain the combined busy periods.
    5.21  
    5.22 -    busy.sort()
    5.23 -    busy = coalesce_freebusy(busy)
    5.24 +    busy = busy.coalesce_freebusy()
    5.25  
    5.26      # Obtain free periods.
    5.27  
    5.28 -    free = invert_freebusy(busy)
    5.29 +    free = busy.invert_freebusy()
    5.30      permitted_values = handler.get_permitted_values()
    5.31      periods = []
    5.32  
    5.33 @@ -153,7 +150,7 @@
    5.34  
    5.35          # Get free periods from the time of each period.
    5.36  
    5.37 -        for found in periods_from(free, period):
    5.38 +        for found in free.periods_from(period):
    5.39  
    5.40              # Skip any periods before the last period.
    5.41  
     6.1 --- a/imiptools/period.py	Tue Feb 09 15:57:23 2016 +0100
     6.2 +++ b/imiptools/period.py	Wed Mar 02 21:17:11 2016 +0100
     6.3 @@ -454,280 +454,328 @@
     6.4      def make_corrected(self, start, end):
     6.5          return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
     6.6  
     6.7 -# Time and period management.
     6.8 +class FreeBusyCollection:
     6.9 +
    6.10 +    "An abstraction for a collection of free/busy periods."
    6.11 +
    6.12 +    def __init__(self, periods=None):
    6.13 +
    6.14 +        """
    6.15 +        Initialise the collection with the given list of 'periods', or start an
    6.16 +        empty collection if no list is given.
    6.17 +        """
    6.18 +
    6.19 +        self.periods = periods or []
    6.20 +
    6.21 +    # List emulation methods.
    6.22  
    6.23 -def can_schedule(freebusy, periods, uid, recurrenceid):
    6.24 +    def __list__(self):
    6.25 +        return self.periods
    6.26 +
    6.27 +    def __iter__(self):
    6.28 +        return iter(self.periods)
    6.29 +
    6.30 +    def __len__(self):
    6.31 +        return len(self.periods)
    6.32 +
    6.33 +    def __getitem__(self, i):
    6.34 +        return self.periods[i]
    6.35 +
    6.36 +    def __iadd__(self, other):
    6.37 +        for period in other:
    6.38 +            self.insert_period(period)
    6.39 +        return self
    6.40 +
    6.41 +    # Operations.
    6.42  
    6.43 -    """
    6.44 -    Return whether the 'freebusy' list can accommodate the given 'periods'
    6.45 -    employing the specified 'uid' and 'recurrenceid'.
    6.46 -    """
    6.47 +    def can_schedule(self, periods, uid, recurrenceid):
    6.48 +
    6.49 +        """
    6.50 +        Return whether the collection can accommodate the given 'periods'
    6.51 +        employing the specified 'uid' and 'recurrenceid'.
    6.52 +        """
    6.53 +
    6.54 +        for conflict in self.have_conflict(periods, True):
    6.55 +            if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
    6.56 +                return False
    6.57 +
    6.58 +        return True
    6.59 +
    6.60 +    def have_conflict(self, periods, get_conflicts=False):
    6.61  
    6.62 -    for conflict in have_conflict(freebusy, periods, True):
    6.63 -        if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
    6.64 +        """
    6.65 +        Return whether any period in the collection overlaps with the given
    6.66 +        'periods', returning a collection of such overlapping periods if
    6.67 +        'get_conflicts' is set to a true value.
    6.68 +        """
    6.69 +
    6.70 +        conflicts = set()
    6.71 +        for p in periods:
    6.72 +            overlapping = self.period_overlaps(p, get_conflicts)
    6.73 +            if overlapping:
    6.74 +                if get_conflicts:
    6.75 +                    conflicts.update(overlapping)
    6.76 +                else:
    6.77 +                    return True
    6.78 +
    6.79 +        if get_conflicts:
    6.80 +            return conflicts
    6.81 +        else:
    6.82              return False
    6.83  
    6.84 -    return True
    6.85 +    def insert_period(self, period):
    6.86  
    6.87 -def have_conflict(freebusy, periods, get_conflicts=False):
    6.88 +        "Insert the given 'period' into the collection."
    6.89  
    6.90 -    """
    6.91 -    Return whether any period in 'freebusy' overlaps with the given 'periods',
    6.92 -    returning a collection of such overlapping periods if 'get_conflicts' is
    6.93 -    set to a true value.
    6.94 -    """
    6.95 +        i = bisect_left(self.periods, period)
    6.96 +        if i == len(self.periods):
    6.97 +            self.periods.append(period)
    6.98 +        elif self.periods[i] != period:
    6.99 +            self.periods.insert(i, period)
   6.100 +
   6.101 +    def remove_periods(self, periods):
   6.102  
   6.103 -    conflicts = set()
   6.104 -    for p in periods:
   6.105 -        overlapping = period_overlaps(freebusy, p, get_conflicts)
   6.106 -        if overlapping:
   6.107 -            if get_conflicts:
   6.108 -                conflicts.update(overlapping)
   6.109 -            else:
   6.110 -                return True
   6.111 +        "Remove the given 'periods' from the collection."
   6.112 +
   6.113 +        for period in periods:
   6.114 +            i = bisect_left(self.periods, period)
   6.115 +            if i < len(self.periods) and self.periods[i] == period:
   6.116 +                del self.periods[i]
   6.117  
   6.118 -    if get_conflicts:
   6.119 -        return conflicts
   6.120 -    else:
   6.121 -        return False
   6.122 +    def remove_event_periods(self, uid, recurrenceid=None):
   6.123  
   6.124 -def insert_period(freebusy, period):
   6.125 -
   6.126 -    "Insert into 'freebusy' the given 'period'."
   6.127 +        """
   6.128 +        Remove from the collection all periods associated with 'uid' and
   6.129 +        'recurrenceid' (which if omitted causes the "parent" object's periods to
   6.130 +        be referenced).
   6.131  
   6.132 -    i = bisect_left(freebusy, period)
   6.133 -    if i == len(freebusy):
   6.134 -        freebusy.append(period)
   6.135 -    elif freebusy[i] != period:
   6.136 -        freebusy.insert(i, period)
   6.137 -
   6.138 -def remove_periods(freebusy, periods):
   6.139 +        Return the removed periods.
   6.140 +        """
   6.141  
   6.142 -    "Remove from 'freebusy' the given 'periods'."
   6.143 -
   6.144 -    for period in periods:
   6.145 -        i = bisect_left(freebusy, period)
   6.146 -        if i < len(freebusy) and freebusy[i] == period:
   6.147 -            del freebusy[i]
   6.148 -
   6.149 -def remove_event_periods(freebusy, uid, recurrenceid=None):
   6.150 +        removed = []
   6.151 +        i = 0
   6.152 +        while i < len(self.periods):
   6.153 +            fb = self.periods[i]
   6.154 +            if fb.uid == uid and fb.recurrenceid == recurrenceid:
   6.155 +                removed.append(self.periods[i])
   6.156 +                del self.periods[i]
   6.157 +            else:
   6.158 +                i += 1
   6.159  
   6.160 -    """
   6.161 -    Remove from 'freebusy' all periods associated with 'uid' and 'recurrenceid'
   6.162 -    (which if omitted causes the "parent" object's periods to be referenced).
   6.163 +        return removed
   6.164  
   6.165 -    Return the removed periods.
   6.166 -    """
   6.167 +    def remove_additional_periods(self, uid, recurrenceids=None):
   6.168  
   6.169 -    removed = []
   6.170 -    i = 0
   6.171 -    while i < len(freebusy):
   6.172 -        fb = freebusy[i]
   6.173 -        if fb.uid == uid and fb.recurrenceid == recurrenceid:
   6.174 -            removed.append(freebusy[i])
   6.175 -            del freebusy[i]
   6.176 -        else:
   6.177 -            i += 1
   6.178 +        """
   6.179 +        Remove from the collection all periods associated with 'uid' having a
   6.180 +        recurrence identifier indicating an additional or modified period.
   6.181  
   6.182 -    return removed
   6.183 +        If 'recurrenceids' is specified, remove all periods associated with
   6.184 +        'uid' that do not have a recurrence identifier in the given list.
   6.185 +
   6.186 +        Return the removed periods.
   6.187 +        """
   6.188  
   6.189 -def remove_additional_periods(freebusy, uid, recurrenceids=None):
   6.190 -
   6.191 -    """
   6.192 -    Remove from 'freebusy' all periods associated with 'uid' having a
   6.193 -    recurrence identifier indicating an additional or modified period.
   6.194 +        removed = []
   6.195 +        i = 0
   6.196 +        while i < len(self.periods):
   6.197 +            fb = self.periods[i]
   6.198 +            if fb.uid == uid and fb.recurrenceid and (
   6.199 +                recurrenceids is None or
   6.200 +                recurrenceids is not None and fb.recurrenceid not in recurrenceids
   6.201 +                ):
   6.202 +                removed.append(self.periods[i])
   6.203 +                del self.periods[i]
   6.204 +            else:
   6.205 +                i += 1
   6.206  
   6.207 -    If 'recurrenceids' is specified, remove all periods associated with 'uid'
   6.208 -    that do not have a recurrence identifier in the given list.
   6.209 +        return removed
   6.210  
   6.211 -    Return the removed periods.
   6.212 -    """
   6.213 +    def remove_affected_period(self, uid, start):
   6.214  
   6.215 -    removed = []
   6.216 -    i = 0
   6.217 -    while i < len(freebusy):
   6.218 -        fb = freebusy[i]
   6.219 -        if fb.uid == uid and fb.recurrenceid and (
   6.220 -            recurrenceids is None or
   6.221 -            recurrenceids is not None and fb.recurrenceid not in recurrenceids
   6.222 -            ):
   6.223 -            removed.append(freebusy[i])
   6.224 -            del freebusy[i]
   6.225 -        else:
   6.226 -            i += 1
   6.227 +        """
   6.228 +        Remove from the collection the period associated with 'uid' that
   6.229 +        provides an occurrence starting at the given 'start' (provided by a
   6.230 +        recurrence identifier, converted to a datetime). A recurrence identifier
   6.231 +        is used to provide an alternative time period whilst also acting as a
   6.232 +        reference to the originally-defined occurrence.
   6.233 +
   6.234 +        Return any removed period in a list.
   6.235 +        """
   6.236  
   6.237 -    return removed
   6.238 +        removed = []
   6.239 +
   6.240 +        search = Period(start, start)
   6.241 +        found = bisect_left(self.periods, search)
   6.242  
   6.243 -def remove_affected_period(freebusy, uid, start):
   6.244 +        while found < len(self.periods):
   6.245 +            fb = self.periods[found]
   6.246 +
   6.247 +            # Stop looking if the start no longer matches the recurrence identifier.
   6.248  
   6.249 -    """
   6.250 -    Remove from 'freebusy' a period associated with 'uid' that provides an
   6.251 -    occurrence starting at the given 'start' (provided by a recurrence
   6.252 -    identifier, converted to a datetime). A recurrence identifier is used to
   6.253 -    provide an alternative time period whilst also acting as a reference to the
   6.254 -    originally-defined occurrence.
   6.255 +            if fb.get_start_point() != search.get_start_point():
   6.256 +                break
   6.257 +
   6.258 +            # If the period belongs to the parent object, remove it and return.
   6.259  
   6.260 -    Return any removed period in a list.
   6.261 -    """
   6.262 -
   6.263 -    removed = []
   6.264 +            if not fb.recurrenceid and uid == fb.uid:
   6.265 +                removed.append(self.periods[found])
   6.266 +                del self.periods[found]
   6.267 +                break
   6.268  
   6.269 -    search = Period(start, start)
   6.270 -    found = bisect_left(freebusy, search)
   6.271 +            # Otherwise, keep looking for a matching period.
   6.272 +
   6.273 +            found += 1
   6.274  
   6.275 -    while found < len(freebusy):
   6.276 -        fb = freebusy[found]
   6.277 +        return removed
   6.278 +
   6.279 +    def periods_from(self, period):
   6.280  
   6.281 -        # Stop looking if the start no longer matches the recurrence identifier.
   6.282 +        "Return the entries in the collection at or after 'period'."
   6.283  
   6.284 -        if fb.get_start_point() != search.get_start_point():
   6.285 -            break
   6.286 +        first = bisect_left(self.periods, period)
   6.287 +        return self.periods[first:]
   6.288  
   6.289 -        # If the period belongs to the parent object, remove it and return.
   6.290 +    def periods_until(self, period):
   6.291 +
   6.292 +        "Return the entries in the collection before 'period'."
   6.293  
   6.294 -        if not fb.recurrenceid and uid == fb.uid:
   6.295 -            removed.append(freebusy[found])
   6.296 -            del freebusy[found]
   6.297 -            break
   6.298 +        last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
   6.299 +        return self.periods[:last]
   6.300 +
   6.301 +    def get_overlapping(self, period):
   6.302  
   6.303 -        # Otherwise, keep looking for a matching period.
   6.304 -
   6.305 -        found += 1
   6.306 -
   6.307 -    return removed
   6.308 -
   6.309 -def periods_from(freebusy, period):
   6.310 +        """
   6.311 +        Return the entries in the collection providing periods overlapping with
   6.312 +        'period'.
   6.313 +        """
   6.314  
   6.315 -    "Return the entries in 'freebusy' at or after 'period'."
   6.316 +        # Find the range of periods potentially overlapping the period in the
   6.317 +        # free/busy collection.
   6.318  
   6.319 -    first = bisect_left(freebusy, period)
   6.320 -    return freebusy[first:]
   6.321 +        startpoints = self.periods_until(period)
   6.322  
   6.323 -def periods_until(freebusy, period):
   6.324 +        # Find the range of periods potentially overlapping the period in a version
   6.325 +        # of the free/busy collection sorted according to end datetimes.
   6.326  
   6.327 -    "Return the entries in 'freebusy' before 'period'."
   6.328 +        endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
   6.329 +        endpoints.sort()
   6.330 +        first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
   6.331 +        endpoints = endpoints[first:]
   6.332  
   6.333 -    last = bisect_right(freebusy, Period(period.get_end(), period.get_end(), period.get_tzid()))
   6.334 -    return freebusy[:last]
   6.335 +        overlapping = set()
   6.336  
   6.337 -def get_overlapping(freebusy, period):
   6.338 +        for p, fb in endpoints:
   6.339 +            if fb.overlaps(period):
   6.340 +                overlapping.add(fb)
   6.341  
   6.342 -    """
   6.343 -    Return the entries in 'freebusy' providing periods overlapping with
   6.344 -    'period'.
   6.345 -    """
   6.346 +        overlapping = list(overlapping)
   6.347 +        overlapping.sort()
   6.348 +        return overlapping
   6.349  
   6.350 -    # Find the range of periods potentially overlapping the period in the
   6.351 -    # free/busy collection.
   6.352 +    def period_overlaps(self, period, get_periods=False):
   6.353  
   6.354 -    startpoints = periods_until(freebusy, period)
   6.355 -
   6.356 -    # Find the range of periods potentially overlapping the period in a version
   6.357 -    # of the free/busy collection sorted according to end datetimes.
   6.358 +        """
   6.359 +        Return whether any period in the collection overlaps with the given
   6.360 +        'period', returning a collection of overlapping periods if 'get_periods'
   6.361 +        is set to a true value.
   6.362 +        """
   6.363  
   6.364 -    endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]
   6.365 -    endpoints.sort()
   6.366 -    first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))
   6.367 -    endpoints = endpoints[first:]
   6.368 -
   6.369 -    overlapping = set()
   6.370 +        overlapping = self.get_overlapping(period)
   6.371  
   6.372 -    for p, fb in endpoints:
   6.373 -        if fb.overlaps(period):
   6.374 -            overlapping.add(fb)
   6.375 +        if get_periods:
   6.376 +            return overlapping
   6.377 +        else:
   6.378 +            return len(overlapping) != 0
   6.379  
   6.380 -    overlapping = list(overlapping)
   6.381 -    overlapping.sort()
   6.382 -    return overlapping
   6.383 +    def remove_overlapping(self, period):
   6.384  
   6.385 -def period_overlaps(freebusy, period, get_periods=False):
   6.386 +        "Remove all periods overlapping with 'period' from the collection."
   6.387 +
   6.388 +        overlapping = self.get_overlapping(period)
   6.389  
   6.390 -    """
   6.391 -    Return whether any period in 'freebusy' overlaps with the given 'period',
   6.392 -    returning a collection of overlapping periods if 'get_periods' is set to a
   6.393 -    true value.
   6.394 -    """
   6.395 +        if overlapping:
   6.396 +            for fb in overlapping:
   6.397 +                self.periods.remove(fb)
   6.398  
   6.399 -    overlapping = get_overlapping(freebusy, period)
   6.400 +    def replace_overlapping(self, period, replacements):
   6.401  
   6.402 -    if get_periods:
   6.403 -        return overlapping
   6.404 -    else:
   6.405 -        return len(overlapping) != 0
   6.406 +        """
   6.407 +        Replace existing periods in the collection within the given 'period',
   6.408 +        using the given 'replacements'.
   6.409 +        """
   6.410  
   6.411 -def remove_overlapping(freebusy, period):
   6.412 +        self.remove_overlapping(period)
   6.413 +        for replacement in replacements:
   6.414 +            self.insert_period(replacement)
   6.415  
   6.416 -    "Remove from 'freebusy' all periods overlapping with 'period'."
   6.417 +    def coalesce_freebusy(self):
   6.418  
   6.419 -    overlapping = get_overlapping(freebusy, period)
   6.420 +        "Coalesce the periods in the collection, returning a new collection."
   6.421  
   6.422 -    if overlapping:
   6.423 -        for fb in overlapping:
   6.424 -            freebusy.remove(fb)
   6.425 +        if not self.periods:
   6.426 +            return FreeBusyCollection(self.periods)
   6.427  
   6.428 -def replace_overlapping(freebusy, period, replacements):
   6.429 +        fb = []
   6.430 +        start = self.periods[0].get_start_point()
   6.431 +        end = self.periods[0].get_end_point()
   6.432  
   6.433 -    """
   6.434 -    Replace existing periods in 'freebusy' within the given 'period', using the
   6.435 -    given 'replacements'.
   6.436 -    """
   6.437 -
   6.438 -    remove_overlapping(freebusy, period)
   6.439 -    for replacement in replacements:
   6.440 -        insert_period(freebusy, replacement)
   6.441 -
   6.442 -def coalesce_freebusy(freebusy):
   6.443 +        for period in self.periods[1:]:
   6.444 +            if period.get_start_point() > end:
   6.445 +                fb.append(FreeBusyPeriod(start, end))
   6.446 +                start = period.get_start_point()
   6.447 +                end = period.get_end_point()
   6.448 +            else:
   6.449 +                end = max(end, period.get_end_point())
   6.450  
   6.451 -    "Coalesce the periods in 'freebusy'."
   6.452 +        fb.append(FreeBusyPeriod(start, end))
   6.453 +        return FreeBusyCollection(fb)
   6.454  
   6.455 -    if not freebusy:
   6.456 -        return freebusy
   6.457 +    def invert_freebusy(self):
   6.458 +
   6.459 +        "Return the free periods from the collection as a new collection."
   6.460  
   6.461 -    fb = []
   6.462 -    start = freebusy[0].get_start_point()
   6.463 -    end = freebusy[0].get_end_point()
   6.464 +        if not self.periods:
   6.465 +            return FreeBusyCollection([FreeBusyPeriod(None, None)])
   6.466 +
   6.467 +        # Coalesce periods that overlap or are adjacent.
   6.468  
   6.469 -    for period in freebusy[1:]:
   6.470 -        if period.get_start_point() > end:
   6.471 -            fb.append(FreeBusyPeriod(start, end))
   6.472 -            start = period.get_start_point()
   6.473 -            end = period.get_end_point()
   6.474 -        else:
   6.475 -            end = max(end, period.get_end_point())
   6.476 +        fb = self.coalesce_freebusy()
   6.477 +        free = []
   6.478 +
   6.479 +        # Add a start-of-time period if appropriate.
   6.480  
   6.481 -    fb.append(FreeBusyPeriod(start, end))
   6.482 -    return fb
   6.483 +        first = fb[0].get_start_point()
   6.484 +        if first:
   6.485 +            free.append(FreeBusyPeriod(None, first))
   6.486  
   6.487 -def invert_freebusy(freebusy):
   6.488 -
   6.489 -    "Return the free periods from 'freebusy'."
   6.490 +        start = fb[0].get_end_point()
   6.491  
   6.492 -    if not freebusy:
   6.493 -        return [FreeBusyPeriod(None, None)]
   6.494 -
   6.495 -    # Coalesce periods that overlap or are adjacent.
   6.496 +        for period in fb[1:]:
   6.497 +            free.append(FreeBusyPeriod(start, period.get_start_point()))
   6.498 +            start = period.get_end_point()
   6.499  
   6.500 -    fb = coalesce_freebusy(freebusy)
   6.501 -    free = []
   6.502 +        # Add an end-of-time period if appropriate.
   6.503  
   6.504 -    # Add a start-of-time period if appropriate.
   6.505 +        if start:
   6.506 +            free.append(FreeBusyPeriod(start, None))
   6.507  
   6.508 -    first = fb[0].get_start_point()
   6.509 -    if first:
   6.510 -        free.append(FreeBusyPeriod(None, first))
   6.511 +        return FreeBusyCollection(free)
   6.512 +
   6.513 +    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
   6.514  
   6.515 -    start = fb[0].get_end_point()
   6.516 +        """
   6.517 +        Update the free/busy details with the given 'periods', 'transp' setting,
   6.518 +        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
   6.519  
   6.520 -    for period in fb[1:]:
   6.521 -        free.append(FreeBusyPeriod(start, period.get_start_point()))
   6.522 -        start = period.get_end_point()
   6.523 +        An optional 'expires' datetime string indicates the expiry time of any
   6.524 +        free/busy offer.
   6.525 +        """
   6.526  
   6.527 -    # Add an end-of-time period if appropriate.
   6.528 +        self.remove_event_periods(uid, recurrenceid)
   6.529  
   6.530 -    if start:
   6.531 -        free.append(FreeBusyPeriod(start, None))
   6.532 -
   6.533 -    return free
   6.534 +        for p in periods:
   6.535 +            self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
   6.536  
   6.537  # Period layout.
   6.538  
   6.539 @@ -1014,19 +1062,4 @@
   6.540  
   6.541      return spans
   6.542  
   6.543 -def update_freebusy(freebusy, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
   6.544 -
   6.545 -    """
   6.546 -    Update the free/busy details with the given 'periods', 'transp' setting,
   6.547 -    'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
   6.548 -
   6.549 -    An optional 'expires' datetime string indicates the expiry time of any
   6.550 -    free/busy offer.
   6.551 -    """
   6.552 -
   6.553 -    remove_event_periods(freebusy, uid, recurrenceid)
   6.554 -
   6.555 -    for p in periods:
   6.556 -        insert_period(freebusy, FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
   6.557 -
   6.558  # vim: tabstop=4 expandtab shiftwidth=4
     7.1 --- a/imipweb/calendar.py	Tue Feb 09 15:57:23 2016 +0100
     7.2 +++ b/imipweb/calendar.py	Wed Mar 02 21:17:11 2016 +0100
     7.3 @@ -3,7 +3,7 @@
     7.4  """
     7.5  A Web interface to an event calendar.
     7.6  
     7.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
     7.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
     7.9  
    7.10  This program is free software; you can redistribute it and/or modify it under
    7.11  the terms of the GNU General Public License as published by the Free Software
    7.12 @@ -26,7 +26,6 @@
    7.13                              get_start_of_next_day, get_timestamp, ends_on_same_day, \
    7.14                              to_date, to_timezone
    7.15  from imiptools.period import add_day_start_points, add_empty_days, add_slots, \
    7.16 -                             get_overlapping, \
    7.17                               get_scale, get_slots, get_spans, partition_by_day, \
    7.18                               remove_end_slot, Period, Point
    7.19  from imipweb.resource import FormUtilities, ResourceClient
    7.20 @@ -323,8 +322,8 @@
    7.21          view_end = view_period.get_end()
    7.22          duration = view_period.get_duration()
    7.23  
    7.24 -        preceding_events = view_start and get_overlapping(freebusy, Period(None, view_start, self.get_tzid())) or []
    7.25 -        following_events = view_end and get_overlapping(freebusy, Period(view_end, None, self.get_tzid())) or []
    7.26 +        preceding_events = view_start and freebusy.get_overlapping(Period(None, view_start, self.get_tzid())) or []
    7.27 +        following_events = view_end and freebusy.get_overlapping(Period(view_end, None, self.get_tzid())) or []
    7.28  
    7.29          last_preceding = preceding_events and to_date(preceding_events[-1].get_end()) + timedelta(1) or None
    7.30          first_following = following_events and to_date(following_events[0].get_start()) or None
     8.1 --- a/imipweb/event.py	Tue Feb 09 15:57:23 2016 +0100
     8.2 +++ b/imipweb/event.py	Wed Mar 02 21:17:11 2016 +0100
     8.3 @@ -3,7 +3,7 @@
     8.4  """
     8.5  A Web interface to a calendar event.
     8.6  
     8.7 -Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
     8.8 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
     8.9  
    8.10  This program is free software; you can redistribute it and/or modify it under
    8.11  the terms of the GNU General Public License as published by the Free Software
    8.12 @@ -23,7 +23,6 @@
    8.13                             uri_parts, uri_values
    8.14  from imiptools.dates import format_datetime, to_timezone
    8.15  from imiptools.mail import Messenger
    8.16 -from imiptools.period import have_conflict
    8.17  from imipweb.data import EventPeriod, event_period_from_period, FormPeriod, PeriodError
    8.18  from imipweb.resource import DateTimeFormUtilities, FormUtilities, ResourceClientForObject
    8.19  
    8.20 @@ -713,7 +712,7 @@
    8.21              partstat = participant_attr and participant_attr.get("PARTSTAT")
    8.22              recurrences = self.obj.get_recurrence_start_points(recurrenceids, tzid)
    8.23  
    8.24 -            for p in have_conflict(freebusy, periods, True):
    8.25 +            for p in freebusy.have_conflict(periods, True):
    8.26                  if not self.recurrenceid and p.is_replaced(recurrences):
    8.27                      continue
    8.28  
     9.1 --- a/tools/make_freebusy.py	Tue Feb 09 15:57:23 2016 +0100
     9.2 +++ b/tools/make_freebusy.py	Wed Mar 02 21:17:11 2016 +0100
     9.3 @@ -37,7 +37,7 @@
     9.4  from imiptools.client import Client
     9.5  from imiptools.data import get_window_end, Object
     9.6  from imiptools.dates import get_default_timezone, to_utc_datetime
     9.7 -from imiptools.period import insert_period
     9.8 +from imiptools.period import FreeBusyCollection
     9.9  from imip_store import FileStore, FilePublisher, FileJournal
    9.10  
    9.11  def make_freebusy(client, participant, store_and_publish, include_needs_action,
    9.12 @@ -86,7 +86,7 @@
    9.13  
    9.14      if not all_events:
    9.15          all_events = store.get_all_events(user)
    9.16 -        fb = []
    9.17 +        fb = FreeBusyCollection()
    9.18  
    9.19      # With providers of additional periods, append to the existing collection.
    9.20  
    9.21 @@ -115,7 +115,7 @@
    9.22          if obj.get_participation(partstat, include_needs_action):
    9.23              for p in obj.get_active_periods(recurrenceids, tzid, window_end):
    9.24                  fbp = obj.get_freebusy_period(p, partstat == "ORG")
    9.25 -                insert_period(fb, fbp)
    9.26 +                fb.insert_period(fbp)
    9.27  
    9.28      # Store and publish the free/busy collection.
    9.29