imip-agent

Annotated imiptools/freebusy/common.py

1309:644b7e259059
2017-10-14 Paul Boddie Support BCC sending suppression so that routines requesting it can still be used with senders that will not support it, usually because there are no outgoing routing destinations for those senders.
paul@1234 1
#!/usr/bin/env python
paul@1234 2
paul@1234 3
"""
paul@1234 4
Managing free/busy periods.
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 bisect import bisect_left, bisect_right
paul@1234 23
from imiptools.dates import format_datetime
paul@1234 24
from imiptools.period import get_overlapping, Period, PeriodBase
paul@1234 25
paul@1234 26
# Conversion functions.
paul@1234 27
paul@1234 28
def from_string(s, encoding):
paul@1234 29
paul@1234 30
    "Interpret 's' using 'encoding', preserving None."
paul@1234 31
paul@1234 32
    if s:
paul@1236 33
        if isinstance(s, unicode):
paul@1236 34
            return s
paul@1236 35
        else:
paul@1236 36
            return unicode(s, encoding)
paul@1234 37
    else:
paul@1234 38
        return s
paul@1234 39
paul@1234 40
def to_string(s, encoding):
paul@1234 41
paul@1234 42
    "Encode 's' using 'encoding', preserving None."
paul@1234 43
paul@1234 44
    if s:
paul@1234 45
        return s.encode(encoding)
paul@1234 46
    else:
paul@1234 47
        return s
paul@1234 48
paul@1236 49
class period_from_tuple:
paul@1236 50
paul@1236 51
    "Convert a tuple to an instance of the given 'period_class'."
paul@1236 52
paul@1236 53
    def __init__(self, period_class):
paul@1236 54
        self.period_class = period_class
paul@1236 55
    def __call__(self, t):
paul@1236 56
        return make_period(t, self.period_class)
paul@1236 57
paul@1236 58
def period_to_tuple(p):
paul@1236 59
paul@1236 60
    "Convert period 'p' to a tuple for serialisation."
paul@1236 61
paul@1236 62
    return p.as_tuple(strings_only=True)
paul@1236 63
paul@1236 64
def make_period(t, period_class):
paul@1236 65
paul@1236 66
    "Convert tuple 't' to an instance of the given 'period_class'."
paul@1236 67
paul@1236 68
    args = []
paul@1236 69
    for arg, column in zip(t, period_class.period_columns):
paul@1236 70
        args.append(from_string(arg, "utf-8"))
paul@1236 71
    return period_class(*args)
paul@1236 72
paul@1236 73
def make_tuple(t, period_class):
paul@1236 74
paul@1236 75
    "Restrict tuple 't' to the columns appropriate for 'period_class'."
paul@1236 76
paul@1236 77
    args = []
paul@1236 78
    for arg, column in zip(t, period_class.period_columns):
paul@1236 79
        args.append(arg)
paul@1236 80
    return tuple(args)
paul@1236 81
paul@1234 82
paul@1234 83
paul@1234 84
# Period abstractions.
paul@1234 85
paul@1234 86
class FreeBusyPeriod(PeriodBase):
paul@1234 87
paul@1234 88
    "A free/busy record abstraction."
paul@1234 89
paul@1236 90
    period_columns = [
paul@1236 91
        "start", "end", "object_uid", "transp", "object_recurrenceid",
paul@1236 92
        "summary", "organiser"
paul@1236 93
        ]
paul@1236 94
paul@1234 95
    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
paul@1234 96
        summary=None, organiser=None):
paul@1234 97
paul@1234 98
        """
paul@1234 99
        Initialise a free/busy period with the given 'start' and 'end' points,
paul@1234 100
        plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
paul@1234 101
        details.
paul@1234 102
        """
paul@1234 103
paul@1234 104
        PeriodBase.__init__(self, start, end)
paul@1236 105
        self.uid = uid or None
paul@1234 106
        self.transp = transp or None
paul@1234 107
        self.recurrenceid = recurrenceid or None
paul@1234 108
        self.summary = summary or None
paul@1234 109
        self.organiser = organiser or None
paul@1234 110
paul@1234 111
    def as_tuple(self, strings_only=False, string_datetimes=False):
paul@1234 112
paul@1234 113
        """
paul@1234 114
        Return the initialisation parameter tuple, converting datetimes and
paul@1234 115
        false value parameters to strings if 'strings_only' is set to a true
paul@1234 116
        value. Otherwise, if 'string_datetimes' is set to a true value, only the
