rdf-construct 0.2.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.
Files changed (88) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +1762 -0
  4. rdf_construct/core/__init__.py +33 -0
  5. rdf_construct/core/config.py +116 -0
  6. rdf_construct/core/ordering.py +219 -0
  7. rdf_construct/core/predicate_order.py +212 -0
  8. rdf_construct/core/profile.py +157 -0
  9. rdf_construct/core/selector.py +64 -0
  10. rdf_construct/core/serialiser.py +232 -0
  11. rdf_construct/core/utils.py +89 -0
  12. rdf_construct/cq/__init__.py +77 -0
  13. rdf_construct/cq/expectations.py +365 -0
  14. rdf_construct/cq/formatters/__init__.py +45 -0
  15. rdf_construct/cq/formatters/json.py +104 -0
  16. rdf_construct/cq/formatters/junit.py +104 -0
  17. rdf_construct/cq/formatters/text.py +146 -0
  18. rdf_construct/cq/loader.py +300 -0
  19. rdf_construct/cq/runner.py +321 -0
  20. rdf_construct/diff/__init__.py +59 -0
  21. rdf_construct/diff/change_types.py +214 -0
  22. rdf_construct/diff/comparator.py +338 -0
  23. rdf_construct/diff/filters.py +133 -0
  24. rdf_construct/diff/formatters/__init__.py +71 -0
  25. rdf_construct/diff/formatters/json.py +192 -0
  26. rdf_construct/diff/formatters/markdown.py +210 -0
  27. rdf_construct/diff/formatters/text.py +195 -0
  28. rdf_construct/docs/__init__.py +60 -0
  29. rdf_construct/docs/config.py +238 -0
  30. rdf_construct/docs/extractors.py +603 -0
  31. rdf_construct/docs/generator.py +360 -0
  32. rdf_construct/docs/renderers/__init__.py +7 -0
  33. rdf_construct/docs/renderers/html.py +803 -0
  34. rdf_construct/docs/renderers/json.py +390 -0
  35. rdf_construct/docs/renderers/markdown.py +628 -0
  36. rdf_construct/docs/search.py +278 -0
  37. rdf_construct/docs/templates/html/base.html.jinja +44 -0
  38. rdf_construct/docs/templates/html/class.html.jinja +152 -0
  39. rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
  40. rdf_construct/docs/templates/html/index.html.jinja +110 -0
  41. rdf_construct/docs/templates/html/instance.html.jinja +90 -0
  42. rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
  43. rdf_construct/docs/templates/html/property.html.jinja +124 -0
  44. rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
  45. rdf_construct/lint/__init__.py +75 -0
  46. rdf_construct/lint/config.py +214 -0
  47. rdf_construct/lint/engine.py +396 -0
  48. rdf_construct/lint/formatters.py +327 -0
  49. rdf_construct/lint/rules.py +692 -0
  50. rdf_construct/main.py +6 -0
  51. rdf_construct/puml2rdf/__init__.py +103 -0
  52. rdf_construct/puml2rdf/config.py +230 -0
  53. rdf_construct/puml2rdf/converter.py +420 -0
  54. rdf_construct/puml2rdf/merger.py +200 -0
  55. rdf_construct/puml2rdf/model.py +202 -0
  56. rdf_construct/puml2rdf/parser.py +565 -0
  57. rdf_construct/puml2rdf/validators.py +451 -0
  58. rdf_construct/shacl/__init__.py +56 -0
  59. rdf_construct/shacl/config.py +166 -0
  60. rdf_construct/shacl/converters.py +520 -0
  61. rdf_construct/shacl/generator.py +364 -0
  62. rdf_construct/shacl/namespaces.py +93 -0
  63. rdf_construct/stats/__init__.py +29 -0
  64. rdf_construct/stats/collector.py +178 -0
  65. rdf_construct/stats/comparator.py +298 -0
  66. rdf_construct/stats/formatters/__init__.py +83 -0
  67. rdf_construct/stats/formatters/json.py +38 -0
  68. rdf_construct/stats/formatters/markdown.py +153 -0
  69. rdf_construct/stats/formatters/text.py +186 -0
  70. rdf_construct/stats/metrics/__init__.py +26 -0
  71. rdf_construct/stats/metrics/basic.py +147 -0
  72. rdf_construct/stats/metrics/complexity.py +137 -0
  73. rdf_construct/stats/metrics/connectivity.py +130 -0
  74. rdf_construct/stats/metrics/documentation.py +128 -0
  75. rdf_construct/stats/metrics/hierarchy.py +207 -0
  76. rdf_construct/stats/metrics/properties.py +88 -0
  77. rdf_construct/uml/__init__.py +22 -0
  78. rdf_construct/uml/context.py +194 -0
  79. rdf_construct/uml/mapper.py +371 -0
  80. rdf_construct/uml/odm_renderer.py +789 -0
  81. rdf_construct/uml/renderer.py +684 -0
  82. rdf_construct/uml/uml_layout.py +393 -0
  83. rdf_construct/uml/uml_style.py +613 -0
  84. rdf_construct-0.2.0.dist-info/METADATA +431 -0
  85. rdf_construct-0.2.0.dist-info/RECORD +88 -0
  86. rdf_construct-0.2.0.dist-info/WHEEL +4 -0
  87. rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
  88. rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,451 @@
