imip-agent

imiptools/data.py

316:a6d9559df15c
2015-02-10 Paul Boddie Fixed missing import after code was previously moved.
     1 #!/usr/bin/env python     2      3 """     4 Interpretation of vCalendar content.     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, timedelta    23 from email.mime.text import MIMEText    24 from imiptools.dates import format_datetime, get_datetime, get_freebusy_period, \    25                             to_utc_datetime    26 from pytz import timezone    27 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node    28 from vRecurrence import get_parameters, get_rule    29 import email.utils    30     31 try:    32     from cStringIO import StringIO    33 except ImportError:    34     from StringIO import StringIO    35     36 class Object:    37     38     "Access to calendar structures."    39     40     def __init__(self, fragment):    41         self.objtype, (self.details, self.attr) = fragment.items()[0]    42     43     def get_items(self, name, all=True):    44         return get_items(self.details, name, all)    45     46     def get_item(self, name):    47         return get_item(self.details, name)    48     49     def get_value_map(self, name):    50         return get_value_map(self.details, name)    51     52     def get_values(self, name, all=True):    53         return get_values(self.details, name, all)    54     55     def get_value(self, name):    56         return get_value(self.details, name)    57     58     def get_utc_datetime(self, name):    59         return get_utc_datetime(self.details, name)    60     61     def get_datetime_item(self, name):    62         return get_datetime_item(self.details, name)    63     64     def to_node(self):    65         return to_node({self.objtype : [(self.details, self.attr)]})    66     67     def to_part(self, method):    68         return to_part(method, [self.to_node()])    69     70     # Direct access to the structure.    71     72     def __getitem__(self, name):    73         return self.details[name]    74     75     def __setitem__(self, name, value):    76         self.details[name] = value    77     78     def __delitem__(self, name):    79         del self.details[name]    80     81     # Computed results.    82     83     def get_periods(self, window_size=100):    84         return get_periods(self, window_size)    85     86     def get_periods_for_freebusy(self, tzid, window_size=100):    87         periods = self.get_periods(window_size)    88         return get_periods_for_freebusy(self, periods, tzid)    89     90 # Construction and serialisation.    91     92 def make_calendar(nodes, method=None):    93     94     """    95     Return a complete calendar node wrapping the given 'nodes' and employing the    96     given 'method', if indicated.    97     """    98     99     return ("VCALENDAR", {},   100             (method and [("METHOD", {}, method)] or []) +   101             [("VERSION", {}, "2.0")] +   102             nodes   103            )   104    105 def make_freebusy(freebusy, uid, organiser, organiser_attr=None, attendee=None, attendee_attr=None):   106        107     """   108     Return a calendar node defining the free/busy details described in the given   109     'freebusy' list, employing the given 'uid', for the given 'organiser' and   110     optional 'organiser_attr', with the optional 'attendee' providing recipient   111     details together with the optional 'attendee_attr'.   112     """   113        114     record = []   115     rwrite = record.append   116        117     rwrite(("ORGANIZER", organiser_attr or {}, organiser))   118    119     if attendee:   120         rwrite(("ATTENDEE", attendee_attr or {}, attendee))    121    122     rwrite(("UID", {}, uid))   123    124     if freebusy:   125         for start, end, uid, transp in freebusy:   126             if transp == "OPAQUE":   127                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join([start, end])))   128    129     return ("VFREEBUSY", {}, record)   130    131 def parse_object(f, encoding, objtype=None):   132    133     """   134     Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is   135     given, only objects of that type will be returned. Otherwise, the root of   136     the content will be returned as a dictionary with a single key indicating   137     the object type.   138    139     Return None if the content was not readable or suitable.   140     """   141    142     try:   143         try:   144             doctype, attrs, elements = obj = parse(f, encoding=encoding)   145             if objtype and doctype == objtype:   146                 return to_dict(obj)[objtype][0]   147             elif not objtype:   148                 return to_dict(obj)   149         finally:   150             f.close()   151    152     # NOTE: Handle parse errors properly.   153    154     except (ParseError, ValueError):   155         pass   156    157     return None   158    159 def to_part(method, calendar):   160    161     """   162     Write using the given 'method', the 'calendar' details to a MIME   163     text/calendar part.   164     """   165    166     encoding = "utf-8"   167     out = StringIO()   168     try:   169         to_stream(out, make_calendar(calendar, method), encoding)   170         part = MIMEText(out.getvalue(), "calendar", encoding)   171         part.set_param("method", method)   172         return part   173    174     finally:   175         out.close()   176    177 def to_stream(out, fragment, encoding="utf-8"):   178     iterwrite(out, encoding=encoding).append(fragment)   179    180 # Structure access functions.   181    182 def get_items(d, name, all=True):   183    184     """   185     Get all items from 'd' for the given 'name', returning single items if   186     'all' is specified and set to a false value and if only one value is   187     present for the name. Return None if no items are found for the name or if   188     many items are found but 'all' is set to a false value.   189     """   190    191     if d.has_key(name):   192         values = d[name]   193         if all:   194             return values   195         elif len(values) == 1:   196             return values[0]   197         else:   198             return None   199     else:   200         return None   201    202 def get_item(d, name):   203     return get_items(d, name, False)   204    205 def get_value_map(d, name):   206    207     """   208     Return a dictionary for all items in 'd' having the given 'name'. The   209     dictionary will map values for the name to any attributes or qualifiers   210     that may have been present.   211     """   212    213     items = get_items(d, name)   214     if items:   215         return dict(items)   216     else:   217         return {}   218    219 def get_values(d, name, all=True):   220     if d.has_key(name):   221         values = d[name]   222         if not all and len(values) == 1:   223             return values[0][0]   224         else:   225             return map(lambda x: x[0], values)   226     else:   227         return None   228    229 def get_value(d, name):   230     return get_values(d, name, False)   231    232 def get_utc_datetime(d, name):   233     dt, attr = get_datetime_item(d, name)   234     return to_utc_datetime(dt)   235    236 def get_datetime_item(d, name):   237     value, attr = get_item(d, name)   238     return get_datetime(value, attr), attr   239    240 def get_addresses(values):   241     return [address for name, address in email.utils.getaddresses(values)]   242    243 def get_address(value):   244     return value.lower().startswith("mailto:") and value.lower()[7:] or value   245    246 def get_uri(value):   247     return value.lower().startswith("mailto:") and value.lower() or ":" in value and value or "mailto:%s" % value.lower()   248    249 uri_value = get_uri   250    251 def uri_values(values):   252     return map(get_uri, values)   253    254 def uri_dict(d):   255     return dict([(get_uri(key), value) for key, value in d.items()])   256    257 def uri_item(item):   258     return get_uri(item[0]), item[1]   259    260 def uri_items(items):   261     return [(get_uri(value), attr) for value, attr in items]   262    263 # Operations on structure data.   264    265 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, partstat_set):   266    267     """   268     Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and   269     'new_dtstamp', and the 'partstat_set' indication, whether the object   270     providing the new information is really newer than the object providing the   271     old information.   272     """   273    274     have_sequence = old_sequence is not None and new_sequence is not None   275     is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence)   276    277     have_dtstamp = old_dtstamp and new_dtstamp   278     is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp   279    280     is_old_sequence = have_sequence and (   281         int(new_sequence) < int(old_sequence) or   282         is_same_sequence and is_old_dtstamp   283         )   284    285     return is_same_sequence and partstat_set or not is_old_sequence   286    287 # NOTE: Need to expose the 100 day window for recurring events in the   288 # NOTE: configuration.   289    290 def get_periods(obj, window_size=100):   291    292     """   293     Return periods for the given object 'obj', confining materialised periods   294     to the given 'window_size' in days starting from the present moment.   295     """   296    297     dtstart = obj.get_utc_datetime("DTSTART")   298     dtend = obj.get_utc_datetime("DTEND")   299    300     # NOTE: Need also DURATION support.   301    302     duration = dtend - dtstart   303    304     # Recurrence rules create multiple instances to be checked.   305     # Conflicts may only be assessed within a period defined by policy   306     # for the agent, with instances outside that period being considered   307     # unchecked.   308    309     window_end = datetime.now() + timedelta(window_size)   310    311     # NOTE: Need also RDATE and EXDATE support.   312    313     rrule = obj.get_value("RRULE")   314    315     if rrule:   316         selector = get_rule(dtstart, rrule)   317         parameters = get_parameters(rrule)   318         periods = []   319         for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS")):   320             start = datetime(*start, tzinfo=timezone("UTC"))   321             end = start + duration   322             periods.append((start, end))   323     else:   324         periods = [(dtstart, dtend)]   325    326     return periods   327    328 def get_periods_for_freebusy(obj, periods, tzid):   329    330     """   331     Get free/busy-compliant periods employed by 'obj' from the given 'periods',   332     using the indicated 'tzid' to convert dates to datetimes.   333     """   334    335     start, start_attr = obj.get_datetime_item("DTSTART")   336     end, end_attr = obj.get_datetime_item("DTEND")   337    338     tzid = start_attr.get("TZID") or end_attr.get("TZID") or tzid   339    340     l = []   341    342     for start, end in periods:   343         start, end = get_freebusy_period(start, end, tzid)   344         l.append((format_datetime(start), format_datetime(end)))   345    346     return l   347    348 # vim: tabstop=4 expandtab shiftwidth=4