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,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,72 @@
|
|
|
1
|
+
"""Refactor module for URI renaming and deprecation.
|
|
2
|
+
|
|
3
|
+
This module provides tools for common ontology maintenance tasks:
|
|
4
|
+
- Renaming URIs (single entities or bulk namespace changes)
|
|
5
|
+
- Deprecating entities with proper OWL annotations
|
|
6
|
+
- Data migration for instance graphs
|
|
7
|
+
|
|
8
|
+
Example usage:
|
|
9
|
+
from rdf_construct.refactor import OntologyRenamer, RenameConfig
|
|
10
|
+
|
|
11
|
+
renamer = OntologyRenamer()
|
|
12
|
+
config = RenameConfig(entities={
|
|
13
|
+
"http://example.org/Buiding": "http://example.org/Building"
|
|
14
|
+
})
|
|
15
|
+
result = renamer.rename(graph, config)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from rdf_construct.refactor.config import (
|
|
19
|
+
RenameConfig,
|
|
20
|
+
RenameMapping,
|
|
21
|
+
DeprecationSpec,
|
|
22
|
+
DeprecationConfig,
|
|
23
|
+
RefactorConfig,
|
|
24
|
+
DataMigrationSpec,
|
|
25
|
+
load_refactor_config,
|
|
26
|
+
create_default_rename_config,
|
|
27
|
+
create_default_deprecation_config,
|
|
28
|
+
)
|
|
29
|
+
from rdf_construct.refactor.renamer import (
|
|
30
|
+
OntologyRenamer,
|
|
31
|
+
RenameResult,
|
|
32
|
+
RenameStats,
|
|
33
|
+
rename_file,
|
|
34
|
+
rename_files,
|
|
35
|
+
)
|
|
36
|
+
from rdf_construct.refactor.deprecator import (
|
|
37
|
+
OntologyDeprecator,
|
|
38
|
+
DeprecationResult,
|
|
39
|
+
DeprecationStats,
|
|
40
|
+
EntityDeprecationInfo,
|
|
41
|
+
deprecate_file,
|
|
42
|
+
generate_deprecation_message,
|
|
43
|
+
)
|
|
44
|
+
from rdf_construct.refactor.formatters import TextFormatter
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Config
|
|
48
|
+
"RenameConfig",
|
|
49
|
+
"RenameMapping",
|
|
50
|
+
"DeprecationSpec",
|
|
51
|
+
"DeprecationConfig",
|
|
52
|
+
"RefactorConfig",
|
|
53
|
+
"DataMigrationSpec",
|
|
54
|
+
"load_refactor_config",
|
|
55
|
+
"create_default_rename_config",
|
|
56
|
+
"create_default_deprecation_config",
|
|
57
|
+
# Renamer
|
|
58
|
+
"OntologyRenamer",
|
|
59
|
+
"RenameResult",
|
|
60
|
+
"RenameStats",
|
|
61
|
+
"rename_file",
|
|
62
|
+
"rename_files",
|
|
63
|
+
# Deprecator
|
|
64
|
+
"OntologyDeprecator",
|
|
65
|
+
"DeprecationResult",
|
|
66
|
+
"DeprecationStats",
|
|
67
|
+
"EntityDeprecationInfo",
|
|
68
|
+
"deprecate_file",
|
|
69
|
+
"generate_deprecation_message",
|
|
70
|
+
# Formatters
|
|
71
|
+
"TextFormatter",
|
|
72
|
+
]
|