1
+ """Validation for PlantUML import.
2
+
3
+ This module provides validation of parsed PlantUML models and
4
+ generated RDF, ensuring consistency and flagging potential issues.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Optional
10
+
11
+ from rdflib import Graph, URIRef, RDF, RDFS
12
+ from rdflib.namespace import OWL
13
+
14
+ from rdf_construct.puml2rdf.model import PumlModel, PumlRelationship, RelationshipType
15
+
16
+
17
+ class Severity(Enum):
18
+ """Severity level for validation issues."""
19
+
20
+ ERROR = "error" # Must be fixed
21
+ WARNING = "warning" # Should be reviewed
22
+ INFO = "info" # Informational
23
+
24
+
25
+ @dataclass
26
+ class ValidationIssue:
27
+ """A validation issue found during checking.
28
+
29
+ Attributes:
30
+ severity: How serious the issue is
31
+ code: Machine-readable issue code
32
+ message: Human-readable description
33
+ entity: The entity this issue relates to
34
+ suggestion: Optional fix suggestion
35
+ """
36
+
37
+ severity: Severity
38
+ code: str
39
+ message: str
40
+ entity: Optional[str] = None
41
+ suggestion: Optional[str] = None
42
+
43
+ def __str__(self) -> str:
44
+ prefix = f"[{self.severity.value.upper()}]"
45
+ entity_str = f" ({self.entity})" if self.entity else ""
46
+ return f"{prefix} {self.code}: {self.message}{entity_str}"
47
+
48
+
49
+ @dataclass
50
+ class ValidationResult:
51
+ """Result of validation.
52
+
53
+ Attributes:
54
+ issues: List of validation issues found
55
+ """
56
+
57
+ issues: list[ValidationIssue]
58
+
59
+ @property
60
+ def has_errors(self) -> bool:
61
+ """Return True if any errors were found."""
62
+ return any(i.severity == Severity.ERROR for i in self.issues)
63
+
64
+ @property
65
+ def has_warnings(self) -> bool:
66
+ """Return True if any warnings were found."""
67
+ return any(i.severity == Severity.WARNING for i in self.issues)
68
+
69
+ @property
70
+ def error_count(self) -> int:
71
+ """Count of errors."""
72
+ return sum(1 for i in self.issues if i.severity == Severity.ERROR)
73
+
74
+ @property
75
+ def warning_count(self) -> int:
76
+ """Count of warnings."""
77
+ return sum(1 for i in self.issues if i.severity == Severity.WARNING)
78
+
79
+ def errors(self) -> list[ValidationIssue]:
80
+ """Return only error-level issues."""
81
+ return [i for i in self.issues if i.severity == Severity.ERROR]
82
+
83
+ def warnings(self) -> list[ValidationIssue]:
84
+ """Return only warning-level issues."""
85
+ return [i for i in self.issues if i.severity == Severity.WARNING]
86
+
87
+
88
+ class PumlModelValidator:
89
+ """Validates parsed PlantUML models for consistency.
90
+
91
+ Checks include:
92
+ - Relationships reference existing classes
93
+ - No duplicate class names
94
+ - Attributes have valid types
95
+ - Inheritance doesn't create cycles
96
+ """
97
+
98
+ def validate(self, model: PumlModel) -> ValidationResult:
99
+ """Validate a parsed PlantUML model.
100
+
101
+ Args:
102
+ model: The parsed model to validate
103
+
104
+ Returns:
105
+ ValidationResult with any issues found
106
+ """
107
+ issues: list[ValidationIssue] = []
108
+
109
+ # Check for duplicate class names
110
+ issues.extend(self._check_duplicate_classes(model))
111
+
112
+ # Check relationship references
113
+ issues.extend(self._check_relationship_references(model))
114
+
115
+ # Check for inheritance cycles
116
+ issues.extend(self._check_inheritance_cycles(model))
117
+
118
+ # Check attribute types
119
+ issues.extend(self._check_attribute_types(model))
120
+
121
+ # Check for classes without any relationships
122
+ issues.extend(self._check_orphan_classes(model))
123
+
124
+ return ValidationResult(issues=issues)
125
+
126
+ def _check_duplicate_classes(self, model: PumlModel) -> list[ValidationIssue]:
127
+ """Check for duplicate class names."""
128
+ issues = []
129
+ seen = set()
130
+
131
+ for cls in model.classes:
132
+ if cls.name in seen:
133
+ issues.append(
134
+ ValidationIssue(
135
+ severity=Severity.ERROR,
136
+ code="DUPLICATE_CLASS",
137
+ message=f"Duplicate class name: {cls.name}",
138
+ entity=cls.name,
139
+ suggestion="Rename one of the classes or use packages to distinguish them",
140
+ )
141
+ )
142
+ seen.add(cls.name)
143
+
144
+ return issues
145
+
146
+ def _check_relationship_references(self, model: PumlModel) -> list[ValidationIssue]:
147
+ """Check that relationships reference existing classes."""
148
+ issues = []
149
+
150
+ # Include both local and qualified names for lookup
151
+ class_names = set()
152
+ for cls in model.classes:
153
+ class_names.add(cls.name) # Local name: "Building"
154
+ class_names.add(cls.qualified_name) # Qualified: "building.Building"
155
+
156
+ for rel in model.relationships:
157
+ if rel.source not in class_names:
158
+ issues.append(
159
+ ValidationIssue(
160
+ severity=Severity.ERROR,
161
+ code="UNKNOWN_CLASS",
162
+ message=f"Relationship references unknown source class: {rel.source}",
163
+ entity=rel.source,
164
+ suggestion=f"Add class declaration for '{rel.source}'",
165
+ )
166
+ )
167
+ if rel.target not in class_names:
168
+ issues.append(
169
+ ValidationIssue(
170
+ severity=Severity.ERROR,
171
+ code="UNKNOWN_CLASS",
172
+ message=f"Relationship references unknown target class: {rel.target}",
173
+ entity=rel.target,
174
+ suggestion=f"Add class declaration for '{rel.target}'",
175
+ )
176
+ )
177
+
178
+ return issues
179
+
180
+ def _check_inheritance_cycles(self, model: PumlModel) -> list[ValidationIssue]:
181
+ """Check for cycles in inheritance hierarchy."""
182
+ issues = []
183
+
184
+ # Build inheritance graph
185
+ inheritance: dict[str, set[str]] = {}
186
+ for rel in model.inheritance_relationships():
187
+ if rel.source not in inheritance:
188
+ inheritance[rel.source] = set()
189
+ inheritance[rel.source].add(rel.target)
190
+
191
+ # Check for cycles using DFS
192
+ def has_cycle(start: str, visited: set[str], path: list[str]) -> Optional[list[str]]:
193
+ if start in path:
194
+ cycle_start = path.index(start)
195
+ return path[cycle_start:] + [start]
196
+
197
+ if start in visited:
198
+ return None
199
+
200
+ visited.add(start)
201
+ path.append(start)
202
+
203
+ for parent in inheritance.get(start, set()):
204
+ cycle = has_cycle(parent, visited, path)
205
+ if cycle:
206
+ return cycle
207
+
208
+ path.pop()
209
+ return None
210
+
211
+ for cls in model.classes:
212
+ cycle = has_cycle(cls.name, set(), [])
213
+ if cycle:
214
+ issues.append(
215
+ ValidationIssue(
216
+ severity=Severity.ERROR,
217
+ code="INHERITANCE_CYCLE",
218
+ message=f"Inheritance cycle detected: {' -> '.join(cycle)}",
219
+ entity=cls.name,
220
+ suggestion="Break the cycle by removing one of the inheritance relationships",
221
+ )
222
+ )
223
+ break # Only report once
224
+
225
+ return issues
226
+
227
+ def _check_attribute_types(self, model: PumlModel) -> list[ValidationIssue]:
228
+ """Check that attribute types are recognized."""
229
+ issues = []
230
+
231
+ known_types = {
232
+ "string", "str", "text",
233
+ "integer", "int",
234
+ "decimal", "float", "double", "number",
235
+ "boolean", "bool",
236
+ "date", "datetime", "time",
237
+ "gYear", "gyear", "gYearMonth",
238
+ "duration",
239
+ "uri", "anyURI", "anyuri", "url",
240
+ "base64", "hexBinary",
241
+ "language", "token",
242
+ }
243
+
244
+ for cls in model.classes:
245
+ for attr in cls.attributes:
246
+ if attr.datatype and attr.datatype.lower() not in known_types:
247
+ if not attr.datatype.startswith("xsd:"):
248
+ issues.append(
249
+ ValidationIssue(
250
+ severity=Severity.WARNING,
251
+ code="UNKNOWN_DATATYPE",
252
+ message=f"Unknown datatype '{attr.datatype}' for attribute '{attr.name}'",
253
+ entity=f"{cls.name}.{attr.name}",
254
+ suggestion="Use standard XSD type or add custom mapping in config",
255
+ )
256
+ )
257
+
258
+ return issues
259
+
260
+ def _check_orphan_classes(self, model: PumlModel) -> list[ValidationIssue]:
261
+ """Check for classes with no relationships."""
262
+ issues = []
263
+
264
+ # Get all classes involved in relationships
265
+ related_classes = set()
266
+ for rel in model.relationships:
267
+ related_classes.add(rel.source)
268
+ related_classes.add(rel.target)
269
+
270
+ for cls in model.classes:
271
+ if cls.name not in related_classes and not cls.attributes:
272
+ issues.append(
273
+ ValidationIssue(
274
+ severity=Severity.INFO,
275
+ code="ISOLATED_CLASS",
276
+ message=f"Class '{cls.name}' has no relationships or attributes",
277
+ entity=cls.name,
278
+ suggestion="Consider adding relationships or attributes",
279
+ )
280
+ )
281
+
282
+ return issues
283
+
284
+
285
+ class RdfValidator:
286
+ """Validates generated RDF for OWL/RDFS consistency.
287
+
288
+ Checks include:
289
+ - Classes are typed as owl:Class
290
+ - Properties have domain and range
291
+ - No dangling references
292
+ """
293
+
294
+ def validate(self, graph: Graph) -> ValidationResult:
295
+ """Validate an RDF graph.
296
+
297
+ Args:
298
+ graph: The graph to validate
299
+
300
+ Returns:
301
+ ValidationResult with any issues found
302
+ """
303
+ issues: list[ValidationIssue] = []
304
+
305
+ # Check class typing
306
+ issues.extend(self._check_class_typing(graph))
307
+
308
+ # Check property completeness
309
+ issues.extend(self._check_property_completeness(graph))
310
+
311
+ # Check for dangling references
312
+ issues.extend(self._check_dangling_references(graph))
313
+
314
+ return ValidationResult(issues=issues)
315
+
316
+ def _check_class_typing(self, graph: Graph) -> list[ValidationIssue]:
317
+ """Check that classes are properly typed."""
318
+ issues = []
319
+
320
+ # Find subjects of rdfs:subClassOf that aren't typed as classes
321
+ for s in graph.subjects(RDFS.subClassOf, None):
322
+ if not isinstance(s, URIRef):
323
+ continue
324
+
325
+ is_class = (
326
+ (s, RDF.type, OWL.Class) in graph
327
+ or (s, RDF.type, RDFS.Class) in graph
328
+ )
329
+ if not is_class:
330
+ issues.append(
331
+ ValidationIssue(
332
+ severity=Severity.WARNING,
333
+ code="UNTYPED_CLASS",
334
+ message=f"Subject of rdfs:subClassOf not typed as class",
335
+ entity=str(s),
336
+ suggestion="Add rdf:type owl:Class triple",
337
+ )
338
+ )
339
+
340
+ return issues
341
+
342
+ def _check_property_completeness(self, graph: Graph) -> list[ValidationIssue]:
343
+ """Check that properties have domain and range."""
344
+ issues = []
345
+
346
+ for prop in graph.subjects(RDF.type, OWL.ObjectProperty):
347
+ if not isinstance(prop, URIRef):
348
+ continue
349
+
350
+ has_domain = any(graph.objects(prop, RDFS.domain))
351
+ has_range = any(graph.objects(prop, RDFS.range))
352
+
353
+ if not has_domain:
354
+ issues.append(
355
+ ValidationIssue(
356
+ severity=Severity.INFO,
357
+ code="MISSING_DOMAIN",
358
+ message="Object property has no domain",
359
+ entity=str(prop),
360
+ )
361
+ )
362
+ if not has_range:
363
+ issues.append(
364
+ ValidationIssue(
365
+ severity=Severity.INFO,
366
+ code="MISSING_RANGE",
367
+ message="Object property has no range",
368
+ entity=str(prop),
369
+ )
370
+ )
371
+
372
+ for prop in graph.subjects(RDF.type, OWL.DatatypeProperty):
373
+ if not isinstance(prop, URIRef):
374
+ continue
375
+
376
+ has_range = any(graph.objects(prop, RDFS.range))
377
+ if not has_range:
378
+ issues.append(
379
+ ValidationIssue(
380
+ severity=Severity.INFO,
381
+ code="MISSING_RANGE",
382
+ message="Datatype property has no range (XSD type)",
383
+ entity=str(prop),
384
+ )
385
+ )
386
+
387
+ return issues
388
+
389
+ def _check_dangling_references(self, graph: Graph) -> list[ValidationIssue]:
390
+ """Check for references to undefined entities."""
391
+ issues = []
392
+
393
+ # Get all defined classes
394
+ defined_classes = set()
395
+ for cls in graph.subjects(RDF.type, OWL.Class):
396
+ defined_classes.add(cls)
397
+ for cls in graph.subjects(RDF.type, RDFS.Class):
398
+ defined_classes.add(cls)
399
+
400
+ # Check domain and range references
401
+ for prop in graph.subjects(RDF.type, OWL.ObjectProperty):
402
+ for domain in graph.objects(prop, RDFS.domain):
403
+ if isinstance(domain, URIRef) and domain not in defined_classes:
404
+ issues.append(
405
+ ValidationIssue(
406
+ severity=Severity.WARNING,
407
+ code="UNDEFINED_DOMAIN",
408
+ message=f"Property domain references undefined class",
409
+ entity=f"{prop} -> {domain}",
410
+ )
411
+ )
412
+
413
+ for rng in graph.objects(prop, RDFS.range):
414
+ if isinstance(rng, URIRef) and rng not in defined_classes:
415
+ # Could be external class - just info
416
+ issues.append(
417
+ ValidationIssue(
418
+ severity=Severity.INFO,
419
+ code="EXTERNAL_RANGE",
420
+ message=f"Property range references class not in this graph",
421
+ entity=f"{prop} -> {rng}",
422
+ )
423
+ )
424
+
425
+ return issues
426
+
427
+
428
+ def validate_puml(model: PumlModel) -> ValidationResult:
429
+ """Convenience function to validate a PlantUML model.
430
+
431
+ Args:
432
+ model: The parsed model to validate
433
+
434
+ Returns:
435
+ ValidationResult with any issues found
436
+ """
437
+ validator = PumlModelValidator()
438
+ return validator.validate(model)
439
+
440
+
441
+ def validate_rdf(graph: Graph) -> ValidationResult:
442
+ """Convenience function to validate an RDF graph.
443
+
444
+ Args:
445
+ graph: The graph to validate
446
+
447
+ Returns:
448
+ ValidationResult with any issues found
449
+ """
450
+ validator = RdfValidator()
451
+ return validator.validate(graph)
@@ -0,0 +1,56 @@
1
+ """SHACL shape generation from OWL ontologies.
2
+
3
+ This module provides tools for generating SHACL validation shapes from
4
+ OWL ontology definitions, converting domain/range, cardinality restrictions,
5
+ and other OWL patterns to equivalent SHACL constraints.
6
+
7
+ Basic usage:
8
+
9
+ from rdf_construct.shacl import generate_shapes, ShaclConfig, StrictnessLevel
10
+
11
+ # Generate shapes with default settings
12
+ graph, turtle = generate_shapes(Path("ontology.ttl"))
13
+
14
+ # Generate with strict level
15
+ config = ShaclConfig(level=StrictnessLevel.STRICT, closed=True)
16
+ graph, turtle = generate_shapes(Path("ontology.ttl"), config)
17
+
18
+ # Generate and write to file
19
+ from rdf_construct.shacl import generate_shapes_to_file
20
+ generate_shapes_to_file(
21
+ Path("ontology.ttl"),
22
+ Path("shapes.ttl"),
23
+ config,
24
+ )
25
+ """
26
+
27
+ from rdf_construct.shacl.config import (
28
+ ShaclConfig,
29
+ Severity,
30
+ StrictnessLevel,
31
+ load_shacl_config,
32
+ )
33
+ from rdf_construct.shacl.converters import PropertyConstraint
34
+ from rdf_construct.shacl.generator import (
35
+ ShapeGenerator,
36
+ generate_shapes,
37
+ generate_shapes_to_file,
38
+ )
39
+ from rdf_construct.shacl.namespaces import SH, SHACL_PREFIXES
40
+
41
+ __all__ = [
42
+ # Configuration
43
+ "ShaclConfig",
44
+ "StrictnessLevel",
45
+ "Severity",
46
+ "load_shacl_config",
47
+ # Generator
48
+ "ShapeGenerator",
49
+ "generate_shapes",
50
+ "generate_shapes_to_file",
51
+ # Converters
52
+ "PropertyConstraint",
53
+ # Namespaces
54
+ "SH",
55
+ "SHACL_PREFIXES",
56
+ ]
@@ -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)