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.
- rdf_construct/__init__.py +1 -1
- rdf_construct/cli.py +1794 -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/localise/__init__.py +114 -0
- rdf_construct/localise/config.py +508 -0
- rdf_construct/localise/extractor.py +427 -0
- rdf_construct/localise/formatters/__init__.py +36 -0
- rdf_construct/localise/formatters/markdown.py +229 -0
- rdf_construct/localise/formatters/text.py +224 -0
- rdf_construct/localise/merger.py +346 -0
- rdf_construct/localise/reporter.py +356 -0
- rdf_construct/merge/__init__.py +165 -0
- rdf_construct/merge/config.py +354 -0
- rdf_construct/merge/conflicts.py +281 -0
- rdf_construct/merge/formatters.py +426 -0
- rdf_construct/merge/merger.py +425 -0
- rdf_construct/merge/migrator.py +339 -0
- rdf_construct/merge/rules.py +377 -0
- rdf_construct/merge/splitter.py +1102 -0
- rdf_construct/refactor/__init__.py +72 -0
- rdf_construct/refactor/config.py +362 -0
- rdf_construct/refactor/deprecator.py +328 -0
- rdf_construct/refactor/formatters/__init__.py +8 -0
- rdf_construct/refactor/formatters/text.py +311 -0
- rdf_construct/refactor/renamer.py +294 -0
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/METADATA +91 -6
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/RECORD +43 -7
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/WHEEL +0 -0
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/entry_points.txt +0 -0
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Basic metrics collection for ontology description.
|
|
2
|
+
|
|
3
|
+
Provides counts of classes, properties, individuals, and triples.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from rdflib import Graph, RDF, RDFS
|
|
7
|
+
from rdflib.namespace import OWL
|
|
8
|
+
|
|
9
|
+
from rdf_construct.describe.models import BasicMetrics
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def collect_metrics(graph: Graph) -> BasicMetrics:
|
|
13
|
+
"""Collect basic count metrics from an RDF graph.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
graph: RDF graph to analyse.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
BasicMetrics with all count values populated.
|
|
20
|
+
"""
|
|
21
|
+
# Total triples
|
|
22
|
+
total_triples = len(graph)
|
|
23
|
+
|
|
24
|
+
# Classes: both owl:Class and rdfs:Class (deduplicated)
|
|
25
|
+
owl_classes = set(graph.subjects(RDF.type, OWL.Class))
|
|
26
|
+
rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
|
|
27
|
+
all_classes = owl_classes | rdfs_classes
|
|
28
|
+
classes = len(all_classes)
|
|
29
|
+
|
|
30
|
+
# Object properties
|
|
31
|
+
object_properties = len(set(graph.subjects(RDF.type, OWL.ObjectProperty)))
|
|
32
|
+
|
|
33
|
+
# Datatype properties
|
|
34
|
+
datatype_properties = len(set(graph.subjects(RDF.type, OWL.DatatypeProperty)))
|
|
35
|
+
|
|
36
|
+
# Annotation properties
|
|
37
|
+
annotation_properties = len(set(graph.subjects(RDF.type, OWL.AnnotationProperty)))
|
|
38
|
+
|
|
39
|
+
# RDF properties (rdf:Property not typed as OWL property)
|
|
40
|
+
all_rdf_props = set(graph.subjects(RDF.type, RDF.Property))
|
|
41
|
+
owl_props = (
|
|
42
|
+
set(graph.subjects(RDF.type, OWL.ObjectProperty))
|
|
43
|
+
| set(graph.subjects(RDF.type, OWL.DatatypeProperty))
|
|
44
|
+
| set(graph.subjects(RDF.type, OWL.AnnotationProperty))
|
|
45
|
+
)
|
|
46
|
+
rdf_properties = len(all_rdf_props - owl_props)
|
|
47
|
+
|
|
48
|
+
# Individuals: typed subjects that aren't classes, properties, or ontology
|
|
49
|
+
individuals = _count_individuals(graph, all_classes)
|
|
50
|
+
|
|
51
|
+
return BasicMetrics(
|
|
52
|
+
total_triples=total_triples,
|
|
53
|
+
classes=classes,
|
|
54
|
+
object_properties=object_properties,
|
|
55
|
+
datatype_properties=datatype_properties,
|
|
56
|
+
annotation_properties=annotation_properties,
|
|
57
|
+
rdf_properties=rdf_properties,
|
|
58
|
+
individuals=individuals,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _count_individuals(graph: Graph, classes: set) -> int:
|
|
63
|
+
"""Count named individuals in the graph.
|
|
64
|
+
|
|
65
|
+
Individuals are typed subjects that are not classes, properties,
|
|
66
|
+
or ontology declarations.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
graph: RDF graph to analyse.
|
|
70
|
+
classes: Set of class URIs already identified.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Count of named individuals.
|
|
74
|
+
"""
|
|
75
|
+
# Gather all property URIs
|
|
76
|
+
properties = (
|
|
77
|
+
set(graph.subjects(RDF.type, OWL.ObjectProperty))
|
|
78
|
+
| set(graph.subjects(RDF.type, OWL.DatatypeProperty))
|
|
79
|
+
| set(graph.subjects(RDF.type, OWL.AnnotationProperty))
|
|
80
|
+
| set(graph.subjects(RDF.type, RDF.Property))
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Ontology declarations
|
|
84
|
+
ontologies = set(graph.subjects(RDF.type, OWL.Ontology))
|
|
85
|
+
|
|
86
|
+
# OWL constructs that shouldn't be counted as individuals
|
|
87
|
+
owl_constructs = {
|
|
88
|
+
OWL.Class,
|
|
89
|
+
OWL.ObjectProperty,
|
|
90
|
+
OWL.DatatypeProperty,
|
|
91
|
+
OWL.AnnotationProperty,
|
|
92
|
+
OWL.Restriction,
|
|
93
|
+
OWL.Ontology,
|
|
94
|
+
OWL.AllDisjointClasses,
|
|
95
|
+
OWL.AllDifferent,
|
|
96
|
+
OWL.NamedIndividual,
|
|
97
|
+
RDFS.Class,
|
|
98
|
+
RDF.Property,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Collect all typed subjects
|
|
102
|
+
all_typed = set()
|
|
103
|
+
for s in graph.subjects(RDF.type, None):
|
|
104
|
+
# Skip blank nodes for counting
|
|
105
|
+
if hasattr(s, "n3") and not str(s).startswith("_:"):
|
|
106
|
+
all_typed.add(s)
|
|
107
|
+
|
|
108
|
+
# Exclude non-individuals
|
|
109
|
+
individuals = all_typed - classes - properties - ontologies - owl_constructs
|
|
110
|
+
|
|
111
|
+
return len(individuals)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_all_classes(graph: Graph) -> set:
|
|
115
|
+
"""Get all classes from the graph.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
graph: RDF graph to query.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Set of class URIRefs (owl:Class and rdfs:Class).
|
|
122
|
+
"""
|
|
123
|
+
owl_classes = set(graph.subjects(RDF.type, OWL.Class))
|
|
124
|
+
rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
|
|
125
|
+
return owl_classes | rdfs_classes
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_all_properties(graph: Graph) -> set:
|
|
129
|
+
"""Get all properties from the graph.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
graph: RDF graph to query.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Set of property URIRefs (all types).
|
|
136
|
+
"""
|
|
137
|
+
props = set()
|
|
138
|
+
for prop_type in (
|
|
139
|
+
OWL.ObjectProperty,
|
|
140
|
+
OWL.DatatypeProperty,
|
|
141
|
+
OWL.AnnotationProperty,
|
|
142
|
+
RDF.Property,
|
|
143
|
+
):
|
|
144
|
+
props |= set(graph.subjects(RDF.type, prop_type))
|
|
145
|
+
return props
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""Data models for ontology description analysis.
|
|
2
|
+
|
|
3
|
+
Provides dataclasses representing all analysis results from the describe
|
|
4
|
+
command, designed for easy serialization to JSON and formatting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OntologyProfile(Enum):
|
|
15
|
+
"""Detected ontology profile/expressiveness level.
|
|
16
|
+
|
|
17
|
+
Ordered from least to most expressive. Each level implies
|
|
18
|
+
capability for reasoning at that level.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
RDF = "rdf"
|
|
22
|
+
RDFS = "rdfs"
|
|
23
|
+
OWL_DL_SIMPLE = "owl_dl_simple"
|
|
24
|
+
OWL_DL_EXPRESSIVE = "owl_dl_expressive"
|
|
25
|
+
OWL_FULL = "owl_full"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def display_name(self) -> str:
|
|
29
|
+
"""Human-readable name for the profile."""
|
|
30
|
+
names = {
|
|
31
|
+
OntologyProfile.RDF: "RDF",
|
|
32
|
+
OntologyProfile.RDFS: "RDFS",
|
|
33
|
+
OntologyProfile.OWL_DL_SIMPLE: "OWL 2 DL (simple)",
|
|
34
|
+
OntologyProfile.OWL_DL_EXPRESSIVE: "OWL 2 DL (expressive)",
|
|
35
|
+
OntologyProfile.OWL_FULL: "OWL 2 Full",
|
|
36
|
+
}
|
|
37
|
+
return names[self]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def reasoning_guidance(self) -> str:
|
|
41
|
+
"""Guidance on reasoning value for this profile."""
|
|
42
|
+
guidance = {
|
|
43
|
+
OntologyProfile.RDF: "No schema; reasoning not applicable",
|
|
44
|
+
OntologyProfile.RDFS: "Subclass/subproperty inference available",
|
|
45
|
+
OntologyProfile.OWL_DL_SIMPLE: "Standard DL reasoning; efficient",
|
|
46
|
+
OntologyProfile.OWL_DL_EXPRESSIVE: "Full DL reasoning; may be computationally expensive",
|
|
47
|
+
OntologyProfile.OWL_FULL: "Undecidable; reasoning may not terminate",
|
|
48
|
+
}
|
|
49
|
+
return guidance[self]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NamespaceCategory(Enum):
|
|
53
|
+
"""Category of namespace usage in the ontology."""
|
|
54
|
+
|
|
55
|
+
LOCAL = "local" # Defined in this ontology
|
|
56
|
+
IMPORTED = "imported" # Declared via owl:imports
|
|
57
|
+
EXTERNAL = "external" # Referenced but not imported
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ImportStatus(Enum):
|
|
61
|
+
"""Status of an import resolution check."""
|
|
62
|
+
|
|
63
|
+
RESOLVABLE = "resolvable"
|
|
64
|
+
UNRESOLVABLE = "unresolvable"
|
|
65
|
+
UNCHECKED = "unchecked"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class OntologyMetadata:
|
|
70
|
+
"""Extracted ontology metadata.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
ontology_iri: The ontology IRI (from owl:Ontology declaration).
|
|
74
|
+
version_iri: The version IRI (owl:versionIRI), if present.
|
|
75
|
+
title: Human-readable title from rdfs:label, dcterms:title, or dc:title.
|
|
76
|
+
description: Description from rdfs:comment, dcterms:description, or dc:description.
|
|
77
|
+
license_uri: License URI, if declared.
|
|
78
|
+
license_label: License label, if available.
|
|
79
|
+
creators: List of creator names/IRIs.
|
|
80
|
+
version_info: Version string from owl:versionInfo.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
ontology_iri: str | None = None
|
|
84
|
+
version_iri: str | None = None
|
|
85
|
+
title: str | None = None
|
|
86
|
+
description: str | None = None
|
|
87
|
+
license_uri: str | None = None
|
|
88
|
+
license_label: str | None = None
|
|
89
|
+
creators: list[str] = field(default_factory=list)
|
|
90
|
+
version_info: str | None = None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def has_iri(self) -> bool:
|
|
94
|
+
"""Whether an ontology IRI is declared."""
|
|
95
|
+
return self.ontology_iri is not None
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> dict[str, Any]:
|
|
98
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
99
|
+
return {
|
|
100
|
+
"ontology_iri": self.ontology_iri,
|
|
101
|
+
"version_iri": self.version_iri,
|
|
102
|
+
"title": self.title,
|
|
103
|
+
"description": self.description,
|
|
104
|
+
"license_uri": self.license_uri,
|
|
105
|
+
"license_label": self.license_label,
|
|
106
|
+
"creators": self.creators,
|
|
107
|
+
"version_info": self.version_info,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class BasicMetrics:
|
|
113
|
+
"""Basic ontology metrics.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
total_triples: Total triple count in the graph.
|
|
117
|
+
classes: Number of classes (owl:Class + rdfs:Class).
|
|
118
|
+
object_properties: Number of owl:ObjectProperty entities.
|
|
119
|
+
datatype_properties: Number of owl:DatatypeProperty entities.
|
|
120
|
+
annotation_properties: Number of owl:AnnotationProperty entities.
|
|
121
|
+
rdf_properties: Number of rdf:Property entities (not typed as OWL).
|
|
122
|
+
individuals: Number of named individuals.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
total_triples: int = 0
|
|
126
|
+
classes: int = 0
|
|
127
|
+
object_properties: int = 0
|
|
128
|
+
datatype_properties: int = 0
|
|
129
|
+
annotation_properties: int = 0
|
|
130
|
+
rdf_properties: int = 0
|
|
131
|
+
individuals: int = 0
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def total_properties(self) -> int:
|
|
135
|
+
"""Total count of all property types."""
|
|
136
|
+
return (
|
|
137
|
+
self.object_properties
|
|
138
|
+
+ self.datatype_properties
|
|
139
|
+
+ self.annotation_properties
|
|
140
|
+
+ self.rdf_properties
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def summary_line(self) -> str:
|
|
145
|
+
"""One-line summary of metrics."""
|
|
146
|
+
parts = [f"{self.total_triples} triples"]
|
|
147
|
+
if self.classes:
|
|
148
|
+
parts.append(f"{self.classes} classes")
|
|
149
|
+
if self.total_properties:
|
|
150
|
+
parts.append(f"{self.total_properties} properties")
|
|
151
|
+
if self.individuals:
|
|
152
|
+
parts.append(f"{self.individuals} individuals")
|
|
153
|
+
return ", ".join(parts)
|
|
154
|
+
|
|
155
|
+
def to_dict(self) -> dict[str, Any]:
|
|
156
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
157
|
+
return {
|
|
158
|
+
"total_triples": self.total_triples,
|
|
159
|
+
"classes": self.classes,
|
|
160
|
+
"object_properties": self.object_properties,
|
|
161
|
+
"datatype_properties": self.datatype_properties,
|
|
162
|
+
"annotation_properties": self.annotation_properties,
|
|
163
|
+
"rdf_properties": self.rdf_properties,
|
|
164
|
+
"individuals": self.individuals,
|
|
165
|
+
"total_properties": self.total_properties,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class ProfileDetection:
|
|
171
|
+
"""Result of ontology profile detection.
|
|
172
|
+
|
|
173
|
+
Attributes:
|
|
174
|
+
profile: Detected ontology profile.
|
|
175
|
+
detected_features: List of features that influenced detection.
|
|
176
|
+
owl_constructs_found: Specific OWL constructs found (for DL/Full distinction).
|
|
177
|
+
violating_constructs: Constructs that pushed profile to OWL Full.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
profile: OntologyProfile = OntologyProfile.RDF
|
|
181
|
+
detected_features: list[str] = field(default_factory=list)
|
|
182
|
+
owl_constructs_found: list[str] = field(default_factory=list)
|
|
183
|
+
violating_constructs: list[str] = field(default_factory=list)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def display_name(self) -> str:
|
|
187
|
+
"""Human-readable profile name."""
|
|
188
|
+
return self.profile.display_name
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def reasoning_guidance(self) -> str:
|
|
192
|
+
"""Guidance on reasoning value."""
|
|
193
|
+
return self.profile.reasoning_guidance
|
|
194
|
+
|
|
195
|
+
def to_dict(self) -> dict[str, Any]:
|
|
196
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
197
|
+
return {
|
|
198
|
+
"profile": self.profile.value,
|
|
199
|
+
"display_name": self.display_name,
|
|
200
|
+
"reasoning_guidance": self.reasoning_guidance,
|
|
201
|
+
"detected_features": self.detected_features,
|
|
202
|
+
"owl_constructs_found": self.owl_constructs_found,
|
|
203
|
+
"violating_constructs": self.violating_constructs,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class NamespaceInfo:
|
|
209
|
+
"""Information about a namespace used in the ontology.
|
|
210
|
+
|
|
211
|
+
Attributes:
|
|
212
|
+
uri: The namespace URI.
|
|
213
|
+
prefix: Bound prefix (if any).
|
|
214
|
+
category: Whether local, imported, or external.
|
|
215
|
+
usage_count: Number of times URIs from this namespace appear.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
uri: str
|
|
219
|
+
prefix: str | None = None
|
|
220
|
+
category: NamespaceCategory = NamespaceCategory.EXTERNAL
|
|
221
|
+
usage_count: int = 0
|
|
222
|
+
|
|
223
|
+
def to_dict(self) -> dict[str, Any]:
|
|
224
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
225
|
+
return {
|
|
226
|
+
"uri": self.uri,
|
|
227
|
+
"prefix": self.prefix,
|
|
228
|
+
"category": self.category.value,
|
|
229
|
+
"usage_count": self.usage_count,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class NamespaceAnalysis:
|
|
235
|
+
"""Complete namespace analysis for the ontology.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
local_namespace: The primary/local namespace (if identifiable).
|
|
239
|
+
namespaces: List of all namespaces found.
|
|
240
|
+
unimported_external: Namespaces that are used but not imported.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
local_namespace: str | None = None
|
|
244
|
+
namespaces: list[NamespaceInfo] = field(default_factory=list)
|
|
245
|
+
unimported_external: list[str] = field(default_factory=list)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def local_count(self) -> int:
|
|
249
|
+
"""Number of local namespaces."""
|
|
250
|
+
return sum(1 for ns in self.namespaces if ns.category == NamespaceCategory.LOCAL)
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def imported_count(self) -> int:
|
|
254
|
+
"""Number of imported namespaces."""
|
|
255
|
+
return sum(1 for ns in self.namespaces if ns.category == NamespaceCategory.IMPORTED)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def external_count(self) -> int:
|
|
259
|
+
"""Number of external (unimported) namespaces."""
|
|
260
|
+
return sum(1 for ns in self.namespaces if ns.category == NamespaceCategory.EXTERNAL)
|
|
261
|
+
|
|
262
|
+
def to_dict(self) -> dict[str, Any]:
|
|
263
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
264
|
+
return {
|
|
265
|
+
"local_namespace": self.local_namespace,
|
|
266
|
+
"namespaces": [ns.to_dict() for ns in self.namespaces],
|
|
267
|
+
"unimported_external": self.unimported_external,
|
|
268
|
+
"counts": {
|
|
269
|
+
"local": self.local_count,
|
|
270
|
+
"imported": self.imported_count,
|
|
271
|
+
"external": self.external_count,
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class ImportInfo:
|
|
278
|
+
"""Information about a declared import.
|
|
279
|
+
|
|
280
|
+
Attributes:
|
|
281
|
+
uri: The imported ontology URI.
|
|
282
|
+
status: Resolution status.
|
|
283
|
+
error: Error message if unresolvable.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
uri: str
|
|
287
|
+
status: ImportStatus = ImportStatus.UNCHECKED
|
|
288
|
+
error: str | None = None
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def is_resolvable(self) -> bool:
|
|
292
|
+
"""Whether the import was resolved successfully."""
|
|
293
|
+
return self.status == ImportStatus.RESOLVABLE
|
|
294
|
+
|
|
295
|
+
def to_dict(self) -> dict[str, Any]:
|
|
296
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
297
|
+
return {
|
|
298
|
+
"uri": self.uri,
|
|
299
|
+
"status": self.status.value,
|
|
300
|
+
"error": self.error,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass
|
|
305
|
+
class ImportAnalysis:
|
|
306
|
+
"""Analysis of ontology imports.
|
|
307
|
+
|
|
308
|
+
Attributes:
|
|
309
|
+
imports: List of declared imports with resolution status.
|
|
310
|
+
resolve_attempted: Whether resolution was attempted.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
imports: list[ImportInfo] = field(default_factory=list)
|
|
314
|
+
resolve_attempted: bool = False
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def count(self) -> int:
|
|
318
|
+
"""Number of declared imports."""
|
|
319
|
+
return len(self.imports)
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def resolvable_count(self) -> int:
|
|
323
|
+
"""Number of resolvable imports."""
|
|
324
|
+
return sum(1 for imp in self.imports if imp.is_resolvable)
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def unresolvable_count(self) -> int:
|
|
328
|
+
"""Number of unresolvable imports."""
|
|
329
|
+
return sum(
|
|
330
|
+
1 for imp in self.imports if imp.status == ImportStatus.UNRESOLVABLE
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def to_dict(self) -> dict[str, Any]:
|
|
334
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
335
|
+
return {
|
|
336
|
+
"imports": [imp.to_dict() for imp in self.imports],
|
|
337
|
+
"resolve_attempted": self.resolve_attempted,
|
|
338
|
+
"counts": {
|
|
339
|
+
"total": self.count,
|
|
340
|
+
"resolvable": self.resolvable_count if self.resolve_attempted else None,
|
|
341
|
+
"unresolvable": self.unresolvable_count if self.resolve_attempted else None,
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@dataclass
|
|
347
|
+
class HierarchyAnalysis:
|
|
348
|
+
"""Class hierarchy analysis.
|
|
349
|
+
|
|
350
|
+
Attributes:
|
|
351
|
+
root_classes: Classes with no superclass (except owl:Thing/rdfs:Resource).
|
|
352
|
+
max_depth: Maximum depth of the hierarchy.
|
|
353
|
+
orphan_classes: Classes that are neither parent nor child of anything.
|
|
354
|
+
has_cycles: Whether cycles were detected.
|
|
355
|
+
cycle_members: URIs involved in cycles (if any).
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
root_classes: list[str] = field(default_factory=list)
|
|
359
|
+
max_depth: int = 0
|
|
360
|
+
orphan_classes: list[str] = field(default_factory=list)
|
|
361
|
+
has_cycles: bool = False
|
|
362
|
+
cycle_members: list[str] = field(default_factory=list)
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def root_count(self) -> int:
|
|
366
|
+
"""Number of root classes."""
|
|
367
|
+
return len(self.root_classes)
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def orphan_count(self) -> int:
|
|
371
|
+
"""Number of orphan classes."""
|
|
372
|
+
return len(self.orphan_classes)
|
|
373
|
+
|
|
374
|
+
def to_dict(self) -> dict[str, Any]:
|
|
375
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
376
|
+
return {
|
|
377
|
+
"root_classes": self.root_classes,
|
|
378
|
+
"root_count": self.root_count,
|
|
379
|
+
"max_depth": self.max_depth,
|
|
380
|
+
"orphan_classes": self.orphan_classes,
|
|
381
|
+
"orphan_count": self.orphan_count,
|
|
382
|
+
"has_cycles": self.has_cycles,
|
|
383
|
+
"cycle_members": self.cycle_members,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass
|
|
388
|
+
class DocumentationCoverage:
|
|
389
|
+
"""Documentation coverage metrics.
|
|
390
|
+
|
|
391
|
+
Attributes:
|
|
392
|
+
classes_with_label: Number of classes with rdfs:label.
|
|
393
|
+
classes_total: Total number of classes.
|
|
394
|
+
classes_with_definition: Number of classes with rdfs:comment or skos:definition.
|
|
395
|
+
properties_with_label: Number of properties with rdfs:label.
|
|
396
|
+
properties_total: Total number of properties.
|
|
397
|
+
properties_with_definition: Number of properties with definitions.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
classes_with_label: int = 0
|
|
401
|
+
classes_total: int = 0
|
|
402
|
+
classes_with_definition: int = 0
|
|
403
|
+
properties_with_label: int = 0
|
|
404
|
+
properties_total: int = 0
|
|
405
|
+
properties_with_definition: int = 0
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def class_label_pct(self) -> float:
|
|
409
|
+
"""Percentage of classes with labels."""
|
|
410
|
+
if self.classes_total == 0:
|
|
411
|
+
return 100.0
|
|
412
|
+
return (self.classes_with_label / self.classes_total) * 100
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def class_definition_pct(self) -> float:
|
|
416
|
+
"""Percentage of classes with definitions."""
|
|
417
|
+
if self.classes_total == 0:
|
|
418
|
+
return 100.0
|
|
419
|
+
return (self.classes_with_definition / self.classes_total) * 100
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def property_label_pct(self) -> float:
|
|
423
|
+
"""Percentage of properties with labels."""
|
|
424
|
+
if self.properties_total == 0:
|
|
425
|
+
return 100.0
|
|
426
|
+
return (self.properties_with_label / self.properties_total) * 100
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def property_definition_pct(self) -> float:
|
|
430
|
+
"""Percentage of properties with definitions."""
|
|
431
|
+
if self.properties_total == 0:
|
|
432
|
+
return 100.0
|
|
433
|
+
return (self.properties_with_definition / self.properties_total) * 100
|
|
434
|
+
|
|
435
|
+
def to_dict(self) -> dict[str, Any]:
|
|
436
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
437
|
+
return {
|
|
438
|
+
"classes": {
|
|
439
|
+
"with_label": self.classes_with_label,
|
|
440
|
+
"with_definition": self.classes_with_definition,
|
|
441
|
+
"total": self.classes_total,
|
|
442
|
+
"label_pct": round(self.class_label_pct, 1),
|
|
443
|
+
"definition_pct": round(self.class_definition_pct, 1),
|
|
444
|
+
},
|
|
445
|
+
"properties": {
|
|
446
|
+
"with_label": self.properties_with_label,
|
|
447
|
+
"with_definition": self.properties_with_definition,
|
|
448
|
+
"total": self.properties_total,
|
|
449
|
+
"label_pct": round(self.property_label_pct, 1),
|
|
450
|
+
"definition_pct": round(self.property_definition_pct, 1),
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@dataclass
|
|
456
|
+
class ReasoningAnalysis:
|
|
457
|
+
"""Analysis of reasoning implications (optional, off by default).
|
|
458
|
+
|
|
459
|
+
Attributes:
|
|
460
|
+
entailment_regime: Applicable entailment regime.
|
|
461
|
+
inferred_superclasses: Sample of superclass inferences.
|
|
462
|
+
inferred_types: Sample of type inferences.
|
|
463
|
+
consistency_notes: Notes on potential consistency issues.
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
entailment_regime: str = "none"
|
|
467
|
+
inferred_superclasses: list[str] = field(default_factory=list)
|
|
468
|
+
inferred_types: list[str] = field(default_factory=list)
|
|
469
|
+
consistency_notes: list[str] = field(default_factory=list)
|
|
470
|
+
|
|
471
|
+
def to_dict(self) -> dict[str, Any]:
|
|
472
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
473
|
+
return {
|
|
474
|
+
"entailment_regime": self.entailment_regime,
|
|
475
|
+
"inferred_superclasses": self.inferred_superclasses,
|
|
476
|
+
"inferred_types": self.inferred_types,
|
|
477
|
+
"consistency_notes": self.consistency_notes,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@dataclass
|
|
482
|
+
class OntologyDescription:
|
|
483
|
+
"""Complete description of an ontology.
|
|
484
|
+
|
|
485
|
+
Aggregates all analysis results into a single structure for
|
|
486
|
+
formatting and serialisation.
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
# Source information
|
|
490
|
+
source: str | Path
|
|
491
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
492
|
+
|
|
493
|
+
# Analysis results
|
|
494
|
+
metadata: OntologyMetadata = field(default_factory=OntologyMetadata)
|
|
495
|
+
metrics: BasicMetrics = field(default_factory=BasicMetrics)
|
|
496
|
+
profile: ProfileDetection = field(default_factory=ProfileDetection)
|
|
497
|
+
namespaces: NamespaceAnalysis = field(default_factory=NamespaceAnalysis)
|
|
498
|
+
imports: ImportAnalysis = field(default_factory=ImportAnalysis)
|
|
499
|
+
hierarchy: HierarchyAnalysis = field(default_factory=HierarchyAnalysis)
|
|
500
|
+
documentation: DocumentationCoverage = field(default_factory=DocumentationCoverage)
|
|
501
|
+
reasoning: ReasoningAnalysis | None = None
|
|
502
|
+
|
|
503
|
+
# Analysis settings
|
|
504
|
+
brief: bool = False
|
|
505
|
+
include_reasoning: bool = False
|
|
506
|
+
|
|
507
|
+
@property
|
|
508
|
+
def verdict(self) -> str:
|
|
509
|
+
"""One-line summary verdict for the ontology."""
|
|
510
|
+
parts = []
|
|
511
|
+
|
|
512
|
+
# Profile
|
|
513
|
+
parts.append(self.profile.display_name)
|
|
514
|
+
|
|
515
|
+
# Size
|
|
516
|
+
parts.append(self.metrics.summary_line)
|
|
517
|
+
|
|
518
|
+
# Import status
|
|
519
|
+
if self.imports.count > 0:
|
|
520
|
+
if self.imports.resolve_attempted:
|
|
521
|
+
if self.imports.unresolvable_count > 0:
|
|
522
|
+
parts.append(
|
|
523
|
+
f"{self.imports.unresolvable_count}/{self.imports.count} imports unresolvable"
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
parts.append(f"{self.imports.count} imports OK")
|
|
527
|
+
else:
|
|
528
|
+
parts.append(f"{self.imports.count} imports")
|
|
529
|
+
|
|
530
|
+
return " | ".join(parts)
|
|
531
|
+
|
|
532
|
+
def to_dict(self) -> dict[str, Any]:
|
|
533
|
+
"""Convert to dictionary for JSON serialisation."""
|
|
534
|
+
result = {
|
|
535
|
+
"source": str(self.source),
|
|
536
|
+
"timestamp": self.timestamp.isoformat(),
|
|
537
|
+
"verdict": self.verdict,
|
|
538
|
+
"metadata": self.metadata.to_dict(),
|
|
539
|
+
"metrics": self.metrics.to_dict(),
|
|
540
|
+
"profile": self.profile.to_dict(),
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if not self.brief:
|
|
544
|
+
result["namespaces"] = self.namespaces.to_dict()
|
|
545
|
+
result["imports"] = self.imports.to_dict()
|
|
546
|
+
result["hierarchy"] = self.hierarchy.to_dict()
|
|
547
|
+
result["documentation"] = self.documentation.to_dict()
|
|
548
|
+
|
|
549
|
+
if self.reasoning is not None:
|
|
550
|
+
result["reasoning"] = self.reasoning.to_dict()
|
|
551
|
+
|
|
552
|
+
return result
|