paul@1234 117
        datetime values are converted to strings.
paul@1234 118
        """
paul@1234 119
paul@1234 120
        null = lambda x: (strings_only and [""] or [x])[0]
paul@1234 121
        return (
paul@1234 122
            (strings_only or string_datetimes) and format_datetime(self.get_start_point()) or self.start,
paul@1234 123
            (strings_only or string_datetimes) and format_datetime(self.get_end_point()) or self.end,
paul@1234 124
            self.uid or null(self.uid),
paul@1234 125
            self.transp or strings_only and "OPAQUE" or None,
paul@1234 126
            self.recurrenceid or null(self.recurrenceid),
paul@1234 127
            self.summary or null(self.summary),
paul@1234 128
            self.organiser or null(self.organiser)
paul@1234 129
            )
paul@1234 130
paul@1234 131
    def __cmp__(self, other):
paul@1234 132
paul@1234 133
        """
paul@1234 134
        Compare this object to 'other', employing the uid if the periods
paul@1234 135
        involved are the same.
paul@1234 136
        """
paul@1234 137
paul@1234 138
        result = PeriodBase.__cmp__(self, other)
paul@1234 139
        if result == 0 and isinstance(other, FreeBusyPeriod):
paul@1234 140
            return cmp((self.uid, self.recurrenceid), (other.uid, other.recurrenceid))
paul@1234 141
        else:
paul@1234 142
            return result
paul@1234 143
paul@1234 144
    def get_key(self):
paul@1234 145
        return self.uid, self.recurrenceid, self.get_start()
paul@1234 146
paul@1234 147
    def __repr__(self):
paul@1234 148
        return "FreeBusyPeriod%r" % (self.as_tuple(),)
paul@1234 149
paul@1234 150
    def get_tzid(self):
paul@1234 151
        return "UTC"
paul@1234 152
paul@1234 153
    # Period and event recurrence logic.
paul@1234 154
paul@1234 155
    def is_replaced(self, recurrences):
paul@1234 156
paul@1234 157
        """
paul@1234 158
        Return whether this period refers to one of the 'recurrences'.
paul@1234 159
        The 'recurrences' must be UTC datetimes corresponding to the start of
paul@1234 160
        the period described by a recurrence.
paul@1234 161
        """
paul@1234 162
paul@1234 163
        for recurrence in recurrences:
paul@1234 164
            if self.is_affected(recurrence):
paul@1234 165
                return True
paul@1234 166
        return False
paul@1234 167
paul@1234 168
    def is_affected(self, recurrence):
paul@1234 169
paul@1234 170
        """
paul@1234 171
        Return whether this period refers to 'recurrence'. The 'recurrence' must
paul@1234 172
        be a UTC datetime corresponding to the start of the period described by
paul@1234 173
        a recurrence.
paul@1234 174
        """
paul@1234 175
paul@1234 176
        return recurrence and self.get_start_point() == recurrence
paul@1234 177
paul@1234 178
    # Value correction methods.
paul@1234 179
paul@1234 180
    def make_corrected(self, start, end):
paul@1234 181
        return self.__class__(start, end)
paul@1234 182
paul@1234 183
class FreeBusyOfferPeriod(FreeBusyPeriod):
paul@1234 184
paul@1234 185
    "A free/busy record abstraction for an offer period."
paul@1234 186
paul@1236 187
    period_columns = FreeBusyPeriod.period_columns + ["expires"]
paul@1236 188
paul@1234 189
    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
paul@1234 190
        summary=None, organiser=None, expires=None):
paul@1234 191
paul@1234 192
        """
paul@1234 193
        Initialise a free/busy period with the given 'start' and 'end' points,
paul@1234 194
        plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
paul@1234 195
        details.
paul@1234 196
paul@1234 197
        An additional 'expires' parameter can be used to indicate an expiry
paul@1234 198
        datetime in conjunction with free/busy offers made when countering
paul@1234 199
        event proposals.
paul@1234 200
        """
paul@1234 201
paul@1234 202
        FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
paul@1234 203
            summary, organiser)
paul@1234 204
        self.expires = expires or None
paul@1234 205
paul@1234 206
    def as_tuple(self, strings_only=False, string_datetimes=False):
paul@1234 207
paul@1234 208
        """
paul@1234 209
        Return the initialisation parameter tuple, converting datetimes and
paul@1234 210
        false value parameters to strings if 'strings_only' is set to a true
