aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bsfs/apps/migrate.py6
-rw-r--r--bsfs/graph/graph.py10
-rw-r--r--bsfs/graph/resolve.py47
-rw-r--r--bsfs/graph/schema.nt3
-rw-r--r--bsfs/namespace/namespace.py2
-rw-r--r--bsfs/query/ast/filter_.py59
-rw-r--r--bsfs/query/validator.py90
-rw-r--r--bsfs/schema/__init__.py9
-rw-r--r--bsfs/schema/schema.py117
-rw-r--r--bsfs/schema/serialize.py259
-rw-r--r--bsfs/schema/types.py195
-rw-r--r--bsfs/triple_store/sparql/distance.py56
-rw-r--r--bsfs/triple_store/sparql/parse_filter.py109
-rw-r--r--bsfs/triple_store/sparql/sparql.py23
-rw-r--r--bsfs/utils/errors.py3
-rw-r--r--bsfs/utils/uuid.py7
-rw-r--r--test/apps/schema-2.nt3
-rw-r--r--test/apps/test_migrate.py10
-rw-r--r--test/graph/ac/test_null.py7
-rw-r--r--test/graph/test_graph.py24
-rw-r--r--test/graph/test_nodes.py11
-rw-r--r--test/graph/test_resolve.py24
-rw-r--r--test/namespace/test_namespace.py12
-rw-r--r--test/query/ast_test/__init__.py (renamed from test/query/ast/__init__.py)0
-rw-r--r--test/query/ast_test/test_filter_.py (renamed from test/query/ast/test_filter_.py)35
-rw-r--r--test/query/test_validator.py59
-rw-r--r--test/schema/test_schema.py322
-rw-r--r--test/schema/test_serialize.py1030
-rw-r--r--test/schema/test_types.py243
-rw-r--r--test/triple_store/sparql/test_distance.py61
-rw-r--r--test/triple_store/sparql/test_parse_filter.py62
-rw-r--r--test/triple_store/sparql/test_sparql.py59
-rw-r--r--test/utils/test_uuid.py4
33 files changed, 2329 insertions, 632 deletions
diff --git a/bsfs/apps/migrate.py b/bsfs/apps/migrate.py
index 91c1661..b9d019f 100644
--- a/bsfs/apps/migrate.py
+++ b/bsfs/apps/migrate.py
@@ -42,15 +42,15 @@ def main(argv):
graph = bsfs.Open(config)
# initialize schema
- schema = bsfs.schema.Schema.Empty()
+ schema = bsfs.schema.Schema()
if len(args.schema) == 0:
# assemble schema from standard input
- schema = schema + bsfs.schema.Schema.from_string(sys.stdin.read())
+ schema = schema + bsfs.schema.from_string(sys.stdin.read())
else:
# assemble schema from input files
for pth in args.schema:
with open(pth, mode='rt', encoding='UTF-8') as ifile:
- schema = schema + bsfs.schema.Schema.from_string(ifile.read())
+ schema = schema + bsfs.schema.from_string(ifile.read())
# migrate schema
graph.migrate(schema, not args.remove)
diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py
index f030fed..2210755 100644
--- a/bsfs/graph/graph.py
+++ b/bsfs/graph/graph.py
@@ -10,7 +10,7 @@ import typing
# bsfs imports
from bsfs.query import ast, validate
-from bsfs.schema import Schema
+from bsfs import schema as bsc
from bsfs.triple_store import TripleStoreBase
from bsfs.utils import URI, typename
@@ -67,11 +67,11 @@ class Graph():
return f'{typename(self)}({str(self._backend)}, {self._user})'
@property
- def schema(self) -> Schema:
+ def schema(self) -> bsc.Schema:
"""Return the store's local schema."""
return self._backend.schema
- def migrate(self, schema: Schema, append: bool = True) -> 'Graph':
+ def migrate(self, schema: bsc.Schema, append: bool = True) -> 'Graph':
"""Migrate the current schema to a new *schema*.
Appends to the current schema by default; control this via *append*.
@@ -79,14 +79,14 @@ class Graph():
"""
# check args
- if not isinstance(schema, Schema):
+ if not isinstance(schema, bsc.Schema):
raise TypeError(schema)
# append to current schema
if append:
schema = schema + self._backend.schema
# add Graph schema requirements
with open(os.path.join(os.path.dirname(__file__), 'schema.nt'), mode='rt', encoding='UTF-8') as ifile:
- schema = schema + Schema.from_string(ifile.read())
+ schema = schema + bsc.from_string(ifile.read())
# migrate schema in backend
# FIXME: consult access controls!
self._backend.schema = schema
diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py
index feb0855..00b778b 100644
--- a/bsfs/graph/resolve.py
+++ b/bsfs/graph/resolve.py
@@ -37,8 +37,6 @@ class Filter():
"""
- T_VERTEX = typing.Union[bsc.Node, bsc.Literal]
-
def __init__(self, schema):
self.schema = schema
@@ -47,7 +45,7 @@ class Filter():
def _parse_filter_expression(
self,
- type_: T_VERTEX,
+ type_: bsc.Vertex,
node: ast.filter.FilterExpression,
) -> ast.filter.FilterExpression:
"""Route *node* to the handler of the respective FilterExpression subclass."""
@@ -65,6 +63,8 @@ class Filter():
return self._and(type_, node)
if isinstance(node, ast.filter.Or):
return self._or(type_, node)
+ if isinstance(node, ast.filter.Distance):
+ return self._distance(type_, node)
if isinstance(node, (ast.filter.Equals, ast.filter.Substring, \
ast.filter.StartsWith, ast.filter.EndsWith)):
return self._value(type_, node)
@@ -73,7 +73,7 @@ class Filter():
# invalid node
raise errors.BackendError(f'expected filter expression, found {node}')
- def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> T_VERTEX:
+ def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> bsc.Vertex:
"""Route *node* to the handler of the respective PredicateExpression subclass."""
if isinstance(node, ast.filter.Predicate):
return self._predicate(node)
@@ -82,7 +82,7 @@ class Filter():
# invalid node
raise errors.BackendError(f'expected predicate expression, found {node}')
- def _predicate(self, node: ast.filter.Predicate) -> T_VERTEX:
+ def _predicate(self, node: ast.filter.Predicate) -> bsc.Vertex:
if not self.schema.has_predicate(node.predicate):
raise errors.ConsistencyError(f'predicate {node.predicate} is not in the schema')
pred = self.schema.predicate(node.predicate)
@@ -91,49 +91,52 @@ class Filter():
dom, rng = rng, dom
return rng
- def _one_of(self, node: ast.filter.OneOf) -> T_VERTEX:
+ def _one_of(self, node: ast.filter.OneOf) -> bsc.Vertex:
# determine domain and range types
rng = None
for pred in node:
# parse child expression
subrng = self._parse_predicate_expression(pred)
# determine the next type
- try:
- if rng is None or subrng > rng: # pick most generic range
- rng = subrng
- except TypeError as err:
- raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err
- if rng is None:
- raise errors.UnreachableError()
+ if rng is None or subrng > rng: # pick most generic range
+ rng = subrng
+ # check range consistency
+ if not subrng <= rng and not subrng >= rng:
+ raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related')
+ if not isinstance(rng, (bsc.Node, bsc.Literal)):
+ raise errors.BackendError(f'the range of node {node} is undefined')
return rng
- def _any(self, type_: T_VERTEX, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument
+ def _any(self, type_: bsc.Vertex, node: ast.filter.Any) -> ast.filter.Any: # pylint: disable=unused-argument
next_type = self._parse_predicate_expression(node.predicate)
return ast.filter.Any(node.predicate, self._parse_filter_expression(next_type, node.expr))
- def _all(self, type_: T_VERTEX, node: ast.filter.All) -> ast.filter.All: # pylint: disable=unused-argument
+ def _all(self, type_: bsc.Vertex, node: ast.filter.All) -> ast.filter.All: # pylint: disable=unused-argument
next_type = self._parse_predicate_expression(node.predicate)
return ast.filter.All(node.predicate, self._parse_filter_expression(next_type, node.expr))
- def _and(self, type_: T_VERTEX, node: ast.filter.And) -> ast.filter.And:
+ def _and(self, type_: bsc.Vertex, node: ast.filter.And) -> ast.filter.And:
return ast.filter.And({self._parse_filter_expression(type_, expr) for expr in node})
- def _or(self, type_: T_VERTEX, node: ast.filter.Or) -> ast.filter.Or:
+ def _or(self, type_: bsc.Vertex, node: ast.filter.Or) -> ast.filter.Or:
return ast.filter.Or({self._parse_filter_expression(type_, expr) for expr in node})
- def _not(self, type_: T_VERTEX, node: ast.filter.Not) -> ast.filter.Not:
+ def _not(self, type_: bsc.Vertex, node: ast.filter.Not) -> ast.filter.Not:
return ast.filter.Not(self._parse_filter_expression(type_, node.expr))
- def _has(self, type_: T_VERTEX, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument
+ def _has(self, type_: bsc.Vertex, node: ast.filter.Has) -> ast.filter.Has: # pylint: disable=unused-argument
+ return node
+
+ def _distance(self, type_: bsc.Vertex, node: ast.filter.Distance): # pylint: disable=unused-argument
return node
- def _value(self, type_: T_VERTEX, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument
+ def _value(self, type_: bsc.Vertex, node: ast.filter._Value) -> ast.filter._Value: # pylint: disable=unused-argument
return node
- def _bounded(self, type_: T_VERTEX, node: ast.filter._Bounded) -> ast.filter._Bounded: # pylint: disable=unused-argument
+ def _bounded(self, type_: bsc.Vertex, node: ast.filter._Bounded) -> ast.filter._Bounded: # pylint: disable=unused-argument
return node
- def _is(self, type_: T_VERTEX, node: ast.filter.Is) -> typing.Union[ast.filter.Or, ast.filter.Is]:
+ def _is(self, type_: bsc.Vertex, node: ast.filter.Is) -> typing.Union[ast.filter.Or, ast.filter.Is]:
# check if action is needed
if not isinstance(node.value, nodes.Nodes):
return node
diff --git a/bsfs/graph/schema.nt b/bsfs/graph/schema.nt
index 8612681..f619746 100644
--- a/bsfs/graph/schema.nt
+++ b/bsfs/graph/schema.nt
@@ -8,7 +8,8 @@ prefix bsfs: <http://bsfs.ai/schema/>
prefix bsm: <http://bsfs.ai/schema/Meta#>
# literals
-xsd:integer rdfs:subClassOf bsfs:Literal .
+bsfs:Number rdfs:subClassOf bsfs:Literal .
+xsd:integer rdfs:subClassOf bsfs:Number .
# predicates
bsm:t_created rdfs:subClassOf bsfs:Predicate ;
diff --git a/bsfs/namespace/namespace.py b/bsfs/namespace/namespace.py
index f652dcd..1d443c1 100644
--- a/bsfs/namespace/namespace.py
+++ b/bsfs/namespace/namespace.py
@@ -59,7 +59,7 @@ class Namespace():
return hash((type(self), self.prefix, self.fsep, self.psep))
def __str__(self) -> str:
- return f'{typename(self)}({self.prefix})'
+ return str(self.prefix)
def __repr__(self) -> str:
return f'{typename(self)}({self.prefix}, {self.fsep}, {self.psep})'
diff --git a/bsfs/query/ast/filter_.py b/bsfs/query/ast/filter_.py
index b129ded..2f0270c 100644
--- a/bsfs/query/ast/filter_.py
+++ b/bsfs/query/ast/filter_.py
@@ -252,8 +252,7 @@ class Has(FilterExpression):
class _Value(FilterExpression):
- """
- """
+ """Matches some value."""
# target value.
value: typing.Any
@@ -277,13 +276,13 @@ class Is(_Value):
class Equals(_Value):
"""Value matches exactly.
- NOTE: Value format must correspond to literal type; can be a string, a number, or a Node
+ NOTE: Value must correspond to literal type.
"""
class Substring(_Value):
"""Value matches a substring
- NOTE: value format must be a string
+ NOTE: value must be a string.
"""
@@ -295,9 +294,49 @@ class EndsWith(_Value):
"""Value ends with a given string."""
+class Distance(FilterExpression):
+ """Distance to a reference is (strictly) below a threshold. Assumes a Feature literal."""
+
+ # FIXME:
+ # (a) pass a node/predicate as anchor instead of a value.
+ # Then we don't need to materialize the reference.
+ # (b) pass a FilterExpression (_Bounded) instead of a threshold.
+ # Then, we could also query values greater than a threshold.
+
+ # reference value.
+ reference: typing.Any
+
+ # distance threshold.
+ threshold: float
+
+ # closed (True) or open (False) bound.
+ strict: bool
+
+ def __init__(
+ self,
+ reference: typing.Any,
+ threshold: float,
+ strict: bool = False,
+ ):
+ self.reference = reference
+ self.threshold = float(threshold)
+ self.strict = bool(strict)
+
+ def __repr__(self) -> str:
+ return f'{typename(self)}({self.reference}, {self.threshold}, {self.strict})'
+
+ def __hash__(self) -> int:
+ return hash((super().__hash__(), tuple(self.reference), self.threshold, self.strict))
+
+ def __eq__(self, other) -> bool:
+ return super().__eq__(other) \
+ and self.reference == other.reference \
+ and self.threshold == other.threshold \
+ and self.strict == other.strict
+
+
class _Bounded(FilterExpression):
- """
- """
+ """Value is bounded by a threshold. Assumes a Number literal."""
# bound.
threshold: float
@@ -327,15 +366,11 @@ class _Bounded(FilterExpression):
class LessThan(_Bounded):
- """Value is (strictly) smaller than threshold.
- NOTE: only on numerical literals
- """
+ """Value is (strictly) smaller than threshold. Assumes a Number literal."""
class GreaterThan(_Bounded):
- """Value is (strictly) larger than threshold
- NOTE: only on numerical literals
- """
+ """Value is (strictly) larger than threshold. Assumes a Number literal."""
class Predicate(PredicateExpression):
diff --git a/bsfs/query/validator.py b/bsfs/query/validator.py
index 352203a..904ac14 100644
--- a/bsfs/query/validator.py
+++ b/bsfs/query/validator.py
@@ -34,9 +34,6 @@ class Filter():
"""
- # vertex types
- T_VERTEX = typing.Union[bsc.Node, bsc.Literal] # FIXME: Shouldn't this be in the schema?
-
# schema to validate against.
schema: bsc.Schema
@@ -64,7 +61,7 @@ class Filter():
## routing methods
- def _parse_filter_expression(self, type_: T_VERTEX, node: ast.filter.FilterExpression):
+ def _parse_filter_expression(self, type_: bsc.Vertex, node: ast.filter.FilterExpression):
"""Route *node* to the handler of the respective FilterExpression subclass."""
if isinstance(node, ast.filter.Is):
return self._is(type_, node)
@@ -72,6 +69,8 @@ class Filter():
return self._not(type_, node)
if isinstance(node, ast.filter.Has):
return self._has(type_, node)
+ if isinstance(node, ast.filter.Distance):
+ return self._distance(type_, node)
if isinstance(node, (ast.filter.Any, ast.filter.All)):
return self._branch(type_, node)
if isinstance(node, (ast.filter.And, ast.filter.Or)):
@@ -83,7 +82,7 @@ class Filter():
# invalid node
raise errors.BackendError(f'expected filter expression, found {node}')
- def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> typing.Tuple[T_VERTEX, T_VERTEX]:
+ def _parse_predicate_expression(self, node: ast.filter.PredicateExpression) -> typing.Tuple[bsc.Vertex, bsc.Vertex]:
"""Route *node* to the handler of the respective PredicateExpression subclass."""
if isinstance(node, ast.filter.Predicate):
return self._predicate(node)
@@ -95,58 +94,47 @@ class Filter():
## predicate expressions
- def _predicate(self, node: ast.filter.Predicate) -> typing.Tuple[T_VERTEX, T_VERTEX]:
+ def _predicate(self, node: ast.filter.Predicate) -> typing.Tuple[bsc.Vertex, bsc.Vertex]:
# predicate exists in the schema
if not self.schema.has_predicate(node.predicate):
raise errors.ConsistencyError(f'predicate {node.predicate} is not in the schema')
# determine domain and range
pred = self.schema.predicate(node.predicate)
+ if not isinstance(pred.range, (bsc.Node, bsc.Literal)):
+ raise errors.BackendError(f'the range of predicate {pred} is undefined')
dom, rng = pred.domain, pred.range
- if rng is None:
- # FIXME: It is a design error that Predicates can have a None range...
- raise errors.BackendError(f'predicate {pred} has no range')
if node.reverse:
dom, rng = rng, dom # type: ignore [assignment] # variable re-use confuses mypy
# return domain and range
return dom, rng
- def _one_of(self, node: ast.filter.OneOf) -> typing.Tuple[T_VERTEX, T_VERTEX]:
+ def _one_of(self, node: ast.filter.OneOf) -> typing.Tuple[bsc.Vertex, bsc.Vertex]:
# determine domain and range types
# NOTE: select the most specific domain and the most generic range
dom, rng = None, None
for pred in node:
# parse child expression
subdom, subrng = self._parse_predicate_expression(pred)
- try:
- # determine overall domain
- if dom is None or subdom < dom: # pick most specific domain
- dom = subdom
- # domains must be related across all child expressions
- if not subdom <= dom and not subdom >= dom:
- raise errors.ConsistencyError(f'domains {subdom} and {dom} are not related')
- except TypeError as err: # compared literal vs. node
- raise errors.ConsistencyError(f'domains {subdom} and {dom} are not of the same type') from err
-
- try:
- # determine overall range
- if rng is None or subrng > rng: # pick most generic range
- rng = subrng
- # ranges must be related across all child expressions
- if not subrng <= rng and not subrng >= rng:
- raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related')
- except TypeError as err: # compared literal vs. node
- raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not of the same type') from err
- # check domain and range
- if dom is None or rng is None:
- # OneOf guarantees at least one expression, these two cases cannot happen
- raise errors.UnreachableError()
- # return domain and range
- return dom, rng
+ # determine overall domain
+ if dom is None or subdom < dom: # pick most specific domain
+ dom = subdom
+ # domains must be related across all child expressions
+ if not subdom <= dom and not subdom >= dom:
+ raise errors.ConsistencyError(f'domains {subdom} and {dom} are not related')
+ # determine overall range
+ if rng is None or subrng > rng: # pick most generic range
+ rng = subrng
+ # ranges must be related across all child expressions
+ if not subrng <= rng and not subrng >= rng:
+ raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related')
+ # OneOf guarantees at least one expression, dom and rng are always bsc.Vertex.
+ # mypy does not realize this, hence we ignore the warning.
+ return dom, rng # type: ignore [return-value]
## intermediates
- def _branch(self, type_: T_VERTEX, node: ast.filter._Branch):
+ def _branch(self, type_: bsc.Vertex, node: ast.filter._Branch):
# type is a Node
if not isinstance(type_, bsc.Node):
raise errors.ConsistencyError(f'expected a Node, found {type_}')
@@ -167,16 +155,16 @@ class Filter():
# child expression is valid
self._parse_filter_expression(rng, node.expr)
- def _agg(self, type_: T_VERTEX, node: ast.filter._Agg):
+ def _agg(self, type_: bsc.Vertex, node: ast.filter._Agg):
for expr in node:
# child expression is valid
self._parse_filter_expression(type_, expr)
- def _not(self, type_: T_VERTEX, node: ast.filter.Not):
+ def _not(self, type_: bsc.Vertex, node: ast.filter.Not):
# child expression is valid
self._parse_filter_expression(type_, node.expr)
- def _has(self, type_: T_VERTEX, node: ast.filter.Has):
+ def _has(self, type_: bsc.Vertex, node: ast.filter.Has):
# type is a Node
if not isinstance(type_, bsc.Node):
raise errors.ConsistencyError(f'expected a Node, found {type_}')
@@ -189,19 +177,30 @@ class Filter():
if not type_ <= dom:
raise errors.ConsistencyError(f'expected type {dom}, found {type_}')
# node.count is a numerical expression
- # FIXME: We have to ensure that ns.xsd.integer is always known in the schema!
- self._parse_filter_expression(self.schema.literal(ns.xsd.integer), node.count)
+ self._parse_filter_expression(self.schema.literal(ns.bsfs.Number), node.count)
+
+ def _distance(self, type_: bsc.Vertex, node: ast.filter.Distance):
+ # type is a Literal
+ if not isinstance(type_, bsc.Feature):
+ raise errors.ConsistencyError(f'expected a Feature, found {type_}')
+ # type exists in the schema
+ if type_ not in self.schema.literals():
+ raise errors.ConsistencyError(f'literal {type_} is not in the schema')
+ # reference matches type_
+ if len(node.reference) != type_.dimension:
+ raise errors.ConsistencyError(f'reference has dimension {len(node.reference)}, expected {type_.dimension}')
+ # FIXME: test dtype
## conditions
- def _is(self, type_: T_VERTEX, node: ast.filter.Is): # pylint: disable=unused-argument # (node)
+ def _is(self, type_: bsc.Vertex, node: ast.filter.Is): # pylint: disable=unused-argument # (node)
if not isinstance(type_, bsc.Node):
raise errors.ConsistencyError(f'expected a Node, found {type_}')
if type_ not in self.schema.nodes():
raise errors.ConsistencyError(f'node {type_} is not in the schema')
- def _value(self, type_: T_VERTEX, node: ast.filter._Value): # pylint: disable=unused-argument # (node)
+ def _value(self, type_: bsc.Vertex, node: ast.filter._Value): # pylint: disable=unused-argument # (node)
# type is a literal
if not isinstance(type_, bsc.Literal):
raise errors.ConsistencyError(f'expected a Literal, found {type_}')
@@ -211,13 +210,16 @@ class Filter():
# FIXME: Check if node.value corresponds to type_
# FIXME: A specific literal might be requested (i.e., a numeric type when used in Has)
- def _bounded(self, type_: T_VERTEX, node: ast.filter._Bounded): # pylint: disable=unused-argument # (node)
+ def _bounded(self, type_: bsc.Vertex, node: ast.filter._Bounded): # pylint: disable=unused-argument # (node)
# type is a literal
if not isinstance(type_, bsc.Literal):
raise errors.ConsistencyError(f'expected a Literal, found {type_}')
# type exists in the schema
if type_ not in self.schema.literals():
raise errors.ConsistencyError(f'literal {type_} is not in the schema')
+ # type must be a numerical
+ if not type_ <= self.schema.literal(ns.bsfs.Number):
+ raise errors.ConsistencyError(f'expected a number type, found {type_}')
# FIXME: Check if node.value corresponds to type_
diff --git a/bsfs/schema/__init__.py b/bsfs/schema/__init__.py
index ad4d456..f53512e 100644
--- a/bsfs/schema/__init__.py
+++ b/bsfs/schema/__init__.py
@@ -9,7 +9,12 @@ import typing
# inner-module imports
from .schema import Schema
-from .types import Literal, Node, Predicate
+from .serialize import from_string, to_string
+from .types import Literal, Node, Predicate, Vertex, Feature, \
+ ROOT_VERTEX, ROOT_NODE, ROOT_LITERAL, \
+ ROOT_NUMBER, ROOT_TIME, \
+ ROOT_ARRAY, ROOT_FEATURE, \
+ ROOT_PREDICATE
# exports
__all__: typing.Sequence[str] = (
@@ -17,6 +22,8 @@ __all__: typing.Sequence[str] = (
'Node',
'Predicate',
'Schema',
+ 'from_string',
+ 'to_string',
)
## EOF ##
diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py
index c5d4571..8d9a821 100644
--- a/bsfs/schema/schema.py
+++ b/bsfs/schema/schema.py
@@ -7,10 +7,8 @@ Author: Matthias Baumgartner, 2022
# imports
from collections import abc, namedtuple
import typing
-import rdflib
# bsfs imports
-from bsfs.namespace import ns
from bsfs.utils import errors, URI, typename
# inner-module imports
@@ -51,11 +49,13 @@ class Schema():
def __init__(
self,
- predicates: typing.Iterable[types.Predicate],
+ predicates: typing.Optional[typing.Iterable[types.Predicate]] = None,
nodes: typing.Optional[typing.Iterable[types.Node]] = None,
literals: typing.Optional[typing.Iterable[types.Literal]] = None,
):
# materialize arguments
+ if predicates is None:
+ predicates = set()
if nodes is None:
nodes = set()
if literals is None:
@@ -63,24 +63,40 @@ class Schema():
nodes = set(nodes)
literals = set(literals)
predicates = set(predicates)
+
+ # add root types to the schema
+ nodes.add(types.ROOT_NODE)
+ literals.add(types.ROOT_LITERAL)
+ predicates.add(types.ROOT_PREDICATE)
+ # add minimally necessary types to the schema
+ literals.add(types.ROOT_NUMBER)
+ literals.add(types.ROOT_TIME)
+ literals.add(types.ROOT_ARRAY)
+ literals.add(types.ROOT_FEATURE)
+
+ # FIXME: ensure that types derive from the right root?
+
# include parents in predicates set
# TODO: review type annotations and ignores for python >= 3.11 (parents is _Type but should be typing.Self)
predicates |= {par for pred in predicates for par in pred.parents()} # type: ignore [misc]
# include predicate domain in nodes set
nodes |= {pred.domain for pred in predicates}
# include predicate range in nodes and literals sets
- prange = {pred.range for pred in predicates if pred.range is not None}
+ prange = {pred.range for pred in predicates}
nodes |= {vert for vert in prange if isinstance(vert, types.Node)}
literals |= {vert for vert in prange if isinstance(vert, types.Literal)}
+ # NOTE: ROOT_PREDICATE has a Vertex as range which is neither in nodes nor literals
+ # FIXME: with the ROOT_VERTEX missing, the schema is not complete anymore!
+
# include parents in nodes and literals sets
- # NOTE: Must be done after predicate domain/range was handled
- # so that their parents are included as well.
+ # NOTE: Must come after predicate domain/range was handled to have their parents as well.
nodes |= {par for node in nodes for par in node.parents()} # type: ignore [misc]
literals |= {par for lit in literals for par in lit.parents()} # type: ignore [misc]
# assign members
self._nodes = {node.uri: node for node in nodes}
self._literals = {lit.uri: lit for lit in literals}
self._predicates = {pred.uri: pred for pred in predicates}
+
# verify unique uris
if len(nodes) != len(self._nodes):
raise errors.ConsistencyError('inconsistent nodes')
@@ -214,6 +230,7 @@ class Schema():
>>> Schema.Union([a, b, c])
"""
+ # FIXME: copy type annotations?
if len(args) == 0:
raise TypeError('Schema.Union requires at least one argument (Schema or Iterable)')
if isinstance(args[0], cls): # args is sequence of Schema instances
@@ -295,92 +312,4 @@ class Schema():
"""Return the Literal matching the *uri*."""
return self._literals[uri]
-
- ## constructors ##
-
-
- @classmethod
- def Empty(cls) -> 'Schema': # pylint: disable=invalid-name # capitalized classmethod
- """Return a minimal Schema."""
- node = types.Node(ns.bsfs.Node, None)
- literal = types.Literal(ns.bsfs.Literal, None)
- predicate = types.Predicate(
- uri=ns.bsfs.Predicate,
- parent=None,
- domain=node,
- range=None,
- unique=False,
- )
- return cls((predicate, ), (node, ), (literal, ))
-
-
- @classmethod
- def from_string(cls, schema: str) -> 'Schema': # pylint: disable=invalid-name # capitalized classmethod
- """Load and return a Schema from a string."""
- # parse string into rdf graph
- graph = rdflib.Graph()
- graph.parse(data=schema, format='turtle')
-
- def _fetch_hierarchically(factory, curr):
- # emit current node
- yield curr
- # walk through childs
- for child in graph.subjects(rdflib.URIRef(ns.rdfs.subClassOf), rdflib.URIRef(curr.uri)):
- # convert to URI
- child = URI(child)
- # check circular dependency
- if child == curr.uri or child in {node.uri for node in curr.parents()}:
- raise errors.ConsistencyError('circular dependency')
- # recurse and emit (sub*)childs
- yield from _fetch_hierarchically(factory, factory(child, curr))
-
- # fetch nodes
- nodes = set(_fetch_hierarchically(types.Node, types.Node(ns.bsfs.Node, None)))
- nodes_lut = {node.uri: node for node in nodes}
- if len(nodes_lut) != len(nodes):
- raise errors.ConsistencyError('inconsistent nodes')
-
- # fetch literals
- literals = set(_fetch_hierarchically(types.Literal, types.Literal(ns.bsfs.Literal, None)))
- literals_lut = {lit.uri: lit for lit in literals}
- if len(literals_lut) != len(literals):
- raise errors.ConsistencyError('inconsistent literals')
-
- # fetch predicates
- def build_predicate(uri, parent):
- uri = rdflib.URIRef(uri)
- # get domain
- domains = set(graph.objects(uri, rdflib.RDFS.domain))
- if len(domains) != 1:
- raise errors.ConsistencyError(f'inconsistent domain: {domains}')
- dom = nodes_lut.get(next(iter(domains)))
- if dom is None:
- raise errors.ConsistencyError('missing domain')
- # get range
- ranges = set(graph.objects(uri, rdflib.RDFS.range))
- if len(ranges) != 1:
- raise errors.ConsistencyError(f'inconsistent range: {ranges}')
- rng = next(iter(ranges))
- rng = nodes_lut.get(rng, literals_lut.get(rng))
- if rng is None:
- raise errors.ConsistencyError('missing range')
- # get unique flag
- uniques = set(graph.objects(uri, rdflib.URIRef(ns.bsfs.unique)))
- if len(uniques) != 1:
- raise errors.ConsistencyError(f'inconsistent unique flags: {uniques}')
- unique = bool(next(iter(uniques)))
- # build Predicate
- return types.Predicate(URI(uri), parent, dom, rng, unique)
-
- root_predicate = types.Predicate(
- uri=ns.bsfs.Predicate,
- parent=None,
- domain=nodes_lut[ns.bsfs.Node],
- range=None, # FIXME: Unclear how to handle this! Can be either a Literal or a Node
- unique=False,
- )
- predicates = _fetch_hierarchically(build_predicate, root_predicate)
- # return Schema
- return cls(predicates, nodes, literals)
-
## EOF ##
diff --git a/bsfs/schema/serialize.py b/bsfs/schema/serialize.py
new file mode 100644
index 0000000..acc009a
--- /dev/null
+++ b/bsfs/schema/serialize.py
@@ -0,0 +1,259 @@
+"""
+
+Part of the BlackStar filesystem (bsfs) module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import itertools
+import typing
+
+# external imports
+import rdflib
+
+# bsfs imports
+from bsfs.namespace import ns
+from bsfs.utils import errors, URI, typename
+
+# inner-module imports
+from . import types
+from . import schema
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'to_string',
+ 'from_string',
+ )
+
+
+## code ##
+
+def from_string(schema_str: str) -> schema.Schema:
+ """Load and return a Schema from a string."""
+ # parse string into rdf graph
+ graph = rdflib.Graph()
+ graph.parse(data=schema_str, format='turtle')
+
+ # helper functions
+ def _fetch_value(
+ subject: URI,
+ predicate: rdflib.URIRef,
+ value_factory: typing.Callable[[typing.Any], typing.Any],
+ ) -> typing.Optional[typing.Any]:
+ """Fetch the object of a given subject and predicate.
+ Raises a `errors.ConsistencyError` if multiple objects match.
+ """
+ values = list(graph.objects(rdflib.URIRef(subject), predicate))
+ if len(values) == 0:
+ return None
+ if len(values) == 1:
+ return value_factory(values[0])
+ raise errors.ConsistencyError(
+ f'{subject} has multiple values for predicate {str(predicate)}, expected zero or one')
+
+ def _convert(value):
+ """Convert the subject type from rdflib to a bsfs native type."""
+ if isinstance(value, rdflib.Literal):
+ return value.value
+ if isinstance(value, rdflib.URIRef):
+ return URI(value)
+ # value is neither a node nor a literal, but e.g. a blank node
+ raise errors.BackendError(f'expected Literal or URIRef, found {typename(value)}')
+
+ def _fetch_hierarchically(factory, curr):
+ """Walk through a rdfs:subClassOf hierarchy, creating symbols along the way."""
+ # emit current node
+ yield curr
+ # walk through childs
+ for child in graph.subjects(rdflib.URIRef(ns.rdfs.subClassOf), rdflib.URIRef(curr.uri)):
+ # fetch annotations
+ annotations = {
+ URI(pred): _convert(value)
+ for pred, value # FIXME: preserve datatype of value?!
+ in graph.predicate_objects(child)
+ if URI(pred) != ns.rdfs.subClassOf
+ }
+ # convert child to URI
+ child = URI(child)
+ # check circular dependency
+ if child == curr.uri or child in {node.uri for node in curr.parents()}:
+ raise errors.ConsistencyError('circular dependency')
+ # recurse and emit (sub*)childs
+ yield from _fetch_hierarchically(factory, factory(child, curr, **annotations))
+
+ # fetch nodes
+ nodes = set(_fetch_hierarchically(types.Node, types.ROOT_NODE))
+ nodes_lut = {node.uri: node for node in nodes}
+ if len(nodes_lut) != len(nodes):
+ raise errors.ConsistencyError('inconsistent nodes')
+
+ # fetch literals
+ def _build_literal(uri, parent, **annotations):
+ """Literal factory."""
+ # break out on root feature type
+ if uri == types.ROOT_FEATURE.uri:
+ return types.ROOT_FEATURE
+ # handle feature types
+ if isinstance(parent, types.Feature):
+ # clean annotations
+ annotations.pop(ns.bsfs.dimension, None)
+ annotations.pop(ns.bsfs.dtype, None)
+ annotations.pop(ns.bsfs.distance, None)
+ # get dimension
+ dimension = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dimension), int)
+ # get dtype
+ dtype = _fetch_value(uri, rdflib.URIRef(ns.bsfs.dtype), URI)
+ # get distance
+ distance = _fetch_value(uri, rdflib.URIRef(ns.bsfs.distance), URI)
+ # return feature
+ return parent.child(URI(uri), dtype=dtype, dimension=dimension, distance=distance, **annotations)
+ # handle non-feature types
+ return parent.child(URI(uri), **annotations)
+
+ literals = set(_fetch_hierarchically(_build_literal, types.ROOT_LITERAL))
+ literals_lut = {lit.uri: lit for lit in literals}
+ if len(literals_lut) != len(literals):
+ raise errors.ConsistencyError('inconsistent literals')
+
+ # fetch predicates
+ def _build_predicate(uri, parent, **annotations):
+ """Predicate factory."""
+ # clean annotations
+ annotations.pop(ns.rdfs.domain, None)
+ annotations.pop(ns.rdfs.range, None)
+ annotations.pop(ns.bsfs.unique, None)
+ # get domain
+ dom = _fetch_value(uri, rdflib.RDFS.domain, URI)
+ if dom is not None and dom not in nodes_lut:
+ raise errors.ConsistencyError(f'predicate {uri} has undefined domain {dom}')
+ if dom is not None:
+ dom = nodes_lut[dom]
+ # get range
+ rng = _fetch_value(uri, rdflib.RDFS.range, URI)
+ if rng is not None and rng not in nodes_lut and rng not in literals_lut:
+ raise errors.ConsistencyError(f'predicate {uri} has undefined range {rng}')
+ if rng is not None:
+ rng = nodes_lut.get(rng, literals_lut.get(rng))
+ # get unique
+ unique = _fetch_value(uri, rdflib.URIRef(ns.bsfs.unique), bool)
+ # build predicate
+ return parent.child(URI(uri), domain=dom, range=rng, unique=unique, **annotations)
+
+ predicates = _fetch_hierarchically(_build_predicate, types.ROOT_PREDICATE)
+
+ return schema.Schema(predicates, nodes, literals)
+
+
+
+def to_string(schema_inst: schema.Schema, fmt: str = 'turtle') -> str:
+ """Serialize a `bsfs.schema.Schema` to a string.
+ See `rdflib.Graph.serialize` for viable formats (default: turtle).
+ """
+
+ # type of emitted triples.
+ T_TRIPLE = typing.Iterator[typing.Tuple[rdflib.URIRef, rdflib.URIRef, rdflib.term.Identifier]]
+
+ def _type(tpe: types._Type) -> T_TRIPLE :
+ """Emit _Type properties (parent, annotations)."""
+ # emit parent
+ if tpe.parent is not None:
+ yield (
+ rdflib.URIRef(tpe.uri),
+ rdflib.URIRef(ns.rdfs.subClassOf),
+ rdflib.URIRef(tpe.parent.uri),
+ )
+ # emit annotations
+ for prop, value in tpe.annotations.items():
+ yield (
+ rdflib.URIRef(tpe.uri),
+ rdflib.URIRef(prop),
+ rdflib.Literal(value), # FIXME: datatype?!
+ )
+
+ def _predicate(pred: types.Predicate) -> T_TRIPLE:
+ """Emit Predicate properties (domain, range, unique)."""
+ # no need to emit anything for the root predicate
+ if pred == types.ROOT_PREDICATE:
+ return
+ # emit domain
+ if pred.domain != getattr(pred.parent, 'domain', None):
+ yield (
+ rdflib.URIRef(pred.uri),
+ rdflib.URIRef(ns.rdfs.domain),
+ rdflib.URIRef(pred.domain.uri),
+ )
+ # emit range
+ if pred.range != getattr(pred.parent, 'range', None):
+ yield (
+ rdflib.URIRef(pred.uri),
+ rdflib.URIRef(ns.rdfs.range),
+ rdflib.URIRef(pred.range.uri),
+ )
+ # emit cardinality
+ if pred.unique != getattr(pred.parent, 'unique', None):
+ yield (
+ rdflib.URIRef(pred.uri),
+ rdflib.URIRef(ns.bsfs.unique),
+ rdflib.Literal(pred.unique, datatype=rdflib.XSD.boolean),
+ )
+
+ def _feature(feat: types.Feature) -> T_TRIPLE:
+ """Emit Feature properties (dimension, dtype, distance)."""
+ # emit size
+ if feat.dimension != getattr(feat.parent, 'dimension', None):
+ yield (
+ rdflib.URIRef(feat.uri),
+ rdflib.URIRef(ns.bsfs.dimension),
+ rdflib.Literal(feat.dimension, datatype=rdflib.XSD.integer),
+ )
+ # emit dtype
+ if feat.dtype != getattr(feat.parent, 'dtype', None):
+ yield (
+ rdflib.URIRef(feat.uri),
+ rdflib.URIRef(ns.bsfs.dtype),
+ rdflib.URIRef(feat.dtype),
+ )
+ # emit distance
+ if feat.distance != getattr(feat.parent, 'distance', None):
+ yield (
+ rdflib.URIRef(feat.uri),
+ rdflib.URIRef(ns.bsfs.distance),
+ rdflib.URIRef(feat.distance),
+ )
+
+ def _parse(node: types._Type) -> T_TRIPLE:
+ """Emit all properties of a type."""
+ # check arg
+ if not isinstance(node, types._Type): # pylint: disable=protected-access
+ raise TypeError(node)
+ # emit _Type essentials
+ yield from _type(node)
+ # emit properties of derived types
+ if isinstance(node, types.Predicate):
+ yield from _predicate(node)
+ if isinstance(node, types.Feature):
+ yield from _feature(node)
+
+ # create graph
+ graph = rdflib.Graph()
+ # add triples to graph
+ nodes = itertools.chain(
+ schema_inst.nodes(),
+ schema_inst.literals(),
+ schema_inst.predicates())
+ for node in nodes:
+ for triple in _parse(node):
+ graph.add(triple)
+ # add known namespaces for readability
+ # FIXME: more generically?
+ graph.bind('bse', rdflib.URIRef(ns.bse['']))
+ graph.bind('bsfs', rdflib.URIRef(ns.bsfs['']))
+ graph.bind('bsm', rdflib.URIRef(ns.bsm['']))
+ graph.bind('rdf', rdflib.URIRef(ns.rdf['']))
+ graph.bind('rdfs', rdflib.URIRef(ns.rdfs['']))
+ graph.bind('schema', rdflib.URIRef(ns.schema['']))
+ graph.bind('xsd', rdflib.URIRef(ns.xsd['']))
+ # serialize to turtle
+ return graph.serialize(format=fmt)
+
+## EOF ##
diff --git a/bsfs/schema/types.py b/bsfs/schema/types.py
index 54a7e99..3a2e10c 100644
--- a/bsfs/schema/types.py
+++ b/bsfs/schema/types.py
@@ -8,6 +8,7 @@ Author: Matthias Baumgartner, 2022
import typing
# bsfs imports
+from bsfs.namespace import ns
from bsfs.utils import errors, URI, typename
# exports
@@ -15,6 +16,7 @@ __all__: typing.Sequence[str] = (
'Literal',
'Node',
'Predicate',
+ 'Feature',
)
@@ -99,9 +101,11 @@ class _Type():
self,
uri: URI,
parent: typing.Optional['_Type'] = None,
+ **annotations: typing.Any,
):
self.uri = uri
self.parent = parent
+ self.annotations = annotations
def parents(self) -> typing.Generator['_Type', None, None]:
"""Generate a list of parent nodes."""
@@ -110,9 +114,17 @@ class _Type():
yield curr
curr = curr.parent
- def get_child(self, uri: URI, **kwargs):
+ def child(
+ self,
+ uri: URI,
+ **kwargs,
+ ):
"""Return a child of the current class."""
- return type(self)(uri, self, **kwargs)
+ return type(self)(
+ uri=uri,
+ parent=self,
+ **kwargs
+ )
def __str__(self) -> str:
return f'{typename(self)}({self.uri})'
@@ -138,8 +150,10 @@ class _Type():
def __lt__(self, other: typing.Any) -> bool:
"""Return True iff *self* is a true subclass of *other*."""
- if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
+ if not isinstance(other, _Type):
return NotImplemented
+ if not isinstance(other, type(self)): # FIXME: necessary?
+ return False
if self.uri == other.uri: # equivalence
return False
if self in other.parents(): # superclass
@@ -151,8 +165,10 @@ class _Type():
def __le__(self, other: typing.Any) -> bool:
"""Return True iff *self* is equivalent or a subclass of *other*."""
- if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
+ if not isinstance(other, _Type):
return NotImplemented
+ if not isinstance(other, type(self)): # FIXME: necessary?
+ return False
if self.uri == other.uri: # equivalence
return True
if self in other.parents(): # superclass
@@ -164,8 +180,10 @@ class _Type():
def __gt__(self, other: typing.Any) -> bool:
"""Return True iff *self* is a true superclass of *other*."""
- if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
+ if not isinstance(other, _Type):
return NotImplemented
+ if not isinstance(other, type(self)): # FIXME: necessary?
+ return False
if self.uri == other.uri: # equivalence
return False
if self in other.parents(): # superclass
@@ -177,8 +195,10 @@ class _Type():
def __ge__(self, other: typing.Any) -> bool:
"""Return True iff *self* is eqiuvalent or a superclass of *other*."""
- if not type(self) is type(other): # type mismatch # pylint: disable=unidiomatic-typecheck
+ if not isinstance(other, _Type):
return NotImplemented
+ if not isinstance(other, type(self)): # FIXME: necessary?
+ return False
if self.uri == other.uri: # equivalence
return True
if self in other.parents(): # superclass
@@ -189,32 +209,95 @@ class _Type():
return False
-class _Vertex(_Type):
+class Vertex(_Type):
"""Graph vertex types. Can be a Node or a Literal."""
- def __init__(self, uri: URI, parent: typing.Optional['_Vertex']):
- super().__init__(uri, parent)
+ parent: typing.Optional['Vertex']
+ def __init__(self, uri: URI, parent: typing.Optional['Vertex'], **kwargs):
+ super().__init__(uri, parent, **kwargs)
-class Node(_Vertex):
+class Node(Vertex):
"""Node type."""
- def __init__(self, uri: URI, parent: typing.Optional['Node']):
- super().__init__(uri, parent)
+ parent: typing.Optional['Node']
+ def __init__(self, uri: URI, parent: typing.Optional['Node'], **kwargs):
+ super().__init__(uri, parent, **kwargs)
-class Literal(_Vertex):
+class Literal(Vertex):
"""Literal type."""
- def __init__(self, uri: URI, parent: typing.Optional['Literal']):
- super().__init__(uri, parent)
+ parent: typing.Optional['Literal']
+ def __init__(self, uri: URI, parent: typing.Optional['Literal'], **kwargs):
+ super().__init__(uri, parent, **kwargs)
+
+
+class Feature(Literal):
+ """Feature type."""
+
+ # Number of feature vector dimensions.
+ dimension: int
+
+ # Feature vector datatype.
+ dtype: URI
+
+ # Distance measure to compare feature vectors.
+ distance: URI
+
+ def __init__(
+ self,
+ # Type members
+ uri: URI,
+ parent: typing.Optional[Literal],
+ # Feature members
+ dimension: int,
+ dtype: URI,
+ distance: URI,
+ **kwargs,
+ ):
+ super().__init__(uri, parent, **kwargs)
+ self.dimension = int(dimension)
+ self.dtype = URI(dtype)
+ self.distance = URI(distance)
+
+ def __hash__(self) -> int:
+ return hash((super().__hash__(), self.dimension, self.dtype, self.distance))
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return super().__eq__(other) \
+ and self.dimension == other.dimension \
+ and self.dtype == other.dtype \
+ and self.distance == other.distance
+ def child(
+ self,
+ uri: URI,
+ dimension: typing.Optional[int] = None,
+ dtype: typing.Optional[URI] = None,
+ distance: typing.Optional[URI] = None,
+ **kwargs,
+ ):
+ """Return a child of the current class."""
+ if dimension is None:
+ dimension = self.dimension
+ if dtype is None:
+ dtype = self.dtype
+ if distance is None:
+ distance = self.distance
+ return super().child(
+ uri=uri,
+ dimension=dimension,
+ dtype=dtype,
+ distance=distance,
+ **kwargs,
+ )
class Predicate(_Type):
- """Predicate type."""
+ """Predicate base type."""
# source type.
domain: Node
# destination type.
- range: typing.Optional[typing.Union[Node, Literal]]
+ range: Vertex
# maximum cardinality of type.
unique: bool
@@ -226,22 +309,23 @@ class Predicate(_Type):
parent: typing.Optional['Predicate'],
# Predicate members
domain: Node,
- range: typing.Optional[typing.Union[Node, Literal]], # pylint: disable=redefined-builtin
+ range: Vertex, # pylint: disable=redefined-builtin
unique: bool,
+ **kwargs,
):
# check arguments
if not isinstance(domain, Node):
raise TypeError(domain)
- if range is not None and not isinstance(range, Node) and not isinstance(range, Literal):
+ if range != ROOT_VERTEX and not isinstance(range, (Node, Literal)):
raise TypeError(range)
# initialize
- super().__init__(uri, parent)
+ super().__init__(uri, parent, **kwargs)
self.domain = domain
self.range = range
- self.unique = unique
+ self.unique = bool(unique)
def __hash__(self) -> int:
- return hash((super().__hash__(), self.domain, self.range, self.unique))
+ return hash((super().__hash__(), self.domain, self.unique, self.range))
def __eq__(self, other: typing.Any) -> bool:
return super().__eq__(other) \
@@ -249,11 +333,11 @@ class Predicate(_Type):
and self.range == other.range \
and self.unique == other.unique
- def get_child(
+ def child(
self,
uri: URI,
domain: typing.Optional[Node] = None,
- range: typing.Optional[_Vertex] = None, # pylint: disable=redefined-builtin
+ range: typing.Optional[Vertex] = None, # pylint: disable=redefined-builtin
unique: typing.Optional[bool] = None,
**kwargs,
):
@@ -264,13 +348,68 @@ class Predicate(_Type):
raise errors.ConsistencyError(f'{domain} must be a subclass of {self.domain}')
if range is None:
range = self.range
- if range is None: # inherited range from ns.bsfs.Predicate
- raise ValueError('range must be defined by the parent or argument')
- if self.range is not None and not range <= self.range:
+ # NOTE: The root predicate has a Vertex as range, which is neither a parent of the root
+ # Node nor Literal. Hence, that test is skipped since a child should be allowed to
+ # specialize from Vertex to anything.
+ if self.range != ROOT_VERTEX and not range <= self.range:
raise errors.ConsistencyError(f'{range} must be a subclass of {self.range}')
if unique is None:
unique = self.unique
- return super().get_child(uri, domain=domain, range=range, unique=unique, **kwargs)
+ return super().child(
+ uri=uri,
+ domain=domain,
+ range=range,
+ unique=unique,
+ **kwargs
+ )
+
+
+# essential vertices
+ROOT_VERTEX = Vertex(
+ uri=ns.bsfs.Vertex,
+ parent=None,
+ )
+
+ROOT_NODE = Node(
+ uri=ns.bsfs.Node,
+ parent=None,
+ )
+ROOT_LITERAL = Literal(
+ uri=ns.bsfs.Literal,
+ parent=None,
+ )
+
+ROOT_NUMBER = Literal(
+ uri=ns.bsfs.Number,
+ parent=ROOT_LITERAL,
+ )
+
+ROOT_TIME = Literal(
+ uri=ns.bsfs.Time,
+ parent=ROOT_LITERAL,
+ )
+
+ROOT_ARRAY = Literal(
+ uri=ns.bsfs.Array,
+ parent=ROOT_LITERAL,
+ )
+
+ROOT_FEATURE = Feature(
+ uri=ns.bsfs.Feature,
+ parent=ROOT_ARRAY,
+ dimension=1,
+ dtype=ns.bsfs.f16,
+ distance=ns.bsfs.euclidean,
+ )
+
+# essential predicates
+ROOT_PREDICATE = Predicate(
+ uri=ns.bsfs.Predicate,
+ parent=None,
+ domain=ROOT_NODE,
+ range=ROOT_VERTEX,
+ unique=False,
+ )
## EOF ##
diff --git a/bsfs/triple_store/sparql/distance.py b/bsfs/triple_store/sparql/distance.py
new file mode 100644
index 0000000..2f5387a
--- /dev/null
+++ b/bsfs/triple_store/sparql/distance.py
@@ -0,0 +1,56 @@
+"""
+
+Part of the BlackStar filesystem (bsfs) module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import typing
+
+# external imports
+import numpy as np
+
+# bsfs imports
+from bsfs.namespace import ns
+
+# constants
+EPS = 1e-9
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'DISTANCE_FU',
+ )
+
+
+## code ##
+
+def euclid(fst, snd) -> float:
+ """Euclidean distance (l2 norm)."""
+ fst = np.array(fst)
+ snd = np.array(snd)
+ return float(np.linalg.norm(fst - snd))
+
+def cosine(fst, snd) -> float:
+ """Cosine distance."""
+ fst = np.array(fst)
+ snd = np.array(snd)
+ if (fst == snd).all():
+ return 0.0
+ nrm0 = np.linalg.norm(fst)
+ nrm1 = np.linalg.norm(snd)
+ return float(1.0 - np.dot(fst, snd) / (nrm0 * nrm1 + EPS))
+
+def manhatten(fst, snd) -> float:
+ """Manhatten (cityblock) distance (l1 norm)."""
+ fst = np.array(fst)
+ snd = np.array(snd)
+ return float(np.abs(fst - snd).sum())
+
+# Known distance functions.
+DISTANCE_FU = {
+ ns.bsfs.euclidean: euclid,
+ ns.bsfs.cosine: cosine,
+ ns.bsfs.manhatten: manhatten,
+}
+
+## EOF ##
diff --git a/bsfs/triple_store/sparql/parse_filter.py b/bsfs/triple_store/sparql/parse_filter.py
index d4db0aa..8b6b976 100644
--- a/bsfs/triple_store/sparql/parse_filter.py
+++ b/bsfs/triple_store/sparql/parse_filter.py
@@ -5,19 +5,29 @@ A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2022
"""
# imports
+import operator
import typing
+# external imports
+import rdflib
+
# bsfs imports
from bsfs import schema as bsc
from bsfs.namespace import ns
from bsfs.query import ast
from bsfs.utils import URI, errors
+# inner-module imports
+from .distance import DISTANCE_FU
+
# exports
__all__: typing.Sequence[str] = (
'Filter',
)
+
+## code ##
+
class _GenHopName():
"""Generator that produces a new unique symbol name with each iteration."""
@@ -46,10 +56,8 @@ class Filter():
# Generator that produces unique symbol names.
ngen: _GenHopName
- # Vertex type.
- T_VERTEX = typing.Union[bsc.Node, bsc.Literal]
-
- def __init__(self, schema):
+ def __init__(self, graph, schema):
+ self.graph = graph
self.schema = schema
self.ngen = _GenHopName()
@@ -79,7 +87,7 @@ class Filter():
}}
'''
- def _parse_filter_expression(self, type_: T_VERTEX, node: ast.filter.FilterExpression, head: str) -> str:
+ def _parse_filter_expression(self, type_: bsc.Vertex, node: ast.filter.FilterExpression, head: str) -> str:
"""Route *node* to the handler of the respective FilterExpression subclass."""
if isinstance(node, ast.filter.Is):
return self._is(type_, node, head)
@@ -87,6 +95,8 @@ class Filter():
return self._not(type_, node, head)
if isinstance(node, ast.filter.Has):
return self._has(type_, node, head)
+ if isinstance(node, ast.filter.Distance):
+ return self._distance(type_, node, head)
if isinstance(node, ast.filter.Any):
return self._any(type_, node, head)
if isinstance(node, ast.filter.All):
@@ -112,9 +122,9 @@ class Filter():
def _parse_predicate_expression(
self,
- type_: T_VERTEX,
+ type_: bsc.Vertex,
node: ast.filter.PredicateExpression
- ) -> typing.Tuple[str, T_VERTEX]:
+ ) -> typing.Tuple[str, bsc.Vertex]:
"""Route *node* to the handler of the respective PredicateExpression subclass."""
if isinstance(node, ast.filter.Predicate):
return self._predicate(type_, node)
@@ -123,7 +133,7 @@ class Filter():
# invalid node
raise errors.BackendError(f'expected predicate expression, found {node}')
- def _one_of(self, node_type: T_VERTEX, node: ast.filter.OneOf) -> typing.Tuple[str, T_VERTEX]:
+ def _one_of(self, node_type: bsc.Vertex, node: ast.filter.OneOf) -> typing.Tuple[str, bsc.Vertex]:
"""
"""
if not isinstance(node_type, bsc.Node):
@@ -134,23 +144,18 @@ class Filter():
puri, subrng = self._parse_predicate_expression(node_type, pred)
# track predicate uris
suburi.add(puri)
- try:
- # check for more generic range
- if rng is None or subrng > rng:
- rng = subrng
- # check range consistency
- if not subrng <= rng and not subrng >= rng:
- raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related')
- except TypeError as err: # subrng and rng are not comparable
- raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related') from err
- if rng is None:
- # for mypy to be certain of the rng type
- # if rng were None, we'd have gotten a TypeError above (None > None)
- raise errors.UnreachableError()
+ # check for more generic range
+ if rng is None or subrng > rng:
+ rng = subrng
+ # check range consistency
+ if not subrng <= rng and not subrng >= rng:
+ raise errors.ConsistencyError(f'ranges {subrng} and {rng} are not related')
# return joint predicate expression and next range
- return '|'.join(suburi), rng
+ # OneOf guarantees at least one expression, rng is always a bsc.Vertex.
+ # mypy does not realize this, hence we ignore the warning.
+ return '|'.join(suburi), rng # type: ignore [return-value]
- def _predicate(self, node_type: T_VERTEX, node: ast.filter.Predicate) -> typing.Tuple[str, T_VERTEX]:
+ def _predicate(self, node_type: bsc.Vertex, node: ast.filter.Predicate) -> typing.Tuple[str, bsc.Vertex]:
"""
"""
# check node_type
@@ -162,9 +167,8 @@ class Filter():
if not self.schema.has_predicate(puri):
raise errors.ConsistencyError(f'predicate {puri} is not in the schema')
pred = self.schema.predicate(puri)
- if pred.range is None:
- # FIXME: It is a design error that Predicates can have a None range...
- raise errors.BackendError(f'predicate {pred} has no range')
+ if not isinstance(pred.range, (bsc.Node, bsc.Literal)):
+ raise errors.BackendError(f'the range of predicate {pred} is undefined')
dom, rng = pred.domain, pred.range
# encapsulate predicate uri
puri = f'<{puri}>' # type: ignore [assignment] # variable re-use confuses mypy
@@ -178,7 +182,7 @@ class Filter():
# return predicate URI and next node type
return puri, rng
- def _any(self, node_type: T_VERTEX, node: ast.filter.Any, head: str) -> str:
+ def _any(self, node_type: bsc.Vertex, node: ast.filter.Any, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Node):
@@ -191,7 +195,7 @@ class Filter():
# combine results
return f'{head} {pred} {nexthead} . {expr}'
- def _all(self, node_type: T_VERTEX, node: ast.filter.All, head: str) -> str:
+ def _all(self, node_type: bsc.Vertex, node: ast.filter.All, head: str) -> str:
"""
"""
# NOTE: All(P, E) := Not(Any(P, Not(E))) and EXISTS(P, ?)
@@ -208,13 +212,13 @@ class Filter():
# return existence and rewritten expression
return f'FILTER EXISTS {{ {head} {pred} {temphead} }} . ' + expr
- def _and(self, node_type: T_VERTEX, node: ast.filter.And, head: str) -> str:
+ def _and(self, node_type: bsc.Vertex, node: ast.filter.And, head: str) -> str:
"""
"""
sub = [self._parse_filter_expression(node_type, expr, head) for expr in node]
return ' . '.join(sub)
- def _or(self, node_type: T_VERTEX, node: ast.filter.Or, head: str) -> str:
+ def _or(self, node_type: bsc.Vertex, node: ast.filter.Or, head: str) -> str:
"""
"""
# potential special case optimization:
@@ -224,7 +228,7 @@ class Filter():
sub = ['{' + expr + '}' for expr in sub]
return ' UNION '.join(sub)
- def _not(self, node_type: T_VERTEX, node: ast.filter.Not, head: str) -> str:
+ def _not(self, node_type: bsc.Vertex, node: ast.filter.Not, head: str) -> str:
"""
"""
expr = self._parse_filter_expression(node_type, node.expr, head)
@@ -235,7 +239,7 @@ class Filter():
# The simplest (and non-interfering) choice is a type statement.
return f'MINUS {{ {head} <{ns.rdf.type}>/<{ns.rdfs.subClassOf}>* <{node_type.uri}> . {expr} }}'
- def _has(self, node_type: T_VERTEX, node: ast.filter.Has, head: str) -> str:
+ def _has(self, node_type: bsc.Vertex, node: ast.filter.Has, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Node):
@@ -248,47 +252,72 @@ class Filter():
# predicate count expression (fetch number of predicates at *head*)
num_preds = f'{{ SELECT (COUNT(distinct {inner}) as {outer}) WHERE {{ {head} {pred} {inner} }} }}'
# count expression
- # FIXME: We have to ensure that ns.xsd.integer is always known in the schema!
count_bounds = self._parse_filter_expression(self.schema.literal(ns.xsd.integer), node.count, outer)
# combine
return num_preds + ' . ' + count_bounds
- def _is(self, node_type: T_VERTEX, node: ast.filter.Is, head: str) -> str:
+ def _distance(self, node_type: bsc.Vertex, node: ast.filter.Distance, head: str) -> str:
+ """
+ """
+ if not isinstance(node_type, bsc.Feature):
+ raise errors.BackendError(f'expected Feature, found {node_type}')
+ if len(node.reference) != node_type.dimension:
+ raise errors.ConsistencyError(
+ f'reference has dimension {len(node.reference)}, expected {node_type.dimension}')
+ # get distance metric
+ dist = DISTANCE_FU[node_type.distance]
+ # get operator
+ cmp = operator.lt if node.strict else operator.le
+ # get candidate values
+ candidates = {
+ f'"{cand}"^^<{node_type.uri}>'
+ for cand
+ in self.graph.objects()
+ if isinstance(cand, rdflib.Literal)
+ and cand.datatype == rdflib.URIRef(node_type.uri)
+ and cmp(dist(cand.value, node.reference), node.threshold)
+ }
+ # combine candidate values
+ values = ' '.join(candidates) if len(candidates) else f'"impossible value"^^<{ns.xsd.string}>'
+ # return sparql fragment
+ return f'VALUES {head} {{ {values} }}'
+
+ def _is(self, node_type: bsc.Vertex, node: ast.filter.Is, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Node):
raise errors.BackendError(f'expected Node, found {node_type}')
return f'VALUES {head} {{ <{node.value}> }}'
- def _equals(self, node_type: T_VERTEX, node: ast.filter.Equals, head: str) -> str:
+ def _equals(self, node_type: bsc.Vertex, node: ast.filter.Equals, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Literal):
raise errors.BackendError(f'expected Literal, found {node}')
return f'VALUES {head} {{ "{node.value}"^^<{node_type.uri}> }}'
- def _substring(self, node_type: T_VERTEX, node: ast.filter.Substring, head: str) -> str:
+ def _substring(self, node_type: bsc.Vertex, node: ast.filter.Substring, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Literal):
raise errors.BackendError(f'expected Literal, found {node_type}')
return f'FILTER contains(str({head}), "{node.value}")'
- def _starts_with(self, node_type: T_VERTEX, node: ast.filter.StartsWith, head: str) -> str:
+ def _starts_with(self, node_type: bsc.Vertex, node: ast.filter.StartsWith, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Literal):
raise errors.BackendError(f'expected Literal, found {node_type}')
return f'FILTER strstarts(str({head}), "{node.value}")'
- def _ends_with(self, node_type: T_VERTEX, node: ast.filter.EndsWith, head: str) -> str:
+ def _ends_with(self, node_type: bsc.Vertex, node: ast.filter.EndsWith, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Literal):
raise errors.BackendError(f'expected Literal, found {node_type}')
return f'FILTER strends(str({head}), "{node.value}")'
- def _less_than(self, node_type: T_VERTEX, node: ast.filter.LessThan, head: str) -> str:
+ def _less_than(self, node_type: bsc.Vertex, node: ast.filter.LessThan, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Literal):
@@ -296,7 +325,7 @@ class Filter():
equality = '=' if not node.strict else ''
return f'FILTER ({head} <{equality} {float(node.threshold)})'
- def _greater_than(self, node_type: T_VERTEX, node: ast.filter.GreaterThan, head: str) -> str:
+ def _greater_than(self, node_type: bsc.Vertex, node: ast.filter.GreaterThan, head: str) -> str:
"""
"""
if not isinstance(node_type, bsc.Literal):
diff --git a/bsfs/triple_store/sparql/sparql.py b/bsfs/triple_store/sparql/sparql.py
index c3cbff6..fedd227 100644
--- a/bsfs/triple_store/sparql/sparql.py
+++ b/bsfs/triple_store/sparql/sparql.py
@@ -11,12 +11,14 @@ import rdflib
# bsfs imports
from bsfs import schema as bsc
+from bsfs.namespace import ns
from bsfs.query import ast
from bsfs.utils import errors, URI
# inner-module imports
from . import parse_filter
from .. import base
+from .distance import DISTANCE_FU
# exports
@@ -94,8 +96,9 @@ class SparqlStore(base.TripleStoreBase):
super().__init__(None)
self._graph = rdflib.Graph()
self._transaction = _Transaction(self._graph)
- self._schema = bsc.Schema.Empty()
- self._filter_parser = parse_filter.Filter(self._schema)
+ # NOTE: parsing bsfs.query.ast.filter.Has requires xsd:integer.
+ self._schema = bsc.Schema(literals={bsc.ROOT_NUMBER.child(ns.xsd.integer)})
+ self._filter_parser = parse_filter.Filter(self._graph, self._schema)
# NOTE: mypy and pylint complain about the **kwargs not being listed (contrasting super)
# However, not having it here is clearer since it's explicit that there are no arguments.
@@ -121,6 +124,16 @@ class SparqlStore(base.TripleStoreBase):
# check compatibility: No contradicting definitions
if not self.schema.consistent_with(schema):
raise errors.ConsistencyError(f'{schema} is inconsistent with {self.schema}')
+ # check distance functions of features
+ invalid = {
+ (cand.uri, cand.distance)
+ for cand
+ in schema.literals()
+ if isinstance(cand, bsc.Feature) and cand.distance not in DISTANCE_FU}
+ if len(invalid) > 0:
+ cand, dist = zip(*invalid)
+ raise errors.UnsupportedError(
+ f'unknown distance function {",".join(dist)} in feature {", ".join(cand)}')
# commit the current transaction
self.commit()
@@ -137,7 +150,7 @@ class SparqlStore(base.TripleStoreBase):
for src, trg in self._graph.subject_objects(rdflib.URIRef(pred.uri)):
self._transaction.remove((src, rdflib.URIRef(pred.uri), trg))
# remove predicate definition
- if pred.parent is not None:
+ if pred.parent is not None: # NOTE: there shouldn't be any predicate w/o parent
self._transaction.remove((
rdflib.URIRef(pred.uri),
rdflib.RDFS.subClassOf,
@@ -157,7 +170,7 @@ class SparqlStore(base.TripleStoreBase):
# remove instance
self._transaction.remove((inst, rdflib.RDF.type, rdflib.URIRef(node.uri)))
# remove node definition
- if node.parent is not None:
+ if node.parent is not None: # NOTE: there shouldn't be any node w/o parent
self._transaction.remove((
rdflib.URIRef(node.uri),
rdflib.RDFS.subClassOf,
@@ -166,7 +179,7 @@ class SparqlStore(base.TripleStoreBase):
for lit in sub.literals:
# remove literal definition
- if lit.parent is not None:
+ if lit.parent is not None: # NOTE: there shouldn't be any literal w/o parent
self._transaction.remove((
rdflib.URIRef(lit.uri),
rdflib.RDFS.subClassOf,
diff --git a/bsfs/utils/errors.py b/bsfs/utils/errors.py
index be9d40e..6ae6484 100644
--- a/bsfs/utils/errors.py
+++ b/bsfs/utils/errors.py
@@ -41,4 +41,7 @@ class ConfigError(_BSFSError):
class BackendError(_BSFSError):
"""Could not parse an AST structure."""
+class UnsupportedError(_BSFSError):
+ """Some requested functionality is not supported by an implementation."""
+
## EOF ##
diff --git a/bsfs/utils/uuid.py b/bsfs/utils/uuid.py
index 6366b18..ba5cf52 100644
--- a/bsfs/utils/uuid.py
+++ b/bsfs/utils/uuid.py
@@ -7,6 +7,7 @@ Author: Matthias Baumgartner, 2022
# imports
from collections import abc
import hashlib
+import json
import os
import platform
import random
@@ -105,4 +106,10 @@ class UCID():
with open(path, 'rb') as ifile:
return HASH(ifile.read()).hexdigest()
+
+ @staticmethod
+ def from_dict(content: dict) -> str:
+ """Get the content from a dict."""
+ return HASH(json.dumps(content).encode('ascii', 'ignore')).hexdigest()
+
## EOF ##
diff --git a/test/apps/schema-2.nt b/test/apps/schema-2.nt
index 525ac99..4c5468f 100644
--- a/test/apps/schema-2.nt
+++ b/test/apps/schema-2.nt
@@ -10,7 +10,8 @@ prefix bse: <http://bsfs.ai/schema/Entity#>
bsfs:Entity rdfs:subClassOf bsfs:Node .
# common definitions
-xsd:integer rdfs:subClassOf bsfs:Literal .
+bsfs:Number rdfs:subClassOf bsfs:Literal .
+xsd:integer rdfs:subClassOf bsfs:Number .
bse:filesize rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Entity ;
diff --git a/test/apps/test_migrate.py b/test/apps/test_migrate.py
index 957509a..230c032 100644
--- a/test/apps/test_migrate.py
+++ b/test/apps/test_migrate.py
@@ -13,7 +13,7 @@ import unittest
import unittest.mock
# bsie imports
-from bsfs.schema import Schema
+from bsfs import schema
# objects to test
from bsfs.apps.migrate import main
@@ -33,21 +33,21 @@ class TestMigrate(unittest.TestCase):
# read schema from file
with open(schema_1) as ifile:
- target = Schema.from_string(ifile.read())
+ target = schema.from_string(ifile.read())
graph = main([config, schema_1])
self.assertTrue(target <= graph.schema)
# read schema from multiple files
with open(schema_1) as ifile:
- target = Schema.from_string(ifile.read())
+ target = schema.from_string(ifile.read())
with open(schema_2) as ifile:
- target = target + Schema.from_string(ifile.read())
+ target = target + schema.from_string(ifile.read())
graph = main([config, schema_1, schema_2])
self.assertTrue(target <= graph.schema)
# read schema from stdin
with open(schema_1, 'rt') as ifile:
- target = Schema.from_string(ifile.read())
+ target = schema.from_string(ifile.read())
with open(schema_1, 'rt') as ifile:
with unittest.mock.patch('sys.stdin', ifile):
graph = main([config])
diff --git a/test/graph/ac/test_null.py b/test/graph/ac/test_null.py
index c863943..e35852d 100644
--- a/test/graph/ac/test_null.py
+++ b/test/graph/ac/test_null.py
@@ -8,7 +8,7 @@ Author: Matthias Baumgartner, 2022
import unittest
# bsie imports
-from bsfs import schema as _schema
+from bsfs import schema as bsc
from bsfs.namespace import ns
from bsfs.query import ast
from bsfs.triple_store import SparqlStore
@@ -23,7 +23,7 @@ from bsfs.graph.ac.null import NullAC
class TestNullAC(unittest.TestCase):
def setUp(self):
self.backend = SparqlStore()
- self.backend.schema = _schema.Schema.from_string('''
+ self.backend.schema = bsc.from_string('''
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
@@ -34,7 +34,8 @@ class TestNullAC(unittest.TestCase):
bsfs:Entity rdfs:subClassOf bsfs:Node .
bsfs:Tag rdfs:subClassOf bsfs:Node .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
# predicates mandated by Nodes
bsm:t_created rdfs:subClassOf bsfs:Predicate ;
diff --git a/test/graph/test_graph.py b/test/graph/test_graph.py
index 8503d5b..f97783b 100644
--- a/test/graph/test_graph.py
+++ b/test/graph/test_graph.py
@@ -25,7 +25,7 @@ class TestGraph(unittest.TestCase):
def setUp(self):
self.user = URI('http://example.com/me')
self.backend = SparqlStore.Open()
- self.backend.schema = schema.Schema.from_string('''
+ self.backend.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 .
@@ -118,14 +118,15 @@ class TestGraph(unittest.TestCase):
schema.Node(ns.bsfs.Node, None)))}), append=False)
# can migrate to compatible schema
- target_1 = schema.Schema.from_string('''
+ target_1 = 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 .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
bse:filename rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Entity ;
@@ -142,12 +143,13 @@ class TestGraph(unittest.TestCase):
# new schema is applied
self.assertLess(target_1, graph.schema)
# graph appends its predicates
- self.assertEqual(graph.schema, target_1 + schema.Schema.from_string('''
+ self.assertEqual(graph.schema, target_1 + 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#>
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
bsm:t_created rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Node ;
rdfs:range xsd:integer ;
@@ -155,14 +157,15 @@ class TestGraph(unittest.TestCase):
'''))
# can overwrite the current schema
- target_2 = schema.Schema.from_string('''
+ target_2 = 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 .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
bse:filename rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Entity ;
@@ -181,12 +184,13 @@ class TestGraph(unittest.TestCase):
# new schema is applied
self.assertLess(target_2, graph.schema)
# graph appends its predicates
- self.assertEqual(graph.schema, target_2 + schema.Schema.from_string('''
+ self.assertEqual(graph.schema, target_2 + 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#>
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
bsm:t_created rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Node ;
rdfs:range xsd:integer ;
@@ -196,7 +200,7 @@ class TestGraph(unittest.TestCase):
def test_get(self):
# setup
graph = Graph(self.backend, self.user)
- graph.migrate(schema.Schema.from_string('''
+ graph.migrate(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/>
diff --git a/test/graph/test_nodes.py b/test/graph/test_nodes.py
index 11ae46d..2870f35 100644
--- a/test/graph/test_nodes.py
+++ b/test/graph/test_nodes.py
@@ -24,7 +24,7 @@ class TestNodes(unittest.TestCase):
def setUp(self):
# initialize backend
self.backend = SparqlStore()
- self.backend.schema = _schema.Schema.from_string('''
+ self.backend.schema = _schema.from_string('''
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
@@ -37,7 +37,8 @@ class TestNodes(unittest.TestCase):
bsfs:Tag rdfs:subClassOf bsfs:Node .
bsfs:User rdfs:subClassOf bsfs:Node .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
# predicates mandated by Nodes
bsm:t_created rdfs:subClassOf bsfs:Predicate ;
@@ -78,7 +79,11 @@ class TestNodes(unittest.TestCase):
(rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
(rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
(rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
- (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Array), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Array)),
+ (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Time), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)),
(rdflib.URIRef(ns.bsm.t_created), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
(rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
(rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py
index 5bc99e4..0918b02 100644
--- a/test/graph/test_resolve.py
+++ b/test/graph/test_resolve.py
@@ -31,7 +31,7 @@ class TestFilter(unittest.TestCase):
"""
def test_call(self):
- schema = bsc.Schema.from_string('''
+ schema = bsc.from_string('''
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
@@ -41,7 +41,17 @@ class TestFilter(unittest.TestCase):
bsfs:Entity rdfs:subClassOf bsfs:Node .
bsfs:Tag rdfs:subClassOf bsfs:Node .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ bsfs:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array .
+ xsd:integer rdfs:subClassOf bsfs:Number .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "5"^^xsd:integer .
+
+ bse:colors rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Colors .
bse:comment rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Entity ;
@@ -65,7 +75,7 @@ class TestFilter(unittest.TestCase):
{URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
tags = graph.nodes(ns.bsfs.Tag,
{URI('http://example.com/me/tag#1234'), URI('http://example.com/me/tag#4321')})
- invalid = nodes.Nodes(None, '', schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid),
+ invalid = nodes.Nodes(None, '', schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid),
{'http://example.com/you/invalid#1234', 'http://example.com/you/invalid#4321'})
resolver = Filter(schema)
@@ -144,12 +154,18 @@ class TestFilter(unittest.TestCase):
self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
ast.filter.Has(ns.bse.comment)),
ast.filter.Has(ns.bse.comment))
+ # for sake of completeness: Distance
+ self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
+ ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4,5], 1))),
+ ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4,5], 1)))
# route errors
self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag),
ast.filter.Predicate(ns.bse.comment))
self.assertRaises(errors.BackendError, resolver, schema.node(ns.bsfs.Tag),
ast.filter.Any(ast.filter.PredicateExpression(), ast.filter.Equals('foo')))
- self.assertRaises(errors.UnreachableError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate)))
+ self.assertRaises(errors.BackendError, resolver._one_of, ast.filter.OneOf(ast.filter.Predicate(ns.bsfs.Predicate)))
+ # for sake of coverage completeness: valid OneOf
+ self.assertIsNotNone(resolver._one_of(ast.filter.OneOf(ast.filter.Predicate(ns.bse.colors))))
# check schema consistency
self.assertRaises(errors.ConsistencyError, resolver, schema.node(ns.bsfs.Tag),
diff --git a/test/namespace/test_namespace.py b/test/namespace/test_namespace.py
index f109653..2536203 100644
--- a/test/namespace/test_namespace.py
+++ b/test/namespace/test_namespace.py
@@ -20,15 +20,15 @@ from bsfs.namespace.namespace import Namespace, ClosedNamespace
class TestNamespace(unittest.TestCase):
def test_essentials(self):
# string conversion
- self.assertEqual(str(Namespace('http://example.org/')), 'Namespace(http://example.org)')
- self.assertEqual(str(Namespace('http://example.org#')), 'Namespace(http://example.org)')
+ self.assertEqual(str(Namespace('http://example.org/')), 'http://example.org')
+ self.assertEqual(str(Namespace('http://example.org#')), 'http://example.org')
self.assertEqual(repr(Namespace('http://example.org/')), 'Namespace(http://example.org, #, /)')
self.assertEqual(repr(Namespace('http://example.org#')), 'Namespace(http://example.org, #, /)')
self.assertEqual(repr(Namespace('http://example.org', fsep='.')), 'Namespace(http://example.org, ., /)')
self.assertEqual(repr(Namespace('http://example.org', psep='.')), 'Namespace(http://example.org, #, .)')
# repeated separators are truncated
- self.assertEqual(str(Namespace('http://example.org////')), 'Namespace(http://example.org)')
- self.assertEqual(str(Namespace('http://example.org####')), 'Namespace(http://example.org)')
+ self.assertEqual(str(Namespace('http://example.org////')), 'http://example.org')
+ self.assertEqual(str(Namespace('http://example.org####')), 'http://example.org')
self.assertEqual(repr(Namespace('http://example.org///##')), 'Namespace(http://example.org, #, /)')
# comparison
class Foo(Namespace): pass
@@ -83,8 +83,8 @@ class TestNamespace(unittest.TestCase):
class TestClosedNamespace(unittest.TestCase):
def test_essentials(self):
# string conversion
- self.assertEqual(str(ClosedNamespace('http://example.org/')), 'ClosedNamespace(http://example.org)')
- self.assertEqual(str(ClosedNamespace('http://example.org#')), 'ClosedNamespace(http://example.org)')
+ self.assertEqual(str(ClosedNamespace('http://example.org/')), 'http://example.org')
+ self.assertEqual(str(ClosedNamespace('http://example.org#')), 'http://example.org')
self.assertEqual(repr(ClosedNamespace('http://example.org/')), 'ClosedNamespace(http://example.org, #, /)')
self.assertEqual(repr(ClosedNamespace('http://example.org#')), 'ClosedNamespace(http://example.org, #, /)')
self.assertEqual(repr(ClosedNamespace('http://example.org', fsep='.')), 'ClosedNamespace(http://example.org, ., /)')
diff --git a/test/query/ast/__init__.py b/test/query/ast_test/__init__.py
index e69de29..e69de29 100644
--- a/test/query/ast/__init__.py
+++ b/test/query/ast_test/__init__.py
diff --git a/test/query/ast/test_filter_.py b/test/query/ast_test/test_filter_.py
index 4f69bdc..9eb92e2 100644
--- a/test/query/ast/test_filter_.py
+++ b/test/query/ast_test/test_filter_.py
@@ -15,7 +15,7 @@ from bsfs.utils import URI
from bsfs.query.ast.filter_ import _Expression, FilterExpression, PredicateExpression
from bsfs.query.ast.filter_ import _Branch, Any, All
from bsfs.query.ast.filter_ import _Agg, And, Or
-from bsfs.query.ast.filter_ import Not, Has
+from bsfs.query.ast.filter_ import Not, Has, Distance
from bsfs.query.ast.filter_ import _Value, Is, Equals, Substring, StartsWith, EndsWith
from bsfs.query.ast.filter_ import _Bounded, LessThan, GreaterThan
from bsfs.query.ast.filter_ import Predicate, OneOf
@@ -284,6 +284,39 @@ class TestValue(unittest.TestCase):
self.assertEqual(cls(f).value, f)
+class TestDistance(unittest.TestCase):
+ def test_essentials(self):
+ ref = (1,2,3)
+ # comparison
+ self.assertEqual(Distance(ref, 3), Distance(ref, 3))
+ self.assertEqual(hash(Distance(ref, 3)), hash(Distance(ref, 3)))
+ # comparison respects type
+ self.assertNotEqual(Distance(ref, 3), FilterExpression())
+ self.assertNotEqual(hash(Distance(ref, 3)), hash(FilterExpression()))
+ # comparison respects reference
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2), 3, False))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2), 3, False)))
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,5,3), 3, False))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,5,3), 3, False)))
+ # comparison respects threshold
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2,3), 3.1, False))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2,3), 3.1, False)))
+ # comparison respects strict flag
+ self.assertNotEqual(Distance((1,2,3), 3, False), Distance((1,2,3), 3, True))
+ self.assertNotEqual(hash(Distance((1,2,3), 3, False)), hash(Distance((1,2,3), 3, True)))
+ # string conversion
+ self.assertEqual(str(Distance(ref, 3, False)), 'Distance((1, 2, 3), 3.0, False)')
+ self.assertEqual(repr(Distance(ref, 3, False)), 'Distance((1, 2, 3), 3.0, False)')
+
+ def test_members(self):
+ self.assertEqual(Distance((1,2,3), 3, False).reference, (1,2,3))
+ self.assertEqual(Distance((3,2,1), 3, False).reference, (3,2,1))
+ self.assertEqual(Distance((1,2,3), 3, False).threshold, 3.0)
+ self.assertEqual(Distance((1,2,3), 53.45, False).threshold, 53.45)
+ self.assertEqual(Distance((1,2,3), 3, False).strict, False)
+ self.assertEqual(Distance((1,2,3), 3, True).strict, True)
+
+
class TestBounded(unittest.TestCase):
def test_essentials(self):
# comparison respects type
diff --git a/test/query/test_validator.py b/test/query/test_validator.py
index 4f8364a..dc9d913 100644
--- a/test/query/test_validator.py
+++ b/test/query/test_validator.py
@@ -21,7 +21,7 @@ from bsfs.query.validator import Filter
class TestFilter(unittest.TestCase):
def setUp(self):
- self.schema = _schema.Schema.from_string('''
+ self.schema = _schema.from_string('''
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
@@ -33,7 +33,19 @@ class TestFilter(unittest.TestCase):
bsfs:Tag rdfs:subClassOf bsfs:Node .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ bsfs:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array .
+ xsd:integer rdfs:subClassOf bsfs:Number .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "5"^^xsd:integer ;
+ bsfs:dtype bsfs:f32 .
+
+ bse:color rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range bsfs:Colors ;
+ bsfs:unique "true"^^xsd:boolean .
bse:comment rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Node ;
@@ -69,8 +81,8 @@ class TestFilter(unittest.TestCase):
self.assertRaises(TypeError, self.validate, '1234', None)
self.assertRaises(TypeError, self.validate, self.schema.literal(ns.bsfs.URI), None)
# root_type must exist in the schema
- self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Image), None)
- self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity).get_child(ns.bsfs.Image), None)
+ self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Image), None)
+ self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity).child(ns.bsfs.Image), None)
# valid query returns true
self.assertTrue(self.validate(self.schema.node(ns.bsfs.Entity),
ast.filter.Any(ast.filter.OneOf(ns.bse.tag, ns.bse.buddy),
@@ -85,6 +97,7 @@ class TestFilter(unittest.TestCase):
),
ast.filter.Not(ast.filter.Any(ns.bse.comment,
ast.filter.Not(ast.filter.Equals('hello world')))),
+ ast.filter.Any(ns.bse.color, ast.filter.Distance([1,2,3,4,5], 3)),
)))))
# invalid paths raise consistency error
self.assertRaises(errors.ConsistencyError, self.validate, self.schema.node(ns.bsfs.Entity),
@@ -130,7 +143,7 @@ class TestFilter(unittest.TestCase):
# type must be a node
self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.literal(ns.bsfs.Literal), None)
# type must be in the schema
- self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), None)
+ self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), None)
# predicate is verified
self.assertRaises(errors.ConsistencyError, self.validate._branch, self.schema.node(ns.bsfs.Entity),
ast.filter.Any(ns.bsfs.Invalid, ast.filter.Is('http://example.com/entity#1234')))
@@ -187,7 +200,7 @@ class TestFilter(unittest.TestCase):
self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.literal(ns.bsfs.Literal),
ast.filter.Has(ns.bse.tag))
# type must be in the schema
- self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid),
ast.filter.Has(ns.bse.tag))
# has checks predicate
self.assertRaises(errors.ConsistencyError, self.validate._has, self.schema.node(ns.bsfs.Entity),
@@ -206,7 +219,7 @@ class TestFilter(unittest.TestCase):
self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.literal(ns.bsfs.Literal),
ast.filter.Is('http://example.com/foo'))
# type must be in the schema
- self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._is, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid),
ast.filter.Is('http://example.com/foo'))
# is accepts correct expressions
self.assertIsNone(self.validate._is(self.schema.node(ns.bsfs.Entity), ast.filter.Is('http://example.com/entity#1234')))
@@ -222,13 +235,13 @@ class TestFilter(unittest.TestCase):
self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.node(ns.bsfs.Node),
ast.filter.EndsWith('hello world'))
# type must be in the schema
- self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid),
ast.filter.Equals('hello world'))
- self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid),
ast.filter.Substring('hello world'))
- self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid),
ast.filter.StartsWith('hello world'))
- self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._value, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid),
ast.filter.EndsWith('hello world'))
# value accepts correct expressions
self.assertIsNone(self.validate._value(self.schema.literal(ns.xsd.string), ast.filter.Equals('hello world')))
@@ -243,14 +256,34 @@ class TestFilter(unittest.TestCase):
self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.node(ns.bsfs.Node),
ast.filter.LessThan(0))
# type must be in the schema
- self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid),
ast.filter.GreaterThan(0))
- self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).get_child(ns.bsfs.Invalid),
+ self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.bsfs.Literal).child(ns.bsfs.Invalid),
+ ast.filter.LessThan(0))
+ # type must be a number
+ self.assertRaises(errors.ConsistencyError, self.validate._bounded, self.schema.literal(ns.xsd.string),
ast.filter.LessThan(0))
# bounded accepts correct expressions
self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.LessThan(0)))
self.assertIsNone(self.validate._bounded(self.schema.literal(ns.xsd.integer), ast.filter.GreaterThan(0)))
+ def test_distance(self):
+ # type must be a literal
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.node(ns.bsfs.Node),
+ ast.filter.Distance([1,2,3], 1, False))
+ # type must be a feature
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Array),
+ ast.filter.Distance([1,2,3], 1, False))
+ # type must be in the schema
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Feature).child(ns.bsfs.Invalid),
+ ast.filter.Distance([1,2,3], 1, False))
+ # FIXME: reference must be a numpy array
+ # reference must have the correct dimension
+ self.assertRaises(errors.ConsistencyError, self.validate._distance, self.schema.literal(ns.bsfs.Colors),
+ ast.filter.Distance([1,2,3], 1, False))
+ # FIXME: reference must have the correct dtype
+ # distance accepts correct expressions
+ self.assertIsNone(self.validate._distance(self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3,4,5], 1, False)))
## main ##
diff --git a/test/schema/test_schema.py b/test/schema/test_schema.py
index 888cdca..32dbc93 100644
--- a/test/schema/test_schema.py
+++ b/test/schema/test_schema.py
@@ -10,7 +10,7 @@ import unittest
# bsfs imports
from bsfs.namespace import ns
-from bsfs.schema import types
+from bsfs.schema import types, from_string
from bsfs.utils import errors
# objects to test
@@ -35,7 +35,8 @@ class TestSchema(unittest.TestCase):
bsfs:Unused rdfs:subClassOf bsfs:Node .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
xsd:boolean rdfs:subClassOf bsfs:Literal .
bse:tag rdfs:subClassOf bsfs:Predicate ;
@@ -55,32 +56,42 @@ class TestSchema(unittest.TestCase):
'''
# nodes
- self.n_root = types.Node(ns.bsfs.Node, None)
- self.n_ent = types.Node(ns.bsfs.Entity, types.Node(ns.bsfs.Node, None))
- self.n_img = types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Entity, types.Node(ns.bsfs.Node, None)))
- self.n_tag = types.Node(ns.bsfs.Tag, types.Node(ns.bsfs.Node, None))
- self.n_unused = types.Node(ns.bsfs.Unused, types.Node(ns.bsfs.Node, None))
+ self.n_root = types.ROOT_NODE
+ self.n_ent = self.n_root.child(ns.bsfs.Entity)
+ self.n_img = self.n_ent.child(ns.bsfs.Image)
+ self.n_tag = self.n_root.child(ns.bsfs.Tag)
+ self.n_unused = self.n_root.child(ns.bsfs.Unused)
self.nodes = [self.n_root, self.n_ent, self.n_img, self.n_tag, self.n_unused]
# literals
- self.l_root = types.Literal(ns.bsfs.Literal, None)
- self.l_string = types.Literal(ns.xsd.string, types.Literal(ns.bsfs.Literal, None))
- self.l_integer = types.Literal(ns.xsd.integer, types.Literal(ns.bsfs.Literal, None))
- self.l_unused = types.Literal(ns.xsd.boolean, types.Literal(ns.bsfs.Literal, None))
- self.literals = [self.l_root, self.l_string, self.l_integer, self.l_unused]
+ self.l_root = types.ROOT_LITERAL
+ self.l_number = types.ROOT_NUMBER
+ self.l_array = types.ROOT_ARRAY
+ self.l_time = types.ROOT_TIME
+ self.l_string = self.l_root.child(ns.xsd.string)
+ self.l_integer = self.l_root.child(ns.xsd.integer)
+ self.l_unused = self.l_root.child(ns.xsd.boolean)
+ self.f_root = types.ROOT_FEATURE
+ self.literals = [self.l_root, self.l_array, self.f_root, self.l_number, self.l_time, self.l_string, self.l_integer, self.l_unused]
# predicates
- self.p_root = types.Predicate(ns.bsfs.Predicate, None, types.Node(ns.bsfs.Node, None), None, False)
- self.p_tag = self.p_root.get_child(ns.bse.tag, self.n_ent, self.n_tag, False)
- self.p_group = self.p_tag.get_child(ns.bse.group, self.n_img, self.n_tag, False)
- self.p_comment = self.p_root.get_child(ns.bse.comment, self.n_root, self.l_string, True)
+ self.p_root = types.ROOT_PREDICATE
+ self.p_tag = self.p_root.child(ns.bse.tag, self.n_ent, self.n_tag, False)
+ self.p_group = self.p_tag.child(ns.bse.group, self.n_img, self.n_tag, False)
+ self.p_comment = self.p_root.child(ns.bse.comment, self.n_root, self.l_string, True)
self.predicates = [self.p_root, self.p_tag, self.p_group, self.p_comment]
def test_construction(self):
+ # no args yields a minimal schema
+ schema = Schema()
+ self.assertSetEqual(set(schema.nodes()), {self.n_root})
+ self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_number, self.l_array, self.l_time, self.f_root})
+ self.assertSetEqual(set(schema.predicates()), {self.p_root})
+
# nodes and literals are optional
schema = Schema(self.predicates)
self.assertSetEqual(set(schema.nodes()), {self.n_root, self.n_ent, self.n_img, self.n_tag})
- self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string})
+ self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_time, self.l_array, self.f_root})
self.assertSetEqual(set(schema.predicates()), set(self.predicates))
# predicates, nodes, and literals are respected
@@ -101,19 +112,19 @@ class TestSchema(unittest.TestCase):
# literals are complete
schema = Schema(self.predicates, self.nodes, None)
- self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string})
+ self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_array, self.l_time, self.f_root})
schema = Schema(self.predicates, self.nodes, [])
- self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string})
+ self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_array, self.l_time, self.f_root})
schema = Schema(self.predicates, self.nodes, [self.l_string])
- self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string})
+ self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_number, self.l_array, self.l_time, self.f_root})
schema = Schema(self.predicates, self.nodes, [self.l_integer])
- self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_integer})
+ self.assertSetEqual(set(schema.literals()), {self.l_root, self.l_string, self.l_integer, self.l_number, self.l_array, self.l_time, self.f_root})
schema = Schema(self.predicates, self.nodes, [self.l_integer, self.l_unused])
self.assertSetEqual(set(schema.literals()), set(self.literals))
# predicates are complete
schema = Schema([], self.nodes, self.literals)
- self.assertSetEqual(set(schema.predicates()), set())
+ self.assertSetEqual(set(schema.predicates()), {self.p_root})
schema = Schema([self.p_group], self.nodes, self.literals)
self.assertSetEqual(set(schema.predicates()), {self.p_root, self.p_tag, self.p_group})
schema = Schema([self.p_group, self.p_comment], self.nodes, self.literals)
@@ -153,20 +164,27 @@ class TestSchema(unittest.TestCase):
self.assertRaises(errors.ConsistencyError, Schema,
{}, {types.Node(ns.bsfs.Foo, None)}, {types.Node(ns.bsfs.Foo, None)})
self.assertRaises(errors.ConsistencyError, Schema,
- {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), None, False)}, {}, {types.Node(ns.bsfs.Foo, None)})
+ {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), types.ROOT_VERTEX, False)}, {}, {types.Node(ns.bsfs.Foo, None)})
self.assertRaises(errors.ConsistencyError, Schema,
- {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), None, False)}, {types.Node(ns.bsfs.Foo, None)}, {})
+ {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), types.ROOT_VERTEX, False)}, {types.Node(ns.bsfs.Foo, None)}, {})
self.assertRaises(errors.ConsistencyError, Schema,
- {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), None, False)}, {types.Node(ns.bsfs.Foo, None)}, {types.Node(ns.bsfs.Foo, None)})
+ {types.Predicate(ns.bsfs.Foo, None, types.Node(ns.bsfs.Node, None), types.ROOT_VERTEX, False)}, {types.Node(ns.bsfs.Foo, None)}, {types.Node(ns.bsfs.Foo, None)})
+
def test_str(self):
+ # string conversion
self.assertEqual(str(Schema([])), 'Schema()')
self.assertEqual(str(Schema([], [], [])), 'Schema()')
self.assertEqual(str(Schema(self.predicates, self.nodes, self.literals)), 'Schema()')
- self.assertEqual(repr(Schema([])), 'Schema([], [], [])')
- self.assertEqual(repr(Schema([], [], [])), 'Schema([], [], [])')
+ # repr conversion with only default nodes, literals, and predicates
+ n = [ns.bsfs.Node]
+ l = [ns.bsfs.Array, ns.bsfs.Feature, ns.bsfs.Literal, ns.bsfs.Number, ns.bsfs.Time]
+ p = [ns.bsfs.Predicate]
+ self.assertEqual(repr(Schema()), f'Schema({n}, {l}, {p})')
+ self.assertEqual(repr(Schema([], [], [])), f'Schema({n}, {l}, {p})')
+ # repr conversion
n = [ns.bsfs.Entity, ns.bsfs.Image, ns.bsfs.Node, ns.bsfs.Tag, ns.bsfs.Unused]
- l = [ns.bsfs.Literal, ns.xsd.boolean, ns.xsd.integer, ns.xsd.string]
+ l = [ns.bsfs.Array, ns.bsfs.Feature, ns.bsfs.Literal, ns.bsfs.Number, ns.bsfs.Time, ns.xsd.boolean, ns.xsd.integer, ns.xsd.string]
p = [ns.bse.comment, ns.bse.group, ns.bse.tag, ns.bsfs.Predicate]
self.assertEqual(repr(Schema(self.predicates, self.nodes, self.literals)), f'Schema({n}, {l}, {p})')
@@ -202,16 +220,16 @@ class TestSchema(unittest.TestCase):
self.assertNotEqual(hash(schema),
hash(Schema([self.p_group, self.p_tag, self.p_root], self.nodes, self.literals)))
self.assertNotEqual(schema,
- Schema(self.predicates + [self.p_root.get_child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals))
+ Schema(self.predicates + [self.p_root.child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals))
self.assertNotEqual(hash(schema),
- hash(Schema(self.predicates + [self.p_root.get_child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals)))
+ hash(Schema(self.predicates + [self.p_root.child(ns.bse.filesize, self.n_ent, self.l_integer)], self.nodes, self.literals)))
def test_order(self):
# setup
class Foo(): pass
- p_foo = self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, True)
- p_sub = p_foo.get_child(ns.bse.sub, self.n_ent, self.l_string, True)
- p_bar = self.p_root.get_child(ns.bse.bar, self.n_ent, self.l_string, True)
+ p_foo = self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, True)
+ p_sub = p_foo.child(ns.bse.sub, self.n_ent, self.l_string, True)
+ p_bar = self.p_root.child(ns.bse.bar, self.n_ent, self.l_string, True)
# can only compare schema to other schema
# <
@@ -258,11 +276,11 @@ class TestSchema(unittest.TestCase):
self.assertTrue(operator.lt(Schema({self.p_tag}), Schema({self.p_group})))
self.assertTrue(operator.le(Schema({self.p_tag}), Schema({self.p_group})))
# subset considers differences in predicates and literals
- self.assertTrue(operator.lt(Schema.Empty(), Schema({self.p_comment})))
+ self.assertTrue(operator.lt(Schema(), Schema({self.p_comment})))
# subset considers differences in predicates, nodes, and literals
- self.assertTrue(operator.lt(Schema({}), Schema.Empty()))
- self.assertTrue(operator.lt(Schema({self.p_tag}), Schema.from_string(self.schema_str)))
- self.assertTrue(operator.le(Schema({self.p_tag}), Schema.from_string(self.schema_str)))
+ self.assertTrue(operator.le(Schema({}), Schema()))
+ self.assertTrue(operator.lt(Schema({self.p_tag}), from_string(self.schema_str)))
+ self.assertTrue(operator.le(Schema({self.p_tag}), from_string(self.schema_str)))
self.assertFalse(operator.lt(Schema({self.p_comment}), Schema({self.p_tag})))
self.assertFalse(operator.le(Schema({self.p_comment}), Schema({self.p_tag})))
@@ -280,54 +298,54 @@ class TestSchema(unittest.TestCase):
self.assertTrue(operator.gt(Schema({self.p_group}), Schema({self.p_tag})))
self.assertTrue(operator.ge(Schema({self.p_group}), Schema({self.p_tag})))
# superset considers differences in predicates and literals
- self.assertTrue(operator.gt(Schema({self.p_comment}), Schema.Empty()))
+ self.assertTrue(operator.gt(Schema({self.p_comment}), Schema()))
# superset considers differences in predicates, nodes, and literals
- self.assertTrue(operator.gt(Schema.Empty(), Schema({})))
- self.assertTrue(operator.gt(Schema.from_string(self.schema_str), Schema({self.p_tag})))
- self.assertTrue(operator.ge(Schema.from_string(self.schema_str), Schema({self.p_tag})))
+ self.assertTrue(operator.ge(Schema(), Schema({})))
+ self.assertTrue(operator.gt(from_string(self.schema_str), Schema({self.p_tag})))
+ self.assertTrue(operator.ge(from_string(self.schema_str), Schema({self.p_tag})))
self.assertFalse(operator.gt(Schema({self.p_tag}), Schema({self.p_comment})))
self.assertFalse(operator.ge(Schema({self.p_tag}), Schema({self.p_comment})))
# inconsistent schema cannot be a subset
self.assertFalse(operator.le(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
self.assertFalse(operator.le(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
+ self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
self.assertFalse(operator.le(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
self.assertFalse(operator.le(Schema({}, {self.n_img}), Schema({}, {
types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))})))
self.assertFalse(operator.le(Schema({}, {}, {self.l_integer}), Schema({}, {}, {
types.Literal(ns.xsd.integer, types.Literal(ns.xsd.number, types.Literal(ns.bsfs.Literal, None)))})))
# inconsistent schema cannot be a true subset
self.assertFalse(operator.lt(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
self.assertFalse(operator.lt(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
+ self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
self.assertFalse(operator.lt(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
self.assertFalse(operator.lt(Schema({}, {self.n_img}), Schema({}, {
types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))})))
self.assertFalse(operator.lt(Schema({}, {}, {self.l_integer}), Schema({}, {}, {
types.Literal(ns.xsd.integer, types.Literal(ns.xsd.number, types.Literal(ns.bsfs.Literal, None)))})))
# inconsistent schema cannot be a superset
self.assertFalse(operator.ge(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
self.assertFalse(operator.ge(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
+ self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
self.assertFalse(operator.ge(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
self.assertFalse(operator.ge(Schema({}, {self.n_img}), Schema({}, {
types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))})))
self.assertFalse(operator.ge(Schema({}, {}, {self.l_integer}), Schema({}, {}, {
types.Literal(ns.xsd.integer, types.Literal(ns.xsd.number, types.Literal(ns.bsfs.Literal, None)))})))
# inconsistent schema cannot be a true superset
self.assertFalse(operator.gt(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_integer, True)}))) # inconsistent w.r.t. literal
self.assertFalse(operator.gt(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
+ self.p_root.child(ns.bse.foo, self.n_img, self.l_string, True)}))) # inconsistent w.r.t. node
self.assertFalse(operator.gt(Schema({p_foo}), Schema({
- self.p_root.get_child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
+ self.p_root.child(ns.bse.foo, self.n_ent, self.l_string, False)}))) # inconsistent w.r.t. unique
self.assertFalse(operator.gt(Schema({}, {self.n_img}), Schema({}, {
types.Node(ns.bsfs.Image, types.Node(ns.bsfs.Node, None))})))
self.assertFalse(operator.gt(Schema({}, {}, {self.l_integer}), Schema({}, {}, {
@@ -351,26 +369,26 @@ class TestSchema(unittest.TestCase):
# difference does not contain predicates from the RHS
diff = Schema({self.p_tag, self.p_comment}).diff(Schema({self.p_group}))
self.assertSetEqual(set(diff.nodes), set())
- self.assertSetEqual(set(diff.literals), {self.l_root, self.l_string})
+ self.assertSetEqual(set(diff.literals), {self.l_string})
self.assertSetEqual(set(diff.predicates), {self.p_comment})
# difference considers extra nodes and literals
diff = Schema({self.p_tag}, {self.n_unused}, {self.l_unused}).diff(Schema({self.p_tag}))
self.assertSetEqual(set(diff.nodes), {self.n_unused})
- self.assertSetEqual(set(diff.literals), {self.l_root, self.l_unused})
+ self.assertSetEqual(set(diff.literals), {self.l_unused})
self.assertSetEqual(set(diff.predicates), set())
# difference considers inconsistent types
diff = Schema({self.p_tag}, {self.n_unused}, {self.l_unused}).diff(
Schema({self.p_tag}, {types.Node(ns.bsfs.Unused, None)}, {types.Literal(ns.xsd.boolean, None)}))
self.assertSetEqual(set(diff.nodes), {self.n_unused})
- self.assertSetEqual(set(diff.literals), {self.l_root, self.l_unused})
+ self.assertSetEqual(set(diff.literals), {self.l_unused})
self.assertSetEqual(set(diff.predicates), set())
# __sub__ is an alias for diff
diff = Schema({self.p_comment}, {self.n_unused}, {self.l_unused}) - Schema({self.p_group})
self.assertSetEqual(set(diff.nodes), {self.n_unused})
- self.assertSetEqual(set(diff.literals), {self.l_root, self.l_string, self.l_unused})
+ self.assertSetEqual(set(diff.literals), {self.l_string, self.l_unused})
self.assertSetEqual(set(diff.predicates), {self.p_comment})
# __sub__ only accepts Schema instances
class Foo(): pass
@@ -547,196 +565,6 @@ class TestSchema(unittest.TestCase):
self.assertFalse(schema.has_predicate(ns.bse.mimetype))
self.assertFalse(schema.has_predicate(self.p_root))
- def test_empty(self):
- self.assertEqual(Schema.Empty(), Schema(
- [types.Predicate(ns.bsfs.Predicate, None, types.Node(ns.bsfs.Node, None), None, False)],
- [types.Node(ns.bsfs.Node, None)],
- [types.Literal(ns.bsfs.Literal, None)],
- ))
-
- def test_from_string(self):
- # from_string creates a schema
- self.assertEqual(
- Schema(self.predicates, self.nodes, self.literals),
- Schema.from_string(self.schema_str))
-
- # schema contains at least the root types
- self.assertEqual(Schema.from_string(''), Schema({self.p_root}, {self.n_root}, {self.l_root}))
-
- # custom example
- self.assertEqual(
- Schema({types.Predicate(ns.bsfs.Predicate, None, self.n_root, None, False).get_child(
- ns.bse.filename, self.n_ent, self.l_string, False)}),
- 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 .
- xsd:string rdfs:subClassOf bsfs:Literal .
-
- bse:filename rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range xsd:string ;
- bsfs:unique "false"^^xsd:boolean .
- '''))
-
- # all nodes must be defined
- self.assertRaises(errors.ConsistencyError, 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#>
-
- xsd:string rdfs:subClassOf bsfs:Literal .
-
- bse:filename rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range xsd:string ;
- bsfs:unique "false"^^xsd:boolean .
- ''')
-
- # all literals must be defined
- self.assertRaises(errors.ConsistencyError, 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 .
-
- bse:filename rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range xsd:string ;
- bsfs:unique "false"^^xsd:boolean .
- ''')
-
- # must not have circular dependencies
- self.assertRaises(errors.ConsistencyError, 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 .
- # ah, a nice circular dependency
- bsfs:Entity rdfs:subClassOf bsfs:Document .
- bsfs:Document rdfs:subClassOf bsfs:Entity .
- bsfs:PDF rdfs:subClassOf bsfs:Document .
- ''')
-
- # range must be a node or literal
- self.assertRaises(errors.ConsistencyError, 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 .
-
- bse:filename rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range xsd:string ;
- bsfs:unique "false"^^xsd:boolean .
- ''')
- self.assertRaises(errors.ConsistencyError, 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 .
-
- bse:filename rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range bsfs:Foo ;
- bsfs:unique "false"^^xsd:boolean .
- ''')
- self.assertRaises(errors.ConsistencyError, 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 .
-
- bse:filename rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range bsfs:Predicate ;
- bsfs:unique "false"^^xsd:boolean .
- ''')
-
- # must be consistent
- self.assertRaises(errors.ConsistencyError, 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:Node .
- bsfs:Document rdfs:subClassOf bsfs:Entity.
- ''')
- self.assertRaises(errors.ConsistencyError, 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/>
-
- xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:name rdfs:subClassOf bsfs:Literal .
- xsd:name rdfs:subClassOf xsd:string .
- ''')
- self.assertRaises(errors.ConsistencyError, 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 .
-
- bse:foo rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Node ;
- rdfs:range bsfs:Node ;
- bsfs:unique "false"^^xsd:boolean .
-
- bse:foo rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity .
-
- ''')
- self.assertRaises(errors.ConsistencyError, 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 .
-
- bse:foo rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Node ;
- rdfs:range bsfs:Node ;
- bsfs:unique "false"^^xsd:boolean .
-
- bse:foo rdfs:subClassOf bsfs:Predicate ;
- rdfs:range bsfs:Entity .
-
- ''')
- self.assertRaises(errors.ConsistencyError, 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 .
-
- bse:foo rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Node ;
- rdfs:range bsfs:Node ;
- bsfs:unique "false"^^xsd:boolean .
-
- bse:foo rdfs:subClassOf bsfs:Predicate ;
- bsfs:unique "true"^^xsd:boolean .
-
- ''')
-
-
-
## main ##
if __name__ == '__main__':
diff --git a/test/schema/test_serialize.py b/test/schema/test_serialize.py
new file mode 100644
index 0000000..fc6b20a
--- /dev/null
+++ b/test/schema/test_serialize.py
@@ -0,0 +1,1030 @@
+"""
+
+Part of the tagit test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import re
+import unittest
+
+# bsfs imports
+from bsfs.namespace import ns
+from bsfs.schema import Schema, types
+from bsfs.utils import errors, URI
+
+# objects to test
+from bsfs.schema.serialize import from_string, to_string
+
+
+## code ##
+
+class TestFromString(unittest.TestCase):
+
+ def test_empty(self):
+ # schema contains at least the root types
+ self.assertEqual(from_string(''), Schema())
+
+
+ def test_circular_dependency(self):
+ # must not have circular dependencies
+ self.assertRaises(errors.ConsistencyError, from_string, '''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ # ah, a nice circular dependency
+ bsfs:Entity rdfs:subClassOf bsfs:Document .
+ bsfs:Document rdfs:subClassOf bsfs:Entity .
+ bsfs:PDF rdfs:subClassOf bsfs:Document .
+ ''')
+
+
+ def test_node(self):
+ # all nodes must be defined
+ self.assertRaises(errors.ConsistencyError, 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#>
+
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+
+ # node definitions must be consistent (cannot re-use a node uri)
+ self.assertRaises(errors.ConsistencyError, 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:Node .
+ bsfs:Document rdfs:subClassOf bsfs:Entity . # conflicting parent
+ ''')
+
+ # additional nodes can be defined
+ n_unused = types.ROOT_NODE.child(ns.bsfs.unused)
+ self.assertEqual(Schema({}, {n_unused}), 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:unused rdfs:subClassOf bsfs:Node . # unused symbol
+ '''))
+
+ # a node can have multiple children
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ n_tag = types.ROOT_NODE.child(ns.bsfs.Tag)
+ n_doc = n_ent.child(ns.bsfs.Document)
+ n_image = n_ent.child(ns.bsfs.Image)
+ self.assertEqual(Schema({}, {n_ent, n_tag, n_doc, n_image}), 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#>
+
+ # nodes inherit from same parent
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+
+ # nodes inherit from same parent
+ bsfs:Document rdfs:subClassOf bsfs:Entity .
+ bsfs:Image rdfs:subClassOf bsfs:Entity .
+ '''))
+
+ # additional nodes can be defined and used
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ l_string = types.ROOT_LITERAL.child(ns.xsd.string)
+ p_filename = types.ROOT_PREDICATE.child(ns.bse.filename,
+ n_ent, l_string, False)
+ self.assertEqual(Schema({p_filename}), 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 .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+ '''))
+
+ # nodes can have annotations
+ self.assertDictEqual(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 .
+
+ ''').node(ns.bsfs.Entity).annotations, {})
+ self.assertDictEqual(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 ;
+ rdfs:label "hello world"^^xsd:string ;
+ bsfs:foo "1234"^^xsd:integer .
+
+ ''').node(ns.bsfs.Entity).annotations, {
+ ns.rdfs.label: 'hello world',
+ ns.bsfs.foo: 1234,
+ })
+
+
+ def test_literal(self):
+ # all literals must be defined
+ self.assertRaises(errors.ConsistencyError, 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 .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ; # undefined symbol
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+
+ # literal definitions must be consistent (cannot re-use a literal uri)
+ self.assertRaises(errors.ConsistencyError, 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/>
+
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ xsd:name rdfs:subClassOf bsfs:Literal .
+ xsd:name rdfs:subClassOf xsd:string . # conflicting parent
+ ''')
+
+ # additional literals can be defined
+ l_unused = types.ROOT_LITERAL.child(ns.xsd.unused)
+ self.assertEqual(Schema({}, {}, {l_unused}), 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#>
+
+ xsd:unused rdfs:subClassOf bsfs:Literal . # unused symbol
+ '''))
+
+ # a literal can have multiple children
+ l_string = types.ROOT_LITERAL.child(ns.xsd.string)
+ l_integer = types.ROOT_NUMBER.child(ns.xsd.integer)
+ l_unsigned = l_integer.child(ns.xsd.unsigned)
+ l_signed = l_integer.child(ns.xsd.signed)
+ self.assertEqual(Schema({}, {}, {l_string, l_integer, l_unsigned, l_signed}), 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#>
+
+ # literals inherit from same parent
+ xsd:string rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
+
+ # literals inherit from same parent
+ xsd:unsigned rdfs:subClassOf xsd:integer .
+ xsd:signed rdfs:subClassOf xsd:integer .
+ '''))
+
+ # additional literals can be defined and used
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ l_string = types.ROOT_LITERAL.child(ns.xsd.string)
+ p_filename = types.ROOT_PREDICATE.child(ns.bse.filename,
+ n_ent, l_string, False)
+ self.assertEqual(Schema({p_filename}), 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 .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+ '''))
+
+ # literals can have annotations
+ self.assertDictEqual(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/>
+
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ ''').literal(ns.xsd.string).annotations, {})
+ self.assertDictEqual(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/>
+
+ xsd:string rdfs:subClassOf bsfs:Literal ;
+ rdfs:label "hello world"^^xsd:string ;
+ bsfs:foo "1234"^^xsd:integer .
+
+ ''').literal(ns.xsd.string).annotations, {
+ ns.rdfs.label: 'hello world',
+ ns.bsfs.foo: 1234,
+ })
+
+
+ def test_predicate(self):
+ # domain must be defined
+ self.assertRaises(errors.ConsistencyError, 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#>
+
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ; # undefined symbol
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+ # domain cannot be a literal
+ self.assertRaises(errors.ConsistencyError, 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:Literal .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ; # literal instead of node
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+
+ # range must be defined
+ self.assertRaises(errors.ConsistencyError, 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 .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ; # undefined symbol
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+ # range must be defined
+ self.assertRaises(errors.ConsistencyError, 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 .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Foo ; # undefined symbol
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+ # range must be a node or a literal
+ self.assertRaises(errors.ConsistencyError, 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 .
+
+ bse:filename rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Predicate ; # invalid symbol
+ bsfs:unique "false"^^xsd:boolean .
+ ''')
+
+ # additional predicates can be defined
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ l_string = types.ROOT_LITERAL.child(ns.xsd.string)
+ p_comment = types.ROOT_PREDICATE.child(ns.bse.comment, domain=n_ent, range=l_string, unique=False)
+ self.assertEqual(Schema({p_comment}), 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 .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+ '''))
+
+ # predicates inherit properties from parents
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ l_string = types.ROOT_LITERAL.child(ns.xsd.string)
+ p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent, range=l_string)
+ p_comment = p_annotation.child(ns.bse.comment, unique=True)
+ self.assertEqual(Schema({p_comment}), 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 .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bsfs:Annotation rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range xsd:string .
+
+ bse:comment rdfs:subClassOf bsfs:Annotation ; # inherits domain/range from bsfs:Annotation
+ bsfs:unique "true"^^xsd:boolean .
+ '''))
+
+ # we can define partial predicates (w/o specifying a usable range)
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ l_string = types.ROOT_LITERAL.child(ns.xsd.string)
+ p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent)
+ p_comment = p_annotation.child(ns.bse.comment, range=l_string, unique=False)
+ self.assertEqual(Schema({p_comment}), 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 .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bsfs:Annotation rdfs:subClassOf bsfs:Predicate ; # derive predicate w/o setting range
+ rdfs:domain bsfs:Entity .
+
+ bse:comment rdfs:subClassOf bsfs:Annotation ; # derived predicate w/ setting range
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+ '''))
+
+ # predicate definition can be split across multiple statements.
+ # statements can be repeated
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, domain=n_ent, range=types.ROOT_NODE, unique=True)
+ self.assertEqual(Schema({p_foo}), 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 .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity.
+ '''))
+
+ # domain must be a subtype of parent's domain
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ n_image = n_ent.child(ns.bsfs.Image)
+ p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, domain=types.ROOT_NODE)
+ p_bar = p_foo.child(ns.bse.bar, domain=n_ent)
+ p_foobar = p_bar.child(ns.bse.foobar, domain=n_image)
+ self.assertEqual(Schema({p_foobar}), 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:Image rdfs:subClassOf bsfs:Entity .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node .
+ bse:bar rdfs:subClassOf bse:foo ;
+ rdfs:domain bsfs:Entity .
+ bse:foobar rdfs:subClassOf bse:bar ;
+ rdfs:domain bsfs:Image .
+ '''))
+ self.assertRaises(errors.ConsistencyError, 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:Image rdfs:subClassOf bsfs:Entity .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Image .
+ bse:bar rdfs:subClassOf bse:foo ;
+ rdfs:domain bsfs:Entity .
+ bse:foobar rdfs:subClassOf bse:bar ;
+ rdfs:domain bsfs:Node .
+ ''')
+
+ # range must be a subtype of parent's range
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ n_image = n_ent.child(ns.bsfs.Image)
+ p_foo = types.ROOT_PREDICATE.child(ns.bse.foo, range=types.ROOT_NODE)
+ p_bar = p_foo.child(ns.bse.bar, range=n_ent)
+ p_foobar = p_bar.child(ns.bse.foobar, range=n_image)
+ self.assertEqual(Schema({p_foobar}), 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:Image rdfs:subClassOf bsfs:Entity .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:range bsfs:Node .
+ bse:bar rdfs:subClassOf bse:foo ;
+ rdfs:range bsfs:Entity .
+ bse:foobar rdfs:subClassOf bse:bar ;
+ rdfs:range bsfs:Image .
+ '''))
+ self.assertRaises(errors.ConsistencyError, 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:Image rdfs:subClassOf bsfs:Entity .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:range bsfs:Image .
+ bse:bar rdfs:subClassOf bse:foo ;
+ rdfs:range bsfs:Entity .
+ bse:foobar rdfs:subClassOf bse:bar ;
+ rdfs:range bsfs:Node .
+ ''')
+
+ # cannot define the same predicate from multiple parents
+ self.assertRaises(errors.ConsistencyError, 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:Annotation rdfs:subClassOf bsfs:Predicate .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:foo rdfs:subClassOf bsfs:Annotation ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ ''')
+ # cannot assign multiple conflicting domains to the same predicate
+ self.assertRaises(errors.ConsistencyError, 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 .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity . # conflicting domain
+ ''')
+ # cannot assign multiple conflicting ranges to the same predicate
+ self.assertRaises(errors.ConsistencyError, 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 .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:range bsfs:Entity . # conflicting range
+ ''')
+ # cannot assign multiple conflicting uniques to the same predicate
+ self.assertRaises(errors.ConsistencyError, 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 .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range bsfs:Node ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:foo rdfs:subClassOf bsfs:Predicate ;
+ bsfs:unique "true"^^xsd:boolean . # conflicting unique
+ ''')
+
+ # predicates can have annotations
+ self.assertDictEqual(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#>
+
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:range bsfs:Node .
+
+ ''').predicate(ns.bse.comment).annotations, {})
+ self.assertDictEqual(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#>
+
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:range bsfs:Node ;
+ rdfs:label "hello world"^^xsd:string ;
+ bsfs:foo "1234"^^xsd:integer .
+
+ ''').predicate(ns.bse.comment).annotations, {
+ ns.rdfs.label: 'hello world',
+ ns.bsfs.foo: 1234,
+ })
+
+
+ def test_feature(self):
+ # additional features can be defined
+ f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors)
+ self.assertEqual(Schema(literals={f_colors}), 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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature .
+
+ '''))
+
+ # features inherit properties from parents
+ f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors, dimension=1234, dtype=ns.bsfs.i32)
+ f_main_colors = f_colors.child(ns.bsfs.MainColor, distance=ns.bsfs.cosine, dtype=ns.bsfs.f16)
+ self.assertEqual(Schema(literals={f_colors, f_main_colors}), 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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ; # inherits distance from bsfs:Feature
+ bsfs:dimension "1234"^^xsd:integer ; # overwrites bsfs:Feature
+ bsfs:dtype bsfs:i32 . # overwrites bsfs:Feature
+
+ bsfs:MainColor rdfs:subClassOf bsfs:Colors ; # inherits dimension from bsfs:Colors
+ bsfs:distance bsfs:cosine ; # overwrites bsfs:Feature
+ bsfs:dtype bsfs:f16 . # overwrites bsfs:Colors
+
+ '''))
+
+ # feature definition can be split across multiple statements.
+ # statements can be repeated
+ f_colors = types.ROOT_FEATURE.child(ns.bsfs.Colors, dimension=1234, dtype=ns.bsfs.f32)
+ self.assertEqual(Schema(literals={f_colors}), 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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "1234"^^xsd:integer .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "1234"^^xsd:integer ; # non-conflicting repetition
+ bsfs:dtype bsfs:f32 .
+ '''))
+
+ # cannot define the same feature from multiple parents
+ self.assertRaises(errors.ConsistencyError, 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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+ bsfs:ColorSpace rdfs:subClassOf bsfs:Feature .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature .
+ bsfs:Colors rdfs:subClassOf bsfs:ColorSpace .
+
+ ''')
+ # cannot assign multiple conflicting dimensions to the same feature
+ self.assertRaises(errors.ConsistencyError, 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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "1234"^^xsd:integer .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "4321"^^xsd:integer . # conflicting dimension
+
+ ''')
+ # cannot assign multiple conflicting dtypes to the same feature
+ self.assertRaises(errors.ConsistencyError, 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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dtype bsfs:f32 .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dtype bsfs:f16 . # conflicting dtype
+ ''')
+ # cannot assign multiple conflicting distance metrics to the same feature
+ self.assertRaises(errors.ConsistencyError, 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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:distance bsfs:euclidean .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:distance bsfs:cosine . # conflicting distance
+ ''')
+
+ # features can have annotations
+ self.assertDictEqual(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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "1234"^^xsd:integer .
+
+ ''').literal(ns.bsfs.Colors).annotations, {})
+ self.assertDictEqual(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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "1234"^^xsd:integer ;
+ rdfs:label "hello world"^^xsd:string ;
+ bsfs:foo "1234"^^xsd:integer .
+
+ ''').literal(ns.bsfs.Colors).annotations, {
+ ns.rdfs.label: 'hello world',
+ ns.bsfs.foo: 1234,
+ })
+
+
+ def test_integration(self):
+ # nodes
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ n_tag = types.ROOT_NODE.child(ns.bsfs.Tag)
+ n_image = n_ent.child(ns.bsfs.Image)
+ # literals
+ l_string = types.ROOT_LITERAL.child(ns.xsd.string)
+ l_array = types.ROOT_LITERAL.child(ns.bsfs.array)
+ l_integer = types.ROOT_NUMBER.child(ns.xsd.integer)
+ l_boolean = types.ROOT_LITERAL.child(ns.xsd.boolean)
+ # predicates
+ p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation)
+ p_tag = types.ROOT_PREDICATE.child(ns.bse.tag, domain=n_ent, range=n_tag)
+ p_group = p_tag.child(ns.bse.group, domain=n_image, unique=True)
+ p_comment = p_annotation.child(ns.bse.comment, range=l_string)
+ # features
+ f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors_spatial'),
+ dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean)
+ f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234'), dimension=1024)
+ f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors_spatial#4321'), dimension=2048)
+ # schema
+ ref = Schema(
+ {p_annotation, p_tag, p_group, p_comment},
+ {n_ent, n_tag, n_image},
+ {l_string, l_integer, l_boolean, f_colors, f_colors1234, f_colors4321})
+ # load from string
+ gen = from_string('''
+ # generic prefixes
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
+
+ # bsfs prefixes
+ prefix bsfs: <http://bsfs.ai/schema/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ # nodes
+ bsfs:Entity rdfs:subClassOf bsfs:Node ;
+ rdfs:label "Principal node"^^xsd:string .
+ bsfs:Tag rdfs:subClassOf bsfs:Node ;
+ rdfs:label "Tag"^^xsd:string .
+ bsfs:Image rdfs:subClassOf bsfs:Entity .
+
+ # literals
+ xsd:string rdfs:subClassOf bsfs:Literal ;
+ rdfs:label "A sequence of characters"^^xsd:string .
+ bsfs:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array.
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
+ xsd:boolean rdfs:subClassOf bsfs:Literal .
+
+
+ # abstract predicates
+ bsfs:Annotation rdfs:subClassOf bsfs:Predicate ;
+ rdfs:label "node annotation"^^xsd:string .
+
+ # feature instances
+ <http://bsfs.ai/schema/Feature/colors_spatial> rdfs:subClassOf bsfs:Feature ;
+ bsfs:dtype bsfs:f16 ;
+ bsfs:distance bsfs:euclidean ;
+ # annotations
+ rdfs:label "ColorsSpatial instances. Dimension depends on instance."^^xsd:string ;
+ bsfs:first_arg "1234"^^xsd:integer ;
+ bsfs:second_arg "hello world"^^xsd:string .
+
+ <http://bsfs.ai/schema/Feature/colors_spatial#1234> rdfs:subClassOf <http://bsfs.ai/schema/Feature/colors_spatial> ;
+ bsfs:dimension "1024"^^xsd:integer ;
+ rdfs:label "Main colors spatial instance"^^xsd:string .
+
+ <http://bsfs.ai/schema/Feature/colors_spatial#4321> rdfs:subClassOf <http://bsfs.ai/schema/Feature/colors_spatial> ;
+ bsfs:dimension "2048"^^xsd:integer .
+
+ # predicate instances
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean ;
+ # annotations
+ rdfs:label "connect entity to a tag"^^xsd:string .
+
+ bse:group rdfs:subClassOf bse:tag ; # subtype of another predicate
+ rdfs:domain bsfs:Image ;
+ bsfs:unique "true"^^xsd:boolean .
+
+ bse:comment rdfs:subClassOf bsfs:Annotation ; # subtype of abstract predicate
+ rdfs:domain bsfs:Node ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ ''')
+ # schemas are equal
+ self.assertEqual(ref, gen)
+ # check annotations
+ self.assertDictEqual(gen.node(ns.bsfs.Entity).annotations, {ns.rdfs.label: 'Principal node'})
+ self.assertDictEqual(gen.node(ns.bsfs.Tag).annotations, {ns.rdfs.label: 'Tag'})
+ self.assertDictEqual(gen.literal(ns.xsd.string).annotations, {ns.rdfs.label: 'A sequence of characters'})
+ self.assertDictEqual(gen.predicate(ns.bsfs.Annotation).annotations, {ns.rdfs.label: 'node annotation'})
+ self.assertDictEqual(gen.literal(URI('http://bsfs.ai/schema/Feature/colors_spatial')).annotations, {
+ ns.rdfs.label: 'ColorsSpatial instances. Dimension depends on instance.',
+ ns.bsfs.first_arg: 1234,
+ ns.bsfs.second_arg: 'hello world',
+ })
+ self.assertDictEqual(gen.literal(URI('http://bsfs.ai/schema/Feature/colors_spatial#1234')).annotations, {
+ ns.rdfs.label: 'Main colors spatial instance'})
+ self.assertDictEqual(gen.predicate(ns.bse.tag).annotations, {ns.rdfs.label: 'connect entity to a tag'})
+
+ # blank nodes result in an error
+ self.assertRaises(errors.BackendError, from_string, '''
+ prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+ prefix bsfs: <http://bsfs.ai/schema/>
+ bsfs:Entity rdfs:subClassOf bsfs:Node ;
+ bsfs:foo _:bar .
+ ''')
+
+
+class TestToString(unittest.TestCase):
+
+ def test_empty(self):
+ self.assertEqual(Schema(), from_string(to_string(Schema())))
+
+ def test_parse(self):
+ schema = Schema()
+ schema._nodes[ns.bsfs.Invalid] = 123 # NOTE: Access protected to force an invalid schema
+ self.assertRaises(TypeError, to_string, schema)
+
+ def test_literal(self):
+ # root literals
+ l_str = types.ROOT_LITERAL.child(ns.xsd.string)
+ # derived literals
+ l_int = types.ROOT_NUMBER.child(ns.xsd.integer)
+ l_unsigned = l_int.child(ns.xsd.unsigned)
+ # create schema
+ schema = Schema(literals={l_int, l_str, l_unsigned})
+
+ schema_str = to_string(schema)
+ # all symbols are serialized
+ self.assertIn('xsd:string', schema_str)
+ self.assertIn('xsd:integer', schema_str)
+ self.assertIn('xsd:unsigned', schema_str)
+ # unserialize yields the original schema
+ self.assertEqual(schema, from_string(schema_str))
+
+ # literals that have no parent are ignored
+ schema = Schema(literals={types.Literal(ns.bsfs.Invalid, None)})
+ self.assertEqual(Schema(), from_string(to_string(schema)))
+ self.assertNotIn('Invalid', to_string(schema))
+
+ # literal annotations are serialized
+ annotations = {
+ ns.rdfs.label: 'hello world',
+ ns.schema.description: 'some text',
+ ns.bsfs.foo: 1234,
+ ns.bsfs.bar: True,
+ }
+ l_str = types.ROOT_LITERAL.child(ns.xsd.string, **annotations)
+ self.assertDictEqual(
+ annotations,
+ from_string(to_string(Schema(literals={l_str}))).literal(ns.xsd.string).annotations)
+
+
+ def test_node(self):
+ # root nodes
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ n_tag = types.ROOT_NODE.child(ns.bsfs.Tag)
+ # derived nodes
+ n_img = n_ent.child(ns.bsfs.Image)
+ n_doc = n_ent.child(ns.bsfs.Document)
+ n_grp = n_tag.child(ns.bsfs.Group)
+ # create schema
+ schema = Schema(nodes={n_ent, n_img, n_doc, n_tag, n_grp})
+
+ schema_str = to_string(schema)
+ # all symbols are serialized
+ self.assertIn('bsfs:Entity', schema_str)
+ self.assertIn('bsfs:Tag', schema_str)
+ self.assertIn('bsfs:Image', schema_str)
+ self.assertIn('bsfs:Document', schema_str)
+ self.assertIn('bsfs:Group', schema_str)
+ # unserialize yields the original schema
+ self.assertEqual(schema, from_string(schema_str))
+
+ # nodes that have no parent are ignored
+ schema = Schema(nodes={types.Node(ns.bsfs.Invalid, None)})
+ self.assertEqual(Schema(), from_string(to_string(schema)))
+ self.assertNotIn('Invalid', to_string(schema))
+
+ # node annotations are serialized
+ annotations = {
+ ns.rdfs.label: 'hello world',
+ ns.schema.description: 'some text',
+ ns.bsfs.foo: 1234,
+ ns.bsfs.bar: True,
+ }
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity, **annotations)
+ self.assertDictEqual(
+ annotations,
+ from_string(to_string(Schema(nodes={n_ent}))).node(ns.bsfs.Entity).annotations)
+
+
+ def test_predicate(self):
+ # auxiliary types
+ n_ent = types.ROOT_NODE.child(ns.bsfs.Entity)
+ l_str = types.ROOT_LITERAL.child(ns.xsd.string)
+ # root predicates
+ p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, domain=n_ent)
+ p_owner = types.ROOT_PREDICATE.child(ns.bse.owner, range=l_str, unique=True)
+ # derived predicates
+ p_comment = p_annotation.child(ns.bse.comment, range=l_str) # inherits domain
+ p_note = p_comment.child(ns.bse.note, unique=True) # inherits domain/range
+ # create schema
+ schema = Schema({p_owner, p_comment, p_note})
+
+ schema_str = to_string(schema)
+ # all symbols are serialized
+ self.assertIn('bsfs:Entity', schema_str)
+ self.assertIn('xsd:string', schema_str)
+ self.assertIn('bsfs:Annotation', schema_str)
+ self.assertIn('bse:comment', schema_str)
+ self.assertIn('bse:owner', schema_str)
+ self.assertIn('bse:note', schema_str)
+ # inherited properties are not serialized
+ self.assertIsNotNone(re.search(r'bse:comment[^\.]*rdfs:range[^\.]', schema_str))
+ self.assertIsNone(re.search(r'bse:comment[^\.]*rdfs:domain[^\.]', schema_str))
+ #p_note has no domain/range
+ self.assertIsNone(re.search(r'bse:note[^\.]*rdfs:domain[^\.]', schema_str))
+ self.assertIsNone(re.search(r'bse:note[^\.]*rdfs:range[^\.]', schema_str))
+ # unserialize yields the original schema
+ self.assertEqual(schema, from_string(schema_str))
+
+ # predicate annotations are serialized
+ annotations = {
+ ns.rdfs.label: 'hello world',
+ ns.schema.description: 'some text',
+ ns.bsfs.foo: 1234,
+ ns.bsfs.bar: False,
+ }
+ p_annotation = types.ROOT_PREDICATE.child(ns.bsfs.Annotation, **annotations)
+ self.assertDictEqual(
+ annotations,
+ from_string(to_string(Schema({p_annotation}))).predicate(ns.bsfs.Annotation).annotations)
+
+
+ def test_feature(self):
+ # root features
+ f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'),
+ distance=ns.bsfs.cosine)
+ # derived features
+ f_colors1234 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#1234'),
+ dimension=1024) # inherits dtype, distance
+ f_colors4321 = f_colors.child(URI('http://bsfs.ai/schema/Feature/colors#4321'),
+ dimension=2048, distance=ns.bsfs.euclidean) # inherits dtype
+ # create schema
+ schema = Schema(literals={f_colors, f_colors1234, f_colors4321})
+
+ schema_str = to_string(schema)
+ # all symbols are serialized
+ self.assertIn('bsfs:Array', schema_str)
+ self.assertIn('<http://bsfs.ai/schema/Feature/colors', schema_str)
+ self.assertIn('<http://bsfs.ai/schema/Feature/colors#1234', schema_str)
+ self.assertIn('<http://bsfs.ai/schema/Feature/colors#4321', schema_str)
+ # inherited properties are not serialized
+ self.assertIsNotNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#1234>[^\.]*bsfs:dimension[^\.]', schema_str))
+ self.assertIsNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#1234>[^\.]*bsfs:dtype[^\.]', schema_str))
+ self.assertIsNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#1234>[^\.]*bsfs:distance[^\.]', schema_str))
+ self.assertIsNotNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#4321>[^\.]*bsfs:dimension[^\.]', schema_str))
+ self.assertIsNotNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#4321>[^\.]*bsfs:distance[^\.]', schema_str))
+ self.assertIsNone(re.search(r'<http://bsfs\.ai/schema/Feature/colors#4321>[^\.]*bsfs:dtype[^\.]', schema_str))
+ # unserialize yields the original schema
+ self.assertEqual(schema, from_string(schema_str))
+
+ # predicate annotations are serialized
+ annotations = {
+ ns.rdfs.label: 'hello world',
+ ns.schema.description: 'some text',
+ ns.bsfs.foo: 1234,
+ ns.bsfs.bar: False,
+ }
+ f_colors = types.ROOT_FEATURE.child(URI('http://bsfs.ai/schema/Feature/colors'),
+ dtype=ns.bsfs.f16, distance=ns.bsfs.euclidean,
+ **annotations)
+ self.assertDictEqual(
+ annotations,
+ from_string(to_string(Schema(literals={f_colors}))).literal(URI('http://bsfs.ai/schema/Feature/colors')).annotations)
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/schema/test_types.py b/test/schema/test_types.py
index 4a49e6e..c5895d2 100644
--- a/test/schema/test_types.py
+++ b/test/schema/test_types.py
@@ -10,15 +10,17 @@ import unittest
# bsfs imports
from bsfs.namespace import ns
+from bsfs.schema.types import ROOT_PREDICATE, ROOT_VERTEX, ROOT_FEATURE
from bsfs.utils import errors
# objects to test
-from bsfs.schema.types import _Type, _Vertex, Node, Literal, Predicate
+from bsfs.schema.types import _Type, Vertex, Node, Literal, Predicate, Feature
## code ##
class TestType(unittest.TestCase):
+
def test_parents(self):
# create some types
fst = _Type('First')
@@ -31,7 +33,25 @@ class TestType(unittest.TestCase):
self.assertListEqual(list(trd.parents()), [snd, fst])
self.assertListEqual(list(frd.parents()), [trd, snd, fst])
- def test_essentials(self):
+ def test_annotations(self):
+ # annotations can be empty
+ self.assertDictEqual(_Type('Foo', None).annotations, {})
+ # annotations are stored
+ self.assertDictEqual(_Type('Foo', None, foo='bar', bar=123).annotations, {
+ 'foo': 'bar',
+ 'bar': 123})
+ # comparison ignores annotations
+ self.assertEqual(
+ _Type('Foo', None, foo='bar', bar='foo'),
+ _Type('Foo', None, hello='world', foobar=1234))
+ self.assertEqual(
+ hash(_Type('Foo', None, foo='bar', bar='foo')),
+ hash(_Type('Foo', None, hello='world', foobar=1234)))
+ # annotations can be passed to child
+ self.assertDictEqual(_Type('First', foo='bar').child('Second', bar='foo').annotations, {
+ 'bar': 'foo'})
+
+ def test_string_conversion(self):
# type w/o parent
self.assertEqual(str(_Type('Foo')), '_Type(Foo)')
self.assertEqual(repr(_Type('Foo')), '_Type(Foo, None)')
@@ -51,14 +71,17 @@ class TestType(unittest.TestCase):
self.assertEqual(str(_Type('Foo', SubType('Bar'))), '_Type(Foo)')
self.assertEqual(repr(_Type('Foo', SubType('Bar'))), '_Type(Foo, SubType(Bar, None))')
- def test_get_child(self):
+ def test_child(self):
# callee is used as parent
- self.assertEqual(_Type('First').get_child('Second'), _Type('Second', _Type('First')))
+ self.assertEqual(_Type('First').child('Second'), _Type('Second', _Type('First')))
# works with multiple parents
- self.assertEqual(_Type('First').get_child('Second').get_child('Third'), _Type('Third', _Type('Second', _Type('First'))))
+ self.assertEqual(_Type('First').child('Second').child('Third'), _Type('Third', _Type('Second', _Type('First'))))
# type persists
class Foo(_Type): pass
- self.assertEqual(Foo('First').get_child('Second'), Foo('Second', Foo('First')))
+ self.assertEqual(Foo('First').child('Second'), Foo('Second', Foo('First')))
+ # annotations are respected
+ self.assertDictEqual(_Type('First', foo='bar').child('Second', bar='foo').annotations, {
+ 'bar': 'foo'})
def test_equality(self):
# equality depends on uri
@@ -76,6 +99,13 @@ class TestType(unittest.TestCase):
# comparison respects parent
self.assertNotEqual(_Type('Foo', _Type('Bar')), _Type('Foo'))
self.assertNotEqual(hash(_Type('Foo', _Type('Bar'))), hash(_Type('Foo')))
+ # comparison ignores annotations
+ self.assertEqual(
+ _Type('Foo', None, foo='bar', bar='foo'),
+ _Type('Foo', None, hello='world', foobar=1234))
+ self.assertEqual(
+ hash(_Type('Foo', None, foo='bar', bar='foo')),
+ hash(_Type('Foo', None, hello='world', foobar=1234)))
def test_order(self):
# create some types.
@@ -109,27 +139,43 @@ class TestType(unittest.TestCase):
self.assertFalse(bike > bicycle)
self.assertFalse(bike >= bicycle)
self.assertFalse(bike == bicycle)
+
+ # comparing different classes returns False ...
+ # ... when classes are hierarchically related
class Foo(_Type): pass
- foo = Foo(bike.uri, bike.parent)
- # cannot compare different types
- self.assertRaises(TypeError, operator.lt, foo, bike)
- self.assertRaises(TypeError, operator.le, foo, bike)
- self.assertRaises(TypeError, operator.gt, foo, bike)
- self.assertRaises(TypeError, operator.ge, foo, bike)
+ foo = Foo('Foo', bike)
+ self.assertFalse(foo < bike)
+ self.assertFalse(foo <= bike)
+ self.assertFalse(foo > bike)
+ self.assertFalse(foo >= bike)
+ # goes both ways
+ self.assertFalse(bike < foo)
+ self.assertFalse(bike <= foo)
+ self.assertFalse(bike > foo)
+ self.assertFalse(bike >= foo)
+ # ... when classes are unrelated
+ class Bar(_Type): pass
+ bar = Bar('Bar', bike)
+ self.assertFalse(foo < bar)
+ self.assertFalse(foo <= bar)
+ self.assertFalse(foo > bar)
+ self.assertFalse(foo >= bar)
# goes both ways
- self.assertRaises(TypeError, operator.lt, bike, foo)
- self.assertRaises(TypeError, operator.le, bike, foo)
- self.assertRaises(TypeError, operator.gt, bike, foo)
- self.assertRaises(TypeError, operator.ge, bike, foo)
+ self.assertFalse(bar < foo)
+ self.assertFalse(bar <= foo)
+ self.assertFalse(bar > foo)
+ self.assertFalse(bar >= foo)
+
class TestPredicate(unittest.TestCase):
def test_construction(self):
# domain must be a node
self.assertRaises(TypeError, Predicate, ns.bse.foo, 1234, None, True)
self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Literal(ns.bsfs.Foo, None), None, True)
- # range must be None, a Literal, or a Node
+ # range must be a Literal, a Node, or the root Vertex
+ self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), None, True)
self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), 1234, True)
- self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), _Vertex(ns.bsfs.Foo, None), True)
+ self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), Vertex(ns.bsfs.Foo, None), True)
self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), _Type(ns.bsfs.Foo, None), True)
class Foo(): pass
self.assertRaises(TypeError, Predicate, ns.bse.foo, None, Node(ns.bsfs.Node, None), Foo(), True)
@@ -138,82 +184,160 @@ class TestPredicate(unittest.TestCase):
n_root = Node(ns.bsfs.Node, None)
n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None))
n_tag = Node(ns.bsfs.Tag, Node(ns.bsfs.Tag, None))
- root = Predicate(
- uri=ns.bsfs.Predicate,
- parent=None,
+ root = ROOT_PREDICATE
+ tag = Predicate(
+ uri=ns.bse.tag,
+ parent=root,
domain=n_root,
- range=None,
+ range=n_tag,
unique=False,
)
# instance is equal to itself
- self.assertEqual(root, root)
- self.assertEqual(hash(root), hash(root))
+ self.assertEqual(tag, tag)
+ self.assertEqual(hash(tag), hash(tag))
# instance is equal to a clone
- self.assertEqual(root, Predicate(ns.bsfs.Predicate, None, n_root, None, False))
- self.assertEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_root, None, False)))
+ self.assertEqual(tag, Predicate(ns.bse.tag, root, n_root, n_tag, False))
+ self.assertEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_root, n_tag, False)))
# equality respects uri
- self.assertNotEqual(root, Predicate(ns.bsfs.Alternative, None, n_root, None, False))
- self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Alternative, None, n_root, None, False)))
+ self.assertNotEqual(tag, Predicate(ns.bsfs.Alternative, root, n_root, n_tag, False))
+ self.assertNotEqual(hash(tag), hash(Predicate(ns.bsfs.Alternative, root, n_root, n_tag, False)))
# equality respects parent
- self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, n_root, n_root, None, False))
- self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, n_root, n_root, None, False)))
+ self.assertNotEqual(tag, Predicate(ns.bse.tag, n_root, n_root, n_tag, False))
+ self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, n_root, n_root, n_tag, False)))
# equality respects domain
- self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, None, n_ent, None, False))
- self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_ent, None, False)))
+ self.assertNotEqual(tag, Predicate(ns.bse.tag, root, n_ent, n_tag, False))
+ self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_ent, n_tag, False)))
# equality respects range
- self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, None, n_root, n_root, False))
- self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_root, n_root, False)))
+ self.assertNotEqual(tag, Predicate(ns.bse.tag, root, n_root, n_root, False))
+ self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_root, n_root, False)))
# equality respects unique
- self.assertNotEqual(root, Predicate(ns.bsfs.Predicate, None, n_root, None, True))
- self.assertNotEqual(hash(root), hash(Predicate(ns.bsfs.Predicate, None, n_root, None, True)))
+ self.assertNotEqual(tag, Predicate(ns.bse.tag, root, n_root, n_tag, True))
+ self.assertNotEqual(hash(tag), hash(Predicate(ns.bse.tag, root, n_root, n_tag, True)))
- def test_get_child(self):
+ def test_child(self):
n_root = Node(ns.bsfs.Node, None)
+ l_root = Literal(ns.bsfs.Literal, None)
n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None))
n_tag = Node(ns.bsfs.Tag, Node(ns.bsfs.Tag, None))
- root = Predicate(
- uri=ns.bsfs.Predicate,
- parent=None,
- domain=n_root,
- range=None,
- unique=False,
- )
+ root = ROOT_PREDICATE
tag = Predicate(
- uri=ns.bsfs.Entity,
+ uri=ns.bse.tag,
parent=root,
domain=n_ent,
range=n_tag,
unique=False,
)
+ # child returns Predicate
+ self.assertIsInstance(tag.child(ns.bse.foo), Predicate)
# uri is respected
- self.assertEqual(ns.bse.foo, tag.get_child(ns.bse.foo).uri)
+ self.assertEqual(ns.bse.foo, tag.child(ns.bse.foo).uri)
# domain is respected
dom = Node(ns.bsfs.Image, n_ent)
- self.assertEqual(dom, tag.get_child(ns.bse.foo, domain=dom).domain)
+ self.assertEqual(dom, tag.child(ns.bse.foo, domain=dom).domain)
# range is respected
rng = Node(ns.bsfs.Group, n_tag)
- self.assertEqual(rng, tag.get_child(ns.bse.foo, range=rng).range)
+ self.assertEqual(rng, tag.child(ns.bse.foo, range=rng).range)
# cannot set range to None
- self.assertEqual(n_tag, tag.get_child(ns.bse.foo, range=None).range)
+ self.assertEqual(n_tag, tag.child(ns.bse.foo, range=None).range)
# unique is respected
- self.assertTrue(tag.get_child(ns.bse.foo, unique=True).unique)
+ self.assertTrue(tag.child(ns.bse.foo, unique=True).unique)
+ # annotations are respected
+ self.assertDictEqual(tag.child(ns.bse.foo, foo='bar', bar=123).annotations, {
+ 'foo': 'bar',
+ 'bar': 123,
+ })
# domain is inherited from parent
- self.assertEqual(n_ent, tag.get_child(ns.bse.foo).domain)
+ self.assertEqual(n_root, root.child(ns.bse.foo).domain)
+ self.assertEqual(n_ent, tag.child(ns.bse.foo).domain)
# range is inherited from parent
- self.assertEqual(n_tag, tag.get_child(ns.bse.foo).range)
+ self.assertEqual(ROOT_VERTEX, root.child(ns.bse.foo).range)
+ self.assertEqual(n_tag, tag.child(ns.bse.foo).range)
# uniqueness is inherited from parent
- self.assertFalse(tag.get_child(ns.bse.foo).unique)
+ self.assertFalse(tag.child(ns.bse.foo).unique)
# domain must be subtype of parent's domain
- self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, domain=n_root)
- self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root))
- # range cannot be None
- self.assertRaises(ValueError, root.get_child, ns.bse.foo)
+ self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, domain=n_root)
+ self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, domain=Node(ns.bsfs.Image, n_root))
# range must be subtype of parent's range
- self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, range=n_root)
- self.assertRaises(errors.ConsistencyError, tag.get_child, ns.bse.foo, range=Node(ns.bsfs.Image, n_root))
+ self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=n_root)
+ self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=Node(ns.bsfs.Image, n_root))
+ self.assertRaises(errors.ConsistencyError, tag.child, ns.bse.foo, range=Literal(ns.bsfs.Tag, l_root))
+ # range can be subtyped from ROOT_VERTEX to Node or Literal
+ self.assertEqual(n_root, root.child(ns.bse.foo, range=n_root).range)
+ self.assertEqual(l_root, root.child(ns.bse.foo, range=l_root).range)
+
+
+class TestFeature(unittest.TestCase):
+ def test_construction(self):
+ n_root = Node(ns.bsfs.Node, None)
+ l_root = Literal(ns.bsfs.Literal, None)
+ # dimension, dtype, and distance are respected
+ feat = Feature(ns.bsfs.Feature, None, 1234, ns.bsfs.float, ns.bsfs.euclidean)
+ self.assertEqual(1234, feat.dimension)
+ self.assertEqual(ns.bsfs.float, feat.dtype)
+ self.assertEqual(ns.bsfs.euclidean, feat.distance)
+
+ def test_equality(self):
+ n_ent = Node(ns.bsfs.Entity, Node(ns.bsfs.Node, None))
+ colors = Feature(
+ uri=ns.bse.colors,
+ parent=ROOT_FEATURE,
+ dimension=1234,
+ dtype=ns.bsfs.float,
+ distance=ns.bsfs.euclidean,
+ )
+ # instance is equal to itself
+ self.assertEqual(colors, colors)
+ self.assertEqual(hash(colors), hash(colors))
+ # instance is equal to a clone
+ self.assertEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.euclidean))
+ self.assertEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.euclidean)))
+ # equality respects dimension
+ self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 4321, ns.bsfs.float, ns.bsfs.euclidean))
+ self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 4321, ns.bsfs.float, ns.bsfs.euclidean)))
+ # equality respects dtype
+ self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.integer, ns.bsfs.euclidean))
+ self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.integer, ns.bsfs.euclidean)))
+ # equality respects distance
+ self.assertNotEqual(colors, Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.cosine))
+ self.assertNotEqual(hash(colors), hash(Feature(ns.bse.colors, ROOT_FEATURE, 1234, ns.bsfs.float, ns.bsfs.cosine)))
+
+ def test_child(self):
+ n_root = Node(ns.bsfs.Node, None)
+ n_ent = Node(ns.bsfs.Entity, n_root)
+ l_root = Literal(ns.bsfs.Literal, None)
+ colors = Feature(
+ uri=ns.bse.colors,
+ parent=ROOT_FEATURE,
+ dimension=1234,
+ dtype=ns.bsfs.float,
+ distance=ns.bsfs.euclidean,
+ )
+
+ # child returns Feature
+ self.assertIsInstance(colors.child(ns.bse.foo), Feature)
+ # uri is respected
+ self.assertEqual(ns.bse.foo, colors.child(ns.bse.foo).uri)
+ # dimension is respected
+ self.assertEqual(4321, colors.child(ns.bse.foo, dimension=4321).dimension)
+ # dtype is respected
+ self.assertEqual(ns.bsfs.integer, colors.child(ns.bse.foo, dtype=ns.bsfs.integer).dtype)
+ # distance is respected
+ self.assertEqual(ns.bsfs.cosine, colors.child(ns.bse.foo, distance=ns.bsfs.cosine).distance)
+ # annotations are respected
+ self.assertDictEqual(colors.child(ns.bse.foo, foo='bar', bar=123).annotations, {
+ 'foo': 'bar',
+ 'bar': 123,
+ })
+
+ # dimension is inherited from parent
+ self.assertEqual(1234, colors.child(ns.bse.foo).dimension)
+ # dtype is inherited from parent
+ self.assertEqual(ns.bsfs.float, colors.child(ns.bse.foo).dtype)
+ # distance is inherited from parent
+ self.assertEqual(ns.bsfs.euclidean, colors.child(ns.bse.foo).distance)
## main ##
@@ -222,4 +346,3 @@ if __name__ == '__main__':
unittest.main()
## EOF ##
-
diff --git a/test/triple_store/sparql/test_distance.py b/test/triple_store/sparql/test_distance.py
new file mode 100644
index 0000000..0659459
--- /dev/null
+++ b/test/triple_store/sparql/test_distance.py
@@ -0,0 +1,61 @@
+"""
+
+Part of the bsfs test suite.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# imports
+import numpy as np
+import unittest
+
+# objects to test
+from bsfs.triple_store.sparql import distance
+
+
+## code ##
+
+class TestDistance(unittest.TestCase):
+
+ def test_euclid(self):
+ # self-distance is zero
+ self.assertEqual(distance.euclid([1,2,3,4], [1,2,3,4]), 0.0)
+ # accepts list-like arguments
+ self.assertAlmostEqual(distance.euclid([1,2,3,4], [2,3,4,5]), 2.0, 3)
+ self.assertAlmostEqual(distance.euclid((1,2,3,4), (2,3,4,5)), 2.0, 3)
+ # dimension can vary
+ self.assertAlmostEqual(distance.euclid([1,2,3], [2,3,4]), 1.732, 3)
+ self.assertAlmostEqual(distance.euclid([1,2,3,4,5], [2,3,4,5,6]), 2.236, 3)
+ # vector can be zero
+ self.assertAlmostEqual(distance.euclid([0,0,0], [1,2,3]), 3.742, 3)
+
+ def test_cosine(self):
+ # self-distance is zero
+ self.assertEqual(distance.cosine([1,2,3,4], [1,2,3,4]), 0.0)
+ # accepts list-like arguments
+ self.assertAlmostEqual(distance.cosine([1,2,3,4], [4,3,2,1]), 0.333, 3)
+ self.assertAlmostEqual(distance.cosine((1,2,3,4), (4,3,2,1)), 0.333, 3)
+ # dimension can vary
+ self.assertAlmostEqual(distance.cosine([1,2,3], [3,2,1]), 0.286, 3)
+ self.assertAlmostEqual(distance.cosine([1,2,3,4,5], [5,4,3,2,1]), 0.364, 3)
+ # vector can be zero
+ self.assertAlmostEqual(distance.cosine([0,0,0], [1,2,3]), 1.0, 3)
+
+ def test_manhatten(self):
+ # self-distance is zero
+ self.assertEqual(distance.manhatten([1,2,3,4], [1,2,3,4]), 0.0)
+ # accepts list-like arguments
+ self.assertAlmostEqual(distance.manhatten([1,2,3,4], [2,3,4,5]), 4.0, 3)
+ self.assertAlmostEqual(distance.manhatten((1,2,3,4), (2,3,4,5)), 4.0, 3)
+ # dimension can vary
+ self.assertAlmostEqual(distance.manhatten([1,2,3], [2,3,4]), 3.0, 3)
+ self.assertAlmostEqual(distance.manhatten([1,2,3,4,5], [2,3,4,5,6]), 5.0, 3)
+ # vector can be zero
+ self.assertAlmostEqual(distance.manhatten([0,0,0], [1,2,3]), 6.0, 3)
+
+
+## main ##
+
+if __name__ == '__main__':
+ unittest.main()
+
+## EOF ##
diff --git a/test/triple_store/sparql/test_parse_filter.py b/test/triple_store/sparql/test_parse_filter.py
index bd19803..8764535 100644
--- a/test/triple_store/sparql/test_parse_filter.py
+++ b/test/triple_store/sparql/test_parse_filter.py
@@ -9,7 +9,7 @@ import rdflib
import unittest
# bsie imports
-from bsfs import schema as _schema
+from bsfs import schema as bsc
from bsfs.namespace import ns
from bsfs.query import ast
from bsfs.utils import errors
@@ -23,21 +23,34 @@ from bsfs.triple_store.sparql.parse_filter import Filter
class TestParseFilter(unittest.TestCase):
def setUp(self):
# schema
- self.schema = _schema.Schema.from_string('''
+ self.schema = bsc.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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+
bsfs:Entity rdfs:subClassOf bsfs:Node .
bsfs:Image rdfs:subClassOf bsfs:Entity .
bsfs:Tag rdfs:subClassOf bsfs:Node .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
bsfs:URI rdfs:subClassOf bsfs:Literal .
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "4"^^xsd:integer ;
+ bsfs:dtype xsd:integer ;
+ bsfs:distance bsfs:euclidean .
+
+ bse:colors rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Colors .
+
bse:comment rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Node ;
rdfs:range xsd:string ;
@@ -70,9 +83,6 @@ class TestParseFilter(unittest.TestCase):
''')
- # parser instance
- self.parser = Filter(self.schema)
-
# graph to test queries
self.graph = rdflib.Graph()
# schema hierarchies
@@ -113,6 +123,13 @@ class TestParseFilter(unittest.TestCase):
# image iso
self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(1234, datatype=rdflib.XSD.integer)))
self.graph.add((rdflib.URIRef('http://example.com/image#4321'), rdflib.URIRef(ns.bse.iso), rdflib.Literal(4321, datatype=rdflib.XSD.integer)))
+ # color features
+ self.graph.add((rdflib.URIRef('http://example.com/entity#1234'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([1,2,3,4], datatype=rdflib.URIRef(ns.bsfs.Colors))))
+ self.graph.add((rdflib.URIRef('http://example.com/entity#4321'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([4,3,2,1], datatype=rdflib.URIRef(ns.bsfs.Colors))))
+ self.graph.add((rdflib.URIRef('http://example.com/image#1234'), rdflib.URIRef(ns.bse.colors), rdflib.Literal([3,4,2,1], datatype=rdflib.URIRef(ns.bsfs.Colors))))
+
+ # parser instance
+ self.parser = Filter(self.graph, self.schema)
def test_routing(self):
@@ -124,7 +141,7 @@ class TestParseFilter(unittest.TestCase):
# __call__ requires a valid root type
self.assertRaises(errors.BackendError, self.parser, self.schema.literal(ns.bsfs.Literal), None)
- self.assertRaises(errors.ConsistencyError, self.parser, self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), None)
+ self.assertRaises(errors.ConsistencyError, self.parser, self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), None)
# __call__ requires a parseable root
self.assertRaises(errors.BackendError, self.parser, self.schema.node(ns.bsfs.Entity), ast.filter.FilterExpression())
# __call__ returns an executable query
@@ -613,6 +630,37 @@ class TestParseFilter(unittest.TestCase):
{'http://example.com/tag#1234'})
+ def test_distance(self):
+ # node colors distance to [2,4,3,1]
+ # entity#1234 [1,2,3,4] 3.742
+ # entity#4321 [4,3,2,1] 2.449
+ # image#1234 [3,4,2,1] 1.414
+
+ # _distance expects a feature
+ self.assertRaises(errors.BackendError, self.parser._distance, self.schema.node(ns.bsfs.Entity), ast.filter.Distance([1,2,3,4], 1), '')
+ # reference must have the correct dimension
+ self.assertRaises(errors.ConsistencyError, self.parser._distance, self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3], 1), '')
+ self.assertRaises(errors.ConsistencyError, self.parser._distance, self.schema.literal(ns.bsfs.Colors), ast.filter.Distance([1,2,3,4,5], 1), '')
+ # _distance respects threshold
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 4)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234', 'http://example.com/entity#4321', 'http://example.com/image#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 3)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#4321', 'http://example.com/image#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 2)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/image#1234'})
+ # result set can be empty
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([2,4,3,1], 1)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+ # _distance respects strict
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4], 0, False)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)},
+ {'http://example.com/entity#1234'})
+ q = self.parser(self.schema.node(ns.bsfs.Entity), ast.filter.Any(ns.bse.colors, ast.filter.Distance([1,2,3,4], 0, True)))
+ self.assertSetEqual({str(guid) for guid, in self.graph.query(q)}, set())
+
def test_one_of(self):
# _one_of expects a node
self.assertRaises(errors.BackendError, self.parser._one_of,
diff --git a/test/triple_store/sparql/test_sparql.py b/test/triple_store/sparql/test_sparql.py
index 3d81de1..7fbfb65 100644
--- a/test/triple_store/sparql/test_sparql.py
+++ b/test/triple_store/sparql/test_sparql.py
@@ -9,7 +9,7 @@ import rdflib
import unittest
# bsie imports
-from bsfs import schema as _schema
+from bsfs import schema as bsc
from bsfs.namespace import ns
from bsfs.query import ast
from bsfs.utils import errors, URI
@@ -22,7 +22,7 @@ from bsfs.triple_store.sparql.sparql import SparqlStore
class TestSparqlStore(unittest.TestCase):
def setUp(self):
- self.schema = _schema.Schema.from_string('''
+ self.schema = bsc.from_string('''
prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>
@@ -33,7 +33,8 @@ class TestSparqlStore(unittest.TestCase):
bsfs:Tag rdfs:subClassOf bsfs:Node .
bsfs:User rdfs:subClassOf bsfs:Node .
xsd:string rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
# non-unique literal
bse:comment rdfs:subClassOf bsfs:Predicate ;
@@ -66,7 +67,11 @@ class TestSparqlStore(unittest.TestCase):
(rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
(rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
(rdflib.URIRef(ns.xsd.string), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
- (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Array), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Array)),
+ (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Time), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)),
(rdflib.URIRef(ns.bse.comment), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
(rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
(rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
@@ -90,7 +95,7 @@ class TestSparqlStore(unittest.TestCase):
def test__has_type(self):
# setup store
store = SparqlStore.Open()
- store.schema = _schema.Schema.from_string('''
+ store.schema = bsc.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/>
@@ -108,7 +113,7 @@ class TestSparqlStore(unittest.TestCase):
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))
+ self.assertRaises(errors.ConsistencyError, store._has_type, URI('http://example.com/me/entity#1234'), store.schema.node(ns.bsfs.Node).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)))
@@ -195,7 +200,7 @@ class TestSparqlStore(unittest.TestCase):
self.assertSetEqual(set(store._graph), instances)
# add some classes to the schema
- curr = curr + _schema.Schema.from_string('''
+ curr = curr + bsc.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/>
@@ -298,7 +303,7 @@ class TestSparqlStore(unittest.TestCase):
# remove some classes from the schema
- curr = _schema.Schema.from_string('''
+ curr = bsc.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/>
@@ -310,7 +315,8 @@ class TestSparqlStore(unittest.TestCase):
bsfs:User rdfs:subClassOf bsfs:Node .
xsd:boolean rdfs:subClassOf bsfs:Literal .
- xsd:integer rdfs:subClassOf bsfs:Literal .
+ bsfs:Number rdfs:subClassOf bsfs:Literal .
+ xsd:integer rdfs:subClassOf bsfs:Number .
bse:filesize rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Entity ;
@@ -351,7 +357,11 @@ class TestSparqlStore(unittest.TestCase):
(rdflib.URIRef(ns.bsfs.Tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
(rdflib.URIRef(ns.bsfs.User), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Node)),
(rdflib.URIRef(ns.xsd.boolean), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
- (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Array), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Feature), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Array)),
+ (rdflib.URIRef(ns.bsfs.Number), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.bsfs.Time), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Literal)),
+ (rdflib.URIRef(ns.xsd.integer), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Number)),
(rdflib.URIRef(ns.bse.shared), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
(rdflib.URIRef(ns.bse.tag), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
(rdflib.URIRef(ns.bse.filesize), rdflib.RDFS.subClassOf, rdflib.URIRef(ns.bsfs.Predicate)),
@@ -382,8 +392,25 @@ class TestSparqlStore(unittest.TestCase):
class Foo(): pass
self.assertRaises(TypeError, setattr, store, 'schema', Foo())
+ # cannot define features w/o known distance function
+ invalid = bsc.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:Array rdfs:subClassOf bsfs:Literal .
+ bsfs:Feature rdfs:subClassOf bsfs:Array .
+
+ bsfs:Colors rdfs:subClassOf bsfs:Feature ;
+ bsfs:dimension "4"^^xsd:integer ;
+ bsfs:distance bsfs:foobar .
+
+ ''')
+ self.assertRaises(errors.UnsupportedError, setattr, store, 'schema', invalid)
+
# cannot migrate to incompatible schema
- invalid = _schema.Schema.from_string('''
+ invalid = bsc.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/>
@@ -399,7 +426,7 @@ class TestSparqlStore(unittest.TestCase):
''')
self.assertRaises(errors.ConsistencyError, setattr, store, 'schema', invalid)
- invalid = _schema.Schema.from_string('''
+ invalid = bsc.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/>
@@ -509,7 +536,7 @@ class TestSparqlStore(unittest.TestCase):
store.set(ent_type, {URI('http://example.com/me/entity#1234')}, self.schema.predicate(ns.bse.filesize), {1234})
store.set(ent_type, {URI('http://example.com/me/entity#4321')}, self.schema.predicate(ns.bse.filesize), {4321})
# node_type must be in the schema
- self.assertRaises(errors.ConsistencyError, set, store.get(self.schema.node(ns.bsfs.Node).get_child(ns.bsfs.Invalid), ast.filter.IsIn(ent_ids)))
+ self.assertRaises(errors.ConsistencyError, set, store.get(self.schema.node(ns.bsfs.Node).child(ns.bsfs.Invalid), ast.filter.IsIn(ent_ids)))
# query must be a filter expression
class Foo(): pass
self.assertRaises(TypeError, set, store.get(ent_type, 1234))
@@ -574,7 +601,7 @@ class TestSparqlStore(unittest.TestCase):
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), {
+ self.assertRaises(errors.ConsistencyError, store.create, self.schema.node(ns.bsfs.Entity).child(ns.bsfs.invalid), {
URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')})
# can create some nodes
@@ -636,7 +663,7 @@ class TestSparqlStore(unittest.TestCase):
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))
+ p_invalid = store.schema.predicate(ns.bsfs.Predicate).child(ns.bsfs.foo, range=store.schema.node(ns.bsfs.Tag))
# create node instances
ent_ids = {
URI('http://example.com/me/entity#1234'),
@@ -659,7 +686,7 @@ class TestSparqlStore(unittest.TestCase):
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),
+ self.assertRaises(errors.ConsistencyError, store.set, self.schema.node(ns.bsfs.Node).child(ns.bse.foo),
ent_ids, p_comment, {'hello world'})
# invalid predicate is not permitted
diff --git a/test/utils/test_uuid.py b/test/utils/test_uuid.py
index 49176d4..0de96ed 100644
--- a/test/utils/test_uuid.py
+++ b/test/utils/test_uuid.py
@@ -83,6 +83,10 @@ class TestUCID(unittest.TestCase):
def test_from_path(self):
self.assertEqual(UCID.from_path(self._path), self._checksum)
+ def test_from_dict(self):
+ self.assertEqual(UCID.from_dict({'hello': 'world', 'foo': 1234, 'bar': False}),
+ '8d2544395a0d2827e3d9ce8cd619d5e3f801e8126bf3f93ee5abd38158959585')
+
## main ##