SW-8-Python-SHACL

Navigation : << 7b-OWL | Index | 9-JSONLD >>

SHACL : Validation et Qualite des Données RDF

Ce notebook explore SHACL (Shapes Constraint Language), la recommandation W3C pour la validation de données RDF. Contrairement a OWL qui raisonne sous l’hypothese du monde ouvert, SHACL permet de définir des contraintes concretes et de verifier que les données d’un graphe RDF les respectent.

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Définir des shapes SHACL pour contraindre un graphe RDF 2. Valider des données avec pySHACL et interpreter le rapport 3. Créer des shapes custom programmatiquement 4. Utiliser SHACL pour garantir la qualite des données

Prerequis

  • Notebook SW-7b-OWL complete ### Duree estimee : 45 minutes

0. Installation des dependances

Installons les deux bibliotheques necessaires : - rdflib : manipulation de graphes RDF (déjà utilisee dans SW-2b) - pyshacl : moteur de validation SHACL pour Python

# Dependances pre-provisionnees (rdflib pyshacl) : voir SemanticWeb/requirements.txt ; imports dans les cellules suivantes.

Verifions que les installations se sont bien passees en important les modules et en affichant leurs versions.

import rdflib
import pyshacl

print(f"rdflib   : {rdflib.__version__}")
print(f"pyshacl  : {pyshacl.__version__}")
print("Installation OK.")
rdflib   : 7.6.0
pyshacl  : 0.31.0
Installation OK.

1. Introduction a SHACL

Le problème : l’hypothese du monde ouvert

En RDF et OWL, on travaille sous l’hypothese du monde ouvert (Open World Assumption, OWA) : l’absence d’information ne signifie pas que cette information est fausse. Si un graphe RDF ne mentionne pas l’age d’une personne, OWL considere simplement que l’age est inconnu, pas qu’il est absent.

C’est un problème pour la qualite des données. Dans une application reelle, on veut pouvoir dire : - “Chaque personne doit avoir un nom” - “L’age doit etre un entier positif” - “Un email doit respecter un format précis”

OWL ne peut pas exprimer ces contraintes de validation. C’est la raison d’etre de SHACL (Shapes Constraint Language).

SHACL : Shapes Constraint Language

SHACL est une recommandation W3C publiee en juillet 2017. Elle définit un vocabulaire RDF pour decrire des contraintes (“shapes”) que les données RDF doivent respecter.

Aspect OWL (monde ouvert) SHACL (monde ferme)
Hypothese Monde ouvert (OWA) Monde ferme (CWA)
Objectif Inference, raisonnement Validation, qualite
Absence de donnee Inconnue Violation potentielle
minCount 1 Implique l’existence Exige la presence
Analogie Definition d’un concept Formulaire de saisie
Standard W3C OWL 2 (2012) SHACL 1.0 (2017)

Point cle : OWL decrit ce qui est vrai dans un domaine. SHACL decrit ce que les données doivent respecter. Les deux sont complementaires : OWL pour modeliser, SHACL pour valider.


2. Concepts fondamentaux de SHACL

SHACL repose sur deux types de formes (shapes) :

NodeShape et PropertyShape

Concept URI SHACL Description
NodeShape sh:NodeShape Forme qui s’applique a un noeud RDF (une ressource)
PropertyShape sh:PropertyShape Forme qui contraint une propriete spécifique
targetClass sh:targetClass Cible tous les noeuds d’une classe donnee
targetNode sh:targetNode Cible un noeud spécifique par son URI
property sh:property Lie une NodeShape a ses PropertyShapes
path sh:path Designe la propriete RDF a contraindre

Architecture d’une shape SHACL

ex:PersonShape a sh:NodeShape ;       # Forme pour les personnes
    sh:targetClass foaf:Person ;       # Cible : toutes les foaf:Person
    sh:property [                      # Contrainte sur une propriete
        sh:path foaf:name ;            # Propriete ciblee
        sh:minCount 1 ;               # Au moins une valeur
        sh:datatype xsd:string ;       # Type attendu
    ] .

Principaux types de contraintes

Catégorie Contrainte Propriete SHACL Exemple
Cardinalite Minimum / Maximum sh:minCount, sh:maxCount sh:minCount 1
Type Type de donnee sh:datatype sh:datatype xsd:string
Classe Classe RDF sh:class sh:class foaf:Person
Chaîne Longueur minimale sh:minLength sh:minLength 2
Chaîne Motif regex sh:pattern sh:pattern "^[A-Z]"
Numérique Borne inclusive sh:minInclusive, sh:maxInclusive sh:minInclusive 0

3. Explorer une shape SHACL existante

Le fichier data/person-shape.ttl contient une forme SHACL pour valider des instances de foaf:Person. Chargeons-le et examinons les contraintes définies.

from rdflib import Graph, Namespace, URIRef, Literal
from rdflib.namespace import RDF, RDFS, XSD, FOAF

# Namespaces
SH = Namespace("http://www.w3.org/ns/shacl#")
EX = Namespace("http://example.org/")

# Charger les formes SHACL
shapes_graph = Graph()
shapes_graph.parse("data/person-shape.ttl", format="turtle")

print(f"Graphe des formes charge : {len(shapes_graph)} triples")
print()
print("=" * 60)
print("Contenu de person-shape.ttl :")
print("=" * 60)
print(shapes_graph.serialize(format="turtle"))
Graphe des formes charge : 25 triples

============================================================
Contenu de person-shape.ttl :
============================================================
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:PersonShape a sh:NodeShape ;
    sh:property [ sh:datatype xsd:string ;
            sh:maxCount 1 ;
            sh:message "Une personne doit avoir exactement un nom (string, min 2 caracteres)"@fr ;
            sh:minCount 1 ;
            sh:minLength 2 ;
            sh:path foaf:name ],
        [ sh:maxCount 1 ;
            sh:message "L'email doit etre au format mailto:user@domain.tld"@fr ;
            sh:path foaf:mbox ;
            sh:pattern "^mailto:.+@.+\\..+$" ],
        [ sh:datatype xsd:integer ;
            sh:maxCount 1 ;
            sh:maxInclusive 150 ;
            sh:message "L'age doit etre un entier entre 0 et 150"@fr ;
            sh:minInclusive 0 ;
            sh:path foaf:age ],
        [ sh:class foaf:Person ;
            sh:message "La propriete knows doit pointer vers une autre Person"@fr ;
            sh:path foaf:knows ] ;
    sh:targetClass foaf:Person .

Analyse structurelle par programmation

Plutot que de lire le fichier Turtle a l’oeil, parcourons le graphe SHACL avec rdflib pour identifier automatiquement les NodeShapes, leurs cibles et les contraintes de chaque propriete.

# Parcourir les NodeShapes et leurs contraintes
for shape in shapes_graph.subjects(RDF.type, SH.NodeShape):
    print(f"NodeShape : {shape}")
    
    # Cible
    for target in shapes_graph.objects(shape, SH.targetClass):
        print(f"  Cible (targetClass) : {target}")
    
    # Proprietes contraintes
    print(f"  Contraintes de propriete :")
    for prop_shape in shapes_graph.objects(shape, SH.property):
        path = list(shapes_graph.objects(prop_shape, SH.path))
        path_str = str(path[0]) if path else "?"
        
        # Collecter les contraintes SHACL (hors path et message)
        constraints = []
        for pred, obj in shapes_graph.predicate_objects(prop_shape):
            pred_local = str(pred).split("#")[-1] if "#" in str(pred) else str(pred).split("/")[-1]
            if pred_local not in ("path", "message") and str(pred).startswith(str(SH)):
                constraints.append(f"{pred_local}={obj}")
        
        # Message
        messages = list(shapes_graph.objects(prop_shape, SH.message))
        msg = str(messages[0]) if messages else "-"
        
        print(f"    - {path_str.split('/')[-1]}")
        for c in constraints:
            print(f"        {c}")
        print(f"        Message: {msg}")
NodeShape : http://example.org/PersonShape
  Cible (targetClass) : http://xmlns.com/foaf/0.1/Person
  Contraintes de propriete :
    - name
        minCount=1
        maxCount=1
        datatype=http://www.w3.org/2001/XMLSchema#string
        minLength=2
        Message: Une personne doit avoir exactement un nom (string, min 2 caracteres)
    - mbox
        maxCount=1
        pattern=^mailto:.+@.+\..+$
        Message: L'email doit etre au format mailto:user@domain.tld
    - age
        maxCount=1
        datatype=http://www.w3.org/2001/XMLSchema#integer
        minInclusive=0
        maxInclusive=150
        Message: L'age doit etre un entier entre 0 et 150
    - knows
        class=http://xmlns.com/foaf/0.1/Person
        Message: La propriete knows doit pointer vers une autre Person

Interpretation : Structure de la forme PersonShape

La forme ex:PersonShape cible toutes les instances de foaf:Person et impose 4 contraintes :

Propriete Contraintes Signification
foaf:name minCount=1, maxCount=1, datatype=string, minLength=2 Exactement un nom, chaîne d’au moins 2 caractères
foaf:mbox maxCount=1, pattern=^mailto:.+@.+\\..+$ Au plus un email, format mailto: valide
foaf:age maxCount=1, datatype=integer, minInclusive=0, maxInclusive=150 Au plus un age, entier entre 0 et 150
foaf:knows class=foaf:Person Les personnes connues doivent etre des foaf:Person

Note : foaf:mbox et foaf:age n’ont pas de minCount, ils sont donc optionnels. Seul foaf:name est obligatoire (minCount=1).


4. pySHACL : Validation en Python

La fonction validate()

pySHACL est l’implementation Python de reference pour la validation SHACL. Sa fonction principale validate() accepte les paramètres suivants :

Paramètre Type Description
data_graph Graph / str Le graphe de données a valider (1er argument positionnel)
shacl_graph Graph / str Le graphe de formes SHACL
inference str Mode d’inference : 'none', 'rdfs', 'owlrl', 'both'
abort_on_first bool Arreter a la première violation (defaut: False)
advanced bool Activer SHACL Advanced Features (règles, etc.)
inplace bool Modifier le graphe de données en place (pour les règles)

La fonction retourne un triplet (conforms, results_graph, results_text) : - conforms : booléen, True si toutes les contraintes sont satisfaites - results_graph : graphe RDF contenant les résultats detailles - results_text : rapport texte lisible

Charger les données de test

Le fichier data/person-data.ttl contient 7 personnes dont 5 presentent des erreurs volontaires. Chargeons-le et examinons son contenu.

