imip-agent

imiptools/data.py

1253:333740ca50b6
2017-09-12 Paul Boddie Consider period replacement status when comparing form periods.
     1 #!/usr/bin/env python     2      3 """     4 Interpretation of vCalendar content.     5      6 Copyright (C) 2014, 2015, 2016, 2017 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 date, datetime, timedelta    24 from email.mime.text import MIMEText    25 from imiptools.dates import format_datetime, get_datetime, \    26                             get_datetime_item as get_item_from_datetime, \    27                             get_datetime_tzid, \    28                             get_duration, get_period, get_period_item, \    29                             get_recurrence_start_point, \    30                             get_time, get_timestamp, get_tzid, to_datetime, \    31                             to_timezone, to_utc_datetime    32 from imiptools.freebusy import FreeBusyPeriod    33 from imiptools.period import Period, RecurringPeriod    34 from vCalendar import iterwrite, parse, ParseError, to_dict, to_node    35 from vRecurrence import get_parameters, get_rule    36 import email.utils    37     38 try:    39     from cStringIO import StringIO    40 except ImportError:    41     from StringIO import StringIO    42     43 class Object:    44     45     "Access to calendar structures."    46     47     def __init__(self, fragment):    48     49         """    50         Initialise the object with the given 'fragment'. This must be a    51         dictionary mapping an object type (such as "VEVENT") to a tuple    52         containing the object details and attributes, each being a dictionary    53         itself.    54     55         The result of parse_object can be processed to obtain a fragment by    56         obtaining a collection of records for an object type. For example:    57     58         l = parse_object(f, encoding, "VCALENDAR")    59         events = l["VEVENT"]    60         event = events[0]    61     62         Then, the specific object must be presented as follows:    63     64         object = Object({"VEVENT" : event})    65     66         A convienience function is also provided to initialise objects:    67     68         object = new_object("VEVENT")    69         """    70     71         self.objtype, (self.details, self.attr) = fragment.items()[0]    72     73     def get_uid(self):    74         return self.get_value("UID")    75     76     def get_recurrenceid(self):    77     78         """    79         Return the recurrence identifier, normalised to a UTC datetime if    80         specified as a datetime or date with accompanying time zone information,    81         maintained as a date or floating datetime otherwise. If no recurrence    82         identifier is present, None is returned.    83     84         Note that this normalised form of the identifier may well not be the    85         same as the originally-specified identifier because that could have been    86         specified using an accompanying TZID attribute, whereas the normalised    87         form is effectively a converted datetime value.    88         """    89     90         if not self.has_key("RECURRENCE-ID"):    91             return None    92         dt, attr = self.get_datetime_item("RECURRENCE-ID")    93     94         # Coerce any date to a UTC datetime if TZID was specified.    95     96         tzid = attr.get("TZID")    97         if tzid:    98             dt = to_timezone(to_datetime(dt, tzid), "UTC")    99         return format_datetime(dt)   100    101     def get_recurrence_start_point(self, recurrenceid, tzid):   102    103         """   104         Return the start point corresponding to the given 'recurrenceid', using   105         the fallback 'tzid' to define the specific point in time referenced by   106         the recurrence identifier if the identifier has a date representation.   107    108         If 'recurrenceid' is given as None, this object's recurrence identifier   109         is used to obtain a start point, but if this object does not provide a   110         recurrence, None is returned.   111    112         A start point is typically used to match free/busy periods which are   113         themselves defined in terms of UTC datetimes.   114         """   115    116         recurrenceid = recurrenceid or self.get_recurrenceid()   117         if recurrenceid:   118             return get_recurrence_start_point(recurrenceid, tzid)   119         else:   120             return None   121    122     def get_recurrence_start_points(self, recurrenceids, tzid):   123         return [self.get_recurrence_start_point(recurrenceid, tzid) for recurrenceid in recurrenceids]   124    125     # Structure access.   126    127     def add(self, obj):   128    129         "Add 'obj' to the structure."   130    131         name = obj.objtype   132         if not self.details.has_key(name):   133             l = self.details[name] = []   134         else:   135             l = self.details[name]   136         l.append((obj.details, obj.attr))   137    138     def copy(self):   139         return Object(self.to_dict())   140    141     def get_items(self, name, all=True):   142         return get_items(self.details, name, all)   143    144     def get_item(self, name):   145         return get_item(self.details, name)   146    147     def get_value_map(self, name):   148         return get_value_map(self.details, name)   149    150     def get_values(self, name, all=True):   151         return get_values(self.details, name, all)   152    153     def get_value(self, name):   154         return get_value(self.details, name)   155    156     def get_utc_datetime(self, name, date_tzid=None):   157         return get_utc_datetime(self.details, name, date_tzid)   158    159     def get_date_value_items(self, name, tzid=None):   160         return get_date_value_items(self.details, name, tzid)   161    162     def get_date_value_item_periods(self, name, tzid=None):   163         return get_date_value_item_periods(self.details, name, self.get_main_period(tzid).get_duration(), tzid)   164    165     def get_period_values(self, name, tzid=None):   166         return get_period_values(self.details, name, tzid)   167    168     def get_datetime(self, name):   169         t = get_datetime_item(self.details, name)   170         if not t: return None   171         dt, attr = t   172         return dt   173    174     def get_datetime_item(self, name):   175         return get_datetime_item(self.details, name)   176    177     def get_duration(self, name):   178         return get_duration(self.get_value(name))   179    180     # Serialisation.   181    182     def to_dict(self):   183         return to_dict(self.to_node())   184    185     def to_node(self):   186         return to_node({self.objtype : [(self.details, self.attr)]})   187    188     def to_part(self, method, encoding="utf-8", line_length=None):   189         return to_part(method, [self.to_node()], encoding, line_length)   190    191     def to_string(self, encoding="utf-8", line_length=None):   192         return to_string(self.to_node(), encoding, line_length)   193    194     # Direct access to the structure.   195    196     def has_key(self, name):   197         return self.details.has_key(name)   198    199     def get(self, name):   200         return self.details.get(name)   201    202     def keys(self):   203         return self.details.keys()   204    205     def __getitem__(self, name):   206         return self.details[name]   207    208     def __setitem__(self, name, value):   209         self.details[name] = value   210    211     def __delitem__(self, name):   212         del self.details[name]   213    214     def remove(self, name):   215         try:   216             del self[name]   217         except KeyError:   218             pass   219    220     def remove_all(self, names):   221         for name in names:   222             self.remove(name)   223    224     def preserve(self, names):   225         for name in self.keys():   226             if not name in names:   227                 self.remove(name)   228    229     # Computed results.   230    231     def get_main_period(self, tzid=None):   232    233         """   234         Return a period object corresponding to the main start-end period for   235         the object.   236         """   237    238         (dtstart, dtstart_attr), (dtend, dtend_attr) = self.get_main_period_items()   239         tzid = tzid or get_tzid(dtstart_attr, dtend_attr)   240         return RecurringPeriod(dtstart, dtend, tzid, "DTSTART", dtstart_attr, dtend_attr)   241    242     def get_main_period_items(self):   243    244         """   245         Return two (value, attributes) items corresponding to the main start-end   246         period for the object.   247         """   248    249         dtstart, dtstart_attr = self.get_datetime_item("DTSTART")   250    251         if self.has_key("DTEND"):   252             dtend, dtend_attr = self.get_datetime_item("DTEND")   253         elif self.has_key("DURATION"):   254             duration = self.get_duration("DURATION")   255             dtend = dtstart + duration   256             dtend_attr = dtstart_attr   257         else:   258             dtend, dtend_attr = dtstart, dtstart_attr   259    260         return (dtstart, dtstart_attr), (dtend, dtend_attr)   261    262     def get_periods(self, tzid, start=None, end=None, inclusive=False):   263    264         """   265         Return periods defined by this object, employing the given 'tzid' where   266         no time zone information is defined, and limiting the collection to a   267         window of time with the given 'start' and 'end'.   268    269         If 'end' is omitted, only explicit recurrences and recurrences from   270         explicitly-terminated rules will be returned.   271    272         If 'inclusive' is set to a true value, any period occurring at the 'end'   273         will be included.   274         """   275    276         return get_periods(self, tzid, start, end, inclusive)   277    278     def has_period(self, tzid, period):   279    280         """   281         Return whether this object, employing the given 'tzid' where no time   282         zone information is defined, has the given 'period'.   283         """   284    285         return period in self.get_periods(tzid, end=period.get_start_point(), inclusive=True)   286    287     def has_recurrence(self, tzid, recurrenceid):   288    289         """   290         Return whether this object, employing the given 'tzid' where no time   291         zone information is defined, has the given 'recurrenceid'.   292         """   293    294         start_point = self.get_recurrence_start_point(recurrenceid, tzid)   295         for p in self.get_periods(tzid, end=start_point, inclusive=True):   296             if p.get_start_point() == start_point:   297                 return True   298         return False   299    300     def get_active_periods(self, recurrenceids, tzid, start=None, end=None):   301    302         """   303         Return all periods specified by this object that are not replaced by   304         those defined by 'recurrenceids', using 'tzid' as a fallback time zone   305         to convert floating dates and datetimes, and using 'start' and 'end' to   306         respectively indicate the start and end of the time window within which   307         periods are considered.   308         """   309    310         # Specific recurrences yield all specified periods.   311    312         periods = self.get_periods(tzid, start, end)   313    314         if self.get_recurrenceid():   315             return periods   316    317         # Parent objects need to have their periods tested against redefined   318         # recurrences.   319    320         active = []   321    322         for p in periods:   323    324             # Subtract any recurrences from the free/busy details of a   325             # parent object.   326    327             if not p.is_replaced(recurrenceids):   328                 active.append(p)   329    330         return active   331    332     def get_freebusy_period(self, period, only_organiser=False):   333    334         """   335         Return a free/busy period for the given 'period' provided by this   336         object, using the 'only_organiser' status to produce a suitable   337         transparency value.   338         """   339    340         return FreeBusyPeriod(   341             period.get_start_point(),   342             period.get_end_point(),   343             self.get_value("UID"),   344             only_organiser and "ORG" or self.get_value("TRANSP") or "OPAQUE",   345             self.get_recurrenceid(),   346             self.get_value("SUMMARY"),   347             get_uri(self.get_value("ORGANIZER"))   348             )   349    350     def get_participation_status(self, participant):   351    352         """   353         Return the participation status of the given 'participant', with the   354         special value "ORG" indicating organiser-only participation.   355         """   356        357         attendees = uri_dict(self.get_value_map("ATTENDEE"))   358         organiser = get_uri(self.get_value("ORGANIZER"))   359    360         attendee_attr = attendees.get(participant)   361         if attendee_attr:   362             return attendee_attr.get("PARTSTAT", "NEEDS-ACTION")   363         elif organiser == participant:   364             return "ORG"   365    366         return None   367    368     def get_participation(self, partstat, include_needs_action=False):   369    370         """   371         Return whether 'partstat' indicates some kind of participation in an   372         event. If 'include_needs_action' is specified as a true value, events   373         not yet responded to will be treated as events with tentative   374         participation.   375         """   376    377         return not partstat in ("DECLINED", "DELEGATED", "NEEDS-ACTION") or \   378                include_needs_action and partstat == "NEEDS-ACTION" or \   379                partstat == "ORG"   380    381     def get_tzid(self):   382    383         """   384         Return a time zone identifier used by the start or end datetimes,   385         potentially suitable for converting dates to datetimes.   386         """   387    388         if not self.has_key("DTSTART"):   389             return None   390         dtstart, dtstart_attr = self.get_datetime_item("DTSTART")   391         if self.has_key("DTEND"):   392             dtend, dtend_attr = self.get_datetime_item("DTEND")   393         else:   394             dtend_attr = None   395         return get_tzid(dtstart_attr, dtend_attr)   396    397     def is_shared(self):   398    399         """   400         Return whether this object is shared based on the presence of a SEQUENCE   401         property.   402         """   403    404         return self.get_value("SEQUENCE") is not None   405    406     def possibly_active_from(self, dt, tzid):   407    408         """   409         Return whether the object is possibly active from or after the given   410         datetime 'dt' using 'tzid' to convert any dates or floating datetimes.   411         """   412    413         dt = to_datetime(dt, tzid)   414         periods = self.get_periods(tzid)   415    416         for p in periods:   417             if p.get_end_point() > dt:   418                 return True   419    420         return self.possibly_recurring_indefinitely()   421    422     def possibly_recurring_indefinitely(self):   423    424         "Return whether this object may recur indefinitely."   425    426         rrule = self.get_value("RRULE")   427         parameters = rrule and get_parameters(rrule)   428         until = parameters and parameters.get("UNTIL")   429         count = parameters and parameters.get("COUNT")   430    431         # Non-recurring periods or constrained recurrences.   432    433         if not rrule or until or count:   434             return False   435    436         # Unconstrained recurring periods will always lie beyond any specified   437         # datetime.   438    439         else:   440             return True   441    442     # Modification methods.   443    444     def set_datetime(self, name, dt, tzid=None):   445    446         """   447         Set a datetime for property 'name' using 'dt' and the optional fallback   448         'tzid', returning whether an update has occurred.   449         """   450    451         if dt:   452             old_value = self.get_value(name)   453             self[name] = [get_item_from_datetime(dt, tzid)]   454             return format_datetime(dt) != old_value   455    456         return False   457    458     def set_period(self, period):   459    460         "Set the given 'period' as the main start and end."   461    462         result = self.set_datetime("DTSTART", period.get_start())   463         result = self.set_datetime("DTEND", period.get_end()) or result   464         if self.has_key("DURATION"):   465             del self["DURATION"]   466    467         return result   468    469     def set_periods(self, periods):   470    471         """   472         Set the given 'periods' as recurrence date properties, replacing the   473         previous RDATE properties and ignoring any RRULE properties.   474         """   475    476         old_values = set(self.get_date_value_item_periods("RDATE") or [])   477         new_rdates = []   478    479         if self.has_key("RDATE"):   480             del self["RDATE"]   481    482         main_changed = False   483    484         for p in periods:   485             if p.origin == "RDATE" and p != self.get_main_period():   486                 new_rdates.append(get_period_item(p.get_start(), p.get_end()))   487             elif p.origin == "DTSTART":   488                 main_changed = self.set_period(p)   489    490         if new_rdates:   491             self["RDATE"] = new_rdates   492    493         return main_changed or old_values != set(self.get_date_value_item_periods("RDATE") or [])   494    495     def set_rule(self, rule):   496    497         """   498         Set the given 'rule' in this object, replacing the previous RRULE   499         property, returning whether the object has changed. The provided 'rule'   500         must be an item.   501         """   502    503         if not rule:   504             return False   505    506         old_rrule = self.get_item("RRULE")   507         self["RRULE"] = [rule]   508         return old_rrule != rule   509    510     def set_exceptions(self, exceptions):   511    512         """   513         Set the given 'exceptions' in this object, replacing the previous EXDATE   514         properties, returning whether the object has changed. The provided   515         'exceptions' must be a collection of items.   516         """   517    518         old_exdates = set(self.get_date_value_item_periods("EXDATE") or [])   519         if exceptions:   520             self["EXDATE"] = exceptions   521             return old_exdates != set(self.get_date_value_item_periods("EXDATE") or [])   522         elif old_exdates:   523             del self["EXDATE"]   524             return True   525         else:   526             return False   527    528     def update_dtstamp(self):   529    530         "Update the DTSTAMP in the object."   531    532         dtstamp = self.get_utc_datetime("DTSTAMP")   533         utcnow = get_time()   534         dtstamp = format_datetime(dtstamp and dtstamp > utcnow and dtstamp or utcnow)   535         self["DTSTAMP"] = [(dtstamp, {})]   536         return dtstamp   537    538     def update_sequence(self, increment=False):   539    540         "Set or update the SEQUENCE in the object."   541    542         sequence = self.get_value("SEQUENCE") or "0"   543         self["SEQUENCE"] = [(str(int(sequence) + (increment and 1 or 0)), {})]   544         return sequence   545    546     def update_exceptions(self, excluded, asserted):   547    548         """   549         Update the exceptions to any rule by applying the list of 'excluded'   550         periods. Where 'asserted' periods are provided, exceptions will be   551         removed corresponding to those periods.   552         """   553    554         old_exdates = self.get_date_value_item_periods("EXDATE") or []   555         new_exdates = set(old_exdates)   556         new_exdates.update(excluded)   557         new_exdates.difference_update(asserted)   558    559         if not new_exdates and self.has_key("EXDATE"):   560             del self["EXDATE"]   561         else:   562             self["EXDATE"] = []   563             for p in new_exdates:   564                 self["EXDATE"].append(get_period_item(p.get_start(), p.get_end()))   565    566         return set(old_exdates) != new_exdates   567    568     def correct_object(self, tzid, permitted_values):   569    570         """   571         Correct the object's period details using the given 'tzid' and   572         'permitted_values'.   573         """   574    575         corrected = set()   576         rdates = []   577    578         for period in self.get_periods(tzid):   579             corrected_period = period.get_corrected(permitted_values)   580    581             if corrected_period is period:   582                 if period.origin == "RDATE":   583                     rdates.append(period)   584                 continue   585    586             if period.origin == "DTSTART":   587                 self.set_period(corrected_period)   588                 corrected.add("DTSTART")   589             elif period.origin == "RDATE":   590                 rdates.append(corrected_period)   591                 corrected.add("RDATE")   592    593         if "RDATE" in corrected:   594             self.set_periods(rdates)   595    596         return corrected   597    598 # Construction and serialisation.   599    600 def make_calendar(nodes, method=None):   601    602     """   603     Return a complete calendar node wrapping the given 'nodes' and employing the   604     given 'method', if indicated.   605     """   606    607     return ("VCALENDAR", {},   608             (method and [("METHOD", {}, method)] or []) +   609             [("VERSION", {}, "2.0")] +   610             nodes   611            )   612    613 def make_freebusy(freebusy, uid, organiser, organiser_attr=None, attendee=None,   614                   attendee_attr=None, period=None):   615        616     """   617     Return a calendar node defining the free/busy details described in the given   618     'freebusy' list, employing the given 'uid', for the given 'organiser' and   619     optional 'organiser_attr', with the optional 'attendee' providing recipient   620     details together with the optional 'attendee_attr'.   621    622     The result will be constrained to the 'period' if specified.   623     """   624        625     record = []   626     rwrite = record.append   627        628     rwrite(("ORGANIZER", organiser_attr or {}, organiser))   629    630     if attendee:   631         rwrite(("ATTENDEE", attendee_attr or {}, attendee))    632    633     rwrite(("UID", {}, uid))   634    635     if freebusy:   636    637         # Get a constrained view if start and end limits are specified.   638    639         if period:   640             periods = freebusy.get_overlapping([period])   641         else:   642             periods = freebusy   643    644         # Write the limits of the resource.   645    646         if periods:   647             rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(periods[0].get_start_point())))   648             rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(periods[-1].get_end_point())))   649         else:   650             rwrite(("DTSTART", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_start_point())))   651             rwrite(("DTEND", {"VALUE" : "DATE-TIME"}, format_datetime(period.get_end_point())))   652    653         for p in periods:   654             if p.transp == "OPAQUE":   655                 rwrite(("FREEBUSY", {"FBTYPE" : "BUSY"}, "/".join(   656                     map(format_datetime, [p.get_start_point(), p.get_end_point()])   657                     )))   658    659     return ("VFREEBUSY", {}, record)   660    661 def parse_object(f, encoding, objtype=None):   662    663     """   664     Parse the iTIP content from 'f' having the given 'encoding'. If 'objtype' is   665     given, only objects of that type will be returned. Otherwise, the root of   666     the content will be returned as a dictionary with a single key indicating   667     the object type.   668    669     Return None if the content was not readable or suitable.   670     """   671    672     try:   673         try:   674             doctype, attrs, elements = obj = parse(f, encoding=encoding)   675             if objtype and doctype == objtype:   676                 return to_dict(obj)[objtype][0]   677             elif not objtype:   678                 return to_dict(obj)   679         finally:   680             f.close()   681    682     # NOTE: Handle parse errors properly.   683    684     except (ParseError, ValueError):   685         pass   686    687     return None   688    689 def parse_string(s, encoding, objtype=None):   690    691     """   692     Parse the iTIP content from 's' having the given 'encoding'. If 'objtype' is   693     given, only objects of that type will be returned. Otherwise, the root of   694     the content will be returned as a dictionary with a single key indicating   695     the object type.   696    697     Return None if the content was not readable or suitable.   698     """   699    700     return parse_object(StringIO(s), encoding, objtype)   701    702 def to_part(method, fragments, encoding="utf-8", line_length=None):   703    704     """   705     Write using the given 'method', the given 'fragments' to a MIME   706     text/calendar part.   707     """   708    709     out = StringIO()   710     try:   711         to_stream(out, make_calendar(fragments, method), encoding, line_length)   712         part = MIMEText(out.getvalue(), "calendar", encoding)   713         part.set_param("method", method)   714         return part   715    716     finally:   717         out.close()   718    719 def to_stream(out, fragment, encoding="utf-8", line_length=None):   720    721     "Write to the 'out' stream the given 'fragment'."   722    723     iterwrite(out, encoding=encoding, line_length=line_length).append(fragment)   724    725 def to_string(fragment, encoding="utf-8", line_length=None):   726    727     "Return a string encoding the given 'fragment'."   728    729     out = StringIO()   730     try:   731         to_stream(out, fragment, encoding, line_length)   732         return out.getvalue()   733    734     finally:   735         out.close()   736    737 def new_object(object_type):   738    739     "Make a new object of the given 'object_type'."   740    741     return Object({object_type : ({}, {})})   742    743 def make_uid(user):   744    745     "Return a unique identifier for a new object by the given 'user'."   746    747     utcnow = get_timestamp()   748     return "imip-agent-%s-%s" % (utcnow, get_address(user))   749    750 # Structure access functions.   751    752 def get_items(d, name, all=True):   753    754     """   755     Get all items from 'd' for the given 'name', returning single items if   756     'all' is specified and set to a false value and if only one value is   757     present for the name. Return None if no items are found for the name or if   758     many items are found but 'all' is set to a false value.   759     """   760    761     if d.has_key(name):   762         items = [(value or None, attr) for value, attr in d[name]]   763         if all:   764             return items   765         elif len(items) == 1:   766             return items[0]   767         else:   768             return None   769     else:   770         return None   771    772 def get_item(d, name):   773     return get_items(d, name, False)   774    775 def get_value_map(d, name):   776    777     """   778     Return a dictionary for all items in 'd' having the given 'name'. The   779     dictionary will map values for the name to any attributes or qualifiers   780     that may have been present.   781     """   782    783     items = get_items(d, name)   784     if items:   785         return dict(items)   786     else:   787         return {}   788    789 def values_from_items(items):   790     return map(lambda x: x[0], items)   791    792 def get_values(d, name, all=True):   793     if d.has_key(name):   794         items = d[name]   795         if not all and len(items) == 1:   796             return items[0][0]   797         else:   798             return values_from_items(items)   799     else:   800         return None   801    802 def get_value(d, name):   803     return get_values(d, name, False)   804    805 def get_date_value_items(d, name, tzid=None):   806    807     """   808     Obtain items from 'd' having the given 'name', where a single item yields   809     potentially many values. Return a list of tuples of the form (value,   810     attributes) where the attributes have been given for the property in 'd'.   811     """   812    813     items = get_items(d, name)   814     if items:   815         all_items = []   816         for item in items:   817             values, attr = item   818             if not attr.has_key("TZID") and tzid:   819                 attr["TZID"] = tzid   820             if not isinstance(values, list):   821                 values = [values]   822             for value in values:   823                 all_items.append((get_datetime(value, attr) or get_period(value, attr), attr))   824         return all_items   825     else:   826         return None   827    828 def get_date_value_item_periods(d, name, duration, tzid=None):   829    830     """   831     Obtain items from 'd' having the given 'name', where a single item yields   832     potentially many values. The 'duration' must be provided to define the   833     length of periods having only a start datetime. Return a list of periods   834     corresponding to the property in 'd'.   835     """   836    837     items = get_date_value_items(d, name, tzid)   838     if not items:   839         return items   840    841     periods = []   842    843     for value, attr in items:   844         if isinstance(value, tuple):   845             periods.append(RecurringPeriod(value[0], value[1], tzid, name, attr))   846         else:   847             periods.append(RecurringPeriod(value, value + duration, tzid, name, attr))   848    849     return periods   850    851 def get_period_values(d, name, tzid=None):   852    853     """   854     Return period values from 'd' for the given property 'name', using 'tzid'   855     where specified to indicate the time zone.   856     """   857    858     values = []   859     for value, attr in get_items(d, name) or []:   860         if not attr.has_key("TZID") and tzid:   861             attr["TZID"] = tzid   862         start, end = get_period(value, attr)   863         values.append(Period(start, end, tzid=tzid))   864     return values   865    866 def get_utc_datetime(d, name, date_tzid=None):   867    868     """   869     Return the value provided by 'd' for 'name' as a datetime in the UTC zone   870     or as a date, converting any date to a datetime if 'date_tzid' is specified.   871     If no datetime or date is available, None is returned.   872     """   873    874     t = get_datetime_item(d, name)   875     if not t:   876         return None   877     else:   878         dt, attr = t   879         return dt is not None and to_utc_datetime(dt, date_tzid) or None   880    881 def get_datetime_item(d, name):   882    883     """   884     Return the value provided by 'd' for 'name' as a datetime or as a date,   885     together with the attributes describing it. Return None if no value exists   886     for 'name' in 'd'.   887     """   888    889     t = get_item(d, name)   890     if not t:   891         return None   892     else:   893         value, attr = t   894         dt = get_datetime(value, attr)   895         tzid = get_datetime_tzid(dt)   896         if tzid:   897             attr["TZID"] = tzid   898         return dt, attr   899    900 # Conversion functions.   901    902 def get_address_parts(values):   903    904     "Return name and address tuples for each of the given 'values'."   905    906     l = []   907     for name, address in values and email.utils.getaddresses(values) or []:   908         if is_mailto_uri(name):   909             name = name[7:] # strip "mailto:"   910         l.append((name, address))   911     return l   912    913 def get_addresses(values):   914    915     """   916     Return only addresses from the given 'values' which may be of the form   917     "Common Name <recipient@domain>", with the latter part being the address   918     itself.   919     """   920    921     return [address for name, address in get_address_parts(values)]   922    923 def get_address(value):   924    925     "Return an e-mail address from the given 'value'."   926    927     if not value: return None   928     return get_addresses([value])[0]   929    930 def get_verbose_address(value, attr=None):   931    932     """   933     Return a verbose e-mail address featuring any name from the given 'value'   934     and any accompanying 'attr' dictionary.   935     """   936    937     l = get_address_parts([value])   938     if not l:   939         return value   940     name, address = l[0]   941     if not name:   942         name = attr and attr.get("CN")   943     if name and address:   944         return "%s <%s>" % (name, address)   945     else:   946         return address   947    948 def is_mailto_uri(value):   949    950     """   951     Return whether 'value' is a mailto: URI, with the protocol potentially being   952     in upper case.   953     """   954    955     return value.lower().startswith("mailto:")   956    957 def get_uri(value):   958    959     "Return a URI for the given 'value'."   960    961     if not value: return None   962    963     # Normalise to "mailto:" or return other URI form.   964    965     return is_mailto_uri(value) and ("mailto:%s" % value[7:]) or \   966            ":" in value and value or \   967            "mailto:%s" % get_address(value)   968    969 def uri_parts(values):   970    971     "Return any common name plus the URI for each of the given 'values'."   972    973     return [(name, get_uri(address)) for name, address in get_address_parts(values)]   974    975 uri_value = get_uri   976    977 def uri_values(values):   978     return map(get_uri, values)   979    980 def uri_dict(d):   981     return dict([(get_uri(key), value) for key, value in d.items()])   982    983 def uri_item(item):   984     return get_uri(item[0]), item[1]   985    986 def uri_items(items):   987     return [(get_uri(value), attr) for value, attr in items]   988    989 # Operations on structure data.   990    991 def is_new_object(old_sequence, new_sequence, old_dtstamp, new_dtstamp, ignore_dtstamp):   992    993     """   994     Return for the given 'old_sequence' and 'new_sequence', 'old_dtstamp' and   995     'new_dtstamp', and the 'ignore_dtstamp' indication, whether the object   996     providing the new information is really newer than the object providing the   997     old information.   998     """   999   1000     have_sequence = old_sequence is not None and new_sequence is not None  1001     is_same_sequence = have_sequence and int(new_sequence) == int(old_sequence)  1002   1003     have_dtstamp = old_dtstamp and new_dtstamp  1004     is_old_dtstamp = have_dtstamp and new_dtstamp < old_dtstamp or old_dtstamp and not new_dtstamp  1005   1006     is_old_sequence = have_sequence and (  1007         int(new_sequence) < int(old_sequence) or  1008         is_same_sequence and is_old_dtstamp  1009         )  1010   1011     return is_same_sequence and ignore_dtstamp or not is_old_sequence  1012   1013 def check_delegation(attendee_map, attendee, attendee_attr):  1014   1015     """  1016     Using the 'attendee_map', check the attributes for the given 'attendee'  1017     provided as 'attendee_attr', following the delegation chain back to the  1018     delegators and forward again to yield the delegate identities in each  1019     case. Pictorially...  1020   1021     attendee -> DELEGATED-FROM -> delegator  1022            ? <-  DELEGATED-TO  <---  1023   1024     Return whether 'attendee' was identified as a delegate by providing the  1025     identity of any delegators referencing the attendee.  1026     """  1027   1028     delegators = []  1029   1030     # The recipient should have a reference to the delegator.  1031   1032     delegated_from = attendee_attr and attendee_attr.get("DELEGATED-FROM")  1033     if delegated_from:  1034   1035         # Examine all delegators.  1036   1037         for delegator in delegated_from:  1038             delegator_attr = attendee_map.get(delegator)  1039   1040             # The delegator should have a reference to the recipient.  1041   1042             delegated_to = delegator_attr and delegator_attr.get("DELEGATED-TO")  1043             if delegated_to and attendee in delegated_to:  1044                 delegators.append(delegator)  1045   1046     return delegators  1047   1048 def get_periods(obj, tzid, start=None, end=None, inclusive=False):  1049   1050     """  1051     Return periods for the given object 'obj', employing the given 'tzid' where  1052     no time zone information is available (for whole day events, for example),  1053     confining materialised periods to after the given 'start' datetime and  1054     before the given 'end' datetime.  1055   1056     If 'end' is omitted, only explicit recurrences and recurrences from  1057     explicitly-terminated rules will be returned.  1058   1059     If 'inclusive' is set to a true value, any period occurring at the 'end'  1060     will be included.  1061     """  1062   1063     rrule = obj.get_value("RRULE")  1064     parameters = rrule and get_parameters(rrule)  1065   1066     # Use localised datetimes.  1067   1068     main_period = obj.get_main_period(tzid)  1069   1070     dtstart = main_period.get_start()  1071     dtstart_attr = main_period.get_start_attr()  1072   1073     # Attempt to get time zone details from the object, using the supplied zone  1074     # only as a fallback.  1075   1076     obj_tzid = obj.get_tzid()  1077   1078     if not rrule:  1079         periods = [main_period]  1080   1081     elif end or parameters and parameters.has_key("UNTIL") or parameters.has_key("COUNT"):  1082   1083         # Recurrence rules create multiple instances to be checked.  1084         # Conflicts may only be assessed within a period defined by policy  1085         # for the agent, with instances outside that period being considered  1086         # unchecked.  1087   1088         selector = get_rule(dtstart, rrule)  1089         periods = []  1090   1091         until = parameters.get("UNTIL")  1092         if until:  1093             until_dt = to_timezone(get_datetime(until, dtstart_attr), obj_tzid)  1094             end = end and min(until_dt, end) or until_dt  1095             inclusive = True  1096   1097         # Define a selection period with a start point. The end will be handled  1098         # in the materialisation process below.  1099   1100         selection_period = Period(start, None)  1101   1102         # Obtain period instances, starting from the main period. Since counting  1103         # must start from the first period, filtering from a start date must be  1104         # done after the instances have been obtained.  1105   1106         for recurrence_start in selector.materialise(dtstart, end, parameters.get("COUNT"), parameters.get("BYSETPOS"), inclusive):  1107   1108             # Determine the resolution of the period.  1109   1110             create = len(recurrence_start) == 3 and date or datetime  1111             recurrence_start = to_timezone(create(*recurrence_start), obj_tzid)  1112             recurrence_end = recurrence_start + main_period.get_duration()  1113   1114             # Create the period with accompanying metadata based on the main  1115             # period and event details.  1116   1117             period = RecurringPeriod(recurrence_start, recurrence_end, tzid, "RRULE", dtstart_attr)  1118   1119             # Filter out periods before the start.  1120   1121             if period.within(selection_period):  1122                 periods.append(period)  1123   1124     else:  1125         periods = []  1126   1127     # Add recurrence dates.  1128   1129     rdates = obj.get_date_value_item_periods("RDATE", tzid)  1130     if rdates:  1131         periods += rdates  1132   1133     # Return a sorted list of the periods.  1134   1135     periods.sort()  1136   1137     # Exclude exception dates.  1138   1139     exdates = obj.get_date_value_item_periods("EXDATE", tzid)  1140   1141     if exdates:  1142         for period in exdates:  1143             i = bisect_left(periods, period)  1144             while i < len(periods) and periods[i] == period:  1145                 del periods[i]  1146   1147     return periods  1148   1149 def get_sender_identities(mapping):  1150   1151     """  1152     Return a mapping from actual senders to the identities for which they  1153     have provided data, extracting this information from the given  1154     'mapping'.  1155     """  1156   1157     senders = {}  1158   1159     for value, attr in mapping.items():  1160         sent_by = attr.get("SENT-BY")  1161         if sent_by:  1162             sender = get_uri(sent_by)  1163         else:  1164             sender = value  1165   1166         if not senders.has_key(sender):  1167             senders[sender] = []  1168   1169         senders[sender].append(value)  1170   1171     return senders  1172   1173 def get_window_end(tzid, days=100, start=None):  1174   1175     """  1176     Return a datetime in the time zone indicated by 'tzid' marking the end of a  1177     window of the given number of 'days'. If 'start' is not indicated, the start  1178     of the window will be the current moment.  1179     """  1180   1181     return to_timezone(start or datetime.now(), tzid) + timedelta(days)  1182   1183 # vim: tabstop=4 expandtab shiftwidth=4