Unverified Commit 60134aef authored by Maxime Lefrançois's avatar Maxime Lefrançois
Browse files

check: support vocabularies (TS 103 673 V1.2.1), exit code 1 on errors

- clause 9.4.3.1: owl:imports may be Vocabulary Version IRIs and Pattern
  Version IRIs; vocabulary documents are checked against their Vocabulary
  IRI and Vocabulary Version IRI (clause 9.8)
- clause 9.4.3.2: check dcterms:title on the document's declaration IRI
- terms only declared in an imported document (e.g., another vocabulary
  of the same project) are no longer counted as declared by the document
- clause 9.6.3: dcterms:conformsTo to standard ontologies outside
  saref.etsi.org no longer produces a warning
- clauses 9.4.5/9.6.4: declare the schema.org metadata terms of clause
  9.4.3.3 before the OWL 2 DL profile check
- saref-dev exits with code 1 when some error is reported
- explicit error when no SAREF project can be deduced from the directory
  name or its git remote origin; use the absolute directory name
parent 708552a3
Loading
Loading
Loading
Loading
+4 −0
Changes for src/saref_pypeline/__main__.py: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -184,6 +184,10 @@ def main():
    duration = end - start
    print(f"Duration: {duration:.3f} seconds")

    if pipeline.error_count:
        print(f"{pipeline.error_count} error(s) reported.", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
+37 −27
Changes for src/saref_pypeline/check/TS103673_Clause_9_4_3_1_Checker.py: 37 added lines, 27 removed lines.
Original line number Diff line number Diff line
from saref_pypeline.check.BaseGraphDocumentSHACLChecker import BaseGraphDocumentSHACLChecker
from rdflib import Graph, RDF, OWL
from saref_pypeline.entities import SAREFGraphDocument
from saref_pypeline.entities import SAREFGraphDocument, SAREFGraphDocumentType

from typing import TYPE_CHECKING

@@ -16,52 +16,61 @@ class Checker(BaseGraphDocumentSHACLChecker):
        super().__init__(__file__, pipeline, document)

    def update_shapes_graph(self, shapes_graph: Graph):
        # clauses 9.8 and 10.3: vocabulary documents and pattern ontological
        # definitions conform to this clause, with the substitutions of the
        # document name, the Ontology IRI, and the Ontology Version IRI
        doc_name = self.document.type.display_name
        version_iri_name = self.document.type.version_iri_name
        shapes_graph += Graph(namespace_manager=shapes_graph.namespace_manager).parse(
            data=f"""
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix sarefsh: <https://saref.etsi.org/shape#> .

sarefsh:versionInfo sh:pattern "^{self.version}$" ;
  sh:message "The ontology document shall contain exactly one metadata `owl:versionInfo`, with value `\\"{self.version}\\"`" .
  sh:message "The {doc_name} shall contain exactly one metadata `owl:versionInfo`, with value `\\"{self.version}\\"`" .

sarefsh:versionIRI sh:pattern "^{self.document.version_iri}$" ;
  sh:message "The ontology document shall contain exactly one metadata `owl:versionIRI`, with value `<{self.document.version_iri}>`" .
  sh:message "The {doc_name} shall contain exactly one metadata `owl:versionIRI`, with value `<{self.document.version_iri}>` (the {version_iri_name})" .

sarefsh:preferredNamespacePrefix sh:pattern "^{self.project.prefix}$" ;
  sh:message "The ontology document shall contain exactly one metadata `vann:preferredNamespacePrefix`, with value `\\"{self.project.prefix}\\"`" .
  sh:message "The {doc_name} shall contain exactly one metadata `vann:preferredNamespacePrefix`, with value `\\"{self.project.prefix}\\"`" .

sarefsh:preferredNamespaceUri sh:message "The ontology document shall contain exactly one metadata `vann:preferredNamespaceUri`, with value `\\"{self.project.namespace}\\"`" .
sarefsh:preferredNamespaceUri sh:message "The {doc_name} shall contain exactly one metadata `vann:preferredNamespaceUri`, with value `\\"{self.project.namespace}\\"`" .
""",
            format="turtle",
        )

        prior_version = self.pipeline.get_prior_version(self.project, self.version)
        if prior_version:
            prior_version_iri = str(self.document.version_iri).replace(
                f"{self.project.namespace}{self.version}/",
                f"{self.project.namespace}{prior_version.version}/",
                1,
            )
        if not prior_version:
            shapes_graph += Graph(
                namespace_manager=shapes_graph.namespace_manager
            ).parse(
                data=f"""
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix sarefsh: <https://saref.etsi.org/shape#> .

            shape = f"""
sarefsh:priorVersion sh:maxCount 0 ;
    sh:message "There exists no prior version to {self.version}, so the ontology document shall not contain a metadata `owl:priorVersion`" .
""",
                format="turtle",
            )
    sh:message "There exists no prior version to {self.version}, so the {doc_name} shall not contain a metadata `owl:priorVersion`" .
"""
        elif self.document.type == SAREFGraphDocumentType.ONTOLOGY:
            shape = f"""
sarefsh:priorVersion sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:pattern "^{prior_version_iri}$" ;
    sh:message "There exists a prior version {prior_version.version}, so the {doc_name} shall contain exactly one metadata `owl:priorVersion`, with value `<{prior_version_iri}>`" .
"""
        else:
            shapes_graph += Graph(
                namespace_manager=shapes_graph.namespace_manager
            ).parse(
            # the document may not exist in the prior version
            shape = f"""
sarefsh:priorVersion sh:maxCount 1 ;
    sh:pattern "^{prior_version_iri}$" ;
    sh:message "If the {doc_name} contains a metadata `owl:priorVersion`, then it shall contain exactly one, with value `<{prior_version_iri}>`" .
"""
        shapes_graph += Graph(namespace_manager=shapes_graph.namespace_manager).parse(
            data=f"""
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix sarefsh: <https://saref.etsi.org/shape#> .

sarefsh:priorVersion sh:minCount 1 ;
    sh:maxCount 1 ;
    sh:pattern "^{self.project.namespace}{prior_version.version}/$" ;
    sh:message "There exists a prior version {prior_version.version}, so the ontology document shall contain exactly one metadata `owl:priorVersion`, with value `<{self.project.namespace}{prior_version.version}/>`" .
""",
{shape}""",
            format="turtle",
        )

@@ -70,9 +79,10 @@ sarefsh:priorVersion sh:minCount 1 ;
    def check_clause(self):
        graph = self.pipeline.dataset.graph(self.document.version_iri)
        ontologies = list(graph.subjects(RDF.type, OWL.Ontology))
        if len(ontologies) != 1 or str(ontologies[0]) != self.project.namespace:
        declaration_iri = self.document.declaration_iri
        if len(ontologies) != 1 or ontologies[0] != declaration_iri:
            self.logger.error(
                f"The ontology document shall contain exactly one ontology declaration, with value `<{self.project.namespace}>`"
                f"The {self.document.type.display_name} shall contain exactly one ontology declaration, with value `<{declaration_iri}>` (the {self.document.type.iri_name})"
            )
            return
        super().check_clause()
+1 −1
Changes for src/saref_pypeline/check/TS103673_Clause_9_4_3_2_Checker.py: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ sarefsh:source sh:message "The ontology document shall contain exactly one metad
        if not any(
            filter(
                lambda o: isinstance(o, Literal) and o.language == "en",
                graph.objects(self.project.ontology_IRI, DCTERMS.title),
                graph.objects(self.document.declaration_iri, DCTERMS.title),
            )
        ):
            self.logger.warning(
+29 −1
Changes for src/saref_pypeline/check/TS103673_Clause_9_4_5_Checker.py: 29 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@ import tempfile
from typing import TYPE_CHECKING
from urllib.request import urlretrieve

from rdflib import Graph, URIRef, DCMITYPE, DCTERMS, RDF, OWL
from rdflib import Graph, Namespace, URIRef, DCMITYPE, DCTERMS, RDF, OWL
import owlready2
import owlready2.reasoning

@@ -36,6 +36,33 @@ def java_major_version(java_cmd="java"):

major = java_major_version(JAVA_CMD)

SCHEMA = Namespace("http://schema.org/")

# terms used for metadata about creators and contributors (TS103673 Clause 9.4.3.3)
METADATA_TERMS = {
    SCHEMA.Person: OWL.Class,
    SCHEMA.Organization: OWL.Class,
    SCHEMA.Affiliation: OWL.Class,
    SCHEMA.givenName: OWL.AnnotationProperty,
    SCHEMA.familyName: OWL.AnnotationProperty,
    SCHEMA.affiliation: OWL.AnnotationProperty,
    SCHEMA.name: OWL.AnnotationProperty,
}


def declare_metadata_terms(graph: Graph):
    """Declare the metadata terms that are used but not declared in the graph."""
    for term, typ in METADATA_TERMS.items():
        if (term, RDF.type, None) in graph:
            continue
        used = (
            (None, term, None) in graph
            if typ == OWL.AnnotationProperty
            else (None, RDF.type, term) in graph
        )
        if used:
            graph.add((term, RDF.type, typ))

class Checker(BaseGraphDocumentChecker):

    def __init__(self, pipeline: "SAREFPipeline", document: SAREFGraphDocument):
@@ -180,6 +207,7 @@ class Checker(BaseGraphDocumentChecker):
                to_serialize = Graph()
                to_serialize += graph_with_imports
                to_serialize.remove((None, OWL.imports, None))
                declare_metadata_terms(to_serialize)
                to_serialize.serialize(
                    destination=temp_file.name, format="ttl", encoding="utf-8"
                )
+2 −2
Changes for src/saref_pypeline/check/resources/shapes/TS103673_Clause_9_4_3_1_Checker.ttl: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -39,8 +39,8 @@ sarefsh:priorVersion sh:path owl:priorVersion ;
sarefsh:imports sh:path owl:imports ;
  sh:severity sh:Violation ;
  sh:nodeKind sh:IRI ;
  sh:pattern "^https://saref.etsi.org/(core|saref4[a-z]{4})/v[0-9]+\\.[0-9]+\\.[0-9]+/$" ;
  sh:message "If the ontology document contains some `owl:imports` declaration, then each value of the declaration shall be an Ontology Version IRI of a SAREF project version" .
  sh:pattern "^https://saref.etsi.org/(core|saref4[a-z]{4})/v[0-9]+\\.[0-9]+\\.[0-9]+/((vocab|patterns?)/[a-zA-Z0-9_-]+/)?$" ;
  sh:message "If the ontology document contains some `owl:imports` declaration, then each value of the declaration shall be an Ontology Version IRI of a SAREF project version, a Vocabulary Version IRI of a SAREF project version, or a Pattern Version IRI of a reference ontology pattern" .

sarefsh:preferredNamespacePrefix sh:path vann:preferredNamespacePrefix ;
  sh:severity sh:Violation ;
Loading