paul@1234 211
        value. Otherwise, if 'string_datetimes' is set to a true value, only the
paul@1234 212
        datetime values are converted to strings.
paul@1234 213
        """
paul@1234 214
paul@1234 215
        null = lambda x: (strings_only and [""] or [x])[0]
paul@1234 216
        return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
paul@1234 217
            self.expires or null(self.expires),)
paul@1234 218
paul@1234 219
    def __repr__(self):
paul@1234 220
        return "FreeBusyOfferPeriod%r" % (self.as_tuple(),)
paul@1234 221
paul@1234 222
class FreeBusyGroupPeriod(FreeBusyPeriod):
paul@1234 223
paul@1234 224
    "A free/busy record abstraction for a quota group period."
paul@1234 225
paul@1236 226
    period_columns = FreeBusyPeriod.period_columns + ["attendee"]
paul@1236 227
paul@1234 228
    def __init__(self, start, end, uid=None, transp=None, recurrenceid=None,
paul@1234 229
        summary=None, organiser=None, attendee=None):
paul@1234 230
paul@1234 231
        """
paul@1234 232
        Initialise a free/busy period with the given 'start' and 'end' points,
paul@1234 233
        plus any 'uid', 'transp', 'recurrenceid', 'summary' and 'organiser'
paul@1234 234
        details.
paul@1234 235
paul@1234 236
        An additional 'attendee' parameter can be used to indicate the identity
paul@1234 237
        of the attendee recording the period.
paul@1234 238
        """
paul@1234 239
paul@1234 240
        FreeBusyPeriod.__init__(self, start, end, uid, transp, recurrenceid,
paul@1234 241
            summary, organiser)
paul@1234 242
        self.attendee = attendee or None
paul@1234 243
paul@1234 244
    def as_tuple(self, strings_only=False, string_datetimes=False):
paul@1234 245
paul@1234 246
        """
paul@1234 247
        Return the initialisation parameter tuple, converting datetimes and
paul@1234 248
        false value parameters to strings if 'strings_only' is set to a true
paul@1234 249
        value. Otherwise, if 'string_datetimes' is set to a true value, only the
paul@1234 250
        datetime values are converted to strings.
paul@1234 251
        """
paul@1234 252
paul@1234 253
        null = lambda x: (strings_only and [""] or [x])[0]
paul@1234 254
        return FreeBusyPeriod.as_tuple(self, strings_only, string_datetimes) + (
paul@1234 255
            self.attendee or null(self.attendee),)
paul@1234 256
paul@1234 257
    def __cmp__(self, other):
paul@1234 258
paul@1234 259
        """
paul@1234 260
        Compare this object to 'other', employing the uid if the periods
paul@1234 261
        involved are the same.
paul@1234 262
        """
paul@1234 263
paul@1234 264
        result = FreeBusyPeriod.__cmp__(self, other)
paul@1234 265
        if isinstance(other, FreeBusyGroupPeriod) and result == 0:
paul@1234 266
            return cmp(self.attendee, other.attendee)
paul@1234 267
        else:
paul@1234 268
            return result
paul@1234 269
paul@1234 270
    def __repr__(self):
paul@1234 271
        return "FreeBusyGroupPeriod%r" % (self.as_tuple(),)
paul@1234 272
paul@1236 273
paul@1236 274
paul@1236 275
# Collection abstractions.
paul@1236 276
paul@1234 277
class FreeBusyCollectionBase:
paul@1234 278
paul@1234 279
    "Common operations on free/busy period collections."
paul@1234 280
paul@1234 281
    period_class = FreeBusyPeriod
paul@1234 282
paul@1234 283
    def __init__(self, mutable=True):
paul@1234 284
        self.mutable = mutable
paul@1234 285
paul@1234 286
    def _check_mutable(self):
paul@1234 287
        if not self.mutable:
paul@1234 288
            raise TypeError, "Cannot mutate this collection."
paul@1234 289
paul@1236 290
    def close(self):
paul@1236 291
paul@1236 292
        "Close the collection."
paul@1236 293
paul@1236 294
        pass
paul@1236 295
paul@1234 296
    def copy(self):
paul@1234 297
paul@1234 298
        "Make an independent mutable copy of the collection."
paul@1234 299
paul@1234 300
        return FreeBusyCollection(list(self), True)
paul@1234 301
paul@1234 302
    def make_period(self, t):
paul@1234 303
paul@1234 304
        """
paul@1234 305
        Make a period using the given tuple of arguments and the collection's
