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,180 @@
|
|
|
1
|
+
"""Namespace analysis for ontology description.
|
|
2
|
+
|
|
3
|
+
Categorises namespaces as local, imported, or external (unimported).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections import Counter
|
|
7
|
+
|
|
8
|
+
from rdflib import Graph, URIRef, BNode
|
|
9
|
+
from rdflib.namespace import OWL, RDF, RDFS, XSD
|
|
10
|
+
|
|
11
|
+
from rdf_construct.describe.models import (
|
|
12
|
+
NamespaceAnalysis,
|
|
13
|
+
NamespaceInfo,
|
|
14
|
+
NamespaceCategory,
|
|
15
|
+
)
|
|
16
|
+
from rdf_construct.describe.imports import get_imported_namespaces
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Well-known vocabulary namespaces (always considered external but expected)
|
|
20
|
+
WELL_KNOWN_NAMESPACES = {
|
|
21
|
+
str(RDF): "rdf",
|
|
22
|
+
str(RDFS): "rdfs",
|
|
23
|
+
str(OWL): "owl",
|
|
24
|
+
str(XSD): "xsd",
|
|
25
|
+
"http://www.w3.org/2004/02/skos/core#": "skos",
|
|
26
|
+
"http://purl.org/dc/elements/1.1/": "dc",
|
|
27
|
+
"http://purl.org/dc/terms/": "dcterms",
|
|
28
|
+
"http://xmlns.com/foaf/0.1/": "foaf",
|
|
29
|
+
"http://www.w3.org/ns/prov#": "prov",
|
|
30
|
+
"http://www.w3.org/ns/shacl#": "sh",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def analyse_namespaces(graph: Graph) -> NamespaceAnalysis:
|
|
35
|
+
"""Analyse namespace usage in the ontology.
|
|
36
|
+
|
|
37
|
+
Categorises each namespace as:
|
|
38
|
+
- LOCAL: Defined in this ontology (contains defined classes/properties)
|
|
39
|
+
- IMPORTED: Declared via owl:imports
|
|
40
|
+
- EXTERNAL: Referenced but not imported (may indicate missing import)
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
graph: RDF graph to analyse.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
NamespaceAnalysis with categorised namespaces.
|
|
47
|
+
"""
|
|
48
|
+
# Get ontology IRI to identify local namespace
|
|
49
|
+
local_namespace = _get_local_namespace(graph)
|
|
50
|
+
|
|
51
|
+
# Get imported namespaces
|
|
52
|
+
imported_ns = get_imported_namespaces(graph)
|
|
53
|
+
|
|
54
|
+
# Count namespace usage across all triples
|
|
55
|
+
ns_usage = _count_namespace_usage(graph)
|
|
56
|
+
|
|
57
|
+
# Get prefix bindings
|
|
58
|
+
prefix_map = {str(uri): prefix for prefix, uri in graph.namespace_manager.namespaces()}
|
|
59
|
+
|
|
60
|
+
# Build namespace info list
|
|
61
|
+
namespaces: list[NamespaceInfo] = []
|
|
62
|
+
unimported_external: list[str] = []
|
|
63
|
+
|
|
64
|
+
for ns_uri, count in sorted(ns_usage.items(), key=lambda x: -x[1]):
|
|
65
|
+
# Determine category
|
|
66
|
+
if local_namespace and ns_uri == local_namespace:
|
|
67
|
+
category = NamespaceCategory.LOCAL
|
|
68
|
+
elif ns_uri in imported_ns:
|
|
69
|
+
category = NamespaceCategory.IMPORTED
|
|
70
|
+
elif ns_uri in WELL_KNOWN_NAMESPACES:
|
|
71
|
+
# Well-known vocabularies are external but expected
|
|
72
|
+
category = NamespaceCategory.EXTERNAL
|
|
73
|
+
else:
|
|
74
|
+
# Unknown external namespace - might be missing import
|
|
75
|
+
category = NamespaceCategory.EXTERNAL
|
|
76
|
+
if ns_uri not in WELL_KNOWN_NAMESPACES:
|
|
77
|
+
unimported_external.append(ns_uri)
|
|
78
|
+
|
|
79
|
+
# Get prefix (from bindings or well-known)
|
|
80
|
+
prefix = prefix_map.get(ns_uri) or WELL_KNOWN_NAMESPACES.get(ns_uri)
|
|
81
|
+
|
|
82
|
+
namespaces.append(NamespaceInfo(
|
|
83
|
+
uri=ns_uri,
|
|
84
|
+
prefix=prefix,
|
|
85
|
+
category=category,
|
|
86
|
+
usage_count=count,
|
|
87
|
+
))
|
|
88
|
+
|
|
89
|
+
return NamespaceAnalysis(
|
|
90
|
+
local_namespace=local_namespace,
|
|
91
|
+
namespaces=namespaces,
|
|
92
|
+
unimported_external=unimported_external,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_local_namespace(graph: Graph) -> str | None:
|
|
97
|
+
"""Determine the local/primary namespace for the ontology.
|
|
98
|
+
|
|
99
|
+
Uses the ontology IRI if declared, otherwise infers from
|
|
100
|
+
most commonly used namespace for defined entities.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
graph: RDF graph to analyse.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Local namespace URI or None if not determinable.
|
|
107
|
+
"""
|
|
108
|
+
# First, try to get from owl:Ontology declaration
|
|
109
|
+
for ontology in graph.subjects(RDF.type, OWL.Ontology):
|
|
110
|
+
if isinstance(ontology, URIRef):
|
|
111
|
+
return _extract_namespace(str(ontology))
|
|
112
|
+
|
|
113
|
+
# Fall back to most common namespace for defined classes
|
|
114
|
+
class_ns = Counter[str]()
|
|
115
|
+
for cls in graph.subjects(RDF.type, OWL.Class):
|
|
116
|
+
if isinstance(cls, URIRef):
|
|
117
|
+
ns = _extract_namespace(str(cls))
|
|
118
|
+
if ns not in WELL_KNOWN_NAMESPACES:
|
|
119
|
+
class_ns[ns] += 1
|
|
120
|
+
|
|
121
|
+
for cls in graph.subjects(RDF.type, RDFS.Class):
|
|
122
|
+
if isinstance(cls, URIRef):
|
|
123
|
+
ns = _extract_namespace(str(cls))
|
|
124
|
+
if ns not in WELL_KNOWN_NAMESPACES:
|
|
125
|
+
class_ns[ns] += 1
|
|
126
|
+
|
|
127
|
+
if class_ns:
|
|
128
|
+
return class_ns.most_common(1)[0][0]
|
|
129
|
+
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _count_namespace_usage(graph: Graph) -> dict[str, int]:
|
|
134
|
+
"""Count how many times each namespace is used in the graph.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
graph: RDF graph to analyse.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Dictionary mapping namespace URI to usage count.
|
|
141
|
+
"""
|
|
142
|
+
ns_count: Counter[str] = Counter()
|
|
143
|
+
|
|
144
|
+
for s, p, o in graph:
|
|
145
|
+
# Count subject namespace (skip blank nodes)
|
|
146
|
+
if isinstance(s, URIRef):
|
|
147
|
+
ns_count[_extract_namespace(str(s))] += 1
|
|
148
|
+
|
|
149
|
+
# Count predicate namespace
|
|
150
|
+
if isinstance(p, URIRef):
|
|
151
|
+
ns_count[_extract_namespace(str(p))] += 1
|
|
152
|
+
|
|
153
|
+
# Count object namespace (if URI)
|
|
154
|
+
if isinstance(o, URIRef):
|
|
155
|
+
ns_count[_extract_namespace(str(o))] += 1
|
|
156
|
+
|
|
157
|
+
return dict(ns_count)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _extract_namespace(uri: str) -> str:
|
|
161
|
+
"""Extract namespace from a URI.
|
|
162
|
+
|
|
163
|
+
Handles both # and / as namespace separators.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
uri: Full URI string.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Namespace portion of the URI.
|
|
170
|
+
"""
|
|
171
|
+
# Split on # first (OWL-style namespaces)
|
|
172
|
+
if "#" in uri:
|
|
173
|
+
return uri.rsplit("#", 1)[0] + "#"
|
|
174
|
+
|
|
175
|
+
# Otherwise split on last /
|
|
176
|
+
if "/" in uri:
|
|
177
|
+
return uri.rsplit("/", 1)[0] + "/"
|
|
178
|
+
|
|
179
|
+
# No separator found, return as-is
|
|
180
|
+
return uri
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""OWL profile detection for ontologies.
|
|
2
|
+
|
|
3
|
+
Detects whether an ontology is RDF, RDFS, OWL 2 DL (simple or expressive),
|
|
4
|
+
or OWL 2 Full based on the constructs used.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rdflib import Graph, RDF, RDFS, URIRef
|
|
8
|
+
from rdflib.namespace import OWL, XSD
|
|
9
|
+
|
|
10
|
+
from rdf_construct.describe.models import OntologyProfile, ProfileDetection
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# OWL constructs that indicate OWL usage (but not necessarily expressive)
|
|
14
|
+
OWL_BASIC_CONSTRUCTS = {
|
|
15
|
+
OWL.Class,
|
|
16
|
+
OWL.ObjectProperty,
|
|
17
|
+
OWL.DatatypeProperty,
|
|
18
|
+
OWL.AnnotationProperty,
|
|
19
|
+
OWL.Ontology,
|
|
20
|
+
OWL.NamedIndividual,
|
|
21
|
+
OWL.Thing,
|
|
22
|
+
OWL.Nothing,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Property characteristics (simple OWL DL)
|
|
26
|
+
OWL_PROPERTY_CHARACTERISTICS = {
|
|
27
|
+
OWL.FunctionalProperty,
|
|
28
|
+
OWL.InverseFunctionalProperty,
|
|
29
|
+
OWL.TransitiveProperty,
|
|
30
|
+
OWL.SymmetricProperty,
|
|
31
|
+
OWL.AsymmetricProperty,
|
|
32
|
+
OWL.ReflexiveProperty,
|
|
33
|
+
OWL.IrreflexiveProperty,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Expressive OWL constructs (restrictions, equivalences, etc.)
|
|
37
|
+
OWL_EXPRESSIVE_CONSTRUCTS = {
|
|
38
|
+
OWL.Restriction,
|
|
39
|
+
OWL.equivalentClass,
|
|
40
|
+
OWL.disjointWith,
|
|
41
|
+
OWL.AllDisjointClasses,
|
|
42
|
+
OWL.AllDifferent,
|
|
43
|
+
OWL.unionOf,
|
|
44
|
+
OWL.intersectionOf,
|
|
45
|
+
OWL.complementOf,
|
|
46
|
+
OWL.oneOf,
|
|
47
|
+
OWL.hasValue,
|
|
48
|
+
OWL.someValuesFrom,
|
|
49
|
+
OWL.allValuesFrom,
|
|
50
|
+
OWL.minCardinality,
|
|
51
|
+
OWL.maxCardinality,
|
|
52
|
+
OWL.cardinality,
|
|
53
|
+
OWL.minQualifiedCardinality,
|
|
54
|
+
OWL.maxQualifiedCardinality,
|
|
55
|
+
OWL.qualifiedCardinality,
|
|
56
|
+
OWL.hasSelf,
|
|
57
|
+
OWL.propertyChainAxiom,
|
|
58
|
+
OWL.hasKey,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# OWL 2 Full constructs (constructs that violate DL constraints)
|
|
62
|
+
# These indicate usage patterns that are undecidable
|
|
63
|
+
OWL_FULL_INDICATORS = {
|
|
64
|
+
# Classes used as individuals or vice versa (punning beyond DL limits)
|
|
65
|
+
# Using owl:Class as a property
|
|
66
|
+
# Self-referential class definitions
|
|
67
|
+
# Using rdf:type with complex expressions
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# RDFS constructs that indicate RDFS-level usage
|
|
71
|
+
RDFS_CONSTRUCTS = {
|
|
72
|
+
RDFS.Class,
|
|
73
|
+
RDFS.subClassOf,
|
|
74
|
+
RDFS.subPropertyOf,
|
|
75
|
+
RDFS.domain,
|
|
76
|
+
RDFS.range,
|
|
77
|
+
RDFS.label,
|
|
78
|
+
RDFS.comment,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Well-known vocabulary namespaces that don't indicate OWL usage
|
|
82
|
+
VOCABULARY_NAMESPACES = {
|
|
83
|
+
str(RDF),
|
|
84
|
+
str(RDFS),
|
|
85
|
+
str(OWL),
|
|
86
|
+
str(XSD),
|
|
87
|
+
"http://www.w3.org/2004/02/skos/core#",
|
|
88
|
+
"http://purl.org/dc/elements/1.1/",
|
|
89
|
+
"http://purl.org/dc/terms/",
|
|
90
|
+
"http://xmlns.com/foaf/0.1/",
|
|
91
|
+
"http://www.w3.org/ns/prov#",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def detect_profile(graph: Graph) -> ProfileDetection:
|
|
96
|
+
"""Detect the OWL profile of an ontology.
|
|
97
|
+
|
|
98
|
+
Analyses the constructs used in the ontology to determine its
|
|
99
|
+
expressiveness level.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
graph: RDF graph to analyse.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
ProfileDetection with profile and supporting evidence.
|
|
106
|
+
"""
|
|
107
|
+
detected_features: list[str] = []
|
|
108
|
+
owl_constructs: list[str] = []
|
|
109
|
+
violating_constructs: list[str] = []
|
|
110
|
+
|
|
111
|
+
# Check for OWL Full indicators first (these are most restrictive)
|
|
112
|
+
owl_full_issues = _detect_owl_full(graph)
|
|
113
|
+
if owl_full_issues:
|
|
114
|
+
violating_constructs.extend(owl_full_issues)
|
|
115
|
+
return ProfileDetection(
|
|
116
|
+
profile=OntologyProfile.OWL_FULL,
|
|
117
|
+
detected_features=["OWL Full constructs detected"],
|
|
118
|
+
owl_constructs_found=owl_constructs,
|
|
119
|
+
violating_constructs=violating_constructs,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Check for expressive OWL constructs
|
|
123
|
+
expressive_found = _find_expressive_constructs(graph)
|
|
124
|
+
if expressive_found:
|
|
125
|
+
owl_constructs.extend(expressive_found)
|
|
126
|
+
detected_features.append("Expressive OWL constructs (restrictions, equivalences)")
|
|
127
|
+
return ProfileDetection(
|
|
128
|
+
profile=OntologyProfile.OWL_DL_EXPRESSIVE,
|
|
129
|
+
detected_features=detected_features,
|
|
130
|
+
owl_constructs_found=owl_constructs,
|
|
131
|
+
violating_constructs=[],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Check for basic OWL constructs
|
|
135
|
+
basic_owl_found = _find_basic_owl_constructs(graph)
|
|
136
|
+
property_chars = _find_property_characteristics(graph)
|
|
137
|
+
|
|
138
|
+
if basic_owl_found or property_chars:
|
|
139
|
+
owl_constructs.extend(basic_owl_found)
|
|
140
|
+
owl_constructs.extend(property_chars)
|
|
141
|
+
|
|
142
|
+
if property_chars:
|
|
143
|
+
detected_features.append("Property characteristics declared")
|
|
144
|
+
if basic_owl_found:
|
|
145
|
+
detected_features.append("OWL class/property declarations")
|
|
146
|
+
|
|
147
|
+
return ProfileDetection(
|
|
148
|
+
profile=OntologyProfile.OWL_DL_SIMPLE,
|
|
149
|
+
detected_features=detected_features,
|
|
150
|
+
owl_constructs_found=owl_constructs,
|
|
151
|
+
violating_constructs=[],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Check for RDFS constructs
|
|
155
|
+
rdfs_found = _find_rdfs_constructs(graph)
|
|
156
|
+
if rdfs_found:
|
|
157
|
+
detected_features.append("RDFS vocabulary in use")
|
|
158
|
+
return ProfileDetection(
|
|
159
|
+
profile=OntologyProfile.RDFS,
|
|
160
|
+
detected_features=detected_features,
|
|
161
|
+
owl_constructs_found=[],
|
|
162
|
+
violating_constructs=[],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Default to pure RDF
|
|
166
|
+
detected_features.append("No schema constructs found")
|
|
167
|
+
return ProfileDetection(
|
|
168
|
+
profile=OntologyProfile.RDF,
|
|
169
|
+
detected_features=detected_features,
|
|
170
|
+
owl_constructs_found=[],
|
|
171
|
+
violating_constructs=[],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _detect_owl_full(graph: Graph) -> list[str]:
|
|
176
|
+
"""Detect constructs that indicate OWL Full.
|
|
177
|
+
|
|
178
|
+
OWL Full allows patterns that are undecidable, including:
|
|
179
|
+
- Metaclasses (classes that are instances of other classes)
|
|
180
|
+
- Properties with classes as values
|
|
181
|
+
- Circular definitions in certain ways
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
graph: RDF graph to analyse.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of OWL Full indicator descriptions.
|
|
188
|
+
"""
|
|
189
|
+
issues: list[str] = []
|
|
190
|
+
|
|
191
|
+
# Check for metaclasses: classes that are rdf:type of other classes
|
|
192
|
+
# This is a common OWL Full pattern
|
|
193
|
+
owl_classes = set(graph.subjects(RDF.type, OWL.Class))
|
|
194
|
+
rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
|
|
195
|
+
all_classes = owl_classes | rdfs_classes
|
|
196
|
+
|
|
197
|
+
for cls in all_classes:
|
|
198
|
+
# Check if this class is an instance of another class (not owl:Class/rdfs:Class)
|
|
199
|
+
for class_type in graph.objects(cls, RDF.type):
|
|
200
|
+
if class_type not in {OWL.Class, RDFS.Class} and class_type in all_classes:
|
|
201
|
+
issues.append(f"Metaclass: {_curie(graph, cls)} is instance of class {_curie(graph, class_type)}")
|
|
202
|
+
|
|
203
|
+
# Check for owl:Class used in unexpected positions
|
|
204
|
+
# For example, as the object of a property that expects individuals
|
|
205
|
+
for s, p, o in graph:
|
|
206
|
+
# Skip type assertions
|
|
207
|
+
if p == RDF.type:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# If object is a class and predicate domain/range suggests individuals
|
|
211
|
+
if o in all_classes:
|
|
212
|
+
# Check if predicate is an object property with individual range
|
|
213
|
+
if (p, RDF.type, OWL.ObjectProperty) in graph:
|
|
214
|
+
prop_range = list(graph.objects(p, RDFS.range))
|
|
215
|
+
# If range is defined and is not a class of classes, this might be Full
|
|
216
|
+
# This is a simplified check; full analysis would require more inference
|
|
217
|
+
|
|
218
|
+
# Check for problematic self-reference patterns
|
|
219
|
+
# e.g., C owl:equivalentClass [ owl:complementOf C ] could be problematic
|
|
220
|
+
for cls in owl_classes:
|
|
221
|
+
equiv_classes = list(graph.objects(cls, OWL.equivalentClass))
|
|
222
|
+
for equiv in equiv_classes:
|
|
223
|
+
# Check for direct self-equivalence to complement
|
|
224
|
+
complement = list(graph.objects(equiv, OWL.complementOf))
|
|
225
|
+
if cls in complement:
|
|
226
|
+
issues.append(f"Self-contradictory equivalence: {_curie(graph, cls)}")
|
|
227
|
+
|
|
228
|
+
return issues
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _find_expressive_constructs(graph: Graph) -> list[str]:
|
|
232
|
+
"""Find expressive OWL constructs in the graph.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
graph: RDF graph to analyse.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of expressive construct descriptions found.
|
|
239
|
+
"""
|
|
240
|
+
found: list[str] = []
|
|
241
|
+
|
|
242
|
+
# Check for restrictions
|
|
243
|
+
restrictions = set(graph.subjects(RDF.type, OWL.Restriction))
|
|
244
|
+
if restrictions:
|
|
245
|
+
found.append(f"owl:Restriction ({len(restrictions)} found)")
|
|
246
|
+
|
|
247
|
+
# Check for class equivalences
|
|
248
|
+
equiv_count = len(list(graph.subject_objects(OWL.equivalentClass)))
|
|
249
|
+
if equiv_count:
|
|
250
|
+
found.append(f"owl:equivalentClass ({equiv_count} axioms)")
|
|
251
|
+
|
|
252
|
+
# Check for disjointness
|
|
253
|
+
disjoint_count = len(list(graph.subject_objects(OWL.disjointWith)))
|
|
254
|
+
if disjoint_count:
|
|
255
|
+
found.append(f"owl:disjointWith ({disjoint_count} axioms)")
|
|
256
|
+
|
|
257
|
+
all_disjoint = set(graph.subjects(RDF.type, OWL.AllDisjointClasses))
|
|
258
|
+
if all_disjoint:
|
|
259
|
+
found.append(f"owl:AllDisjointClasses ({len(all_disjoint)} found)")
|
|
260
|
+
|
|
261
|
+
# Check for set operators
|
|
262
|
+
union_of = len(list(graph.subject_objects(OWL.unionOf)))
|
|
263
|
+
if union_of:
|
|
264
|
+
found.append(f"owl:unionOf ({union_of} uses)")
|
|
265
|
+
|
|
266
|
+
intersection_of = len(list(graph.subject_objects(OWL.intersectionOf)))
|
|
267
|
+
if intersection_of:
|
|
268
|
+
found.append(f"owl:intersectionOf ({intersection_of} uses)")
|
|
269
|
+
|
|
270
|
+
complement_of = len(list(graph.subject_objects(OWL.complementOf)))
|
|
271
|
+
if complement_of:
|
|
272
|
+
found.append(f"owl:complementOf ({complement_of} uses)")
|
|
273
|
+
|
|
274
|
+
one_of = len(list(graph.subject_objects(OWL.oneOf)))
|
|
275
|
+
if one_of:
|
|
276
|
+
found.append(f"owl:oneOf ({one_of} uses)")
|
|
277
|
+
|
|
278
|
+
# Check for property chains
|
|
279
|
+
chains = len(list(graph.subject_objects(OWL.propertyChainAxiom)))
|
|
280
|
+
if chains:
|
|
281
|
+
found.append(f"owl:propertyChainAxiom ({chains} chains)")
|
|
282
|
+
|
|
283
|
+
# Check for keys
|
|
284
|
+
keys = len(list(graph.subject_objects(OWL.hasKey)))
|
|
285
|
+
if keys:
|
|
286
|
+
found.append(f"owl:hasKey ({keys} keys)")
|
|
287
|
+
|
|
288
|
+
return found
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _find_basic_owl_constructs(graph: Graph) -> list[str]:
|
|
292
|
+
"""Find basic OWL constructs in the graph.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
graph: RDF graph to analyse.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of basic OWL construct descriptions found.
|
|
299
|
+
"""
|
|
300
|
+
found: list[str] = []
|
|
301
|
+
|
|
302
|
+
# Check for owl:Ontology
|
|
303
|
+
ontologies = set(graph.subjects(RDF.type, OWL.Ontology))
|
|
304
|
+
if ontologies:
|
|
305
|
+
found.append("owl:Ontology declaration")
|
|
306
|
+
|
|
307
|
+
# Check for owl:Class (distinct from rdfs:Class)
|
|
308
|
+
owl_classes = set(graph.subjects(RDF.type, OWL.Class))
|
|
309
|
+
if owl_classes:
|
|
310
|
+
found.append(f"owl:Class ({len(owl_classes)} found)")
|
|
311
|
+
|
|
312
|
+
# Check for OWL property types
|
|
313
|
+
obj_props = set(graph.subjects(RDF.type, OWL.ObjectProperty))
|
|
314
|
+
if obj_props:
|
|
315
|
+
found.append(f"owl:ObjectProperty ({len(obj_props)} found)")
|
|
316
|
+
|
|
317
|
+
data_props = set(graph.subjects(RDF.type, OWL.DatatypeProperty))
|
|
318
|
+
if data_props:
|
|
319
|
+
found.append(f"owl:DatatypeProperty ({len(data_props)} found)")
|
|
320
|
+
|
|
321
|
+
ann_props = set(graph.subjects(RDF.type, OWL.AnnotationProperty))
|
|
322
|
+
if ann_props:
|
|
323
|
+
found.append(f"owl:AnnotationProperty ({len(ann_props)} found)")
|
|
324
|
+
|
|
325
|
+
# Check for named individuals
|
|
326
|
+
individuals = set(graph.subjects(RDF.type, OWL.NamedIndividual))
|
|
327
|
+
if individuals:
|
|
328
|
+
found.append(f"owl:NamedIndividual ({len(individuals)} found)")
|
|
329
|
+
|
|
330
|
+
return found
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _find_property_characteristics(graph: Graph) -> list[str]:
|
|
334
|
+
"""Find OWL property characteristic declarations.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
graph: RDF graph to analyse.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List of property characteristic descriptions found.
|
|
341
|
+
"""
|
|
342
|
+
found: list[str] = []
|
|
343
|
+
|
|
344
|
+
for char in OWL_PROPERTY_CHARACTERISTICS:
|
|
345
|
+
props = set(graph.subjects(RDF.type, char))
|
|
346
|
+
if props:
|
|
347
|
+
# Get local name of the characteristic
|
|
348
|
+
char_name = str(char).split("#")[-1]
|
|
349
|
+
found.append(f"owl:{char_name} ({len(props)} properties)")
|
|
350
|
+
|
|
351
|
+
# Check for inverse properties
|
|
352
|
+
inverse_of = len(list(graph.subject_objects(OWL.inverseOf)))
|
|
353
|
+
if inverse_of:
|
|
354
|
+
found.append(f"owl:inverseOf ({inverse_of} pairs)")
|
|
355
|
+
|
|
356
|
+
return found
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _find_rdfs_constructs(graph: Graph) -> list[str]:
|
|
360
|
+
"""Find RDFS-level constructs in the graph.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
graph: RDF graph to analyse.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
List of RDFS construct descriptions found.
|
|
367
|
+
"""
|
|
368
|
+
found: list[str] = []
|
|
369
|
+
|
|
370
|
+
# Check for rdfs:Class (not owl:Class)
|
|
371
|
+
rdfs_classes = set(graph.subjects(RDF.type, RDFS.Class))
|
|
372
|
+
owl_classes = set(graph.subjects(RDF.type, OWL.Class))
|
|
373
|
+
pure_rdfs = rdfs_classes - owl_classes
|
|
374
|
+
if pure_rdfs:
|
|
375
|
+
found.append(f"rdfs:Class ({len(pure_rdfs)} found)")
|
|
376
|
+
|
|
377
|
+
# Check for subclass assertions
|
|
378
|
+
subclass = len(list(graph.subject_objects(RDFS.subClassOf)))
|
|
379
|
+
if subclass:
|
|
380
|
+
found.append(f"rdfs:subClassOf ({subclass} axioms)")
|
|
381
|
+
|
|
382
|
+
# Check for subproperty assertions
|
|
383
|
+
subprop = len(list(graph.subject_objects(RDFS.subPropertyOf)))
|
|
384
|
+
if subprop:
|
|
385
|
+
found.append(f"rdfs:subPropertyOf ({subprop} axioms)")
|
|
386
|
+
|
|
387
|
+
# Check for domain/range
|
|
388
|
+
domain = len(list(graph.subject_objects(RDFS.domain)))
|
|
389
|
+
range_count = len(list(graph.subject_objects(RDFS.range)))
|
|
390
|
+
if domain or range_count:
|
|
391
|
+
found.append(f"Domain/range declarations ({domain + range_count} total)")
|
|
392
|
+
|
|
393
|
+
return found
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _curie(graph: Graph, uri: URIRef) -> str:
|
|
397
|
+
"""Convert URI to CURIE or short form for display.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
graph: Graph with namespace bindings.
|
|
401
|
+
uri: URI to convert.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
CURIE or shortened URI string.
|
|
405
|
+
"""
|
|
406
|
+
try:
|
|
407
|
+
return graph.namespace_manager.normalizeUri(uri)
|
|
408
|
+
except Exception:
|
|
409
|
+
# Fall back to just the local name
|
|
410
|
+
s = str(uri)
|
|
411
|
+
if "#" in s:
|
|
412
|
+
return s.split("#")[-1]
|
|
413
|
+
elif "/" in s:
|
|
414
|
+
return s.split("/")[-1]
|
|
415
|
+
return s
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rdf-construct
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Semantic RDF manipulation toolkit - order, serialize, and diff RDF ontologies
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -50,6 +50,7 @@ Description-Content-Type: text/markdown
|
|
|
50
50
|
## Features
|
|
51
51
|
|
|
52
52
|
- **Semantic Ordering**: Serialise RDF/Turtle with intelligent ordering instead of alphabetical chaos
|
|
53
|
+
- **Ontology Description**: Quick orientation to unfamiliar ontologies with profile detection
|
|
53
54
|
- **Documentation Generation**: Create navigable HTML, Markdown, or JSON documentation from ontologies
|
|
54
55
|
- **UML Generation**: Create PlantUML class diagrams from RDF ontologies
|
|
55
56
|
- **PUML2RDF**: Convert PlantUML diagrams to RDF/OWL ontologies (diagram-first design)
|
|
@@ -64,6 +65,7 @@ Description-Content-Type: text/markdown
|
|
|
64
65
|
- **Ontology Statistics**: Comprehensive metrics with comparison mode
|
|
65
66
|
- **Flexible Styling**: Configure colours, layouts, and visual themes for diagrams
|
|
66
67
|
- **Profile-Based**: Define multiple strategies in YAML configuration
|
|
68
|
+
- **Multi-Format Input**: Supports Turtle, RDF/XML, JSON-LD, N-Triples
|
|
67
69
|
- **Deterministic**: Same input + profile = same output, always
|
|
68
70
|
|
|
69
71
|
## Why?
|
|
@@ -93,6 +95,19 @@ pip install -e .
|
|
|
93
95
|
poetry install
|
|
94
96
|
```
|
|
95
97
|
|
|
98
|
+
### Describe an Ontology
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Quick orientation to an unfamiliar ontology
|
|
102
|
+
rdf-construct describe ontology.ttl
|
|
103
|
+
|
|
104
|
+
# Brief summary (metadata + metrics + profile)
|
|
105
|
+
rdf-construct describe ontology.ttl --brief
|
|
106
|
+
|
|
107
|
+
# JSON output for scripting
|
|
108
|
+
rdf-construct describe ontology.ttl --format json
|
|
109
|
+
```
|
|
110
|
+
|
|
96
111
|
### Compare Ontology Versions
|
|
97
112
|
|
|
98
113
|
```bash
|
|
@@ -243,15 +258,16 @@ rdf-construct localise report ontology.ttl --languages en,de,fr
|
|
|
243
258
|
|
|
244
259
|
**For Users**:
|
|
245
260
|
- [Getting Started](docs/user_guides/GETTING_STARTED.md) - 5-minute quick start
|
|
261
|
+
- [Describe Guide](docs/user_guides/DESCRIBE_GUIDE.md) - Quick ontology orientation
|
|
246
262
|
- [Docs Guide](docs/user_guides/DOCS_GUIDE.md) - Documentation generation
|
|
247
263
|
- [UML Guide](docs/user_guides/UML_GUIDE.md) - Complete UML features
|
|
248
|
-
- [
|
|
264
|
+
- [PUML2RDF Guide](docs/user_guides/PUML2RDF_GUIDE.md) - Diagram-first design
|
|
249
265
|
- [SHACL Guide](docs/user_guides/SHACL_GUIDE.md) - SHACL shape generation
|
|
250
266
|
- [Diff Guide](docs/user_guides/DIFF_GUIDE.md) - Semantic ontology comparison
|
|
251
267
|
- [Lint Guide](docs/user_guides/LINT_GUIDE.md) - Ontology quality checking
|
|
252
268
|
- [CQ Testing Guide](docs/user_guides/CQ_TEST_GUIDE.md) - Competency question testing
|
|
253
269
|
- [Stats Guide](docs/user_guides/STATS_GUIDE.md) - Ontology metrics
|
|
254
|
-
- [Merge Guide](docs/user_guides/
|
|
270
|
+
- [Merge & Split Guide](docs/user_guides/MERGE_SPLIT_GUIDE.md) - Combining and modularising ontologies
|
|
255
271
|
- [Refactor Guide](docs/user_guides/REFACTOR_GUIDE.md) - Renaming and deprecation
|
|
256
272
|
- [Localise Guide](docs/user_guides/LOCALISE_GUIDE.md) - Multi-language translations
|
|
257
273
|
- [CLI Reference](docs/user_guides/CLI_REFERENCE.md) - All commands and options
|
|
@@ -409,7 +425,7 @@ properties:
|
|
|
409
425
|
|
|
410
426
|
## Project Status
|
|
411
427
|
|
|
412
|
-
**Current**: v0.
|
|
428
|
+
**Current**: v0.4.0 - Feature complete for core ontology workflows
|
|
413
429
|
**License**: MIT
|
|
414
430
|
|
|
415
431
|
### Implemented
|
|
@@ -425,10 +441,15 @@ properties:
|
|
|
425
441
|
✅ Ontology linting (11 rules)
|
|
426
442
|
✅ Competency question testing
|
|
427
443
|
✅ Ontology statistics
|
|
444
|
+
✅ Ontology merging and splitting
|
|
445
|
+
✅ Ontology refactoring (rename, deprecate)
|
|
446
|
+
✅ Multi-language translation management
|
|
447
|
+
✅ Ontology description and profile detection
|
|
448
|
+
✅ Multi-format input support (Turtle, RDF/XML, JSON-LD, N-Triples)
|
|
428
449
|
✅ Comprehensive documentation
|
|
429
450
|
|
|
430
451
|
### (Possible) Roadmap
|
|
431
|
-
- [ ]
|
|
452
|
+
- [ ] OWL 2 named profile detection (EL, RL, QL)
|
|
432
453
|
- [ ] Streaming mode for very large graphs
|
|
433
454
|
- [ ] Web UI for diagram configuration
|
|
434
455
|
- [ ] Additional lint rules
|
|
@@ -491,6 +512,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
491
512
|
|
|
492
513
|
---
|
|
493
514
|
|
|
494
|
-
**Status**: v0.
|
|
515
|
+
**Status**: v0.4.0
|
|
495
516
|
**Python**: 3.10+ required
|
|
496
517
|
**Maintainer**: See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
518
|
+
|