imip-agent

Annotated imip_store.py

334:d44eb4aa42ac
2015-02-12 Paul Boddie Added initial support for receiving and accessing recurring event instances. recurring-events
paul@2 1
#!/usr/bin/env python
paul@2 2
paul@146 3
"""
paul@146 4
A simple filesystem-based store of calendar data.
paul@146 5
paul@146 6
Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>
paul@146 7
paul@146 8
This program is free software; you can redistribute it and/or modify it under
paul@146 9
the terms of the GNU General Public License as published by the Free Software
paul@146 10
Foundation; either version 3 of the License, or (at your option) any later
paul@146 11
version.
paul@146 12
paul@146 13
This program is distributed in the hope that it will be useful, but WITHOUT
paul@146 14
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
paul@146 15
FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
paul@146 16
details.
paul@146 17
paul@146 18
You should have received a copy of the GNU General Public License along with
paul@146 19
this program.  If not, see <http://www.gnu.org/licenses/>.
paul@146 20
"""
paul@146 21
paul@30 22
from datetime import datetime
paul@68 23
from imiptools.config import STORE_DIR, PUBLISH_DIR
paul@301 24
from imiptools.data import make_calendar, parse_object, to_stream
paul@147 25
from imiptools.filesys import fix_permissions, FileBase
paul@147 26
from os.path import exists, isfile, join
paul@234 27
from os import listdir, remove
paul@303 28
from time import sleep
paul@15 29
paul@50 30
class FileStore(FileBase):
paul@50 31
paul@50 32
    "A file store of tabular free/busy data and objects."
paul@50 33
paul@147 34
    def __init__(self, store_dir=STORE_DIR):
paul@147 35
        FileBase.__init__(self, store_dir)
paul@147 36
paul@303 37
    def acquire_lock(self, user, timeout=None):
paul@303 38
        FileBase.acquire_lock(self, timeout, user)
paul@303 39
paul@303 40
    def release_lock(self, user):
paul@303 41
        FileBase.release_lock(self, user)
paul@303 42
paul@329 43
    def _get_object(self, user, filename):
paul@329 44
paul@329 45
        """
paul@329 46
        Return the parsed object for the given 'user' having the given
paul@329 47
        'filename'.
paul@329 48
        """
paul@329 49
paul@329 50
        self.acquire_lock(user)
paul@329 51
        try:
paul@329 52
            f = open(filename, "rb")
paul@329 53
            try:
paul@329 54
                return parse_object(f, "utf-8")
paul@329 55
            finally:
paul@329 56
                f.close()
paul@329 57
        finally:
paul@329 58
            self.release_lock(user)
paul@329 59
paul@329 60
    def _set_object(self, user, filename, node):
paul@329 61
paul@329 62
        """
paul@329 63
        Set an object for the given 'user' having the given 'filename', using
paul@329 64
        'node' to define the object.
paul@329 65
        """
paul@329 66
paul@329 67
        self.acquire_lock(user)
paul@329 68
        try:
paul@329 69
            f = open(filename, "wb")
paul@329 70
            try:
paul@329 71
                to_stream(f, node)
paul@329 72
            finally:
paul@329 73
                f.close()
paul@329 74
                fix_permissions(filename)
paul@329 75
        finally:
paul@329 76
            self.release_lock(user)
paul@329 77
paul@329 78
        return True
paul@329 79
paul@329 80
    def _remove_object(self, filename):
paul@329 81
paul@329 82
        "Remove the object with the given 'filename'."
paul@329 83
paul@329 84
        try:
paul@329 85
            remove(filename)
paul@329 86
        except OSError:
paul@329 87
            return False
paul@329 88
paul@329 89
        return True
paul@329 90
paul@119 91
    def get_events(self, user):
paul@119 92
paul@119 93
        "Return a list of event identifiers."
paul@119 94
paul@138 95
        filename = self.get_object_in_store(user, "objects")
paul@119 96
        if not filename or not exists(filename):
paul@119 97
            return None
paul@119 98
paul@119 99
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@119 100
paul@50 101
    def get_event(self, user, uid):
paul@50 102
paul@50 103
        "Get the event for the given 'user' with the given 'uid'."
paul@50 104
paul@138 105
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 106
        if not filename or not exists(filename):
