Source code for djangordf.reasoning.base
"""Reasoner ABC plus the public ``materialize`` entry point."""
from typing import List, Optional, Sequence, Union
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
from rdflib import URIRef
from ..conf import get_backend
_DEFAULT_INTERNAL_GRAPH = URIRef("urn:djangordf:default")
def _resolve_graph(value, fallback) -> URIRef:
if value is None:
return fallback
return URIRef(value)
def _backend_triple_count(backend, graph_iri: URIRef) -> int:
sparql = (
"SELECT (COUNT(*) AS ?n) WHERE { "
f"GRAPH <{graph_iri}> {{ ?s ?p ?o }} }}"
)
result = backend.query(sparql)
for row in result:
return int(row[0])
return 0
[docs]
class Reasoner:
"""Base class for reasoners.
Subclasses override :meth:`rules` (returning a sequence of
SPARQL ``INSERT-WHERE`` strings) to declare their inference
rules. The base :meth:`materialize` runs them in a fixpoint
loop. Override :meth:`materialize` directly for non-SPARQL
reasoners (see :class:`OWLRLReasoner`).
"""
[docs]
def rules(self, source_graph: URIRef, target_graph: URIRef) -> Sequence[str]:
"""Return the SPARQL ``INSERT-WHERE`` strings for one pass."""
raise NotImplementedError
[docs]
def materialize(
self,
backend=None,
*,
source_graph=None,
target_graph=None,
max_iterations: int = 50,
) -> int:
"""Run rules to a fixpoint. Returns the number of newly
inferred triples added to ``target_graph``."""
backend = backend if backend is not None else get_backend()
src = _resolve_graph(source_graph, _DEFAULT_INTERNAL_GRAPH)
tgt = _resolve_graph(target_graph, src)
before = _backend_triple_count(backend, tgt)
previous = before
for _ in range(max_iterations):
for sparql in self.rules(src, tgt):
backend.update(sparql)
current = _backend_triple_count(backend, tgt)
if current == previous:
break
previous = current
return _backend_triple_count(backend, tgt) - before
[docs]
class CompositeReasoner(Reasoner):
"""Run several reasoners sequentially within one fixpoint envelope."""
def __init__(self, *reasoners: Reasoner):
if not reasoners:
raise ValueError("CompositeReasoner requires at least one reasoner")
self.reasoners: List[Reasoner] = list(reasoners)
[docs]
def rules(self, source_graph: URIRef, target_graph: URIRef) -> Sequence[str]:
composite: List[str] = []
for reasoner in self.reasoners:
composite.extend(reasoner.rules(source_graph, target_graph))
return composite
def _resolve_reasoner(reasoner) -> Reasoner:
if isinstance(reasoner, Reasoner):
return reasoner
if reasoner is None:
configured = getattr(settings, "DJANGORDF_REASONER", None)
if configured is None:
raise ImproperlyConfigured(
"No reasoner supplied and DJANGORDF_REASONER is not set."
)
reasoner = configured
if isinstance(reasoner, str):
cls = import_string(reasoner)
return cls()
if isinstance(reasoner, type) and issubclass(reasoner, Reasoner):
return reasoner()
raise TypeError(
f"Cannot resolve reasoner from {reasoner!r}; expected a Reasoner "
f"instance, class, or dotted path."
)
[docs]
def materialize(
reasoner: Optional[Union[str, Reasoner, type]] = None,
backend=None,
*,
source_graph=None,
target_graph=None,
max_iterations: int = 50,
) -> int:
"""Programmatic entry point. Resolves ``reasoner`` from the
argument (instance / class / dotted path) or
``settings.DJANGORDF_REASONER`` and runs ``materialize`` on it."""
return _resolve_reasoner(reasoner).materialize(
backend=backend,
source_graph=source_graph,
target_graph=target_graph,
max_iterations=max_iterations,
)