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.
Files changed (110) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +3429 -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/localise/__init__.py +114 -0
  51. rdf_construct/localise/config.py +508 -0
  52. rdf_construct/localise/extractor.py +427 -0
  53. rdf_construct/localise/formatters/__init__.py +36 -0
  54. rdf_construct/localise/formatters/markdown.py +229 -0
  55. rdf_construct/localise/formatters/text.py +224 -0
  56. rdf_construct/localise/merger.py +346 -0
  57. rdf_construct/localise/reporter.py +356 -0
  58. rdf_construct/main.py +6 -0
  59. rdf_construct/merge/__init__.py +165 -0
  60. rdf_construct/merge/config.py +354 -0
  61. rdf_construct/merge/conflicts.py +281 -0
  62. rdf_construct/merge/formatters.py +426 -0
  63. rdf_construct/merge/merger.py +425 -0
  64. rdf_construct/merge/migrator.py +339 -0
  65. rdf_construct/merge/rules.py +377 -0
  66. rdf_construct/merge/splitter.py +1102 -0
  67. rdf_construct/puml2rdf/__init__.py +103 -0
  68. rdf_construct/puml2rdf/config.py +230 -0
  69. rdf_construct/puml2rdf/converter.py +420 -0
  70. rdf_construct/puml2rdf/merger.py +200 -0
  71. rdf_construct/puml2rdf/model.py +202 -0
  72. rdf_construct/puml2rdf/parser.py +565 -0
  73. rdf_construct/puml2rdf/validators.py +451 -0
  74. rdf_construct/refactor/__init__.py +72 -0
  75. rdf_construct/refactor/config.py +362 -0
  76. rdf_construct/refactor/deprecator.py +328 -0
  77. rdf_construct/refactor/formatters/__init__.py +8 -0
  78. rdf_construct/refactor/formatters/text.py +311 -0
  79. rdf_construct/refactor/renamer.py +294 -0
  80. rdf_construct/shacl/__init__.py +56 -0
  81. rdf_construct/shacl/config.py +166 -0
  82. rdf_construct/shacl/converters.py +520 -0
  83. rdf_construct/shacl/generator.py +364 -0
  84. rdf_construct/shacl/namespaces.py +93 -0
  85. rdf_construct/stats/__init__.py +29 -0
  86. rdf_construct/stats/collector.py +178 -0
  87. rdf_construct/stats/comparator.py +298 -0
  88. rdf_construct/stats/formatters/__init__.py +83 -0
  89. rdf_construct/stats/formatters/json.py +38 -0
  90. rdf_construct/stats/formatters/markdown.py +153 -0
  91. rdf_construct/stats/formatters/text.py +186 -0
  92. rdf_construct/stats/metrics/__init__.py +26 -0
  93. rdf_construct/stats/metrics/basic.py +147 -0
  94. rdf_construct/stats/metrics/complexity.py +137 -0
  95. rdf_construct/stats/metrics/connectivity.py +130 -0
  96. rdf_construct/stats/metrics/documentation.py +128 -0
  97. rdf_construct/stats/metrics/hierarchy.py +207 -0
  98. rdf_construct/stats/metrics/properties.py +88 -0
  99. rdf_construct/uml/__init__.py +22 -0
  100. rdf_construct/uml/context.py +194 -0
  101. rdf_construct/uml/mapper.py +371 -0
  102. rdf_construct/uml/odm_renderer.py +789 -0
  103. rdf_construct/uml/renderer.py +684 -0
  104. rdf_construct/uml/uml_layout.py +393 -0
  105. rdf_construct/uml/uml_style.py +613 -0
  106. rdf_construct-0.3.0.dist-info/METADATA +496 -0
  107. rdf_construct-0.3.0.dist-info/RECORD +110 -0
  108. rdf_construct-0.3.0.dist-info/WHEEL +4 -0
  109. rdf_construct-0.3.0.dist-info/entry_points.txt +3 -0
  110. 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
+ ]