aboutsummaryrefslogtreecommitdiffstats
path: root/bsfs
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-01-21 22:32:33 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-01-21 22:32:33 +0100
commit9310610a7edf4dcbb934aedcecff1d11348197bb (patch)
treee7d342d8f25ae40e6bdaf33549f7145bb8697f23 /bsfs
parentc196d2ce73d8351a18c19bcddd4b06d224e644fc (diff)
downloadbsfs-9310610a7edf4dcbb934aedcecff1d11348197bb.tar.gz
bsfs-9310610a7edf4dcbb934aedcecff1d11348197bb.tar.bz2
bsfs-9310610a7edf4dcbb934aedcecff1d11348197bb.zip
nodes predicate walk sugar
Diffstat (limited to 'bsfs')
-rw-r--r--bsfs/graph/nodes.py15
-rw-r--r--bsfs/graph/walk.py120
-rw-r--r--bsfs/schema/schema.py4
3 files changed, 138 insertions, 1 deletions
diff --git a/bsfs/graph/nodes.py b/bsfs/graph/nodes.py
index a4ba45f..18ab30d 100644
--- a/bsfs/graph/nodes.py
+++ b/bsfs/graph/nodes.py
@@ -19,6 +19,7 @@ from bsfs.utils import errors, URI, typename
# inner-module imports
from . import ac
from . import result
+from . import walk
# exports
__all__: typing.Sequence[str] = (
@@ -87,6 +88,18 @@ class Nodes():
"""Return all node guids."""
return iter(self._guids)
+ @property
+ def schema(self) -> bsc.Schema:
+ """Return the store's local schema."""
+ return self._backend.schema
+
+ def __getattr__(self, name: str):
+ try:
+ return super().__getattr__(name) # type: ignore [misc] # parent has no getattr
+ except AttributeError:
+ pass
+ return walk.Walk(self, walk.Walk.step(self.schema, self.node_type, name))
+
def set(
self,
pred: URI, # FIXME: URI or bsc.Predicate?
@@ -141,7 +154,7 @@ class Nodes():
if view not in (dict, list):
raise ValueError(f'expected dict or list, found {view}')
# process paths: create fetch ast, build name mapping, and find unique paths
- schema = self._backend.schema
+ schema = self.schema
statements = set()
name2path = {}
unique_paths = set() # paths that result in a single (unique) value
diff --git a/bsfs/graph/walk.py b/bsfs/graph/walk.py
new file mode 100644
index 0000000..63ef5e9
--- /dev/null
+++ b/bsfs/graph/walk.py
@@ -0,0 +1,120 @@
+"""
+
+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
+
+# bsfs imports
+from bsfs import schema as bsc
+
+# inner-module imports
+# NOTE: circular import! OK as long as only used for type annotations.
+from . import nodes # pylint: disable=cyclic-import
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'Walk',
+ )
+
+
+## code ##
+
+class Walk(abc.Hashable, abc.Callable): # type: ignore [misc] # invalid base class (Callable)
+ """Syntactic sugar for `Nodes` to build and act on predicate paths via members."""
+
+ # Link to Nodes instance.
+ _root: 'nodes.Nodes'
+
+ # Current predicate path.
+ _path: typing.Tuple[bsc.Predicate, ...]
+
+ def __init__(
+ self,
+ root: 'nodes.Nodes',
+ path: typing.Sequence[bsc.Predicate],
+ ):
+ self._root = root
+ self._path = tuple(path)
+
+ @property
+ def tail(self):
+ """Return the node type at the end of the path."""
+ return self._path[-1].range
+
+
+ ## comparison
+
+ def __hash__(self) -> int:
+ """Return an integer hash that identifies the instance."""
+ return hash((type(self), self._root, self._path))
+
+ def __eq__(self, other) -> bool:
+ """Compare against *other* backend."""
+ return isinstance(other, type(self)) \
+ and self._root == other._root \
+ and self._path == other._path
+
+
+ ## representation
+
+ def __repr__(self) -> str:
+ """Return a formal string representation."""
+ path = ', '.join(pred.uri for pred in self._path)
+ return f'Walk({self._root.node_type.uri}, ({path}))'
+
+ def __str__(self) -> str:
+ """Return an informal string representation."""
+ path = ', '.join(pred.uri for pred in self._path)
+ return f'Walk(@{self._root.node_type.uri}: {path})'
+
+
+ ## walk
+
+ @staticmethod
+ def step(
+ schema: bsc.Schema,
+ node: bsc.Node,
+ name: str,
+ ) -> typing.Tuple[bsc.Predicate]:
+ """Get an predicate at *node* whose fragment matches *name*."""
+ predicates = tuple(
+ pred
+ for pred
+ in schema.predicates_at(node)
+ if pred.uri.get('fragment', None) == name
+ )
+ if len(predicates) == 0: # no fragment found for name
+ raise ValueError(f'no available predicate matches {name}')
+ if len(predicates) > 1: # ambiguous name
+ raise ValueError(f'{name} matches multiple predicates')
+ # append predicate to walk
+ return predicates # type: ignore [return-value] # size is one
+
+ def __getattr__(self, name: str) -> 'Walk':
+ """Alias for `Walk.step(name)`."""
+ try:
+ return super().__getattr__(name)
+ except AttributeError:
+ pass
+ # get predicate
+ pred = self.step(self._root.schema, self.tail, name)
+ # append predicate to walk
+ return Walk(self._root, self._path + pred)
+
+
+ ## get paths ##
+
+ def get(self, **kwargs) -> typing.Any:
+ """Alias for `Nodes.get(..)`."""
+ return self._root.get(tuple(pred.uri for pred in self._path), **kwargs)
+
+ def __call__(self, **kwargs) -> typing.Any: # pylint: disable=arguments-differ
+ """Alias for `Walk.get(...)`."""
+ return self.get(**kwargs)
+
+
+## EOF ##
diff --git a/bsfs/schema/schema.py b/bsfs/schema/schema.py
index 8d9a821..1644926 100644
--- a/bsfs/schema/schema.py
+++ b/bsfs/schema/schema.py
@@ -312,4 +312,8 @@ class Schema():
"""Return the Literal matching the *uri*."""
return self._literals[uri]
+ def predicates_at(self, node: types.Node) -> typing.Iterator[types.Predicate]:
+ """Return predicates that have domain *node* (or superclass thereof)."""
+ return iter(pred for pred in self._predicates.values() if node <= pred.domain)
+
## EOF ##