graph.pyx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import logging
  2. from functools import wraps
  3. from rdflib import Graph
  4. from rdflib.term import Node
  5. from lakesuperior import env
  6. from cpython.mem cimport PyMem_Malloc, PyMem_Free
  7. from libc.string cimport memcmp
  8. from lakesuperior.cy_include cimport calg
  9. from lakesuperior.cy_include cimport cylmdb as lmdb
  10. from lakesuperior.store.ldp_rs cimport term
  11. from lakesuperior.store.ldp_rs.lmdb_triplestore cimport (
  12. KLEN, DBL_KLEN, TRP_KLEN, TripleKey, LmdbTriplestore)
  13. from lakesuperior.store.ldp_rs.keyset cimport Keyset
  14. from lakesuperior.store.ldp_rs.triple cimport Triple
  15. from lakesuperior.util.hash cimport Hash64, hash64
  16. logger = logging.getLogger(__name__)
  17. def use_data(fn):
  18. """
  19. Decorator to indicate that a set operation between two SimpleGraph
  20. instances should use the ``data`` property of the second term. The second
  21. term can also be a simple set.
  22. """
  23. @wraps(fn)
  24. def _wrapper(self, other):
  25. if isinstance(other, SimpleGraph):
  26. other = other.data
  27. return _wrapper
  28. cdef unsigned int set_item_hash_fn(calg.SetValue data):
  29. """
  30. Hash function for the CAlg set implementation.
  31. https://fragglet.github.io/c-algorithms/doc/set_8h.html#6c7986a2a80d7a3cb7b9d74e1c6fef97
  32. :param SetItem *data: Pointer to a SetItem structure.
  33. """
  34. cdef:
  35. Hash64 hash
  36. term.Buffer sr_data
  37. sr_data.addr = (<SetItem>data).data
  38. sr_data.sz = (<SetItem>data).size
  39. hash64(&sr_data, &hash)
  40. return hash
  41. cdef bint set_item_cmp_fn(calg.SetValue v1, calg.SetValue v2):
  42. """
  43. Compare function for two CAlg set items.
  44. https://fragglet.github.io/c-algorithms/doc/set_8h.html#40fa2c86d5b003c1b0b0e8dd1e4df9f4
  45. """
  46. if (<SetItem *>v1)[0].size != (<SetItem *>v2)[0].size:
  47. return False
  48. return memcmp(
  49. (<SetItem *>v1)[0].data, (<SetItem *>v2)[0].data,
  50. (<SetItem *>v1)[0].size)
  51. cdef class SimpleGraph:
  52. """
  53. Fast and simple implementation of a graph.
  54. Most functions should mimic RDFLib's graph with less overhead. It uses
  55. the same funny but functional slicing notation.
  56. A SimpleGraph can be obtained from a
  57. :py:class:`lakesuperior.store.keyset.Keyset` which is convenient bacause
  58. a Keyset can be obtained very efficiently from querying a store, then also
  59. very efficiently filtered and eventually converted into a set of readable
  60. terms.
  61. An instance of this class can also be converted to and from a
  62. ``rdflib.Graph`` instance.
  63. """
  64. def __cinit__(
  65. self, calg.Set *cdata=NULL, Keyset keyset=None, store=None,
  66. set data=set()):
  67. """
  68. Initialize the graph with pre-existing data or by looking up a store.
  69. One of ``cdata``, ``keyset``, or ``data`` can be provided. If more than
  70. one of these is provided, precedence is given in the mentioned order.
  71. If none of them is specified, an empty graph is initialized.
  72. :param rdflib.URIRef uri: The graph URI.
  73. This will serve as the subject for some queries.
  74. :param calg.Set cdata: Initial data as a C ``Set`` struct.
  75. :param Keyset keyset: Keyset to create the graph from. Keys will be
  76. converted to set elements.
  77. :param lakesuperior.store.ldp_rs.LmdbTripleStore store: store to
  78. look up the keyset. Only used if ``keyset`` is specified. If not
  79. set, the environment store is used.
  80. :param set data: Initial data as a set of 3-tuples of RDFLib terms.
  81. :param tuple lookup: tuple of a 3-tuple of lookup terms, and a context.
  82. E.g. ``((URIRef('urn:ns:a'), None, None), URIRef('urn:ns:ctx'))``.
  83. Any and all elements may be ``None``.
  84. :param lmdbStore store: the store to look data up.
  85. """
  86. self.store = store or env.app_defaults.rdf_store
  87. cdef:
  88. size_t i = 0
  89. TripleKey spok
  90. term.Buffer pk_t
  91. if cdata is not NULL:
  92. # Build data from provided C set.
  93. self._data = cdata
  94. else:
  95. # Initialize empty data set.
  96. self._data = calg.set_new(set_item_hash_fn, set_item_cmp_fn)
  97. if keyset is not None:
  98. # Populate with triples extracted from provided key set.
  99. while keyset.next(spok):
  100. self.store.lookup_term(spok[:KLEN], &pk_t)
  101. term.deserialize(&pk_t, self._trp.s)
  102. self.store.lookup_term(spok[KLEN:DBL_KLEN], &pk_t)
  103. term.deserialize(&pk_t, self._trp.p)
  104. self.store.lookup_term(spok[DBL_KLEN:TRP_KLEN], &pk_t)
  105. term.deserialize(&pk_t, self._trp.o)
  106. calg.set_insert(self._data, &self._trp)
  107. else:
  108. # Populate with provided Python set.
  109. self._trp = <Triple *>PyMem_Malloc(sizeof(Triple) * len(data))
  110. for s, p, o in data:
  111. term.from_rdflib(s, self._trp[i].s)
  112. term.from_rdflib(p, self._trp[i].p)
  113. term.from_rdflib(o, self._trp[i].o)
  114. calg.set_insert(self._data, self._trp)
  115. def __dealloc__(self):
  116. PyMem_Free(self._trp)
  117. @property
  118. def data(self):
  119. """
  120. Triple data as a Python set.
  121. :rtype: set
  122. """
  123. return self._data_as_set()
  124. cdef void _data_from_lookup(
  125. self, LmdbTriplestore store, tuple trp_ptn, ctx=None) except *:
  126. """
  127. Look up triples in the triplestore and load them into ``data``.
  128. :param tuple lookup: 3-tuple of RDFlib terms or ``None``.
  129. :param LmdbTriplestore store: Reference to a LMDB triplestore. This
  130. is normally set to ``lakesuperior.env.app_globals.rdf_store``.
  131. """
  132. cdef:
  133. size_t i
  134. unsigned char spok[TRP_KLEN]
  135. self._data = calg.set_new(set_item_hash_fn, set_item_cmp_fn)
  136. with store.txn_ctx():
  137. keyset = store.triple_keys(trp_ptn, ctx)
  138. for i in range(keyset.ct):
  139. spok = keyset.data + i * TRP_KLEN
  140. self.data.add(store.from_trp_key(spok[: TRP_KLEN]))
  141. strp = serialize_triple(self._trp)
  142. calg.set_insert(self._data, strp)
  143. cdef _data_as_set(self):
  144. """
  145. Convert triple data to a Python set.
  146. :rtype: set
  147. """
  148. pass
  149. # Basic set operations.
  150. def add(self, dataset):
  151. """ Set union. """
  152. self.data.add(dataset)
  153. def remove(self, item):
  154. """
  155. Remove one item from the graph.
  156. :param tuple item: A 3-tuple of RDFlib terms. Only exact terms, i.e.
  157. wildcards are not accepted.
  158. """
  159. self.data.remove(item)
  160. def __len__(self):
  161. """ Number of triples in the graph. """
  162. return len(self.data)
  163. @use_data
  164. def __eq__(self, other):
  165. """ Equality operator between ``SimpleGraph`` instances. """
  166. return self.data == other
  167. def __repr__(self):
  168. """
  169. String representation of the graph.
  170. It provides the number of triples in the graph and memory address of
  171. the instance.
  172. """
  173. return (f'<{self.__class__.__name__} @{hex(id(self))} '
  174. f'length={len(self.data)}>')
  175. def __str__(self):
  176. """ String dump of the graph triples. """
  177. return str(self.data)
  178. @use_data
  179. def __sub__(self, other):
  180. """ Set subtraction. """
  181. return self.data - other
  182. @use_data
  183. def __isub__(self, other):
  184. """ In-place set subtraction. """
  185. self.data -= other
  186. return self
  187. @use_data
  188. def __and__(self, other):
  189. """ Set intersection. """
  190. return self.data & other
  191. @use_data
  192. def __iand__(self, other):
  193. """ In-place set intersection. """
  194. self.data &= other
  195. return self
  196. @use_data
  197. def __or__(self, other):
  198. """ Set union. """
  199. return self.data | other
  200. @use_data
  201. def __ior__(self, other):
  202. """ In-place set union. """
  203. self.data |= other
  204. return self
  205. @use_data
  206. def __xor__(self, other):
  207. """ Set exclusive intersection (XOR). """
  208. return self.data ^ other
  209. @use_data
  210. def __ixor__(self, other):
  211. """ In-place set exclusive intersection (XOR). """
  212. self.data ^= other
  213. return self
  214. def __contains__(self, item):
  215. """
  216. Whether the graph contains a triple.
  217. :rtype: boolean
  218. """
  219. return item in self.data
  220. def __iter__(self):
  221. """ Graph iterator. It iterates over the set triples. """
  222. return self.data.__iter__()
  223. # Slicing.
  224. def __getitem__(self, item):
  225. """
  226. Slicing function.
  227. It behaves similarly to `RDFLib graph slicing
  228. <https://rdflib.readthedocs.io/en/stable/utilities.html#slicing-graphs>`__
  229. """
  230. if isinstance(item, slice):
  231. s, p, o = item.start, item.stop, item.step
  232. return self._slice(s, p, o)
  233. else:
  234. raise TypeError(f'Wrong slice format: {item}.')
  235. cpdef void set(self, tuple trp) except *:
  236. """
  237. Set a single value for subject and predicate.
  238. Remove all triples matching ``s`` and ``p`` before adding ``s p o``.
  239. """
  240. self.remove_triples((trp[0], trp[1], None))
  241. if None in trp:
  242. raise ValueError(f'Invalid triple: {trp}')
  243. self.data.add(trp)
  244. cpdef void remove_triples(self, pattern) except *:
  245. """
  246. Remove triples by pattern.
  247. The pattern used is similar to :py:meth:`LmdbTripleStore.delete`.
  248. """
  249. s, p, o = pattern
  250. for match in self.lookup(s, p, o):
  251. logger.debug(f'Removing from graph: {match}.')
  252. self.data.remove(match)
  253. cpdef object as_rdflib(self):
  254. """
  255. Return the data set as an RDFLib Graph.
  256. :rtype: rdflib.Graph
  257. """
  258. gr = Graph()
  259. for trp in self.data:
  260. gr.add(trp)
  261. return gr
  262. cdef _slice(self, s, p, o):
  263. """
  264. Return terms filtered by other terms.
  265. This behaves like the rdflib.Graph slicing policy.
  266. """
  267. if s is None and p is None and o is None:
  268. return self.data
  269. elif s is None and p is None:
  270. return {(r[0], r[1]) for r in self.data if r[2] == o}
  271. elif s is None and o is None:
  272. return {(r[0], r[2]) for r in self.data if r[1] == p}
  273. elif p is None and o is None:
  274. return {(r[1], r[2]) for r in self.data if r[0] == s}
  275. elif s is None:
  276. return {r[0] for r in self.data if r[1] == p and r[2] == o}
  277. elif p is None:
  278. return {r[1] for r in self.data if r[0] == s and r[2] == o}
  279. elif o is None:
  280. return {r[2] for r in self.data if r[0] == s and r[1] == p}
  281. else:
  282. # all given
  283. return (s,p,o) in self.data
  284. cpdef lookup(self, s, p, o):
  285. """
  286. Look up triples by a pattern.
  287. """
  288. logger.debug(f'Looking up in graph: {s}, {p}, {o}.')
  289. if s is None and p is None and o is None:
  290. return self.data
  291. elif s is None and p is None:
  292. return {r for r in self.data if r[2] == o}
  293. elif s is None and o is None:
  294. return {r for r in self.data if r[1] == p}
  295. elif p is None and o is None:
  296. return {r for r in self.data if r[0] == s}
  297. elif s is None:
  298. return {r for r in self.data if r[1] == p and r[2] == o}
  299. elif p is None:
  300. return {r for r in self.data if r[0] == s and r[2] == o}
  301. elif o is None:
  302. return {r for r in self.data if r[0] == s and r[1] == p}
  303. else:
  304. # all given
  305. return (s,p,o) if (s, p, o) in self.data else set()
  306. cpdef set terms(self, str type):
  307. """
  308. Get all terms of a type: subject, predicate or object.
  309. :param str type: One of ``s``, ``p`` or ``o``.
  310. """
  311. i = 'spo'.index(type)
  312. return {r[i] for r in self.data}
  313. cdef class Imr(SimpleGraph):
  314. """
  315. In-memory resource data container.
  316. This is an extension of :py:class:`~SimpleGraph` that adds a subject URI to
  317. the data set and some convenience methods.
  318. An instance of this class can be converted to a ``rdflib.Resource``
  319. instance.
  320. Some set operations that produce a new object (``-``, ``|``, ``&``, ``^``)
  321. will create a new ``Imr`` instance with the same subject URI.
  322. """
  323. def __init__(self, str uri, *args, **kwargs):
  324. """
  325. Initialize the graph with pre-existing data or by looking up a store.
  326. Either ``data``, or ``lookup`` *and* ``store``, can be provide.
  327. ``lookup`` and ``store`` have precedence. If none of them is specified,
  328. an empty graph is initialized.
  329. :param rdflib.URIRef uri: The graph URI.
  330. This will serve as the subject for some queries.
  331. :param set data: Initial data as a set of 3-tuples of RDFLib terms.
  332. :param tuple lookup: tuple of a 3-tuple of lookup terms, and a context.
  333. E.g. ``((URIRef('urn:ns:a'), None, None), URIRef('urn:ns:ctx'))``.
  334. Any and all elements may be ``None``.
  335. :param lmdbStore store: the store to look data up.
  336. """
  337. super().__init__(*args, **kwargs)
  338. self.uri = uri
  339. @property
  340. def identifier(self):
  341. """
  342. IMR URI. For compatibility with RDFLib Resource.
  343. :rtype: string
  344. """
  345. return self.uri
  346. @property
  347. def graph(self):
  348. """
  349. Return a SimpleGraph with the same data.
  350. :rtype: SimpleGraph
  351. """
  352. return SimpleGraph(self.data)
  353. def __repr__(self):
  354. """
  355. String representation of an Imr.
  356. This includes the subject URI, number of triples contained and the
  357. memory address of the instance.
  358. """
  359. return (f'<{self.__class__.__name__} @{hex(id(self))} uri={self.uri}, '
  360. f'length={len(self.data)}>')
  361. @use_data
  362. def __sub__(self, other):
  363. """
  364. Set difference. This creates a new Imr with the same subject URI.
  365. """
  366. return self.__class__(uri=self.uri, data=self.data - other)
  367. @use_data
  368. def __and__(self, other):
  369. """
  370. Set intersection. This creates a new Imr with the same subject URI.
  371. """
  372. return self.__class__(uri=self.uri, data=self.data & other)
  373. @use_data
  374. def __or__(self, other):
  375. """
  376. Set union. This creates a new Imr with the same subject URI.
  377. """
  378. return self.__class__(uri=self.uri, data=self.data | other)
  379. @use_data
  380. def __xor__(self, other):
  381. """
  382. Set exclusive OR (XOR). This creates a new Imr with the same subject
  383. URI.
  384. """
  385. return self.__class__(uri=self.uri, data=self.data ^ other)
  386. def __getitem__(self, item):
  387. """
  388. Supports slicing notation.
  389. """
  390. if isinstance(item, slice):
  391. s, p, o = item.start, item.stop, item.step
  392. return self._slice(s, p, o)
  393. elif isinstance(item, Node):
  394. # If a Node is given, return all values for that predicate.
  395. return {
  396. r[2] for r in self.data
  397. if r[0] == self.uri and r[1] == item}
  398. else:
  399. raise TypeError(f'Wrong slice format: {item}.')
  400. def value(self, p, strict=False):
  401. """
  402. Get an individual value.
  403. :param rdflib.termNode p: Predicate to search for.
  404. :param bool strict: If set to ``True`` the method raises an error if
  405. more than one value is found. If ``False`` (the default) only
  406. the first found result is returned.
  407. :rtype: rdflib.term.Node
  408. """
  409. values = self[p]
  410. if strict and len(values) > 1:
  411. raise RuntimeError('More than one value found for {}, {}.'.format(
  412. self.uri, p))
  413. for ret in values:
  414. return ret
  415. return None
  416. cpdef as_rdflib(self):
  417. """
  418. Return the IMR as a RDFLib Resource.
  419. :rtype: rdflib.Resource
  420. """
  421. gr = Graph()
  422. for trp in self.data:
  423. gr.add(trp)
  424. return gr.resource(identifier=self.uri)