aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bsfs/graph/ac/base.py6
-rw-r--r--bsfs/graph/ac/null.py6
-rw-r--r--bsfs/graph/graph.py60
-rw-r--r--bsfs/graph/resolve.py9
-rw-r--r--test/graph/ac/test_null.py3
-rw-r--r--test/graph/test_graph.py95
-rw-r--r--test/graph/test_resolve.py3
7 files changed, 151 insertions, 31 deletions
diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py
index 0b9f988..2759557 100644
--- a/bsfs/graph/ac/base.py
+++ b/bsfs/graph/ac/base.py
@@ -83,7 +83,11 @@ class AccessControlBase(abc.ABC):
"""Return nodes that are allowed to be created."""
@abc.abstractmethod
- def filter_read(self, node_type: schema.Node, query: ast.filter.FilterExpression) -> ast.filter.FilterExpression:
+ def filter_read(
+ self,
+ node_type: schema.Node,
+ query: typing.Optional[ast.filter.FilterExpression],
+ ) -> typing.Optional[ast.filter.FilterExpression]:
"""Re-write a filter *query* to get (i.e., read) *node_type* nodes."""
@abc.abstractmethod
diff --git a/bsfs/graph/ac/null.py b/bsfs/graph/ac/null.py
index 6a923a5..e67b55d 100644
--- a/bsfs/graph/ac/null.py
+++ b/bsfs/graph/ac/null.py
@@ -50,7 +50,11 @@ class NullAC(base.AccessControlBase):
"""Return nodes that are allowed to be created."""
return guids
- def filter_read(self, node_type: schema.Node, query: ast.filter.FilterExpression) -> ast.filter.FilterExpression:
+ def filter_read(
+ self,
+ node_type: schema.Node,
+ query: typing.Optional[ast.filter.FilterExpression]
+ ) -> typing.Optional[ast.filter.FilterExpression]:
"""Re-write a filter *query* to get (i.e., read) *node_type* nodes."""
return query
diff --git a/bsfs/graph/graph.py b/bsfs/graph/graph.py
index a356533..11fe835 100644
--- a/bsfs/graph/graph.py
+++ b/bsfs/graph/graph.py
@@ -113,6 +113,7 @@ class Graph():
*node_type*) once some data is assigned to them.
"""
+ # get node type
type_ = self.schema.node(node_type)
# NOTE: Nodes constructor materializes guids.
return _nodes.Nodes(self._backend, self._ac, type_, guids)
@@ -120,15 +121,51 @@ class Graph():
def node(self, node_type: URI, guid: URI) -> _nodes.Nodes:
"""Return node *guid* of type *node_type* as a `bsfs.graph.Nodes` instance.
- Note that the *guids* need not to exist (however, the *node_type* has
+ Note that the *guid* need not to exist (however, the *node_type* has
to be part of the schema). An inexistent guid will be created (using
*node_type*) once some data is assigned to them.
"""
return self.nodes(node_type, {guid})
- def get(self, node_type: URI, query: ast.filter.FilterExpression) -> _nodes.Nodes: # FIXME: How about empty query?
- """Return a `Nodes` instance over all nodes of type *node_type* that match the *subject* query."""
+ def empty(self, node_type: URI) -> _nodes.Nodes:
+ """Return a `Nodes` instance with type *node_type* but no nodes."""
+ return self.nodes(node_type, set())
+
+ def get(
+ self,
+ node_type: URI,
+ query: typing.Optional[ast.filter.FilterExpression],
+ ) -> _nodes.Nodes:
+ """Return a `Nodes` instance over all nodes of type *node_type* that match the *query*."""
+ # return Nodes instance
+ type_ = self.schema.node(node_type)
+ return _nodes.Nodes(self._backend, self._ac, type_, self.__get(node_type, query))
+
+ def sorted(
+ self,
+ node_type: URI,
+ query: typing.Optional[ast.filter.FilterExpression],
+ # FIXME: sort ast
+ ) -> typing.Iterator[_nodes.Nodes]:
+ """Return a iterator over `Nodes` instances over all nodes of type *node_type* that match the *query*."""
+ # FIXME: Order should be a parameter
+ # return iterator over Nodes instances
+ type_ = self.schema.node(node_type)
+ for guid in self.__get(node_type, query):
+ yield _nodes.Nodes(self._backend, self._ac, type_, {guid})
+
+ def all(self, node_type: URI) -> _nodes.Nodes:
+ """Return all instances of type *node_type*."""
+ type_ = self.schema.node(node_type)
+ return _nodes.Nodes(self._backend, self._ac, type_, self.__get(node_type, None))
+
+ def __get(
+ self,
+ node_type: URI,
+ query: typing.Optional[ast.filter.FilterExpression],
+ ) -> typing.Iterator[URI]:
+ """Build and execute a get query."""
# get node type
type_ = self.schema.node(node_type)
# resolve Nodes instances
@@ -136,18 +173,9 @@ class Graph():
# add access controls to query
query = self._ac.filter_read(type_, query)
# validate query
- self._validate(type_, query)
- # query the backend
- guids = self._backend.get(type_, query) # no need to materialize
- # return Nodes instance
- return _nodes.Nodes(self._backend, self._ac, type_, guids)
-
- def all(self, node_type: URI) -> _nodes.Nodes:
- """Return all instances of type *node_type*."""
- # get node type
- type_ = self.schema.node(node_type)
- guids = self._backend.get(type_, None) # no need to materialize
- return _nodes.Nodes(self._backend, self._ac, type_, guids)
-
+ if query is not None:
+ self._validate(type_, query)
+ # query the backend and return the (non-materialized) result
+ return self._backend.get(type_, query)
## EOF ##
diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py
index 4677401..b3ab001 100644
--- a/bsfs/graph/resolve.py
+++ b/bsfs/graph/resolve.py
@@ -40,8 +40,13 @@ class Filter():
def __init__(self, schema):
self.schema = schema
- def __call__(self, root_type: bsc.Node, node: ast.filter.FilterExpression):
- # FIXME: node can be None!
+ def __call__(
+ self,
+ root_type: bsc.Node,
+ node: typing.Optional[ast.filter.FilterExpression],
+ ):
+ if node is None:
+ return None
return self._parse_filter_expression(root_type, node)
def _parse_filter_expression(
diff --git a/test/graph/ac/test_null.py b/test/graph/ac/test_null.py
index e33d46a..544a01e 100644
--- a/test/graph/ac/test_null.py
+++ b/test/graph/ac/test_null.py
@@ -131,7 +131,10 @@ class TestNullAC(unittest.TestCase):
ast.filter.Any(ns.bse.tag, ast.filter.Is('http://example.com/tag#4321')),
ast.filter.Any(ns.bse.author, ast.filter.Equals('Me, Myself, and I')))
ac = NullAC(self.backend, self.user)
+ # NullAC returns query
self.assertEqual(query, ac.filter_read(self.ent_type, query))
+ # query can be none
+ self.assertIsNone(ac.filter_read(self.ent_type, None))
## main ##
diff --git a/test/graph/test_graph.py b/test/graph/test_graph.py
index d89d346..93f8db7 100644
--- a/test/graph/test_graph.py
+++ b/test/graph/test_graph.py
@@ -5,6 +5,8 @@ A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2022
"""
# imports
+from functools import reduce
+import operator
import unittest
# bsie imports
@@ -97,18 +99,14 @@ class TestGraph(unittest.TestCase):
# node_type must be in the schema
self.assertRaises(KeyError, graph.nodes, ns.bsfs.Invalid, guids)
- def test_all(self):
+ def test_empty(self):
graph = Graph(self.backend, self.ac)
- # resulting nodes can be empty
- self.assertEqual(graph.all(ns.bsfs.Entity),
+ # returns a Nodes instance
+ self.assertEqual(
+ graph.empty(ns.bsfs.Entity),
Nodes(self.backend, self.ac, graph.schema.node(ns.bsfs.Entity), set()))
- # resulting nodes contains all nodes of the respective type
- guids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}
- self.backend.create(graph.schema.node(ns.bsfs.Entity), guids)
- self.assertEqual(graph.all(ns.bsfs.Entity),
- Nodes(self.backend, self.ac, graph.schema.node(ns.bsfs.Entity), guids))
# node_type must be in the schema
- self.assertRaises(KeyError, graph.all, ns.bsfs.Invalid)
+ self.assertRaises(KeyError, graph.empty, ns.bsfs.Invalid)
def test_migrate(self):
# setup
@@ -248,10 +246,10 @@ class TestGraph(unittest.TestCase):
graph.node(ns.bsfs.Tag, URI('http://example.com/tag#1234')).set(ns.bse.comment, 'foo')
graph.node(ns.bsfs.Tag, URI('http://example.com/tag#4321')).set(ns.bse.comment, 'bar')
- # get exception for invalid query
+ # invalid query raises exception
self.assertRaises(errors.ConsistencyError, graph.get, ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Equals('hello world')))
- # query returns nodes
+ # get returns nodes
self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags))), ents)
self.assertEqual(graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('foo'))),
graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')))
@@ -262,6 +260,81 @@ class TestGraph(unittest.TestCase):
ast.filter.Any(ns.bse.tag, ast.filter.All(ns.bse.comment, ast.filter.Equals('bar'))))),
ents)
+ # query can be None
+ self.assertEqual(graph.get(ns.bsfs.Entity, None), ents)
+
+ def test_sorted(self):
+ # setup
+ graph = Graph(self.backend, self.ac)
+ 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/>
+ prefix bse: <http://bsfs.ai/schema/Entity#>
+
+ bsfs:Entity rdfs:subClassOf bsfs:Node .
+ bsfs:Tag rdfs:subClassOf bsfs:Node .
+ xsd:string rdfs:subClassOf bsfs:Literal .
+
+ bse:tag rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Entity ;
+ rdfs:range bsfs:Tag ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ bse:comment rdfs:subClassOf bsfs:Predicate ;
+ rdfs:domain bsfs:Node ;
+ rdfs:range xsd:string ;
+ bsfs:unique "false"^^xsd:boolean .
+
+ '''))
+ # add some instances
+ ents = [
+ # default is alphabetical order
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')),
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#4321')),
+ ]
+ tags = graph.nodes(ns.bsfs.Tag, {URI('http://example.com/tag#1234'), URI('http://example.com/tag#4321')})
+ # add some node links
+ reduce(operator.add, ents).set(ns.bse.tag, tags)
+ # add some literals
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'hello world')
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'foo')
+ graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234')).set(ns.bse.comment, 'foobar')
+ graph.node(ns.bsfs.Tag, URI('http://example.com/tag#1234')).set(ns.bse.comment, 'foo')
+ graph.node(ns.bsfs.Tag, URI('http://example.com/tag#4321')).set(ns.bse.comment, 'bar')
+
+ # invalid query raises exception
+ self.assertRaises(errors.ConsistencyError, list, graph.sorted(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Equals('hello world'))))
+
+ # get returns nodes
+ self.assertListEqual(list(graph.sorted(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags)))), ents)
+ self.assertListEqual(list(graph.sorted(ns.bsfs.Entity, ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('foo')))),
+ [graph.node(ns.bsfs.Entity, URI('http://example.com/entity#1234'))])
+ self.assertListEqual(list(graph.sorted(ns.bsfs.Node, ast.filter.Any(ns.bse.comment, ast.filter.StartsWith('foo')))), [
+ graph.node(ns.bsfs.Node, URI('http://example.com/entity#1234')),
+ graph.node(ns.bsfs.Node, URI('http://example.com/tag#1234')),
+ ])
+ self.assertListEqual(list(graph.sorted(ns.bsfs.Entity, ast.filter.Or(
+ ast.filter.Any(ns.bse.comment, ast.filter.EndsWith('bar')),
+ ast.filter.Any(ns.bse.tag, ast.filter.All(ns.bse.comment, ast.filter.Equals('bar')))))),
+ ents)
+
+ # query can be None
+ self.assertListEqual(list(graph.sorted(ns.bsfs.Entity, None)), ents)
+
+
+ def test_all(self):
+ graph = Graph(self.backend, self.ac)
+ # resulting nodes can be empty
+ self.assertEqual(graph.all(ns.bsfs.Entity),
+ Nodes(self.backend, self.ac, graph.schema.node(ns.bsfs.Entity), set()))
+ # resulting nodes contains all nodes of the respective type
+ guids = {URI('http://example.com/me/entity#1234'), URI('http://example.com/me/entity#4321')}
+ self.backend.create(graph.schema.node(ns.bsfs.Entity), guids)
+ self.assertEqual(graph.all(ns.bsfs.Entity),
+ Nodes(self.backend, self.ac, graph.schema.node(ns.bsfs.Entity), guids))
+ # node_type must be in the schema
+ self.assertRaises(KeyError, graph.all, ns.bsfs.Invalid)
diff --git a/test/graph/test_resolve.py b/test/graph/test_resolve.py
index 0918b02..b4d76c7 100644
--- a/test/graph/test_resolve.py
+++ b/test/graph/test_resolve.py
@@ -79,6 +79,9 @@ class TestFilter(unittest.TestCase):
{'http://example.com/you/invalid#1234', 'http://example.com/you/invalid#4321'})
resolver = Filter(schema)
+ # query can be None
+ self.assertIsNone(resolver(schema.node(ns.bsfs.Entity), None))
+
# immediate Is
self.assertEqual(resolver(schema.node(ns.bsfs.Entity),
ast.filter.Is(ents)),