paul@50 107
            return None
paul@50 108
paul@329 109
        return self._get_object(user, filename)
paul@50 110
paul@50 111
    def set_event(self, user, uid, node):
paul@50 112
paul@50 113
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@50 114
paul@138 115
        filename = self.get_object_in_store(user, "objects", uid)
paul@50 116
        if not filename:
paul@50 117
            return False
paul@50 118
paul@329 119
        return self._set_object(user, filename, node)
paul@15 120
paul@234 121
    def remove_event(self, user, uid):
paul@234 122
paul@234 123
        "Remove an event for 'user' having the given 'uid'."
paul@234 124
paul@234 125
        filename = self.get_object_in_store(user, "objects", uid)
paul@234 126
        if not filename:
paul@234 127
            return False
paul@234 128
paul@329 129
        return self._remove_object(filename)
paul@234 130
paul@334 131
    def get_recurrences(self, user, uid):
paul@334 132
paul@334 133
        """
paul@334 134
        Get additional event instances for an event of the given 'user' with the
paul@334 135
        indicated 'uid'.
paul@334 136
        """
paul@334 137
paul@334 138
        filename = self.get_object_in_store(user, "recurrences", uid)
paul@334 139
        if not filename or not exists(filename):
paul@334 140
            return None
paul@334 141
paul@334 142
        return [name for name in listdir(filename) if isfile(join(filename, name))]
paul@334 143
paul@334 144
    def get_recurrence(self, user, uid, recurrenceid):
paul@334 145
paul@334 146
        """
paul@334 147
        For the event of the given 'user' with the given 'uid', return the
paul@334 148
        specific recurrence indicated by the 'recurrenceid'.
paul@334 149
        """
paul@334 150
paul@334 151
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 152
        if not filename or not exists(filename):
paul@334 153
            return None
paul@334 154
paul@334 155
        return self._get_object(user, filename)
paul@334 156
paul@334 157
    def set_recurrence(self, user, uid, recurrenceid, node):
paul@334 158
paul@334 159
        "Set an event for 'user' having the given 'uid' and 'node'."
paul@334 160
paul@334 161
        filename = self.get_object_in_store(user, "recurrences", uid, recurrenceid)
paul@334 162
        if not filename:
paul@334 163
            return False
paul@334 164
paul@334 165
        return self._set_object(user, filename, node)
paul@334 166
paul@334 167
    def remove_recurrence(self, user, uid, recurrenceid):
paul@334 168
paul@334 169
        "Remove an event for 'user' having the given 'uid'."
paul@334 170
paul@334 171
        filename = self.get_object_in_store(user, "objects", uid)
paul@334 172
        if not filename:
paul@334 173
            return False
paul@334 174
paul@334 175
        return self._remove_object(filename)
paul@334 176
paul@15 177
    def get_freebusy(self, user):
paul@15 178
paul@15 179
        "Get free/busy details for the given 'user'."
paul@15 180
paul@52 181
        filename = self.get_object_in_store(user, "freebusy")
paul@15 182
        if not filename or not exists(filename):
paul@167 183
            return []
paul@112 184
        else:
paul@303 185
            return self._get_freebusy(user, filename)
paul@2 186
paul@112 187
    def get_freebusy_for_other(self, user, other):
paul@112 188
paul@112 189
        "For the given 'user', get free/busy details for the 'other' user."
paul@112 190
paul@112 191
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@167 192
        if not filename or not exists(filename):
paul@167 193
            return []
paul@112 194
        else:
paul@303 195
            return self._get_freebusy(user, filename)
paul@303 196
paul@303 197
    def _get_freebusy(self, user, filename):
paul@112 198
paul@303 199
        "For the given 'user', get the free/busy details from 'filename'."
paul@303 200
paul@303 201
        self.acquire_lock(user)
paul@2 202
        try:
paul@303 203
            f = open(filename)
paul@303 204
            try:
paul@303 205
                l = []
paul@303 206
                for line in f.readlines():
paul@303 207
                    l.append(tuple(line.strip().split("\t")))
paul@303 208
                return l
paul@303 209
            finally:
paul@303 210
                f.close()
paul@2 211
        finally:
paul@303 212
            self.release_lock(user)
