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,311 @@
|
|
|
1
|
+
"""Text formatter for refactor command output.
|
|
2
|
+
|
|
3
|
+
Provides formatted output for dry-run previews and results.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rdf_construct.refactor.config import RenameConfig, RenameMapping, DeprecationSpec
|
|
9
|
+
from rdf_construct.refactor.renamer import RenameResult, RenameStats
|
|
10
|
+
from rdf_construct.refactor.deprecator import DeprecationResult, EntityDeprecationInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TextFormatter:
|
|
14
|
+
"""Text formatter for refactor results and previews.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
use_colour: Whether to use ANSI colour codes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, use_colour: bool = True):
|
|
21
|
+
"""Initialize formatter.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
use_colour: Whether to use ANSI colour codes.
|
|
25
|
+
"""
|
|
26
|
+
self.use_colour = use_colour
|
|
27
|
+
|
|
28
|
+
def _colour(self, text: str, colour: str) -> str:
|
|
29
|
+
"""Apply ANSI colour code if enabled.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
text: Text to colour.
|
|
33
|
+
colour: Colour name (green, red, yellow, cyan, bold).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Coloured text (or original if colour disabled).
|
|
37
|
+
"""
|
|
38
|
+
if not self.use_colour:
|
|
39
|
+
return text
|
|
40
|
+
|
|
41
|
+
codes = {
|
|
42
|
+
"green": "\033[32m",
|
|
43
|
+
"red": "\033[31m",
|
|
44
|
+
"yellow": "\033[33m",
|
|
45
|
+
"cyan": "\033[36m",
|
|
46
|
+
"bold": "\033[1m",
|
|
47
|
+
"dim": "\033[2m",
|
|
48
|
+
"reset": "\033[0m",
|
|
49
|
+
}
|
|
50
|
+
return f"{codes.get(colour, '')}{text}{codes['reset']}"
|
|
51
|
+
|
|
52
|
+
def format_rename_preview(
|
|
53
|
+
self,
|
|
54
|
+
mappings: list[RenameMapping],
|
|
55
|
+
source_file: str,
|
|
56
|
+
source_triples: int,
|
|
57
|
+
literal_mentions: dict[str, int] | None = None,
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Format a dry-run preview for rename operation.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
mappings: List of rename mappings to apply.
|
|
63
|
+
source_file: Name of source file.
|
|
64
|
+
source_triples: Number of triples in source.
|
|
65
|
+
literal_mentions: Count of mentions in literals (won't be changed).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Formatted preview string.
|
|
69
|
+
"""
|
|
70
|
+
lines = [
|
|
71
|
+
self._colour("Refactoring Preview: Rename", "bold"),
|
|
72
|
+
"=" * 27,
|
|
73
|
+
"",
|
|
74
|
+
f"Source: {source_file} ({source_triples:,} triples)",
|
|
75
|
+
"",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Group by source type
|
|
79
|
+
namespace_mappings = [m for m in mappings if m.source == "namespace"]
|
|
80
|
+
explicit_mappings = [m for m in mappings if m.source == "explicit"]
|
|
81
|
+
|
|
82
|
+
if namespace_mappings:
|
|
83
|
+
# Group by namespace
|
|
84
|
+
namespaces: dict[str, list[RenameMapping]] = {}
|
|
85
|
+
for m in namespace_mappings:
|
|
86
|
+
# Extract namespace prefix
|
|
87
|
+
from_str = str(m.from_uri)
|
|
88
|
+
to_str = str(m.to_uri)
|
|
89
|
+
# Find common prefix
|
|
90
|
+
ns_from = from_str.rsplit("#", 1)[0] + "#" if "#" in from_str else from_str.rsplit("/", 1)[0] + "/"
|
|
91
|
+
ns_to = to_str.rsplit("#", 1)[0] + "#" if "#" in to_str else to_str.rsplit("/", 1)[0] + "/"
|
|
92
|
+
key = f"{ns_from} → {ns_to}"
|
|
93
|
+
if key not in namespaces:
|
|
94
|
+
namespaces[key] = []
|
|
95
|
+
namespaces[key].append(m)
|
|
96
|
+
|
|
97
|
+
lines.append(self._colour("Namespace renames:", "cyan"))
|
|
98
|
+
for ns_change, ns_mappings in namespaces.items():
|
|
99
|
+
lines.append(f" {ns_change}")
|
|
100
|
+
lines.append(f" - {len(ns_mappings)} entities affected")
|
|
101
|
+
lines.append("")
|
|
102
|
+
|
|
103
|
+
if explicit_mappings:
|
|
104
|
+
lines.append(self._colour("Entity renames:", "cyan"))
|
|
105
|
+
for m in explicit_mappings:
|
|
106
|
+
from_local = str(m.from_uri).split("#")[-1].split("/")[-1]
|
|
107
|
+
to_local = str(m.to_uri).split("#")[-1].split("/")[-1]
|
|
108
|
+
lines.append(f" {from_local} → {to_local}")
|
|
109
|
+
|
|
110
|
+
# Check for literal mentions
|
|
111
|
+
if literal_mentions and str(m.from_uri) in literal_mentions:
|
|
112
|
+
count = literal_mentions[str(m.from_uri)]
|
|
113
|
+
lines.append(
|
|
114
|
+
f" └─ {count} literal mention(s) "
|
|
115
|
+
f"{self._colour('(NOT changed)', 'yellow')}"
|
|
116
|
+
)
|
|
117
|
+
lines.append("")
|
|
118
|
+
|
|
119
|
+
# Summary
|
|
120
|
+
lines.append(self._colour("Totals:", "bold"))
|
|
121
|
+
lines.append(f" - {len(mappings)} entities to rename")
|
|
122
|
+
if namespace_mappings:
|
|
123
|
+
lines.append(f" - {len(namespace_mappings)} from namespace rules")
|
|
124
|
+
if explicit_mappings:
|
|
125
|
+
lines.append(f" - {len(explicit_mappings)} from explicit rules")
|
|
126
|
+
lines.append("")
|
|
127
|
+
lines.append(self._colour("Run without --dry-run to apply changes.", "dim"))
|
|
128
|
+
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
def format_rename_result(self, result: RenameResult) -> str:
|
|
132
|
+
"""Format the result of a rename operation.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
result: Result from rename operation.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Formatted result string.
|
|
139
|
+
"""
|
|
140
|
+
if not result.success:
|
|
141
|
+
return self._colour(f"✗ Rename failed: {result.error}", "red")
|
|
142
|
+
|
|
143
|
+
lines = [
|
|
144
|
+
self._colour("Rename Complete", "green"),
|
|
145
|
+
"",
|
|
146
|
+
f" Source triples: {result.source_triples:,}",
|
|
147
|
+
f" Result triples: {result.result_triples:,}",
|
|
148
|
+
"",
|
|
149
|
+
self._colour("Changes:", "cyan"),
|
|
150
|
+
f" - Subjects renamed: {result.stats.subjects_renamed:,}",
|
|
151
|
+
f" - Predicates renamed: {result.stats.predicates_renamed:,}",
|
|
152
|
+
f" - Objects renamed: {result.stats.objects_renamed:,}",
|
|
153
|
+
f" - Total URI substitutions: {result.stats.total_renames:,}",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
if result.stats.namespace_entities > 0:
|
|
157
|
+
lines.append(f" - Via namespace rules: {result.stats.namespace_entities:,}")
|
|
158
|
+
if result.stats.explicit_entities > 0:
|
|
159
|
+
lines.append(f" - Via explicit rules: {result.stats.explicit_entities:,}")
|
|
160
|
+
|
|
161
|
+
if result.stats.literal_mentions:
|
|
162
|
+
lines.append("")
|
|
163
|
+
lines.append(
|
|
164
|
+
self._colour(
|
|
165
|
+
f" ⚠ {len(result.stats.literal_mentions)} entities mentioned in literals "
|
|
166
|
+
"(not modified)",
|
|
167
|
+
"yellow",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
def format_deprecation_preview(
|
|
174
|
+
self,
|
|
175
|
+
specs: list[DeprecationSpec],
|
|
176
|
+
entity_info: list[EntityDeprecationInfo] | None = None,
|
|
177
|
+
source_file: str = "",
|
|
178
|
+
source_triples: int = 0,
|
|
179
|
+
) -> str:
|
|
180
|
+
"""Format a dry-run preview for deprecation operation.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
specs: List of deprecation specifications.
|
|
184
|
+
entity_info: Optional entity information from dry run.
|
|
185
|
+
source_file: Name of source file.
|
|
186
|
+
source_triples: Number of triples in source.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Formatted preview string.
|
|
190
|
+
"""
|
|
191
|
+
lines = [
|
|
192
|
+
self._colour("Refactoring Preview: Deprecate", "bold"),
|
|
193
|
+
"=" * 30,
|
|
194
|
+
"",
|
|
195
|
+
f"Source: {source_file} ({source_triples:,} triples)" if source_file else "",
|
|
196
|
+
"",
|
|
197
|
+
self._colour("Entities to deprecate:", "cyan"),
|
|
198
|
+
"",
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
for i, spec in enumerate(specs):
|
|
202
|
+
# Extract local name
|
|
203
|
+
local_name = spec.entity.split("#")[-1].split("/")[-1]
|
|
204
|
+
lines.append(f" {self._colour(local_name, 'bold')}")
|
|
205
|
+
|
|
206
|
+
# Show current state if available
|
|
207
|
+
if entity_info and i < len(entity_info):
|
|
208
|
+
info = entity_info[i]
|
|
209
|
+
if not info.found:
|
|
210
|
+
lines.append(f" {self._colour('⚠ Entity not found in graph', 'yellow')}")
|
|
211
|
+
else:
|
|
212
|
+
if info.was_already_deprecated:
|
|
213
|
+
lines.append(f" {self._colour('Already deprecated', 'yellow')}")
|
|
214
|
+
if info.current_labels:
|
|
215
|
+
lines.append(f" rdfs:label: \"{info.current_labels[0]}\"")
|
|
216
|
+
if info.reference_count > 0:
|
|
217
|
+
lines.append(
|
|
218
|
+
f" {self._colour(f'Referenced {info.reference_count} times', 'dim')}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Show what will be added
|
|
222
|
+
lines.append(" Will add:")
|
|
223
|
+
lines.append(f" owl:deprecated {self._colour('true', 'green')}")
|
|
224
|
+
|
|
225
|
+
if spec.replaced_by:
|
|
226
|
+
repl_local = spec.replaced_by.split("#")[-1].split("/")[-1]
|
|
227
|
+
lines.append(f" dcterms:isReplacedBy {self._colour(repl_local, 'cyan')}")
|
|
228
|
+
|
|
229
|
+
if spec.message:
|
|
230
|
+
msg_preview = spec.message[:50] + "..." if len(spec.message) > 50 else spec.message
|
|
231
|
+
lines.append(f" rdfs:comment \"DEPRECATED: {msg_preview}\"")
|
|
232
|
+
|
|
233
|
+
lines.append("")
|
|
234
|
+
|
|
235
|
+
# Summary
|
|
236
|
+
lines.append(self._colour("Summary:", "bold"))
|
|
237
|
+
lines.append(f" - {len(specs)} entities will be marked deprecated")
|
|
238
|
+
with_replacement = len([s for s in specs if s.replaced_by])
|
|
239
|
+
lines.append(f" - {with_replacement} with replacement")
|
|
240
|
+
lines.append(f" - {len(specs) - with_replacement} without replacement")
|
|
241
|
+
lines.append("")
|
|
242
|
+
lines.append(
|
|
243
|
+
self._colour(
|
|
244
|
+
"Note: Deprecation marks entities but does not rename or migrate.",
|
|
245
|
+
"dim",
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
lines.append(
|
|
249
|
+
self._colour(
|
|
250
|
+
" Use 'refactor rename' to actually migrate references.",
|
|
251
|
+
"dim",
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
lines.append("")
|
|
255
|
+
lines.append(self._colour("Run without --dry-run to apply changes.", "dim"))
|
|
256
|
+
|
|
257
|
+
return "\n".join(lines)
|
|
258
|
+
|
|
259
|
+
def format_deprecation_result(self, result: DeprecationResult) -> str:
|
|
260
|
+
"""Format the result of a deprecation operation.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
result: Result from deprecation operation.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Formatted result string.
|
|
267
|
+
"""
|
|
268
|
+
if not result.success:
|
|
269
|
+
return self._colour(f"✗ Deprecation failed: {result.error}", "red")
|
|
270
|
+
|
|
271
|
+
lines = [
|
|
272
|
+
self._colour("Deprecation Complete", "green"),
|
|
273
|
+
"",
|
|
274
|
+
f" Source triples: {result.source_triples:,}",
|
|
275
|
+
f" Result triples: {result.result_triples:,}",
|
|
276
|
+
"",
|
|
277
|
+
self._colour("Changes:", "cyan"),
|
|
278
|
+
f" - Entities deprecated: {result.stats.entities_deprecated}",
|
|
279
|
+
f" - Triples added: {result.stats.triples_added}",
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
if result.stats.entities_not_found > 0:
|
|
283
|
+
lines.append(
|
|
284
|
+
self._colour(
|
|
285
|
+
f" ⚠ Entities not found: {result.stats.entities_not_found}",
|
|
286
|
+
"yellow",
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if result.stats.entities_already_deprecated > 0:
|
|
291
|
+
lines.append(
|
|
292
|
+
f" - Already deprecated: {result.stats.entities_already_deprecated}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Show details of deprecated entities
|
|
296
|
+
if result.entity_info:
|
|
297
|
+
lines.append("")
|
|
298
|
+
lines.append("Details:")
|
|
299
|
+
for info in result.entity_info:
|
|
300
|
+
local_name = info.uri.split("#")[-1].split("/")[-1]
|
|
301
|
+
if not info.found:
|
|
302
|
+
lines.append(f" {self._colour('⚠', 'yellow')} {local_name} - not found")
|
|
303
|
+
elif info.was_already_deprecated and info.triples_added == 0:
|
|
304
|
+
lines.append(f" ○ {local_name} - already deprecated, no changes")
|
|
305
|
+
else:
|
|
306
|
+
lines.append(f" {self._colour('✓', 'green')} {local_name}")
|
|
307
|
+
if info.replaced_by:
|
|
308
|
+
repl_local = info.replaced_by.split("#")[-1].split("/")[-1]
|
|
309
|
+
lines.append(f" → replaced by {repl_local}")
|
|
310
|
+
|
|
311
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""URI renaming logic for ontology refactoring.
|
|
2
|
+
|
|
3
|
+
This module handles renaming URIs in RDF graphs:
|
|
4
|
+
- Single entity renames (fixing typos, etc.)
|
|
5
|
+
- Bulk namespace changes (project/org renames)
|
|
6
|
+
- Predicate position handling (URIs as predicates are also renamed)
|
|
7
|
+
|
|
8
|
+
The renamer does NOT modify text inside literals - comments mentioning
|
|
9
|
+
renamed entities are left unchanged (this is intentional to avoid
|
|
10
|
+
corrupting documentation).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from rdflib import Graph, URIRef, Literal, BNode
|
|
18
|
+
|
|
19
|
+
from rdf_construct.refactor.config import RenameConfig, RenameMapping
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RenameStats:
|
|
24
|
+
"""Statistics from a rename operation.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
subjects_renamed: URIs renamed in subject position.
|
|
28
|
+
predicates_renamed: URIs renamed in predicate position.
|
|
29
|
+
objects_renamed: URIs renamed in object position.
|
|
30
|
+
entities_by_source: Count of entities by mapping source.
|
|
31
|
+
literal_mentions: Count of mentions in literals (NOT renamed).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
subjects_renamed: int = 0
|
|
35
|
+
predicates_renamed: int = 0
|
|
36
|
+
objects_renamed: int = 0
|
|
37
|
+
entities_by_source: dict[str, int] = field(default_factory=dict)
|
|
38
|
+
literal_mentions: dict[str, int] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def total_renames(self) -> int:
|
|
42
|
+
"""Total number of URI substitutions made."""
|
|
43
|
+
return self.subjects_renamed + self.predicates_renamed + self.objects_renamed
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def namespace_entities(self) -> int:
|
|
47
|
+
"""Number of entities renamed by namespace rules."""
|
|
48
|
+
return self.entities_by_source.get("namespace", 0)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def explicit_entities(self) -> int:
|
|
52
|
+
"""Number of entities renamed by explicit rules."""
|
|
53
|
+
return self.entities_by_source.get("explicit", 0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class RenameResult:
|
|
58
|
+
"""Result of a rename operation.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
renamed_graph: The graph with URIs renamed.
|
|
62
|
+
stats: Rename statistics.
|
|
63
|
+
success: Whether the operation succeeded.
|
|
64
|
+
error: Error message if success is False.
|
|
65
|
+
mappings_applied: List of mappings that were actually applied.
|
|
66
|
+
source_triples: Original triple count.
|
|
67
|
+
result_triples: Final triple count.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
renamed_graph: Graph | None = None
|
|
71
|
+
stats: RenameStats = field(default_factory=RenameStats)
|
|
72
|
+
success: bool = True
|
|
73
|
+
error: str | None = None
|
|
74
|
+
mappings_applied: list[RenameMapping] = field(default_factory=list)
|
|
75
|
+
source_triples: int = 0
|
|
76
|
+
result_triples: int = 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OntologyRenamer:
|
|
80
|
+
"""Renames URIs in RDF ontology graphs.
|
|
81
|
+
|
|
82
|
+
Handles both single entity renames and bulk namespace changes.
|
|
83
|
+
The renamer processes subjects, predicates, and objects, but
|
|
84
|
+
intentionally leaves literal values unchanged.
|
|
85
|
+
|
|
86
|
+
Example usage:
|
|
87
|
+
renamer = OntologyRenamer()
|
|
88
|
+
config = RenameConfig(entities={
|
|
89
|
+
"http://example.org/Buiding": "http://example.org/Building"
|
|
90
|
+
})
|
|
91
|
+
result = renamer.rename(graph, config)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def rename(
|
|
95
|
+
self,
|
|
96
|
+
graph: Graph,
|
|
97
|
+
config: RenameConfig,
|
|
98
|
+
) -> RenameResult:
|
|
99
|
+
"""Rename URIs in a graph according to configuration.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
graph: Source RDF graph.
|
|
103
|
+
config: Rename configuration with namespace and entity mappings.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
RenameResult with the modified graph and statistics.
|
|
107
|
+
"""
|
|
108
|
+
result = RenameResult()
|
|
109
|
+
result.source_triples = len(graph)
|
|
110
|
+
|
|
111
|
+
# Build concrete mappings from config
|
|
112
|
+
mappings = config.build_mappings(graph)
|
|
113
|
+
if not mappings:
|
|
114
|
+
# Nothing to rename
|
|
115
|
+
result.renamed_graph = graph
|
|
116
|
+
result.result_triples = len(graph)
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
# Create URI lookup map for efficient substitution
|
|
120
|
+
uri_map: dict[URIRef, tuple[URIRef, str]] = {
|
|
121
|
+
m.from_uri: (m.to_uri, m.source) for m in mappings
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Track which mappings were actually applied
|
|
125
|
+
applied_mappings: set[URIRef] = set()
|
|
126
|
+
|
|
127
|
+
# Create new graph with renamed URIs
|
|
128
|
+
renamed_graph = Graph()
|
|
129
|
+
|
|
130
|
+
# Copy namespace bindings, updating if needed
|
|
131
|
+
old_ns_to_new: dict[str, str] = {}
|
|
132
|
+
if config.namespaces:
|
|
133
|
+
old_ns_to_new = config.namespaces
|
|
134
|
+
|
|
135
|
+
for prefix, ns in graph.namespace_manager.namespaces():
|
|
136
|
+
ns_str = str(ns)
|
|
137
|
+
new_ns_str = ns_str
|
|
138
|
+
for old_ns, new_ns in old_ns_to_new.items():
|
|
139
|
+
if ns_str.startswith(old_ns) or ns_str == old_ns:
|
|
140
|
+
new_ns_str = ns_str.replace(old_ns, new_ns, 1)
|
|
141
|
+
break
|
|
142
|
+
renamed_graph.bind(prefix, new_ns_str, override=True)
|
|
143
|
+
|
|
144
|
+
# Process each triple
|
|
145
|
+
for s, p, o in graph:
|
|
146
|
+
new_s, new_p, new_o = s, p, o
|
|
147
|
+
|
|
148
|
+
# Check subject
|
|
149
|
+
if isinstance(s, URIRef) and s in uri_map:
|
|
150
|
+
new_s = uri_map[s][0]
|
|
151
|
+
result.stats.subjects_renamed += 1
|
|
152
|
+
applied_mappings.add(s)
|
|
153
|
+
source = uri_map[s][1]
|
|
154
|
+
result.stats.entities_by_source[source] = (
|
|
155
|
+
result.stats.entities_by_source.get(source, 0) + 1
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Check predicate
|
|
159
|
+
if isinstance(p, URIRef) and p in uri_map:
|
|
160
|
+
new_p = uri_map[p][0]
|
|
161
|
+
result.stats.predicates_renamed += 1
|
|
162
|
+
applied_mappings.add(p)
|
|
163
|
+
# Don't double-count in entities_by_source
|
|
164
|
+
|
|
165
|
+
# Check object (only URIRefs, not Literals)
|
|
166
|
+
if isinstance(o, URIRef) and o in uri_map:
|
|
167
|
+
new_o = uri_map[o][0]
|
|
168
|
+
result.stats.objects_renamed += 1
|
|
169
|
+
applied_mappings.add(o)
|
|
170
|
+
# Don't double-count in entities_by_source
|
|
171
|
+
|
|
172
|
+
renamed_graph.add((new_s, new_p, new_o))
|
|
173
|
+
|
|
174
|
+
# Scan for literal mentions (informational only)
|
|
175
|
+
for mapping in mappings:
|
|
176
|
+
old_local = str(mapping.from_uri).split("#")[-1].split("/")[-1]
|
|
177
|
+
for s, p, o in graph:
|
|
178
|
+
if isinstance(o, Literal) and old_local in str(o):
|
|
179
|
+
key = str(mapping.from_uri)
|
|
180
|
+
result.stats.literal_mentions[key] = (
|
|
181
|
+
result.stats.literal_mentions.get(key, 0) + 1
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Build list of applied mappings
|
|
185
|
+
result.mappings_applied = [m for m in mappings if m.from_uri in applied_mappings]
|
|
186
|
+
|
|
187
|
+
result.renamed_graph = renamed_graph
|
|
188
|
+
result.result_triples = len(renamed_graph)
|
|
189
|
+
result.success = True
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
def rename_single(
|
|
194
|
+
self,
|
|
195
|
+
graph: Graph,
|
|
196
|
+
from_uri: str,
|
|
197
|
+
to_uri: str,
|
|
198
|
+
) -> RenameResult:
|
|
199
|
+
"""Convenience method for renaming a single URI.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
graph: Source RDF graph.
|
|
203
|
+
from_uri: URI to rename.
|
|
204
|
+
to_uri: New URI.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
RenameResult with the modified graph.
|
|
208
|
+
"""
|
|
209
|
+
config = RenameConfig(entities={from_uri: to_uri})
|
|
210
|
+
return self.rename(graph, config)
|
|
211
|
+
|
|
212
|
+
def rename_namespace(
|
|
213
|
+
self,
|
|
214
|
+
graph: Graph,
|
|
215
|
+
from_namespace: str,
|
|
216
|
+
to_namespace: str,
|
|
217
|
+
) -> RenameResult:
|
|
218
|
+
"""Convenience method for bulk namespace rename.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
graph: Source RDF graph.
|
|
222
|
+
from_namespace: Old namespace prefix.
|
|
223
|
+
to_namespace: New namespace prefix.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
RenameResult with the modified graph.
|
|
227
|
+
"""
|
|
228
|
+
config = RenameConfig(namespaces={from_namespace: to_namespace})
|
|
229
|
+
return self.rename(graph, config)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def rename_file(
|
|
233
|
+
source_path: Path,
|
|
234
|
+
output_path: Path,
|
|
235
|
+
config: RenameConfig,
|
|
236
|
+
) -> RenameResult:
|
|
237
|
+
"""Convenience function to rename URIs in a file.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
source_path: Path to source RDF file.
|
|
241
|
+
output_path: Path to write renamed output.
|
|
242
|
+
config: Rename configuration.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
RenameResult with statistics.
|
|
246
|
+
"""
|
|
247
|
+
# Load source graph
|
|
248
|
+
graph = Graph()
|
|
249
|
+
try:
|
|
250
|
+
graph.parse(source_path.as_posix())
|
|
251
|
+
except Exception as e:
|
|
252
|
+
result = RenameResult()
|
|
253
|
+
result.success = False
|
|
254
|
+
result.error = f"Failed to parse {source_path}: {e}"
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
# Perform rename
|
|
258
|
+
renamer = OntologyRenamer()
|
|
259
|
+
result = renamer.rename(graph, config)
|
|
260
|
+
|
|
261
|
+
if not result.success:
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
# Write output
|
|
265
|
+
if result.renamed_graph:
|
|
266
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
result.renamed_graph.serialize(destination=output_path.as_posix(), format="turtle")
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def rename_files(
|
|
273
|
+
source_paths: list[Path],
|
|
274
|
+
output_dir: Path,
|
|
275
|
+
config: RenameConfig,
|
|
276
|
+
) -> list[tuple[Path, RenameResult]]:
|
|
277
|
+
"""Rename URIs in multiple files.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
source_paths: Paths to source RDF files.
|
|
281
|
+
output_dir: Directory to write renamed outputs.
|
|
282
|
+
config: Rename configuration.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of (output_path, result) tuples.
|
|
286
|
+
"""
|
|
287
|
+
results: list[tuple[Path, RenameResult]] = []
|
|
288
|
+
|
|
289
|
+
for source_path in source_paths:
|
|
290
|
+
output_path = output_dir / source_path.name
|
|
291
|
+
result = rename_file(source_path, output_path, config)
|
|
292
|
+
results.append((output_path, result))
|
|
293
|
+
|
|
294
|
+
return results
|
|
@@ -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
|
+
]
|