diff options
Diffstat (limited to 'bsfs/graph/graph.py')
-rw-r--r-- | bsfs/graph/graph.py | 100 |
1 files changed, 60 insertions, 40 deletions
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 ## |