rdf4j.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. from __future__ import annotations
  2. from textwrap import dedent
  3. from typing import Any, Generator, Iterable, Iterator, Mapping, Optional, Tuple
  4. from rdflib import Graph
  5. from rdflib.contrib.rdf4j import has_httpx
  6. from rdflib.contrib.rdf4j.exceptions import RepositoryNotFoundError
  7. from rdflib.graph import (
  8. DATASET_DEFAULT_GRAPH_ID,
  9. Dataset,
  10. _ContextType,
  11. _QuadType,
  12. _TriplePatternType,
  13. _TripleType,
  14. )
  15. from rdflib.store import VALID_STORE, Store
  16. from rdflib.term import BNode, Node, URIRef, Variable
  17. if has_httpx:
  18. from rdflib.contrib.rdf4j import RDF4JClient
  19. def _inject_prefixes(query: str, extra_bindings: Mapping[str, Any]) -> str:
  20. bindings = set(list(extra_bindings.items()))
  21. if not bindings:
  22. return query
  23. return "\n".join(
  24. [
  25. "\n".join(["PREFIX %s: <%s>" % (k, v) for k, v in bindings]),
  26. "", # separate ns_bindings from query with an empty line
  27. query,
  28. ]
  29. )
  30. def _node_to_sparql(node: Node) -> str:
  31. if isinstance(node, BNode):
  32. raise Exception(
  33. "SPARQL-based stores do not support BNodes"
  34. "See http://www.w3.org/TR/sparql11-query/#BGPsparqlBNodes"
  35. )
  36. return node.n3()
  37. def _default_repo_config(repository_id: str) -> str:
  38. return dedent(
  39. f"""
  40. PREFIX config: <tag:rdf4j.org,2023:config/>
  41. [] a config:Repository ;
  42. config:rep.id "{repository_id}" ;
  43. config:rep.impl
  44. [
  45. config:rep.type "openrdf:SailRepository" ;
  46. config:sail.impl
  47. [
  48. config:native.tripleIndexers "spoc,posc" ;
  49. config:sail.defaultQueryEvaluationMode "STANDARD" ;
  50. config:sail.iterationCacheSyncThreshold "10000" ;
  51. config:sail.type "openrdf:NativeStore" ;
  52. ] ;
  53. ] ;
  54. .
  55. """
  56. )
  57. class RDF4JStore(Store):
  58. """An RDF4J store."""
  59. context_aware = True
  60. formula_aware = False
  61. transaction_aware = False
  62. graph_aware = True
  63. def __init__(
  64. self,
  65. base_url: str,
  66. repository_id: str,
  67. configuration: str | None = None,
  68. auth: tuple[str, str] | None = None,
  69. timeout: float = 30.0,
  70. create: bool = False,
  71. **kwargs,
  72. ):
  73. if configuration is None:
  74. configuration = _default_repo_config(repository_id)
  75. self._client = RDF4JClient(base_url, auth, timeout, **kwargs)
  76. self._repository_id = repository_id
  77. self._repo = None
  78. self.open(configuration, create)
  79. super().__init__()
  80. @property
  81. def client(self):
  82. return self._client
  83. @property
  84. def repo(self):
  85. if self._repo is None:
  86. self._repo = self.client.repositories.get(self._repository_id)
  87. return self._repo
  88. def open(
  89. self, configuration: str | tuple[str, str] | None, create: bool = False
  90. ) -> int | None:
  91. try:
  92. # Try connecting to the repository.
  93. self.repo.health()
  94. except RepositoryNotFoundError:
  95. if create:
  96. self.client.repositories.create(self._repository_id, configuration)
  97. self.repo.health()
  98. else:
  99. raise Exception(f"Repository {self._repository_id} not found.")
  100. return VALID_STORE
  101. def close(self, commit_pending_transaction: bool = False) -> None:
  102. self.client.close()
  103. def add(
  104. self,
  105. triple: _TripleType,
  106. context: _ContextType | None = None,
  107. quoted: bool = False,
  108. ) -> None:
  109. s, p, o = triple
  110. graph_name = (
  111. ""
  112. if context is None or context.identifier == DATASET_DEFAULT_GRAPH_ID
  113. else context.identifier.n3()
  114. )
  115. statement = f"{s.n3()} {p.n3()} {o.n3()} {graph_name} ."
  116. self.repo.upload(statement)
  117. def addN(self, quads: Iterable[_QuadType]) -> None: # noqa: N802
  118. statements = ""
  119. for s, p, o, c in quads:
  120. graph_name = (
  121. ""
  122. if c is None or c.identifier == DATASET_DEFAULT_GRAPH_ID
  123. else c.identifier.n3()
  124. )
  125. statement = f"{s.n3()} {p.n3()} {o.n3()} {graph_name} .\n"
  126. statements += statement
  127. self.repo.upload(statements)
  128. def remove(
  129. self,
  130. triple: _TriplePatternType,
  131. context: Optional[_ContextType] = None,
  132. ) -> None:
  133. s, p, o = triple
  134. g = context.identifier if context is not None else None
  135. self.repo.delete(s, p, o, g)
  136. def triples(
  137. self,
  138. triple_pattern: _TriplePatternType,
  139. context: Optional[_ContextType] = None,
  140. ) -> Iterator[Tuple[_TripleType, Iterator[Optional[_ContextType]]]]:
  141. s, p, o = triple_pattern
  142. graph_name = context.identifier if context is not None else None
  143. result_graph = self.repo.get(s, p, o, graph_name)
  144. if isinstance(result_graph, Dataset):
  145. for s, p, o, g in result_graph:
  146. yield (s, p, o), iter([Graph(self, identifier=g)])
  147. else:
  148. # It's a Graph object.
  149. for triple in result_graph:
  150. # Returning None for _ContextType as it's not used by the caller.
  151. yield triple, iter([None])
  152. def contexts(
  153. self, triple: Optional[_TripleType] = None
  154. ) -> Generator[_ContextType, None, None]:
  155. if triple is None:
  156. for graph_name in self.repo.graph_names():
  157. yield Graph(self, identifier=graph_name)
  158. else:
  159. s, p, o = triple
  160. params = (
  161. _node_to_sparql(s if s else Variable("s")),
  162. _node_to_sparql(p if p else Variable("p")),
  163. _node_to_sparql(o if o else Variable("o")),
  164. )
  165. query = (
  166. "SELECT DISTINCT ?graph WHERE { GRAPH ?graph { %s %s %s } }" % params
  167. )
  168. result = self.repo.query(query)
  169. for row in result:
  170. yield Graph(self, identifier=row["graph"])
  171. def bind(self, prefix: str, namespace: URIRef, override: bool = True) -> None:
  172. # Note: RDF4J namespaces always override.
  173. self.repo.namespaces.set(prefix, namespace)
  174. def prefix(self, namespace: URIRef) -> Optional[str]:
  175. namespace_prefixes = dict(
  176. [(x.namespace, x.prefix) for x in self.repo.namespaces.list()]
  177. )
  178. return namespace_prefixes.get(str(namespace))
  179. def namespace(self, prefix: str) -> Optional[URIRef]:
  180. result = self.repo.namespaces.get(prefix)
  181. return URIRef(result) if result is not None else None
  182. def namespaces(self) -> Iterator[Tuple[str, URIRef]]:
  183. for result in self.repo.namespaces.list():
  184. yield result.prefix, URIRef(result.namespace)
  185. def add_graph(self, graph: Graph) -> None:
  186. if graph.identifier != DATASET_DEFAULT_GRAPH_ID:
  187. # Note: this is a no-op since RDF4J doesn't support empty named graphs.
  188. self.repo.update(f"CREATE SILENT GRAPH {graph.identifier.n3()}")
  189. def remove_graph(self, graph: Graph) -> None:
  190. self.repo.graphs.clear(graph.identifier)
  191. def __len__(self, context: _ContextType | None = None) -> int:
  192. return self.repo.size(context if context is None else context.identifier)