paul@1234 306
        column details.
paul@1234 307
        """
paul@1234 308
paul@1236 309
        return make_period(t, self.period_class)
paul@1234 310
paul@1234 311
    def make_tuple(self, t):
paul@1234 312
paul@1234 313
        """
paul@1234 314
        Return a tuple from the given tuple 't' conforming to the collection's
paul@1234 315
        column details.
paul@1234 316
        """
paul@1234 317
paul@1236 318
        return make_tuple(t, self.period_class)
paul@1234 319
paul@1234 320
    # List emulation methods.
paul@1234 321
paul@1234 322
    def __iadd__(self, periods):
paul@1234 323
        self.insert_periods(periods)
paul@1234 324
        return self
paul@1234 325
paul@1234 326
    def append(self, period):
paul@1234 327
        self.insert_period(period)
paul@1234 328
paul@1234 329
    # Operations.
paul@1234 330
paul@1236 331
    def insert_period(self, period):
paul@1236 332
paul@1236 333
        """
paul@1236 334
        Insert the given 'period' into the collection.
paul@1236 335
paul@1236 336
        This should be implemented in subclasses.
paul@1236 337
        """
paul@1236 338
paul@1236 339
        pass
paul@1236 340
paul@1234 341
    def insert_periods(self, periods):
paul@1234 342
paul@1234 343
        "Insert the given 'periods' into the collection."
paul@1234 344
paul@1234 345
        for p in periods:
paul@1234 346
            self.insert_period(p)
paul@1234 347
paul@1234 348
    def can_schedule(self, periods, uid, recurrenceid):
paul@1234 349
paul@1234 350
        """
paul@1234 351
        Return whether the collection can accommodate the given 'periods'
paul@1234 352
        employing the specified 'uid' and 'recurrenceid'.
paul@1234 353
        """
paul@1234 354
paul@1234 355
        for conflict in self.have_conflict(periods, True):
paul@1234 356
            if conflict.uid != uid or conflict.recurrenceid != recurrenceid:
paul@1234 357
                return False
paul@1234 358
paul@1234 359
        return True
paul@1234 360
paul@1234 361
    def have_conflict(self, periods, get_conflicts=False):
paul@1234 362
paul@1234 363
        """
paul@1234 364
        Return whether any period in the collection overlaps with the given
paul@1234 365
        'periods', returning a collection of such overlapping periods if
paul@1234 366
        'get_conflicts' is set to a true value.
paul@1234 367
        """
paul@1234 368
paul@1234 369
        conflicts = set()
paul@1234 370
        for p in periods:
paul@1234 371
            overlapping = self.period_overlaps(p, get_conflicts)
paul@1234 372
            if overlapping:
paul@1234 373
                if get_conflicts:
paul@1234 374
                    conflicts.update(overlapping)
paul@1234 375
                else:
paul@1234 376
                    return True
paul@1234 377
paul@1234 378
        if get_conflicts:
paul@1234 379
            return conflicts
paul@1234 380
        else:
paul@1234 381
            return False
paul@1234 382
paul@1234 383
    def period_overlaps(self, period, get_periods=False):
paul@1234 384
paul@1234 385
        """
paul@1234 386
        Return whether any period in the collection overlaps with the given
paul@1234 387
        'period', returning a collection of overlapping periods if 'get_periods'
paul@1234 388
        is set to a true value.
paul@1234 389
        """
paul@1234 390
paul@1234 391
        overlapping = self.get_overlapping([period])
paul@1234 392
paul@1234 393
        if get_periods:
paul@1234 394
            return overlapping
paul@1234 395
        else:
paul@1234 396
            return len(overlapping) != 0
paul@1234 397
paul@1234 398
    def replace_overlapping(self, period, replacements):
paul@1234 399
paul@1234 400
        """
paul@1234 401
        Replace existing periods in the collection within the given 'period',
paul@1234 402
        using the given 'replacements'.