paul@2 213
paul@15 214
    def set_freebusy(self, user, freebusy):
paul@15 215
paul@15 216
        "For the given 'user', set 'freebusy' details."
paul@15 217
paul@52 218
        filename = self.get_object_in_store(user, "freebusy")
paul@15 219
        if not filename:
paul@15 220
            return False
paul@15 221
paul@303 222
        self._set_freebusy(user, filename, freebusy)
paul@15 223
        return True
paul@15 224
paul@110 225
    def set_freebusy_for_other(self, user, freebusy, other):
paul@110 226
paul@110 227
        "For the given 'user', set 'freebusy' details for the 'other' user."
paul@110 228
paul@110 229
        filename = self.get_object_in_store(user, "freebusy-other", other)
paul@110 230
        if not filename:
paul@110 231
            return False
paul@110 232
paul@303 233
        self._set_freebusy(user, filename, freebusy)
paul@112 234
        return True
paul@112 235
paul@303 236
    def _set_freebusy(self, user, filename, freebusy):
paul@303 237
paul@303 238
        """
paul@303 239
        For the given 'user', write to the file having the given 'filename' the
paul@303 240
        'freebusy' details.
paul@303 241
        """
paul@303 242
paul@303 243
        self.acquire_lock(user)
paul@110 244
        try:
paul@303 245
            f = open(filename, "w")
paul@303 246
            try:
paul@303 247
                for item in freebusy:
paul@303 248
                    f.write("\t".join([(value or "OPAQUE") for value in item]) + "\n")
paul@303 249
            finally:
paul@303 250
                f.close()
paul@303 251
                fix_permissions(filename)
paul@110 252
        finally:
paul@303 253
            self.release_lock(user)
paul@110 254
paul@142 255
    def _get_requests(self, user, queue):
paul@66 256
paul@142 257
        "Get requests for the given 'user' from the given 'queue'."
paul@66 258
paul@142 259
        filename = self.get_object_in_store(user, queue)
paul@81 260
        if not filename or not exists(filename):
paul@66 261
            return None
paul@66 262
paul@303 263
        self.acquire_lock(user)
paul@66 264
        try:
paul@303 265
            f = open(filename)
paul@303 266
            try:
paul@303 267
                return [line.strip() for line in f.readlines()]
paul@303 268
            finally:
paul@303 269
                f.close()
paul@66 270
        finally:
paul@303 271
            self.release_lock(user)
paul@66 272
paul@142 273
    def get_requests(self, user):
paul@142 274
paul@142 275
        "Get requests for the given 'user'."
paul@142 276
paul@142 277
        return self._get_requests(user, "requests")
paul@142 278
paul@142 279
    def get_cancellations(self, user):
paul@142 280
paul@142 281
        "Get cancellations for the given 'user'."
paul@55 282
paul@142 283
        return self._get_requests(user, "cancellations")
paul@142 284
paul@142 285
    def _set_requests(self, user, requests, queue):
paul@66 286
paul@142 287
        """
paul@142 288
        For the given 'user', set the list of queued 'requests' in the given
paul@142 289
        'queue'.
paul@142 290
        """
paul@142 291
paul@142 292
        filename = self.get_object_in_store(user, queue)
paul@66 293
        if not filename:
paul@66 294
            return False
paul@66 295
paul@303 296
        self.acquire_lock(user)
paul@66 297
        try:
paul@303 298
            f = open(filename, "w")
paul@303 299
            try:
paul@303 300
                for request in requests:
paul@303 301
                    print >>f, request
paul@303 302
            finally:
paul@303 303
                f.close()
paul@303 304
                fix_permissions(filename)
paul@66 305
        finally:
paul@303 306
            self.release_lock(user)
paul@66 307
paul@66 308
        return True
paul@66 309
paul@142 310
    def set_requests(self, user, requests):
paul@142 311
paul@142 312
        "For the given 'user', set the list of queued 'requests'."
paul@142 313
paul@142 314
        return self._set_requests(user, requests, "requests")
paul@142 315
paul@142 316
    def set_cancellations(self, user, cancellations):
paul@66 317
paul@142 318
        "For the given 'user', set the list of queued 'cancellations'."
paul@142 319
paul@142 320
        return self._set_requests(user, cancellations, "cancellations")
