1.1 --- a/imiptools/period.py Wed Mar 02 21:17:11 2016 +0100
1.2 +++ b/imiptools/period.py Thu Mar 03 23:20:47 2016 +0100
1.3 @@ -454,33 +454,12 @@
1.4 def make_corrected(self, start, end):
1.5 return self.__class__(start, end, self.tzid, self.origin, self.get_start_attr(), self.get_end_attr())
1.6
1.7 -class FreeBusyCollection:
1.8 -
1.9 - "An abstraction for a collection of free/busy periods."
1.10 -
1.11 - def __init__(self, periods=None):
1.12 +class FreeBusyCollectionBase:
1.13
1.14 - """
1.15 - Initialise the collection with the given list of 'periods', or start an
1.16 - empty collection if no list is given.
1.17 - """
1.18 -
1.19 - self.periods = periods or []
1.20 + "Common operations on free/busy period collections."
1.21
1.22 # List emulation methods.
1.23
1.24 - def __list__(self):
1.25 - return self.periods
1.26 -
1.27 - def __iter__(self):
1.28 - return iter(self.periods)
1.29 -
1.30 - def __len__(self):
1.31 - return len(self.periods)
1.32 -
1.33 - def __getitem__(self, i):
1.34 - return self.periods[i]
1.35 -
1.36 def __iadd__(self, other):
1.37 for period in other:
1.38 self.insert_period(period)
1.39 @@ -523,6 +502,137 @@
1.40 else:
1.41 return False
1.42
1.43 + def period_overlaps(self, period, get_periods=False):
1.44 +
1.45 + """
1.46 + Return whether any period in the collection overlaps with the given
1.47 + 'period', returning a collection of overlapping periods if 'get_periods'
1.48 + is set to a true value.
1.49 + """
1.50 +
1.51 + overlapping = self.get_overlapping(period)
1.52 +
1.53 + if get_periods:
1.54 + return overlapping
1.55 + else:
1.56 + return len(overlapping) != 0
1.57 +
1.58 + def replace_overlapping(self, period, replacements):
1.59 +
1.60 + """
1.61 + Replace existing periods in the collection within the given 'period',
1.62 + using the given 'replacements'.
1.63 + """
1.64 +
1.65 + self.remove_overlapping(period)
1.66 + for replacement in replacements:
1.67 + self.insert_period(replacement)
1.68 +
1.69 + def coalesce_freebusy(self):
1.70 +
1.71 + "Coalesce the periods in the collection, returning a new collection."
1.72 +
1.73 + if not self:
1.74 + return FreeBusyCollection()
1.75 +
1.76 + fb = []
1.77 +
1.78 + it = iter(self)
1.79 + period = it.next()
1.80 +
1.81 + start = period.get_start_point()
1.82 + end = period.get_end_point()
1.83 +
1.84 + try:
1.85 + while True:
1.86 + period = it.next()
1.87 + if period.get_start_point() > end:
1.88 + fb.append(FreeBusyPeriod(start, end))
1.89 + start = period.get_start_point()
1.90 + end = period.get_end_point()
1.91 + else:
1.92 + end = max(end, period.get_end_point())
1.93 + except StopIteration:
1.94 + pass
1.95 +
1.96 + fb.append(FreeBusyPeriod(start, end))
1.97 + return FreeBusyCollection(fb)
1.98 +
1.99 + def invert_freebusy(self):
1.100 +
1.101 + "Return the free periods from the collection as a new collection."
1.102 +
1.103 + if not self:
1.104 + return FreeBusyCollection([FreeBusyPeriod(None, None)])
1.105 +
1.106 + # Coalesce periods that overlap or are adjacent.
1.107 +
1.108 + fb = self.coalesce_freebusy()
1.109 + free = []
1.110 +
1.111 + # Add a start-of-time period if appropriate.
1.112 +
1.113 + first = fb[0].get_start_point()
1.114 + if first:
1.115 + free.append(FreeBusyPeriod(None, first))
1.116 +
1.117 + start = fb[0].get_end_point()
1.118 +
1.119 + for period in fb[1:]:
1.120 + free.append(FreeBusyPeriod(start, period.get_start_point()))
1.121 + start = period.get_end_point()
1.122 +
1.123 + # Add an end-of-time period if appropriate.
1.124 +
1.125 + if start:
1.126 + free.append(FreeBusyPeriod(start, None))
1.127 +
1.128 + return FreeBusyCollection(free)
1.129 +
1.130 + def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
1.131 +
1.132 + """
1.133 + Update the free/busy details with the given 'periods', 'transp' setting,
1.134 + 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.135 +
1.136 + An optional 'expires' datetime string indicates the expiry time of any
1.137 + free/busy offer.
1.138 + """
1.139 +
1.140 + self.remove_event_periods(uid, recurrenceid)
1.141 +
1.142 + for p in periods:
1.143 + self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
1.144 +
1.145 +class FreeBusyCollection(FreeBusyCollectionBase):
1.146 +
1.147 + "An abstraction for a collection of free/busy periods."
1.148 +
1.149 + def __init__(self, periods=None):
1.150 +
1.151 + """
1.152 + Initialise the collection with the given list of 'periods', or start an
1.153 + empty collection if no list is given.
1.154 + """
1.155 +
1.156 + self.periods = periods or []
1.157 +
1.158 + # List emulation methods.
1.159 +
1.160 + def __nonzero__(self):
1.161 + return bool(self.periods)
1.162 +
1.163 + def __iter__(self):
1.164 + return iter(self.periods)
1.165 +
1.166 + def __len__(self):
1.167 + return len(self.periods)
1.168 +
1.169 + def __getitem__(self, i):
1.170 + return self.periods[i]
1.171 +
1.172 + # Operations.
1.173 +
1.174 def insert_period(self, period):
1.175
1.176 "Insert the given 'period' into the collection."
1.177 @@ -673,21 +783,6 @@
1.178 overlapping.sort()
1.179 return overlapping
1.180
1.181 - def period_overlaps(self, period, get_periods=False):
1.182 -
1.183 - """
1.184 - Return whether any period in the collection overlaps with the given
1.185 - 'period', returning a collection of overlapping periods if 'get_periods'
1.186 - is set to a true value.
1.187 - """
1.188 -
1.189 - overlapping = self.get_overlapping(period)
1.190 -
1.191 - if get_periods:
1.192 - return overlapping
1.193 - else:
1.194 - return len(overlapping) != 0
1.195 -
1.196 def remove_overlapping(self, period):
1.197
1.198 "Remove all periods overlapping with 'period' from the collection."
1.199 @@ -698,84 +793,245 @@
1.200 for fb in overlapping:
1.201 self.periods.remove(fb)
1.202
1.203 - def replace_overlapping(self, period, replacements):
1.204 +class FreeBusyDatabaseCollection(FreeBusyCollectionBase):
1.205 +
1.206 + """
1.207 + An abstraction for a collection of free/busy periods stored in a database
1.208 + system.
1.209 + """
1.210 +
1.211 + def __init__(self, cursor, table_name):
1.212
1.213 """
1.214 - Replace existing periods in the collection within the given 'period',
1.215 - using the given 'replacements'.
1.216 + Initialise the collection with the given 'cursor' and 'table_name'.
1.217 + """
1.218 +
1.219 + self.cursor = cursor
1.220 + self.table_name = table_name
1.221 +
1.222 + # Special database-related operations.
1.223 +
1.224 + def placeholders(self, values):
1.225 + return ", ".join(["?"] * len(values))
1.226 +
1.227 + def initialise(self):
1.228 +
1.229 + "Create the database table required to hold the collection."
1.230 +
1.231 + query = """\
1.232 +create table %(table)s (
1.233 + start varchar not null,
1.234 + end varchar not null,
1.235 + uid varchar,
1.236 + transp varchar,
1.237 + recurrenceid varchar,
1.238 + summary varchar,
1.239 + organiser varchar,
1.240 + expires varchar
1.241 + )""" % {"table" : self.table_name}
1.242 +
1.243 + self.cursor.execute(query)
1.244 +
1.245 + # List emulation methods.
1.246 +
1.247 + def __nonzero__(self):
1.248 + query = "select count(*) from %(table)s" % {"table" : self.table_name}
1.249 + self.cursor.execute(query)
1.250 + result = self.cursor.fetchone()
1.251 + return result[0]
1.252 +
1.253 + def __iter__(self):
1.254 + query = "select * from %(table)s" % {"table" : self.table_name}
1.255 + self.cursor.execute(query)
1.256 + return iter(map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall()))
1.257 +
1.258 + def __len__(self):
1.259 + return len(list(iter(self)))
1.260 +
1.261 + def __getitem__(self, i):
1.262 + return list(iter(self))[i]
1.263 +
1.264 + # Operations.
1.265 +
1.266 + def insert_period(self, period):
1.267 +
1.268 + "Insert the given 'period' into the collection."
1.269 +
1.270 + values = period.as_tuple(strings_only=True)
1.271 + query = "insert into %(table)s values (%(columns)s)" % {
1.272 + "table" : self.table_name,
1.273 + "columns" : self.placeholders(values)
1.274 + }
1.275 + self.cursor.execute(query, values)
1.276 +
1.277 + def remove_periods(self, periods):
1.278 +
1.279 + "Remove the given 'periods' from the collection."
1.280 +
1.281 + for period in periods:
1.282 + values = period.as_tuple(strings_only=True)
1.283 + query = """\
1.284 +delete from %(table)s
1.285 +where start = ? and end = ? and uid = ? and transp = ? and recurrenceid = ? and summary = ? and organiser = ? and expires = ?
1.286 +""" % {"table" : self.table_name}
1.287 + self.cursor.execute(query, values)
1.288 +
1.289 + def remove_event_periods(self, uid, recurrenceid=None):
1.290 +
1.291 + """
1.292 + Remove from the collection all periods associated with 'uid' and
1.293 + 'recurrenceid' (which if omitted causes the "parent" object's periods to
1.294 + be referenced).
1.295 +
1.296 + Return the removed periods.
1.297 """
1.298
1.299 - self.remove_overlapping(period)
1.300 - for replacement in replacements:
1.301 - self.insert_period(replacement)
1.302 + if recurrenceid:
1.303 + condition = "where uid = ? and recurrenceid = ?"
1.304 + values = (uid, recurrenceid)
1.305 + else:
1.306 + condition = "where uid = ?"
1.307 + values = (uid,)
1.308
1.309 - def coalesce_freebusy(self):
1.310 -
1.311 - "Coalesce the periods in the collection, returning a new collection."
1.312 -
1.313 - if not self.periods:
1.314 - return FreeBusyCollection(self.periods)
1.315 + query = "select * from %(table)s for update %(condition)s" % {
1.316 + "table" : self.table_name,
1.317 + "condition" : condition
1.318 + }
1.319 + self.cursor.execute(query, values)
1.320 + removed = self.cursor.fetchall()
1.321
1.322 - fb = []
1.323 - start = self.periods[0].get_start_point()
1.324 - end = self.periods[0].get_end_point()
1.325 + query = "delete from %(table)s %(condition)s" % {
1.326 + "table" : self.table_name,
1.327 + "condition" : condition
1.328 + }
1.329 + self.cursor.execute(query, values)
1.330
1.331 - for period in self.periods[1:]:
1.332 - if period.get_start_point() > end:
1.333 - fb.append(FreeBusyPeriod(start, end))
1.334 - start = period.get_start_point()
1.335 - end = period.get_end_point()
1.336 - else:
1.337 - end = max(end, period.get_end_point())
1.338 + return map(lambda t: FreeBusyPeriod(*t), removed)
1.339 +
1.340 + def remove_additional_periods(self, uid, recurrenceids=None):
1.341
1.342 - fb.append(FreeBusyPeriod(start, end))
1.343 - return FreeBusyCollection(fb)
1.344 -
1.345 - def invert_freebusy(self):
1.346 + """
1.347 + Remove from the collection all periods associated with 'uid' having a
1.348 + recurrence identifier indicating an additional or modified period.
1.349
1.350 - "Return the free periods from the collection as a new collection."
1.351 + If 'recurrenceids' is specified, remove all periods associated with
1.352 + 'uid' that do not have a recurrence identifier in the given list.
1.353
1.354 - if not self.periods:
1.355 - return FreeBusyCollection([FreeBusyPeriod(None, None)])
1.356 + Return the removed periods.
1.357 + """
1.358
1.359 - # Coalesce periods that overlap or are adjacent.
1.360 -
1.361 - fb = self.coalesce_freebusy()
1.362 - free = []
1.363 -
1.364 - # Add a start-of-time period if appropriate.
1.365 + if recurrenceids is None:
1.366 + condition = "where uid = ? and recurrenceid is not null"
1.367 + values = (uid,)
1.368 + else:
1.369 + condition = "where uid = ? and recurrenceid is not null and recurrenceid not in ?"
1.370 + values = (uid, recurrenceid)
1.371
1.372 - first = fb[0].get_start_point()
1.373 - if first:
1.374 - free.append(FreeBusyPeriod(None, first))
1.375 -
1.376 - start = fb[0].get_end_point()
1.377 + query = "select * from %(table)s for update %(condition)s" % {
1.378 + "table" : self.table_name,
1.379 + "condition" : condition
1.380 + }
1.381 + self.cursor.execute(query, values)
1.382 + removed = self.cursor.fetchall()
1.383
1.384 - for period in fb[1:]:
1.385 - free.append(FreeBusyPeriod(start, period.get_start_point()))
1.386 - start = period.get_end_point()
1.387 -
1.388 - # Add an end-of-time period if appropriate.
1.389 + query = "delete from %(table)s %(condition)s" % {
1.390 + "table" : self.table_name,
1.391 + "condition" : condition
1.392 + }
1.393 + self.cursor.execute(query, values)
1.394
1.395 - if start:
1.396 - free.append(FreeBusyPeriod(start, None))
1.397 + return map(lambda t: FreeBusyPeriod(*t), removed)
1.398
1.399 - return FreeBusyCollection(free)
1.400 -
1.401 - def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
1.402 + def remove_affected_period(self, uid, start):
1.403
1.404 """
1.405 - Update the free/busy details with the given 'periods', 'transp' setting,
1.406 - 'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
1.407 + Remove from the collection the period associated with 'uid' that
1.408 + provides an occurrence starting at the given 'start' (provided by a
1.409 + recurrence identifier, converted to a datetime). A recurrence identifier
1.410 + is used to provide an alternative time period whilst also acting as a
1.411 + reference to the originally-defined occurrence.
1.412
1.413 - An optional 'expires' datetime string indicates the expiry time of any
1.414 - free/busy offer.
1.415 + Return any removed period in a list.
1.416 """
1.417
1.418 - self.remove_event_periods(uid, recurrenceid)
1.419 + condition = "where uid = ? and start = ? and recurrenceid is null"
1.420 + values = (uid, start)
1.421 +
1.422 + query = "select * from %(table)s %(condition)s" % {
1.423 + "table" : self.table_name,
1.424 + "condition" : condition
1.425 + }
1.426 + self.cursor.execute(query, values)
1.427 + removed = self.cursor.fetchall()
1.428 +
1.429 + query = "delete from %(table)s %(condition)s" % {
1.430 + "table" : self.table_name,
1.431 + "condition" : condition
1.432 + }
1.433 + self.cursor.execute(query, values)
1.434 +
1.435 + return map(lambda t: FreeBusyPeriod(*t), removed)
1.436 +
1.437 + def periods_from(self, period):
1.438 +
1.439 + "Return the entries in the collection at or after 'period'."
1.440 +
1.441 + condition = "where start >= ?"
1.442 + values = (format_datetime(period.get_start_point()),)
1.443 +
1.444 + query = "select * from %(table)s %(condition)s" % {
1.445 + "table" : self.table_name,
1.446 + "condition" : condition
1.447 + }
1.448 + self.cursor.execute(query, values)
1.449 +
1.450 + return map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall())
1.451 +
1.452 + def periods_until(self, period):
1.453 +
1.454 + "Return the entries in the collection before 'period'."
1.455
1.456 - for p in periods:
1.457 - self.insert_period(FreeBusyPeriod(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires))
1.458 + condition = "where start < ?"
1.459 + values = (format_datetime(period.get_end_point()),)
1.460 +
1.461 + query = "select * from %(table)s %(condition)s" % {
1.462 + "table" : self.table_name,
1.463 + "condition" : condition
1.464 + }
1.465 + self.cursor.execute(query, values)
1.466 +
1.467 + return map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall())
1.468 +
1.469 + def get_overlapping(self, period):
1.470 +
1.471 + """
1.472 + Return the entries in the collection providing periods overlapping with
1.473 + 'period'.
1.474 + """
1.475 +
1.476 + condition = "where start < ? and end > ?"
1.477 + values = (format_datetime(period.get_end_point()), format_datetime(period.get_start_point()))
1.478 +
1.479 + query = "select * from %(table)s %(condition)s" % {
1.480 + "table" : self.table_name,
1.481 + "condition" : condition
1.482 + }
1.483 + self.cursor.execute(query, values)
1.484 +
1.485 + return map(lambda t: FreeBusyPeriod(*t), self.cursor.fetchall())
1.486 +
1.487 + def remove_overlapping(self, period):
1.488 +
1.489 + "Remove all periods overlapping with 'period' from the collection."
1.490 +
1.491 + condition = "where start < ? and end > ?"
1.492 + values = (format_datetime(period.get_end_point()), format_datetime(period.get_start_point()))
1.493 +
1.494 + query = "delete from %(table)s %(condition)s" % {
1.495 + "table" : self.table_name,
1.496 + "condition" : condition
1.497 + }
1.498 + self.cursor.execute(query, values)
1.499
1.500 # Period layout.
1.501