paul@1234 403
        """
paul@1234 404
paul@1234 405
        self._check_mutable()
paul@1234 406
paul@1234 407
        self.remove_overlapping(period)
paul@1234 408
        for replacement in replacements:
paul@1234 409
            self.insert_period(replacement)
paul@1234 410
paul@1234 411
    def coalesce_freebusy(self):
paul@1234 412
paul@1234 413
        "Coalesce the periods in the collection, returning a new collection."
paul@1234 414
paul@1234 415
        if not self:
paul@1234 416
            return FreeBusyCollection()
paul@1234 417
paul@1234 418
        fb = []
paul@1234 419
paul@1234 420
        it = iter(self)
paul@1234 421
        period = it.next()
paul@1234 422
paul@1234 423
        start = period.get_start_point()
paul@1234 424
        end = period.get_end_point()
paul@1234 425
paul@1234 426
        try:
paul@1234 427
            while True:
paul@1234 428
                period = it.next()
paul@1234 429
                if period.get_start_point() > end:
paul@1234 430
                    fb.append(self.period_class(start, end))
paul@1234 431
                    start = period.get_start_point()
paul@1234 432
                    end = period.get_end_point()
paul@1234 433
                else:
paul@1234 434
                    end = max(end, period.get_end_point())
paul@1234 435
        except StopIteration:
paul@1234 436
            pass
paul@1234 437
paul@1234 438
        fb.append(self.period_class(start, end))
paul@1234 439
        return FreeBusyCollection(fb)
paul@1234 440
paul@1234 441
    def invert_freebusy(self):
paul@1234 442
paul@1234 443
        "Return the free periods from the collection as a new collection."
paul@1234 444
paul@1234 445
        if not self:
paul@1234 446
            return FreeBusyCollection([self.period_class(None, None)])
paul@1234 447
paul@1234 448
        # Coalesce periods that overlap or are adjacent.
paul@1234 449
paul@1234 450
        fb = self.coalesce_freebusy()
paul@1234 451
        free = []
paul@1234 452
paul@1234 453
        # Add a start-of-time period if appropriate.
paul@1234 454
paul@1234 455
        first = fb[0].get_start_point()
paul@1234 456
        if first:
paul@1234 457
            free.append(self.period_class(None, first))
paul@1234 458
paul@1234 459
        start = fb[0].get_end_point()
paul@1234 460
paul@1234 461
        for period in fb[1:]:
paul@1234 462
            free.append(self.period_class(start, period.get_start_point()))
paul@1234 463
            start = period.get_end_point()
paul@1234 464
paul@1234 465
        # Add an end-of-time period if appropriate.
paul@1234 466
paul@1234 467
        if start:
paul@1234 468
            free.append(self.period_class(start, None))
paul@1234 469
paul@1234 470
        return FreeBusyCollection(free)
paul@1234 471
paul@1234 472
    def _update_freebusy(self, periods, uid, recurrenceid):
paul@1234 473
paul@1234 474
        """
paul@1234 475
        Update the free/busy details with the given 'periods', using the given
paul@1234 476
        'uid' plus 'recurrenceid' to remove existing periods.
paul@1234 477
        """
paul@1234 478
paul@1234 479
        self._check_mutable()
paul@1234 480
paul@1234 481
        self.remove_specific_event_periods(uid, recurrenceid)
paul@1234 482
paul@1234 483
        self.insert_periods(periods)
paul@1234 484
paul@1234 485
    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser):
paul@1234 486
paul@1234 487
        """
paul@1234 488
        Update the free/busy details with the given 'periods', 'transp' setting,
paul@1234 489
        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
paul@1234 490
        """
paul@1234 491
paul@1234 492
        new_periods = []
paul@1234 493
paul@1234 494
        for p in periods:
paul@1234 495
            new_periods.append(
paul@1234 496
                self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser)
paul@1234 497
                )
paul@1234 498
paul@1234 499
        self._update_freebusy(new_periods, uid, recurrenceid)
paul@1234 500
paul@1234 501
class SupportAttendee:
paul@1234 502
paul@1234 503
    "A mix-in that supports the affected attendee in free/busy periods."
paul@1234 504
paul@1234 505
    period_class = FreeBusyGroupPeriod
paul@1234 506
paul@1234 507
    def _update_freebusy(self, periods, uid, recurrenceid, attendee=None):
paul@1234 508
paul@1234 509
        """
paul@1234 510
        Update the free/busy details with the given 'periods', using the given
paul@1234 511
        'uid' plus 'recurrenceid' and 'attendee' to remove existing periods.
paul@1234 512
        """
paul@1234 513
paul@1234 514
        self._check_mutable()
paul@1234 515
paul@1234 516
        self.remove_specific_event_periods(uid, recurrenceid, attendee)
paul@1234 517
paul@1234 518
        self.insert_periods(periods)
paul@1234 519
paul@1234 520
    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, attendee=None):
paul@1234 521
paul@1234 522
        """
