# imports import os import typing # bsfs imports from bsfs.query import ast, validate from bsfs import schema as bsc from bsfs.triple_store import TripleStoreBase from bsfs.utils import URI, typename # inner-module imports from . import ac from . import nodes as _nodes from . import resolve # exports __all__: typing.Sequence[str] = ( 'Graph', ) ## code ## class 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*. """ # link to the triple storage backend. _backend: TripleStoreBase # access controls. _ac: ac.AccessControlBase def __init__( self, backend: TripleStoreBase, access_control: ac.AccessControlBase, ): # store members self._backend = backend self._ac = access_control # ensure Graph schema requirements self.migrate(self._backend.schema) def __hash__(self) -> int: 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._ac == other._ac def __repr__(self) -> str: return f'{typename(self)}({repr(self._backend)}, {self._ac})' def __str__(self) -> str: return f'{typename(self)}({str(self._backend)})' @property def schema(self) -> bsc.Schema: """Return the store's local schema.""" return self._backend.schema def migrate(self, schema: bsc.Schema, append: bool = True) -> 'Graph': """Migrate the current schema to a new *schema*. Appends to the current schema by default; control this via *append*. The `Graph` may add additional classes to the schema that are required for its interals. """ # check args if not isinstance(schema, bsc.Schema): raise TypeError(schema) # append to current schema if append: schema = schema + self._backend.schema # add Graph schema requirements with open(os.path.join(os.path.dirname(__file__), 'schema.nt'), mode='rt', encoding='UTF-8') as ifile: schema = schema + bsc.from_string(ifile.read()) # migrate schema in backend # FIXME: consult access controls! self._backend.schema = schema # return self return self def nodes(self, node_type: URI, guids: typing.Iterable[URI]) -> _nodes.Nodes: """Return nodes *guids* of type *node_type* as a `bsfs.graph.Nodes` instance. Note that the *guids* need not to exist (however, the *node_type* has to be part of the schema). Inexistent guids will be created (using *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) 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 *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 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 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 ##