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.
Files changed (88) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +1762 -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/main.py +6 -0
  51. rdf_construct/puml2rdf/__init__.py +103 -0
  52. rdf_construct/puml2rdf/config.py +230 -0
  53. rdf_construct/puml2rdf/converter.py +420 -0
  54. rdf_construct/puml2rdf/merger.py +200 -0
  55. rdf_construct/puml2rdf/model.py +202 -0
  56. rdf_construct/puml2rdf/parser.py +565 -0
  57. rdf_construct/puml2rdf/validators.py +451 -0
  58. rdf_construct/shacl/__init__.py +56 -0
  59. rdf_construct/shacl/config.py +166 -0
  60. rdf_construct/shacl/converters.py +520 -0
  61. rdf_construct/shacl/generator.py +364 -0
  62. rdf_construct/shacl/namespaces.py +93 -0
  63. rdf_construct/stats/__init__.py +29 -0
  64. rdf_construct/stats/collector.py +178 -0
  65. rdf_construct/stats/comparator.py +298 -0
  66. rdf_construct/stats/formatters/__init__.py +83 -0
  67. rdf_construct/stats/formatters/json.py +38 -0
  68. rdf_construct/stats/formatters/markdown.py +153 -0
  69. rdf_construct/stats/formatters/text.py +186 -0
  70. rdf_construct/stats/metrics/__init__.py +26 -0
  71. rdf_construct/stats/metrics/basic.py +147 -0
  72. rdf_construct/stats/metrics/complexity.py +137 -0
  73. rdf_construct/stats/metrics/connectivity.py +130 -0
  74. rdf_construct/stats/metrics/documentation.py +128 -0
  75. rdf_construct/stats/metrics/hierarchy.py +207 -0
  76. rdf_construct/stats/metrics/properties.py +88 -0
  77. rdf_construct/uml/__init__.py +22 -0
  78. rdf_construct/uml/context.py +194 -0
  79. rdf_construct/uml/mapper.py +371 -0
  80. rdf_construct/uml/odm_renderer.py +789 -0
  81. rdf_construct/uml/renderer.py +684 -0
  82. rdf_construct/uml/uml_layout.py +393 -0
  83. rdf_construct/uml/uml_style.py +613 -0
  84. rdf_construct-0.2.0.dist-info/METADATA +431 -0
  85. rdf_construct-0.2.0.dist-info/RECORD +88 -0
  86. rdf_construct-0.2.0.dist-info/WHEEL +4 -0
  87. rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
  88. 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
+ ]