rdf-construct 0.3.0__py3-none-any.whl → 0.4.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 +1 -1
- rdf_construct/cli.py +127 -0
- rdf_construct/describe/__init__.py +93 -0
- rdf_construct/describe/analyzer.py +176 -0
- rdf_construct/describe/documentation.py +146 -0
- rdf_construct/describe/formatters/__init__.py +47 -0
- rdf_construct/describe/formatters/json.py +65 -0
- rdf_construct/describe/formatters/markdown.py +275 -0
- rdf_construct/describe/formatters/text.py +315 -0
- rdf_construct/describe/hierarchy.py +232 -0
- rdf_construct/describe/imports.py +213 -0
- rdf_construct/describe/metadata.py +187 -0
- rdf_construct/describe/metrics.py +145 -0
- rdf_construct/describe/models.py +552 -0
- rdf_construct/describe/namespaces.py +180 -0
- rdf_construct/describe/profiles.py +415 -0
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/METADATA +28 -6
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/RECORD +21 -7
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/WHEEL +0 -0
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/entry_points.txt +0 -0
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Markdown formatter for ontology description output.
|
|
2
|
+
|
|
3
|
+
Produces GitHub/GitLab compatible Markdown for documentation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from rdf_construct.describe.models import (
|
|
7
|
+
OntologyDescription,
|
|
8
|
+
OntologyProfile,
|
|
9
|
+
NamespaceCategory,
|
|
10
|
+
ImportStatus,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_markdown(description: OntologyDescription) -> str:
|
|
15
|
+
"""Format ontology description as Markdown.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
description: OntologyDescription to format.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Markdown string.
|
|
22
|
+
"""
|
|
23
|
+
lines: list[str] = []
|
|
24
|
+
|
|
25
|
+
# Header
|
|
26
|
+
meta = description.metadata
|
|
27
|
+
title = meta.title or "Ontology Description"
|
|
28
|
+
lines.append(f"# {title}")
|
|
29
|
+
lines.append("")
|
|
30
|
+
|
|
31
|
+
# Summary box
|
|
32
|
+
lines.append("> **Verdict:** " + description.verdict)
|
|
33
|
+
lines.append("")
|
|
34
|
+
|
|
35
|
+
# Metadata section
|
|
36
|
+
lines.append("## Metadata")
|
|
37
|
+
lines.append("")
|
|
38
|
+
|
|
39
|
+
lines.append("| Property | Value |")
|
|
40
|
+
lines.append("|----------|-------|")
|
|
41
|
+
|
|
42
|
+
if meta.ontology_iri:
|
|
43
|
+
lines.append(f"| IRI | `{meta.ontology_iri}` |")
|
|
44
|
+
else:
|
|
45
|
+
lines.append("| IRI | *(not declared)* |")
|
|
46
|
+
|
|
47
|
+
if meta.version_iri:
|
|
48
|
+
lines.append(f"| Version IRI | `{meta.version_iri}` |")
|
|
49
|
+
if meta.version_info:
|
|
50
|
+
lines.append(f"| Version | {meta.version_info} |")
|
|
51
|
+
if meta.license_uri or meta.license_label:
|
|
52
|
+
license_str = meta.license_label or f"`{meta.license_uri}`"
|
|
53
|
+
lines.append(f"| License | {license_str} |")
|
|
54
|
+
if meta.creators:
|
|
55
|
+
lines.append(f"| Creator(s) | {', '.join(meta.creators)} |")
|
|
56
|
+
|
|
57
|
+
lines.append("")
|
|
58
|
+
|
|
59
|
+
if meta.description:
|
|
60
|
+
lines.append(f"**Description:** {meta.description}")
|
|
61
|
+
lines.append("")
|
|
62
|
+
|
|
63
|
+
# Metrics section
|
|
64
|
+
lines.append("## Metrics")
|
|
65
|
+
lines.append("")
|
|
66
|
+
|
|
67
|
+
m = description.metrics
|
|
68
|
+
lines.append("| Metric | Count |")
|
|
69
|
+
lines.append("|--------|-------|")
|
|
70
|
+
lines.append(f"| Triples | {m.total_triples:,} |")
|
|
71
|
+
lines.append(f"| Classes | {m.classes} |")
|
|
72
|
+
lines.append(f"| Properties | {m.total_properties} |")
|
|
73
|
+
|
|
74
|
+
if m.total_properties > 0:
|
|
75
|
+
parts = []
|
|
76
|
+
if m.object_properties:
|
|
77
|
+
parts.append(f"{m.object_properties} object")
|
|
78
|
+
if m.datatype_properties:
|
|
79
|
+
parts.append(f"{m.datatype_properties} datatype")
|
|
80
|
+
if m.annotation_properties:
|
|
81
|
+
parts.append(f"{m.annotation_properties} annotation")
|
|
82
|
+
if m.rdf_properties:
|
|
83
|
+
parts.append(f"{m.rdf_properties} rdf")
|
|
84
|
+
lines.append(f"| ↳ Breakdown | {', '.join(parts)} |")
|
|
85
|
+
|
|
86
|
+
lines.append(f"| Individuals | {m.individuals} |")
|
|
87
|
+
lines.append("")
|
|
88
|
+
|
|
89
|
+
# Profile section
|
|
90
|
+
lines.append("## Profile")
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
p = description.profile
|
|
94
|
+
profile_badge = _profile_badge(p.profile)
|
|
95
|
+
lines.append(f"**Detected:** {profile_badge}")
|
|
96
|
+
lines.append("")
|
|
97
|
+
lines.append(f"*{p.reasoning_guidance}*")
|
|
98
|
+
lines.append("")
|
|
99
|
+
|
|
100
|
+
if p.owl_constructs_found:
|
|
101
|
+
lines.append("**OWL Constructs:**")
|
|
102
|
+
lines.append("")
|
|
103
|
+
for construct in p.owl_constructs_found[:10]:
|
|
104
|
+
lines.append(f"- {construct}")
|
|
105
|
+
if len(p.owl_constructs_found) > 10:
|
|
106
|
+
lines.append(f"- *...and {len(p.owl_constructs_found) - 10} more*")
|
|
107
|
+
lines.append("")
|
|
108
|
+
|
|
109
|
+
if p.violating_constructs:
|
|
110
|
+
lines.append("⚠️ **DL Violations:**")
|
|
111
|
+
lines.append("")
|
|
112
|
+
for violation in p.violating_constructs:
|
|
113
|
+
lines.append(f"- {violation}")
|
|
114
|
+
lines.append("")
|
|
115
|
+
|
|
116
|
+
# Brief mode stops here
|
|
117
|
+
if description.brief:
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
# Namespace section
|
|
121
|
+
lines.append("## Namespaces")
|
|
122
|
+
lines.append("")
|
|
123
|
+
|
|
124
|
+
ns = description.namespaces
|
|
125
|
+
lines.append(f"- **Local:** {ns.local_count}")
|
|
126
|
+
lines.append(f"- **Imported:** {ns.imported_count}")
|
|
127
|
+
lines.append(f"- **External:** {ns.external_count}")
|
|
128
|
+
lines.append("")
|
|
129
|
+
|
|
130
|
+
if ns.local_namespace:
|
|
131
|
+
lines.append(f"Primary namespace: `{ns.local_namespace}`")
|
|
132
|
+
lines.append("")
|
|
133
|
+
|
|
134
|
+
# Top namespaces table
|
|
135
|
+
if ns.namespaces:
|
|
136
|
+
lines.append("### Top Namespaces by Usage")
|
|
137
|
+
lines.append("")
|
|
138
|
+
lines.append("| Prefix | Namespace | Usage | Category |")
|
|
139
|
+
lines.append("|--------|-----------|-------|----------|")
|
|
140
|
+
|
|
141
|
+
top_ns = sorted(ns.namespaces, key=lambda x: -x.usage_count)[:10]
|
|
142
|
+
for nsi in top_ns:
|
|
143
|
+
prefix = f"`{nsi.prefix}:`" if nsi.prefix else "-"
|
|
144
|
+
category = {
|
|
145
|
+
NamespaceCategory.LOCAL: "🏠 Local",
|
|
146
|
+
NamespaceCategory.IMPORTED: "📦 Imported",
|
|
147
|
+
NamespaceCategory.EXTERNAL: "🔗 External",
|
|
148
|
+
}[nsi.category]
|
|
149
|
+
lines.append(f"| {prefix} | `{nsi.uri}` | {nsi.usage_count} | {category} |")
|
|
150
|
+
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
if ns.unimported_external:
|
|
154
|
+
lines.append("### ⚠️ Unimported External Namespaces")
|
|
155
|
+
lines.append("")
|
|
156
|
+
lines.append("These namespaces are referenced but not declared via `owl:imports`:")
|
|
157
|
+
lines.append("")
|
|
158
|
+
for uri in ns.unimported_external:
|
|
159
|
+
lines.append(f"- `{uri}`")
|
|
160
|
+
lines.append("")
|
|
161
|
+
|
|
162
|
+
# Imports section
|
|
163
|
+
lines.append("## Imports")
|
|
164
|
+
lines.append("")
|
|
165
|
+
|
|
166
|
+
imp = description.imports
|
|
167
|
+
if imp.count == 0:
|
|
168
|
+
lines.append("*No imports declared.*")
|
|
169
|
+
lines.append("")
|
|
170
|
+
else:
|
|
171
|
+
lines.append(f"**{imp.count} direct import(s) declared:**")
|
|
172
|
+
lines.append("")
|
|
173
|
+
|
|
174
|
+
for imp_info in imp.imports:
|
|
175
|
+
if imp_info.status == ImportStatus.RESOLVABLE:
|
|
176
|
+
status = "✅"
|
|
177
|
+
elif imp_info.status == ImportStatus.UNRESOLVABLE:
|
|
178
|
+
status = "❌"
|
|
179
|
+
if imp_info.error:
|
|
180
|
+
status += f" ({imp_info.error})"
|
|
181
|
+
else:
|
|
182
|
+
status = "❓"
|
|
183
|
+
|
|
184
|
+
lines.append(f"- {status} `{imp_info.uri}`")
|
|
185
|
+
|
|
186
|
+
lines.append("")
|
|
187
|
+
|
|
188
|
+
# Hierarchy section
|
|
189
|
+
lines.append("## Class Hierarchy")
|
|
190
|
+
lines.append("")
|
|
191
|
+
|
|
192
|
+
h = description.hierarchy
|
|
193
|
+
lines.append(f"- **Root classes:** {h.root_count}")
|
|
194
|
+
lines.append(f"- **Maximum depth:** {h.max_depth}")
|
|
195
|
+
lines.append(f"- **Orphan classes:** {h.orphan_count}")
|
|
196
|
+
|
|
197
|
+
if h.has_cycles:
|
|
198
|
+
lines.append("")
|
|
199
|
+
lines.append("⚠️ **Cycles detected in hierarchy:**")
|
|
200
|
+
for member in h.cycle_members[:5]:
|
|
201
|
+
lines.append(f"- `{member}`")
|
|
202
|
+
|
|
203
|
+
lines.append("")
|
|
204
|
+
|
|
205
|
+
if h.root_classes and h.root_count <= 15:
|
|
206
|
+
lines.append("### Root Classes")
|
|
207
|
+
lines.append("")
|
|
208
|
+
for root in h.root_classes:
|
|
209
|
+
lines.append(f"- `{root}`")
|
|
210
|
+
lines.append("")
|
|
211
|
+
|
|
212
|
+
# Documentation section
|
|
213
|
+
lines.append("## Documentation Coverage")
|
|
214
|
+
lines.append("")
|
|
215
|
+
|
|
216
|
+
d = description.documentation
|
|
217
|
+
lines.append("| Entity | Labels | Definitions |")
|
|
218
|
+
lines.append("|--------|--------|-------------|")
|
|
219
|
+
|
|
220
|
+
class_label = f"{d.class_label_pct:.0f}% ({d.classes_with_label}/{d.classes_total})"
|
|
221
|
+
class_def = f"{d.class_definition_pct:.0f}% ({d.classes_with_definition}/{d.classes_total})"
|
|
222
|
+
lines.append(f"| Classes | {class_label} | {class_def} |")
|
|
223
|
+
|
|
224
|
+
if d.properties_total > 0:
|
|
225
|
+
prop_label = f"{d.property_label_pct:.0f}% ({d.properties_with_label}/{d.properties_total})"
|
|
226
|
+
prop_def = f"{d.property_definition_pct:.0f}% ({d.properties_with_definition}/{d.properties_total})"
|
|
227
|
+
lines.append(f"| Properties | {prop_label} | {prop_def} |")
|
|
228
|
+
|
|
229
|
+
lines.append("")
|
|
230
|
+
|
|
231
|
+
# Coverage assessment
|
|
232
|
+
overall = (d.class_label_pct + d.class_definition_pct) / 2
|
|
233
|
+
if overall >= 80:
|
|
234
|
+
lines.append("📗 **Well documented**")
|
|
235
|
+
elif overall >= 50:
|
|
236
|
+
lines.append("📙 **Partially documented**")
|
|
237
|
+
else:
|
|
238
|
+
lines.append("📕 **Needs documentation**")
|
|
239
|
+
|
|
240
|
+
lines.append("")
|
|
241
|
+
|
|
242
|
+
# Reasoning section (if included)
|
|
243
|
+
if description.reasoning is not None:
|
|
244
|
+
lines.append("## Reasoning Analysis")
|
|
245
|
+
lines.append("")
|
|
246
|
+
|
|
247
|
+
r = description.reasoning
|
|
248
|
+
lines.append(f"**Entailment regime:** {r.entailment_regime}")
|
|
249
|
+
lines.append("")
|
|
250
|
+
|
|
251
|
+
if r.consistency_notes:
|
|
252
|
+
lines.append("### Notes")
|
|
253
|
+
lines.append("")
|
|
254
|
+
for note in r.consistency_notes:
|
|
255
|
+
lines.append(f"- {note}")
|
|
256
|
+
lines.append("")
|
|
257
|
+
|
|
258
|
+
# Footer
|
|
259
|
+
lines.append("---")
|
|
260
|
+
lines.append(f"*Generated by rdf-construct describe at {description.timestamp.isoformat()}*")
|
|
261
|
+
lines.append("")
|
|
262
|
+
|
|
263
|
+
return "\n".join(lines)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _profile_badge(profile: OntologyProfile) -> str:
|
|
267
|
+
"""Get a badge/emoji for the profile level."""
|
|
268
|
+
badges = {
|
|
269
|
+
OntologyProfile.RDF: "📄 RDF",
|
|
270
|
+
OntologyProfile.RDFS: "📋 RDFS",
|
|
271
|
+
OntologyProfile.OWL_DL_SIMPLE: "🟢 OWL 2 DL (simple)",
|
|
272
|
+
OntologyProfile.OWL_DL_EXPRESSIVE: "🟡 OWL 2 DL (expressive)",
|
|
273
|
+
OntologyProfile.OWL_FULL: "🔴 OWL 2 Full",
|
|
274
|
+
}
|
|
275
|
+
return badges.get(profile, profile.display_name)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Text formatter for ontology description output.
|
|
2
|
+
|
|
3
|
+
Produces human-readable terminal output with optional colour.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from rdf_construct.describe.models import (
|
|
7
|
+
OntologyDescription,
|
|
8
|
+
OntologyProfile,
|
|
9
|
+
NamespaceCategory,
|
|
10
|
+
ImportStatus,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_text(
|
|
15
|
+
description: OntologyDescription,
|
|
16
|
+
use_colour: bool = True,
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Format ontology description as terminal-friendly text.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
description: OntologyDescription to format.
|
|
22
|
+
use_colour: Whether to include ANSI colour codes.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Formatted text string.
|
|
26
|
+
"""
|
|
27
|
+
lines: list[str] = []
|
|
28
|
+
|
|
29
|
+
# Header
|
|
30
|
+
lines.append(_header("Ontology Description", use_colour))
|
|
31
|
+
lines.append(f"Source: {description.source}")
|
|
32
|
+
lines.append("")
|
|
33
|
+
|
|
34
|
+
# Verdict (one-line summary)
|
|
35
|
+
lines.append(_label("Verdict", use_colour) + description.verdict)
|
|
36
|
+
lines.append("")
|
|
37
|
+
|
|
38
|
+
# Metadata section
|
|
39
|
+
lines.append(_section("Metadata", use_colour))
|
|
40
|
+
meta = description.metadata
|
|
41
|
+
|
|
42
|
+
if meta.ontology_iri:
|
|
43
|
+
lines.append(f" IRI: {meta.ontology_iri}")
|
|
44
|
+
else:
|
|
45
|
+
lines.append(f" IRI: {_dim('(not declared)', use_colour)}")
|
|
46
|
+
|
|
47
|
+
if meta.version_iri:
|
|
48
|
+
lines.append(f" Version IRI: {meta.version_iri}")
|
|
49
|
+
if meta.version_info:
|
|
50
|
+
lines.append(f" Version: {meta.version_info}")
|
|
51
|
+
|
|
52
|
+
if meta.title:
|
|
53
|
+
lines.append(f" Title: {meta.title}")
|
|
54
|
+
if meta.description:
|
|
55
|
+
# Truncate long descriptions
|
|
56
|
+
desc = meta.description
|
|
57
|
+
if len(desc) > 100:
|
|
58
|
+
desc = desc[:97] + "..."
|
|
59
|
+
lines.append(f" Description: {desc}")
|
|
60
|
+
|
|
61
|
+
if meta.license_uri or meta.license_label:
|
|
62
|
+
license_str = meta.license_label or meta.license_uri
|
|
63
|
+
lines.append(f" License: {license_str}")
|
|
64
|
+
|
|
65
|
+
if meta.creators:
|
|
66
|
+
lines.append(f" Creator(s): {', '.join(meta.creators)}")
|
|
67
|
+
|
|
68
|
+
lines.append("")
|
|
69
|
+
|
|
70
|
+
# Metrics section
|
|
71
|
+
lines.append(_section("Metrics", use_colour))
|
|
72
|
+
m = description.metrics
|
|
73
|
+
lines.append(f" Triples: {m.total_triples:,}")
|
|
74
|
+
lines.append(f" Classes: {m.classes}")
|
|
75
|
+
|
|
76
|
+
# Property breakdown
|
|
77
|
+
if m.total_properties > 0:
|
|
78
|
+
lines.append(f" Properties: {m.total_properties}")
|
|
79
|
+
if m.object_properties:
|
|
80
|
+
lines.append(f" Object: {m.object_properties}")
|
|
81
|
+
if m.datatype_properties:
|
|
82
|
+
lines.append(f" Datatype: {m.datatype_properties}")
|
|
83
|
+
if m.annotation_properties:
|
|
84
|
+
lines.append(f" Annotation: {m.annotation_properties}")
|
|
85
|
+
if m.rdf_properties:
|
|
86
|
+
lines.append(f" RDF: {m.rdf_properties}")
|
|
87
|
+
|
|
88
|
+
lines.append(f" Individuals: {m.individuals}")
|
|
89
|
+
lines.append("")
|
|
90
|
+
|
|
91
|
+
# Profile section
|
|
92
|
+
lines.append(_section("Profile", use_colour))
|
|
93
|
+
p = description.profile
|
|
94
|
+
profile_colour = _profile_colour(p.profile, use_colour)
|
|
95
|
+
lines.append(f" Detected: {profile_colour}")
|
|
96
|
+
lines.append(f" {_dim('(' + p.reasoning_guidance + ')', use_colour)}")
|
|
97
|
+
|
|
98
|
+
if p.owl_constructs_found:
|
|
99
|
+
lines.append(f" Constructs: {', '.join(p.owl_constructs_found[:5])}")
|
|
100
|
+
if len(p.owl_constructs_found) > 5:
|
|
101
|
+
lines.append(f" ...and {len(p.owl_constructs_found) - 5} more")
|
|
102
|
+
|
|
103
|
+
if p.violating_constructs:
|
|
104
|
+
lines.append(f" {_warn('DL Violations:', use_colour)} {', '.join(p.violating_constructs[:3])}")
|
|
105
|
+
|
|
106
|
+
lines.append("")
|
|
107
|
+
|
|
108
|
+
# Brief mode stops here
|
|
109
|
+
if description.brief:
|
|
110
|
+
return "\n".join(lines)
|
|
111
|
+
|
|
112
|
+
# Namespace section
|
|
113
|
+
lines.append(_section("Namespaces", use_colour))
|
|
114
|
+
ns = description.namespaces
|
|
115
|
+
|
|
116
|
+
if ns.local_namespace:
|
|
117
|
+
lines.append(f" Local: {ns.local_namespace}")
|
|
118
|
+
|
|
119
|
+
lines.append(f" Local: {ns.local_count}, Imported: {ns.imported_count}, External: {ns.external_count}")
|
|
120
|
+
|
|
121
|
+
# Show top namespaces by usage
|
|
122
|
+
top_ns = sorted(ns.namespaces, key=lambda x: -x.usage_count)[:5]
|
|
123
|
+
for nsi in top_ns:
|
|
124
|
+
prefix = f"{nsi.prefix}:" if nsi.prefix else ""
|
|
125
|
+
cat_label = {
|
|
126
|
+
NamespaceCategory.LOCAL: _green("[local]", use_colour),
|
|
127
|
+
NamespaceCategory.IMPORTED: _cyan("[imported]", use_colour),
|
|
128
|
+
NamespaceCategory.EXTERNAL: _dim("[external]", use_colour),
|
|
129
|
+
}[nsi.category]
|
|
130
|
+
lines.append(f" {prefix} {nsi.uri} ({nsi.usage_count} uses) {cat_label}")
|
|
131
|
+
|
|
132
|
+
if ns.unimported_external:
|
|
133
|
+
lines.append("")
|
|
134
|
+
lines.append(f" {_warn('Unimported external:', use_colour)}")
|
|
135
|
+
for uri in ns.unimported_external[:3]:
|
|
136
|
+
lines.append(f" - {uri}")
|
|
137
|
+
if len(ns.unimported_external) > 3:
|
|
138
|
+
lines.append(f" ...and {len(ns.unimported_external) - 3} more")
|
|
139
|
+
|
|
140
|
+
lines.append("")
|
|
141
|
+
|
|
142
|
+
# Imports section
|
|
143
|
+
lines.append(_section("Imports", use_colour))
|
|
144
|
+
imp = description.imports
|
|
145
|
+
|
|
146
|
+
if imp.count == 0:
|
|
147
|
+
lines.append(f" {_dim('No imports declared', use_colour)}")
|
|
148
|
+
else:
|
|
149
|
+
lines.append(f" Declared: {imp.count} (direct imports only)")
|
|
150
|
+
|
|
151
|
+
for imp_info in imp.imports:
|
|
152
|
+
if imp_info.status == ImportStatus.RESOLVABLE:
|
|
153
|
+
status = _green("✓", use_colour)
|
|
154
|
+
elif imp_info.status == ImportStatus.UNRESOLVABLE:
|
|
155
|
+
status = _red("✗", use_colour)
|
|
156
|
+
if imp_info.error:
|
|
157
|
+
status += f" {_dim(imp_info.error, use_colour)}"
|
|
158
|
+
else:
|
|
159
|
+
status = _dim("?", use_colour)
|
|
160
|
+
|
|
161
|
+
lines.append(f" {status} {imp_info.uri}")
|
|
162
|
+
|
|
163
|
+
lines.append("")
|
|
164
|
+
|
|
165
|
+
# Hierarchy section
|
|
166
|
+
lines.append(_section("Class Hierarchy", use_colour))
|
|
167
|
+
h = description.hierarchy
|
|
168
|
+
|
|
169
|
+
lines.append(f" Root classes: {h.root_count}")
|
|
170
|
+
if h.root_classes:
|
|
171
|
+
roots_display = ", ".join(h.root_classes[:5])
|
|
172
|
+
if len(h.root_classes) > 5:
|
|
173
|
+
roots_display += f", ...and {len(h.root_classes) - 5} more"
|
|
174
|
+
lines.append(f" {roots_display}")
|
|
175
|
+
|
|
176
|
+
lines.append(f" Maximum depth: {h.max_depth}")
|
|
177
|
+
lines.append(f" Orphan classes: {h.orphan_count}")
|
|
178
|
+
|
|
179
|
+
if h.has_cycles:
|
|
180
|
+
lines.append(f" {_warn('Cycles detected:', use_colour)} {', '.join(h.cycle_members[:3])}")
|
|
181
|
+
|
|
182
|
+
lines.append("")
|
|
183
|
+
|
|
184
|
+
# Documentation section
|
|
185
|
+
lines.append(_section("Documentation Coverage", use_colour))
|
|
186
|
+
d = description.documentation
|
|
187
|
+
|
|
188
|
+
# Classes
|
|
189
|
+
label_pct = d.class_label_pct
|
|
190
|
+
def_pct = d.class_definition_pct
|
|
191
|
+
label_bar = _progress_bar(label_pct, use_colour)
|
|
192
|
+
def_bar = _progress_bar(def_pct, use_colour)
|
|
193
|
+
|
|
194
|
+
lines.append(f" Classes with labels: {label_bar} {label_pct:.0f}% ({d.classes_with_label}/{d.classes_total})")
|
|
195
|
+
lines.append(f" Classes with definitions: {def_bar} {def_pct:.0f}% ({d.classes_with_definition}/{d.classes_total})")
|
|
196
|
+
|
|
197
|
+
# Properties
|
|
198
|
+
if d.properties_total > 0:
|
|
199
|
+
prop_label_pct = d.property_label_pct
|
|
200
|
+
prop_def_pct = d.property_definition_pct
|
|
201
|
+
prop_label_bar = _progress_bar(prop_label_pct, use_colour)
|
|
202
|
+
prop_def_bar = _progress_bar(prop_def_pct, use_colour)
|
|
203
|
+
|
|
204
|
+
lines.append(f" Properties with labels: {prop_label_bar} {prop_label_pct:.0f}% ({d.properties_with_label}/{d.properties_total})")
|
|
205
|
+
lines.append(f" Properties with definitions: {prop_def_bar} {prop_def_pct:.0f}% ({d.properties_with_definition}/{d.properties_total})")
|
|
206
|
+
|
|
207
|
+
lines.append("")
|
|
208
|
+
|
|
209
|
+
# Reasoning section (if included)
|
|
210
|
+
if description.reasoning is not None:
|
|
211
|
+
lines.append(_section("Reasoning Analysis", use_colour))
|
|
212
|
+
r = description.reasoning
|
|
213
|
+
lines.append(f" Entailment regime: {r.entailment_regime}")
|
|
214
|
+
|
|
215
|
+
if r.consistency_notes:
|
|
216
|
+
lines.append(f" Notes:")
|
|
217
|
+
for note in r.consistency_notes[:3]:
|
|
218
|
+
lines.append(f" - {note}")
|
|
219
|
+
|
|
220
|
+
lines.append("")
|
|
221
|
+
|
|
222
|
+
return "\n".join(lines)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _header(text: str, use_colour: bool) -> str:
|
|
226
|
+
"""Format a main header."""
|
|
227
|
+
if use_colour:
|
|
228
|
+
return f"\033[1;36m{text}\033[0m" # Bold cyan
|
|
229
|
+
return f"=== {text} ==="
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _section(text: str, use_colour: bool) -> str:
|
|
233
|
+
"""Format a section header."""
|
|
234
|
+
if use_colour:
|
|
235
|
+
return f"\033[1m{text}\033[0m" # Bold
|
|
236
|
+
return f"--- {text} ---"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _label(text: str, use_colour: bool) -> str:
|
|
240
|
+
"""Format a label."""
|
|
241
|
+
if use_colour:
|
|
242
|
+
return f"\033[1;33m{text}:\033[0m " # Bold yellow
|
|
243
|
+
return f"{text}: "
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _dim(text: str, use_colour: bool) -> str:
|
|
247
|
+
"""Format dim/grey text."""
|
|
248
|
+
if use_colour:
|
|
249
|
+
return f"\033[2m{text}\033[0m" # Dim
|
|
250
|
+
return text
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _green(text: str, use_colour: bool) -> str:
|
|
254
|
+
"""Format green text."""
|
|
255
|
+
if use_colour:
|
|
256
|
+
return f"\033[32m{text}\033[0m"
|
|
257
|
+
return text
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _red(text: str, use_colour: bool) -> str:
|
|
261
|
+
"""Format red text."""
|
|
262
|
+
if use_colour:
|
|
263
|
+
return f"\033[31m{text}\033[0m"
|
|
264
|
+
return text
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _cyan(text: str, use_colour: bool) -> str:
|
|
268
|
+
"""Format cyan text."""
|
|
269
|
+
if use_colour:
|
|
270
|
+
return f"\033[36m{text}\033[0m"
|
|
271
|
+
return text
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _warn(text: str, use_colour: bool) -> str:
|
|
275
|
+
"""Format warning text (yellow)."""
|
|
276
|
+
if use_colour:
|
|
277
|
+
return f"\033[33m{text}\033[0m"
|
|
278
|
+
return text
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _profile_colour(profile: OntologyProfile, use_colour: bool) -> str:
|
|
282
|
+
"""Get coloured profile name based on level."""
|
|
283
|
+
name = profile.display_name
|
|
284
|
+
|
|
285
|
+
if not use_colour:
|
|
286
|
+
return name
|
|
287
|
+
|
|
288
|
+
colour_map = {
|
|
289
|
+
OntologyProfile.RDF: "\033[2m", # Dim
|
|
290
|
+
OntologyProfile.RDFS: "\033[36m", # Cyan
|
|
291
|
+
OntologyProfile.OWL_DL_SIMPLE: "\033[32m", # Green
|
|
292
|
+
OntologyProfile.OWL_DL_EXPRESSIVE: "\033[33m", # Yellow
|
|
293
|
+
OntologyProfile.OWL_FULL: "\033[31m", # Red
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
colour = colour_map.get(profile, "")
|
|
297
|
+
return f"{colour}{name}\033[0m"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _progress_bar(percentage: float, use_colour: bool, width: int = 10) -> str:
|
|
301
|
+
"""Create a simple progress bar."""
|
|
302
|
+
filled = int(percentage / 100 * width)
|
|
303
|
+
empty = width - filled
|
|
304
|
+
|
|
305
|
+
bar = "█" * filled + "░" * empty
|
|
306
|
+
|
|
307
|
+
if use_colour:
|
|
308
|
+
if percentage >= 80:
|
|
309
|
+
return f"\033[32m{bar}\033[0m" # Green
|
|
310
|
+
elif percentage >= 50:
|
|
311
|
+
return f"\033[33m{bar}\033[0m" # Yellow
|
|
312
|
+
else:
|
|
313
|
+
return f"\033[31m{bar}\033[0m" # Red
|
|
314
|
+
|
|
315
|
+
return f"[{bar}]"
|