Source code for buildingmotif.dataclasses.model

from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING, List, Optional

import rdflib
import rdflib.query
import rfc3987

from buildingmotif import get_building_motif
from buildingmotif.dataclasses.shape_collection import ShapeCollection
from buildingmotif.dataclasses.validation import ValidationContext
from buildingmotif.utils import Triple, copy_graph, shacl_inference, skolemize_shapes

if TYPE_CHECKING:
    from buildingmotif import BuildingMOTIF
    from buildingmotif.dataclasses.compiled_model import CompiledModel


def _validate_uri(uri: str):
    parsed = rfc3987.parse(uri)
    if not parsed["scheme"]:
        raise ValueError(
            f"{uri} does not look like a valid URI, trying to serialize this will break."
        )


[docs]@dataclass class Model: """This class mirrors :py:class:`database.tables.DBModel`.""" _id: int _name: str _description: str _graph: rdflib.Graph _bm: "BuildingMOTIF" _manifest_id: int
[docs] @classmethod def create(cls, name: str, description: str = "") -> "Model": """Create a new model. :param name: new model name :type name: str :param description: new model description :type description: str :return: new model :rtype: Model """ _validate_uri(name) g = rdflib.Graph() g.add((rdflib.URIRef(name), rdflib.RDF.type, rdflib.OWL.Ontology)) if description: g.add( (rdflib.URIRef(name), rdflib.RDFS.comment, rdflib.Literal(description)) ) return cls.from_graph(g)
[docs] @classmethod def from_graph(cls, graph: rdflib.Graph) -> "Model": """Create a new model from a graph. The name of the model is taken from the ontology declaration in the graph (subject of rdf:type owl:Ontology triple). The description of the model can be set through an RDFS comment on the ontology :param graph: graph to create model from :type graph: rdflib.Graph :return: new model :rtype: Model """ bm = get_building_motif() name = graph.value(predicate=rdflib.RDF.type, object=rdflib.OWL.Ontology) if name is None: raise ValueError("Graph does not contain an ontology declaration") _validate_uri(name) # the 'description' is the rdfs:comment of the ontology description = graph.value(name, rdflib.RDFS.comment) description = str(description) if description is not None else "" db_model = bm.table_connection.create_db_model(name, description) graph = bm.graph_connection.create_graph(db_model.graph_id, graph) # below, we normalize the name to a string so it matches the database type return cls( _id=db_model.id, _name=str(db_model.name), _description=db_model.description, _graph=graph, _bm=bm, _manifest_id=db_model.manifest_id, )
[docs] @classmethod def from_file(cls, url_or_path: str) -> "Model": """Create a new model from a file. :param url_or_path: url or path to file :type url_or_path: str :return: new model :rtype: Model """ graph = rdflib.Graph() # if guess_format doesn't match anything, it will return None, # which tells graph.parse to guess 'turtle' # if graph parsing fails, it will raise an exception graph.parse(url_or_path, format=rdflib.util.guess_format(url_or_path)) return cls.from_graph(graph)
[docs] @classmethod def load(cls, id: Optional[int] = None, name: Optional[str] = None) -> "Model": """Get model from database by id or name. :param id: model id, defaults to None :type id: Optional[int], optional :param name: model name, defaults to None :type name: Optional[str], optional :raises Exception: if neither id nor name provided :return: model :rtype: Model """ bm = get_building_motif() if id is not None: db_model = bm.table_connection.get_db_model(id) elif name is not None: db_model = bm.table_connection.get_db_model_by_name(name) else: raise Exception("Neither id nor name provided to load Model") graph = bm.graph_connection.get_graph(db_model.graph_id) return cls( _id=db_model.id, _name=db_model.name, _description=db_model.description, _graph=graph, _bm=bm, _manifest_id=db_model.manifest_id, )
@property def id(self) -> Optional[int]: return self._id @id.setter def id(self, new_id): raise AttributeError("Cannot modify db id") @cached_property def graph(self) -> rdflib.Graph: return self._graph @property def name(self): return self._name @name.setter def name(self, new_name: str): self._bm.table_connection.update_db_model_name(self._id, new_name) self._name = new_name @property def description(self): return self._description @description.setter def description(self, new_description: str): self._bm.table_connection.update_db_model_description(self._id, new_description) self._description = new_description
[docs] def add_triples(self, *triples: Triple) -> None: """Add the given triples to the model. :param triples: a sequence of triples to add to the graph :type triples: Triple """ for triple in triples: self.graph.add(triple)
[docs] def add_graph(self, graph: rdflib.Graph) -> None: """Add the given graph to the model. :param graph: the graph to add to the model :type graph: rdflib.Graph """ self.graph += graph
[docs] def validate( self, shape_collections: Optional[List[ShapeCollection]] = None, error_on_missing_imports: bool = True, shacl_engine: Optional[str] = None, ) -> "ValidationContext": """Validates this model against the given list of ShapeCollections. If no list is provided, the model will be validated against the model's "manifest". If a list of shape collections is provided, the manifest will *not* be automatically included in the set of shape collections. Loads all of the ShapeCollections into a single graph. :param shape_collections: a list of ShapeCollections against which the graph should be validated. If an empty list or None is provided, the model will be validated against the model's manifest. :type shape_collections: List[ShapeCollection] :param error_on_missing_imports: if True, raises an error if any of the dependency ontologies are missing (i.e. they need to be loaded into BuildingMOTIF), defaults to True :type error_on_missing_imports: bool, optional :return: An object containing useful properties/methods to deal with the validation results :param shacl_engine: the SHACL engine to use for validation, defaults to whatever is set in the BuildingMOTIF object :type shacl_engine: str, optional :rtype: ValidationContext """ compiled_model = self.compile(shape_collections or [self.get_manifest()]) return compiled_model.validate(error_on_missing_imports)
[docs] def compile( self, shape_collections: Optional[List["ShapeCollection"]] = None ) -> "CompiledModel": """Compile the graph of a model against a set of ShapeCollections. :param shape_collections: list of ShapeCollections to compile the model against. Defaults to the model's manifest. :type shape_collections: List[ShapeCollection], optional :param shacl_engine: the SHACL engine to use for validation, defaults to whatever is set in the BuildingMOTIF object :type shacl_engine: str, optional :return: copy of model's graph that has been compiled against the ShapeCollections :rtype: Graph """ from buildingmotif.dataclasses.compiled_model import CompiledModel ontology_graph = rdflib.Graph() if shape_collections is None: shape_collections = [self.get_manifest()] for shape_collection in shape_collections: ontology_graph += shape_collection.graph ontology_graph = skolemize_shapes(ontology_graph) model_graph = copy_graph(self.graph).skolemize() compiled_graph = shacl_inference( model_graph, ontology_graph, engine=self._bm.shacl_engine ) return CompiledModel(self, shape_collections, compiled_graph)
[docs] def get_manifest(self) -> ShapeCollection: """Get ShapeCollection from model. :return: model's shape collection :rtype: ShapeCollection """ return ShapeCollection.load(self._manifest_id)
[docs] def update_manifest(self, manifest: ShapeCollection): """Updates the manifest for this model by adding in the contents of the shape graph inside the provided SHapeCollection :param manifest: the ShapeCollection containing additional shapes against which to validate this model :type manifest: ShapeCollection """ self.get_manifest().graph += manifest.graph