def create_calendar(client, parent, name, id=None): """ Create a new calendar with display name `name` in `parent`. """ path = None if id is None: id = str(uuid.uuid1()) name = dav.DisplayName(name) cal = cdav.CalendarCollection() coll = dav.Collection() + cal type = dav.ResourceType() + coll prop = dav.Prop() + [type, name] set = dav.Set() + prop mkcol = dav.Mkcol() + set q = etree.tostring(mkcol.xmlelement(), encoding="utf-8", xml_declaration=True) path = url.join(parent.url.path, id) r = client.mkcol(path, q) if r.status == 201: path = url.make(parent.url, path) else: raise error.MkcolError(r.raw) return (id, path)
def children(self, type=None): """ List children, using a propfind (resourcetype) on the parent object, at depth = 1. """ c = [] depth = 1 properties = {} props = [dav.ResourceType(), dav.DisplayName()] response = self._query_properties(props, depth) properties = self._handle_prop_response( response=response, props=props, type=type, what='tag') for path in list(properties.keys()): resource_type = properties[path][dav.ResourceType.tag] resource_name = properties[path][dav.DisplayName.tag] if resource_type == type or type is None: # TODO: investigate the RFCs thoroughly - why does a "get # members of this collection"-request also return the # collection URL itself? # And why is the strip_trailing_slash-method needed? # The collection URL should always end with a slash according # to RFC 2518, section 5.2. if (self.url.strip_trailing_slash() != self.url.join(path).strip_trailing_slash()): c.append((self.url.join(path), resource_type, resource_name)) return c
def children(self, type=None): """ List children, using a propfind (resourcetype) on the parent object, at depth = 1. """ c = [] depth = 1 properties = {} props = [dav.ResourceType(), ] response = self._query_properties(props, depth) for r in response.tree.findall(dav.Response.tag): # We use canonicalized urls to index children href = str(self.url.join(URL.objectify(r.find(dav.Href.tag).text)).canonical()) assert(href) properties[href] = {} for p in props: t = r.find(".//" + p.tag) if len(list(t)) > 0: if type is not None: val = t.find(".//" + type) else: val = t.find(".//*") if val is not None: val = val.tag else: val = None else: val = t.text properties[href][p.tag] = val for path in properties.keys(): resource_type = properties[path][dav.ResourceType.tag] if resource_type == type or type is None: path = URL.objectify(path) ## TODO: investigate the RFCs thoroughly - why does a "get ## members of this collection"-request also return the collection URL itself? ## And why is the strip_trailing_slash-method needed? The collection URL ## should always end with a slash according to RFC 2518, section 5.2. if self.url.strip_trailing_slash() != path.strip_trailing_slash(): c.append((path, resource_type)) return c
def children(client, parent, type=None): """ List children, using a propfind (resourcetype) on the parent object, at depth = 1. TODO: There should be a better way. """ c = [] response = get_properties(client, parent, [ dav.ResourceType(), ], 1) for path in response.keys(): if path != parent.url.path: resource_type = response[path][dav.ResourceType.tag] if resource_type == type or type is None: c.append((url.make(parent.url, path), resource_type)) return c
def _create(self, name, id=None, supported_calendar_component_set=None): """ Create a new calendar with display name `name` in `parent`. """ if id is None: id = str(uuid.uuid1()) self.id = id path = self.parent.url.join(id) self.url = path ## TODO: mkcalendar seems to ignore the body on most servers? ## at least the name doesn't get set this way. ## zimbra gives 500 (!) if body is omitted ... cal = cdav.CalendarCollection() coll = dav.Collection() + cal type = dav.ResourceType() + coll prop = dav.Prop() + [ type, ] if name: display_name = dav.DisplayName(name) prop += [ display_name, ] if supported_calendar_component_set: sccs = cdav.SupportedCalendarComponentSet() for scc in supported_calendar_component_set: sccs += cdav.Comp(scc) prop += sccs set = dav.Set() + prop mkcol = cdav.Mkcalendar() + set r = self._query(root=mkcol, query_method='mkcalendar', url=path, expected_return_value=201) if name: try: self.set_properties([display_name]) except: self.delete() raise ## Special hack for Zimbra! The calendar we've made exists at ## the specified URL, and we can do operations like ls, even ## PUT an event to the calendar. Zimbra will enforce that the ## event uuid matches the event url, and return either 201 or ## 302 - but alas, try to do a GET towards the event and we ## get 404! But turn around and replace the calendar ID with ## the calendar name in the URL and hey ... it works! ## TODO: write test cases for calendars with non-trivial ## names and calendars with names already matching existing ## calendar urls and ensure they pass. zimbra_url = self.parent.url.join(name) try: ret = self.client.request(zimbra_url) if ret.status == 404: raise error.NotFoundError ## insane server self.url = zimbra_url except error.NotFoundError: ## sane server pass
def _create(self, name, id=None): """ Create a new calendar with display name `name` in `parent`. """ if id is None: id = str(uuid.uuid1()) self.id = id path = self.parent.url.join(id) self.url = path ## TODO: mkcalendar seems to ignore the body on most servers? ## at least the name doesn't get set this way. ## zimbra gives 500 (!) if body is omitted ... if name: display_name = dav.DisplayName(name) cal = cdav.CalendarCollection() coll = dav.Collection() + cal type = dav.ResourceType() + coll prop = dav.Prop() + [type,] if name: prop += [display_name,] set = dav.Set() + prop mkcol = cdav.Mkcalendar() + set q = etree.tostring(mkcol.xmlelement(), encoding="utf-8", xml_declaration=True) r = self.client.mkcalendar(path, q) if r.status != 201: raise error.MkcalendarError(r.raw) if name: try: self.set_properties([display_name]) except: self.delete() raise ## Special hack for Zimbra! The calendar we've made exists at ## the specified URL, and we can do operations like ls, even ## PUT an event to the calendar. Zimbra will enforce that the ## event uuid matches the event url, and return either 201 or ## 302 - but alas, try to do a GET towards the event and we ## get 404! But turn around and replace the calendar ID with ## the calendar name in the URL and hey ... it works! ## TODO: write test cases for calendars with non-trivial ## names and calendars with names already matching existing ## calendar urls and ensure they pass. zimbra_url = self.parent.url.join(name) try: ret = self.client.request(zimbra_url) if ret.status == 404: raise error.NotFoundError ## insane server self.url = zimbra_url except error.NotFoundError: ## sane server pass
def test_xml_parsing(self): """ DAVResponse has quite some code to parse the XML received from the server. This test contains real XML received from various caldav servers, and the expected result from the parse methods. """ xml = """ <multistatus xmlns="DAV:"> <response xmlns="DAV:"> <href>/</href> <propstat> <prop> <current-user-principal xmlns="DAV:"> <href xmlns="DAV:">/17149682/principal/</href> </current-user-principal> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> </multistatus> """ expected_result = { '/': { '{DAV:}current-user-principal': '/17149682/principal/' } } assert_equal( MockedDAVResponse(xml).expand_simple_props( props=[dav.CurrentUserPrincipal()]), expected_result) xml = """ <multistatus xmlns="DAV:"> <response xmlns="DAV:"> <href>/17149682/principal/</href> <propstat> <prop> <calendar-home-set xmlns="urn:ietf:params:xml:ns:caldav"> <href xmlns="DAV:">https://p62-caldav.icloud.com:443/17149682/calendars/</href> </calendar-home-set> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> </multistatus>""" expected_result = { '/17149682/principal/': { '{urn:ietf:params:xml:ns:caldav}calendar-home-set': 'https://p62-caldav.icloud.com:443/17149682/calendars/' } } assert_equal( MockedDAVResponse(xml).expand_simple_props( props=[cdav.CalendarHomeSet()]), expected_result) xml = """ <multistatus xmlns="DAV:"> <response xmlns="DAV:"> <href>/</href> <propstat> <prop> <current-user-principal xmlns="DAV:"> <href xmlns="DAV:">/17149682/principal/</href> </current-user-principal> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> </multistatus>""" expected_result = { '/': { '{DAV:}current-user-principal': '/17149682/principal/' } } assert_equal( MockedDAVResponse(xml).expand_simple_props( props=[dav.CurrentUserPrincipal()]), expected_result) xml = """ <multistatus xmlns="DAV:"> <response> <href>/17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/</href> <propstat> <prop> </prop> <status>HTTP/1.1 200 OK</status> </propstat> <propstat> <prop> <calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/> </prop> <status>HTTP/1.1 404 Not Found</status> </propstat> </response> <response> <href>/17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/20010712T182145Z-123401%40example.com.ics</href> <propstat> <prop> <calendar-data xmlns="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN BEGIN:VEVENT UID:[email protected] DTSTAMP:20060712T182145Z DTSTART:20060714T170000Z DTEND:20060715T040000Z SUMMARY:Bastille Day Party END:VEVENT END:VCALENDAR </calendar-data> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> </multistatus> """ expected_result = { '/17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/': { '{urn:ietf:params:xml:ns:caldav}calendar-data': None }, '/17149682/calendars/testcalendar-84439d0b-ce46-4416-b978-7b4009122c64/[email protected]': { '{urn:ietf:params:xml:ns:caldav}calendar-data': 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Example Corp.//CalDAV Client//EN\nBEGIN:VEVENT\nUID:[email protected]\nDTSTAMP:20060712T182145Z\nDTSTART:20060714T170000Z\nDTEND:20060715T040000Z\nSUMMARY:Bastille Day Party\nEND:VEVENT\nEND:VCALENDAR\n' } } assert_equal( MockedDAVResponse(xml).expand_simple_props( props=[cdav.CalendarData()]), expected_result) xml = """ <multistatus xmlns="DAV:"> <response xmlns="DAV:"> <href>/17149682/calendars/</href> <propstat> <prop> <resourcetype xmlns="DAV:"> <collection/> </resourcetype> <displayname xmlns="DAV:">Ny Test</displayname> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> <response xmlns="DAV:"> <href>/17149682/calendars/06888b87-397f-11eb-943b-3af9d3928d42/</href> <propstat> <prop> <resourcetype xmlns="DAV:"> <collection/> <calendar xmlns="urn:ietf:params:xml:ns:caldav"/> </resourcetype> <displayname xmlns="DAV:">calfoo3</displayname> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> <response xmlns="DAV:"> <href>/17149682/calendars/inbox/</href> <propstat> <prop> <resourcetype xmlns="DAV:"> <collection/> <schedule-inbox xmlns="urn:ietf:params:xml:ns:caldav"/> </resourcetype> </prop> <status>HTTP/1.1 200 OK</status> </propstat> <propstat> <prop> <displayname xmlns="DAV:"/> </prop> <status>HTTP/1.1 404 Not Found</status> </propstat> </response> <response xmlns="DAV:"> <href>/17149682/calendars/testcalendar-e2910e0a-feab-4b51-b3a8-55828acaa912/</href> <propstat> <prop> <resourcetype xmlns="DAV:"> <collection/> <calendar xmlns="urn:ietf:params:xml:ns:caldav"/> </resourcetype> <displayname xmlns="DAV:">Yep</displayname> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> </multistatus> """ expected_result = { '/17149682/calendars/': { '{DAV:}resourcetype': ['{DAV:}collection'], '{DAV:}displayname': 'Ny Test' }, '/17149682/calendars/06888b87-397f-11eb-943b-3af9d3928d42/': { '{DAV:}resourcetype': [ '{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar' ], '{DAV:}displayname': 'calfoo3' }, '/17149682/calendars/inbox/': { '{DAV:}resourcetype': [ '{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}schedule-inbox' ], '{DAV:}displayname': None }, '/17149682/calendars/testcalendar-e2910e0a-feab-4b51-b3a8-55828acaa912/': { '{DAV:}resourcetype': [ '{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar' ], '{DAV:}displayname': 'Yep' } } assert_equal( MockedDAVResponse(xml).expand_simple_props( props=[dav.DisplayName()], multi_value_props=[dav.ResourceType()]), expected_result) xml = """ <multistatus xmlns="DAV:"> <response xmlns="DAV:"> <href>/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/</href> <propstat> <prop> <getetag xmlns="DAV:">"kkkgopik"</getetag> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> <response xmlns="DAV:"> <href>/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/1761bf8c-6363-11eb-8fe4-74e5f9bfd8c1.ics</href> <propstat> <prop> <getetag xmlns="DAV:">"kkkgorwx"</getetag> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> <response xmlns="DAV:"> <href>/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/20010712T182145Z-123401%40example.com.ics</href> <propstat> <prop> <getetag xmlns="DAV:">"kkkgoqqu"</getetag> </prop> <status>HTTP/1.1 200 OK</status> </propstat> </response> <sync-token>HwoQEgwAAAh4yw8ntwAAAAAYAhgAIhUIopml463FieB4EKq9+NSn04DrkQEoAA==</sync-token> </multistatus> """ expected_results = { '/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/': { '{DAV:}getetag': '"kkkgopik"', '{urn:ietf:params:xml:ns:caldav}calendar-data': None }, '/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/1761bf8c-6363-11eb-8fe4-74e5f9bfd8c1.ics': { '{DAV:}getetag': '"kkkgorwx"', '{urn:ietf:params:xml:ns:caldav}calendar-data': None }, '/17149682/calendars/testcalendar-f96b3bf0-09e1-4f3d-b891-3a25c99a2894/[email protected]': { '{DAV:}getetag': '"kkkgoqqu"', '{urn:ietf:params:xml:ns:caldav}calendar-data': None } }