Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix tutorials #310

Merged
merged 13 commits into from
May 13, 2024
181 changes: 173 additions & 8 deletions buildingmotif/dataclasses/validation.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import re
from collections import defaultdict
from dataclasses import dataclass, field
from functools import cached_property
from itertools import chain
from secrets import token_hex
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Set, Tuple, Union

import rdflib
from rdflib import Graph, URIRef
from rdflib.term import Node
from rdflib.term import BNode, Node

from buildingmotif import get_building_motif
from buildingmotif.dataclasses.shape_collection import ShapeCollection
Expand Down Expand Up @@ -94,6 +95,51 @@ class PathClassCount(GraphDiff):
maxc: Optional[int]
classname: URIRef

@classmethod
def from_validation_report(cls, report: Graph) -> List["PathClassCount"]:
"""Construct PathClassCount objects from a SHACL validation report.

:param report: the SHACL validation report
:type report: Graph
:return: a list of PathClassCount objects
:rtype: List[PathClassCount]
"""

query = """
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?focus ?path ?minc ?maxc ?classname WHERE {
?result sh:sourceShape/sh:qualifiedValueShape? ?shape .
{ ?result sh:sourceConstraintComponent sh:CountConstraintComponent }
UNION
{ ?result sh:sourceConstraintComponent sh:QualifiedMinCountConstraintComponent }
?result sh:focusNode ?focus .
?shape sh:resultPath ?path .
{
?shape sh:class ?classname .
?shape sh:minCount ?minc .
OPTIONAL { ?shape sh:maxCount ?maxc }
}
UNION
{
?shape sh:qualifiedValueShape [ sh:class ?classname ] .
?shape sh:qualifiedMinCount ?minc .
OPTIONAL { ?shape sh:qualifiedMaxCount ?maxc }
}
}"""
results = report.query(query)
return [
cls(
focus,
report,
report,
path,
minc,
maxc,
classname,
)
for focus, path, minc, maxc, classname in results
]

def reason(self) -> str:
"""Human-readable explanation of this GraphDiff."""
return f"{self.focus} needs between {self.minc} and {self.maxc} instances of \
Expand All @@ -109,11 +155,14 @@ def resolve(self, lib: "Library") -> List["Template"]:
"""
assert self.focus is not None
body = Graph()
# extract everything after the last "delimiter" character from self.classname
name = re.split(r"[#\/]", self.classname)[-1]
focus = re.split(r"[#\/]", self.focus)[-1]
for _ in range(self.minc or 0):
inst = _gensym()
body.add((self.focus, self.path, inst))
body.add((inst, A, self.classname))
return [lib.create_template(f"resolve_{token_hex(4)}", body)]
return [lib.create_template(f"resolve_{focus}{name}", body)]


@dataclass(frozen=True, unsafe_hash=True)
Expand All @@ -130,6 +179,54 @@ class PathShapeCount(GraphDiff):
extra_body: Optional[Graph] = field(hash=False)
extra_deps: Optional[Tuple] = field(hash=False)

@classmethod
def from_validation_report(
cls, report: Graph
) -> Generator["PathShapeCount", None, None]:
"""Construct PathShapeCount objects from a SHACL validation report.

:param report: the SHACL validation report
:type report: Graph
:return: a list of PathShapeCount objects
:rtype: List[PathShapeCount]
"""
query = """
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?focus ?path ?minc ?maxc ?shapename WHERE {
?result sh:sourceShape ?shape .
?result sh:resultPath ?path .
{ ?result sh:sourceConstraintComponent sh:CountConstraintComponent }
UNION
{ ?result sh:sourceConstraintComponent sh:QualifiedMinCountConstraintComponent }
?result sh:focusNode ?focus .
{
?shape sh:node ?shapename .
?shape sh:minCount ?minc .
OPTIONAL { ?shape sh:maxCount ?maxc }
}
UNION
{
?shape sh:qualifiedValueShape [ sh:node ?shapename ] .
?shape sh:qualifiedMinCount ?minc .
OPTIONAL { ?shape sh:qualifiedMaxCount ?maxc }
}

}"""
results = report.query(query)
for (focus, path, minc, maxc, shapename) in results:
extra_body, deps = get_template_parts_from_shape(shapename, report)
yield cls(
focus,
report,
report,
path,
minc,
maxc,
shapename,
extra_body,
tuple(deps),
)

def reason(self) -> str:
"""Human-readable explanation of this GraphDiff."""
return f"{self.focus} needs between {self.minc} and {self.maxc} instances of \
Expand All @@ -142,6 +239,9 @@ def resolve(self, lib: "Library") -> List["Template"]:
if self.extra_deps:
for dep in self.extra_deps:
dep["args"] = {k: str(v)[len(PARAM) :] for k, v in dep["args"].items()}
# extract everything after the last "delimiter" character from self.shapename
name = re.split(r"[#\/]", self.shapename)[-1]
focus = re.split(r"[#\/]", self.focus)[-1]
for _ in range(self.minc or 0):
body = Graph()
inst = PARAM["name"]
Expand All @@ -150,7 +250,7 @@ def resolve(self, lib: "Library") -> List["Template"]:
if self.extra_body:
replace_nodes(self.extra_body, {PARAM.name: inst})
body += self.extra_body
templ = lib.create_template(f"resolve{token_hex(4)}", body)
templ = lib.create_template(f"resolve{focus}{name}", body)
if self.extra_deps:
from buildingmotif.dataclasses.template import Template

Expand All @@ -171,6 +271,45 @@ class RequiredPath(GraphDiff):
minc: Optional[int]
maxc: Optional[int]

@classmethod
def from_validation_report(cls, report: Graph) -> List["RequiredPath"]:
"""Construct RequiredPath objects from a SHACL validation report.

