imip-agent

imiptools/data.py

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