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,684 @@
|
|
|
1
|
+
"""PlantUML renderer with everything-as-class approach.
|
|
2
|
+
|
|
3
|
+
Renders RDF entities as PlantUML class diagrams where:
|
|
4
|
+
- Classes, properties, and instances all render as UML classes
|
|
5
|
+
- Stereotypes indicate entity type
|
|
6
|
+
- Relationships use appropriately styled arrows
|
|
7
|
+
- Datatype property values render as notes with dotted connections
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
|
|
14
|
+
from rdflib import Graph, URIRef, RDF, RDFS, Literal
|
|
15
|
+
from rdflib.namespace import OWL, XSD
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def qname(graph: Graph, uri: URIRef) -> str:
|
|
19
|
+
"""Get qualified name (prefix:local) for a URI.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
graph: RDF graph with namespace bindings
|
|
23
|
+
uri: URI to convert to QName
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
QName string (e.g., 'ex:Animal') or full URI if no prefix found
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
return graph.namespace_manager.normalizeUri(uri)
|
|
30
|
+
except Exception:
|
|
31
|
+
return str(uri)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def safe_label(graph: Graph, uri: URIRef, camelcase: bool = False) -> str:
|
|
35
|
+
"""Get a safe label for display in PlantUML.
|
|
36
|
+
|
|
37
|
+
Uses rdfs:label if available, otherwise falls back to QName.
|
|
38
|
+
Strips quotes and handles multi-line labels.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
graph: RDF graph containing the entity
|
|
42
|
+
uri: URI to get label for
|
|
43
|
+
camelcase: Whether to convert spaces to camelCase (for property names)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Safe string for use in PlantUML
|
|
47
|
+
"""
|
|
48
|
+
# Try to get rdfs:label
|
|
49
|
+
labels = list(graph.objects(uri, RDFS.label))
|
|
50
|
+
if labels:
|
|
51
|
+
label = str(labels[0])
|
|
52
|
+
# Clean up label for PlantUML
|
|
53
|
+
label = label.replace('"', "'").replace("\n", " ").strip()
|
|
54
|
+
|
|
55
|
+
# Convert spaces to camelCase only if requested (for property names)
|
|
56
|
+
if camelcase:
|
|
57
|
+
words = label.split()
|
|
58
|
+
if len(words) > 1:
|
|
59
|
+
# Ensure first word is lowercase for camelCase properties
|
|
60
|
+
label = words[0].lower() + "".join(word.capitalize() for word in words[1:])
|
|
61
|
+
|
|
62
|
+
return label
|
|
63
|
+
|
|
64
|
+
# Fallback to QName
|
|
65
|
+
qn = qname(graph, uri)
|
|
66
|
+
|
|
67
|
+
# For camelCase, ensure first character is lowercase
|
|
68
|
+
if camelcase and qn:
|
|
69
|
+
# Only lowercase the local part after the namespace prefix
|
|
70
|
+
if ":" in qn:
|
|
71
|
+
prefix, local = qn.split(":", 1)
|
|
72
|
+
if local:
|
|
73
|
+
local = local[0].lower() + local[1:]
|
|
74
|
+
qn = f"{prefix}:{local}"
|
|
75
|
+
else:
|
|
76
|
+
qn = qn[0].lower() + qn[1:]
|
|
77
|
+
|
|
78
|
+
return qn
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def escape_plantuml(text: str) -> str:
|
|
82
|
+
"""Escape special characters for PlantUML.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
text: Text to escape
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Escaped text safe for PlantUML
|
|
89
|
+
"""
|
|
90
|
+
# PlantUML is generally forgiving, but we'll handle basic escaping
|
|
91
|
+
return text.replace('"', "'")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def plantuml_identifier(graph: Graph, uri: URIRef) -> str:
|
|
95
|
+
"""Convert RDF URI to PlantUML identifier using dot notation.
|
|
96
|
+
|
|
97
|
+
PlantUML uses package.Class notation, not prefix:Class notation.
|
|
98
|
+
This function converts RDF QNames to proper PlantUML identifiers.
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
"building:Building" → "building.Building"
|
|
102
|
+
"ies:Entity" → "ies.Entity"
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
graph: RDF graph with namespace bindings
|
|
106
|
+
uri: URI to convert to PlantUML identifier
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
PlantUML identifier string with dot notation
|
|
110
|
+
"""
|
|
111
|
+
qn = qname(graph, uri)
|
|
112
|
+
|
|
113
|
+
# Convert prefix:local to prefix.local for PlantUML
|
|
114
|
+
if ":" in qn:
|
|
115
|
+
prefix, local = qn.split(":", 1)
|
|
116
|
+
return f"{prefix}.{local}"
|
|
117
|
+
|
|
118
|
+
# No namespace prefix - return as is
|
|
119
|
+
return qn
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class PlantUMLRenderer:
|
|
123
|
+
"""Renders RDF entities as styled PlantUML class diagrams.
|
|
124
|
+
|
|
125
|
+
Everything renders as a UML class:
|
|
126
|
+
- OWL/RDFS classes with stereotypes like <<owl:Class>>
|
|
127
|
+
- Properties as classes with stereotypes like <<owl:ObjectProperty>>
|
|
128
|
+
- Instances as classes with stereotypes showing their types
|
|
129
|
+
|
|
130
|
+
Relationships:
|
|
131
|
+
- rdfs:subClassOf and rdfs:subPropertyOf: --|> with labels
|
|
132
|
+
- rdf:type: red --> with <<rdf:type>> [#red] label
|
|
133
|
+
- domain/range: black --> with <<rdfs:domain>>/<<rdfs:range>> labels
|
|
134
|
+
- Object properties between instances: black --> with property name
|
|
135
|
+
- Datatype properties: dotted line to note .. with property name
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
graph: RDF graph being rendered
|
|
139
|
+
entities: Dictionary of selected entities to render
|
|
140
|
+
style: Style scheme to apply (optional)
|
|
141
|
+
layout: Layout configuration to apply (optional)
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
graph: Graph,
|
|
147
|
+
entities: dict[str, set[URIRef]],
|
|
148
|
+
style: Optional = None, # Will import StyleScheme type hint later
|
|
149
|
+
layout: Optional = None, # Will import LayoutConfig type hint later
|
|
150
|
+
):
|
|
151
|
+
"""Initialise renderer with graph, entities, and optional styling.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
graph: RDF graph containing the entities
|
|
155
|
+
entities: Dictionary of entity sets (classes, properties, instances)
|
|
156
|
+
style: Optional style scheme to apply
|
|
157
|
+
layout: Optional layout configuration to apply
|
|
158
|
+
"""
|
|
159
|
+
self.graph = graph
|
|
160
|
+
self.entities = entities
|
|
161
|
+
self.style = style
|
|
162
|
+
self.layout = layout
|
|
163
|
+
self._note_counter = 0 # For generating unique note IDs
|
|
164
|
+
|
|
165
|
+
def _get_arrow_direction(self) -> str:
|
|
166
|
+
"""Get arrow direction hint from layout config.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Direction string: 'u' (up), 'd' (down), 'l' (left), 'r' (right), or ''
|
|
170
|
+
"""
|
|
171
|
+
if self.layout and self.layout.arrow_direction:
|
|
172
|
+
direction_map = {
|
|
173
|
+
"up": "u",
|
|
174
|
+
"down": "d",
|
|
175
|
+
"left": "l",
|
|
176
|
+
"right": "r",
|
|
177
|
+
}
|
|
178
|
+
return direction_map.get(self.layout.arrow_direction, "u")
|
|
179
|
+
return "u" # Default: up (for top-to-bottom layout)
|
|
180
|
+
|
|
181
|
+
def _get_class_stereotype(self, cls: URIRef) -> str:
|
|
182
|
+
"""Get stereotype for a class entity.
|
|
183
|
+
|
|
184
|
+
Checks rdf:type to determine if it's owl:Class, rdfs:Class, etc.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
cls: Class URI
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Stereotype string like <<owl:Class>> or <<rdfs:Class>>
|
|
191
|
+
"""
|
|
192
|
+
# Check if it's typed as owl:Class or rdfs:Class
|
|
193
|
+
types = list(self.graph.objects(cls, RDF.type))
|
|
194
|
+
|
|
195
|
+
for t in types:
|
|
196
|
+
type_qn = qname(self.graph, t)
|
|
197
|
+
if type_qn in ("owl:Class", "rdfs:Class", "owl:Restriction"):
|
|
198
|
+
return f"<< (C, #FFFFFF) {type_qn} >>"
|
|
199
|
+
|
|
200
|
+
# Default to rdfs:Class if not explicitly typed
|
|
201
|
+
return "<< (C, #FFFFFF) rdfs:Class >>"
|
|
202
|
+
|
|
203
|
+
def _get_property_stereotype(self, prop: URIRef) -> str:
|
|
204
|
+
"""Get stereotype for a property entity.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
prop: Property URI
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Stereotype string like <<owl:ObjectProperty>>
|
|
211
|
+
"""
|
|
212
|
+
types = list(self.graph.objects(prop, RDF.type))
|
|
213
|
+
|
|
214
|
+
for t in types:
|
|
215
|
+
type_qn = qname(self.graph, t)
|
|
216
|
+
if type_qn in (
|
|
217
|
+
"owl:ObjectProperty",
|
|
218
|
+
"owl:DatatypeProperty",
|
|
219
|
+
"owl:AnnotationProperty",
|
|
220
|
+
"rdf:Property",
|
|
221
|
+
):
|
|
222
|
+
if type_qn == "owl:ObjectProperty":
|
|
223
|
+
type_symbol = "O"
|
|
224
|
+
elif type_qn == "owl:DatatypeProperty":
|
|
225
|
+
type_symbol = "D"
|
|
226
|
+
elif type_qn == "owl:AnnotationProperty":
|
|
227
|
+
type_symbol = "A"
|
|
228
|
+
else:
|
|
229
|
+
type_symbol = "P"
|
|
230
|
+
return f"<< ({type_symbol}, #FFFFFF) {type_qn} >>"
|
|
231
|
+
|
|
232
|
+
# Default
|
|
233
|
+
return "<< (P, #FFFFFF) rdf:Property >>"
|
|
234
|
+
|
|
235
|
+
def _get_instance_stereotype(self, instance: URIRef) -> str:
|
|
236
|
+
"""Get stereotype for an instance showing all its types.
|
|
237
|
+
|
|
238
|
+
For instances with multiple types, create comma-separated stereotype.
|
|
239
|
+
Example: <<ies:Entity>> or <<building:Structure, ies:Asset>>
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
instance: Instance URI
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Stereotype string with all types
|
|
246
|
+
"""
|
|
247
|
+
types = list(self.graph.objects(instance, RDF.type))
|
|
248
|
+
|
|
249
|
+
# Filter out property/class types (instances shouldn't have these)
|
|
250
|
+
type_qnames = []
|
|
251
|
+
for t in types:
|
|
252
|
+
type_qn = qname(self.graph, t)
|
|
253
|
+
# Skip metaclass types
|
|
254
|
+
if type_qn not in ("owl:Class", "rdfs:Class", "owl:ObjectProperty",
|
|
255
|
+
"owl:DatatypeProperty", "owl:AnnotationProperty"):
|
|
256
|
+
type_qnames.append(type_qn)
|
|
257
|
+
|
|
258
|
+
if type_qnames:
|
|
259
|
+
return f"<< (I, #FFFFFF) {', '.join(type_qnames)} >>"
|
|
260
|
+
|
|
261
|
+
# Fallback if no suitable types found
|
|
262
|
+
return " (I, #FFFFFF) <<owl:NamedIndividual>>"
|
|
263
|
+
|
|
264
|
+
def render_class(self, cls: URIRef) -> list[str]:
|
|
265
|
+
"""Render a RDF concept as PlantUML class.
|
|
266
|
+
|
|
267
|
+
Classes render without attributes (datatype properties become
|
|
268
|
+
dotted-line relationships instead).
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
cls: Class URI to render
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
List of PlantUML lines for this class
|
|
275
|
+
"""
|
|
276
|
+
lines = []
|
|
277
|
+
|
|
278
|
+
class_name = plantuml_identifier(self.graph, cls)
|
|
279
|
+
stereotype = self._get_class_stereotype(cls)
|
|
280
|
+
|
|
281
|
+
# Get colour styling if configured
|
|
282
|
+
color_spec = ""
|
|
283
|
+
if self.style:
|
|
284
|
+
# Use style system if available
|
|
285
|
+
palette = self.style.get_class_style(self.graph, cls, is_instance=False)
|
|
286
|
+
if palette and hasattr(palette, 'to_plantuml'):
|
|
287
|
+
color_spec = f" {palette.to_plantuml()}"
|
|
288
|
+
|
|
289
|
+
# Render as empty class with stereotype
|
|
290
|
+
lines.append(f"class {class_name} {stereotype}{color_spec}")
|
|
291
|
+
|
|
292
|
+
return lines
|
|
293
|
+
|
|
294
|
+
def render_property(self, prop: URIRef) -> list[str]:
|
|
295
|
+
"""Render a property as a class.
|
|
296
|
+
|
|
297
|
+
Properties render as gray classes with appropriate stereotypes.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
prop: Property URI to render
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
List of PlantUML lines for this property
|
|
304
|
+
"""
|
|
305
|
+
lines = []
|
|
306
|
+
|
|
307
|
+
prop_name = plantuml_identifier(self.graph, prop)
|
|
308
|
+
stereotype = self._get_property_stereotype(prop)
|
|
309
|
+
|
|
310
|
+
# Properties are typically gray
|
|
311
|
+
# Use style system if available, otherwise default gray
|
|
312
|
+
color_spec = " #CCCCCC"
|
|
313
|
+
if self.style:
|
|
314
|
+
palette = self.style.get_property_style(self.graph, prop)
|
|
315
|
+
if palette and hasattr(palette, 'to_plantuml'):
|
|
316
|
+
color_spec = f" {palette.to_plantuml()}"
|
|
317
|
+
|
|
318
|
+
lines.append(f"class {prop_name} {stereotype}{color_spec}")
|
|
319
|
+
|
|
320
|
+
return lines
|
|
321
|
+
|
|
322
|
+
def render_instance(self, instance: URIRef) -> list[str]:
|
|
323
|
+
"""Render an instance as a class with type stereotype.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
instance: Instance URI to render
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of PlantUML lines for this instance
|
|
330
|
+
"""
|
|
331
|
+
lines = []
|
|
332
|
+
|
|
333
|
+
instance_name = plantuml_identifier(self.graph, instance)
|
|
334
|
+
instance_label = safe_label(self.graph, instance, camelcase=False)
|
|
335
|
+
stereotype = self._get_instance_stereotype(instance)
|
|
336
|
+
|
|
337
|
+
# Get colour styling for instances
|
|
338
|
+
color_spec = ""
|
|
339
|
+
if self.style:
|
|
340
|
+
palette = self.style.get_class_style(self.graph, instance, is_instance=True)
|
|
341
|
+
if palette and hasattr(palette, 'to_plantuml'):
|
|
342
|
+
color_spec = f" {palette.to_plantuml()}"
|
|
343
|
+
|
|
344
|
+
# Render as class with stereotype and optional custom label
|
|
345
|
+
if instance_label != qname(self.graph, instance):
|
|
346
|
+
lines.append(f'class "{instance_label}" as {instance_name} {stereotype}{color_spec}')
|
|
347
|
+
else:
|
|
348
|
+
lines.append(f"class {instance_name} {stereotype}{color_spec}")
|
|
349
|
+
|
|
350
|
+
return lines
|
|
351
|
+
|
|
352
|
+
def render_subclass_relationships(self) -> list[str]:
|
|
353
|
+
"""Render rdfs:subClassOf relationships with labels.
|
|
354
|
+
|
|
355
|
+
Uses layout-configured arrow direction if available.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
List of PlantUML relationship lines
|
|
359
|
+
"""
|
|
360
|
+
lines = []
|
|
361
|
+
direction = self._get_arrow_direction()
|
|
362
|
+
|
|
363
|
+
for cls in self.entities.get("classes", set()):
|
|
364
|
+
for parent in self.graph.objects(cls, RDFS.subClassOf):
|
|
365
|
+
if parent in self.entities.get("classes", set()):
|
|
366
|
+
child_name = plantuml_identifier(self.graph, cls)
|
|
367
|
+
parent_name = plantuml_identifier(self.graph, parent)
|
|
368
|
+
|
|
369
|
+
lines.append(
|
|
370
|
+
f"{child_name} -{direction}-|> {parent_name} : <<rdfs:subClassOf>>"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return lines
|
|
374
|
+
|
|
375
|
+
def render_subproperty_relationships(self) -> list[str]:
|
|
376
|
+
"""Render rdfs:subPropertyOf relationships with labels.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of PlantUML relationship lines
|
|
380
|
+
"""
|
|
381
|
+
lines = []
|
|
382
|
+
direction = self._get_arrow_direction()
|
|
383
|
+
|
|
384
|
+
# Collect all properties
|
|
385
|
+
all_props = (
|
|
386
|
+
self.entities.get("object_properties", set()) |
|
|
387
|
+
self.entities.get("datatype_properties", set()) |
|
|
388
|
+
self.entities.get("annotation_properties", set())
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
for prop in all_props:
|
|
392
|
+
for parent_prop in self.graph.objects(prop, RDFS.subPropertyOf):
|
|
393
|
+
if parent_prop in all_props:
|
|
394
|
+
child_name = plantuml_identifier(self.graph, prop)
|
|
395
|
+
parent_name = plantuml_identifier(self.graph, parent_prop)
|
|
396
|
+
|
|
397
|
+
lines.append(
|
|
398
|
+
f"{child_name} -{direction}-|> {parent_name} : <<rdfs:subPropertyOf>>"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return lines
|
|
402
|
+
|
|
403
|
+
def render_type_relationships(self) -> list[str]:
|
|
404
|
+
"""Render rdf:type relationships as red arrows.
|
|
405
|
+
|
|
406
|
+
Red arrows from instances to their type classes.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
List of PlantUML relationship lines
|
|
410
|
+
"""
|
|
411
|
+
lines = []
|
|
412
|
+
direction = self._get_arrow_direction()
|
|
413
|
+
|
|
414
|
+
for instance in self.entities.get("instances", set()):
|
|
415
|
+
instance_name = plantuml_identifier(self.graph, instance)
|
|
416
|
+
|
|
417
|
+
for cls in self.graph.objects(instance, RDF.type):
|
|
418
|
+
# Skip metaclass types
|
|
419
|
+
type_qn = qname(self.graph, cls)
|
|
420
|
+
if type_qn in ("owl:Class", "rdfs:Class", "owl:ObjectProperty",
|
|
421
|
+
"owl:DatatypeProperty", "owl:AnnotationProperty"):
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
if cls in self.entities.get("classes", set()):
|
|
425
|
+
class_name = plantuml_identifier(self.graph, cls)
|
|
426
|
+
|
|
427
|
+
# Get arrow color from style
|
|
428
|
+
arrow_color = "#FF0000" # Default red
|
|
429
|
+
if self.style and hasattr(self.style, 'arrow_colors'):
|
|
430
|
+
arrow_color = self.style.arrow_colors.get_color("type")
|
|
431
|
+
|
|
432
|
+
lines.append(f"{instance_name} -{direction}[{arrow_color}]-> {class_name} : <<rdf:type>>")
|
|
433
|
+
|
|
434
|
+
return lines
|
|
435
|
+
|
|
436
|
+
def render_property_domain_range(self) -> list[str]:
|
|
437
|
+
"""Render domain and range relationships from property classes.
|
|
438
|
+
|
|
439
|
+
Black arrows from property to domain/range classes.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
List of PlantUML relationship lines
|
|
443
|
+
"""
|
|
444
|
+
lines = []
|
|
445
|
+
direction = self._get_arrow_direction()
|
|
446
|
+
|
|
447
|
+
# Collect all properties
|
|
448
|
+
all_props = (
|
|
449
|
+
self.entities.get("object_properties", set()) |
|
|
450
|
+
self.entities.get("datatype_properties", set()) |
|
|
451
|
+
self.entities.get("annotation_properties", set())
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
for prop in all_props:
|
|
455
|
+
prop_name = plantuml_identifier(self.graph, prop)
|
|
456
|
+
|
|
457
|
+
# Render domain relationships
|
|
458
|
+
for domain in self.graph.objects(prop, RDFS.domain):
|
|
459
|
+
if domain in self.entities.get("classes", set()):
|
|
460
|
+
domain_name = plantuml_identifier(self.graph, domain)
|
|
461
|
+
lines.append(
|
|
462
|
+
f"{prop_name} -{direction}-> {domain_name} : <<rdfs:domain>>"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Render range relationships
|
|
466
|
+
for range_cls in self.graph.objects(prop, RDFS.range):
|
|
467
|
+
# Check if range is a class (for object properties)
|
|
468
|
+
if range_cls in self.entities.get("classes", set()):
|
|
469
|
+
range_name = plantuml_identifier(self.graph, range_cls)
|
|
470
|
+
lines.append(
|
|
471
|
+
f"{prop_name} -{direction}-> {range_name} : <<rdfs:range>>"
|
|
472
|
+
)
|
|
473
|
+
# For datatype properties, range might be XSD type - skip those
|
|
474
|
+
|
|
475
|
+
return lines
|
|
476
|
+
|
|
477
|
+
def render_instance_object_properties(self) -> list[str]:
|
|
478
|
+
"""Render object property relationships between instances.
|
|
479
|
+
|
|
480
|
+
Black arrows labeled with property name (camelCase).
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
List of PlantUML relationship lines
|
|
484
|
+
"""
|
|
485
|
+
lines = []
|
|
486
|
+
direction = self._get_arrow_direction()
|
|
487
|
+
|
|
488
|
+
obj_props = self.entities.get("object_properties", set())
|
|
489
|
+
instances = self.entities.get("instances", set())
|
|
490
|
+
|
|
491
|
+
for subj in instances:
|
|
492
|
+
for prop in obj_props:
|
|
493
|
+
for obj in self.graph.objects(subj, prop):
|
|
494
|
+
if obj in instances:
|
|
495
|
+
subj_name = plantuml_identifier(self.graph, subj)
|
|
496
|
+
obj_name = plantuml_identifier(self.graph, obj)
|
|
497
|
+
prop_qname = qname(self.graph, prop)
|
|
498
|
+
|
|
499
|
+
# Ensure property name is camelCase
|
|
500
|
+
prop_label = safe_label(self.graph, prop, camelcase=True)
|
|
501
|
+
|
|
502
|
+
lines.append(
|
|
503
|
+
f"{subj_name} -{direction}-> {obj_name} : <<{prop_qname}>>"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return lines
|
|
507
|
+
|
|
508
|
+
def render_instance_datatype_properties(self) -> list[str]:
|
|
509
|
+
"""Render datatype property values as notes with dotted connections.
|
|
510
|
+
|
|
511
|
+
Creates a note for each literal value and connects with dotted line.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
List of PlantUML lines for notes and connections
|
|
515
|
+
"""
|
|
516
|
+
lines = []
|
|
517
|
+
direction = self._get_arrow_direction()
|
|
518
|
+
|
|
519
|
+
datatype_props = self.entities.get("datatype_properties", set())
|
|
520
|
+
instances = self.entities.get("instances", set())
|
|
521
|
+
|
|
522
|
+
for instance in instances:
|
|
523
|
+
instance_name = plantuml_identifier(self.graph, instance)
|
|
524
|
+
|
|
525
|
+
for prop in datatype_props:
|
|
526
|
+
for value in self.graph.objects(instance, prop):
|
|
527
|
+
if isinstance(value, Literal):
|
|
528
|
+
# Create unique note ID
|
|
529
|
+
self._note_counter += 1
|
|
530
|
+
note_id = f"N{self._note_counter}"
|
|
531
|
+
|
|
532
|
+
# Escape literal value for PlantUML
|
|
533
|
+
value_str = escape_plantuml(str(value))
|
|
534
|
+
|
|
535
|
+
# Create note
|
|
536
|
+
lines.append(f'note "{value_str}" as {note_id}')
|
|
537
|
+
|
|
538
|
+
# Connect with dotted line
|
|
539
|
+
prop_qname = qname(self.graph, prop)
|
|
540
|
+
lines.append(
|
|
541
|
+
f"{instance_name} .{direction}. {note_id} : <<{prop_qname}>>"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
return lines
|
|
545
|
+
|
|
546
|
+
def render_class_datatype_properties(self) -> list[str]:
|
|
547
|
+
"""Render datatype properties on classes as dotted lines to notes.
|
|
548
|
+
|
|
549
|
+
For classes with domain restrictions, show the range as a note.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
List of PlantUML lines for notes and connections
|
|
553
|
+
"""
|
|
554
|
+
lines = []
|
|
555
|
+
direction = self._get_arrow_direction()
|
|
556
|
+
|
|
557
|
+
datatype_props = self.entities.get("datatype_properties", set())
|
|
558
|
+
classes = self.entities.get("classes", set())
|
|
559
|
+
|
|
560
|
+
for cls in classes:
|
|
561
|
+
cls_name = plantuml_identifier(self.graph, cls)
|
|
562
|
+
|
|
563
|
+
for prop in datatype_props:
|
|
564
|
+
# Check if this property has this class as domain
|
|
565
|
+
domains = list(self.graph.objects(prop, RDFS.domain))
|
|
566
|
+
if cls not in domains:
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
# Get range to show type
|
|
570
|
+
ranges = list(self.graph.objects(prop, RDFS.range))
|
|
571
|
+
if ranges:
|
|
572
|
+
# Create note showing the property and its type
|
|
573
|
+
self._note_counter += 1
|
|
574
|
+
note_id = f"N{self._note_counter}"
|
|
575
|
+
|
|
576
|
+
range_type = qname(self.graph, ranges[0])
|
|
577
|
+
# Simplify XSD types
|
|
578
|
+
if range_type.startswith("xsd:"):
|
|
579
|
+
range_type = range_type[4:]
|
|
580
|
+
|
|
581
|
+
prop_label = safe_label(self.graph, prop, camelcase=True)
|
|
582
|
+
|
|
583
|
+
lines.append(f'note "{prop_label}: {range_type}" as {note_id}')
|
|
584
|
+
|
|
585
|
+
prop_qname = qname(self.graph, prop)
|
|
586
|
+
lines.append(
|
|
587
|
+
f"{cls_name} .{direction}. {note_id} : <<{prop_qname}>>"
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
return lines
|
|
591
|
+
|
|
592
|
+
def render(self) -> str:
|
|
593
|
+
"""Render complete PlantUML diagram.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Complete PlantUML diagram as string
|
|
597
|
+
"""
|
|
598
|
+
lines = ["@startuml", ""]
|
|
599
|
+
|
|
600
|
+
# Add layout directives
|
|
601
|
+
if self.layout:
|
|
602
|
+
directives = getattr(self.layout, 'get_plantuml_directives', lambda: [])()
|
|
603
|
+
lines.extend(directives)
|
|
604
|
+
lines.append("")
|
|
605
|
+
|
|
606
|
+
# Add style directives
|
|
607
|
+
if self.style:
|
|
608
|
+
directives = getattr(self.style, 'get_plantuml_directives', lambda: [])()
|
|
609
|
+
if directives:
|
|
610
|
+
lines.extend(directives)
|
|
611
|
+
lines.append("")
|
|
612
|
+
|
|
613
|
+
# Render all classes
|
|
614
|
+
for cls in sorted(self.entities.get("classes", set()), key=lambda x: qname(self.graph, x)):
|
|
615
|
+
lines.extend(self.render_class(cls))
|
|
616
|
+
|
|
617
|
+
if self.entities.get("classes"):
|
|
618
|
+
lines.append("")
|
|
619
|
+
|
|
620
|
+
# Render all properties as classes
|
|
621
|
+
all_props = (
|
|
622
|
+
self.entities.get("object_properties", set()) |
|
|
623
|
+
self.entities.get("datatype_properties", set()) |
|
|
624
|
+
self.entities.get("annotation_properties", set())
|
|
625
|
+
)
|
|
626
|
+
for prop in sorted(all_props, key=lambda x: qname(self.graph, x)):
|
|
627
|
+
lines.extend(self.render_property(prop))
|
|
628
|
+
|
|
629
|
+
if all_props:
|
|
630
|
+
lines.append("")
|
|
631
|
+
|
|
632
|
+
# Render all instances as classes
|
|
633
|
+
for instance in sorted(self.entities.get("instances", set()), key=lambda x: qname(self.graph, x)):
|
|
634
|
+
lines.extend(self.render_instance(instance))
|
|
635
|
+
|
|
636
|
+
if self.entities.get("instances"):
|
|
637
|
+
lines.append("")
|
|
638
|
+
|
|
639
|
+
# Render relationships
|
|
640
|
+
lines.extend(self.render_subclass_relationships())
|
|
641
|
+
lines.extend(self.render_subproperty_relationships())
|
|
642
|
+
lines.extend(self.render_type_relationships())
|
|
643
|
+
lines.extend(self.render_property_domain_range())
|
|
644
|
+
lines.extend(self.render_instance_object_properties())
|
|
645
|
+
|
|
646
|
+
if any([self.entities.get("classes"), all_props, self.entities.get("instances")]):
|
|
647
|
+
lines.append("")
|
|
648
|
+
|
|
649
|
+
# Render datatype properties as notes
|
|
650
|
+
lines.extend(self.render_instance_datatype_properties())
|
|
651
|
+
lines.extend(self.render_class_datatype_properties())
|
|
652
|
+
|
|
653
|
+
lines.append("")
|
|
654
|
+
lines.append("@enduml")
|
|
655
|
+
|
|
656
|
+
return "\n".join(lines)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def render_plantuml(
|
|
660
|
+
graph: Graph,
|
|
661
|
+
entities: dict[str, set[URIRef]],
|
|
662
|
+
output_path: Optional[Path] = None,
|
|
663
|
+
style: Optional = None,
|
|
664
|
+
layout: Optional = None,
|
|
665
|
+
) -> str:
|
|
666
|
+
"""Render entities from RDF graph as PlantUML class diagram.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
graph: RDF graph containing entities
|
|
670
|
+
entities: Dictionary of entity sets to render
|
|
671
|
+
output_path: Optional path to write PlantUML file
|
|
672
|
+
style: Optional style scheme
|
|
673
|
+
layout: Optional layout configuration
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
PlantUML diagram text
|
|
677
|
+
"""
|
|
678
|
+
renderer = PlantUMLRenderer(graph, entities, style=style, layout=layout)
|
|
679
|
+
diagram = renderer.render()
|
|
680
|
+
|
|
681
|
+
if output_path:
|
|
682
|
+
output_path.write_text(diagram)
|
|
683
|
+
|
|
684
|
+
return diagram
|