diff options
author | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-23 08:23:49 -0400 |
---|---|---|
committer | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-23 08:23:49 -0400 |
commit | 524849338fcb0cb40bcdb18f4dbe7e9660074f20 (patch) | |
tree | aabf60b524121b08a32680d0e917c92e8fec8276 | |
parent | b3e6648328dd00dd53e60633b23a40ed6ff578e5 (diff) | |
download | pykolab-524849338fcb0cb40bcdb18f4dbe7e9660074f20.tar.gz |
Add methods to dump Kolab XML objects as dict()
-rw-r--r-- | pykolab/itip/__init__.py | 2 | ||||
-rw-r--r-- | pykolab/xml/__init__.py | 2 | ||||
-rw-r--r-- | pykolab/xml/attendee.py | 50 | ||||
-rw-r--r-- | pykolab/xml/contact.py | 4 | ||||
-rw-r--r-- | pykolab/xml/contact_reference.py | 21 | ||||
-rw-r--r-- | pykolab/xml/event.py | 72 | ||||
-rw-r--r-- | pykolab/xml/recurrence_rule.py | 117 | ||||
-rw-r--r-- | tests/unit/test-002-attendee.py | 20 | ||||
-rw-r--r-- | tests/unit/test-003-event.py | 244 |
9 files changed, 446 insertions, 86 deletions
diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py index 17da24e..816ee1d 100644 --- a/pykolab/itip/__init__.py +++ b/pykolab/itip/__init__.py @@ -144,6 +144,8 @@ def check_event_conflict(kolab_event, itip_event): if kolab_event.uid == itip_event['uid']: return conflict + # TODO: don't consider conflict if event has TRANSP:TRANSPARENT + _es = to_dt(kolab_event.get_start()) _ee = to_dt(kolab_event.get_ical_dtend()) # use iCal style end date: next day for all-day events diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py index 2bb42d4..3e12716 100644 --- a/pykolab/xml/__init__.py +++ b/pykolab/xml/__init__.py @@ -4,6 +4,7 @@ from attendee import participant_status_label from contact import Contact from contact_reference import ContactReference +from recurrence_rule import RecurrenceRule from event import Event from event import EventIntegrityError @@ -19,6 +20,7 @@ __all__ = [ "Contact", "ContactReference", "Event", + "RecurrenceRule", "event_from_ical", "event_from_string", "to_dt", diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py index 5d469c2..7921280 100644 --- a/pykolab/xml/attendee.py +++ b/pykolab/xml/attendee.py @@ -56,6 +56,13 @@ class Attendee(kolabformat.Attendee): "FALSE": False, } + properties_map = { + 'role': 'get_role', + 'rsvp': 'rsvp', + 'partstat': 'get_participant_status', + 'cutype': 'get_cutype', + } + def __init__( self, email, @@ -97,6 +104,12 @@ class Attendee(kolabformat.Attendee): if not participant_status == None: self.set_participant_status(participant_status) + def copy_from(self, obj): + if isinstance(obj, kolabformat.Attendee): + kolabformat.Attendee.__init__(self, obj) + self.contactreference = ContactReference(obj.contact()) + self.email = self.contactreference.get_email() + def delegate_from(self, delegators): crefs = [] @@ -138,8 +151,11 @@ class Attendee(kolabformat.Attendee): self.setDelegatedTo(list(set(crefs))) - def get_cutype(self): - return self.cutype() + def get_cutype(self, translated=False): + cutype = self.cutype() + if translated: + return self._translate_value(cutype, self.cutype_map) + return cutype def get_delegated_from(self): return self.delegatedFrom() @@ -161,16 +177,22 @@ class Attendee(kolabformat.Attendee): def get_participant_status(self, translated=False): partstat = self.partStat() if translated: - partstat_name_map = dict([(v, k) for (k, v) in self.participant_status_map.iteritems()]) - return partstat_name_map[partstat] if partstat_name_map.has_key(partstat) else 'UNKNOWN' + return self._translate_value(partstat, self.participant_status_map) return partstat - def get_role(self): - return self.role() + def get_role(self, translated=False): + role = self.role() + if translated: + return self._translate_value(role, self.role_map) + return role def get_rsvp(self): return self.rsvp() + def _translate_value(self, val, map): + name_map = dict([(v, k) for (k, v) in map.iteritems()]) + return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + def set_cutype(self, cutype): if cutype in self.cutype_map.keys(): self.setCutype(self.cutype_map[cutype]) @@ -202,6 +224,22 @@ class Attendee(kolabformat.Attendee): def set_rsvp(self, rsvp): self.setRSVP(rsvp) + def to_dict(self): + data = self.contactreference.to_dict() + data.pop('type', None) + + for p, getter in self.properties_map.iteritems(): + val = None + args = {} + if hasattr(self, getter): + if getter.startswith('get_'): + args = dict(translated=True) + val = getattr(self, getter)(**args) + if val is not None: + data[p] = val + + return data + def __str__(self): return self.email diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py index 1577b58..9a2c103 100644 --- a/pykolab/xml/contact.py +++ b/pykolab/xml/contact.py @@ -39,5 +39,9 @@ class Contact(kolabformat.Contact): def set_name(self, name): self.setName(name) + def to_ditc(self): + # TODO: implement this + return dict(name=self.name()) + def __str__(self): return kolabformat.writeContact(self) diff --git a/pykolab/xml/contact_reference.py b/pykolab/xml/contact_reference.py index 0d6dec5..5a832da 100644 --- a/pykolab/xml/contact_reference.py +++ b/pykolab/xml/contact_reference.py @@ -11,9 +11,18 @@ import kolabformat """ class ContactReference(kolabformat.ContactReference): + properties_map = { + 'email': 'email', + 'name': 'name', + 'type': 'type', + 'uid': 'uid', + } + def __init__(self, email=None): if email == None: kolabformat.ContactReference.__init__(self) + elif isinstance(email, kolabformat.ContactReference): + kolabformat.ContactReference.__init__(self, email.email(), email.name(), email.uid()) else: kolabformat.ContactReference.__init__(self, email) @@ -31,3 +40,15 @@ class ContactReference(kolabformat.ContactReference): def set_name(self, name): self.setName(name) + + def to_dict(self): + data = dict() + + for p, getter in self.properties_map.iteritems(): + val = None + if hasattr(self, getter): + val = getattr(self, getter)() + if val is not None: + data[p] = val + + return data diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py index 4ac4997..8e41a92 100644 --- a/pykolab/xml/event.py +++ b/pykolab/xml/event.py @@ -18,6 +18,7 @@ from pykolab.translate import _ from os import path from attendee import Attendee from contact_reference import ContactReference +from recurrence_rule import RecurrenceRule log = pykolab.getLogger('pykolab.xml_event') @@ -55,6 +56,37 @@ class Event(object): "CONFIDENTIAL": kolabformat.ClassConfidential, } + properties_map = { + # property: getter + "uid": "get_uid", + "created": "get_created", + "lastmodified-date": "get_lastmodified", + "sequence": "sequence", + "classification": "get_classification", + "categories": "categories", + "start": "get_start", + "end": "get_end", + "duration": "get_duration", + "transparency": "transparency", + "rrule": "recurrenceRule", + "rdate": "recurrenceDates", + "exdate": "exceptionDates", + "recurrence-id": "recurrenceID", + "summary": "summary", + "description": "description", + "priority": "priority", + "status": "get_status", + "location": "location", + "organizer": "organizer", + "attendee": "get_attendees", + "attach": "attachments", + "url": "url", + "alarm": "alarms", + "x-custom": "customProperties", + # TODO: add to_dict() support for these + # "exception": "exceptions", + } + def __init__(self, from_ical="", from_string=""): self._attendees = [] self._categories = [] @@ -271,7 +303,7 @@ class Event(object): def get_created(self): try: - return xmlutils.from_cdatetime(self.event.created(), False) + return xmlutils.from_cdatetime(self.event.created(), True) except ValueError: return datetime.datetime.now() @@ -479,7 +511,7 @@ class Event(object): except: self.__str__() - return xmlutils.from_cdatetime(self.event.lastModified(), False) + return xmlutils.from_cdatetime(self.event.lastModified(), True) def get_organizer(self): organizer = self.event.organizer() @@ -780,6 +812,42 @@ class Event(object): else: raise EventIntegrityError, kolabformat.errorMessage() + def to_dict(self): + data = dict() + + for p, getter in self.properties_map.iteritems(): + val = None + if hasattr(self, getter): + val = getattr(self, getter)() + elif hasattr(self.event, getter): + val = getattr(self.event, getter)() + + if isinstance(val, kolabformat.cDateTime): + val = xmlutils.from_cdatetime(val, True) + elif isinstance(val, kolabformat.vectordatetime): + val = [xmlutils.from_cdatetime(x, True) for x in val] + elif isinstance(val, kolabformat.vectors): + val = [str(x) for x in val] + elif isinstance(val, kolabformat.vectorcs): + for x in val: + data[x.identifier] = x.value + val = None + elif isinstance(val, kolabformat.ContactReference): + val = ContactReference(val).to_dict() + elif isinstance(val, kolabformat.RecurrenceRule): + val = RecurrenceRule(val).to_dict() + elif isinstance(val, kolabformat.vectorattachment): + val = [dict(fmttype=x.mimetype(), label=x.label(), uri=x.uri()) for x in val] + elif isinstance(val, kolabformat.vectoralarm): + val = [dict(type=x.type()) for x in val] + elif isinstance(val, list): + val = [x.to_dict() for x in val if hasattr(x, 'to_dict')] + + if val is not None: + data[p] = val + + return data + def to_message(self): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase diff --git a/pykolab/xml/recurrence_rule.py b/pykolab/xml/recurrence_rule.py new file mode 100644 index 0000000..eb17fd5 --- /dev/null +++ b/pykolab/xml/recurrence_rule.py @@ -0,0 +1,117 @@ +import kolabformat +from pykolab.xml import utils as xmlutils + +""" + def setFrequency(self, *args): return _kolabformat.RecurrenceRule_setFrequency(self, *args) + def frequency(self): return _kolabformat.RecurrenceRule_frequency(self) + def setWeekStart(self, *args): return _kolabformat.RecurrenceRule_setWeekStart(self, *args) + def weekStart(self): return _kolabformat.RecurrenceRule_weekStart(self) + def setEnd(self, *args): return _kolabformat.RecurrenceRule_setEnd(self, *args) + def end(self): return _kolabformat.RecurrenceRule_end(self) + def setCount(self, *args): return _kolabformat.RecurrenceRule_setCount(self, *args) + def count(self): return _kolabformat.RecurrenceRule_count(self) + def setInterval(self, *args): return _kolabformat.RecurrenceRule_setInterval(self, *args) + def interval(self): return _kolabformat.RecurrenceRule_interval(self) + def setBysecond(self, *args): return _kolabformat.RecurrenceRule_setBysecond(self, *args) + def bysecond(self): return _kolabformat.RecurrenceRule_bysecond(self) + def setByminute(self, *args): return _kolabformat.RecurrenceRule_setByminute(self, *args) + def byminute(self): return _kolabformat.RecurrenceRule_byminute(self) + def setByhour(self, *args): return _kolabformat.RecurrenceRule_setByhour(self, *args) + def byhour(self): return _kolabformat.RecurrenceRule_byhour(self) + def setByday(self, *args): return _kolabformat.RecurrenceRule_setByday(self, *args) + def byday(self): return _kolabformat.RecurrenceRule_byday(self) + def setBymonthday(self, *args): return _kolabformat.RecurrenceRule_setBymonthday(self, *args) + def bymonthday(self): return _kolabformat.RecurrenceRule_bymonthday(self) + def setByyearday(self, *args): return _kolabformat.RecurrenceRule_setByyearday(self, *args) + def byyearday(self): return _kolabformat.RecurrenceRule_byyearday(self) + def setByweekno(self, *args): return _kolabformat.RecurrenceRule_setByweekno(self, *args) + def byweekno(self): return _kolabformat.RecurrenceRule_byweekno(self) + def setBymonth(self, *args): return _kolabformat.RecurrenceRule_setBymonth(self, *args) + def bymonth(self): return _kolabformat.RecurrenceRule_bymonth(self) + def isValid(self): return _kolabformat.RecurrenceRule_isValid(self) +""" + +class RecurrenceRule(kolabformat.RecurrenceRule): + frequency_map = { + None: kolabformat.RecurrenceRule.FreqNone, + "YEARLY": kolabformat.RecurrenceRule.Yearly, + "MONTHLY": kolabformat.RecurrenceRule.Monthly, + "WEEKLY": kolabformat.RecurrenceRule.Weekly, + "DAILY": kolabformat.RecurrenceRule.Daily, + "HOURLY": kolabformat.RecurrenceRule.Hourly, + "MINUTELY": kolabformat.RecurrenceRule.Minutely, + "SECONDLY": kolabformat.RecurrenceRule.Secondly + } + + weekday_map = { + "MO": kolabformat.Monday, + "TU": kolabformat.Tuesday, + "WE": kolabformat.Wednesday, + "TH": kolabformat.Thursday, + "FR": kolabformat.Friday, + "SA": kolabformat.Saturday, + "SU": kolabformat.Sunday + } + + properties_map = { + 'frequency': 'get_frequency', + 'interval': 'interval', + 'count': 'count', + 'until': 'end', + 'bymonth': 'bymonth', + 'byday': 'byday', + 'byyearday': 'byyearday', + 'byweekno': 'byweekno', + 'byhour': 'byhour', + 'byminute': 'byminute', + 'wkst': 'get_weekstart' + } + + def __init__(self, rrule=None): + if rrule == None: + kolabformat.RecurrenceRule.__init__(self) + else: + kolabformat.RecurrenceRule.__init__(self, rrule) + + def get_frequency(self, translated=False): + freq = self.frequency() + if translated: + return self._translate_value(freq, self.frequency_map) + return freq + + def get_weekstart(self, translated=False): + wkst = self.weekStart() + if translated: + return self._translate_value(wkst, self.weekday_map) + return wkst + + def _translate_value(self, val, map): + name_map = dict([(v, k) for (k, v) in map.iteritems()]) + return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + + def to_dict(self): + if not self.isValid() or self.frequency() == kolabformat.RecurrenceRule.FreqNone: + return None + + data = dict() + + for p, getter in self.properties_map.iteritems(): + val = None + args = {} + if hasattr(self, getter): + if getter.startswith('get_'): + args = dict(translated=True) + if hasattr(self, getter): + val = getattr(self, getter)(**args) + if isinstance(val, kolabformat.cDateTime): + val = xmlutils.from_cdatetime(val, True) + elif isinstance(val, kolabformat.vectori): + val = [int(v) for x in val] + elif isinstance(val, kolabformat.vectordaypos): + val = ["%d%s" % (x.occurence, self._translate_value(x.weekday)) for x in val] + if val is not None: + data[p] = val + + return data + + diff --git a/tests/unit/test-002-attendee.py b/tests/unit/test-002-attendee.py index 8bcee3c..d7584e3 100644 --- a/tests/unit/test-002-attendee.py +++ b/tests/unit/test-002-attendee.py @@ -108,5 +108,25 @@ class TestEventXML(unittest.TestCase): self.assertEqual(participant_status_label(kolabformat.PartTentative), "Tentatively Accepted") self.assertEqual(participant_status_label('UNKNOWN'), "UNKNOWN") + def test_020_to_dict(self): + name = "Doe, Jane" + role = 'OPT-PARTICIPANT' + cutype = 'RESOURCE' + partstat = 'ACCEPTED' + self.attendee.set_name(name) + self.attendee.set_rsvp(True) + self.attendee.set_role(role) + self.attendee.set_cutype(cutype) + self.attendee.set_participant_status(partstat) + + data = self.attendee.to_dict() + self.assertIsInstance(data, dict) + self.assertEqual(data['role'], role) + self.assertEqual(data['cutype'], cutype) + self.assertEqual(data['partstat'], partstat) + self.assertEqual(data['name'], name) + self.assertEqual(data['email'], 'jane@doe.org') + self.assertTrue(data['rsvp']) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py index 2c5a478..5017091 100644 --- a/tests/unit/test-003-event.py +++ b/tests/unit/test-003-event.py @@ -65,6 +65,136 @@ END:VALARM END:VEVENT """ +xml_event = """ +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <prodid> + <text>Libkolabxml-1.1</text> + </prodid> + <version> + <text>2.0</text> + </version> + <x-kolab-version> + <text>3.1.0</text> + </x-kolab-version> + </properties> + <components> + <vevent> + <properties> + <uid> + <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text> + </uid> + <created> + <date-time>2014-07-07T01:28:23Z</date-time> + </created> + <dtstamp> + <date-time>2014-07-07T01:28:23Z</date-time> + </dtstamp> + <sequence> + <integer>1</integer> + </sequence> + <class> + <text>PUBLIC</text> + </class> + <dtstart> + <parameters> + <tzid> + <text>/kolab.org/Europe/London</text> + </tzid> + </parameters> + <date-time>2014-08-13T10:00:00</date-time> + </dtstart> + <dtend> + <parameters> + <tzid><text>/kolab.org/Europe/London</text></tzid> + </parameters> + <date-time>2014-08-13T14:00:00</date-time> + </dtend> + <rrule> + <recur> + <freq>DAILY</freq> + <until> + <date>2014-07-25</date> + </until> + </recur> + </rrule> + <exdate> + <parameters> + <tzid> + <text>/kolab.org/Europe/Berlin</text> + </tzid> + </parameters> + <date>2014-07-19</date> + <date>2014-07-26</date> + <date>2014-07-12</date> + <date>2014-07-13</date> + <date>2014-07-20</date> + <date>2014-07-27</date> + <date>2014-07-05</date> + <date>2014-07-06</date> + </exdate> + <summary> + <text>test</text> + </summary> + <description> + <text>test</text> + </description> + <priority> + <integer>5</integer> + </priority> + <status> + <text>CANCELLED</text> + </status> + <location> + <text>Room 101</text> + </location> + <organizer> + <parameters> + <cn><text>Doe, John</text></cn> + </parameters> + <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address> + </organizer> + <attendee> + <parameters> + <partstat><text>ACCEPTED</text></partstat> + <role><text>REQ-PARTICIPANT</text></role> + <rsvp><boolean>true</boolean></rsvp> + </parameters> + <cal-address>mailto:%3Cjane%40example.org%3E</cal-address> + </attendee> + <attendee> + <parameters> + <partstat><text>TENTATIVE</text></partstat> + <role><text>OPT-PARTICIPANT</text></role> + </parameters> + <cal-address>mailto:%3Csomebody%40else.com%3E</cal-address> + </attendee> + <attach> + <parameters> + <fmttype> + <text>text/html</text> + </fmttype> + <x-label> + <text>noname.1395223627.5555</text> + </x-label> + </parameters> + <uri>cid:noname.1395223627.5555</uri> + </attach> + <x-custom> + <identifier>X-MOZ-RECEIVED-DTSTAMP</identifier> + <value>20140224T155612Z</value> + </x-custom> + <x-custom> + <identifier>X-GWSHOW-AS</identifier> + <value>BUSY</value> + </x-custom> + </properties> + </vevent> + </components> + </vcalendar> +</icalendar> +""" class TestEventXML(unittest.TestCase): event = Event() @@ -181,7 +311,7 @@ METHOD:REQUEST event = event_from_ical(ical.walk('VEVENT')[0].to_ical()) self.assertEqual(event.get_location(), "Location") - self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11") + self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11+00:00") self.assertEqual(event.get_description(), "Description\n2 lines") self.assertEqual(event.get_url(), "http://somelink.com/foo") self.assertEqual(event.get_transparency(), False) @@ -310,83 +440,7 @@ END:VEVENT self.assertEqual(self.event.get_last_occurrence(), None) def test_022_load_from_xml(self): - xml = """ -<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> - <vcalendar> - <properties> - <prodid> - <text>Libkolabxml-1.1</text> - </prodid> - <version> - <text>2.0</text> - </version> - <x-kolab-version> - <text>3.1.0</text> - </x-kolab-version> - </properties> - <components> - <vevent> - <properties> - <uid> - <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text> - </uid> - <created> - <date-time>2014-07-07T01:28:23Z</date-time> - </created> - <dtstamp> - <date-time>2014-07-07T01:28:23Z</date-time> - </dtstamp> - <sequence> - <integer>1</integer> - </sequence> - <class> - <text>PUBLIC</text> - </class> - <dtstart> - <parameters> - <tzid> - <text>/kolab.org/Europe/London</text> - </tzid> - </parameters> - <date-time>2014-08-13T10:00:00</date-time> - </dtstart> - <dtend> - <parameters> - <tzid><text>/kolab.org/Europe/London</text></tzid> - </parameters> - <date-time>2014-08-13T14:00:00</date-time> - </dtend> - <summary> - <text>test</text> - </summary> - <organizer> - <parameters> - <cn><text>Doe, John</text></cn> - </parameters> - <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address> - </organizer> - <attendee> - <parameters> - <partstat><text>ACCEPTED</text></partstat> - <role><text>REQ-PARTICIPANT</text></role> - <rsvp><boolean>true</boolean></rsvp> - </parameters> - <cal-address>mailto:%3Cjane%40example.org%3E</cal-address> - </attendee> - <attendee> - <parameters> - <partstat><text>TENTATIVE</text></partstat> - <role><text>OPT-PARTICIPANT</text></role> - </parameters> - <cal-address>mailto:%3Csomebody%40else.com%3E</cal-address> - </attendee> - </properties> - </vevent> - </components> - </vcalendar> -</icalendar> -""" - event = event_from_string(xml) + event = event_from_string(xml_event) self.assertEqual(event.uid, '75c740bb-b3c6-442c-8021-ecbaeb0a025e') self.assertEqual(event.get_attendee_by_email("jane@example.org").get_participant_status(), kolabformat.PartAccepted) self.assertEqual(event.get_sequence(), 1) @@ -432,6 +486,40 @@ END:VEVENT event = event_from_ical(vevent) self.assertRaises(EventIntegrityError, event.to_message) + def test_025_to_dict(self): + data = event_from_string(xml_event).to_dict() + + self.assertIsInstance(data, dict) + self.assertIsInstance(data['start'], datetime.datetime) + self.assertIsInstance(data['end'], datetime.datetime) + self.assertIsInstance(data['created'], datetime.datetime) + self.assertIsInstance(data['lastmodified-date'], datetime.datetime) + self.assertEqual(data['uid'], '75c740bb-b3c6-442c-8021-ecbaeb0a025e') + self.assertEqual(data['summary'], 'test') + self.assertEqual(data['location'], 'Room 101') + self.assertEqual(data['description'], 'test') + self.assertEqual(data['priority'], 5) + self.assertEqual(data['status'], 'CANCELLED') + self.assertEqual(data['sequence'], 1) + self.assertEqual(data['transparency'], False) + self.assertEqual(data['X-GWSHOW-AS'], 'BUSY') + + self.assertIsInstance(data['organizer'], dict) + self.assertEqual(data['organizer']['email'], 'john@example.org') + + self.assertEqual(len(data['attendee']), 2) + self.assertIsInstance(data['attendee'][0], dict) + + self.assertEqual(len(data['attach']), 1) + self.assertIsInstance(data['attach'][0], dict) + self.assertEqual(data['attach'][0]['fmttype'], 'text/html') + + self.assertIsInstance(data['rrule'], dict) + self.assertEqual(data['rrule']['frequency'], 'DAILY') + self.assertEqual(data['rrule']['interval'], 1) + self.assertEqual(data['rrule']['wkst'], 'MO') + self.assertIsInstance(data['rrule']['until'], datetime.date) + if __name__ == '__main__': unittest.main() |