rdf-construct 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdf_construct/__init__.py +12 -0
- rdf_construct/__main__.py +0 -0
- rdf_construct/cli.py +3429 -0
- rdf_construct/core/__init__.py +33 -0
- rdf_construct/core/config.py +116 -0
- rdf_construct/core/ordering.py +219 -0
- rdf_construct/core/predicate_order.py +212 -0
- rdf_construct/core/profile.py +157 -0
- rdf_construct/core/selector.py +64 -0
- rdf_construct/core/serialiser.py +232 -0
- rdf_construct/core/utils.py +89 -0
- rdf_construct/cq/__init__.py +77 -0
- rdf_construct/cq/expectations.py +365 -0
- rdf_construct/cq/formatters/__init__.py +45 -0
- rdf_construct/cq/formatters/json.py +104 -0
- rdf_construct/cq/formatters/junit.py +104 -0
- rdf_construct/cq/formatters/text.py +146 -0
- rdf_construct/cq/loader.py +300 -0
- rdf_construct/cq/runner.py +321 -0
- rdf_construct/diff/__init__.py +59 -0
- rdf_construct/diff/change_types.py +214 -0
- rdf_construct/diff/comparator.py +338 -0
- rdf_construct/diff/filters.py +133 -0
- rdf_construct/diff/formatters/__init__.py +71 -0
- rdf_construct/diff/formatters/json.py +192 -0
- rdf_construct/diff/formatters/markdown.py +210 -0
- rdf_construct/diff/formatters/text.py +195 -0
- rdf_construct/docs/__init__.py +60 -0
- rdf_construct/docs/config.py +238 -0
- rdf_construct/docs/extractors.py +603 -0
- rdf_construct/docs/generator.py +360 -0
- rdf_construct/docs/renderers/__init__.py +7 -0
- rdf_construct/docs/renderers/html.py +803 -0
- rdf_construct/docs/renderers/json.py +390 -0
- rdf_construct/docs/renderers/markdown.py +628 -0
- rdf_construct/docs/search.py +278 -0
- rdf_construct/docs/templates/html/base.html.jinja +44 -0
- rdf_construct/docs/templates/html/class.html.jinja +152 -0
- rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
- rdf_construct/docs/templates/html/index.html.jinja +110 -0
- rdf_construct/docs/templates/html/instance.html.jinja +90 -0
- rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
- rdf_construct/docs/templates/html/property.html.jinja +124 -0
- rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
- rdf_construct/lint/__init__.py +75 -0
- rdf_construct/lint/config.py +214 -0
- rdf_construct/lint/engine.py +396 -0
- rdf_construct/lint/formatters.py +327 -0
- rdf_construct/lint/rules.py +692 -0
- rdf_construct/localise/__init__.py +114 -0
- rdf_construct/localise/config.py +508 -0
- rdf_construct/localise/extractor.py +427 -0
- rdf_construct/localise/formatters/__init__.py +36 -0
- rdf_construct/localise/formatters/markdown.py +229 -0
- rdf_construct/localise/formatters/text.py +224 -0
- rdf_construct/localise/merger.py +346 -0
- rdf_construct/localise/reporter.py +356 -0
- rdf_construct/main.py +6 -0
- rdf_construct/merge/__init__.py +165 -0
- rdf_construct/merge/config.py +354 -0
- rdf_construct/merge/conflicts.py +281 -0
- rdf_construct/merge/formatters.py +426 -0
- rdf_construct/merge/merger.py +425 -0
- rdf_construct/merge/migrator.py +339 -0
- rdf_construct/merge/rules.py +377 -0
- rdf_construct/merge/splitter.py +1102 -0
- rdf_construct/puml2rdf/__init__.py +103 -0
- rdf_construct/puml2rdf/config.py +230 -0
- rdf_construct/puml2rdf/converter.py +420 -0
- rdf_construct/puml2rdf/merger.py +200 -0
- rdf_construct/puml2rdf/model.py +202 -0
- rdf_construct/puml2rdf/parser.py +565 -0
- rdf_construct/puml2rdf/validators.py +451 -0
- rdf_construct/refactor/__init__.py +72 -0
- rdf_construct/refactor/config.py +362 -0
- rdf_construct/refactor/deprecator.py +328 -0
- rdf_construct/refactor/formatters/__init__.py +8 -0
- rdf_construct/refactor/formatters/text.py +311 -0
- rdf_construct/refactor/renamer.py +294 -0
- rdf_construct/shacl/__init__.py +56 -0
- rdf_construct/shacl/config.py +166 -0
- rdf_construct/shacl/converters.py +520 -0
- rdf_construct/shacl/generator.py +364 -0
- rdf_construct/shacl/namespaces.py +93 -0
- rdf_construct/stats/__init__.py +29 -0
- rdf_construct/stats/collector.py +178 -0
- rdf_construct/stats/comparator.py +298 -0
- rdf_construct/stats/formatters/__init__.py +83 -0
- rdf_construct/stats/formatters/json.py +38 -0
- rdf_construct/stats/formatters/markdown.py +153 -0
- rdf_construct/stats/formatters/text.py +186 -0
- rdf_construct/stats/metrics/__init__.py +26 -0
- rdf_construct/stats/metrics/basic.py +147 -0
- rdf_construct/stats/metrics/complexity.py +137 -0
- rdf_construct/stats/metrics/connectivity.py +130 -0
- rdf_construct/stats/metrics/documentation.py +128 -0
- rdf_construct/stats/metrics/hierarchy.py +207 -0
- rdf_construct/stats/metrics/properties.py +88 -0
- rdf_construct/uml/__init__.py +22 -0
- rdf_construct/uml/context.py +194 -0
- rdf_construct/uml/mapper.py +371 -0
- rdf_construct/uml/odm_renderer.py +789 -0
- rdf_construct/uml/renderer.py +684 -0
- rdf_construct/uml/uml_layout.py +393 -0
- rdf_construct/uml/uml_style.py +613 -0
- rdf_construct-0.3.0.dist-info/METADATA +496 -0
- rdf_construct-0.3.0.dist-info/RECORD +110 -0
- rdf_construct-0.3.0.dist-info/WHEEL +4 -0
- rdf_construct-0.3.0.dist-info/entry_points.txt +3 -0
- rdf_construct-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Configuration handling for SHACL shape generation."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from rdflib import URIRef
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StrictnessLevel(Enum):
|
|
13
|
+
"""Strictness levels for SHACL generation.
|
|
14
|
+
|
|
15
|
+
Controls how many OWL patterns are converted to SHACL constraints.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
MINIMAL: Only basic type constraints (sh:class, sh:datatype).
|
|
19
|
+
STANDARD: Adds cardinality, functional properties, and common patterns.
|
|
20
|
+
STRICT: Maximum constraints including sh:closed, all cardinalities.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
MINIMAL = "minimal"
|
|
24
|
+
STANDARD = "standard"
|
|
25
|
+
STRICT = "strict"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Severity(Enum):
|
|
29
|
+
"""SHACL constraint severity levels.
|
|
30
|
+
|
|
31
|
+
Maps to sh:Violation, sh:Warning, sh:Info.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
VIOLATION = "violation"
|
|
35
|
+
WARNING = "warning"
|
|
36
|
+
INFO = "info"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ShaclConfig:
|
|
41
|
+
"""Configuration for SHACL shape generation.
|
|
42
|
+
|
|
43
|
+
Controls which OWL patterns are converted and how shapes are structured.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
level: Strictness level for conversion.
|
|
47
|
+
default_severity: Default severity for generated constraints.
|
|
48
|
+
closed: Generate closed shapes (no extra properties allowed).
|
|
49
|
+
shape_namespace: Namespace suffix for generated shapes.
|
|
50
|
+
target_classes: Optional list of classes to generate shapes for.
|
|
51
|
+
exclude_classes: Classes to skip during generation.
|
|
52
|
+
include_labels: Include rdfs:label as sh:name.
|
|
53
|
+
include_descriptions: Include rdfs:comment as sh:description.
|
|
54
|
+
inherit_constraints: Inherit property constraints from superclasses.
|
|
55
|
+
generate_property_shapes: Generate top-level PropertyShapes.
|
|
56
|
+
ignored_properties: Properties to ignore in closed shapes.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
level: StrictnessLevel = StrictnessLevel.STANDARD
|
|
60
|
+
default_severity: Severity = Severity.VIOLATION
|
|
61
|
+
closed: bool = False
|
|
62
|
+
shape_namespace: str = "shapes#"
|
|
63
|
+
target_classes: list[str] = field(default_factory=list)
|
|
64
|
+
exclude_classes: list[str] = field(default_factory=list)
|
|
65
|
+
include_labels: bool = True
|
|
66
|
+
include_descriptions: bool = True
|
|
67
|
+
inherit_constraints: bool = True
|
|
68
|
+
generate_property_shapes: bool = False
|
|
69
|
+
ignored_properties: list[str] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_yaml(cls, path: Path) -> "ShaclConfig":
|
|
73
|
+
"""Load configuration from YAML file.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
path: Path to YAML configuration file.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Populated ShaclConfig instance.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
FileNotFoundError: If configuration file doesn't exist.
|
|
83
|
+
ValueError: If configuration is invalid.
|
|
84
|
+
"""
|
|
85
|
+
with open(path) as f:
|
|
86
|
+
data = yaml.safe_load(f) or {}
|
|
87
|
+
|
|
88
|
+
return cls.from_dict(data)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_dict(cls, data: dict[str, Any]) -> "ShaclConfig":
|
|
92
|
+
"""Create configuration from dictionary.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
data: Configuration dictionary.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Populated ShaclConfig instance.
|
|
99
|
+
"""
|
|
100
|
+
# Handle enum conversions
|
|
101
|
+
level_str = data.get("level", "standard").lower()
|
|
102
|
+
try:
|
|
103
|
+
level = StrictnessLevel(level_str)
|
|
104
|
+
except ValueError:
|
|
105
|
+
valid = ", ".join(s.value for s in StrictnessLevel)
|
|
106
|
+
raise ValueError(f"Invalid strictness level '{level_str}'. Valid: {valid}")
|
|
107
|
+
|
|
108
|
+
severity_str = data.get("default_severity", "violation").lower()
|
|
109
|
+
try:
|
|
110
|
+
severity = Severity(severity_str)
|
|
111
|
+
except ValueError:
|
|
112
|
+
valid = ", ".join(s.value for s in Severity)
|
|
113
|
+
raise ValueError(f"Invalid severity '{severity_str}'. Valid: {valid}")
|
|
114
|
+
|
|
115
|
+
return cls(
|
|
116
|
+
level=level,
|
|
117
|
+
default_severity=severity,
|
|
118
|
+
closed=data.get("closed", False),
|
|
119
|
+
shape_namespace=data.get("shape_namespace", "shapes#"),
|
|
120
|
+
target_classes=data.get("target_classes", []),
|
|
121
|
+
exclude_classes=data.get("exclude_classes", []),
|
|
122
|
+
include_labels=data.get("include_labels", True),
|
|
123
|
+
include_descriptions=data.get("include_descriptions", True),
|
|
124
|
+
inherit_constraints=data.get("inherit_constraints", True),
|
|
125
|
+
generate_property_shapes=data.get("generate_property_shapes", False),
|
|
126
|
+
ignored_properties=data.get("ignored_properties", []),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def should_generate_for(self, class_uri: URIRef, graph: "Graph") -> bool:
|
|
130
|
+
"""Check if a shape should be generated for this class.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
class_uri: The class URI to check.
|
|
134
|
+
graph: The source graph (for CURIE expansion).
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
True if shape should be generated.
|
|
138
|
+
"""
|
|
139
|
+
class_str = str(class_uri)
|
|
140
|
+
|
|
141
|
+
# If explicit targets specified, only generate for those
|
|
142
|
+
if self.target_classes:
|
|
143
|
+
return any(class_str.endswith(t) or t in class_str for t in self.target_classes)
|
|
144
|
+
|
|
145
|
+
# Check exclusions
|
|
146
|
+
if self.exclude_classes:
|
|
147
|
+
return not any(
|
|
148
|
+
class_str.endswith(e) or e in class_str for e in self.exclude_classes
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def load_shacl_config(path: Path | None) -> ShaclConfig:
|
|
155
|
+
"""Load SHACL configuration from file or return defaults.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
path: Optional path to configuration file.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
ShaclConfig instance (defaults if no path provided).
|
|
162
|
+
"""
|
|
163
|
+
if path is None:
|
|
164
|
+
return ShaclConfig()
|
|
165
|
+
|
|
166
|
+
return ShaclConfig.from_yaml(path)
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
"""OWL to SHACL conversion rules.
|
|
2
|
+
|
|
3
|
+
Each converter handles a specific OWL pattern and produces equivalent SHACL
|
|
4
|
+
constraints. Converters are composable and applied by the generator.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from rdflib import BNode, Graph, Literal, Namespace, RDF, RDFS, URIRef, XSD
|
|
12
|
+
from rdflib.namespace import OWL
|
|
13
|
+
|
|
14
|
+
from .namespaces import SH
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .config import ShaclConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PropertyConstraint:
|
|
22
|
+
"""Represents a property constraint to be added to a shape.
|
|
23
|
+
|
|
24
|
+
Accumulates constraints that will become a sh:property blank node.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
path: The property path (usually the property URI).
|
|
28
|
+
node_class: Expected class for object values (sh:class).
|
|
29
|
+
datatype: Expected datatype for literal values (sh:datatype).
|
|
30
|
+
min_count: Minimum cardinality (sh:minCount).
|
|
31
|
+
max_count: Maximum cardinality (sh:maxCount).
|
|
32
|
+
node_kind: Node kind constraint (sh:nodeKind).
|
|
33
|
+
name: Human-readable name (sh:name).
|
|
34
|
+
description: Description (sh:description).
|
|
35
|
+
in_values: Enumeration of allowed values (sh:in).
|
|
36
|
+
pattern: Regex pattern for string values (sh:pattern).
|
|
37
|
+
min_inclusive: Minimum value inclusive (sh:minInclusive).
|
|
38
|
+
max_inclusive: Maximum value inclusive (sh:maxInclusive).
|
|
39
|
+
order: Property display order (sh:order).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
path: URIRef
|
|
43
|
+
node_class: URIRef | None = None
|
|
44
|
+
datatype: URIRef | None = None
|
|
45
|
+
min_count: int | None = None
|
|
46
|
+
max_count: int | None = None
|
|
47
|
+
node_kind: URIRef | None = None
|
|
48
|
+
name: str | None = None
|
|
49
|
+
description: str | None = None
|
|
50
|
+
in_values: list[URIRef | Literal] = field(default_factory=list)
|
|
51
|
+
pattern: str | None = None
|
|
52
|
+
min_inclusive: Literal | None = None
|
|
53
|
+
max_inclusive: Literal | None = None
|
|
54
|
+
order: int | None = None
|
|
55
|
+
|
|
56
|
+
def merge(self, other: "PropertyConstraint") -> "PropertyConstraint":
|
|
57
|
+
"""Merge another constraint into this one.
|
|
58
|
+
|
|
59
|
+
Values from other take precedence for single-value fields.
|
|
60
|
+
Lists are combined.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
other: Constraint to merge from.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
New merged PropertyConstraint.
|
|
67
|
+
"""
|
|
68
|
+
return PropertyConstraint(
|
|
69
|
+
path=self.path,
|
|
70
|
+
node_class=other.node_class or self.node_class,
|
|
71
|
+
datatype=other.datatype or self.datatype,
|
|
72
|
+
min_count=max(
|
|
73
|
+
filter(None, [self.min_count, other.min_count]), default=None
|
|
74
|
+
),
|
|
75
|
+
max_count=min(
|
|
76
|
+
filter(None, [self.max_count, other.max_count]), default=None
|
|
77
|
+
) if self.max_count is not None or other.max_count is not None else None,
|
|
78
|
+
node_kind=other.node_kind or self.node_kind,
|
|
79
|
+
name=other.name or self.name,
|
|
80
|
+
description=other.description or self.description,
|
|
81
|
+
in_values=list(set(self.in_values + other.in_values)),
|
|
82
|
+
pattern=other.pattern or self.pattern,
|
|
83
|
+
min_inclusive=other.min_inclusive or self.min_inclusive,
|
|
84
|
+
max_inclusive=other.max_inclusive or self.max_inclusive,
|
|
85
|
+
order=other.order or self.order,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def to_rdf(self, shapes_graph: Graph) -> BNode:
|
|
89
|
+
"""Convert constraint to RDF representation.
|
|
90
|
+
|
|
91
|
+
Creates a blank node with sh:property predicates.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
shapes_graph: Graph to add triples to.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Blank node representing the property shape.
|
|
98
|
+
"""
|
|
99
|
+
prop_shape = BNode()
|
|
100
|
+
|
|
101
|
+
shapes_graph.add((prop_shape, SH.path, self.path))
|
|
102
|
+
|
|
103
|
+
if self.node_class:
|
|
104
|
+
shapes_graph.add((prop_shape, SH["class"], self.node_class))
|
|
105
|
+
|
|
106
|
+
if self.datatype:
|
|
107
|
+
shapes_graph.add((prop_shape, SH.datatype, self.datatype))
|
|
108
|
+
|
|
109
|
+
if self.min_count is not None:
|
|
110
|
+
shapes_graph.add((prop_shape, SH.minCount, Literal(self.min_count)))
|
|
111
|
+
|
|
112
|
+
if self.max_count is not None:
|
|
113
|
+
shapes_graph.add((prop_shape, SH.maxCount, Literal(self.max_count)))
|
|
114
|
+
|
|
115
|
+
if self.node_kind:
|
|
116
|
+
shapes_graph.add((prop_shape, SH.nodeKind, self.node_kind))
|
|
117
|
+
|
|
118
|
+
if self.name:
|
|
119
|
+
shapes_graph.add((prop_shape, SH.name, Literal(self.name)))
|
|
120
|
+
|
|
121
|
+
if self.description:
|
|
122
|
+
shapes_graph.add((prop_shape, SH.description, Literal(self.description)))
|
|
123
|
+
|
|
124
|
+
if self.in_values:
|
|
125
|
+
# Create an RDF list for sh:in
|
|
126
|
+
in_list = _create_rdf_list(shapes_graph, self.in_values)
|
|
127
|
+
shapes_graph.add((prop_shape, SH["in"], in_list))
|
|
128
|
+
|
|
129
|
+
if self.pattern:
|
|
130
|
+
shapes_graph.add((prop_shape, SH.pattern, Literal(self.pattern)))
|
|
131
|
+
|
|
132
|
+
if self.min_inclusive is not None:
|
|
133
|
+
shapes_graph.add((prop_shape, SH.minInclusive, self.min_inclusive))
|
|
134
|
+
|
|
135
|
+
if self.max_inclusive is not None:
|
|
136
|
+
shapes_graph.add((prop_shape, SH.maxInclusive, self.max_inclusive))
|
|
137
|
+
|
|
138
|
+
if self.order is not None:
|
|
139
|
+
shapes_graph.add((prop_shape, SH.order, Literal(self.order)))
|
|
140
|
+
|
|
141
|
+
return prop_shape
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _create_rdf_list(graph: Graph, items: list) -> BNode:
|
|
145
|
+
"""Create an RDF list from Python list.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
graph: Graph to add list triples to.
|
|
149
|
+
items: Items to include in list.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Head node of the RDF list.
|
|
153
|
+
"""
|
|
154
|
+
if not items:
|
|
155
|
+
return RDF.nil
|
|
156
|
+
|
|
157
|
+
head = BNode()
|
|
158
|
+
current = head
|
|
159
|
+
|
|
160
|
+
for i, item in enumerate(items):
|
|
161
|
+
graph.add((current, RDF.first, item))
|
|
162
|
+
|
|
163
|
+
if i < len(items) - 1:
|
|
164
|
+
next_node = BNode()
|
|
165
|
+
graph.add((current, RDF.rest, next_node))
|
|
166
|
+
current = next_node
|
|
167
|
+
else:
|
|
168
|
+
graph.add((current, RDF.rest, RDF.nil))
|
|
169
|
+
|
|
170
|
+
return head
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Converter(ABC):
|
|
174
|
+
"""Base class for OWL to SHACL converters.
|
|
175
|
+
|
|
176
|
+
Each converter handles a specific OWL pattern and produces
|
|
177
|
+
property constraints or modifies the shape graph directly.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
@abstractmethod
|
|
181
|
+
def convert_for_class(
|
|
182
|
+
self,
|
|
183
|
+
cls: URIRef,
|
|
184
|
+
source_graph: Graph,
|
|
185
|
+
config: "ShaclConfig",
|
|
186
|
+
) -> list[PropertyConstraint]:
|
|
187
|
+
"""Extract constraints for a given class.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
cls: The class to extract constraints for.
|
|
191
|
+
source_graph: The OWL ontology graph.
|
|
192
|
+
config: Generation configuration.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
List of property constraints for this class.
|
|
196
|
+
"""
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class DomainRangeConverter(Converter):
|
|
201
|
+
"""Convert rdfs:domain/range to sh:property with sh:class/sh:datatype.
|
|
202
|
+
|
|
203
|
+
For each property with rdfs:domain pointing to this class, creates
|
|
204
|
+
a property constraint. The rdfs:range determines whether sh:class
|
|
205
|
+
or sh:datatype is used.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
# Common XSD datatypes
|
|
209
|
+
XSD_DATATYPES = {
|
|
210
|
+
XSD.string, XSD.integer, XSD.int, XSD.long, XSD.short, XSD.byte,
|
|
211
|
+
XSD.decimal, XSD.float, XSD.double, XSD.boolean, XSD.date,
|
|
212
|
+
XSD.dateTime, XSD.time, XSD.duration, XSD.gYear, XSD.gMonth,
|
|
213
|
+
XSD.gDay, XSD.gYearMonth, XSD.gMonthDay, XSD.anyURI, XSD.base64Binary,
|
|
214
|
+
XSD.hexBinary, XSD.normalizedString, XSD.token, XSD.language,
|
|
215
|
+
XSD.nonPositiveInteger, XSD.negativeInteger, XSD.nonNegativeInteger,
|
|
216
|
+
XSD.positiveInteger, XSD.unsignedLong, XSD.unsignedInt,
|
|
217
|
+
XSD.unsignedShort, XSD.unsignedByte,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def convert_for_class(
|
|
221
|
+
self,
|
|
222
|
+
cls: URIRef,
|
|
223
|
+
source_graph: Graph,
|
|
224
|
+
config: "ShaclConfig",
|
|
225
|
+
) -> list[PropertyConstraint]:
|
|
226
|
+
"""Find properties with domain of this class and create constraints."""
|
|
227
|
+
constraints: list[PropertyConstraint] = []
|
|
228
|
+
|
|
229
|
+
# Find all properties with this class as domain
|
|
230
|
+
for prop in source_graph.subjects(RDFS.domain, cls):
|
|
231
|
+
if not isinstance(prop, URIRef):
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
constraint = PropertyConstraint(path=prop)
|
|
235
|
+
|
|
236
|
+
# Get range if defined
|
|
237
|
+
range_value = source_graph.value(prop, RDFS.range)
|
|
238
|
+
if range_value:
|
|
239
|
+
if self._is_datatype(range_value, source_graph):
|
|
240
|
+
constraint.datatype = range_value
|
|
241
|
+
else:
|
|
242
|
+
constraint.node_class = range_value
|
|
243
|
+
|
|
244
|
+
# Add label as name if configured
|
|
245
|
+
if config.include_labels:
|
|
246
|
+
label = source_graph.value(prop, RDFS.label)
|
|
247
|
+
if label:
|
|
248
|
+
constraint.name = str(label)
|
|
249
|
+
|
|
250
|
+
# Add comment as description if configured
|
|
251
|
+
if config.include_descriptions:
|
|
252
|
+
comment = source_graph.value(prop, RDFS.comment)
|
|
253
|
+
if comment:
|
|
254
|
+
constraint.description = str(comment)
|
|
255
|
+
|
|
256
|
+
constraints.append(constraint)
|
|
257
|
+
|
|
258
|
+
return constraints
|
|
259
|
+
|
|
260
|
+
def _is_datatype(self, uri: URIRef, graph: Graph) -> bool:
|
|
261
|
+
"""Check if URI represents a datatype."""
|
|
262
|
+
if uri in self.XSD_DATATYPES:
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
# Check if it's declared as rdfs:Datatype
|
|
266
|
+
if (uri, RDF.type, RDFS.Datatype) in graph:
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
# Check namespace - XSD URIs are datatypes
|
|
270
|
+
return str(uri).startswith(str(XSD))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class CardinalityConverter(Converter):
|
|
274
|
+
"""Convert OWL cardinality restrictions to sh:minCount/sh:maxCount.
|
|
275
|
+
|
|
276
|
+
Handles:
|
|
277
|
+
- owl:cardinality → sh:minCount + sh:maxCount
|
|
278
|
+
- owl:minCardinality → sh:minCount
|
|
279
|
+
- owl:maxCardinality → sh:maxCount
|
|
280
|
+
- owl:qualifiedCardinality (with owl:onClass/owl:onDataRange)
|
|
281
|
+
- owl:someValuesFrom → sh:minCount 1
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
def convert_for_class(
|
|
285
|
+
self,
|
|
286
|
+
cls: URIRef,
|
|
287
|
+
source_graph: Graph,
|
|
288
|
+
config: "ShaclConfig",
|
|
289
|
+
) -> list[PropertyConstraint]:
|
|
290
|
+
"""Extract cardinality restrictions from class definition."""
|
|
291
|
+
constraints: list[PropertyConstraint] = []
|
|
292
|
+
|
|
293
|
+
# Find restrictions that this class is a subclass of
|
|
294
|
+
for superclass in source_graph.objects(cls, RDFS.subClassOf):
|
|
295
|
+
if not isinstance(superclass, BNode):
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Check if it's an owl:Restriction
|
|
299
|
+
if (superclass, RDF.type, OWL.Restriction) not in source_graph:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
on_prop = source_graph.value(superclass, OWL.onProperty)
|
|
303
|
+
if not isinstance(on_prop, URIRef):
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
constraint = PropertyConstraint(path=on_prop)
|
|
307
|
+
has_constraint = False
|
|
308
|
+
|
|
309
|
+
# Exact cardinality
|
|
310
|
+
exact = source_graph.value(superclass, OWL.cardinality)
|
|
311
|
+
if exact:
|
|
312
|
+
constraint.min_count = int(exact)
|
|
313
|
+
constraint.max_count = int(exact)
|
|
314
|
+
has_constraint = True
|
|
315
|
+
|
|
316
|
+
# Minimum cardinality
|
|
317
|
+
min_card = source_graph.value(superclass, OWL.minCardinality)
|
|
318
|
+
if min_card:
|
|
319
|
+
constraint.min_count = int(min_card)
|
|
320
|
+
has_constraint = True
|
|
321
|
+
|
|
322
|
+
# Maximum cardinality
|
|
323
|
+
max_card = source_graph.value(superclass, OWL.maxCardinality)
|
|
324
|
+
if max_card:
|
|
325
|
+
constraint.max_count = int(max_card)
|
|
326
|
+
has_constraint = True
|
|
327
|
+
|
|
328
|
+
# Qualified cardinality
|
|
329
|
+
qual_card = source_graph.value(superclass, OWL.qualifiedCardinality)
|
|
330
|
+
if qual_card:
|
|
331
|
+
constraint.min_count = int(qual_card)
|
|
332
|
+
constraint.max_count = int(qual_card)
|
|
333
|
+
# Also get the qualification
|
|
334
|
+
on_class = source_graph.value(superclass, OWL.onClass)
|
|
335
|
+
if on_class:
|
|
336
|
+
constraint.node_class = on_class
|
|
337
|
+
on_data = source_graph.value(superclass, OWL.onDataRange)
|
|
338
|
+
if on_data:
|
|
339
|
+
constraint.datatype = on_data
|
|
340
|
+
has_constraint = True
|
|
341
|
+
|
|
342
|
+
# Qualified min cardinality
|
|
343
|
+
qual_min = source_graph.value(superclass, OWL.minQualifiedCardinality)
|
|
344
|
+
if qual_min:
|
|
345
|
+
constraint.min_count = int(qual_min)
|
|
346
|
+
on_class = source_graph.value(superclass, OWL.onClass)
|
|
347
|
+
if on_class:
|
|
348
|
+
constraint.node_class = on_class
|
|
349
|
+
has_constraint = True
|
|
350
|
+
|
|
351
|
+
# Qualified max cardinality
|
|
352
|
+
qual_max = source_graph.value(superclass, OWL.maxQualifiedCardinality)
|
|
353
|
+
if qual_max:
|
|
354
|
+
constraint.max_count = int(qual_max)
|
|
355
|
+
on_class = source_graph.value(superclass, OWL.onClass)
|
|
356
|
+
if on_class:
|
|
357
|
+
constraint.node_class = on_class
|
|
358
|
+
has_constraint = True
|
|
359
|
+
|
|
360
|
+
# someValuesFrom implies at least one value
|
|
361
|
+
some_from = source_graph.value(superclass, OWL.someValuesFrom)
|
|
362
|
+
if some_from:
|
|
363
|
+
constraint.min_count = 1
|
|
364
|
+
if isinstance(some_from, URIRef):
|
|
365
|
+
# Could be a class or datatype
|
|
366
|
+
if self._is_datatype(some_from, source_graph):
|
|
367
|
+
constraint.datatype = some_from
|
|
368
|
+
else:
|
|
369
|
+
constraint.node_class = some_from
|
|
370
|
+
has_constraint = True
|
|
371
|
+
|
|
372
|
+
# allValuesFrom constrains the type but not cardinality
|
|
373
|
+
all_from = source_graph.value(superclass, OWL.allValuesFrom)
|
|
374
|
+
if all_from and isinstance(all_from, URIRef):
|
|
375
|
+
if self._is_datatype(all_from, source_graph):
|
|
376
|
+
constraint.datatype = all_from
|
|
377
|
+
else:
|
|
378
|
+
constraint.node_class = all_from
|
|
379
|
+
has_constraint = True
|
|
380
|
+
|
|
381
|
+
if has_constraint:
|
|
382
|
+
constraints.append(constraint)
|
|
383
|
+
|
|
384
|
+
return constraints
|
|
385
|
+
|
|
386
|
+
def _is_datatype(self, uri: URIRef, graph: Graph) -> bool:
|
|
387
|
+
"""Check if URI represents a datatype."""
|
|
388
|
+
return str(uri).startswith(str(XSD)) or (uri, RDF.type, RDFS.Datatype) in graph
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class FunctionalPropertyConverter(Converter):
|
|
392
|
+
"""Convert owl:FunctionalProperty to sh:maxCount 1.
|
|
393
|
+
|
|
394
|
+
Also handles owl:InverseFunctionalProperty (adds maxCount 1 for the property).
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def convert_for_class(
|
|
398
|
+
self,
|
|
399
|
+
cls: URIRef,
|
|
400
|
+
source_graph: Graph,
|
|
401
|
+
config: "ShaclConfig",
|
|
402
|
+
) -> list[PropertyConstraint]:
|
|
403
|
+
"""Find functional properties with domain of this class."""
|
|
404
|
+
constraints: list[PropertyConstraint] = []
|
|
405
|
+
|
|
406
|
+
# Get all functional properties
|
|
407
|
+
functional_props = set(source_graph.subjects(RDF.type, OWL.FunctionalProperty))
|
|
408
|
+
|
|
409
|
+
# Find properties with this class as domain
|
|
410
|
+
for prop in source_graph.subjects(RDFS.domain, cls):
|
|
411
|
+
if prop in functional_props and isinstance(prop, URIRef):
|
|
412
|
+
constraints.append(PropertyConstraint(path=prop, max_count=1))
|
|
413
|
+
|
|
414
|
+
return constraints
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class EnumerationConverter(Converter):
|
|
418
|
+
"""Convert owl:oneOf to sh:in constraint.
|
|
419
|
+
|
|
420
|
+
When a property's range is a class defined with owl:oneOf,
|
|
421
|
+
creates a sh:in constraint with the enumerated values.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
def convert_for_class(
|
|
425
|
+
self,
|
|
426
|
+
cls: URIRef,
|
|
427
|
+
source_graph: Graph,
|
|
428
|
+
config: "ShaclConfig",
|
|
429
|
+
) -> list[PropertyConstraint]:
|
|
430
|
+
"""Find properties with enumerated ranges."""
|
|
431
|
+
constraints: list[PropertyConstraint] = []
|
|
432
|
+
|
|
433
|
+
# Find properties with domain of this class
|
|
434
|
+
for prop in source_graph.subjects(RDFS.domain, cls):
|
|
435
|
+
if not isinstance(prop, URIRef):
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
range_value = source_graph.value(prop, RDFS.range)
|
|
439
|
+
if not range_value:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
# Check if range has owl:oneOf
|
|
443
|
+
one_of = source_graph.value(range_value, OWL.oneOf)
|
|
444
|
+
if one_of:
|
|
445
|
+
values = self._extract_list(source_graph, one_of)
|
|
446
|
+
if values:
|
|
447
|
+
constraints.append(PropertyConstraint(path=prop, in_values=values))
|
|
448
|
+
|
|
449
|
+
return constraints
|
|
450
|
+
|
|
451
|
+
def _extract_list(self, graph: Graph, head: BNode | URIRef) -> list:
|
|
452
|
+
"""Extract values from RDF list."""
|
|
453
|
+
values = []
|
|
454
|
+
current = head
|
|
455
|
+
|
|
456
|
+
while current and current != RDF.nil:
|
|
457
|
+
first = graph.value(current, RDF.first)
|
|
458
|
+
if first:
|
|
459
|
+
values.append(first)
|
|
460
|
+
current = graph.value(current, RDF.rest)
|
|
461
|
+
|
|
462
|
+
return values
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class SymmetricPropertyConverter(Converter):
|
|
466
|
+
"""Handle symmetric properties.
|
|
467
|
+
|
|
468
|
+
For symmetric properties, if domain is defined, the property
|
|
469
|
+
should also be valid in the reverse direction.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
def convert_for_class(
|
|
473
|
+
self,
|
|
474
|
+
cls: URIRef,
|
|
475
|
+
source_graph: Graph,
|
|
476
|
+
config: "ShaclConfig",
|
|
477
|
+
) -> list[PropertyConstraint]:
|
|
478
|
+
"""Handle symmetric properties - no additional SHACL needed."""
|
|
479
|
+
# Symmetric properties don't need special SHACL handling
|
|
480
|
+
# beyond what domain/range provides
|
|
481
|
+
return []
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# All converters in order of application
|
|
485
|
+
ALL_CONVERTERS: list[type[Converter]] = [
|
|
486
|
+
DomainRangeConverter,
|
|
487
|
+
CardinalityConverter,
|
|
488
|
+
FunctionalPropertyConverter,
|
|
489
|
+
EnumerationConverter,
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def get_converters_for_level(level: "StrictnessLevel") -> list[Converter]:
|
|
494
|
+
"""Get converter instances appropriate for strictness level.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
level: The strictness level.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
List of instantiated converters.
|
|
501
|
+
"""
|
|
502
|
+
from .config import StrictnessLevel
|
|
503
|
+
|
|
504
|
+
if level == StrictnessLevel.MINIMAL:
|
|
505
|
+
return [DomainRangeConverter()]
|
|
506
|
+
|
|
507
|
+
elif level == StrictnessLevel.STANDARD:
|
|
508
|
+
return [
|
|
509
|
+
DomainRangeConverter(),
|
|
510
|
+
CardinalityConverter(),
|
|
511
|
+
FunctionalPropertyConverter(),
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
else: # STRICT
|
|
515
|
+
return [
|
|
516
|
+
DomainRangeConverter(),
|
|
517
|
+
CardinalityConverter(),
|
|
518
|
+
FunctionalPropertyConverter(),
|
|
519
|
+
EnumerationConverter(),
|
|
520
|
+
]
|