aboutsummaryrefslogtreecommitdiffstats
path: root/bsfs/graph/nodes.py
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2022-12-08 16:36:19 +0100
committerMatthias Baumgartner <dev@igsor.net>2022-12-08 16:36:19 +0100
commite8492489098ef5f8566214e083cd2c2d1d449f5a (patch)
treeaf2f31c9fd1e13502ef36b9a5db845b29a92c250 /bsfs/graph/nodes.py
parent547aa08b1f05ec0cdf725c34a7b1d1512b694063 (diff)
downloadbsfs-e8492489098ef5f8566214e083cd2c2d1d449f5a.tar.gz
bsfs-e8492489098ef5f8566214e083cd2c2d1d449f5a.tar.bz2
bsfs-e8492489098ef5f8566214e083cd2c2d1d449f5a.zip
sparql triple store and graph (nodes, mostly)
Diffstat (limited to 'bsfs/graph/nodes.py')
-rw-r--r--bsfs/graph/nodes.py243
1 files changed, 243 insertions, 0 deletions
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 ##