store.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. """## Types of store
  2. `Context-aware`: An RDF store capable of storing statements within contexts
  3. is considered context-aware. Essentially, such a store is able to partition
  4. the RDF model it represents into individual, named, and addressable
  5. sub-graphs.
  6. Relevant Notation3 reference regarding formulae, quoted statements, and such:
  7. http://www.w3.org/DesignIssues/Notation3.html
  8. `Formula-aware`: An RDF store capable of distinguishing between statements
  9. that are asserted and statements that are quoted is considered formula-aware.
  10. `Transaction-capable`: capable of providing transactional integrity to the
  11. RDF operations performed on it.
  12. `Graph-aware`: capable of keeping track of empty graphs.
  13. """
  14. from __future__ import annotations
  15. import pickle
  16. from io import BytesIO
  17. from typing import (
  18. TYPE_CHECKING,
  19. Any,
  20. Dict,
  21. Generator,
  22. Iterable,
  23. Iterator,
  24. Mapping,
  25. Optional,
  26. Tuple,
  27. Union,
  28. )
  29. from rdflib.events import Dispatcher, Event
  30. if TYPE_CHECKING:
  31. from rdflib.graph import (
  32. Graph,
  33. _ContextType,
  34. _QuadType,
  35. _TripleChoiceType,
  36. _TriplePatternType,
  37. _TripleType,
  38. )
  39. from rdflib.plugins.sparql.sparql import Query, Update
  40. from rdflib.query import Result
  41. from rdflib.term import Identifier, Node, URIRef
  42. # Constants representing the state of a Store (returned by the open method)
  43. VALID_STORE = 1
  44. CORRUPTED_STORE = 0
  45. NO_STORE = -1
  46. UNKNOWN: None = None
  47. Pickler = pickle.Pickler
  48. Unpickler = pickle.Unpickler
  49. UnpicklingError = pickle.UnpicklingError
  50. __all__ = [
  51. "StoreCreatedEvent",
  52. "TripleAddedEvent",
  53. "TripleRemovedEvent",
  54. "NodePickler",
  55. "Store",
  56. ]
  57. class StoreCreatedEvent(Event):
  58. """This event is fired when the Store is created.
  59. Attributes:
  60. configuration: String used to create the store
  61. """
  62. class TripleAddedEvent(Event):
  63. """This event is fired when a triple is added.
  64. Attributes:
  65. triple: The triple added to the graph.
  66. context: The context of the triple, if any.
  67. graph: The graph to which the triple was added.
  68. """
  69. class TripleRemovedEvent(Event):
  70. """This event is fired when a triple is removed.
  71. Attributes:
  72. triple: The triple removed from the graph.
  73. context: The context of the triple, if any.
  74. graph: The graph from which the triple was removed.
  75. """
  76. class NodePickler:
  77. def __init__(self) -> None:
  78. self._objects: Dict[str, Any] = {}
  79. self._ids: Dict[Any, str] = {}
  80. self._get_object = self._objects.__getitem__
  81. def _get_ids(self, key: Any) -> Optional[str]:
  82. try:
  83. return self._ids.get(key)
  84. except TypeError:
  85. return None
  86. def register(self, object: Any, id: str) -> None:
  87. self._objects[id] = object
  88. self._ids[object] = id
  89. def loads(self, s: bytes) -> Node:
  90. up = Unpickler(BytesIO(s))
  91. # NOTE on type error: https://github.com/python/mypy/issues/2427
  92. # type error: Cannot assign to a method
  93. up.persistent_load = self._get_object # type: ignore[assignment]
  94. try:
  95. return up.load()
  96. except KeyError as e:
  97. raise UnpicklingError("Could not find Node class for %s" % e)
  98. def dumps(
  99. self, obj: Node, protocol: Optional[Any] = None, bin: Optional[Any] = None
  100. ):
  101. src = BytesIO()
  102. p = Pickler(src)
  103. # NOTE on type error: https://github.com/python/mypy/issues/2427
  104. # type error: Cannot assign to a method
  105. p.persistent_id = self._get_ids # type: ignore[assignment]
  106. p.dump(obj)
  107. return src.getvalue()
  108. def __getstate__(self) -> Mapping[str, Any]:
  109. state = self.__dict__.copy()
  110. del state["_get_object"]
  111. state.update(
  112. {"_ids": tuple(self._ids.items()), "_objects": tuple(self._objects.items())}
  113. )
  114. return state
  115. def __setstate__(self, state: Mapping[str, Any]) -> None:
  116. self.__dict__.update(state)
  117. self._ids = dict(self._ids)
  118. self._objects = dict(self._objects)
  119. self._get_object = self._objects.__getitem__
  120. class Store:
  121. # Properties
  122. context_aware: bool = False
  123. formula_aware: bool = False
  124. transaction_aware: bool = False
  125. graph_aware: bool = False
  126. def __init__(
  127. self,
  128. configuration: Optional[str] = None,
  129. identifier: Optional[Identifier] = None,
  130. ):
  131. """Initialize the Store.
  132. Args:
  133. identifier: URIRef of the Store. Defaults to CWD
  134. configuration: String containing information open can use to
  135. connect to datastore.
  136. """
  137. self.__node_pickler: Optional[NodePickler] = None
  138. self.dispatcher = Dispatcher()
  139. if configuration:
  140. self.open(configuration)
  141. @property
  142. def node_pickler(self) -> NodePickler:
  143. if self.__node_pickler is None:
  144. from rdflib.graph import Graph, QuotedGraph
  145. from rdflib.term import BNode, Literal, URIRef, Variable
  146. self.__node_pickler = np = NodePickler()
  147. np.register(self, "S")
  148. np.register(URIRef, "U")
  149. np.register(BNode, "B")
  150. np.register(Literal, "L")
  151. np.register(Graph, "G")
  152. np.register(QuotedGraph, "Q")
  153. np.register(Variable, "V")
  154. return self.__node_pickler
  155. # Database management methods
  156. # NOTE: Can't find any stores using this, we should consider deprecating it.
  157. def create(self, configuration: str) -> None:
  158. self.dispatcher.dispatch(StoreCreatedEvent(configuration=configuration))
  159. def open(
  160. self, configuration: Union[str, tuple[str, str]], create: bool = False
  161. ) -> Optional[int]:
  162. """Opens the store specified by the configuration string.
  163. Args:
  164. configuration: Store configuration string
  165. create: If True, a store will be created if it doesn't exist.
  166. If False and the store doesn't exist, an exception is raised.
  167. Returns:
  168. One of: VALID_STORE, CORRUPTED_STORE, or NO_STORE
  169. Raises:
  170. Exception: If there are insufficient permissions to open the store.
  171. """
  172. return UNKNOWN
  173. def close(self, commit_pending_transaction: bool = False) -> None:
  174. """Closes the database connection.
  175. Args:
  176. commit_pending_transaction: Whether to commit all pending
  177. transactions before closing (if the store is transactional).
  178. """
  179. def destroy(self, configuration: str) -> None:
  180. """Destroys the instance of the store.
  181. Args:
  182. configuration: The configuration string identifying the store instance.
  183. """
  184. def gc(self) -> None:
  185. """Allows the store to perform any needed garbage collection."""
  186. pass
  187. # RDF APIs
  188. def add(
  189. self,
  190. triple: _TripleType,
  191. context: _ContextType,
  192. quoted: bool = False,
  193. ) -> None:
  194. """Adds the given statement to a specific context or to the model.
  195. Args:
  196. triple: The triple to add
  197. context: The context to add the triple to
  198. quoted: If True, indicates this statement is quoted/hypothetical
  199. (for formula-aware stores)
  200. Note:
  201. It should be an error to not specify a context and have the quoted
  202. argument be True. It should also be an error for the quoted argument
  203. to be True when the store is not formula-aware.
  204. """
  205. self.dispatcher.dispatch(TripleAddedEvent(triple=triple, context=context))
  206. def addN(self, quads: Iterable[_QuadType]) -> None: # noqa: N802
  207. """Adds each item in the list of statements to a specific context.
  208. The quoted argument is interpreted by formula-aware stores to indicate this
  209. statement is quoted/hypothetical.
  210. Note:
  211. The default implementation is a redirect to add.
  212. Args:
  213. quads: An iterable of quads to add
  214. """
  215. for s, p, o, c in quads:
  216. assert c is not None, "Context associated with %s %s %s is None!" % (
  217. s,
  218. p,
  219. o,
  220. )
  221. self.add((s, p, o), c)
  222. def remove(
  223. self,
  224. triple: _TriplePatternType,
  225. context: Optional[_ContextType] = None,
  226. ) -> None:
  227. """Remove the set of triples matching the pattern from the store"""
  228. self.dispatcher.dispatch(TripleRemovedEvent(triple=triple, context=context))
  229. def triples_choices(
  230. self,
  231. triple: _TripleChoiceType,
  232. context: Optional[_ContextType] = None,
  233. ) -> Generator[
  234. Tuple[
  235. _TripleType,
  236. Iterator[Optional[_ContextType]],
  237. ],
  238. None,
  239. None,
  240. ]:
  241. """
  242. A variant of triples that can take a list of terms instead of a single
  243. term in any slot. Stores can implement this to optimize the response
  244. time from the default 'fallback' implementation, which will iterate
  245. over each term in the list and dispatch to triples
  246. """
  247. subject, predicate, object_ = triple
  248. if isinstance(object_, list):
  249. assert not isinstance(subject, list), "object_ / subject are both lists"
  250. assert not isinstance(predicate, list), "object_ / predicate are both lists"
  251. if object_:
  252. for obj in object_:
  253. for (s1, p1, o1), cg in self.triples(
  254. (subject, predicate, obj), context
  255. ):
  256. yield (s1, p1, o1), cg
  257. else:
  258. for (s1, p1, o1), cg in self.triples(
  259. (subject, predicate, None), context
  260. ):
  261. yield (s1, p1, o1), cg
  262. elif isinstance(subject, list):
  263. assert not isinstance(predicate, list), "subject / predicate are both lists"
  264. if subject:
  265. for subj in subject:
  266. for (s1, p1, o1), cg in self.triples(
  267. (subj, predicate, object_), context
  268. ):
  269. yield (s1, p1, o1), cg
  270. else:
  271. for (s1, p1, o1), cg in self.triples(
  272. (None, predicate, object_), context
  273. ):
  274. yield (s1, p1, o1), cg
  275. elif isinstance(predicate, list):
  276. assert not isinstance(subject, list), "predicate / subject are both lists"
  277. if predicate:
  278. for pred in predicate:
  279. for (s1, p1, o1), cg in self.triples(
  280. (subject, pred, object_), context
  281. ):
  282. yield (s1, p1, o1), cg
  283. else:
  284. for (s1, p1, o1), cg in self.triples((subject, None, object_), context):
  285. yield (s1, p1, o1), cg
  286. # type error: Missing return statement
  287. def triples( # type: ignore[return]
  288. self,
  289. triple_pattern: _TriplePatternType,
  290. context: Optional[_ContextType] = None,
  291. ) -> Iterator[Tuple[_TripleType, Iterator[Optional[_ContextType]]]]:
  292. """
  293. A generator over all the triples matching the pattern. Pattern can
  294. include any objects for used for comparing against nodes in the store,
  295. for example, REGEXTerm, URIRef, Literal, BNode, Variable, Graph,
  296. QuotedGraph, Date? DateRange?
  297. Args:
  298. context: A conjunctive query can be indicated by either
  299. providing a value of None, or a specific context can be
  300. queries by passing a Graph instance (if store is context aware).
  301. """
  302. subject, predicate, object = triple_pattern
  303. # variants of triples will be done if / when optimization is needed
  304. # type error: Missing return statement
  305. def __len__(self, context: Optional[_ContextType] = None) -> int: # type: ignore[empty-body]
  306. """
  307. Number of statements in the store. This should only account for non-
  308. quoted (asserted) statements if the context is not specified,
  309. otherwise it should return the number of statements in the formula or
  310. context given.
  311. Args:
  312. context: a graph instance to query or None
  313. """
  314. # type error: Missing return statement
  315. def contexts( # type: ignore[empty-body]
  316. self, triple: Optional[_TripleType] = None
  317. ) -> Generator[_ContextType, None, None]:
  318. """
  319. Generator over all contexts in the graph. If triple is specified,
  320. a generator over all contexts the triple is in.
  321. if store is graph_aware, may also return empty contexts
  322. :returns: a generator over Nodes
  323. """
  324. # TODO FIXME: the result of query is inconsistent.
  325. def query(
  326. self,
  327. query: Union[Query, str],
  328. initNs: Mapping[str, Any], # noqa: N803
  329. initBindings: Mapping[str, Identifier], # noqa: N803
  330. queryGraph: str, # noqa: N803
  331. **kwargs: Any,
  332. ) -> Result:
  333. """If stores provide their own SPARQL implementation, override this.
  334. queryGraph is None, a URIRef or `__UNION__`
  335. If None the graph is specified in the query-string/object
  336. If URIRef it specifies the graph to query,
  337. If `__UNION__` the union of all named graphs should be queried
  338. (This is used by ConjunctiveGraphs
  339. Values other than None obviously only makes sense for
  340. context-aware stores.)
  341. """
  342. raise NotImplementedError
  343. def update(
  344. self,
  345. update: Union[Update, str],
  346. initNs: Mapping[str, Any], # noqa: N803
  347. initBindings: Mapping[str, Identifier], # noqa: N803
  348. queryGraph: str, # noqa: N803
  349. **kwargs: Any,
  350. ) -> None:
  351. """If stores provide their own (SPARQL) Update implementation, override this.
  352. queryGraph is None, a URIRef or `__UNION__`
  353. If None the graph is specified in the query-string/object
  354. If URIRef it specifies the graph to query,
  355. If `__UNION__` the union of all named graphs should be queried
  356. (This is used by ConjunctiveGraphs
  357. Values other than None obviously only makes sense for
  358. context-aware stores.)
  359. """
  360. raise NotImplementedError
  361. # Optional Namespace methods
  362. def bind(self, prefix: str, namespace: URIRef, override: bool = True) -> None:
  363. """Bind a namespace to a prefix.
  364. Args:
  365. prefix: The prefix to bind the namespace to.
  366. namespace: The URIRef of the namespace to bind.
  367. override: If True, rebind even if the given namespace is already bound
  368. to another prefix
  369. """
  370. def prefix(self, namespace: URIRef) -> Optional[str]:
  371. """"""
  372. def namespace(self, prefix: str) -> Optional[URIRef]:
  373. """ """
  374. def namespaces(self) -> Iterator[Tuple[str, URIRef]]:
  375. """ """
  376. # This is here so that the function becomes an empty generator.
  377. # See https://stackoverflow.com/q/13243766 and
  378. # https://www.python.org/dev/peps/pep-0255/#why-a-new-keyword-for-yield-why-not-a-builtin-function-instead
  379. if False:
  380. yield None # type: ignore[unreachable]
  381. # Optional Transactional methods
  382. def commit(self) -> None:
  383. """ """
  384. def rollback(self) -> None:
  385. """ """
  386. # Optional graph methods
  387. def add_graph(self, graph: Graph) -> None:
  388. """Add a graph to the store, no effect if the graph already
  389. exists.
  390. Args:
  391. graph: a Graph instance
  392. """
  393. raise Exception("Graph method called on non-graph_aware store")
  394. def remove_graph(self, graph: Graph) -> None:
  395. """Remove a graph from the store, this should also remove all
  396. triples in the graph
  397. Args:
  398. graphid: a Graph instance
  399. """
  400. raise Exception("Graph method called on non-graph_aware store")