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.
Files changed (88) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +1762 -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/main.py +6 -0
  51. rdf_construct/puml2rdf/__init__.py +103 -0
  52. rdf_construct/puml2rdf/config.py +230 -0
  53. rdf_construct/puml2rdf/converter.py +420 -0
  54. rdf_construct/puml2rdf/merger.py +200 -0
  55. rdf_construct/puml2rdf/model.py +202 -0
  56. rdf_construct/puml2rdf/parser.py +565 -0
  57. rdf_construct/puml2rdf/validators.py +451 -0
  58. rdf_construct/shacl/__init__.py +56 -0
  59. rdf_construct/shacl/config.py +166 -0
  60. rdf_construct/shacl/converters.py +520 -0
  61. rdf_construct/shacl/generator.py +364 -0
  62. rdf_construct/shacl/namespaces.py +93 -0
  63. rdf_construct/stats/__init__.py +29 -0
  64. rdf_construct/stats/collector.py +178 -0
  65. rdf_construct/stats/comparator.py +298 -0
  66. rdf_construct/stats/formatters/__init__.py +83 -0
  67. rdf_construct/stats/formatters/json.py +38 -0
  68. rdf_construct/stats/formatters/markdown.py +153 -0
  69. rdf_construct/stats/formatters/text.py +186 -0
  70. rdf_construct/stats/metrics/__init__.py +26 -0
  71. rdf_construct/stats/metrics/basic.py +147 -0
  72. rdf_construct/stats/metrics/complexity.py +137 -0
  73. rdf_construct/stats/metrics/connectivity.py +130 -0
  74. rdf_construct/stats/metrics/documentation.py +128 -0
  75. rdf_construct/stats/metrics/hierarchy.py +207 -0
  76. rdf_construct/stats/metrics/properties.py +88 -0
  77. rdf_construct/uml/__init__.py +22 -0
  78. rdf_construct/uml/context.py +194 -0
  79. rdf_construct/uml/mapper.py +371 -0
  80. rdf_construct/uml/odm_renderer.py +789 -0
  81. rdf_construct/uml/renderer.py +684 -0
  82. rdf_construct/uml/uml_layout.py +393 -0
  83. rdf_construct/uml/uml_style.py +613 -0
  84. rdf_construct-0.2.0.dist-info/METADATA +431 -0
  85. rdf_construct-0.2.0.dist-info/RECORD +88 -0
  86. rdf_construct-0.2.0.dist-info/WHEEL +4 -0
  87. rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
  88. rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,613 @@
