From e8492489098ef5f8566214e083cd2c2d1d449f5a Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Thu, 8 Dec 2022 16:36:19 +0100 Subject: sparql triple store and graph (nodes, mostly) --- bsfs/graph/__init__.py | 15 +++ bsfs/graph/ac/__init__.py | 20 ++++ bsfs/graph/ac/base.py | 67 +++++++++++++ bsfs/graph/ac/null.py | 53 ++++++++++ bsfs/graph/graph.py | 65 +++++++++++++ bsfs/graph/nodes.py | 243 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 463 insertions(+) create mode 100644 bsfs/graph/__init__.py create mode 100644 bsfs/graph/ac/__init__.py create mode 100644 bsfs/graph/ac/base.py create mode 100644 bsfs/graph/ac/null.py create mode 100644 bsfs/graph/graph.py create mode 100644 bsfs/graph/nodes.py (limited to 'bsfs/graph') diff --git a/bsfs/graph/__init__.py b/bsfs/graph/__init__.py new file mode 100644 index 0000000..3a131e9 --- /dev/null +++ b/bsfs/graph/__init__.py @@ -0,0 +1,15 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# inner-module imports + +# exports +__all__: typing.Sequence[str] = [] + +## EOF ## diff --git a/bsfs/graph/ac/__init__.py b/bsfs/graph/ac/__init__.py new file mode 100644 index 0000000..420de01 --- /dev/null +++ b/bsfs/graph/ac/__init__.py @@ -0,0 +1,20 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# inner-module imports +from .base import AccessControlBase +from .null import NullAC + +# exports +__all__: typing.Sequence[str] = ( + 'AccessControlBase', + 'NullAC', + ) + +## EOF ## diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py new file mode 100644 index 0000000..70475d2 --- /dev/null +++ b/bsfs/graph/ac/base.py @@ -0,0 +1,67 @@ +""" + +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 + +# bsfs imports +from bsfs import schema as _schema +from bsfs.triple_store import TripleStoreBase +from bsfs.utils import URI + +# exports +__all__: typing.Sequence[str] = ( + 'AccessControlBase', + ) + + +## code ## + +class AccessControlBase(abc.ABC): + """ + """ + + # + __backend: TripleStoreBase + + # + __user: URI + + def __init__( + self, + backend: TripleStoreBase, + user: URI, + ): + self.__backend = backend + self.__user = URI(user) + + @abc.abstractmethod + def is_protected_predicate(self, pred: _schema.Predicate) -> bool: + """Return True if a predicate cannot be modified manually.""" + + @abc.abstractmethod + def create(self, node_type: _schema.Node, guids: typing.Iterable[URI]): + """Perform post-creation operations on nodes, e.g. ownership information.""" + + @abc.abstractmethod + def link_from_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes for which outbound links can be written.""" + + @abc.abstractmethod + def link_to_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes for which inbound links can be written.""" + + @abc.abstractmethod + def write_literal(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes to which literals can be attached.""" + + @abc.abstractmethod + def createable(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes that are allowed to be created.""" + + +## EOF ## diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py new file mode 100644 index 0000000..a39b7b9 --- /dev/null +++ b/bsfs/graph/ac/null.py @@ -0,0 +1,53 @@ +""" + +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 _schema +from bsfs.namespace import ns +from bsfs.utils import URI + +# inner-module imports +from . import base + +# exports +__all__: typing.Sequence[str] = ( + 'NullAC', + ) + + +## code ## + +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 + + def create(self, node_type: _schema.Node, guids: typing.Iterable[URI]): + """Perform post-creation operations on nodes, e.g. ownership information.""" + + def link_from_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes for which outbound links can be written.""" + return guids + + def link_to_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes for which inbound links can be written.""" + return guids + + def write_literal(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes to which literals can be attached.""" + return guids + + def createable(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + """Return nodes that are allowed to be created.""" + return guids + +## EOF ## diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py new file mode 100644 index 0000000..06271f6 --- /dev/null +++ b/bsfs/graph/graph.py @@ -0,0 +1,65 @@ +""" + +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.schema import Schema +from bsfs.triple_store import TripleStoreBase +from bsfs.utils import URI, typename + +# inner-module imports +from . import nodes + +# exports +__all__: typing.Sequence[str] = ( + 'Graph', + ) + + +## code ## + +class Graph(): + """ + """ + # link to the triple storage backend. + __backend: TripleStoreBase + + # user uri. + __user: URI + + def __init__(self, backend: TripleStoreBase, user: URI): + self.__backend = backend + self.__user = user + + def __hash__(self) -> int: + return hash((type(self), self.__backend, self.__user)) + + def __eq__(self, other) -> bool: + return isinstance(other, type(self)) \ + and self.__backend == other.__backend \ + and self.__user == other.__user + + def __repr__(self) -> str: + return f'{typename(self)}(backend={repr(self.__backend)}, user={self.__user})' + + def __str__(self) -> str: + return f'{typename(self)}({str(self.__backend)}, {self.__user})' + + @property + def schema(self) -> Schema: + """Return the store's local schema.""" + return self.__backend.schema + + def nodes(self, node_type: URI, guids: typing.Iterable[URI]) -> nodes.Nodes: + """ + """ + node_type = self.schema.node(node_type) + # NOTE: Nodes constructor materializes guids. + return nodes.Nodes(self.__backend, self.__user, node_type, guids) + +## EOF ## diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py new file mode 100644 index 0000000..7d2e9b3 --- /dev/null +++ b/bsfs/graph/nodes.py @@ -0,0 +1,243 @@ +""" + +Part of the BlackStar filesystem (bsfs) module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import itertools +import time +import typing + +# bsfs imports +from bsfs import schema as _schema +from bsfs.namespace import ns +from bsfs.triple_store import TripleStoreBase +from bsfs.utils import errors, URI, typename + +# inner-module imports +from . import ac + +# exports +__all__: typing.Sequence[str] = ( + 'Nodes', + ) + + +## code ## + +class Nodes(): + """ + NOTE: guids may or may not exist. This is not verified as nodes are created on demand. + """ + + # triple store backend. + __backend: TripleStoreBase + + # user uri. + __user: URI + + # node type. + __node_type: _schema.Node + + # guids of nodes. Can be empty. + __guids: typing.Set[URI] + + def __init__( + self, + backend: TripleStoreBase, + user: URI, + node_type: _schema.Node, + guids: typing.Iterable[URI], + ): + self.__backend = backend + self.__user = user + self.__node_type = node_type + self.__guids = set(guids) + 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.__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)))) + + def __repr__(self) -> str: + return f'{typename(self)}({self.__backend}, {self.__user}, {self.__node_type}, {self.__guids})' + + def __str__(self) -> str: + return f'{typename(self)}({self.__node_type}, {self.__guids})' + + @property + def node_type(self) -> _schema.Node: + """Return the node's type.""" + return self.__node_type + + @property + def guids(self) -> typing.Iterator[URI]: + """Return all node guids.""" + return iter(self.__guids) + + def set( + self, + pred: URI, # FIXME: URI or _schema.Predicate? + value: typing.Any, + ) -> 'Nodes': + """ + """ + try: + # insert triples + self.__set(pred, value) + # save changes + self.__backend.commit() + + except ( + errors.PermissionDeniedError, # tried to set a protected predicate (ns.bsm.t_created) + 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 + ValueError, # multiple values passed to unique predicate + ): + # revert changes + self.__backend.rollback() + # notify the client + raise + + return self + + def set_from_iterable( + self, + predicate_values: typing.Iterable[typing.Tuple[URI, typing.Any]], # FIXME: URI or _schema.Predicate? + ) -> 'Nodes': + """ + """ + # TODO: Could group predicate_values by predicate to gain some efficiency + # TODO: ignore errors on some predicates; For now this could leave residual + # data (e.g. some nodes were created, some not). + try: + # insert triples + for pred, value in predicate_values: + self.__set(pred, value) + # save changes + self.__backend.commit() + + except ( + errors.PermissionDeniedError, # tried to set a protected predicate (ns.bsm.t_created) + 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 + ValueError, # multiple values passed to unique predicate + ): + # revert changes + self.__backend.rollback() + # notify the client + raise + + return self + + def __set( + self, + predicate: URI, + value: typing.Any, + #on_error: str = 'ignore', # ignore, rollback + ): + """ + """ + # get normalized predicate. Raises KeyError if *pred* not in the schema. + pred = self.__backend.schema.predicate(predicate) + + # node_type must be a subclass of the predicate's domain + node_type = self.node_type + if not node_type <= pred.domain: + raise errors.ConsistencyError(f'{node_type} must be a subclass of {pred.domain}') + + # check reserved predicates (access controls, metadata, internal structures) + # 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): + raise errors.PermissionDeniedError(pred) + + # set operation affects all nodes (if possible) + guids = set(self.guids) + + # ensure subject node existence; create nodes if need be + guids = set(self._ensure_nodes(node_type, guids)) + + # check value + if isinstance(pred.range, _schema.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. + guids = set(self.__ac.write_literal(node_type, guids)) + + # insert literals + # TODO: Support passing iterators as values for non-unique predicates + self.__backend.set( + node_type, + guids, + pred, + [value], + ) + + elif isinstance(pred.range, _schema.Node): + # check value type + if not isinstance(value, Nodes): + raise TypeError(value) + # value's node_type must be a subclass of the predicate's range + if not value.node_type <= pred.range: + raise errors.ConsistencyError(f'{value.node_type} must be a subclass of {pred.range}') + + # check link permissions on source 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)) + + # 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)) + + # insert node links + self.__backend.set( + node_type, + guids, + pred, + targets, + ) + + else: + raise errors.UnreachableError() + + def _ensure_nodes( + self, + node_type: _schema.Node, + guids: typing.Iterable[URI], + ): + # check node existence + guids = set(guids) + existing = set(self.__backend.exists(node_type, guids)) + # get nodes to be created + missing = guids - existing + # create nodes if need be + if len(missing) > 0: + # check which missing nodes can be created + 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) + # return available nodes + return existing | missing + +## EOF ## -- cgit v1.2.3 From ebc3ccb5fdce950649bfcbf18f88ecb4a9dbcad0 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 18 Dec 2022 13:53:34 +0100 Subject: import fixes --- bsfs/graph/__init__.py | 5 ++++- bsfs/graph/ac/base.py | 14 +++++++------- bsfs/graph/ac/null.py | 14 +++++++------- bsfs/graph/graph.py | 6 +++--- 4 files changed, 21 insertions(+), 18 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/__init__.py b/bsfs/graph/__init__.py index 3a131e9..82d2235 100644 --- a/bsfs/graph/__init__.py +++ b/bsfs/graph/__init__.py @@ -8,8 +8,11 @@ Author: Matthias Baumgartner, 2022 import typing # inner-module imports +from .graph import Graph # exports -__all__: typing.Sequence[str] = [] +__all__: typing.Sequence[str] = ( + 'Graph', + ) ## EOF ## diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py index 70475d2..eef444b 100644 --- a/bsfs/graph/ac/base.py +++ b/bsfs/graph/ac/base.py @@ -9,7 +9,7 @@ import abc import typing # bsfs imports -from bsfs import schema as _schema +from bsfs import schema from bsfs.triple_store import TripleStoreBase from bsfs.utils import URI @@ -40,27 +40,27 @@ class AccessControlBase(abc.ABC): self.__user = URI(user) @abc.abstractmethod - def is_protected_predicate(self, pred: _schema.Predicate) -> bool: + def is_protected_predicate(self, pred: schema.Predicate) -> bool: """Return True if a predicate cannot be modified manually.""" @abc.abstractmethod - def create(self, node_type: _schema.Node, guids: typing.Iterable[URI]): + def create(self, node_type: schema.Node, guids: typing.Iterable[URI]): """Perform post-creation operations on nodes, e.g. ownership information.""" @abc.abstractmethod - def link_from_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def link_from_node(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes for which outbound links can be written.""" @abc.abstractmethod - def link_to_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def link_to_node(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes for which inbound links can be written.""" @abc.abstractmethod - def write_literal(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def write_literal(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes to which literals can be attached.""" @abc.abstractmethod - def createable(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def createable(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes that are allowed to be created.""" diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py index a39b7b9..288a0da 100644 --- a/bsfs/graph/ac/null.py +++ b/bsfs/graph/ac/null.py @@ -8,7 +8,7 @@ Author: Matthias Baumgartner, 2022 import typing # bsfs imports -from bsfs import schema as _schema +from bsfs import schema from bsfs.namespace import ns from bsfs.utils import URI @@ -27,26 +27,26 @@ class NullAC(base.AccessControlBase): """ """ - def is_protected_predicate(self, pred: _schema.Predicate) -> bool: + 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 - def create(self, node_type: _schema.Node, guids: typing.Iterable[URI]): + def create(self, node_type: schema.Node, guids: typing.Iterable[URI]): """Perform post-creation operations on nodes, e.g. ownership information.""" - def link_from_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def link_from_node(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes for which outbound links can be written.""" return guids - def link_to_node(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def link_to_node(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes for which inbound links can be written.""" return guids - def write_literal(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def write_literal(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes to which literals can be attached.""" return guids - def createable(self, node_type: _schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: + def createable(self, node_type: schema.Node, guids: typing.Iterable[URI]) -> typing.Iterable[URI]: """Return nodes that are allowed to be created.""" return guids diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index 06271f6..d5e1b88 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -13,7 +13,7 @@ from bsfs.triple_store import TripleStoreBase from bsfs.utils import URI, typename # inner-module imports -from . import nodes +from . import nodes as _nodes # exports __all__: typing.Sequence[str] = ( @@ -55,11 +55,11 @@ class Graph(): """Return the store's local schema.""" return self.__backend.schema - def nodes(self, node_type: URI, guids: typing.Iterable[URI]) -> nodes.Nodes: """ + def nodes(self, node_type: URI, guids: typing.Iterable[URI]) -> _nodes.Nodes: """ node_type = self.schema.node(node_type) # NOTE: Nodes constructor materializes guids. - return nodes.Nodes(self.__backend, self.__user, node_type, guids) + return _nodes.Nodes(self._backend, self._user, type_, guids) ## EOF ## -- cgit v1.2.3 From edd5390b6db1550f6a80a46f0eaf5f3916997532 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 18 Dec 2022 14:06:58 +0100 Subject: information hiding --- bsfs/graph/ac/base.py | 12 +++++------ bsfs/graph/graph.py | 20 +++++++++--------- bsfs/graph/nodes.py | 58 +++++++++++++++++++++++++-------------------------- 3 files changed, 45 insertions(+), 45 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py index eef444b..80742d7 100644 --- a/bsfs/graph/ac/base.py +++ b/bsfs/graph/ac/base.py @@ -25,19 +25,19 @@ class AccessControlBase(abc.ABC): """ """ - # - __backend: TripleStoreBase + # The triple store backend. + _backend: TripleStoreBase - # - __user: URI + # The current user. + _user: URI def __init__( self, backend: TripleStoreBase, user: URI, ): - self.__backend = backend - self.__user = URI(user) + self._backend = backend + self._user = URI(user) @abc.abstractmethod def is_protected_predicate(self, pred: schema.Predicate) -> bool: diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index d5e1b88..71973c2 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -27,33 +27,33 @@ class Graph(): """ """ # link to the triple storage backend. - __backend: TripleStoreBase + _backend: TripleStoreBase # user uri. - __user: URI + _user: URI def __init__(self, backend: TripleStoreBase, user: URI): - self.__backend = backend - self.__user = user + self._backend = backend + self._user = user def __hash__(self) -> int: - return hash((type(self), self.__backend, self.__user)) + return hash((type(self), self._backend, self._user)) def __eq__(self, other) -> bool: return isinstance(other, type(self)) \ - and self.__backend == other.__backend \ - and self.__user == other.__user + and self._backend == other._backend \ + and self._user == other._user def __repr__(self) -> str: - return f'{typename(self)}(backend={repr(self.__backend)}, user={self.__user})' + return f'{typename(self)}(backend={repr(self._backend)}, user={self._user})' def __str__(self) -> str: - return f'{typename(self)}({str(self.__backend)}, {self.__user})' + return f'{typename(self)}({str(self._backend)}, {self._user})' @property def schema(self) -> Schema: """Return the store's local schema.""" - return self.__backend.schema + return self._backend.schema """ def nodes(self, node_type: URI, guids: typing.Iterable[URI]) -> _nodes.Nodes: diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 7d2e9b3..7b0e8f4 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -32,16 +32,16 @@ class Nodes(): """ # triple store backend. - __backend: TripleStoreBase + _backend: TripleStoreBase # user uri. - __user: URI + _user: URI # node type. - __node_type: _schema.Node + _node_type: _schema.Node # guids of nodes. Can be empty. - __guids: typing.Set[URI] + _guids: typing.Set[URI] def __init__( self, @@ -50,37 +50,37 @@ class Nodes(): node_type: _schema.Node, guids: typing.Iterable[URI], ): - self.__backend = backend - self.__user = user - self.__node_type = node_type - self.__guids = set(guids) - self.__ac = ac.NullAC(self.__backend, self.__user) + self._backend = backend + self._user = user + self._node_type = node_type + self._guids = set(guids) + 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.__node_type == other.__node_type \ - and self.__guids == other.__guids + and self._backend == other._backend \ + and self._user == other._user \ + 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._user, 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._user}, {self._node_type}, {self._guids})' def __str__(self) -> str: - return f'{typename(self)}({self.__node_type}, {self.__guids})' + return f'{typename(self)}({self._node_type}, {self._guids})' @property def node_type(self) -> _schema.Node: """Return the node's type.""" - return self.__node_type + return self._node_type @property def guids(self) -> typing.Iterator[URI]: """Return all node guids.""" - return iter(self.__guids) + return iter(self._guids) def set( self, @@ -93,7 +93,7 @@ class Nodes(): # insert triples self.__set(pred, value) # save changes - self.__backend.commit() + self._backend.commit() except ( errors.PermissionDeniedError, # tried to set a protected predicate (ns.bsm.t_created) @@ -103,7 +103,7 @@ class Nodes(): ValueError, # multiple values passed to unique predicate ): # revert changes - self.__backend.rollback() + self._backend.rollback() # notify the client raise @@ -123,7 +123,7 @@ class Nodes(): for pred, value in predicate_values: self.__set(pred, value) # save changes - self.__backend.commit() + self._backend.commit() except ( errors.PermissionDeniedError, # tried to set a protected predicate (ns.bsm.t_created) @@ -133,7 +133,7 @@ class Nodes(): ValueError, # multiple values passed to unique predicate ): # revert changes - self.__backend.rollback() + self._backend.rollback() # notify the client raise @@ -148,7 +148,7 @@ class Nodes(): """ """ # get normalized predicate. Raises KeyError if *pred* not in the schema. - pred = self.__backend.schema.predicate(predicate) + pred = self._backend.schema.predicate(predicate) # node_type must be a subclass of the predicate's domain node_type = self.node_type @@ -177,7 +177,7 @@ class Nodes(): # insert literals # TODO: Support passing iterators as values for non-unique predicates - self.__backend.set( + self._backend.set( node_type, guids, pred, @@ -206,7 +206,7 @@ class Nodes(): targets = set(self.__ac.link_to_node(value.node_type, targets)) # insert node links - self.__backend.set( + self._backend.set( node_type, guids, pred, @@ -223,7 +223,7 @@ class Nodes(): ): # check node existence guids = set(guids) - existing = set(self.__backend.exists(node_type, guids)) + existing = set(self._backend.exists(node_type, guids)) # get nodes to be created missing = guids - existing # create nodes if need be @@ -231,10 +231,10 @@ class Nodes(): # check which missing nodes can be created missing = set(self.__ac.createable(node_type, missing)) # create nodes - self.__backend.create(node_type, missing) + 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.set(node_type, missing, + self._backend.schema.predicate(ns.bsm.t_created), [time.time()]) # add permission triples self.__ac.create(node_type, missing) # return available nodes -- cgit v1.2.3 From 3165c3609a5061135ff7393747f8dc3f7f7abe0c Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 18 Dec 2022 14:07:56 +0100 Subject: graph schema migration --- bsfs/graph/graph.py | 24 ++++++++++++++++++++++++ bsfs/graph/schema.nt | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 bsfs/graph/schema.nt (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index 71973c2..4a36ff6 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -5,6 +5,7 @@ A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # imports +import os import typing # bsfs imports @@ -35,6 +36,8 @@ class Graph(): def __init__(self, backend: TripleStoreBase, user: URI): self._backend = backend self._user = user + # ensure Graph schema requirements + self.migrate(self._backend.schema) def __hash__(self) -> int: return hash((type(self), self._backend, self._user)) @@ -55,7 +58,28 @@ class Graph(): """Return the store's local schema.""" return self._backend.schema + def migrate(self, schema: Schema, append: bool = True) -> 'Graph': + """Migrate the current schema to a new *schema*. + + Appends to the current schema by default; control this via *append*. + The `Graph` may add additional classes to the schema that are required for its interals. + """ + # check args + if not isinstance(schema, 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()) + # migrate schema in backend + # FIXME: consult access controls! + self._backend.schema = schema + # return self + return self + def nodes(self, node_type: URI, guids: typing.Iterable[URI]) -> _nodes.Nodes: """ node_type = self.schema.node(node_type) diff --git a/bsfs/graph/schema.nt b/bsfs/graph/schema.nt new file mode 100644 index 0000000..8612681 --- /dev/null +++ b/bsfs/graph/schema.nt @@ -0,0 +1,18 @@ + +# generic prefixes +prefix rdfs: +prefix xsd: + +# bsfs prefixes +prefix bsfs: +prefix bsm: + +# literals +xsd:integer rdfs:subClassOf bsfs:Literal . + +# predicates +bsm:t_created rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + -- cgit v1.2.3 From e19c8f9d0818a147832df0945188ea14de9c7690 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 18 Dec 2022 14:15:18 +0100 Subject: documentation, types, and style fixes --- bsfs/graph/ac/base.py | 6 +++++- bsfs/graph/ac/null.py | 3 +-- bsfs/graph/graph.py | 17 +++++++++++++++-- bsfs/graph/nodes.py | 44 +++++++++----------------------------------- 4 files changed, 30 insertions(+), 40 deletions(-) (limited to 'bsfs/graph') diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py index 80742d7..bc9aeb3 100644 --- a/bsfs/graph/ac/base.py +++ b/bsfs/graph/ac/base.py @@ -22,7 +22,11 @@ __all__: typing.Sequence[str] = ( ## code ## class AccessControlBase(abc.ABC): - """ + """Defines the interface for access control policies. + + An access control policy governs which actions a user may take to query + or to manipulate a graph. + """ # The triple store backend. diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py index 288a0da..36838bd 100644 --- a/bsfs/graph/ac/null.py +++ b/bsfs/graph/ac/null.py @@ -24,8 +24,7 @@ __all__: typing.Sequence[str] = ( ## code ## class NullAC(base.AccessControlBase): - """ - """ + """The NULL access control implements a dummy policy that allows any action to any user.""" 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 4a36ff6..87f7a31 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -25,8 +25,15 @@ __all__: typing.Sequence[str] = ( ## code ## class Graph(): + """The Graph class is + + 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*. + """ - """ + # link to the triple storage backend. _backend: TripleStoreBase @@ -81,8 +88,14 @@ class Graph(): return self def nodes(self, node_type: URI, guids: typing.Iterable[URI]) -> _nodes.Nodes: + """Return nodes *guids* of type *node_type* as a `bsfs.graph.Nodes` instance. + + Note that the *guids* need not to exist (however, the *node_type* has + to be part of the schema). Inexistent guids will be created (using + *node_type*) once some data is assigned to them. + """ - node_type = self.schema.node(node_type) + type_ = self.schema.node(node_type) # NOTE: Nodes constructor materializes guids. return _nodes.Nodes(self._backend, self._user, type_, guids) diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py index 7b0e8f4..c417a0e 100644 --- a/bsfs/graph/nodes.py +++ b/bsfs/graph/nodes.py @@ -5,7 +5,6 @@ A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # imports -import itertools import time import typing @@ -87,34 +86,14 @@ class Nodes(): pred: URI, # FIXME: URI or _schema.Predicate? value: typing.Any, ) -> 'Nodes': - """ - """ - try: - # insert triples - self.__set(pred, value) - # save changes - self._backend.commit() - - except ( - errors.PermissionDeniedError, # tried to set a protected predicate (ns.bsm.t_created) - 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 - ValueError, # multiple values passed to unique predicate - ): - # revert changes - self._backend.rollback() - # notify the client - raise - - return self + """Set predicate *pred* to *value*.""" + return self.set_from_iterable([(pred, value)]) def set_from_iterable( self, predicate_values: typing.Iterable[typing.Tuple[URI, typing.Any]], # FIXME: URI or _schema.Predicate? ) -> 'Nodes': - """ - """ + """Set mutliple predicate-value pairs at once.""" # TODO: Could group predicate_values by predicate to gain some efficiency # TODO: ignore errors on some predicates; For now this could leave residual # data (e.g. some nodes were created, some not). @@ -137,14 +116,11 @@ class Nodes(): # notify the client raise + # FIXME: How about other errors? Shouldn't I then rollback as well?! + return self - def __set( - self, - predicate: URI, - value: typing.Any, - #on_error: str = 'ignore', # ignore, rollback - ): + def __set(self, predicate: URI, value: typing.Any): """ """ # get normalized predicate. Raises KeyError if *pred* not in the schema. @@ -216,11 +192,9 @@ class Nodes(): else: raise errors.UnreachableError() - def _ensure_nodes( - self, - node_type: _schema.Node, - guids: typing.Iterable[URI], - ): + def _ensure_nodes(self, node_type: _schema.Node, guids: typing.Iterable[URI]): + """ + """ # check node existence guids = set(guids) existing = set(self._backend.exists(node_type, guids)) -- cgit v1.2.3 From 8ed8dbb4010a9a75cf6e61d185327825fe783776 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sun, 18 Dec 2022 14:16:40 +0100 Subject: Graph.node interface --- bsfs/graph/graph.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'bsfs/graph') diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py index 87f7a31..b7b9f1c 100644 --- a/bsfs/graph/graph.py +++ b/bsfs/graph/graph.py @@ -99,4 +99,15 @@ class Graph(): # NOTE: Nodes constructor materializes guids. return _nodes.Nodes(self._backend, self._user, 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. + + Note that the *guids* 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. + + """ + type_ = self.schema.node(node_type) + return _nodes.Nodes(self._backend, self._user, type_, {guid}) + ## EOF ## -- cgit v1.2.3