import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape
from .error import XmlError
from typing import Optional, Iterator, Generator, Tuple
class DavParser:
"""DAV XML request content and response payload parsing."""
@classmethod
def _iterparse(cls, content: Generator[bytes, None, None]) -> Iterator[ET.Element]:
parser: ET.XMLPullParser = ET.XMLPullParser(("end",))
try:
for chunk in content:
try:
parser.feed(chunk)
for event, elem in parser.read_events():
if event == "end":
yield elem
except Exception as e:
content.close()
raise XmlError(f"Cannot parse XML response: {str(e)}") from None
finally:
parser.close()
@classmethod
def _parse_propstat(cls, elements: Iterator[ET.Element], code: int = 200) -> Iterator[Tuple[str, ET.Element]]:
for response in elements:
if response.tag == "{DAV:}response":
href: Optional[ET.Element] = response.find("{DAV:}href")
if href is None or not href.text:
continue
for propstat in response.findall("{DAV:}propstat"):
status: Optional[ET.Element] = propstat.find("{DAV:}status")
if status is None or status.text is None or f" {code} " not in status.text:
continue
yield href.text, propstat
@classmethod
def parse_propstat_for_status(cls, content: Generator[bytes, None, None]) -> Iterator[str]:
"""Response hrefs with success statuscode."""
for href, _ in cls._parse_propstat(cls._iterparse(content), code=200):
yield href
@classmethod
def parse_list_for_calendars(cls, content: Generator[bytes, None, None]) -> Iterator[Tuple[str, Optional[str]]]:
"""Calendar/list hrefs and displaynames."""
for href, propstat in cls._parse_propstat(cls._iterparse(content)):
if propstat.find("./{DAV:}prop/{DAV:}resourcetype/{urn:ietf:params:xml:ns:caldav}calendar") is None:
continue
name: Optional[ET.Element] = propstat.find("./{DAV:}prop/{DAV:}displayname")
yield href, name.text if name is not None and name.text else None
@classmethod
def parse_get_for_calendar(cls, content: Generator[bytes, None, None]) -> Iterator[Tuple[str, Optional[str], str]]:
"""Task hrefs, etags, and data."""
for href, propstat in cls._parse_propstat(cls._iterparse(content)):
getetag: Optional[ET.Element] = propstat.find("./{DAV:}prop/{DAV:}getetag")
getcontenttype: Optional[ET.Element] = propstat.find("./{DAV:}prop/{DAV:}getcontenttype")
if getcontenttype is None or getcontenttype.text != "text/calendar; charset=utf-8; component=vtodo":
continue
calendar: Optional[ET.Element] = propstat.find("./{DAV:}prop/{urn:ietf:params:xml:ns:caldav}calendar-data")
if calendar is None or not calendar.text:
continue
yield href, getetag.text if getetag is not None and getetag.text else None, calendar.text
@classmethod
def get_propfind_calendars(cls) -> bytes:
"""Request calendar/list properties."""
# language=XML
return b"""<x0:propfind xmlns:x0="DAV:">
<x0:prop>
<x0:resourcetype/><x0:displayname/>
</x0:prop>
</x0:propfind>"""
@classmethod
def get_propfind_calendar(cls) -> bytes:
"""Request task properties."""
# language=XML
return b"""<x0:propfind xmlns:x0="DAV:" xmlns:x1="urn:ietf:params:xml:ns:caldav">
<x0:prop>
<x0:getcontenttype/><x0:getetag/><x1:calendar-data/>
</x0:prop>
</x0:propfind>"""
@classmethod
def get_report_calendar(cls, completed_filter: Optional[bool]) -> bytes:
"""Request tasks of a calendar/list."""
if completed_filter is None:
task_filter: bytes = b''
elif completed_filter:
task_filter = b'<x1:prop-filter name="completed"><x1:is-defined/></x1:prop-filter>'
else:
task_filter = b'<x1:prop-filter name="completed"><x1:is-not-defined/></x1:prop-filter>'
return b"""<x1:calendar-query xmlns:x0="DAV:" xmlns:x1="urn:ietf:params:xml:ns:caldav">
<x0:prop>
<x0:getcontenttype/><x0:getetag/><x1:calendar-data/>
</x0:prop>
<x1:filter>
<x1:comp-filter name="VCALENDAR">
<x1:comp-filter name="VTODO">
%b
</x1:comp-filter>
</x1:comp-filter>
</x1:filter>
</x1:calendar-query>""" % task_filter
@classmethod
def get_mkcol_calendar(cls, name: str) -> bytes:
"""Request creating a calendar/list."""
return b"""<x0:mkcol xmlns:x0="DAV:" xmlns:x1="urn:ietf:params:xml:ns:caldav">
<x0:set><x0:prop>
<x0:displayname>%b</x0:displayname>
<x0:resourcetype>
<x0:collection/><x1:calendar/>
</x0:resourcetype>
<x1:supported-calendar-component-set>
<x1:comp name="VTODO"/>
</x1:supported-calendar-component-set>
</x0:prop></x0:set>
</x0:mkcol>""" % escape(name).encode("utf-8", errors="strict")
@classmethod
def get_property_update(cls, name: str) -> bytes:
"""Request a calendar/list update."""
return b"""<x0:propertyupdate xmlns:x0="DAV:">
<x0:set><x0:prop>
<x0:displayname>%b</x0:displayname>
</x0:prop></x0:set>
</x0:propertyupdate>""" % escape(name).encode("utf-8", errors="strict")