xmlresults.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. """A Parser for SPARQL results in XML:
  2. http://www.w3.org/TR/rdf-sparql-XMLres/
  3. Bits and pieces borrowed from:
  4. http://projects.bigasterisk.com/sparqlhttp/
  5. Authors: Drew Perttula, Gunnar Aastrand Grimnes
  6. """
  7. from __future__ import annotations
  8. import logging
  9. import xml.etree.ElementTree as xml_etree # noqa: N813
  10. from io import BytesIO
  11. from typing import (
  12. IO,
  13. TYPE_CHECKING,
  14. Any,
  15. BinaryIO,
  16. Dict,
  17. Optional,
  18. Sequence,
  19. TextIO,
  20. Tuple,
  21. Union,
  22. cast,
  23. )
  24. from xml.dom import XML_NAMESPACE
  25. from xml.sax.saxutils import XMLGenerator
  26. from xml.sax.xmlreader import AttributesNSImpl
  27. from rdflib.query import Result, ResultException, ResultParser, ResultSerializer
  28. from rdflib.term import BNode, Identifier, Literal, URIRef, Variable
  29. try:
  30. # https://adamj.eu/tech/2021/12/29/python-type-hints-optional-imports/
  31. import lxml.etree as lxml_etree
  32. FOUND_LXML = True
  33. except ImportError:
  34. FOUND_LXML = False
  35. SPARQL_XML_NAMESPACE = "http://www.w3.org/2005/sparql-results#"
  36. RESULTS_NS_ET = "{%s}" % SPARQL_XML_NAMESPACE
  37. log = logging.getLogger(__name__)
  38. class XMLResultParser(ResultParser):
  39. """A Parser for SPARQL results in XML."""
  40. # TODO FIXME: content_type should be a keyword only arg.
  41. def parse(self, source: IO, content_type: Optional[str] = None) -> Result: # type: ignore[override]
  42. return XMLResult(source)
  43. class XMLResult(Result):
  44. def __init__(self, source: IO, content_type: Optional[str] = None):
  45. parser_encoding: Optional[str] = None
  46. if hasattr(source, "encoding"):
  47. if TYPE_CHECKING:
  48. assert isinstance(source, TextIO)
  49. parser_encoding = "utf-8"
  50. source_str = source.read()
  51. source = BytesIO(source_str.encode(parser_encoding))
  52. else:
  53. if TYPE_CHECKING:
  54. assert isinstance(source, BinaryIO)
  55. if FOUND_LXML:
  56. lxml_parser = lxml_etree.XMLParser(huge_tree=True, encoding=parser_encoding)
  57. tree = cast(
  58. xml_etree.ElementTree,
  59. lxml_etree.parse(source, parser=lxml_parser),
  60. )
  61. else:
  62. xml_parser = xml_etree.XMLParser(encoding=parser_encoding)
  63. tree = xml_etree.parse(source, parser=xml_parser)
  64. boolean = tree.find(RESULTS_NS_ET + "boolean")
  65. results = tree.find(RESULTS_NS_ET + "results")
  66. if boolean is not None:
  67. type_ = "ASK"
  68. elif results is not None:
  69. type_ = "SELECT"
  70. else:
  71. raise ResultException("No RDF result-bindings or boolean answer found!")
  72. Result.__init__(self, type_)
  73. if type_ == "SELECT":
  74. self.bindings = []
  75. for result in results: # type: ignore[union-attr]
  76. if result.tag != f"{RESULTS_NS_ET}result":
  77. # This is here because with lxml this also gets comments,
  78. # not just elements. Also this should not operate on non
  79. # "result" elements.
  80. continue
  81. r = {}
  82. for binding in result:
  83. if binding.tag != f"{RESULTS_NS_ET}binding":
  84. # This is here because with lxml this also gets
  85. # comments, not just elements. Also this should not
  86. # operate on non "binding" elements.
  87. continue
  88. # type error: error: Argument 1 to "Variable" has incompatible type "Union[str, None, Any]"; expected "str"
  89. # NOTE on type error: Element.get() can return None, and
  90. # this will invariably fail if passed into Variable
  91. # constructor as value
  92. r[Variable(binding.get("name"))] = parseTerm(binding[0]) # type: ignore[arg-type] # FIXME
  93. self.bindings.append(r)
  94. self.vars = [
  95. # type error: Argument 1 to "Variable" has incompatible type "Optional[str]"; expected "str"
  96. # NOTE on type error: Element.get() can return None, and this
  97. # will invariably fail if passed into Variable constructor as
  98. # value
  99. Variable(x.get("name")) # type: ignore[arg-type] # FIXME
  100. for x in tree.findall(
  101. "./%shead/%svariable" % (RESULTS_NS_ET, RESULTS_NS_ET)
  102. )
  103. ]
  104. else:
  105. self.askAnswer = boolean.text.lower().strip() == "true" # type: ignore[union-attr]
  106. def parseTerm(element: xml_etree.Element) -> Union[URIRef, Literal, BNode]:
  107. """rdflib object (Literal, URIRef, BNode) for the given
  108. elementtree element"""
  109. tag, text = element.tag, element.text
  110. if tag == RESULTS_NS_ET + "literal":
  111. if text is None:
  112. text = ""
  113. datatype = None
  114. lang = None
  115. if element.get("datatype", None):
  116. # type error: Argument 1 to "URIRef" has incompatible type "Optional[str]"; expected "str"
  117. datatype = URIRef(element.get("datatype")) # type: ignore[arg-type]
  118. elif element.get("{%s}lang" % XML_NAMESPACE, None):
  119. lang = element.get("{%s}lang" % XML_NAMESPACE)
  120. ret = Literal(text, datatype=datatype, lang=lang)
  121. return ret
  122. elif tag == RESULTS_NS_ET + "uri":
  123. # type error: Argument 1 to "URIRef" has incompatible type "Optional[str]"; expected "str"
  124. return URIRef(text) # type: ignore[arg-type]
  125. elif tag == RESULTS_NS_ET + "bnode":
  126. return BNode(text)
  127. else:
  128. raise TypeError("unknown binding type %r" % element)
  129. class XMLResultSerializer(ResultSerializer):
  130. """Serializes SPARQL results into XML format."""
  131. def __init__(self, result: Result):
  132. ResultSerializer.__init__(self, result)
  133. def serialize(self, stream: IO, encoding: str = "utf-8", **kwargs: Any) -> None:
  134. writer = SPARQLXMLWriter(stream, encoding)
  135. if self.result.type == "ASK":
  136. writer.write_header([])
  137. # type error: Argument 1 to "write_ask" of "SPARQLXMLWriter" has incompatible type "Optional[bool]"; expected "bool"
  138. writer.write_ask(self.result.askAnswer) # type: ignore[arg-type]
  139. else:
  140. # type error: Argument 1 to "write_header" of "SPARQLXMLWriter" has incompatible type "Optional[List[Variable]]"; expected "Sequence[Variable]"
  141. writer.write_header(self.result.vars) # type: ignore[arg-type]
  142. writer.write_results_header()
  143. for b in self.result.bindings:
  144. writer.write_start_result()
  145. for key, val in b.items():
  146. writer.write_binding(key, val)
  147. writer.write_end_result()
  148. writer.close()
  149. # TODO: Rewrite with ElementTree?
  150. class SPARQLXMLWriter:
  151. """
  152. Python saxutils-based SPARQL XML Writer
  153. """
  154. def __init__(self, output: IO, encoding: str = "utf-8"):
  155. writer = XMLGenerator(output, encoding)
  156. writer.startDocument()
  157. writer.startPrefixMapping("", SPARQL_XML_NAMESPACE)
  158. writer.startPrefixMapping("xml", XML_NAMESPACE)
  159. writer.startElementNS(
  160. (SPARQL_XML_NAMESPACE, "sparql"), "sparql", AttributesNSImpl({}, {})
  161. )
  162. self.writer = writer
  163. self._output = output
  164. self._encoding = encoding
  165. self._results = False
  166. def write_header(self, allvarsL: Sequence[Variable]) -> None:
  167. self.writer.startElementNS(
  168. (SPARQL_XML_NAMESPACE, "head"), "head", AttributesNSImpl({}, {})
  169. )
  170. for i in range(0, len(allvarsL)):
  171. attr_vals = {
  172. (None, "name"): str(allvarsL[i]),
  173. }
  174. attr_qnames = {
  175. (None, "name"): "name",
  176. }
  177. self.writer.startElementNS(
  178. (SPARQL_XML_NAMESPACE, "variable"),
  179. "variable",
  180. # type error: Argument 1 to "AttributesNSImpl" has incompatible type "Dict[Tuple[None, str], str]"; expected "Mapping[Tuple[str, str], str]"
  181. # type error: Argument 2 to "AttributesNSImpl" has incompatible type "Dict[Tuple[None, str], str]"; expected "Mapping[Tuple[str, str], str]" [arg-type]
  182. AttributesNSImpl(attr_vals, attr_qnames), # type: ignore[arg-type]
  183. )
  184. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "variable"), "variable")
  185. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "head"), "head")
  186. def write_ask(self, val: bool) -> None:
  187. self.writer.startElementNS(
  188. (SPARQL_XML_NAMESPACE, "boolean"), "boolean", AttributesNSImpl({}, {})
  189. )
  190. self.writer.characters(str(val).lower())
  191. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "boolean"), "boolean")
  192. def write_results_header(self) -> None:
  193. self.writer.startElementNS(
  194. (SPARQL_XML_NAMESPACE, "results"), "results", AttributesNSImpl({}, {})
  195. )
  196. self._results = True
  197. def write_start_result(self) -> None:
  198. self.writer.startElementNS(
  199. (SPARQL_XML_NAMESPACE, "result"), "result", AttributesNSImpl({}, {})
  200. )
  201. self._resultStarted = True
  202. def write_end_result(self) -> None:
  203. assert self._resultStarted
  204. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "result"), "result")
  205. self._resultStarted = False
  206. def write_binding(self, name: Variable, val: Identifier) -> None:
  207. assert self._resultStarted
  208. attr_vals: Dict[Tuple[Optional[str], str], str] = {
  209. (None, "name"): str(name),
  210. }
  211. attr_qnames: Dict[Tuple[Optional[str], str], str] = {
  212. (None, "name"): "name",
  213. }
  214. self.writer.startElementNS(
  215. (SPARQL_XML_NAMESPACE, "binding"),
  216. "binding",
  217. # type error: Argument 1 to "AttributesNSImpl" has incompatible type "Dict[Tuple[None, str], str]"; expected "Mapping[Tuple[str, str], str]"
  218. # type error: Argument 2 to "AttributesNSImpl" has incompatible type "Dict[Tuple[None, str], str]"; expected "Mapping[Tuple[str, str], str]"
  219. AttributesNSImpl(attr_vals, attr_qnames), # type: ignore[arg-type, unused-ignore]
  220. )
  221. if isinstance(val, URIRef):
  222. self.writer.startElementNS(
  223. (SPARQL_XML_NAMESPACE, "uri"), "uri", AttributesNSImpl({}, {})
  224. )
  225. self.writer.characters(val)
  226. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "uri"), "uri")
  227. elif isinstance(val, BNode):
  228. self.writer.startElementNS(
  229. (SPARQL_XML_NAMESPACE, "bnode"), "bnode", AttributesNSImpl({}, {})
  230. )
  231. self.writer.characters(val)
  232. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "bnode"), "bnode")
  233. elif isinstance(val, Literal):
  234. attr_vals = {}
  235. attr_qnames = {}
  236. if val.language:
  237. attr_vals[(XML_NAMESPACE, "lang")] = val.language
  238. attr_qnames[(XML_NAMESPACE, "lang")] = "xml:lang"
  239. elif val.datatype:
  240. attr_vals[(None, "datatype")] = val.datatype
  241. attr_qnames[(None, "datatype")] = "datatype"
  242. self.writer.startElementNS(
  243. (SPARQL_XML_NAMESPACE, "literal"),
  244. "literal",
  245. # type error: Argument 1 to "AttributesNSImpl" has incompatible type "Dict[Tuple[Optional[str], str], str]"; expected "Mapping[Tuple[str, str], str]"
  246. # type error: Argument 2 to "AttributesNSImpl" has incompatible type "Dict[Tuple[Optional[str], str], str]"; expected "Mapping[Tuple[str, str], str]"
  247. AttributesNSImpl(attr_vals, attr_qnames), # type: ignore[arg-type, unused-ignore]
  248. )
  249. self.writer.characters(val)
  250. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "literal"), "literal")
  251. else:
  252. raise Exception("Unsupported RDF term: %s" % val)
  253. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "binding"), "binding")
  254. def close(self) -> None:
  255. if self._results:
  256. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "results"), "results")
  257. self.writer.endElementNS((SPARQL_XML_NAMESPACE, "sparql"), "sparql")
  258. self.writer.endDocument()