diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-03-02 12:19:58 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-03-02 12:19:58 +0100 |
commit | 87f437380c1dd8f420437cddc028c0f3174ee1c9 (patch) | |
tree | acebd2de636477cf6b498256a67f6863abd5a9c1 | |
parent | d70e78bbdd9d9b5727f18a82fce08f20bdbbba19 (diff) | |
download | bsfs-87f437380c1dd8f420437cddc028c0f3174ee1c9.tar.gz bsfs-87f437380c1dd8f420437cddc028c0f3174ee1c9.tar.bz2 bsfs-87f437380c1dd8f420437cddc028c0f3174ee1c9.zip |
Node getters in bsfs.Graph:
* Empty nodes instance (Graph.empty)
* Order-preserving get query (Graph.sorted)
* Collect common code in private Graph.__get
* Empty query in Graph.get
* Empty query in Graph.resolve.Filter
* Empty query in AC: filter_read
-rw-r--r-- | bsfs/graph/ac/base.py | 6 | ||||
-rw-r--r-- | bsfs/graph/ac/null.py | 6 | ||||
-rw-r--r-- | bsfs/graph/graph.py | 60 | ||||
-rw-r--r-- | bsfs/graph/resolve.py | 9 | ||||
-rw-r--r-- | test/graph/ac/test_null.py | 3 | ||||
-rw-r--r-- | test/graph/test_graph.py | 95 | ||||
-rw-r--r-- | test/graph/test_resolve.py | 3 |
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)), |