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,338 @@
|
|
|
1
|
+
"""Core comparison logic for semantic RDF graph diffing.
|
|
2
|
+
|
|
3
|
+
The algorithm:
|
|
4
|
+
1. Parse both graphs into rdflib Graph objects
|
|
5
|
+
2. Compute triple sets: added = new - old, removed = old - new
|
|
6
|
+
3. Group changes by subject
|
|
7
|
+
4. Classify entities as added/removed/modified
|
|
8
|
+
5. Determine entity types and extract metadata
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from rdflib import Graph, RDF, RDFS, URIRef, BNode, Literal
|
|
15
|
+
from rdflib.namespace import OWL
|
|
16
|
+
from rdflib.term import Node
|
|
17
|
+
|
|
18
|
+
from rdf_construct.diff.change_types import (
|
|
19
|
+
ChangeType,
|
|
20
|
+
EntityChange,
|
|
21
|
+
EntityType,
|
|
22
|
+
GraphDiff,
|
|
23
|
+
TripleChange,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compare_graphs(
|
|
28
|
+
old_graph: Graph,
|
|
29
|
+
new_graph: Graph,
|
|
30
|
+
old_path: str = "old",
|
|
31
|
+
new_path: str = "new",
|
|
32
|
+
ignore_predicates: set[URIRef] | None = None,
|
|
33
|
+
) -> GraphDiff:
|
|
34
|
+
"""Compare two RDF graphs and return semantic differences.
|
|
35
|
+
|
|
36
|
+
Uses set operations on triples to find changes, then groups by subject
|
|
37
|
+
and classifies each entity as added, removed, or modified.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
old_graph: The baseline/original graph.
|
|
41
|
+
new_graph: The new/updated graph.
|
|
42
|
+
old_path: Display name for the old graph.
|
|
43
|
+
new_path: Display name for the new graph.
|
|
44
|
+
ignore_predicates: Set of predicates to ignore in comparison.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
GraphDiff containing all semantic changes.
|
|
48
|
+
"""
|
|
49
|
+
ignore_predicates = ignore_predicates or set()
|
|
50
|
+
|
|
51
|
+
# Compute triple-level differences using set operations
|
|
52
|
+
old_triples = set(old_graph)
|
|
53
|
+
new_triples = set(new_graph)
|
|
54
|
+
|
|
55
|
+
added_triples = new_triples - old_triples
|
|
56
|
+
removed_triples = old_triples - new_triples
|
|
57
|
+
|
|
58
|
+
# Filter out ignored predicates
|
|
59
|
+
if ignore_predicates:
|
|
60
|
+
added_triples = {
|
|
61
|
+
(s, p, o) for s, p, o in added_triples if p not in ignore_predicates
|
|
62
|
+
}
|
|
63
|
+
removed_triples = {
|
|
64
|
+
(s, p, o) for s, p, o in removed_triples if p not in ignore_predicates
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Group changes by subject
|
|
68
|
+
changes_by_subject: dict[Node, dict[str, list[tuple]]] = defaultdict(
|
|
69
|
+
lambda: {"added": [], "removed": []}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for s, p, o in added_triples:
|
|
73
|
+
changes_by_subject[s]["added"].append((p, o))
|
|
74
|
+
|
|
75
|
+
for s, p, o in removed_triples:
|
|
76
|
+
changes_by_subject[s]["removed"].append((p, o))
|
|
77
|
+
|
|
78
|
+
# Track if we encountered blank nodes
|
|
79
|
+
has_blank_nodes = any(isinstance(s, BNode) for s in changes_by_subject)
|
|
80
|
+
|
|
81
|
+
# All subjects in each graph (for determining new vs removed entities)
|
|
82
|
+
old_subjects = {s for s, _, _ in old_graph}
|
|
83
|
+
new_subjects = {s for s, _, _ in new_graph}
|
|
84
|
+
|
|
85
|
+
# Classify each changed subject
|
|
86
|
+
added_entities: list[EntityChange] = []
|
|
87
|
+
removed_entities: list[EntityChange] = []
|
|
88
|
+
modified_entities: list[EntityChange] = []
|
|
89
|
+
|
|
90
|
+
for subject, changes in changes_by_subject.items():
|
|
91
|
+
# Skip blank nodes for deep analysis (just flag them)
|
|
92
|
+
if isinstance(subject, BNode):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
subject_in_old = subject in old_subjects
|
|
96
|
+
subject_in_new = subject in new_subjects
|
|
97
|
+
|
|
98
|
+
# Determine entity type from the appropriate graph
|
|
99
|
+
if subject_in_new:
|
|
100
|
+
entity_type = _determine_entity_type(new_graph, subject)
|
|
101
|
+
label = _get_label(new_graph, subject)
|
|
102
|
+
superclasses = _get_superclasses(new_graph, subject)
|
|
103
|
+
else:
|
|
104
|
+
entity_type = _determine_entity_type(old_graph, subject)
|
|
105
|
+
label = _get_label(old_graph, subject)
|
|
106
|
+
superclasses = _get_superclasses(old_graph, subject)
|
|
107
|
+
|
|
108
|
+
# Create triple changes
|
|
109
|
+
added_triple_changes = [
|
|
110
|
+
TripleChange(predicate=p, object=o, is_addition=True)
|
|
111
|
+
for p, o in changes["added"]
|
|
112
|
+
]
|
|
113
|
+
removed_triple_changes = [
|
|
114
|
+
TripleChange(predicate=p, object=o, is_addition=False)
|
|
115
|
+
for p, o in changes["removed"]
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
# Classify the entity change
|
|
119
|
+
if not subject_in_old and subject_in_new:
|
|
120
|
+
# New entity (all its triples are in added)
|
|
121
|
+
entity = EntityChange(
|
|
122
|
+
uri=subject,
|
|
123
|
+
entity_type=entity_type,
|
|
124
|
+
change_type=ChangeType.ADDED,
|
|
125
|
+
label=label,
|
|
126
|
+
added_triples=added_triple_changes,
|
|
127
|
+
removed_triples=[],
|
|
128
|
+
superclasses=superclasses,
|
|
129
|
+
)
|
|
130
|
+
added_entities.append(entity)
|
|
131
|
+
|
|
132
|
+
elif subject_in_old and not subject_in_new:
|
|
133
|
+
# Removed entity (all its triples are in removed)
|
|
134
|
+
entity = EntityChange(
|
|
135
|
+
uri=subject,
|
|
136
|
+
entity_type=entity_type,
|
|
137
|
+
change_type=ChangeType.REMOVED,
|
|
138
|
+
label=label,
|
|
139
|
+
added_triples=[],
|
|
140
|
+
removed_triples=removed_triple_changes,
|
|
141
|
+
superclasses=superclasses,
|
|
142
|
+
)
|
|
143
|
+
removed_entities.append(entity)
|
|
144
|
+
|
|
145
|
+
else:
|
|
146
|
+
# Modified entity (exists in both, but has changes)
|
|
147
|
+
entity = EntityChange(
|
|
148
|
+
uri=subject,
|
|
149
|
+
entity_type=entity_type,
|
|
150
|
+
change_type=ChangeType.MODIFIED,
|
|
151
|
+
label=label,
|
|
152
|
+
added_triples=added_triple_changes,
|
|
153
|
+
removed_triples=removed_triple_changes,
|
|
154
|
+
superclasses=superclasses,
|
|
155
|
+
)
|
|
156
|
+
modified_entities.append(entity)
|
|
157
|
+
|
|
158
|
+
# Sort entities by label/URI for consistent output
|
|
159
|
+
added_entities.sort(key=lambda e: e.label or str(e.uri))
|
|
160
|
+
removed_entities.sort(key=lambda e: e.label or str(e.uri))
|
|
161
|
+
modified_entities.sort(key=lambda e: e.label or str(e.uri))
|
|
162
|
+
|
|
163
|
+
return GraphDiff(
|
|
164
|
+
old_path=old_path,
|
|
165
|
+
new_path=new_path,
|
|
166
|
+
added=added_entities,
|
|
167
|
+
removed=removed_entities,
|
|
168
|
+
modified=modified_entities,
|
|
169
|
+
blank_node_warning=has_blank_nodes,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def compare_files(
|
|
174
|
+
old_path: Path,
|
|
175
|
+
new_path: Path,
|
|
176
|
+
ignore_predicates: set[URIRef] | None = None,
|
|
177
|
+
) -> GraphDiff:
|
|
178
|
+
"""Compare two RDF files and return semantic differences.
|
|
179
|
+
|
|
180
|
+
Convenience wrapper that handles file loading.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
old_path: Path to the baseline/original file.
|
|
184
|
+
new_path: Path to the new/updated file.
|
|
185
|
+
ignore_predicates: Set of predicates to ignore in comparison.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
GraphDiff containing all semantic changes.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
FileNotFoundError: If either file doesn't exist.
|
|
192
|
+
ValueError: If files can't be parsed as RDF.
|
|
193
|
+
"""
|
|
194
|
+
old_graph = _load_graph(old_path)
|
|
195
|
+
new_graph = _load_graph(new_path)
|
|
196
|
+
|
|
197
|
+
return compare_graphs(
|
|
198
|
+
old_graph,
|
|
199
|
+
new_graph,
|
|
200
|
+
old_path=old_path.name,
|
|
201
|
+
new_path=new_path.name,
|
|
202
|
+
ignore_predicates=ignore_predicates,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _load_graph(path: Path) -> Graph:
|
|
207
|
+
"""Load an RDF file into a graph, guessing format from extension.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
path: Path to the RDF file.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Loaded rdflib Graph.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
FileNotFoundError: If file doesn't exist.
|
|
217
|
+
ValueError: If file can't be parsed.
|
|
218
|
+
"""
|
|
219
|
+
if not path.exists():
|
|
220
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
221
|
+
|
|
222
|
+
graph = Graph()
|
|
223
|
+
|
|
224
|
+
# Guess format from extension
|
|
225
|
+
suffix = path.suffix.lower()
|
|
226
|
+
format_map = {
|
|
227
|
+
".ttl": "turtle",
|
|
228
|
+
".turtle": "turtle",
|
|
229
|
+
".rdf": "xml",
|
|
230
|
+
".xml": "xml",
|
|
231
|
+
".owl": "xml",
|
|
232
|
+
".nt": "nt",
|
|
233
|
+
".ntriples": "nt",
|
|
234
|
+
".n3": "n3",
|
|
235
|
+
".jsonld": "json-ld",
|
|
236
|
+
".json": "json-ld",
|
|
237
|
+
}
|
|
238
|
+
fmt = format_map.get(suffix, "turtle")
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
graph.parse(str(path), format=fmt)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
raise ValueError(f"Failed to parse {path}: {e}")
|
|
244
|
+
|
|
245
|
+
return graph
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _determine_entity_type(graph: Graph, subject: URIRef) -> EntityType:
|
|
249
|
+
"""Determine the semantic type of an entity.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
graph: The RDF graph containing the entity.
|
|
253
|
+
subject: The entity URI.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
The EntityType classification.
|
|
257
|
+
"""
|
|
258
|
+
types = set(graph.objects(subject, RDF.type))
|
|
259
|
+
|
|
260
|
+
# Check for ontology
|
|
261
|
+
if OWL.Ontology in types:
|
|
262
|
+
return EntityType.ONTOLOGY
|
|
263
|
+
|
|
264
|
+
# Check for classes
|
|
265
|
+
if OWL.Class in types or RDFS.Class in types:
|
|
266
|
+
return EntityType.CLASS
|
|
267
|
+
|
|
268
|
+
# Check for property types
|
|
269
|
+
if OWL.ObjectProperty in types:
|
|
270
|
+
return EntityType.OBJECT_PROPERTY
|
|
271
|
+
if OWL.DatatypeProperty in types:
|
|
272
|
+
return EntityType.DATATYPE_PROPERTY
|
|
273
|
+
if OWL.AnnotationProperty in types:
|
|
274
|
+
return EntityType.ANNOTATION_PROPERTY
|
|
275
|
+
if RDF.Property in types:
|
|
276
|
+
return EntityType.OBJECT_PROPERTY # Default property type
|
|
277
|
+
|
|
278
|
+
# If it has any type assertions (but not class/property), it's an individual
|
|
279
|
+
if types:
|
|
280
|
+
return EntityType.INDIVIDUAL
|
|
281
|
+
|
|
282
|
+
return EntityType.OTHER
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _get_label(graph: Graph, subject: URIRef) -> str | None:
|
|
286
|
+
"""Get a human-readable label for an entity.
|
|
287
|
+
|
|
288
|
+
Tries rdfs:label first, then skos:prefLabel, then falls back to
|
|
289
|
+
the local name from the URI.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
graph: The RDF graph containing the entity.
|
|
293
|
+
subject: The entity URI.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Label string or None.
|
|
297
|
+
"""
|
|
298
|
+
# Try rdfs:label
|
|
299
|
+
for label in graph.objects(subject, RDFS.label):
|
|
300
|
+
if isinstance(label, Literal):
|
|
301
|
+
return str(label)
|
|
302
|
+
|
|
303
|
+
# Try skos:prefLabel
|
|
304
|
+
SKOS = URIRef("http://www.w3.org/2004/02/skos/core#")
|
|
305
|
+
for label in graph.objects(subject, SKOS + "prefLabel"):
|
|
306
|
+
if isinstance(label, Literal):
|
|
307
|
+
return str(label)
|
|
308
|
+
|
|
309
|
+
# Fall back to local name from URI
|
|
310
|
+
if isinstance(subject, URIRef):
|
|
311
|
+
uri_str = str(subject)
|
|
312
|
+
if "#" in uri_str:
|
|
313
|
+
return uri_str.split("#")[-1]
|
|
314
|
+
elif "/" in uri_str:
|
|
315
|
+
return uri_str.split("/")[-1]
|
|
316
|
+
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _get_superclasses(graph: Graph, subject: URIRef) -> list[URIRef] | None:
|
|
321
|
+
"""Get direct superclasses of a class.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
graph: The RDF graph containing the entity.
|
|
325
|
+
subject: The entity URI.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
List of superclass URIs, or None if not a class.
|
|
329
|
+
"""
|
|
330
|
+
types = set(graph.objects(subject, RDF.type))
|
|
331
|
+
if OWL.Class not in types and RDFS.Class not in types:
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
superclasses = [
|
|
335
|
+
sc for sc in graph.objects(subject, RDFS.subClassOf) if isinstance(sc, URIRef)
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
return superclasses if superclasses else None
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Filtering logic for diff results.
|
|
2
|
+
|
|
3
|
+
Supports filtering by:
|
|
4
|
+
- Change type (added, removed, modified)
|
|
5
|
+
- Entity type (classes, properties, instances)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from rdf_construct.diff.change_types import ChangeType, EntityChange, EntityType, GraphDiff
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Maps CLI filter strings to internal types
|
|
12
|
+
CHANGE_TYPE_MAP = {
|
|
13
|
+
"added": ChangeType.ADDED,
|
|
14
|
+
"removed": ChangeType.REMOVED,
|
|
15
|
+
"modified": ChangeType.MODIFIED,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ENTITY_TYPE_MAP = {
|
|
19
|
+
"classes": EntityType.CLASS,
|
|
20
|
+
"class": EntityType.CLASS,
|
|
21
|
+
"object_properties": EntityType.OBJECT_PROPERTY,
|
|
22
|
+
"object-properties": EntityType.OBJECT_PROPERTY,
|
|
23
|
+
"objprops": EntityType.OBJECT_PROPERTY,
|
|
24
|
+
"datatype_properties": EntityType.DATATYPE_PROPERTY,
|
|
25
|
+
"datatype-properties": EntityType.DATATYPE_PROPERTY,
|
|
26
|
+
"dataprops": EntityType.DATATYPE_PROPERTY,
|
|
27
|
+
"annotation_properties": EntityType.ANNOTATION_PROPERTY,
|
|
28
|
+
"annotation-properties": EntityType.ANNOTATION_PROPERTY,
|
|
29
|
+
"annprops": EntityType.ANNOTATION_PROPERTY,
|
|
30
|
+
"properties": None, # Special: matches all property types
|
|
31
|
+
"individuals": EntityType.INDIVIDUAL,
|
|
32
|
+
"instances": EntityType.INDIVIDUAL,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Property types for the "properties" filter
|
|
36
|
+
PROPERTY_TYPES = {
|
|
37
|
+
EntityType.OBJECT_PROPERTY,
|
|
38
|
+
EntityType.DATATYPE_PROPERTY,
|
|
39
|
+
EntityType.ANNOTATION_PROPERTY,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def filter_diff(
|
|
44
|
+
diff: GraphDiff,
|
|
45
|
+
show_types: set[str] | None = None,
|
|
46
|
+
hide_types: set[str] | None = None,
|
|
47
|
+
entity_types: set[str] | None = None,
|
|
48
|
+
) -> GraphDiff:
|
|
49
|
+
"""Filter a GraphDiff by change types and entity types.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
diff: The diff to filter.
|
|
53
|
+
show_types: If provided, only include these change types.
|
|
54
|
+
hide_types: If provided, exclude these change types.
|
|
55
|
+
entity_types: If provided, only include these entity types.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A new GraphDiff with filtered results.
|
|
59
|
+
|
|
60
|
+
Note:
|
|
61
|
+
show_types and hide_types are mutually exclusive in practice,
|
|
62
|
+
but if both are provided, show_types is applied first.
|
|
63
|
+
"""
|
|
64
|
+
# Determine which change types to include
|
|
65
|
+
include_change_types = set(ChangeType)
|
|
66
|
+
|
|
67
|
+
if show_types:
|
|
68
|
+
include_change_types = {
|
|
69
|
+
CHANGE_TYPE_MAP[t.lower()]
|
|
70
|
+
for t in show_types
|
|
71
|
+
if t.lower() in CHANGE_TYPE_MAP
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if hide_types:
|
|
75
|
+
for t in hide_types:
|
|
76
|
+
if t.lower() in CHANGE_TYPE_MAP:
|
|
77
|
+
include_change_types.discard(CHANGE_TYPE_MAP[t.lower()])
|
|
78
|
+
|
|
79
|
+
# Determine which entity types to include
|
|
80
|
+
include_entity_types: set[EntityType] | None = None
|
|
81
|
+
if entity_types:
|
|
82
|
+
include_entity_types = set()
|
|
83
|
+
for et in entity_types:
|
|
84
|
+
et_lower = et.lower()
|
|
85
|
+
if et_lower in ENTITY_TYPE_MAP:
|
|
86
|
+
mapped = ENTITY_TYPE_MAP[et_lower]
|
|
87
|
+
if mapped is None: # "properties" special case
|
|
88
|
+
include_entity_types.update(PROPERTY_TYPES)
|
|
89
|
+
else:
|
|
90
|
+
include_entity_types.add(mapped)
|
|
91
|
+
|
|
92
|
+
# Filter the entities
|
|
93
|
+
def should_include(entity: EntityChange) -> bool:
|
|
94
|
+
if include_entity_types is not None:
|
|
95
|
+
if entity.entity_type not in include_entity_types:
|
|
96
|
+
return False
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
filtered_added = []
|
|
100
|
+
filtered_removed = []
|
|
101
|
+
filtered_modified = []
|
|
102
|
+
|
|
103
|
+
if ChangeType.ADDED in include_change_types:
|
|
104
|
+
filtered_added = [e for e in diff.added if should_include(e)]
|
|
105
|
+
|
|
106
|
+
if ChangeType.REMOVED in include_change_types:
|
|
107
|
+
filtered_removed = [e for e in diff.removed if should_include(e)]
|
|
108
|
+
|
|
109
|
+
if ChangeType.MODIFIED in include_change_types:
|
|
110
|
+
filtered_modified = [e for e in diff.modified if should_include(e)]
|
|
111
|
+
|
|
112
|
+
return GraphDiff(
|
|
113
|
+
old_path=diff.old_path,
|
|
114
|
+
new_path=diff.new_path,
|
|
115
|
+
added=filtered_added,
|
|
116
|
+
removed=filtered_removed,
|
|
117
|
+
modified=filtered_modified,
|
|
118
|
+
blank_node_warning=diff.blank_node_warning,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_filter_string(filter_str: str) -> set[str]:
|
|
123
|
+
"""Parse a comma-separated filter string into a set.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
filter_str: Comma-separated list (e.g., "added,removed")
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Set of individual filter terms.
|
|
130
|
+
"""
|
|
131
|
+
if not filter_str:
|
|
132
|
+
return set()
|
|
133
|
+
return {f.strip() for f in filter_str.split(",") if f.strip()}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Diff output formatters.
|
|
2
|
+
|
|
3
|
+
Available formatters:
|
|
4
|
+
- text: Plain text for terminal output
|
|
5
|
+
- markdown: Markdown for release notes
|
|
6
|
+
- json: JSON for programmatic use
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from rdflib import Graph
|
|
10
|
+
|
|
11
|
+
from rdf_construct.diff.change_types import GraphDiff
|
|
12
|
+
from rdf_construct.diff.formatters.text import format_text
|
|
13
|
+
from rdf_construct.diff.formatters.markdown import format_markdown
|
|
14
|
+
from rdf_construct.diff.formatters.json import format_json
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Format name to formatter function mapping
|
|
18
|
+
FORMATTERS = {
|
|
19
|
+
"text": format_text,
|
|
20
|
+
"markdown": format_markdown,
|
|
21
|
+
"md": format_markdown,
|
|
22
|
+
"json": format_json,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_formatter(format_name: str):
|
|
27
|
+
"""Get a formatter function by name.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
format_name: One of 'text', 'markdown', 'md', 'json'
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Formatter function.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
KeyError: If format_name is not recognized.
|
|
37
|
+
"""
|
|
38
|
+
name = format_name.lower()
|
|
39
|
+
if name not in FORMATTERS:
|
|
40
|
+
available = ", ".join(FORMATTERS.keys())
|
|
41
|
+
raise KeyError(f"Unknown format '{format_name}'. Available: {available}")
|
|
42
|
+
return FORMATTERS[name]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_diff(
|
|
46
|
+
diff: GraphDiff,
|
|
47
|
+
format_name: str = "text",
|
|
48
|
+
graph: Graph | None = None,
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Format a diff result using the specified formatter.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
diff: The diff result to format.
|
|
54
|
+
format_name: Output format ('text', 'markdown', 'json').
|
|
55
|
+
graph: Optional graph for CURIE formatting.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Formatted string.
|
|
59
|
+
"""
|
|
60
|
+
formatter = get_formatter(format_name)
|
|
61
|
+
return formatter(diff, graph)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"format_text",
|
|
66
|
+
"format_markdown",
|
|
67
|
+
"format_json",
|
|
68
|
+
"format_diff",
|
|
69
|
+
"get_formatter",
|
|
70
|
+
"FORMATTERS",
|
|
71
|
+
]
|