rdf-construct 0.2.1__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.
Files changed (43) hide show
  1. rdf_construct/__init__.py +1 -1
  2. rdf_construct/cli.py +1794 -0
  3. rdf_construct/describe/__init__.py +93 -0
  4. rdf_construct/describe/analyzer.py +176 -0
  5. rdf_construct/describe/documentation.py +146 -0
  6. rdf_construct/describe/formatters/__init__.py +47 -0
  7. rdf_construct/describe/formatters/json.py +65 -0
  8. rdf_construct/describe/formatters/markdown.py +275 -0
  9. rdf_construct/describe/formatters/text.py +315 -0
  10. rdf_construct/describe/hierarchy.py +232 -0
  11. rdf_construct/describe/imports.py +213 -0
  12. rdf_construct/describe/metadata.py +187 -0
  13. rdf_construct/describe/metrics.py +145 -0
  14. rdf_construct/describe/models.py +552 -0
  15. rdf_construct/describe/namespaces.py +180 -0
  16. rdf_construct/describe/profiles.py +415 -0
  17. rdf_construct/localise/__init__.py +114 -0
  18. rdf_construct/localise/config.py +508 -0
  19. rdf_construct/localise/extractor.py +427 -0
  20. rdf_construct/localise/formatters/__init__.py +36 -0
  21. rdf_construct/localise/formatters/markdown.py +229 -0
  22. rdf_construct/localise/formatters/text.py +224 -0
  23. rdf_construct/localise/merger.py +346 -0
  24. rdf_construct/localise/reporter.py +356 -0
  25. rdf_construct/merge/__init__.py +165 -0
  26. rdf_construct/merge/config.py +354 -0
  27. rdf_construct/merge/conflicts.py +281 -0
  28. rdf_construct/merge/formatters.py +426 -0
  29. rdf_construct/merge/merger.py +425 -0
  30. rdf_construct/merge/migrator.py +339 -0
  31. rdf_construct/merge/rules.py +377 -0
  32. rdf_construct/merge/splitter.py +1102 -0
  33. rdf_construct/refactor/__init__.py +72 -0
  34. rdf_construct/refactor/config.py +362 -0
  35. rdf_construct/refactor/deprecator.py +328 -0
  36. rdf_construct/refactor/formatters/__init__.py +8 -0
  37. rdf_construct/refactor/formatters/text.py +311 -0
  38. rdf_construct/refactor/renamer.py +294 -0
  39. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/METADATA +91 -6
  40. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/RECORD +43 -7
  41. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/WHEEL +0 -0
  42. {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/entry_points.txt +0 -0
  43. {rdf_construct-0.2.1.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}]"