imip-agent

imiptools/period.py

1073:562e94fc8827
2016-03-06 Paul Boddie Adjusted the __nonzero__ and __len__ methods of the database collection. freebusy-collections
     1 #!/usr/bin/env python     2      3 """     4 Managing and presenting periods of time.     5      6 Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from bisect import bisect_left, bisect_right, insort_left    23 from datetime import date, datetime, timedelta    24 from imiptools.dates import check_permitted_values, correct_datetime, \    25                             format_datetime, get_datetime, \    26                             get_datetime_attributes, \    27                             get_recurrence_start, get_recurrence_start_point, \    28                             get_start_of_day, \    29                             get_tzid, \    30                             to_timezone, to_utc_datetime    31     32 def ifnone(x, y):    33     if x is None: return y    34     else: return x    35     36 class Comparable:    37     38     "A date/datetime wrapper that allows comparisons with other types."    39     40     def __init__(self, dt):    41         self.dt = dt    42     43     def __cmp__(self, other):    44         dt = None    45         odt = None    46     47         # Find any dates/datetimes.    48     49         if isinstance(self.dt, date):    50             dt = self.dt    51         if isinstance(other, date):    52             odt = other    53         elif isinstance(other, Comparable):    54             if isinstance(other.dt, date):    55                 odt = other.dt    56             else:    57                 other = other.dt    58     59         if dt and odt:    60             return cmp(dt, odt)    61         elif dt:    62             return other.__rcmp__(dt)    63         elif odt:    64             return self.dt.__cmp__(odt)    65         else:    66             return self.dt.__cmp__(other)    67     68 class PointInTime:    69     70     "A base class for special values."    71     72     pass    73     74 class StartOfTime(PointInTime):    75     76     "A special value that compares earlier than other values."    77     78     def __cmp__(self, other):    79         if isinstance(other, StartOfTime):    80             return 0    81         else:    82             return -1    83     84     def __rcmp__(self, other):    85         return -self.__cmp__(other)    86     87     def __nonzero__(self):    88         return False    89     90     def __hash__(self):    91         return 0    92     93 class EndOfTime(PointInTime):    94     95     "A special value that compares later than other values."    96     97     def __cmp__(self, other):    98         if isinstance(other, EndOfTime):    99             return 0   100         else:   101             return 1   102    103     def __rcmp__(self, other):   104         return -self.__cmp__(other)   105    106     def __nonzero__(self):   107         return False   108    109     def __hash__(self):   110         return 0   111    112 class Endless:   113    114     "A special value indicating an endless period."   115    116     def __cmp__(self, other):   117         if isinstance(other, Endless):   118             return 0   119         else:   120             return 1   121    122     def __rcmp__(self, other):   123         return -self.__cmp__(other)   124    125     def __nonzero__(self):   126         return True   127    128 class PeriodBase:   129    130     "A basic period abstraction."   131    132     def __init__(self, start, end):   133         if isinstance(start, (date, PointInTime)):   134             self.start = start   135         else:   136             self.start = get_datetime(start) or StartOfTime()   137         if isinstance(end, (date, PointInTime)):   138             self.end = end   139         else:   140             self.end = get_datetime(end) or EndOfTime()   141    142     def as_tuple(self):   143         return self.start, self.end   144    145     def __hash__(self):   146         return hash((self.get_start(), self.get_end()))   147    148     def __cmp__(self, other):   149    150         "Return a comparison result against 'other' using points in time."   151    152         if isinstance(other, PeriodBase):   153             return cmp(   154                 (Comparable(ifnone(self.get_start_point(), StartOfTime())), Comparable(ifnone(self.get_end_point(), EndOfTime()))),   155                 (Comparable(ifnone(other.get_start_point(), StartOfTime())), Comparable(ifnone(other.get_end_point(), EndOfTime())))   156                 )   157         else:   158             return 1   159    160     def overlaps(self, other):   161         return Comparable(ifnone(self.get_end_point(), EndOfTime())) > Comparable(ifnone(other.get_start_point(), StartOfTime())) and \   162                Comparable(ifnone(self.get_start_point(), StartOfTime())) < Comparable(ifnone(other.get_end_point(), EndOfTime()))   163    164     def within(self, other):   165         return Comparable(ifnone(self.get_start_point(), StartOfTime())) >= Comparable(ifnone(other.get_start_point(), StartOfTime())) and \   166                Comparable(ifnone(self.get_end_point(), EndOfTime())) <= Comparable(ifnone(other.get_end_point(), EndOfTime()))   167    168     def common(self, other):   169         start = max(Comparable(ifnone(self.get_start_point(), StartOfTime())), Comparable(ifnone(other.get_start_point(), StartOfTime())))   170         end = min(Comparable(ifnone(self.get_end_point(), EndOfTime())), Comparable(ifnone(other.get_end_point(), EndOfTime())))   171         if start <= end:   172             return self.make_corrected(start.dt, end.dt)   173         else:   174             return None   175    176     def get_key(self):   177         return self.get_start(), self.get_end()   178    179     # Datetime and metadata methods.   180    181     def get_start(self):   182         return self.start   183    184     def get_end(self):   185         return self.end   186    187     def get_start_attr(self):   188         return get_datetime_attributes(self.start, self.tzid)   189    190     def get_end_attr(self):   191         return get_datetime_attributes(self.end, self.tzid)   192    193     def get_start_item(self):   194         return self.get_start(), self.get_start_attr()   195    196     def get_end_item(self):   197         return self.get_end(), self.get_end_attr()   198    199     def get_start_point(self):   200         return self.start   201    202     def get_end_point(self):   203         return self.end   204    205     def get_duration(self):   206         start = self.get_start_point()   207         end = self.get_end_point()   208         if start and end:   209             return end - start   210         else:   211             return Endless()   212    213 class Period(PeriodBase):   214    215     "A simple period abstraction."   216    217     def __init__(self, start, end, tzid=None, origin=None):   218    219         """   220         Initialise a period with the given 'start' and 'end', having a   221         contextual 'tzid', if specified, and an indicated 'origin'.   222    223         All metadata from the start and end points are derived from the supplied   224         dates/datetimes.   225         """   226    227         PeriodBase.__init__(self, start, end)   228         self.tzid = tzid   229         self.origin = origin   230    231     def as_tuple(self):   232         return self.start, self.end, self.tzid, self.origin   233    234     def __repr__(self):   235         return "Period%r" % (self.as_tuple(),)   236    237     # Datetime and metadata methods.   238    239     def get_tzid(self):   240         return get_tzid(self.get_start_attr(), self.get_end_attr()) or self.tzid   241    242     def get_start_point(self):   243         start = self.get_start()   244         if isinstance(start, PointInTime): return start   245         else: return to_utc_datetime(start, self.get_tzid())   246    247     def get_end_point(self):   248         end = self.get_end()   249         if isinstance(end, PointInTime): return end   250         else: return to_utc_datetime(end, self.get_tzid())   251    252     # Period and event recurrence logic.   253    254     def is_replaced(self, recurrenceids):   255    256         """   257         Return whether this period refers to one of the 'recurrenceids'.   258         The 'recurrenceids' should be normalised to UTC datetimes according to   259         time zone information provided by their objects or be floating dates or   260         datetimes requiring conversion using contextual time zone information.   261         """   262    263         for recurrenceid in recurrenceids:   264             if self.is_affected(recurrenceid):   265                 return recurrenceid   266         return None   267    268     def is_affected(self, recurrenceid):   269    270         """   271         Return whether this period refers to 'recurrenceid'. The 'recurrenceid'   272         should be normalised to UTC datetimes according to time zone information   273         provided by their objects. Otherwise, this period's contextual time zone   274         information is used to convert any date or floating datetime   275         representation to a point in time.   276         """   277    278         if not recurrenceid:   279             return None   280         d = get_recurrence_start(recurrenceid)   281         dt = get_recurrence_start_point(recurrenceid, self.tzid)   282         if self.get_start() == d or self.get_start_point() == dt:   283             return recurrenceid   284         return None   285    286     # Value correction methods.   287    288     def with_duration(self, duration):   289    290         """   291         Return a version of this period with the same start point but with the   292         given 'duration'.   293         """   294    295         return self.make_corrected(self.get_start(), self.get_start() + duration)   296    297     def check_permitted(self, permitted_values):   298    299         "Check the period against the given 'permitted_values'."   300    301         start = self.get_start()   302         end = self.get_end()   303         start_errors = check_permitted_values(start, permitted_values)   304         end_errors = check_permitted_values(end, permitted_values)   305    306         if not (start_errors or end_errors):   307             return None   308    309         return start_errors, end_errors   310    311     def get_corrected(self, permitted_values):   312    313         "Return a corrected version of this period."   314    315         errors = self.check_permitted(permitted_values)   316    317         if not errors:   318             return self   319    320         start_errors, end_errors = errors   321    322         start = self.get_start()   323         end = self.get_end()   324    325         if start_errors:   326             start = correct_datetime(start, permitted_values)   327         if end_errors:   328             end = correct_datetime(end, permitted_values)   329    330         return self.make_corrected(start, end)   331    332     def make_corrected(self, start, end):   333         return self.__class__(start, end, self.tzid, self.origin)   334    335 class FreeBusyPeriod(PeriodBase):   336    337     "A free/busy record abstraction."   338    339     def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, summary=None, organiser=None, expires=None):   340    341         """   342         Initialise a free/busy period with the given 'start' and 'end' points,   343         plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'   344         details.   345    346         An additional 'expires' parameter can be used to indicate an expiry   347         datetime in conjunction with free/busy offers made when countering   348         event proposals.   349         """   350    351         PeriodBase.__init__(self, start, end)   352         self.uid = uid   353         self.transp = transp or None   354         self.recurrenceid = recurrenceid or None   355         self.summary = summary or None   356         self.organiser = organiser or None   357         self.expires = expires or None   358    359     def as_tuple(self, strings_only=False):   360    361         """   362         Return the initialisation parameter tuple, converting false value   363         parameters to strings if 'strings_only' is set to a true value.   364         """   365    366         null = lambda x: (strings_only and [""] or [x])[0]   367         return (   368             strings_only and format_datetime(self.get_start_point()) or self.start,   369             strings_only and format_datetime(self.get_end_point()) or self.end,   370             self.uid or null(self.uid),   371             self.transp or strings_only and "OPAQUE" or None,   372             self.recurrenceid or null(self.recurrenceid),   373             self.summary or null(self.summary),   374             self.organiser or null(self.organiser),   375             self.expires or null(self.expires)   376             )   377    378     def __cmp__(self, other):   379    380         """   381         Compare this object to 'other', employing the uid if the periods   382         involved are the same.   383         """   384    385         result = PeriodBase.__cmp__(self, other)   386         if result == 0 and isinstance(other, FreeBusyPeriod):   387             return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))   388         else:   389             return result   390    391     def get_key(self):   392         return self.uid, self.recurrenceid, self.get_start()   393    394     def __repr__(self):   395         return "FreeBusyPeriod%r" % (self.as_tuple(),)   396    397     def get_tzid(self):   398         return "UTC"   399    400     # Period and event recurrence logic.   401    402     def is_replaced(self, recurrences):   403    404         """   405         Return whether this period refers to one of the 'recurrences'.   406         The 'recurrences' must be UTC datetimes corresponding to the start of   407         the period described by a recurrence.   408         """   409    410         for recurrence in recurrences:   411             if self.is_affected(recurrence):   412                 return True   413         return False   414    415     def is_affected(self, recurrence):   416    417         """   418         Return whether this period refers to 'recurrence'. The 'recurrence' must   419         be a UTC datetime corresponding to the start of the period described by   420         a recurrence.   421         """   422    423         return recurrence and self.get_start_point() == recurrence   424    425     # Value correction methods.   426    427     def make_corrected(self, start, end):   428         return self.__class__(start, end)   429    430 class RecurringPeriod(Period):   431        432     """   433     A period with iCalendar metadata attributes and origin information from an   434     object.   435     """   436        437     def __init__(self, start, end, tzid=None, origin=None, start_attr=None, end_attr=None):   438         Period.__init__(self, start, end, tzid, origin)   439         self.start_attr = start_attr   440         self.end_attr = end_attr   441    442     def get_start_attr(self):   443         return self.start_attr   444    445     def get_end_attr(self):   446         return self.end_attr   447    448     def as_tuple(self):   449         return self.start, self.end, self.tzid, self.origin, self.start_attr, self.end_attr   450        451     def __repr__(self):   452         return "RecurringPeriod%r" % (self.as_tuple(),)   453    454     def make_corrected(self, start, end):   455         return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())   456    457 class FreeBusyCollectionBase:   458    459     "Common operations on free/busy period collections."   460    461     def __init__(self, mutable=True):   462         self.mutable = mutable   463    464     def _check_mutable(self):   465         if not self.mutable:   466             raise TypeError, "Cannot mutate this collection."   467    468     def copy(self):   469    470         "Make an independent mutable copy of the collection."   471    472         return FreeBusyCollection(list(self), True)   473    474     # List emulation methods.   475    476     def __iadd__(self, other):   477         for period in other:   478             self.insert_period(period)   479         return self   480    481     # Operations.   482    483     def can_schedule(self, periods, uid, recurrenceid):   484    485         """   486         Return whether the collection can accommodate the given 'periods'   487         employing the specified 'uid' and 'recurrenceid'.   488         """   489    490         for conflict in self.have_conflict(periods, True):   491             if conflict.uid != uid or conflict.recurrenceid != recurrenceid:   492                 return False   493    494         return True   495    496     def have_conflict(self, periods, get_conflicts=False):   497    498         """   499         Return whether any period in the collection overlaps with the given   500         'periods', returning a collection of such overlapping periods if   501         'get_conflicts' is set to a true value.   502         """   503    504         conflicts = set()   505         for p in periods:   506             overlapping = self.period_overlaps(p, get_conflicts)   507             if overlapping:   508                 if get_conflicts:   509                     conflicts.update(overlapping)   510                 else:   511                     return True   512    513         if get_conflicts:   514             return conflicts   515         else:   516             return False   517    518     def period_overlaps(self, period, get_periods=False):   519    520         """   521         Return whether any period in the collection overlaps with the given   522         'period', returning a collection of overlapping periods if 'get_periods'   523         is set to a true value.   524         """   525    526         overlapping = self.get_overlapping(period)   527    528         if get_periods:   529             return overlapping   530         else:   531             return len(overlapping) != 0   532    533     def replace_overlapping(self, period, replacements):   534    535         """   536         Replace existing periods in the collection within the given 'period',   537         using the given 'replacements'.   538         """   539    540         self._check_mutable()   541    542         self.remove_overlapping(period)   543         for replacement in replacements:   544             self.insert_period(replacement)   545    546     def coalesce_freebusy(self):   547    548         "Coalesce the periods in the collection, returning a new collection."   549    550         if not self:   551             return FreeBusyCollection()   552    553         fb = []   554    555         it = iter(self)   556         period = it.next()   557    558         start = period.get_start_point()   559         end = period.get_end_point()   560    561         try:   562             while True:   563                 period = it.next()   564                 if period.get_start_point() > end:   565                     fb.append(FreeBusyPeriod(start, end))   566                     start = period.get_start_point()   567                     end = period.get_end_point()   568                 else:   569                     end = max(end, period.get_end_point())   570         except StopIteration:   571             pass   572    573         fb.append(FreeBusyPeriod(start, end))   574         return FreeBusyCollection(fb)   575    576     def invert_freebusy(self):   577    578         "Return the free periods from the collection as a new collection."   579    580         if not self:   581             return FreeBusyCollection([FreeBusyPeriod(None, None)])   582    583         # Coalesce periods that overlap or are adjacent.   584    585         fb = self.coalesce_freebusy()   586         free = []   587    588         # Add a start-of-time period if appropriate.   589    590         first = fb[0].get_start_point()   591         if first:   592             free.append(FreeBusyPeriod(None, first))   593    594         start = fb[0].get_end_point()   595    596         for period in fb[1:]:   597             free.append(FreeBusyPeriod(start, period.get_start_point()))   598             start = period.get_end_point()   599    600         # Add an end-of-time period if appropriate.   601    602         if start:   603             free.append(FreeBusyPeriod(start, None))   604    605         return FreeBusyCollection(free)   606    607     def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):   608    609         """   610         Update the free/busy details with the given 'periods', 'transp' setting,   611         'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.   612    613         An optional 'expires' datetime string indicates the expiry time of any   614         free/busy offer.   615         """   616    617         self._check_mutable()   618    619         self.remove_event_periods(uid, recurrenceid)   620    621         for p in periods:   622             self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))   623    624 class FreeBusyCollection(FreeBusyCollectionBase):   625    626     "An abstraction for a collection of free/busy periods."   627    628     def __init__(self, periods=None, mutable=True):   629    630         """   631         Initialise the collection with the given list of 'periods', or start an   632         empty collection if no list is given.   633         """   634    635         FreeBusyCollectionBase.__init__(self, mutable)   636         self.periods = periods or []   637    638     # List emulation methods.   639    640     def __nonzero__(self):   641         return bool(self.periods)   642    643     def __iter__(self):   644         return iter(self.periods)   645    646     def __len__(self):   647         return len(self.periods)   648    649     def __getitem__(self, i):   650         return self.periods[i]   651    652     # Operations.   653    654     def insert_period(self, period):   655    656         "Insert the given 'period' into the collection."   657    658         self._check_mutable()   659    660         i = bisect_left(self.periods, period)   661         if i == len(self.periods):   662             self.periods.append(period)   663         elif self.periods[i] != period:   664             self.periods.insert(i, period)   665    666     def remove_periods(self, periods):   667    668         "Remove the given 'periods' from the collection."   669    670         self._check_mutable()   671    672         for period in periods:   673             i = bisect_left(self.periods, period)   674             if i < len(self.periods) and self.periods[i] == period:   675                 del self.periods[i]   676    677     def remove_event_periods(self, uid, recurrenceid=None):   678    679         """   680         Remove from the collection all periods associated with 'uid' and   681         'recurrenceid' (which if omitted causes the "parent" object's periods to   682         be referenced).   683    684         Return the removed periods.   685         """   686    687         self._check_mutable()   688    689         removed = []   690         i = 0   691         while i < len(self.periods):   692             fb = self.periods[i]   693             if fb.uid == uid and fb.recurrenceid == recurrenceid:   694                 removed.append(self.periods[i])   695                 del self.periods[i]   696             else:   697                 i += 1   698    699         return removed   700    701     def remove_additional_periods(self, uid, recurrenceids=None):   702    703         """   704         Remove from the collection all periods associated with 'uid' having a   705         recurrence identifier indicating an additional or modified period.   706    707         If 'recurrenceids' is specified, remove all periods associated with   708         'uid' that do not have a recurrence identifier in the given list.   709    710         Return the removed periods.   711         """   712    713         self._check_mutable()   714    715         removed = []   716         i = 0   717         while i < len(self.periods):   718             fb = self.periods[i]   719             if fb.uid == uid and fb.recurrenceid and (   720                 recurrenceids is None or   721                 recurrenceids is not None and fb.recurrenceid not in recurrenceids   722                 ):   723                 removed.append(self.periods[i])   724                 del self.periods[i]   725             else:   726                 i += 1   727    728         return removed   729    730     def remove_affected_period(self, uid, start):   731    732         """   733         Remove from the collection the period associated with 'uid' that   734         provides an occurrence starting at the given 'start' (provided by a   735         recurrence identifier, converted to a datetime). A recurrence identifier   736         is used to provide an alternative time period whilst also acting as a   737         reference to the originally-defined occurrence.   738    739         Return any removed period in a list.   740         """   741    742         self._check_mutable()   743    744         removed = []   745    746         search = Period(start, start)   747         found = bisect_left(self.periods, search)   748    749         while found < len(self.periods):   750             fb = self.periods[found]   751    752             # Stop looking if the start no longer matches the recurrence identifier.   753    754             if fb.get_start_point() != search.get_start_point():   755                 break   756    757             # If the period belongs to the parent object, remove it and return.   758    759             if not fb.recurrenceid and uid == fb.uid:   760                 removed.append(self.periods[found])   761                 del self.periods[found]   762                 break   763    764             # Otherwise, keep looking for a matching period.   765    766             found += 1   767    768         return removed   769    770     def periods_from(self, period):   771    772         "Return the entries in the collection at or after 'period'."   773    774         first = bisect_left(self.periods, period)   775         return self.periods[first:]   776    777     def periods_until(self, period):   778    779         "Return the entries in the collection before 'period'."   780    781         last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))   782         return self.periods[:last]   783    784     def get_overlapping(self, period):   785    786         """   787         Return the entries in the collection providing periods overlapping with   788         'period'.   789         """   790    791         # Find the range of periods potentially overlapping the period in the   792         # free/busy collection.   793    794         startpoints = self.periods_until(period)   795    796         # Find the range of periods potentially overlapping the period in a version   797         # of the free/busy collection sorted according to end datetimes.   798    799         endpoints = [(Period(fb.get_end_point(), fb.get_end_point()), fb) for fb in startpoints]   800         endpoints.sort()   801         first = bisect_left(endpoints, (Period(period.get_start_point(), period.get_start_point()),))   802         endpoints = endpoints[first:]   803    804         overlapping = set()   805    806         for p, fb in endpoints:   807             if fb.overlaps(period):   808                 overlapping.add(fb)   809    810         overlapping = list(overlapping)   811         overlapping.sort()   812         return overlapping   813    814     def remove_overlapping(self, period):   815    816         "Remove all periods overlapping with 'period' from the collection."   817    818         self._check_mutable()   819    820         overlapping = self.get_overlapping(period)   821    822         if overlapping:   823             for fb in overlapping:   824                 self.periods.remove(fb)   825    826 class FreeBusyDatabaseCollection(FreeBusyCollectionBase):   827    828     """   829     An abstraction for a collection of free/busy periods stored in a database   830     system.   831     """   832    833     def __init__(self, cursor, table_name, mutable=True):   834    835         """   836         Initialise the collection with the given 'cursor' and 'table_name'.   837         """   838    839         FreeBusyCollectionBase.__init__(self, mutable)   840         self.cursor = cursor   841         self.table_name = table_name   842    843     # Special database-related operations.   844    845     def placeholders(self, values):   846         return ", ".join(["?"] * len(values))   847    848     def initialise(self):   849    850         "Create the database table required to hold the collection."   851    852         query = """\   853 create table %(table)s (   854     start varchar not null,   855     end varchar not null,   856     uid varchar,   857     transp varchar,   858     recurrenceid varchar,   859     summary varchar,   860     organiser varchar,   861     expires varchar   862     )""" % {"table" : self.table_name}   863    864         self.cursor.execute(query)   865    866     # List emulation methods.   867    868     def __nonzero__(self):   869         return len(self) and True or False   870    871     def __iter__(self):   872         query = "select * from %(table)s" % {"table" : self.table_name}   873         self.cursor.execute(query)   874         return iter(map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall()))   875    876     def __len__(self):   877         query = "select count(*) from %(table)s" % {"table" : self.table_name}   878         self.cursor.execute(query)   879         result = self.cursor.fetchone()   880         return result and result[0] or 0   881    882     def __getitem__(self, i):   883         return list(iter(self))[i]   884    885     # Operations.   886    887     def insert_period(self, period):   888    889         "Insert the given 'period' into the collection."   890    891         self._check_mutable()   892    893         values = period.as_tuple(strings_only=True)   894         query = "insert into %(table)s values (%(columns)s)" % {   895             "table" : self.table_name,   896             "columns" : self.placeholders(values)   897             }   898         self.cursor.execute(query, values)   899    900     def remove_periods(self, periods):   901    902         "Remove the given 'periods' from the collection."   903    904         self._check_mutable()   905    906         for period in periods:   907             values = period.as_tuple(strings_only=True)   908             query = """\   909 delete from %(table)s   910 where start = ? and end = ? and uid = ? and transp = ? and recurrenceid = ?   911 and summary = ? and organiser = ? and expires = ?   912 """ % {"table" : self.table_name}   913             self.cursor.execute(query, values)   914    915     def remove_event_periods(self, uid, recurrenceid=None):   916    917         """   918         Remove from the collection all periods associated with 'uid' and   919         'recurrenceid' (which if omitted causes the "parent" object's periods to   920         be referenced).   921    922         Return the removed periods.   923         """   924    925         self._check_mutable()   926    927         if recurrenceid:   928             condition = "where uid = ? and recurrenceid = ?"   929             values = (uid, recurrenceid)   930         else:   931             condition = "where uid = ?"   932             values = (uid,)   933    934         query = "select * from %(table)s for update %(condition)s" % {   935             "table" : self.table_name,   936             "condition" : condition   937             }   938         self.cursor.execute(query, values)   939         removed = self.cursor.fetchall()   940    941         query = "delete from %(table)s %(condition)s" % {   942             "table" : self.table_name,   943             "condition" : condition   944             }   945         self.cursor.execute(query, values)   946    947         return map(lambda t: FreeBusyPeriod(*t), removed)   948    949     def remove_additional_periods(self, uid, recurrenceids=None):   950    951         """   952         Remove from the collection all periods associated with 'uid' having a   953         recurrence identifier indicating an additional or modified period.   954    955         If 'recurrenceids' is specified, remove all periods associated with   956         'uid' that do not have a recurrence identifier in the given list.   957    958         Return the removed periods.   959         """   960    961         self._check_mutable()   962    963         if recurrenceids is None:   964             condition = "where uid = ? and recurrenceid is not null"   965             values = (uid,)   966         else:   967             condition = "where uid = ? and recurrenceid is not null and recurrenceid not in ?"   968             values = (uid, recurrenceid)   969    970         query = "select * from %(table)s for update %(condition)s" % {   971             "table" : self.table_name,   972             "condition" : condition   973             }   974         self.cursor.execute(query, values)   975         removed = self.cursor.fetchall()   976    977         query = "delete from %(table)s %(condition)s" % {   978             "table" : self.table_name,   979             "condition" : condition   980             }   981         self.cursor.execute(query, values)   982    983         return map(lambda t: FreeBusyPeriod(*t), removed)   984    985     def remove_affected_period(self, uid, start):   986    987         """   988         Remove from the collection the period associated with 'uid' that   989         provides an occurrence starting at the given 'start' (provided by a   990         recurrence identifier, converted to a datetime). A recurrence identifier   991         is used to provide an alternative time period whilst also acting as a   992         reference to the originally-defined occurrence.   993    994         Return any removed period in a list.   995         """   996    997         self._check_mutable()   998    999         condition = "where uid = ? and start = ? and recurrenceid is null"  1000         values = (uid, start)  1001   1002         query = "select * from %(table)s %(condition)s" % {  1003             "table" : self.table_name,  1004             "condition" : condition  1005             }  1006         self.cursor.execute(query, values)  1007         removed = self.cursor.fetchall()  1008   1009         query = "delete from %(table)s %(condition)s" % {  1010             "table" : self.table_name,  1011             "condition" : condition  1012             }  1013         self.cursor.execute(query, values)  1014   1015         return map(lambda t: FreeBusyPeriod(*t), removed)  1016   1017     def periods_from(self, period):  1018   1019         "Return the entries in the collection at or after 'period'."  1020   1021         condition = "where start >= ?"  1022         values = (format_datetime(period.get_start_point()),)  1023   1024         query = "select * from %(table)s %(condition)s" % {  1025             "table" : self.table_name,  1026             "condition" : condition  1027             }  1028         self.cursor.execute(query, values)  1029   1030         return map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall())  1031   1032     def periods_until(self, period):  1033   1034         "Return the entries in the collection before 'period'."  1035   1036         condition = "where start < ?"  1037         values = (format_datetime(period.get_end_point()),)  1038   1039         query = "select * from %(table)s %(condition)s" % {  1040             "table" : self.table_name,  1041             "condition" : condition  1042             }  1043         self.cursor.execute(query, values)  1044   1045         return map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall())  1046   1047     def get_overlapping(self, period):  1048   1049         """  1050         Return the entries in the collection providing periods overlapping with  1051         'period'.  1052         """  1053   1054         condition = "where start < ? and end > ?"  1055         values = (format_datetime(period.get_end_point()), format_datetime(period.get_start_point()))  1056   1057         query = "select * from %(table)s %(condition)s" % {  1058             "table" : self.table_name,  1059             "condition" : condition  1060             }  1061         self.cursor.execute(query, values)  1062   1063         return map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall())  1064   1065     def remove_overlapping(self, period):  1066   1067         "Remove all periods overlapping with 'period' from the collection."  1068   1069         self._check_mutable()  1070   1071         condition = "where start < ? and end > ?"  1072         values = (format_datetime(period.get_end_point()), format_datetime(period.get_start_point()))  1073   1074         query = "delete from %(table)s %(condition)s" % {  1075             "table" : self.table_name,  1076             "condition" : condition  1077             }  1078         self.cursor.execute(query, values)  1079   1080 # Period layout.  1081   1082 def get_scale(periods, tzid, view_period=None):  1083   1084     """  1085     Return a time scale from the given list of 'periods'.  1086   1087     The given 'tzid' is used to make sure that the times are defined according  1088     to the chosen time zone.  1089   1090     An optional 'view_period' is used to constrain the scale to the given  1091     period.  1092   1093     The returned scale is a mapping from time to (starting, ending) tuples,  1094     where starting and ending are collections of periods.  1095     """  1096   1097     scale = {}  1098     view_start = view_period and to_timezone(view_period.get_start_point(), tzid) or None  1099     view_end = view_period and to_timezone(view_period.get_end_point(), tzid) or None  1100   1101     for p in periods:  1102   1103         # Add a point and this event to the starting list.  1104   1105         start = to_timezone(p.get_start(), tzid)  1106         start = view_start and max(start, view_start) or start  1107         if not scale.has_key(start):  1108             scale[start] = [], []  1109         scale[start][0].append(p)  1110   1111         # Add a point and this event to the ending list.  1112   1113         end = to_timezone(p.get_end(), tzid)  1114         end = view_end and min(end, view_end) or end  1115         if not scale.has_key(end):  1116             scale[end] = [], []  1117         scale[end][1].append(p)  1118   1119     return scale  1120   1121 class Point:  1122   1123     "A qualified point in time."  1124   1125     PRINCIPAL, REPEATED = 0, 1  1126   1127     def __init__(self, point, indicator=None):  1128         self.point = point  1129         self.indicator = indicator or self.PRINCIPAL  1130   1131     def __hash__(self):  1132         return hash((self.point, self.indicator))  1133   1134     def __cmp__(self, other):  1135         if isinstance(other, Point):  1136             return cmp((self.point, self.indicator), (other.point, other.indicator))  1137         elif isinstance(other, datetime):  1138             return cmp(self.point, other)  1139         else:  1140             return 1  1141   1142     def __eq__(self, other):  1143         return self.__cmp__(other) == 0  1144   1145     def __ne__(self, other):  1146         return not self == other  1147   1148     def __lt__(self, other):  1149         return self.__cmp__(other) < 0  1150   1151     def __le__(self, other):  1152         return self.__cmp__(other) <= 0  1153   1154     def __gt__(self, other):  1155         return not self <= other  1156   1157     def __ge__(self, other):  1158         return not self < other  1159   1160     def __repr__(self):  1161         return "Point(%r, Point.%s)" % (self.point, self.indicator and "REPEATED" or "PRINCIPAL")  1162   1163 def get_slots(scale):  1164   1165     """  1166     Return an ordered list of time slots from the given 'scale'.  1167   1168     Each slot is a tuple containing details of a point in time for the start of  1169     the slot, together with a list of parallel event periods.  1170   1171     Each point in time is described as a Point representing the actual point in  1172     time together with an indicator of the nature of the point in time (as a  1173     principal point in a time scale or as a repeated point used to terminate  1174     events occurring for an instant in time).  1175     """  1176   1177     slots = []  1178     active = []  1179   1180     points = scale.items()  1181     points.sort()  1182   1183     for point, (starting, ending) in points:  1184         ending = set(ending)  1185         instants = ending.intersection(starting)  1186   1187         # Discard all active events ending at or before this start time.  1188         # Free up the position in the active list.  1189   1190         for t in ending.difference(instants):  1191             i = active.index(t)  1192             active[i] = None  1193   1194         # For each event starting at the current point, fill any newly-vacated  1195         # position or add to the end of the active list.  1196   1197         for t in starting:  1198             try:  1199                 i = active.index(None)  1200                 active[i] = t  1201             except ValueError:  1202                 active.append(t)  1203   1204         # Discard vacant positions from the end of the active list.  1205   1206         while active and active[-1] is None:  1207             active.pop()  1208   1209         # Add an entry for the time point before "instants".  1210   1211         slots.append((Point(point), active[:]))  1212   1213         # Discard events ending at the same time as they began.  1214   1215         if instants:  1216             for t in instants:  1217                 i = active.index(t)  1218                 active[i] = None  1219   1220             # Discard vacant positions from the end of the active list.  1221   1222             while active and active[-1] is None:  1223                 active.pop()  1224   1225             # Add another entry for the time point after "instants".  1226   1227             slots.append((Point(point, Point.REPEATED), active[:]))  1228   1229     return slots  1230   1231 def add_day_start_points(slots, tzid):  1232   1233     """  1234     Introduce into the 'slots' any day start points required by multi-day  1235     periods. The 'tzid' is required to make sure that appropriate time zones  1236     are chosen and not necessarily those provided by the existing time points.  1237     """  1238   1239     new_slots = []  1240     current_date = None  1241     previously_active = []  1242   1243     for point, active in slots:  1244         start_of_day = get_start_of_day(point.point, tzid)  1245         this_date = point.point.date()  1246   1247         # For each new day, add a slot for the start of the day where periods  1248         # are active and where no such slot already exists.  1249   1250         if this_date != current_date:  1251   1252             # Fill in days where events remain active.  1253   1254             if current_date:  1255                 current_date += timedelta(1)  1256                 while current_date < this_date:  1257                     new_slots.append((Point(get_start_of_day(current_date, tzid)), previously_active))  1258                     current_date += timedelta(1)  1259             else:  1260                 current_date = this_date  1261   1262             # Add any continuing periods.  1263   1264             if point.point != start_of_day:  1265                 new_slots.append((Point(start_of_day), previously_active))  1266   1267         # Add the currently active periods at this point in time.  1268   1269         previously_active = active  1270   1271     for t in new_slots:  1272         insort_left(slots, t)  1273   1274 def remove_end_slot(slots, view_period):  1275   1276     """  1277     Remove from 'slots' any slot situated at the end of the given 'view_period'.  1278     """  1279   1280     end = view_period.get_end_point()  1281     if not end or not slots:  1282         return  1283     i = bisect_left(slots, (Point(end), None))  1284     if i < len(slots):  1285         del slots[i:]  1286   1287 def add_slots(slots, points):  1288   1289     """  1290     Introduce into the 'slots' entries for those in 'points' that are not  1291     already present, propagating active periods from time points preceding  1292     those added.  1293     """  1294   1295     new_slots = []  1296   1297     for point in points:  1298         i = bisect_left(slots, (point,)) # slots is [(point, active)...]  1299         if i < len(slots) and slots[i][0] == point:  1300             continue  1301   1302         new_slots.append((point, i > 0 and slots[i-1][1] or []))  1303   1304     for t in new_slots:  1305         insort_left(slots, t)  1306   1307 def partition_by_day(slots):  1308   1309     """  1310     Return a mapping from dates to time points provided by 'slots'.  1311     """  1312   1313     d = {}  1314   1315     for point, value in slots:  1316         day = point.point.date()  1317         if not d.has_key(day):  1318             d[day] = []  1319         d[day].append((point, value))  1320   1321     return d  1322   1323 def add_empty_days(days, tzid, start=None, end=None):  1324   1325     """  1326     Add empty days to 'days' between busy days, and optionally from the given  1327     'start' day and until the given 'end' day.  1328     """  1329   1330     last_day = start - timedelta(1)  1331     all_days = days.keys()  1332     all_days.sort()  1333   1334     for day in all_days:  1335         if last_day:  1336             empty_day = last_day + timedelta(1)  1337             while empty_day < day:  1338                 days[empty_day] = [(Point(get_start_of_day(empty_day, tzid)), None)]  1339                 empty_day += timedelta(1)  1340         last_day = day  1341   1342     if end:  1343         empty_day = last_day + timedelta(1)  1344         while empty_day < end:  1345             days[empty_day] = [(Point(get_start_of_day(empty_day, tzid)), None)]  1346             empty_day += timedelta(1)  1347   1348 def get_spans(slots):  1349   1350     "Inspect the given 'slots', returning a mapping of period keys to spans."  1351   1352     points = [point for point, active in slots]  1353     spans = {}  1354   1355     for _point, active in slots:  1356         for p in active:  1357             if p:  1358                 key = p.get_key()  1359                 start_slot = bisect_left(points, p.get_start())  1360                 end_slot = bisect_left(points, p.get_end())  1361                 spans[key] = end_slot - start_slot  1362   1363     return spans  1364   1365 # vim: tabstop=4 expandtab shiftwidth=4