:param report: the SHACL validation report
:type report: Graph
:return: a list of RequiredPath objects
:rtype: List[RequiredPath]
"""
query = """
PREFIX sh: <http://www.w3.org/ns/shacl#>
SELECT ?focus ?path ?minc ?maxc WHERE {
?result sh:sourceShape ?shape .
?result sh:resultPath ?path .
{ ?result sh:sourceConstraintComponent sh:CountConstraintComponent }
UNION
{ ?result sh:sourceConstraintComponent sh:QualifiedMinCountConstraintComponent }
?result sh:focusNode ?focus .
{
?shape sh:minCount ?minc .
OPTIONAL { ?shape sh:maxCount ?maxc }
} UNION {
?shape sh:qualifiedMinCount ?minc .
OPTIONAL { ?shape sh:qualifiedMaxCount ?maxc }
}
}"""
results = report.query(query)
return [
cls(
focus,
report,
report,
path,
minc,
maxc,
)
for focus, path, minc, maxc in results
]

def reason(self) -> str:
"""Human-readable explanation of this GraphDiff."""
return f"{self.focus} needs between {self.minc} and {self.maxc} uses \
Expand All @@ -186,10 +325,13 @@ def resolve(self, lib: "Library") -> List["Template"]:
"""
assert self.focus is not None
body = Graph()
# extract everything after the last "delimiter" character from self.shapename
name = re.split(r"[#\/]", self.path)[-1]
focus = re.split(r"[#\/]", self.focus)[-1]
for _ in range(self.minc or 0):
inst = _gensym()
body.add((self.focus, self.path, inst))
return [lib.create_template(f"resolve{token_hex(4)}", body)]
return [lib.create_template(f"resolve{focus}{name}", body)]


@dataclass(frozen=True)
Expand All @@ -212,8 +354,9 @@ def resolve(self, lib: "Library") -> List["Template"]:
"""
assert self.focus is not None
body = Graph()
name = re.split(r"[#\/]", self.classname)[-1]
body.add((self.focus, A, self.classname))
return [lib.create_template(f"resolve{token_hex(4)}", body)]
return [lib.create_template(f"resolveSelf{name}", body)]


@dataclass(frozen=True)
Expand All @@ -238,10 +381,11 @@ def resolve(self, lib: "Library") -> List["Template"]:
:rtype: List[Template]
"""
templs = []
name = re.split(r"[#\/]", self.classname)[-1]
for _ in range(self.expectedCount):
template_body = Graph()
template_body.add((PARAM["name"], A, self.classname))
templs.append(lib.create_template(f"resolve{token_hex(4)}", template_body))
templs.append(lib.create_template(f"resolveAdd{name}", template_body))
return templs


Expand Down Expand Up @@ -276,6 +420,24 @@ def as_templates(self) -> List["Template"]:
"""
return diffset_to_templates(self.diffset)

def get_broken_entities(self) -> Set[URIRef]:
"""Get the set of entities that are broken in the model.

:return: set of entities that are broken
:rtype: Set[URIRef]
"""
return {diff or "Model" for diff in self.diffset}

def get_diffs_for_entity(self, entity: URIRef) -> Set[GraphDiff]:
"""Get the set of diffs for a specific entity.

:param entity: the entity to get diffs for
:type entity: URIRef
:return: set of diffs for the entity
:rtype: Set[GraphDiff]
"""
return self.diffset.get(entity, set())

def get_reasons_with_severity(
self, severity: Union[URIRef, str]
) -> Dict[Optional[URIRef], Set[GraphDiff]]:
Expand Down Expand Up @@ -326,6 +488,7 @@ def _report_to_diffset(self) -> Dict[Optional[URIRef], Set[GraphDiff]]:

g = self.report + self.shapes_graph
diffs: Dict[Optional[URIRef], Set[GraphDiff]] = defaultdict(set)

for result in g.objects(predicate=SH.result):
# check if the failure is due to our count constraint component
focus = g.value(result, SH.focusNode)
Expand Down Expand Up @@ -355,6 +518,8 @@ def _report_to_diffset(self) -> Dict[Optional[URIRef], Set[GraphDiff]]:
):
requiring_shape = g.value(result, SH.sourceShape)
expected_class = g.value(requiring_shape, SH["class"])
if expected_class is None or isinstance(expected_class, BNode):
continue
diffs[focus].add(
RequiredClass(focus, validation_report, g, expected_class)
)
Expand Down Expand Up @@ -454,7 +619,7 @@ def diffset_to_templates(
continue

templ_lists = (diff.resolve(lib) for diff in diffset)
templs = list(filter(None, chain.from_iterable(templ_lists)))
templs: List[Template] = list(filter(None, chain.from_iterable(templ_lists)))
if len(templs) <= 1:
templates.extend(templs)
continue
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/csv-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Assume we have the following Template in a library called `csv-tutorial`:

```yml
```yaml
my-thermostat:
body: >
@prefix P: <urn:___param___#> .
Expand Down
Loading
Loading