hext.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. """
  2. This is a rdflib plugin for parsing Hextuple files, which are Newline-Delimited JSON
  3. (ndjson) files, into Conjunctive. The store that backs the graph *must* be able to
  4. handle contexts, i.e. multiple graphs.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import warnings
  9. from io import TextIOWrapper
  10. from typing import TYPE_CHECKING, Any, BinaryIO, List, Optional, TextIO, Union
  11. from rdflib.graph import ConjunctiveGraph, Dataset, Graph
  12. from rdflib.parser import InputSource, Parser
  13. from rdflib.term import BNode, Literal, URIRef
  14. try:
  15. import orjson
  16. _HAS_ORJSON = True
  17. except ImportError:
  18. orjson = None # type: ignore[assignment, unused-ignore]
  19. _HAS_ORJSON = False
  20. if TYPE_CHECKING:
  21. from io import BufferedReader
  22. __all__ = ["HextuplesParser"]
  23. class HextuplesParser(Parser):
  24. """
  25. An RDFLib parser for Hextuples
  26. """
  27. def __init__(self):
  28. super(HextuplesParser, self).__init__()
  29. self.default_context: Optional[Graph] = None
  30. self.skolemize = False
  31. def _parse_hextuple(
  32. self, ds: Union[Dataset, ConjunctiveGraph], tup: List[Union[str, None]]
  33. ) -> None:
  34. # all values check
  35. # subject, predicate, value, datatype cannot be None
  36. # language and graph may be None
  37. if tup[0] is None or tup[1] is None or tup[2] is None or tup[3] is None:
  38. raise ValueError(
  39. f"subject, predicate, value, datatype cannot be None. Given: {tup}"
  40. )
  41. # 1 - subject
  42. s: Union[URIRef, BNode]
  43. if tup[0].startswith("_"):
  44. s = BNode(value=tup[0].replace("_:", ""))
  45. if self.skolemize:
  46. s = s.skolemize()
  47. else:
  48. s = URIRef(tup[0])
  49. # 2 - predicate
  50. p = URIRef(tup[1])
  51. # 3 - value
  52. o: Union[URIRef, BNode, Literal]
  53. if tup[3] == "globalId":
  54. o = URIRef(tup[2])
  55. elif tup[3] == "localId":
  56. o = BNode(value=tup[2].replace("_:", ""))
  57. if self.skolemize:
  58. o = o.skolemize()
  59. else: # literal
  60. if tup[4] is None:
  61. o = Literal(tup[2], datatype=URIRef(tup[3]))
  62. else:
  63. o = Literal(tup[2], lang=tup[4])
  64. # 6 - context
  65. if tup[5] is not None:
  66. c = (
  67. BNode(tup[5].replace("_:", ""))
  68. if tup[5].startswith("_:")
  69. else URIRef(tup[5])
  70. )
  71. if isinstance(c, BNode) and self.skolemize:
  72. c = c.skolemize()
  73. ds.get_context(c).add((s, p, o))
  74. elif self.default_context is not None:
  75. self.default_context.add((s, p, o))
  76. else:
  77. raise Exception("No context to parse into!")
  78. # type error: Signature of "parse" incompatible with supertype "Parser"
  79. def parse(self, source: InputSource, graph: Graph, skolemize: bool = False, **kwargs: Any) -> None: # type: ignore[override]
  80. if kwargs.get("encoding") not in [None, "utf-8"]:
  81. warnings.warn(
  82. f"Hextuples files are always utf-8 encoded, "
  83. f"I was passed: {kwargs.get('encoding')}, "
  84. "but I'm still going to use utf-8"
  85. )
  86. assert (
  87. graph.store.context_aware
  88. ), "Hextuples Parser needs a context-aware store!"
  89. self.skolemize = skolemize
  90. # Set default_union to True to mimic ConjunctiveGraph behavior
  91. ds = Dataset(store=graph.store, default_union=True)
  92. ds_default = ds.default_context # the DEFAULT_DATASET_GRAPH_ID
  93. if isinstance(graph, (Dataset, ConjunctiveGraph)):
  94. self.default_context = graph.default_context
  95. elif graph.identifier is not None:
  96. if graph.identifier == ds_default.identifier:
  97. self.default_context = graph
  98. else:
  99. self.default_context = ds.get_context(graph.identifier)
  100. else:
  101. # mypy thinks this is unreachable, but graph.identifier can be None
  102. self.default_context = ds_default # type: ignore[unreachable]
  103. if self.default_context is not ds_default:
  104. ds.default_context = self.default_context
  105. ds.remove_graph(ds_default) # remove the original unused default graph
  106. try:
  107. text_stream: Optional[TextIO] = source.getCharacterStream()
  108. except (AttributeError, LookupError):
  109. text_stream = None
  110. try:
  111. binary_stream: Optional[BinaryIO] = source.getByteStream()
  112. except (AttributeError, LookupError):
  113. binary_stream = None
  114. if text_stream is None and binary_stream is None:
  115. raise ValueError(
  116. f"Source does not have a character stream or a byte stream and cannot be used {type(source)}"
  117. )
  118. if TYPE_CHECKING:
  119. assert text_stream is not None or binary_stream is not None
  120. use_stream: Union[TextIO, BinaryIO]
  121. if _HAS_ORJSON:
  122. if binary_stream is not None:
  123. use_stream = binary_stream
  124. else:
  125. if TYPE_CHECKING:
  126. assert isinstance(text_stream, TextIOWrapper)
  127. use_stream = text_stream
  128. loads = orjson.loads
  129. else:
  130. if text_stream is not None:
  131. use_stream = text_stream
  132. else:
  133. if TYPE_CHECKING:
  134. assert isinstance(binary_stream, BufferedReader)
  135. use_stream = TextIOWrapper(binary_stream, encoding="utf-8")
  136. loads = json.loads
  137. for line in use_stream: # type: Union[str, bytes]
  138. if len(line) == 0 or line.isspace():
  139. # Skipping empty lines because this is what was being done before for the first and last lines, albeit in an rather indirect way.
  140. # The result is that we accept input that would otherwise be invalid.
  141. # Possibly we should just let this result in an error.
  142. continue
  143. # this complex handing is because the 'value' component is
  144. # allowed to be "" but not None
  145. # all other "" values are treated as None
  146. raw_line: List[str] = loads(line)
  147. hex_tuple_line = [x if x != "" else None for x in raw_line]
  148. if raw_line[2] == "":
  149. hex_tuple_line[2] = ""
  150. self._parse_hextuple(ds, hex_tuple_line)