aboutsummaryrefslogtreecommitdiffstats
path: root/bsfs/graph
diff options
context:
space:
mode:
Diffstat (limited to 'bsfs/graph')
-rw-r--r--bsfs/graph/__init__.py5
-rw-r--r--bsfs/graph/ac/__init__.py5
-rw-r--r--bsfs/graph/ac/base.py27
-rw-r--r--bsfs/graph/ac/null.py13
-rw-r--r--bsfs/graph/graph.py100
-rw-r--r--bsfs/graph/nodes.py113
-rw-r--r--bsfs/graph/resolve.py27
-rw-r--r--bsfs/graph/result.py5
-rw-r--r--bsfs/graph/schema.nt13
-rw-r--r--bsfs/graph/walk.py5
10 files changed, 168 insertions, 145 deletions
diff --git a/bsfs/graph/__init__.py b/bsfs/graph/__init__.py
index 82d2235..8d38d23 100644
--- a/bsfs/graph/__init__.py
+++ b/bsfs/graph/__init__.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
import typing
diff --git a/bsfs/graph/ac/__init__.py b/bsfs/graph/ac/__init__.py
index 420de01..11b45df 100644
--- a/bsfs/graph/ac/__init__.py
+++ b/bsfs/graph/ac/__init__.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
import typing
diff --git a/bsfs/graph/ac/base.py b/bsfs/graph/ac/base.py
index 79b09e5..e85c1dd 100644
--- a/bsfs/graph/ac/base.py
+++ b/bsfs/graph/ac/base.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
import abc
import typing
@@ -12,7 +7,7 @@ import typing
from bsfs import schema
from bsfs.query import ast
from bsfs.triple_store import TripleStoreBase
-from bsfs.utils import URI
+from bsfs.utils import URI, typename
# exports
__all__: typing.Sequence[str] = (
@@ -44,6 +39,20 @@ class AccessControlBase(abc.ABC):
self._backend = backend
self._user = URI(user)
+ def __str__(self) -> str:
+ return f'{typename(self)}({self._user})'
+
+ def __repr__(self) -> str:
+ return f'{typename(self)}({self._user})'
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return isinstance(other, type(self)) \
+ and self._backend == other._backend \
+ and self._user == other._user
+
+ def __hash__(self) -> int:
+ return hash((type(self), self._backend, self._user))
+
@abc.abstractmethod
def is_protected_predicate(self, pred: schema.Predicate) -> bool:
"""Return True if a predicate cannot be modified manually."""
@@ -69,7 +78,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..c9ec7d0 100644
--- a/bsfs/graph/ac/null.py
+++ b/bsfs/graph/ac/null.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
import typing
@@ -29,7 +24,7 @@ class NullAC(base.AccessControlBase):
def is_protected_predicate(self, pred: schema.Predicate) -> bool:
"""Return True if a predicate cannot be modified manually."""
- return pred.uri == ns.bsm.t_created
+ return pred.uri == ns.bsn.t_created
def create(self, node_type: schema.Node, guids: typing.Iterable[URI]):
"""Perform post-creation operations on nodes, e.g. ownership information."""
@@ -50,7 +45,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 df2e3a5..1b4c212 100644
--- a/bsfs/graph/graph.py
+++ b/bsfs/graph/graph.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
import os
import typing
@@ -28,9 +23,7 @@ __all__: typing.Sequence[str] = (
## code ##
class Graph():
- """The Graph class is
-
- The Graph class provides a convenient interface to query and access a graph.
+ """The Graph class provides a convenient interface to query and access a graph.
Since it logically builds on the concept of graphs it is easier to
navigate than raw triple stores. Naturally, it uses a triple store
as *backend*. It also controls actions via access permissions to a *user*.
@@ -40,31 +33,33 @@ class Graph():
# link to the triple storage backend.
_backend: TripleStoreBase
- # user uri.
- _user: URI
+ # access controls.
+ _ac: ac.AccessControlBase
- def __init__(self, backend: TripleStoreBase, user: URI):
+ def __init__(
+ self,
+ backend: TripleStoreBase,
+ access_control: ac.AccessControlBase,
+ ):
+ # store members
self._backend = backend
- self._user = user
- self._resolver = resolve.Filter(self._backend.schema)
- self._validate = validate.Filter(self._backend.schema)
- self._ac = ac.NullAC(self._backend, self._user)
+ self._ac = access_control
# ensure Graph schema requirements
self.migrate(self._backend.schema)
def __hash__(self) -> int:
- return hash((type(self), self._backend, self._user))
+ return hash((type(self), self._backend, self._ac))
def __eq__(self, other) -> bool:
return isinstance(other, type(self)) \
and self._backend == other._backend \
- and self._user == other._user
+ and self._ac == other._ac
def __repr__(self) -> str:
- return f'{typename(self)}(backend={repr(self._backend)}, user={self._user})'
+ return f'{typename(self)}({repr(self._backend)}, {self._ac})'
def __str__(self) -> str:
- return f'{typename(self)}({str(self._backend)}, {self._user})'
+ return f'{typename(self)}({str(self._backend)})'
@property
def schema(self) -> bsc.Schema:
@@ -90,9 +85,6 @@ class Graph():
# migrate schema in backend
# FIXME: consult access controls!
self._backend.schema = schema
- # re-initialize members
- self._resolver.schema = self.schema
- self._validate.schema = self.schema
# return self
return self
@@ -104,41 +96,69 @@ 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._user, type_, guids)
+ return _nodes.Nodes(self._backend, self._ac, type_, guids)
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."""
- # get node type
- type_ = self.schema.node(node_type)
- # resolve Nodes instances
- query = self._resolver(type_, query)
- # 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
+ 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
- return _nodes.Nodes(self._backend, self._user, type_, guids)
+ 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)
- guids = self._backend.get(type_, None) # no need to materialize
- return _nodes.Nodes(self._backend, self._user, type_, guids)
-
+ # resolve Nodes instances
+ query = resolve.Filter(self._backend.schema).resolve(type_, query)
+ # add access controls to query
+ query = self._ac.filter_read(type_, query)
+ # validate query
+ if query is not None:
+ validate.Filter(self._backend.schema).validate(type_, query)
+ # query the backend and return the (non-materialized) result
+ return self._backend.get(type_, query)
## EOF ##
diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py
index bc71a32..47b0217 100644
--- a/bsfs/graph/nodes.py
+++ b/bsfs/graph/nodes.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
from collections import abc
import time
@@ -30,15 +25,17 @@ __all__: typing.Sequence[str] = (
## code ##
class Nodes():
- """
+ """Container for graph nodes, provides operations on nodes.
+
+ NOTE: Should not be created directly but only via `bsfs.graph.Graph`.
NOTE: guids may or may not exist. This is not verified as nodes are created on demand.
"""
# triple store backend.
_backend: TripleStoreBase
- # user uri.
- _user: URI
+ # access controls.
+ _ac: ac.AccessControlBase
# node type.
_node_type: bsc.Node
@@ -49,31 +46,29 @@ class Nodes():
def __init__(
self,
backend: TripleStoreBase,
- user: URI,
+ access_control: ac.AccessControlBase,
node_type: bsc.Node,
guids: typing.Iterable[URI],
):
# set main members
self._backend = backend
- self._user = user
+ self._ac = access_control
self._node_type = node_type
- self._guids = set(guids)
- # create helper instances
- # FIXME: Assumes that the schema does not change while the instance is in use!
- self._ac = ac.NullAC(self._backend, self._user)
+ # convert to URI since this is not guaranteed by Graph
+ self._guids = {URI(guid) for guid in guids}
def __eq__(self, other: typing.Any) -> bool:
return isinstance(other, Nodes) \
and self._backend == other._backend \
- and self._user == other._user \
+ and self._ac == other._ac \
and self._node_type == other._node_type \
and self._guids == other._guids
def __hash__(self) -> int:
- return hash((type(self), self._backend, self._user, self._node_type, tuple(sorted(self._guids))))
+ return hash((type(self), self._backend, self._ac, self._node_type, tuple(sorted(self._guids))))
def __repr__(self) -> str:
- return f'{typename(self)}({self._backend}, {self._user}, {self._node_type}, {self._guids})'
+ return f'{typename(self)}({self._backend}, {self._ac}, {self._node_type}, {self._guids})'
def __str__(self) -> str:
return f'{typename(self)}({self._node_type}, {self._guids})'
@@ -94,44 +89,44 @@ class Nodes():
return self._backend.schema
def __add__(self, other: typing.Any) -> 'Nodes':
- """Concatenate guids. Backend, user, and node type must match."""
+ """Concatenate guids. Backend, AC, and node type must match."""
if not isinstance(other, type(self)):
return NotImplemented
if self._backend != other._backend:
raise ValueError(other)
- if self._user != other._user:
+ if self._ac != other._ac:
raise ValueError(other)
if self.node_type != other.node_type:
raise ValueError(other)
- return Nodes(self._backend, self._user, self.node_type, self._guids | other._guids)
+ return Nodes(self._backend, self._ac, self.node_type, self._guids | other._guids)
def __or__(self, other: typing.Any) -> 'Nodes':
- """Concatenate guids. Backend, user, and node type must match."""
+ """Concatenate guids. Backend, AC, and node type must match."""
return self.__add__(other)
def __sub__(self, other: typing.Any) -> 'Nodes':
- """Subtract guids. Backend, user, and node type must match."""
+ """Subtract guids. Backend, AC, and node type must match."""
if not isinstance(other, type(self)):
return NotImplemented
if self._backend != other._backend:
raise ValueError(other)
- if self._user != other._user:
+ if self._ac != other._ac:
raise ValueError(other)
if self.node_type != other.node_type:
raise ValueError(other)
- return Nodes(self._backend, self._user, self.node_type, self._guids - other._guids)
+ return Nodes(self._backend, self._ac, self.node_type, self._guids - other._guids)
def __and__(self, other: typing.Any) -> 'Nodes':
- """Intersect guids. Backend, user, and node type must match."""
+ """Intersect guids. Backend, AC, and node type must match."""
if not isinstance(other, type(self)):
return NotImplemented
if self._backend != other._backend:
raise ValueError(other)
- if self._user != other._user:
+ if self._ac != other._ac:
raise ValueError(other)
if self.node_type != other.node_type:
raise ValueError(other)
- return Nodes(self._backend, self._user, self.node_type, self._guids & other._guids)
+ return Nodes(self._backend, self._ac, self.node_type, self._guids & other._guids)
def __len__(self) -> int:
"""Return the number of guids."""
@@ -140,7 +135,7 @@ class Nodes():
def __iter__(self) -> typing.Iterator['Nodes']:
"""Iterate over individual guids. Returns `Nodes` instances."""
return iter(
- Nodes(self._backend, self._user, self.node_type, {guid})
+ Nodes(self._backend, self._ac, self.node_type, {guid})
for guid in self._guids
)
@@ -175,7 +170,7 @@ class Nodes():
self._backend.commit()
except (
- errors.PermissionDeniedError, # tried to set a protected predicate (ns.bsm.t_created)
+ errors.PermissionDeniedError, # tried to set a protected predicate
errors.ConsistencyError, # node types are not in the schema or don't match the predicate
errors.InstanceError, # guids/values don't have the correct type
TypeError, # value is supposed to be a Nodes instance
@@ -250,32 +245,38 @@ class Nodes():
# add access controls to fetch
fetch = self._ac.fetch_read(self.node_type, fetch)
- # compose filter ast
- filter = ast.filter.IsIn(self.guids) # pylint: disable=redefined-builtin
- # add access controls to filter
- filter = self._ac.filter_read(self.node_type, filter)
-
- # validate queries
- validate.Filter(self._backend.schema)(self.node_type, filter)
- validate.Fetch(self._backend.schema)(self.node_type, fetch)
-
- # process results, convert if need be
- def triple_iter():
- # query the backend
- triples = self._backend.fetch(self.node_type, filter, fetch)
- # process triples
- for root, name, raw in triples:
- # get node
- node = Nodes(self._backend, self._user, self.node_type, {root})
- # get path
- path, tail = name2path[name]
- # covert raw to value
- if isinstance(tail.range, bsc.Node):
- value = Nodes(self._backend, self._user, tail.range, {raw})
- else:
- value = raw
- # emit triple
- yield node, path, value
+ if len(self._guids) == 0:
+ # shortcut: no need to query; no triples
+ # FIXME: if the Fetch query can given by the user, we might want to check its validity
+ def triple_iter():
+ return []
+ else:
+ # compose filter ast
+ filter = ast.filter.IsIn(self.guids) # pylint: disable=redefined-builtin
+ # add access controls to filter
+ filter = self._ac.filter_read(self.node_type, filter) # type: ignore [assignment]
+
+ # validate queries
+ validate.Filter(self._backend.schema).validate(self.node_type, filter)
+ validate.Fetch(self._backend.schema).validate(self.node_type, fetch)
+
+ # process results, convert if need be
+ def triple_iter():
+ # query the backend
+ triples = self._backend.fetch(self.node_type, filter, fetch)
+ # process triples
+ for root, name, raw in triples:
+ # get node
+ node = Nodes(self._backend, self._ac, self.node_type, {root})
+ # get path
+ path, tail = name2path[name]
+ # covert raw to value
+ if isinstance(tail.range, bsc.Node):
+ value = Nodes(self._backend, self._ac, tail.range, {raw})
+ else:
+ value = raw
+ # emit triple
+ yield node, path, value
# simplify by default
view_kwargs['node'] = view_kwargs.get('node', len(self._guids) != 1)
@@ -393,7 +394,7 @@ class Nodes():
self._backend.create(node_type, missing)
# add bookkeeping triples
self._backend.set(node_type, missing,
- self._backend.schema.predicate(ns.bsm.t_created), [time.time()])
+ self._backend.schema.predicate(ns.bsn.t_created), [time.time()])
# add permission triples
self._ac.create(node_type, missing)
# return available nodes
diff --git a/bsfs/graph/resolve.py b/bsfs/graph/resolve.py
index 4677401..a58eb67 100644
--- a/bsfs/graph/resolve.py
+++ b/bsfs/graph/resolve.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
import typing
@@ -32,16 +27,30 @@ class Filter():
input: Any(ns.bse.tag, Is(Nodes(...)))
output: Any(ns.bse.tag, Or(Is(...), Is(...), ...)))
- >>> tags = graph.node(ns.bsfs.Tag, 'http://example.com/me/tag#1234')
- >>> graph.get(ns.bsfs.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags)))
+ >>> tags = graph.node(ns.bsn.Tag, 'http://example.com/me/tag#1234')
+ >>> graph.get(ns.bsn.Entity, ast.filter.Any(ns.bse.tag, ast.filter.Is(tags)))
"""
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],
+ ):
+ """Alias for `Resolve.resolve`."""
+ return self.resolve(root_type, node)
+
+ def resolve(
+ self,
+ root_type: bsc.Node,
+ node: typing.Optional[ast.filter.FilterExpression],
+ ):
+ """Resolve Nodes instances of a *node* query starting at *root_type*."""
+ if node is None:
+ return None
return self._parse_filter_expression(root_type, node)
def _parse_filter_expression(
diff --git a/bsfs/graph/result.py b/bsfs/graph/result.py
index 31822f1..0fcbb13 100644
--- a/bsfs/graph/result.py
+++ b/bsfs/graph/result.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
from collections import defaultdict
import typing
diff --git a/bsfs/graph/schema.nt b/bsfs/graph/schema.nt
index f619746..37bba5e 100644
--- a/bsfs/graph/schema.nt
+++ b/bsfs/graph/schema.nt
@@ -4,16 +4,17 @@ 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 bsm: <http://bsfs.ai/schema/Meta#>
+prefix bsfs: <https://schema.bsfs.io/core/>
+prefix bsl: <https://schema.bsfs.io/core/Literal/>
+prefix bsn: <https://schema.bsfs.io/core/Node#>
# literals
-bsfs:Number rdfs:subClassOf bsfs:Literal .
-xsd:integer rdfs:subClassOf bsfs:Number .
+bsl:Number rdfs:subClassOf bsfs:Literal .
+xsd:float rdfs:subClassOf bsl:Number .
# predicates
-bsm:t_created rdfs:subClassOf bsfs:Predicate ;
+bsn:t_created rdfs:subClassOf bsfs:Predicate ;
rdfs:domain bsfs:Node ;
- rdfs:range xsd:integer ;
+ rdfs:range xsd:float ;
bsfs:unique "true"^^xsd:boolean .
diff --git a/bsfs/graph/walk.py b/bsfs/graph/walk.py
index 1b1cfa0..6415c9b 100644
--- a/bsfs/graph/walk.py
+++ b/bsfs/graph/walk.py
@@ -1,9 +1,4 @@
-"""
-Part of the BlackStar filesystem (bsfs) module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
# imports
from collections import abc
import typing