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,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
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Localise module for multi-language translation management.
|
|
2
|
+
|
|
3
|
+
This module provides tools for managing translations in RDF ontologies:
|
|
4
|
+
- Extract translatable strings to YAML files for translators
|
|
5
|
+
- Merge completed translations back into ontologies
|
|
6
|
+
- Report on translation coverage across languages
|
|
7
|
+
|
|
8
|
+
Example usage:
|
|
9
|
+
from rdf_construct.localise import (
|
|
10
|
+
StringExtractor,
|
|
11
|
+
TranslationMerger,
|
|
12
|
+
CoverageReporter,
|
|
13
|
+
extract_strings,
|
|
14
|
+
merge_translations,
|
|
15
|
+
generate_coverage_report,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Extract strings for German translation
|
|
19
|
+
result = extract_strings(
|
|
20
|
+
source=Path("ontology.ttl"),
|
|
21
|
+
target_language="de",
|
|
22
|
+
output=Path("translations/de.yml"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Merge completed translations
|
|
26
|
+
result = merge_translations(
|
|
27
|
+
source=Path("ontology.ttl"),
|
|
28
|
+
translation_files=[Path("translations/de.yml")],
|
|
29
|
+
output=Path("localised.ttl"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Generate coverage report
|
|
33
|
+
report = generate_coverage_report(
|
|
34
|
+
source=Path("ontology.ttl"),
|
|
35
|
+
languages=["en", "de", "fr"],
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from rdf_construct.localise.config import (
|
|
40
|
+
TranslationStatus,
|
|
41
|
+
TranslationEntry,
|
|
42
|
+
EntityTranslations,
|
|
43
|
+
TranslationFile,
|
|
44
|
+
TranslationFileMetadata,
|
|
45
|
+
TranslationSummary,
|
|
46
|
+
ExtractConfig,
|
|
47
|
+
MergeConfig,
|
|
48
|
+
ExistingStrategy,
|
|
49
|
+
LocaliseConfig,
|
|
50
|
+
create_default_config,
|
|
51
|
+
load_localise_config,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
from rdf_construct.localise.extractor import (
|
|
55
|
+
StringExtractor,
|
|
56
|
+
ExtractionResult,
|
|
57
|
+
extract_strings,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
from rdf_construct.localise.merger import (
|
|
61
|
+
TranslationMerger,
|
|
62
|
+
MergeResult,
|
|
63
|
+
MergeStats,
|
|
64
|
+
merge_translations,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
from rdf_construct.localise.reporter import (
|
|
68
|
+
CoverageReporter,
|
|
69
|
+
CoverageReport,
|
|
70
|
+
LanguageCoverage,
|
|
71
|
+
PropertyCoverage,
|
|
72
|
+
generate_coverage_report,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
from rdf_construct.localise.formatters import (
|
|
76
|
+
TextFormatter,
|
|
77
|
+
MarkdownFormatter,
|
|
78
|
+
get_formatter,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
# Config
|
|
83
|
+
"TranslationStatus",
|
|
84
|
+
"TranslationEntry",
|
|
85
|
+
"EntityTranslations",
|
|
86
|
+
"TranslationFile",
|
|
87
|
+
"TranslationFileMetadata",
|
|
88
|
+
"TranslationSummary",
|
|
89
|
+
"ExtractConfig",
|
|
90
|
+
"MergeConfig",
|
|
91
|
+
"ExistingStrategy",
|
|
92
|
+
"LocaliseConfig",
|
|
93
|
+
"create_default_config",
|
|
94
|
+
"load_localise_config",
|
|
95
|
+
# Extractor
|
|
96
|
+
"StringExtractor",
|
|
97
|
+
"ExtractionResult",
|
|
98
|
+
"extract_strings",
|
|
99
|
+
# Merger
|
|
100
|
+
"TranslationMerger",
|
|
101
|
+
"MergeResult",
|
|
102
|
+
"MergeStats",
|
|
103
|
+
"merge_translations",
|
|
104
|
+
# Reporter
|
|
105
|
+
"CoverageReporter",
|
|
106
|
+
"CoverageReport",
|
|
107
|
+
"LanguageCoverage",
|
|
108
|
+
"PropertyCoverage",
|
|
109
|
+
"generate_coverage_report",
|
|
110
|
+
# Formatters
|
|
111
|
+
"TextFormatter",
|
|
112
|
+
"MarkdownFormatter",
|
|
113
|
+
"get_formatter",
|
|
114
|
+
]
|