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,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."
|