# HG changeset patch # User Paul Boddie # Date 1495550049 -7200 # Node ID 4fc327b8d31cbd87ac1306d036e3457242db55bf # Parent 68e9e6ca22840b7306567551301823e57f40e1c6 Moved free/busy period classes into a separate module. Introduced a convenience method for returning database results where only a single value is taken from each result tuple. diff -r 68e9e6ca2284 -r 4fc327b8d31c imiptools/client.py --- a/imiptools/client.py Tue May 23 16:31:27 2017 +0200 +++ b/imiptools/client.py Tue May 23 16:34:09 2017 +0200 @@ -28,7 +28,7 @@ from imiptools.dates import check_permitted_values, format_datetime, get_default_timezone, \ get_duration, get_timestamp from imiptools.i18n import get_translator -from imiptools.period import SupportAttendee, SupportExpires +from imiptools.freebusy import SupportAttendee, SupportExpires from imiptools.profile import Preferences from imiptools.stores import get_store, get_publisher, get_journal diff -r 68e9e6ca2284 -r 4fc327b8d31c imiptools/data.py --- a/imiptools/data.py Tue May 23 16:31:27 2017 +0200 +++ b/imiptools/data.py Tue May 23 16:34:09 2017 +0200 @@ -3,7 +3,7 @@ """ Interpretation of vCalendar content. -Copyright (C) 2014, 2015, 2016 Paul Boddie +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -29,7 +29,8 @@ get_recurrence_start_point, \ get_time, get_timestamp, get_tzid, to_datetime, \ to_timezone, to_utc_datetime -from imiptools.period import FreeBusyPeriod, Period, RecurringPeriod +from imiptools.freebusy import FreeBusyPeriod +from imiptools.period import Period, RecurringPeriod from vCalendar import iterwrite, parse, ParseError, to_dict, to_node from vRecurrence import get_parameters, get_rule import email.utils diff -r 68e9e6ca2284 -r 4fc327b8d31c imiptools/freebusy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imiptools/freebusy.py Tue May 23 16:34:09 2017 +0200 @@ -0,0 +1,1079 @@ +#!/usr/bin/env python + +""" +Managing free/busy periods. + +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +from bisect import bisect_left, bisect_right +from imiptools.dates import format_datetime +from imiptools.sql import DatabaseOperations + +def from_string(s, encoding): + if s: + return unicode(s, encoding) + else: + return s + +def to_string(s, encoding): + if s: + return s.encode(encoding) + else: + return s + +from imiptools.period import get_overlapping, Period, PeriodBase + +class FreeBusyPeriod(PeriodBase): + + "A free/busy record abstraction." + + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, + summary=None, organiser=None): + + """ + Initialise a free/busy period with the given 'start' and 'end' points, + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' + details. + """ + + PeriodBase.__init__(self, start, end) + self.uid = uid + self.transp = transp or None + self.recurrenceid = recurrenceid or None + self.summary = summary or None + self.organiser = organiser or None + + def as_tuple(self, strings_only=False, string_datetimes=False): + + """ + Return the initialisation parameter tuple, converting datetimes and + false value parameters to strings if 'strings_only' is set to a true + value. Otherwise, if 'string_datetimes' is set to a true value, only the + datetime values are converted to strings. + """ + + null = lambda x: (strings_only and [""] or [x])[0] + return ( + (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start, + (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end, + self.uid or null(self.uid), + self.transp or strings_only and "OPAQUE" or None, + self.recurrenceid or null(self.recurrenceid), + self.summary or null(self.summary), + self.organiser or null(self.organiser) + ) + + def __cmp__(self, other): + + """ + Compare this object to 'other', employing the uid if the periods + involved are the same. + """ + + result = PeriodBase.__cmp__(self, other) + if result == 0 and isinstance(other, FreeBusyPeriod): + return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid)) + else: + return result + + def get_key(self): + return self.uid, self.recurrenceid, self.get_start() + + def __repr__(self): + return "FreeBusyPeriod%r" % (self.as_tuple(),) + + def get_tzid(self): + return "UTC" + + # Period and event recurrence logic. + + def is_replaced(self, recurrences): + + """ + Return whether this period refers to one of the 'recurrences'. + The 'recurrences' must be UTC datetimes corresponding to the start of + the period described by a recurrence. + """ + + for recurrence in recurrences: + if self.is_affected(recurrence): + return True + return False + + def is_affected(self, recurrence): + + """ + Return whether this period refers to 'recurrence'. The 'recurrence' must + be a UTC datetime corresponding to the start of the period described by + a recurrence. + """ + + return recurrence and self.get_start_point() == recurrence + + # Value correction methods. + + def make_corrected(self, start, end): + return self.__class__(start, end) + +class FreeBusyOfferPeriod(FreeBusyPeriod): + + "A free/busy record abstraction for an offer period." + + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, + summary=None, organiser=None, expires=None): + + """ + Initialise a free/busy period with the given 'start' and 'end' points, + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' + details. + + An additional 'expires' parameter can be used to indicate an expiry + datetime in conjunction with free/busy offers made when countering + event proposals. + """ + + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, + summary, organiser) + self.expires = expires or None + + def as_tuple(self, strings_only=False, string_datetimes=False): + + """ + Return the initialisation parameter tuple, converting datetimes and + false value parameters to strings if 'strings_only' is set to a true + value. Otherwise, if 'string_datetimes' is set to a true value, only the + datetime values are converted to strings. + """ + + null = lambda x: (strings_only and [""] or [x])[0] + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( + self.expires or null(self.expires),) + + def __repr__(self): + return "FreeBusyOfferPeriod%r" % (self.as_tuple(),) + +class FreeBusyGroupPeriod(FreeBusyPeriod): + + "A free/busy record abstraction for a quota group period." + + def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, + summary=None, organiser=None, attendee=None): + + """ + Initialise a free/busy period with the given 'start' and 'end' points, + plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' + details. + + An additional 'attendee' parameter can be used to indicate the identity + of the attendee recording the period. + """ + + FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, + summary, organiser) + self.attendee = attendee or None + + def as_tuple(self, strings_only=False, string_datetimes=False): + + """ + Return the initialisation parameter tuple, converting datetimes and + false value parameters to strings if 'strings_only' is set to a true + value. Otherwise, if 'string_datetimes' is set to a true value, only the + datetime values are converted to strings. + """ + + null = lambda x: (strings_only and [""] or [x])[0] + return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( + self.attendee or null(self.attendee),) + + def __cmp__(self, other): + + """ + Compare this object to 'other', employing the uid if the periods + involved are the same. + """ + + result = FreeBusyPeriod.__cmp__(self, other) + if isinstance(other, FreeBusyGroupPeriod) and result == 0: + return cmp(self.attendee, other.attendee) + else: + return result + + def __repr__(self): + return "FreeBusyGroupPeriod%r" % (self.as_tuple(),) + +class FreeBusyCollectionBase: + + "Common operations on free/busy period collections." + + period_columns = [ + "start", "end", "object_uid", "transp", "object_recurrenceid", + "summary", "organiser" + ] + + period_class = FreeBusyPeriod + + def __init__(self, mutable=True): + self.mutable = mutable + + def _check_mutable(self): + if not self.mutable: + raise TypeError, "Cannot mutate this collection." + + def copy(self): + + "Make an independent mutable copy of the collection." + + return FreeBusyCollection(list(self), True) + + def make_period(self, t): + + """ + Make a period using the given tuple of arguments and the collection's + column details. + """ + + args = [] + for arg, column in zip(t, self.period_columns): + args.append(from_string(arg, "utf-8")) + return self.period_class(*args) + + def make_tuple(self, t): + + """ + Return a tuple from the given tuple 't' conforming to the collection's + column details. + """ + + args = [] + for arg, column in zip(t, self.period_columns): + args.append(arg) + return tuple(args) + + # List emulation methods. + + def __iadd__(self, periods): + for period in periods: + self.insert_period(period) + return self + + def append(self, period): + self.insert_period(period) + + # Operations. + + def can_schedule(self, periods, uid, recurrenceid): + + """ + Return whether the collection can accommodate the given 'periods' + employing the specified 'uid' and 'recurrenceid'. + """ + + for conflict in self.have_conflict(periods, True): + if conflict.uid != uid or conflict.recurrenceid != recurrenceid: + return False + + return True + + def have_conflict(self, periods, get_conflicts=False): + + """ + Return whether any period in the collection overlaps with the given + 'periods', returning a collection of such overlapping periods if + 'get_conflicts' is set to a true value. + """ + + conflicts = set() + for p in periods: + overlapping = self.period_overlaps(p, get_conflicts) + if overlapping: + if get_conflicts: + conflicts.update(overlapping) + else: + return True + + if get_conflicts: + return conflicts + else: + return False + + def period_overlaps(self, period, get_periods=False): + + """ + Return whether any period in the collection overlaps with the given + 'period', returning a collection of overlapping periods if 'get_periods' + is set to a true value. + """ + + overlapping = self.get_overlapping([period]) + + if get_periods: + return overlapping + else: + return len(overlapping) != 0 + + def replace_overlapping(self, period, replacements): + + """ + Replace existing periods in the collection within the given 'period', + using the given 'replacements'. + """ + + self._check_mutable() + + self.remove_overlapping(period) + for replacement in replacements: + self.insert_period(replacement) + + def coalesce_freebusy(self): + + "Coalesce the periods in the collection, returning a new collection." + + if not self: + return FreeBusyCollection() + + fb = [] + + it = iter(self) + period = it.next() + + start = period.get_start_point() + end = period.get_end_point() + + try: + while True: + period = it.next() + if period.get_start_point() > end: + fb.append(self.period_class(start, end)) + start = period.get_start_point() + end = period.get_end_point() + else: + end = max(end, period.get_end_point()) + except StopIteration: + pass + + fb.append(self.period_class(start, end)) + return FreeBusyCollection(fb) + + def invert_freebusy(self): + + "Return the free periods from the collection as a new collection." + + if not self: + return FreeBusyCollection([self.period_class(None, None)]) + + # Coalesce periods that overlap or are adjacent. + + fb = self.coalesce_freebusy() + free = [] + + # Add a start-of-time period if appropriate. + + first = fb[0].get_start_point() + if first: + free.append(self.period_class(None, first)) + + start = fb[0].get_end_point() + + for period in fb[1:]: + free.append(self.period_class(start, period.get_start_point())) + start = period.get_end_point() + + # Add an end-of-time period if appropriate. + + if start: + free.append(self.period_class(start, None)) + + return FreeBusyCollection(free) + + def _update_freebusy(self, periods, uid, recurrenceid): + + """ + Update the free/busy details with the given 'periods', using the given + 'uid' plus 'recurrenceid' to remove existing periods. + """ + + self._check_mutable() + + self.remove_specific_event_periods(uid, recurrenceid) + + for p in periods: + self.insert_period(p) + + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser): + + """ + Update the free/busy details with the given 'periods', 'transp' setting, + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. + """ + + new_periods = [] + + for p in periods: + new_periods.append( + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser) + ) + + self._update_freebusy(new_periods, uid, recurrenceid) + +class SupportAttendee: + + "A mix-in that supports the affected attendee in free/busy periods." + + period_columns = FreeBusyCollectionBase.period_columns + ["attendee"] + period_class = FreeBusyGroupPeriod + + def _update_freebusy(self, periods, uid, recurrenceid, attendee=None): + + """ + Update the free/busy details with the given 'periods', using the given + 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods. + """ + + self._check_mutable() + + self.remove_specific_event_periods(uid, recurrenceid, attendee) + + for p in periods: + self.insert_period(p) + + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None): + + """ + Update the free/busy details with the given 'periods', 'transp' setting, + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. + + An optional 'attendee' indicates the attendee affected by the period. + """ + + new_periods = [] + + for p in periods: + new_periods.append( + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee) + ) + + self._update_freebusy(new_periods, uid, recurrenceid, attendee) + +class SupportExpires: + + "A mix-in that supports the expiry datetime in free/busy periods." + + period_columns = FreeBusyCollectionBase.period_columns + ["expires"] + period_class = FreeBusyOfferPeriod + + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None): + + """ + Update the free/busy details with the given 'periods', 'transp' setting, + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. + + An optional 'expires' datetime string indicates the expiry time of any + free/busy offer. + """ + + new_periods = [] + + for p in periods: + new_periods.append( + self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires) + ) + + self._update_freebusy(new_periods, uid, recurrenceid) + +class FreeBusyCollection(FreeBusyCollectionBase): + + "An abstraction for a collection of free/busy periods." + + def __init__(self, periods=None, mutable=True): + + """ + Initialise the collection with the given list of 'periods', or start an + empty collection if no list is given. If 'mutable' is indicated, the + collection may be changed; otherwise, an exception will be raised. + """ + + FreeBusyCollectionBase.__init__(self, mutable) + self.periods = periods or [] + + # List emulation methods. + + def __nonzero__(self): + return bool(self.periods) + + def __iter__(self): + return iter(self.periods) + + def __len__(self): + return len(self.periods) + + def __getitem__(self, i): + return self.periods[i] + + # Operations. + + def insert_period(self, period): + + "Insert the given 'period' into the collection." + + self._check_mutable() + + i = bisect_left(self.periods, period) + if i == len(self.periods): + self.periods.append(period) + elif self.periods[i] != period: + self.periods.insert(i, period) + + def remove_periods(self, periods): + + "Remove the given 'periods' from the collection." + + self._check_mutable() + + for period in periods: + i = bisect_left(self.periods, period) + if i < len(self.periods) and self.periods[i] == period: + del self.periods[i] + + def remove_event_periods(self, uid, recurrenceid=None): + + """ + Remove from the collection all periods associated with 'uid' and + 'recurrenceid' (which if omitted causes the "parent" object's periods to + be referenced). + + Return the removed periods. + """ + + self._check_mutable() + + removed = [] + i = 0 + while i < len(self.periods): + fb = self.periods[i] + if fb.uid == uid and fb.recurrenceid == recurrenceid: + removed.append(self.periods[i]) + del self.periods[i] + else: + i += 1 + + return removed + + # Specific period removal when updating event details. + + remove_specific_event_periods = remove_event_periods + + def remove_additional_periods(self, uid, recurrenceids=None): + + """ + Remove from the collection all periods associated with 'uid' having a + recurrence identifier indicating an additional or modified period. + + If 'recurrenceids' is specified, remove all periods associated with + 'uid' that do not have a recurrence identifier in the given list. + + Return the removed periods. + """ + + self._check_mutable() + + removed = [] + i = 0 + while i < len(self.periods): + fb = self.periods[i] + if fb.uid == uid and fb.recurrenceid and ( + recurrenceids is None or + recurrenceids is not None and fb.recurrenceid not in recurrenceids + ): + removed.append(self.periods[i]) + del self.periods[i] + else: + i += 1 + + return removed + + def remove_affected_period(self, uid, start): + + """ + Remove from the collection the period associated with 'uid' that + provides an occurrence starting at the given 'start' (provided by a + recurrence identifier, converted to a datetime). A recurrence identifier + is used to provide an alternative time period whilst also acting as a + reference to the originally-defined occurrence. + + Return any removed period in a list. + """ + + self._check_mutable() + + removed = [] + + search = Period(start, start) + found = bisect_left(self.periods, search) + + while found < len(self.periods): + fb = self.periods[found] + + # Stop looking if the start no longer matches the recurrence identifier. + + if fb.get_start_point() != search.get_start_point(): + break + + # If the period belongs to the parent object, remove it and return. + + if not fb.recurrenceid and uid == fb.uid: + removed.append(self.periods[found]) + del self.periods[found] + break + + # Otherwise, keep looking for a matching period. + + found += 1 + + return removed + + def periods_from(self, period): + + "Return the entries in the collection at or after 'period'." + + first = bisect_left(self.periods, period) + return self.periods[first:] + + def periods_until(self, period): + + "Return the entries in the collection before 'period'." + + last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid())) + return self.periods[:last] + + def get_overlapping(self, periods): + + """ + Return the entries in the collection providing periods overlapping with + the given sorted collection of 'periods'. + """ + + return get_overlapping(self.periods, periods) + + def remove_overlapping(self, period): + + "Remove all periods overlapping with 'period' from the collection." + + self._check_mutable() + + overlapping = self.get_overlapping([period]) + + if overlapping: + for fb in overlapping: + self.periods.remove(fb) + +class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection): + + "A collection of quota group free/busy objects." + + def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): + + """ + Remove from the collection all periods associated with 'uid' and + 'recurrenceid' (which if omitted causes the "parent" object's periods to + be referenced) and any 'attendee'. + + Return the removed periods. + """ + + self._check_mutable() + + removed = [] + i = 0 + while i < len(self.periods): + fb = self.periods[i] + if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee: + removed.append(self.periods[i]) + del self.periods[i] + else: + i += 1 + + return removed + +class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection): + + "A collection of offered free/busy objects." + + pass + +class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations): + + """ + An abstraction for a collection of free/busy periods stored in a database + system. + """ + + def __init__(self, cursor, table_name, column_names=None, filter_values=None, + mutable=True, paramstyle=None): + + """ + Initialise the collection with the given 'cursor' and with the + 'table_name', 'column_names' and 'filter_values' configuring the + selection of data. If 'mutable' is indicated, the collection may be + changed; otherwise, an exception will be raised. + """ + + FreeBusyCollectionBase.__init__(self, mutable) + DatabaseOperations.__init__(self, column_names, filter_values, paramstyle) + self.cursor = cursor + self.table_name = table_name + + # List emulation methods. + + def __nonzero__(self): + return len(self) and True or False + + def __iter__(self): + query, values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }) + self.cursor.execute(query, values) + return iter(map(lambda t: self.make_period(t), self.cursor.fetchall())) + + def __len__(self): + query, values = self.get_query( + "select count(*) from %(table)s :condition" % { + "table" : self.table_name + }) + self.cursor.execute(query, values) + result = self.cursor.fetchone() + return result and int(result[0]) or 0 + + def __getitem__(self, i): + return list(iter(self))[i] + + # Operations. + + def insert_period(self, period): + + "Insert the given 'period' into the collection." + + self._check_mutable() + + columns, values = self.period_columns, period.as_tuple(string_datetimes=True) + + query, values = self.get_query( + "insert into %(table)s (:columns) values (:values)" % { + "table" : self.table_name + }, + columns, [to_string(v, "utf-8") for v in values]) + + self.cursor.execute(query, values) + + def remove_periods(self, periods): + + "Remove the given 'periods' from the collection." + + self._check_mutable() + + for period in periods: + values = period.as_tuple(string_datetimes=True) + + query, values = self.get_query( + "delete from %(table)s :condition" % { + "table" : self.table_name + }, + self.period_columns, [to_string(v, "utf-8") for v in values]) + + self.cursor.execute(query, values) + + def remove_event_periods(self, uid, recurrenceid=None): + + """ + Remove from the collection all periods associated with 'uid' and + 'recurrenceid' (which if omitted causes the "parent" object's periods to + be referenced). + + Return the removed periods. + """ + + self._check_mutable() + + if recurrenceid: + columns, values = ["object_uid", "object_recurrenceid"], [uid, recurrenceid] + else: + columns, values = ["object_uid", "object_recurrenceid is null"], [uid] + + query, _values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, _values) + removed = self.cursor.fetchall() + + query, values = self.get_query( + "delete from %(table)s :condition" % { + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + return map(lambda t: self.make_period(t), removed) + + # Specific period removal when updating event details. + + remove_specific_event_periods = remove_event_periods + + def remove_additional_periods(self, uid, recurrenceids=None): + + """ + Remove from the collection all periods associated with 'uid' having a + recurrence identifier indicating an additional or modified period. + + If 'recurrenceids' is specified, remove all periods associated with + 'uid' that do not have a recurrence identifier in the given list. + + Return the removed periods. + """ + + self._check_mutable() + + if not recurrenceids: + columns, values = ["object_uid", "object_recurrenceid is not null"], [uid] + else: + columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)] + + query, _values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, _values) + removed = self.cursor.fetchall() + + query, values = self.get_query( + "delete from %(table)s :condition" % { + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + return map(lambda t: self.make_period(t), removed) + + def remove_affected_period(self, uid, start): + + """ + Remove from the collection the period associated with 'uid' that + provides an occurrence starting at the given 'start' (provided by a + recurrence identifier, converted to a datetime). A recurrence identifier + is used to provide an alternative time period whilst also acting as a + reference to the originally-defined occurrence. + + Return any removed period in a list. + """ + + self._check_mutable() + + start = format_datetime(start) + + columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start] + + query, _values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, _values) + removed = self.cursor.fetchall() + + query, values = self.get_query( + "delete from %(table)s :condition" % { + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + return map(lambda t: self.make_period(t), removed) + + def periods_from(self, period): + + "Return the entries in the collection at or after 'period'." + + start = format_datetime(period.get_start_point()) + + columns, values = [], [] + + if start: + columns.append("start >= ?") + values.append(start) + + query, values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + return map(lambda t: self.make_period(t), self.cursor.fetchall()) + + def periods_until(self, period): + + "Return the entries in the collection before 'period'." + + end = format_datetime(period.get_end_point()) + + columns, values = [], [] + + if end: + columns.append("start < ?") + values.append(end) + + query, values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + return map(lambda t: self.make_period(t), self.cursor.fetchall()) + + def get_overlapping(self, periods): + + """ + Return the entries in the collection providing periods overlapping with + the given sorted collection of 'periods'. + """ + + overlapping = set() + + for period in periods: + columns, values = self._get_period_values(period) + + query, values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + overlapping.update(map(lambda t: self.make_period(t), self.cursor.fetchall())) + + overlapping = list(overlapping) + overlapping.sort() + return overlapping + + def remove_overlapping(self, period): + + "Remove all periods overlapping with 'period' from the collection." + + self._check_mutable() + + columns, values = self._get_period_values(period) + + query, values = self.get_query( + "delete from %(table)s :condition" % { + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + def _get_period_values(self, period): + + start = format_datetime(period.get_start_point()) + end = format_datetime(period.get_end_point()) + + columns, values = [], [] + + if end: + columns.append("start < ?") + values.append(end) + if start: + columns.append("end > ?") + values.append(start) + + return columns, values + +class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection): + + "A collection of quota group free/busy objects." + + def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): + + """ + Remove from the collection all periods associated with 'uid' and + 'recurrenceid' (which if omitted causes the "parent" object's periods to + be referenced) and any 'attendee'. + + Return the removed periods. + """ + + self._check_mutable() + + columns, values = ["object_uid"], [uid] + + if recurrenceid: + columns.append("object_recurrenceid") + values.append(recurrenceid) + else: + columns.append("object_recurrenceid is null") + + if attendee: + columns.append("attendee") + values.append(attendee) + else: + columns.append("attendee is null") + + query, _values = self.get_query( + "select %(columns)s from %(table)s :condition" % { + "columns" : self.columnlist(self.period_columns), + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, _values) + removed = self.cursor.fetchall() + + query, values = self.get_query( + "delete from %(table)s :condition" % { + "table" : self.table_name + }, + columns, values) + + self.cursor.execute(query, values) + + return map(lambda t: self.make_period(t), removed) + +class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection): + + "A collection of offered free/busy objects." + + pass + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 68e9e6ca2284 -r 4fc327b8d31c imiptools/handlers/common.py --- a/imiptools/handlers/common.py Tue May 23 16:31:27 2017 +0200 +++ b/imiptools/handlers/common.py Tue May 23 16:34:09 2017 +0200 @@ -3,7 +3,7 @@ """ Common handler functionality for different entities. -Copyright (C) 2014, 2015, 2016 Paul Boddie +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -22,7 +22,8 @@ from imiptools.data import get_address, get_uri, make_freebusy, to_part, \ uri_dict from imiptools.dates import format_datetime -from imiptools.period import FreeBusyPeriod, Period +from imiptools.freebusy import FreeBusyPeriod +from imiptools.period import Period class CommonFreebusy: diff -r 68e9e6ca2284 -r 4fc327b8d31c imiptools/period.py --- a/imiptools/period.py Tue May 23 16:31:27 2017 +0200 +++ b/imiptools/period.py Tue May 23 16:34:09 2017 +0200 @@ -3,7 +3,7 @@ """ Managing and presenting periods of time. -Copyright (C) 2014, 2015, 2016 Paul Boddie +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,33 +19,20 @@ this program. If not, see . """ -from bisect import bisect_left, bisect_right, insort_left +from bisect import bisect_left, insort_left from datetime import date, datetime, timedelta from imiptools.dates import check_permitted_values, correct_datetime, \ - format_datetime, get_datetime, \ + get_datetime, \ get_datetime_attributes, \ get_recurrence_start, get_recurrence_start_point, \ get_start_of_day, \ get_tzid, \ to_timezone, to_utc_datetime -from imiptools.sql import DatabaseOperations def ifnone(x, y): if x is None: return y else: return x -def from_string(s, encoding): - if s: - return unicode(s, encoding) - else: - return s - -def to_string(s, encoding): - if s: - return s.encode(encoding) - else: - return s - class Comparable: "A date/datetime wrapper that allows comparisons with other types." @@ -357,184 +344,6 @@ def make_corrected(self, start, end): return self.__class__(start, end, self.tzid, self.origin) -class FreeBusyPeriod(PeriodBase): - - "A free/busy record abstraction." - - def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, - summary=None, organiser=None): - - """ - Initialise a free/busy period with the given 'start' and 'end' points, - plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' - details. - """ - - PeriodBase.__init__(self, start, end) - self.uid = uid - self.transp = transp or None - self.recurrenceid = recurrenceid or None - self.summary = summary or None - self.organiser = organiser or None - - def as_tuple(self, strings_only=False, string_datetimes=False): - - """ - Return the initialisation parameter tuple, converting datetimes and - false value parameters to strings if 'strings_only' is set to a true - value. Otherwise, if 'string_datetimes' is set to a true value, only the - datetime values are converted to strings. - """ - - null = lambda x: (strings_only and [""] or [x])[0] - return ( - (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start, - (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end, - self.uid or null(self.uid), - self.transp or strings_only and "OPAQUE" or None, - self.recurrenceid or null(self.recurrenceid), - self.summary or null(self.summary), - self.organiser or null(self.organiser) - ) - - def __cmp__(self, other): - - """ - Compare this object to 'other', employing the uid if the periods - involved are the same. - """ - - result = PeriodBase.__cmp__(self, other) - if result == 0 and isinstance(other, FreeBusyPeriod): - return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid)) - else: - return result - - def get_key(self): - return self.uid, self.recurrenceid, self.get_start() - - def __repr__(self): - return "FreeBusyPeriod%r" % (self.as_tuple(),) - - def get_tzid(self): - return "UTC" - - # Period and event recurrence logic. - - def is_replaced(self, recurrences): - - """ - Return whether this period refers to one of the 'recurrences'. - The 'recurrences' must be UTC datetimes corresponding to the start of - the period described by a recurrence. - """ - - for recurrence in recurrences: - if self.is_affected(recurrence): - return True - return False - - def is_affected(self, recurrence): - - """ - Return whether this period refers to 'recurrence'. The 'recurrence' must - be a UTC datetime corresponding to the start of the period described by - a recurrence. - """ - - return recurrence and self.get_start_point() == recurrence - - # Value correction methods. - - def make_corrected(self, start, end): - return self.__class__(start, end) - -class FreeBusyOfferPeriod(FreeBusyPeriod): - - "A free/busy record abstraction for an offer period." - - def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, - summary=None, organiser=None, expires=None): - - """ - Initialise a free/busy period with the given 'start' and 'end' points, - plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' - details. - - An additional 'expires' parameter can be used to indicate an expiry - datetime in conjunction with free/busy offers made when countering - event proposals. - """ - - FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, - summary, organiser) - self.expires = expires or None - - def as_tuple(self, strings_only=False, string_datetimes=False): - - """ - Return the initialisation parameter tuple, converting datetimes and - false value parameters to strings if 'strings_only' is set to a true - value. Otherwise, if 'string_datetimes' is set to a true value, only the - datetime values are converted to strings. - """ - - null = lambda x: (strings_only and [""] or [x])[0] - return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( - self.expires or null(self.expires),) - - def __repr__(self): - return "FreeBusyOfferPeriod%r" % (self.as_tuple(),) - -class FreeBusyGroupPeriod(FreeBusyPeriod): - - "A free/busy record abstraction for a quota group period." - - def __init__(self, start, end, uid=None, transp=None, recurrenceid=None, - summary=None, organiser=None, attendee=None): - - """ - Initialise a free/busy period with the given 'start' and 'end' points, - plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser' - details. - - An additional 'attendee' parameter can be used to indicate the identity - of the attendee recording the period. - """ - - FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid, - summary, organiser) - self.attendee = attendee or None - - def as_tuple(self, strings_only=False, string_datetimes=False): - - """ - Return the initialisation parameter tuple, converting datetimes and - false value parameters to strings if 'strings_only' is set to a true - value. Otherwise, if 'string_datetimes' is set to a true value, only the - datetime values are converted to strings. - """ - - null = lambda x: (strings_only and [""] or [x])[0] - return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + ( - self.attendee or null(self.attendee),) - - def __cmp__(self, other): - - """ - Compare this object to 'other', employing the uid if the periods - involved are the same. - """ - - result = FreeBusyPeriod.__cmp__(self, other) - if isinstance(other, FreeBusyGroupPeriod) and result == 0: - return cmp(self.attendee, other.attendee) - else: - return result - - def __repr__(self): - return "FreeBusyGroupPeriod%r" % (self.as_tuple(),) - class RecurringPeriod(Period): """ @@ -562,867 +371,6 @@ def make_corrected(self, start, end): return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr()) -class FreeBusyCollectionBase: - - "Common operations on free/busy period collections." - - period_columns = [ - "start", "end", "object_uid", "transp", "object_recurrenceid", - "summary", "organiser" - ] - - period_class = FreeBusyPeriod - - def __init__(self, mutable=True): - self.mutable = mutable - - def _check_mutable(self): - if not self.mutable: - raise TypeError, "Cannot mutate this collection." - - def copy(self): - - "Make an independent mutable copy of the collection." - - return FreeBusyCollection(list(self), True) - - def make_period(self, t): - - """ - Make a period using the given tuple of arguments and the collection's - column details. - """ - - args = [] - for arg, column in zip(t, self.period_columns): - args.append(from_string(arg, "utf-8")) - return self.period_class(*args) - - def make_tuple(self, t): - - """ - Return a tuple from the given tuple 't' conforming to the collection's - column details. - """ - - args = [] - for arg, column in zip(t, self.period_columns): - args.append(arg) - return tuple(args) - - # List emulation methods. - - def __iadd__(self, periods): - for period in periods: - self.insert_period(period) - return self - - def append(self, period): - self.insert_period(period) - - # Operations. - - def can_schedule(self, periods, uid, recurrenceid): - - """ - Return whether the collection can accommodate the given 'periods' - employing the specified 'uid' and 'recurrenceid'. - """ - - for conflict in self.have_conflict(periods, True): - if conflict.uid != uid or conflict.recurrenceid != recurrenceid: - return False - - return True - - def have_conflict(self, periods, get_conflicts=False): - - """ - Return whether any period in the collection overlaps with the given - 'periods', returning a collection of such overlapping periods if - 'get_conflicts' is set to a true value. - """ - - conflicts = set() - for p in periods: - overlapping = self.period_overlaps(p, get_conflicts) - if overlapping: - if get_conflicts: - conflicts.update(overlapping) - else: - return True - - if get_conflicts: - return conflicts - else: - return False - - def period_overlaps(self, period, get_periods=False): - - """ - Return whether any period in the collection overlaps with the given - 'period', returning a collection of overlapping periods if 'get_periods' - is set to a true value. - """ - - overlapping = self.get_overlapping([period]) - - if get_periods: - return overlapping - else: - return len(overlapping) != 0 - - def replace_overlapping(self, period, replacements): - - """ - Replace existing periods in the collection within the given 'period', - using the given 'replacements'. - """ - - self._check_mutable() - - self.remove_overlapping(period) - for replacement in replacements: - self.insert_period(replacement) - - def coalesce_freebusy(self): - - "Coalesce the periods in the collection, returning a new collection." - - if not self: - return FreeBusyCollection() - - fb = [] - - it = iter(self) - period = it.next() - - start = period.get_start_point() - end = period.get_end_point() - - try: - while True: - period = it.next() - if period.get_start_point() > end: - fb.append(self.period_class(start, end)) - start = period.get_start_point() - end = period.get_end_point() - else: - end = max(end, period.get_end_point()) - except StopIteration: - pass - - fb.append(self.period_class(start, end)) - return FreeBusyCollection(fb) - - def invert_freebusy(self): - - "Return the free periods from the collection as a new collection." - - if not self: - return FreeBusyCollection([self.period_class(None, None)]) - - # Coalesce periods that overlap or are adjacent. - - fb = self.coalesce_freebusy() - free = [] - - # Add a start-of-time period if appropriate. - - first = fb[0].get_start_point() - if first: - free.append(self.period_class(None, first)) - - start = fb[0].get_end_point() - - for period in fb[1:]: - free.append(self.period_class(start, period.get_start_point())) - start = period.get_end_point() - - # Add an end-of-time period if appropriate. - - if start: - free.append(self.period_class(start, None)) - - return FreeBusyCollection(free) - - def _update_freebusy(self, periods, uid, recurrenceid): - - """ - Update the free/busy details with the given 'periods', using the given - 'uid' plus 'recurrenceid' to remove existing periods. - """ - - self._check_mutable() - - self.remove_specific_event_periods(uid, recurrenceid) - - for p in periods: - self.insert_period(p) - - def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser): - - """ - Update the free/busy details with the given 'periods', 'transp' setting, - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. - """ - - new_periods = [] - - for p in periods: - new_periods.append( - self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser) - ) - - self._update_freebusy(new_periods, uid, recurrenceid) - -class SupportAttendee: - - "A mix-in that supports the affected attendee in free/busy periods." - - period_columns = FreeBusyCollectionBase.period_columns + ["attendee"] - period_class = FreeBusyGroupPeriod - - def _update_freebusy(self, periods, uid, recurrenceid, attendee=None): - - """ - Update the free/busy details with the given 'periods', using the given - 'uid' plus 'recurrenceid' and 'attendee' to remove existing periods. - """ - - self._check_mutable() - - self.remove_specific_event_periods(uid, recurrenceid, attendee) - - for p in periods: - self.insert_period(p) - - def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None): - - """ - Update the free/busy details with the given 'periods', 'transp' setting, - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. - - An optional 'attendee' indicates the attendee affected by the period. - """ - - new_periods = [] - - for p in periods: - new_periods.append( - self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee) - ) - - self._update_freebusy(new_periods, uid, recurrenceid, attendee) - -class SupportExpires: - - "A mix-in that supports the expiry datetime in free/busy periods." - - period_columns = FreeBusyCollectionBase.period_columns + ["expires"] - period_class = FreeBusyOfferPeriod - - def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None): - - """ - Update the free/busy details with the given 'periods', 'transp' setting, - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details. - - An optional 'expires' datetime string indicates the expiry time of any - free/busy offer. - """ - - new_periods = [] - - for p in periods: - new_periods.append( - self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires) - ) - - self._update_freebusy(new_periods, uid, recurrenceid) - -class FreeBusyCollection(FreeBusyCollectionBase): - - "An abstraction for a collection of free/busy periods." - - def __init__(self, periods=None, mutable=True): - - """ - Initialise the collection with the given list of 'periods', or start an - empty collection if no list is given. If 'mutable' is indicated, the - collection may be changed; otherwise, an exception will be raised. - """ - - FreeBusyCollectionBase.__init__(self, mutable) - self.periods = periods or [] - - # List emulation methods. - - def __nonzero__(self): - return bool(self.periods) - - def __iter__(self): - return iter(self.periods) - - def __len__(self): - return len(self.periods) - - def __getitem__(self, i): - return self.periods[i] - - # Operations. - - def insert_period(self, period): - - "Insert the given 'period' into the collection." - - self._check_mutable() - - i = bisect_left(self.periods, period) - if i == len(self.periods): - self.periods.append(period) - elif self.periods[i] != period: - self.periods.insert(i, period) - - def remove_periods(self, periods): - - "Remove the given 'periods' from the collection." - - self._check_mutable() - - for period in periods: - i = bisect_left(self.periods, period) - if i < len(self.periods) and self.periods[i] == period: - del self.periods[i] - - def remove_event_periods(self, uid, recurrenceid=None): - - """ - Remove from the collection all periods associated with 'uid' and - 'recurrenceid' (which if omitted causes the "parent" object's periods to - be referenced). - - Return the removed periods. - """ - - self._check_mutable() - - removed = [] - i = 0 - while i < len(self.periods): - fb = self.periods[i] - if fb.uid == uid and fb.recurrenceid == recurrenceid: - removed.append(self.periods[i]) - del self.periods[i] - else: - i += 1 - - return removed - - # Specific period removal when updating event details. - - remove_specific_event_periods = remove_event_periods - - def remove_additional_periods(self, uid, recurrenceids=None): - - """ - Remove from the collection all periods associated with 'uid' having a - recurrence identifier indicating an additional or modified period. - - If 'recurrenceids' is specified, remove all periods associated with - 'uid' that do not have a recurrence identifier in the given list. - - Return the removed periods. - """ - - self._check_mutable() - - removed = [] - i = 0 - while i < len(self.periods): - fb = self.periods[i] - if fb.uid == uid and fb.recurrenceid and ( - recurrenceids is None or - recurrenceids is not None and fb.recurrenceid not in recurrenceids - ): - removed.append(self.periods[i]) - del self.periods[i] - else: - i += 1 - - return removed - - def remove_affected_period(self, uid, start): - - """ - Remove from the collection the period associated with 'uid' that - provides an occurrence starting at the given 'start' (provided by a - recurrence identifier, converted to a datetime). A recurrence identifier - is used to provide an alternative time period whilst also acting as a - reference to the originally-defined occurrence. - - Return any removed period in a list. - """ - - self._check_mutable() - - removed = [] - - search = Period(start, start) - found = bisect_left(self.periods, search) - - while found < len(self.periods): - fb = self.periods[found] - - # Stop looking if the start no longer matches the recurrence identifier. - - if fb.get_start_point() != search.get_start_point(): - break - - # If the period belongs to the parent object, remove it and return. - - if not fb.recurrenceid and uid == fb.uid: - removed.append(self.periods[found]) - del self.periods[found] - break - - # Otherwise, keep looking for a matching period. - - found += 1 - - return removed - - def periods_from(self, period): - - "Return the entries in the collection at or after 'period'." - - first = bisect_left(self.periods, period) - return self.periods[first:] - - def periods_until(self, period): - - "Return the entries in the collection before 'period'." - - last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid())) - return self.periods[:last] - - def get_overlapping(self, periods): - - """ - Return the entries in the collection providing periods overlapping with - the given sorted collection of 'periods'. - """ - - return get_overlapping(self.periods, periods) - - def remove_overlapping(self, period): - - "Remove all periods overlapping with 'period' from the collection." - - self._check_mutable() - - overlapping = self.get_overlapping([period]) - - if overlapping: - for fb in overlapping: - self.periods.remove(fb) - -class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection): - - "A collection of quota group free/busy objects." - - def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): - - """ - Remove from the collection all periods associated with 'uid' and - 'recurrenceid' (which if omitted causes the "parent" object's periods to - be referenced) and any 'attendee'. - - Return the removed periods. - """ - - self._check_mutable() - - removed = [] - i = 0 - while i < len(self.periods): - fb = self.periods[i] - if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee: - removed.append(self.periods[i]) - del self.periods[i] - else: - i += 1 - - return removed - -class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection): - - "A collection of offered free/busy objects." - - pass - -class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations): - - """ - An abstraction for a collection of free/busy periods stored in a database - system. - """ - - def __init__(self, cursor, table_name, column_names=None, filter_values=None, - mutable=True, paramstyle=None): - - """ - Initialise the collection with the given 'cursor' and with the - 'table_name', 'column_names' and 'filter_values' configuring the - selection of data. If 'mutable' is indicated, the collection may be - changed; otherwise, an exception will be raised. - """ - - FreeBusyCollectionBase.__init__(self, mutable) - DatabaseOperations.__init__(self, column_names, filter_values, paramstyle) - self.cursor = cursor - self.table_name = table_name - - # List emulation methods. - - def __nonzero__(self): - return len(self) and True or False - - def __iter__(self): - query, values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }) - self.cursor.execute(query, values) - return iter(map(lambda t: self.make_period(t), self.cursor.fetchall())) - - def __len__(self): - query, values = self.get_query( - "select count(*) from %(table)s :condition" % { - "table" : self.table_name - }) - self.cursor.execute(query, values) - result = self.cursor.fetchone() - return result and int(result[0]) or 0 - - def __getitem__(self, i): - return list(iter(self))[i] - - # Operations. - - def insert_period(self, period): - - "Insert the given 'period' into the collection." - - self._check_mutable() - - columns, values = self.period_columns, period.as_tuple(string_datetimes=True) - - query, values = self.get_query( - "insert into %(table)s (:columns) values (:values)" % { - "table" : self.table_name - }, - columns, [to_string(v, "utf-8") for v in values]) - - self.cursor.execute(query, values) - - def remove_periods(self, periods): - - "Remove the given 'periods' from the collection." - - self._check_mutable() - - for period in periods: - values = period.as_tuple(string_datetimes=True) - - query, values = self.get_query( - "delete from %(table)s :condition" % { - "table" : self.table_name - }, - self.period_columns, [to_string(v, "utf-8") for v in values]) - - self.cursor.execute(query, values) - - def remove_event_periods(self, uid, recurrenceid=None): - - """ - Remove from the collection all periods associated with 'uid' and - 'recurrenceid' (which if omitted causes the "parent" object's periods to - be referenced). - - Return the removed periods. - """ - - self._check_mutable() - - if recurrenceid: - columns, values = ["object_uid", "object_recurrenceid"], [uid, recurrenceid] - else: - columns, values = ["object_uid", "object_recurrenceid is null"], [uid] - - query, _values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, _values) - removed = self.cursor.fetchall() - - query, values = self.get_query( - "delete from %(table)s :condition" % { - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - return map(lambda t: self.make_period(t), removed) - - # Specific period removal when updating event details. - - remove_specific_event_periods = remove_event_periods - - def remove_additional_periods(self, uid, recurrenceids=None): - - """ - Remove from the collection all periods associated with 'uid' having a - recurrence identifier indicating an additional or modified period. - - If 'recurrenceids' is specified, remove all periods associated with - 'uid' that do not have a recurrence identifier in the given list. - - Return the removed periods. - """ - - self._check_mutable() - - if not recurrenceids: - columns, values = ["object_uid", "object_recurrenceid is not null"], [uid] - else: - columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)] - - query, _values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, _values) - removed = self.cursor.fetchall() - - query, values = self.get_query( - "delete from %(table)s :condition" % { - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - return map(lambda t: self.make_period(t), removed) - - def remove_affected_period(self, uid, start): - - """ - Remove from the collection the period associated with 'uid' that - provides an occurrence starting at the given 'start' (provided by a - recurrence identifier, converted to a datetime). A recurrence identifier - is used to provide an alternative time period whilst also acting as a - reference to the originally-defined occurrence. - - Return any removed period in a list. - """ - - self._check_mutable() - - start = format_datetime(start) - - columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start] - - query, _values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, _values) - removed = self.cursor.fetchall() - - query, values = self.get_query( - "delete from %(table)s :condition" % { - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - return map(lambda t: self.make_period(t), removed) - - def periods_from(self, period): - - "Return the entries in the collection at or after 'period'." - - start = format_datetime(period.get_start_point()) - - columns, values = [], [] - - if start: - columns.append("start >= ?") - values.append(start) - - query, values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - return map(lambda t: self.make_period(t), self.cursor.fetchall()) - - def periods_until(self, period): - - "Return the entries in the collection before 'period'." - - end = format_datetime(period.get_end_point()) - - columns, values = [], [] - - if end: - columns.append("start < ?") - values.append(end) - - query, values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - return map(lambda t: self.make_period(t), self.cursor.fetchall()) - - def get_overlapping(self, periods): - - """ - Return the entries in the collection providing periods overlapping with - the given sorted collection of 'periods'. - """ - - overlapping = set() - - for period in periods: - columns, values = self._get_period_values(period) - - query, values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - overlapping.update(map(lambda t: self.make_period(t), self.cursor.fetchall())) - - overlapping = list(overlapping) - overlapping.sort() - return overlapping - - def remove_overlapping(self, period): - - "Remove all periods overlapping with 'period' from the collection." - - self._check_mutable() - - columns, values = self._get_period_values(period) - - query, values = self.get_query( - "delete from %(table)s :condition" % { - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - def _get_period_values(self, period): - - start = format_datetime(period.get_start_point()) - end = format_datetime(period.get_end_point()) - - columns, values = [], [] - - if end: - columns.append("start < ?") - values.append(end) - if start: - columns.append("end > ?") - values.append(start) - - return columns, values - -class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection): - - "A collection of quota group free/busy objects." - - def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): - - """ - Remove from the collection all periods associated with 'uid' and - 'recurrenceid' (which if omitted causes the "parent" object's periods to - be referenced) and any 'attendee'. - - Return the removed periods. - """ - - self._check_mutable() - - columns, values = ["object_uid"], [uid] - - if recurrenceid: - columns.append("object_recurrenceid") - values.append(recurrenceid) - else: - columns.append("object_recurrenceid is null") - - if attendee: - columns.append("attendee") - values.append(attendee) - else: - columns.append("attendee is null") - - query, _values = self.get_query( - "select %(columns)s from %(table)s :condition" % { - "columns" : self.columnlist(self.period_columns), - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, _values) - removed = self.cursor.fetchall() - - query, values = self.get_query( - "delete from %(table)s :condition" % { - "table" : self.table_name - }, - columns, values) - - self.cursor.execute(query, values) - - return map(lambda t: self.make_period(t), removed) - -class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection): - - "A collection of offered free/busy objects." - - pass - def get_overlapping(first, second): """ diff -r 68e9e6ca2284 -r 4fc327b8d31c imiptools/stores/database/common.py --- a/imiptools/stores/database/common.py Tue May 23 16:31:27 2017 +0200 +++ b/imiptools/stores/database/common.py Tue May 23 16:34:09 2017 +0200 @@ -3,7 +3,7 @@ """ A database store of calendar data. -Copyright (C) 2014, 2015, 2016 Paul Boddie +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -24,11 +24,13 @@ from datetime import datetime from imiptools.data import parse_string, to_string from imiptools.dates import format_datetime, get_datetime, to_timezone -from imiptools.period import FreeBusyDatabaseCollection, \ - FreeBusyGroupDatabaseCollection, \ - FreeBusyOffersDatabaseCollection +from imiptools.freebusy import FreeBusyDatabaseCollection, \ + FreeBusyGroupDatabaseCollection, \ + FreeBusyOffersDatabaseCollection from imiptools.sql import DatabaseOperations +def first(l): return l[0] + class DatabaseStoreBase(DatabaseOperations): "A database store supporting user-specific locking." @@ -56,6 +58,15 @@ "freebusy_provider_datetimes" : self.freebusy_provider_datetimes_table, } + def get_single_values(self): + + """ + Return the cursor results as a list of single values from each of the + result tuples. + """ + + return map(first, self.cursor.fetchall()) + class DatabaseStore(DatabaseStoreBase, StoreBase): "A database store of tabular free/busy data and objects." @@ -79,7 +90,7 @@ "union all select store_user from %(recurrences)s" \ ") as users") self.cursor.execute(query) - return [r[0] for r in self.cursor.fetchall()] + return self.get_single_values() # Event and event metadata access. @@ -119,7 +130,7 @@ columns, values) self.cursor.execute(query, values) - return [r[0] for r in self.cursor.fetchall()] + return self.get_single_values() def get_cancelled_events(self, user): @@ -505,7 +516,7 @@ columns, values) self.cursor.execute(query, values) - return [r[0] for r in self.cursor.fetchall()] + return self.get_single_values() # Tentative free/busy periods related to countering. @@ -641,7 +652,7 @@ columns, values) self.cursor.execute(query, values) - return [r[0] for r in self.cursor.fetchall()] + return self.get_single_values() def get_counter(self, user, other, uid, recurrenceid=None): @@ -852,7 +863,7 @@ "union all select quota from quota_limits" \ ") as quotas") self.cursor.execute(query) - return [r[0] for r in self.cursor.fetchall()] + return self.get_single_values() def get_quota_users(self, quota): @@ -869,7 +880,7 @@ columns, values) self.cursor.execute(query, values) - return [r[0] for r in self.cursor.fetchall()] + return self.get_single_values() # Delegate information for the quota. @@ -885,7 +896,7 @@ columns, values) self.cursor.execute(query, values) - return [r[0] for r in self.cursor.fetchall()] + return self.get_single_values() def set_delegates(self, quota, delegates): diff -r 68e9e6ca2284 -r 4fc327b8d31c imiptools/stores/file.py --- a/imiptools/stores/file.py Tue May 23 16:31:27 2017 +0200 +++ b/imiptools/stores/file.py Tue May 23 16:34:09 2017 +0200 @@ -26,14 +26,16 @@ from imiptools.data import make_calendar, parse_object, to_stream from imiptools.dates import format_datetime, get_datetime, to_timezone from imiptools.filesys import fix_permissions, FileBase -from imiptools.period import FreeBusyPeriod, FreeBusyGroupPeriod, \ - FreeBusyOfferPeriod, FreeBusyCollection, \ - FreeBusyGroupCollection, FreeBusyOffersCollection +from imiptools.freebusy import FreeBusyPeriod, FreeBusyGroupPeriod, \ + FreeBusyOfferPeriod, FreeBusyCollection, \ + FreeBusyGroupCollection, FreeBusyOffersCollection from imiptools.text import get_table, set_defaults from os.path import isdir, isfile, join from os import listdir, remove, rmdir import codecs +# Obtain defaults from the settings. + STORE_DIR = settings["STORE_DIR"] PUBLISH_DIR = settings["PUBLISH_DIR"] JOURNAL_DIR = settings["JOURNAL_DIR"] diff -r 68e9e6ca2284 -r 4fc327b8d31c imipweb/resource.py --- a/imipweb/resource.py Tue May 23 16:31:27 2017 +0200 +++ b/imipweb/resource.py Tue May 23 16:34:09 2017 +0200 @@ -3,7 +3,7 @@ """ Common resource functionality for Web calendar clients. -Copyright (C) 2014, 2015, 2016 Paul Boddie +Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -23,7 +23,7 @@ from imiptools.client import Client, ClientForObject from imiptools.data import get_uri from imiptools.dates import format_datetime, to_date -from imiptools.period import FreeBusyCollection +from imiptools.freebusy import FreeBusyCollection from imipweb.data import event_period_from_period, form_period_from_period, \ FormDate, PeriodError from imipweb.env import CGIEnvironment diff -r 68e9e6ca2284 -r 4fc327b8d31c tools/make_freebusy.py --- a/tools/make_freebusy.py Tue May 23 16:31:27 2017 +0200 +++ b/tools/make_freebusy.py Tue May 23 16:34:09 2017 +0200 @@ -38,8 +38,8 @@ from imiptools.client import Client from imiptools.data import get_window_end, Object from imiptools.dates import get_default_timezone, to_utc_datetime -from imiptools.period import FreeBusyCollection, FreeBusyGroupCollection, \ - FreeBusyGroupPeriod +from imiptools.freebusy import FreeBusyCollection, FreeBusyGroupCollection, \ + FreeBusyGroupPeriod from imiptools.stores import get_store, get_publisher, get_journal def make_freebusy(client, participants, storage, store_and_publish,