Source code for buildingmotif.shape_builder.shape

from typing import List, Optional, Tuple, Union

from rdflib import RDF, BNode, Graph, Literal, URIRef
from rdflib.collection import Collection
from rdflib.term import Node

from buildingmotif.namespaces import CONSTRAINT, SH, A, bind_prefixes


[docs]class Shape(Graph): """Base class for constructing shapes programatically""" def __init__( self, identifier: Optional[Union[Node, str]] = None, message: Optional[str] = None, ) -> None: """ :param identifier: id for shape :type identifier: Optional[Union[Node, str]] :param message: sh:message annotation :type message: str """ super().__init__(identifier=identifier) bind_prefixes(self) if message: self.add((self.identifier, SH["message"], Literal(message)))
[docs] def add_property(self, property: URIRef, object: Node): """Add property to shape :param property: ref of property :type property: URIRef :param object: ref of object :type object: Node""" # This same functionality could be added with just use of add. # This design is useful to allow adding properties in the # builder paradigm. self.add((self, property, object)) return self
[docs] def add_list_property( self, property: URIRef, nodes: Union[List[Node], Tuple[Node, ...]] ): """Add property which references list to shape :param property: ref of property :type property: URIRef :param nodes: nodes to include in list :type nodes: Union[List[Node], Tuple[Node, ...]]""" identifier = BNode() Collection(self, identifier, nodes) self.add((self, property, identifier)) return self
[docs] def OR(self, *nodes: Node): """add OR property :param nodes: list of nodes to OR :type nodes: Union[List[Node], Tuple[Node, ...]] """ self.add_list_property(SH["or"], nodes) return self
[docs] def AND(self, *nodes: Node): """add AND property :param nodes: list of nodes to AND :type nodes: Union[List[Node], Tuple[Node, ...]] """ self.add_list_property(SH["and"], nodes) return self
[docs] def NOT(self, node: Node): """add NOT property :param nodes: list of nodes to NOT :type nodes: Union[List[Node], Tuple[Node, ...]] """ self.add((self, SH["not"], node)) return self
[docs] def XONE(self, *nodes: Node): """add XONE property :param nodes: list of nodes to XONE :type nodes: Union[List[Node], Tuple[Node, ...]] """ self.add_list_property(SH["xone"], nodes) return self
[docs] def add(self, triple: Tuple[Node, Node, Node]): (s, p, o) = triple if isinstance(o, Shape): # if the object being added is of type Shape # add the whole graph to this graph self += o o = o.identifier if s == self: s = s.identifier triple = (s, p, o) return super().add(triple)
[docs]class NodeShape(Shape): """Class for constructing Node Shapes programatically""" def __init__( self, identifier: Optional[Union[Node, str]] = None, message: Optional[str] = None, ) -> None: """ :param identifier: id for shape :type identifier: Optional[Union[Node, str]] :param message: sh:message annotation :type message: str """ super().__init__(identifier=identifier, message=message) self.add((self, A, SH["NodeShape"]))
[docs] def of_class(self, class_: Node, active=False): """Add constraint that target much be of a certain class :param class_: class of target :type class_: Node :param active: should shape actively target the class or not :type active: bool""" predicate = SH["targetClass"] if active else SH["class"] self.add((self, predicate, class_)) self.add((self, CONSTRAINT["class"], class_)) return self
[docs] def always_run(self): """Add blank node target This target insures that the shape will always be evaluated. If the shape has properties this can cause it to fail.""" self.add((self, SH["targetNode"], BNode())) return self
[docs] def count(self, exactly: int = None): """Add an exact count constraint. :param exactly: exact number of instances of class to match :type exactly: int""" if exactly: self.add((self, CONSTRAINT["exactCount"], Literal(exactly))) return self
[docs] def has_property(self, property: Union[Node, URIRef]): """Add property constraint. :param property: property shape or property to add constraint for :type property: Union[Node, URIRef]""" if isinstance(property, URIRef): property = PropertyShape().has_path(property) self.add((self, SH["property"], property)) return self
[docs]class PropertyShape(Shape): def __init__( self, identifier: Optional[Union[Node, str]] = None, message: Optional[str] = None, ) -> None: """ :param identifier: id for shape :type identifier: Optional[Union[Node, str]] :param message: sh:message annotation :type message: str """ super().__init__(identifier=identifier, message=message) self.add((self, RDF.type, SH["PropertyShape"]))
[docs] def has_path( self, path: Node, zero_or_one: bool = False, zero_or_more: bool = False, one_or_more: bool = False, ) -> "PropertyShape": """Add path constraint to shape. zero_or_one, zero_or_more, and one_or_more flags are mutually exclusive :param path: path to add constraint for :type path: Node :param zero_or_one: match zero or one instances of path :type zero_or_one: bool :param zero_or_more: match zero or more instances of path :type zero_or_more: bool :param one_or_more: match one or more instances of path :type one_or_more: bool""" if zero_or_one or zero_or_more or one_or_more: path_constraint = None if zero_or_one: path_constraint = SH["path-zero-or-one"] elif zero_or_more: path_constraint = SH["path-zero-or-more"] elif one_or_more: path_constraint = SH["path-one-or-more"] self.add((self, SH["path"], Shape().add_property(path_constraint, path))) else: self.add((self, SH["path"], path)) return self
[docs] def matches( self, target: Node, type: URIRef, min: int = None, max: int = None, exactly: int = None, qualified: bool = False, ): """Add target matches constraint to property shape :param target: target node to specify what should be matched, usually shape or class :type target: Node :param type: sh:class or sh:node :type type: URIRef :param min: min count of matched entities :type min: int :param max: max count of matched entities :type max: int :param exactly: exact count of matched entities (takes precidence over min/max) :type exactly: int :param qualified: Is this property qualified or universal :type qualified: bool """ if min is None and max is None and exactly is None: if qualified: raise ValueError("min, max or exactly must have a value") else: self.add((self, type, target)) return self if exactly is not None: min = max = exactly if qualified: blank_node = BNode() self.add((blank_node, type, target)) self.add((self, SH["qualifiedValueShape"], blank_node)) if min is not None: self.add((self, SH["qualifiedMinCount"], Literal(min))) if max is not None: self.add((self, SH["qualifiedMaxCount"], Literal(max))) else: self.add((self, type, target)) if min is not None: self.add((self, SH["minCount"], Literal(min))) if max is not None: self.add((self, SH["maxCount"], Literal(max))) return self
[docs] def matches_class( self, class_: URIRef, min: int = None, max: int = None, exactly: int = None, qualified=False, ): """Add target matches class constraint to property shape :param class_: target class what should be matched :type class_: Node :param min: min count of matched entities :type min: int :param max: max count of matched entities :type max: int :param exactly: exact count of matched entities (takes precidence over min/max) :type exactly: int :param qualified: Is this property qualified or universal :type qualified: bool """ return self.matches(class_, SH["class"], min, max, exactly, qualified)
[docs] def matches_shape( self, shape: Node, min: int = None, max: int = None, exactly: int = None, qualified=False, ): """Add target matches shape constraint to property shape :param shape: target shape what should be matched :type shape: Node :param min: min count of matched entities :type min: int :param max: max count of matched entities :type max: int :param exactly: exact count of matched entities (takes precidence over min/max) :type exactly: int :param qualified: Is this property qualified or universal :type qualified: bool """ return self.matches(shape, SH["node"], min, max, exactly, qualified)
[docs]def OR(*nodes: Node) -> Shape: """add OR property :param nodes: list of nodes to OR :type nodes: Union[List[Node], Tuple[Node, ...]] """ return Shape().OR(*nodes)
[docs]def AND(*nodes: Node) -> Shape: """add AND property :param nodes: list of nodes to AND :type nodes: Union[List[Node], Tuple[Node, ...]] """ return Shape().AND(*nodes)
[docs]def NOT(node: Node) -> Shape: """add NOT property :param nodes: list of nodes to NOT :type nodes: Union[List[Node], Tuple[Node, ...]] """ return Shape().NOT(node)
[docs]def XONE(*nodes: Node) -> Shape: """add XONE property :param nodes: list of nodes to XONE :type nodes: Union[List[Node], Tuple[Node, ...]] """ return Shape().XONE(*nodes)