# -*- coding: utf-8 -*- from __future__ import unicode_literals from datetime import date from datetime import datetime from datetime import time from datetime import timedelta from icalendar.parser import Parameters import unittest from icalendar.prop import vDatetime from icalendar.windows_to_olson import WINDOWS_TO_OLSON import pytz class TestProp(unittest.TestCase): def test_prop_vBinary(self): from ..prop import vBinary txt = b'This is gibberish' txt_ical = b'VGhpcyBpcyBnaWJiZXJpc2g=' self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) # The roundtrip test txt = b'Binary data \x13 \x56' txt_ical = b'QmluYXJ5IGRhdGEgEyBW' self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) self.assertIsInstance(vBinary('txt').params, Parameters) self.assertEqual( vBinary('txt').params, {'VALUE': 'BINARY', 'ENCODING': 'BASE64'} ) # Long data should not have line breaks, as that would interfere txt = b'a' * 99 txt_ical = b'YWFh' * 33 self.assertEqual(vBinary(txt).to_ical(), txt_ical) self.assertEqual(vBinary.from_ical(txt_ical), txt) def test_prop_vBoolean(self): from ..prop import vBoolean self.assertEqual(vBoolean(True).to_ical(), b'TRUE') self.assertEqual(vBoolean(0).to_ical(), b'FALSE') # The roundtrip test self.assertEqual(vBoolean.from_ical(vBoolean(True).to_ical()), True) self.assertEqual(vBoolean.from_ical('true'), True) def test_prop_vCalAddress(self): from ..prop import vCalAddress txt = b'MAILTO:maxm@mxm.dk' a = vCalAddress(txt) a.params['cn'] = 'Max M' self.assertEqual(a.to_ical(), txt) self.assertIsInstance(a.params, Parameters) self.assertEqual(a.params, {'CN': 'Max M'}) self.assertEqual(vCalAddress.from_ical(txt), 'MAILTO:maxm@mxm.dk') def test_prop_vFloat(self): from ..prop import vFloat self.assertEqual(vFloat(1.0).to_ical(), b'1.0') self.assertEqual(vFloat.from_ical('42'), 42.0) self.assertEqual(vFloat(42).to_ical(), b'42.0') def test_prop_vInt(self): from ..prop import vInt self.assertEqual(vInt(42).to_ical(), b'42') self.assertEqual(vInt.from_ical('13'), 13) self.assertRaises(ValueError, vInt.from_ical, '1s3') def test_prop_vDDDLists(self): from ..prop import vDDDLists dt_list = vDDDLists.from_ical('19960402T010000Z') self.assertTrue(isinstance(dt_list, list)) self.assertEqual(len(dt_list), 1) self.assertTrue(isinstance(dt_list[0], datetime)) self.assertEqual(str(dt_list[0]), '1996-04-02 01:00:00+00:00') p = '19960402T010000Z,19960403T010000Z,19960404T010000Z' dt_list = vDDDLists.from_ical(p) self.assertEqual(len(dt_list), 3) self.assertEqual(str(dt_list[0]), '1996-04-02 01:00:00+00:00') self.assertEqual(str(dt_list[2]), '1996-04-04 01:00:00+00:00') dt_list = vDDDLists([]) self.assertEqual(dt_list.to_ical(), b'') dt_list = vDDDLists([datetime(2000, 1, 1)]) self.assertEqual(dt_list.to_ical(), b'20000101T000000') dt_list = vDDDLists([datetime(2000, 1, 1), datetime(2000, 11, 11)]) self.assertEqual(dt_list.to_ical(), b'20000101T000000,20001111T000000') def test_prop_vDDDTypes(self): from ..prop import vDDDTypes self.assertTrue(isinstance(vDDDTypes.from_ical('20010101T123000'), datetime)) self.assertEqual(vDDDTypes.from_ical('20010101T123000Z'), pytz.utc.localize(datetime(2001, 1, 1, 12, 30))) self.assertTrue(isinstance(vDDDTypes.from_ical('20010101'), date)) self.assertEqual(vDDDTypes.from_ical('P31D'), timedelta(31)) self.assertEqual(vDDDTypes.from_ical('-P31D'), timedelta(-31)) # Bad input self.assertRaises(ValueError, vDDDTypes, 42) def test_prop_vDate(self): from ..prop import vDate self.assertEqual(vDate(date(2001, 1, 1)).to_ical(), b'20010101') self.assertEqual(vDate(date(1899, 1, 1)).to_ical(), b'18990101') self.assertEqual(vDate.from_ical('20010102'), date(2001, 1, 2)) self.assertRaises(ValueError, vDate, 'd') def test_prop_vDatetime(self): from ..prop import vDatetime dt = datetime(2001, 1, 1, 12, 30, 0) self.assertEqual(vDatetime(dt).to_ical(), b'20010101T123000') self.assertEqual(vDatetime.from_ical('20000101T120000'), datetime(2000, 1, 1, 12, 0)) dutc = pytz.utc.localize(datetime(2001, 1, 1, 12, 30, 0)) self.assertEqual(vDatetime(dutc).to_ical(), b'20010101T123000Z') dutc = pytz.utc.localize(datetime(1899, 1, 1, 12, 30, 0)) self.assertEqual(vDatetime(dutc).to_ical(), b'18990101T123000Z') self.assertEqual(vDatetime.from_ical('20010101T000000'), datetime(2001, 1, 1, 0, 0)) self.assertRaises(ValueError, vDatetime.from_ical, '20010101T000000A') utc = vDatetime.from_ical('20010101T000000Z') self.assertEqual(vDatetime(utc).to_ical(), b'20010101T000000Z') # 1 minute before transition to DST dat = vDatetime.from_ical('20120311T015959', 'America/Denver') self.assertEqual(dat.strftime('%Y%m%d%H%M%S %z'), '20120311015959 -0700') # After transition to DST dat = vDatetime.from_ical('20120311T030000', 'America/Denver') self.assertEqual(dat.strftime('%Y%m%d%H%M%S %z'), '20120311030000 -0600') dat = vDatetime.from_ical('20101010T000000', 'Europe/Vienna') self.assertEqual(vDatetime(dat).to_ical(), b'20101010T000000') def test_prop_vDuration(self): from ..prop import vDuration self.assertEqual(vDuration(timedelta(11)).to_ical(), b'P11D') self.assertEqual(vDuration(timedelta(-14)).to_ical(), b'-P14D') self.assertEqual( vDuration(timedelta(1, 7384)).to_ical(), b'P1DT2H3M4S' ) self.assertEqual(vDuration(timedelta(1, 7380)).to_ical(), b'P1DT2H3M') self.assertEqual(vDuration(timedelta(1, 7200)).to_ical(), b'P1DT2H') self.assertEqual(vDuration(timedelta(0, 7200)).to_ical(), b'PT2H') self.assertEqual(vDuration(timedelta(0, 7384)).to_ical(), b'PT2H3M4S') self.assertEqual(vDuration(timedelta(0, 184)).to_ical(), b'PT3M4S') self.assertEqual(vDuration(timedelta(0, 22)).to_ical(), b'PT22S') self.assertEqual(vDuration(timedelta(0, 3622)).to_ical(), b'PT1H0M22S') self.assertEqual(vDuration(timedelta(days=1, hours=5)).to_ical(), b'P1DT5H') self.assertEqual(vDuration(timedelta(hours=-5)).to_ical(), b'-PT5H') self.assertEqual(vDuration(timedelta(days=-1, hours=-5)).to_ical(), b'-P1DT5H') # How does the parsing work? self.assertEqual(vDuration.from_ical('PT1H0M22S'), timedelta(0, 3622)) self.assertRaises(ValueError, vDuration.from_ical, 'kox') self.assertEqual(vDuration.from_ical('-P14D'), timedelta(-14)) self.assertRaises(ValueError, vDuration, 11) def test_prop_vPeriod(self): from ..prop import vPeriod # One day in exact datetimes per = (datetime(2000, 1, 1), datetime(2000, 1, 2)) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/20000102T000000') per = (datetime(2000, 1, 1), timedelta(days=31)) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/P31D') # Roundtrip p = vPeriod.from_ical('20000101T000000/20000102T000000') self.assertEqual( p, (datetime(2000, 1, 1, 0, 0), datetime(2000, 1, 2, 0, 0)) ) self.assertEqual(vPeriod(p).to_ical(), b'20000101T000000/20000102T000000') self.assertEqual(vPeriod.from_ical('20000101T000000/P31D'), (datetime(2000, 1, 1, 0, 0), timedelta(31))) # Roundtrip with absolute time p = vPeriod.from_ical('20000101T000000Z/20000102T000000Z') self.assertEqual(vPeriod(p).to_ical(), b'20000101T000000Z/20000102T000000Z') # And an error self.assertRaises(ValueError, vPeriod.from_ical, '20000101T000000/Psd31D') # Timezoned dk = pytz.timezone('Europe/Copenhagen') start = dk.localize(datetime(2000, 1, 1)) end = dk.localize(datetime(2000, 1, 2)) per = (start, end) self.assertEqual(vPeriod(per).to_ical(), b'20000101T000000/20000102T000000') self.assertEqual(vPeriod(per).params['TZID'], 'Europe/Copenhagen') p = vPeriod((dk.localize(datetime(2000, 1, 1)), timedelta(days=31))) self.assertEqual(p.to_ical(), b'20000101T000000/P31D') def test_prop_vWeekday(self): from ..prop import vWeekday self.assertEqual(vWeekday('mo').to_ical(), b'MO') self.assertRaises(ValueError, vWeekday, 'erwer') self.assertEqual(vWeekday.from_ical('mo'), 'MO') self.assertEqual(vWeekday.from_ical('+3mo'), '+3MO') self.assertRaises(ValueError, vWeekday.from_ical, 'Saturday') self.assertEqual(vWeekday('+mo').to_ical(), b'+MO') self.assertEqual(vWeekday('+3mo').to_ical(), b'+3MO') self.assertEqual(vWeekday('-tu').to_ical(), b'-TU') def test_prop_vFrequency(self): from ..prop import vFrequency self.assertRaises(ValueError, vFrequency, 'bad test') self.assertEqual(vFrequency('daily').to_ical(), b'DAILY') self.assertEqual(vFrequency('daily').from_ical('MONTHLY'), 'MONTHLY') def test_prop_vRecur(self): from ..prop import vRecur # Let's see how close we can get to one from the rfc: # FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30 r = dict({'freq': 'yearly', 'interval': 2}) r.update({ 'bymonth': 1, 'byday': 'su', 'byhour': [8, 9], 'byminute': 30 }) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) r = vRecur(FREQ='yearly', INTERVAL=2) r.update({ 'BYMONTH': 1, 'BYDAY': 'su', 'BYHOUR': [8, 9], 'BYMINUTE': 30, }) self.assertEqual( r.to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) r = vRecur(freq='DAILY', count=10) r['bysecond'] = [0, 15, 30, 45] self.assertEqual(r.to_ical(), b'FREQ=DAILY;COUNT=10;BYSECOND=0,15,30,45') r = vRecur(freq='DAILY', until=datetime(2005, 1, 1, 12, 0, 0)) self.assertEqual(r.to_ical(), b'FREQ=DAILY;UNTIL=20050101T120000') # How do we fare with regards to parsing? r = vRecur.from_ical('FREQ=DAILY;INTERVAL=2;COUNT=10') self.assertEqual(r, {'COUNT': [10], 'FREQ': ['DAILY'], 'INTERVAL': [2]}) self.assertEqual( vRecur(r).to_ical(), b'FREQ=DAILY;COUNT=10;INTERVAL=2' ) r = vRecur.from_ical('FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=-SU;' 'BYHOUR=8,9;BYMINUTE=30') self.assertEqual( r, {'BYHOUR': [8, 9], 'BYDAY': ['-SU'], 'BYMINUTE': [30], 'BYMONTH': [1], 'FREQ': ['YEARLY'], 'INTERVAL': [2]} ) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=-SU;' b'BYMONTH=1' ) # Some examples from the spec r = vRecur.from_ical('FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1') self.assertEqual(vRecur(r).to_ical(), b'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1') p = 'FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30' r = vRecur.from_ical(p) self.assertEqual( vRecur(r).to_ical(), b'FREQ=YEARLY;INTERVAL=2;BYMINUTE=30;BYHOUR=8,9;BYDAY=SU;BYMONTH=1' ) # and some errors self.assertRaises(ValueError, vRecur.from_ical, 'BYDAY=12') # when key is not RFC-compliant, parse it as vText r = vRecur.from_ical('FREQ=MONTHLY;BYOTHER=TEXT;BYEASTER=-3') self.assertEqual(vRecur(r).to_ical(), b'FREQ=MONTHLY;BYEASTER=-3;BYOTHER=TEXT') def test_prop_vText(self): from ..prop import vText self.assertEqual(vText('Simple text').to_ical(), b'Simple text') # Escaped text t = vText('Text ; with escaped, chars') self.assertEqual(t.to_ical(), b'Text \\; with escaped\\, chars') # Escaped newlines self.assertEqual(vText('Text with escaped\\N chars').to_ical(), b'Text with escaped\\n chars') # If you pass a unicode object, it will be utf-8 encoded. As this is # the (only) standard that RFC 2445 support. t = vText('international chars \xe4\xf6\xfc') self.assertEqual(t.to_ical(), b'international chars \xc3\xa4\xc3\xb6\xc3\xbc') # and parsing? self.assertEqual(vText.from_ical('Text \\; with escaped\\, chars'), 'Text ; with escaped, chars') t = vText.from_ical('A string with\\; some\\\\ characters in\\it') self.assertEqual(t, "A string with; some\\ characters in\\it") # We are forgiving to utf-8 encoding errors: # We intentionally use a string with unexpected encoding # self.assertEqual(vText.from_ical(b'Ol\xe9'), 'Ol\ufffd') # Notice how accented E character, encoded with latin-1, got replaced # with the official U+FFFD REPLACEMENT CHARACTER. def test_prop_vTime(self): from ..prop import vTime self.assertEqual(vTime(12, 30, 0).to_ical(), '123000') self.assertEqual(vTime.from_ical('123000'), time(12, 30)) # We should also fail, right? self.assertRaises(ValueError, vTime.from_ical, '263000') def test_prop_vUri(self): from ..prop import vUri self.assertEqual(vUri('http://www.example.com/').to_ical(), b'http://www.example.com/') self.assertEqual(vUri.from_ical('http://www.example.com/'), 'http://www.example.com/') def test_prop_vGeo(self): from ..prop import vGeo # Pass a list self.assertEqual(vGeo([1.2, 3.0]).to_ical(), '1.2;3.0') # Pass a tuple self.assertEqual(vGeo((1.2, 3.0)).to_ical(), '1.2;3.0') g = vGeo.from_ical('37.386013;-122.082932') self.assertEqual(g, (float('37.386013'), float('-122.082932'))) self.assertEqual(vGeo(g).to_ical(), '37.386013;-122.082932') self.assertRaises(ValueError, vGeo, 'g') def test_prop_vUTCOffset(self): from ..prop import vUTCOffset self.assertEqual(vUTCOffset(timedelta(hours=2)).to_ical(), '+0200') self.assertEqual(vUTCOffset(timedelta(hours=-5)).to_ical(), '-0500') self.assertEqual(vUTCOffset(timedelta()).to_ical(), '+0000') self.assertEqual(vUTCOffset(timedelta(minutes=-30)).to_ical(), '-0030') self.assertEqual( vUTCOffset(timedelta(hours=2, minutes=-30)).to_ical(), '+0130' ) self.assertEqual(vUTCOffset(timedelta(hours=1, minutes=30)).to_ical(), '+0130') # Support seconds self.assertEqual(vUTCOffset(timedelta(hours=1, minutes=30, seconds=7)).to_ical(), '+013007') # Parsing self.assertEqual(vUTCOffset.from_ical('0000'), timedelta(0)) self.assertEqual(vUTCOffset.from_ical('-0030'), timedelta(-1, 84600)) self.assertEqual(vUTCOffset.from_ical('+0200'), timedelta(0, 7200)) self.assertEqual(vUTCOffset.from_ical('+023040'), timedelta(0, 9040)) self.assertEqual(vUTCOffset(vUTCOffset.from_ical('+0230')).to_ical(), '+0230') # And a few failures self.assertRaises(ValueError, vUTCOffset.from_ical, '+323k') self.assertRaises(ValueError, vUTCOffset.from_ical, '+2400') def test_prop_vInline(self): from ..prop import vInline self.assertEqual(vInline('Some text'), 'Some text') self.assertEqual(vInline.from_ical('Some text'), 'Some text') t2 = vInline('other text') t2.params['cn'] = 'Test Osterone' self.assertIsInstance(t2.params, Parameters) self.assertEqual(t2.params, {'CN': 'Test Osterone'}) def test_prop_TypesFactory(self): from ..prop import TypesFactory # To get a type you can use it like this. factory = TypesFactory() datetime_parser = factory['date-time'] self.assertEqual(datetime_parser(datetime(2001, 1, 1)).to_ical(), b'20010101T000000') # A typical use is when the parser tries to find a content type and use # text as the default value = '20050101T123000' value_type = 'date-time' self.assertEqual(factory.get(value_type, 'text').from_ical(value), datetime(2005, 1, 1, 12, 30)) # It can also be used to directly encode property and parameter values self.assertEqual( factory.to_ical('comment', 'by Rasmussen, Max M\xfcller'), b'by Rasmussen\\, Max M\xc3\xbcller' ) self.assertEqual(factory.to_ical('priority', 1), b'1') self.assertEqual(factory.to_ical('cn', 'Rasmussen, Max M\xfcller'), b'Rasmussen\\, Max M\xc3\xbcller') self.assertEqual( factory.from_ical('cn', b'Rasmussen\\, Max M\xc3\xb8ller'), 'Rasmussen, Max M\xf8ller' ) class TestPropertyValues(unittest.TestCase): def test_vDDDLists_timezone(self): """Test vDDDLists with timezone information. """ from .. import Event vevent = Event() at = pytz.timezone('Europe/Vienna') dt1 = at.localize(datetime(2013, 1, 1)) dt2 = at.localize(datetime(2013, 1, 2)) dt3 = at.localize(datetime(2013, 1, 3)) vevent.add('rdate', [dt1, dt2]) vevent.add('exdate', dt3) ical = vevent.to_ical() self.assertTrue( b'RDATE;TZID=Europe/Vienna:20130101T000000,20130102T000000' in ical ) self.assertTrue(b'EXDATE;TZID=Europe/Vienna:20130103T000000' in ical) class TestWindowsOlsonMapping(unittest.TestCase): """Test the mappings from windows to olson tzids""" def test_windows_timezone(self): """test that an example""" self.assertEqual( vDatetime.from_ical('20170507T181920', 'Eastern Standard Time'), pytz.timezone('America/New_York').localize(datetime(2017, 5, 7, 18, 19, 20)) ) def test_all(self): """test if all mappings actually map to valid pytz tzids""" for olson in WINDOWS_TO_OLSON.values(): pytz.timezone(olson)