diff --git a/radicale_vobject/change_tz.py b/radicale_vobject/change_tz.py new file mode 100644 index 0000000..b217012 --- /dev/null +++ b/radicale_vobject/change_tz.py @@ -0,0 +1,101 @@ +"""Translate an ics file's events to a different timezone.""" + +from optparse import OptionParser +from radicale_vobject import icalendar, base + +try: + import PyICU +except: + PyICU = None + +from datetime import datetime + + +def change_tz(cal, new_timezone, default, utc_only=False, utc_tz=icalendar.utc): + """ + Change the timezone of the specified component. + + Args: + cal (Component): the component to change + new_timezone (tzinfo): the timezone to change to + default (tzinfo): a timezone to assume if the dtstart or dtend in cal + doesn't have an existing timezone + utc_only (bool): only convert dates that are in utc + utc_tz (tzinfo): the tzinfo to compare to for UTC when processing + utc_only=True + """ + + for vevent in getattr(cal, 'vevent_list', []): + start = getattr(vevent, 'dtstart', None) + end = getattr(vevent, 'dtend', None) + for node in (start, end): + if node: + dt = node.value + if (isinstance(dt, datetime) and + (not utc_only or dt.tzinfo == utc_tz)): + if dt.tzinfo is None: + dt = dt.replace(tzinfo=default) + node.value = dt.astimezone(new_timezone) + + +def main(): + options, args = get_options() + if PyICU is None: + print("Failure. change_tz requires PyICU, exiting") + elif options.list: + for tz_string in PyICU.TimeZone.createEnumeration(): + print(tz_string) + elif args: + utc_only = options.utc + if utc_only: + which = "only UTC" + else: + which = "all" + print("Converting {0!s} events".format(which)) + ics_file = args[0] + if len(args) > 1: + timezone = PyICU.ICUtzinfo.getInstance(args[1]) + else: + timezone = PyICU.ICUtzinfo.default + print("... Reading {0!s}".format(ics_file)) + cal = base.readOne(open(ics_file)) + change_tz(cal, timezone, PyICU.ICUtzinfo.default, utc_only) + + out_name = ics_file + '.converted' + print("... Writing {0!s}".format(out_name)) + + with open(out_name, 'wb') as out: + cal.serialize(out) + + print("Done") + + +version = "0.1" + + +def get_options(): + # Configuration options + + usage = """usage: %prog [options] ics_file [timezone]""" + parser = OptionParser(usage=usage, version=version) + parser.set_description("change_tz will convert the timezones in an ics file. ") + + parser.add_option("-u", "--only-utc", dest="utc", action="store_true", + default=False, help="Only change UTC events.") + parser.add_option("-l", "--list", dest="list", action="store_true", + default=False, help="List available timezones") + + (cmdline_options, args) = parser.parse_args() + if not args and not cmdline_options.list: + print("error: too few arguments given") + print + print(parser.format_help()) + return False, False + + return cmdline_options, args + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("Aborted") diff --git a/radicale_vobject/tests/test_base.py b/radicale_vobject/tests/test_base.py new file mode 100644 index 0000000..8e2ff6b --- /dev/null +++ b/radicale_vobject/tests/test_base.py @@ -0,0 +1,951 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import datetime +import dateutil +import os +import re +import sys +import unittest +import json + +from dateutil.tz import tzutc +from dateutil.rrule import rrule, rruleset, WEEKLY, MONTHLY + +from radicale_vobject import base, iCalendar +from radicale_vobject import icalendar + +from radicale_vobject.base import __behaviorRegistry as behavior_registry +from radicale_vobject.base import ContentLine, parseLine, ParseError +from radicale_vobject.base import readComponents, textLineToContentLine + +from radicale_vobject.change_tz import change_tz + +from radicale_vobject.icalendar import MultiDateBehavior, PeriodBehavior, \ + RecurringComponent, utc +from radicale_vobject.icalendar import parseDtstart, stringToTextValues, \ + stringToPeriod, timedeltaToString + +two_hours = datetime.timedelta(hours=2) + + +def get_test_filepath(path): + """ + Helper function to get the filepath of test files. + """ + return os.path.join(os.path.dirname(__file__), "test_files", path) + + +def get_test_file(path): + """ + Helper function to open and read test files. + """ + filepath = get_test_filepath(path) + if sys.version_info[0] < 3: + # On python 2, this library operates on bytes. + f = open(filepath, 'r') + else: + # On python 3, it operates on unicode. We need to specify an encoding + # for systems for which the preferred encoding isn't utf-8 (e.g windows) + f = open(filepath, 'r', encoding='utf-8') + text = f.read() + f.close() + return text + + +class TestCalendarSerializing(unittest.TestCase): + """ + Test creating an iCalendar file + """ + max_diff = None + + def test_scratchbuild(self): + """ + CreateCalendar 2.0 format from scratch + """ + test_cal = get_test_file("simple_2_0_test.ics") + cal = base.newFromBehavior('vcalendar', '2.0') + cal.add('vevent') + cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9) + cal.vevent.add('description').value = "Test event" + cal.vevent.add('created').value = \ + datetime.datetime(2006, 1, 1, 10, + tzinfo=dateutil.tz.tzical( + get_test_filepath("timezones.ics")).get('US/Pacific')) + cal.vevent.add('uid').value = "Not very random UID" + cal.vevent.add('dtstamp').value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc()) + + # Note we're normalizing line endings, because no one got time for that. + self.assertEqual( + cal.serialize().replace('\r\n', '\n'), + test_cal.replace('\r\n', '\n') + ) + + def test_unicode(self): + """ + Test unicode characters + """ + test_cal = get_test_file("utf8_test.ics") + vevent = base.readOne(test_cal).vevent + vevent2 = base.readOne(vevent.serialize()) + self.assertEqual(str(vevent), str(vevent2)) + + self.assertEqual( + vevent.summary.value, + 'The title こんにちはキティ' + ) + + if sys.version_info[0] < 3: + test_cal = test_cal.decode('utf-8') + vevent = base.readOne(test_cal).vevent + vevent2 = base.readOne(vevent.serialize()) + self.assertEqual(str(vevent), str(vevent2)) + self.assertEqual( + vevent.summary.value, + u'The title こんにちはキティ' + ) + + def test_wrapping(self): + """ + Should support input file with a long text field covering multiple lines + """ + test_journal = get_test_file("journal.ics") + vobj = base.readOne(test_journal) + vjournal = base.readOne(vobj.serialize()) + self.assertTrue('Joe, Lisa, and Bob' in vjournal.description.value) + self.assertTrue('Tuesday.\n2.' in vjournal.description.value) + + def test_multiline(self): + """ + Multi-text serialization test + """ + category = base.newFromBehavior('categories') + category.value = ['Random category'] + self.assertEqual( + category.serialize().strip(), + "CATEGORIES:Random category" + ) + + category.value.append('Other category') + self.assertEqual( + category.serialize().strip(), + "CATEGORIES:Random category,Other category" + ) + + def test_semicolon_separated(self): + """ + Semi-colon separated multi-text serialization test + """ + request_status = base.newFromBehavior('request-status') + request_status.value = ['5.1', 'Service unavailable'] + self.assertEqual( + request_status.serialize().strip(), + "REQUEST-STATUS:5.1;Service unavailable" + ) + + @staticmethod + def test_unicode_multiline(): + """ + Test multiline unicode characters + """ + cal = iCalendar() + cal.add('method').value = 'REQUEST' + cal.add('vevent') + cal.vevent.add('created').value = datetime.datetime.now() + cal.vevent.add('summary').value = 'Классное событие' + cal.vevent.add('description').value = ('Классное событие Классное событие Классное событие Классное событие ' + 'Классное событие Классsdssdное событие') + + # json tries to encode as utf-8 and it would break if some chars could not be encoded + json.dumps(cal.serialize()) + + @staticmethod + def test_ical_to_hcal(): + """ + Serializing iCalendar to hCalendar. + + Since Hcalendar is experimental and the behavior doesn't seem to want to load, + This test will have to wait. + + + tzs = dateutil.tz.tzical(get_test_filepath("timezones.ics")) + cal = base.newFromBehavior('hcalendar') + self.assertEqual( + str(cal.behavior), + "" + ) + cal.add('vevent') + cal.vevent.add('summary').value = "this is a note" + cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator" + cal.vevent.add('dtstart').value = datetime.date(2006,2,27) + cal.vevent.add('location').value = "a place" + cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2) + + event2 = cal.add('vevent') + event2.add('summary').value = "Another one" + event2.add('description').value = "The greatest thing ever!" + event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific')) + event2.add('location').value = "somewhere else" + event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6) + hcal = cal.serialize() + """ + #self.assertEqual( + # str(hcal), + # """ + # + # this is a note: + # Monday, February 27 + # - Tuesday, February 28 + # at a place + # + # + # + # Another one: + # Thursday, December 17, 16:42 + # - Wednesday, December 23, 16:42 + # at somewhere else + #
The greatest thing ever!
+ #
+ # """ + #) + + +class TestBehaviors(unittest.TestCase): + """ + Test Behaviors + """ + def test_general_behavior(self): + """ + Tests for behavior registry, getting and creating a behavior. + """ + # Check expected behavior registry. + self.assertEqual( + sorted(behavior_registry.keys()), + ['', 'ACTION', 'ADR', 'AVAILABLE', 'BUSYTYPE', 'CALSCALE', + 'CATEGORIES', 'CLASS', 'COMMENT', 'COMPLETED', 'CONTACT', + 'CREATED', 'DAYLIGHT', 'DESCRIPTION', 'DTEND', 'DTSTAMP', + 'DTSTART', 'DUE', 'DURATION', 'EXDATE', 'EXRULE', 'FN', 'FREEBUSY', + 'LABEL', 'LAST-MODIFIED', 'LOCATION', 'METHOD', 'N', 'ORG', + 'PHOTO', 'PRODID', 'RDATE', 'RECURRENCE-ID', 'RELATED-TO', + 'REQUEST-STATUS', 'RESOURCES', 'REV', 'RRULE', 'STANDARD', 'STATUS', + 'SUMMARY', 'TRANSP', 'TRIGGER', 'UID', 'VALARM', 'VAVAILABILITY', + 'VCALENDAR', 'VCARD', 'VEVENT', 'VFREEBUSY', 'VJOURNAL', + 'VTIMEZONE', 'VTODO'] + ) + + # test get_behavior + behavior = base.getBehavior('VCALENDAR') + self.assertEqual( + str(behavior), + "" + ) + self.assertTrue(behavior.isComponent) + + self.assertEqual( + base.getBehavior("invalid_name"), + None + ) + # test for ContentLine (not a component) + non_component_behavior = base.getBehavior('RDATE') + self.assertFalse(non_component_behavior.isComponent) + + def test_MultiDateBehavior(self): + """ + Test MultiDateBehavior + """ + parseRDate = MultiDateBehavior.transformToNative + self.assertEqual( + str(parseRDate(textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))), + "" + ) + self.assertEqual( + str(parseRDate(textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))), + "" + ) + + def test_periodBehavior(self): + """ + Test PeriodBehavior + """ + line = ContentLine('test', [], '', isNative=True) + line.behavior = PeriodBehavior + line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)] + + self.assertEqual( + line.transformFromNative().value, + '20060216T100000/PT2H' + ) + self.assertEqual( + line.transformToNative().value, + [(datetime.datetime(2006, 2, 16, 10, 0), + datetime.timedelta(0, 7200))] + ) + + line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours)) + + self.assertEqual( + line.serialize().strip(), + 'TEST:20060216T100000/PT2H,20060516T100000/PT2H' + ) + + +class TestVTodo(unittest.TestCase): + """ + VTodo Tests + """ + def test_vtodo(self): + """ + Test VTodo + """ + vtodo = get_test_file("vtodo.ics") + obj = base.readOne(vtodo) + obj.vtodo.add('completed') + obj.vtodo.completed.value = datetime.datetime(2015,5,5,13,30) + self.assertEqual(obj.vtodo.completed.serialize()[0:23], + 'COMPLETED:20150505T1330') + obj = base.readOne(obj.serialize()) + self.assertEqual(obj.vtodo.completed.value, + datetime.datetime(2015,5,5,13,30)) + + +class TestVobject(unittest.TestCase): + """ + VObject Tests + """ + max_diff = None + + @classmethod + def setUpClass(cls): + """ + Method for setting up class fixture before running tests in the class. + Fetches test file. + """ + cls.simple_test_cal = get_test_file("simple_test.ics") + + def test_readComponents(self): + """ + Test if reading components correctly + """ + cal = next(readComponents(self.simple_test_cal)) + + self.assertEqual(str(cal), "]>]>") + self.assertEqual(str(cal.vevent.summary), "") + + def test_parseLine(self): + """ + Test line parsing + """ + self.assertEqual(parseLine("BLAH:"), ('BLAH', [], '', None)) + self.assertEqual( + parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"), + ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None) + ) + self.assertEqual( + parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA'), + ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None) + ) + self.assertEqual( + parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"), + ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None) + ) + self.assertEqual( + parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'), + ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None) + ) + self.assertEqual( + parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;'), + ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1') + ) + self.assertRaises(ParseError, parseLine, ":") + + +class TestGeneralFileParsing(unittest.TestCase): + """ + General tests for parsing ics files. + """ + def test_readOne(self): + """ + Test reading first component of ics + """ + cal = get_test_file("silly_test.ics") + silly = base.readOne(cal) + self.assertEqual( + str(silly), + ", , ]>" + ) + self.assertEqual( + str(silly.stuff), + "" + ) + + def test_importing(self): + """ + Test importing ics + """ + cal = get_test_file("standard_test.ics") + c = base.readOne(cal, validate=True) + self.assertEqual( + str(c.vevent.valarm.trigger), + "" + ) + + self.assertEqual( + str(c.vevent.dtstart.value), + "2002-10-28 14:00:00-08:00" + ) + self.assertTrue( + isinstance(c.vevent.dtstart.value, datetime.datetime) + ) + self.assertEqual( + str(c.vevent.dtend.value), + "2002-10-28 15:00:00-08:00" + ) + self.assertTrue( + isinstance(c.vevent.dtend.value, datetime.datetime) + ) + self.assertEqual( + c.vevent.dtstamp.value, + datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc()) + ) + + vevent = c.vevent.transformFromNative() + self.assertEqual( + str(vevent.rrule), + "" + ) + + def test_bad_stream(self): + """ + Test bad ics stream + """ + cal = get_test_file("badstream.ics") + self.assertRaises(ParseError, base.readOne, cal) + + def test_bad_line(self): + """ + Test bad line in ics file + """ + cal = get_test_file("badline.ics") + self.assertRaises(ParseError, base.readOne, cal) + + newcal = base.readOne(cal, ignoreUnreadable=True) + self.assertEqual( + str(newcal.vevent.x_bad_underscore), + '' + ) + + def test_parseParams(self): + """ + Test parsing parameters + """ + self.assertEqual( + base.parseParams(';ALTREP="http://www.wiz.org"'), + [['ALTREP', 'http://www.wiz.org']] + ) + self.assertEqual( + base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'), + [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], + ['NEXT', 'Nope'], ['BAR']] + ) + + +class TestVcards(unittest.TestCase): + """ + Test VCards + """ + @classmethod + def setUpClass(cls): + """ + Method for setting up class fixture before running tests in the class. + Fetches test file. + """ + cls.test_file = get_test_file("vcard_with_groups.ics") + cls.card = base.readOne(cls.test_file) + + def test_vcard_creation(self): + """ + Test creating a vCard + """ + vcard = base.newFromBehavior('vcard', '3.0') + self.assertEqual( + str(vcard), + "" + ) + + def test_default_behavior(self): + """ + Default behavior test. + """ + card = self.card + self.assertEqual( + base.getBehavior('note'), + None + ) + self.assertEqual( + str(card.note.value), + "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line." + ) + + def test_with_groups(self): + """ + vCard groups test + """ + card = self.card + self.assertEqual( + str(card.group), + 'home' + ) + self.assertEqual( + str(card.tel.group), + 'home' + ) + + card.group = card.tel.group = 'new' + self.assertEqual( + str(card.tel.serialize().strip()), + 'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' + ) + self.assertEqual( + str(card.serialize().splitlines()[0]), + 'new.BEGIN:VCARD' + ) + + + def test_vcard_3_parsing(self): + """ + VCARD 3.0 parse test + """ + test_file = get_test_file("simple_3_0_test.ics") + card = base.readOne(test_file) + # value not rendering correctly? + #self.assertEqual( + # card.adr.value, + # "" + #) + self.assertEqual( + card.org.value, + ["University of Novosibirsk", "Department of Octopus Parthenogenesis"] + ) + + for _ in range(3): + new_card = base.readOne(card.serialize()) + self.assertEqual(new_card.org.value, card.org.value) + card = new_card + + +class TestIcalendar(unittest.TestCase): + """ + Tests for icalendar.py + """ + max_diff = None + def test_parseDTStart(self): + """ + Should take a content line and return a datetime object. + """ + self.assertEqual( + parseDtstart(textLineToContentLine("DTSTART:20060509T000000")), + datetime.datetime(2006, 5, 9, 0, 0) + ) + + def test_regexes(self): + """ + Test regex patterns + """ + self.assertEqual( + re.findall(base.patterns['name'], '12foo-bar:yay'), + ['12foo-bar', 'yay'] + ) + self.assertEqual( + re.findall(base.patterns['safe_char'], 'a;b"*,cd'), + ['a', 'b', '*', 'c', 'd'] + ) + self.assertEqual( + re.findall(base.patterns['qsafe_char'], 'a;b"*,cd'), + ['a', ';', 'b', '*', ',', 'c', 'd'] + ) + self.assertEqual( + re.findall(base.patterns['param_value'], + '"quoted";not-quoted;start"after-illegal-quote', + re.VERBOSE), + ['"quoted"', '', 'not-quoted', '', 'start', '', + 'after-illegal-quote', ''] + ) + match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') + self.assertEqual( + match.group('value'), + 'value:;"' + ) + self.assertEqual( + match.group('name'), + 'TEST' + ) + self.assertEqual( + match.group('params'), + ';ALTREP="http://www.wiz.org"' + ) + + def test_stringToTextValues(self): + """ + Test string lists + """ + self.assertEqual( + stringToTextValues(''), + [''] + ) + self.assertEqual( + stringToTextValues('abcd,efgh'), + ['abcd', 'efgh'] + ) + + def test_stringToPeriod(self): + """ + Test datetime strings + """ + self.assertEqual( + stringToPeriod("19970101T180000Z/19970102T070000Z"), + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), + datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc())) + ) + self.assertEqual( + stringToPeriod("19970101T180000Z/PT1H"), + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), + datetime.timedelta(0, 3600)) + ) + + def test_timedeltaToString(self): + """ + Test timedelta strings + """ + self.assertEqual( + timedeltaToString(two_hours), + 'PT2H' + ) + self.assertEqual( + timedeltaToString(datetime.timedelta(minutes=20)), + 'PT20M' + ) + + def test_vtimezone_creation(self): + """ + Test timezones + """ + tzs = dateutil.tz.tzical(get_test_filepath("timezones.ics")) + pacific = icalendar.TimezoneComponent(tzs.get('US/Pacific')) + self.assertEqual( + str(pacific), + ">" + ) + santiago = icalendar.TimezoneComponent(tzs.get('Santiago')) + self.assertEqual( + str(santiago), + ">" + ) + for year in range(2001, 2010): + for month in (2, 9): + dt = datetime.datetime(year, month, 15, + tzinfo=tzs.get('Santiago')) + self.assertTrue(dt.replace(tzinfo=tzs.get('Santiago')), dt) + + @staticmethod + def test_timezone_serializing(): + """ + Serializing with timezones test + """ + tzs = dateutil.tz.tzical(get_test_filepath("timezones.ics")) + pacific = tzs.get('US/Pacific') + cal = base.Component('VCALENDAR') + cal.setBehavior(icalendar.VCalendar2_0) + ev = cal.add('vevent') + ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9, + tzinfo=pacific) + evruleset = rruleset() + evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4], + until=datetime.datetime(2005, 12, 15, 9))) + evruleset.rrule(rrule(MONTHLY, bymonthday=[-1,-5])) + evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific)) + ev.rruleset = evruleset + ev.add('duration').value = datetime.timedelta(hours=1) + + apple = tzs.get('America/Montreal') + ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple) + + def test_pytz_timezone_serializing(self): + """ + Serializing with timezones from pytz test + """ + try: + import pytz + except ImportError: + return self.skipTest("pytz not installed") # NOQA + + # Avoid conflicting cached tzinfo from other tests + def unregister_tzid(tzid): + """Clear tzid from icalendar TZID registry""" + if icalendar.getTzid(tzid, False): + icalendar.registerTzid(tzid, None) + + unregister_tzid('US/Eastern') + eastern = pytz.timezone('US/Eastern') + cal = base.Component('VCALENDAR') + cal.setBehavior(icalendar.VCalendar2_0) + ev = cal.add('vevent') + ev.add('dtstart').value = eastern.localize( + datetime.datetime(2008, 10, 12, 9)) + serialized = cal.serialize() + + expected_vtimezone = get_test_file("tz_us_eastern.ics") + self.assertIn( + expected_vtimezone.replace('\r\n', '\n'), + serialized.replace('\r\n', '\n') + ) + + # Exhaustively test all zones (just looking for no errors) + for tzname in pytz.all_timezones: + unregister_tzid(tzname) + tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname)) + tz.serialize() + + def test_freeBusy(self): + """ + Test freebusy components + """ + test_cal = get_test_file("freebusy.ics") + + vfb = base.newFromBehavior('VFREEBUSY') + vfb.add('uid').value = 'test' + vfb.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) + vfb.add('dtend').value = vfb.dtstart.value + two_hours + vfb.add('freebusy').value = [(vfb.dtstart.value, two_hours / 2)] + vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)] + + self.assertEqual( + vfb.serialize().replace('\r\n', '\n'), + test_cal.replace('\r\n', '\n') + ) + + def test_availablity(self): + """ + Test availability components + """ + test_cal = get_test_file("availablity.ics") + + vcal = base.newFromBehavior('VAVAILABILITY') + vcal.add('uid').value = 'test' + vcal.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + vcal.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) + vcal.add('dtend').value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) + vcal.add('busytype').value = "BUSY" + + av = base.newFromBehavior('AVAILABLE') + av.add('uid').value = 'test1' + av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) + av.add('dtend').value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) + av.add('summary').value = "Available in the morning" + + vcal.add(av) + + self.assertEqual( + vcal.serialize().replace('\r\n', '\n'), + test_cal.replace('\r\n', '\n') + ) + + def test_recurrence(self): + """ + Ensure date valued UNTILs in rrules are in a reasonable timezone, + and include that day (12/28 in this test) + """ + test_file = get_test_file("recurrence.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual( + dates[0], + datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc()) + ) + self.assertEqual( + dates[1], + datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc()) + ) + self.assertEqual( + dates[-1], + datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc()) + ) + + def test_recurring_component(self): + """ + Test recurring events + """ + vevent = RecurringComponent(name='VEVENT') + + # init + self.assertTrue(vevent.isNative) + + # rruleset should be None at this point. + # No rules have been passed or created. + self.assertEqual(vevent.rruleset, None) + + # Now add start and rule for recurring event + vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9) + vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" + self.assertEqual( + list(vevent.rruleset), + [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] + ) + self.assertEqual( + list(vevent.getrruleset(addRDate=True)), + [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)] + ) + + # Also note that dateutil will expand all-day events (datetime.date values) + # to datetime.datetime value with time 0 and no timezone. + vevent.dtstart.value = datetime.date(2005,3,18) + self.assertEqual( + list(vevent.rruleset), + [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] + ) + self.assertEqual( + list(vevent.getrruleset(True)), + [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] + ) + + def test_recurrence_without_tz(self): + """ + Test recurring vevent missing any time zone definitions. + """ + test_file = get_test_file("recurrence-without-tz.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) + self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) + self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) + + def test_recurrence_offset_naive(self): + """ + Ensure recurring vevent missing some time zone definitions is + parsing. See isseu #75. + """ + test_file = get_test_file("recurrence-offset-naive.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) + self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) + self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) + + +class TestChangeTZ(unittest.TestCase): + """ + Tests for change_tz.change_tz + """ + class StubCal(object): + class StubEvent(object): + class Node(object): + def __init__(self, value): + self.value = value + + def __init__(self, dtstart, dtend): + self.dtstart = self.Node(dtstart) + self.dtend = self.Node(dtend) + + def __init__(self, dates): + """ + dates is a list of tuples (dtstart, dtend) + """ + self.vevent_list = [self.StubEvent(*d) for d in dates] + + def test_change_tz(self): + """ + Change the timezones of events in a component to a different + timezone + """ + + # Setup - create a stub vevent list + old_tz = dateutil.tz.gettz('UTC') # 0:00 + new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 + + dates = [ + (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz)), + (datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), + datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz))] + + cal = self.StubCal(dates) + + # Exercise - change the timezone + change_tz(cal, new_tz, dateutil.tz.gettz('UTC')) + + # Test - that the tzs were converted correctly + expected_new_dates = [ + (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz)), + (datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz))] + + for vevent, expected_datepair in zip(cal.vevent_list, + expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + def test_change_tz_utc_only(self): + """ + Change any UTC timezones of events in a component to a different + timezone + """ + + # Setup - create a stub vevent list + utc_tz = dateutil.tz.gettz('UTC') # 0:00 + non_utc_tz = dateutil.tz.gettz('America/Santiago') # -4:00 + new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 + + dates = [ + (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz))] + + cal = self.StubCal(dates) + + # Exercise - change the timezone passing utc_only=True + change_tz(cal, new_tz, dateutil.tz.gettz('UTC'), utc_only=True) + + # Test - that only the utc item has changed + expected_new_dates = [ + (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + dates[0][1])] + + for vevent, expected_datepair in zip(cal.vevent_list, + expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + def test_change_tz_default(self): + """ + Change the timezones of events in a component to a different + timezone, passing a default timezone that is assumed when the events + don't have one + """ + + # Setup - create a stub vevent list + new_tz = dateutil.tz.gettz('America/Chicago') # -5:00 + + dates = [ + (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None))] + + cal = self.StubCal(dates) + + # Exercise - change the timezone + change_tz(cal, new_tz, dateutil.tz.gettz('UTC')) + + # Test - that the tzs were converted correctly + expected_new_dates = [ + (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz))] + + for vevent, expected_datepair in zip(cal.vevent_list, + expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + +if __name__ == '__main__': + unittest.main() diff --git a/radicale_vobject/tests/test_files/availablity.ics b/radicale_vobject/tests/test_files/availablity.ics new file mode 100644 index 0000000..446db07 --- /dev/null +++ b/radicale_vobject/tests/test_files/availablity.ics @@ -0,0 +1,14 @@ +BEGIN:VAVAILABILITY +UID:test +DTSTART:20060216T000000Z +DTEND:20060217T000000Z +BEGIN:AVAILABLE +UID:test1 +DTSTART:20060216T090000Z +DTEND:20060216T120000Z +DTSTAMP:20060215T000000Z +SUMMARY:Available in the morning +END:AVAILABLE +BUSYTYPE:BUSY +DTSTAMP:20060215T000000Z +END:VAVAILABILITY diff --git a/radicale_vobject/tests/test_files/badline.ics b/radicale_vobject/tests/test_files/badline.ics new file mode 100644 index 0000000..ed81a6b --- /dev/null +++ b/radicale_vobject/tests/test_files/badline.ics @@ -0,0 +1,10 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +VERSION:2.0 +BEGIN:VEVENT +DTSTART:19870405T020000 +X-BAD/SLASH:TRUE +X-BAD_UNDERSCORE:TRUE +UID:EC9439B1-FF65-11D6-9973-003065F99D04 +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/badstream.ics b/radicale_vobject/tests/test_files/badstream.ics new file mode 100644 index 0000000..42a3220 --- /dev/null +++ b/radicale_vobject/tests/test_files/badstream.ics @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +CALSCALE:GREGORIAN +X-WR-TIMEZONE;VALUE=TEXT:US/Pacific +METHOD:PUBLISH +PRODID:-//Apple Computer\, Inc//iCal 1.0//EN +X-WR-CALNAME;VALUE=TEXT:Example +VERSION:2.0 +BEGIN:VEVENT +DTSTART:20021028T140000Z +BEGIN:VALARM +TRIGGER:a20021028120000 +ACTION:DISPLAY +DESCRIPTION:This trigger has a nonsensical value +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/freebusy.ics b/radicale_vobject/tests/test_files/freebusy.ics new file mode 100644 index 0000000..92561d5 --- /dev/null +++ b/radicale_vobject/tests/test_files/freebusy.ics @@ -0,0 +1,8 @@ +BEGIN:VFREEBUSY +UID:test +DTSTART:20060216T010000Z +DTEND:20060216T030000Z +DTSTAMP:20060215T000000Z +FREEBUSY:20060216T010000Z/PT1H +FREEBUSY:20060216T010000Z/20060216T030000Z +END:VFREEBUSY diff --git a/radicale_vobject/tests/test_files/journal.ics b/radicale_vobject/tests/test_files/journal.ics new file mode 100644 index 0000000..5c4d7d6 --- /dev/null +++ b/radicale_vobject/tests/test_files/journal.ics @@ -0,0 +1,15 @@ +BEGIN:VJOURNAL +UID:19970901T130000Z-123405@example.com +DTSTAMP:19970901T130000Z +DTSTART;VALUE=DATE:19970317 +SUMMARY:Staff meeting minutes +DESCRIPTION:1. Staff meeting: Participants include Joe\, + Lisa\, and Bob. Aurora project plans were reviewed. + There is currently no budget reserves for this project. + Lisa will escalate to management. Next meeting on Tuesday.\n + 2. Telephone Conference: ABC Corp. sales representative + called to discuss new printer. Promised to get us a demo by + Friday.\n3. Henry Miller (Handsoff Insurance): Car was + totaled by tree. Is looking into a loaner car. 555-2323 + (tel). +END:VJOURNAL diff --git a/radicale_vobject/tests/test_files/more_tests.txt b/radicale_vobject/tests/test_files/more_tests.txt new file mode 100644 index 0000000..779e79b --- /dev/null +++ b/radicale_vobject/tests/test_files/more_tests.txt @@ -0,0 +1,85 @@ + +Unicode in vCards +................. + +>>> import vobject +>>> card = vobject.vCard() +>>> card.add('fn').value = u'Hello\u1234 World!' +>>> card.add('n').value = vobject.vcard.Name('World', u'Hello\u1234') +>>> card.add('adr').value = vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA') +>>> card +, , ]> +>>> card.serialize() +u'BEGIN:VCARD\r\nVERSION:3.0\r\nADR:;;5\u1234 Nowhere\\, Apt 1;Berkeley;CA;94704;USA\r\nFN:Hello\u1234 World!\r\nN:World;Hello\u1234;;;\r\nEND:VCARD\r\n' +>>> print(card.serialize()) +BEGIN:VCARD +VERSION:3.0 +ADR:;;5ሴ Nowhere\, Apt 1;Berkeley;CA;94704;USA +FN:Helloሴ World! +N:World;Helloሴ;;; +END:VCARD + +Helper function +............... +>>> from pkg_resources import resource_stream +>>> def get_stream(path): +... try: +... return resource_stream(__name__, 'test_files/' + path) +... except: # different paths, depending on whether doctest is run directly +... return resource_stream(__name__, path) + +Unicode in TZID +............... +>>> f = get_stream("tzid_8bit.ics") +>>> cal = vobject.readOne(f) +>>> print(cal.vevent.dtstart.value) +2008-05-30 15:00:00+06:00 +>>> print(cal.vevent.dtstart.serialize()) +DTSTART;TZID=Екатеринбург:20080530T150000 + +Commas in TZID +.............. +>>> f = get_stream("ms_tzid.ics") +>>> cal = vobject.readOne(f) +>>> print(cal.vevent.dtstart.value) +2008-05-30 15:00:00+10:00 + +Equality in vCards +.................. + +>>> card.adr.value == vobject.vcard.Address('Just a street') +False +>>> card.adr.value == vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA') +True + +Organization (org) +.................. + +>>> card.add('org').value = ["Company, Inc.", "main unit", "sub-unit"] +>>> print(card.org.serialize()) +ORG:Company\, Inc.;main unit;sub-unit + +Ruby escapes semi-colons in rrules +.................................. + +>>> f = get_stream("ruby_rrule.ics") +>>> cal = vobject.readOne(f) +>>> iter(cal.vevent.rruleset).next() +datetime.datetime(2003, 1, 1, 7, 0) + +quoted-printable +................ + +>>> vcf = 'BEGIN:VCARD\nVERSION:2.1\nN;ENCODING=QUOTED-PRINTABLE:;=E9\nFN;ENCODING=QUOTED-PRINTABLE:=E9\nTEL;HOME:0111111111\nEND:VCARD\n\n' +>>> vcf = vobject.readOne(vcf) +>>> vcf.n.value + +>>> vcf.n.value.given +u'\xe9' +>>> vcf.serialize() +'BEGIN:VCARD\r\nVERSION:2.1\r\nFN:\xc3\xa9\r\nN:;\xc3\xa9;;;\r\nTEL:0111111111\r\nEND:VCARD\r\n' + +>>> vcs = 'BEGIN:VCALENDAR\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nVERSION:1.0\r\nBEGIN:VEVENT\r\nDESCRIPTION;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:foo =C3=A5=0Abar =C3=A4=\r\n=0Abaz =C3=B6\r\nUID:20080406T152030Z-7822\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n' +>>> vcs = vobject.readOne(vcs, allowQP = True) +>>> vcs.serialize() +'BEGIN:VCALENDAR\r\nVERSION:1.0\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nBEGIN:VEVENT\r\nUID:20080406T152030Z-7822\r\nDESCRIPTION:foo \xc3\xa5\\nbar \xc3\xa4\\nbaz \xc3\xb6\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n' diff --git a/radicale_vobject/tests/test_files/ms_tzid.ics b/radicale_vobject/tests/test_files/ms_tzid.ics new file mode 100644 index 0000000..0db2c5c --- /dev/null +++ b/radicale_vobject/tests/test_files/ms_tzid.ics @@ -0,0 +1,39 @@ +BEGIN:VCALENDAR +PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Canberra, Melbourne, Sydney +BEGIN:STANDARD +DTSTART:20010325T020000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3;UNTIL=20050327T070000Z +TZOFFSETFROM:+1100 +TZOFFSETTO:+1000 +TZNAME:Standard Time +END:STANDARD +BEGIN:STANDARD +DTSTART:20060402T020000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z +TZOFFSETFROM:+1100 +TZOFFSETTO:+1000 +TZNAME:Standard Time +END:STANDARD +BEGIN:STANDARD +DTSTART:20070325T020000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +TZOFFSETFROM:+1100 +TZOFFSETTO:+1000 +TZNAME:Standard Time +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20001029T020000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:+1000 +TZOFFSETTO:+1100 +TZNAME:Daylight Savings Time +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:CommaTest +DTSTART;TZID="Canberra, Melbourne, Sydney":20080530T150000 +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/radicale_vobject/tests/test_files/recurrence-offset-naive.ics b/radicale_vobject/tests/test_files/recurrence-offset-naive.ics new file mode 100644 index 0000000..5a9156d --- /dev/null +++ b/radicale_vobject/tests/test_files/recurrence-offset-naive.ics @@ -0,0 +1,9 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +DTSTART;VALUE=DATE:20130117 +DTEND;VALUE=DATE:20130118 +RRULE:FREQ=WEEKLY;UNTIL=20130330T230000Z;BYDAY=TH +SUMMARY:Meeting +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/recurrence-without-tz.ics b/radicale_vobject/tests/test_files/recurrence-without-tz.ics new file mode 100644 index 0000000..d4ae3b9 --- /dev/null +++ b/radicale_vobject/tests/test_files/recurrence-without-tz.ics @@ -0,0 +1,9 @@ +BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +DTSTART;VALUE=DATE:20130117 +DTEND;VALUE=DATE:20130118 +RRULE:FREQ=WEEKLY;UNTIL=20130330;BYDAY=TH +SUMMARY:Meeting +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/recurrence.ics b/radicale_vobject/tests/test_files/recurrence.ics new file mode 100644 index 0000000..f592234 --- /dev/null +++ b/radicale_vobject/tests/test_files/recurrence.ics @@ -0,0 +1,30 @@ +BEGIN:VCALENDAR +VERSION + :2.0 +PRODID + :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN +BEGIN:VEVENT +CREATED + :20060327T214227Z +LAST-MODIFIED + :20060313T080829Z +DTSTAMP + :20060116T231602Z +UID + :70922B3051D34A9E852570EC00022388 +SUMMARY + :Monthly - All Hands Meeting with Joe Smith +STATUS + :CONFIRMED +CLASS + :PUBLIC +RRULE + :FREQ=MONTHLY;UNTIL=20061228;INTERVAL=1;BYDAY=4TH +DTSTART + :20060126T230000Z +DTEND + :20060127T000000Z +DESCRIPTION + :Repeat Meeting: - Occurs every 4th Thursday of each month +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/ruby_rrule.ics b/radicale_vobject/tests/test_files/ruby_rrule.ics new file mode 100644 index 0000000..6999513 --- /dev/null +++ b/radicale_vobject/tests/test_files/ruby_rrule.ics @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +PRODID:-//LinkeSOFT GmbH//NONSGML DIMEX//EN +BEGIN:VEVENT +SEQUENCE:0 +RRULE:FREQ=DAILY\;COUNT=10 +DTEND:20030101T080000 +UID:2008-05-29T17:31:42+02:00_865561242 +CATEGORIES:Unfiled +SUMMARY:Something +DTSTART:20030101T070000 +DTSTAMP:20080529T152100 +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/radicale_vobject/tests/test_files/silly_test.ics b/radicale_vobject/tests/test_files/silly_test.ics new file mode 100644 index 0000000..2ee72db --- /dev/null +++ b/radicale_vobject/tests/test_files/silly_test.ics @@ -0,0 +1,5 @@ +sillyname:name +profile:sillyprofile +stuff:folded + line +morestuff;asinine:this line is not folded, but in practice probably ought to be, as it is exceptionally long, and moreover demonstratively stupid diff --git a/radicale_vobject/tests/test_files/simple_2_0_test.ics b/radicale_vobject/tests/test_files/simple_2_0_test.ics new file mode 100644 index 0000000..9d8370b --- /dev/null +++ b/radicale_vobject/tests/test_files/simple_2_0_test.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//PYVOBJECT//NONSGML Version 1//EN +BEGIN:VEVENT +UID:Not very random UID +DTSTART:20060509T000000 +CREATED:20060101T180000Z +DESCRIPTION:Test event +DTSTAMP:20170626T000000Z +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/simple_3_0_test.ics b/radicale_vobject/tests/test_files/simple_3_0_test.ics new file mode 100644 index 0000000..1faf80d --- /dev/null +++ b/radicale_vobject/tests/test_files/simple_3_0_test.ics @@ -0,0 +1,13 @@ +BEGIN:VCARD +VERSION:3.0 +FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) +N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto) +NICKNAME:gnat and gnu and pluto +BDAY;value=date:02-10 +TEL;type=HOME:+01-(0)2-765.43.21 +TEL;type=CELL:+01-(0)5-555.55.55 +ACCOUNT;type=HOME:010-1234567-05 +ADR;type=HOME:;;Haight Street 512\;\nEscape\, Test;Novosibirsk;;80214;Gnuland +TEL;type=HOME:+01-(0)2-876.54.32 +ORG:University of Novosibirsk;Department of Octopus Parthenogenesis +END:VCARD diff --git a/radicale_vobject/tests/test_files/simple_test.ics b/radicale_vobject/tests/test_files/simple_test.ics new file mode 100644 index 0000000..aefb51e --- /dev/null +++ b/radicale_vobject/tests/test_files/simple_test.ics @@ -0,0 +1,5 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +SUMMARY;blah=hi!:Bastille Day Party +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/standard_test.ics b/radicale_vobject/tests/test_files/standard_test.ics new file mode 100644 index 0000000..4593fe1 --- /dev/null +++ b/radicale_vobject/tests/test_files/standard_test.ics @@ -0,0 +1,41 @@ +BEGIN:VCALENDAR +CALSCALE:GREGORIAN +X-WR-TIMEZONE;VALUE=TEXT:US/Pacific +METHOD:PUBLISH +PRODID:-//Apple Computer\, Inc//iCal 1.0//EN +X-WR-CALNAME;VALUE=TEXT:Example +VERSION:2.0 +BEGIN:VEVENT +SEQUENCE:5 +DTSTART;TZID=US/Pacific:20021028T140000 +RRULE:FREQ=Weekly;COUNT=10 +DTSTAMP:20021028T011706Z +SUMMARY:Coffee with Jason +UID:EC9439B1-FF65-11D6-9973-003065F99D04 +DTEND;TZID=US/Pacific:20021028T150000 +BEGIN:VALARM +TRIGGER;VALUE=DURATION:-P1D +ACTION:DISPLAY +DESCRIPTION:Event reminder\, with comma\nand line feed +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +X-LIC-LOCATION:Random location +TZID:US/Pacific +LAST-MODIFIED:19870101T000000Z +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +TZNAME:PST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +TZNAME:PDT +END:DAYLIGHT +END:VTIMEZONE +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/timezones.ics b/radicale_vobject/tests/test_files/timezones.ics new file mode 100644 index 0000000..e839223 --- /dev/null +++ b/radicale_vobject/tests/test_files/timezones.ics @@ -0,0 +1,107 @@ +BEGIN:VTIMEZONE +TZID:US/Pacific +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +TZNAME:PST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +TZNAME:PDT +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VTIMEZONE +TZID:US/Eastern +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VTIMEZONE +TZID:Santiago +BEGIN:STANDARD +DTSTART:19700314T000000 +TZOFFSETFROM:-0300 +TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA +TZNAME:Pacific SA Standard Time +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19701010T000000 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0300 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA +TZNAME:Pacific SA Daylight Time +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VTIMEZONE +TZID:W. Europe +BEGIN:STANDARD +DTSTART:19701025T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +TZNAME:W. Europe Standard Time +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19700329T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +TZNAME:W. Europe Daylight Time +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VTIMEZONE +TZID:US/Fictitious-Eastern +LAST-MODIFIED:19870101T000000Z +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VTIMEZONE +TZID:America/Montreal +LAST-MODIFIED:20051013T233643Z +BEGIN:DAYLIGHT +DTSTART:20050403T070000 +TZOFFSETTO:-0400 +TZOFFSETFROM:+0000 +TZNAME:EDT +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:20051030T020000 +TZOFFSETTO:-0500 +TZOFFSETFROM:-0400 +TZNAME:EST +END:STANDARD +END:VTIMEZONE diff --git a/radicale_vobject/tests/test_files/tz_us_eastern.ics b/radicale_vobject/tests/test_files/tz_us_eastern.ics new file mode 100644 index 0000000..e611477 --- /dev/null +++ b/radicale_vobject/tests/test_files/tz_us_eastern.ics @@ -0,0 +1,31 @@ +BEGIN:VTIMEZONE +TZID:US/Eastern +BEGIN:STANDARD +DTSTART:20001029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;UNTIL=20061029T060000Z +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:STANDARD +DTSTART:20071104T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +TZNAME:EST +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20000402T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z +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 +END:VTIMEZONE diff --git a/radicale_vobject/tests/test_files/tzid_8bit.ics b/radicale_vobject/tests/test_files/tzid_8bit.ics new file mode 100644 index 0000000..91d35d2 --- /dev/null +++ b/radicale_vobject/tests/test_files/tzid_8bit.ics @@ -0,0 +1,23 @@ +BEGIN:VCALENDAR +PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Екатеринбург +BEGIN:STANDARD +DTSTART:16011028T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:+0600 +TZOFFSETTO:+0500 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010325T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +TZOFFSETFROM:+0500 +TZOFFSETTO:+0600 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:CyrillicTest +DTSTART;TZID=Екатеринбург:20080530T150000 +END:VEVENT +END:VCALENDAR diff --git a/radicale_vobject/tests/test_files/utf8_test.ics b/radicale_vobject/tests/test_files/utf8_test.ics new file mode 100644 index 0000000..fbb9ada --- /dev/null +++ b/radicale_vobject/tests/test_files/utf8_test.ics @@ -0,0 +1,39 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +CALSCALE:GREGORIAN +PRODID:-//EVDB//www.evdb.com//EN +VERSION:2.0 +X-WR-CALNAME:EVDB Event Feed +BEGIN:VEVENT +DTSTART:20060922T000100Z +DTEND:20060922T050100Z +DTSTAMP:20050914T163414Z +SUMMARY:The title こんにちはキティ +DESCRIPTION:hello\nHere is a description\n\n\nこんにちはキティ + \n\n\n\nZwei Java-schwere Entwicklerpositionen und irgendeine Art sond + erbar-klingende Netzsichtbarmachungöffnung\, an einer interessanten F + irma im Gebäude\, in dem ich angerufenen Semantic Research bearbeite. + 1. Zauberer Des Semantica Software Engineer 2. Älterer Semantica Sof + tware-Englisch-3. Graph/Semantica Netz-Visualization/Navigation Sie ei + ngestufte Software-Entwicklung für die Regierung. Die Firma ist stark + und die Projekte sind sehr kühl und schließen irgendeinen Spielraum + ein. Wenn ich Ihnen irgendwie mehr erkläre\, muß ich Sie töten. Ps + . Tat schnell -- jemand ist\, wenn es hier interviewt\, wie ich dieses + schreibe. Er schaut intelligent (er trägt Kleidhosen) Semantica Soft + ware Engineer FIRMA: Semantische Forschung\, Inc. REPORTS ZU: Vizeprä + sident\, Produkt-Entwicklung POSITION: San Diego (Pint Loma) WEB SITE: + www.semanticresearch.com email: dorie@semanticresearch.com FIRMA-HINT + ERGRUND Semantische Forschung ist der führende Versorger der semantis + cher Netzwerkanschluß gegründeten nicht linearen Wissen Darstellung + Werkzeuge. Die Firma stellt diese Werkzeuge zum Intel\, zur reg.\, zum + EDU und zu den kommerziellen Märkten zur Verfügung. BRINGEN SIE ZUS + AMMENFASSUNG IN POSITION Semantische Forschung\, Inc. basiert in San D + iego\, Ca im alten realen Weltsan Diego Haus...\, das wir den Weltbest + en Platz haben zum zu arbeiten. Wir suchen nach Superstarentwicklern\, + um uns in der fortfahrenden Entwicklung unserer Semantica Produktseri + e zu unterstützen. +LOCATION:こんにちはキティ +SEQUENCE:0 +UID:E0-001-000276068-2 +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/radicale_vobject/tests/test_files/vcard_with_groups.ics b/radicale_vobject/tests/test_files/vcard_with_groups.ics new file mode 100644 index 0000000..d64ff90 --- /dev/null +++ b/radicale_vobject/tests/test_files/vcard_with_groups.ics @@ -0,0 +1,18 @@ +home.begin:vcard +version:3.0 +source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE +name:Meister Berger +fn:Meister Berger +n:Berger;Meister +bday;value=date:1963-09-21 +o:Universit=E6t G=F6rlitz +title:Mayor +title;language=de;value=text:Burgermeister +note:The Mayor of the great city of + Goerlitz in the great country of Germany.\nNext line. +email;internet:mb@goerlitz.de +home.tel;type=fax,voice;type=msg:+49 3581 123456 +home.label:Hufenshlagel 1234\n + 02828 Goerlitz\n + Deutschland +END:VCARD diff --git a/radicale_vobject/tests/test_files/vtodo.ics b/radicale_vobject/tests/test_files/vtodo.ics new file mode 100644 index 0000000..26b577c --- /dev/null +++ b/radicale_vobject/tests/test_files/vtodo.ics @@ -0,0 +1,13 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VTODO +UID:20070313T123432Z-456553@example.com +DTSTAMP:20070313T123432Z +DUE;VALUE=DATE:20070501 +SUMMARY:Submit Quebec Income Tax Return for 2006 +CLASS:CONFIDENTIAL +CATEGORIES:FAMILY,FINANCE +STATUS:NEEDS-ACTION +END:VTODO +END:VCALENDAR diff --git a/setup.cfg b/setup.cfg index 83848de..7ee94d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,5 +5,11 @@ test = pytest python-tag = py3 [tool:pytest] -addopts = --flake8 --isort --cov radicale -r s -norecursedirs = dist .cache .git build Radicale.egg-info .eggs venv radicale_vobject +addopts = --flake8 --isort --cov radicale --cov radicale_vobject -r s +norecursedirs = dist .cache .git build Radicale.egg-info .eggs venv + +[isort] +skip = radicale_vobject + +[flake8] +exclude = radicale_vobject/*