| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078 |
- """Definitions and behavior for iCalendar, also known as vCalendar 2.0"""
- from __future__ import print_function
- import datetime
- import logging
- import random # for generating a UID
- import socket
- import string
- import base64
- from dateutil import rrule, tz
- import six
- try:
- import pytz
- except ImportError:
- class Pytz:
- """fake pytz module (pytz is not required)"""
- class AmbiguousTimeError(Exception):
- """pytz error for ambiguous times
- during transition daylight->standard"""
- class NonExistentTimeError(Exception):
- """pytz error for non-existent times
- during transition standard->daylight"""
- pytz = Pytz # keeps quantifiedcode happy
- from . import behavior
- from .base import (VObjectError, NativeError, ValidateError, ParseError,
- Component, ContentLine, logger, registerBehavior,
- backslashEscape, foldOneLine)
- # ------------------------------- Constants ------------------------------------
- DATENAMES = ("rdate", "exdate")
- RULENAMES = ("exrule", "rrule")
- DATESANDRULES = ("exrule", "rrule", "rdate", "exdate")
- PRODID = u"-//PYVOBJECT//NONSGML Version 1//EN"
- WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU"
- FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY',
- 'SECONDLY')
- zeroDelta = datetime.timedelta(0)
- twoHours = datetime.timedelta(hours=2)
- # ---------------------------- TZID registry -----------------------------------
- __tzidMap = {}
- def toUnicode(s):
- """
- Take a string or unicode, turn it into unicode, decoding as utf-8
- """
- if isinstance(s, six.binary_type):
- s = s.decode('utf-8')
- return s
- def registerTzid(tzid, tzinfo):
- """
- Register a tzid -> tzinfo mapping.
- """
- __tzidMap[toUnicode(tzid)] = tzinfo
- def getTzid(tzid, smart=True):
- """
- Return the tzid if it exists, or None.
- """
- tz = __tzidMap.get(toUnicode(tzid), None)
- if smart and tzid and not tz:
- try:
- from pytz import timezone, UnknownTimeZoneError
- try:
- tz = timezone(tzid)
- registerTzid(toUnicode(tzid), tz)
- except UnknownTimeZoneError as e:
- logging.error(e)
- except ImportError as e:
- logging.error(e)
- return tz
- utc = tz.tzutc()
- registerTzid("UTC", utc)
- # -------------------- Helper subclasses ---------------------------------------
- class TimezoneComponent(Component):
- """
- A VTIMEZONE object.
- VTIMEZONEs are parsed by tz.tzical, the resulting datetime.tzinfo
- subclass is stored in self.tzinfo, self.tzid stores the TZID associated
- with this timezone.
- @ivar name:
- The uppercased name of the object, in this case always 'VTIMEZONE'.
- @ivar tzinfo:
- A datetime.tzinfo subclass representing this timezone.
- @ivar tzid:
- The string used to refer to this timezone.
- """
- def __init__(self, tzinfo=None, *args, **kwds):
- """
- Accept an existing Component or a tzinfo class.
- """
- super(TimezoneComponent, self).__init__(*args, **kwds)
- self.isNative = True
- # hack to make sure a behavior is assigned
- if self.behavior is None:
- self.behavior = VTimezone
- if tzinfo is not None:
- self.tzinfo = tzinfo
- if not hasattr(self, 'name') or self.name == '':
- self.name = 'VTIMEZONE'
- self.useBegin = True
- @classmethod
- def registerTzinfo(obj, tzinfo):
- """
- Register tzinfo if it's not already registered, return its tzid.
- """
- tzid = obj.pickTzid(tzinfo)
- if tzid and not getTzid(tzid, False):
- registerTzid(tzid, tzinfo)
- return tzid
- def gettzinfo(self):
- # workaround for dateutil failing to parse some experimental properties
- good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom',
- 'tzoffsetto', 'tzid')
- # serialize encodes as utf-8, cStringIO will leave utf-8 alone
- buffer = six.StringIO()
- # allow empty VTIMEZONEs
- if len(self.contents) == 0:
- return None
- def customSerialize(obj):
- if isinstance(obj, Component):
- foldOneLine(buffer, u"BEGIN:" + obj.name)
- for child in obj.lines():
- if child.name.lower() in good_lines:
- child.serialize(buffer, 75, validate=False)
- for comp in obj.components():
- customSerialize(comp)
- foldOneLine(buffer, u"END:" + obj.name)
- customSerialize(self)
- buffer.seek(0) # tzical wants to read a stream
- return tz.tzical(buffer).get()
- def settzinfo(self, tzinfo, start=2000, end=2030):
- """
- Create appropriate objects in self to represent tzinfo.
- Collapse DST transitions to rrules as much as possible.
- Assumptions:
- - DST <-> Standard transitions occur on the hour
- - never within a month of one another
- - twice or fewer times a year
- - never in the month of December
- - DST always moves offset exactly one hour later
- - tzinfo classes dst method always treats times that could be in either
- offset as being in the later regime
- """
- def fromLastWeek(dt):
- """
- How many weeks from the end of the month dt is, starting from 1.
- """
- weekDelta = datetime.timedelta(weeks=1)
- n = 1
- current = dt + weekDelta
- while current.month == dt.month:
- n += 1
- current += weekDelta
- return n
- # lists of dictionaries defining rules which are no longer in effect
- completed = {'daylight': [], 'standard': []}
- # dictionary defining rules which are currently in effect
- working = {'daylight': None, 'standard': None}
- # rule may be based on nth week of the month or the nth from the last
- for year in range(start, end + 1):
- newyear = datetime.datetime(year, 1, 1)
- for transitionTo in 'daylight', 'standard':
- transition = getTransition(transitionTo, year, tzinfo)
- oldrule = working[transitionTo]
- if transition == newyear:
- # transitionTo is in effect for the whole year
- rule = {'end' : None,
- 'start' : newyear,
- 'month' : 1,
- 'weekday' : None,
- 'hour' : None,
- 'plus' : None,
- 'minus' : None,
- 'name' : tzinfo.tzname(newyear),
- 'offset' : tzinfo.utcoffset(newyear),
- 'offsetfrom' : tzinfo.utcoffset(newyear)}
- if oldrule is None:
- # transitionTo was not yet in effect
- working[transitionTo] = rule
- else:
- # transitionTo was already in effect
- if (oldrule['offset'] != tzinfo.utcoffset(newyear)):
- # old rule was different, it shouldn't continue
- oldrule['end'] = year - 1
- completed[transitionTo].append(oldrule)
- working[transitionTo] = rule
- elif transition is None:
- # transitionTo is not in effect
- if oldrule is not None:
- # transitionTo used to be in effect
- oldrule['end'] = year - 1
- completed[transitionTo].append(oldrule)
- working[transitionTo] = None
- else:
- # an offset transition was found
- try:
- old_offset = tzinfo.utcoffset(transition - twoHours)
- name = tzinfo.tzname(transition)
- offset = tzinfo.utcoffset(transition)
- except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError):
- # guaranteed that tzinfo is a pytz timezone
- is_dst = (transitionTo == "daylight")
- old_offset = tzinfo.utcoffset(transition - twoHours, is_dst=is_dst)
- name = tzinfo.tzname(transition, is_dst=is_dst)
- offset = tzinfo.utcoffset(transition, is_dst=is_dst)
- rule = {'end' : None, # None, or an integer year
- 'start' : transition, # the datetime of transition
- 'month' : transition.month,
- 'weekday' : transition.weekday(),
- 'hour' : transition.hour,
- 'name' : name,
- 'plus' : int(
- (transition.day - 1)/ 7 + 1), # nth week of the month
- 'minus' : fromLastWeek(transition), # nth from last week
- 'offset' : offset,
- 'offsetfrom' : old_offset}
- if oldrule is None:
- working[transitionTo] = rule
- else:
- plusMatch = rule['plus'] == oldrule['plus']
- minusMatch = rule['minus'] == oldrule['minus']
- truth = plusMatch or minusMatch
- for key in 'month', 'weekday', 'hour', 'offset':
- truth = truth and rule[key] == oldrule[key]
- if truth:
- # the old rule is still true, limit to plus or minus
- if not plusMatch:
- oldrule['plus'] = None
- if not minusMatch:
- oldrule['minus'] = None
- else:
- # the new rule did not match the old
- oldrule['end'] = year - 1
- completed[transitionTo].append(oldrule)
- working[transitionTo] = rule
- for transitionTo in 'daylight', 'standard':
- if working[transitionTo] is not None:
- completed[transitionTo].append(working[transitionTo])
- self.tzid = []
- self.daylight = []
- self.standard = []
- self.add('tzid').value = self.pickTzid(tzinfo, True)
- # old = None # unused?
- for transitionTo in 'daylight', 'standard':
- for rule in completed[transitionTo]:
- comp = self.add(transitionTo)
- dtstart = comp.add('dtstart')
- dtstart.value = rule['start']
- if rule['name'] is not None:
- comp.add('tzname').value = rule['name']
- line = comp.add('tzoffsetto')
- line.value = deltaToOffset(rule['offset'])
- line = comp.add('tzoffsetfrom')
- line.value = deltaToOffset(rule['offsetfrom'])
- if rule['plus'] is not None:
- num = rule['plus']
- elif rule['minus'] is not None:
- num = -1 * rule['minus']
- else:
- num = None
- if num is not None:
- dayString = ";BYDAY=" + str(num) + WEEKDAYS[rule['weekday']]
- else:
- dayString = ""
- if rule['end'] is not None:
- if rule['hour'] is None:
- # all year offset, with no rule
- endDate = datetime.datetime(rule['end'], 1, 1)
- else:
- weekday = rrule.weekday(rule['weekday'], num)
- du_rule = rrule.rrule(rrule.YEARLY,
- bymonth=rule['month'], byweekday=weekday,
- dtstart=datetime.datetime(
- rule['end'], 1, 1, rule['hour']
- )
- )
- endDate = du_rule[0]
- endDate = endDate.replace(tzinfo=utc) - rule['offsetfrom']
- endString = ";UNTIL=" + dateTimeToString(endDate)
- else:
- endString = ''
- new_rule = "FREQ=YEARLY{0!s};BYMONTH={1!s}{2!s}"\
- .format(dayString, rule['month'], endString)
- comp.add('rrule').value = new_rule
- tzinfo = property(gettzinfo, settzinfo)
- # prevent Component's __setattr__ from overriding the tzinfo property
- normal_attributes = Component.normal_attributes + ['tzinfo']
- @staticmethod
- def pickTzid(tzinfo, allowUTC=False):
- """
- Given a tzinfo class, use known APIs to determine TZID, or use tzname.
- """
- if tzinfo is None or (not allowUTC and tzinfo_eq(tzinfo, utc)):
- # If tzinfo is UTC, we don't need a TZID
- return None
- # try PyICU's tzid key
- if hasattr(tzinfo, 'tzid'):
- return toUnicode(tzinfo.tzid)
- # try pytz zone key
- if hasattr(tzinfo, 'zone'):
- return toUnicode(tzinfo.zone)
- # try tzical's tzid key
- elif hasattr(tzinfo, '_tzid'):
- return toUnicode(tzinfo._tzid)
- else:
- # return tzname for standard (non-DST) time
- notDST = datetime.timedelta(0)
- for month in range(1, 13):
- dt = datetime.datetime(2000, month, 1)
- if tzinfo.dst(dt) == notDST:
- return toUnicode(tzinfo.tzname(dt))
- # there was no standard time in 2000!
- raise VObjectError("Unable to guess TZID for tzinfo {0!s}"
- .format(tzinfo))
- def __str__(self):
- return "<VTIMEZONE | {0}>".format(getattr(self, 'tzid', 'No TZID'))
- def __repr__(self):
- return self.__str__()
- def prettyPrint(self, level, tabwidth):
- pre = ' ' * level * tabwidth
- print(pre, self.name)
- print(pre, "TZID:", self.tzid)
- print('')
- class RecurringComponent(Component):
- """
- A vCalendar component like VEVENT or VTODO which may recur.
- Any recurring component can have one or multiple RRULE, RDATE,
- EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a
- variety of children that don't have any recurrence information.
- In the example below, note that dtstart is included in the rruleset.
- This is not the default behavior for dateutil's rrule implementation unless
- dtstart would already have been a member of the recurrence rule, and as a
- result, COUNT is wrong. This can be worked around when getting rruleset by
- adjusting count down by one if an rrule has a count and dtstart isn't in its
- result set, but by default, the rruleset property doesn't do this work
- around, to access it getrruleset must be called with addRDate set True.
- @ivar rruleset:
- A U{rruleset<https://moin.conectiva.com.br/DateUtil>}.
- """
- def __init__(self, *args, **kwds):
- super(RecurringComponent, self).__init__(*args, **kwds)
- self.isNative = True
- def getrruleset(self, addRDate=False):
- """
- Get an rruleset created from self.
- If addRDate is True, add an RDATE for dtstart if it's not included in
- an RRULE or RDATE, and count is decremented if it exists.
- Note that for rules which don't match DTSTART, DTSTART may not appear
- in list(rruleset), although it should. By default, an RDATE is not
- created in these cases, and count isn't updated, so dateutil may list
- a spurious occurrence.
- """
- rruleset = None
- for name in DATESANDRULES:
- addfunc = None
- for line in self.contents.get(name, ()):
- # don't bother creating a rruleset unless there's a rule
- if rruleset is None:
- rruleset = rrule.rruleset()
- if addfunc is None:
- addfunc = getattr(rruleset, name)
- try:
- dtstart = self.dtstart.value
- except (AttributeError, KeyError):
- # Special for VTODO - try DUE property instead
- try:
- if self.name == "VTODO":
- dtstart = self.due.value
- else:
- # if there's no dtstart, just return None
- logging.error('failed to get dtstart with VTODO')
- return None
- except (AttributeError, KeyError):
- # if there's no due, just return None
- logging.error('failed to find DUE at all.')
- return None
- if name in DATENAMES:
- if type(line.value[0]) == datetime.datetime:
- list(map(addfunc, line.value))
- elif type(line.value[0]) == datetime.date:
- for dt in line.value:
- addfunc(datetime.datetime(dt.year, dt.month, dt.day))
- else:
- # ignore RDATEs with PERIOD values for now
- pass
- elif name in RULENAMES:
- # a Ruby iCalendar library escapes semi-colons in rrules,
- # so also remove any backslashes
- value = line.value.replace('\\', '')
- # If dtstart has no time zone, `until`
- # shouldn't get one, either:
- ignoretz = (not isinstance(dtstart, datetime.datetime) or
- dtstart.tzinfo is None)
- try:
- until = rrule.rrulestr(value, ignoretz=ignoretz)._until
- except ValueError:
- # WORKAROUND: dateutil<=2.7.2 doesn't set the time zone
- # of dtstart
- if ignoretz:
- raise
- utc_now = datetime.datetime.now(datetime.timezone.utc)
- until = rrule.rrulestr(value, dtstart=utc_now)._until
- if until is not None and isinstance(dtstart,
- datetime.datetime) and \
- (until.tzinfo != dtstart.tzinfo):
- # dateutil converts the UNTIL date to a datetime,
- # check to see if the UNTIL parameter value was a date
- vals = dict(pair.split('=') for pair in
- value.upper().split(';'))
- if len(vals.get('UNTIL', '')) == 8:
- until = datetime.datetime.combine(until.date(),
- dtstart.time())
- # While RFC2445 says UNTIL MUST be UTC, Chandler allows
- # floating recurring events, and uses floating UNTIL
- # values. Also, some odd floating UNTIL but timezoned
- # DTSTART values have shown up in the wild, so put
- # floating UNTIL values DTSTART's timezone
- if until.tzinfo is None:
- until = until.replace(tzinfo=dtstart.tzinfo)
- if dtstart.tzinfo is not None:
- until = until.astimezone(dtstart.tzinfo)
- # RFC2445 actually states that UNTIL must be a UTC
- # value. Whilst the changes above work OK, one problem
- # case is if DTSTART is floating but UNTIL is properly
- # specified as UTC (or with a TZID). In that case
- # dateutil will fail datetime comparisons. There is no
- # easy solution to this as there is no obvious timezone
- # (at this point) to do proper floating time offset
- # comparisons. The best we can do is treat the UNTIL
- # value as floating. This could mean incorrect
- # determination of the last instance. The better
- # solution here is to encourage clients to use COUNT
- # rather than UNTIL when DTSTART is floating.
- if dtstart.tzinfo is None:
- until = until.replace(tzinfo=None)
- value_without_until = ';'.join(
- pair for pair in value.split(';')
- if pair.split('=')[0].upper() != 'UNTIL')
- rule = rrule.rrulestr(value_without_until,
- dtstart=dtstart, ignoretz=ignoretz)
- rule._until = until
- # add the rrule or exrule to the rruleset
- addfunc(rule)
- if (name == 'rrule' or name == 'rdate') and addRDate:
- # rlist = rruleset._rrule if name == 'rrule' else rruleset._rdate
- try:
- # dateutils does not work with all-day
- # (datetime.date) items so we need to convert to a
- # datetime.datetime (which is what dateutils
- # does internally)
- if not isinstance(dtstart, datetime.datetime):
- adddtstart = datetime.datetime.fromordinal(dtstart.toordinal())
- else:
- adddtstart = dtstart
- if name == 'rrule':
- if rruleset._rrule[-1][0] != adddtstart:
- rruleset.rdate(adddtstart)
- added = True
- if rruleset._rrule[-1]._count is not None:
- rruleset._rrule[-1]._count -= 1
- else:
- added = False
- elif name == 'rdate':
- if rruleset._rdate[0] != adddtstart:
- rruleset.rdate(adddtstart)
- added = True
- else:
- added = False
- except IndexError:
- # it's conceivable that an rrule has 0 datetimes
- added = False
- return rruleset
- def setrruleset(self, rruleset):
- # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
- try:
- dtstart = self.dtstart.value
- except (AttributeError, KeyError):
- if self.name == "VTODO":
- dtstart = self.due.value
- else:
- raise
- isDate = datetime.date == type(dtstart)
- if isDate:
- dtstart = datetime.datetime(dtstart.year, dtstart.month, dtstart.day)
- untilSerialize = dateToString
- else:
- # make sure to convert time zones to UTC
- untilSerialize = lambda x: dateTimeToString(x, True)
- for name in DATESANDRULES:
- if name in self.contents:
- del self.contents[name]
- setlist = getattr(rruleset, '_' + name)
- if name in DATENAMES:
- setlist = list(setlist) # make a copy of the list
- if name == 'rdate' and dtstart in setlist:
- setlist.remove(dtstart)
- if isDate:
- setlist = [dt.date() for dt in setlist]
- if len(setlist) > 0:
- self.add(name).value = setlist
- elif name in RULENAMES:
- for rule in setlist:
- buf = six.StringIO()
- buf.write('FREQ=')
- buf.write(FREQUENCIES[rule._freq])
- values = {}
- if rule._interval != 1:
- values['INTERVAL'] = [str(rule._interval)]
- if rule._wkst != 0: # wkst defaults to Monday
- values['WKST'] = [WEEKDAYS[rule._wkst]]
- if rule._bysetpos is not None:
- values['BYSETPOS'] = [str(i) for i in rule._bysetpos]
- if rule._count is not None:
- values['COUNT'] = [str(rule._count)]
- elif rule._until is not None:
- values['UNTIL'] = [untilSerialize(rule._until)]
- days = []
- if (rule._byweekday is not None and (
- rrule.WEEKLY != rule._freq or
- len(rule._byweekday) != 1 or
- rule._dtstart.weekday() != rule._byweekday[0])):
- # ignore byweekday if freq is WEEKLY and day correlates
- # with dtstart because it was automatically set by dateutil
- days.extend(WEEKDAYS[n] for n in rule._byweekday)
- if rule._bynweekday is not None:
- days.extend(n + WEEKDAYS[day] for day, n in rule._bynweekday)
- if len(days) > 0:
- values['BYDAY'] = days
- if rule._bymonthday is not None and len(rule._bymonthday) > 0:
- if not (rule._freq <= rrule.MONTHLY and
- len(rule._bymonthday) == 1 and
- rule._bymonthday[0] == rule._dtstart.day):
- # ignore bymonthday if it's generated by dateutil
- values['BYMONTHDAY'] = [str(n) for n in rule._bymonthday]
- if rule._bynmonthday is not None and len(rule._bynmonthday) > 0:
- values.setdefault('BYMONTHDAY', []).extend(str(n) for n in rule._bynmonthday)
- if rule._bymonth is not None and len(rule._bymonth) > 0:
- if (rule._byweekday is not None or
- len(rule._bynweekday or ()) > 0 or
- not (rule._freq == rrule.YEARLY and
- len(rule._bymonth) == 1 and
- rule._bymonth[0] == rule._dtstart.month)):
- # ignore bymonth if it's generated by dateutil
- values['BYMONTH'] = [str(n) for n in rule._bymonth]
- if rule._byyearday is not None:
- values['BYYEARDAY'] = [str(n) for n in rule._byyearday]
- if rule._byweekno is not None:
- values['BYWEEKNO'] = [str(n) for n in rule._byweekno]
- # byhour, byminute, bysecond are always ignored for now
- for key, paramvals in values.items():
- buf.write(';')
- buf.write(key)
- buf.write('=')
- buf.write(','.join(paramvals))
- self.add(name).value = buf.getvalue()
- rruleset = property(getrruleset, setrruleset)
- def __setattr__(self, name, value):
- """
- For convenience, make self.contents directly accessible.
- """
- if name == 'rruleset':
- self.setrruleset(value)
- else:
- super(RecurringComponent, self).__setattr__(name, value)
- class TextBehavior(behavior.Behavior):
- """
- Provide backslash escape encoding/decoding for single valued properties.
- TextBehavior also deals with base64 encoding if the ENCODING parameter is
- explicitly set to BASE64.
- """
- base64string = 'BASE64' # vCard uses B
- @classmethod
- def decode(cls, line):
- """
- Remove backslash escaping from line.value.
- """
- if line.encoded:
- encoding = getattr(line, 'encoding_param', None)
- if encoding and encoding.upper() == cls.base64string:
- line.value = base64.b64decode(line.value)
- else:
- line.value = stringToTextValues(line.value)[0]
- line.encoded = False
- @classmethod
- def encode(cls, line):
- """
- Backslash escape line.value.
- """
- if not line.encoded:
- encoding = getattr(line, 'encoding_param', None)
- if encoding and encoding.upper() == cls.base64string:
- line.value = base64.b64encode(line.value.encode('utf-8')).decode('utf-8').replace('\n', '')
- else:
- line.value = backslashEscape(line.value)
- line.encoded = True
- class VCalendarComponentBehavior(behavior.Behavior):
- defaultBehavior = TextBehavior
- isComponent = True
- class RecurringBehavior(VCalendarComponentBehavior):
- """
- Parent Behavior for components which should be RecurringComponents.
- """
- hasNative = True
- @staticmethod
- def transformToNative(obj):
- """
- Turn a recurring Component into a RecurringComponent.
- """
- if not obj.isNative:
- object.__setattr__(obj, '__class__', RecurringComponent)
- obj.isNative = True
- return obj
- @staticmethod
- def transformFromNative(obj):
- if obj.isNative:
- object.__setattr__(obj, '__class__', Component)
- obj.isNative = False
- return obj
- @staticmethod
- def generateImplicitParameters(obj):
- """
- Generate a UID and DTSTAMP if one does not exist.
- This is just a dummy implementation, for now.
- """
- if not hasattr(obj, 'uid'):
- rand = int(random.random() * 100000)
- now = datetime.datetime.now(utc)
- now = dateTimeToString(now)
- host = socket.gethostname()
- obj.add(ContentLine('UID', [], "{0} - {1}@{2}".format(now, rand,
- host)))
- if not hasattr(obj, 'dtstamp'):
- now = datetime.datetime.now(utc)
- obj.add('dtstamp').value = now
- class DateTimeBehavior(behavior.Behavior):
- """
- Parent Behavior for ContentLines containing one DATE-TIME.
- """
- hasNative = True
- @staticmethod
- def transformToNative(obj):
- """
- Turn obj.value into a datetime.
- RFC2445 allows times without time zone information, "floating times"
- in some properties. Mostly, this isn't what you want, but when parsing
- a file, real floating times are noted by setting to 'TRUE' the
- X-VOBJ-FLOATINGTIME-ALLOWED parameter.
- """
- if obj.isNative:
- return obj
- obj.isNative = True
- if obj.value == '':
- return obj
- obj.value = obj.value
- # we're cheating a little here, parseDtstart allows DATE
- obj.value = parseDtstart(obj)
- if obj.value.tzinfo is None:
- obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE']
- if obj.params.get('TZID'):
- # Keep a copy of the original TZID around
- obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.params['TZID']]
- del obj.params['TZID']
- return obj
- @classmethod
- def transformFromNative(cls, obj):
- """
- Replace the datetime in obj.value with an ISO 8601 string.
- """
- if obj.isNative:
- obj.isNative = False
- tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo)
- obj.value = dateTimeToString(obj.value, cls.forceUTC)
- if not cls.forceUTC and tzid is not None:
- obj.tzid_param = tzid
- if obj.params.get('X-VOBJ-ORIGINAL-TZID'):
- if not hasattr(obj, 'tzid_param'):
- obj.tzid_param = obj.x_vobj_original_tzid_param
- del obj.params['X-VOBJ-ORIGINAL-TZID']
- return obj
- class UTCDateTimeBehavior(DateTimeBehavior):
- """
- A value which must be specified in UTC.
- """
- forceUTC = True
- class DateOrDateTimeBehavior(behavior.Behavior):
- """
- Parent Behavior for ContentLines containing one DATE or DATE-TIME.
- """
- hasNative = True
- @staticmethod
- def transformToNative(obj):
- """
- Turn obj.value into a date or datetime.
- """
- if obj.isNative:
- return obj
- obj.isNative = True
- if obj.value == '':
- return obj
- obj.value = obj.value
- obj.value = parseDtstart(obj, allowSignatureMismatch=True)
- if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME':
- if hasattr(obj, 'tzid_param'):
- # Keep a copy of the original TZID around
- obj.params['X-VOBJ-ORIGINAL-TZID'] = [obj.tzid_param]
- del obj.tzid_param
- return obj
- @staticmethod
- def transformFromNative(obj):
- """
- Replace the date or datetime in obj.value with an ISO 8601 string.
- """
- if type(obj.value) == datetime.date:
- obj.isNative = False
- obj.value_param = 'DATE'
- obj.value = dateToString(obj.value)
- return obj
- else:
- return DateTimeBehavior.transformFromNative(obj)
- class MultiDateBehavior(behavior.Behavior):
- """
- Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or
- PERIOD.
- """
- hasNative = True
- @staticmethod
- def transformToNative(obj):
- """
- Turn obj.value into a list of dates, datetimes, or
- (datetime, timedelta) tuples.
- """
- if obj.isNative:
- return obj
- obj.isNative = True
- if obj.value == '':
- obj.value = []
- return obj
- tzinfo = getTzid(getattr(obj, 'tzid_param', None))
- valueParam = getattr(obj, 'value_param', "DATE-TIME").upper()
- valTexts = obj.value.split(",")
- if valueParam == "DATE":
- obj.value = [stringToDate(x) for x in valTexts]
- elif valueParam == "DATE-TIME":
- obj.value = [stringToDateTime(x, tzinfo) for x in valTexts]
- elif valueParam == "PERIOD":
- obj.value = [stringToPeriod(x, tzinfo) for x in valTexts]
- return obj
- @staticmethod
- def transformFromNative(obj):
- """
- Replace the date, datetime or period tuples in obj.value with
- appropriate strings.
- """
- if obj.value and type(obj.value[0]) == datetime.date:
- obj.isNative = False
- obj.value_param = 'DATE'
- obj.value = ','.join([dateToString(val) for val in obj.value])
- return obj
- # Fixme: handle PERIOD case
- else:
- if obj.isNative:
- obj.isNative = False
- transformed = []
- tzid = None
- for val in obj.value:
- if tzid is None and type(val) == datetime.datetime:
- tzid = TimezoneComponent.registerTzinfo(val.tzinfo)
- if tzid is not None:
- obj.tzid_param = tzid
- transformed.append(dateTimeToString(val))
- obj.value = ','.join(transformed)
- return obj
- class MultiTextBehavior(behavior.Behavior):
- """
- Provide backslash escape encoding/decoding of each of several values.
- After transformation, value is a list of strings.
- """
- listSeparator = ","
- @classmethod
- def decode(cls, line):
- """
- Remove backslash escaping from line.value, then split on commas.
- """
- if line.encoded:
- line.value = stringToTextValues(line.value,
- listSeparator=cls.listSeparator)
- line.encoded = False
- @classmethod
- def encode(cls, line):
- """
- Backslash escape line.value.
- """
- if not line.encoded:
- line.value = cls.listSeparator.join(backslashEscape(val)
- for val in line.value)
- line.encoded = True
- class SemicolonMultiTextBehavior(MultiTextBehavior):
- listSeparator = ";"
- # ------------------------ Registered Behavior subclasses ----------------------
- class VCalendar2_0(VCalendarComponentBehavior):
- """
- vCalendar 2.0 behavior. With added VAVAILABILITY support.
- """
- name = 'VCALENDAR'
- description = 'vCalendar 2.0, also known as iCalendar.'
- versionString = '2.0'
- sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone')
- knownChildren = {
- 'CALSCALE': (0, 1, None), # min, max, behaviorRegistry id
- 'METHOD': (0, 1, None),
- 'VERSION': (0, 1, None), # required, but auto-generated
- 'PRODID': (1, 1, None),
- 'VTIMEZONE': (0, None, None),
- 'VEVENT': (0, None, None),
- 'VTODO': (0, None, None),
- 'VJOURNAL': (0, None, None),
- 'VFREEBUSY': (0, None, None),
- 'VAVAILABILITY': (0, None, None),
- }
- @classmethod
- def generateImplicitParameters(cls, obj):
- """
- Create PRODID, VERSION and VTIMEZONEs if needed.
- VTIMEZONEs will need to exist whenever TZID parameters exist or when
- datetimes with tzinfo exist.
- """
- for comp in obj.components():
- if comp.behavior is not None:
- comp.behavior.generateImplicitParameters(comp)
- if not hasattr(obj, 'prodid'):
- obj.add(ContentLine('PRODID', [], PRODID))
- if not hasattr(obj, 'version'):
- obj.add(ContentLine('VERSION', [], cls.versionString))
- tzidsUsed = {}
- def findTzids(obj, table):
- if isinstance(obj, ContentLine) and (obj.behavior is None or
- not obj.behavior.forceUTC):
- if getattr(obj, 'tzid_param', None):
- table[obj.tzid_param] = 1
- else:
- if type(obj.value) == list:
- for item in obj.value:
- tzinfo = getattr(obj.value, 'tzinfo', None)
- tzid = TimezoneComponent.registerTzinfo(tzinfo)
- if tzid:
- table[tzid] = 1
- else:
- tzinfo = getattr(obj.value, 'tzinfo', None)
- tzid = TimezoneComponent.registerTzinfo(tzinfo)
- if tzid:
- table[tzid] = 1
- for child in obj.getChildren():
- if obj.name != 'VTIMEZONE':
- findTzids(child, table)
- findTzids(obj, tzidsUsed)
- oldtzids = [toUnicode(x.tzid.value) for x in getattr(obj, 'vtimezone_list', [])]
- for tzid in tzidsUsed.keys():
- tzid = toUnicode(tzid)
- if tzid != u'UTC' and tzid not in oldtzids:
- obj.add(TimezoneComponent(tzinfo=getTzid(tzid)))
- @classmethod
- def serialize(cls, obj, buf, lineLength, validate=True):
- """
- Set implicit parameters, do encoding, return unicode string.
- If validate is True, raise VObjectError if the line doesn't validate
- after implicit parameters are generated.
- Default is to call base.defaultSerialize.
- """
- cls.generateImplicitParameters(obj)
- if validate:
- cls.validate(obj, raiseException=True)
- if obj.isNative:
- transformed = obj.transformFromNative()
- undoTransform = True
- else:
- transformed = obj
- undoTransform = False
- out = None
- outbuf = buf or six.StringIO()
- if obj.group is None:
- groupString = ''
- else:
- groupString = obj.group + '.'
- if obj.useBegin:
- foldOneLine(outbuf, "{0}BEGIN:{1}".format(groupString, obj.name),
- lineLength)
- try:
- first_props = [s for s in cls.sortFirst if s in obj.contents \
- and not isinstance(obj.contents[s][0], Component)]
- first_components = [s for s in cls.sortFirst if s in obj.contents \
- and isinstance(obj.contents[s][0], Component)]
- except Exception:
- first_props = first_components = []
- # first_components = []
- prop_keys = sorted(list(k for k in obj.contents.keys() if k not in first_props \
- and not isinstance(obj.contents[k][0], Component)))
- comp_keys = sorted(list(k for k in obj.contents.keys() if k not in first_components \
- and isinstance(obj.contents[k][0], Component)))
- sorted_keys = first_props + prop_keys + first_components + comp_keys
- children = [o for k in sorted_keys for o in obj.contents[k]]
- for child in children:
- # validate is recursive, we only need to validate once
- child.serialize(outbuf, lineLength, validate=False)
- if obj.useBegin:
- foldOneLine(outbuf, "{0}END:{1}".format(groupString, obj.name),
- lineLength)
- out = buf or outbuf.getvalue()
- if undoTransform:
- obj.transformToNative()
- return out
- registerBehavior(VCalendar2_0)
- class VTimezone(VCalendarComponentBehavior):
- """
- Timezone behavior.
- """
- name = 'VTIMEZONE'
- hasNative = True
- description = 'A grouping of component properties that defines a time zone.'
- sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight')
- knownChildren = {
- 'TZID': (1, 1, None), # min, max, behaviorRegistry id
- 'LAST-MODIFIED': (0, 1, None),
- 'TZURL': (0, 1, None),
- 'STANDARD': (0, None, None), # NOTE: One of Standard or
- 'DAYLIGHT': (0, None, None) # Daylight must appear
- }
- @classmethod
- def validate(cls, obj, raiseException, *args):
- if not hasattr(obj, 'tzid') or obj.tzid.value is None:
- if raiseException:
- m = "VTIMEZONE components must contain a valid TZID"
- raise ValidateError(m)
- return False
- if 'standard' in obj.contents or 'daylight' in obj.contents:
- return super(VTimezone, cls).validate(obj, raiseException, *args)
- else:
- if raiseException:
- m = "VTIMEZONE components must contain a STANDARD or a DAYLIGHT\
- component"
- raise ValidateError(m)
- return False
- @staticmethod
- def transformToNative(obj):
- if not obj.isNative:
- object.__setattr__(obj, '__class__', TimezoneComponent)
- obj.isNative = True
- obj.registerTzinfo(obj.tzinfo)
- return obj
- @staticmethod
- def transformFromNative(obj):
- return obj
- registerBehavior(VTimezone)
- class TZID(behavior.Behavior):
- """
- Don't use TextBehavior for TZID.
- RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any
- encoding or decoding. Unfortunately, some Microsoft products use commas
- in TZIDs which should NOT be treated as a multi-valued text property, nor
- do we want to escape them. Leaving them alone works for Microsoft's breakage,
- and doesn't affect compliant iCalendar streams.
- """
- registerBehavior(TZID)
- class DaylightOrStandard(VCalendarComponentBehavior):
- hasNative = False
- knownChildren = {'DTSTART': (1, 1, None), # min, max, behaviorRegistry id
- 'RRULE': (0, 1, None)}
- registerBehavior(DaylightOrStandard, 'STANDARD')
- registerBehavior(DaylightOrStandard, 'DAYLIGHT')
- class VEvent(RecurringBehavior):
- """
- Event behavior.
- """
- name = 'VEVENT'
- sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
- description = 'A grouping of component properties, and possibly including \
- "VALARM" calendar components, that represents a scheduled \
- amount of time on a calendar.'
- knownChildren = {
- 'DTSTART': (0, 1, None), # min, max, behaviorRegistry id
- 'CLASS': (0, 1, None),
- 'CREATED': (0, 1, None),
- 'DESCRIPTION': (0, 1, None),
- 'GEO': (0, 1, None),
- 'LAST-MODIFIED': (0, 1, None),
- 'LOCATION': (0, 1, None),
- 'ORGANIZER': (0, 1, None),
- 'PRIORITY': (0, 1, None),
- 'DTSTAMP': (1, 1, None), # required
- 'SEQUENCE': (0, 1, None),
- 'STATUS': (0, 1, None),
- 'SUMMARY': (0, 1, None),
- 'TRANSP': (0, 1, None),
- 'UID': (1, 1, None),
- 'URL': (0, 1, None),
- 'RECURRENCE-ID': (0, 1, None),
- 'DTEND': (0, 1, None), # NOTE: Only one of DtEnd or
- 'DURATION': (0, 1, None), # Duration can appear
- 'ATTACH': (0, None, None),
- 'ATTENDEE': (0, None, None),
- 'CATEGORIES': (0, None, None),
- 'COMMENT': (0, None, None),
- 'CONTACT': (0, None, None),
- 'EXDATE': (0, None, None),
- 'EXRULE': (0, None, None),
- 'REQUEST-STATUS': (0, None, None),
- 'RELATED-TO': (0, None, None),
- 'RESOURCES': (0, None, None),
- 'RDATE': (0, None, None),
- 'RRULE': (0, None, None),
- 'VALARM': (0, None, None)
- }
- @classmethod
- def validate(cls, obj, raiseException, *args):
- if 'dtend' in obj.contents and 'duration' in obj.contents:
- if raiseException:
- m = "VEVENT components cannot contain both DTEND and DURATION\
- components"
- raise ValidateError(m)
- return False
- else:
- return super(VEvent, cls).validate(obj, raiseException, *args)
- registerBehavior(VEvent)
- class VTodo(RecurringBehavior):
- """
- To-do behavior.
- """
- name = 'VTODO'
- description = 'A grouping of component properties and possibly "VALARM" \
- calendar components that represent an action-item or \
- assignment.'
- knownChildren = {
- 'DTSTART': (0, 1, None), # min, max, behaviorRegistry id
- 'CLASS': (0, 1, None),
- 'COMPLETED': (0, 1, None),
- 'CREATED': (0, 1, None),
- 'DESCRIPTION': (0, 1, None),
- 'GEO': (0, 1, None),
- 'LAST-MODIFIED': (0, 1, None),
- 'LOCATION': (0, 1, None),
- 'ORGANIZER': (0, 1, None),
- 'PERCENT': (0, 1, None),
- 'PRIORITY': (0, 1, None),
- 'DTSTAMP': (1, 1, None),
- 'SEQUENCE': (0, 1, None),
- 'STATUS': (0, 1, None),
- 'SUMMARY': (0, 1, None),
- 'UID': (0, 1, None),
- 'URL': (0, 1, None),
- 'RECURRENCE-ID': (0, 1, None),
- 'DUE': (0, 1, None), # NOTE: Only one of Due or
- 'DURATION': (0, 1, None), # Duration can appear
- 'ATTACH': (0, None, None),
- 'ATTENDEE': (0, None, None),
- 'CATEGORIES': (0, None, None),
- 'COMMENT': (0, None, None),
- 'CONTACT': (0, None, None),
- 'EXDATE': (0, None, None),
- 'EXRULE': (0, None, None),
- 'REQUEST-STATUS': (0, None, None),
- 'RELATED-TO': (0, None, None),
- 'RESOURCES': (0, None, None),
- 'RDATE': (0, None, None),
- 'RRULE': (0, None, None),
- 'VALARM': (0, None, None)
- }
- @classmethod
- def validate(cls, obj, raiseException, *args):
- if 'due' in obj.contents and 'duration' in obj.contents:
- if raiseException:
- m = "VTODO components cannot contain both DUE and DURATION\
- components"
- raise ValidateError(m)
- return False
- else:
- return super(VTodo, cls).validate(obj, raiseException, *args)
- registerBehavior(VTodo)
- class VJournal(RecurringBehavior):
- """
- Journal entry behavior.
- """
- name = 'VJOURNAL'
- knownChildren = {
- 'DTSTART': (0, 1, None), # min, max, behaviorRegistry id
- 'CLASS': (0, 1, None),
- 'CREATED': (0, 1, None),
- 'DESCRIPTION': (0, 1, None),
- 'LAST-MODIFIED': (0, 1, None),
- 'ORGANIZER': (0, 1, None),
- 'DTSTAMP': (1, 1, None),
- 'SEQUENCE': (0, 1, None),
- 'STATUS': (0, 1, None),
- 'SUMMARY': (0, 1, None),
- 'UID': (0, 1, None),
- 'URL': (0, 1, None),
- 'RECURRENCE-ID': (0, 1, None),
- 'ATTACH': (0, None, None),
- 'ATTENDEE': (0, None, None),
- 'CATEGORIES': (0, None, None),
- 'COMMENT': (0, None, None),
- 'CONTACT': (0, None, None),
- 'EXDATE': (0, None, None),
- 'EXRULE': (0, None, None),
- 'REQUEST-STATUS': (0, None, None),
- 'RELATED-TO': (0, None, None),
- 'RDATE': (0, None, None),
- 'RRULE': (0, None, None)
- }
- registerBehavior(VJournal)
- class VFreeBusy(VCalendarComponentBehavior):
- """
- Free/busy state behavior.
- """
- name = 'VFREEBUSY'
- description = 'A grouping of component properties that describe either a \
- request for free/busy time, describe a response to a request \
- for free/busy time or describe a published set of busy time.'
- sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
- knownChildren = {
- 'DTSTART': (0, 1, None), # min, max, behaviorRegistry id
- 'CONTACT': (0, 1, None),
- 'DTEND': (0, 1, None),
- 'DURATION': (0, 1, None),
- 'ORGANIZER': (0, 1, None),
- 'DTSTAMP': (1, 1, None),
- 'UID': (0, 1, None),
- 'URL': (0, 1, None),
- 'ATTENDEE': (0, None, None),
- 'COMMENT': (0, None, None),
- 'FREEBUSY': (0, None, None),
- 'REQUEST-STATUS': (0, None, None)
- }
- registerBehavior(VFreeBusy)
- class VAlarm(VCalendarComponentBehavior):
- """
- Alarm behavior.
- """
- name = 'VALARM'
- description = 'Alarms describe when and how to provide alerts about events \
- and to-dos.'
- knownChildren = {
- 'ACTION': (1, 1, None), # min, max, behaviorRegistry id
- 'TRIGGER': (1, 1, None),
- 'DURATION': (0, 1, None),
- 'REPEAT': (0, 1, None),
- 'DESCRIPTION': (0, 1, None)
- }
- @staticmethod
- def generateImplicitParameters(obj):
- """
- Create default ACTION and TRIGGER if they're not set.
- """
- try:
- obj.action
- except AttributeError:
- obj.add('action').value = 'AUDIO'
- try:
- obj.trigger
- except AttributeError:
- obj.add('trigger').value = datetime.timedelta(0)
- @classmethod
- def validate(cls, obj, raiseException, *args):
- """
- # TODO
- if obj.contents.has_key('dtend') and obj.contents.has_key('duration'):
- if raiseException:
- m = "VEVENT components cannot contain both DTEND and DURATION\
- components"
- raise ValidateError(m)
- return False
- else:
- return super(VEvent, cls).validate(obj, raiseException, *args)
- """
- return True
- registerBehavior(VAlarm)
- class VAvailability(VCalendarComponentBehavior):
- """
- Availability state behavior.
- Used to represent user's available time slots.
- """
- name = 'VAVAILABILITY'
- description = 'A component used to represent a user\'s available time slots.'
- sortFirst = ('uid', 'dtstart', 'duration', 'dtend')
- knownChildren = {
- 'UID': (1, 1, None), # min, max, behaviorRegistry id
- 'DTSTAMP': (1, 1, None),
- 'BUSYTYPE': (0, 1, None),
- 'CREATED': (0, 1, None),
- 'DTSTART': (0, 1, None),
- 'LAST-MODIFIED': (0, 1, None),
- 'ORGANIZER': (0, 1, None),
- 'SEQUENCE': (0, 1, None),
- 'SUMMARY': (0, 1, None),
- 'URL': (0, 1, None),
- 'DTEND': (0, 1, None),
- 'DURATION': (0, 1, None),
- 'CATEGORIES': (0, None, None),
- 'COMMENT': (0, None, None),
- 'CONTACT': (0, None, None),
- 'AVAILABLE': (0, None, None),
- }
- @classmethod
- def validate(cls, obj, raiseException, *args):
- if 'dtend' in obj.contents and 'duration' in obj.contents:
- if raiseException:
- m = "VAVAILABILITY components cannot contain both DTEND and DURATION components"
- raise ValidateError(m)
- return False
- else:
- return super(VAvailability, cls).validate(obj, raiseException, *args)
- registerBehavior(VAvailability)
- class Available(RecurringBehavior):
- """
- Event behavior.
- """
- name = 'AVAILABLE'
- sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend')
- description = 'Defines a period of time in which a user is normally available.'
- knownChildren = {
- 'DTSTAMP': (1, 1, None), # min, max, behaviorRegistry id
- 'DTSTART': (1, 1, None),
- 'UID': (1, 1, None),
- 'DTEND': (0, 1, None), # NOTE: One of DtEnd or
- 'DURATION': (0, 1, None), # Duration must appear, but not both
- 'CREATED': (0, 1, None),
- 'LAST-MODIFIED': (0, 1, None),
- 'RECURRENCE-ID': (0, 1, None),
- 'RRULE': (0, 1, None),
- 'SUMMARY': (0, 1, None),
- 'CATEGORIES': (0, None, None),
- 'COMMENT': (0, None, None),
- 'CONTACT': (0, None, None),
- 'EXDATE': (0, None, None),
- 'RDATE': (0, None, None),
- }
- @classmethod
- def validate(cls, obj, raiseException, *args):
- has_dtend = 'dtend' in obj.contents
- has_duration = 'duration' in obj.contents
- if has_dtend and has_duration:
- if raiseException:
- m = "AVAILABLE components cannot contain both DTEND and DURATION\
- properties"
- raise ValidateError(m)
- return False
- elif not (has_dtend or has_duration):
- if raiseException:
- m = "AVAILABLE components must contain one of DTEND or DURATION\
- properties"
- raise ValidateError(m)
- return False
- else:
- return super(Available, cls).validate(obj, raiseException, *args)
- registerBehavior(Available)
- class Duration(behavior.Behavior):
- """
- Behavior for Duration ContentLines. Transform to datetime.timedelta.
- """
- name = 'DURATION'
- hasNative = True
- @staticmethod
- def transformToNative(obj):
- """
- Turn obj.value into a datetime.timedelta.
- """
- if obj.isNative:
- return obj
- obj.isNative = True
- obj.value = obj.value
- if obj.value == '':
- return obj
- else:
- deltalist = stringToDurations(obj.value)
- # When can DURATION have multiple durations? For now:
- if len(deltalist) == 1:
- obj.value = deltalist[0]
- return obj
- else:
- raise ParseError("DURATION must have a single duration string.")
- @staticmethod
- def transformFromNative(obj):
- """
- Replace the datetime.timedelta in obj.value with an RFC2445 string.
- """
- if not obj.isNative:
- return obj
- obj.isNative = False
- obj.value = timedeltaToString(obj.value)
- return obj
- registerBehavior(Duration)
- class Trigger(behavior.Behavior):
- """
- DATE-TIME or DURATION
- """
- name = 'TRIGGER'
- description = 'This property specifies when an alarm will trigger.'
- hasNative = True
- forceUTC = True
- @staticmethod
- def transformToNative(obj):
- """
- Turn obj.value into a timedelta or datetime.
- """
- if obj.isNative:
- return obj
- value = getattr(obj, 'value_param', 'DURATION').upper()
- if hasattr(obj, 'value_param'):
- del obj.value_param
- if obj.value == '':
- obj.isNative = True
- return obj
- elif value == 'DURATION':
- try:
- return Duration.transformToNative(obj)
- except ParseError:
- logger.warning("TRIGGER not recognized as DURATION, trying "
- "DATE-TIME, because iCal sometimes exports "
- "DATE-TIMEs without setting VALUE=DATE-TIME")
- try:
- obj.isNative = False
- dt = DateTimeBehavior.transformToNative(obj)
- return dt
- except:
- msg = "TRIGGER with no VALUE not recognized as DURATION " \
- "or as DATE-TIME"
- raise ParseError(msg)
- elif value == 'DATE-TIME':
- # TRIGGERs with DATE-TIME values must be in UTC, we could validate
- # that fact, for now we take it on faith.
- return DateTimeBehavior.transformToNative(obj)
- else:
- raise ParseError("VALUE must be DURATION or DATE-TIME")
- @staticmethod
- def transformFromNative(obj):
- if type(obj.value) == datetime.datetime:
- obj.value_param = 'DATE-TIME'
- return UTCDateTimeBehavior.transformFromNative(obj)
- elif type(obj.value) == datetime.timedelta:
- return Duration.transformFromNative(obj)
- else:
- raise NativeError("Native TRIGGER values must be timedelta or "
- "datetime")
- registerBehavior(Trigger)
- class PeriodBehavior(behavior.Behavior):
- """
- A list of (date-time, timedelta) tuples.
- """
- hasNative = True
- @staticmethod
- def transformToNative(obj):
- """
- Convert comma separated periods into tuples.
- """
- if obj.isNative:
- return obj
- obj.isNative = True
- if obj.value == '':
- obj.value = []
- return obj
- tzinfo = getTzid(getattr(obj, 'tzid_param', None))
- obj.value = [stringToPeriod(x, tzinfo) for x in obj.value.split(",")]
- return obj
- @classmethod
- def transformFromNative(cls, obj):
- """
- Convert the list of tuples in obj.value to strings.
- """
- if obj.isNative:
- obj.isNative = False
- transformed = []
- for tup in obj.value:
- transformed.append(periodToString(tup, cls.forceUTC))
- if len(transformed) > 0:
- tzid = TimezoneComponent.registerTzinfo(tup[0].tzinfo)
- if not cls.forceUTC and tzid is not None:
- obj.tzid_param = tzid
- obj.value = ','.join(transformed)
- return obj
- class FreeBusy(PeriodBehavior):
- """
- Free or busy period of time, must be specified in UTC.
- """
- name = 'FREEBUSY'
- forceUTC = True
- registerBehavior(FreeBusy, 'FREEBUSY')
- class RRule(behavior.Behavior):
- """
- Dummy behavior to avoid having RRULEs being treated as text lines (and thus
- having semi-colons inaccurately escaped).
- """
- registerBehavior(RRule, 'RRULE')
- registerBehavior(RRule, 'EXRULE')
- # ------------------------ Registration of common classes ----------------------
- utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP']
- list(map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList))
- dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID']
- list(map(lambda x: registerBehavior(DateOrDateTimeBehavior, x),
- dateTimeOrDateList))
- registerBehavior(MultiDateBehavior, 'RDATE')
- registerBehavior(MultiDateBehavior, 'EXDATE')
- textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION',
- 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO',
- 'UID', 'ACTION', 'BUSYTYPE']
- list(map(lambda x: registerBehavior(TextBehavior, x), textList))
- list(map(lambda x: registerBehavior(MultiTextBehavior, x), ['CATEGORIES',
- 'RESOURCES']))
- registerBehavior(SemicolonMultiTextBehavior, 'REQUEST-STATUS')
- # ------------------------ Serializing helper functions ------------------------
- def numToDigits(num, places):
- """
- Helper, for converting numbers to textual digits.
- """
- s = str(num)
- if len(s) < places:
- return ("0" * (places - len(s))) + s
- elif len(s) > places:
- return s[len(s)-places:]
- else:
- return s
- def timedeltaToString(delta):
- """
- Convert timedelta to an ical DURATION.
- """
- if delta.days == 0:
- sign = 1
- else:
- sign = delta.days / abs(delta.days)
- delta = abs(delta)
- days = delta.days
- hours = int(delta.seconds / 3600)
- minutes = int((delta.seconds % 3600) / 60)
- seconds = int(delta.seconds % 60)
- output = ''
- if sign == -1:
- output += '-'
- output += 'P'
- if days:
- output += '{}D'.format(days)
- if hours or minutes or seconds:
- output += 'T'
- elif not days: # Deal with zero duration
- output += 'T0S'
- if hours:
- output += '{}H'.format(hours)
- if minutes:
- output += '{}M'.format(minutes)
- if seconds:
- output += '{}S'.format(seconds)
- return output
- def timeToString(dateOrDateTime):
- """
- Wraps dateToString and dateTimeToString, returning the results
- of either based on the type of the argument
- """
- if hasattr(dateOrDateTime, 'hour'):
- return dateTimeToString(dateOrDateTime)
- return dateToString(dateOrDateTime)
- def dateToString(date):
- year = numToDigits(date.year, 4)
- month = numToDigits(date.month, 2)
- day = numToDigits(date.day, 2)
- return year + month + day
- def dateTimeToString(dateTime, convertToUTC=False):
- """
- Ignore tzinfo unless convertToUTC. Output string.
- """
- if dateTime.tzinfo and convertToUTC:
- dateTime = dateTime.astimezone(utc)
- datestr = "{0}{1}{2}T{3}{4}{5}".format(
- numToDigits(dateTime.year, 4),
- numToDigits(dateTime.month, 2),
- numToDigits(dateTime.day, 2),
- numToDigits(dateTime.hour, 2),
- numToDigits(dateTime.minute, 2),
- numToDigits(dateTime.second, 2),
- )
- if tzinfo_eq(dateTime.tzinfo, utc):
- datestr += "Z"
- return datestr
- def deltaToOffset(delta):
- absDelta = abs(delta)
- hours = int(absDelta.seconds / 3600)
- hoursString = numToDigits(hours, 2)
- minutesString = '00'
- if absDelta == delta:
- signString = "+"
- else:
- signString = "-"
- return signString + hoursString + minutesString
- def periodToString(period, convertToUTC=False):
- txtstart = dateTimeToString(period[0], convertToUTC)
- if isinstance(period[1], datetime.timedelta):
- txtend = timedeltaToString(period[1])
- else:
- txtend = dateTimeToString(period[1], convertToUTC)
- return txtstart + "/" + txtend
- # ----------------------- Parsing functions ------------------------------------
- def isDuration(s):
- s = s.upper()
- return (s.find("P") != -1) and (s.find("P") < 2)
- def stringToDate(s):
- year = int(s[0:4])
- month = int(s[4:6])
- day = int(s[6:8])
- return datetime.date(year, month, day)
- def stringToDateTime(s, tzinfo=None):
- """
- Returns datetime.datetime object.
- """
- try:
- year = int(s[0:4])
- month = int(s[4:6])
- day = int(s[6:8])
- hour = int(s[9:11])
- minute = int(s[11:13])
- second = int(s[13:15])
- if len(s) > 15:
- if s[15] == 'Z':
- tzinfo = getTzid('UTC')
- except:
- raise ParseError("'{0!s}' is not a valid DATE-TIME".format(s))
- year = year and year or 2000
- if tzinfo is not None and hasattr(tzinfo,'localize'): # PyTZ case
- return tzinfo.localize(datetime.datetime(year, month, day, hour, minute, second))
- return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo)
- # DQUOTE included to work around iCal's penchant for backslash escaping it,
- # although it isn't actually supposed to be escaped according to rfc2445 TEXT
- escapableCharList = '\\;,Nn"'
- def stringToTextValues(s, listSeparator=',', charList=None, strict=False):
- """
- Returns list of strings.
- """
- if charList is None:
- charList = escapableCharList
- def escapableChar(c):
- return c in charList
- def error(msg):
- if strict:
- raise ParseError(msg)
- else:
- logging.error(msg)
- # vars which control state machine
- charIterator = enumerate(s)
- state = "read normal"
- current = []
- results = []
- while True:
- try:
- charIndex, char = next(charIterator)
- except:
- char = "eof"
- if state == "read normal":
- if char == '\\':
- state = "read escaped char"
- elif char == listSeparator:
- state = "read normal"
- current = "".join(current)
- results.append(current)
- current = []
- elif char == "eof":
- state = "end"
- else:
- state = "read normal"
- current.append(char)
- elif state == "read escaped char":
- if escapableChar(char):
- state = "read normal"
- if char in 'nN':
- current.append('\n')
- else:
- current.append(char)
- else:
- state = "read normal"
- # leave unrecognized escaped characters for later passes
- current.append('\\' + char)
- elif state == "end": # an end state
- if len(current) or len(results) == 0:
- current = "".join(current)
- results.append(current)
- return results
- elif state == "error": # an end state
- return results
- else:
- state = "error"
- error("unknown state: '{0!s}' reached in {1!s}".format(state, s))
- def stringToDurations(s, strict=False):
- """
- Returns list of timedelta objects.
- """
- def makeTimedelta(sign, week, day, hour, minute, sec):
- if sign == "-":
- sign = -1
- else:
- sign = 1
- week = int(week)
- day = int(day)
- hour = int(hour)
- minute = int(minute)
- sec = int(sec)
- return sign * datetime.timedelta(weeks=week, days=day, hours=hour,
- minutes=minute, seconds=sec)
- def error(msg):
- if strict:
- raise ParseError(msg)
- else:
- raise ParseError(msg)
- # vars which control state machine
- charIterator = enumerate(s)
- state = "start"
- durations = []
- current = ""
- sign = None
- week = 0
- day = 0
- hour = 0
- minute = 0
- sec = 0
- while True:
- try:
- charIndex, char = next(charIterator)
- except:
- char = "eof"
- if state == "start":
- if char == '+':
- state = "start"
- sign = char
- elif char == '-':
- state = "start"
- sign = char
- elif char.upper() == 'P':
- state = "read field"
- elif char == "eof":
- state = "error"
- error("got end-of-line while reading in duration: " + s)
- elif char in string.digits:
- state = "read field"
- current = current + char # update this part when updating "read field"
- else:
- state = "error"
- error("got unexpected character {0} reading in duration: {1}"
- .format(char, s))
- elif state == "read field":
- if (char in string.digits):
- state = "read field"
- current = current + char # update part above when updating "read field"
- elif char.upper() == 'T':
- state = "read field"
- elif char.upper() == 'W':
- state = "read field"
- week = current
- current = ""
- elif char.upper() == 'D':
- state = "read field"
- day = current
- current = ""
- elif char.upper() == 'H':
- state = "read field"
- hour = current
- current = ""
- elif char.upper() == 'M':
- state = "read field"
- minute = current
- current = ""
- elif char.upper() == 'S':
- state = "read field"
- sec = current
- current = ""
- elif char == ",":
- state = "start"
- durations.append(makeTimedelta(sign, week, day, hour, minute,
- sec))
- current = ""
- sign = None
- week = None
- day = None
- hour = None
- minute = None
- sec = None
- elif char == "eof":
- state = "end"
- else:
- state = "error"
- error("got unexpected character reading in duration: " + s)
- elif state == "end": # an end state
- if (sign or week or day or hour or minute or sec):
- durations.append(makeTimedelta(sign, week, day, hour, minute,
- sec))
- return durations
- elif state == "error": # an end state
- error("in error state")
- return durations
- else:
- state = "error"
- error("unknown state: '{0!s}' reached in {1!s}".format(state, s))
- def parseDtstart(contentline, allowSignatureMismatch=False):
- """
- Convert a contentline's value into a date or date-time.
- A variety of clients don't serialize dates with the appropriate VALUE
- parameter, so rather than failing on these (technically invalid) lines,
- if allowSignatureMismatch is True, try to parse both varieties.
- """
- tzinfo = getTzid(getattr(contentline, 'tzid_param', None))
- valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper()
- if valueParam == "DATE":
- return stringToDate(contentline.value)
- elif valueParam == "DATE-TIME":
- try:
- return stringToDateTime(contentline.value, tzinfo)
- except:
- if allowSignatureMismatch:
- return stringToDate(contentline.value)
- else:
- raise
- def stringToPeriod(s, tzinfo=None):
- values = s.split("/")
- start = stringToDateTime(values[0], tzinfo)
- valEnd = values[1]
- if isDuration(valEnd): # period-start = date-time "/" dur-value
- delta = stringToDurations(valEnd)[0]
- return (start, delta)
- else:
- return (start, stringToDateTime(valEnd, tzinfo))
- def getTransition(transitionTo, year, tzinfo):
- """
- Return the datetime of the transition to/from DST, or None.
- """
- def firstTransition(iterDates, test):
- """
- Return the last date not matching test, or None if all tests matched.
- """
- success = None
- for dt in iterDates:
- if not test(dt):
- success = dt
- else:
- if success is not None:
- return success
- return success # may be None
- def generateDates(year, month=None, day=None):
- """
- Iterate over possible dates with unspecified values.
- """
- months = range(1, 13)
- days = range(1, 32)
- hours = range(0, 24)
- if month is None:
- for month in months:
- yield datetime.datetime(year, month, 1)
- elif day is None:
- for day in days:
- try:
- yield datetime.datetime(year, month, day)
- except ValueError:
- pass
- else:
- for hour in hours:
- yield datetime.datetime(year, month, day, hour)
- assert transitionTo in ('daylight', 'standard')
- if transitionTo == 'daylight':
- def test(dt):
- try:
- return tzinfo.dst(dt) != zeroDelta
- except pytz.NonExistentTimeError:
- return True # entering daylight time
- except pytz.AmbiguousTimeError:
- return False # entering standard time
- elif transitionTo == 'standard':
- def test(dt):
- try:
- return tzinfo.dst(dt) == zeroDelta
- except pytz.NonExistentTimeError:
- return False # entering daylight time
- except pytz.AmbiguousTimeError:
- return True # entering standard time
- newyear = datetime.datetime(year, 1, 1)
- monthDt = firstTransition(generateDates(year), test)
- if monthDt is None:
- return newyear
- elif monthDt.month == 12:
- return None
- else:
- # there was a good transition somewhere in a non-December month
- month = monthDt.month
- day = firstTransition(generateDates(year, month), test).day
- uncorrected = firstTransition(generateDates(year, month, day), test)
- if transitionTo == 'standard':
- # assuming tzinfo.dst returns a new offset for the first
- # possible hour, we need to add one hour for the offset change
- # and another hour because firstTransition returns the hour
- # before the transition
- return uncorrected + datetime.timedelta(hours=2)
- else:
- return uncorrected + datetime.timedelta(hours=1)
- def tzinfo_eq(tzinfo1, tzinfo2, startYear=2000, endYear=2020):
- """
- Compare offsets and DST transitions from startYear to endYear.
- """
- if tzinfo1 == tzinfo2:
- return True
- elif tzinfo1 is None or tzinfo2 is None:
- return False
- def dt_test(dt):
- if dt is None:
- return True
- return tzinfo1.utcoffset(dt) == tzinfo2.utcoffset(dt)
- if not dt_test(datetime.datetime(startYear, 1, 1)):
- return False
- for year in range(startYear, endYear):
- for transitionTo in 'daylight', 'standard':
- t1 = getTransition(transitionTo, year, tzinfo1)
- t2 = getTransition(transitionTo, year, tzinfo2)
- if t1 != t2 or not dt_test(t1):
- return False
- return True
- # ------------------- Testing and running functions ----------------------------
- if __name__ == '__main__':
- import tests
- tests._test()
|