rdf-construct 0.3.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 (110) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +3429 -0
  4. rdf_construct/core/__init__.py +33 -0
  5. rdf_construct/core/config.py +116 -0
  6. rdf_construct/core/ordering.py +219 -0
  7. rdf_construct/core/predicate_order.py +212 -0
  8. rdf_construct/core/profile.py +157 -0
  9. rdf_construct/core/selector.py +64 -0
  10. rdf_construct/core/serialiser.py +232 -0
  11. rdf_construct/core/utils.py +89 -0
  12. rdf_construct/cq/__init__.py +77 -0
  13. rdf_construct/cq/expectations.py +365 -0
  14. rdf_construct/cq/formatters/__init__.py +45 -0
  15. rdf_construct/cq/formatters/json.py +104 -0
  16. rdf_construct/cq/formatters/junit.py +104 -0
  17. rdf_construct/cq/formatters/text.py +146 -0
  18. rdf_construct/cq/loader.py +300 -0
  19. rdf_construct/cq/runner.py +321 -0
  20. rdf_construct/diff/__init__.py +59 -0
  21. rdf_construct/diff/change_types.py +214 -0
  22. rdf_construct/diff/comparator.py +338 -0
  23. rdf_construct/diff/filters.py +133 -0
  24. rdf_construct/diff/formatters/__init__.py +71 -0
  25. rdf_construct/diff/formatters/json.py +192 -0
  26. rdf_construct/diff/formatters/markdown.py +210 -0
  27. rdf_construct/diff/formatters/text.py +195 -0
  28. rdf_construct/docs/__init__.py +60 -0
  29. rdf_construct/docs/config.py +238 -0
  30. rdf_construct/docs/extractors.py +603 -0
  31. rdf_construct/docs/generator.py +360 -0
  32. rdf_construct/docs/renderers/__init__.py +7 -0
  33. rdf_construct/docs/renderers/html.py +803 -0
  34. rdf_construct/docs/renderers/json.py +390 -0
  35. rdf_construct/docs/renderers/markdown.py +628 -0
  36. rdf_construct/docs/search.py +278 -0
  37. rdf_construct/docs/templates/html/base.html.jinja +44 -0
  38. rdf_construct/docs/templates/html/class.html.jinja +152 -0
  39. rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
  40. rdf_construct/docs/templates/html/index.html.jinja +110 -0
  41. rdf_construct/docs/templates/html/instance.html.jinja +90 -0
  42. rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
  43. rdf_construct/docs/templates/html/property.html.jinja +124 -0
  44. rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
  45. rdf_construct/lint/__init__.py +75 -0
  46. rdf_construct/lint/config.py +214 -0
  47. rdf_construct/lint/engine.py +396 -0
  48. rdf_construct/lint/formatters.py +327 -0
  49. rdf_construct/lint/rules.py +692 -0
  50. rdf_construct/localise/__init__.py +114 -0
  51. rdf_construct/localise/config.py +508 -0
  52. rdf_construct/localise/extractor.py +427 -0
  53. rdf_construct/localise/formatters/__init__.py +36 -0
  54. rdf_construct/localise/formatters/markdown.py +229 -0
  55. rdf_construct/localise/formatters/text.py +224 -0
  56. rdf_construct/localise/merger.py +346 -0
  57. rdf_construct/localise/reporter.py +356 -0
  58. rdf_construct/main.py +6 -0
  59. rdf_construct/merge/__init__.py +165 -0
  60. rdf_construct/merge/config.py +354 -0
  61. rdf_construct/merge/conflicts.py +281 -0
  62. rdf_construct/merge/formatters.py +426 -0
  63. rdf_construct/merge/merger.py +425 -0
  64. rdf_construct/merge/migrator.py +339 -0
  65. rdf_construct/merge/rules.py +377 -0
  66. rdf_construct/merge/splitter.py +1102 -0
  67. rdf_construct/puml2rdf/__init__.py +103 -0
  68. rdf_construct/puml2rdf/config.py +230 -0
  69. rdf_construct/puml2rdf/converter.py +420 -0
  70. rdf_construct/puml2rdf/merger.py +200 -0
  71. rdf_construct/puml2rdf/model.py +202 -0
  72. rdf_construct/puml2rdf/parser.py +565 -0
  73. rdf_construct/puml2rdf/validators.py +451 -0
  74. rdf_construct/refactor/__init__.py +72 -0
  75. rdf_construct/refactor/config.py +362 -0
  76. rdf_construct/refactor/deprecator.py +328 -0
  77. rdf_construct/refactor/formatters/__init__.py +8 -0
  78. rdf_construct/refactor/formatters/text.py +311 -0
  79. rdf_construct/refactor/renamer.py +294 -0
  80. rdf_construct/shacl/__init__.py +56 -0
  81. rdf_construct/shacl/config.py +166 -0
  82. rdf_construct/shacl/converters.py +520 -0
  83. rdf_construct/shacl/generator.py +364 -0
  84. rdf_construct/shacl/namespaces.py +93 -0
  85. rdf_construct/stats/__init__.py +29 -0
  86. rdf_construct/stats/collector.py +178 -0
  87. rdf_construct/stats/comparator.py +298 -0
  88. rdf_construct/stats/formatters/__init__.py +83 -0
  89. rdf_construct/stats/formatters/json.py +38 -0
  90. rdf_construct/stats/formatters/markdown.py +153 -0
  91. rdf_construct/stats/formatters/text.py +186 -0
  92. rdf_construct/stats/metrics/__init__.py +26 -0
  93. rdf_construct/stats/metrics/basic.py +147 -0
  94. rdf_construct/stats/metrics/complexity.py +137 -0
  95. rdf_construct/stats/metrics/connectivity.py +130 -0
  96. rdf_construct/stats/metrics/documentation.py +128 -0
  97. rdf_construct/stats/metrics/hierarchy.py +207 -0
  98. rdf_construct/stats/metrics/properties.py +88 -0
  99. rdf_construct/uml/__init__.py +22 -0
  100. rdf_construct/uml/context.py +194 -0
  101. rdf_construct/uml/mapper.py +371 -0
  102. rdf_construct/uml/odm_renderer.py +789 -0
  103. rdf_construct/uml/renderer.py +684 -0
  104. rdf_construct/uml/uml_layout.py +393 -0
  105. rdf_construct/uml/uml_style.py +613 -0
  106. rdf_construct-0.3.0.dist-info/METADATA +496 -0
  107. rdf_construct-0.3.0.dist-info/RECORD +110 -0
  108. rdf_construct-0.3.0.dist-info/WHEEL +4 -0
  109. rdf_construct-0.3.0.dist-info/entry_points.txt +3 -0
  110. rdf_construct-0.3.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)