aboutsummaryrefslogtreecommitdiffstats
path: root/bsfs
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2022-12-22 20:33:00 +0100
committerMatthias Baumgartner <dev@igsor.net>2022-12-22 20:33:00 +0100
commitca7ee6c59d2eb3f4ec4d16e392d12d946cd85e4d (patch)
treee6290053c00e06fda9e41ac0a602ff53d91a38ac /bsfs
parent73e39cb4967949025aefe874f401e27b0abb772c (diff)
downloadbsfs-ca7ee6c59d2eb3f4ec4d16e392d12d946cd85e4d.tar.gz
bsfs-ca7ee6c59d2eb3f4ec4d16e392d12d946cd85e4d.tar.bz2
bsfs-ca7ee6c59d2eb3f4ec4d16e392d12d946cd85e4d.zip
filter-ast based get interface in graph.
* Graph interface: Graph.get added * Node instance resolver so that Nodes can be used in a filter ast * AC interface: filter_read added to interface * upstream test adjustments of previous sparql store changes
Diffstat (limited to 'bsfs')
-rw-r--r--bsfs/graph/ac/base.py4
-rw-r--r--bsfs/graph/ac/null.py5
-rw-r--r--bsfs/graph/graph.py28
-rw-r--r--bsfs/graph/resolve.py161
4 files changed, 193 insertions, 5 deletions
diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py
index bc9aeb3..0703e2e 100644
--- a/bsfs/graph/ac/base.py
+++ b/bsfs/graph/ac/base.py
@@ -10,6 +10,7 @@ import typing
# bsfs imports
from bsfs import schema
+from bsfs.query import ast
from bsfs.triple_store import TripleStoreBase
from bsfs.utils import URI
@@ -67,5 +68,8 @@ class AccessControlBase(abc.ABC):
def createable(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]:
"""Return nodes that are allowed to be created."""
+ @abc.abstractmethod
+ def filter_read(self, node_type: schema.Node, query: ast.filter.FilterExpression) -> ast.filter.FilterExpression:
+ """Re-write a filter *query* to get (i.e., read) *node_type* nodes."""
## EOF ##
diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py
index 36838bd..12b4e87 100644
--- a/bsfs/graph/ac/null.py
+++ b/bsfs/graph/ac/null.py
@@ -10,6 +10,7 @@ import typing
# bsfs imports
from bsfs import schema
from bsfs.namespace import ns
+from bsfs.query import ast
from bsfs.utils import URI
# inner-module imports
@@ -49,4 +50,8 @@ class NullAC(base.AccessControlBase):
"""Return nodes that are allowed to be created."""
return guids
+ def filter_read(self, node_type: schema.Node, query: ast.filter.FilterExpression) -> ast.filter.FilterExpression:
+ """Re-write a filter *query* to get (i.e., read) *node_type* nodes."""
+ return query
+
## EOF ##
diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py
index 51fe75d..f030fed 100644
--- a/bsfs/graph/graph.py
+++ b/bsfs/graph/graph.py
@@ -9,13 +9,15 @@ import os
import typing
# bsfs imports
-from bsfs.query import ast
+from bsfs.query import ast, validate
from bsfs.schema import Schema
from bsfs.triple_store import TripleStoreBase
from bsfs.utils import URI, typename
# inner-module imports
+from . import ac
from . import nodes as _nodes
+from . import resolve
# exports
__all__: typing.Sequence[str] = (
@@ -44,6 +46,9 @@ class Graph():
def __init__(self, backend: TripleStoreBase, user: URI):
self._backend = backend
self._user = user
+ self._resolver = resolve.Filter(self._backend.schema)
+ self._validate = validate.Filter(self._backend.schema)
+ self._ac = ac.NullAC(self._backend, self._user)
# ensure Graph schema requirements
self.migrate(self._backend.schema)
@@ -85,6 +90,9 @@ class Graph():
# migrate schema in backend
# FIXME: consult access controls!
self._backend.schema = schema
+ # re-initialize members
+ self._resolver.schema = self.schema
+ self._validate.schema = self.schema
# return self
return self
@@ -108,11 +116,21 @@ class Graph():
*node_type*) once some data is assigned to them.
"""
- type_ = self.schema.node(node_type)
- return _nodes.Nodes(self._backend, self._user, type_, {guid})
+ return self.nodes(node_type, {guid})
- def get(self, node_type: URI, subject: ast.filter.FilterExpression) -> _nodes.Nodes:
+ def get(self, node_type: URI, query: ast.filter.FilterExpression) -> _nodes.Nodes: # FIXME: How about empty query?
"""Return a `Nodes` instance over all nodes of type *node_type* that match the *subject* query."""
- raise NotImplementedError()
+ # get node type
+ type_ = self.schema.node(node_type)
+ # resolve Nodes instances
+ query = self._resolver(type_, query)
+ # add access controls to query
+ query = self._ac.filter_read(type_, query)
+ # validate query
+ self._validate(type_, query)
+ # query the backend
+ guids = self._backend.get(type_, query) # no need to materialize
+ # return Nodes instance
+ return _nodes.Nodes(self._backend, self._user, type_, guids)
## EOF ##
diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py
new file mode 100644
index 0000000..feb0855
--- /dev/null
+++ b/bsfs/graph/resolve.py
@@ -0,0 +1,161 @@
+"""
+
+Part of the BlackStar filesystem (bsfs) module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import typing
+
+# bsfs imports
+from bsfs import schema as bsc
+from bsfs.query import ast
+from bsfs.utils import errors
+
+# inner-module imports
+from . import nodes
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'Filter',
+ )
+
+
+## code ##
+
+class Filter():
+ """Rewrites the query to replace `bsfs.graph.nodes.Nodes` instances with the respective URI.
+ Does only limited type checking and schema validation.
+ Use `bsfs.schema.validate.Filter` to do so.
+
+ Example:
+ input: Any(ns.bse.tag, Is(Nodes(...)))
+ output: Any(ns.bse.tag, Or(Is(...), Is(...), ...)))
+
+ >>> tags = graph.node(ns.bsfs.Tag, 'http://example.com/me/tag#1234')
+ >>> graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags)))
+
+ """
+
+ T_VERTEX = typing.Union[bsc.Node, bsc.Literal]
+
+ def __init__(self, schema):
+ self.schema = schema
+
+ def __call__(self, root_type: bsc.Node, node: ast.filter.FilterExpression):
+ return self._parse_filter_expression(root_type, node)
+
+ def _parse_filter_expression(
+ self,
+ type_: T_VERTEX,
+ node: ast.filter.FilterExpression,
+ ) -> ast.filter.FilterExpression:
+ """Route *node* to the handler of the respective FilterExpression subclass."""
+ if isinstance(node, ast.filter.Is):
+ return self._is(type_, node)
+ if isinstance(node, ast.filter.Not):
+ return self._not(type_, node)
+ if isinstance(node, ast.filter.Has):
+ return self._has(type_, node)
+ if isinstance(node, ast.filter.Any):
+ return self._any(type_, node)
+ if isinstance(node, ast.filter.All):
+ return self._all(type_, node)
+ if isinstance(node, ast.filter.And):
+ return self._and(type_, node)
+ if isinstance(node, ast.filter.Or):
+ return self._or(type_, node)
+ if isinstance(node, (ast.filter.Equals, ast.filter.Substring, \
+ ast.filter.StartsWith, ast.filter.EndsWith)):
+ return self._value(type_, node)
+ if isinstance(node, (ast.filter.LessThan, ast.filter.GreaterThan)):
+ return self._bounded(type_, node)
+ # invalid node
+ raise errors.BackendError(f'expected filter expression, found {node}')
+
+ def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> T_VERTEX:
+ """Route *node* to the handler of the respective PredicateExpression subclass."""
+ if isinstance(node, ast.filter.Predicate):
+ return self._predicate(node)
+ if isinstance(node, ast.filter.OneOf):
+ return self._one_of(node)
+ # invalid node
+ raise errors.BackendError(f'expected predicate expression, found {node}')
+
+ def _predicate(self, node: ast.filter.Predicate) -> T_VERTEX:
+ if not self.schema.has_predicate(node.predicate):
+ raise errors.ConsistencyError(f'predicate {node.predicate} is not in the schema')
+ pred = self.schema.predicate(node.predicate)
+ dom, rng = pred.domain, pred.range
+ if node.reverse:
+ dom, rng = rng, dom
+ return rng
+
+ def _one_of(self, node: ast.filter.OneOf) -> T_VERTEX:
+ # determine domain and range types
+ rng = None
+ for pred in node:
+ # parse child expression
+ subrng = self._parse_predicate_expression(pred)
+ # determine the next type
+ try:
+ if rng is None or subrng > rng: # pick most generic range
+ rng = subrng
+ except TypeError as err:
+ raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err
+ if rng is None:
+ raise errors.UnreachableError()
+ return rng
+
+ def _any(self, type_: T_VERTEX, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument
+ next_type = self._parse_predicate_expression(node.predicate)
+ return ast.filter.Any(node.predicate, self._parse_filter_expression(next_type, node.expr))
+
+ def _all(self, type_: T_VERTEX, node: ast.filter.All) -> ast.filter.All: # pylint: disable=unused-argument
+ next_type = self._parse_predicate_expression(node.predicate)
+ return ast.filter.All(node.predicate, self._parse_filter_expression(next_type, node.expr))
+
+ def _and(self, type_: T_VERTEX, node: ast.filter.And) -> ast.filter.And:
+ return ast.filter.And({self._parse_filter_expression(type_, expr) for expr in node})
+
+ def _or(self, type_: T_VERTEX, node: ast.filter.Or) -> ast.filter.Or:
+ return ast.filter.Or({self._parse_filter_expression(type_, expr) for expr in node})
+
+ def _not(self, type_: T_VERTEX, node: ast.filter.Not) -> ast.filter.Not:
+ return ast.filter.Not(self._parse_filter_expression(type_, node.expr))
+
+ def _has(self, type_: T_VERTEX, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument
+ return node
+
+ def _value(self, type_: T_VERTEX, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument
+ return node
+
+ def _bounded(self, type_: T_VERTEX, node: ast.filter._Bounded) -> ast.filter._Bounded: # pylint: disable=unused-argument
+ return node
+
+ def _is(self, type_: T_VERTEX, node: ast.filter.Is) -> typing.Union[ast.filter.Or, ast.filter.Is]:
+ # check if action is needed
+ if not isinstance(node.value, nodes.Nodes):
+ return node
+ # check schema consistency
+ if node.value.node_type not in self.schema.nodes():
+ raise errors.ConsistencyError(f'node {node.value.node_type} is not in the schema')
+ # check type compatibility
+ if not isinstance(type_, bsc.Node):
+ raise errors.ConsistencyError(f'expected a node, found {type_}')
+ if not node.value.node_type <= type_:
+ raise errors.ConsistencyError(f'expected type {type_} or subtype thereof, found {node.value.node_type}')
+ # NOTE: We assume that the node type is checked when writing to the backend.
+ # Links to any of the guids can therefore only exist if the type matches.
+ # Hence, we don't add a type check/constrain here.
+ return ast.filter.Or(ast.filter.Is(guid) for guid in node.value.guids)
+ # optimized code, removing unnecessary ast.filter.Or
+ #guids = set(node.value.guids)
+ #if len(guids) == 0:
+ # raise errors.BackendError(f'')
+ #if len(guids) == 1:
+ # return ast.filter.Nodeid(next(iter(guids)))
+ #return ast.filter.Or(ast.filter.Is(guid) for guid in guids)
+
+
+## EOF ##