opyml/opyml/opml.py

93 lines
2.8 KiB
Python

import json
from defusedxml import ElementTree
from typing import Optional
from xml.etree import ElementTree as XmlBuilder
from .body import Body
from .head import Head
from .util import _dict_exclude_none
class OPML:
"""The root OPML element."""
def __init__(
self,
version: Optional[str] = None,
head: Optional[Head] = None,
body: Optional[Body] = None,
) -> None:
"""Create a new OPML document."""
self.version: str = version if version is not None else "2.0"
"""The OPML spec version of this document.
Must be `1.0`, `1.1` or `2.0`."""
if self.version not in ["1.0", "1.1", "2.0"]:
raise ValueError(f"unsupported version: {version}")
self.head: Optional[Head] = head
"""The Head child object. Contains the metadata of the OPML document."""
self.body: Body = Body() if body is None else body
"""The Body child object. Contains all the Outline elements."""
@staticmethod
def from_xml(xml: str) -> "OPML":
"""Parse an OPML document from an XML string."""
root = ElementTree.fromstring(xml)
if root.tag != "opml":
raise ValueError("root element is not <opml>")
# OPML elements must have a version attribute.
if "version" not in root.attrib:
raise ValueError("missing <opml> version attribute")
# And the version attribute can only be 1.0, 1.1 or 2.0.
version = root.attrib.get("version")
if version not in ["1.0", "1.1", "2.0"]:
raise ValueError(f"unsupported version: {version}")
# OPML elements must have a <body> child element.
if True not in map(lambda e: e.tag == "body", root):
raise ValueError("root <opml> element is missing a child <body>")
opml = OPML(version)
for child in root:
if child.tag == "head":
opml.head = Head.from_element_tree(child)
elif child.tag == "body":
opml.body = Body.from_element_tree(child)
# OPML documents must contain at least 1 <outline> element.
if len(opml.body.outlines) == 0:
raise ValueError("<body> contains no <outline> elements")
return opml
def to_xml(self) -> str:
"""Output the OPML document as an XML string."""
opml = XmlBuilder.Element("opml")
opml.attrib["version"] = self.version
if self.head is not None:
self.head.insert_xml_element(opml)
self.body.insert_xml_element(opml)
return XmlBuilder.tostring(opml, encoding="unicode")
def to_json(self) -> str:
"""Output the OPML document as a JSON string."""
return json.dumps(
self,
default=lambda o: _dict_exclude_none(o.__dict__),
indent=2,
sort_keys=True,
)