def __init__(self): from pycalendar.icalendar.calendar import Calendar self.dbpath = None self.calendar = Calendar() self.tzcache = {} self.stdtzcache = set() self.notstdtzcache = set()
def getTimezone(self, tzid): """ Generate a PyCalendar containing the requested timezone. """ # We will just use our existing TimezoneCache here calendar = Calendar() try: vtz = readVTZ(tzid) calendar.addComponent(vtz.getComponents()[0].duplicate()) except TimezoneException: # Check if an alias exists and create data for that if tzid in self.aliases: try: vtz = readVTZ(self.aliases[tzid]) except TimezoneException: log.error("Failed to find timezone data for alias: %s" % (tzid,)) return None else: vtz = vtz.duplicate() vtz.getComponents()[0].getProperties("TZID")[0].setValue(tzid) addVTZ(tzid, vtz) calendar.addComponent(vtz.getComponents()[0].duplicate()) else: log.error("Failed to find timezone data for: %s" % (tzid,)) return None return calendar
def generateZoneinfoFiles(self, outputdir, minYear, maxYear=2018, links=True, windowsAliases=None, filterzones=None): # Empty current directory try: for root, dirs, files in os.walk(outputdir, topdown=False): for name in files: os.remove(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) except OSError: pass for zone in self.zones.itervalues(): if filterzones and zone.name not in filterzones: continue cal = Calendar() vtz = zone.vtimezone(cal, self.rules, minYear, maxYear) cal.addComponent(vtz) icsdata = cal.getText() fpath = os.path.join(outputdir, zone.name + ".ics") if not os.path.exists(os.path.dirname(fpath)): os.makedirs(os.path.dirname(fpath)) with open(fpath, "w") as f: f.write(icsdata) if self.verbose: print("Write path: %s" % (fpath,)) if links: if windowsAliases is not None: self.parseWindowsAliases(windowsAliases) link_list = [] for linkTo, linkFrom in sorted(self.links.iteritems(), key=lambda x: x[0]): # Check for existing output file fromPath = os.path.join(outputdir, linkFrom + ".ics") if not os.path.exists(fromPath): print("Missing link from: %s to %s" % (linkFrom, linkTo,)) continue with open(fromPath) as f: icsdata = f.read() icsdata = icsdata.replace(linkFrom, linkTo) toPath = os.path.join(outputdir, linkTo + ".ics") if not os.path.exists(os.path.dirname(toPath)): os.makedirs(os.path.dirname(toPath)) with open(toPath, "w") as f: f.write(icsdata) if self.verbose: print("Write link: %s" % (linkTo,)) link_list.append("%s\t%s" % (linkTo, linkFrom,)) # Generate link mapping file linkPath = os.path.join(outputdir, "links.txt") with open(linkPath, "w") as f: f.write("\n".join(link_list))
def _doNonEquality(caldata): cal1 = Calendar() cal1.parse(StringIO.StringIO(caldata)) cal2 = Calendar() cal2.parse(StringIO.StringIO(caldata)) cal2.addProperty(Property("X-FOO", "BAR")) self.assertNotEqual(cal1, cal2)
def loadCalendar(file, verbose): cal = Calendar() if verbose: print "Parsing calendar data: %s" % (file,) with open(file, "r") as fin: try: cal.parse(fin) except InvalidData, e: print "Failed to parse bad data: %s" % (e.mData,) raise
def _doRoundtrip(caldata, jcaldata): cal1 = Calendar.parseText(caldata) test1 = cal1.getText() cal2 = Calendar.parseJSONData(jcaldata) test2 = cal2.getText() self.assertEqual( test1, test2, "\n".join( difflib.unified_diff( str(test1).splitlines(), test2.splitlines())))
def _doTest(): result = None if test[0] == '@': if '=' in test: attr, value = test[1:].split('=') value = value[1:-1] else: attr = test[1:] value = None if attr not in node.keys(): result = " Missing attribute returned in XML for %s\n" % (path,) if value is not None and node.get(attr) != value: result = " Incorrect attribute value returned in XML for %s\n" % (path,) elif test[0] == '=': if node.text != test[1:]: result = " Incorrect value returned in XML for %s\n" % (path,) elif test[0] == '!': if node.text == test[1:]: result = " Incorrect value returned in XML for %s\n" % (path,) elif test[0] == '*': if node.text is None or node.text.find(test[1:]) == -1: result = " Incorrect value returned in XML for %s\n" % (path,) elif test[0] == '$': if node.text is None or node.text.find(test[1:]) != -1: result = " Incorrect value returned in XML for %s\n" % (path,) elif test[0] == '+': if node.text is None or not node.text.startswith(test[1:]): result = " Incorrect value returned in XML for %s\n" % (path,) elif test[0] == '^': if "=" in test: element, value = test[1:].split("=", 1) else: element = test[1:] value = None for child in node.getchildren(): if child.tag == element and (value is None or child.text == value): break else: result = " Missing child returned in XML for %s\n" % (path,) # Try to parse as iCalendar elif test == 'icalendar': try: Calendar.parseText(node.text) except: result = " Incorrect value returned in iCalendar for %s\n" % (path,) # Try to parse as JSON elif test == 'json': try: json.loads(node.text) except: result = " Incorrect value returned in XML for %s\n" % (path,) return result
def loadCalendar(file, verbose): cal = Calendar() if verbose: print "Parsing calendar data: %s" % (file, ) fin = open(file, "r") try: cal.parse(fin) except InvalidData, e: print "Failed to parse bad data: %s" % (e.mData, ) raise
def setUp(self): super(TestTimezoneDB, self).setUp() # Standard components explicitly added for vtz in StandardTZs: cal = Calendar() TimezoneDatabase.getTimezoneDatabase()._addStandardTimezone(cal.parseComponent(StringIO(vtz))) # Just parsing will add as non-standard for vtz in NonStandardTZs: Calendar.parseData(vtz)
def _doRoundtrip(caldata, resultdata=None): test1 = resultdata if resultdata is not None else caldata cal = Calendar() cal.parse(StringIO.StringIO(caldata)) test2 = cal.getTextXML() self.assertEqual( test1, test2, "\n".join( difflib.unified_diff( str(test1).splitlines(), test2.splitlines())))
def _doRoundtrip(caldata, jcaldata): cal1 = Calendar.parseText(caldata) test1 = cal1.getText() cal2 = Calendar.parseJSONData(jcaldata) test2 = cal2.getText() self.assertEqual( test1, test2, "\n".join(difflib.unified_diff(str(test1).splitlines(), test2.splitlines())) )
def loadCalendar(files, verbose): cal = Calendar() for file in files: if verbose: print "Parsing calendar data: %s" % (file, ) with open(file, "r") as fin: try: cal.parse(fin) except InvalidData, e: print "Failed to parse bad data: %s" % (e.mData, ) raise
def vtimezones(self, minYear, maxYear=2018, filterzones=None): """ Generate iCalendar data for all VTIMEZONEs or just those specified """ cal = Calendar() for zone in self.zones.itervalues(): if filterzones and zone.name not in filterzones: continue vtz = zone.vtimezone(cal, self.rules, minYear, maxYear) cal.addComponent(vtz) return cal.getText()
def getTimezoneInCalendar(tzid): """ Return a VTIMEZONE inside a valid VCALENDAR """ tz = TimezoneDatabase.getTimezone(tzid) if tz is not None: from pycalendar.icalendar.calendar import Calendar cal = Calendar() cal.addComponent(tz.duplicate(cal)) return cal else: return None
def _doRoundtrip(caldata, resultdata=None): test1 = resultdata if resultdata is not None else caldata cal = Calendar() cal.parse(StringIO.StringIO(caldata)) test2 = cal.getTextXML() self.assertEqual( test1, test2, "\n".join(difflib.unified_diff(str(test1).splitlines(), test2.splitlines())) )
def _getTimezoneFromServer(self, tzinfo): # List all from the server url = "%s?action=get&tzid=%s" % (self.uri, tzinfo.tzid,) log.debug("Getting timezone from secondary server: %s" % (url,)) response = (yield getURL(url)) if response is None or response.code / 100 != 2: returnValue(None) ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0] ct = ct.split(";", 1) ct = ct[0] if ct not in ("text/calendar",): log.error("Invalid content-type '%s' for tzid : %s" % (ct, tzinfo.tzid,)) returnValue(None) ical = response.data try: calendar = Calendar.parseText(ical) except InvalidData: log.error("Invalid calendar data for tzid: %s" % (tzinfo.tzid,)) returnValue(None) ical = calendar.getText() tzinfo.md5 = hashlib.md5(ical).hexdigest() try: tzpath = os.path.join(self.basepath, tzinfo.tzid) + ".ics" if not os.path.exists(os.path.dirname(tzpath)): os.makedirs(os.path.dirname(tzpath)) f = open(tzpath, "w") f.write(ical) f.close() except IOError, e: log.error("Unable to write calendar file for %s: %s" % (tzinfo.tzid, str(e),))
def setUp(self): super(TestTimezoneDBCache, self).setUp() # Use temp dbpath tmpdir = tempfile.mkdtemp() TimezoneDatabase.createTimezoneDatabase(tmpdir) # Save standard components to temp directory for vtz in StandardTZs: cal = Calendar() tz = cal.parseComponent(StringIO(vtz)) tzid_parts = tz.getID().split("/") if not os.path.exists(os.path.join(tmpdir, tzid_parts[0])): os.makedirs(os.path.join(tmpdir, tzid_parts[0])) with open(os.path.join(tmpdir, "{}.ics".format(tz.getID())), "w") as f: f.write(vtz)
def _getTimezoneFromServer(self, tzinfo): # List all from the server url = "%s?action=get&tzid=%s" % (self.uri, tzinfo.tzid) log.debug("Getting timezone from secondary server: %s" % (url,)) response = (yield getURL(url)) if response is None or response.code / 100 != 2: returnValue(None) ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0] ct = ct.split(";", 1) ct = ct[0] if ct not in ("text/calendar",): log.error("Invalid content-type '%s' for tzid : %s" % (ct, tzinfo.tzid)) returnValue(None) ical = response.data try: calendar = Calendar.parseText(ical) except InvalidData: log.error("Invalid calendar data for tzid: %s" % (tzinfo.tzid,)) returnValue(None) ical = calendar.getText() tzinfo.md5 = hashlib.md5(ical).hexdigest() try: tzpath = os.path.join(self.basepath, tzinfo.tzid) + ".ics" if not os.path.exists(os.path.dirname(tzpath)): os.makedirs(os.path.dirname(tzpath)) f = open(tzpath, "w") f.write(ical) f.close() except IOError, e: log.error("Unable to write calendar file for %s: %s" % (tzinfo.tzid, str(e)))
def testjCalExample1(self): jcaldata = """["vcalendar", [ ["calscale", {}, "text", "GREGORIAN"], ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], ["version", {}, "text", "2.0"] ], [ ["vevent", [ ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], ["dtstart", {}, "date", "2008-10-06"], ["summary", {}, "text", "Planning meeting"], ["uid", {}, "text", "4088E990AD89CB3DBB484909"] ], [] ] ] ] """ icaldata = """BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//Example Inc.//Example Calendar//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20080205T191224Z DTSTART;VALUE=DATE:20081006 SUMMARY:Planning meeting UID:4088E990AD89CB3DBB484909 END:VEVENT END:VCALENDAR """.replace("\n", "\r\n") cal1 = Calendar.parseText(icaldata) test1 = cal1.getText() cal2 = Calendar.parseJSONData(jcaldata) test2 = cal2.getText() self.assertEqual( test1, test2, "\n".join(difflib.unified_diff(str(test1).splitlines(), test2.splitlines())) )
def testDuplicateWithRecurrenceChange(self): data = ( """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;COUNT=400 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), ) cal1 = Calendar() cal1.parse(StringIO.StringIO(data[0])) cal2 = cal1.duplicate() vevent = cal2.getComponents()[0] rrules = vevent.getRecurrenceSet() for rrule in rrules.getRules(): rrule.setUseCount(True) rrule.setCount(400) rrules.changed() self.assertEqual(data[0], str(cal1)) self.assertEqual(data[1], str(cal2))
def testjCalExample1(self): jcaldata = """["vcalendar", [ ["calscale", {}, "text", "GREGORIAN"], ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], ["version", {}, "text", "2.0"] ], [ ["vevent", [ ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], ["dtstart", {}, "date", "2008-10-06"], ["summary", {}, "text", "Planning meeting"], ["uid", {}, "text", "4088E990AD89CB3DBB484909"] ], [] ] ] ] """ icaldata = """BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//Example Inc.//Example Calendar//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20080205T191224Z DTSTART;VALUE=DATE:20081006 SUMMARY:Planning meeting UID:4088E990AD89CB3DBB484909 END:VEVENT END:VCALENDAR """.replace("\n", "\r\n") cal1 = Calendar.parseText(icaldata) test1 = cal1.getText() cal2 = Calendar.parseJSONData(jcaldata) test2 = cal2.getText() self.assertEqual( test1, test2, "\n".join( difflib.unified_diff( str(test1).splitlines(), test2.splitlines())))
def _doEquality(caldata): cal1 = Calendar() cal1.parse(StringIO.StringIO(caldata)) cal2 = Calendar() cal2.parse(StringIO.StringIO(caldata)) self.assertEqual(cal1, cal2, "%s\n\n%s" % ( cal1, cal2, ))
def _doDuplicateRoundtrip(caldata): cal = Calendar() cal.parse(StringIO.StringIO(caldata)) cal = cal.duplicate() s = StringIO.StringIO() cal.generate(s) self.assertEqual(caldata, s.getvalue())
def testAddCN(self): data = ( """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 ORGANIZER:[email protected] SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), "まだ", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 ORGANIZER;CN=まだ:[email protected] SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), ) cal1 = Calendar() cal1.parse(StringIO.StringIO(data[0])) vevent = cal1.getComponents("VEVENT")[0] organizer = vevent.getProperties("ORGANIZER")[0] organizer.addParameter(Parameter("CN", data[1])) cal2 = Calendar() cal2.parse(StringIO.StringIO(data[2])) self.assertEqual(str(cal1), str(cal2))
def _doRoundtrip(caldata, resultdata): test1 = json.dumps(json.loads(resultdata), indent=2, separators=(',', ':'), sort_keys=True) cal = Calendar.parseText(caldata) test2 = cal.getTextJSON(sort_keys=True) self.assertEqual( test1, test2, "\n".join(difflib.unified_diff(str(test1).splitlines(), test2.splitlines())) )
def _doRoundtrip(caldata, resultdata): test1 = json.dumps(json.loads(resultdata), indent=2, separators=(',', ':'), sort_keys=True) cal = Calendar.parseText(caldata) test2 = cal.getTextJSON(sort_keys=True) self.assertEqual( test1, test2, "\n".join( difflib.unified_diff( str(test1).splitlines(), test2.splitlines())))
def validate(fname): """ Check whether the contents of the specified file is valid iCalendar or vCard data. """ with open(fname) as f: data = f.read() ParserContext.allRaise() if data.find("BEGIN:VCALENDAR") != -1: try: cal = Calendar.parseText(data) except ErrorBase as e: print("Failed to parse iCalendar: {}: {}".format( e.mReason, e.mData, )) sys.exit(1) elif data.find("BEGIN:VCARD") != -1: try: cal = Card.parseText(data) except ErrorBase as e: print("Failed to parse vCard: {}: {}".format( e.mReason, e.mData, )) sys.exit(1) else: print("Failed to find valid iCalendar or vCard data") sys.exit(1) _ignore_fixed, unfixed = cal.validate(doFix=False, doRaise=False) if unfixed: print("List of problems: {}".format(unfixed, )) else: print("No problems") # Control character check - only HTAB, CR, LF allowed for characters in the range 0x00-0x1F s = str(data) if len( s.translate( None, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" )) != len(s): for ctr, i in enumerate(data): if i in "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F": print("Control character {} at position {}".format( ord(i), ctr, ))
def _doEquality(caldata): cal1 = Calendar() cal1.parse(StringIO.StringIO(caldata)) cal2 = Calendar() cal2.parse(StringIO.StringIO(caldata)) self.assertEqual(cal1, cal2, "%s\n\n%s" % (cal1, cal2,))
def validate(fname): """ Check whether the contents of the specified file is valid iCalendar or vCard data. """ data = open(fname).read() ParserContext.allRaise() if data.find("BEGIN:VCALENDAR") != -1: try: cal = Calendar.parseText(data) except ErrorBase, e: print "Failed to parse iCalendar: %r" % (e, ) sys.exit(1)
def validate(fname): """ Check whether the contents of the specified file is valid iCalendar or vCard data. """ with open(fname) as f: data = f.read() ParserContext.allRaise() if data.find("BEGIN:VCALENDAR") != -1: try: cal = Calendar.parseText(data) except ErrorBase, e: print "Failed to parse iCalendar: %r" % (e,) sys.exit(1)
def _calProperty(self, propertyname, respdata): try: cal = Calendar.parseText(respdata) except Exception: return None # propname is a path consisting of component names and the last one a property name # e.g. VEVENT/ATTACH bits = propertyname.split("/") components = bits[:-1] prop = bits[-1] bits = prop.split("$") pname = bits[0] pvalue = bits[1] if len(bits) > 1 else None while components: for c in cal.getComponents(): if c.getType() == components[0]: cal = c components = components[1:] break else: break if components: return None props = cal.getProperties(pname) if pvalue: for prop in props: if prop.getValue().getValue() == pvalue: return prop else: return None else: return props[0] if props else None
def testParseBlank(self): data = ( """ BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """ BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), ) save = ParserContext.BLANK_LINES_IN_DATA for item in data: ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_RAISE self.assertRaises(InvalidData, Calendar.parseText, item) ParserContext.BLANK_LINES_IN_DATA = ParserContext.PARSER_IGNORE lines = item.split("\r\n") result = "\r\n".join([line for line in lines if line]) + "\r\n" self.assertEqual(str(Calendar.parseText(item)), result) ParserContext.BLANK_LINES_IN_DATA = save
def clear(self): from pycalendar.icalendar.calendar import Calendar self.calendar = Calendar() self.tzcache.clear() self.stdtzcache.clear()
def __init__(self): from pycalendar.icalendar.calendar import Calendar self.dbpath = None self.calendar = Calendar() self.tzcache = {} self.stdtzcache = set()
class TimezoneDatabase(object): """ On demand timezone database cache. This scans a TZdb directory for .ics files matching a TZID and caches the component data in a calendar from whence the actual component is returned. """ sTimezoneDatabase = None @staticmethod def createTimezoneDatabase(dbpath): TimezoneDatabase.sTimezoneDatabase = TimezoneDatabase() TimezoneDatabase.sTimezoneDatabase.setPath(dbpath) @staticmethod def clearTimezoneDatabase(): if TimezoneDatabase.sTimezoneDatabase is not None: TimezoneDatabase.sTimezoneDatabase.clear() def __init__(self): from pycalendar.icalendar.calendar import Calendar self.dbpath = None self.calendar = Calendar() self.tzcache = {} self.stdtzcache = set() def setPath(self, dbpath): self.dbpath = dbpath def clear(self): from pycalendar.icalendar.calendar import Calendar self.calendar = Calendar() self.tzcache.clear() self.stdtzcache.clear() @staticmethod def getTimezoneDatabase(): if TimezoneDatabase.sTimezoneDatabase is None: TimezoneDatabase.sTimezoneDatabase = TimezoneDatabase() return TimezoneDatabase.sTimezoneDatabase @staticmethod def getTimezone(tzid): return TimezoneDatabase.getTimezoneDatabase()._getTimezone(tzid) @staticmethod def getTimezoneInCalendar(tzid): """ Return a VTIMEZONE inside a valid VCALENDAR """ tz = TimezoneDatabase.getTimezone(tzid) if tz is not None: from pycalendar.icalendar.calendar import Calendar cal = Calendar() cal.addComponent(tz.duplicate(cal)) return cal else: return None @staticmethod def getTimezoneOffsetSeconds(tzid, dt, relative_to_utc=False): # Cache it first tz = TimezoneDatabase.getTimezone(tzid) if tz is not None: return tz.getTimezoneOffsetSeconds(dt, relative_to_utc) else: return 0 @staticmethod def getTimezoneDescriptor(tzid, dt): # Cache it first tz = TimezoneDatabase.getTimezone(tzid) if tz is not None: return tz.getTimezoneDescriptor(dt) else: return "" @staticmethod def isStandardTimezone(tzid): return TimezoneDatabase.getTimezoneDatabase()._isStandardTimezone(tzid) def cacheTimezone(self, tzid): """ Load the specified timezone identifier's timezone data from a file and parse it into the L{Calendar} used to store timezones used by this object. @param tzid: the timezone identifier to load @type tzid: L{str} """ if self.dbpath is None: return tzpath = os.path.join(self.dbpath, "%s.ics" % (tzid,)) tzpath = os.path.normpath(tzpath) if tzpath.startswith(self.dbpath) and os.path.isfile(tzpath): try: with open(tzpath) as f: self.calendar.parseComponent(f) except (IOError, InvalidData): raise NoTimezoneInDatabase(self.dbpath, tzid) else: raise NoTimezoneInDatabase(self.dbpath, tzid) def addTimezone(self, tz): """ Add the specified VTIMEZONE component to this object's L{Calendar} cache. This component is assumed to be a non-standard timezone - i.e., not loaded from the timezone database. @param tz: the VTIMEZONE component to add @type tz: L{Component} """ copy = tz.duplicate(self.calendar) self.calendar.addComponent(copy) self.tzcache[copy.getID()] = copy def _addStandardTimezone(self, tz): """ Same as L{addTimezone} except that the timezone is marked as a standard timezone. This is only meant to be used for testing which happens int he absence of a real standard timezone database. @param tz: the VTIMEZONE component to add @type tz: L{Component} """ if tz.getID() not in self.tzcache: self.addTimezone(tz) self.stdtzcache.add(tz.getID()) def _isStandardTimezone(self, tzid): """ Add the specified VTIMEZONE component to this object's L{Calendar} cache. This component is assumed to be a non-standard timezone - i.e., not loaded from the timezone database. @param tzid: the timezone identifier to lookup @type tzid: L{str} """ return tzid in self.stdtzcache def _getTimezone(self, tzid): """ Get a timezone matching the specified timezone identifier. Use this object's cache - if not in the cache try to load it from a tz database file and store in this object's calendar. @param tzid: the timezone identifier to lookup @type tzid: L{str} """ if tzid not in self.tzcache: tz = self.calendar.getTimezone(tzid) if tz is None: try: self.cacheTimezone(tzid) except NoTimezoneInDatabase: pass tz = self.calendar.getTimezone(tzid) self.tzcache[tzid] = tz if tz is not None and tzid is not None: self.stdtzcache.add(tzid) return self.tzcache[tzid] @staticmethod def mergeTimezones(cal, tzs): """ Merge each timezone from other calendar. """ tzdb = TimezoneDatabase.getTimezoneDatabase() # Not if our own calendar if cal is tzdb.calendar: return # Merge each timezone from other calendar for tz in tzs: tzdb.mergeTimezone(tz) def mergeTimezone(self, tz): """ If the supplied VTIMEZONE is not in our cache then store it in memory. """ if self._getTimezone(tz.getID()) is None: self.addTimezone(tz)
def testOffsets(self): data = ( ( """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//calendarserver.org//Zonal//EN BEGIN:VTIMEZONE TZID:America/New_York X-LIC-LOCATION:America/New_York BEGIN:STANDARD DTSTART:18831118T120358 RDATE:18831118T120358 TZNAME:EST TZOFFSETFROM:-045602 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19200101T000000 RDATE:19200101T000000 RDATE:19420101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:EST TZOFFSETFROM:-0500 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19200328T020000 RDATE:19200328T020000 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19201031T020000 RDATE:19201031T020000 RDATE:19450930T020000 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19210424T020000 RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19210925T020000 RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:EWT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T190000 RDATE:19450814T190000 TZNAME:EPT TZOFFSETFROM:-0400 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19460428T020000 RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19460929T020000 RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19551030T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE END:VCALENDAR """, ( (DateTime(1942, 2, 8), False, -5), (DateTime(1942, 2, 10), False, -4), (DateTime(2011, 1, 1), False, -5), (DateTime(2011, 4, 1), False, -4), (DateTime(2011, 10, 24), False, -4), (DateTime(2011, 11, 8), False, -5), (DateTime(2006, 1, 1), False, -5), (DateTime(2006, 4, 1), False, -5), (DateTime(2006, 5, 1), False, -4), (DateTime(2006, 10, 1), False, -4), (DateTime(2006, 10, 24), False, -4), (DateTime(2006, 11, 8), False, -5), (DateTime(2014, 3, 8, 23, 0, 0), False, -5), (DateTime(2014, 3, 9, 0, 0, 0), False, -5), (DateTime(2014, 3, 9, 3, 0, 0), False, -4), (DateTime(2014, 3, 9, 8, 0, 0), False, -4), (DateTime(2014, 3, 8, 23, 0, 0), True, -5), (DateTime(2014, 3, 9, 0, 0, 0), True, -5), (DateTime(2014, 3, 9, 3, 0, 0), True, -5), (DateTime(2014, 3, 9, 8, 0, 0), True, -4), (DateTime(2014, 11, 1, 23, 0, 0), False, -4), (DateTime(2014, 11, 2, 0, 0, 0), False, -4), (DateTime(2014, 11, 2, 3, 0, 0), False, -5), (DateTime(2014, 11, 2, 8, 0, 0), False, -5), (DateTime(2014, 11, 1, 23, 0, 0), True, -4), (DateTime(2014, 11, 2, 0, 0, 0), True, -4), (DateTime(2014, 11, 2, 3, 0, 0), True, -4), (DateTime(2014, 11, 2, 8, 0, 0), True, -5), ) ), ( """BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//calendarserver.org//Zonal//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Etc/GMT+8 X-LIC-LOCATION:Etc/GMT+8 BEGIN:STANDARD DTSTART:18000101T000000 RDATE:18000101T000000 TZNAME:GMT+8 TZOFFSETFROM:-0800 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE END:VCALENDAR """, ( (DateTime(1942, 2, 8), False, -8), (DateTime(1942, 2, 10), False, -8), (DateTime(2011, 1, 1), False, -8), (DateTime(2011, 4, 1), False, -8), ) ), ) for tzdata, offsets in data: cal = Calendar.parseText(tzdata.replace("\n", "\r\n")) tz = cal.getComponents()[0] for dt, relative_to_utc, offset in offsets: tzoffset = tz.getTimezoneOffsetSeconds(dt, relative_to_utc) self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s with caching" % (tz.getID(), dt,)) for dt, relative_to_utc, offset in reversed(offsets): tzoffset = tz.getTimezoneOffsetSeconds(dt, relative_to_utc) self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s with caching, reversed" % (tz.getID(), dt,)) for dt, relative_to_utc, offset in offsets: tz.mCachedExpandAllMax = None tzoffset = tz.getTimezoneOffsetSeconds(dt, relative_to_utc) self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s without caching" % (tz.getID(), dt,)) for dt, relative_to_utc, offset in reversed(offsets): tz.mCachedExpandAllMax = None tzoffset = tz.getTimezoneOffsetSeconds(dt, relative_to_utc) self.assertEqual(tzoffset, offset * 60 * 60, "Failed to match offset for %s at %s without caching, reversed" % (tz.getID(), dt,))
def verify(self, manager, uri, response, respdata, args): #@UnusedVariable # Must have status 200 if response.status != 200: return False, " HTTP Status Code Wrong: %d" % (response.status,) # Get expected FREEBUSY info busy = args.get("busy", []) tentative = args.get("tentative", []) unavailable = args.get("unavailable", []) duration = args.get("duration") is not None # Parse data as calendar object try: calendar = Calendar.parseText(respdata) # Check for calendar if calendar is None: raise ValueError("Not a calendar: %s" % (respdata,)) # Only one component comps = calendar.getComponents("VFREEBUSY") if len(comps) != 1: raise ValueError("Wrong number or unexpected components in calendar") # Must be VFREEBUSY fb = comps[0] # Extract periods busyp = [] tentativep = [] unavailablep = [] for fp in fb.getProperties("FREEBUSY"): periods = fp.getValue().getValues() # Convert start/duration to start/end for i in range(len(periods)): periods[i].getValue().setUseDuration(duration) # Check param fbtype = "BUSY" if fp.hasParameter("FBTYPE"): fbtype = fp.getParameterValue("FBTYPE") if fbtype == "BUSY": busyp.extend(periods) elif fbtype == "BUSY-TENTATIVE": tentativep.extend(periods) elif fbtype == "BUSY-UNAVAILABLE": unavailablep.extend(periods) else: raise ValueError("Unknown FBTYPE: %s" % (fbtype,)) # Set sizes must match if ( (len(busy) != len(busyp)) or (len(unavailable) != len(unavailablep)) or (len(tentative) != len(tentativep)) ): raise ValueError("Period list sizes do not match.") # Convert to string sets busy = set(busy) busyp = [x.getValue().getText() for x in busyp] busyp = set(busyp) tentative = set(tentative) tentativep = [x.getValue().getText() for x in tentativep] tentativep = set(tentativep) unavailable = set(unavailable) unavailablep = [x.getValue().getText() for x in unavailablep] unavailablep = set(unavailablep) # Compare all periods if len(busyp.symmetric_difference(busy)): raise ValueError("Busy periods do not match: {}".format(busyp.symmetric_difference(busy))) elif len(tentativep.symmetric_difference(tentative)): raise ValueError("Busy-tentative periods do not match") elif len(unavailablep.symmetric_difference(unavailable)): raise ValueError("Busy-unavailable periods do not match") except InvalidData: return False, " HTTP response data is not a calendar" except ValueError, txt: return False, " HTTP response data is invalid: %s" % (txt,)
import twistedcaldav.customxml import twistedcaldav.timezonexml twistedcaldav # Shhh.. pyflakes # # DefaultHTTPHandler # from txweb2.http_headers import DefaultHTTPHandler, last, singleHeader DefaultHTTPHandler.updateParsers({ "If-Schedule-Tag-Match": (last, str), }) DefaultHTTPHandler.updateGenerators({ "Schedule-Tag": (str, singleHeader), }) # Do some PyCalendar init from pycalendar.icalendar.calendar import Calendar from pycalendar.icalendar.property import Property from pycalendar.vcard.card import Card from pycalendar.value import Value Calendar.setPRODID("-//CALENDARSERVER.ORG//NONSGML Version 1//EN") Card.setPRODID("-//CALENDARSERVER.ORG//NONSGML Version 1//EN") # These are properties we use directly and we want the default value type set for TEXT Property.registerDefaultValue("X-CALENDARSERVER-PRIVATE-COMMENT", Value.VALUETYPE_TEXT) Property.registerDefaultValue("X-CALENDARSERVER-ATTENDEE-COMMENT", Value.VALUETYPE_TEXT)
def testConversions(self): tzdata = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//calendarserver.org//Zonal//EN BEGIN:VTIMEZONE TZID:America/New_York X-LIC-LOCATION:America/New_York BEGIN:STANDARD DTSTART:18831118T120358 RDATE:18831118T120358 TZNAME:EST TZOFFSETFROM:-045602 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19200101T000000 RDATE:19200101T000000 RDATE:19420101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:EST TZOFFSETFROM:-0500 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19200328T020000 RDATE:19200328T020000 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19201031T020000 RDATE:19201031T020000 RDATE:19450930T020000 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19210424T020000 RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19210925T020000 RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:EWT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T190000 RDATE:19450814T190000 TZNAME:EPT TZOFFSETFROM:-0400 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19460428T020000 RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19460929T020000 RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19551030T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE BEGIN:VTIMEZONE TZID:America/Los_Angeles X-LIC-LOCATION:America/Los_Angeles BEGIN:STANDARD DTSTART:18831118T120702 RDATE:18831118T120702 TZNAME:PST TZOFFSETFROM:-075258 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T100000Z;BYDAY=-1SU;BYMONTH=3 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T090000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:PWT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T160000 RDATE:19450814T160000 TZNAME:PPT TZOFFSETFROM:-0700 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19450930T020000 RDATE:19450930T020000 RDATE:19490101T020000 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:STANDARD DTSTART:19460101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:PST TZOFFSETFROM:-0800 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19480314T020000 RDATE:19480314T020000 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19500430T020000 RRULE:FREQ=YEARLY;UNTIL=19660424T100000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19500924T020000 RRULE:FREQ=YEARLY;UNTIL=19610924T090000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:STANDARD DTSTART:19621028T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T090000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T100000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T100000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYDAY=1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE END:VCALENDAR """ data = ( ( DateTime(2014, 3, 8, 23, 0, 0, Timezone(tzid="America/New_York")), DateTime(2014, 3, 8, 20, 0, 0, Timezone(tzid="America/Los_Angeles")), ), ( DateTime(2014, 3, 9, 3, 0, 0, Timezone(utc=True)), DateTime(2014, 3, 8, 19, 0, 0, Timezone(tzid="America/Los_Angeles")), ), ( DateTime(2014, 3, 9, 13, 0, 0, Timezone(utc=True)), DateTime(2014, 3, 9, 6, 0, 0, Timezone(tzid="America/Los_Angeles")), ), ) Calendar.parseText(tzdata.replace("\n", "\r\n")) for dtfrom, dtto in data: self.assertEqual(dtfrom, dtto) newdtfrom = dtfrom.duplicate() newdtfrom.adjustTimezone(dtto.getTimezone()) self.assertEqual(newdtfrom, dtto) self.assertEqual(newdtfrom.getHours(), dtto.getHours())
def verify(self, manager, uri, response, respdata, args): #@UnusedVariable # Must have status 200 if response.status != 200: return False, " HTTP Status Code Wrong: %d" % (response.status,) # Get expected FREEBUSY info users = args.get("attendee", []) busy = args.get("busy", []) tentative = args.get("tentative", []) unavailable = args.get("unavailable", []) # Extract each calendar-data object try: tree = ElementTree(file=StringIO.StringIO(respdata)) except ExpatError: return False, " Could not parse proper XML response\n" for calendar in tree.findall("./{urn:ietf:params:xml:ns:caldav}response/{urn:ietf:params:xml:ns:caldav}calendar-data"): # Parse data as calendar object try: calendar = Calendar.parseText(calendar.text) # Check for calendar if calendar is None: raise ValueError("Not a calendar: %s" % (calendar,)) # Only one component comps = calendar.getComponents("VFREEBUSY") if len(comps) != 1: raise ValueError("Wrong number or unexpected components in calendar") # Must be VFREEBUSY fb = comps[0] # Check for attendee value for attendee in fb.getProperties("ATTENDEE"): if attendee.getValue().getValue() in users: users.remove(attendee.getValue().getValue()) break else: continue # Extract periods busyp = [] tentativep = [] unavailablep = [] for fp in fb.getProperties("FREEBUSY"): periods = fp.getValue().getValues() # Convert start/duration to start/end for i in range(len(periods)): periods[i].getValue().setUseDuration(False) # Check param fbtype = "BUSY" if fp.hasParameter("FBTYPE"): fbtype = fp.getParameterValue("FBTYPE") if fbtype == "BUSY": busyp.extend(periods) elif fbtype == "BUSY-TENTATIVE": tentativep.extend(periods) elif fbtype == "BUSY-UNAVAILABLE": unavailablep.extend(periods) else: raise ValueError("Unknown FBTYPE: %s" % (fbtype,)) # Set sizes must match if ((len(busy) != len(busyp)) or (len(unavailable) != len(unavailablep)) or (len(tentative) != len(tentativep))): raise ValueError("Period list sizes do not match.") # Convert to string sets busy = set(busy) busyp = [x.getValue().getText() for x in busyp] busyp = set(busyp) tentative = set(tentative) tentativep = [x.getValue().getText() for x in tentativep] tentativep = set(tentativep) unavailable = set(unavailable) unavailablep = [x.getValue().getText() for x in unavailablep] unavailablep = set(unavailablep) # Compare all periods if len(busyp.symmetric_difference(busy)): raise ValueError("Busy periods do not match") elif len(tentativep.symmetric_difference(tentative)): raise ValueError("Busy-tentative periods do not match") elif len(unavailablep.symmetric_difference(unavailable)): raise ValueError("Busy-unavailable periods do not match") break except InvalidData: return False, " HTTP response data is not a calendar" except ValueError, txt: return False, " HTTP response data is invalid: %s" % (txt,)
def verify(self, manager, uri, response, respdata, args, is_json=False): #@UnusedVariable # Get arguments files = args.get("filepath", []) if manager.data_dir: files = map(lambda x: os.path.join(manager.data_dir, x), files) caldata = args.get("data", []) filters = args.get("filter", []) statusCode = args.get("status", ["200", "201", "207"]) doTimezones = args.get("doTimezones", None) if "EMAIL parameter" not in manager.server_info.features: filters.append("ATTENDEE:EMAIL") filters.append("ORGANIZER:EMAIL") filters.append("ATTENDEE:X-CALENDARSERVER-DTSTAMP") filters.append("ATTENDEE:X-CALENDARSERVER-AUTO") filters.append("ATTENDEE:X-CALENDARSERVER-RESET-PARTSTAT") filters.append("CALSCALE") filters.append("PRODID") filters.append("DTSTAMP") filters.append("CREATED") filters.append("LAST-MODIFIED") filters.append("X-WR-CALNAME") for afilter in tuple(filters): if afilter[0] == "!" and afilter[1:] in filters: filters.remove(afilter[1:]) filters = filter(lambda x: x[0] != "!", filters) if doTimezones is None: doTimezones = "timezones-by-reference" not in manager.server_info.features else: doTimezones = doTimezones == "true" # status code must be 200, 201, 207 or explicitly specified code if str(response.status) not in statusCode: return False, " HTTP Status Code Wrong: %d" % (response.status,) # look for response data if not respdata: return False, " No response body" # look for one file if len(files) != 1 and len(caldata) != 1: return False, " No file to compare response to" # read in all data from specified file or use provided data if len(files): fd = open(files[0], "r") try: try: data = fd.read() finally: fd.close() except: data = None else: data = caldata[0] if len(caldata) else None if data is None: return False, " Could not read data file" data = manager.server_info.extrasubs(manager.server_info.subs(data)) def removePropertiesParameters(component): if not doTimezones: for subcomponent in tuple(component.getComponents()): if subcomponent.getType() == "VTIMEZONE": component.removeComponent(subcomponent) for subcomponent in component.getComponents(): removePropertiesParameters(subcomponent) allProps = [] for properties in component.getProperties().itervalues(): allProps.extend(properties) for property in allProps: # Always reset DTSTAMP on these properties if property.getName() in ("ATTENDEE", "X-CALENDARSERVER-ATTENDEE-COMMENT"): if property.hasParameter("X-CALENDARSERVER-DTSTAMP"): property.replaceParameter(Parameter("X-CALENDARSERVER-DTSTAMP", "20080101T000000Z")) for filter in filters: if ":" in filter: propname, parameter = filter.split(":") if property.getName() == propname: if property.hasParameter(parameter): property.removeParameters(parameter) else: if property.getName() == filter: component.removeProperty(property) def reconcileRecurrenceOverrides(calendar1, calendar2): """ Make sure that the same set of overridden components appears in both calendar objects. """ def _getRids(calendar): """ Get all the recurrence ids of the specified calendar. """ results = set() master = None for subcomponent in calendar.getComponents(): if isinstance(subcomponent, ComponentRecur): rid = subcomponent.getRecurrenceID() if rid: results.add(rid.duplicateAsUTC()) else: master = subcomponent return results, master def _addOverrides(calendar, master, missing_rids): """ Derive instances for the missing overrides in the specified calendar object. """ if master is None or not missing_rids: return for rid in missing_rids: # If we were fed an already derived component, use that, otherwise make a new one newcomp = calendar.deriveComponent(rid) if newcomp is not None: calendar.addComponent(newcomp) rids1, master1 = _getRids(calendar1) rids2, master2 = _getRids(calendar2) _addOverrides(calendar1, master1, rids2 - rids1) _addOverrides(calendar2, master2, rids1 - rids2) try: format = Calendar.sFormatJSON if is_json else Calendar.sFormatText resp_calendar = Calendar.parseData(respdata, format=format) removePropertiesParameters(resp_calendar) data_calendar = Calendar.parseData(data, format=format) removePropertiesParameters(data_calendar) reconcileRecurrenceOverrides(resp_calendar, data_calendar) respdata = resp_calendar.getText(includeTimezones=Calendar.NO_TIMEZONES, format=format) data = data_calendar.getText(includeTimezones=Calendar.NO_TIMEZONES, format=format) result = resp_calendar == data_calendar if result: return True, "" else: error_diff = "\n".join([line for line in unified_diff(data.split("\n"), respdata.split("\n"))]) return False, " Response data does not exactly match file data%s" % (error_diff,) except Exception, e: return False, " Response data is not calendar data: %s" % (e,)
def test_vtimezone(self): zonedef = """Zone America/New_York\t-4:56:02\t-\tLMT\t1883 Nov 18 12:03:58 \t\t\t-5:00\tUS\tE%sT""" rules = """Rule\tUS\t1918\t1919\t-\tMar\tlastSun\t2:00\t1:00\tD Rule\tUS\t1918\t1919\t-\tOct\tlastSun\t2:00\t0\tS Rule\tUS\t1942\tonly\t-\tFeb\t9\t2:00\t1:00\tW Rule\tUS\t1945\tonly\t-\tAug\t14\t23:00u\t1:00\tP Rule\tUS\t1945\tonly\t-\tSep\t30\t2:00\t0\tS Rule\tUS\t1967\t2006\t-\tOct\tlastSun\t2:00\t0\tS Rule\tUS\t1967\t1973\t-\tApr\tlastSun\t2:00\t1:00\tD Rule\tUS\t1974\tonly\t-\tJan\t6\t2:00\t1:00\tD Rule\tUS\t1975\tonly\t-\tFeb\t23\t2:00\t1:00\tD Rule\tUS\t1976\t1986\t-\tApr\tlastSun\t2:00\t1:00\tD Rule\tUS\t1987\t2006\t-\tApr\tSun>=1\t2:00\t1:00\tD Rule\tUS\t2007\tmax\t-\tMar\tSun>=8\t2:00\t1:00\tD Rule\tUS\t2007\tmax\t-\tNov\tSun>=1\t2:00\t0\tS""" result = """BEGIN:VTIMEZONE TZID:America/New_York X-LIC-LOCATION:America/New_York BEGIN:DAYLIGHT DTSTART:20060402T020000 RDATE:20060402T020000 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20061029T020000 RDATE:20061029T020000 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE """.replace("\n", "\r\n") zone = Zone() zone.parse(zonedef) ruleset = RuleSet() ruleset.parse(rules) rules = {ruleset.name: ruleset} cal = Calendar() vtz = zone.vtimezone(cal, rules, 2006, 2011) self.assertEqual(str(vtz), result)
def verify(self, manager, uri, response, respdata, args): #@UnusedVariable # Get arguments files = args.get("filepath", []) caldata = args.get("data", []) filters = args.get("filter", []) statusCode = args.get("status", ["200", "201", "207"]) doTimezones = args.get("doTimezones", None) if "EMAIL parameter" not in manager.server_info.features: filters.append("ATTENDEE:EMAIL") filters.append("ORGANIZER:EMAIL") filters.append("ATTENDEE:X-CALENDARSERVER-DTSTAMP") filters.append("CALSCALE") filters.append("PRODID") filters.append("DTSTAMP") filters.append("CREATED") filters.append("LAST-MODIFIED") filters.append("X-WR-CALNAME") for afilter in tuple(filters): if afilter[0] == "!" and afilter[1:] in filters: filters.remove(afilter[1:]) filters = filter(lambda x: x[0] != "!", filters) if doTimezones is None: doTimezones = "timezones-by-reference" not in manager.server_info.features else: doTimezones = doTimezones == "true" # status code must be 200, 201, 207 or explicitly specified code if str(response.status) not in statusCode: return False, " HTTP Status Code Wrong: %d" % (response.status,) # look for response data if not respdata: return False, " No response body" # look for one file if len(files) != 1 and len(caldata) != 1: return False, " No file to compare response to" # read in all data from specified file or use provided data if len(files): fd = open(files[0], "r") try: try: data = fd.read() finally: fd.close() except: data = None else: data = caldata[0] if len(caldata) else None if data is None: return False, " Could not read data file" data = manager.server_info.extrasubs(manager.server_info.subs(data)) def removePropertiesParameters(component): if not doTimezones: for subcomponent in tuple(component.getComponents()): if subcomponent.getType() == "VTIMEZONE": component.removeComponent(subcomponent) for subcomponent in component.getComponents(): removePropertiesParameters(subcomponent) allProps = [] for properties in component.getProperties().itervalues(): allProps.extend(properties) for property in allProps: # Always reset DTSTAMP on these properties if property.getName() in ("ATTENDEE", "X-CALENDARSERVER-ATTENDEE-COMMENT"): if property.hasParameter("X-CALENDARSERVER-DTSTAMP"): property.replaceParameter(Parameter("X-CALENDARSERVER-DTSTAMP", "20080101T000000Z")) for filter in filters: if ":" in filter: propname, parameter = filter.split(":") if property.getName() == propname: if property.hasParameter(parameter): property.removeParameters(parameter) else: if property.getName() == filter: component.removeProperty(property) try: resp_calendar = Calendar.parseText(respdata) removePropertiesParameters(resp_calendar) respdata = resp_calendar.getText() data_calendar = Calendar.parseText(data) removePropertiesParameters(data_calendar) data = data_calendar.getText() result = respdata == data if result: return True, "" else: error_diff = "\n".join([line for line in unified_diff(data.split("\n"), respdata.split("\n"))]) return False, " Response data does not exactly match file data%s" % (error_diff,) except Exception, e: return False, " Response data is not calendar data: %s" % (e,)
class TimezoneDatabase(object): """ On demand timezone database cache. This scans a TZdb directory for .ics files matching a TZID and caches the component data in a calendar from whence the actual component is returned. """ sTimezoneDatabase = None @staticmethod def createTimezoneDatabase(dbpath): TimezoneDatabase.sTimezoneDatabase = TimezoneDatabase() TimezoneDatabase.sTimezoneDatabase.setPath(dbpath) @staticmethod def clearTimezoneDatabase(): if TimezoneDatabase.sTimezoneDatabase is not None: TimezoneDatabase.sTimezoneDatabase.clear() def __init__(self): from pycalendar.icalendar.calendar import Calendar self.dbpath = None self.calendar = Calendar() self.tzcache = {} self.stdtzcache = set() def setPath(self, dbpath): self.dbpath = dbpath def clear(self): from pycalendar.icalendar.calendar import Calendar self.calendar = Calendar() self.tzcache.clear() self.stdtzcache.clear() @staticmethod def getTimezoneDatabase(): if TimezoneDatabase.sTimezoneDatabase is None: TimezoneDatabase.sTimezoneDatabase = TimezoneDatabase() return TimezoneDatabase.sTimezoneDatabase @staticmethod def getTimezone(tzid): return TimezoneDatabase.getTimezoneDatabase()._getTimezone(tzid) @staticmethod def getTimezoneInCalendar(tzid): """ Return a VTIMEZONE inside a valid VCALENDAR """ tz = TimezoneDatabase.getTimezone(tzid) if tz is not None: from pycalendar.icalendar.calendar import Calendar cal = Calendar() cal.addComponent(tz.duplicate(cal)) return cal else: return None @staticmethod def getTimezoneOffsetSeconds(tzid, dt, relative_to_utc=False): # Cache it first tz = TimezoneDatabase.getTimezone(tzid) if tz is not None: return tz.getTimezoneOffsetSeconds(dt, relative_to_utc) else: return 0 @staticmethod def getTimezoneDescriptor(tzid, dt): # Cache it first tz = TimezoneDatabase.getTimezone(tzid) if tz is not None: return tz.getTimezoneDescriptor(dt) else: return "" @staticmethod def isStandardTimezone(tzid): return TimezoneDatabase.getTimezoneDatabase()._isStandardTimezone(tzid) def cacheTimezone(self, tzid): """ Load the specified timezone identifier's timezone data from a file and parse it into the L{Calendar} used to store timezones used by this object. @param tzid: the timezone identifier to load @type tzid: L{str} """ if self.dbpath is None: return tzpath = os.path.join(self.dbpath, "%s.ics" % (tzid, )) tzpath = os.path.normpath(tzpath) if tzpath.startswith(self.dbpath) and os.path.isfile(tzpath): try: with open(tzpath) as f: self.calendar.parseComponent(f) except (IOError, InvalidData): raise NoTimezoneInDatabase(self.dbpath, tzid) else: raise NoTimezoneInDatabase(self.dbpath, tzid) def addTimezone(self, tz): """ Add the specified VTIMEZONE component to this object's L{Calendar} cache. This component is assumed to be a non-standard timezone - i.e., not loaded from the timezone database. @param tz: the VTIMEZONE component to add @type tz: L{Component} """ copy = tz.duplicate(self.calendar) self.calendar.addComponent(copy) self.tzcache[copy.getID()] = copy def _addStandardTimezone(self, tz): """ Same as L{addTimezone} except that the timezone is marked as a standard timezone. This is only meant to be used for testing which happens int he absence of a real standard timezone database. @param tz: the VTIMEZONE component to add @type tz: L{Component} """ if tz.getID() not in self.tzcache: self.addTimezone(tz) self.stdtzcache.add(tz.getID()) def _isStandardTimezone(self, tzid): """ Add the specified VTIMEZONE component to this object's L{Calendar} cache. This component is assumed to be a non-standard timezone - i.e., not loaded from the timezone database. @param tzid: the timezone identifier to lookup @type tzid: L{str} """ return tzid in self.stdtzcache def _getTimezone(self, tzid): """ Get a timezone matching the specified timezone identifier. Use this object's cache - if not in the cache try to load it from a tz database file and store in this object's calendar. @param tzid: the timezone identifier to lookup @type tzid: L{str} """ if tzid not in self.tzcache: tz = self.calendar.getTimezone(tzid) if tz is None: try: self.cacheTimezone(tzid) except NoTimezoneInDatabase: pass tz = self.calendar.getTimezone(tzid) self.tzcache[tzid] = tz if tz is not None and tzid is not None: self.stdtzcache.add(tzid) return self.tzcache[tzid] @staticmethod def mergeTimezones(cal, tzs): """ Merge each timezone from other calendar. """ tzdb = TimezoneDatabase.getTimezoneDatabase() # Not if our own calendar if cal is tzdb.calendar: return # Merge each timezone from other calendar for tz in tzs: tzdb.mergeTimezone(tz) def mergeTimezone(self, tz): """ If the supplied VTIMEZONE is not in our cache then store it in memory. """ if self._getTimezone(tz.getID()) is None: self.addTimezone(tz)
def testParseComponent(self): data1 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n") data2 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//Example Inc.//Example Calendar//EN BEGIN:VTIMEZONE TZID:America/Montreal LAST-MODIFIED:20040110T032845Z BEGIN:DAYLIGHT DTSTART:20000404T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20001026T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE END:VCALENDAR """.replace("\n", "\r\n") result = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VTIMEZONE TZID:America/Montreal LAST-MODIFIED:20040110T032845Z BEGIN:DAYLIGHT DTSTART:20000404T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20001026T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20020101 DTEND;VALUE=DATE:20020102 DTSTAMP:20020101T000000Z RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n") cal = Calendar() cal.parse(StringIO.StringIO(data1)) cal.parseComponent(StringIO.StringIO(data2)) self.assertEqual(str(cal), result)
# # DefaultHTTPHandler # from txweb2.http_headers import DefaultHTTPHandler, last, singleHeader DefaultHTTPHandler.updateParsers({ "If-Schedule-Tag-Match": (last, str), }) DefaultHTTPHandler.updateGenerators({ "Schedule-Tag": (str, singleHeader), }) # Do some PyCalendar init from pycalendar.icalendar.calendar import Calendar from pycalendar.icalendar.property import Property from pycalendar.vcard.card import Card from pycalendar.value import Value Calendar.setPRODID("-//CALENDARSERVER.ORG//NONSGML Version 1//EN") Card.setPRODID("-//CALENDARSERVER.ORG//NONSGML Version 1//EN") # These are properties we use directly and we want the default value type set for TEXT Property.registerDefaultValue("X-CALENDARSERVER-PRIVATE-COMMENT", Value.VALUETYPE_TEXT) Property.registerDefaultValue("X-CALENDARSERVER-ATTENDEE-COMMENT", Value.VALUETYPE_TEXT) Property.registerDefaultValue("X-APPLE-TRAVEL-DURATION", Value.VALUETYPE_DURATION, always_write_value=True) Property.registerDefaultValue("X-APPLE-TRAVEL-START", Value.VALUETYPE_URI, always_write_value=True) Property.registerDefaultValue("X-APPLE-TRAVEL-RETURN-DURATION", Value.VALUETYPE_DURATION, always_write_value=True) Property.registerDefaultValue("X-APPLE-TRAVEL-RETURN", Value.VALUETYPE_URI, always_write_value=True)
def testCachePreserveOnAdjustment(self): # UTC first dt = DateTime(2012, 6, 7, 12, 0, 0, Timezone(tzid="utc")) dt.getPosixTime() # check existing cache is complete self.assertTrue(dt.mPosixTimeCached) self.assertNotEqual(dt.mPosixTime, 0) self.assertEqual(dt.mTZOffset, None) # duplicate preserves cache details dt2 = dt.duplicate() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, dt.mTZOffset) # adjust preserves cache details dt2.adjustToUTC() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, dt.mTZOffset) # Now timezone tzdata = """BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//calendarserver.org//Zonal//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:America/Pittsburgh BEGIN:STANDARD DTSTART:18831118T120358 RDATE:18831118T120358 TZNAME:EST TZOFFSETFROM:-045602 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19200101T000000 RDATE:19200101T000000 RDATE:19420101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:EST TZOFFSETFROM:-0500 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19200328T020000 RDATE:19200328T020000 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19201031T020000 RDATE:19201031T020000 RDATE:19450930T020000 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19210424T020000 RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19210925T020000 RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:EWT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T190000 RDATE:19450814T190000 TZNAME:EPT TZOFFSETFROM:-0400 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19460428T020000 RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19460929T020000 RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19551030T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE END:VCALENDAR """.replace("\n", "\r\n") Calendar.parseText(tzdata) dt = DateTime(2012, 6, 7, 12, 0, 0, Timezone(tzid="America/Pittsburgh")) dt.getPosixTime() # check existing cache is complete self.assertTrue(dt.mPosixTimeCached) self.assertNotEqual(dt.mPosixTime, 0) self.assertEqual(dt.mTZOffset, -14400) # duplicate preserves cache details dt2 = dt.duplicate() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, dt.mTZOffset) # adjust preserves cache details dt2.adjustToUTC() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, 0)
def execute(self, cmdname, options): longlist = False path = None rtype = False displayname = False ctag = False etag = False supported_components = False synctoken = False details = False opts, args = getopt.getopt(shlex.split(options), 'acdDeilrs') for name, _ignore_value in opts: if name == "-a": pass elif name == "-c": ctag = True longlist = True elif name == "-d": displayname = True longlist = True elif name == "-D": details = True elif name == "-e": etag = True longlist = True elif name == "-i": supported_components = True longlist = True elif name == "-l": longlist = True elif name == "-r": rtype = True longlist = True elif name == "-s": synctoken = True longlist = True else: print "Unknown option: %s" % (name, ) print self.usage(cmdname) raise WrongOptions if len(args) > 1: print "Wrong number of arguments: %d" % (len(args), ) print self.usage(cmdname) raise WrongOptions elif args: path = args[0] if not path.startswith("/"): path = os.path.join(self.shell.wd, path) else: path = self.shell.wd if not path.endswith("/"): path += "/" resource = URL(url=path) props = (davxml.resourcetype, ) if longlist: props += ( davxml.getcontentlength, davxml.getlastmodified, ) if ctag: props += (csxml.getctag, ) if displayname: props += (davxml.displayname, ) if etag: props += (davxml.getetag, ) if supported_components: props += (caldavxml.supported_calendar_component_set, ) if synctoken: props += (davxml.synctoken, ) results = self.shell.account.session.getPropertiesOnHierarchy( resource, props) items = results.keys() items.sort() lines = [] for rurl in items: summaries = [] if details: data = self.shell.account.session.readData(URL(url=rurl)) try: cobject = Calendar.parseData(data[0]) for comp in cobject.getComponents(): for summary in [ p.getValue().getValue() for p in comp.getProperties("SUMMARY") ]: if summary not in summaries: summaries.append(summary) except: summaries = ["<Could not parse>"] rurl = urllib.unquote(rurl) if rurl == path: continue line = [] if longlist: props = results[urllib.quote(rurl)] size = props.get(davxml.getcontentlength, "-") if not size: size = "0" line.append("%s" % (size, )) modtime = props.get(davxml.getlastmodified, "-") line.append(modtime) line.append(rurl[len(path):]) if rtype: if isinstance(props.get(davxml.resourcetype), str): line.append("type:-") else: line.append("type:%s" % (",".join([ child.tag.split("}")[1] for child in props.get( davxml.resourcetype).getchildren() ]))) if displayname: line.append("name:'%s'" % (props.get(davxml.displayname, '-'), )) if ctag: line.append("ctag:'%s'" % (props.get(csxml.getctag, '-'), )) if etag: line.append("etag:'%s'" % (props.get(davxml.getetag, '-'), )) if supported_components and props.get( caldavxml.supported_calendar_component_set ) is not None: line.append("comp:%s" % (",".join([ child.get("name", "") for child in props.get( caldavxml.supported_calendar_component_set). getchildren() ]))) if synctoken: line.append("sync:'%s'" % (props.get(davxml.synctoken, '-'), )) else: line.append(rurl[len(path):]) if details: line.append("summary: {}".format("|".join(summaries))) lines.append(line) if lines: # Get column widths widths = [0] * len(lines[0]) for line in lines: for ctr, col in enumerate(line): widths[ctr] = max(widths[ctr], len(col)) # Write out each one for line in lines: for ctr, col in enumerate(line): if ctr in (0, 1) and longlist: print col.rjust(widths[ctr] + 2), else: print col.ljust(widths[ctr] + 2), print return True
def testGetVEvents(self): data = ( ( "Non-recurring match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110601 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), (DateTime(2011, 6, 1), ), ), ( "Non-recurring no-match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110501 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), (), ), ( "Recurring match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110601 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY;COUNT=2 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), ( DateTime(2011, 6, 1), DateTime(2011, 6, 2), ), ), ( "Recurring no match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110501 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY;COUNT=2 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), (), ), ( "Recurring with override match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART:20110601T120000 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY;COUNT=2 SUMMARY:New Year's Day END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110602T120000 DTSTART;VALUE=DATE:20110602T130000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), ( DateTime(2011, 6, 1, 12, 0, 0), DateTime(2011, 6, 2, 13, 0, 0), ), ), ( "Recurring with override no match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART:20110501T120000 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY;COUNT=2 SUMMARY:New Year's Day END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110502T120000 DTSTART;VALUE=DATE:20110502T130000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), (), ), ( "Recurring partial match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110531 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY;COUNT=2 SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), (DateTime(2011, 6, 1), ), ), ( "Recurring with override partial match", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART:20110531T120000 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY;COUNT=2 SUMMARY:New Year's Day END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110601T120000 DTSTART;VALUE=DATE:20110601T130000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), (DateTime(2011, 6, 1, 13, 0, 0), ), ), ) for title, caldata, result in data: calendar = Calendar.parseText(caldata) instances = [] calendar.getVEvents( Period( start=DateTime(2011, 6, 1), end=DateTime(2011, 7, 1), ), instances) instances = tuple( [instance.getInstanceStart() for instance in instances]) self.assertEqual( instances, result, "Failed in %s: got %s, expected %s" % (title, instances, result))
def anonymizeData(directoryMap, data): try: pyobj = Calendar.parseText(data) except Exception, e: print("Failed to parse (%s): %s" % (e, data)) return None
def testMasterComponent(self): data = ( ( "1.1 Non-recurring no VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110601 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110601 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT """.replace("\n", "\r\n"), ), ( "1.2 Non-recurring with VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VTIMEZONE TZID:Etc/GMT+1 X-LIC-LOCATION:Etc/GMT+1 BEGIN:STANDARD DTSTART:18000101T000000 RDATE:18000101T000000 TZNAME:GMT+1 TZOFFSETFROM:-0100 TZOFFSETTO:-0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;TZID=Etc/GMT+1:20110601T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;TZID=Etc/GMT+1:20110601T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT """.replace("\n", "\r\n"), ), ( "2.1 Recurring no VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110601 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY SUMMARY:New Year's Day END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110602 DTSTART;VALUE=DATE:20110602 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110601 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY SUMMARY:New Year's Day END:VEVENT """.replace("\n", "\r\n"), ), ( "2.2 Recurring with VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VTIMEZONE TZID:Etc/GMT+1 X-LIC-LOCATION:Etc/GMT+1 BEGIN:STANDARD DTSTART:18000101T000000 RDATE:18000101T000000 TZNAME:GMT+1 TZOFFSETFROM:-0100 TZOFFSETTO:-0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;TZID=Etc/GMT+1:20110601T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day RRULE:FREQ=DAILY END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;TZID=Etc/GMT+1:20110602T000000 DTSTART;TZID=Etc/GMT+1:20110602T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), """BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;TZID=Etc/GMT+1:20110601T000000 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY SUMMARY:New Year's Day END:VEVENT """.replace("\n", "\r\n"), ), ( "3.1 Recurring no master, no VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110602 DTSTART;VALUE=DATE:20110602 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), "", ), ( "3.2 Recurring no master, with VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VTIMEZONE TZID:Etc/GMT+1 X-LIC-LOCATION:Etc/GMT+1 BEGIN:STANDARD DTSTART:18000101T000000 RDATE:18000101T000000 TZNAME:GMT+1 TZOFFSETFROM:-0100 TZOFFSETTO:-0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;TZID=Etc/GMT+1:20110602T000000 DTSTART;TZID=Etc/GMT+1:20110602T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), "", ), ) for title, caldata, result in data: calendar = Calendar.parseText(caldata) master = calendar.masterComponent() if master is None: master = "" self.assertEqual( str(master), result, "Failed in %s: got %s, expected %s" % (title, master, result))