imip-agent

imip_store.py

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