paul@1234 523
        Update the free/busy details with the given 'periods', 'transp' setting,
paul@1234 524
        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
paul@1234 525
paul@1234 526
        An optional 'attendee' indicates the attendee affected by the period.
paul@1234 527
        """
paul@1234 528
paul@1234 529
        new_periods = []
paul@1234 530
paul@1234 531
        for p in periods:
paul@1234 532
            new_periods.append(
paul@1234 533
                self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, attendee)
paul@1234 534
                )
paul@1234 535
paul@1234 536
        self._update_freebusy(new_periods, uid, recurrenceid, attendee)
paul@1234 537
paul@1234 538
class SupportExpires:
paul@1234 539
paul@1234 540
    "A mix-in that supports the expiry datetime in free/busy periods."
paul@1234 541
paul@1234 542
    period_class = FreeBusyOfferPeriod
paul@1234 543
paul@1234 544
    def update_freebusy(self, periods, transp, uid, recurrenceid, summary, organiser, expires=None):
paul@1234 545
paul@1234 546
        """
paul@1234 547
        Update the free/busy details with the given 'periods', 'transp' setting,
paul@1234 548
        'uid' plus 'recurrenceid' and 'summary' and 'organiser' details.
paul@1234 549
paul@1234 550
        An optional 'expires' datetime string indicates the expiry time of any
paul@1234 551
        free/busy offer.
paul@1234 552
        """
paul@1234 553
paul@1234 554
        new_periods = []
paul@1234 555
paul@1234 556
        for p in periods:
paul@1234 557
            new_periods.append(
paul@1234 558
                self.period_class(p.get_start_point(), p.get_end_point(), uid, transp, recurrenceid, summary, organiser, expires)
paul@1234 559
                )
paul@1234 560
paul@1234 561
        self._update_freebusy(new_periods, uid, recurrenceid)
paul@1234 562
paul@1234 563
paul@1234 564
paul@1234 565
# Simple abstractions suitable for use with file-based representations and as
paul@1234 566
# general copies of collections.
paul@1234 567
paul@1234 568
class FreeBusyCollection(FreeBusyCollectionBase):
paul@1234 569
paul@1234 570
    "An abstraction for a collection of free/busy periods."
paul@1234 571
paul@1234 572
    def __init__(self, periods=None, mutable=True):
paul@1234 573
paul@1234 574
        """
paul@1234 575
        Initialise the collection with the given list of 'periods', or start an
paul@1234 576
        empty collection if no list is given. If 'mutable' is indicated, the
paul@1234 577
        collection may be changed; otherwise, an exception will be raised.
paul@1234 578
        """
paul@1234 579
paul@1234 580
        FreeBusyCollectionBase.__init__(self, mutable)
paul@1236 581
paul@1236 582
        if periods is not None:
paul@1236 583
            self.periods = periods
paul@1236 584
        else:
paul@1236 585
            self.periods = []
paul@1236 586
paul@1236 587
    def get_filename(self):
paul@1236 588
paul@1236 589
        "Return any filename for the periods collection."
paul@1236 590
paul@1236 591
        if hasattr(self.periods, "filename"):
paul@1236 592
            return self.periods.filename
paul@1236 593
        else:
paul@1236 594
            return None
paul@1236 595
paul@1236 596
    def close(self):
paul@1236 597
paul@1236 598
        "Close the collection."
paul@1236 599
paul@1236 600
        if hasattr(self.periods, "close"):
paul@1236 601
            self.periods.close()
paul@1234 602
paul@1234 603
    # List emulation methods.
paul@1234 604
paul@1234 605
    def __nonzero__(self):
paul@1234 606
        return bool(self.periods)
paul@1234 607
paul@1234 608
    def __iter__(self):
paul@1234 609
        return iter(self.periods)
paul@1234 610
paul@1234 611
    def __len__(self):
paul@1234 612
        return len(self.periods)
paul@1234 613
paul@1234 614
    def __getitem__(self, i):
paul@1234 615
        return self.periods[i]
paul@1234 616
paul@1236 617
    # Dictionary emulation methods (even though this is not a mapping).
paul@1236 618
paul@1236 619
    def clear(self):
paul@1236 620
        del self.periods[:]
paul@1236 621
paul@1234 622
    # Operations.
paul@1234 623
paul@1234 624
    def insert_period(self, period):
paul@1234 625
paul@1234 626
        "Insert the given 'period' into the collection."
paul@1234 627
paul@1234 628
        self._check_mutable()
paul@1234 629
paul@1234 630
        i = bisect_left(self.periods, period)
paul@1234 631
        if i == len(self.periods):
paul@1234 632
            self.periods.append(period)
paul@1234 633
        elif self.periods[i] != period:
paul@1234 634
            self.periods.insert(i, period)
paul@1234 635
paul@1234 636
    def remove_periods(self, periods):
paul@1234 637
paul@1234 638
        "Remove the given 'periods' from the collection."
paul@1234 639
paul@1234 640
        self._check_mutable()
paul@1234 641
paul@1234 642
        for period in periods:
paul@1234 643
            i = bisect_left(self.periods, period)
paul@1234 644
            if i < len(self.periods) and self.periods[i] == period:
paul@1234 645
                del self.periods[i]
paul@1234 646
paul@1243 647
    def remove_periods_before(self, period):
paul@1243 648
paul@1243 649
        "Remove the entries in the collection before 'period'."
paul@1243 650
paul@1243 651
        last = bisect_right(self.periods, period)
paul@1243 652
        self.remove_periods(self.periods[:last])
paul@1243 653
paul@1234 654
    def remove_event_periods(self, uid, recurrenceid=None, participant=None):
paul@1234 655
paul@1234 656
        """
