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,362 @@
1
+ """Configuration dataclasses for the refactor command.
2
+
3
+ Defines configuration structures for:
4
+ - URI renaming (single and bulk namespace)
5
+ - Deprecation specifications
6
+ - Data migration settings
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum, auto
11
+ from pathlib import Path
12
+ from typing import Any, Literal
13
+
14
+ import yaml
15
+ from rdflib import Graph, URIRef
16
+
17
+
18
+ @dataclass
19
+ class RenameMapping:
20
+ """A single URI rename mapping.
21
+
22
+ Attributes:
23
+ from_uri: Source URI to rename.
24
+ to_uri: Target URI to rename to.
25
+ source: How this mapping was determined.
26
+ """
27
+
28
+ from_uri: URIRef
29
+ to_uri: URIRef
30
+ source: Literal["explicit", "namespace"]
31
+
32
+
33
+ @dataclass
34
+ class RenameConfig:
35
+ """Configuration for URI renaming operations.
36
+
37
+ Supports both explicit entity renames and bulk namespace remapping.
38
+ Namespace rules are applied first, then explicit entity renames,
39
+ allowing fine-grained overrides after namespace changes.
40
+
41
+ Attributes:
42
+ namespaces: Old namespace -> new namespace mappings.
43
+ entities: Explicit old URI -> new URI mappings.
44
+ """
45
+
46
+ namespaces: dict[str, str] = field(default_factory=dict)
47
+ entities: dict[str, str] = field(default_factory=dict)
48
+
49
+ def build_mappings(self, graph: Graph) -> list[RenameMapping]:
50
+ """Expand namespace rules to concrete URI mappings.
51
+
52
+ Scans the graph for all URIs and creates RenameMapping entries
53
+ for those matching namespace patterns. Explicit entity mappings
54
+ override any namespace-derived mappings.
55
+
56
+ Args:
57
+ graph: RDF graph to scan for URIs.
58
+
59
+ Returns:
60
+ List of RenameMapping objects.
61
+ """
62
+ mappings: dict[URIRef, RenameMapping] = {}
63
+
64
+ # Phase 1: Apply namespace mappings
65
+ if self.namespaces:
66
+ # Collect all URIs from the graph
67
+ all_uris: set[URIRef] = set()
68
+ for s, p, o in graph:
69
+ if isinstance(s, URIRef):
70
+ all_uris.add(s)
71
+ if isinstance(p, URIRef):
72
+ all_uris.add(p)
73
+ if isinstance(o, URIRef):
74
+ all_uris.add(o)
75
+
76
+ # Apply namespace mappings
77
+ for uri in all_uris:
78
+ uri_str = str(uri)
79
+ for old_ns, new_ns in self.namespaces.items():
80
+ if uri_str.startswith(old_ns):
81
+ new_uri_str = uri_str.replace(old_ns, new_ns, 1)
82
+ mappings[uri] = RenameMapping(
83
+ from_uri=uri,
84
+ to_uri=URIRef(new_uri_str),
85
+ source="namespace",
86
+ )
87
+ break
88
+
89
+ # Phase 2: Apply explicit entity mappings (override namespace)
90
+ for old_uri_str, new_uri_str in self.entities.items():
91
+ old_uri = URIRef(old_uri_str)
92
+ mappings[old_uri] = RenameMapping(
93
+ from_uri=old_uri,
94
+ to_uri=URIRef(new_uri_str),
95
+ source="explicit",
96
+ )
97
+
98
+ return list(mappings.values())
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict[str, Any]) -> "RenameConfig":
102
+ """Create from dictionary.
103
+
104
+ Args:
105
+ data: Dictionary with rename configuration.
106
+
107
+ Returns:
108
+ RenameConfig instance.
109
+ """
110
+ return cls(
111
+ namespaces=data.get("namespaces", {}),
112
+ entities=data.get("entities", {}),
113
+ )
114
+
115
+
116
+ @dataclass
117
+ class DeprecationSpec:
118
+ """Specification for deprecating a single entity.
119
+
120
+ Attributes:
121
+ entity: URI of entity to deprecate.
122
+ replaced_by: Optional URI of replacement entity.
123
+ message: Deprecation message for rdfs:comment.
124
+ version: Optional version when deprecated.
125
+ """
126
+
127
+ entity: str
128
+ replaced_by: str | None = None
129
+ message: str | None = None
130
+ version: str | None = None
131
+
132
+ @classmethod
133
+ def from_dict(cls, data: dict[str, Any]) -> "DeprecationSpec":
134
+ """Create from dictionary.
135
+
136
+ Args:
137
+ data: Dictionary with deprecation specification.
138
+
139
+ Returns:
140
+ DeprecationSpec instance.
141
+ """
142
+ return cls(
143
+ entity=data["entity"],
144
+ replaced_by=data.get("replaced_by"),
145
+ message=data.get("message"),
146
+ version=data.get("version"),
147
+ )
148
+
149
+
150
+ @dataclass
151
+ class DeprecationConfig:
152
+ """Configuration for bulk deprecation operations.
153
+
154
+ Attributes:
155
+ deprecations: List of deprecation specifications.
156
+ """
157
+
158
+ deprecations: list[DeprecationSpec] = field(default_factory=list)
159
+
160
+ @classmethod
161
+ def from_dict(cls, data: dict[str, Any]) -> "DeprecationConfig":
162
+ """Create from dictionary.
163
+
164
+ Args:
165
+ data: Dictionary with deprecation configuration.
166
+
167
+ Returns:
168
+ DeprecationConfig instance.
169
+ """
170
+ specs = [DeprecationSpec.from_dict(d) for d in data.get("deprecations", [])]
171
+ return cls(deprecations=specs)
172
+
173
+
174
+ @dataclass
175
+ class DataMigrationSpec:
176
+ """Specification for data graph migration.
177
+
178
+ Attributes:
179
+ sources: Paths to data files to migrate.
180
+ output_dir: Directory for migrated outputs.
181
+ output: Single output file (for merging all data).
182
+ """
183
+
184
+ sources: list[str] = field(default_factory=list)
185
+ output_dir: str | None = None
186
+ output: str | None = None
187
+
188
+
189
+ @dataclass
190
+ class RefactorConfig:
191
+ """Complete configuration for a refactor operation.
192
+
193
+ Can contain either rename or deprecation (or both) configurations.
194
+
195
+ Attributes:
196
+ rename: Rename configuration (namespaces and entities).
197
+ deprecations: List of deprecation specifications.
198
+ migrate_data: Optional data migration configuration.
199
+ source_files: Source ontology files to process.
200
+ output: Output file path (for single file).
201
+ output_dir: Output directory (for multiple files).
202
+ dry_run: If True, report what would happen without writing.
203
+ """
204
+
205
+ rename: RenameConfig | None = None
206
+ deprecations: list[DeprecationSpec] = field(default_factory=list)
207
+ migrate_data: DataMigrationSpec | None = None
208
+ source_files: list[Path] = field(default_factory=list)
209
+ output: Path | None = None
210
+ output_dir: Path | None = None
211
+ dry_run: bool = False
212
+
213
+ @classmethod
214
+ def from_yaml(cls, path: Path) -> "RefactorConfig":
215
+ """Load configuration from a YAML file.
216
+
217
+ Args:
218
+ path: Path to YAML configuration file.
219
+
220
+ Returns:
221
+ RefactorConfig instance.
222
+
223
+ Raises:
224
+ FileNotFoundError: If config file doesn't exist.
225
+ ValueError: If config is invalid.
226
+ """
227
+ if not path.exists():
228
+ raise FileNotFoundError(f"Config file not found: {path}")
229
+
230
+ with open(path) as f:
231
+ data = yaml.safe_load(f)
232
+
233
+ return cls.from_dict(data)
234
+
235
+ @classmethod
236
+ def from_dict(cls, data: dict[str, Any]) -> "RefactorConfig":
237
+ """Create from dictionary.
238
+
239
+ Args:
240
+ data: Dictionary with configuration.
241
+
242
+ Returns:
243
+ RefactorConfig instance.
244
+ """
245
+ # Parse rename config
246
+ rename = None
247
+ if "rename" in data:
248
+ rename = RenameConfig.from_dict(data["rename"])
249
+
250
+ # Parse deprecations
251
+ deprecations = []
252
+ if "deprecations" in data:
253
+ deprecations = [DeprecationSpec.from_dict(d) for d in data["deprecations"]]
254
+
255
+ # Parse data migration
256
+ migrate_data = None
257
+ if "migrate_data" in data:
258
+ mig = data["migrate_data"]
259
+ migrate_data = DataMigrationSpec(
260
+ sources=mig.get("sources", []),
261
+ output_dir=mig.get("output_dir"),
262
+ output=mig.get("output"),
263
+ )
264
+
265
+ # Parse source files
266
+ sources = [Path(p) for p in data.get("source_files", [])]
267
+
268
+ # Parse output
269
+ output = Path(data["output"]) if data.get("output") else None
270
+ output_dir = Path(data["output_dir"]) if data.get("output_dir") else None
271
+
272
+ return cls(
273
+ rename=rename,
274
+ deprecations=deprecations,
275
+ migrate_data=migrate_data,
276
+ source_files=sources,
277
+ output=output,
278
+ output_dir=output_dir,
279
+ dry_run=data.get("dry_run", False),
280
+ )
281
+
282
+
283
+ def load_refactor_config(path: Path) -> RefactorConfig:
284
+ """Load refactor configuration from a YAML file.
285
+
286
+ Args:
287
+ path: Path to configuration file.
288
+
289
+ Returns:
290
+ RefactorConfig instance.
291
+ """
292
+ return RefactorConfig.from_yaml(path)
293
+
294
+
295
+ def create_default_rename_config() -> str:
296
+ """Generate default rename configuration as YAML string.
297
+
298
+ Returns:
299
+ YAML configuration template.
300
+ """
301
+ return '''# rdf-construct refactor rename configuration
302
+ # See REFACTOR_GUIDE.md for full documentation
303
+
304
+ # Source files to process
305
+ source_files:
306
+ - ontology.ttl
307
+
308
+ # Output file
309
+ output: renamed.ttl
310
+
311
+ # Rename configuration
312
+ rename:
313
+ # Namespace mappings (applied first)
314
+ namespaces:
315
+ "http://old.example.org/v1#": "http://example.org/v2#"
316
+ # "http://temp.local/": "http://example.org/v2#"
317
+
318
+ # Individual entity renames (applied after namespace)
319
+ entities:
320
+ # Fix typos
321
+ # "http://example.org/v2#Buiding": "http://example.org/v2#Building"
322
+ # "http://example.org/v2#hasAddres": "http://example.org/v2#hasAddress"
323
+
324
+ # Optional data migration
325
+ # migrate_data:
326
+ # sources:
327
+ # - data/*.ttl
328
+ # output_dir: data/migrated/
329
+ '''
330
+
331
+
332
+ def create_default_deprecation_config() -> str:
333
+ """Generate default deprecation configuration as YAML string.
334
+
335
+ Returns:
336
+ YAML configuration template.
337
+ """
338
+ return '''# rdf-construct refactor deprecation configuration
339
+ # See REFACTOR_GUIDE.md for full documentation
340
+
341
+ # Source files to process
342
+ source_files:
343
+ - ontology.ttl
344
+
345
+ # Output file
346
+ output: deprecated.ttl
347
+
348
+ # Deprecation specifications
349
+ deprecations:
350
+ - entity: "http://example.org/ont#LegacyPerson"
351
+ replaced_by: "http://example.org/ont#Agent"
352
+ message: "Deprecated in v2.0. Use Agent for both people and organisations."
353
+ version: "2.0.0"
354
+
355
+ - entity: "http://example.org/ont#hasAddress"
356
+ replaced_by: "http://example.org/ont#locatedAt"
357
+ message: "Renamed for consistency with location vocabulary."
358
+
359
+ - entity: "http://example.org/ont#TemporaryClass"
360
+ # No replacement - just deprecated
361
+ message: "Experimental class removed. No replacement available."
362
+ '''
@@ -0,0 +1,328 @@
1
+ """Deprecation workflow for ontology entities.
2
+
3
+ This module handles marking ontology entities as deprecated:
4
+ - Adds owl:deprecated true
5
+ - Adds dcterms:isReplacedBy with replacement URI
6
+ - Updates rdfs:comment with deprecation notice
7
+ - Preserves all existing properties
8
+
9
+ Deprecation marks entities but does NOT rename or migrate references.
10
+ Use 'refactor rename' to actually migrate references after deprecation.
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, Namespace
18
+ from rdflib.namespace import RDF, RDFS, OWL, DCTERMS
19
+
20
+ from rdf_construct.refactor.config import DeprecationSpec, DeprecationConfig
21
+
22
+
23
+ # Dublin Core Terms namespace
24
+ DCTERMS = Namespace("http://purl.org/dc/terms/")
25
+
26
+
27
+ @dataclass
28
+ class EntityDeprecationInfo:
29
+ """Information about a deprecated entity.
30
+
31
+ Attributes:
32
+ uri: URI of the deprecated entity.
33
+ found: Whether the entity was found in the graph.
34
+ current_labels: Current rdfs:label values.
35
+ current_comments: Current rdfs:comment values.
36
+ was_already_deprecated: Whether owl:deprecated was already present.
37
+ triples_added: Number of triples added.
38
+ reference_count: Number of references to this entity in the graph.
39
+ replaced_by: Replacement entity URI (if specified).
40
+ message: Deprecation message added.
41
+ """
42
+
43
+ uri: str
44
+ found: bool = True
45
+ current_labels: list[str] = field(default_factory=list)
46
+ current_comments: list[str] = field(default_factory=list)
47
+ was_already_deprecated: bool = False
48
+ triples_added: int = 0
49
+ reference_count: int = 0
50
+ replaced_by: str | None = None
51
+ message: str | None = None
52
+
53
+
54
+ @dataclass
55
+ class DeprecationStats:
56
+ """Statistics from a deprecation operation.
57
+
58
+ Attributes:
59
+ entities_deprecated: Number of entities marked deprecated.
60
+ entities_not_found: Number of entities not found in graph.
61
+ entities_already_deprecated: Entities that were already deprecated.
62
+ triples_added: Total triples added.
63
+ """
64
+
65
+ entities_deprecated: int = 0
66
+ entities_not_found: int = 0
67
+ entities_already_deprecated: int = 0
68
+ triples_added: int = 0
69
+
70
+
71
+ @dataclass
72
+ class DeprecationResult:
73
+ """Result of a deprecation operation.
74
+
75
+ Attributes:
76
+ deprecated_graph: The graph with deprecations added.
77
+ stats: Deprecation statistics.
78
+ success: Whether the operation succeeded.
79
+ error: Error message if success is False.
80
+ entity_info: Detailed info about each entity processed.
81
+ source_triples: Original triple count.
82
+ result_triples: Final triple count.
83
+ """
84
+
85
+ deprecated_graph: Graph | None = None
86
+ stats: DeprecationStats = field(default_factory=DeprecationStats)
87
+ success: bool = True
88
+ error: str | None = None
89
+ entity_info: list[EntityDeprecationInfo] = field(default_factory=list)
90
+ source_triples: int = 0
91
+ result_triples: int = 0
92
+
93
+
94
+ class OntologyDeprecator:
95
+ """Marks ontology entities as deprecated.
96
+
97
+ Adds standard OWL deprecation annotations:
98
+ - owl:deprecated true
99
+ - dcterms:isReplacedBy (if replacement specified)
100
+ - Prepends "DEPRECATED: " to rdfs:comment
101
+
102
+ Example usage:
103
+ deprecator = OntologyDeprecator()
104
+ result = deprecator.deprecate(
105
+ graph,
106
+ entity="http://example.org/OldClass",
107
+ replaced_by="http://example.org/NewClass",
108
+ message="Use NewClass instead."
109
+ )
110
+ """
111
+
112
+ def deprecate(
113
+ self,
114
+ graph: Graph,
115
+ entity: str,
116
+ replaced_by: str | None = None,
117
+ message: str | None = None,
118
+ version: str | None = None,
119
+ ) -> DeprecationResult:
120
+ """Mark a single entity as deprecated.
121
+
122
+ Args:
123
+ graph: Source RDF graph (will be modified in-place).
124
+ entity: URI of entity to deprecate.
125
+ replaced_by: Optional URI of replacement entity.
126
+ message: Optional deprecation message.
127
+ version: Optional version when deprecated.
128
+
129
+ Returns:
130
+ DeprecationResult with updated graph.
131
+ """
132
+ result = DeprecationResult()
133
+ result.source_triples = len(graph)
134
+
135
+ entity_uri = URIRef(entity)
136
+ info = EntityDeprecationInfo(uri=entity)
137
+
138
+ # Check if entity exists in the graph
139
+ entity_exists = False
140
+ for s, p, o in graph:
141
+ if s == entity_uri:
142
+ entity_exists = True
143
+ break
144
+ if o == entity_uri:
145
+ info.reference_count += 1
146
+
147
+ if not entity_exists:
148
+ # Entity not found as subject - check if it's referenced
149
+ info.found = False
150
+ result.stats.entities_not_found += 1
151
+ result.entity_info.append(info)
152
+ result.deprecated_graph = graph
153
+ result.result_triples = len(graph)
154
+ return result
155
+
156
+ # Get current labels and comments
157
+ for label in graph.objects(entity_uri, RDFS.label):
158
+ if isinstance(label, Literal):
159
+ info.current_labels.append(str(label))
160
+
161
+ for comment in graph.objects(entity_uri, RDFS.comment):
162
+ if isinstance(comment, Literal):
163
+ info.current_comments.append(str(comment))
164
+
165
+ # Check if already deprecated
166
+ for obj in graph.objects(entity_uri, OWL.deprecated):
167
+ if str(obj).lower() == "true":
168
+ info.was_already_deprecated = True
169
+ result.stats.entities_already_deprecated += 1
170
+ break
171
+
172
+ # Add owl:deprecated true (if not already present)
173
+ if not info.was_already_deprecated:
174
+ graph.add((entity_uri, OWL.deprecated, Literal(True)))
175
+ info.triples_added += 1
176
+ result.stats.entities_deprecated += 1
177
+
178
+ # Add dcterms:isReplacedBy if replacement specified
179
+ if replaced_by:
180
+ replaced_by_uri = URIRef(replaced_by)
181
+ # Remove any existing isReplacedBy
182
+ graph.remove((entity_uri, DCTERMS.isReplacedBy, None))
183
+ graph.add((entity_uri, DCTERMS.isReplacedBy, replaced_by_uri))
184
+ info.triples_added += 1
185
+ info.replaced_by = replaced_by
186
+
187
+ # Add/update deprecation comment
188
+ if message:
189
+ # Build full deprecation message
190
+ deprecation_msg = f"DEPRECATED: {message}"
191
+ if version:
192
+ deprecation_msg = f"DEPRECATED (v{version}): {message}"
193
+ info.message = deprecation_msg
194
+
195
+ # Check if there's an existing comment to update
196
+ existing_deprecated_comment = None
197
+ for comment in list(graph.objects(entity_uri, RDFS.comment)):
198
+ if isinstance(comment, Literal) and str(comment).startswith("DEPRECATED"):
199
+ existing_deprecated_comment = comment
200
+ break
201
+
202
+ if existing_deprecated_comment:
203
+ # Remove old deprecation comment
204
+ graph.remove((entity_uri, RDFS.comment, existing_deprecated_comment))
205
+
206
+ # Add new deprecation comment
207
+ graph.add((entity_uri, RDFS.comment, Literal(deprecation_msg, lang="en")))
208
+ info.triples_added += 1
209
+
210
+ # Ensure dcterms namespace is bound
211
+ graph.bind("dcterms", DCTERMS, override=False)
212
+
213
+ result.stats.triples_added += info.triples_added
214
+ result.entity_info.append(info)
215
+ result.deprecated_graph = graph
216
+ result.result_triples = len(graph)
217
+ result.success = True
218
+
219
+ return result
220
+
221
+ def deprecate_bulk(
222
+ self,
223
+ graph: Graph,
224
+ specs: list[DeprecationSpec],
225
+ ) -> DeprecationResult:
226
+ """Mark multiple entities as deprecated.
227
+
228
+ Args:
229
+ graph: Source RDF graph (will be modified in-place).
230
+ specs: List of deprecation specifications.
231
+
232
+ Returns:
233
+ DeprecationResult with updated graph.
234
+ """
235
+ combined_result = DeprecationResult()
236
+ combined_result.source_triples = len(graph)
237
+
238
+ for spec in specs:
239
+ result = self.deprecate(
240
+ graph=graph,
241
+ entity=spec.entity,
242
+ replaced_by=spec.replaced_by,
243
+ message=spec.message,
244
+ version=spec.version,
245
+ )
246
+
247
+ # Combine stats
248
+ combined_result.stats.entities_deprecated += result.stats.entities_deprecated
249
+ combined_result.stats.entities_not_found += result.stats.entities_not_found
250
+ combined_result.stats.entities_already_deprecated += (
251
+ result.stats.entities_already_deprecated
252
+ )
253
+ combined_result.stats.triples_added += result.stats.triples_added
254
+
255
+ # Combine entity info
256
+ combined_result.entity_info.extend(result.entity_info)
257
+
258
+ combined_result.deprecated_graph = graph
259
+ combined_result.result_triples = len(graph)
260
+ combined_result.success = True
261
+
262
+ return combined_result
263
+
264
+
265
+ def deprecate_file(
266
+ source_path: Path,
267
+ output_path: Path,
268
+ specs: list[DeprecationSpec],
269
+ ) -> DeprecationResult:
270
+ """Convenience function to deprecate entities in a file.
271
+
272
+ Args:
273
+ source_path: Path to source RDF file.
274
+ output_path: Path to write updated output.
275
+ specs: List of deprecation specifications.
276
+
277
+ Returns:
278
+ DeprecationResult with statistics.
279
+ """
280
+ # Load source graph
281
+ graph = Graph()
282
+ try:
283
+ graph.parse(source_path.as_posix())
284
+ except Exception as e:
285
+ result = DeprecationResult()
286
+ result.success = False
287
+ result.error = f"Failed to parse {source_path}: {e}"
288
+ return result
289
+
290
+ # Perform deprecation
291
+ deprecator = OntologyDeprecator()
292
+ result = deprecator.deprecate_bulk(graph, specs)
293
+
294
+ if not result.success:
295
+ return result
296
+
297
+ # Write output
298
+ if result.deprecated_graph:
299
+ output_path.parent.mkdir(parents=True, exist_ok=True)
300
+ result.deprecated_graph.serialize(destination=output_path.as_posix(), format="turtle")
301
+
302
+ return result
303
+
304
+
305
+ def generate_deprecation_message(
306
+ replaced_by: str | None,
307
+ message: str | None,
308
+ version: str | None,
309
+ ) -> str:
310
+ """Generate a standard deprecation message.
311
+
312
+ Args:
313
+ replaced_by: Replacement entity URI.
314
+ message: Custom message.
315
+ version: Version when deprecated.
316
+
317
+ Returns:
318
+ Formatted deprecation message.
319
+ """
320
+ if message:
321
+ return message
322
+
323
+ if replaced_by:
324
+ # Extract local name from URI
325
+ local_name = replaced_by.split("#")[-1].split("/")[-1]
326
+ return f"Use {local_name} instead."
327
+
328
+ return "This entity is deprecated and should not be used."
@@ -0,0 +1,8 @@
1
+ """Formatters for refactor command output.
2
+
3
+ This package provides formatters for dry-run previews and result output.
4
+ """
5
+
6
+ from rdf_construct.refactor.formatters.text import TextFormatter
7
+
8
+ __all__ = ["TextFormatter"]