# Charger les donnees de test
data_graph = Graph()
data_graph.parse("data/person-data.ttl", format="turtle")

print(f"Graphe de donnees charge : {len(data_graph)} triples")
print()

# Lister les personnes avec leurs proprietes
print("Personnes dans le graphe :")
print(f"{'Personne':<12s} {'Nom':<20s} {'Age':<8s} {'Email':<25s} {'Knows':<12s}")
print("-" * 77)

for person in sorted(data_graph.subjects(RDF.type, FOAF.Person), key=str):
    name = list(data_graph.objects(person, FOAF.name))
    age = list(data_graph.objects(person, FOAF.age))
    mbox = list(data_graph.objects(person, FOAF.mbox))
    knows = list(data_graph.objects(person, FOAF.knows))
    
    name_str = str(name[0]) if name else "[ABSENT]"
    age_str = str(age[0]) if age else "-"
    mbox_str = str(mbox[0]).split('/')[-1] if mbox else "-"
    knows_str = str(knows[0]).split('/')[-1] if knows else "-"
    
    print(f"{str(person).split('/')[-1]:<12s} {name_str:<20s} {age_str:<8s} {mbox_str:<25s} {knows_str:<12s}")
Graphe de donnees charge : 25 triples

Personnes dans le graphe :
Personne     Nom                  Age      Email                     Knows       
-----------------------------------------------------------------------------
alice        Alice Dupont         30       mailto:alice@example.com  bob         
bob          Bob Martin           25       -                         alice       
charlie      [ABSENT]             40       -                         -           
diana        Diana Leroy          -5       -                         -           
eve          Eve Bernard          -        not-an-email              -           
frank        F                    55       -                         -           
grace        Grace Moreau         -        -                         myDog       

Exécution de la validation

Validons les données contre les formes SHACL. L’option inference='rdfs' permet a pySHACL de prendre en compte les inferences RDFS (sous-classes, sous-proprietes) lors de la validation.

from pyshacl import validate

# Validation SHACL
conforms, results_graph, results_text = validate(
    data_graph,
    shacl_graph=shapes_graph,
    inference='rdfs',
    abort_on_first=False
)

print(f"Les donnees sont conformes : {conforms}")
print()
print("=" * 60)
print("Rapport de validation :")
print("=" * 60)
print(results_text)
Les donnees sont conformes : False

