rdf-construct 0.2.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 +12 -0
- rdf_construct/__main__.py +0 -0
- rdf_construct/cli.py +1762 -0
- rdf_construct/core/__init__.py +33 -0
- rdf_construct/core/config.py +116 -0
- rdf_construct/core/ordering.py +219 -0
- rdf_construct/core/predicate_order.py +212 -0
- rdf_construct/core/profile.py +157 -0
- rdf_construct/core/selector.py +64 -0
- rdf_construct/core/serialiser.py +232 -0
- rdf_construct/core/utils.py +89 -0
- rdf_construct/cq/__init__.py +77 -0
- rdf_construct/cq/expectations.py +365 -0
- rdf_construct/cq/formatters/__init__.py +45 -0
- rdf_construct/cq/formatters/json.py +104 -0
- rdf_construct/cq/formatters/junit.py +104 -0
- rdf_construct/cq/formatters/text.py +146 -0
- rdf_construct/cq/loader.py +300 -0
- rdf_construct/cq/runner.py +321 -0
- rdf_construct/diff/__init__.py +59 -0
- rdf_construct/diff/change_types.py +214 -0
- rdf_construct/diff/comparator.py +338 -0
- rdf_construct/diff/filters.py +133 -0
- rdf_construct/diff/formatters/__init__.py +71 -0
- rdf_construct/diff/formatters/json.py +192 -0
- rdf_construct/diff/formatters/markdown.py +210 -0
- rdf_construct/diff/formatters/text.py +195 -0
- rdf_construct/docs/__init__.py +60 -0
- rdf_construct/docs/config.py +238 -0
- rdf_construct/docs/extractors.py +603 -0
- rdf_construct/docs/generator.py +360 -0
- rdf_construct/docs/renderers/__init__.py +7 -0
- rdf_construct/docs/renderers/html.py +803 -0
- rdf_construct/docs/renderers/json.py +390 -0
- rdf_construct/docs/renderers/markdown.py +628 -0
- rdf_construct/docs/search.py +278 -0
- rdf_construct/docs/templates/html/base.html.jinja +44 -0
- rdf_construct/docs/templates/html/class.html.jinja +152 -0
- rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
- rdf_construct/docs/templates/html/index.html.jinja +110 -0
- rdf_construct/docs/templates/html/instance.html.jinja +90 -0
- rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
- rdf_construct/docs/templates/html/property.html.jinja +124 -0
- rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
- rdf_construct/lint/__init__.py +75 -0
- rdf_construct/lint/config.py +214 -0
- rdf_construct/lint/engine.py +396 -0
- rdf_construct/lint/formatters.py +327 -0
- rdf_construct/lint/rules.py +692 -0
- rdf_construct/main.py +6 -0
- rdf_construct/puml2rdf/__init__.py +103 -0
- rdf_construct/puml2rdf/config.py +230 -0
- rdf_construct/puml2rdf/converter.py +420 -0
- rdf_construct/puml2rdf/merger.py +200 -0
- rdf_construct/puml2rdf/model.py +202 -0
- rdf_construct/puml2rdf/parser.py +565 -0
- rdf_construct/puml2rdf/validators.py +451 -0
- rdf_construct/shacl/__init__.py +56 -0
- rdf_construct/shacl/config.py +166 -0
- rdf_construct/shacl/converters.py +520 -0
- rdf_construct/shacl/generator.py +364 -0
- rdf_construct/shacl/namespaces.py +93 -0
- rdf_construct/stats/__init__.py +29 -0
- rdf_construct/stats/collector.py +178 -0
- rdf_construct/stats/comparator.py +298 -0
- rdf_construct/stats/formatters/__init__.py +83 -0
- rdf_construct/stats/formatters/json.py +38 -0
- rdf_construct/stats/formatters/markdown.py +153 -0
- rdf_construct/stats/formatters/text.py +186 -0
- rdf_construct/stats/metrics/__init__.py +26 -0
- rdf_construct/stats/metrics/basic.py +147 -0
- rdf_construct/stats/metrics/complexity.py +137 -0
- rdf_construct/stats/metrics/connectivity.py +130 -0
- rdf_construct/stats/metrics/documentation.py +128 -0
- rdf_construct/stats/metrics/hierarchy.py +207 -0
- rdf_construct/stats/metrics/properties.py +88 -0
- rdf_construct/uml/__init__.py +22 -0
- rdf_construct/uml/context.py +194 -0
- rdf_construct/uml/mapper.py +371 -0
- rdf_construct/uml/odm_renderer.py +789 -0
- rdf_construct/uml/renderer.py +684 -0
- rdf_construct/uml/uml_layout.py +393 -0
- rdf_construct/uml/uml_style.py +613 -0
- rdf_construct-0.2.0.dist-info/METADATA +431 -0
- rdf_construct-0.2.0.dist-info/RECORD +88 -0
- rdf_construct-0.2.0.dist-info/WHEEL +4 -0
- rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
- rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Documentation metrics for RDF ontologies.
|
|
2
|
+
|
|
3
|
+
Analyses documentation coverage: labels, comments, definitions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from rdflib import Graph, RDF, RDFS, Namespace
|
|
9
|
+
from rdflib.namespace import OWL, DCTERMS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Common documentation namespaces
|
|
13
|
+
SKOS = Namespace("http://www.w3.org/2004/02/skos/core#")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DocumentationStats:
|
|
18
|
+
"""Documentation statistics for an ontology.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
classes_labelled: Classes with rdfs:label or skos:prefLabel.
|
|
22
|
+
classes_labelled_pct: Proportion of classes with labels (0.0 - 1.0).
|
|
23
|
+
classes_documented: Classes with rdfs:comment or skos:definition.
|
|
24
|
+
classes_documented_pct: Proportion of classes with documentation (0.0 - 1.0).
|
|
25
|
+
properties_labelled: Properties with labels.
|
|
26
|
+
properties_labelled_pct: Proportion of properties with labels (0.0 - 1.0).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
classes_labelled: int = 0
|
|
30
|
+
classes_labelled_pct: float = 0.0
|
|
31
|
+
classes_documented: int = 0
|
|
32
|
+
classes_documented_pct: float = 0.0
|
|
33
|
+
properties_labelled: int = 0
|
|
34
|
+
properties_labelled_pct: float = 0.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Predicates considered as labels
|
|
38
|
+
LABEL_PREDICATES = (
|
|
39
|
+
RDFS.label,
|
|
40
|
+
SKOS.prefLabel,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Predicates considered as documentation
|
|
44
|
+
DOC_PREDICATES = (
|
|
45
|
+
RDFS.comment,
|
|
46
|
+
SKOS.definition,
|
|
47
|
+
DCTERMS.description,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_all_classes(graph: Graph) -> set:
|
|
52
|
+
"""Get all classes from the graph."""
|
|
53
|
+
classes = set(graph.subjects(RDF.type, OWL.Class))
|
|
54
|
+
classes |= set(graph.subjects(RDF.type, RDFS.Class))
|
|
55
|
+
return classes
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_all_properties(graph: Graph) -> set:
|
|
59
|
+
"""Get all properties from the graph."""
|
|
60
|
+
props = set()
|
|
61
|
+
for prop_type in (
|
|
62
|
+
OWL.ObjectProperty,
|
|
63
|
+
OWL.DatatypeProperty,
|
|
64
|
+
OWL.AnnotationProperty,
|
|
65
|
+
RDF.Property,
|
|
66
|
+
):
|
|
67
|
+
props |= set(graph.subjects(RDF.type, prop_type))
|
|
68
|
+
return props
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _has_any_predicate(graph: Graph, subject: object, predicates: tuple) -> bool:
|
|
72
|
+
"""Check if subject has any of the given predicates.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
graph: RDF graph to query.
|
|
76
|
+
subject: Subject to check.
|
|
77
|
+
predicates: Tuple of predicates to look for.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if subject has at least one of the predicates.
|
|
81
|
+
"""
|
|
82
|
+
for pred in predicates:
|
|
83
|
+
if graph.value(subject, pred) is not None:
|
|
84
|
+
return True
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def collect_documentation_stats(graph: Graph) -> DocumentationStats:
|
|
89
|
+
"""Collect documentation statistics from an RDF graph.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
graph: RDF graph to analyse.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
DocumentationStats with all documentation metrics populated.
|
|
96
|
+
"""
|
|
97
|
+
classes = _get_all_classes(graph)
|
|
98
|
+
properties = _get_all_properties(graph)
|
|
99
|
+
|
|
100
|
+
total_classes = len(classes)
|
|
101
|
+
total_props = len(properties)
|
|
102
|
+
|
|
103
|
+
if total_classes == 0 and total_props == 0:
|
|
104
|
+
return DocumentationStats()
|
|
105
|
+
|
|
106
|
+
# Count classes with labels
|
|
107
|
+
classes_with_label = sum(
|
|
108
|
+
1 for c in classes if _has_any_predicate(graph, c, LABEL_PREDICATES)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Count classes with documentation
|
|
112
|
+
classes_with_doc = sum(
|
|
113
|
+
1 for c in classes if _has_any_predicate(graph, c, DOC_PREDICATES)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Count properties with labels
|
|
117
|
+
props_with_label = sum(
|
|
118
|
+
1 for p in properties if _has_any_predicate(graph, p, LABEL_PREDICATES)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return DocumentationStats(
|
|
122
|
+
classes_labelled=classes_with_label,
|
|
123
|
+
classes_labelled_pct=round(classes_with_label / total_classes, 3) if total_classes else 0.0,
|
|
124
|
+
classes_documented=classes_with_doc,
|
|
125
|
+
classes_documented_pct=round(classes_with_doc / total_classes, 3) if total_classes else 0.0,
|
|
126
|
+
properties_labelled=props_with_label,
|
|
127
|
+
properties_labelled_pct=round(props_with_label / total_props, 3) if total_props else 0.0,
|
|
128
|
+
)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Hierarchy metrics for RDF ontologies.
|
|
2
|
+
|
|
3
|
+
Analyses class hierarchy structure: depth, branching factor, orphan classes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from rdflib import Graph, RDF, RDFS
|
|
10
|
+
from rdflib.namespace import OWL
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class HierarchyStats:
|
|
15
|
+
"""Hierarchy statistics for an ontology.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
root_classes: Classes with no superclass (except owl:Thing).
|
|
19
|
+
leaf_classes: Classes with no subclasses.
|
|
20
|
+
max_depth: Maximum depth of the class hierarchy.
|
|
21
|
+
avg_depth: Average depth across all classes.
|
|
22
|
+
avg_branching: Average number of direct subclasses per class.
|
|
23
|
+
orphan_classes: Classes not connected to the main hierarchy.
|
|
24
|
+
orphan_rate: Proportion of orphan classes (0.0 - 1.0).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
root_classes: int = 0
|
|
28
|
+
leaf_classes: int = 0
|
|
29
|
+
max_depth: int = 0
|
|
30
|
+
avg_depth: float = 0.0
|
|
31
|
+
avg_branching: float = 0.0
|
|
32
|
+
orphan_classes: int = 0
|
|
33
|
+
orphan_rate: float = 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_all_classes(graph: Graph) -> set:
|
|
37
|
+
"""Get all classes from the graph."""
|
|
38
|
+
classes = set(graph.subjects(RDF.type, OWL.Class))
|
|
39
|
+
classes |= set(graph.subjects(RDF.type, RDFS.Class))
|
|
40
|
+
return classes
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_hierarchy(graph: Graph, classes: set) -> tuple[dict, dict]:
|
|
44
|
+
"""Build parent/child adjacency lists from the class hierarchy.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
graph: RDF graph to query.
|
|
48
|
+
classes: Set of class URIRefs.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (parents_of, children_of) dictionaries.
|
|
52
|
+
"""
|
|
53
|
+
parents_of: dict = defaultdict(set) # child -> {parents}
|
|
54
|
+
children_of: dict = defaultdict(set) # parent -> {children}
|
|
55
|
+
|
|
56
|
+
for cls in classes:
|
|
57
|
+
for parent in graph.objects(cls, RDFS.subClassOf):
|
|
58
|
+
# Only consider class-class relationships
|
|
59
|
+
if parent in classes:
|
|
60
|
+
parents_of[cls].add(parent)
|
|
61
|
+
children_of[parent].add(cls)
|
|
62
|
+
|
|
63
|
+
return dict(parents_of), dict(children_of)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _compute_depths(classes: set, parents_of: dict) -> dict:
|
|
67
|
+
"""Compute depth of each class in the hierarchy.
|
|
68
|
+
|
|
69
|
+
Depth is the length of the longest path from a root class.
|
|
70
|
+
Root classes (no parents) have depth 0.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
classes: Set of all class URIRefs.
|
|
74
|
+
parents_of: Dictionary mapping class -> set of parent classes.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dictionary mapping class -> depth.
|
|
78
|
+
"""
|
|
79
|
+
depths: dict = {}
|
|
80
|
+
|
|
81
|
+
def compute_depth(cls: object, visited: set) -> int:
|
|
82
|
+
"""Recursively compute depth, handling cycles."""
|
|
83
|
+
if cls in depths:
|
|
84
|
+
return depths[cls]
|
|
85
|
+
if cls in visited:
|
|
86
|
+
# Cycle detected
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
visited.add(cls)
|
|
90
|
+
parents = parents_of.get(cls, set())
|
|
91
|
+
|
|
92
|
+
if not parents:
|
|
93
|
+
# Root class
|
|
94
|
+
depth = 0
|
|
95
|
+
else:
|
|
96
|
+
# Depth is max parent depth + 1
|
|
97
|
+
depth = max(compute_depth(p, visited) for p in parents) + 1
|
|
98
|
+
|
|
99
|
+
depths[cls] = depth
|
|
100
|
+
return depth
|
|
101
|
+
|
|
102
|
+
for cls in classes:
|
|
103
|
+
if cls not in depths:
|
|
104
|
+
compute_depth(cls, set())
|
|
105
|
+
|
|
106
|
+
return depths
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _find_roots(classes: set, parents_of: dict) -> set:
|
|
110
|
+
"""Find root classes (those with no parent in the class set).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
classes: Set of all class URIRefs.
|
|
114
|
+
parents_of: Dictionary mapping class -> set of parent classes.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Set of root class URIRefs.
|
|
118
|
+
"""
|
|
119
|
+
return {cls for cls in classes if not parents_of.get(cls)}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _find_leaves(classes: set, children_of: dict) -> set:
|
|
123
|
+
"""Find leaf classes (those with no children).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
classes: Set of all class URIRefs.
|
|
127
|
+
children_of: Dictionary mapping class -> set of child classes.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Set of leaf class URIRefs.
|
|
131
|
+
"""
|
|
132
|
+
return {cls for cls in classes if not children_of.get(cls)}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _find_orphans(classes: set, parents_of: dict, children_of: dict) -> set:
|
|
136
|
+
"""Find orphan classes (not connected to any other class).
|
|
137
|
+
|
|
138
|
+
An orphan class has no parents and no children in the hierarchy.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
classes: Set of all class URIRefs.
|
|
142
|
+
parents_of: Dictionary mapping class -> set of parent classes.
|
|
143
|
+
children_of: Dictionary mapping class -> set of child classes.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Set of orphan class URIRefs.
|
|
147
|
+
"""
|
|
148
|
+
return {
|
|
149
|
+
cls for cls in classes
|
|
150
|
+
if not parents_of.get(cls) and not children_of.get(cls)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _compute_avg_branching(classes: set, children_of: dict) -> float:
|
|
155
|
+
"""Compute average branching factor (subclasses per class).
|
|
156
|
+
|
|
157
|
+
Only considers classes that have at least one subclass.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
classes: Set of all class URIRefs.
|
|
161
|
+
children_of: Dictionary mapping class -> set of child classes.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Average number of subclasses per parent class.
|
|
165
|
+
"""
|
|
166
|
+
parent_counts = [len(children) for cls, children in children_of.items() if children]
|
|
167
|
+
if not parent_counts:
|
|
168
|
+
return 0.0
|
|
169
|
+
return sum(parent_counts) / len(parent_counts)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def collect_hierarchy_stats(graph: Graph) -> HierarchyStats:
|
|
173
|
+
"""Collect hierarchy statistics from an RDF graph.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
graph: RDF graph to analyse.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
HierarchyStats with all hierarchy metrics populated.
|
|
180
|
+
"""
|
|
181
|
+
classes = _get_all_classes(graph)
|
|
182
|
+
|
|
183
|
+
if not classes:
|
|
184
|
+
return HierarchyStats()
|
|
185
|
+
|
|
186
|
+
parents_of, children_of = _build_hierarchy(graph, classes)
|
|
187
|
+
depths = _compute_depths(classes, parents_of)
|
|
188
|
+
|
|
189
|
+
roots = _find_roots(classes, parents_of)
|
|
190
|
+
leaves = _find_leaves(classes, children_of)
|
|
191
|
+
orphans = _find_orphans(classes, parents_of, children_of)
|
|
192
|
+
|
|
193
|
+
max_depth = max(depths.values()) if depths else 0
|
|
194
|
+
avg_depth = sum(depths.values()) / len(depths) if depths else 0.0
|
|
195
|
+
avg_branching = _compute_avg_branching(classes, children_of)
|
|
196
|
+
|
|
197
|
+
orphan_rate = len(orphans) / len(classes) if classes else 0.0
|
|
198
|
+
|
|
199
|
+
return HierarchyStats(
|
|
200
|
+
root_classes=len(roots),
|
|
201
|
+
leaf_classes=len(leaves),
|
|
202
|
+
max_depth=max_depth,
|
|
203
|
+
avg_depth=round(avg_depth, 2),
|
|
204
|
+
avg_branching=round(avg_branching, 2),
|
|
205
|
+
orphan_classes=len(orphans),
|
|
206
|
+
orphan_rate=round(orphan_rate, 3),
|
|
207
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Property metrics for RDF ontologies.
|
|
2
|
+
|
|
3
|
+
Analyses property definitions: domain/range coverage, special characteristics.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from rdflib import Graph, RDF, RDFS
|
|
9
|
+
from rdflib.namespace import OWL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PropertyStats:
|
|
14
|
+
"""Property statistics for an ontology.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
with_domain: Properties that have rdfs:domain defined.
|
|
18
|
+
with_range: Properties that have rdfs:range defined.
|
|
19
|
+
domain_coverage: Proportion of properties with domain (0.0 - 1.0).
|
|
20
|
+
range_coverage: Proportion of properties with range (0.0 - 1.0).
|
|
21
|
+
inverse_pairs: Number of owl:inverseOf pairs.
|
|
22
|
+
functional: Number of owl:FunctionalProperty properties.
|
|
23
|
+
symmetric: Number of owl:SymmetricProperty properties.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
with_domain: int = 0
|
|
27
|
+
with_range: int = 0
|
|
28
|
+
domain_coverage: float = 0.0
|
|
29
|
+
range_coverage: float = 0.0
|
|
30
|
+
inverse_pairs: int = 0
|
|
31
|
+
functional: int = 0
|
|
32
|
+
symmetric: int = 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_all_properties(graph: Graph) -> set:
|
|
36
|
+
"""Get all properties from the graph (object + datatype + annotation)."""
|
|
37
|
+
props = set()
|
|
38
|
+
for prop_type in (
|
|
39
|
+
OWL.ObjectProperty,
|
|
40
|
+
OWL.DatatypeProperty,
|
|
41
|
+
OWL.AnnotationProperty,
|
|
42
|
+
RDF.Property,
|
|
43
|
+
):
|
|
44
|
+
props |= set(graph.subjects(RDF.type, prop_type))
|
|
45
|
+
return props
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def collect_property_stats(graph: Graph) -> PropertyStats:
|
|
49
|
+
"""Collect property statistics from an RDF graph.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
graph: RDF graph to analyse.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
PropertyStats with all property metrics populated.
|
|
56
|
+
"""
|
|
57
|
+
properties = _get_all_properties(graph)
|
|
58
|
+
total = len(properties)
|
|
59
|
+
|
|
60
|
+
if total == 0:
|
|
61
|
+
return PropertyStats()
|
|
62
|
+
|
|
63
|
+
# Count properties with domain
|
|
64
|
+
with_domain = sum(1 for p in properties if graph.value(p, RDFS.domain) is not None)
|
|
65
|
+
|
|
66
|
+
# Count properties with range
|
|
67
|
+
with_range = sum(1 for p in properties if graph.value(p, RDFS.range) is not None)
|
|
68
|
+
|
|
69
|
+
# Count inverse pairs (each owl:inverseOf creates a pair)
|
|
70
|
+
# Count unique pairs (A inverseOf B = B inverseOf A)
|
|
71
|
+
inverse_subjects = set(graph.subjects(OWL.inverseOf, None))
|
|
72
|
+
inverse_pairs = len(inverse_subjects) # Each subject represents one pair relationship
|
|
73
|
+
|
|
74
|
+
# Count functional properties
|
|
75
|
+
functional = len(set(graph.subjects(RDF.type, OWL.FunctionalProperty)))
|
|
76
|
+
|
|
77
|
+
# Count symmetric properties
|
|
78
|
+
symmetric = len(set(graph.subjects(RDF.type, OWL.SymmetricProperty)))
|
|
79
|
+
|
|
80
|
+
return PropertyStats(
|
|
81
|
+
with_domain=with_domain,
|
|
82
|
+
with_range=with_range,
|
|
83
|
+
domain_coverage=round(with_domain / total, 3) if total else 0.0,
|
|
84
|
+
range_coverage=round(with_range / total, 3) if total else 0.0,
|
|
85
|
+
inverse_pairs=inverse_pairs,
|
|
86
|
+
functional=functional,
|
|
87
|
+
symmetric=symmetric,
|
|
88
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""UML diagram generation from RDF ontologies.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to generate PlantUML class diagrams
|
|
4
|
+
from RDF/OWL ontologies based on YAML-defined contexts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .context import UMLConfig, UMLContext, load_uml_config
|
|
8
|
+
from .mapper import collect_diagram_entities
|
|
9
|
+
from .renderer import render_plantuml
|
|
10
|
+
from .odm_renderer import ODMRenderer, render_odm_plantuml
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"UMLConfig",
|
|
14
|
+
"UMLContext",
|
|
15
|
+
"load_uml_config",
|
|
16
|
+
"collect_diagram_entities",
|
|
17
|
+
"render_plantuml",
|
|
18
|
+
"uml_layout",
|
|
19
|
+
"uml_style",
|
|
20
|
+
"ODMRenderer",
|
|
21
|
+
"render_odm_plantuml",
|
|
22
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""UML context configuration for selecting RDF subsets to diagram.
|
|
2
|
+
|
|
3
|
+
A context defines what classes, properties, and individuals to include
|
|
4
|
+
in a PlantUML diagram, along with filtering and inclusion rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UMLContext:
|
|
14
|
+
"""Represents a UML diagramming context from YAML configuration.
|
|
15
|
+
|
|
16
|
+
A context specifies which RDF entities to include in a PlantUML diagram,
|
|
17
|
+
how to traverse hierarchies, and which relationships to show.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
name: Context identifier
|
|
21
|
+
description: Human-readable description
|
|
22
|
+
mode: Selection mode ('default' or 'explicit')
|
|
23
|
+
root_classes: List of root class CURIEs to start from (default mode)
|
|
24
|
+
focus_classes: Explicit list of classes to include (default mode)
|
|
25
|
+
include_descendants: Whether to include subclasses (default mode)
|
|
26
|
+
max_depth: Maximum depth when traversing hierarchies (default mode)
|
|
27
|
+
properties: Property inclusion configuration
|
|
28
|
+
include_instances: Whether to include individuals
|
|
29
|
+
selector: Selector key for bulk selection (default mode)
|
|
30
|
+
explicit_classes: Explicit class list (explicit mode)
|
|
31
|
+
explicit_object_properties: Explicit object property list (explicit mode)
|
|
32
|
+
explicit_datatype_properties: Explicit datatype property list (explicit mode)
|
|
33
|
+
explicit_annotation_properties: Explicit annotation property list (explicit mode)
|
|
34
|
+
explicit_instances: Explicit instance list (explicit mode)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, name: str, config: dict[str, Any]):
|
|
38
|
+
"""Initialize a UML context from configuration.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Context identifier
|
|
42
|
+
config: Context configuration dictionary from YAML
|
|
43
|
+
"""
|
|
44
|
+
self.name = name
|
|
45
|
+
self.description = config.get("description", "")
|
|
46
|
+
|
|
47
|
+
# Determine mode (default vs explicit)
|
|
48
|
+
self.mode = config.get("mode", "default")
|
|
49
|
+
|
|
50
|
+
if self.mode == "explicit":
|
|
51
|
+
# Explicit mode: directly specify all entities
|
|
52
|
+
self.explicit_classes = config.get("classes", [])
|
|
53
|
+
self.explicit_object_properties = config.get("object_properties", [])
|
|
54
|
+
self.explicit_datatype_properties = config.get("datatype_properties", [])
|
|
55
|
+
self.explicit_annotation_properties = config.get("annotation_properties", [])
|
|
56
|
+
self.explicit_instances = config.get("instances", [])
|
|
57
|
+
|
|
58
|
+
# These are not used in explicit mode but set defaults for compatibility
|
|
59
|
+
self.root_classes = []
|
|
60
|
+
self.focus_classes = []
|
|
61
|
+
self.selector = None
|
|
62
|
+
self.include_descendants = False
|
|
63
|
+
self.max_depth = None
|
|
64
|
+
self.property_mode = "explicit"
|
|
65
|
+
self.property_include = []
|
|
66
|
+
self.property_exclude = []
|
|
67
|
+
self.include_instances = bool(self.explicit_instances)
|
|
68
|
+
|
|
69
|
+
else:
|
|
70
|
+
# Default mode: existing strategies
|
|
71
|
+
# Class selection strategies
|
|
72
|
+
self.root_classes = config.get("root_classes", [])
|
|
73
|
+
self.focus_classes = config.get("focus_classes", [])
|
|
74
|
+
self.selector = config.get("selector") # e.g., "classes"
|
|
75
|
+
|
|
76
|
+
# Traversal settings
|
|
77
|
+
self.include_descendants = config.get("include_descendants", False)
|
|
78
|
+
self.max_depth = config.get("max_depth")
|
|
79
|
+
|
|
80
|
+
# Property configuration
|
|
81
|
+
prop_config = config.get("properties", {})
|
|
82
|
+
if isinstance(prop_config, dict):
|
|
83
|
+
self.property_mode = prop_config.get("mode", "domain_based")
|
|
84
|
+
self.property_include = prop_config.get("include", [])
|
|
85
|
+
self.property_exclude = prop_config.get("exclude", [])
|
|
86
|
+
else:
|
|
87
|
+
# Simple boolean for backward compatibility
|
|
88
|
+
self.property_mode = "all" if prop_config else "none"
|
|
89
|
+
self.property_include = []
|
|
90
|
+
self.property_exclude = []
|
|
91
|
+
|
|
92
|
+
# Instances
|
|
93
|
+
self.include_instances = config.get("include_instances", False)
|
|
94
|
+
|
|
95
|
+
# Explicit mode attributes not used but set for compatibility
|
|
96
|
+
self.explicit_classes = []
|
|
97
|
+
self.explicit_object_properties = []
|
|
98
|
+
self.explicit_datatype_properties = []
|
|
99
|
+
self.explicit_annotation_properties = []
|
|
100
|
+
self.explicit_instances = []
|
|
101
|
+
|
|
102
|
+
# Style reference (will be used later)
|
|
103
|
+
self.style = config.get("style", "default")
|
|
104
|
+
|
|
105
|
+
def has_class_selection(self) -> bool:
|
|
106
|
+
"""Check if context has any class selection criteria."""
|
|
107
|
+
if self.mode == "explicit":
|
|
108
|
+
return bool(self.explicit_classes)
|
|
109
|
+
return bool(
|
|
110
|
+
self.root_classes or self.focus_classes or self.selector
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def __repr__(self) -> str:
|
|
114
|
+
if self.mode == "explicit":
|
|
115
|
+
return (
|
|
116
|
+
f"UMLContext(name={self.name!r}, mode='explicit', "
|
|
117
|
+
f"classes={len(self.explicit_classes)})"
|
|
118
|
+
)
|
|
119
|
+
return (
|
|
120
|
+
f"UMLContext(name={self.name!r}, mode='default', "
|
|
121
|
+
f"roots={len(self.root_classes)}, "
|
|
122
|
+
f"focus={len(self.focus_classes)})"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class UMLConfig:
|
|
127
|
+
"""Configuration for UML diagram generation.
|
|
128
|
+
|
|
129
|
+
Loads and manages YAML-based UML context specifications with support
|
|
130
|
+
for multiple contexts, default settings, and shared configuration via
|
|
131
|
+
YAML anchors.
|
|
132
|
+
|
|
133
|
+
Attributes:
|
|
134
|
+
defaults: Default settings applied across contexts
|
|
135
|
+
contexts: Dictionary of available UML contexts
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, yaml_path: Path | str):
|
|
139
|
+
"""Load UML configuration from a YAML file.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
yaml_path: Path to YAML configuration file
|
|
143
|
+
"""
|
|
144
|
+
yaml_path = Path(yaml_path)
|
|
145
|
+
self.config = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
146
|
+
|
|
147
|
+
self.defaults = self.config.get("defaults", {}) or {}
|
|
148
|
+
|
|
149
|
+
# Load contexts
|
|
150
|
+
self.contexts = {}
|
|
151
|
+
for ctx_name, ctx_config in (self.config.get("contexts", {}) or {}).items():
|
|
152
|
+
self.contexts[ctx_name] = UMLContext(ctx_name, ctx_config)
|
|
153
|
+
|
|
154
|
+
def get_context(self, name: str) -> UMLContext:
|
|
155
|
+
"""Get a context by name.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
name: Context identifier
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
UMLContext instance
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
KeyError: If context name not found
|
|
165
|
+
"""
|
|
166
|
+
if name not in self.contexts:
|
|
167
|
+
raise KeyError(
|
|
168
|
+
f"Context '{name}' not found. Available contexts: "
|
|
169
|
+
f"{', '.join(self.contexts.keys())}"
|
|
170
|
+
)
|
|
171
|
+
return self.contexts[name]
|
|
172
|
+
|
|
173
|
+
def list_contexts(self) -> list[str]:
|
|
174
|
+
"""Get list of available context names.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of context identifier strings
|
|
178
|
+
"""
|
|
179
|
+
return list(self.contexts.keys())
|
|
180
|
+
|
|
181
|
+
def __repr__(self) -> str:
|
|
182
|
+
return f"UMLConfig(contexts={list(self.contexts.keys())})"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def load_uml_config(path: Path | str) -> UMLConfig:
|
|
186
|
+
"""Load UML configuration from a YAML file.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
path: Path to YAML configuration file
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
UMLConfig instance
|
|
193
|
+
"""
|
|
194
|
+
return UMLConfig(path)
|