imip-agent

imiptools/data.py

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