paul@1234 | 1 | #!/usr/bin/env python |
paul@1234 | 2 | |
paul@1234 | 3 | """ |
paul@1234 | 4 | Managing free/busy period collections using database representations. |
paul@1234 | 5 | |
paul@1234 | 6 | Copyright (C) 2014, 2015, 2016, 2017 Paul Boddie <paul@boddie.org.uk> |
paul@1234 | 7 | |
paul@1234 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@1234 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@1234 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@1234 | 11 | version. |
paul@1234 | 12 | |
paul@1234 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT |
paul@1234 | 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paul@1234 | 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
paul@1234 | 16 | details. |
paul@1234 | 17 | |
paul@1234 | 18 | You should have received a copy of the GNU General Public License along with |
paul@1234 | 19 | this program. If not, see <http://www.gnu.org/licenses/>. |
paul@1234 | 20 | """ |
paul@1234 | 21 | |
paul@1234 | 22 | from imiptools.dates import format_datetime |
paul@1234 | 23 | from imiptools.freebusy.common import FreeBusyCollectionBase, SupportAttendee, \ |
paul@1234 | 24 | SupportExpires, from_string, to_string |
paul@1234 | 25 | from imiptools.sql import DatabaseOperations |
paul@1234 | 26 | |
paul@1234 | 27 | try: |
paul@1234 | 28 | from cStringIO import StringIO |
paul@1234 | 29 | except ImportError: |
paul@1234 | 30 | from StringIO import StringIO |
paul@1234 | 31 | |
paul@1234 | 32 | # Conversion functions. |
paul@1234 | 33 | |
paul@1234 | 34 | def to_copy_string(s, encoding): |
paul@1234 | 35 | |
paul@1234 | 36 | """ |
paul@1234 | 37 | Encode 's' using 'encoding' as a string suitable for use in tabular data |
paul@1234 | 38 | acceptable to the PostgreSQL COPY command with \N as null. |
paul@1234 | 39 | """ |
paul@1234 | 40 | |
paul@1234 | 41 | s = to_string(s, encoding) |
paul@1234 | 42 | return s is None and "\\N" or s |
paul@1234 | 43 | |
paul@1234 | 44 | def to_copy_file(records): |
paul@1234 | 45 | |
paul@1234 | 46 | """ |
paul@1234 | 47 | Encode the given 'records' and store them in a file-like object for use with |
paul@1234 | 48 | a tabular import mechanism. Return the file-like object. |
paul@1234 | 49 | """ |
paul@1234 | 50 | |
paul@1234 | 51 | io = StringIO() |
paul@1234 | 52 | for values in records: |
paul@1234 | 53 | l = [] |
paul@1234 | 54 | for v in values: |
paul@1234 | 55 | l.append(to_copy_string(v, "utf-8")) |
paul@1234 | 56 | io.write("\t".join(l)) |
paul@1234 | 57 | io.write("\n") |
paul@1234 | 58 | io.seek(0) |
paul@1234 | 59 | return io |
paul@1234 | 60 | |
paul@1234 | 61 | def quote_column(column): |
paul@1234 | 62 | |
paul@1234 | 63 | "Quote 'column' using the SQL keyword quoting notation." |
paul@1234 | 64 | |
paul@1234 | 65 | return '"%s"' % column |
paul@1234 | 66 | |
paul@1234 | 67 | |
paul@1234 | 68 | |
paul@1234 | 69 | # Collection abstractions. |
paul@1234 | 70 | |
paul@1234 | 71 | class FreeBusyDatabaseCollection(FreeBusyCollectionBase, DatabaseOperations): |
paul@1234 | 72 | |
paul@1234 | 73 | """ |
paul@1234 | 74 | An abstraction for a collection of free/busy periods stored in a database |
paul@1234 | 75 | system. |
paul@1234 | 76 | """ |
paul@1234 | 77 | |
paul@1234 | 78 | def __init__(self, cursor, table_name, column_names=None, filter_values=None, |
paul@1234 | 79 | mutable=True, paramstyle=None): |
paul@1234 | 80 | |
paul@1234 | 81 | """ |
paul@1234 | 82 | Initialise the collection with the given 'cursor' and with the |
paul@1234 | 83 | 'table_name', 'column_names' and 'filter_values' configuring the |
paul@1234 | 84 | selection of data. If 'mutable' is indicated, the collection may be |
paul@1234 | 85 | changed; otherwise, an exception will be raised. |
paul@1234 | 86 | """ |
paul@1234 | 87 | |
paul@1234 | 88 | FreeBusyCollectionBase.__init__(self, mutable) |
paul@1234 | 89 | DatabaseOperations.__init__(self, column_names, filter_values, paramstyle) |
paul@1234 | 90 | self.cursor = cursor |
paul@1234 | 91 | self.table_name = table_name |
paul@1234 | 92 | |
paul@1236 | 93 | self.period_columns = self.period_class.period_columns |
paul@1236 | 94 | |
paul@1234 | 95 | # List emulation methods. |
paul@1234 | 96 | |
paul@1234 | 97 | def __nonzero__(self): |
paul@1234 | 98 | return len(self) and True or False |
paul@1234 | 99 | |
paul@1234 | 100 | def __iter__(self): |
paul@1234 | 101 | query, values = self.get_query( |
paul@1234 | 102 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 103 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 104 | "table" : self.table_name |
paul@1234 | 105 | }) |
paul@1234 | 106 | self.cursor.execute(query, values) |
paul@1234 | 107 | return iter(map(lambda t: self.make_period(t), self.cursor.fetchall())) |
paul@1234 | 108 | |
paul@1234 | 109 | def __len__(self): |
paul@1234 | 110 | query, values = self.get_query( |
paul@1234 | 111 | "select count(*) from %(table)s :condition" % { |
paul@1234 | 112 | "table" : self.table_name |
paul@1234 | 113 | }) |
paul@1234 | 114 | self.cursor.execute(query, values) |
paul@1234 | 115 | result = self.cursor.fetchone() |
paul@1234 | 116 | return result and int(result[0]) or 0 |
paul@1234 | 117 | |
paul@1234 | 118 | def __getitem__(self, i): |
paul@1234 | 119 | return list(iter(self))[i] |
paul@1234 | 120 | |
paul@1236 | 121 | # Dictionary emulation methods (even though this is not a mapping). |
paul@1236 | 122 | |
paul@1236 | 123 | def clear(self): |
paul@1236 | 124 | query, values = self.get_query( |
paul@1236 | 125 | "delete from %(table)s :condition" % { |
paul@1236 | 126 | "table" : self.table_name |
paul@1236 | 127 | }) |
paul@1236 | 128 | self.cursor.execute(query, values) |
paul@1236 | 129 | |
paul@1234 | 130 | # Operations. |
paul@1234 | 131 | |
paul@1234 | 132 | def insert_period(self, period): |
paul@1234 | 133 | |
paul@1234 | 134 | "Insert the given 'period' into the collection." |
paul@1234 | 135 | |
paul@1234 | 136 | self._check_mutable() |
paul@1234 | 137 | |
paul@1234 | 138 | columns, values = self.period_columns, period.as_tuple(string_datetimes=True) |
paul@1234 | 139 | |
paul@1234 | 140 | query, values = self.get_query( |
paul@1234 | 141 | "insert into %(table)s (:columns) values (:values)" % { |
paul@1234 | 142 | "table" : self.table_name |
paul@1234 | 143 | }, |
paul@1234 | 144 | columns, [to_string(v, "utf-8") for v in values]) |
paul@1234 | 145 | |
paul@1234 | 146 | self.cursor.execute(query, values) |
paul@1234 | 147 | |
paul@1234 | 148 | def insert_periods(self, periods): |
paul@1234 | 149 | |
paul@1234 | 150 | "Insert the given 'periods' into the collection." |
paul@1234 | 151 | |
paul@1234 | 152 | if not hasattr(self.cursor, "copy_from"): |
paul@1234 | 153 | return FreeBusyCollectionBase.insert_periods(self, periods) |
paul@1234 | 154 | |
paul@1234 | 155 | self._check_mutable() |
paul@1234 | 156 | |
paul@1234 | 157 | columns = self.merge_default_columns(self.period_columns) |
paul@1234 | 158 | |
paul@1234 | 159 | all_values = [] |
paul@1234 | 160 | for period in periods: |
paul@1234 | 161 | all_values.append(self.merge_default_values(period.as_tuple(string_datetimes=True))) |
paul@1234 | 162 | |
paul@1234 | 163 | f = to_copy_file(all_values) |
paul@1234 | 164 | |
paul@1234 | 165 | # Copy from the file-like object to the table. |
paul@1234 | 166 | |
paul@1234 | 167 | self.cursor.copy_from(f, self.table_name, columns=map(quote_column, columns)) |
paul@1234 | 168 | |
paul@1234 | 169 | def remove_periods(self, periods): |
paul@1234 | 170 | |
paul@1234 | 171 | "Remove the given 'periods' from the collection." |
paul@1234 | 172 | |
paul@1234 | 173 | self._check_mutable() |
paul@1234 | 174 | |
paul@1234 | 175 | for period in periods: |
paul@1234 | 176 | values = period.as_tuple(string_datetimes=True) |
paul@1234 | 177 | |
paul@1234 | 178 | query, values = self.get_query( |
paul@1234 | 179 | "delete from %(table)s :condition" % { |
paul@1234 | 180 | "table" : self.table_name |
paul@1234 | 181 | }, |
paul@1234 | 182 | self.period_columns, [to_string(v, "utf-8") for v in values]) |
paul@1234 | 183 | |
paul@1234 | 184 | self.cursor.execute(query, values) |
paul@1234 | 185 | |
paul@1234 | 186 | def remove_event_periods(self, uid, recurrenceid=None, participant=None): |
paul@1234 | 187 | |
paul@1234 | 188 | """ |
paul@1234 | 189 | Remove from the collection all periods associated with 'uid' and |
paul@1234 | 190 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1234 | 191 | be referenced). |
paul@1234 | 192 | |
paul@1234 | 193 | If 'participant' is specified, only remove periods for which the |
paul@1234 | 194 | participant is given as attending. |
paul@1234 | 195 | |
paul@1234 | 196 | Return the removed periods. |
paul@1234 | 197 | """ |
paul@1234 | 198 | |
paul@1234 | 199 | self._check_mutable() |
paul@1234 | 200 | |
paul@1234 | 201 | columns, values = ["object_uid"], [uid] |
paul@1234 | 202 | |
paul@1234 | 203 | if recurrenceid: |
paul@1234 | 204 | columns.append("object_recurrenceid") |
paul@1234 | 205 | values.append(recurrenceid) |
paul@1234 | 206 | else: |
paul@1234 | 207 | columns.append("object_recurrenceid is null") |
paul@1234 | 208 | |
paul@1234 | 209 | if participant: |
paul@1234 | 210 | columns.append("attendee") |
paul@1234 | 211 | values.append(participant) |
paul@1234 | 212 | |
paul@1234 | 213 | query, _values = self.get_query( |
paul@1234 | 214 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 215 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 216 | "table" : self.table_name |
paul@1234 | 217 | }, |
paul@1234 | 218 | columns, values) |
paul@1234 | 219 | |
paul@1234 | 220 | self.cursor.execute(query, _values) |
paul@1234 | 221 | removed = self.cursor.fetchall() |
paul@1234 | 222 | |
paul@1234 | 223 | query, values = self.get_query( |
paul@1234 | 224 | "delete from %(table)s :condition" % { |
paul@1234 | 225 | "table" : self.table_name |
paul@1234 | 226 | }, |
paul@1234 | 227 | columns, values) |
paul@1234 | 228 | |
paul@1234 | 229 | self.cursor.execute(query, values) |
paul@1234 | 230 | |
paul@1234 | 231 | return map(lambda t: self.make_period(t), removed) |
paul@1234 | 232 | |
paul@1234 | 233 | # Specific period removal when updating event details. |
paul@1234 | 234 | |
paul@1234 | 235 | remove_specific_event_periods = remove_event_periods |
paul@1234 | 236 | |
paul@1234 | 237 | def remove_additional_periods(self, uid, recurrenceids=None): |
paul@1234 | 238 | |
paul@1234 | 239 | """ |
paul@1234 | 240 | Remove from the collection all periods associated with 'uid' having a |
paul@1234 | 241 | recurrence identifier indicating an additional or modified period. |
paul@1234 | 242 | |
paul@1234 | 243 | If 'recurrenceids' is specified, remove all periods associated with |
paul@1234 | 244 | 'uid' that do not have a recurrence identifier in the given list. |
paul@1234 | 245 | |
paul@1234 | 246 | Return the removed periods. |
paul@1234 | 247 | """ |
paul@1234 | 248 | |
paul@1234 | 249 | self._check_mutable() |
paul@1234 | 250 | |
paul@1234 | 251 | if not recurrenceids: |
paul@1234 | 252 | columns, values = ["object_uid", "object_recurrenceid is not null"], [uid] |
paul@1234 | 253 | else: |
paul@1234 | 254 | columns, values = ["object_uid", "object_recurrenceid not in ?", "object_recurrenceid is not null"], [uid, tuple(recurrenceids)] |
paul@1234 | 255 | |
paul@1234 | 256 | query, _values = self.get_query( |
paul@1234 | 257 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 258 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 259 | "table" : self.table_name |
paul@1234 | 260 | }, |
paul@1234 | 261 | columns, values) |
paul@1234 | 262 | |
paul@1234 | 263 | self.cursor.execute(query, _values) |
paul@1234 | 264 | removed = self.cursor.fetchall() |
paul@1234 | 265 | |
paul@1234 | 266 | query, values = self.get_query( |
paul@1234 | 267 | "delete from %(table)s :condition" % { |
paul@1234 | 268 | "table" : self.table_name |
paul@1234 | 269 | }, |
paul@1234 | 270 | columns, values) |
paul@1234 | 271 | |
paul@1234 | 272 | self.cursor.execute(query, values) |
paul@1234 | 273 | |
paul@1234 | 274 | return map(lambda t: self.make_period(t), removed) |
paul@1234 | 275 | |
paul@1234 | 276 | def remove_affected_period(self, uid, start, participant=None): |
paul@1234 | 277 | |
paul@1234 | 278 | """ |
paul@1234 | 279 | Remove from the collection the period associated with 'uid' that |
paul@1234 | 280 | provides an occurrence starting at the given 'start' (provided by a |
paul@1234 | 281 | recurrence identifier, converted to a datetime). A recurrence identifier |
paul@1234 | 282 | is used to provide an alternative time period whilst also acting as a |
paul@1234 | 283 | reference to the originally-defined occurrence. |
paul@1234 | 284 | |
paul@1234 | 285 | If 'participant' is specified, only remove periods for which the |
paul@1234 | 286 | participant is given as attending. |
paul@1234 | 287 | |
paul@1234 | 288 | Return any removed period in a list. |
paul@1234 | 289 | """ |
paul@1234 | 290 | |
paul@1234 | 291 | self._check_mutable() |
paul@1234 | 292 | |
paul@1234 | 293 | start = format_datetime(start) |
paul@1234 | 294 | |
paul@1234 | 295 | columns, values = ["object_uid", "start", "object_recurrenceid is null"], [uid, start] |
paul@1234 | 296 | |
paul@1234 | 297 | if participant: |
paul@1234 | 298 | columns.append("attendee") |
paul@1234 | 299 | values.append(participant) |
paul@1234 | 300 | |
paul@1234 | 301 | query, _values = self.get_query( |
paul@1234 | 302 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 303 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 304 | "table" : self.table_name |
paul@1234 | 305 | }, |
paul@1234 | 306 | columns, values) |
paul@1234 | 307 | |
paul@1234 | 308 | self.cursor.execute(query, _values) |
paul@1234 | 309 | removed = self.cursor.fetchall() |
paul@1234 | 310 | |
paul@1234 | 311 | query, values = self.get_query( |
paul@1234 | 312 | "delete from %(table)s :condition" % { |
paul@1234 | 313 | "table" : self.table_name |
paul@1234 | 314 | }, |
paul@1234 | 315 | columns, values) |
paul@1234 | 316 | |
paul@1234 | 317 | self.cursor.execute(query, values) |
paul@1234 | 318 | |
paul@1234 | 319 | return map(lambda t: self.make_period(t), removed) |
paul@1234 | 320 | |
paul@1234 | 321 | def periods_from(self, period): |
paul@1234 | 322 | |
paul@1234 | 323 | "Return the entries in the collection at or after 'period'." |
paul@1234 | 324 | |
paul@1234 | 325 | start = format_datetime(period.get_start_point()) |
paul@1234 | 326 | |
paul@1234 | 327 | columns, values = [], [] |
paul@1234 | 328 | |
paul@1234 | 329 | if start: |
paul@1234 | 330 | columns.append("start >= ?") |
paul@1234 | 331 | values.append(start) |
paul@1234 | 332 | |
paul@1234 | 333 | query, values = self.get_query( |
paul@1234 | 334 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 335 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 336 | "table" : self.table_name |
paul@1234 | 337 | }, |
paul@1234 | 338 | columns, values) |
paul@1234 | 339 | |
paul@1234 | 340 | self.cursor.execute(query, values) |
paul@1234 | 341 | |
paul@1234 | 342 | return map(lambda t: self.make_period(t), self.cursor.fetchall()) |
paul@1234 | 343 | |
paul@1234 | 344 | def periods_until(self, period): |
paul@1234 | 345 | |
paul@1234 | 346 | "Return the entries in the collection before 'period'." |
paul@1234 | 347 | |
paul@1234 | 348 | end = format_datetime(period.get_end_point()) |
paul@1234 | 349 | |
paul@1234 | 350 | columns, values = [], [] |
paul@1234 | 351 | |
paul@1234 | 352 | if end: |
paul@1234 | 353 | columns.append("start < ?") |
paul@1234 | 354 | values.append(end) |
paul@1234 | 355 | |
paul@1234 | 356 | query, values = self.get_query( |
paul@1234 | 357 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 358 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 359 | "table" : self.table_name |
paul@1234 | 360 | }, |
paul@1234 | 361 | columns, values) |
paul@1234 | 362 | |
paul@1234 | 363 | self.cursor.execute(query, values) |
paul@1234 | 364 | |
paul@1234 | 365 | return map(lambda t: self.make_period(t), self.cursor.fetchall()) |
paul@1234 | 366 | |
paul@1234 | 367 | def get_overlapping(self, periods): |
paul@1234 | 368 | |
paul@1234 | 369 | """ |
paul@1234 | 370 | Return the entries in the collection providing periods overlapping with |
paul@1234 | 371 | the given sorted collection of 'periods'. |
paul@1234 | 372 | """ |
paul@1234 | 373 | |
paul@1234 | 374 | overlapping = set() |
paul@1234 | 375 | |
paul@1234 | 376 | for period in periods: |
paul@1234 | 377 | columns, values = self._get_period_values(period) |
paul@1234 | 378 | |
paul@1234 | 379 | query, values = self.get_query( |
paul@1234 | 380 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 381 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 382 | "table" : self.table_name |
paul@1234 | 383 | }, |
paul@1234 | 384 | columns, values) |
paul@1234 | 385 | |
paul@1234 | 386 | self.cursor.execute(query, values) |
paul@1234 | 387 | |
paul@1234 | 388 | overlapping.update(map(lambda t: self.make_period(t), self.cursor.fetchall())) |
paul@1234 | 389 | |
paul@1234 | 390 | overlapping = list(overlapping) |
paul@1234 | 391 | overlapping.sort() |
paul@1234 | 392 | return overlapping |
paul@1234 | 393 | |
paul@1234 | 394 | def remove_overlapping(self, period): |
paul@1234 | 395 | |
paul@1234 | 396 | "Remove all periods overlapping with 'period' from the collection." |
paul@1234 | 397 | |
paul@1234 | 398 | self._check_mutable() |
paul@1234 | 399 | |
paul@1234 | 400 | columns, values = self._get_period_values(period) |
paul@1234 | 401 | |
paul@1234 | 402 | query, values = self.get_query( |
paul@1234 | 403 | "delete from %(table)s :condition" % { |
paul@1234 | 404 | "table" : self.table_name |
paul@1234 | 405 | }, |
paul@1234 | 406 | columns, values) |
paul@1234 | 407 | |
paul@1234 | 408 | self.cursor.execute(query, values) |
paul@1234 | 409 | |
paul@1234 | 410 | def _get_period_values(self, period): |
paul@1234 | 411 | |
paul@1234 | 412 | start = format_datetime(period.get_start_point()) |
paul@1234 | 413 | end = format_datetime(period.get_end_point()) |
paul@1234 | 414 | |
paul@1234 | 415 | columns, values = [], [] |
paul@1234 | 416 | |
paul@1234 | 417 | if end: |
paul@1234 | 418 | columns.append("start < ?") |
paul@1234 | 419 | values.append(end) |
paul@1234 | 420 | if start: |
paul@1234 | 421 | columns.append("end > ?") |
paul@1234 | 422 | values.append(start) |
paul@1234 | 423 | |
paul@1234 | 424 | return columns, values |
paul@1234 | 425 | |
paul@1234 | 426 | class FreeBusyGroupDatabaseCollection(SupportAttendee, FreeBusyDatabaseCollection): |
paul@1234 | 427 | |
paul@1234 | 428 | "A collection of quota group free/busy objects." |
paul@1234 | 429 | |
paul@1234 | 430 | def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None): |
paul@1234 | 431 | |
paul@1234 | 432 | """ |
paul@1234 | 433 | Remove from the collection all periods associated with 'uid' and |
paul@1234 | 434 | 'recurrenceid' (which if omitted causes the "parent" object's periods to |
paul@1234 | 435 | be referenced) and any 'attendee'. |
paul@1234 | 436 | |
paul@1234 | 437 | Return the removed periods. |
paul@1234 | 438 | """ |
paul@1234 | 439 | |
paul@1234 | 440 | self._check_mutable() |
paul@1234 | 441 | |
paul@1234 | 442 | columns, values = ["object_uid"], [uid] |
paul@1234 | 443 | |
paul@1234 | 444 | if recurrenceid: |
paul@1234 | 445 | columns.append("object_recurrenceid") |
paul@1234 | 446 | values.append(recurrenceid) |
paul@1234 | 447 | else: |
paul@1234 | 448 | columns.append("object_recurrenceid is null") |
paul@1234 | 449 | |
paul@1234 | 450 | if attendee: |
paul@1234 | 451 | columns.append("attendee") |
paul@1234 | 452 | values.append(attendee) |
paul@1234 | 453 | else: |
paul@1234 | 454 | columns.append("attendee is null") |
paul@1234 | 455 | |
paul@1234 | 456 | query, _values = self.get_query( |
paul@1234 | 457 | "select %(columns)s from %(table)s :condition" % { |
paul@1234 | 458 | "columns" : self.columnlist(self.period_columns), |
paul@1234 | 459 | "table" : self.table_name |
paul@1234 | 460 | }, |
paul@1234 | 461 | columns, values) |
paul@1234 | 462 | |
paul@1234 | 463 | self.cursor.execute(query, _values) |
paul@1234 | 464 | removed = self.cursor.fetchall() |
paul@1234 | 465 | |
paul@1234 | 466 | query, values = self.get_query( |
paul@1234 | 467 | "delete from %(table)s :condition" % { |
paul@1234 | 468 | "table" : self.table_name |
paul@1234 | 469 | }, |
paul@1234 | 470 | columns, values) |
paul@1234 | 471 | |
paul@1234 | 472 | self.cursor.execute(query, values) |
paul@1234 | 473 | |
paul@1234 | 474 | return map(lambda t: self.make_period(t), removed) |
paul@1234 | 475 | |
paul@1234 | 476 | class FreeBusyOffersDatabaseCollection(SupportExpires, FreeBusyDatabaseCollection): |
paul@1234 | 477 | |
paul@1234 | 478 | "A collection of offered free/busy objects." |
paul@1234 | 479 | |
paul@1234 | 480 | pass |
paul@1234 | 481 | |
paul@1234 | 482 | # vim: tabstop=4 expandtab shiftwidth=4 |