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,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
+ ]