1
+ """PlantUML styling configuration for RDF class diagrams.
2
+
3
+ Provides color schemes, arrow styles, and visual formatting for different
4
+ RDF entity types based on their semantic roles.
5
+
6
+ Added instance-specific styling based on rdf:type hierarchy.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Optional
11
+
12
+ import yaml
13
+ from rdflib import Graph, URIRef, RDF, RDFS
14
+ from rdflib.namespace import OWL
15
+
16
+
17
+ class ColorPalette:
18
+ """Color definitions for a single entity type.
19
+
20
+ Attributes:
21
+ border: Border/line color (hex or PlantUML color name)
22
+ fill: Fill/background color (hex or PlantUML color name)
23
+ text: Text color (optional, defaults to black)
24
+ line_style: Line style (e.g., 'bold', 'dashed', 'dotted')
25
+ """
26
+
27
+ def __init__(self, config: dict[str, Any]):
28
+ """Initialise colour palette from configuration.
29
+
30
+ Args:
31
+ config: Dictionary with colour specifications
32
+ """
33
+ self.border = config.get("border", "#000000")
34
+ self.fill = config.get("fill", "#FFFFFF")
35
+ self.text = config.get("text")
36
+ self.line_style = config.get("line_style")
37
+
38
+ def to_plantuml(self) -> str:
39
+ """Generate PlantUML color specification.
40
+
41
+ Returns string in format: #back:FILL;line:BORDER;line.STYLE;text:TEXT
42
+ Not just #FILL
43
+
44
+ Returns:
45
+ PlantUML color spec string with # prefix, or empty if no styling
46
+ """
47
+ if not self.fill and not self.border and not self.text:
48
+ return ""
49
+
50
+ parts = []
51
+
52
+ # Background fill (note: PlantUML uses 'back:', not just fill)
53
+ if self.fill:
54
+ fill_hex = self.fill.lstrip('#')
55
+ parts.append(f"back:{fill_hex}")
56
+
57
+ # Border colour and style
58
+ if self.border:
59
+ border_hex = self.border.lstrip('#')
60
+ parts.append(f"line:{border_hex}")
61
+
62
+ if self.line_style:
63
+ parts.append(f"line.{self.line_style}")
64
+
65
+ # Text colour
66
+ if self.text:
67
+ text_hex = self.text.lstrip('#')
68
+ parts.append(f"text:{text_hex}")
69
+
70
+ return f"#{';'.join(parts)}" if parts else ""
71
+
72
+
73
+ class ArrowStyle:
74
+ """Style specification for relationship arrows.
75
+
76
+ Attributes:
77
+ color: Arrow line color
78
+ thickness: Line thickness (e.g., 1, 2, 3)
79
+ style: Line style ('bold', 'dashed', 'dotted', 'hidden')
80
+ label_color: Color for relationship labels
81
+ """
82
+
83
+ def __init__(self, config: dict[str, Any]):
84
+ """Initialize arrow style from configuration.
85
+
86
+ Args:
87
+ config: Dictionary with arrow style specifications
88
+ """
89
+ self.color = config.get("color", "#000000")
90
+ self.thickness = config.get("thickness")
91
+ self.style = config.get("style")
92
+ self.label_color = config.get("label_color")
93
+
94
+ def to_plantuml_directive(self) -> Optional[str]:
95
+ """Generate PlantUML skinparam directive for this arrow style.
96
+
97
+ Returns:
98
+ Skinparam directive or None if no customization needed
99
+ """
100
+ if not (self.color or self.thickness or self.style):
101
+ return None
102
+
103
+ parts = []
104
+ if self.color:
105
+ parts.append(f"skinparam arrowColor {self.color}")
106
+ if self.thickness:
107
+ parts.append(f"skinparam arrowThickness {self.thickness}")
108
+
109
+ return "\n".join(parts) if parts else None
110
+
111
+ def __repr__(self) -> str:
112
+ return f"ArrowStyle(color={self.color}, style={self.style})"
113
+
114
+
115
+ class ArrowColorConfig:
116
+ """Configuration for arrow colors based on relationship type.
117
+
118
+ Attributes:
119
+ type_arrow_color: Color for rdf:type relationships (default red)
120
+ subclass_arrow_color: Color for rdfs:subClassOf relationships
121
+ property_arrow_color: Color for object property relationships
122
+ domain_range_arrow_color: Color for domain/range relationships
123
+ datatype_arrow_color: Color for datatype property relationships
124
+ """
125
+
126
+ def __init__(self, config: dict[str, str]):
127
+ """Initialize arrow colors from configuration.
128
+
129
+ Args:
130
+ config: Dictionary mapping relationship types to hex colors
131
+ """
132
+ self.type_arrow_color = config.get("type", "#FF0000") # Red
133
+ self.subclass_arrow_color = config.get("subclass", "#000000") # Black
134
+ self.property_arrow_color = config.get("property", "#000000") # Black
135
+ self.domain_range_arrow_color = config.get("domain_range", "#000000") # Black
136
+ self.datatype_arrow_color = config.get("datatype", "#000000") # Black
137
+
138
+ def get_color(self, relationship_type: str) -> str:
139
+ """Get color for a specific relationship type.
140
+
141
+ Args:
142
+ relationship_type: Type of relationship
143
+ ('type', 'subclass', 'property', 'domain_range', 'datatype')
144
+
145
+ Returns:
146
+ Hex color code
147
+ """
148
+ color_map = {
149
+ "type": self.type_arrow_color,
150
+ "subclass": self.subclass_arrow_color,
151
+ "property": self.property_arrow_color,
152
+ "domain_range": self.domain_range_arrow_color,
153
+ "datatype": self.datatype_arrow_color,
154
+ }
155
+ return color_map.get(relationship_type, "#000000")
156
+
157
+
158
+ class StyleScheme:
159
+ """Complete styling scheme for UML diagrams.
160
+
161
+ Attributes:
162
+ name: Scheme identifier
163
+ description: Human-readable description
164
+ class_styles: Mapping of class patterns to color palettes
165
+ instance_styles: Mapping of instance type patterns to colour palettes
166
+ instance_style_default: Default style for instances (fallback)
167
+ arrow_styles: Mapping of relationship types to arrow styles
168
+ show_stereotypes: Whether to display UML stereotypes
169
+ stereotype_map: Mapping of RDF types to stereotype labels
170
+ """
171
+
172
+ def __init__(self, name: str, config: dict[str, Any]):
173
+ """Initialise style scheme from configuration.
174
+
175
+ Args:
176
+ name: Scheme identifier
177
+ config: Style configuration dictionary from YAML
178
+ """
179
+ self.name = name
180
+ self.description = config.get("description", "")
181
+
182
+ # Class styling
183
+ class_config = config.get("classes", {})
184
+ self.class_styles = {}
185
+
186
+ # By namespace
187
+ by_namespace = class_config.get("by_namespace", {})
188
+ for ns_prefix, palette_config in by_namespace.items():
189
+ self.class_styles[f"ns:{ns_prefix}"] = ColorPalette(palette_config)
190
+
191
+ # By type (for specific classes)
192
+ by_type = class_config.get("by_type", {})
193
+ for type_key, palette_config in by_type.items():
194
+ self.class_styles[f"type:{type_key}"] = ColorPalette(palette_config)
195
+
196
+ # Default class style
197
+ if "default" in class_config:
198
+ self.class_styles["default"] = ColorPalette(class_config["default"])
199
+
200
+ # Instance styling
201
+ instance_config = config.get("instances", {})
202
+ self.instance_styles = {}
203
+
204
+ # Load by_type instance styles
205
+ by_type_instances = instance_config.get("by_type", {})
206
+ for type_key, palette_config in by_type_instances.items():
207
+ self.instance_styles[f"type:{type_key}"] = ColorPalette(palette_config)
208
+
209
+ # Default instance style (fallback if no by_type match)
210
+ if "default" in instance_config:
211
+ self.instance_style_default = ColorPalette(instance_config["default"])
212
+ else:
213
+ # Legacy support: if no 'default' key but instance_config has color keys
214
+ # treat the whole config as a palette
215
+ if any(k in instance_config for k in ["border", "fill", "text"]):
216
+ self.instance_style_default = ColorPalette(instance_config)
217
+ else:
218
+ self.instance_style_default = None
219
+
220
+ # Legacy support for inherit_class_border flag
221
+ self.instance_inherit_class_border = instance_config.get(
222
+ "inherit_class_border", False
223
+ )
224
+ # Inherit_class_text flag (for text colour matching class fill)
225
+ self.instance_inherit_class_text = instance_config.get(
226
+ "inherit_class_text", False
227
+ )
228
+
229
+ # Arrow styling
230
+ arrow_config = config.get("arrows", {})
231
+ self.arrow_styles = {}
232
+ for arrow_type, arrow_cfg in arrow_config.items():
233
+ self.arrow_styles[arrow_type] = ArrowStyle(arrow_cfg)
234
+
235
+ # Arrow color configuration
236
+ arrow_color_config = config.get("arrow_colors", {})
237
+ self.arrow_colors = ArrowColorConfig(arrow_color_config)
238
+
239
+ # Stereotype configuration
240
+ self.show_stereotypes = config.get("show_stereotypes", False)
241
+ self.stereotype_map = config.get("stereotype_map", {})
242
+
243
+ def get_class_style(
244
+ self, graph: Graph, cls: URIRef, is_instance: bool = False
245
+ ) -> Optional[ColorPalette]:
246
+ """Get color palette for a specific class or instance.
247
+
248
+ Selection priority:
249
+ 1. Instance-specific styling (if is_instance=True) via get_instance_style()
250
+ 2. Explicit type mapping (by_type)
251
+ 3. Inheritance-based lookup (traverse rdfs:subClassOf)
252
+ 4. Namespace-based coloring (by_namespace)
253
+ 5. Default class style
254
+
255
+ Args:
256
+ graph: RDF graph containing the class
257
+ cls: Class URI
258
+ is_instance: Whether this is an instance rather than a class
259
+
260
+ Returns:
261
+ ColorPalette or None if no style defined
262
+ """
263
+ # Priority 1: Instance-specific styling
264
+ if is_instance:
265
+ return self.get_instance_style(graph, cls)
266
+
267
+ # Priority 2: Check for explicit type mapping
268
+ qn = graph.namespace_manager.normalizeUri(cls)
269
+ type_key = f"type:{qn}"
270
+ if type_key in self.class_styles:
271
+ return self.class_styles[type_key]
272
+
273
+ # Priority 3: INHERITANCE-BASED LOOKUP
274
+ # Walk up rdfs:subClassOf hierarchy to find styled superclass
275
+ style = self._get_inherited_style(graph, cls)
276
+ if style:
277
+ return style
278
+
279
+ # Priority 4: Namespace-based coloring
280
+ if ":" in qn:
281
+ ns_prefix = qn.split(":")[0]
282
+ ns_key = f"ns:{ns_prefix}"
283
+ if ns_key in self.class_styles:
284
+ return self.class_styles[ns_key]
285
+
286
+ # Priority 5: Default
287
+ return self.class_styles.get("default")
288
+
289
+ def get_instance_style(
290
+ self, graph: Graph, instance: URIRef
291
+ ) -> Optional[ColorPalette]:
292
+ """Get color palette for an instance based on its rdf:type hierarchy.
293
+
294
+ Selection priority:
295
+ 1. Explicit type mapping in instances.by_type (using first rdf:type)
296
+ 2. Walk up rdf:type's superclass hierarchy to find styled class
297
+ 3. Default instance style
298
+ 4. Fall back to None
299
+
300
+ If inherit_class_text is enabled, text color matches the class border color.
301
+
302
+ Args:
303
+ graph: RDF graph containing the instance
304
+ instance: Instance URI
305
+
306
+ Returns:
307
+ ColorPalette for the instance, or None if no style defined
308
+ """
309
+ # Get all rdf:type declarations for this instance
310
+ instance_types = list(graph.objects(instance, RDF.type))
311
+
312
+ # Filter out metaclass types that shouldn't affect instance styling
313
+ metaclass_types = {
314
+ OWL.Class, RDFS.Class,
315
+ OWL.ObjectProperty, OWL.DatatypeProperty,
316
+ OWL.AnnotationProperty, RDF.Property
317
+ }
318
+ valid_types = [t for t in instance_types if t not in metaclass_types]
319
+
320
+ if not valid_types:
321
+ # No valid types - use default
322
+ return self.instance_style_default
323
+
324
+ # Use the first declared type as primary
325
+ primary_type = valid_types[0]
326
+ primary_type_qn = graph.namespace_manager.normalizeUri(primary_type)
327
+
328
+ # Priority 1: Check for explicit instance type styling
329
+ type_key = f"type:{primary_type_qn}"
330
+ if type_key in self.instance_styles:
331
+ palette = self.instance_styles[type_key]
332
+
333
+ # Apply text color inheritance if enabled
334
+ if self.instance_inherit_class_text and palette:
335
+ return self._apply_class_text_inheritance(
336
+ graph, primary_type, palette
337
+ )
338
+ return palette
339
+
340
+ # Priority 2: Walk up the type's class hierarchy to find styled class
341
+ # This allows instances to inherit colors from their class hierarchy
342
+ styled_class_palette = self._get_inherited_style(graph, primary_type)
343
+ if styled_class_palette:
344
+ # Create instance-specific palette based on class colors
345
+ instance_palette = ColorPalette({
346
+ "border": styled_class_palette.border,
347
+ "fill": "#000000", # Instances have black fill
348
+ "text": styled_class_palette.border
349
+ })
350
+ return instance_palette
351
+
352
+ # Priority 3: Default instance style
353
+ if self.instance_style_default:
354
+ if self.instance_inherit_class_text:
355
+ return self._apply_class_text_inheritance(
356
+ graph, primary_type, self.instance_style_default
357
+ )
358
+ return self.instance_style_default
359
+
360
+ # No styling found
361
+ return None
362
+
363
+ def _apply_class_text_inheritance(
364
+ self, graph: Graph, class_uri: URIRef, base_palette: ColorPalette
365
+ ) -> ColorPalette:
366
+ """Apply class text color inheritance to an instance palette.
367
+
368
+ Looks up the class's border color and applies it as text color.
369
+
370
+ Args:
371
+ graph: RDF graph
372
+ class_uri: Class URI to get colors from
373
+ base_palette: Base instance palette to modify
374
+
375
+ Returns:
376
+ New ColorPalette with inherited text color
377
+ """
378
+ # Get the class's styling
379
+ class_palette = self.get_class_style(graph, class_uri, is_instance=False)
380
+
381
+ if class_palette and class_palette.border:
382
+ # Use class border color as instance text color
383
+ return ColorPalette({
384
+ "border": base_palette.border,
385
+ "fill": base_palette.fill,
386
+ "text": class_palette.border,
387
+ "line_style": base_palette.line_style
388
+ })
389
+
390
+ return base_palette
391
+
392
+ def _get_inherited_style(
393
+ self, graph: Graph, cls: URIRef, visited: Optional[set] = None
394
+ ) -> Optional[ColorPalette]:
395
+ """Walk up rdfs:subClassOf hierarchy to find styled superclass.
396
+
397
+ This enables classes to inherit styles from their superclasses.
398
+ For example, building:Structure inherits from ies:Entity,
399
+ so it should get Entity's yellow color.
400
+
401
+ Args:
402
+ graph: RDF graph containing the class hierarchy
403
+ cls: Class URI to find style for
404
+ visited: Set of already-visited classes (prevents infinite loops)
405
+
406
+ Returns:
407
+ ColorPalette from nearest styled superclass, or None
408
+ """
409
+ if visited is None:
410
+ visited = set()
411
+
412
+ # Prevent infinite loops in case of circular inheritance
413
+ if cls in visited:
414
+ return None
415
+ visited.add(cls)
416
+
417
+ # Get all direct superclasses
418
+ superclasses = list(graph.objects(cls, RDFS.subClassOf))
419
+
420
+ # Check each superclass
421
+ for superclass in superclasses:
422
+ # Skip if not a proper URI (could be blank node)
423
+ if not isinstance(superclass, URIRef):
424
+ continue
425
+
426
+ # Check if this superclass has explicit styling
427
+ super_qn = graph.namespace_manager.normalizeUri(superclass)
428
+ type_key = f"type:{super_qn}"
429
+
430
+ if type_key in self.class_styles:
431
+ # Found a styled superclass!
432
+ return self.class_styles[type_key]
433
+
434
+ # Recursively check this superclass's parents
435
+ inherited = self._get_inherited_style(graph, superclass, visited)
436
+ if inherited:
437
+ return inherited
438
+
439
+ # No styled superclass found
440
+ return None
441
+
442
+ def get_property_style(
443
+ self, graph: Graph, prop: URIRef
444
+ ) -> Optional[ColorPalette]:
445
+ """Get colour palette for property class.
446
+
447
+ Properties render as classes with specific styling (typically gray).
448
+ Can be customised by namespace or specific property types.
449
+
450
+ Args:
451
+ graph: RDF graph containing the property
452
+ prop: Property URI
453
+
454
+ Returns:
455
+ Color palette for the property, or None for default styling
456
+ """
457
+ # Check for property-specific styling first
458
+ # Get QName using graph's namespace manager
459
+ prop_qname = graph.namespace_manager.normalizeUri(prop)
460
+ if prop_qname in self.class_styles:
461
+ return self.class_styles[prop_qname]
462
+
463
+ # Check namespace-based styling
464
+ if ":" in prop_qname:
465
+ ns_prefix = prop_qname.split(":")[0]
466
+ ns_key = f"ns:{ns_prefix}"
467
+ if ns_key in self.class_styles:
468
+ return self.class_styles[ns_key]
469
+
470
+ # Check for property type styling
471
+ # (e.g., different colors for ObjectProperty vs DatatypeProperty)
472
+ for prop_type in graph.objects(prop, RDF.type):
473
+ # Get QName using graph's namespace manager
474
+ type_qname = graph.namespace_manager.normalizeUri(prop_type)
475
+ type_key = f"type:{type_qname}"
476
+ if type_key in self.class_styles:
477
+ return self.class_styles[type_key]
478
+
479
+ # Default: gray for all properties
480
+ return ColorPalette({
481
+ "fill": "#CCCCCC",
482
+ "border": "#666666",
483
+ "text": "#000000"
484
+ })
485
+
486
+ def get_arrow_style(self, relationship_type: str) -> Optional[ArrowStyle]:
487
+ """Get arrow style for a relationship type.
488
+
489
+ Args:
490
+ relationship_type: Type of relationship ('subclass', 'instance',
491
+ 'object_property', 'rdf_type', etc.)
492
+
493
+ Returns:
494
+ ArrowStyle or None if no specific style defined
495
+ """
496
+ return self.arrow_styles.get(relationship_type)
497
+
498
+ def get_stereotype(
499
+ self, graph: Graph, entity: URIRef, is_instance: bool = False
500
+ ) -> Optional[str]:
501
+ """Get stereotype label for entity."""
502
+ if not self.show_stereotypes:
503
+ return None
504
+
505
+ # Handle instances with multiple types
506
+ if is_instance:
507
+ types = []
508
+ metaclass_types = {
509
+ "owl:Class", "rdfs:Class",
510
+ "owl:ObjectProperty", "owl:DatatypeProperty",
511
+ "owl:AnnotationProperty", "rdf:Property"
512
+ }
513
+
514
+ for rdf_type in graph.objects(entity, RDF.type):
515
+ # Get QName using graph's namespace manager
516
+ type_qname = graph.namespace_manager.normalizeUri(rdf_type)
517
+ if type_qname not in metaclass_types:
518
+ types.append(type_qname)
519
+
520
+ if types:
521
+ types.sort()
522
+ return f"<<{', '.join(types)}>>"
523
+ return "<<owl:NamedIndividual>>"
524
+
525
+ # Handle classes/properties (existing logic)
526
+ for rdf_type in graph.objects(entity, RDF.type):
527
+ # Get QName using graph's namespace manager
528
+ type_qname = graph.namespace_manager.normalizeUri(rdf_type)
529
+ if type_qname in self.stereotype_map:
530
+ return self.stereotype_map[type_qname]
531
+ if type_qname in ("owl:Class", "rdfs:Class",
532
+ "owl:ObjectProperty", "owl:DatatypeProperty",
533
+ "owl:AnnotationProperty", "rdf:Property"):
534
+ return f"<<{type_qname}>>"
535
+
536
+ return None
537
+
538
+ def __repr__(self) -> str:
539
+ return (
540
+ f"StyleScheme(name={self.name!r}, "
541
+ f"classes={len(self.class_styles)}, "
542
+ f"instances={len(self.instance_styles)})"
543
+ )
544
+
545
+
546
+ class StyleConfig:
547
+ """Configuration for PlantUML styling.
548
+
549
+ Loads and manages YAML-based style specifications with support
550
+ for multiple schemes and shared configuration via YAML anchors.
551
+
552
+ Attributes:
553
+ defaults: Default styling settings
554
+ schemes: Dictionary of available style schemes
555
+ """
556
+
557
+ def __init__(self, yaml_path: Path | str):
558
+ """Load style configuration from a YAML file.
559
+
560
+ Args:
561
+ yaml_path: Path to YAML style configuration file
562
+ """
563
+ yaml_path = Path(yaml_path)
564
+ self.config = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
565
+
566
+ self.defaults = self.config.get("defaults", {}) or {}
567
+
568
+ # Load schemes
569
+ self.schemes = {}
570
+ for scheme_name, scheme_config in (self.config.get("schemes", {}) or {}).items():
571
+ self.schemes[scheme_name] = StyleScheme(scheme_name, scheme_config)
572
+
573
+ def get_scheme(self, name: str) -> StyleScheme:
574
+ """Get a style scheme by name.
575
+
576
+ Args:
577
+ name: Scheme identifier
578
+
579
+ Returns:
580
+ StyleScheme instance
581
+
582
+ Raises:
583
+ KeyError: If scheme name not found
584
+ """
585
+ if name not in self.schemes:
586
+ raise KeyError(
587
+ f"Style scheme '{name}' not found. Available schemes: "
588
+ f"{', '.join(self.schemes.keys())}"
589
+ )
590
+ return self.schemes[name]
591
+
592
+ def list_schemes(self) -> list[str]:
593
+ """Get list of available scheme names.
594
+
595
+ Returns:
596
+ List of scheme identifier strings
597
+ """
598
+ return list(self.schemes.keys())
599
+
600
+ def __repr__(self) -> str:
601
+ return f"StyleConfig(schemes={list(self.schemes.keys())})"
602
+
603
+
604
+ def load_style_config(path: Path | str) -> StyleConfig:
605
+ """Load style configuration from a YAML file.
606
+
607
+ Args:
608
+ path: Path to YAML style configuration file
609
+
610
+ Returns:
611
+ StyleConfig instance
612
+ """
613
+ return StyleConfig(path)