Source code for rdflib.extras.shacl
"""
Utilities for interacting with SHACL Shapes Graphs more easily.
"""
from __future__ import annotations
from typing import Optional, Union
from rdflib import Graph, Literal, URIRef, paths
from rdflib.namespace import RDF, SH
from rdflib.paths import Path
from rdflib.term import Node
[docs]class SHACLPathError(Exception):
pass
# This implementation is roughly based on
# pyshacl.helper.sparql_query_helper::SPARQLQueryHelper._shacl_path_to_sparql_path
[docs]def parse_shacl_path(
shapes_graph: Graph,
path_identifier: Node,
) -> Union[URIRef, Path]:
"""
Parse a valid SHACL path (e.g. the object of a triple with predicate sh:path)
from a :class:`~rdflib.graph.Graph` as a :class:`~rdflib.term.URIRef` if the path
is simply a predicate or a :class:`~rdflib.paths.Path` otherwise.
:param shapes_graph: A :class:`~rdflib.graph.Graph` containing the path to be parsed
:param path_identifier: A :class:`~rdflib.term.Node` of the path
:return: A :class:`~rdflib.term.URIRef` or a :class:`~rdflib.paths.Path`
"""
path: Optional[Union[URIRef, Path]] = None
# Literals are not allowed.
if isinstance(path_identifier, Literal):
raise TypeError("Literals are not a valid SHACL path.")
# If a path is a URI, that's the whole path.
elif isinstance(path_identifier, URIRef):
if path_identifier == RDF.nil:
raise SHACLPathError(
"A list of SHACL Paths must contain at least two path items."
)
path = path_identifier
# Handle Sequence Paths
elif shapes_graph.value(path_identifier, RDF.first) is not None:
sequence = list(shapes_graph.items(path_identifier))
if len(sequence) < 2:
raise SHACLPathError(
"A list of SHACL Sequence Paths must contain at least two path items."
)
path = paths.SequencePath(
*(parse_shacl_path(shapes_graph, path) for path in sequence)
)
# Handle sh:inversePath
elif inverse_path := shapes_graph.value(path_identifier, SH.inversePath):
path = paths.InvPath(parse_shacl_path(shapes_graph, inverse_path))
# Handle sh:alternativePath
elif alternative_path := shapes_graph.value(path_identifier, SH.alternativePath):
alternatives = list(shapes_graph.items(alternative_path))
if len(alternatives) < 2:
raise SHACLPathError(
"List of SHACL alternate paths must have at least two path items."
)
path = paths.AlternativePath(
*(
parse_shacl_path(shapes_graph, alternative)
for alternative in alternatives
)
)
# Handle sh:zeroOrMorePath
elif zero_or_more_path := shapes_graph.value(path_identifier, SH.zeroOrMorePath):
path = paths.MulPath(parse_shacl_path(shapes_graph, zero_or_more_path), "*")
# Handle sh:oneOrMorePath
elif one_or_more_path := shapes_graph.value(path_identifier, SH.oneOrMorePath):
path = paths.MulPath(parse_shacl_path(shapes_graph, one_or_more_path), "+")
# Handle sh:zeroOrOnePath
elif zero_or_one_path := shapes_graph.value(path_identifier, SH.zeroOrOnePath):
path = paths.MulPath(parse_shacl_path(shapes_graph, zero_or_one_path), "?")
# Raise error if none of the above options were found
elif path is None:
raise SHACLPathError(f"Cannot parse {repr(path_identifier)} as a SHACL Path.")
return path