imip-agent

imiptools/data.py

555:66a327808a46
2015-05-17 Paul Boddie Removed superfluous UTC conversion.
     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 bisect import bisect_left    23 from datetime import datetime, timedelta    24 from email.mime.text import MIMEText    25 from imiptools.dates import format_datetime, get_datetime, get_duration, \    26                             get_freebusy_period, get_period, get_tzid, \    27                             to_timezone, to_utc_datetime    28 from imiptools.period import Period, RecurringPeriod, period_overlaps    29 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node    30 from vRecurrence import get_parameters, get_rule    31 import email.utils    32     33 try:    34     from cStringIO import StringIO    35 except ImportError:    36     from StringIO import StringIO    37     38 class Object:    39     40     "Access to calendar structures."    41     42     def __init__(self, fragment):    43         self.objtype, (self.details, self.attr) = fragment.items()[0]    44     45     def get_uid(self):    46         return self.get_value("UID")    47     48     def get_recurrenceid(self):    49         return format_datetime(self.get_utc_datetime("RECURRENCE-ID"))    50     51     # Structure access.    52     53     def copy(self):    54         return Object(to_dict(self.to_node()))    55     56     def get_items(self, name, all=True):    57         return get_items(self.details, name, all)    58     59     def get_item(self, name):    60         return get_item(self.details, name)    61     62     def get_value_map(self, name):    63         return get_value_map(self.details, name)    64     65     def get_values(self, name, all=True):    66         return get_values(self.details, name, all)    67     68     def get_value(self, name):    69         return get_value(self.details, name)    70     71     def get_utc_datetime(self, name, date_tzid=None):    72         return get_utc_datetime(self.details, name, date_tzid)    73     74     def get_date_values(self, name, tzid=None):    75         items = get_date_value_items(self.details, name, tzid)    76         return items and [value for value, attr in items]    77     78     def get_date_value_items(self, name, tzid=None):    79         return get_date_value_items(self.details, name, tzid)    80     81     def get_datetime(self, name):    82         dt, attr = get_datetime_item(self.details, name)    83         return dt    84     85     def get_datetime_item(self, name):    86         return get_datetime_item(self.details, name)    87     88     def get_duration(self, name):    89         return get_duration(self.get_value(name))    90     91     def to_node(self):    92         return to_node({self.objtype : [(self.details, self.attr)]})    93     94     def to_part(self, method):    95         return to_part(method, [self.to_node()])    96     97     # Direct access to the structure.    98     99     def has_key(self, name):   100         return self.details.has_key(name)   101    102     def get(self, name):   103         return self.details.get(name)   104    105     def __getitem__(self, name):   106         return self.details[name]   107    108     def __setitem__(self, name, value):   109         self.details[name] = value   110    111     def __delitem__(self, name):   112         del self.details[name]   113    114     def remove(self, name):   115         try:   116             del self[name]   117         except KeyError:   118             pass   119    120     def remove_all(self, names):   121         for name in names:   122             self.remove(name)   123    124     # Computed results.   125    126     def get_periods(self, tzid, end):   127         return get_periods(self, tzid, end)   128    129     def get_periods_for_freebusy(self, tzid, end):   130         periods = self.get_periods(tzid, end)   131         return get_periods_for_freebusy(self, periods, tzid)   132    133     def get_tzid(self):   134         dtstart, dtstart_attr = self.get_datetime_item("DTSTART")   135         dtend, dtend_attr = self.get_datetime_item("DTEND")   136         return get_tzid(dtstart_attr, dtend_attr)   137    138 # Construction and serialisation.   139    140 def make_calendar(nodes, method=None):   141    142     """   143     Return a complete calendar node wrapping the given 'nodes' and employing the   144     given 'method', if indicated.   145     """   146    147     return ("VCALENDAR", {},   148             (method and [("METHOD", {}, method)] or []) +   149             [("VERSION", {}, "2.0")] +   150             nodes   151            )   152    153 def make_freebusy(freebusy, uid, organiser, organiser_attr=None, attendee=None,   154     attendee_attr=None, dtstart=None, dtend=None):   155        156     """   157     Return a calendar node defining the free/busy details described in the given   158     'freebusy' list, employing the given 'uid', for the given 'organiser' and   159     optional 'organiser_attr', with the optional 'attendee' providing recipient   160     details together with the optional 'attendee_attr'.   161    162     The result will be constrained to the 'dtstart' and 'dtend' period if these   163     parameters are given.   164     """   165        166     record = []   167     rwrite = record.append   168        169     rwrite(("ORGANIZER", organiser_attr or {}, organiser))   170    171     if attendee:   172         rwrite(("ATTENDEE", attendee_attr or {}, attendee))    173    174     rwrite(("UID", {}, uid))   175    176     if freebusy:   177    178         # Get a constrained view if start and end limits are specified.   179    180         periods = dtstart and dtend and \   181             period_overlaps(freebusy, Period(dtstart, dtend), True) or \   182             freebusy   183    184         # Write the limits of the resource.   185    186         rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(periods[0].get_start())))   187         rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(periods[-1].get_end())))   188    189         for p in periods:   190             if p.transp == "OPAQUE":   191                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(   192                     map(format_datetime, [p.get_start(), p.get_end()])   193                     )))   194    195     return ("VFREEBUSY", {}, record)   196    197 def parse_object(f, encoding, objtype=None):   198    199     """   200     Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is   201     given, only objects of that type will be returned. Otherwise, the root of   202     the content will be returned as a dictionary with a single key indicating   203     the object type.   204    205     Return None if the content was not readable or suitable.   206     """   207    208     try:   209         try:   210             doctype, attrs, elements = obj = parse(f, encoding=encoding)   211             if objtype and doctype == objtype:   212                 return to_dict(obj)[objtype][0]   213             elif not objtype:   214                 return to_dict(obj)   215         finally:   216             f.close()   217    218     # NOTE: Handle parse errors properly.   219    220     except (ParseError, ValueError):   221         pass   222    223     return None   224    225 def to_part(method, calendar):   226    227     """   228     Write using the given 'method', the 'calendar' details to a MIME   229     text/calendar part.   230     """   231    232     encoding = "utf-8"   233     out = StringIO()   234     try:   235         to_stream(out, make_calendar(calendar, method), encoding)   236         part = MIMEText(out.getvalue(), "calendar", encoding)   237         part.set_param("method", method)   238         return part   239    240     finally:   241         out.close()   242    243 def to_stream(out, fragment, encoding="utf-8"):   244     iterwrite(out, encoding=encoding).append(fragment)   245    246 # Structure access functions.   247    248 def get_items(d, name, all=True):   249    250     """   251     Get all items from 'd' for the given 'name', returning single items if   252     'all' is specified and set to a false value and if only one value is   253     present for the name. Return None if no items are found for the name or if   254     many items are found but 'all' is set to a false value.   255     """   256    257     if d.has_key(name):   258         items = d[name]   259         if all:   260             return items   261         elif len(items) == 1:   262             return items[0]   263         else:   264             return None   265     else:   266         return None   267    268 def get_item(d, name):   269     return get_items(d, name, False)   270    271 def get_value_map(d, name):   272    273     """   274     Return a dictionary for all items in 'd' having the given 'name'. The   275     dictionary will map values for the name to any attributes or qualifiers   276     that may have been present.   277     """   278    279     items = get_items(d, name)   280     if items:   281         return dict(items)   282     else:   283         return {}   284    285 def values_from_items(items):   286     return map(lambda x: x[0], items)   287    288 def get_values(d, name, all=True):   289     if d.has_key(name):   290         items = d[name]   291         if not all and len(items) == 1:   292             return items[0][0]   293         else:   294             return values_from_items(items)   295     else:   296         return None   297    298 def get_value(d, name):   299     return get_values(d, name, False)   300    301 def get_date_value_items(d, name, tzid=None):   302    303     """   304     Obtain items from 'd' having the given 'name', where a single item yields   305     potentially many values. Return a list of tuples of the form (value,   306     attributes) where the attributes have been given for the property in 'd'.   307     """   308    309     items = get_items(d, name)   310     if items:   311         all_items = []   312         for item in items:   313             values, attr = item   314             if not attr.has_key("TZID") and tzid:   315                 attr["TZID"] = tzid   316             if not isinstance(values, list):   317                 values = [values]   318             for value in values:   319                 all_items.append((get_datetime(value, attr) or get_period(value, attr), attr))   320         return all_items   321     else:   322         return None   323    324 def get_utc_datetime(d, name, date_tzid=None):   325    326     """   327     Return the value provided by 'd' for 'name' as a datetime in the UTC zone   328     or as a date, converting any date to a datetime if 'date_tzid' is specified.   329     """   330    331     t = get_datetime_item(d, name)   332     if not t:   333         return None   334     else:   335         dt, attr = t   336         return to_utc_datetime(dt, date_tzid)   337    338 def get_datetime_item(d, name):   339     t = get_item(d, name)   340     if not t:   341         return None   342     else:   343         value, attr = t   344         return get_datetime(value, attr), attr   345    346 # Conversion functions.   347    348 def get_addresses(values):   349     return [address for name, address in email.utils.getaddresses(values)]   350    351 def get_address(value):   352     value = value.lower()   353     return value.startswith("mailto:") and value[7:] or value   354    355 def get_uri(value):   356     return value.lower().startswith("mailto:") and value.lower() or ":" in value and value or "mailto:%s" % value.lower()   357    358 uri_value = get_uri   359    360 def uri_values(values):   361     return map(get_uri, values)   362    363 def uri_dict(d):   364     return dict([(get_uri(key), value) for key, value in d.items()])   365    366 def uri_item(item):   367     return get_uri(item[0]), item[1]   368    369 def uri_items(items):   370     return [(get_uri(value), attr) for value, attr in items]   371    372 # Operations on structure data.   373    374 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, partstat_set):   375    376     """   377     Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and   378     'new_dtstamp', and the 'partstat_set' indication, whether the object   379     providing the new information is really newer than the object providing the   380     old information.   381     """   382    383     have_sequence = old_sequence is not None and new_sequence is not None   384     is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence)   385    386     have_dtstamp = old_dtstamp and new_dtstamp   387     is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp   388    389     is_old_sequence = have_sequence and (   390         int(new_sequence) < int(old_sequence) or   391         is_same_sequence and is_old_dtstamp   392         )   393    394     return is_same_sequence and partstat_set or not is_old_sequence   395    396 # NOTE: Need to expose the 100 day window for recurring events in the   397 # NOTE: configuration.   398    399 def get_window_end(tzid, window_size=100):   400     return to_timezone(datetime.now(), tzid) + timedelta(window_size)   401    402 def get_periods(obj, tzid, window_end, inclusive=False):   403    404     """   405     Return periods for the given object 'obj', confining materialised periods   406     to before the given 'window_end' datetime. If 'inclusive' is set to a true   407     value, any period occurring at the 'window_end' will be included.   408     """   409    410     rrule = obj.get_value("RRULE")   411    412     # Use localised datetimes.   413    414     dtstart, dtstart_attr = obj.get_datetime_item("DTSTART")   415    416     if obj.has_key("DTEND"):   417         dtend, dtend_attr = obj.get_datetime_item("DTEND")   418         duration = dtend - dtstart   419     elif obj.has_key("DURATION"):   420         duration = obj.get_duration("DURATION")   421         dtend = dtstart + duration   422         dtend_attr = dtstart_attr   423     else:   424         dtend, dtend_attr = dtstart, dtstart_attr   425    426     tzid = get_tzid(dtstart_attr, dtend_attr) or tzid   427    428     if not rrule:   429         periods = [RecurringPeriod(dtstart, dtend, tzid, "DTSTART", dtstart_attr, dtend_attr)]   430     else:   431         # Recurrence rules create multiple instances to be checked.   432         # Conflicts may only be assessed within a period defined by policy   433         # for the agent, with instances outside that period being considered   434         # unchecked.   435    436         selector = get_rule(dtstart, rrule)   437         parameters = get_parameters(rrule)   438         periods = []   439    440         until = parameters.get("UNTIL")   441         if until:   442             window_end = min(to_timezone(get_datetime(until, dtstart_attr), tzid), window_end)   443             inclusive = True   444    445         for start in selector.materialise(dtstart, window_end, parameters.get("COUNT"), parameters.get("BYSETPOS"), inclusive):   446             start = to_timezone(datetime(*start), tzid)   447             end = start + duration   448             periods.append(RecurringPeriod(start, end, tzid, "RRULE"))   449    450     # Add recurrence dates.   451    452     rdates = obj.get_date_value_items("RDATE", tzid)   453    454     if rdates:   455         for rdate, rdate_attr in rdates:   456             if isinstance(rdate, tuple):   457                 periods.append(RecurringPeriod(rdate[0], rdate[1], tzid, "RDATE", rdate_attr))   458             else:   459                 periods.append(RecurringPeriod(rdate, rdate + duration, tzid, "RDATE", rdate_attr))   460    461     # Return a sorted list of the periods.   462    463     periods.sort()   464    465     # Exclude exception dates.   466    467     exdates = obj.get_date_values("EXDATE", tzid)   468    469     if exdates:   470         for exdate in exdates:   471             if isinstance(exdate, tuple):   472                 period = Period(exdate[0], exdate[1])   473             else:   474                 period = Period(exdate, exdate + duration)   475             i = bisect_left(periods, period)   476             while i < len(periods) and periods[i] == period:   477                 del periods[i]   478    479     return periods   480    481 def get_periods_for_freebusy(obj, periods, tzid):   482    483     """   484     Get free/busy-compliant periods employed by 'obj' from the given 'periods',   485     using the indicated 'tzid' to convert dates to datetimes.   486     """   487    488     tzid = obj.get_tzid() or tzid   489    490     l = []   491    492     for p in periods:   493         start, end = get_freebusy_period(p.get_start(), p.get_end(), tzid)   494    495         # Create a new period for free/busy purposes with the converted   496         # datetime information.   497    498         l.append(p.__class__(start, end, *p.as_tuple()[2:]))   499    500     return l   501    502 # vim: tabstop=4 expandtab shiftwidth=4