aboutsummaryrefslogtreecommitdiffstats
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
parent547aa08b1f05ec0cdf725c34a7b1d1512b694063 (diff)
downloadbsfs-e8492489098ef5f8566214e083cd2c2d1d449f5a.tar.gz
bsfs-e8492489098ef5f8566214e083cd2c2d1d449f5a.tar.bz2
bsfs-e8492489098ef5f8566214e083cd2c2d1d449f5a.zip
sparql triple store and graph (nodes, mostly)
-rw-r--r--bsfs/graph/__init__.py15
-rw-r--r--bsfs/graph/ac/__init__.py20
-rw-r--r--bsfs/graph/ac/base.py67
-rw-r--r--bsfs/graph/ac/null.py53
-rw-r--r--bsfs/graph/graph.py65
-rw-r--r--bsfs/graph/nodes.py243
-rw-r--r--bsfs/triple_store/__init__.py20
-rw-r--r--bsfs/triple_store/base.py128
-rw-r--r--bsfs/triple_store/sparql.py253
-rw-r--r--test/graph/__init__.py0
-rw-r--r--test/graph/ac/__init__.py0
-rw-r--r--test/graph/ac/test_null.py102
-rw-r--r--test/graph/test_graph.py93
-rw-r--r--test/graph/test_nodes.py361
-rw-r--r--test/triple_store/__init__.py0
-rw-r--r--test/triple_store/test_base.py150
-rw-r--r--test/triple_store/test_sparql.py769
17 files changed, 2339 insertions, 0 deletions
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 ##
diff --git a/bsfs/triple_store/__init__.py b/bsfs/triple_store/__init__.py
new file mode 100644
index 0000000..fb5a8a9
--- /dev/null
+++ b/bsfs/triple_store/__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 TripleStoreBase
+from .sparql import SparqlStore
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'SparqlStore',
+ 'TripleStoreBase',
+ )
+
+## EOF ##
diff --git a/bsfs/triple_store/base.py b/bsfs/triple_store/base.py
new file mode 100644
index 0000000..a2668c3
--- /dev/null
+++ b/bsfs/triple_store/base.py
@@ -0,0 +1,128 @@
+"""
+
+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
+
+# inner-module imports
+from bsfs.utils import URI, typename
+import bsfs.schema as _schema
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'TripleStoreBase',
+ )
+
+
+## code ##
+
+class TripleStoreBase(abc.ABC):
+ """
+ """
+
+ # storage's URI. None implies a temporary location.
+ uri: typing.Optional[URI] = None
+
+ def __init__(self, uri: typing.Optional[URI] = None):
+ self.uri = uri
+
+ def __hash__(self) -> int:
+ uri = self.uri if self.uri is not None else id(self)
+ return hash((type(self), uri))
+
+ def __eq__(self, other) -> bool:
+ return isinstance(other, type(self)) \
+ and (( self.uri is not None \
+ and other.uri is not None \
+ and self.uri == other.uri ) \
+ or id(self) == id(other))
+
+ def __repr__(self) -> str:
+ return f'{typename(self)}(uri={self.uri})'
+
+ def __str__(self) -> str:
+ return f'{typename(self)}(uri={self.uri})'
+
+ def is_persistent(self) -> bool:
+ """Return True if data is stored persistently."""
+ return self.uri is not None
+
+
+ @classmethod
+ @abc.abstractmethod
+ def Open(
+ cls,
+ uri: str,
+ **kwargs: typing.Any,
+ ) -> 'TripleStoreBase':
+ """Return a TripleStoreBase instance connected to *uri*."""
+
+ @abc.abstractmethod
+ def commit(self):
+ """Commit the current transaction."""
+
+ @abc.abstractmethod
+ def rollback(self):
+ """Undo changes since the last commit."""
+
+ @property
+ @abc.abstractmethod
+ def schema(self) -> _schema.Schema:
+ """Return the store's local schema."""
+
+ @schema.setter
+ def schema(self, schema: _schema.Schema):
+ """Migrate to new schema by adding or removing class definitions.
+
+ Commits before and after the migration.
+
+ Instances of removed classes will be deleted irreversably.
+ Note that modifying an existing class is not directly supported.
+ Also, it is generally discouraged, since changing definitions may
+ lead to inconsistencies across multiple clients in a distributed
+ setting. Instead, consider introducing a new class under its own
+ uri. Such a migration would look as follows:
+
+ 1. Add new class definitions.
+ 2. Create instances of the new classes and copy relevant data.
+ 3. Remove the old definitions.
+
+ To modify a class, i.e., re-use a previous uri with a new
+ class definition, you would have to migrate via temporary
+ class definitions, and thus repeat the above procedure two times.
+
+ """
+
+ @abc.abstractmethod
+ def exists(
+ self,
+ node_type: _schema.Node,
+ guids: typing.Iterable[URI],
+ ):
+ """
+ """
+
+ @abc.abstractmethod
+ def create(
+ self,
+ node_type: _schema.Node,
+ guids: typing.Iterable[URI],
+ ):
+ """Create *guid* nodes with type *subject*."""
+
+ @abc.abstractmethod
+ def set(
+ self,
+ node_type: _schema.Node, # FIXME: is the node_type even needed? Couldn't I infer from the predicate?
+ guids: typing.Iterable[URI],
+ predicate: _schema.Predicate,
+ values: typing.Iterable[typing.Any],
+ ):
+ """
+ """
+
+## EOF ##
diff --git a/bsfs/triple_store/sparql.py b/bsfs/triple_store/sparql.py
new file mode 100644
index 0000000..3eab869
--- /dev/null
+++ b/bsfs/triple_store/sparql.py
@@ -0,0 +1,253 @@
+"""
+
+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 typing
+import rdflib
+
+# bsfs imports
+from bsfs.utils import URI
+from bsfs.utils import errors
+import bsfs.schema as _schema
+
+# inner-module imports
+from . import base
+
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'SparqlStore',
+ )
+
+
+## code ##
+
+class Transaction():
+ """Lightweight rdflib transactions for in-memory databases."""
+
+ def __init__(self, graph):
+ self._graph = graph
+ self.commit() # initialize
+
+ def commit(self):
+ self._added = []
+ self._removed = []
+
+ def rollback(self):
+ for triple in self._added:
+ self._graph.remove(triple)
+ for triple in self._removed:
+ self._graph.add(triple)
+
+ def add(self, triple):
+ if triple not in self._graph:
+ self._added.append(triple)
+ self._graph.add(triple)
+
+ def remove(self, triple):
+ if triple in self._graph:
+ self._removed.append(triple)
+ self._graph.remove(triple)
+
+
+class SparqlStore(base.TripleStoreBase):
+ """
+ """
+
+ def __init__(self, uri: typing.Optional[URI] = None):
+ super().__init__(uri)
+ self.graph = rdflib.Graph()
+ self.transaction = Transaction(self.graph)
+ self.__schema = _schema.Schema.Empty()
+
+ @classmethod
+ def Open(
+ cls,
+ uri: str,
+ **kwargs: typing.Any,
+ ) -> 'SparqlStore':
+ return cls(None)
+
+ def commit(self):
+ self.transaction.commit()
+
+ def rollback(self):
+ self.transaction.rollback()
+
+ @property
+ def schema(self) -> _schema.Schema:
+ """Return the current schema."""
+ return self.__schema
+
+ @schema.setter
+ def schema(self, schema: _schema.Schema):
+ """Migrate to new schema by adding or removing class definitions.
+
+ Commits before and after the migration.
+
+ Instances of removed classes will be deleted irreversably.
+ Note that modifying an existing class is not directly supported.
+ Also, it is generally discouraged, since changing definitions may
+ lead to inconsistencies across multiple clients in a distributed
+ setting. Instead, consider introducing a new class under its own
+ uri. Such a migration would look as follows:
+
+ 1. Add new class definitions.
+ 2. Create instances of the new classes and copy relevant data.
+ 3. Remove the old definitions.
+
+ To modify a class, i.e., re-use a previous uri with a new
+ class definition, you would have to migrate via temporary
+ class definitions, and thus repeat the above procedure two times.
+
+ """
+ # check args: Schema instanace
+ if not isinstance(schema, _schema.Schema):
+ raise TypeError(schema)
+ # check compatibility: No contradicting definitions
+ if not self.schema.consistent_with(schema):
+ raise errors.ConsistencyError(f'{schema} is inconsistent with {self.schema}')
+
+ # commit the current transaction
+ self.commit()
+
+ # adjust instances:
+ # nothing to do for added classes
+ # delete instances of removed classes
+
+ # get deleted classes
+ sub = self.schema - schema
+
+ # remove predicate instances
+ for pred in sub.predicates:
+ for src, trg in self.graph.subject_objects(rdflib.URIRef(pred.uri)):
+ self.transaction.remove((src, rdflib.URIRef(pred.uri), trg))
+
+ # remove node instances
+ for node in sub.nodes:
+ # iterate through node instances
+ for inst in self.graph.subjects(rdflib.RDF.type, rdflib.URIRef(node.uri)):
+ # remove triples where the instance is in the object position
+ for src, pred in self.graph.subject_predicates(inst):
+ self.transaction.remove((src, pred, inst))
+ # remove triples where the instance is in the subject position
+ for pred, trg in self.graph.predicate_objects(inst):
+ self.transaction.remove((inst, pred, trg))
+ # remove instance
+ self.transaction.remove((inst, rdflib.RDF.type, rdflib.URIRef(node.uri)))
+
+ # NOTE: Nothing to do for literals
+
+ # commit instance changes
+ self.commit()
+
+ # migrate schema
+ self.__schema = schema
+
+
+ def _has_type(self, subject: URI, node_type: _schema.Node) -> bool:
+ """Return True if *subject* is a node of class *node_type* or a subclass thereof."""
+ if node_type not in self.schema.nodes():
+ raise errors.ConsistencyError(f'{node_type} is not defined in the schema')
+
+ subject_types = list(self.graph.objects(rdflib.URIRef(subject), rdflib.RDF.type))
+ if len(subject_types) == 0:
+ return False
+ elif len(subject_types) == 1:
+ node = self.schema.node(URI(subject_types[0]))
+ if node == node_type:
+ return True
+ elif node_type in node.parents():
+ return True
+ else:
+ return False
+ else:
+ raise errors.UnreachableError()
+
+ def exists(
+ self,
+ node_type: _schema.Node,
+ guids: typing.Iterable[URI],
+ ):
+ """
+ """
+ return {subj for subj in guids if self._has_type(subj, node_type)}
+
+ def create(
+ self,
+ node_type: _schema.Node,
+ guids: typing.Iterable[URI],
+ ):
+ """
+ """
+ # check node_type
+ if node_type not in self.schema.nodes():
+ raise errors.ConsistencyError(f'{node_type} is not defined in the schema')
+ # check and create guids
+ for guid in guids:
+ guid = rdflib.URIRef(guid)
+ # check node existence
+ if (guid, rdflib.RDF.type, None) in self.graph:
+ # FIXME: node exists and may have a different type! ignore? raise? report?
+ continue
+ # add node
+ self.transaction.add((guid, rdflib.RDF.type, rdflib.URIRef(node_type.uri)))
+
+ def set(
+ self,
+ node_type: _schema.Node, # FIXME: is the node_type even needed? Couldn't I infer from the predicate?
+ guids: typing.Iterable[URI],
+ predicate: _schema.Predicate,
+ values: typing.Iterable[typing.Any],
+ ):
+ # check node_type
+ if node_type not in self.schema.nodes():
+ raise errors.ConsistencyError(f'{node_type} is not defined in the schema')
+ # check predicate
+ if predicate not in self.schema.predicates():
+ raise errors.ConsistencyError(f'{predicate} is not defined in the schema')
+ if not node_type <= predicate.domain:
+ raise errors.ConsistencyError(f'{node_type} must be a subclass of {predicate.domain}')
+ # NOTE: predicate.range is in the schema since predicate is in the schema.
+ # check values
+ if len(values) == 0:
+ return
+ if predicate.unique and len(values) != 1:
+ raise ValueError(values)
+ if isinstance(predicate.range, _schema.Node):
+ values = set(values) # materialize to safeguard against iterators passed as argument
+ inconsistent = {val for val in values if not self._has_type(val, predicate.range)}
+ # catches nodes that don't exist and nodes that have an inconsistent type
+ if len(inconsistent) > 0:
+ raise errors.InstanceError(inconsistent)
+ # check guids
+ # FIXME: Fail or skip inexistent nodes?
+ guids = set(guids)
+ inconsistent = {guid for guid in guids if not self._has_type(guid, node_type)}
+ if len(inconsistent) > 0:
+ raise errors.InstanceError(inconsistent)
+
+ # add triples
+ pred = rdflib.URIRef(predicate.uri)
+ for guid, value in itertools.product(guids, values):
+ guid = rdflib.URIRef(guid)
+ # convert value
+ if isinstance(predicate.range, _schema.Literal):
+ value = rdflib.Literal(value, datatype=rdflib.URIRef(predicate.range.uri))
+ elif isinstance(predicate.range, _schema.Node):
+ value = rdflib.URIRef(value)
+ else:
+ raise errors.UnreachableError()
+ # clear triples for unique predicates
+ if predicate.unique:
+ for obj in self.graph.objects(guid, pred):
+ if obj != value:
+ self.transaction.remove((guid, pred, obj))
+ # add triple
+ self.transaction.add((guid, pred, value))
+
+## EOF ##
diff --git a/test/graph/__init__.py b/test/graph/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/graph/__init__.py
diff --git a/test/graph/ac/__init__.py b/test/graph/ac/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/graph/ac/__init__.py
diff --git a/test/graph/ac/test_null.py b/test/graph/ac/test_null.py
new file mode 100644
index 0000000..f39c9be
--- /dev/null
+++ b/test/graph/ac/test_null.py
@@ -0,0 +1,102 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import unittest
+
+# bsie imports
+from bsfs import schema as _schema
+from bsfs.namespace import ns
+from bsfs.triple_store import SparqlStore
+from bsfs.utils import URI
+
+# objects to test
+from bsfs.graph.ac.null import NullAC
+
+
+## code ##
+
+class TestNullAC(unittest.TestCase):
+ def setUp(self):
+ self.backend = SparqlStore()
+ self.backend.schema = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bsm: <http://bsfs.ai/schema/Meta#>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Literal .
+
+ # predicates mandated by Nodes
+ bsm:t_created rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ # additionally defined predicates
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:author rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:filesize rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ ''')
+ self.user = URI('http://www.example.com/me')
+ self.p_author = self.backend.schema.predicate(ns.bse.author)
+ self.p_filesize = self.backend.schema.predicate(ns.bse.filesize)
+ self.p_tag = self.backend.schema.predicate(ns.bse.tag)
+ self.p_created = self.backend.schema.predicate(ns.bsm.t_created)
+ self.ent_type = self.backend.schema.node(ns.bsfs.Entity)
+ self.ent_ids = {URI('http://www.example.com/me/entity#1234'), URI('http://www.example.com/me/entity#4321')}
+
+ def test_is_protected_predicate(self):
+ ac = NullAC(self.backend, self.user)
+ self.assertTrue(ac.is_protected_predicate(self.p_created))
+ self.assertFalse(ac.is_protected_predicate(self.p_filesize))
+ self.assertFalse(ac.is_protected_predicate(self.p_author))
+ self.assertFalse(ac.is_protected_predicate(self.p_tag))
+
+ def test_create(self):
+ ac = NullAC(self.backend, self.user)
+ self.assertEqual(None, ac.create(self.ent_type, self.ent_ids))
+
+ def test_link_from_node(self):
+ ac = NullAC(self.backend, self.user)
+ self.assertSetEqual(self.ent_ids, ac.link_from_node(self.ent_type, self.ent_ids))
+
+ def test_link_to_node(self):
+ ac = NullAC(self.backend, self.user)
+ self.assertSetEqual(self.ent_ids, ac.link_to_node(self.ent_type, self.ent_ids))
+
+ def test_write_literal(self):
+ ac = NullAC(self.backend, self.user)
+ self.assertSetEqual(self.ent_ids, ac.write_literal(self.ent_type, self.ent_ids))
+
+ def test_createable(self):
+ ac = NullAC(self.backend, self.user)
+ self.assertSetEqual(self.ent_ids, ac.createable(self.ent_type, self.ent_ids))
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/graph/test_graph.py b/test/graph/test_graph.py
new file mode 100644
index 0000000..96046ef
--- /dev/null
+++ b/test/graph/test_graph.py
@@ -0,0 +1,93 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import unittest
+
+# bsie imports
+from bsfs import schema as _schema
+from bsfs.namespace import ns
+from bsfs.triple_store import SparqlStore
+from bsfs.utils import URI
+from bsfs.graph.nodes import Nodes
+
+# objects to test
+from bsfs.graph.graph import Graph
+
+
+## code ##
+
+class TestGraph(unittest.TestCase):
+ def setUp(self):
+ self.user = URI('http://example.com/me')
+ self.backend = SparqlStore.Open(None)
+ self.backend.schema = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ ''')
+
+ def test_str(self):
+ self.assertEqual(str(Graph(self.backend, self.user)),
+ 'Graph(SparqlStore(uri=None), http://example.com/me)')
+ self.assertEqual(repr(Graph(self.backend, self.user)),
+ 'Graph(backend=SparqlStore(uri=None), user=http://example.com/me)')
+ # str respects backend
+ class Foo(SparqlStore): pass
+ self.assertEqual(str(Graph(Foo.Open(None), self.user)),
+ 'Graph(Foo(uri=None), http://example.com/me)')
+ self.assertEqual(repr(Graph(Foo.Open(None), self.user)),
+ 'Graph(backend=Foo(uri=None), user=http://example.com/me)')
+ # str respect user
+ self.assertEqual(str(Graph(self.backend, URI('http://example.com/you'))),
+ 'Graph(SparqlStore(uri=None), http://example.com/you)')
+ self.assertEqual(repr(Graph(self.backend, URI('http://example.com/you'))),
+ 'Graph(backend=SparqlStore(uri=None), user=http://example.com/you)')
+ # str respects type
+ class Bar(Graph): pass
+ self.assertEqual(str(Bar(self.backend, self.user)),
+ 'Bar(SparqlStore(uri=None), http://example.com/me)')
+ self.assertEqual(repr(Bar(self.backend, self.user)),
+ 'Bar(backend=SparqlStore(uri=None), user=http://example.com/me)')
+
+ def test_equality(self):
+ graph = Graph(self.backend, self.user)
+ # instance is equal to itself
+ self.assertEqual(graph, graph)
+ self.assertEqual(hash(graph), hash(graph))
+ # instance is equal to a clone
+ self.assertEqual(graph, Graph(self.backend, self.user))
+ self.assertEqual(hash(graph), hash(Graph(self.backend, self.user)))
+ # equality respects backend
+ self.assertNotEqual(graph, Graph(SparqlStore.Open(None), self.user))
+ self.assertNotEqual(hash(graph), hash(Graph(SparqlStore.Open(None), self.user)))
+ # equality respects user
+ self.assertNotEqual(graph, Graph(self.backend, URI('http://example.com/you')))
+ self.assertNotEqual(hash(graph), hash(Graph(self.backend, URI('http://example.com/you'))))
+
+ def test_essentials(self):
+ graph = Graph(self.backend, self.user)
+ # schema
+ self.assertEqual(graph.schema, self.backend.schema)
+ self.assertRaises(AttributeError, setattr, graph, 'schema', None)
+
+ def test_nodes(self):
+ graph = Graph(self.backend, self.user)
+ guids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}
+ # returns a Nodes instance
+ self.assertEqual(
+ graph.nodes(ns.bsfs.Entity, guids),
+ Nodes(self.backend, self.user, graph.schema.node(ns.bsfs.Entity), guids))
+ # node_type must be in the schema
+ self.assertRaises(KeyError, graph.nodes, ns.bsfs.Invalid, guids)
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py
new file mode 100644
index 0000000..1fbd1e5
--- /dev/null
+++ b/test/graph/test_nodes.py
@@ -0,0 +1,361 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import rdflib
+import unittest
+
+# bsie imports
+from bsfs import schema as _schema
+from bsfs.namespace import ns
+from bsfs.triple_store.sparql import SparqlStore
+from bsfs.utils import errors, URI
+
+# objects to test
+from bsfs.graph.nodes import Nodes
+
+
+## code ##
+
+class TestNodes(unittest.TestCase):
+ def setUp(self):
+ # initialize backend
+ self.backend = SparqlStore()
+ self.backend.schema = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bsm: <http://bsfs.ai/schema/Meta#>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+ prefix bst: <http://bsfs.ai/schema/Tag#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+ bsfs:User rdfs:subClassOf bsfs:Node .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Literal .
+
+ # predicates mandated by Nodes
+ bsm:t_created rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ # additionally defined predicates
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:filesize rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:author rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:User ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bst:representative rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Tag ;
+ rdfs:range bsfs:Entity ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ ''')
+ # Nodes constructor args
+ self.user = URI('http://example.com/me')
+ # set args
+ self.tag_type = self.backend.schema.node(ns.bsfs.Tag)
+ self.ent_type = self.backend.schema.node(ns.bsfs.Entity)
+ self.user_type = self.backend.schema.node(ns.bsfs.User)
+ self.p_filesize = self.backend.schema.predicate(ns.bse.filesize)
+ self.p_author = self.backend.schema.predicate(ns.bse.author)
+ self.p_tag = self.backend.schema.predicate(ns.bse.tag)
+ self.p_representative = self.backend.schema.predicate(URI('http://bsfs.ai/schema/Tag#representative'))
+ self.t_created = self.backend.schema.predicate(ns.bsm.t_created)
+ self.ent_ids = {
+ URI('http://example.com/me/entity#1234'),
+ URI('http://example.com/me/entity#4321'),
+ }
+ self.tag_ids = {
+ URI('http://example.com/me/tag#1234'),
+ URI('http://example.com/me/tag#4321'),
+ }
+
+ def test_str(self):
+ # str baseline
+ nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids)
+ self.assertEqual(str(nodes), f'Nodes({self.ent_type}, {self.ent_ids})')
+ self.assertEqual(repr(nodes), f'Nodes({self.backend}, {self.user}, {self.ent_type}, {self.ent_ids})')
+ # str respects node_type
+ nodes = Nodes(self.backend, self.user, self.tag_type, self.tag_ids)
+ self.assertEqual(str(nodes), f'Nodes({self.tag_type}, {self.tag_ids})')
+ self.assertEqual(repr(nodes), f'Nodes({self.backend}, {self.user}, {self.tag_type}, {self.tag_ids})')
+ # str respects guids
+ nodes = Nodes(self.backend, self.user, self.ent_type, {URI('http://example.com/me/entity#foo')})
+ self.assertEqual(str(nodes), f'Nodes({self.ent_type}, {{\'http://example.com/me/entity#foo\'}})')
+ self.assertEqual(repr(nodes), f'Nodes({self.backend}, {self.user}, {self.ent_type}, {{\'http://example.com/me/entity#foo\'}})')
+ # repr respects backend
+ class Foo(SparqlStore): pass
+ backend = Foo.Open(None)
+ backend.schema = self.backend.schema
+ nodes = Nodes(backend, self.user, self.ent_type, self.ent_ids)
+ self.assertEqual(repr(nodes), f'Nodes({backend}, {self.user}, {self.ent_type}, {self.ent_ids})')
+ # repr respects user
+ nodes = Nodes(self.backend, URI('http://example.com/you'), self.ent_type, self.ent_ids)
+ self.assertEqual(repr(nodes), f'Nodes({self.backend}, http://example.com/you, {self.ent_type}, {self.ent_ids})')
+
+ def test_equality(self):
+ nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids)
+ # instance is equal to itself
+ self.assertEqual(nodes, nodes)
+ self.assertEqual(hash(nodes), hash(nodes))
+ # instance is equal to a clone
+ self.assertEqual(nodes, Nodes(self.backend, self.user, self.ent_type, self.ent_ids))
+ self.assertEqual(Nodes(self.backend, self.user, self.ent_type, self.ent_ids), nodes)
+ self.assertEqual(hash(nodes), hash(Nodes(self.backend, self.user, self.ent_type, self.ent_ids)))
+ # equality respects backend
+ backend = SparqlStore.Open(None)
+ backend.schema = self.backend.schema
+ self.assertNotEqual(nodes, Nodes(backend, self.user, self.ent_type, self.ent_ids))
+ self.assertNotEqual(hash(nodes), hash(Nodes(backend, self.user, self.ent_type, self.ent_ids)))
+ # equality respects user
+ self.assertNotEqual(nodes, Nodes(self.backend, URI('http://example.com/you'), self.ent_type, self.ent_ids))
+ self.assertNotEqual(hash(nodes), hash(Nodes(self.backend, URI('http://example.com/you'), self.ent_type, self.ent_ids)))
+ # equality respects node_type
+ self.assertNotEqual(nodes, Nodes(self.backend, self.user, self.tag_type, self.ent_ids))
+ self.assertNotEqual(hash(nodes), hash(Nodes(self.backend, self.user, self.tag_type, self.ent_ids)))
+ # equality respects guids
+ self.assertNotEqual(nodes, Nodes(self.backend, self.user, self.ent_type, self.tag_ids))
+ self.assertNotEqual(hash(nodes), hash(Nodes(self.backend, self.user, self.ent_type, self.tag_ids)))
+
+ def test_properties(self):
+ # node_type
+ self.assertEqual(self.ent_type, Nodes(
+ self.backend, self.user, self.ent_type, self.ent_ids).node_type)
+ self.assertEqual(self.tag_type, Nodes(
+ self.backend, self.user, self.tag_type, self.tag_ids).node_type)
+ # guids
+ self.assertSetEqual(self.ent_ids, set(Nodes(
+ self.backend, self.user, self.ent_type, self.ent_ids).guids))
+ self.assertSetEqual(self.tag_ids, set(Nodes(
+ self.backend, self.user, self.tag_type, self.tag_ids).guids))
+
+ def test__ensure_nodes(self):
+ nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids)
+
+ # missing nodes are created
+ self.assertSetEqual(self.ent_ids, nodes._ensure_nodes(self.ent_type, self.ent_ids))
+ # get creation time from backend manually
+ time_triples = list(self.backend.graph.objects(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri)))
+ t_ent_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
+ # check triples
+ self.assertSetEqual(set(self.backend.graph), {
+ # entity definitions
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ # bookkeeping
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ })
+
+ # existing nodes remain unchanged
+ self.assertSetEqual(self.ent_ids, nodes._ensure_nodes(self.ent_type, self.ent_ids))
+ self.assertSetEqual(set(self.backend.graph), {
+ # entity definitions
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ # bookkeeping
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ })
+
+ # type and guids don't need to match the node instance's members
+ self.assertSetEqual(self.tag_ids, nodes._ensure_nodes(self.tag_type, self.tag_ids))
+ # get creation time from backend manually
+ time_triples = list(self.backend.graph.objects(rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri)))
+ t_tag_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
+ # check triples
+ self.assertSetEqual(set(self.backend.graph), {
+ # previous triples
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ # new triples
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_tag_created, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_tag_created, datatype=rdflib.XSD.integer)),
+ })
+
+ def test___set(self):
+ # setup
+ nodes = Nodes(self.backend, self.user, self.ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
+ self.assertSetEqual(set(self.backend.graph), set())
+ set_ = nodes._Nodes__set
+
+ # node_type must match predicate's domain
+ self.assertRaises(errors.ConsistencyError, set_, self.p_representative.uri, self.ent_ids)
+
+ # cannot set protected predicates
+ self.assertRaises(errors.PermissionDeniedError, set_, self.t_created.uri, 1234)
+
+ # set literal value
+ set_(self.p_filesize.uri, 1234)
+ # get creation time from backend manually
+ time_triples = list(self.backend.graph.objects(rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri)))
+ t_ent_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
+ # verify triples
+ self.assertSetEqual(set(self.backend.graph), {
+ # entity definitions
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ # bookkeeping
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ # literals
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ })
+
+ # set node value
+ tags = Nodes(self.backend, self.user, self.tag_type, {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')})
+ set_(self.p_tag.uri, tags)
+ # get creation time from backend manually
+ time_triples = list(self.backend.graph.objects(rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri)))
+ t_tag_created = float(time_triples[0]) if len(time_triples) > 0 else 0.0
+ # verify triples
+ self.assertSetEqual(set(self.backend.graph), {
+ # previous values
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_ent_created, datatype=rdflib.XSD.integer)),
+ # tag definitions
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ # tag bookkeeping
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_tag_created, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.URIRef(self.t_created.uri), rdflib.Literal(t_tag_created, datatype=rdflib.XSD.integer)),
+ # entity -> tag links
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ })
+ # value must be a nodes instance
+ self.assertRaises(TypeError, set_, self.p_tag.uri, 'foobar')
+ self.assertRaises(TypeError, set_, self.p_tag.uri, self.tag_ids)
+ self.assertRaises(TypeError, set_, self.p_tag.uri, URI('http://example.com/me/tag#1234'))
+ # value's node_type must match the predicate's range
+ self.assertRaises(errors.ConsistencyError, set_, self.p_tag.uri,
+ Nodes(self.backend, self.user, self.ent_type, self.ent_ids))
+
+ def test_set(self):
+ self.assertSetEqual(set(self.backend.graph), set())
+ nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids)
+ # can set literal values
+ self.assertEqual(nodes, nodes.set(self.p_filesize.uri, 1234))
+ self.assertTrue(set(self.backend.graph).issuperset({
+ # nodes exist
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ # links exist
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ }))
+ # can set node values
+ self.assertEqual(nodes, nodes.set(self.p_tag.uri, Nodes(self.backend, self.user, self.tag_type, self.tag_ids)))
+ self.assertTrue(set(self.backend.graph).issuperset({
+ # nodes exist
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ # links exist
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ }))
+
+ # cannot set protected predicate
+ curr = set(self.backend.graph)
+ self.assertRaises(errors.PermissionDeniedError, nodes.set, self.t_created.uri, 12345)
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # predicate.domain must match node_type
+ self.assertRaises(errors.ConsistencyError, nodes.set, self.p_representative.uri, nodes)
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # value's node_type must match predicate's range
+ self.assertRaises(errors.ConsistencyError, nodes.set, self.p_tag.uri, nodes)
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # value type must match predicate's range type
+ self.assertRaises(TypeError, nodes.set, self.p_tag.uri, 'invalid')
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # cannot assing multiple values to unique predicate
+ self.assertRaises(ValueError, nodes.set, self.p_author.uri,
+ Nodes(self.backend, self.user, self.user_type, {URI('http://example.com/me/user#1234'), URI('http://example.com/me/user#4321')}))
+ self.assertSetEqual(curr, set(self.backend.graph))
+
+
+ def test_set_from_iterable(self):
+ self.assertSetEqual(set(self.backend.graph), set())
+ nodes = Nodes(self.backend, self.user, self.ent_type, self.ent_ids)
+ # can set literal and node values simultaneously
+ self.assertEqual(nodes, nodes.set_from_iterable({
+ self.p_filesize.uri: 1234,
+ self.p_tag.uri: Nodes(self.backend, self.user, self.tag_type, self.tag_ids),
+ }.items()))
+ self.assertTrue(set(self.backend.graph).issuperset({
+ # nodes exist
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Entity')),
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef('http://bsfs.ai/schema/Tag')),
+ # links exist
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(self.p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ }))
+
+ # cannot set protected predicate
+ curr = set(self.backend.graph)
+ self.assertRaises(errors.PermissionDeniedError, nodes.set_from_iterable, ((self.p_filesize.uri, 1234), (self.t_created.uri, 12345)))
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # predicate.domain must match node_type
+ self.assertRaises(errors.ConsistencyError, nodes.set_from_iterable, ((self.p_filesize.uri, 1234), (self.p_representative.uri, nodes)))
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # value's node_type must match predicate's range
+ self.assertRaises(errors.ConsistencyError, nodes.set_from_iterable, ((self.p_filesize.uri, 1234), (self.p_tag.uri, nodes)))
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # value type must match predicate's range type
+ self.assertRaises(TypeError, nodes.set_from_iterable, ((self.p_filesize.uri, 1234), (self.p_tag.uri, 'invalid')))
+ self.assertSetEqual(curr, set(self.backend.graph))
+ # cannot assing multiple values to unique predicate
+ self.assertRaises(ValueError, nodes.set_from_iterable, ((self.p_filesize.uri, 1234),
+ (self.p_author.uri, Nodes(self.backend, self.user, self.user_type, {URI('http://example.com/me/user#1234'), URI('http://example.com/me/user#4321')}))))
+ self.assertSetEqual(curr, set(self.backend.graph))
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/triple_store/__init__.py b/test/triple_store/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/triple_store/__init__.py
diff --git a/test/triple_store/test_base.py b/test/triple_store/test_base.py
new file mode 100644
index 0000000..a4b0559
--- /dev/null
+++ b/test/triple_store/test_base.py
@@ -0,0 +1,150 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import unittest
+
+# bsie imports
+from bsfs.utils import URI
+
+# objects to test
+from bsfs.triple_store.base import TripleStoreBase
+
+
+## code ##
+
+class DummyBase(TripleStoreBase):
+ @classmethod
+ def Open(cls, uri, **kwargs):
+ return cls(uri)
+
+ def commit(self):
+ pass
+
+ def rollback(self):
+ pass
+
+ @property
+ def schema(self):
+ pass
+
+ @schema.setter
+ def schema(self, schema):
+ pass
+
+ def exists(self, node_type, guids):
+ pass
+
+ def create(self, node_type, guids):
+ pass
+
+ def set(self, node_type, guids, predicate, values):
+ pass
+
+class DummyStore(DummyBase):
+ pass
+
+class DummyAlternative(DummyBase):
+ pass
+
+
+class TestTripleStoreBase(unittest.TestCase):
+
+ def test_equality(self):
+ # identical instances are equal
+ store = DummyStore.Open(None)
+ self.assertEqual(store, store)
+ self.assertEqual(hash(store), hash(store))
+ store = DummyStore.Open(URI('http://example.com/store'))
+ self.assertEqual(store, store)
+ self.assertEqual(hash(store), hash(store))
+ # in-memory storages are not equal
+ # NOTE: Don't use
+ # >>> self.assertNotEqual(hash(DummyStore(None)), hash(DummyStore(None)))
+ # The two stores are created subsequently since each of them is deleted
+ # right after hashing. Because the two instances never exist at the same
+ # time, their id may (and typically will) be identical.
+ # This only matters when the `id` function is used, i.e. when uri=None.
+ a, b = DummyStore.Open(None), DummyStore.Open(None)
+ self.assertNotEqual(a, b)
+ self.assertNotEqual(hash(a), hash(b))
+ a, b = DummyStore.Open(None), DummyStore.Open(URI('http://example.com/store'))
+ self.assertNotEqual(a, b)
+ self.assertNotEqual(hash(a), hash(b))
+ a, b = DummyStore.Open(URI('http://example.com/store')), DummyStore.Open(None)
+ self.assertNotEqual(a, b)
+ self.assertNotEqual(hash(a), hash(b))
+ a, b = DummyStore.Open(None), DummyStore.Open(URI('http://example.com/alternative'))
+ self.assertNotEqual(a, b)
+ self.assertNotEqual(hash(a), hash(b))
+ # equality respects uri
+ self.assertEqual(
+ DummyStore.Open(URI('http://example.com/store')),
+ DummyStore.Open(URI('http://example.com/store')))
+ self.assertEqual(
+ hash(DummyStore.Open(URI('http://example.com/alternative'))),
+ hash(DummyStore.Open(URI('http://example.com/alternative'))))
+ self.assertNotEqual(
+ DummyStore.Open(URI('http://example.com/store')),
+ DummyStore.Open(URI('http://example.com/alternative')))
+ self.assertNotEqual(
+ DummyStore.Open(URI('http://example.com/store')),
+ hash(DummyStore.Open(URI('http://example.com/alternative'))))
+ # equality respects type
+ self.assertNotEqual(DummyStore.Open(None), None)
+ self.assertNotEqual(hash(DummyStore.Open(None)), hash(None))
+ self.assertNotEqual(DummyStore.Open(None), 'hello world')
+ self.assertNotEqual(hash(DummyStore.Open(None)), hash('hello world'))
+ self.assertNotEqual(DummyStore.Open(None), 1234)
+ self.assertNotEqual(hash(DummyStore.Open(None)), hash(1234))
+ class Foo(): pass
+ f = Foo()
+ self.assertNotEqual(DummyStore.Open(None), f)
+ self.assertNotEqual(hash(DummyStore.Open(None)), hash(f))
+ self.assertNotEqual(
+ DummyStore.Open(None),
+ DummyAlternative.Open(None))
+ self.assertNotEqual(
+ hash(DummyStore.Open(None)),
+ hash(DummyAlternative.Open(None)))
+
+ def test_string_conversion(self):
+ # string conversion respects uri
+ self.assertEqual('DummyStore(uri=http://example.com/store)',
+ str(DummyStore.Open(URI('http://example.com/store'))))
+ self.assertEqual('DummyStore(uri=http://example.com/store)',
+ repr(DummyStore.Open(URI('http://example.com/store'))))
+ self.assertEqual('DummyStore(uri=http://example.com/alternative)',
+ str(DummyStore.Open(URI('http://example.com/alternative'))))
+ self.assertEqual('DummyStore(uri=http://example.com/alternative)',
+ repr(DummyStore.Open(URI('http://example.com/alternative'))))
+ self.assertEqual('DummyStore(uri=None)',
+ str(DummyStore.Open(None)))
+ self.assertEqual('DummyStore(uri=None)',
+ repr(DummyStore.Open(None)))
+ # string conversion respects type
+ self.assertEqual('DummyAlternative(uri=http://example.com/store)',
+ str(DummyAlternative.Open(URI('http://example.com/store'))))
+
+ def test_uri(self):
+ # uri returns correct value
+ self.assertEqual(None,
+ DummyStore.Open(None).uri)
+ self.assertEqual(URI('http://example.com/store'),
+ DummyStore.Open(URI('http://example.com/store')).uri)
+ self.assertEqual(URI('http://example.com/alternative'),
+ DummyStore.Open(URI('http://example.com/alternative')).uri)
+ # persistence respects uri
+ self.assertFalse(DummyStore.Open(None).is_persistent())
+ self.assertTrue(DummyStore.Open(URI('http://example.com/store')).is_persistent())
+ self.assertTrue(DummyStore.Open(URI('http://example.com/alternative')).is_persistent())
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/triple_store/test_sparql.py b/test/triple_store/test_sparql.py
new file mode 100644
index 0000000..f047235
--- /dev/null
+++ b/test/triple_store/test_sparql.py
@@ -0,0 +1,769 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import rdflib
+import unittest
+
+# bsie imports
+from bsfs import schema as _schema
+from bsfs.namespace import ns
+from bsfs.utils import errors, URI
+
+# objects to test
+from bsfs.triple_store.sparql import SparqlStore
+
+
+## code ##
+
+class TestSparqlStore(unittest.TestCase):
+ def setUp(self):
+ self.schema = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+ bsfs:User rdfs:subClassOf bsfs:Node .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Literal .
+
+ # non-unique literal
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ # unique literal
+ bse:filesize rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ # non-unique node
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ # unique node
+ bse:author rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:User ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ ''')
+
+ def test_essentials(self):
+ store = SparqlStore(None)
+ # equality
+ self.assertEqual(store, store)
+ self.assertEqual(hash(store), hash(store))
+ self.assertNotEqual(store, SparqlStore(None))
+ self.assertNotEqual(hash(store), hash(SparqlStore(None)))
+ # string conversion
+ self.assertEqual(str(store), 'SparqlStore(uri=None)')
+ self.assertEqual(repr(store), 'SparqlStore(uri=None)')
+ # open
+ self.assertIsInstance(SparqlStore.Open(None), SparqlStore)
+
+
+ def test__has_type(self):
+ # setup store
+ store = SparqlStore(None)
+ store.schema = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Document rdfs:subClassOf bsfs:Entity .
+ bsfs:Image rdfs:subClassOf bsfs:Entity .
+ bsfs:PDF rdfs:subClassOf bsfs:Document .
+
+ ''')
+ # add some instances
+ store.create(store.schema.node(ns.bsfs.Entity), {URI('http://example.com/me/entity#1234')})
+ store.create(store.schema.node(ns.bsfs.Document), {URI('http://example.com/me/document#1234')})
+ store.create(store.schema.node(ns.bsfs.Image), {URI('http://example.com/me/image#1234')})
+ store.create(store.schema.node(ns.bsfs.PDF), {URI('http://example.com/me/pdf#1234')})
+
+ # node_type must be in the schema
+ self.assertRaises(errors.ConsistencyError, store._has_type, URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.Node).get_child(ns.bsfs.invalid))
+
+ # returns False on inexistent nodes
+ self.assertFalse(store._has_type(URI('http://example.com/me/entity#4321'), store.schema.node(ns.bsfs.Entity)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/document#4321'), store.schema.node(ns.bsfs.Document)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/image#4321'), store.schema.node(ns.bsfs.Image)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/pdf#4321'), store.schema.node(ns.bsfs.PDF)))
+
+ # _has_type checks direct types
+ self.assertTrue(store._has_type(URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.Entity)))
+ self.assertTrue(store._has_type(URI('http://example.com/me/document#1234'), store.schema.node(ns.bsfs.Document)))
+ self.assertTrue(store._has_type(URI('http://example.com/me/image#1234'), store.schema.node(ns.bsfs.Image)))
+ self.assertTrue(store._has_type(URI('http://example.com/me/pdf#1234'), store.schema.node(ns.bsfs.PDF)))
+
+ # _has_type checks type hierarchy
+ self.assertFalse(store._has_type(URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.Document)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.Image)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.PDF)))
+
+ self.assertTrue(store._has_type(URI('http://example.com/me/document#1234'), store.schema.node(ns.bsfs.Entity)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/document#1234'), store.schema.node(ns.bsfs.Image)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/document#1234'), store.schema.node(ns.bsfs.PDF)))
+
+ self.assertTrue(store._has_type(URI('http://example.com/me/image#1234'), store.schema.node(ns.bsfs.Entity)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/image#1234'), store.schema.node(ns.bsfs.Document)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/image#1234'), store.schema.node(ns.bsfs.PDF)))
+
+ self.assertTrue(store._has_type(URI('http://example.com/me/pdf#1234'), store.schema.node(ns.bsfs.Entity)))
+ self.assertTrue(store._has_type(URI('http://example.com/me/pdf#1234'), store.schema.node(ns.bsfs.Document)))
+ self.assertFalse(store._has_type(URI('http://example.com/me/pdf#1234'), store.schema.node(ns.bsfs.Image)))
+
+
+ def test_schema(self):
+ # setup
+ store = SparqlStore(None)
+ curr = self.schema
+ p_comment = curr.predicate(ns.bse.comment)
+ p_filesize = curr.predicate(ns.bse.filesize)
+ p_tag = curr.predicate(ns.bse.tag)
+ p_author = curr.predicate(ns.bse.author)
+
+ # migrate to an initial schema
+ store.schema = curr
+ # store has migrated
+ self.assertEqual(store.schema, curr)
+
+ # add some instances
+ ent_ids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}
+ tag_ids = {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')}
+ store.create(curr.node(ns.bsfs.Entity), ent_ids)
+ store.create(curr.node(ns.bsfs.Tag), tag_ids)
+ store.create(curr.node(ns.bsfs.User), {URI('http://example.com/me')})
+ # add some triples
+ store.set(curr.node(ns.bsfs.Entity), ent_ids, p_comment, {'foo', 'bar'})
+ store.set(curr.node(ns.bsfs.Entity), ent_ids, p_filesize, {1234})
+ store.set(curr.node(ns.bsfs.Entity), ent_ids, p_tag,
+ {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')})
+ store.set(curr.node(ns.bsfs.Entity), ent_ids, p_author,
+ {URI('http://example.com/me')})
+ # check instances
+ instances = {
+ # node instances
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ (rdflib.URIRef('http://example.com/me'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.User)),
+ # comments
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foo', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('bar', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foo', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('bar', datatype=rdflib.XSD.string)),
+ # filesize
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ # tags
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ # author
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me')),
+ }
+ self.assertSetEqual(set(store.graph), instances)
+
+ # add some classes to the schema
+ curr = curr + _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+ prefix bst: <http://bsfs.ai/schema/Tag#>
+ prefix bsc: <http://bsfs.ai/schema/Collection#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+ bsfs:Collection rdfs:subClassOf bsfs:Node .
+ xsd:boolean rdfs:subClassOf bsfs:Literal .
+
+ # literal
+ bse:shared rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:boolean ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ # node
+ bse:partOf rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Collection ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ # predicates across auxiliary node classes
+ bst:usedIn rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Tag ;
+ rdfs:range bsfs:Collection ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bsc:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Collection ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bst:principal rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Tag ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ ''')
+ # store migrated to the new schema
+ store.schema = curr
+ self.assertEqual(store.schema, curr)
+ # instances have not changed
+ self.assertSetEqual(set(store.graph), instances)
+ # add some instances of the new classes
+ p_partOf = curr.predicate(ns.bse.partOf)
+ p_shared = curr.predicate(ns.bse.shared)
+ p_usedIn = curr.predicate('http://bsfs.ai/schema/Tag#usedIn')
+ p_ctag = curr.predicate('http://bsfs.ai/schema/Collection#tag')
+ p_principal = curr.predicate('http://bsfs.ai/schema/Tag#principal')
+ store.create(curr.node(ns.bsfs.Collection), {URI('http://example.com/me/collection#1234'), URI('http://example.com/me/collection#4321')})
+ # add some more triples
+ store.set(curr.node(ns.bsfs.Entity), ent_ids, p_shared, {True})
+ store.set(curr.node(ns.bsfs.Entity), ent_ids, p_partOf,
+ {URI('http://example.com/me/collection#1234'), URI('http://example.com/me/collection#4321')})
+ store.set(curr.node(ns.bsfs.Tag), {URI('http://example.com/me/tag#1234')}, p_usedIn,
+ {URI('http://example.com/me/collection#1234')})
+ store.set(curr.node(ns.bsfs.Collection), {URI('http://example.com/me/collection#4321')}, p_ctag,
+ {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')})
+ store.set(curr.node(ns.bsfs.Tag), {URI('http://example.com/me/tag#1234')}, p_principal,
+ {URI('http://example.com/me/collection#1234')})
+ # new instances are now in the graph
+ self.assertSetEqual(set(store.graph), instances | {
+ # collections
+ (rdflib.URIRef('http://example.com/me/collection#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Collection)),
+ (rdflib.URIRef('http://example.com/me/collection#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Collection)),
+ # partOf
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_partOf.uri), rdflib.URIRef('http://example.com/me/collection#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_partOf.uri), rdflib.URIRef('http://example.com/me/collection#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_partOf.uri), rdflib.URIRef('http://example.com/me/collection#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_partOf.uri), rdflib.URIRef('http://example.com/me/collection#4321')),
+ # shared
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_shared.uri), rdflib.Literal('true', datatype=rdflib.XSD.boolean)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_shared.uri), rdflib.Literal('true', datatype=rdflib.XSD.boolean)),
+ # auxiliary node connections
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(p_usedIn.uri), rdflib.URIRef('http://example.com/me/collection#1234')),
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.URIRef(p_principal.uri), rdflib.URIRef('http://example.com/me/collection#1234')),
+ (rdflib.URIRef('http://example.com/me/collection#4321'), rdflib.URIRef(p_ctag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/collection#4321'), rdflib.URIRef(p_ctag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ })
+
+
+ # remove some classes from the schema
+ curr = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+ prefix bst: <http://bsfs.ai/schema/Tag#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+ bsfs:User rdfs:subClassOf bsfs:Node .
+
+ xsd:boolean rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Literal .
+
+ bse:filesize rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:integer ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:shared rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:boolean ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bst:principal rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Tag ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ # removed: bsfs:Collection
+ # removed: xsd:string
+ # removed: bse:comment (bsfs:Entity -> xsd:string)
+ # removed: bse:partOf (bsfs:Entity -> bsfs:Collection)
+ # removed: bse:author (bsfs:entity -> bsfs:User)
+ # removed: bst:usedIn (bsfs:Tag -> bsfs:Collection)
+ # removed: bsc:tag (bsfs:Collection -> bsfs:Tag)
+
+ ''')
+ # store migrated to the new schema
+ store.schema = curr
+ self.assertEqual(store.schema, curr)
+ # instances of old classes were removed
+ self.assertSetEqual(set(store.graph), {
+ # node instances
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ (rdflib.URIRef('http://example.com/me'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.User)),
+ # filesize
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ # tags
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ # shared
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_shared.uri), rdflib.Literal('true', datatype=rdflib.XSD.boolean)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_shared.uri), rdflib.Literal('true', datatype=rdflib.XSD.boolean)),
+ })
+
+ # can only assign schema instances
+ self.assertRaises(TypeError, setattr, store, 'schema', None)
+ self.assertRaises(TypeError, setattr, store, 'schema', 1234)
+ self.assertRaises(TypeError, setattr, store, 'schema', 'foo')
+ class Foo(): pass
+ self.assertRaises(TypeError, setattr, store, 'schema', Foo())
+
+ # cannot migrate to incompatible schema
+ invalid = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Entity . # inconsistent with previous tag definition
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ ''')
+ self.assertRaises(errors.ConsistencyError, setattr, store, 'schema', invalid)
+ invalid = _schema.Schema.from_string('''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:User rdfs:subClassOf bsfs:Node .
+
+ # inconsistent predicate
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:User;
+ bsfs:unique "false"^^xsd:boolean .
+
+ ''')
+ self.assertRaises(errors.ConsistencyError, setattr, store, 'schema', invalid)
+
+
+ def test_transaction(self):
+ # store setup
+ store = SparqlStore(None)
+ store.schema = self.schema
+ p_tag = store.schema.predicate(ns.bse.tag)
+ p_filesize = store.schema.predicate(ns.bse.filesize)
+ # prepare node types
+ ent_type = store.schema.node(ns.bsfs.Entity)
+ tag_type = store.schema.node(ns.bsfs.Tag)
+ ent_ids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}
+ tag_ids = {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')}
+ # target instances
+ instances = {
+ # node instances
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ # links
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ }
+
+ # add some data
+ store.create(ent_type, ent_ids)
+ store.create(tag_type, tag_ids)
+ store.set(ent_type, ent_ids, p_tag, tag_ids)
+ store.set(ent_type, ent_ids, p_filesize, {1234})
+ # current transaction is visible
+ self.assertSetEqual(set(store.graph), instances | {
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ })
+
+ # rollback undoes previous changes
+ store.rollback()
+ self.assertSetEqual(set(store.graph), set())
+
+ # add some data once more
+ store.create(ent_type, ent_ids)
+ store.create(tag_type, tag_ids)
+ store.set(ent_type, ent_ids, p_tag, tag_ids)
+ store.set(ent_type, ent_ids, p_filesize, {1234})
+ # current transaction is visible
+ self.assertSetEqual(set(store.graph), instances | {
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ })
+
+ # commit saves changes
+ store.commit()
+ self.assertSetEqual(set(store.graph), instances | {
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ })
+
+ # add additional data
+ store.create(ent_type, {URI('http://example.com/me/entity#hello')})
+ store.set(ent_type, {URI('http://example.com/me/entity#hello')}, p_tag, tag_ids)
+ store.set(ent_type, ent_ids, p_filesize, {4321})
+ self.assertSetEqual(set(store.graph), instances | {
+ (rdflib.URIRef('http://example.com/me/entity#hello'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#hello'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#hello'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(4321, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(4321, datatype=rdflib.XSD.integer)),
+ })
+
+ # rollback undoes only changes since last commit
+ store.rollback()
+ self.assertSetEqual(set(store.graph), instances | {
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal(1234, datatype=rdflib.XSD.integer)),
+ })
+
+
+ def test_exists(self):
+ # store setup
+ store = SparqlStore(None)
+ store.schema = self.schema
+ # prepare node types
+ ent_type = store.schema.node(ns.bsfs.Entity)
+ tag_type = store.schema.node(ns.bsfs.Tag)
+ # create node instances
+ ent_ids = {
+ URI('http://example.com/me/entity#1234'),
+ URI('http://example.com/me/entity#4321'),
+ }
+ tag_ids = {
+ URI('http://example.com/me/tag#1234'),
+ URI('http://example.com/me/tag#4321'),
+ }
+ store.create(ent_type, ent_ids)
+ store.create(tag_type, tag_ids)
+
+ # exists returns all existing nodes of the correct type
+ self.assertSetEqual(ent_ids, store.exists(ent_type, ent_ids))
+ self.assertSetEqual(tag_ids, store.exists(tag_type, tag_ids))
+ # exists returns only nodes that match the type
+ self.assertSetEqual(set(), store.exists(ent_type, tag_ids))
+ self.assertSetEqual({URI('http://example.com/me/entity#1234')}, store.exists(ent_type, {
+ URI('http://example.com/me/tag#1234'),
+ URI('http://example.com/me/entity#1234'),
+ }))
+ # exists returns only nodes that exist
+ self.assertSetEqual(set(), store.exists(ent_type, {
+ URI('http://example.com/me/entity#foo'),
+ URI('http://example.com/me/entity#bar'),
+ }))
+ self.assertSetEqual({URI('http://example.com/me/entity#1234')}, store.exists(ent_type, {
+ URI('http://example.com/me/entity#foo'),
+ URI('http://example.com/me/entity#1234'),
+ }))
+
+
+ def test_create(self):
+ # setup
+ store = SparqlStore(None)
+ store.schema = self.schema
+
+ # node type must be valid
+ self.assertRaises(errors.ConsistencyError, store.create, self.schema.node(ns.bsfs.Entity).get_child(ns.bsfs.invalid), {
+ URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
+
+ # can create some nodes
+ ent_type = store.schema.node(ns.bsfs.Entity)
+ store.create(ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
+ self.assertSetEqual(set(store.graph), {
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ })
+
+ # existing nodes are skipped
+ store.create(ent_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#5678')})
+ self.assertSetEqual(set(store.graph), {
+ # previous triples
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ # new triples
+ (rdflib.URIRef('http://example.com/me/entity#5678'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ })
+
+ # can create nodes of a different type
+ tag_type = store.schema.node(ns.bsfs.Tag)
+ store.create(tag_type, {URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')})
+ self.assertSetEqual(set(store.graph), {
+ # previous triples
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#5678'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ # new triples
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ })
+
+ # creation does not change types of existing nodes
+ tag_type = store.schema.node(ns.bsfs.Tag)
+ store.create(tag_type, {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
+ self.assertSetEqual(set(store.graph), {
+ # previous triples
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ (rdflib.URIRef('http://example.com/me/entity#5678'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Entity)),
+ # new triples
+ (rdflib.URIRef('http://example.com/me/tag#1234'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ (rdflib.URIRef('http://example.com/me/tag#4321'), rdflib.RDF.type, rdflib.URIRef(ns.bsfs.Tag)),
+ })
+
+
+ def test_set(self):
+ # store setup
+ store = SparqlStore(None)
+ store.schema = self.schema
+ # prepare node types
+ ent_type = store.schema.node(ns.bsfs.Entity)
+ user_type = store.schema.node(ns.bsfs.User)
+ tag_type = store.schema.node(ns.bsfs.Tag)
+ # prepare predicates
+ p_filesize = store.schema.predicate(ns.bse.filesize)
+ p_comment = store.schema.predicate(ns.bse.comment)
+ p_author = store.schema.predicate(ns.bse.author)
+ p_tag = store.schema.predicate(ns.bse.tag)
+ p_invalid = store.schema.predicate(ns.bsfs.Predicate).get_child(ns.bsfs.foo, range=store.schema.node(ns.bsfs.Tag))
+ # create node instances
+ ent_ids = {
+ URI('http://example.com/me/entity#1234'),
+ URI('http://example.com/me/entity#4321'),
+ }
+ tag_ids = {
+ URI('http://example.com/me/tag#1234'),
+ URI('http://example.com/me/tag#4321'),
+ URI('http://example.com/me/tag#foo'),
+ URI('http://example.com/me/tag#bar'),
+ URI('http://example.com/me/tag#foobar'),
+ URI('http://example.com/me/tag#xyz'),
+ }
+ user_ids = {
+ URI('http://example.com/me/user#1234'),
+ URI('http://example.com/me/user#4321'),
+ }
+ store.create(ent_type, ent_ids)
+ store.create(tag_type, tag_ids)
+ store.create(user_type, user_ids)
+
+ # invalid node_type is not permitted
+ self.assertRaises(errors.ConsistencyError, store.set, self.schema.node(ns.bsfs.Node).get_child(ns.bse.foo),
+ ent_ids, p_comment, {'hello world'})
+
+ # invalid predicate is not permitted
+ self.assertRaises(errors.ConsistencyError, store.set, ent_type, ent_ids, p_invalid, {'http://example.com/me/tag#1234'})
+
+ # predicate must match node_type
+ self.assertRaises(errors.ConsistencyError, store.set, tag_type, tag_ids, p_filesize, {1234})
+
+ # empty value does not change the graph
+ plen = len(store.graph)
+ store.set(ent_type, ent_ids, p_filesize, [])
+ store.set(ent_type, ent_ids, p_comment, [])
+ store.set(ent_type, ent_ids, p_author, [])
+ store.set(ent_type, ent_ids, p_tag, [])
+ self.assertEqual(plen, len(store.graph))
+
+ # cannot set multiple values on unique predicates
+ self.assertRaises(ValueError, store.set, ent_type, ent_ids, p_filesize, {1234, 4321})
+ self.assertRaises(ValueError, store.set, ent_type, ent_ids, p_author, {URI('http://example.com/me/user#1234'), URI('http://example.com/me/user#4321')})
+
+ # value nodes must exist
+ self.assertRaises(errors.InstanceError, store.set, ent_type, ent_ids, p_author, {URI('http://example.com/me/user#invalid')})
+ self.assertRaises(errors.InstanceError, store.set, ent_type, ent_ids, p_tag, {URI('http://example.com/me/tag#invalid')})
+
+ # value node types must be consistent with the predicate
+ self.assertRaises(errors.InstanceError, store.set, ent_type, ent_ids, p_author, {URI('http://example.com/me/entity#1234')})
+ self.assertRaises(errors.InstanceError, store.set, ent_type, ent_ids, p_tag, {URI('http://example.com/me/entity#1234')})
+
+ # all value nodes must exist and be consistent
+ self.assertRaises(errors.InstanceError, store.set, ent_type, ent_ids, p_tag, {
+ URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#invalid'), URI('http://example.com/me/entity#1234')})
+
+
+ # set unique literal
+ store.set(ent_type, ent_ids, p_filesize, {1234})
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+ # re-assigning the same node changes nothing
+ store.set(ent_type, ent_ids, p_filesize, {1234})
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+ # cannot set multiple unique literals
+ self.assertRaises(ValueError, store.set, ent_type, ent_ids, p_filesize, {1234, 4321}) # same test as above
+ # unique literals are overwritten by set
+ store.set(ent_type, ent_ids, p_filesize, {4321})
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('4321', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+ self.assertNotIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('4321', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+ self.assertNotIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_filesize.uri), rdflib.Literal('1234', datatype=rdflib.XSD.integer)),
+ set(store.graph))
+
+ # set non-unique literal
+ store.set(ent_type, ent_ids, p_comment, {'foobar'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foobar', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foobar', datatype=rdflib.XSD.string)),
+ }))
+ # re-assigning the same node changes nothing
+ store.set(ent_type, ent_ids, p_comment, {'foobar'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foobar', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foobar', datatype=rdflib.XSD.string)),
+ }))
+ # can set multiple non-unique literals at once
+ store.set(ent_type, ent_ids, p_comment, {'foo', 'bar'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foo', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('bar', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foo', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('bar', datatype=rdflib.XSD.string)),
+ }))
+ # non-unique literals are appended by set
+ store.set(ent_type, ent_ids, p_comment, {'hello world'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foo', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('bar', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_comment.uri), rdflib.Literal('hello world', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('foo', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('bar', datatype=rdflib.XSD.string)),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_comment.uri), rdflib.Literal('hello world', datatype=rdflib.XSD.string)),
+ }))
+
+ # set unique node
+ store.set(ent_type, ent_ids, p_author, {URI('http://example.com/me/user#1234')})
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#1234')),
+ set(store.graph))
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#1234')),
+ set(store.graph))
+ # re-assigning the same node changes nothing
+ store.set(ent_type, ent_ids, p_author, {URI('http://example.com/me/user#1234')})
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#1234')),
+ set(store.graph))
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#1234')),
+ set(store.graph))
+ # cannot set multiple unique nodes
+ self.assertRaises(ValueError, store.set, ent_type, ent_ids, p_author, {URI('http://example.com/me/user#1234'), URI('http://example.com/me/user#4321')})
+ # unique nodes are overwritten by set
+ store.set(ent_type, ent_ids, p_author, {URI('http://example.com/me/user#4321')})
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#4321')),
+ set(store.graph))
+ self.assertNotIn(
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#1234')),
+ set(store.graph))
+ self.assertIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#4321')),
+ set(store.graph))
+ self.assertNotIn(
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_author.uri), rdflib.URIRef('http://example.com/me/user#1234')),
+ set(store.graph))
+
+ # set non-unique node
+ store.set(ent_type, ent_ids, p_tag, {'http://example.com/me/tag#foobar'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#foobar')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#foobar')),
+ }))
+ # re-assigning the same node changes nothing
+ store.set(ent_type, ent_ids, p_tag, {'http://example.com/me/tag#foobar'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#foobar')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#foobar')),
+ }))
+ # can set multiple non-unique literals at once
+ store.set(ent_type, ent_ids, p_tag, {'http://example.com/me/tag#1234', 'http://example.com/me/tag#4321'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ }))
+ # non-unique nodes are appended by set
+ store.set(ent_type, ent_ids, p_tag, {'http://example.com/me/tag#foo', 'http://example.com/me/tag#bar'})
+ self.assertTrue(set(store.graph).issuperset({
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#foo')),
+ (rdflib.URIRef('http://example.com/me/entity#1234'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#bar')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#1234')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#4321')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#foo')),
+ (rdflib.URIRef('http://example.com/me/entity#4321'), rdflib.URIRef(p_tag.uri), rdflib.URIRef('http://example.com/me/tag#bar')),
+ }))
+
+ # nothing happens when no guids are given
+ plen = len(store.graph)
+ store.set(ent_type, set(), p_comment, {'xyz'})
+ store.set(ent_type, set(), p_tag, {URI('http://example.com/me/tag#xyz')})
+ self.assertEqual(plen, len(store.graph))
+
+ # guids must be instances of node_type
+ self.assertRaises(errors.InstanceError, store.set, ent_type, tag_ids, p_comment, {'xyz'})
+ # inexistent guids
+ self.assertRaises(errors.InstanceError, store.set, ent_type, {URI('http://example.com/me/entity#foobar')}, p_comment, {'xyz'})
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##