paul@1234 657
        Remove from the collection all periods associated with 'uid' and
paul@1234 658
        'recurrenceid' (which if omitted causes the "parent" object's periods to
paul@1234 659
        be referenced).
paul@1234 660
paul@1234 661
        If 'participant' is specified, only remove periods for which the
paul@1234 662
        participant is given as attending.
paul@1234 663
paul@1234 664
        Return the removed periods.
paul@1234 665
        """
paul@1234 666
paul@1234 667
        self._check_mutable()
paul@1234 668
paul@1234 669
        removed = []
paul@1234 670
        i = 0
paul@1234 671
        while i < len(self.periods):
paul@1234 672
            fb = self.periods[i]
paul@1234 673
paul@1234 674
            if fb.uid == uid and fb.recurrenceid == recurrenceid and \
paul@1234 675
               (not participant or participant == fb.attendee):
paul@1234 676
paul@1234 677
                removed.append(self.periods[i])
paul@1234 678
                del self.periods[i]
paul@1234 679
            else:
paul@1234 680
                i += 1
paul@1234 681
paul@1234 682
        return removed
paul@1234 683
paul@1234 684
    # Specific period removal when updating event details.
paul@1234 685
paul@1234 686
    remove_specific_event_periods = remove_event_periods
paul@1234 687
paul@1234 688
    def remove_additional_periods(self, uid, recurrenceids=None):
paul@1234 689
paul@1234 690
        """
paul@1234 691
        Remove from the collection all periods associated with 'uid' having a
paul@1234 692
        recurrence identifier indicating an additional or modified period.
paul@1234 693
paul@1234 694
        If 'recurrenceids' is specified, remove all periods associated with
paul@1234 695
        'uid' that do not have a recurrence identifier in the given list.
paul@1234 696
paul@1234 697
        Return the removed periods.
paul@1234 698
        """
paul@1234 699
paul@1234 700
        self._check_mutable()
paul@1234 701
paul@1234 702
        removed = []
paul@1234 703
        i = 0
paul@1234 704
        while i < len(self.periods):
paul@1234 705
            fb = self.periods[i]
paul@1234 706
            if fb.uid == uid and fb.recurrenceid and (
paul@1234 707
                recurrenceids is None or
paul@1234 708
                recurrenceids is not None and fb.recurrenceid not in recurrenceids
paul@1234 709
                ):
paul@1234 710
                removed.append(self.periods[i])
paul@1234 711
                del self.periods[i]
paul@1234 712
            else:
paul@1234 713
                i += 1
paul@1234 714
paul@1234 715
        return removed
paul@1234 716
paul@1234 717
    def remove_affected_period(self, uid, start, participant=None):
paul@1234 718
paul@1234 719
        """
paul@1234 720
        Remove from the collection the period associated with 'uid' that
paul@1234 721
        provides an occurrence starting at the given 'start' (provided by a
paul@1234 722
        recurrence identifier, converted to a datetime). A recurrence identifier
paul@1234 723
        is used to provide an alternative time period whilst also acting as a
paul@1234 724
        reference to the originally-defined occurrence.
paul@1234 725
paul@1234 726
        If 'participant' is specified, only remove periods for which the
paul@1234 727
        participant is given as attending.