============================================================
Rapport de validation :
============================================================
Validation Report
Conforms: False
Results (5):
Constraint Violation in ClassConstraintComponent (http://www.w3.org/ns/shacl#ClassConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:class foaf:Person ; sh:message Literal("La propriete knows doit pointer vers une autre Person", lang=fr) ; sh:path foaf:knows ]
    Focus Node: ex:grace
    Value Node: ex:myDog
    Result Path: foaf:knows
    Message: La propriete knows doit pointer vers une autre Person
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:maxCount Literal("1", datatype=xsd:integer) ; sh:message Literal("Une personne doit avoir exactement un nom (string, min 2 caracteres)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minLength Literal("2", datatype=xsd:integer) ; sh:path foaf:name ]
    Focus Node: ex:charlie
    Result Path: foaf:name
    Message: Une personne doit avoir exactement un nom (string, min 2 caracteres)
Constraint Violation in MinInclusiveConstraintComponent (http://www.w3.org/ns/shacl#MinInclusiveConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:integer ; sh:maxCount Literal("1", datatype=xsd:integer) ; sh:maxInclusive Literal("150", datatype=xsd:integer) ; sh:message Literal("L'age doit etre un entier entre 0 et 150", lang=fr) ; sh:minInclusive Literal("0", datatype=xsd:integer) ; sh:path foaf:age ]
    Focus Node: ex:diana
    Value Node: Literal("-5", datatype=xsd:integer)
    Result Path: foaf:age
    Message: L'age doit etre un entier entre 0 et 150
Constraint Violation in MinLengthConstraintComponent (http://www.w3.org/ns/shacl#MinLengthConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:maxCount Literal("1", datatype=xsd:integer) ; sh:message Literal("Une personne doit avoir exactement un nom (string, min 2 caracteres)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minLength Literal("2", datatype=xsd:integer) ; sh:path foaf:name ]
    Focus Node: ex:frank
    Value Node: Literal("F")
    Result Path: foaf:name
    Message: Une personne doit avoir exactement un nom (string, min 2 caracteres)
Constraint Violation in PatternConstraintComponent (http://www.w3.org/ns/shacl#PatternConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:maxCount Literal("1", datatype=xsd:integer) ; sh:message Literal("L'email doit etre au format mailto:user@domain.tld", lang=fr) ; sh:path foaf:mbox ; sh:pattern Literal("^mailto:.+@.+\..+$") ]
    Focus Node: ex:eve
    Value Node: <file:///<repo>MyIA.AI.Notebooks/SymbolicAI/SemanticWeb/data/not-an-email>
    Result Path: foaf:mbox
    Message: L'email doit etre au format mailto:user@domain.tld

Analyse structuree du graphe de résultats

Le rapport texte est utile pour un humain, mais on peut aussi analyser le graphe de résultats par programmation. Chaque violation est un noeud de type sh:ValidationResult avec des proprietes structurees.

# Analyser le graphe de resultats par programmation
violations = []

for result in results_graph.subjects(RDF.type, SH.ValidationResult):
    focus = list(results_graph.objects(result, SH.focusNode))
    path = list(results_graph.objects(result, SH.resultPath))
    severity = list(results_graph.objects(result, SH.resultSeverity))
    message = list(results_graph.objects(result, SH.resultMessage))
    value = list(results_graph.objects(result, SH.value))
    
    violations.append({
        "focus": str(focus[0]).split("/")[-1] if focus else "?",
        "path": str(path[0]).split("/")[-1] if path else "-",
        "severity": str(severity[0]).split("#")[-1] if severity else "?",
        "message": str(message[0]) if message else "-",
        "value": str(value[0]) if value else "-"
    })

# Affichage tabulaire
print(f"Nombre total de violations : {len(violations)}")
print()
print(f"{'Noeud':<12} {'Propriete':<10} {'Severite':<12} {'Message'}")
print("-" * 90)
for v in sorted(violations, key=lambda x: x["focus"]):
    print(f"{v['focus']:<12} {v['path']:<10} {v['severity']:<12} {v['message'][:55]}")
Nombre total de violations : 5

Noeud        Propriete  Severite     Message
------------------------------------------------------------------------------------------
charlie      name       Violation    Une personne doit avoir exactement un nom (string, min 
diana        age        Violation    L'age doit etre un entier entre 0 et 150
eve          mbox       Violation    L'email doit etre au format mailto:user@domain.tld
frank        name       Violation    Une personne doit avoir exactement un nom (string, min 
grace        knows      Violation    La propriete knows doit pointer vers une autre Person

Interpretation : Les 5 erreurs detectees

Le fichier person-data.ttl contenait 5 erreurs volontaires, toutes detectees par pySHACL :

Personne Propriete Type de violation Explication
alice - Aucune Conforme : toutes les proprietes respectent les contraintes
bob - Aucune Conforme (email optionnel, pas de minCount sur mbox)
charlie foaf:name minCount Pas de nom alors que minCount=1 est exige
diana foaf:age minInclusive Age negatif (-5) alors que minInclusive=0
eve foaf:mbox pattern Email not-an-email ne correspond pas au motif ^mailto:
frank foaf:name minLength Nom “F” (1 caractère) trop court, minLength=2
grace foaf:knows class ex:myDog est un ex:Dog, pas un foaf:Person

Points cles : 1. Alice et Bob passent la validation car leurs données sont completes et conformes 2. Bob n’a pas d’email mais c’est conforme : foaf:mbox n’a pas de minCount 3. Chaque violation est independante : un même noeud pourrait cumuler plusieurs violations


5. Ecrire des shapes custom en Python

Plutot que de charger des fichiers Turtle, on peut construire des formes SHACL directement en Python avec rdflib. Cela permet de créer des validations dynamiques, generees par du code.

Catalogue des contraintes

Catégorie Contrainte Propriete SHACL Description
Cardinalite Minimum sh:minCount Nombre minimum de valeurs
Cardinalite Maximum sh:maxCount Nombre maximum de valeurs
Type Type de donnee sh:datatype Type XSD attendu
Type Classe RDF sh:class La valeur doit etre d’une classe donnee
Type Type de noeud sh:nodeKind IRI, BlankNode ou Literal
Chaîne Motif regex sh:pattern Expression reguliere
Chaîne Longueur min sh:minLength Nombre minimum de caractères
Chaîne Longueur max sh:maxLength Nombre maximum de caractères
Numérique Borne min sh:minInclusive Valeur minimale (inclusive)
Numérique Borne max sh:maxInclusive Valeur maximale (inclusive)
Valeur Valeur exacte sh:hasValue Doit contenir cette valeur
Valeur Liste sh:in Doit etre dans cette liste
Unicite Langue unique sh:uniqueLang Une seule valeur par tag de langue

Construire une ArticleShape

Creons une forme SHACL pour valider des articles (schema:Article) avec les contraintes suivantes : - Titre obligatoire (string, min 5 caractères) - Au moins un auteur (doit etre un schema:Person) - Date de publication optionnelle (format xsd:date) - Nombre de mots optionnel (entier entre 100 et 50000)

from rdflib import Graph, Namespace, URIRef, Literal, BNode
from rdflib.namespace import RDF, XSD, FOAF

SH = Namespace("http://www.w3.org/ns/shacl#")
EX = Namespace("http://example.org/")
SCHEMA = Namespace("http://schema.org/")

# Creer un nouveau graphe de formes
custom_shapes = Graph()
custom_shapes.bind("sh", SH)
custom_shapes.bind("ex", EX)
custom_shapes.bind("schema", SCHEMA)
custom_shapes.bind("xsd", XSD)

# --- NodeShape pour un Article ---
article_shape = EX.ArticleShape
custom_shapes.add((article_shape, RDF.type, SH.NodeShape))
custom_shapes.add((article_shape, SH.targetClass, SCHEMA.Article))

# Contrainte 1 : titre obligatoire (minCount + datatype + minLength)
title_prop = BNode()
custom_shapes.add((article_shape, SH.property, title_prop))
custom_shapes.add((title_prop, SH.path, SCHEMA.headline))
custom_shapes.add((title_prop, SH.minCount, Literal(1)))
custom_shapes.add((title_prop, SH.maxCount, Literal(1)))
custom_shapes.add((title_prop, SH.datatype, XSD.string))
custom_shapes.add((title_prop, SH.minLength, Literal(5)))
custom_shapes.add((title_prop, SH.message, Literal("Un article doit avoir un titre (string, min 5 caracteres)", lang="fr")))

# Contrainte 2 : auteur obligatoire, doit etre un schema:Person
author_prop = BNode()
custom_shapes.add((article_shape, SH.property, author_prop))
custom_shapes.add((author_prop, SH.path, SCHEMA.author))
custom_shapes.add((author_prop, SH.minCount, Literal(1)))
custom_shapes.add((author_prop, SH["class"], SCHEMA.Person))
custom_shapes.add((author_prop, SH.message, Literal("Un article doit avoir au moins un auteur (schema:Person)", lang="fr")))

# Contrainte 3 : date de publication optionnelle (xsd:date)
date_prop = BNode()
custom_shapes.add((article_shape, SH.property, date_prop))
custom_shapes.add((date_prop, SH.path, SCHEMA.datePublished))
custom_shapes.add((date_prop, SH.maxCount, Literal(1)))
custom_shapes.add((date_prop, SH.datatype, XSD.date))
custom_shapes.add((date_prop, SH.message, Literal("La date de publication doit etre au format xsd:date", lang="fr")))

# Contrainte 4 : nombre de mots optionnel (entier entre 100 et 50000)
words_prop = BNode()
custom_shapes.add((article_shape, SH.property, words_prop))
custom_shapes.add((words_prop, SH.path, SCHEMA.wordCount))
custom_shapes.add((words_prop, SH.maxCount, Literal(1)))
custom_shapes.add((words_prop, SH.datatype, XSD.integer))
custom_shapes.add((words_prop, SH.minInclusive, Literal(100)))
custom_shapes.add((words_prop, SH.maxInclusive, Literal(50000)))
custom_shapes.add((words_prop, SH.message, Literal("Le nombre de mots doit etre entre 100 et 50000", lang="fr")))

print("Forme ArticleShape creee avec 4 contraintes de propriete")
print()
print(custom_shapes.serialize(format="turtle"))
Forme ArticleShape creee avec 4 contraintes de propriete

@prefix ex: <http://example.org/> .
@prefix schema1: <http://schema.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:ArticleShape a sh:NodeShape ;
    sh:property [ sh:datatype xsd:string ;
            sh:maxCount 1 ;
            sh:message "Un article doit avoir un titre (string, min 5 caracteres)"@fr ;
            sh:minCount 1 ;
            sh:minLength 5 ;
            sh:path schema1:headline ],
        [ sh:class schema1:Person ;
            sh:message "Un article doit avoir au moins un auteur (schema:Person)"@fr ;
            sh:minCount 1 ;
            sh:path schema1:author ],
        [ sh:datatype xsd:integer ;
            sh:maxCount 1 ;
            sh:maxInclusive 50000 ;
            sh:message "Le nombre de mots doit etre entre 100 et 50000"@fr ;
            sh:minInclusive 100 ;
            sh:path schema1:wordCount ],
        [ sh:datatype xsd:date ;
            sh:maxCount 1 ;
            sh:message "La date de publication doit etre au format xsd:date"@fr ;
            sh:path schema1:datePublished ] ;
    sh:targetClass schema1:Article .

Test de la forme personnalisee

Creons un jeu de données avec des articles valides et invalides pour verifier notre forme ArticleShape.

# Creer des donnees de test pour ArticleShape
test_data = Graph()
test_data.bind("ex", EX)
test_data.bind("schema", SCHEMA)

# Auteur valide
test_data.add((EX.jean, RDF.type, SCHEMA.Person))
test_data.add((EX.jean, SCHEMA.name, Literal("Jean Dupont")))

# Article 1 : valide (toutes les proprietes correctes)
test_data.add((EX.article1, RDF.type, SCHEMA.Article))
test_data.add((EX.article1, SCHEMA.headline, Literal("Introduction au Web Semantique", datatype=XSD.string)))
test_data.add((EX.article1, SCHEMA.author, EX.jean))
test_data.add((EX.article1, SCHEMA.datePublished, Literal("2024-01-15", datatype=XSD.date)))
test_data.add((EX.article1, SCHEMA.wordCount, Literal(5000, datatype=XSD.integer)))

# Article 2 : titre trop court ("RDF" = 3 caracteres < 5)
test_data.add((EX.article2, RDF.type, SCHEMA.Article))
test_data.add((EX.article2, SCHEMA.headline, Literal("RDF", datatype=XSD.string)))
test_data.add((EX.article2, SCHEMA.author, EX.jean))

# Article 3 : pas de titre, pas d'auteur
test_data.add((EX.article3, RDF.type, SCHEMA.Article))
test_data.add((EX.article3, SCHEMA.wordCount, Literal(200, datatype=XSD.integer)))

# Article 4 : nombre de mots hors bornes (10 < 100)
test_data.add((EX.article4, RDF.type, SCHEMA.Article))
test_data.add((EX.article4, SCHEMA.headline, Literal("Article avec trop peu de mots", datatype=XSD.string)))
test_data.add((EX.article4, SCHEMA.author, EX.jean))
test_data.add((EX.article4, SCHEMA.wordCount, Literal(10, datatype=XSD.integer)))

# Valider
conforms2, results2, text2 = validate(
    test_data,
    shacl_graph=custom_shapes,
    inference='rdfs',
    abort_on_first=False
)

print(f"Conforme : {conforms2}")
print()
print(text2)
Conforme : False

Validation Report
Conforms: False
Results (4):
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:class schema1:Person ; sh:message Literal("Un article doit avoir au moins un auteur (schema:Person)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path schema1:author ]
    Focus Node: ex:article3
    Result Path: schema1:author
    Message: Un article doit avoir au moins un auteur (schema:Person)
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:maxCount Literal("1", datatype=xsd:integer) ; sh:message Literal("Un article doit avoir un titre (string, min 5 caracteres)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minLength Literal("5", datatype=xsd:integer) ; sh:path schema1:headline ]
    Focus Node: ex:article3
    Result Path: schema1:headline
    Message: Un article doit avoir un titre (string, min 5 caracteres)
Constraint Violation in MinInclusiveConstraintComponent (http://www.w3.org/ns/shacl#MinInclusiveConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:integer ; sh:maxCount Literal("1", datatype=xsd:integer) ; sh:maxInclusive Literal("50000", datatype=xsd:integer) ; sh:message Literal("Le nombre de mots doit etre entre 100 et 50000", lang=fr) ; sh:minInclusive Literal("100", datatype=xsd:integer) ; sh:path schema1:wordCount ]
    Focus Node: ex:article4
    Value Node: Literal("10", datatype=xsd:integer)
    Result Path: schema1:wordCount
    Message: Le nombre de mots doit etre entre 100 et 50000
Constraint Violation in MinLengthConstraintComponent (http://www.w3.org/ns/shacl#MinLengthConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:maxCount Literal("1", datatype=xsd:integer) ; sh:message Literal("Un article doit avoir un titre (string, min 5 caracteres)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minLength Literal("5", datatype=xsd:integer) ; sh:path schema1:headline ]
    Focus Node: ex:article2
    Value Node: Literal("RDF", datatype=xsd:string)
    Result Path: schema1:headline
    Message: Un article doit avoir un titre (string, min 5 caracteres)

Interpretation : Validation des articles

Article Violations Detail
article1 0 Conforme : titre, auteur, date et wordCount respectent toutes les contraintes
article2 1 Titre “RDF” trop court (3 < 5 caractères requis par minLength)
article3 2 Pas de titre (minCount=1) + pas d’auteur (minCount=1)
article4 1 wordCount=10 < minInclusive=100

Bonne pratique : Ajoutez toujours des messages explicites (sh:message) pour faciliter le diagnostic. Les messages en francais ou en anglais sont plus utiles que les messages generiques de pySHACL.


6. Severites SHACL

SHACL définit trois niveaux de severite pour les violations. Par defaut, toutes les contraintes ont la severite sh:Violation, mais on peut les ajuster.

Severite URI Comportement Usage typique
Violation sh:Violation Erreur bloquante (defaut) Donnee manquante obligatoire
Warning sh:Warning Avertissement non bloquant Donnee recommandee mais optionnelle
Info sh:Info Information, suggestion Bonne pratique non imposee

Le booléen conforms retourne False uniquement pour les sh:Violation. Les warnings et infos n’affectent pas la conformite.

# Demonstrer les 3 niveaux de severite
severity_shapes_ttl = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .

ex:EmployeeShape a sh:NodeShape ;
    sh:targetClass ex:Employee ;
    # VIOLATION : le nom est obligatoire
    sh:property [
        sh:path foaf:name ;
        sh:minCount 1 ;
        sh:severity sh:Violation ;
        sh:message "Le nom est obligatoire (Violation)"@fr ;
    ] ;
    # WARNING : l'email est recommande mais pas obligatoire
    sh:property [
        sh:path foaf:mbox ;
        sh:minCount 1 ;
        sh:severity sh:Warning ;
        sh:message "L'email est recommande (Warning)"@fr ;
    ] ;
    # INFO : le telephone est souhaitable
    sh:property [
        sh:path ex:phone ;
        sh:minCount 1 ;
        sh:severity sh:Info ;
        sh:message "Le telephone est souhaitable (Info)"@fr ;
    ] .
"""

severity_shapes = Graph()
severity_shapes.parse(data=severity_shapes_ttl, format="turtle")

# Employe sans nom, sans email, sans telephone
severity_data = Graph()
severity_data.bind("ex", EX)
severity_data.add((EX.emp1, RDF.type, EX.Employee))
# Pas de nom, pas d'email, pas de telephone

conforms_sev, results_sev, text_sev = validate(
    severity_data,
    shacl_graph=severity_shapes,
    inference='rdfs',
    abort_on_first=False
)

print(f"Conforme : {conforms_sev}")
print()

# Classer les resultats par severite
for result in results_sev.subjects(RDF.type, SH.ValidationResult):
    severity = list(results_sev.objects(result, SH.resultSeverity))
    message = list(results_sev.objects(result, SH.resultMessage))
    sev_str = str(severity[0]).split("#")[-1] if severity else "?"
    msg_str = str(message[0]) if message else "-"
    print(f"  [{sev_str:<10s}] {msg_str}")
Conforme : False

  [Violation ] Le nom est obligatoire (Violation)
  [Warning   ] L'email est recommande (Warning)
  [Info      ] Le telephone est souhaitable (Info)

Interpretation : Impact de la severite

Bien que les 3 contraintes soient violees, le booléen conforms ne depend que de sh:Violation :

Severite Contrainte violee Impact sur conforms
sh:Violation Pas de nom conforms = False
sh:Warning Pas d’email Pas d’impact (avertissement seulement)
sh:Info Pas de telephone Pas d’impact (information seulement)

Bonne pratique : Utilisez sh:Warning pour les proprietes recommandees (best practices) et sh:Info pour les suggestions. Reservez sh:Violation aux exigences absolues.


7. Contraintes avancees : opérateurs logiques

SHACL permet de combiner des contraintes avec des opérateurs logiques :

Opérateur Propriete SHACL Sémantique
OU sh:or Au moins une des sous-formes doit etre satisfaite
ET sh:and Toutes les sous-formes doivent etre satisfaites
NON sh:not La sous-forme ne doit PAS etre satisfaite
OU exclusif sh:xone Exactement une sous-forme satisfaite

Exemple : un Contact doit avoir un email OU un telephone

from rdflib.collection import Collection

# Forme avancee avec sh:or
adv_shapes = Graph()
adv_shapes.bind("sh", SH)
adv_shapes.bind("ex", EX)
adv_shapes.bind("schema", SCHEMA)
adv_shapes.bind("xsd", XSD)

contact_shape = EX.ContactShape
adv_shapes.add((contact_shape, RDF.type, SH.NodeShape))
adv_shapes.add((contact_shape, SH.targetClass, EX.Contact))

# sh:or - Au moins email OU telephone
# Option A : a un email
option_email = BNode()
email_inner = BNode()
adv_shapes.add((option_email, SH.property, email_inner))
adv_shapes.add((email_inner, SH.path, SCHEMA.email))
adv_shapes.add((email_inner, SH.minCount, Literal(1)))

# Option B : a un telephone
option_phone = BNode()
phone_inner = BNode()
adv_shapes.add((option_phone, SH.property, phone_inner))
adv_shapes.add((phone_inner, SH.path, SCHEMA.telephone))
adv_shapes.add((phone_inner, SH.minCount, Literal(1)))

# Creer la liste RDF pour sh:or
or_list = BNode()
Collection(adv_shapes, or_list, [option_email, option_phone])
adv_shapes.add((contact_shape, SH["or"], or_list))
adv_shapes.add((contact_shape, SH.message, Literal("Un contact doit avoir un email ou un telephone", lang="fr")))

print("Forme ContactShape avec sh:or creee")
print()
print(adv_shapes.serialize(format="turtle"))
Forme ContactShape avec sh:or creee

@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix schema1: <http://schema.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:ContactShape a sh:NodeShape ;
    sh:message "Un contact doit avoir un email ou un telephone"@fr ;
    sh:or ( [ sh:property [ sh:minCount 1 ;
                        sh:path schema1:email ] ] [ sh:property [ sh:minCount 1 ;
                        sh:path schema1:telephone ] ] ) ;
    sh:targetClass ex:Contact .

Test des contraintes avancees

Creons des contacts qui testent différents scénarios : avec email seul, telephone seul, les deux, ou aucun.

# Donnees de test pour les contraintes avancees
contact_data = Graph()
contact_data.bind("ex", EX)
contact_data.bind("schema", SCHEMA)

# Contact 1 : email seulement -> OK
contact_data.add((EX.contact1, RDF.type, EX.Contact))
contact_data.add((EX.contact1, SCHEMA.name, Literal("Alice")))
contact_data.add((EX.contact1, SCHEMA.email, Literal("alice@example.com")))

# Contact 2 : telephone seulement -> OK
contact_data.add((EX.contact2, RDF.type, EX.Contact))
contact_data.add((EX.contact2, SCHEMA.name, Literal("Bob")))
contact_data.add((EX.contact2, SCHEMA.telephone, Literal("+33 1 23 45 67 89")))

# Contact 3 : ni email ni telephone -> VIOLATION (sh:or)
contact_data.add((EX.contact3, RDF.type, EX.Contact))
contact_data.add((EX.contact3, SCHEMA.name, Literal("Charlie")))

# Contact 4 : les deux -> OK
contact_data.add((EX.contact4, RDF.type, EX.Contact))
contact_data.add((EX.contact4, SCHEMA.name, Literal("Diana")))
contact_data.add((EX.contact4, SCHEMA.email, Literal("diana@example.com")))
contact_data.add((EX.contact4, SCHEMA.telephone, Literal("+33 6 12 34 56 78")))

# Valider
conforms3, results3, text3 = validate(
    contact_data,
    shacl_graph=adv_shapes,
    inference='rdfs',
    abort_on_first=False
)

print(f"Conforme : {conforms3}")
print()
print(text3)
Conforme : False

Validation Report
Conforms: False
Results (1):
Constraint Violation in OrConstraintComponent (http://www.w3.org/ns/shacl#OrConstraintComponent):
    Severity: sh:Violation
    Source Shape: ex:ContactShape
    Focus Node: ex:contact3
    Value Node: ex:contact3
    Message: Un contact doit avoir un email ou un telephone

Interpretation : Contraintes logiques

Contact Email Telephone Résultat Raison
contact1 Oui Non Conforme sh:or satisfait par email
contact2 Non Oui Conforme sh:or satisfait par telephone
contact3 Non Non Violation sh:or non satisfait (aucune branche valide)
contact4 Oui Oui Conforme sh:or satisfait par les deux (OU inclusif)

Point cle : sh:or est un OU inclusif : avoir les deux options satisfaites est aussi valide. Pour exiger exactement une option, utilisez sh:xone.


8. Exemples et Exercices

Exemple guide 1 : Corriger les erreurs de person-data.ttl

Les 5 personnes invalides dans person-data.ttl ont chacune une erreur. Voici comment les corriger par programmation en modifiant le graphe data_graph, puis relancer la validation pour verifier que toutes les personnes passent.

Rappel des erreurs : - charlie : pas de nom - diana : age negatif (-5) - eve : email mal forme - frank : nom trop court (“F”) - grace : knows pointe vers un non-Person

# Exemple guide 1 : Corriger les erreurs dans le graphe de donnees

# Recharger les donnees pour partir d'une copie propre
fixed_data = Graph()
fixed_data.parse("data/person-data.ttl", format="turtle")

# Correction 1 : Ajouter un nom a charlie
fixed_data.add((EX.charlie, FOAF.name, Literal("Charlie Durand")))

# Correction 2 : Corriger l'age de diana (remplacer -5 par 25)
for _,_, o in fixed_data.triples((EX.diana, FOAF.age, None)):
    fixed_data.remove((EX.diana, FOAF.age, o))
fixed_data.add((EX.diana, FOAF.age, Literal(25, datatype=XSD.integer)))

# Correction 3 : Corriger l'email d'eve
for _, _, o in fixed_data.triples((EX.eve, FOAF.mbox, None)):
    fixed_data.remove((EX.eve, FOAF.mbox, o))

fixed_data.add((EX.eve, FOAF.mbox, URIRef("mailto:eve@example.com")))

# Correction 4 : Allonger le nom de frank
for _, _, o in fixed_data.triples((EX.frank, FOAF.name, None)):
    fixed_data.remove((EX.frank, FOAF.name, o))

fixed_data.add((EX.frank, FOAF.name, Literal("Frank")))

# Correction 5 : Faire de myDog un Person (ou changer le knows)
for _, _, o in fixed_data.triples((EX.grace, FOAF.knows, None)):
    fixed_data.remove((EX.grace, FOAF.knows, o))

# Valider le graphe corrige
conforms_fix, _, text_fix = validate(
    fixed_data, shacl_graph=shapes_graph, inference='rdfs', abort_on_first=False
)
print(f"Conforme apres corrections : {conforms_fix}")
if not conforms_fix:
    print(text_fix)
else:
    print("Toutes les personnes sont maintenant conformes.")
Conforme apres corrections : True
Toutes les personnes sont maintenant conformes.

Exemple guide 2 : Forme SHACL pour un Livre

Construction d’une forme SHACL ex:BookShape pour valider des instances de schema:Book avec les contraintes suivantes : - Titre (schema:name) : obligatoire, chaîne, min 1 caractère - ISBN (schema:isbn) : optionnel, doit respecter le motif EAN-13 ^97[89]-\d-\d{4}-\d{4}-\d$ - Annee (schema:datePublished) : optionnel, entier entre 1450 (Gutenberg) et 2030 - Auteur (schema:author) : au moins un, doit etre un schema:Person

Puis creation de 4 livres (2 valides, 2 invalides) et validation.

# Exemple guide 2 : Construction de la forme BookShape

book_shapes = Graph()
book_shapes.bind("sh", SH)
book_shapes.bind("ex", EX)
book_shapes.bind("schema", SCHEMA)
book_shapes.bind("xsd", XSD)

book_shape = EX.BookShape
book_shapes.add((book_shape, RDF.type, SH.NodeShape))
book_shapes.add((book_shape, SH.targetClass, SCHEMA.Book))

# Contrainte 1 : Titre obligatoire (minCount, datatype, minLength)
title_shape = EX.titleShape
book_shapes.add((book_shape, SH.property, title_shape))
book_shapes.add((title_shape, SH.path, SCHEMA.name))
book_shapes.add((title_shape, SH.minCount, Literal(1)))
book_shapes.add((title_shape, SH.datatype, XSD.string))
book_shapes.add((title_shape, SH.minLength, Literal(1)))

# Contrainte 2 : ISBN avec pattern (format EAN-13 : 978-X-XXXX-XXXX-X)
isbn_shape = EX.isbnShape
book_shapes.add((book_shape, SH.property, isbn_shape))
book_shapes.add((isbn_shape, SH.path, SCHEMA.isbn))
book_shapes.add((isbn_shape, SH.datatype, XSD.string))
book_shapes.add((isbn_shape, SH.pattern,
                 Literal(r"^97[89]-\d-\d{4}-\d{4}-\d$")))

# Contrainte 3 : Annee entre 1450 et 2030 (minInclusive, maxInclusive)
year_shape = EX.yearShape
book_shapes.add((book_shape, SH.property, year_shape))
book_shapes.add((year_shape, SH.path, SCHEMA.datePublished))
book_shapes.add((year_shape, SH.datatype, XSD.integer))
book_shapes.add((year_shape, SH.minInclusive, Literal(1450)))
book_shapes.add((year_shape, SH.maxInclusive, Literal(2030)))

# Contrainte 4 : Au moins un auteur de type schema:Person
author_shape = EX.authorShape
book_shapes.add((book_shape, SH.property, author_shape))
book_shapes.add((author_shape, SH.path, SCHEMA.author))
book_shapes.add((author_shape, SH.minCount, Literal(1)))
book_shapes.add((author_shape, SH.nodeKind, SH.IRI))
book_shapes.add((author_shape, SH["class"], SCHEMA.Person))

print("Forme BookShape :")
print(book_shapes.serialize(format="turtle"))

# Donnees de test (2 valides + 2 invalides)
book_data = Graph()
book_data.add((EX.book1, RDF.type, SCHEMA.Book))
book_data.add((EX.book1, SCHEMA.name, Literal("Clean Code")))
book_data.add((EX.book1, SCHEMA.isbn, Literal("978-0-1323-5088-4")))
book_data.add((EX.book1, SCHEMA.datePublished, Literal(2008)))
book_data.add((EX.book1, SCHEMA.author, EX.author1))
book_data.add((EX.author1, RDF.type, SCHEMA.Person))

book_data.add((EX.book2, RDF.type, SCHEMA.Book))
book_data.add((EX.book2, SCHEMA.name, Literal("Design Patterns")))
book_data.add((EX.book2, SCHEMA.isbn, Literal("978-0-2016-3361-0")))
book_data.add((EX.book2, SCHEMA.datePublished, Literal(1994)))
book_data.add((EX.book2, SCHEMA.author, EX.author2))
book_data.add((EX.author2, RDF.type, SCHEMA.Person))

book_data.add((EX.book3, RDF.type, SCHEMA.Book))
book_data.add((EX.book3, SCHEMA.name, Literal("")))
book_data.add((EX.book3, SCHEMA.isbn, Literal("123-INVALID")))

book_data.add((EX.book4, RDF.type, SCHEMA.Book))
book_data.add((EX.book4, SCHEMA.name, Literal("Future Book")))
book_data.add((EX.book4, SCHEMA.datePublished, Literal(3000)))
book_data.add((EX.book4, SCHEMA.author, EX.robot))

# Validation
conforms_book, _, text_book = validate(
    book_data,
    shacl_graph=book_shapes,
    inference='rdfs',
    abort_on_first=False
)

print(conforms_book)
print(text_book)
Forme BookShape :
@prefix ex: <http://example.org/> .
@prefix schema1: <http://schema.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ex:BookShape a sh:NodeShape ;
    sh:property ex:authorShape,
        ex:isbnShape,
        ex:titleShape,
        ex:yearShape ;
    sh:targetClass schema1:Book .

ex:authorShape sh:class schema1:Person ;
    sh:minCount 1 ;
    sh:nodeKind sh:IRI ;
    sh:path schema1:author .

ex:isbnShape sh:datatype xsd:string ;
    sh:path schema1:isbn ;
    sh:pattern "^97[89]-\\d-\\d{4}-\\d{4}-\\d$" .

ex:titleShape sh:datatype xsd:string ;
    sh:minCount 1 ;
    sh:minLength 1 ;
    sh:path schema1:name .

ex:yearShape sh:datatype xsd:integer ;
    sh:maxInclusive 2030 ;
    sh:minInclusive 1450 ;
    sh:path schema1:datePublished .


False
Validation Report
Conforms: False
Results (5):
Constraint Violation in ClassConstraintComponent (http://www.w3.org/ns/shacl#ClassConstraintComponent):
    Severity: sh:Violation
    Source Shape: ex:authorShape
    Focus Node: <http://example.org/book4>
    Value Node: <http://example.org/robot>
    Result Path: schema1:author
    Message: Value does not have class schema1:Person
Constraint Violation in MaxInclusiveConstraintComponent (http://www.w3.org/ns/shacl#MaxInclusiveConstraintComponent):
    Severity: sh:Violation
    Source Shape: ex:yearShape
    Focus Node: <http://example.org/book4>
    Value Node: Literal("3000", datatype=xsd:integer)
    Result Path: schema1:datePublished
    Message: Value is not <= Literal("2030", datatype=xsd:integer)
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: ex:authorShape
    Focus Node: <http://example.org/book3>
    Result Path: schema1:author
    Message: Less than 1 values on <http://example.org/book3>->schema1:author
Constraint Violation in MinLengthConstraintComponent (http://www.w3.org/ns/shacl#MinLengthConstraintComponent):
    Severity: sh:Violation
    Source Shape: ex:titleShape
    Focus Node: <http://example.org/book3>
    Value Node: Literal("")
    Result Path: schema1:name
    Message: String length not >= Literal("1", datatype=xsd:integer)
Constraint Violation in PatternConstraintComponent (http://www.w3.org/ns/shacl#PatternConstraintComponent):
    Severity: sh:Violation
    Source Shape: ex:isbnShape
    Focus Node: <http://example.org/book3>
    Value Node: Literal("123-INVALID")
    Result Path: schema1:isbn
    Message: Value does not match pattern '^97[89]-\d-\d{4}-\d{4}-\d$'

Exemple guide 3 : Contraintes avec severites multiples

Creation d’une forme SHACL ex:StudentShape pour valider des etudiants (ex:Student) avec les contraintes suivantes et les severites indiquees :

Propriete Contrainte Severite
foaf:name Obligatoire, chaîne sh:Violation
ex:studentId Obligatoire, motif ^[A-Z]{2}\d{6}$ sh:Violation
foaf:mbox Recommande (minCount 1) sh:Warning
ex:phone Souhaitable (minCount 1) sh:Info

Test avec 3 etudiants : un complet, un sans email/telephone, un sans nom.

# Exemple guide 3 : StudentShape avec severites multiples

student_shapes_ttl = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .

ex:StudentShape a sh:NodeShape ;
    sh:targetClass ex:Student ;
    
    sh:property [
        sh:path foaf:name ;
        sh:minCount 1 ;
        sh:datatype xsd:string ;
        sh:severity sh:Violation ;
    ] ;

    sh:property [
        sh:path ex:studentId ;
        sh:minCount 1 ;
        sh:pattern "^[A-Z]{2}\\\\d{6}$" ;
        sh:severity sh:Violation ;
    ] ;

    sh:property [
        sh:path foaf:mbox ;
        sh:minCount 1 ;
        sh:severity sh:Warning ;
    ] ;

    sh:property [
        sh:path ex:phone ;
        sh:minCount 1 ;
        sh:severity sh:Info ;
    ] .    
"""

student_shapes = Graph()
student_shapes.parse(data=student_shapes_ttl, format="turtle")
student_data = Graph()

student_data.add((EX.alice, RDF.type, EX.Student))
student_data.add((EX.alice, FOAF.name, Literal("Alice Martin")))
student_data.add((EX.alice, EX.studentId, Literal("AB123456")))
student_data.add((EX.alice, FOAF.mbox, Literal("alice@example.com")))
student_data.add((EX.alice, EX.phone, Literal("0123456789")))

student_data.add((EX.bob, RDF.type, EX.Student))
student_data.add((EX.bob, FOAF.name, Literal("Bob Dupont")))
student_data.add((EX.bob, EX.studentId, Literal("CD654321")))

student_data.add((EX.charlie, RDF.type, EX.Student))
student_data.add((EX.charlie, EX.studentId, Literal("EF111111")))


conforms, results_graph, results_text = validate(
    student_data,
    shacl_graph=student_shapes,
    inference='rdfs',
    abort_on_first=False
)

print("Conforme :", conforms)
print()
print(results_text)
Conforme : False

Validation Report
Conforms: False
Results (5):
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path foaf:name ; sh:severity sh:Violation ]
    Focus Node: <http://example.org/charlie>
    Result Path: foaf:name
    Message: Less than 1 values on <http://example.org/charlie>->foaf:name
Validation Result in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Info
    Source Shape: [ sh:minCount Literal("1", datatype=xsd:integer) ; sh:path ex:phone ; sh:severity sh:Info ]
    Focus Node: <http://example.org/bob>
    Result Path: ex:phone
    Message: Less than 1 values on <http://example.org/bob>->ex:phone
Validation Result in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Info
    Source Shape: [ sh:minCount Literal("1", datatype=xsd:integer) ; sh:path ex:phone ; sh:severity sh:Info ]
    Focus Node: <http://example.org/charlie>
    Result Path: ex:phone
    Message: Less than 1 values on <http://example.org/charlie>->ex:phone
Validation Result in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Warning
    Source Shape: [ sh:minCount Literal("1", datatype=xsd:integer) ; sh:path foaf:mbox ; sh:severity sh:Warning ]
    Focus Node: <http://example.org/bob>
    Result Path: foaf:mbox
    Message: Less than 1 values on <http://example.org/bob>->foaf:mbox
Validation Result in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Warning
    Source Shape: [ sh:minCount Literal("1", datatype=xsd:integer) ; sh:path foaf:mbox ; sh:severity sh:Warning ]
    Focus Node: <http://example.org/charlie>
    Result Path: foaf:mbox
    Message: Less than 1 values on <http://example.org/charlie>->foaf:mbox

Exemples guides (solutions proposees par @Sosolalt)

Les exemples ci-dessous ont ete resolus par @Sosolalt (EPITA-IS, promo 2028). Ils servent de modèle pour comprendre les concepts abordes dans ce notebook.

Exemple guide 4 : MovieShape - Valider et corriger un graphe de films

Solution proposee par @Sosolalt (EPITA-IS, promo 2028).

# Exercice 1 : Definir MovieShape et valider des donnees de films

# Graphe de formes MovieShape
movie_shapes = Graph()
movie_shapes.bind("sh", SH)
movie_shapes.bind("ex", EX)
movie_shapes.bind("schema", SCHEMA)
movie_shapes.bind("xsd", XSD)

movie_shape = EX.MovieShape
movie_shapes.add((movie_shape, RDF.type, SH.NodeShape))
movie_shapes.add((movie_shape, SH.targetClass, SCHEMA.Movie))

# Contrainte 1 : titre obligatoire (string, min 1 caractere)
title_ps = BNode()
movie_shapes.add((movie_shape, SH.property, title_ps))
movie_shapes.add((title_ps, SH.path, SCHEMA.name))
movie_shapes.add((title_ps, SH.minCount, Literal(1)))
movie_shapes.add((title_ps, SH.datatype, XSD.string))
movie_shapes.add((title_ps, SH.minLength, Literal(1)))
movie_shapes.add((title_ps, SH.message, Literal("Un film doit avoir un titre", lang="fr")))

# Contrainte 2 : annee de publication entre 1900 et 2030
year_ps = BNode()
movie_shapes.add((movie_shape, SH.property, year_ps))
movie_shapes.add((year_ps, SH.path, SCHEMA.datePublished))
movie_shapes.add((year_ps, SH.datatype, XSD.integer))
movie_shapes.add((year_ps, SH.minInclusive, Literal(1900)))
movie_shapes.add((year_ps, SH.maxInclusive, Literal(2030)))
movie_shapes.add((year_ps, SH.message, Literal("L'annee doit etre comprise entre 1900 et 2030", lang="fr")))

# Contrainte 3 : au moins un acteur de type schema:Person
actor_ps = BNode()
movie_shapes.add((movie_shape, SH.property, actor_ps))
movie_shapes.add((actor_ps, SH.path, SCHEMA.actor))
movie_shapes.add((actor_ps, SH.minCount, Literal(1)))
movie_shapes.add((actor_ps, SH["class"], SCHEMA.Person))
movie_shapes.add((actor_ps, SH.message, Literal("Un film doit avoir au moins un acteur (schema:Person)", lang="fr")))

# Graphe de donnees : 2 films valides + 3 invalides
movie_data = Graph()
movie_data.bind("ex", EX)
movie_data.bind("schema", SCHEMA)

# Acteurs (schema:Person)
for actor in (EX.actorA, EX.actorB, EX.actorC):
    movie_data.add((actor, RDF.type, SCHEMA.Person))

# Film valide 1
movie_data.add((EX.movie1, RDF.type, SCHEMA.Movie))
movie_data.add((EX.movie1, SCHEMA.name, Literal("Inception", datatype=XSD.string)))
movie_data.add((EX.movie1, SCHEMA.datePublished, Literal(2010, datatype=XSD.integer)))
movie_data.add((EX.movie1, SCHEMA.actor, EX.actorA))

# Film valide 2
movie_data.add((EX.movie2, RDF.type, SCHEMA.Movie))
movie_data.add((EX.movie2, SCHEMA.name, Literal("The Matrix", datatype=XSD.string)))
movie_data.add((EX.movie2, SCHEMA.datePublished, Literal(1999, datatype=XSD.integer)))
movie_data.add((EX.movie2, SCHEMA.actor, EX.actorB))

# Film invalide 1 : pas de titre, pas d'acteur
movie_data.add((EX.movie3, RDF.type, SCHEMA.Movie))
movie_data.add((EX.movie3, SCHEMA.datePublished, Literal(2005, datatype=XSD.integer)))

# Film invalide 2 : annee hors bornes (1850 < 1900)
movie_data.add((EX.movie4, RDF.type, SCHEMA.Movie))
movie_data.add((EX.movie4, SCHEMA.name, Literal("Vieux Film", datatype=XSD.string)))
movie_data.add((EX.movie4, SCHEMA.datePublished, Literal(1850, datatype=XSD.integer)))
movie_data.add((EX.movie4, SCHEMA.actor, EX.actorC))

# Film invalide 3 : acteur qui n'est pas un schema:Person
movie_data.add((EX.movie5, RDF.type, SCHEMA.Movie))
movie_data.add((EX.movie5, SCHEMA.name, Literal("Film Sans Acteur Valide", datatype=XSD.string)))
movie_data.add((EX.movie5, SCHEMA.datePublished, Literal(2020, datatype=XSD.integer)))
movie_data.add((EX.movie5, SCHEMA.actor, EX.studioX))
movie_data.add((EX.studioX, RDF.type, SCHEMA.Organization))

# Validation
conforms_movie, _, text_movie = validate(
    movie_data, shacl_graph=movie_shapes, inference='rdfs', abort_on_first=False
)
print(f"Conforme : {conforms_movie}")
print()
print(text_movie)
Conforme : False

Validation Report
Conforms: False
Results (4):
Constraint Violation in ClassConstraintComponent (http://www.w3.org/ns/shacl#ClassConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:class schema1:Person ; sh:message Literal("Un film doit avoir au moins un acteur (schema:Person)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path schema1:actor ]
    Focus Node: ex:movie5
    Value Node: ex:studioX
    Result Path: schema1:actor
    Message: Un film doit avoir au moins un acteur (schema:Person)
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:class schema1:Person ; sh:message Literal("Un film doit avoir au moins un acteur (schema:Person)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path schema1:actor ]
    Focus Node: ex:movie3
    Result Path: schema1:actor
    Message: Un film doit avoir au moins un acteur (schema:Person)
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:message Literal("Un film doit avoir un titre", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minLength Literal("1", datatype=xsd:integer) ; sh:path schema1:name ]
    Focus Node: ex:movie3
    Result Path: schema1:name
    Message: Un film doit avoir un titre
Constraint Violation in MinInclusiveConstraintComponent (http://www.w3.org/ns/shacl#MinInclusiveConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:integer ; sh:maxInclusive Literal("2030", datatype=xsd:integer) ; sh:message Literal("L'annee doit etre comprise entre 1900 et 2030", lang=fr) ; sh:minInclusive Literal("1900", datatype=xsd:integer) ; sh:path schema1:datePublished ]
    Focus Node: ex:movie4
    Value Node: Literal("1850", datatype=xsd:integer)
    Result Path: schema1:datePublished
    Message: L'annee doit etre comprise entre 1900 et 2030

Exemple guide 5 : EventShape - Forme SHACL pour un Événement

Solution proposee par @Sosolalt (EPITA-IS, promo 2028).

# Exercice 2 : Construction de EventShape

# Graphe de formes
event_shapes = Graph()
event_shapes.bind("sh", SH)
event_shapes.bind("ex", EX)
event_shapes.bind("schema", SCHEMA)
event_shapes.bind("xsd", XSD)

event_shape = EX.EventShape
event_shapes.add((event_shape, RDF.type, SH.NodeShape))
event_shapes.add((event_shape, SH.targetClass, SCHEMA.Event))

# Contrainte 1 : nom obligatoire, chaine, min 3 caracteres
name_ps = BNode()
event_shapes.add((event_shape, SH.property, name_ps))
event_shapes.add((name_ps, SH.path, SCHEMA.name))
event_shapes.add((name_ps, SH.minCount, Literal(1)))
event_shapes.add((name_ps, SH.datatype, XSD.string))
event_shapes.add((name_ps, SH.minLength, Literal(3)))
event_shapes.add((name_ps, SH.message, Literal("Le nom est obligatoire (min 3 caracteres)", lang="fr")))

# Contrainte 2 : date de debut obligatoire, format xsd:date
start_ps = BNode()
event_shapes.add((event_shape, SH.property, start_ps))
event_shapes.add((start_ps, SH.path, SCHEMA.startDate))
event_shapes.add((start_ps, SH.minCount, Literal(1)))
event_shapes.add((start_ps, SH.datatype, XSD.date))
event_shapes.add((start_ps, SH.message, Literal("La date de debut est obligatoire (xsd:date)", lang="fr")))

# Contrainte 3 : lieu obligatoire, doit etre un schema:Place
loc_ps = BNode()
event_shapes.add((event_shape, SH.property, loc_ps))
event_shapes.add((loc_ps, SH.path, SCHEMA.location))
event_shapes.add((loc_ps, SH.minCount, Literal(1)))
event_shapes.add((loc_ps, SH["class"], SCHEMA.Place))
event_shapes.add((loc_ps, SH.message, Literal("Le lieu est obligatoire (schema:Place)", lang="fr")))

# Contrainte 4 : capacite max optionnelle, entier entre 1 et 100000
cap_ps = BNode()
event_shapes.add((event_shape, SH.property, cap_ps))
event_shapes.add((cap_ps, SH.path, SCHEMA.maximumAttendeeCapacity))
event_shapes.add((cap_ps, SH.maxCount, Literal(1)))
event_shapes.add((cap_ps, SH.datatype, XSD.integer))
event_shapes.add((cap_ps, SH.minInclusive, Literal(1)))
event_shapes.add((cap_ps, SH.maxInclusive, Literal(100000)))
event_shapes.add((cap_ps, SH.message, Literal("La capacite doit etre un entier entre 1 et 100000", lang="fr")))

# Donnees de test (1 valide, 2 invalides)
event_data = Graph()
event_data.bind("ex", EX)
event_data.bind("schema", SCHEMA)

# Lieu valide
event_data.add((EX.venue1, RDF.type, SCHEMA.Place))
event_data.add((EX.venue1, SCHEMA.name, Literal("Palais des Congres")))

# Evenement valide
event_data.add((EX.event1, RDF.type, SCHEMA.Event))
event_data.add((EX.event1, SCHEMA.name, Literal("Conference Web Semantique", datatype=XSD.string)))
event_data.add((EX.event1, SCHEMA.startDate, Literal("2024-06-15", datatype=XSD.date)))
event_data.add((EX.event1, SCHEMA.location, EX.venue1))
event_data.add((EX.event1, SCHEMA.maximumAttendeeCapacity, Literal(500, datatype=XSD.integer)))

# Evenement invalide 1 : nom trop court (< 3) et pas de lieu
event_data.add((EX.event2, RDF.type, SCHEMA.Event))
event_data.add((EX.event2, SCHEMA.name, Literal("AI", datatype=XSD.string)))
event_data.add((EX.event2, SCHEMA.startDate, Literal("2024-07-01", datatype=XSD.date)))

# Evenement invalide 2 : pas de date, lieu non Place, capacite hors bornes
event_data.add((EX.event3, RDF.type, SCHEMA.Event))
event_data.add((EX.event3, SCHEMA.name, Literal("Festival", datatype=XSD.string)))
event_data.add((EX.venue2, RDF.type, SCHEMA.Organization))
event_data.add((EX.event3, SCHEMA.location, EX.venue2))
event_data.add((EX.event3, SCHEMA.maximumAttendeeCapacity, Literal(200000, datatype=XSD.integer)))

# Validation
conforms_event, _, text_event = validate(
    event_data, shacl_graph=event_shapes, inference='rdfs', abort_on_first=False
)
print(f"Conforme : {conforms_event}")
print()
print(text_event)
Conforme : False

Validation Report
Conforms: False
Results (5):
Constraint Violation in ClassConstraintComponent (http://www.w3.org/ns/shacl#ClassConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:class schema1:Place ; sh:message Literal("Le lieu est obligatoire (schema:Place)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path schema1:location ]
    Focus Node: ex:event3
    Value Node: ex:venue2
    Result Path: schema1:location
    Message: Le lieu est obligatoire (schema:Place)
Constraint Violation in MaxInclusiveConstraintComponent (http://www.w3.org/ns/shacl#MaxInclusiveConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:integer ; sh:maxCount Literal("1", datatype=xsd:integer) ; sh:maxInclusive Literal("100000", datatype=xsd:integer) ; sh:message Literal("La capacite doit etre un entier entre 1 et 100000", lang=fr) ; sh:minInclusive Literal("1", datatype=xsd:integer) ; sh:path schema1:maximumAttendeeCapacity ]
    Focus Node: ex:event3
    Value Node: Literal("200000", datatype=xsd:integer)
    Result Path: schema1:maximumAttendeeCapacity
    Message: La capacite doit etre un entier entre 1 et 100000
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:class schema1:Place ; sh:message Literal("Le lieu est obligatoire (schema:Place)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path schema1:location ]
    Focus Node: ex:event2
    Result Path: schema1:location
    Message: Le lieu est obligatoire (schema:Place)
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:date ; sh:message Literal("La date de debut est obligatoire (xsd:date)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path schema1:startDate ]
    Focus Node: ex:event3
    Result Path: schema1:startDate
    Message: La date de debut est obligatoire (xsd:date)
Constraint Violation in MinLengthConstraintComponent (http://www.w3.org/ns/shacl#MinLengthConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:message Literal("Le nom est obligatoire (min 3 caracteres)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minLength Literal("3", datatype=xsd:integer) ; sh:path schema1:name ]
    Focus Node: ex:event2
    Value Node: Literal("AI", datatype=xsd:string)
    Result Path: schema1:name
    Message: Le nom est obligatoire (min 3 caracteres)

Exemple guide 6 : ProductShape - Severites et contrainte logique

Solution proposee par @Sosolalt (EPITA-IS, promo 2028).

# Exercice 3 : ProductShape avec severites et sh:or

product_shapes_ttl = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

ex:ProductShape a sh:NodeShape ;
    sh:targetClass ex:Product ;

    # Nom : obligatoire, chaine, min 2 caracteres (Violation)
    sh:property [
        sh:path schema:name ;
        sh:minCount 1 ;
        sh:datatype xsd:string ;
        sh:minLength 2 ;
        sh:severity sh:Violation ;
        sh:message "Le nom est obligatoire (string, min 2 caracteres)"@fr ;
    ] ;

    # Prix : obligatoire, entre 0 et 100000 (Violation)
    sh:property [
        sh:path schema:price ;
        sh:minCount 1 ;
        sh:minInclusive 0 ;
        sh:maxInclusive 100000 ;
        sh:severity sh:Violation ;
        sh:message "Le prix est obligatoire et doit etre entre 0 et 100000"@fr ;
    ] ;

    # Categorie : recommandee (Warning)
    sh:property [
        sh:path schema:category ;
        sh:minCount 1 ;
        sh:severity sh:Warning ;
        sh:message "La categorie est recommandee"@fr ;
    ] ;

    # Description : suggeree, min 10 caracteres (Info)
    sh:property [
        sh:path schema:description ;
        sh:minLength 10 ;
        sh:severity sh:Info ;
        sh:message "Une description d'au moins 10 caracteres est suggeree"@fr ;
    ] ;

    # Contrainte logique : une marque OU un fabricant
    sh:or (
        [ sh:path schema:brand ; sh:minCount 1 ]
        [ sh:path schema:manufacturer ; sh:minCount 1 ]
    ) ;
    sh:message "Un produit doit avoir une marque (brand) ou un fabricant (manufacturer)"@fr .
"""

product_shapes = Graph()
product_shapes.parse(data=product_shapes_ttl, format="turtle")

# Donnees de test
product_data = Graph()
product_data.bind("ex", EX)
product_data.bind("schema", SCHEMA)

# Produit valide : nom, prix, categorie, description longue, et une marque
product_data.add((EX.prod1, RDF.type, EX.Product))
product_data.add((EX.prod1, SCHEMA.name, Literal("Clavier mecanique")))
product_data.add((EX.prod1, SCHEMA.price, Literal(89, datatype=XSD.integer)))
product_data.add((EX.prod1, SCHEMA.category, Literal("Peripheriques")))
product_data.add((EX.prod1, SCHEMA.description, Literal("Clavier mecanique retroeclaire avec switches bleus")))
product_data.add((EX.prod1, SCHEMA.brand, Literal("KeyCo")))

# Produit invalide :
# - nom trop court "X" (Violation)
# - prix hors bornes 200000 (Violation)
# - ni marque ni fabricant (Violation via sh:or)
# - pas de categorie (Warning)
# - description trop courte "court" (Info)
product_data.add((EX.prod2, RDF.type, EX.Product))
product_data.add((EX.prod2, SCHEMA.name, Literal("X")))
product_data.add((EX.prod2, SCHEMA.price, Literal(200000, datatype=XSD.integer)))
product_data.add((EX.prod2, SCHEMA.description, Literal("court")))

# Validation
conforms_prod, results_prod, text_prod = validate(
    product_data, shacl_graph=product_shapes, inference='rdfs', abort_on_first=False
)
print(f"Conforme : {conforms_prod}")
print()
print(text_prod)
Conforme : False

Validation Report
Conforms: False
Results (5):
Constraint Violation in MaxInclusiveConstraintComponent (http://www.w3.org/ns/shacl#MaxInclusiveConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:maxInclusive Literal("100000", datatype=xsd:integer) ; sh:message Literal("Le prix est obligatoire et doit etre entre 0 et 100000", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minInclusive Literal("0", datatype=xsd:integer) ; sh:path schema1:price ; sh:severity sh:Violation ]
    Focus Node: ex:prod2
    Value Node: Literal("200000", datatype=xsd:integer)
    Result Path: schema1:price
    Message: Le prix est obligatoire et doit etre entre 0 et 100000
Constraint Violation in MinLengthConstraintComponent (http://www.w3.org/ns/shacl#MinLengthConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:datatype xsd:string ; sh:message Literal("Le nom est obligatoire (string, min 2 caracteres)", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:minLength Literal("2", datatype=xsd:integer) ; sh:path schema1:name ; sh:severity sh:Violation ]
    Focus Node: ex:prod2
    Value Node: Literal("X")
    Result Path: schema1:name
    Message: Le nom est obligatoire (string, min 2 caracteres)
Constraint Violation in OrConstraintComponent (http://www.w3.org/ns/shacl#OrConstraintComponent):
    Severity: sh:Violation
    Source Shape: ex:ProductShape
    Focus Node: ex:prod2
    Value Node: ex:prod2
    Message: Un produit doit avoir une marque (brand) ou un fabricant (manufacturer)
Validation Result in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Warning
    Source Shape: [ sh:message Literal("La categorie est recommandee", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path schema1:category ; sh:severity sh:Warning ]
    Focus Node: ex:prod2
    Result Path: schema1:category
    Message: La categorie est recommandee
Validation Result in MinLengthConstraintComponent (http://www.w3.org/ns/shacl#MinLengthConstraintComponent):
    Severity: sh:Info
    Source Shape: [ sh:message Literal("Une description d'au moins 10 caracteres est suggeree", lang=fr) ; sh:minLength Literal("10", datatype=xsd:integer) ; sh:path schema1:description ; sh:severity sh:Info ]
    Focus Node: ex:prod2
    Value Node: Literal("court")
    Result Path: schema1:description
    Message: Une description d'au moins 10 caracteres est suggeree

Interprétation : que dit ce rapport de validation à sévérités multiples ?

Le rapport renvoyé par pyshacl mélange trois niveaux de sévérité sur le produit invalide ex:prod2. Cette hiérarchie est centrale pour comprendre comment SHACL sépare le blocage de la simple remarque.

1. Pourquoi conforms = False alors qu’il y a 5 résultats ? Le booléen conforms est piloté uniquement par les sh:Violation. Ici on en compte trois : le nom trop court ("X"), le prix hors bornes (200000) et l’absence simultanée de marque et de fabricant. Les sh:Warning (catégorie manquante) et sh:Info (description trop courte) sont bien présents dans le rapport mais ne font pas basculer conforms : ils informent l’utilisateur sans invalider la donnée. C’est ce qui distingue une erreur fatale d’une alerte ou d’une suggestion.

2. Le rôle de sh:or (contrainte logique OU). La troisième Violation provient d’un sh:or : la forme exige qu’un produit ait soit une schema:brand soit un schema:manufacturer. Comme prod2 n’a ni l’un ni l’autre, aucune branche du OU n’est satisfaite → Violation. Si prod2 avait eu seulement une marque, la contrainte serait passée. sh:or (comme sh:and, sh:not, sh:xone) permet d’exprimer des combinaisons logiques que des contraintes atomiques ne sauraient capturer.

3. Le design intentionnel des trois sévérités.

Sévérité Sémantique Effet sur conforms Cas typique
sh:Violation (défaut) Contrainte dure violée False Champ obligatoire absent, type incorrect
sh:Warning Anomalie suspecte inchangé Donnée facultative mais recommandée absente
sh:Info Remarque informative inchangé Convention de style non respectée

À retenir : un conforms = True ne signifie pas « zéro résultat dans le rapport » — il signifie « zéro sh:Violation ». Pour un audit complet, il faut donc lire le rapport entier (Violations + Warnings + Infos), pas seulement le booléen. Inversement, un pipeline automatisé qui rejette les données peut se contenter de conforms : les Warning/Info sont destinés à un humain, pas à un gate automatique.

Exemple guide 7 : BibliographicRecordShape - Dataset bibliothecaire

Solution proposee par @Sosolalt (EPITA-IS, promo 2028).

# Exercice final : Valider un dataset bibliotheque avec SHACL
from rdflib import Graph, Namespace, Literal
from rdflib.namespace import DC, RDF, XSD
from pyshacl import validate

SH = Namespace("http://www.w3.org/ns/shacl#")
EX = Namespace("http://example.org/")

# Forme SHACL pour une notice bibliographique
biblio_shapes_ttl = r"""
@prefix sh:  <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex:  <http://example.org/> .
@prefix dc:  <http://purl.org/dc/elements/1.1/> .

ex:BibliographicRecordShape a sh:NodeShape ;
    sh:targetClass ex:BibliographicRecord ;

    # Titre : exactement 1, type string
    sh:property [
        sh:path dc:title ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:datatype xsd:string ;
        sh:message "Une notice doit avoir exactement un titre (string)"@fr ;
    ] ;

    # Createur : au moins 1
    sh:property [
        sh:path dc:creator ;
        sh:minCount 1 ;
        sh:message "Une notice doit avoir au moins un createur"@fr ;
    ] ;

    # Date : motif YYYY-MM-DD
    sh:property [
        sh:path dc:date ;
        sh:pattern "^\\d{4}-\\d{2}-\\d{2}$" ;
        sh:message "La date doit etre au format YYYY-MM-DD"@fr ;
    ] ;

    # Type : parmi Book, Article, Thesis
    sh:property [
        sh:path dc:type ;
        sh:in ( "Book" "Article" "Thesis" ) ;
        sh:message "Le type doit etre Book, Article ou Thesis"@fr ;
    ] ;

    # Langue : facultative (au plus une valeur)
    sh:property [
        sh:path dc:language ;
        sh:maxCount 1 ;
        sh:message "Une seule langue au maximum"@fr ;
    ] .
"""

biblio_shapes = Graph()
biblio_shapes.parse(data=biblio_shapes_ttl, format="turtle")

# Donnees de test (2 valides, 1 invalide)
biblio_data = Graph()
biblio_data.bind("ex", EX)
biblio_data.bind("dc", DC)

# Notice valide 1 (complete)
biblio_data.add((EX.rec1, RDF.type, EX.BibliographicRecord))
biblio_data.add((EX.rec1, DC.title, Literal("Le Web Semantique", datatype=XSD.string)))
biblio_data.add((EX.rec1, DC.creator, Literal("Jean Dupont")))
biblio_data.add((EX.rec1, DC.date, Literal("2021-05-12")))
biblio_data.add((EX.rec1, DC.type, Literal("Book")))
biblio_data.add((EX.rec1, DC.language, Literal("fr")))

# Notice valide 2 (plusieurs createurs, sans langue -> facultative)
biblio_data.add((EX.rec2, RDF.type, EX.BibliographicRecord))
biblio_data.add((EX.rec2, DC.title, Literal("Introduction a RDF", datatype=XSD.string)))
biblio_data.add((EX.rec2, DC.creator, Literal("Alice Martin")))
biblio_data.add((EX.rec2, DC.creator, Literal("Bob Durand")))
biblio_data.add((EX.rec2, DC.date, Literal("2019-11-03")))
biblio_data.add((EX.rec2, DC.type, Literal("Article")))

# Notice invalide : pas de createur, date mal formee, type hors liste
biblio_data.add((EX.rec3, RDF.type, EX.BibliographicRecord))
biblio_data.add((EX.rec3, DC.title, Literal("Notice incomplete", datatype=XSD.string)))
biblio_data.add((EX.rec3, DC.date, Literal("12/05/2021")))
biblio_data.add((EX.rec3, DC.type, Literal("Magazine")))

# Validation
conforms_biblio, results_biblio, text_biblio = validate(
    biblio_data, shacl_graph=biblio_shapes, inference='rdfs', abort_on_first=False
)
print(f"Conforme : {conforms_biblio}")
print()
print(text_biblio)
Conforme : False

Validation Report
Conforms: False
Results (3):
Constraint Violation in InConstraintComponent (http://www.w3.org/ns/shacl#InConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:in ( Literal("Book") Literal("Article") Literal("Thesis") ) ; sh:message Literal("Le type doit etre Book, Article ou Thesis", lang=fr) ; sh:path dc:type ]
    Focus Node: ex:rec3
    Value Node: Literal("Magazine")
    Result Path: dc:type
    Message: Le type doit etre Book, Article ou Thesis
Constraint Violation in MinCountConstraintComponent (http://www.w3.org/ns/shacl#MinCountConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:message Literal("Une notice doit avoir au moins un createur", lang=fr) ; sh:minCount Literal("1", datatype=xsd:integer) ; sh:path dc:creator ]
    Focus Node: ex:rec3
    Result Path: dc:creator
    Message: Une notice doit avoir au moins un createur
Constraint Violation in PatternConstraintComponent (http://www.w3.org/ns/shacl#PatternConstraintComponent):
    Severity: sh:Violation
    Source Shape: [ sh:message Literal("La date doit etre au format YYYY-MM-DD", lang=fr) ; sh:path dc:date ; sh:pattern Literal("^\d{4}-\d{2}-\d{2}$") ]
    Focus Node: ex:rec3
    Value Node: Literal("12/05/2021")
    Result Path: dc:date
    Message: La date doit etre au format YYYY-MM-DD

Exercices a completer

Ces exercices sont a realiser par l’etudiant. Les stubs utilisent des marqueurs TODO.

Exercice 1 : Valider et corriger un graphe de films

Le fichier data/movie-data.ttl (a créer) contient des instances de schema:Movie avec des erreurs. Definissez une forme ex:MovieShape (titre obligatoire, annee entre 1900 et 2030, au moins un acteur de type schema:Person), puis créez un graphe de données avec 2 films valides et 3 films invalides. Validez et corrigez les erreurs.

# Exercice 1 : Definir MovieShape et valider des donnees de films

# TODO etudiant : creez le graphe de formes MovieShape
movie_shapes = Graph()
# TODO etudiant : ajoutez les contraintes (titre, annee, acteur)

# TODO etudiant : creez le graphe de donnees avec 2 films valides et 3 invalides
movie_data = Graph()
# TODO etudiant : ajoutez les films

# TODO etudiant : validez et affichez les resultats
print("Exercice a completer")
Exercice a completer

Exercice 2 : Forme SHACL pour un Événement

Créez une forme SHACL ex:EventShape pour valider des événements (schema:Event) avec : - Nom (schema:name) : obligatoire, chaîne, min 3 caractères - Date de debut (schema:startDate) : obligatoire, format xsd:date - Lieu (schema:location) : obligatoire, doit etre un schema:Place - Nombre max de participants (schema:maximumAttendeeCapacity) : optionnel, entier entre 1 et 100000

Créez 3 événements de test (1 valide, 2 invalides) et validez.

# Exercice 2 : Construction de EventShape

# TODO etudiant : creez le graphe de formes
event_shapes = Graph()
# TODO etudiant : definissez EventShape avec les 4 contraintes

# TODO etudiant : creez les donnees de test (1 valide, 2 invalides)
event_data = Graph()

# TODO etudiant : validez et affichez les resultats
print("Exercice a completer")
Exercice a completer

Exercice 3 : Produit avec severites et contrainte logique

Créez une forme ex:ProductShape pour valider des produits (ex:Product) avec :

Propriete Contrainte Severite
schema:name Obligatoire, chaîne, min 2 caractères sh:Violation
schema:price Obligatoire, entre 0 et 100000 sh:Violation
schema:category Obligatoire sh:Warning
schema:description Recommande (minLength 10) sh:Info

Ajoutez aussi une contrainte sh:or : le produit doit avoir soit un schema:brand soit un schema:manufacturer.

# Exercice 3 : ProductShape avec severites et sh:or

# TODO etudiant : creez la forme en Turtle ou par programmation
product_shapes_ttl = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

# TODO etudiant : completez ProductShape
"""

# TODO etudiant : parsez, creez des donnees de test, et validez
print("Exercice a completer")
Exercice a completer

Exercice 4 : Valider un dataset bibliothecaire avec SHACL

Créez une forme SHACL ex:BibliographicRecordShape qui valide des notices bibliographiques. Chaque notice doit avoir : - dc:title (exactement 1, type string) - dc:creator (au moins 1) - dc:date (pattern YYYY-MM-DD) - dc:type parmi {“Book”, “Article”, “Thesis”} - dc:language (facultatif, default “fr”)

Créez ensuite 3 notices de test (2 valides, 1 invalide) et verifiez la conformite.

print("Exercice a completer")
# Exercice final : Valider un dataset bibliotheque avec SHACL
from rdflib import Graph, Namespace, Literal
from rdflib.namespace import DC, RDF, XSD
from pyshacl import validate

# TODO etudiant : Creez la forme SHACL et les donnees de test
Exercice a completer

Resume

Tableau recapitulatif

Concept Ce que nous avons appris
SHACL Recommandation W3C pour la validation de données RDF (monde ferme)
NodeShape Forme qui cible des noeuds RDF (par classe ou par URI)
PropertyShape Contrainte sur une propriete spécifique (cardinalite, type, motif, etc.)
pySHACL validate(data_graph, shacl_graph=..., inference=..., abort_on_first=...)
Rapport Triplet (conforms, results_graph, results_text)
Severites sh:Violation (bloquant), sh:Warning (avertissement), sh:Info (suggestion)
Opérateurs logiques sh:or, sh:and, sh:not, sh:xone
Construction Shapes créées par programmation avec rdflib (BNode, add)

Section 8 : Exemples et Exercices

Type Titre Contenu
Exemple guide 1 Corriger les erreurs de person-data.ttl Correction des 5 violations par programmation
Exemple guide 2 Forme SHACL pour un Livre BookShape avec titre, ISBN, annee, auteur
Exemple guide 3 Contraintes avec severites multiples StudentShape avec Violation/Warning/Info
Exercice 1 Valider et corriger un graphe de films MovieShape + données a créer
Exercice 2 Forme SHACL pour un Événement EventShape avec contraintes
Exercice 3 Produit avec severites et contrainte logique ProductShape + sh:or
Exercice 4 Valider un dataset bibliothecaire BibliographicRecordShape

Contraintes SHACL – Reference rapide

Catégorie Propriete SHACL Exemple
Cardinalite sh:minCount, sh:maxCount sh:minCount 1
Type sh:datatype, sh:class, sh:nodeKind sh:datatype xsd:string
Chaîne sh:pattern, sh:minLength, sh:maxLength sh:pattern "^[A-Z]"
Numérique sh:minInclusive, sh:maxInclusive sh:minInclusive 0
Valeur sh:hasValue, sh:in sh:in ("FR" "DE")
Logique sh:or, sh:and, sh:not, sh:xone Liste de sous-formes

Ressources supplementaires


Navigation : << 7b-OWL | Index | 9-JSONLD >>

Conclusion

Ce notebook a permis d’explorer les aspects essentiels de sw 8 python shacl. Les points cles :

  • Les concepts fondamentaux ont ete presentes et illustres
  • Les Exercices proposent une mise en pratique progressive
  • Les résultats obtenus permettent de valider la comprehension

Pour aller plus loin : approfondir les aspects avances du sujet et explorer les liens avec d’autres domaines.

Retour au sommet