From 791918039979d0743fd2ea4b9a5e74593ff96fd0 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 19 Dec 2022 13:32:34 +0100 Subject: query ast file structures and essential interfaces --- bsfs/graph/graph.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index b7b9f1c..10e5904 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -9,6 +9,7 @@ import os import typing # bsfs imports +from bsfs.query import ast from bsfs.schema import Schema from bsfs.triple_store import TripleStoreBase from bsfs.utils import URI, typename @@ -110,4 +111,8 @@ class Graph(): type_ = self.schema.node(node_type) return _nodes.Nodes(self._backend, self._user, type_, {guid}) + def get(self, node_type: URI, subject: ast.filter.FilterExpression) -> Nodes: + """Return a `Nodes` instance over all nodes of type *node_type* that match the *subject* query.""" + raise NotImplementedError() + ## EOF ## -- cgit v1.2.3 From a0f2308adcb226d28de3355bc7115a6d9b669462 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 19 Dec 2022 13:40:02 +0100 Subject: import fixes --- bsfs/graph/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index 10e5904..51fe75d 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -111,7 +111,7 @@ class Graph(): type_ = self.schema.node(node_type) return _nodes.Nodes(self._backend, self._user, type_, {guid}) - def get(self, node_type: URI, subject: ast.filter.FilterExpression) -> Nodes: + def get(self, node_type: URI, subject: ast.filter.FilterExpression) -> _nodes.Nodes: """Return a `Nodes` instance over all nodes of type *node_type* that match the *subject* query.""" raise NotImplementedError() -- cgit v1.2.3 From ca7ee6c59d2eb3f4ec4d16e392d12d946cd85e4d Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 22 Dec 2022 20:33:00 +0100 Subject: 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 --- bsfs/graph/ac/base.py | 4 ++ bsfs/graph/ac/null.py | 5 ++ bsfs/graph/graph.py | 28 +++++++-- bsfs/graph/resolve.py | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 bsfs/graph/resolve.py (limited to 'bsfs/graph') 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 ## -- cgit v1.2.3 From 7f5a2920ef311b2077300714d7700313077a0bf6 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 22 Dec 2022 20:35:38 +0100 Subject: cosmetic changes --- bsfs/graph/nodes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index c417a0e..5a93f77 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -53,7 +53,7 @@ class Nodes(): self._user = user self._node_type = node_type self._guids = set(guids) - self.__ac = ac.NullAC(self._backend, self._user) + self._ac = ac.NullAC(self._backend, self._user) def __eq__(self, other: typing.Any) -> bool: return isinstance(other, Nodes) \ @@ -135,7 +135,7 @@ class Nodes(): # FIXME: Needed? Could be integrated into other AC methods (by passing the predicate!) # This could allow more fine-grained predicate control (e.g. based on ownership) # rather than a global approach like this. - if self.__ac.is_protected_predicate(pred): + if self._ac.is_protected_predicate(pred): raise errors.PermissionDeniedError(pred) # set operation affects all nodes (if possible) @@ -149,7 +149,7 @@ class Nodes(): # check write permissions on existing nodes # As long as the user has write permissions, we don't restrict # the creation or modification of literal values. - guids = set(self.__ac.write_literal(node_type, guids)) + guids = set(self._ac.write_literal(node_type, guids)) # insert literals # TODO: Support passing iterators as values for non-unique predicates @@ -172,14 +172,14 @@ class Nodes(): # Link permissions cover adding and removing links on the source node. # Specifically, link permissions also allow to remove links to other # nodes if needed (e.g. for unique predicates). - guids = set(self.__ac.link_from_node(node_type, guids)) + guids = set(self._ac.link_from_node(node_type, guids)) # get link targets targets = set(value.guids) # ensure existence of value nodes; create nodes if need be targets = set(self._ensure_nodes(value.node_type, targets)) # check link permissions on target nodes - targets = set(self.__ac.link_to_node(value.node_type, targets)) + targets = set(self._ac.link_to_node(value.node_type, targets)) # insert node links self._backend.set( @@ -203,14 +203,14 @@ class Nodes(): # create nodes if need be if len(missing) > 0: # check which missing nodes can be created - missing = set(self.__ac.createable(node_type, missing)) + missing = set(self._ac.createable(node_type, missing)) # create nodes self._backend.create(node_type, missing) # add bookkeeping triples self._backend.set(node_type, missing, self._backend.schema.predicate(ns.bsm.t_created), [time.time()]) # add permission triples - self.__ac.create(node_type, missing) + self._ac.create(node_type, missing) # return available nodes return existing | missing -- cgit v1.2.3 From 3940cb3c79937a431ba2ae3b57fd0c6c2ccfff33 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:12:43 +0100 Subject: use Vertex in type annotations --- bsfs/graph/resolve.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index feb0855..e398a5e 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -37,8 +37,6 @@ class Filter(): """ - T_VERTEX = typing.Union[bsc.Node, bsc.Literal] - def __init__(self, schema): self.schema = schema @@ -47,7 +45,7 @@ class Filter(): def _parse_filter_expression( self, - type_: T_VERTEX, + type_: bsc.Vertex, node: ast.filter.FilterExpression, ) -> ast.filter.FilterExpression: """Route *node* to the handler of the respective FilterExpression subclass.""" @@ -73,7 +71,7 @@ class Filter(): # invalid node raise errors.BackendError(f'expected filter expression, found {node}') - def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> T_VERTEX: + def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> bsc.Vertex: """Route *node* to the handler of the respective PredicateExpression subclass.""" if isinstance(node, ast.filter.Predicate): return self._predicate(node) @@ -82,7 +80,7 @@ class Filter(): # invalid node raise errors.BackendError(f'expected predicate expression, found {node}') - def _predicate(self, node: ast.filter.Predicate) -> T_VERTEX: + def _predicate(self, node: ast.filter.Predicate) -> bsc.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) @@ -91,7 +89,7 @@ class Filter(): dom, rng = rng, dom return rng - def _one_of(self, node: ast.filter.OneOf) -> T_VERTEX: + def _one_of(self, node: ast.filter.OneOf) -> bsc.Vertex: # determine domain and range types rng = None for pred in node: @@ -107,33 +105,33 @@ class Filter(): raise errors.UnreachableError() return rng - def _any(self, type_: T_VERTEX, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument + def _any(self, type_: bsc.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 + def _all(self, type_: bsc.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: + def _and(self, type_: bsc.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: + def _or(self, type_: bsc.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: + def _not(self, type_: bsc.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 + def _has(self, type_: bsc.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 + def _value(self, type_: bsc.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 + def _bounded(self, type_: bsc.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]: + def _is(self, type_: bsc.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 -- cgit v1.2.3 From 6b3e32b29799a8143e8ce9d20c5f27e3e166b9bb Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:17:07 +0100 Subject: changed path to from_string in clients --- bsfs/graph/graph.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index f030fed..2210755 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -10,7 +10,7 @@ import typing # bsfs imports from bsfs.query import ast, validate -from bsfs.schema import Schema +from bsfs import schema as bsc from bsfs.triple_store import TripleStoreBase from bsfs.utils import URI, typename @@ -67,11 +67,11 @@ class Graph(): return f'{typename(self)}({str(self._backend)}, {self._user})' @property - def schema(self) -> Schema: + def schema(self) -> bsc.Schema: """Return the store's local schema.""" return self._backend.schema - def migrate(self, schema: Schema, append: bool = True) -> 'Graph': + def migrate(self, schema: bsc.Schema, append: bool = True) -> 'Graph': """Migrate the current schema to a new *schema*. Appends to the current schema by default; control this via *append*. @@ -79,14 +79,14 @@ class Graph(): """ # check args - if not isinstance(schema, Schema): + if not isinstance(schema, bsc.Schema): raise TypeError(schema) # append to current schema if append: schema = schema + self._backend.schema # add Graph schema requirements with open(os.path.join(os.path.dirname(__file__), 'schema.nt'), mode='rt', encoding='UTF-8') as ifile: - schema = schema + Schema.from_string(ifile.read()) + schema = schema + bsc.from_string(ifile.read()) # migrate schema in backend # FIXME: consult access controls! self._backend.schema = schema -- cgit v1.2.3 From 7e7284d5fc01c0a081aa79d67736f51069864a7d Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:22:59 +0100 Subject: adapt to non-optional range in query checks --- bsfs/graph/resolve.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index e398a5e..9b5f631 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -101,8 +101,8 @@ class Filter(): 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() + if not isinstance(rng, (bsc.Node, bsc.Literal)): + raise errors.BackendError(f'the range of node {node} is undefined') return rng def _any(self, type_: bsc.Vertex, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument -- cgit v1.2.3 From b0ff4ed674ad78bf113c3cc0c2ccd187ccb91048 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 12 Jan 2023 10:26:30 +0100 Subject: number literal adaptions --- bsfs/graph/schema.nt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/schema.nt b/bsfs/graph/schema.nt index 8612681..f619746 100644 --- a/bsfs/graph/schema.nt +++ b/bsfs/graph/schema.nt @@ -8,7 +8,8 @@ prefix bsfs: prefix bsm: # literals -xsd:integer rdfs:subClassOf bsfs:Literal . +bsfs:Number rdfs:subClassOf bsfs:Literal . +xsd:integer rdfs:subClassOf bsfs:Number . # predicates bsm:t_created rdfs:subClassOf bsfs:Predicate ; -- cgit v1.2.3 From 60257ed3c2aa6ea2891f362a691bde9d7ef17831 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 13 Jan 2023 12:22:34 +0100 Subject: schema type comparison across classes --- bsfs/graph/resolve.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index 9b5f631..b671204 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -96,11 +96,11 @@ class Filter(): # 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 or subrng > rng: # pick most generic range + rng = subrng + # check range consistency + if not subrng <= rng and not subrng >= rng: + raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') if not isinstance(rng, (bsc.Node, bsc.Literal)): raise errors.BackendError(f'the range of node {node} is undefined') return rng -- cgit v1.2.3 From 80a97bfa9f22d0d6dd25928fe1754a3a0d1de78a Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 15 Jan 2023 21:00:12 +0100 Subject: Distance filter ast node --- bsfs/graph/resolve.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'bsfs/graph') diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index b671204..00b778b 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -63,6 +63,8 @@ class Filter(): return self._and(type_, node) if isinstance(node, ast.filter.Or): return self._or(type_, node) + if isinstance(node, ast.filter.Distance): + return self._distance(type_, node) if isinstance(node, (ast.filter.Equals, ast.filter.Substring, \ ast.filter.StartsWith, ast.filter.EndsWith)): return self._value(type_, node) @@ -125,6 +127,9 @@ class Filter(): def _has(self, type_: bsc.Vertex, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument return node + def _distance(self, type_: bsc.Vertex, node: ast.filter.Distance): # pylint: disable=unused-argument + return node + def _value(self, type_: bsc.Vertex, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument return node -- cgit v1.2.3 From c196d2ce73d8351a18c19bcddd4b06d224e644fc Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 21 Jan 2023 18:27:22 +0100 Subject: Fetch in graph including results view --- bsfs/graph/ac/base.py | 4 ++ bsfs/graph/ac/null.py | 4 ++ bsfs/graph/nodes.py | 137 ++++++++++++++++++++++++++++++++++++++++++++++---- bsfs/graph/result.py | 112 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 bsfs/graph/result.py (limited to 'bsfs/graph') diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py index 0703e2e..79b09e5 100644 --- a/bsfs/graph/ac/base.py +++ b/bsfs/graph/ac/base.py @@ -72,4 +72,8 @@ class AccessControlBase(abc.ABC): 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.""" + @abc.abstractmethod + def fetch_read(self, node_type: schema.Node, query: ast.fetch.FetchExpression) -> ast.fetch.FetchExpression: + """Re-write a fetch *query* to get (i.e, read) values for *node_type* nodes.""" + ## EOF ## diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py index 12b4e87..6a923a5 100644 --- a/bsfs/graph/ac/null.py +++ b/bsfs/graph/ac/null.py @@ -54,4 +54,8 @@ class NullAC(base.AccessControlBase): """Re-write a filter *query* to get (i.e., read) *node_type* nodes.""" return query + def fetch_read(self, node_type: schema.Node, query: ast.fetch.FetchExpression) -> ast.fetch.FetchExpression: + """Re-write a fetch *query* to get (i.e, read) values for *node_type* nodes.""" + return query + ## EOF ## diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 5a93f77..a4ba45f 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -5,17 +5,20 @@ A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # imports +from collections import abc import time import typing # bsfs imports -from bsfs import schema as _schema +from bsfs import schema as bsc from bsfs.namespace import ns +from bsfs.query import ast, validate from bsfs.triple_store import TripleStoreBase from bsfs.utils import errors, URI, typename # inner-module imports from . import ac +from . import result # exports __all__: typing.Sequence[str] = ( @@ -37,7 +40,7 @@ class Nodes(): _user: URI # node type. - _node_type: _schema.Node + _node_type: bsc.Node # guids of nodes. Can be empty. _guids: typing.Set[URI] @@ -46,13 +49,16 @@ class Nodes(): self, backend: TripleStoreBase, user: URI, - node_type: _schema.Node, + node_type: bsc.Node, guids: typing.Iterable[URI], ): + # set main members self._backend = backend self._user = user self._node_type = node_type self._guids = set(guids) + # create helper instances + # FIXME: Assumes that the schema does not change while the instance is in use! self._ac = ac.NullAC(self._backend, self._user) def __eq__(self, other: typing.Any) -> bool: @@ -72,7 +78,7 @@ class Nodes(): return f'{typename(self)}({self._node_type}, {self._guids})' @property - def node_type(self) -> _schema.Node: + def node_type(self) -> bsc.Node: """Return the node's type.""" return self._node_type @@ -83,7 +89,7 @@ class Nodes(): def set( self, - pred: URI, # FIXME: URI or _schema.Predicate? + pred: URI, # FIXME: URI or bsc.Predicate? value: typing.Any, ) -> 'Nodes': """Set predicate *pred* to *value*.""" @@ -91,7 +97,7 @@ class Nodes(): def set_from_iterable( self, - predicate_values: typing.Iterable[typing.Tuple[URI, typing.Any]], # FIXME: URI or _schema.Predicate? + predicate_values: typing.Iterable[typing.Tuple[URI, typing.Any]], # FIXME: URI or bsc.Predicate? ) -> 'Nodes': """Set mutliple predicate-value pairs at once.""" # TODO: Could group predicate_values by predicate to gain some efficiency @@ -120,6 +126,119 @@ class Nodes(): return self + def get( + self, + *paths: typing.Union[URI, typing.Iterable[URI]], + view: typing.Union[typing.Type[list], typing.Type[dict]] = dict, + **view_kwargs, + ) -> typing.Any: + """Get values or nodes at *paths*. + Return an iterator (view=list) or a dict (view=dict) over the results. + """ + # check args + if len(paths) == 0: + raise AttributeError('expected at least one path, found none') + if view not in (dict, list): + raise ValueError(f'expected dict or list, found {view}') + # process paths: create fetch ast, build name mapping, and find unique paths + schema = self._backend.schema + statements = set() + name2path = {} + unique_paths = set() # paths that result in a single (unique) value + normpath: typing.Tuple[URI, ...] + for idx, path in enumerate(paths): + # normalize path + if isinstance(path, str): + normpath = (URI(path), ) + elif isinstance(path, abc.Iterable): + if not all(isinstance(step, str) for step in path): + raise TypeError(path) + normpath = tuple(URI(step) for step in path) + else: + raise TypeError(path) + # check path's schema consistency + if not all(schema.has_predicate(pred) for pred in normpath): + raise errors.ConsistencyError(f'path is not fully covered by the schema: {path}') + # check path's uniqueness + if all(schema.predicate(pred).unique for pred in normpath): + unique_paths.add(path) + # fetch tail predicate + tail = schema.predicate(normpath[-1]) + # determine tail ast node type + factory = ast.fetch.Node if isinstance(tail.range, bsc.Node) else ast.fetch.Value + # assign name + name = f'fetch{idx}' + name2path[name] = (path, tail) + # create tail ast node + curr: ast.fetch.FetchExpression = factory(tail.uri, name) + # walk towards front + hop: URI + for hop in normpath[-2::-1]: + curr = ast.fetch.Fetch(hop, curr) + # add to fetch query + statements.add(curr) + # aggregate fetch statements + if len(statements) == 1: + fetch = next(iter(statements)) + else: + fetch = ast.fetch.All(*statements) + # add access controls to fetch + fetch = self._ac.fetch_read(self.node_type, fetch) + + # compose filter ast + filter = ast.filter.IsIn(self.guids) # pylint: disable=redefined-builtin + # add access controls to filter + filter = self._ac.filter_read(self.node_type, filter) + + # validate queries + validate.Filter(self._backend.schema)(self.node_type, filter) + validate.Fetch(self._backend.schema)(self.node_type, fetch) + + # process results, convert if need be + def triple_iter(): + # query the backend + triples = self._backend.fetch(self.node_type, filter, fetch) + # process triples + for root, name, raw in triples: + # get node + node = Nodes(self._backend, self._user, self.node_type, {root}) + # get path + path, tail = name2path[name] + # covert raw to value + if isinstance(tail.range, bsc.Node): + value = Nodes(self._backend, self._user, tail.range, {raw}) + else: + value = raw + # emit triple + yield node, path, value + + # simplify by default + view_kwargs['node'] = view_kwargs.get('node', len(self._guids) == 1) + view_kwargs['path'] = view_kwargs.get('path', len(paths) == 1) + view_kwargs['value'] = view_kwargs.get('value', True) + + # return results view + if view == list: + return result.to_list_view( + triple_iter(), + # aggregation args + **view_kwargs, + ) + + if view == dict: + return result.to_dict_view( + triple_iter(), + # context + len(self._guids) == 1, + len(paths) == 1, + unique_paths, + # aggregation args + **view_kwargs, + ) + + raise errors.UnreachableError() # view was already checked + + def __set(self, predicate: URI, value: typing.Any): """ """ @@ -145,7 +264,7 @@ class Nodes(): guids = set(self._ensure_nodes(node_type, guids)) # check value - if isinstance(pred.range, _schema.Literal): + if isinstance(pred.range, bsc.Literal): # check write permissions on existing nodes # As long as the user has write permissions, we don't restrict # the creation or modification of literal values. @@ -160,7 +279,7 @@ class Nodes(): [value], ) - elif isinstance(pred.range, _schema.Node): + elif isinstance(pred.range, bsc.Node): # check value type if not isinstance(value, Nodes): raise TypeError(value) @@ -192,7 +311,7 @@ class Nodes(): else: raise errors.UnreachableError() - def _ensure_nodes(self, node_type: _schema.Node, guids: typing.Iterable[URI]): + def _ensure_nodes(self, node_type: bsc.Node, guids: typing.Iterable[URI]): """ """ # check node existence diff --git a/bsfs/graph/result.py b/bsfs/graph/result.py new file mode 100644 index 0000000..3009801 --- /dev/null +++ b/bsfs/graph/result.py @@ -0,0 +1,112 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +from collections import defaultdict +import typing + +# bsfs imports +from bsfs.utils import URI + +# exports +__all__: typing.Sequence[str] = ( + 'to_list_view', + 'to_dict_view', + ) + + +## code ## + +def to_list_view( + triples, + # aggregators + node: bool, + path: bool, + value: bool, # pylint: disable=unused-argument + ): + """Return an iterator over results. + + Dependent on the *node*, *path*, and *value* flags, + the respective component is omitted. + + """ + if node and path: + return iter(val for _, _, val in triples) + if node: + return iter((pred, val) for _, pred, val in triples) + if path: + return iter((subj, val) for subj, _, val in triples) + return iter((subj, pred, val) for subj, pred, val in triples) + + +def to_dict_view( + triples, + # context + one_node: bool, + one_path: bool, + unique_paths: typing.Set[typing.Union[URI, typing.Iterable[URI]]], + # aggregators + node: bool, + path: bool, + value: bool, + ) -> typing.Any: + """Return a dict of results. + + Note that triples are materialized to create this view. + + The returned structure depends on the *node*, *path*, and *value* flags. + If all flags are set to False, returns a dict(node -> dict(path -> set(values))). + Setting a flag to true omits or simplifies the respective component (if possible). + + """ + # NOTE: To create a dict, we need to materialize or make further assumptions + # (e.g., sorted in a specific order). + + data: typing.Any # disable type checks on data since it's very flexibly typed. + + # FIXME: type of data can be overwritten later on (if value) + + if node and path: + data = set() + elif node ^ path: + data = defaultdict(set) + else: + data = defaultdict(lambda: defaultdict(set)) + + for subj, pred, val in triples: + unique = pred in unique_paths + if node and path: + if value and unique and one_node and one_path: + return val + data.add(val) + elif node: + # remove node from result, group by predicate + if value and unique and one_node: + data[pred] = val + else: + data[pred].add(val) + elif path: + # remove predicate from result, group by node + if value and unique and one_path: + data[subj] = val + else: + data[subj].add(val) + else: + if value and unique: + data[subj][pred] = val + else: + data[subj][pred].add(val) + + # FIXME: Combine multiple Nodes instances into one? + + # convert defaultdict to ordinary dict + if node and path: + return data + if node ^ path: + return dict(data) + return {key: dict(val) for key, val in data.items()} + +## EOF ## -- cgit v1.2.3 From 9310610a7edf4dcbb934aedcecff1d11348197bb Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 21 Jan 2023 22:32:33 +0100 Subject: nodes predicate walk sugar --- bsfs/graph/nodes.py | 15 ++++++- bsfs/graph/walk.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 bsfs/graph/walk.py (limited to 'bsfs/graph') diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index a4ba45f..18ab30d 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -19,6 +19,7 @@ from bsfs.utils import errors, URI, typename # inner-module imports from . import ac from . import result +from . import walk # exports __all__: typing.Sequence[str] = ( @@ -87,6 +88,18 @@ class Nodes(): """Return all node guids.""" return iter(self._guids) + @property + def schema(self) -> bsc.Schema: + """Return the store's local schema.""" + return self._backend.schema + + def __getattr__(self, name: str): + try: + return super().__getattr__(name) # type: ignore [misc] # parent has no getattr + except AttributeError: + pass + return walk.Walk(self, walk.Walk.step(self.schema, self.node_type, name)) + def set( self, pred: URI, # FIXME: URI or bsc.Predicate? @@ -141,7 +154,7 @@ class Nodes(): if view not in (dict, list): raise ValueError(f'expected dict or list, found {view}') # process paths: create fetch ast, build name mapping, and find unique paths - schema = self._backend.schema + schema = self.schema statements = set() name2path = {} unique_paths = set() # paths that result in a single (unique) value diff --git a/bsfs/graph/walk.py b/bsfs/graph/walk.py new file mode 100644 index 0000000..63ef5e9 --- /dev/null +++ b/bsfs/graph/walk.py @@ -0,0 +1,120 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +from collections import abc +import typing + +# bsfs imports +from bsfs import schema as bsc + +# inner-module imports +# NOTE: circular import! OK as long as only used for type annotations. +from . import nodes # pylint: disable=cyclic-import + +# exports +__all__: typing.Sequence[str] = ( + 'Walk', + ) + + +## code ## + +class Walk(abc.Hashable, abc.Callable): # type: ignore [misc] # invalid base class (Callable) + """Syntactic sugar for `Nodes` to build and act on predicate paths via members.""" + + # Link to Nodes instance. + _root: 'nodes.Nodes' + + # Current predicate path. + _path: typing.Tuple[bsc.Predicate, ...] + + def __init__( + self, + root: 'nodes.Nodes', + path: typing.Sequence[bsc.Predicate], + ): + self._root = root + self._path = tuple(path) + + @property + def tail(self): + """Return the node type at the end of the path.""" + return self._path[-1].range + + + ## comparison + + def __hash__(self) -> int: + """Return an integer hash that identifies the instance.""" + return hash((type(self), self._root, self._path)) + + def __eq__(self, other) -> bool: + """Compare against *other* backend.""" + return isinstance(other, type(self)) \ + and self._root == other._root \ + and self._path == other._path + + + ## representation + + def __repr__(self) -> str: + """Return a formal string representation.""" + path = ', '.join(pred.uri for pred in self._path) + return f'Walk({self._root.node_type.uri}, ({path}))' + + def __str__(self) -> str: + """Return an informal string representation.""" + path = ', '.join(pred.uri for pred in self._path) + return f'Walk(@{self._root.node_type.uri}: {path})' + + + ## walk + + @staticmethod + def step( + schema: bsc.Schema, + node: bsc.Node, + name: str, + ) -> typing.Tuple[bsc.Predicate]: + """Get an predicate at *node* whose fragment matches *name*.""" + predicates = tuple( + pred + for pred + in schema.predicates_at(node) + if pred.uri.get('fragment', None) == name + ) + if len(predicates) == 0: # no fragment found for name + raise ValueError(f'no available predicate matches {name}') + if len(predicates) > 1: # ambiguous name + raise ValueError(f'{name} matches multiple predicates') + # append predicate to walk + return predicates # type: ignore [return-value] # size is one + + def __getattr__(self, name: str) -> 'Walk': + """Alias for `Walk.step(name)`.""" + try: + return super().__getattr__(name) + except AttributeError: + pass + # get predicate + pred = self.step(self._root.schema, self.tail, name) + # append predicate to walk + return Walk(self._root, self._path + pred) + + + ## get paths ## + + def get(self, **kwargs) -> typing.Any: + """Alias for `Nodes.get(..)`.""" + return self._root.get(tuple(pred.uri for pred in self._path), **kwargs) + + def __call__(self, **kwargs) -> typing.Any: # pylint: disable=arguments-differ + """Alias for `Walk.get(...)`.""" + return self.get(**kwargs) + + +## EOF ## -- cgit v1.2.3 From 04bb201c6162e81dbdefcb1cff9595180fa66917 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 21 Jan 2023 22:34:49 +0100 Subject: minor notes --- bsfs/graph/result.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'bsfs/graph') diff --git a/bsfs/graph/result.py b/bsfs/graph/result.py index 3009801..688929b 100644 --- a/bsfs/graph/result.py +++ b/bsfs/graph/result.py @@ -20,6 +20,11 @@ __all__: typing.Sequence[str] = ( ## code ## +# FIXME: node, path, value seem counter-intuitive: +# node.get(..., node=True) removes the node part. +# wouldn't it make more sense if node=True keeps the node part +# and node=False drops it? + def to_list_view( triples, # aggregators -- cgit v1.2.3 From 1392951dfc82af05e7a5999baa1c0a4fc72083b8 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 21 Jan 2023 23:03:50 +0100 Subject: Nodes magic methods for convenience --- bsfs/graph/nodes.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) (limited to 'bsfs/graph') diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 18ab30d..85e5fdb 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -93,6 +93,57 @@ class Nodes(): """Return the store's local schema.""" return self._backend.schema + def __add__(self, other: typing.Any) -> 'Nodes': + """Concatenate guids. Backend, user, and node type must match.""" + if not isinstance(other, type(self)): + return NotImplemented + if self._backend != other._backend: + raise ValueError(other) + if self._user != other._user: + raise ValueError(other) + if self.node_type != other.node_type: + raise ValueError(other) + return Nodes(self._backend, self._user, self.node_type, self._guids | other._guids) + + def __or__(self, other: typing.Any) -> 'Nodes': + """Concatenate guids. Backend, user, and node type must match.""" + return self.__add__(other) + + def __sub__(self, other: typing.Any) -> 'Nodes': + """Subtract guids. Backend, user, and node type must match.""" + if not isinstance(other, type(self)): + return NotImplemented + if self._backend != other._backend: + raise ValueError(other) + if self._user != other._user: + raise ValueError(other) + if self.node_type != other.node_type: + raise ValueError(other) + return Nodes(self._backend, self._user, self.node_type, self._guids - other._guids) + + def __and__(self, other: typing.Any) -> 'Nodes': + """Intersect guids. Backend, user, and node type must match.""" + if not isinstance(other, type(self)): + return NotImplemented + if self._backend != other._backend: + raise ValueError(other) + if self._user != other._user: + raise ValueError(other) + if self.node_type != other.node_type: + raise ValueError(other) + return Nodes(self._backend, self._user, self.node_type, self._guids & other._guids) + + def __len__(self) -> int: + """Return the number of guids.""" + return len(self._guids) + + def __iter__(self) -> typing.Iterator['Nodes']: + """Iterate over individual guids. Returns `Nodes` instances.""" + return iter( + Nodes(self._backend, self._user, self.node_type, {guid}) + for guid in self._guids + ) + def __getattr__(self, name: str): try: return super().__getattr__(name) # type: ignore [misc] # parent has no getattr -- cgit v1.2.3 From 72e0bd78dc9cc1d74c3061b028040b64c0efcf9f Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 30 Jan 2023 09:52:18 +0100 Subject: flip graph fethc result flags --- bsfs/graph/nodes.py | 6 +++--- bsfs/graph/result.py | 29 +++++++++++++++++------------ 2 files changed, 20 insertions(+), 15 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 85e5fdb..9990714 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -277,9 +277,9 @@ class Nodes(): yield node, path, value # simplify by default - view_kwargs['node'] = view_kwargs.get('node', len(self._guids) == 1) - view_kwargs['path'] = view_kwargs.get('path', len(paths) == 1) - view_kwargs['value'] = view_kwargs.get('value', True) + view_kwargs['node'] = view_kwargs.get('node', len(self._guids) != 1) + view_kwargs['path'] = view_kwargs.get('path', len(paths) != 1) + view_kwargs['value'] = view_kwargs.get('value', False) # return results view if view == list: diff --git a/bsfs/graph/result.py b/bsfs/graph/result.py index 688929b..00607f4 100644 --- a/bsfs/graph/result.py +++ b/bsfs/graph/result.py @@ -38,11 +38,11 @@ def to_list_view( the respective component is omitted. """ - if node and path: + if not node and not path: return iter(val for _, _, val in triples) - if node: + if not node: return iter((pred, val) for _, pred, val in triples) - if path: + if not path: return iter((subj, val) for subj, _, val in triples) return iter((subj, pred, val) for subj, pred, val in triples) @@ -57,6 +57,7 @@ def to_dict_view( node: bool, path: bool, value: bool, + default: typing.Optional[typing.Any] = None, ) -> typing.Any: """Return a dict of results. @@ -74,7 +75,7 @@ def to_dict_view( # FIXME: type of data can be overwritten later on (if value) - if node and path: + if not node and not path: data = set() elif node ^ path: data = defaultdict(set) @@ -83,24 +84,24 @@ def to_dict_view( for subj, pred, val in triples: unique = pred in unique_paths - if node and path: - if value and unique and one_node and one_path: + if not node and not path: + if not value and unique and one_node and one_path: return val data.add(val) - elif node: + elif not node: # remove node from result, group by predicate - if value and unique and one_node: + if not value and unique and one_node: data[pred] = val else: data[pred].add(val) - elif path: + elif not path: # remove predicate from result, group by node - if value and unique and one_path: + if not value and unique and one_path: data[subj] = val else: data[subj].add(val) else: - if value and unique: + if not value and unique: data[subj][pred] = val else: data[subj][pred].add(val) @@ -108,7 +109,11 @@ def to_dict_view( # FIXME: Combine multiple Nodes instances into one? # convert defaultdict to ordinary dict - if node and path: + if not node and not path and not value \ + and len(unique_paths) > 0 and one_node and one_path \ + and len(data) == 0: + return default + if not node and not path: return data if node ^ path: return dict(data) -- cgit v1.2.3 From f31a0d005785d474a37ec769c1f7f5e27aa08a57 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Wed, 8 Feb 2023 21:08:24 +0100 Subject: minor comments --- bsfs/graph/nodes.py | 2 ++ bsfs/graph/resolve.py | 1 + bsfs/graph/result.py | 2 ++ bsfs/graph/walk.py | 4 ++-- 4 files changed, 7 insertions(+), 2 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 9990714..bc71a32 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -199,6 +199,7 @@ class Nodes(): """Get values or nodes at *paths*. Return an iterator (view=list) or a dict (view=dict) over the results. """ + # FIXME: user-provided Fetch query AST? # check args if len(paths) == 0: raise AttributeError('expected at least one path, found none') @@ -345,6 +346,7 @@ class Nodes(): elif isinstance(pred.range, bsc.Node): # check value type + # FIXME: value could be a set of Nodes if not isinstance(value, Nodes): raise TypeError(value) # value's node_type must be a subclass of the predicate's range diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index 00b778b..4677401 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -41,6 +41,7 @@ class Filter(): self.schema = schema def __call__(self, root_type: bsc.Node, node: ast.filter.FilterExpression): + # FIXME: node can be None! return self._parse_filter_expression(root_type, node) def _parse_filter_expression( diff --git a/bsfs/graph/result.py b/bsfs/graph/result.py index 00607f4..31822f1 100644 --- a/bsfs/graph/result.py +++ b/bsfs/graph/result.py @@ -109,10 +109,12 @@ def to_dict_view( # FIXME: Combine multiple Nodes instances into one? # convert defaultdict to ordinary dict + # pylint: disable=too-many-boolean-expressions if not node and not path and not value \ and len(unique_paths) > 0 and one_node and one_path \ and len(data) == 0: return default + # pylint: enable=too-many-boolean-expressions if not node and not path: return data if node ^ path: diff --git a/bsfs/graph/walk.py b/bsfs/graph/walk.py index 63ef5e9..1b1cfa0 100644 --- a/bsfs/graph/walk.py +++ b/bsfs/graph/walk.py @@ -88,9 +88,9 @@ class Walk(abc.Hashable, abc.Callable): # type: ignore [misc] # invalid base cla if pred.uri.get('fragment', None) == name ) if len(predicates) == 0: # no fragment found for name - raise ValueError(f'no available predicate matches {name}') + raise ValueError(f'no available predicate matches {name}') # FIXME: Custom exception if len(predicates) > 1: # ambiguous name - raise ValueError(f'{name} matches multiple predicates') + raise ValueError(f'{name} matches multiple predicates') # FIXME: Custom exception # append predicate to walk return predicates # type: ignore [return-value] # size is one -- cgit v1.2.3 From c0218a8dffcdc3a7a5568f66bb959139fe514ad5 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Wed, 8 Feb 2023 21:14:36 +0100 Subject: Graph.all to retrieve all nodes --- bsfs/graph/graph.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index 2210755..df2e3a5 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -133,4 +133,12 @@ class Graph(): # return Nodes instance return _nodes.Nodes(self._backend, self._user, type_, guids) + def all(self, node_type: URI) -> _nodes.Nodes: + """Return all instances of type *node_type*.""" + # get node type + type_ = self.schema.node(node_type) + guids = self._backend.get(type_, None) # no need to materialize + return _nodes.Nodes(self._backend, self._user, type_, guids) + + ## EOF ## -- cgit v1.2.3 From f9eec185bf3d857c220e5d78de75ec6713437330 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Wed, 1 Mar 2023 12:39:42 +0100 Subject: Construct Graph and Nodes with AC instead of user --- bsfs/graph/ac/base.py | 16 +++++++++++++++- bsfs/graph/graph.py | 35 +++++++++++++++++++++++------------ bsfs/graph/nodes.py | 41 ++++++++++++++++++++--------------------- 3 files changed, 58 insertions(+), 34 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py index 79b09e5..0b9f988 100644 --- a/bsfs/graph/ac/base.py +++ b/bsfs/graph/ac/base.py @@ -12,7 +12,7 @@ import typing from bsfs import schema from bsfs.query import ast from bsfs.triple_store import TripleStoreBase -from bsfs.utils import URI +from bsfs.utils import URI, typename # exports __all__: typing.Sequence[str] = ( @@ -44,6 +44,20 @@ class AccessControlBase(abc.ABC): self._backend = backend self._user = URI(user) + def __str__(self) -> str: + return f'{typename(self)}({self._user})' + + def __repr__(self) -> str: + return f'{typename(self)}({self._user})' + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, type(self)) \ + and self._backend == other._backend \ + and self._user == other._user + + def __hash__(self) -> int: + return hash((type(self), self._backend, self._user)) + @abc.abstractmethod def is_protected_predicate(self, pred: schema.Predicate) -> bool: """Return True if a predicate cannot be modified manually.""" diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index df2e3a5..a74da01 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -40,31 +40,42 @@ class Graph(): # link to the triple storage backend. _backend: TripleStoreBase - # user uri. - _user: URI + # access controls. + _ac: ac.AccessControlBase - def __init__(self, backend: TripleStoreBase, user: URI): + # query resolver. + _resolver: resolve.Filter + + # query validator. + _validate: validate.Filter + + def __init__( + self, + backend: TripleStoreBase, + access_control: ac.AccessControlBase, + ): + # store members self._backend = backend - self._user = user + self._ac = access_control + # helper classes 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) def __hash__(self) -> int: - return hash((type(self), self._backend, self._user)) + return hash((type(self), self._backend, self._ac)) def __eq__(self, other) -> bool: return isinstance(other, type(self)) \ and self._backend == other._backend \ - and self._user == other._user + and self._ac == other._ac def __repr__(self) -> str: - return f'{typename(self)}(backend={repr(self._backend)}, user={self._user})' + return f'{typename(self)}({repr(self._backend)}, {self._ac})' def __str__(self) -> str: - return f'{typename(self)}({str(self._backend)}, {self._user})' + return f'{typename(self)}({str(self._backend)})' @property def schema(self) -> bsc.Schema: @@ -106,7 +117,7 @@ class Graph(): """ type_ = self.schema.node(node_type) # NOTE: Nodes constructor materializes guids. - return _nodes.Nodes(self._backend, self._user, type_, guids) + return _nodes.Nodes(self._backend, self._ac, type_, guids) def node(self, node_type: URI, guid: URI) -> _nodes.Nodes: """Return node *guid* of type *node_type* as a `bsfs.graph.Nodes` instance. @@ -131,14 +142,14 @@ class Graph(): # 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) + return _nodes.Nodes(self._backend, self._ac, type_, guids) def all(self, node_type: URI) -> _nodes.Nodes: """Return all instances of type *node_type*.""" # get node type type_ = self.schema.node(node_type) guids = self._backend.get(type_, None) # no need to materialize - return _nodes.Nodes(self._backend, self._user, type_, guids) + return _nodes.Nodes(self._backend, self._ac, type_, guids) ## EOF ## diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index bc71a32..91cbb5d 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -37,8 +37,8 @@ class Nodes(): # triple store backend. _backend: TripleStoreBase - # user uri. - _user: URI + # access controls. + _ac: ac.AccessControlBase # node type. _node_type: bsc.Node @@ -49,31 +49,30 @@ class Nodes(): def __init__( self, backend: TripleStoreBase, - user: URI, + access_control: ac.AccessControlBase, node_type: bsc.Node, guids: typing.Iterable[URI], ): # set main members self._backend = backend - self._user = user + self._ac = access_control self._node_type = node_type self._guids = set(guids) # create helper instances # FIXME: Assumes that the schema does not change while the instance is in use! - self._ac = ac.NullAC(self._backend, self._user) def __eq__(self, other: typing.Any) -> bool: return isinstance(other, Nodes) \ and self._backend == other._backend \ - and self._user == other._user \ + and self._ac == other._ac \ and self._node_type == other._node_type \ and self._guids == other._guids def __hash__(self) -> int: - return hash((type(self), self._backend, self._user, self._node_type, tuple(sorted(self._guids)))) + return hash((type(self), self._backend, self._ac, self._node_type, tuple(sorted(self._guids)))) def __repr__(self) -> str: - return f'{typename(self)}({self._backend}, {self._user}, {self._node_type}, {self._guids})' + return f'{typename(self)}({self._backend}, {self._ac}, {self._node_type}, {self._guids})' def __str__(self) -> str: return f'{typename(self)}({self._node_type}, {self._guids})' @@ -94,44 +93,44 @@ class Nodes(): return self._backend.schema def __add__(self, other: typing.Any) -> 'Nodes': - """Concatenate guids. Backend, user, and node type must match.""" + """Concatenate guids. Backend, AC, and node type must match.""" if not isinstance(other, type(self)): return NotImplemented if self._backend != other._backend: raise ValueError(other) - if self._user != other._user: + if self._ac != other._ac: raise ValueError(other) if self.node_type != other.node_type: raise ValueError(other) - return Nodes(self._backend, self._user, self.node_type, self._guids | other._guids) + return Nodes(self._backend, self._ac, self.node_type, self._guids | other._guids) def __or__(self, other: typing.Any) -> 'Nodes': - """Concatenate guids. Backend, user, and node type must match.""" + """Concatenate guids. Backend, AC, and node type must match.""" return self.__add__(other) def __sub__(self, other: typing.Any) -> 'Nodes': - """Subtract guids. Backend, user, and node type must match.""" + """Subtract guids. Backend, AC, and node type must match.""" if not isinstance(other, type(self)): return NotImplemented if self._backend != other._backend: raise ValueError(other) - if self._user != other._user: + if self._ac != other._ac: raise ValueError(other) if self.node_type != other.node_type: raise ValueError(other) - return Nodes(self._backend, self._user, self.node_type, self._guids - other._guids) + return Nodes(self._backend, self._ac, self.node_type, self._guids - other._guids) def __and__(self, other: typing.Any) -> 'Nodes': - """Intersect guids. Backend, user, and node type must match.""" + """Intersect guids. Backend, AC, and node type must match.""" if not isinstance(other, type(self)): return NotImplemented if self._backend != other._backend: raise ValueError(other) - if self._user != other._user: + if self._ac != other._ac: raise ValueError(other) if self.node_type != other.node_type: raise ValueError(other) - return Nodes(self._backend, self._user, self.node_type, self._guids & other._guids) + return Nodes(self._backend, self._ac, self.node_type, self._guids & other._guids) def __len__(self) -> int: """Return the number of guids.""" @@ -140,7 +139,7 @@ class Nodes(): def __iter__(self) -> typing.Iterator['Nodes']: """Iterate over individual guids. Returns `Nodes` instances.""" return iter( - Nodes(self._backend, self._user, self.node_type, {guid}) + Nodes(self._backend, self._ac, self.node_type, {guid}) for guid in self._guids ) @@ -266,12 +265,12 @@ class Nodes(): # process triples for root, name, raw in triples: # get node - node = Nodes(self._backend, self._user, self.node_type, {root}) + node = Nodes(self._backend, self._ac, self.node_type, {root}) # get path path, tail = name2path[name] # covert raw to value if isinstance(tail.range, bsc.Node): - value = Nodes(self._backend, self._user, tail.range, {raw}) + value = Nodes(self._backend, self._ac, tail.range, {raw}) else: value = raw # emit triple -- cgit v1.2.3 From a5695dcc4e1be8fccd0b35ba4672cdb3c2c119d7 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Wed, 1 Mar 2023 13:06:59 +0100 Subject: minor fixes --- bsfs/graph/graph.py | 4 +--- bsfs/graph/schema.nt | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index a74da01..a356533 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -28,9 +28,7 @@ __all__: typing.Sequence[str] = ( ## code ## class Graph(): - """The Graph class is - - The Graph class provides a convenient interface to query and access a graph. + """The Graph class provides a convenient interface to query and access a graph. Since it logically builds on the concept of graphs it is easier to navigate than raw triple stores. Naturally, it uses a triple store as *backend*. It also controls actions via access permissions to a *user*. diff --git a/bsfs/graph/schema.nt b/bsfs/graph/schema.nt index f619746..cba5e80 100644 --- a/bsfs/graph/schema.nt +++ b/bsfs/graph/schema.nt @@ -9,11 +9,11 @@ prefix bsm: # literals bsfs:Number rdfs:subClassOf bsfs:Literal . -xsd:integer rdfs:subClassOf bsfs:Number . +xsd:float rdfs:subClassOf bsfs:Number . # predicates bsm:t_created rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Node ; - rdfs:range xsd:integer ; + rdfs:range xsd:float ; bsfs:unique "true"^^xsd:boolean . -- cgit v1.2.3 From 87f437380c1dd8f420437cddc028c0f3174ee1c9 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 2 Mar 2023 12:19:58 +0100 Subject: Node getters in bsfs.Graph: * Empty nodes instance (Graph.empty) * Order-preserving get query (Graph.sorted) * Collect common code in private Graph.__get * Empty query in Graph.get * Empty query in Graph.resolve.Filter * Empty query in AC: filter_read --- bsfs/graph/ac/base.py | 6 +++++- bsfs/graph/ac/null.py | 6 +++++- bsfs/graph/graph.py | 60 +++++++++++++++++++++++++++++++++++++-------------- bsfs/graph/resolve.py | 9 ++++++-- 4 files changed, 61 insertions(+), 20 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py index 0b9f988..2759557 100644 --- a/bsfs/graph/ac/base.py +++ b/bsfs/graph/ac/base.py @@ -83,7 +83,11 @@ class AccessControlBase(abc.ABC): """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: + def filter_read( + self, + node_type: schema.Node, + query: typing.Optional[ast.filter.FilterExpression], + ) -> typing.Optional[ast.filter.FilterExpression]: """Re-write a filter *query* to get (i.e., read) *node_type* nodes.""" @abc.abstractmethod diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py index 6a923a5..e67b55d 100644 --- a/bsfs/graph/ac/null.py +++ b/bsfs/graph/ac/null.py @@ -50,7 +50,11 @@ 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: + def filter_read( + self, + node_type: schema.Node, + query: typing.Optional[ast.filter.FilterExpression] + ) -> typing.Optional[ast.filter.FilterExpression]: """Re-write a filter *query* to get (i.e., read) *node_type* nodes.""" return query diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index a356533..11fe835 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -113,6 +113,7 @@ class Graph(): *node_type*) once some data is assigned to them. """ + # get node type type_ = self.schema.node(node_type) # NOTE: Nodes constructor materializes guids. return _nodes.Nodes(self._backend, self._ac, type_, guids) @@ -120,15 +121,51 @@ class Graph(): def node(self, node_type: URI, guid: URI) -> _nodes.Nodes: """Return node *guid* of type *node_type* as a `bsfs.graph.Nodes` instance. - Note that the *guids* need not to exist (however, the *node_type* has + Note that the *guid* need not to exist (however, the *node_type* has to be part of the schema). An inexistent guid will be created (using *node_type*) once some data is assigned to them. """ return self.nodes(node_type, {guid}) - 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.""" + def empty(self, node_type: URI) -> _nodes.Nodes: + """Return a `Nodes` instance with type *node_type* but no nodes.""" + return self.nodes(node_type, set()) + + def get( + self, + node_type: URI, + query: typing.Optional[ast.filter.FilterExpression], + ) -> _nodes.Nodes: + """Return a `Nodes` instance over all nodes of type *node_type* that match the *query*.""" + # return Nodes instance + type_ = self.schema.node(node_type) + return _nodes.Nodes(self._backend, self._ac, type_, self.__get(node_type, query)) + + def sorted( + self, + node_type: URI, + query: typing.Optional[ast.filter.FilterExpression], + # FIXME: sort ast + ) -> typing.Iterator[_nodes.Nodes]: + """Return a iterator over `Nodes` instances over all nodes of type *node_type* that match the *query*.""" + # FIXME: Order should be a parameter + # return iterator over Nodes instances + type_ = self.schema.node(node_type) + for guid in self.__get(node_type, query): + yield _nodes.Nodes(self._backend, self._ac, type_, {guid}) + + def all(self, node_type: URI) -> _nodes.Nodes: + """Return all instances of type *node_type*.""" + type_ = self.schema.node(node_type) + return _nodes.Nodes(self._backend, self._ac, type_, self.__get(node_type, None)) + + def __get( + self, + node_type: URI, + query: typing.Optional[ast.filter.FilterExpression], + ) -> typing.Iterator[URI]: + """Build and execute a get query.""" # get node type type_ = self.schema.node(node_type) # resolve Nodes instances @@ -136,18 +173,9 @@ class Graph(): # 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._ac, type_, guids) - - def all(self, node_type: URI) -> _nodes.Nodes: - """Return all instances of type *node_type*.""" - # get node type - type_ = self.schema.node(node_type) - guids = self._backend.get(type_, None) # no need to materialize - return _nodes.Nodes(self._backend, self._ac, type_, guids) - + if query is not None: + self._validate(type_, query) + # query the backend and return the (non-materialized) result + return self._backend.get(type_, query) ## EOF ## diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index 4677401..b3ab001 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -40,8 +40,13 @@ class Filter(): def __init__(self, schema): self.schema = schema - def __call__(self, root_type: bsc.Node, node: ast.filter.FilterExpression): - # FIXME: node can be None! + def __call__( + self, + root_type: bsc.Node, + node: typing.Optional[ast.filter.FilterExpression], + ): + if node is None: + return None return self._parse_filter_expression(root_type, node) def _parse_filter_expression( -- cgit v1.2.3 From 36d07cc6e0ec0f53001bfc5045437a374ebb895f Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 2 Mar 2023 14:55:57 +0100 Subject: empty fetch result in Nodes --- bsfs/graph/nodes.py | 58 +++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 26 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 91cbb5d..c6bd5d8 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -249,32 +249,38 @@ class Nodes(): # add access controls to fetch fetch = self._ac.fetch_read(self.node_type, fetch) - # compose filter ast - filter = ast.filter.IsIn(self.guids) # pylint: disable=redefined-builtin - # add access controls to filter - filter = self._ac.filter_read(self.node_type, filter) - - # validate queries - validate.Filter(self._backend.schema)(self.node_type, filter) - validate.Fetch(self._backend.schema)(self.node_type, fetch) - - # process results, convert if need be - def triple_iter(): - # query the backend - triples = self._backend.fetch(self.node_type, filter, fetch) - # process triples - for root, name, raw in triples: - # get node - node = Nodes(self._backend, self._ac, self.node_type, {root}) - # get path - path, tail = name2path[name] - # covert raw to value - if isinstance(tail.range, bsc.Node): - value = Nodes(self._backend, self._ac, tail.range, {raw}) - else: - value = raw - # emit triple - yield node, path, value + if len(self._guids) == 0: + # shortcut: no need to query; no triples + # FIXME: if the Fetch query can given by the user, we might want to check its validity + def triple_iter(): + return [] + else: + # compose filter ast + filter = ast.filter.IsIn(self.guids) # pylint: disable=redefined-builtin + # add access controls to filter + filter = self._ac.filter_read(self.node_type, filter) # type: ignore [assignment] + + # validate queries + validate.Filter(self._backend.schema)(self.node_type, filter) + validate.Fetch(self._backend.schema)(self.node_type, fetch) + + # process results, convert if need be + def triple_iter(): + # query the backend + triples = self._backend.fetch(self.node_type, filter, fetch) + # process triples + for root, name, raw in triples: + # get node + node = Nodes(self._backend, self._ac, self.node_type, {root}) + # get path + path, tail = name2path[name] + # covert raw to value + if isinstance(tail.range, bsc.Node): + value = Nodes(self._backend, self._ac, tail.range, {raw}) + else: + value = raw + # emit triple + yield node, path, value # simplify by default view_kwargs['node'] = view_kwargs.get('node', len(self._guids) != 1) -- cgit v1.2.3 From 2e07f33314c238e42bfadc5f39805f93ffbc622e Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 2 Mar 2023 15:10:05 +0100 Subject: removed author and license notices from individual files --- bsfs/graph/__init__.py | 5 ----- bsfs/graph/ac/__init__.py | 5 ----- bsfs/graph/ac/base.py | 5 ----- bsfs/graph/ac/null.py | 5 ----- bsfs/graph/graph.py | 5 ----- bsfs/graph/nodes.py | 5 ----- bsfs/graph/resolve.py | 5 ----- bsfs/graph/result.py | 5 ----- bsfs/graph/walk.py | 5 ----- 9 files changed, 45 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/__init__.py b/bsfs/graph/__init__.py index 82d2235..8d38d23 100644 --- a/bsfs/graph/__init__.py +++ b/bsfs/graph/__init__.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import typing diff --git a/bsfs/graph/ac/__init__.py b/bsfs/graph/ac/__init__.py index 420de01..11b45df 100644 --- a/bsfs/graph/ac/__init__.py +++ b/bsfs/graph/ac/__init__.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import typing diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py index 2759557..e85c1dd 100644 --- a/bsfs/graph/ac/base.py +++ b/bsfs/graph/ac/base.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import abc import typing diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py index e67b55d..3a391aa 100644 --- a/bsfs/graph/ac/null.py +++ b/bsfs/graph/ac/null.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import typing diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index 11fe835..ade51a5 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import os import typing diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index c6bd5d8..c3530c1 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports from collections import abc import time diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index b3ab001..213ac4c 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import typing diff --git a/bsfs/graph/result.py b/bsfs/graph/result.py index 31822f1..0fcbb13 100644 --- a/bsfs/graph/result.py +++ b/bsfs/graph/result.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports from collections import defaultdict import typing diff --git a/bsfs/graph/walk.py b/bsfs/graph/walk.py index 1b1cfa0..6415c9b 100644 --- a/bsfs/graph/walk.py +++ b/bsfs/graph/walk.py @@ -1,9 +1,4 @@ -""" -Part of the BlackStar filesystem (bsfs) module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports from collections import abc import typing -- cgit v1.2.3 From 28a021483c13e974e00b6159f0653b0727df9d10 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 2 Mar 2023 16:40:00 +0100 Subject: prohibit certain characters in URI and ensure URIs in bsfs.graph --- bsfs/graph/nodes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index c3530c1..84996c7 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -52,9 +52,8 @@ class Nodes(): self._backend = backend self._ac = access_control self._node_type = node_type - self._guids = set(guids) - # create helper instances - # FIXME: Assumes that the schema does not change while the instance is in use! + # convert to URI since this is not guaranteed by Graph + self._guids = {URI(guid) for guid in guids} def __eq__(self, other: typing.Any) -> bool: return isinstance(other, Nodes) \ -- cgit v1.2.3 From 6b9379d75198082054c35e44bc2cd880353a7485 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 2 Mar 2023 16:40:43 +0100 Subject: hardening --- bsfs/graph/graph.py | 16 ++-------------- bsfs/graph/nodes.py | 8 +++++--- bsfs/graph/resolve.py | 8 ++++++++ 3 files changed, 15 insertions(+), 17 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index ade51a5..1b4c212 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -36,12 +36,6 @@ class Graph(): # access controls. _ac: ac.AccessControlBase - # query resolver. - _resolver: resolve.Filter - - # query validator. - _validate: validate.Filter - def __init__( self, backend: TripleStoreBase, @@ -50,9 +44,6 @@ class Graph(): # store members self._backend = backend self._ac = access_control - # helper classes - self._resolver = resolve.Filter(self._backend.schema) - self._validate = validate.Filter(self._backend.schema) # ensure Graph schema requirements self.migrate(self._backend.schema) @@ -94,9 +85,6 @@ 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 @@ -164,12 +152,12 @@ class Graph(): # get node type type_ = self.schema.node(node_type) # resolve Nodes instances - query = self._resolver(type_, query) + query = resolve.Filter(self._backend.schema).resolve(type_, query) # add access controls to query query = self._ac.filter_read(type_, query) # validate query if query is not None: - self._validate(type_, query) + validate.Filter(self._backend.schema).validate(type_, query) # query the backend and return the (non-materialized) result return self._backend.get(type_, query) diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 84996c7..74f4c4f 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -25,7 +25,9 @@ __all__: typing.Sequence[str] = ( ## code ## class Nodes(): - """ + """Container for graph nodes, provides operations on nodes. + + NOTE: Should not be created directly but only via `bsfs.graph.Graph`. NOTE: guids may or may not exist. This is not verified as nodes are created on demand. """ @@ -255,8 +257,8 @@ class Nodes(): filter = self._ac.filter_read(self.node_type, filter) # type: ignore [assignment] # validate queries - validate.Filter(self._backend.schema)(self.node_type, filter) - validate.Fetch(self._backend.schema)(self.node_type, fetch) + validate.Filter(self._backend.schema).validate(self.node_type, filter) + validate.Fetch(self._backend.schema).validate(self.node_type, fetch) # process results, convert if need be def triple_iter(): diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index 213ac4c..0ba1e36 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -40,6 +40,14 @@ class Filter(): root_type: bsc.Node, node: typing.Optional[ast.filter.FilterExpression], ): + """Alias for `Resolve.resolve`.""" + return self.resolve(root_type, node) + + def resolve( + self, + root_type: bsc.Node, + node: typing.Optional[ast.filter.FilterExpression], + ): if node is None: return None return self._parse_filter_expression(root_type, node) -- cgit v1.2.3 From 2c6c23f85e7f2123c508f9ff8a4aa776948bb589 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 2 Mar 2023 16:46:11 +0100 Subject: minor style fixes --- bsfs/graph/resolve.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bsfs/graph') diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index 0ba1e36..95dcfc1 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -48,6 +48,7 @@ class Filter(): root_type: bsc.Node, node: typing.Optional[ast.filter.FilterExpression], ): + """Resolve Nodes instances of a *node* query starting at *root_type*.""" if node is None: return None return self._parse_filter_expression(root_type, node) -- cgit v1.2.3 From 4fead04055be4967d9ea3b24ff61fe37a93108dd Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 4 Mar 2023 13:31:11 +0100 Subject: namespace refactoring and cleanup --- bsfs/graph/ac/null.py | 2 +- bsfs/graph/nodes.py | 4 ++-- bsfs/graph/resolve.py | 4 ++-- bsfs/graph/schema.nt | 11 ++++++----- 4 files changed, 11 insertions(+), 10 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py index 3a391aa..c9ec7d0 100644 --- a/bsfs/graph/ac/null.py +++ b/bsfs/graph/ac/null.py @@ -24,7 +24,7 @@ class NullAC(base.AccessControlBase): def is_protected_predicate(self, pred: schema.Predicate) -> bool: """Return True if a predicate cannot be modified manually.""" - return pred.uri == ns.bsm.t_created + return pred.uri == ns.bsn.t_created def create(self, node_type: schema.Node, guids: typing.Iterable[URI]): """Perform post-creation operations on nodes, e.g. ownership information.""" diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 74f4c4f..47b0217 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -170,7 +170,7 @@ class Nodes(): self._backend.commit() except ( - errors.PermissionDeniedError, # tried to set a protected predicate (ns.bsm.t_created) + errors.PermissionDeniedError, # tried to set a protected predicate errors.ConsistencyError, # node types are not in the schema or don't match the predicate errors.InstanceError, # guids/values don't have the correct type TypeError, # value is supposed to be a Nodes instance @@ -394,7 +394,7 @@ class Nodes(): self._backend.create(node_type, missing) # add bookkeeping triples self._backend.set(node_type, missing, - self._backend.schema.predicate(ns.bsm.t_created), [time.time()]) + self._backend.schema.predicate(ns.bsn.t_created), [time.time()]) # add permission triples self._ac.create(node_type, missing) # return available nodes diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py index 95dcfc1..a58eb67 100644 --- a/bsfs/graph/resolve.py +++ b/bsfs/graph/resolve.py @@ -27,8 +27,8 @@ class Filter(): 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))) + >>> tags = graph.node(ns.bsn.Tag, 'http://example.com/me/tag#1234') + >>> graph.get(ns.bsn.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags))) """ diff --git a/bsfs/graph/schema.nt b/bsfs/graph/schema.nt index cba5e80..37bba5e 100644 --- a/bsfs/graph/schema.nt +++ b/bsfs/graph/schema.nt @@ -4,15 +4,16 @@ prefix rdfs: prefix xsd: # bsfs prefixes -prefix bsfs: -prefix bsm: +prefix bsfs: +prefix bsl: +prefix bsn: # literals -bsfs:Number rdfs:subClassOf bsfs:Literal . -xsd:float rdfs:subClassOf bsfs:Number . +bsl:Number rdfs:subClassOf bsfs:Literal . +xsd:float rdfs:subClassOf bsl:Number . # predicates -bsm:t_created rdfs:subClassOf bsfs:Predicate ; +bsn:t_created rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Node ; rdfs:range xsd:float ; bsfs:unique "true"^^xsd:boolean . -- cgit v1.2.3