paul@1234 728
paul@1234 729
        Return any removed period in a list.
paul@1234 730
        """
paul@1234 731
paul@1234 732
        self._check_mutable()
paul@1234 733
paul@1234 734
        removed = []
paul@1234 735
paul@1234 736
        search = Period(start, start)
paul@1234 737
        found = bisect_left(self.periods, search)
paul@1234 738
paul@1234 739
        while found < len(self.periods):
paul@1234 740
            fb = self.periods[found]
paul@1234 741
paul@1234 742
            # Stop looking if the start no longer matches the recurrence identifier.
paul@1234 743
paul@1234 744
            if fb.get_start_point() != search.get_start_point():
paul@1234 745
                break
paul@1234 746
paul@1234 747
            # If the period belongs to the parent object, remove it and return.
paul@1234 748
paul@1234 749
            if not fb.recurrenceid and uid == fb.uid and \
paul@1234 750
               (not participant or participant == fb.attendee):
paul@1234 751
paul@1234 752
                removed.append(self.periods[found])
paul@1234 753
                del self.periods[found]
paul@1234 754
                break
paul@1234 755
paul@1234 756
            # Otherwise, keep looking for a matching period.
paul@1234 757
paul@1234 758
            found += 1
paul@1234 759
paul@1234 760
        return removed
paul@1234 761
paul@1234 762
    def periods_from(self, period):
paul@1234 763
paul@1234 764
        "Return the entries in the collection at or after 'period'."
paul@1234 765
paul@1234 766
        first = bisect_left(self.periods, period)
paul@1234 767
        return self.periods[first:]
paul@1234 768
paul@1234 769
    def periods_until(self, period):
paul@1234 770
paul@1234 771
        "Return the entries in the collection before 'period'."
paul@1234 772
paul@1234 773
        last = bisect_right(self.periods, Period(period.get_end(), period.get_end(), period.get_tzid()))
paul@1234 774
        return self.periods[:last]
paul@1234 775
paul@1234 776
    def get_overlapping(self, periods):
paul@1234 777
paul@1234 778
        """
paul@1234 779
        Return the entries in the collection providing periods overlapping with
paul@1234 780
        the given sorted collection of 'periods'.
paul@1234 781
        """
paul@1234 782
paul@1234 783
        return get_overlapping(self.periods, periods)
paul@1234 784
paul@1234 785
    def remove_overlapping(self, period):
paul@1234 786
paul@1234 787
        "Remove all periods overlapping with 'period' from the collection."
paul@1234 788
paul@1234 789
        self._check_mutable()
paul@1234 790
paul@1234 791
        overlapping = self.get_overlapping([period])
paul@1234 792
paul@1234 793
        if overlapping:
paul@1234 794
            for fb in overlapping:
paul@1234 795
                self.periods.remove(fb)
paul@1234 796
paul@1234 797
class FreeBusyGroupCollection(SupportAttendee, FreeBusyCollection):
paul@1234 798
paul@1234 799
    "A collection of quota group free/busy objects."
paul@1234 800
paul@1234 801
    def remove_specific_event_periods(self, uid, recurrenceid=None, attendee=None):
paul@1234 802
paul@1234 803
        """
paul@1234 804
        Remove from the collection all periods associated with 'uid' and
paul@1234 805
        'recurrenceid' (which if omitted causes the "parent" object's periods to
paul@1234 806
        be referenced) and any 'attendee'.
paul@1234 807
paul@1234 808
        Return the removed periods.
paul@1234 809
        """
paul@1234 810
paul@1234 811
        self._check_mutable()
paul@1234 812
paul@1234 813
        removed = []
paul@1234 814
        i = 0
paul@1234 815
        while i < len(self.periods):
paul@1234 816
            fb = self.periods[i]
paul@1234 817
            if fb.uid == uid and fb.recurrenceid == recurrenceid and fb.attendee == attendee:
paul@1234 818
                removed.append(self.periods[i])
paul@1234 819
                del self.periods[i]
paul@1234 820
            else:
paul@1234 821
                i += 1
paul@1234 822
paul@1234 823
        return removed
paul@1234 824
paul@1234 825
class FreeBusyOffersCollection(SupportExpires, FreeBusyCollection):
paul@1234 826
paul@1234 827
    "A collection of offered free/busy objects."
paul@1234 828
paul@1234 829
    pass
paul@1234 830
paul@1234 831
# vim: tabstop=4 expandtab shiftwidth=4