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)