diff options
-rw-r--r-- | bsfs/graph/__init__.py | 15 | ||||
-rw-r--r-- | bsfs/graph/ac/__init__.py | 20 | ||||
-rw-r--r-- | bsfs/graph/ac/base.py | 67 | ||||
-rw-r--r-- | bsfs/graph/ac/null.py | 53 | ||||
-rw-r--r-- | bsfs/graph/graph.py | 65 | ||||
-rw-r--r-- | bsfs/graph/nodes.py | 243 | ||||
-rw-r--r-- | bsfs/triple_store/__init__.py | 20 | ||||
-rw-r--r-- | bsfs/triple_store/base.py | 128 | ||||
-rw-r--r-- | bsfs/triple_store/sparql.py | 253 | ||||
-rw-r--r-- | test/graph/__init__.py | 0 | ||||
-rw-r--r-- | test/graph/ac/__init__.py | 0 | ||||
-rw-r--r-- | test/graph/ac/test_null.py | 102 | ||||
-rw-r--r-- | test/graph/test_graph.py | 93 | ||||
-rw-r--r-- | test/graph/test_nodes.py | 361 | ||||
-rw-r--r-- | test/triple_store/__init__.py | 0 | ||||
-rw-r--r-- | test/triple_store/test_base.py | 150 | ||||
-rw-r--r-- | test/triple_store/test_sparql.py | 769 |
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 ## |