rdf-construct 0.2.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 +1762 -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/main.py +6 -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/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.2.0.dist-info/METADATA +431 -0
- rdf_construct-0.2.0.dist-info/RECORD +88 -0
- rdf_construct-0.2.0.dist-info/WHEEL +4 -0
- rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
- rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""JSON formatter for diff output - designed for programmatic use and scripting."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rdflib import Graph, URIRef, Literal, BNode
|
|
7
|
+
from rdflib.term import Node
|
|
8
|
+
|
|
9
|
+
from rdf_construct.diff.change_types import (
|
|
10
|
+
EntityChange,
|
|
11
|
+
EntityType,
|
|
12
|
+
GraphDiff,
|
|
13
|
+
TripleChange,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_json(diff: GraphDiff, graph: Graph | None = None, indent: int = 2) -> str:
|
|
18
|
+
"""Format diff as JSON for programmatic use.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
diff: The diff result to format.
|
|
22
|
+
graph: Optional graph for CURIE formatting.
|
|
23
|
+
indent: JSON indentation (default 2).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
JSON formatted diff.
|
|
27
|
+
"""
|
|
28
|
+
result = _build_json_structure(diff, graph)
|
|
29
|
+
return json.dumps(result, indent=indent, ensure_ascii=False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_json_structure(diff: GraphDiff, graph: Graph | None = None) -> dict[str, Any]:
|
|
33
|
+
"""Build the JSON structure for a diff."""
|
|
34
|
+
return {
|
|
35
|
+
"comparison": {
|
|
36
|
+
"old": diff.old_path,
|
|
37
|
+
"new": diff.new_path,
|
|
38
|
+
},
|
|
39
|
+
"identical": diff.is_identical,
|
|
40
|
+
"added": _format_entity_group_json(diff.added, graph),
|
|
41
|
+
"removed": _format_entity_group_json(diff.removed, graph),
|
|
42
|
+
"modified": _format_modified_entities_json(diff.modified, graph),
|
|
43
|
+
"summary": diff.summary,
|
|
44
|
+
"warnings": _build_warnings(diff),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _format_entity_group_json(
|
|
49
|
+
entities: list[EntityChange], graph: Graph | None = None
|
|
50
|
+
) -> dict[str, list[dict]]:
|
|
51
|
+
"""Format a group of entities by type for JSON output."""
|
|
52
|
+
result: dict[str, list[dict]] = {
|
|
53
|
+
"classes": [],
|
|
54
|
+
"object_properties": [],
|
|
55
|
+
"datatype_properties": [],
|
|
56
|
+
"annotation_properties": [],
|
|
57
|
+
"individuals": [],
|
|
58
|
+
"other": [],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type_mapping = {
|
|
62
|
+
EntityType.CLASS: "classes",
|
|
63
|
+
EntityType.OBJECT_PROPERTY: "object_properties",
|
|
64
|
+
EntityType.DATATYPE_PROPERTY: "datatype_properties",
|
|
65
|
+
EntityType.ANNOTATION_PROPERTY: "annotation_properties",
|
|
66
|
+
EntityType.INDIVIDUAL: "individuals",
|
|
67
|
+
EntityType.ONTOLOGY: "other",
|
|
68
|
+
EntityType.OTHER: "other",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for entity in entities:
|
|
72
|
+
key = type_mapping.get(entity.entity_type, "other")
|
|
73
|
+
result[key].append(_format_entity_json(entity, graph))
|
|
74
|
+
|
|
75
|
+
# Remove empty categories
|
|
76
|
+
return {k: v for k, v in result.items() if v}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _format_entity_json(entity: EntityChange, graph: Graph | None = None) -> dict:
|
|
80
|
+
"""Format a single entity for JSON output."""
|
|
81
|
+
result = {
|
|
82
|
+
"uri": str(entity.uri),
|
|
83
|
+
"label": entity.label,
|
|
84
|
+
"type": entity.entity_type.value,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if entity.superclasses:
|
|
88
|
+
result["superclasses"] = [str(sc) for sc in entity.superclasses]
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _format_modified_entities_json(
|
|
94
|
+
entities: list[EntityChange], graph: Graph | None = None
|
|
95
|
+
) -> list[dict]:
|
|
96
|
+
"""Format modified entities with detailed changes."""
|
|
97
|
+
result = []
|
|
98
|
+
|
|
99
|
+
for entity in entities:
|
|
100
|
+
entity_dict = {
|
|
101
|
+
"uri": str(entity.uri),
|
|
102
|
+
"label": entity.label,
|
|
103
|
+
"type": entity.entity_type.value,
|
|
104
|
+
"changes": [],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Format added triples
|
|
108
|
+
for change in entity.added_triples:
|
|
109
|
+
entity_dict["changes"].append({
|
|
110
|
+
"action": "added",
|
|
111
|
+
"predicate": str(change.predicate),
|
|
112
|
+
"predicate_curie": _format_uri(change.predicate, graph),
|
|
113
|
+
"object": _format_object_json(change.object, graph),
|
|
114
|
+
"category": change.category.value,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
# Format removed triples
|
|
118
|
+
for change in entity.removed_triples:
|
|
119
|
+
entity_dict["changes"].append({
|
|
120
|
+
"action": "removed",
|
|
121
|
+
"predicate": str(change.predicate),
|
|
122
|
+
"predicate_curie": _format_uri(change.predicate, graph),
|
|
123
|
+
"object": _format_object_json(change.object, graph),
|
|
124
|
+
"category": change.category.value,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
result.append(entity_dict)
|
|
128
|
+
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _format_object_json(obj: Node, graph: Graph | None = None) -> dict:
|
|
133
|
+
"""Format an RDF object for JSON output."""
|
|
134
|
+
if isinstance(obj, URIRef):
|
|
135
|
+
return {
|
|
136
|
+
"type": "uri",
|
|
137
|
+
"value": str(obj),
|
|
138
|
+
"curie": _format_uri(obj, graph),
|
|
139
|
+
}
|
|
140
|
+
elif isinstance(obj, BNode):
|
|
141
|
+
return {
|
|
142
|
+
"type": "bnode",
|
|
143
|
+
"value": str(obj),
|
|
144
|
+
}
|
|
145
|
+
elif isinstance(obj, Literal):
|
|
146
|
+
result = {
|
|
147
|
+
"type": "literal",
|
|
148
|
+
"value": str(obj),
|
|
149
|
+
}
|
|
150
|
+
if obj.language:
|
|
151
|
+
result["language"] = obj.language
|
|
152
|
+
if obj.datatype:
|
|
153
|
+
result["datatype"] = str(obj.datatype)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
return {"type": "unknown", "value": str(obj)}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _format_uri(uri: URIRef | BNode, graph: Graph | None = None) -> str:
|
|
160
|
+
"""Format a URI as a CURIE if possible."""
|
|
161
|
+
if isinstance(uri, BNode):
|
|
162
|
+
return f"_:{uri}"
|
|
163
|
+
|
|
164
|
+
if graph is not None:
|
|
165
|
+
try:
|
|
166
|
+
curie = graph.namespace_manager.normalizeUri(uri)
|
|
167
|
+
if curie and not curie.startswith("<"):
|
|
168
|
+
return curie
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
uri_str = str(uri)
|
|
173
|
+
if "#" in uri_str:
|
|
174
|
+
return uri_str.split("#")[-1]
|
|
175
|
+
elif "/" in uri_str:
|
|
176
|
+
parts = uri_str.split("/")
|
|
177
|
+
return parts[-1] if parts[-1] else parts[-2]
|
|
178
|
+
|
|
179
|
+
return uri_str
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _build_warnings(diff: GraphDiff) -> list[str]:
|
|
183
|
+
"""Build list of warning messages."""
|
|
184
|
+
warnings = []
|
|
185
|
+
|
|
186
|
+
if diff.blank_node_warning:
|
|
187
|
+
warnings.append(
|
|
188
|
+
"Blank node changes were detected but not fully analysed. "
|
|
189
|
+
"Consider skolemising blank nodes for detailed comparison."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return warnings
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Markdown formatter for diff output - designed for release notes and changelogs."""
|
|
2
|
+
|
|
3
|
+
from rdflib import Graph, URIRef, Literal, BNode
|
|
4
|
+
from rdflib.term import Node
|
|
5
|
+
|
|
6
|
+
from rdf_construct.diff.change_types import (
|
|
7
|
+
EntityChange,
|
|
8
|
+
EntityType,
|
|
9
|
+
GraphDiff,
|
|
10
|
+
TripleChange,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Section headings for entity types
|
|
15
|
+
ENTITY_TYPE_HEADINGS = {
|
|
16
|
+
EntityType.CLASS: "Classes",
|
|
17
|
+
EntityType.OBJECT_PROPERTY: "Object Properties",
|
|
18
|
+
EntityType.DATATYPE_PROPERTY: "Datatype Properties",
|
|
19
|
+
EntityType.ANNOTATION_PROPERTY: "Annotation Properties",
|
|
20
|
+
EntityType.INDIVIDUAL: "Instances",
|
|
21
|
+
EntityType.ONTOLOGY: "Ontology Metadata",
|
|
22
|
+
EntityType.OTHER: "Other",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_markdown(diff: GraphDiff, graph: Graph | None = None) -> str:
|
|
27
|
+
"""Format diff as Markdown for release notes.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
diff: The diff result to format.
|
|
31
|
+
graph: Optional graph for CURIE formatting.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Markdown formatted diff.
|
|
35
|
+
"""
|
|
36
|
+
lines: list[str] = []
|
|
37
|
+
|
|
38
|
+
# Title
|
|
39
|
+
lines.append(f"# Ontology Changes: {diff.old_path} → {diff.new_path}")
|
|
40
|
+
lines.append("")
|
|
41
|
+
|
|
42
|
+
if diff.is_identical:
|
|
43
|
+
lines.append("No semantic differences found.")
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
# Summary at top
|
|
47
|
+
summary = diff.summary
|
|
48
|
+
lines.append("## Summary")
|
|
49
|
+
lines.append("")
|
|
50
|
+
lines.append(
|
|
51
|
+
f"- **{summary['added']}** entities added"
|
|
52
|
+
)
|
|
53
|
+
lines.append(
|
|
54
|
+
f"- **{summary['removed']}** entities removed"
|
|
55
|
+
)
|
|
56
|
+
lines.append(
|
|
57
|
+
f"- **{summary['modified']}** entities modified"
|
|
58
|
+
)
|
|
59
|
+
lines.append("")
|
|
60
|
+
|
|
61
|
+
# Added entities
|
|
62
|
+
if diff.added:
|
|
63
|
+
lines.append("## Added")
|
|
64
|
+
lines.append("")
|
|
65
|
+
lines.extend(_format_entity_group(diff.added, graph))
|
|
66
|
+
lines.append("")
|
|
67
|
+
|
|
68
|
+
# Removed entities
|
|
69
|
+
if diff.removed:
|
|
70
|
+
lines.append("## Removed")
|
|
71
|
+
lines.append("")
|
|
72
|
+
lines.extend(_format_entity_group(diff.removed, graph))
|
|
73
|
+
lines.append("")
|
|
74
|
+
|
|
75
|
+
# Modified entities
|
|
76
|
+
if diff.modified:
|
|
77
|
+
lines.append("## Modified")
|
|
78
|
+
lines.append("")
|
|
79
|
+
for entity in diff.modified:
|
|
80
|
+
lines.extend(_format_modified_entity_md(entity, graph))
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
# Blank node warning
|
|
84
|
+
if diff.blank_node_warning:
|
|
85
|
+
lines.append("---")
|
|
86
|
+
lines.append("")
|
|
87
|
+
lines.append(
|
|
88
|
+
"*Note: Blank node changes were detected but not fully analysed. "
|
|
89
|
+
"Consider skolemising blank nodes for detailed comparison.*"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _format_entity_group(
|
|
96
|
+
entities: list[EntityChange], graph: Graph | None = None
|
|
97
|
+
) -> list[str]:
|
|
98
|
+
"""Format a group of entities by type."""
|
|
99
|
+
lines: list[str] = []
|
|
100
|
+
|
|
101
|
+
# Group by entity type
|
|
102
|
+
by_type: dict[EntityType, list[EntityChange]] = {}
|
|
103
|
+
for entity in entities:
|
|
104
|
+
if entity.entity_type not in by_type:
|
|
105
|
+
by_type[entity.entity_type] = []
|
|
106
|
+
by_type[entity.entity_type].append(entity)
|
|
107
|
+
|
|
108
|
+
# Output each type group
|
|
109
|
+
for entity_type in EntityType:
|
|
110
|
+
if entity_type not in by_type:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
heading = ENTITY_TYPE_HEADINGS.get(entity_type, "Other")
|
|
114
|
+
lines.append(f"### {heading}")
|
|
115
|
+
lines.append("")
|
|
116
|
+
|
|
117
|
+
for entity in by_type[entity_type]:
|
|
118
|
+
lines.append(_format_entity_bullet(entity, graph))
|
|
119
|
+
|
|
120
|
+
lines.append("")
|
|
121
|
+
|
|
122
|
+
return lines
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _format_entity_bullet(entity: EntityChange, graph: Graph | None = None) -> str:
|
|
126
|
+
"""Format a single entity as a markdown bullet."""
|
|
127
|
+
uri_str = _format_uri(entity.uri, graph)
|
|
128
|
+
label = entity.label or uri_str
|
|
129
|
+
|
|
130
|
+
# Build the bullet
|
|
131
|
+
bullet = f"- **{label}**"
|
|
132
|
+
|
|
133
|
+
# Add superclass info for classes
|
|
134
|
+
if entity.superclasses:
|
|
135
|
+
superclass_strs = [_format_uri(sc, graph) for sc in entity.superclasses]
|
|
136
|
+
bullet += f" — subclass of {', '.join(superclass_strs)}"
|
|
137
|
+
|
|
138
|
+
return bullet
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _format_modified_entity_md(
|
|
142
|
+
entity: EntityChange, graph: Graph | None = None
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
"""Format a modified entity with detailed changes."""
|
|
145
|
+
lines: list[str] = []
|
|
146
|
+
|
|
147
|
+
uri_str = _format_uri(entity.uri, graph)
|
|
148
|
+
label = entity.label or uri_str
|
|
149
|
+
|
|
150
|
+
lines.append(f"### {label}")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
# Additions
|
|
154
|
+
if entity.added_triples:
|
|
155
|
+
lines.append("**Added:**")
|
|
156
|
+
lines.append("")
|
|
157
|
+
for change in entity.added_triples:
|
|
158
|
+
pred_str = _format_uri(change.predicate, graph)
|
|
159
|
+
obj_str = _format_object(change.object, graph)
|
|
160
|
+
lines.append(f"- `{pred_str}` → {obj_str}")
|
|
161
|
+
lines.append("")
|
|
162
|
+
|
|
163
|
+
# Removals
|
|
164
|
+
if entity.removed_triples:
|
|
165
|
+
lines.append("**Removed:**")
|
|
166
|
+
lines.append("")
|
|
167
|
+
for change in entity.removed_triples:
|
|
168
|
+
pred_str = _format_uri(change.predicate, graph)
|
|
169
|
+
obj_str = _format_object(change.object, graph)
|
|
170
|
+
lines.append(f"- ~~`{pred_str}` → {obj_str}~~")
|
|
171
|
+
lines.append("")
|
|
172
|
+
|
|
173
|
+
return lines
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _format_uri(uri: URIRef | BNode, graph: Graph | None = None) -> str:
|
|
177
|
+
"""Format a URI as a CURIE if possible."""
|
|
178
|
+
if isinstance(uri, BNode):
|
|
179
|
+
return f"_:{uri}"
|
|
180
|
+
|
|
181
|
+
if graph is not None:
|
|
182
|
+
try:
|
|
183
|
+
curie = graph.namespace_manager.normalizeUri(uri)
|
|
184
|
+
if curie and not curie.startswith("<"):
|
|
185
|
+
return curie
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
uri_str = str(uri)
|
|
190
|
+
if "#" in uri_str:
|
|
191
|
+
return uri_str.split("#")[-1]
|
|
192
|
+
elif "/" in uri_str:
|
|
193
|
+
parts = uri_str.split("/")
|
|
194
|
+
return parts[-1] if parts[-1] else parts[-2]
|
|
195
|
+
|
|
196
|
+
return uri_str
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _format_object(obj: Node, graph: Graph | None = None) -> str:
|
|
200
|
+
"""Format an RDF object (URI, BNode, or Literal)."""
|
|
201
|
+
if isinstance(obj, URIRef):
|
|
202
|
+
return f"`{_format_uri(obj, graph)}`"
|
|
203
|
+
elif isinstance(obj, BNode):
|
|
204
|
+
return f"`_:{obj}`"
|
|
205
|
+
elif isinstance(obj, Literal):
|
|
206
|
+
value = str(obj)
|
|
207
|
+
if obj.language:
|
|
208
|
+
return f'"{value}"@{obj.language}'
|
|
209
|
+
return f'"{value}"'
|
|
210
|
+
return str(obj)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Text formatter for diff output - designed for terminal display."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from rdflib import Graph, URIRef, Literal, BNode
|
|
6
|
+
from rdflib.term import Node
|
|
7
|
+
|
|
8
|
+
from rdf_construct.diff.change_types import (
|
|
9
|
+
EntityChange,
|
|
10
|
+
EntityType,
|
|
11
|
+
GraphDiff,
|
|
12
|
+
PredicateCategory,
|
|
13
|
+
TripleChange,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DiffFormatter(Protocol):
|
|
18
|
+
"""Protocol for diff formatters."""
|
|
19
|
+
|
|
20
|
+
def format(self, diff: GraphDiff, graph: Graph | None = None) -> str:
|
|
21
|
+
"""Format a diff result as a string.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
diff: The diff result to format.
|
|
25
|
+
graph: Optional graph for CURIE formatting.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Formatted string representation.
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Human-readable names for entity types
|
|
34
|
+
ENTITY_TYPE_NAMES = {
|
|
35
|
+
EntityType.CLASS: "Class",
|
|
36
|
+
EntityType.OBJECT_PROPERTY: "ObjectProperty",
|
|
37
|
+
EntityType.DATATYPE_PROPERTY: "DataProperty",
|
|
38
|
+
EntityType.ANNOTATION_PROPERTY: "AnnotationProperty",
|
|
39
|
+
EntityType.INDIVIDUAL: "Instance",
|
|
40
|
+
EntityType.ONTOLOGY: "Ontology",
|
|
41
|
+
EntityType.OTHER: "Entity",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_text(diff: GraphDiff, graph: Graph | None = None) -> str:
|
|
46
|
+
"""Format diff as plain text for terminal output.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
diff: The diff result to format.
|
|
50
|
+
graph: Optional graph for CURIE formatting.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Plain text diff representation.
|
|
54
|
+
"""
|
|
55
|
+
lines: list[str] = []
|
|
56
|
+
|
|
57
|
+
# Header
|
|
58
|
+
lines.append(f"Comparing {diff.old_path} → {diff.new_path}")
|
|
59
|
+
lines.append("")
|
|
60
|
+
|
|
61
|
+
if diff.is_identical:
|
|
62
|
+
lines.append("No semantic differences found.")
|
|
63
|
+
return "\n".join(lines)
|
|
64
|
+
|
|
65
|
+
# Added entities
|
|
66
|
+
if diff.added:
|
|
67
|
+
lines.append(f"ADDED ({len(diff.added)} entities):")
|
|
68
|
+
for entity in diff.added:
|
|
69
|
+
lines.append(_format_added_entity(entity, graph))
|
|
70
|
+
lines.append("")
|
|
71
|
+
|
|
72
|
+
# Removed entities
|
|
73
|
+
if diff.removed:
|
|
74
|
+
lines.append(f"REMOVED ({len(diff.removed)} entities):")
|
|
75
|
+
for entity in diff.removed:
|
|
76
|
+
lines.append(_format_removed_entity(entity, graph))
|
|
77
|
+
lines.append("")
|
|
78
|
+
|
|
79
|
+
# Modified entities
|
|
80
|
+
if diff.modified:
|
|
81
|
+
lines.append(f"MODIFIED ({len(diff.modified)} entities):")
|
|
82
|
+
for entity in diff.modified:
|
|
83
|
+
lines.extend(_format_modified_entity(entity, graph))
|
|
84
|
+
lines.append("")
|
|
85
|
+
|
|
86
|
+
# Summary
|
|
87
|
+
summary = diff.summary
|
|
88
|
+
lines.append(
|
|
89
|
+
f"Summary: {summary['added']} added, "
|
|
90
|
+
f"{summary['removed']} removed, "
|
|
91
|
+
f"{summary['modified']} modified"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Blank node warning
|
|
95
|
+
if diff.blank_node_warning:
|
|
96
|
+
lines.append("")
|
|
97
|
+
lines.append(
|
|
98
|
+
"Note: Blank node changes detected but not fully analysed. "
|
|
99
|
+
"Consider skolemising blank nodes for detailed comparison."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _format_uri(uri: URIRef | BNode, graph: Graph | None = None) -> str:
|
|
106
|
+
"""Format a URI as a CURIE if possible, or full URI otherwise."""
|
|
107
|
+
if isinstance(uri, BNode):
|
|
108
|
+
return f"_:{uri}"
|
|
109
|
+
|
|
110
|
+
if graph is not None:
|
|
111
|
+
try:
|
|
112
|
+
curie = graph.namespace_manager.normalizeUri(uri)
|
|
113
|
+
if curie and not curie.startswith("<"):
|
|
114
|
+
return curie
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Fallback: extract local name
|
|
119
|
+
uri_str = str(uri)
|
|
120
|
+
if "#" in uri_str:
|
|
121
|
+
return uri_str.split("#")[-1]
|
|
122
|
+
elif "/" in uri_str:
|
|
123
|
+
parts = uri_str.split("/")
|
|
124
|
+
return parts[-1] if parts[-1] else parts[-2]
|
|
125
|
+
|
|
126
|
+
return uri_str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _format_object(obj: Node, graph: Graph | None = None) -> str:
|
|
130
|
+
"""Format an RDF object (URI, BNode, or Literal)."""
|
|
131
|
+
if isinstance(obj, URIRef):
|
|
132
|
+
return _format_uri(obj, graph)
|
|
133
|
+
elif isinstance(obj, BNode):
|
|
134
|
+
return f"_:{obj}"
|
|
135
|
+
elif isinstance(obj, Literal):
|
|
136
|
+
value = str(obj)
|
|
137
|
+
if obj.language:
|
|
138
|
+
return f'"{value}"@{obj.language}'
|
|
139
|
+
elif obj.datatype:
|
|
140
|
+
dtype = _format_uri(obj.datatype, graph)
|
|
141
|
+
# Skip xsd:string as it's the default
|
|
142
|
+
if "string" in dtype.lower():
|
|
143
|
+
return f'"{value}"'
|
|
144
|
+
return f'"{value}"^^{dtype}'
|
|
145
|
+
return f'"{value}"'
|
|
146
|
+
return str(obj)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _format_added_entity(entity: EntityChange, graph: Graph | None = None) -> str:
|
|
150
|
+
"""Format a single added entity."""
|
|
151
|
+
type_name = ENTITY_TYPE_NAMES.get(entity.entity_type, "Entity")
|
|
152
|
+
uri_str = _format_uri(entity.uri, graph)
|
|
153
|
+
|
|
154
|
+
# Build description
|
|
155
|
+
desc = f" + {type_name} {uri_str}"
|
|
156
|
+
|
|
157
|
+
# Add superclass info for classes
|
|
158
|
+
if entity.superclasses:
|
|
159
|
+
superclass_strs = [_format_uri(sc, graph) for sc in entity.superclasses]
|
|
160
|
+
desc += f" (subclass of {', '.join(superclass_strs)})"
|
|
161
|
+
|
|
162
|
+
return desc
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _format_removed_entity(entity: EntityChange, graph: Graph | None = None) -> str:
|
|
166
|
+
"""Format a single removed entity."""
|
|
167
|
+
type_name = ENTITY_TYPE_NAMES.get(entity.entity_type, "Entity")
|
|
168
|
+
uri_str = _format_uri(entity.uri, graph)
|
|
169
|
+
|
|
170
|
+
return f" - {type_name} {uri_str}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _format_modified_entity(
|
|
174
|
+
entity: EntityChange, graph: Graph | None = None
|
|
175
|
+
) -> list[str]:
|
|
176
|
+
"""Format a modified entity with its changes."""
|
|
177
|
+
lines: list[str] = []
|
|
178
|
+
|
|
179
|
+
type_name = ENTITY_TYPE_NAMES.get(entity.entity_type, "Entity")
|
|
180
|
+
uri_str = _format_uri(entity.uri, graph)
|
|
181
|
+
|
|
182
|
+
lines.append(f" ~ {type_name} {uri_str}")
|
|
183
|
+
|
|
184
|
+
# Group and format changes
|
|
185
|
+
for change in entity.added_triples:
|
|
186
|
+
pred_str = _format_uri(change.predicate, graph)
|
|
187
|
+
obj_str = _format_object(change.object, graph)
|
|
188
|
+
lines.append(f" + {pred_str} {obj_str}")
|
|
189
|
+
|
|
190
|
+
for change in entity.removed_triples:
|
|
191
|
+
pred_str = _format_uri(change.predicate, graph)
|
|
192
|
+
obj_str = _format_object(change.object, graph)
|
|
193
|
+
lines.append(f" - {pred_str} {obj_str}")
|
|
194
|
+
|
|
195
|
+
return lines
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Documentation generation module for rdf-construct.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for generating comprehensive, navigable
|
|
4
|
+
documentation from RDF ontologies in HTML, Markdown, or JSON formats.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
from rdf_construct.docs import generate_docs
|
|
8
|
+
|
|
9
|
+
result = generate_docs(
|
|
10
|
+
source=Path("ontology.ttl"),
|
|
11
|
+
output_dir=Path("docs/"),
|
|
12
|
+
output_format="html",
|
|
13
|
+
)
|
|
14
|
+
print(f"Generated {result.total_pages} pages")
|
|
15
|
+
|
|
16
|
+
For more control, use the DocsGenerator class directly:
|
|
17
|
+
from rdf_construct.docs import DocsGenerator, DocsConfig
|
|
18
|
+
|
|
19
|
+
config = DocsConfig(
|
|
20
|
+
output_dir=Path("docs/"),
|
|
21
|
+
format="html",
|
|
22
|
+
include_instances=True,
|
|
23
|
+
include_search=True,
|
|
24
|
+
)
|
|
25
|
+
generator = DocsGenerator(config)
|
|
26
|
+
result = generator.generate_from_file(Path("ontology.ttl"))
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from rdf_construct.docs.config import DocsConfig, load_docs_config
|
|
30
|
+
from rdf_construct.docs.extractors import (
|
|
31
|
+
ClassInfo,
|
|
32
|
+
ExtractedEntities,
|
|
33
|
+
InstanceInfo,
|
|
34
|
+
OntologyInfo,
|
|
35
|
+
PropertyInfo,
|
|
36
|
+
extract_all,
|
|
37
|
+
)
|
|
38
|
+
from rdf_construct.docs.generator import DocsGenerator, GenerationResult, generate_docs
|
|
39
|
+
from rdf_construct.docs.search import SearchEntry, generate_search_index
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# Main interface
|
|
43
|
+
"generate_docs",
|
|
44
|
+
"DocsGenerator",
|
|
45
|
+
"GenerationResult",
|
|
46
|
+
# Configuration
|
|
47
|
+
"DocsConfig",
|
|
48
|
+
"load_docs_config",
|
|
49
|
+
# Data classes
|
|
50
|
+
"ClassInfo",
|
|
51
|
+
"PropertyInfo",
|
|
52
|
+
"InstanceInfo",
|
|
53
|
+
"OntologyInfo",
|
|
54
|
+
"ExtractedEntities",
|
|
55
|
+
# Extraction
|
|
56
|
+
"extract_all",
|
|
57
|
+
# Search
|
|
58
|
+
"SearchEntry",
|
|
59
|
+
"generate_search_index",
|
|
60
|
+
]
|