paul@55 321
paul@142 322
    def _set_request(self, user, request, queue):
paul@142 323
paul@142 324
        "For the given 'user', set the queued 'request' in the given 'queue'."
paul@142 325
paul@142 326
        filename = self.get_object_in_store(user, queue)
paul@55 327
        if not filename:
paul@55 328
            return False
paul@55 329
paul@303 330
        self.acquire_lock(user)
paul@55 331
        try:
paul@303 332
            f = open(filename, "a")
paul@303 333
            try:
paul@303 334
                print >>f, request
paul@303 335
            finally:
paul@303 336
                f.close()
paul@303 337
                fix_permissions(filename)
paul@55 338
        finally:
paul@303 339
            self.release_lock(user)
paul@55 340
paul@55 341
        return True
paul@55 342
paul@142 343
    def set_request(self, user, request):
paul@142 344
paul@142 345
        "For the given 'user', set the queued 'request'."
paul@142 346
paul@142 347
        return self._set_request(user, request, "requests")
paul@142 348
paul@142 349
    def set_cancellation(self, user, cancellation):
paul@142 350
paul@142 351
        "For the given 'user', set the queued 'cancellation'."
paul@142 352
paul@142 353
        return self._set_request(user, cancellation, "cancellations")
paul@142 354
paul@66 355
    def queue_request(self, user, uid):
paul@66 356
paul@66 357
        "Queue a request for 'user' having the given 'uid'."
paul@66 358
paul@81 359
        requests = self.get_requests(user) or []
paul@66 360
paul@66 361
        if uid not in requests:
paul@66 362
            return self.set_request(user, uid)
paul@66 363
paul@66 364
        return False
paul@66 365
paul@105 366
    def dequeue_request(self, user, uid):
paul@105 367
paul@105 368
        "Dequeue a request for 'user' having the given 'uid'."
paul@105 369
paul@105 370
        requests = self.get_requests(user) or []
paul@105 371
paul@105 372
        try:
paul@105 373
            requests.remove(uid)
paul@105 374
            self.set_requests(user, requests)
paul@105 375
        except ValueError:
paul@105 376
            return False
paul@105 377
        else:
paul@105 378
            return True
paul@105 379
paul@142 380
    def cancel_event(self, user, uid):
paul@142 381
paul@142 382
        "Queue an event for cancellation for 'user' having the given 'uid'."
paul@142 383
paul@142 384
        cancellations = self.get_cancellations(user) or []
paul@142 385
paul@142 386
        if uid not in cancellations:
paul@142 387
            return self.set_cancellation(user, uid)
paul@142 388
paul@142 389
        return False
paul@142 390
paul@30 391
class FilePublisher(FileBase):
paul@30 392
paul@30 393
    "A publisher of objects."
paul@30 394
paul@30 395
    def __init__(self, store_dir=PUBLISH_DIR):
paul@30 396
        FileBase.__init__(self, store_dir)
paul@30 397
paul@30 398
    def set_freebusy(self, user, freebusy):
paul@30 399
paul@30 400
        "For the given 'user', set 'freebusy' details."
paul@30 401
paul@52 402
        filename = self.get_object_in_store(user, "freebusy")
paul@30 403
        if not filename:
paul@30 404
            return False
paul@30 405
paul@30 406
        record = []
paul@30 407
        rwrite = record.append
paul@30 408
paul@30 409
        rwrite(("ORGANIZER", {}, user))
paul@30 410
        rwrite(("UID", {}, user))
paul@30 411
        rwrite(("DTSTAMP", {}, datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")))
paul@30 412
paul@112 413
        for start, end, uid, transp in freebusy:
paul@119 414
            if not transp or transp == "OPAQUE":
paul@112 415
                rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end])))
paul@30 416
paul@30 417
        f = open(filename, "w")
paul@30 418
        try:
paul@30 419
            to_stream(f, make_calendar([("VFREEBUSY", {}, record)], "PUBLISH"))
paul@30 420
        finally:
paul@30 421
            f.close()
paul@103 422
            fix_permissions(filename)
paul@30 423
paul@30 424
        return True
paul@30 425
paul@2 426
# vim: tabstop=4 expandtab shiftwidth=4