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
rdf_construct/cli.py
ADDED
|
@@ -0,0 +1,1762 @@
|
|
|
1
|
+
"""Command-line interface for rdf-construct."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rdflib import Graph, RDF, URIRef
|
|
8
|
+
from rdflib.namespace import OWL
|
|
9
|
+
|
|
10
|
+
from rdf_construct.core import (
|
|
11
|
+
OrderingConfig,
|
|
12
|
+
build_section_graph,
|
|
13
|
+
extract_prefix_map,
|
|
14
|
+
rebind_prefixes,
|
|
15
|
+
select_subjects,
|
|
16
|
+
serialise_turtle,
|
|
17
|
+
sort_subjects,
|
|
18
|
+
expand_curie,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from rdf_construct.uml import (
|
|
22
|
+
load_uml_config,
|
|
23
|
+
collect_diagram_entities,
|
|
24
|
+
render_plantuml,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from rdf_construct.uml.uml_style import load_style_config
|
|
28
|
+
from rdf_construct.uml.uml_layout import load_layout_config
|
|
29
|
+
from rdf_construct.uml.odm_renderer import render_odm_plantuml
|
|
30
|
+
|
|
31
|
+
from rdf_construct.lint import (
|
|
32
|
+
LintEngine,
|
|
33
|
+
LintConfig,
|
|
34
|
+
load_lint_config,
|
|
35
|
+
find_config_file,
|
|
36
|
+
get_formatter,
|
|
37
|
+
list_rules,
|
|
38
|
+
get_all_rules,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
LINT_LEVELS = ["strict", "standard", "relaxed"]
|
|
42
|
+
LINT_FORMATS = ["text", "json"]
|
|
43
|
+
|
|
44
|
+
from rdf_construct.diff import compare_files, format_diff, filter_diff, parse_filter_string
|
|
45
|
+
|
|
46
|
+
from rdf_construct.puml2rdf import (
|
|
47
|
+
ConversionConfig,
|
|
48
|
+
PlantUMLParser,
|
|
49
|
+
PumlToRdfConverter,
|
|
50
|
+
load_import_config,
|
|
51
|
+
merge_with_existing,
|
|
52
|
+
validate_puml,
|
|
53
|
+
validate_rdf,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
from rdf_construct.cq import load_test_suite, CQTestRunner, format_results
|
|
57
|
+
|
|
58
|
+
from rdf_construct.stats import (
|
|
59
|
+
collect_stats,
|
|
60
|
+
compare_stats,
|
|
61
|
+
format_stats,
|
|
62
|
+
format_comparison,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Valid rendering modes
|
|
66
|
+
RENDERING_MODES = ["default", "odm"]
|
|
67
|
+
|
|
68
|
+
@click.group()
|
|
69
|
+
@click.version_option()
|
|
70
|
+
def cli():
|
|
71
|
+
"""rdf-construct: Semantic RDF manipulation toolkit.
|
|
72
|
+
|
|
73
|
+
Tools for working with RDF ontologies:
|
|
74
|
+
|
|
75
|
+
\b
|
|
76
|
+
- lint: Check ontology quality (structural issues, documentation, best practices)
|
|
77
|
+
- uml: Generate PlantUML class diagrams
|
|
78
|
+
- order: Reorder Turtle files with semantic awareness
|
|
79
|
+
|
|
80
|
+
Use COMMAND --help for detailed options.
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@cli.command()
|
|
86
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
87
|
+
@click.argument("config", type=click.Path(exists=True, path_type=Path))
|
|
88
|
+
@click.option(
|
|
89
|
+
"--profile",
|
|
90
|
+
"-p",
|
|
91
|
+
multiple=True,
|
|
92
|
+
help="Profile(s) to generate (default: all profiles in config)",
|
|
93
|
+
)
|
|
94
|
+
@click.option(
|
|
95
|
+
"--outdir",
|
|
96
|
+
"-o",
|
|
97
|
+
type=click.Path(path_type=Path),
|
|
98
|
+
default="src/ontology",
|
|
99
|
+
help="Output directory (default: src/ontology)",
|
|
100
|
+
)
|
|
101
|
+
def order(source: Path, config: Path, profile: tuple[str, ...], outdir: Path):
|
|
102
|
+
"""Reorder RDF Turtle files according to semantic profiles.
|
|
103
|
+
|
|
104
|
+
SOURCE: Input RDF Turtle file (.ttl)
|
|
105
|
+
CONFIG: YAML configuration file defining ordering profiles
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
|
|
109
|
+
# Generate all profiles defined in config
|
|
110
|
+
rdf-construct order ontology.ttl order.yml
|
|
111
|
+
|
|
112
|
+
# Generate only specific profiles
|
|
113
|
+
rdf-construct order ontology.ttl order.yml -p alpha -p logical_topo
|
|
114
|
+
|
|
115
|
+
# Custom output directory
|
|
116
|
+
rdf-construct order ontology.ttl order.yml -o output/
|
|
117
|
+
"""
|
|
118
|
+
# Load configuration
|
|
119
|
+
ordering_config = OrderingConfig(config)
|
|
120
|
+
|
|
121
|
+
# Determine which profiles to generate
|
|
122
|
+
if profile:
|
|
123
|
+
profiles_to_gen = list(profile)
|
|
124
|
+
else:
|
|
125
|
+
profiles_to_gen = ordering_config.list_profiles()
|
|
126
|
+
|
|
127
|
+
# Validate requested profiles exist
|
|
128
|
+
for prof_name in profiles_to_gen:
|
|
129
|
+
if prof_name not in ordering_config.profiles:
|
|
130
|
+
click.secho(
|
|
131
|
+
f"Error: Profile '{prof_name}' not found in config.", fg="red", err=True
|
|
132
|
+
)
|
|
133
|
+
available = ", ".join(ordering_config.list_profiles())
|
|
134
|
+
click.echo(f"Available profiles: {available}", err=True)
|
|
135
|
+
raise click.Abort()
|
|
136
|
+
|
|
137
|
+
# Create output directory
|
|
138
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
# Parse source RDF
|
|
141
|
+
click.echo(f"Loading {source}...")
|
|
142
|
+
graph = Graph()
|
|
143
|
+
graph.parse(source.as_posix(), format="turtle")
|
|
144
|
+
prefix_map = extract_prefix_map(graph)
|
|
145
|
+
|
|
146
|
+
# Generate each profile
|
|
147
|
+
for prof_name in profiles_to_gen:
|
|
148
|
+
click.echo(f"Constructing profile: {prof_name}")
|
|
149
|
+
prof = ordering_config.get_profile(prof_name)
|
|
150
|
+
|
|
151
|
+
ordered_subjects: list = []
|
|
152
|
+
seen: set = set()
|
|
153
|
+
|
|
154
|
+
# Process each section
|
|
155
|
+
for sec in prof.sections:
|
|
156
|
+
if not isinstance(sec, dict) or not sec:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
sec_name, sec_cfg = next(iter(sec.items()))
|
|
160
|
+
|
|
161
|
+
# Handle header section - ontology metadata
|
|
162
|
+
if sec_name == "header":
|
|
163
|
+
ontology_subjects = [
|
|
164
|
+
s for s in graph.subjects(RDF.type, OWL.Ontology) if s not in seen
|
|
165
|
+
]
|
|
166
|
+
for s in ontology_subjects:
|
|
167
|
+
ordered_subjects.append(s)
|
|
168
|
+
seen.add(s)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Regular sections
|
|
172
|
+
sec_cfg = sec_cfg or {}
|
|
173
|
+
select_key = sec_cfg.get("select", sec_name)
|
|
174
|
+
sort_mode = sec_cfg.get("sort", "qname_alpha")
|
|
175
|
+
roots_cfg = sec_cfg.get("roots")
|
|
176
|
+
|
|
177
|
+
# Select and sort subjects
|
|
178
|
+
chosen = select_subjects(graph, select_key, ordering_config.selectors)
|
|
179
|
+
chosen = [s for s in chosen if s not in seen]
|
|
180
|
+
|
|
181
|
+
ordered = sort_subjects(graph, set(chosen), sort_mode, roots_cfg)
|
|
182
|
+
|
|
183
|
+
for s in ordered:
|
|
184
|
+
if s not in seen:
|
|
185
|
+
ordered_subjects.append(s)
|
|
186
|
+
seen.add(s)
|
|
187
|
+
|
|
188
|
+
# Build output graph
|
|
189
|
+
out_graph = build_section_graph(graph, ordered_subjects)
|
|
190
|
+
|
|
191
|
+
# Rebind prefixes if configured
|
|
192
|
+
if ordering_config.defaults.get("preserve_prefix_order", True):
|
|
193
|
+
if ordering_config.prefix_order:
|
|
194
|
+
rebind_prefixes(out_graph, ordering_config.prefix_order, prefix_map)
|
|
195
|
+
|
|
196
|
+
# Get predicate ordering for this profile
|
|
197
|
+
predicate_order = ordering_config.get_predicate_order(prof_name)
|
|
198
|
+
|
|
199
|
+
# Serialise with predicate ordering
|
|
200
|
+
out_file = outdir / f"{source.stem}-{prof_name}.ttl"
|
|
201
|
+
serialise_turtle(out_graph, ordered_subjects, out_file, predicate_order)
|
|
202
|
+
click.secho(f" ✓ {out_file}", fg="green")
|
|
203
|
+
|
|
204
|
+
click.secho(
|
|
205
|
+
f"\nConstructed {len(profiles_to_gen)} profile(s) in {outdir}/", fg="cyan"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@cli.command()
|
|
210
|
+
@click.argument("config", type=click.Path(exists=True, path_type=Path))
|
|
211
|
+
def profiles(config: Path):
|
|
212
|
+
"""List available profiles in a configuration file.
|
|
213
|
+
|
|
214
|
+
CONFIG: YAML configuration file to inspect
|
|
215
|
+
"""
|
|
216
|
+
ordering_config = OrderingConfig(config)
|
|
217
|
+
|
|
218
|
+
click.secho("Available profiles:", fg="cyan", bold=True)
|
|
219
|
+
click.echo()
|
|
220
|
+
|
|
221
|
+
for prof_name in ordering_config.list_profiles():
|
|
222
|
+
prof = ordering_config.get_profile(prof_name)
|
|
223
|
+
click.secho(f" {prof_name}", fg="green", bold=True)
|
|
224
|
+
if prof.description:
|
|
225
|
+
click.echo(f" {prof.description}")
|
|
226
|
+
click.echo(f" Sections: {len(prof.sections)}")
|
|
227
|
+
click.echo()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@cli.command()
|
|
231
|
+
@click.argument("sources", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path))
|
|
232
|
+
@click.option(
|
|
233
|
+
"--config",
|
|
234
|
+
"-C",
|
|
235
|
+
required=True,
|
|
236
|
+
type=click.Path(exists=True, path_type=Path),
|
|
237
|
+
help="YAML configuration file defining UML contexts",
|
|
238
|
+
)
|
|
239
|
+
@click.option(
|
|
240
|
+
"--context",
|
|
241
|
+
"-c",
|
|
242
|
+
multiple=True,
|
|
243
|
+
help="Context(s) to generate (default: all contexts in config)",
|
|
244
|
+
)
|
|
245
|
+
@click.option(
|
|
246
|
+
"--outdir",
|
|
247
|
+
"-o",
|
|
248
|
+
type=click.Path(path_type=Path),
|
|
249
|
+
default="diagrams",
|
|
250
|
+
help="Output directory (default: diagrams)",
|
|
251
|
+
)
|
|
252
|
+
@click.option(
|
|
253
|
+
"--style-config",
|
|
254
|
+
type=click.Path(exists=True, path_type=Path),
|
|
255
|
+
help="Path to style configuration YAML (e.g., examples/uml_styles.yml)"
|
|
256
|
+
)
|
|
257
|
+
@click.option(
|
|
258
|
+
"--style", "-s",
|
|
259
|
+
help="Style scheme name to use (e.g., 'default', 'ies_semantic')"
|
|
260
|
+
)
|
|
261
|
+
@click.option(
|
|
262
|
+
"--layout-config",
|
|
263
|
+
type=click.Path(exists=True, path_type=Path),
|
|
264
|
+
help="Path to layout configuration YAML (e.g., examples/uml_layouts.yml)"
|
|
265
|
+
)
|
|
266
|
+
@click.option(
|
|
267
|
+
"--layout", "-l",
|
|
268
|
+
help="Layout name to use (e.g., 'hierarchy', 'compact')"
|
|
269
|
+
)
|
|
270
|
+
@click.option(
|
|
271
|
+
"--rendering-mode", "-r",
|
|
272
|
+
type=click.Choice(RENDERING_MODES, case_sensitive=False),
|
|
273
|
+
default="default",
|
|
274
|
+
help="Rendering mode: 'default' (custom stereotypes) or 'odm' (OMG ODM RDF Profile compliant)"
|
|
275
|
+
)
|
|
276
|
+
def uml(sources, config, context, outdir, style_config, style, layout_config, layout, rendering_mode):
|
|
277
|
+
"""Generate UML class diagrams from RDF ontologies.
|
|
278
|
+
|
|
279
|
+
SOURCES: One or more RDF Turtle files (.ttl). The first file is the primary
|
|
280
|
+
source; additional files provide supporting definitions (e.g., imported
|
|
281
|
+
ontologies for complete class hierarchies).
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
|
|
285
|
+
# Basic usage - single source
|
|
286
|
+
rdf-construct uml ontology.ttl -C contexts.yml
|
|
287
|
+
|
|
288
|
+
# Multiple sources - primary + supporting ontology
|
|
289
|
+
rdf-construct uml building.ttl ies4.ttl -C contexts.yml
|
|
290
|
+
|
|
291
|
+
# Multiple sources with styling (hierarchy inheritance works!)
|
|
292
|
+
rdf-construct uml building.ttl ies4.ttl -C contexts.yml \\
|
|
293
|
+
--style-config ies_colours.yml --style ies_full
|
|
294
|
+
|
|
295
|
+
# Generate specific context with ODM mode
|
|
296
|
+
rdf-construct uml building.ttl ies4.ttl -C contexts.yml -c core -r odm
|
|
297
|
+
"""
|
|
298
|
+
# Load style if provided
|
|
299
|
+
style_scheme = None
|
|
300
|
+
if style_config and style:
|
|
301
|
+
style_cfg = load_style_config(style_config)
|
|
302
|
+
try:
|
|
303
|
+
style_scheme = style_cfg.get_scheme(style)
|
|
304
|
+
click.echo(f"Using style: {style}")
|
|
305
|
+
except KeyError as e:
|
|
306
|
+
click.secho(str(e), fg="red", err=True)
|
|
307
|
+
click.echo(f"Available styles: {', '.join(style_cfg.list_schemes())}")
|
|
308
|
+
raise click.Abort()
|
|
309
|
+
|
|
310
|
+
# Load layout if provided
|
|
311
|
+
layout_cfg = None
|
|
312
|
+
if layout_config and layout:
|
|
313
|
+
layout_mgr = load_layout_config(layout_config)
|
|
314
|
+
try:
|
|
315
|
+
layout_cfg = layout_mgr.get_layout(layout)
|
|
316
|
+
click.echo(f"Using layout: {layout}")
|
|
317
|
+
except KeyError as e:
|
|
318
|
+
click.secho(str(e), fg="red", err=True)
|
|
319
|
+
click.echo(f"Available layouts: {', '.join(layout_mgr.list_layouts())}")
|
|
320
|
+
raise click.Abort()
|
|
321
|
+
|
|
322
|
+
# Display rendering mode
|
|
323
|
+
if rendering_mode == "odm":
|
|
324
|
+
click.echo("Using rendering mode: ODM RDF Profile (OMG compliant)")
|
|
325
|
+
else:
|
|
326
|
+
click.echo("Using rendering mode: default")
|
|
327
|
+
|
|
328
|
+
# Load UML configuration
|
|
329
|
+
uml_config = load_uml_config(config)
|
|
330
|
+
|
|
331
|
+
# Determine which contexts to generate
|
|
332
|
+
if context:
|
|
333
|
+
contexts_to_gen = list(context)
|
|
334
|
+
else:
|
|
335
|
+
contexts_to_gen = uml_config.list_contexts()
|
|
336
|
+
|
|
337
|
+
# Validate requested contexts exist
|
|
338
|
+
for ctx_name in contexts_to_gen:
|
|
339
|
+
if ctx_name not in uml_config.contexts:
|
|
340
|
+
click.secho(
|
|
341
|
+
f"Error: Context '{ctx_name}' not found in config.", fg="red", err=True
|
|
342
|
+
)
|
|
343
|
+
available = ", ".join(uml_config.list_contexts())
|
|
344
|
+
click.echo(f"Available contexts: {available}", err=True)
|
|
345
|
+
raise click.Abort()
|
|
346
|
+
|
|
347
|
+
# Create output directory
|
|
348
|
+
# ToDo - handle exceptions properly
|
|
349
|
+
outdir = Path(outdir)
|
|
350
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
|
|
352
|
+
# Parse source RDF files into a single graph
|
|
353
|
+
# The first source is considered the "primary" (used for output naming)
|
|
354
|
+
primary_source = sources[0]
|
|
355
|
+
graph = Graph()
|
|
356
|
+
|
|
357
|
+
for source in sources:
|
|
358
|
+
click.echo(f"Loading {source}...")
|
|
359
|
+
# Guess format from extension
|
|
360
|
+
suffix = source.suffix.lower()
|
|
361
|
+
if suffix in (".ttl", ".turtle"):
|
|
362
|
+
fmt = "turtle"
|
|
363
|
+
elif suffix in (".rdf", ".xml", ".owl"):
|
|
364
|
+
fmt = "xml"
|
|
365
|
+
elif suffix in (".nt", ".ntriples"):
|
|
366
|
+
fmt = "nt"
|
|
367
|
+
elif suffix in (".n3",):
|
|
368
|
+
fmt = "n3"
|
|
369
|
+
elif suffix in (".jsonld", ".json"):
|
|
370
|
+
fmt = "json-ld"
|
|
371
|
+
else:
|
|
372
|
+
fmt = "turtle" # Default to turtle
|
|
373
|
+
|
|
374
|
+
graph.parse(source.as_posix(), format=fmt)
|
|
375
|
+
|
|
376
|
+
if len(sources) > 1:
|
|
377
|
+
click.echo(f" Merged {len(sources)} source files ({len(graph)} triples total)")
|
|
378
|
+
|
|
379
|
+
# Get selectors from defaults (if any)
|
|
380
|
+
selectors = uml_config.defaults.get("selectors", {})
|
|
381
|
+
|
|
382
|
+
# Generate each context
|
|
383
|
+
for ctx_name in contexts_to_gen:
|
|
384
|
+
click.echo(f"Generating diagram: {ctx_name}")
|
|
385
|
+
ctx = uml_config.get_context(ctx_name)
|
|
386
|
+
|
|
387
|
+
# Select entities
|
|
388
|
+
entities = collect_diagram_entities(graph, ctx, selectors)
|
|
389
|
+
|
|
390
|
+
# Build output filename (include mode suffix for ODM)
|
|
391
|
+
if rendering_mode == "odm":
|
|
392
|
+
out_file = outdir / f"{primary_source.stem}-{ctx_name}-odm.puml"
|
|
393
|
+
else:
|
|
394
|
+
out_file = outdir / f"{primary_source.stem}-{ctx_name}.puml"
|
|
395
|
+
|
|
396
|
+
# Render with optional style and layout
|
|
397
|
+
if rendering_mode == "odm":
|
|
398
|
+
render_odm_plantuml(graph, entities, out_file, style_scheme, layout_cfg)
|
|
399
|
+
else:
|
|
400
|
+
render_plantuml(graph, entities, out_file, style_scheme, layout_cfg)
|
|
401
|
+
|
|
402
|
+
click.secho(f" ✓ {out_file}", fg="green")
|
|
403
|
+
click.echo(
|
|
404
|
+
f" Classes: {len(entities['classes'])}, "
|
|
405
|
+
f"Properties: {len(entities['object_properties']) + len(entities['datatype_properties'])}, "
|
|
406
|
+
f"Instances: {len(entities['instances'])}"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
click.secho(
|
|
410
|
+
f"\nGenerated {len(contexts_to_gen)} diagram(s) in {outdir}/", fg="cyan"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@cli.command()
|
|
415
|
+
@click.argument("config", type=click.Path(exists=True, path_type=Path))
|
|
416
|
+
def contexts(config: Path):
|
|
417
|
+
"""List available UML contexts in a configuration file.
|
|
418
|
+
|
|
419
|
+
CONFIG: YAML configuration file to inspect
|
|
420
|
+
"""
|
|
421
|
+
uml_config = load_uml_config(config)
|
|
422
|
+
|
|
423
|
+
click.secho("Available UML contexts:", fg="cyan", bold=True)
|
|
424
|
+
click.echo()
|
|
425
|
+
|
|
426
|
+
for ctx_name in uml_config.list_contexts():
|
|
427
|
+
ctx = uml_config.get_context(ctx_name)
|
|
428
|
+
click.secho(f" {ctx_name}", fg="green", bold=True)
|
|
429
|
+
if ctx.description:
|
|
430
|
+
click.echo(f" {ctx.description}")
|
|
431
|
+
|
|
432
|
+
# Show selection strategy
|
|
433
|
+
if ctx.root_classes:
|
|
434
|
+
click.echo(f" Roots: {', '.join(ctx.root_classes)}")
|
|
435
|
+
elif ctx.focus_classes:
|
|
436
|
+
click.echo(f" Focus: {', '.join(ctx.focus_classes)}")
|
|
437
|
+
elif ctx.selector:
|
|
438
|
+
click.echo(f" Selector: {ctx.selector}")
|
|
439
|
+
|
|
440
|
+
if ctx.include_descendants:
|
|
441
|
+
depth_str = f"depth={ctx.max_depth}" if ctx.max_depth else "unlimited"
|
|
442
|
+
click.echo(f" Includes descendants ({depth_str})")
|
|
443
|
+
|
|
444
|
+
click.echo(f" Properties: {ctx.property_mode}")
|
|
445
|
+
click.echo()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@cli.command()
|
|
449
|
+
@click.argument("sources", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path))
|
|
450
|
+
@click.option(
|
|
451
|
+
"--level",
|
|
452
|
+
"-l",
|
|
453
|
+
type=click.Choice(["strict", "standard", "relaxed"], case_sensitive=False),
|
|
454
|
+
default="standard",
|
|
455
|
+
help="Strictness level (default: standard)",
|
|
456
|
+
)
|
|
457
|
+
@click.option(
|
|
458
|
+
"--format",
|
|
459
|
+
"-f",
|
|
460
|
+
"output_format", # Renamed to avoid shadowing builtin
|
|
461
|
+
type=click.Choice(["text", "json"], case_sensitive=False),
|
|
462
|
+
default="text",
|
|
463
|
+
help="Output format (default: text)",
|
|
464
|
+
)
|
|
465
|
+
@click.option(
|
|
466
|
+
"--config",
|
|
467
|
+
"-c",
|
|
468
|
+
type=click.Path(exists=True, path_type=Path),
|
|
469
|
+
help="Path to .rdf-lint.yml configuration file",
|
|
470
|
+
)
|
|
471
|
+
@click.option(
|
|
472
|
+
"--enable",
|
|
473
|
+
"-e",
|
|
474
|
+
multiple=True,
|
|
475
|
+
help="Enable specific rules (can be used multiple times)",
|
|
476
|
+
)
|
|
477
|
+
@click.option(
|
|
478
|
+
"--disable",
|
|
479
|
+
"-d",
|
|
480
|
+
multiple=True,
|
|
481
|
+
help="Disable specific rules (can be used multiple times)",
|
|
482
|
+
)
|
|
483
|
+
@click.option(
|
|
484
|
+
"--no-colour",
|
|
485
|
+
"--no-color",
|
|
486
|
+
is_flag=True,
|
|
487
|
+
help="Disable coloured output",
|
|
488
|
+
)
|
|
489
|
+
@click.option(
|
|
490
|
+
"--list-rules",
|
|
491
|
+
"list_rules_flag",
|
|
492
|
+
is_flag=True,
|
|
493
|
+
help="List available rules and exit",
|
|
494
|
+
)
|
|
495
|
+
@click.option(
|
|
496
|
+
"--init",
|
|
497
|
+
"init_config",
|
|
498
|
+
is_flag=True,
|
|
499
|
+
help="Generate a default .rdf-lint.yml config file and exit",
|
|
500
|
+
)
|
|
501
|
+
def lint(
|
|
502
|
+
sources: tuple[Path, ...],
|
|
503
|
+
level: str,
|
|
504
|
+
output_format: str,
|
|
505
|
+
config: Path | None,
|
|
506
|
+
enable: tuple[str, ...],
|
|
507
|
+
disable: tuple[str, ...],
|
|
508
|
+
no_colour: bool,
|
|
509
|
+
list_rules_flag: bool, # Must match the name above
|
|
510
|
+
init_config: bool,
|
|
511
|
+
):
|
|
512
|
+
"""Check RDF ontologies for quality issues.
|
|
513
|
+
|
|
514
|
+
Performs static analysis to detect structural problems, missing
|
|
515
|
+
documentation, and best practice violations.
|
|
516
|
+
|
|
517
|
+
\b
|
|
518
|
+
SOURCES: One or more RDF files to check (.ttl, .rdf, .owl, etc.)
|
|
519
|
+
|
|
520
|
+
\b
|
|
521
|
+
Exit codes:
|
|
522
|
+
0 - No issues found
|
|
523
|
+
1 - Warnings found (no errors)
|
|
524
|
+
2 - Errors found
|
|
525
|
+
|
|
526
|
+
\b
|
|
527
|
+
Examples:
|
|
528
|
+
# Basic usage
|
|
529
|
+
rdf-construct lint ontology.ttl
|
|
530
|
+
|
|
531
|
+
# Multiple files
|
|
532
|
+
rdf-construct lint core.ttl domain.ttl
|
|
533
|
+
|
|
534
|
+
# Strict mode (warnings become errors)
|
|
535
|
+
rdf-construct lint ontology.ttl --level strict
|
|
536
|
+
|
|
537
|
+
# JSON output for CI
|
|
538
|
+
rdf-construct lint ontology.ttl --format json
|
|
539
|
+
|
|
540
|
+
# Use config file
|
|
541
|
+
rdf-construct lint ontology.ttl --config .rdf-lint.yml
|
|
542
|
+
|
|
543
|
+
# Enable/disable specific rules
|
|
544
|
+
rdf-construct lint ontology.ttl --enable orphan-class --disable missing-comment
|
|
545
|
+
|
|
546
|
+
# List available rules
|
|
547
|
+
rdf-construct lint --list-rules
|
|
548
|
+
"""
|
|
549
|
+
# Handle --init flag
|
|
550
|
+
if init_config:
|
|
551
|
+
from .lint import create_default_config
|
|
552
|
+
|
|
553
|
+
config_path = Path(".rdf-lint.yml")
|
|
554
|
+
if config_path.exists():
|
|
555
|
+
click.secho(f"Config file already exists: {config_path}", fg="red", err=True)
|
|
556
|
+
raise click.Abort()
|
|
557
|
+
|
|
558
|
+
config_content = create_default_config()
|
|
559
|
+
config_path.write_text(config_content)
|
|
560
|
+
click.secho(f"Created {config_path}", fg="green")
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
# Handle --list-rules flag
|
|
564
|
+
if list_rules_flag:
|
|
565
|
+
from .lint import get_all_rules
|
|
566
|
+
|
|
567
|
+
rules = get_all_rules()
|
|
568
|
+
click.secho("Available lint rules:", fg="cyan", bold=True)
|
|
569
|
+
click.echo()
|
|
570
|
+
|
|
571
|
+
# Group by category
|
|
572
|
+
categories: dict[str, list] = {}
|
|
573
|
+
for rule_id, spec in sorted(rules.items()):
|
|
574
|
+
cat = spec.category
|
|
575
|
+
if cat not in categories:
|
|
576
|
+
categories[cat] = []
|
|
577
|
+
categories[cat].append(spec)
|
|
578
|
+
|
|
579
|
+
for category, specs in sorted(categories.items()):
|
|
580
|
+
click.secho(f" {category.title()}", fg="yellow", bold=True)
|
|
581
|
+
for spec in specs:
|
|
582
|
+
severity_color = {
|
|
583
|
+
"error": "red",
|
|
584
|
+
"warning": "yellow",
|
|
585
|
+
"info": "blue",
|
|
586
|
+
}[spec.default_severity.value]
|
|
587
|
+
click.echo(
|
|
588
|
+
f" {spec.rule_id}: "
|
|
589
|
+
f"{click.style(spec.default_severity.value, fg=severity_color)} - "
|
|
590
|
+
f"{spec.description}"
|
|
591
|
+
)
|
|
592
|
+
click.echo()
|
|
593
|
+
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
# Validate we have sources for actual linting
|
|
597
|
+
if not sources:
|
|
598
|
+
click.secho("Error: No source files specified.", fg="red", err=True)
|
|
599
|
+
raise click.Abort()
|
|
600
|
+
|
|
601
|
+
lint_config: LintConfig
|
|
602
|
+
|
|
603
|
+
if config:
|
|
604
|
+
# Load from specified config file
|
|
605
|
+
try:
|
|
606
|
+
lint_config = load_lint_config(config)
|
|
607
|
+
click.echo(f"Using config: {config}")
|
|
608
|
+
except (FileNotFoundError, ValueError) as e:
|
|
609
|
+
click.secho(f"Error loading config: {e}", fg="red", err=True)
|
|
610
|
+
raise click.Abort()
|
|
611
|
+
else:
|
|
612
|
+
# Try to find config file automatically
|
|
613
|
+
found_config = find_config_file()
|
|
614
|
+
if found_config:
|
|
615
|
+
try:
|
|
616
|
+
lint_config = load_lint_config(found_config)
|
|
617
|
+
click.echo(f"Using config: {found_config}")
|
|
618
|
+
except (FileNotFoundError, ValueError) as e:
|
|
619
|
+
click.secho(f"Error loading config: {e}", fg="red", err=True)
|
|
620
|
+
raise click.Abort()
|
|
621
|
+
else:
|
|
622
|
+
lint_config = LintConfig()
|
|
623
|
+
|
|
624
|
+
# Apply CLI overrides
|
|
625
|
+
lint_config.level = level
|
|
626
|
+
|
|
627
|
+
if enable:
|
|
628
|
+
lint_config.enabled_rules = set(enable)
|
|
629
|
+
if disable:
|
|
630
|
+
lint_config.disabled_rules.update(disable)
|
|
631
|
+
|
|
632
|
+
# Create engine and run
|
|
633
|
+
engine = LintEngine(lint_config)
|
|
634
|
+
|
|
635
|
+
click.echo(f"Scanning {len(sources)} file(s)...")
|
|
636
|
+
click.echo()
|
|
637
|
+
|
|
638
|
+
summary = engine.lint_files(list(sources))
|
|
639
|
+
|
|
640
|
+
# Format and output results
|
|
641
|
+
use_colour = not no_colour and output_format == "text"
|
|
642
|
+
formatter = get_formatter(output_format, use_colour=use_colour)
|
|
643
|
+
|
|
644
|
+
output = formatter.format_summary(summary)
|
|
645
|
+
click.echo(output)
|
|
646
|
+
|
|
647
|
+
# Exit with appropriate code
|
|
648
|
+
raise SystemExit(summary.exit_code)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@cli.command()
|
|
652
|
+
@click.argument("old_file", type=click.Path(exists=True, path_type=Path))
|
|
653
|
+
@click.argument("new_file", type=click.Path(exists=True, path_type=Path))
|
|
654
|
+
@click.option(
|
|
655
|
+
"--output",
|
|
656
|
+
"-o",
|
|
657
|
+
type=click.Path(path_type=Path),
|
|
658
|
+
help="Write output to file instead of stdout",
|
|
659
|
+
)
|
|
660
|
+
@click.option(
|
|
661
|
+
"--format",
|
|
662
|
+
"-f",
|
|
663
|
+
"output_format",
|
|
664
|
+
type=click.Choice(["text", "markdown", "md", "json"], case_sensitive=False),
|
|
665
|
+
default="text",
|
|
666
|
+
help="Output format (default: text)",
|
|
667
|
+
)
|
|
668
|
+
@click.option(
|
|
669
|
+
"--show",
|
|
670
|
+
type=str,
|
|
671
|
+
help="Show only these change types (comma-separated: added,removed,modified)",
|
|
672
|
+
)
|
|
673
|
+
@click.option(
|
|
674
|
+
"--hide",
|
|
675
|
+
type=str,
|
|
676
|
+
help="Hide these change types (comma-separated: added,removed,modified)",
|
|
677
|
+
)
|
|
678
|
+
@click.option(
|
|
679
|
+
"--entities",
|
|
680
|
+
type=str,
|
|
681
|
+
help="Show only these entity types (comma-separated: classes,properties,instances)",
|
|
682
|
+
)
|
|
683
|
+
@click.option(
|
|
684
|
+
"--ignore-predicates",
|
|
685
|
+
type=str,
|
|
686
|
+
help="Ignore these predicates in comparison (comma-separated CURIEs)",
|
|
687
|
+
)
|
|
688
|
+
def diff(
|
|
689
|
+
old_file: Path,
|
|
690
|
+
new_file: Path,
|
|
691
|
+
output: Path | None,
|
|
692
|
+
output_format: str,
|
|
693
|
+
show: str | None,
|
|
694
|
+
hide: str | None,
|
|
695
|
+
entities: str | None,
|
|
696
|
+
ignore_predicates: str | None,
|
|
697
|
+
):
|
|
698
|
+
"""Compare two RDF files and show semantic differences.
|
|
699
|
+
|
|
700
|
+
Compares OLD_FILE to NEW_FILE and reports changes, ignoring cosmetic
|
|
701
|
+
differences like statement order, prefix bindings, and whitespace.
|
|
702
|
+
|
|
703
|
+
\b
|
|
704
|
+
Examples:
|
|
705
|
+
rdf-construct diff v1.0.ttl v1.1.ttl
|
|
706
|
+
rdf-construct diff v1.0.ttl v1.1.ttl --format markdown -o CHANGELOG.md
|
|
707
|
+
rdf-construct diff old.ttl new.ttl --show added,removed
|
|
708
|
+
rdf-construct diff old.ttl new.ttl --entities classes
|
|
709
|
+
|
|
710
|
+
\b
|
|
711
|
+
Exit codes:
|
|
712
|
+
0 - Graphs are semantically identical
|
|
713
|
+
1 - Differences were found
|
|
714
|
+
2 - Error occurred
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
# Parse ignored predicates
|
|
719
|
+
ignore_preds: set[URIRef] | None = None
|
|
720
|
+
if ignore_predicates:
|
|
721
|
+
temp_graph = Graph()
|
|
722
|
+
temp_graph.parse(str(old_file), format="turtle")
|
|
723
|
+
|
|
724
|
+
ignore_preds = set()
|
|
725
|
+
for pred_str in ignore_predicates.split(","):
|
|
726
|
+
pred_str = pred_str.strip()
|
|
727
|
+
uri = expand_curie(temp_graph, pred_str)
|
|
728
|
+
if uri:
|
|
729
|
+
ignore_preds.add(uri)
|
|
730
|
+
else:
|
|
731
|
+
click.secho(
|
|
732
|
+
f"Warning: Could not expand predicate '{pred_str}'",
|
|
733
|
+
fg="yellow",
|
|
734
|
+
err=True,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
# Perform comparison
|
|
738
|
+
click.echo(f"Comparing {old_file.name} → {new_file.name}...", err=True)
|
|
739
|
+
diff_result = compare_files(old_file, new_file, ignore_predicates=ignore_preds)
|
|
740
|
+
|
|
741
|
+
# Apply filters
|
|
742
|
+
if show or hide or entities:
|
|
743
|
+
show_types = parse_filter_string(show) if show else None
|
|
744
|
+
hide_types = parse_filter_string(hide) if hide else None
|
|
745
|
+
entity_types = parse_filter_string(entities) if entities else None
|
|
746
|
+
|
|
747
|
+
diff_result = filter_diff(
|
|
748
|
+
diff_result,
|
|
749
|
+
show_types=show_types,
|
|
750
|
+
hide_types=hide_types,
|
|
751
|
+
entity_types=entity_types,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# Load graph for CURIE formatting
|
|
755
|
+
graph_for_format = None
|
|
756
|
+
if output_format in ("text", "markdown", "md"):
|
|
757
|
+
graph_for_format = Graph()
|
|
758
|
+
graph_for_format.parse(str(new_file), format="turtle")
|
|
759
|
+
|
|
760
|
+
# Format output
|
|
761
|
+
formatted = format_diff(diff_result, format_name=output_format, graph=graph_for_format)
|
|
762
|
+
|
|
763
|
+
# Write output
|
|
764
|
+
if output:
|
|
765
|
+
output.write_text(formatted)
|
|
766
|
+
click.secho(f"✓ Wrote diff to {output}", fg="green", err=True)
|
|
767
|
+
else:
|
|
768
|
+
click.echo(formatted)
|
|
769
|
+
|
|
770
|
+
# Exit code: 0 if identical, 1 if different
|
|
771
|
+
if diff_result.is_identical:
|
|
772
|
+
click.secho("Graphs are semantically identical.", fg="green", err=True)
|
|
773
|
+
sys.exit(0)
|
|
774
|
+
else:
|
|
775
|
+
sys.exit(1)
|
|
776
|
+
|
|
777
|
+
except FileNotFoundError as e:
|
|
778
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
779
|
+
sys.exit(2)
|
|
780
|
+
except ValueError as e:
|
|
781
|
+
click.secho(f"Error parsing RDF: {e}", fg="red", err=True)
|
|
782
|
+
sys.exit(2)
|
|
783
|
+
except Exception as e:
|
|
784
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
785
|
+
sys.exit(2)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
@cli.command()
|
|
789
|
+
@click.argument("sources", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path))
|
|
790
|
+
@click.option(
|
|
791
|
+
"--output",
|
|
792
|
+
"-o",
|
|
793
|
+
type=click.Path(path_type=Path),
|
|
794
|
+
default="docs",
|
|
795
|
+
help="Output directory (default: docs)",
|
|
796
|
+
)
|
|
797
|
+
@click.option(
|
|
798
|
+
"--format",
|
|
799
|
+
"-f",
|
|
800
|
+
"output_format",
|
|
801
|
+
type=click.Choice(["html", "markdown", "md", "json"], case_sensitive=False),
|
|
802
|
+
default="html",
|
|
803
|
+
help="Output format (default: html)",
|
|
804
|
+
)
|
|
805
|
+
@click.option(
|
|
806
|
+
"--config",
|
|
807
|
+
"-C",
|
|
808
|
+
type=click.Path(exists=True, path_type=Path),
|
|
809
|
+
help="Path to configuration YAML file",
|
|
810
|
+
)
|
|
811
|
+
@click.option(
|
|
812
|
+
"--template",
|
|
813
|
+
"-t",
|
|
814
|
+
type=click.Path(exists=True, path_type=Path),
|
|
815
|
+
help="Path to custom template directory",
|
|
816
|
+
)
|
|
817
|
+
@click.option(
|
|
818
|
+
"--single-page",
|
|
819
|
+
is_flag=True,
|
|
820
|
+
help="Generate single-page documentation",
|
|
821
|
+
)
|
|
822
|
+
@click.option(
|
|
823
|
+
"--title",
|
|
824
|
+
help="Override ontology title",
|
|
825
|
+
)
|
|
826
|
+
@click.option(
|
|
827
|
+
"--no-search",
|
|
828
|
+
is_flag=True,
|
|
829
|
+
help="Disable search index generation (HTML only)",
|
|
830
|
+
)
|
|
831
|
+
@click.option(
|
|
832
|
+
"--no-instances",
|
|
833
|
+
is_flag=True,
|
|
834
|
+
help="Exclude instances from documentation",
|
|
835
|
+
)
|
|
836
|
+
@click.option(
|
|
837
|
+
"--include",
|
|
838
|
+
type=str,
|
|
839
|
+
help="Include only these entity types (comma-separated: classes,properties,instances)",
|
|
840
|
+
)
|
|
841
|
+
@click.option(
|
|
842
|
+
"--exclude",
|
|
843
|
+
type=str,
|
|
844
|
+
help="Exclude these entity types (comma-separated: classes,properties,instances)",
|
|
845
|
+
)
|
|
846
|
+
def docs(
|
|
847
|
+
sources: tuple[Path, ...],
|
|
848
|
+
output: Path,
|
|
849
|
+
output_format: str,
|
|
850
|
+
config: Path | None,
|
|
851
|
+
template: Path | None,
|
|
852
|
+
single_page: bool,
|
|
853
|
+
title: str | None,
|
|
854
|
+
no_search: bool,
|
|
855
|
+
no_instances: bool,
|
|
856
|
+
include: str | None,
|
|
857
|
+
exclude: str | None,
|
|
858
|
+
):
|
|
859
|
+
"""Generate documentation from RDF ontologies.
|
|
860
|
+
|
|
861
|
+
SOURCES: One or more RDF files to generate documentation from.
|
|
862
|
+
|
|
863
|
+
\b
|
|
864
|
+
Examples:
|
|
865
|
+
# Basic HTML documentation
|
|
866
|
+
rdf-construct docs ontology.ttl
|
|
867
|
+
|
|
868
|
+
# Markdown output to custom directory
|
|
869
|
+
rdf-construct docs ontology.ttl --format markdown -o api-docs/
|
|
870
|
+
|
|
871
|
+
# Single-page HTML with custom title
|
|
872
|
+
rdf-construct docs ontology.ttl --single-page --title "My Ontology"
|
|
873
|
+
|
|
874
|
+
# JSON output for custom rendering
|
|
875
|
+
rdf-construct docs ontology.ttl --format json
|
|
876
|
+
|
|
877
|
+
# Use custom templates
|
|
878
|
+
rdf-construct docs ontology.ttl --template my-templates/
|
|
879
|
+
|
|
880
|
+
# Generate from multiple sources (merged)
|
|
881
|
+
rdf-construct docs domain.ttl foundation.ttl -o docs/
|
|
882
|
+
|
|
883
|
+
\b
|
|
884
|
+
Output formats:
|
|
885
|
+
html - Navigable HTML pages with search (default)
|
|
886
|
+
markdown - GitHub/GitLab compatible Markdown
|
|
887
|
+
json - Structured JSON for custom rendering
|
|
888
|
+
"""
|
|
889
|
+
from rdflib import Graph
|
|
890
|
+
|
|
891
|
+
from rdf_construct.docs import DocsConfig, DocsGenerator, load_docs_config
|
|
892
|
+
|
|
893
|
+
# Load or create configuration
|
|
894
|
+
if config:
|
|
895
|
+
doc_config = load_docs_config(config)
|
|
896
|
+
else:
|
|
897
|
+
doc_config = DocsConfig()
|
|
898
|
+
|
|
899
|
+
# Apply CLI overrides
|
|
900
|
+
doc_config.output_dir = output
|
|
901
|
+
doc_config.format = "markdown" if output_format == "md" else output_format
|
|
902
|
+
doc_config.single_page = single_page
|
|
903
|
+
doc_config.include_search = not no_search
|
|
904
|
+
doc_config.include_instances = not no_instances
|
|
905
|
+
|
|
906
|
+
if template:
|
|
907
|
+
doc_config.template_dir = template
|
|
908
|
+
if title:
|
|
909
|
+
doc_config.title = title
|
|
910
|
+
|
|
911
|
+
# Parse include/exclude filters
|
|
912
|
+
if include:
|
|
913
|
+
types = [t.strip().lower() for t in include.split(",")]
|
|
914
|
+
doc_config.include_classes = "classes" in types
|
|
915
|
+
doc_config.include_object_properties = "properties" in types or "object_properties" in types
|
|
916
|
+
doc_config.include_datatype_properties = "properties" in types or "datatype_properties" in types
|
|
917
|
+
doc_config.include_annotation_properties = "properties" in types or "annotation_properties" in types
|
|
918
|
+
doc_config.include_instances = "instances" in types
|
|
919
|
+
|
|
920
|
+
if exclude:
|
|
921
|
+
types = [t.strip().lower() for t in exclude.split(",")]
|
|
922
|
+
if "classes" in types:
|
|
923
|
+
doc_config.include_classes = False
|
|
924
|
+
if "properties" in types:
|
|
925
|
+
doc_config.include_object_properties = False
|
|
926
|
+
doc_config.include_datatype_properties = False
|
|
927
|
+
doc_config.include_annotation_properties = False
|
|
928
|
+
if "instances" in types:
|
|
929
|
+
doc_config.include_instances = False
|
|
930
|
+
|
|
931
|
+
# Load RDF sources
|
|
932
|
+
click.echo(f"Loading {len(sources)} source file(s)...")
|
|
933
|
+
graph = Graph()
|
|
934
|
+
|
|
935
|
+
for source in sources:
|
|
936
|
+
click.echo(f" Parsing {source.name}...")
|
|
937
|
+
|
|
938
|
+
# Determine format from extension
|
|
939
|
+
suffix = source.suffix.lower()
|
|
940
|
+
format_map = {
|
|
941
|
+
".ttl": "turtle",
|
|
942
|
+
".turtle": "turtle",
|
|
943
|
+
".rdf": "xml",
|
|
944
|
+
".xml": "xml",
|
|
945
|
+
".owl": "xml",
|
|
946
|
+
".nt": "nt",
|
|
947
|
+
".ntriples": "nt",
|
|
948
|
+
".n3": "n3",
|
|
949
|
+
".jsonld": "json-ld",
|
|
950
|
+
".json": "json-ld",
|
|
951
|
+
}
|
|
952
|
+
rdf_format = format_map.get(suffix, "turtle")
|
|
953
|
+
|
|
954
|
+
graph.parse(str(source), format=rdf_format)
|
|
955
|
+
|
|
956
|
+
click.echo(f" Total: {len(graph)} triples")
|
|
957
|
+
click.echo()
|
|
958
|
+
|
|
959
|
+
# Generate documentation
|
|
960
|
+
click.echo(f"Generating {doc_config.format} documentation...")
|
|
961
|
+
|
|
962
|
+
generator = DocsGenerator(doc_config)
|
|
963
|
+
result = generator.generate(graph)
|
|
964
|
+
|
|
965
|
+
# Summary
|
|
966
|
+
click.echo()
|
|
967
|
+
click.secho(f"✓ Generated {result.total_pages} files to {result.output_dir}/", fg="green")
|
|
968
|
+
click.echo(f" Classes: {result.classes_count}")
|
|
969
|
+
click.echo(f" Properties: {result.properties_count}")
|
|
970
|
+
click.echo(f" Instances: {result.instances_count}")
|
|
971
|
+
|
|
972
|
+
# Show entry point
|
|
973
|
+
if doc_config.format == "html":
|
|
974
|
+
index_path = result.output_dir / "index.html"
|
|
975
|
+
click.echo()
|
|
976
|
+
click.secho(f"Open {index_path} in your browser to view the documentation.", fg="cyan")
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
@cli.command("shacl-gen")
|
|
980
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
981
|
+
@click.option(
|
|
982
|
+
"--output",
|
|
983
|
+
"-o",
|
|
984
|
+
type=click.Path(path_type=Path),
|
|
985
|
+
help="Output file path (default: <source>-shapes.ttl)",
|
|
986
|
+
)
|
|
987
|
+
@click.option(
|
|
988
|
+
"--format",
|
|
989
|
+
"-f",
|
|
990
|
+
"output_format",
|
|
991
|
+
type=click.Choice(["turtle", "ttl", "json-ld", "jsonld"], case_sensitive=False),
|
|
992
|
+
default="turtle",
|
|
993
|
+
help="Output format (default: turtle)",
|
|
994
|
+
)
|
|
995
|
+
@click.option(
|
|
996
|
+
"--level",
|
|
997
|
+
"-l",
|
|
998
|
+
type=click.Choice(["minimal", "standard", "strict"], case_sensitive=False),
|
|
999
|
+
default="standard",
|
|
1000
|
+
help="Strictness level for constraint generation (default: standard)",
|
|
1001
|
+
)
|
|
1002
|
+
@click.option(
|
|
1003
|
+
"--config",
|
|
1004
|
+
"-C",
|
|
1005
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1006
|
+
help="YAML configuration file",
|
|
1007
|
+
)
|
|
1008
|
+
@click.option(
|
|
1009
|
+
"--classes",
|
|
1010
|
+
type=str,
|
|
1011
|
+
help="Comma-separated list of classes to generate shapes for",
|
|
1012
|
+
)
|
|
1013
|
+
@click.option(
|
|
1014
|
+
"--closed",
|
|
1015
|
+
is_flag=True,
|
|
1016
|
+
help="Generate closed shapes (no extra properties allowed)",
|
|
1017
|
+
)
|
|
1018
|
+
@click.option(
|
|
1019
|
+
"--default-severity",
|
|
1020
|
+
type=click.Choice(["violation", "warning", "info"], case_sensitive=False),
|
|
1021
|
+
default="violation",
|
|
1022
|
+
help="Default severity for generated constraints",
|
|
1023
|
+
)
|
|
1024
|
+
@click.option(
|
|
1025
|
+
"--no-labels",
|
|
1026
|
+
is_flag=True,
|
|
1027
|
+
help="Don't include rdfs:label as sh:name",
|
|
1028
|
+
)
|
|
1029
|
+
@click.option(
|
|
1030
|
+
"--no-descriptions",
|
|
1031
|
+
is_flag=True,
|
|
1032
|
+
help="Don't include rdfs:comment as sh:description",
|
|
1033
|
+
)
|
|
1034
|
+
@click.option(
|
|
1035
|
+
"--no-inherit",
|
|
1036
|
+
is_flag=True,
|
|
1037
|
+
help="Don't inherit constraints from superclasses",
|
|
1038
|
+
)
|
|
1039
|
+
def shacl_gen(
|
|
1040
|
+
source: Path,
|
|
1041
|
+
output: Path | None,
|
|
1042
|
+
output_format: str,
|
|
1043
|
+
level: str,
|
|
1044
|
+
config: Path | None,
|
|
1045
|
+
classes: str | None,
|
|
1046
|
+
closed: bool,
|
|
1047
|
+
default_severity: str,
|
|
1048
|
+
no_labels: bool,
|
|
1049
|
+
no_descriptions: bool,
|
|
1050
|
+
no_inherit: bool,
|
|
1051
|
+
):
|
|
1052
|
+
"""Generate SHACL validation shapes from OWL ontology.
|
|
1053
|
+
|
|
1054
|
+
Converts OWL class definitions to SHACL NodeShapes, extracting
|
|
1055
|
+
constraints from domain/range declarations, cardinality restrictions,
|
|
1056
|
+
functional properties, and other OWL patterns.
|
|
1057
|
+
|
|
1058
|
+
SOURCE: Input RDF ontology file (.ttl, .rdf, .owl, etc.)
|
|
1059
|
+
|
|
1060
|
+
\b
|
|
1061
|
+
Strictness levels:
|
|
1062
|
+
minimal - Basic type constraints only (sh:class, sh:datatype)
|
|
1063
|
+
standard - Adds cardinality and functional property constraints
|
|
1064
|
+
strict - Maximum constraints including sh:closed, enumerations
|
|
1065
|
+
|
|
1066
|
+
\b
|
|
1067
|
+
Examples:
|
|
1068
|
+
# Basic generation
|
|
1069
|
+
rdf-construct shacl-gen ontology.ttl
|
|
1070
|
+
|
|
1071
|
+
# Generate with strict constraints
|
|
1072
|
+
rdf-construct shacl-gen ontology.ttl --level strict --closed
|
|
1073
|
+
|
|
1074
|
+
# Custom output path and format
|
|
1075
|
+
rdf-construct shacl-gen ontology.ttl -o shapes.ttl --format turtle
|
|
1076
|
+
|
|
1077
|
+
# Focus on specific classes
|
|
1078
|
+
rdf-construct shacl-gen ontology.ttl --classes "ex:Building,ex:Floor"
|
|
1079
|
+
|
|
1080
|
+
# Use configuration file
|
|
1081
|
+
rdf-construct shacl-gen ontology.ttl --config shacl-config.yml
|
|
1082
|
+
|
|
1083
|
+
# Generate warnings instead of violations
|
|
1084
|
+
rdf-construct shacl-gen ontology.ttl --default-severity warning
|
|
1085
|
+
"""
|
|
1086
|
+
from rdf_construct.shacl import (
|
|
1087
|
+
generate_shapes_to_file,
|
|
1088
|
+
load_shacl_config,
|
|
1089
|
+
ShaclConfig,
|
|
1090
|
+
StrictnessLevel,
|
|
1091
|
+
Severity,
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
# Determine output path
|
|
1095
|
+
if output is None:
|
|
1096
|
+
suffix = ".json" if "json" in output_format.lower() else ".ttl"
|
|
1097
|
+
output = source.with_stem(f"{source.stem}-shapes").with_suffix(suffix)
|
|
1098
|
+
|
|
1099
|
+
# Normalise format string
|
|
1100
|
+
if output_format.lower() in ("ttl", "turtle"):
|
|
1101
|
+
output_format = "turtle"
|
|
1102
|
+
elif output_format.lower() in ("json-ld", "jsonld"):
|
|
1103
|
+
output_format = "json-ld"
|
|
1104
|
+
|
|
1105
|
+
try:
|
|
1106
|
+
# Load configuration from file or build from CLI options
|
|
1107
|
+
if config:
|
|
1108
|
+
shacl_config = load_shacl_config(config)
|
|
1109
|
+
click.echo(f"Loaded configuration from {config}")
|
|
1110
|
+
else:
|
|
1111
|
+
shacl_config = ShaclConfig()
|
|
1112
|
+
|
|
1113
|
+
# Apply CLI overrides
|
|
1114
|
+
shacl_config.level = StrictnessLevel(level.lower())
|
|
1115
|
+
|
|
1116
|
+
if classes:
|
|
1117
|
+
shacl_config.target_classes = [c.strip() for c in classes.split(",")]
|
|
1118
|
+
|
|
1119
|
+
if closed:
|
|
1120
|
+
shacl_config.closed = True
|
|
1121
|
+
|
|
1122
|
+
shacl_config.default_severity = Severity(default_severity.lower())
|
|
1123
|
+
|
|
1124
|
+
if no_labels:
|
|
1125
|
+
shacl_config.include_labels = False
|
|
1126
|
+
|
|
1127
|
+
if no_descriptions:
|
|
1128
|
+
shacl_config.include_descriptions = False
|
|
1129
|
+
|
|
1130
|
+
if no_inherit:
|
|
1131
|
+
shacl_config.inherit_constraints = False
|
|
1132
|
+
|
|
1133
|
+
# Generate shapes
|
|
1134
|
+
click.echo(f"Generating SHACL shapes from {source}...")
|
|
1135
|
+
click.echo(f" Level: {shacl_config.level.value}")
|
|
1136
|
+
|
|
1137
|
+
if shacl_config.target_classes:
|
|
1138
|
+
click.echo(f" Target classes: {', '.join(shacl_config.target_classes)}")
|
|
1139
|
+
|
|
1140
|
+
shapes_graph = generate_shapes_to_file(
|
|
1141
|
+
source,
|
|
1142
|
+
output,
|
|
1143
|
+
shacl_config,
|
|
1144
|
+
output_format,
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
# Count generated shapes
|
|
1148
|
+
from rdf_construct.shacl import SH
|
|
1149
|
+
num_shapes = len(list(shapes_graph.subjects(
|
|
1150
|
+
predicate=None, object=SH.NodeShape
|
|
1151
|
+
)))
|
|
1152
|
+
|
|
1153
|
+
click.secho(f"✓ Generated {num_shapes} shape(s) to {output}", fg="green")
|
|
1154
|
+
|
|
1155
|
+
if shacl_config.closed:
|
|
1156
|
+
click.echo(" (closed shapes enabled)")
|
|
1157
|
+
|
|
1158
|
+
except FileNotFoundError as e:
|
|
1159
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1160
|
+
raise SystemExit(1)
|
|
1161
|
+
except ValueError as e:
|
|
1162
|
+
click.secho(f"Configuration error: {e}", fg="red", err=True)
|
|
1163
|
+
raise SystemExit(1)
|
|
1164
|
+
except Exception as e:
|
|
1165
|
+
click.secho(f"Error generating shapes: {e}", fg="red", err=True)
|
|
1166
|
+
raise SystemExit(1)
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
# Output format choices
|
|
1170
|
+
OUTPUT_FORMATS = ["turtle", "ttl", "xml", "rdfxml", "jsonld", "json-ld", "nt", "ntriples"]
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
@cli.command()
|
|
1174
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
1175
|
+
@click.option(
|
|
1176
|
+
"--output",
|
|
1177
|
+
"-o",
|
|
1178
|
+
type=click.Path(path_type=Path),
|
|
1179
|
+
help="Output file path (default: source name with .ttl extension)",
|
|
1180
|
+
)
|
|
1181
|
+
@click.option(
|
|
1182
|
+
"--format",
|
|
1183
|
+
"-f",
|
|
1184
|
+
"output_format",
|
|
1185
|
+
type=click.Choice(OUTPUT_FORMATS, case_sensitive=False),
|
|
1186
|
+
default="turtle",
|
|
1187
|
+
help="Output RDF format (default: turtle)",
|
|
1188
|
+
)
|
|
1189
|
+
@click.option(
|
|
1190
|
+
"--namespace",
|
|
1191
|
+
"-n",
|
|
1192
|
+
help="Default namespace URI for the ontology",
|
|
1193
|
+
)
|
|
1194
|
+
@click.option(
|
|
1195
|
+
"--config",
|
|
1196
|
+
"-C",
|
|
1197
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1198
|
+
help="Path to YAML configuration file",
|
|
1199
|
+
)
|
|
1200
|
+
@click.option(
|
|
1201
|
+
"--merge",
|
|
1202
|
+
"-m",
|
|
1203
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1204
|
+
help="Existing ontology file to merge with",
|
|
1205
|
+
)
|
|
1206
|
+
@click.option(
|
|
1207
|
+
"--validate",
|
|
1208
|
+
"-v",
|
|
1209
|
+
is_flag=True,
|
|
1210
|
+
help="Validate only, don't generate output",
|
|
1211
|
+
)
|
|
1212
|
+
@click.option(
|
|
1213
|
+
"--strict",
|
|
1214
|
+
is_flag=True,
|
|
1215
|
+
help="Treat warnings as errors",
|
|
1216
|
+
)
|
|
1217
|
+
@click.option(
|
|
1218
|
+
"--language",
|
|
1219
|
+
"-l",
|
|
1220
|
+
default="en",
|
|
1221
|
+
help="Language tag for labels/comments (default: en)",
|
|
1222
|
+
)
|
|
1223
|
+
@click.option(
|
|
1224
|
+
"--no-labels",
|
|
1225
|
+
is_flag=True,
|
|
1226
|
+
help="Don't auto-generate rdfs:label triples",
|
|
1227
|
+
)
|
|
1228
|
+
def puml2rdf(
|
|
1229
|
+
source: Path,
|
|
1230
|
+
output: Path | None,
|
|
1231
|
+
output_format: str,
|
|
1232
|
+
namespace: str | None,
|
|
1233
|
+
config: Path | None,
|
|
1234
|
+
merge: Path | None,
|
|
1235
|
+
validate: bool,
|
|
1236
|
+
strict: bool,
|
|
1237
|
+
language: str,
|
|
1238
|
+
no_labels: bool,
|
|
1239
|
+
):
|
|
1240
|
+
"""Convert PlantUML class diagram to RDF ontology.
|
|
1241
|
+
|
|
1242
|
+
Parses a PlantUML file and generates an RDF/OWL ontology.
|
|
1243
|
+
Supports classes, attributes, inheritance, and associations.
|
|
1244
|
+
|
|
1245
|
+
SOURCE: PlantUML file (.puml or .plantuml)
|
|
1246
|
+
|
|
1247
|
+
\b
|
|
1248
|
+
Examples:
|
|
1249
|
+
# Basic conversion
|
|
1250
|
+
rdf-construct puml2rdf design.puml
|
|
1251
|
+
|
|
1252
|
+
# Custom output and namespace
|
|
1253
|
+
rdf-construct puml2rdf design.puml -o ontology.ttl -n http://example.org/ont#
|
|
1254
|
+
|
|
1255
|
+
# Validate without generating
|
|
1256
|
+
rdf-construct puml2rdf design.puml --validate
|
|
1257
|
+
|
|
1258
|
+
# Merge with existing ontology
|
|
1259
|
+
rdf-construct puml2rdf design.puml --merge existing.ttl
|
|
1260
|
+
|
|
1261
|
+
# Use configuration file
|
|
1262
|
+
rdf-construct puml2rdf design.puml -C import-config.yml
|
|
1263
|
+
|
|
1264
|
+
\b
|
|
1265
|
+
Exit codes:
|
|
1266
|
+
0 - Success
|
|
1267
|
+
1 - Validation warnings (with --strict)
|
|
1268
|
+
2 - Parse or validation errors
|
|
1269
|
+
"""
|
|
1270
|
+
# Normalise output format
|
|
1271
|
+
format_map = {
|
|
1272
|
+
"ttl": "turtle",
|
|
1273
|
+
"rdfxml": "xml",
|
|
1274
|
+
"json-ld": "json-ld",
|
|
1275
|
+
"jsonld": "json-ld",
|
|
1276
|
+
"ntriples": "nt",
|
|
1277
|
+
}
|
|
1278
|
+
rdf_format = format_map.get(output_format.lower(), output_format.lower())
|
|
1279
|
+
|
|
1280
|
+
# Determine output path
|
|
1281
|
+
if output is None and not validate:
|
|
1282
|
+
ext_map = {"turtle": ".ttl", "xml": ".rdf", "json-ld": ".jsonld", "nt": ".nt"}
|
|
1283
|
+
ext = ext_map.get(rdf_format, ".ttl")
|
|
1284
|
+
output = source.with_suffix(ext)
|
|
1285
|
+
|
|
1286
|
+
# Load configuration if provided
|
|
1287
|
+
if config:
|
|
1288
|
+
try:
|
|
1289
|
+
import_config = load_import_config(config)
|
|
1290
|
+
conversion_config = import_config.to_conversion_config()
|
|
1291
|
+
except Exception as e:
|
|
1292
|
+
click.secho(f"Error loading config: {e}", fg="red", err=True)
|
|
1293
|
+
sys.exit(2)
|
|
1294
|
+
else:
|
|
1295
|
+
conversion_config = ConversionConfig()
|
|
1296
|
+
|
|
1297
|
+
# Override config with CLI options
|
|
1298
|
+
if namespace:
|
|
1299
|
+
conversion_config.default_namespace = namespace
|
|
1300
|
+
if language:
|
|
1301
|
+
conversion_config.language = language
|
|
1302
|
+
if no_labels:
|
|
1303
|
+
conversion_config.generate_labels = False
|
|
1304
|
+
|
|
1305
|
+
# Parse PlantUML file
|
|
1306
|
+
click.echo(f"Parsing {source.name}...")
|
|
1307
|
+
parser = PlantUMLParser()
|
|
1308
|
+
|
|
1309
|
+
try:
|
|
1310
|
+
parse_result = parser.parse_file(source)
|
|
1311
|
+
except Exception as e:
|
|
1312
|
+
click.secho(f"Error reading file: {e}", fg="red", err=True)
|
|
1313
|
+
sys.exit(2)
|
|
1314
|
+
|
|
1315
|
+
# Report parse errors
|
|
1316
|
+
if parse_result.errors:
|
|
1317
|
+
click.secho("Parse errors:", fg="red", err=True)
|
|
1318
|
+
for error in parse_result.errors:
|
|
1319
|
+
click.echo(f" Line {error.line_number}: {error.message}", err=True)
|
|
1320
|
+
sys.exit(2)
|
|
1321
|
+
|
|
1322
|
+
# Report parse warnings
|
|
1323
|
+
if parse_result.warnings:
|
|
1324
|
+
click.secho("Parse warnings:", fg="yellow", err=True)
|
|
1325
|
+
for warning in parse_result.warnings:
|
|
1326
|
+
click.echo(f" {warning}", err=True)
|
|
1327
|
+
|
|
1328
|
+
model = parse_result.model
|
|
1329
|
+
click.echo(
|
|
1330
|
+
f" Found: {len(model.classes)} classes, "
|
|
1331
|
+
f"{len(model.relationships)} relationships"
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
# Validate model
|
|
1335
|
+
model_validation = validate_puml(model)
|
|
1336
|
+
|
|
1337
|
+
if model_validation.has_errors:
|
|
1338
|
+
click.secho("Model validation errors:", fg="red", err=True)
|
|
1339
|
+
for issue in model_validation.errors():
|
|
1340
|
+
click.echo(f" {issue}", err=True)
|
|
1341
|
+
sys.exit(2)
|
|
1342
|
+
|
|
1343
|
+
if model_validation.has_warnings:
|
|
1344
|
+
click.secho("Model validation warnings:", fg="yellow", err=True)
|
|
1345
|
+
for issue in model_validation.warnings():
|
|
1346
|
+
click.echo(f" {issue}", err=True)
|
|
1347
|
+
if strict:
|
|
1348
|
+
click.secho("Aborting due to --strict mode", fg="red", err=True)
|
|
1349
|
+
sys.exit(1)
|
|
1350
|
+
|
|
1351
|
+
# If validate-only mode, stop here
|
|
1352
|
+
if validate:
|
|
1353
|
+
if model_validation.has_warnings:
|
|
1354
|
+
click.secho(
|
|
1355
|
+
f"Validation complete: {model_validation.warning_count} warnings",
|
|
1356
|
+
fg="yellow",
|
|
1357
|
+
)
|
|
1358
|
+
else:
|
|
1359
|
+
click.secho("Validation complete: no issues found", fg="green")
|
|
1360
|
+
sys.exit(0)
|
|
1361
|
+
|
|
1362
|
+
# Convert to RDF
|
|
1363
|
+
click.echo("Converting to RDF...")
|
|
1364
|
+
converter = PumlToRdfConverter(conversion_config)
|
|
1365
|
+
conversion_result = converter.convert(model)
|
|
1366
|
+
|
|
1367
|
+
if conversion_result.warnings:
|
|
1368
|
+
click.secho("Conversion warnings:", fg="yellow", err=True)
|
|
1369
|
+
for warning in conversion_result.warnings:
|
|
1370
|
+
click.echo(f" {warning}", err=True)
|
|
1371
|
+
|
|
1372
|
+
graph = conversion_result.graph
|
|
1373
|
+
click.echo(f" Generated: {len(graph)} triples")
|
|
1374
|
+
|
|
1375
|
+
# Validate generated RDF
|
|
1376
|
+
rdf_validation = validate_rdf(graph)
|
|
1377
|
+
if rdf_validation.has_warnings:
|
|
1378
|
+
click.secho("RDF validation warnings:", fg="yellow", err=True)
|
|
1379
|
+
for issue in rdf_validation.warnings():
|
|
1380
|
+
click.echo(f" {issue}", err=True)
|
|
1381
|
+
|
|
1382
|
+
# Merge with existing if requested
|
|
1383
|
+
if merge:
|
|
1384
|
+
click.echo(f"Merging with {merge.name}...")
|
|
1385
|
+
try:
|
|
1386
|
+
merge_result = merge_with_existing(graph, merge)
|
|
1387
|
+
graph = merge_result.graph
|
|
1388
|
+
click.echo(
|
|
1389
|
+
f" Added: {merge_result.added_count}, "
|
|
1390
|
+
f"Preserved: {merge_result.preserved_count}"
|
|
1391
|
+
)
|
|
1392
|
+
if merge_result.conflicts:
|
|
1393
|
+
click.secho("Merge conflicts:", fg="yellow", err=True)
|
|
1394
|
+
for conflict in merge_result.conflicts[:5]: # Limit output
|
|
1395
|
+
click.echo(f" {conflict}", err=True)
|
|
1396
|
+
if len(merge_result.conflicts) > 5:
|
|
1397
|
+
click.echo(
|
|
1398
|
+
f" ... and {len(merge_result.conflicts) - 5} more",
|
|
1399
|
+
err=True,
|
|
1400
|
+
)
|
|
1401
|
+
except Exception as e:
|
|
1402
|
+
click.secho(f"Error merging: {e}", fg="red", err=True)
|
|
1403
|
+
sys.exit(2)
|
|
1404
|
+
|
|
1405
|
+
# Serialise output
|
|
1406
|
+
try:
|
|
1407
|
+
graph.serialize(str(output), format=rdf_format)
|
|
1408
|
+
click.secho(f"✓ Wrote {output}", fg="green")
|
|
1409
|
+
click.echo(
|
|
1410
|
+
f" Classes: {len(conversion_result.class_uris)}, "
|
|
1411
|
+
f"Properties: {len(conversion_result.property_uris)}"
|
|
1412
|
+
)
|
|
1413
|
+
except Exception as e:
|
|
1414
|
+
click.secho(f"Error writing output: {e}", fg="red", err=True)
|
|
1415
|
+
sys.exit(2)
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
@cli.command("cq-test")
|
|
1419
|
+
@click.argument("ontology", type=click.Path(exists=True, path_type=Path))
|
|
1420
|
+
@click.argument("test_file", type=click.Path(exists=True, path_type=Path))
|
|
1421
|
+
@click.option(
|
|
1422
|
+
"--data",
|
|
1423
|
+
"-d",
|
|
1424
|
+
multiple=True,
|
|
1425
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1426
|
+
help="Additional data file(s) to load alongside the ontology",
|
|
1427
|
+
)
|
|
1428
|
+
@click.option(
|
|
1429
|
+
"--tag",
|
|
1430
|
+
"-t",
|
|
1431
|
+
multiple=True,
|
|
1432
|
+
help="Only run tests with these tags (can specify multiple)",
|
|
1433
|
+
)
|
|
1434
|
+
@click.option(
|
|
1435
|
+
"--exclude-tag",
|
|
1436
|
+
"-x",
|
|
1437
|
+
multiple=True,
|
|
1438
|
+
help="Exclude tests with these tags (can specify multiple)",
|
|
1439
|
+
)
|
|
1440
|
+
@click.option(
|
|
1441
|
+
"--format",
|
|
1442
|
+
"-f",
|
|
1443
|
+
"output_format",
|
|
1444
|
+
type=click.Choice(["text", "json", "junit"], case_sensitive=False),
|
|
1445
|
+
default="text",
|
|
1446
|
+
help="Output format (default: text)",
|
|
1447
|
+
)
|
|
1448
|
+
@click.option(
|
|
1449
|
+
"--output",
|
|
1450
|
+
"-o",
|
|
1451
|
+
type=click.Path(path_type=Path),
|
|
1452
|
+
help="Write output to file instead of stdout",
|
|
1453
|
+
)
|
|
1454
|
+
@click.option(
|
|
1455
|
+
"--verbose",
|
|
1456
|
+
"-v",
|
|
1457
|
+
is_flag=True,
|
|
1458
|
+
help="Show verbose output (query text, timing details)",
|
|
1459
|
+
)
|
|
1460
|
+
@click.option(
|
|
1461
|
+
"--fail-fast",
|
|
1462
|
+
is_flag=True,
|
|
1463
|
+
help="Stop on first failure",
|
|
1464
|
+
)
|
|
1465
|
+
def cq_test(
|
|
1466
|
+
ontology: Path,
|
|
1467
|
+
test_file: Path,
|
|
1468
|
+
data: tuple[Path, ...],
|
|
1469
|
+
tag: tuple[str, ...],
|
|
1470
|
+
exclude_tag: tuple[str, ...],
|
|
1471
|
+
output_format: str,
|
|
1472
|
+
output: Path | None,
|
|
1473
|
+
verbose: bool,
|
|
1474
|
+
fail_fast: bool,
|
|
1475
|
+
):
|
|
1476
|
+
"""Run competency question tests against an ontology.
|
|
1477
|
+
|
|
1478
|
+
Validates whether an ontology can answer competency questions expressed
|
|
1479
|
+
as SPARQL queries with expected results.
|
|
1480
|
+
|
|
1481
|
+
ONTOLOGY: RDF file containing the ontology to test
|
|
1482
|
+
TEST_FILE: YAML file containing competency question tests
|
|
1483
|
+
|
|
1484
|
+
\b
|
|
1485
|
+
Examples:
|
|
1486
|
+
# Run all tests
|
|
1487
|
+
rdf-construct cq-test ontology.ttl cq-tests.yml
|
|
1488
|
+
|
|
1489
|
+
# Run with additional sample data
|
|
1490
|
+
rdf-construct cq-test ontology.ttl cq-tests.yml --data sample-data.ttl
|
|
1491
|
+
|
|
1492
|
+
# Run only tests tagged 'core'
|
|
1493
|
+
rdf-construct cq-test ontology.ttl cq-tests.yml --tag core
|
|
1494
|
+
|
|
1495
|
+
# Generate JUnit XML for CI
|
|
1496
|
+
rdf-construct cq-test ontology.ttl cq-tests.yml --format junit -o results.xml
|
|
1497
|
+
|
|
1498
|
+
# Verbose output with timing
|
|
1499
|
+
rdf-construct cq-test ontology.ttl cq-tests.yml --verbose
|
|
1500
|
+
|
|
1501
|
+
\b
|
|
1502
|
+
Exit codes:
|
|
1503
|
+
0 - All tests passed
|
|
1504
|
+
1 - One or more tests failed
|
|
1505
|
+
2 - Error occurred (invalid file, parse error, etc.)
|
|
1506
|
+
"""
|
|
1507
|
+
try:
|
|
1508
|
+
# Load ontology
|
|
1509
|
+
click.echo(f"Loading ontology: {ontology.name}...", err=True)
|
|
1510
|
+
graph = Graph()
|
|
1511
|
+
graph.parse(str(ontology), format=_infer_format(ontology))
|
|
1512
|
+
|
|
1513
|
+
# Load additional data files
|
|
1514
|
+
if data:
|
|
1515
|
+
for data_file in data:
|
|
1516
|
+
click.echo(f"Loading data: {data_file.name}...", err=True)
|
|
1517
|
+
graph.parse(str(data_file), format=_infer_format(data_file))
|
|
1518
|
+
|
|
1519
|
+
# Load test suite
|
|
1520
|
+
click.echo(f"Loading tests: {test_file.name}...", err=True)
|
|
1521
|
+
suite = load_test_suite(test_file)
|
|
1522
|
+
|
|
1523
|
+
# Filter by tags
|
|
1524
|
+
if tag or exclude_tag:
|
|
1525
|
+
include_tags = set(tag) if tag else None
|
|
1526
|
+
exclude_tags = set(exclude_tag) if exclude_tag else None
|
|
1527
|
+
suite = suite.filter_by_tags(include_tags, exclude_tags)
|
|
1528
|
+
|
|
1529
|
+
if not suite.questions:
|
|
1530
|
+
click.secho("No tests to run (check tag filters)", fg="yellow", err=True)
|
|
1531
|
+
sys.exit(0)
|
|
1532
|
+
|
|
1533
|
+
# Run tests
|
|
1534
|
+
click.echo(f"Running {len(suite.questions)} test(s)...", err=True)
|
|
1535
|
+
click.echo("", err=True)
|
|
1536
|
+
|
|
1537
|
+
runner = CQTestRunner(fail_fast=fail_fast, verbose=verbose)
|
|
1538
|
+
results = runner.run(graph, suite, ontology_file=ontology)
|
|
1539
|
+
|
|
1540
|
+
# Format output
|
|
1541
|
+
formatted = format_results(results, format_name=output_format, verbose=verbose)
|
|
1542
|
+
|
|
1543
|
+
# Write output
|
|
1544
|
+
if output:
|
|
1545
|
+
output.write_text(formatted)
|
|
1546
|
+
click.secho(f"✓ Results written to {output}", fg="green", err=True)
|
|
1547
|
+
else:
|
|
1548
|
+
click.echo(formatted)
|
|
1549
|
+
|
|
1550
|
+
# Exit code based on results
|
|
1551
|
+
if results.has_errors:
|
|
1552
|
+
sys.exit(2)
|
|
1553
|
+
elif results.has_failures:
|
|
1554
|
+
sys.exit(1)
|
|
1555
|
+
else:
|
|
1556
|
+
sys.exit(0)
|
|
1557
|
+
|
|
1558
|
+
except FileNotFoundError as e:
|
|
1559
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1560
|
+
sys.exit(2)
|
|
1561
|
+
except ValueError as e:
|
|
1562
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1563
|
+
sys.exit(2)
|
|
1564
|
+
except Exception as e:
|
|
1565
|
+
click.secho(f"Error: {type(e).__name__}: {e}", fg="red", err=True)
|
|
1566
|
+
sys.exit(2)
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _infer_format(path: Path) -> str:
|
|
1570
|
+
"""Infer RDF format from file extension."""
|
|
1571
|
+
suffix = path.suffix.lower()
|
|
1572
|
+
format_map = {
|
|
1573
|
+
".ttl": "turtle",
|
|
1574
|
+
".turtle": "turtle",
|
|
1575
|
+
".rdf": "xml",
|
|
1576
|
+
".xml": "xml",
|
|
1577
|
+
".owl": "xml",
|
|
1578
|
+
".nt": "nt",
|
|
1579
|
+
".ntriples": "nt",
|
|
1580
|
+
".n3": "n3",
|
|
1581
|
+
".jsonld": "json-ld",
|
|
1582
|
+
".json": "json-ld",
|
|
1583
|
+
}
|
|
1584
|
+
return format_map.get(suffix, "turtle")
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
@cli.command()
|
|
1588
|
+
@click.argument("files", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path))
|
|
1589
|
+
@click.option(
|
|
1590
|
+
"--output",
|
|
1591
|
+
"-o",
|
|
1592
|
+
type=click.Path(path_type=Path),
|
|
1593
|
+
help="Write output to file instead of stdout",
|
|
1594
|
+
)
|
|
1595
|
+
@click.option(
|
|
1596
|
+
"--format",
|
|
1597
|
+
"-f",
|
|
1598
|
+
"output_format",
|
|
1599
|
+
type=click.Choice(["text", "json", "markdown", "md"], case_sensitive=False),
|
|
1600
|
+
default="text",
|
|
1601
|
+
help="Output format (default: text)",
|
|
1602
|
+
)
|
|
1603
|
+
@click.option(
|
|
1604
|
+
"--compare",
|
|
1605
|
+
is_flag=True,
|
|
1606
|
+
help="Compare two ontology files (requires exactly 2 files)",
|
|
1607
|
+
)
|
|
1608
|
+
@click.option(
|
|
1609
|
+
"--include",
|
|
1610
|
+
type=str,
|
|
1611
|
+
help="Include only these metric categories (comma-separated: basic,hierarchy,properties,documentation,complexity,connectivity)",
|
|
1612
|
+
)
|
|
1613
|
+
@click.option(
|
|
1614
|
+
"--exclude",
|
|
1615
|
+
type=str,
|
|
1616
|
+
help="Exclude these metric categories (comma-separated)",
|
|
1617
|
+
)
|
|
1618
|
+
def stats(
|
|
1619
|
+
files: tuple[Path, ...],
|
|
1620
|
+
output: Path | None,
|
|
1621
|
+
output_format: str,
|
|
1622
|
+
compare: bool,
|
|
1623
|
+
include: str | None,
|
|
1624
|
+
exclude: str | None,
|
|
1625
|
+
):
|
|
1626
|
+
"""Compute and display ontology statistics.
|
|
1627
|
+
|
|
1628
|
+
Analyses one or more RDF ontology files and displays comprehensive metrics
|
|
1629
|
+
about structure, complexity, and documentation coverage.
|
|
1630
|
+
|
|
1631
|
+
\b
|
|
1632
|
+
Examples:
|
|
1633
|
+
# Basic statistics
|
|
1634
|
+
rdf-construct stats ontology.ttl
|
|
1635
|
+
|
|
1636
|
+
# JSON output for programmatic use
|
|
1637
|
+
rdf-construct stats ontology.ttl --format json -o stats.json
|
|
1638
|
+
|
|
1639
|
+
# Markdown for documentation
|
|
1640
|
+
rdf-construct stats ontology.ttl --format markdown >> README.md
|
|
1641
|
+
|
|
1642
|
+
# Compare two versions
|
|
1643
|
+
rdf-construct stats v1.ttl v2.ttl --compare
|
|
1644
|
+
|
|
1645
|
+
# Only show specific categories
|
|
1646
|
+
rdf-construct stats ontology.ttl --include basic,documentation
|
|
1647
|
+
|
|
1648
|
+
# Exclude some categories
|
|
1649
|
+
rdf-construct stats ontology.ttl --exclude connectivity,complexity
|
|
1650
|
+
|
|
1651
|
+
\b
|
|
1652
|
+
Metric Categories:
|
|
1653
|
+
basic - Counts (triples, classes, properties, individuals)
|
|
1654
|
+
hierarchy - Structure (depth, branching, orphans)
|
|
1655
|
+
properties - Coverage (domain, range, functional, symmetric)
|
|
1656
|
+
documentation - Labels and comments
|
|
1657
|
+
complexity - Multiple inheritance, OWL axioms
|
|
1658
|
+
connectivity - Most connected class, isolated classes
|
|
1659
|
+
|
|
1660
|
+
\b
|
|
1661
|
+
Exit codes:
|
|
1662
|
+
0 - Success
|
|
1663
|
+
1 - Error occurred
|
|
1664
|
+
"""
|
|
1665
|
+
try:
|
|
1666
|
+
# Validate file count for compare mode
|
|
1667
|
+
if compare:
|
|
1668
|
+
if len(files) != 2:
|
|
1669
|
+
click.secho(
|
|
1670
|
+
"Error: --compare requires exactly 2 files",
|
|
1671
|
+
fg="red",
|
|
1672
|
+
err=True,
|
|
1673
|
+
)
|
|
1674
|
+
sys.exit(1)
|
|
1675
|
+
|
|
1676
|
+
# Parse include/exclude categories
|
|
1677
|
+
include_set: set[str] | None = None
|
|
1678
|
+
exclude_set: set[str] | None = None
|
|
1679
|
+
|
|
1680
|
+
if include:
|
|
1681
|
+
include_set = {cat.strip().lower() for cat in include.split(",")}
|
|
1682
|
+
if exclude:
|
|
1683
|
+
exclude_set = {cat.strip().lower() for cat in exclude.split(",")}
|
|
1684
|
+
|
|
1685
|
+
# Load graphs
|
|
1686
|
+
graphs: list[tuple[Graph, Path]] = []
|
|
1687
|
+
for filepath in files:
|
|
1688
|
+
click.echo(f"Loading {filepath}...", err=True)
|
|
1689
|
+
graph = Graph()
|
|
1690
|
+
graph.parse(str(filepath), format="turtle")
|
|
1691
|
+
graphs.append((graph, filepath))
|
|
1692
|
+
click.echo(f" Loaded {len(graph)} triples", err=True)
|
|
1693
|
+
|
|
1694
|
+
if compare:
|
|
1695
|
+
# Comparison mode
|
|
1696
|
+
old_graph, old_path = graphs[0]
|
|
1697
|
+
new_graph, new_path = graphs[1]
|
|
1698
|
+
|
|
1699
|
+
click.echo("Collecting statistics...", err=True)
|
|
1700
|
+
old_stats = collect_stats(
|
|
1701
|
+
old_graph,
|
|
1702
|
+
source=str(old_path),
|
|
1703
|
+
include=include_set,
|
|
1704
|
+
exclude=exclude_set,
|
|
1705
|
+
)
|
|
1706
|
+
new_stats = collect_stats(
|
|
1707
|
+
new_graph,
|
|
1708
|
+
source=str(new_path),
|
|
1709
|
+
include=include_set,
|
|
1710
|
+
exclude=exclude_set,
|
|
1711
|
+
)
|
|
1712
|
+
|
|
1713
|
+
click.echo("Comparing versions...", err=True)
|
|
1714
|
+
comparison = compare_stats(old_stats, new_stats)
|
|
1715
|
+
|
|
1716
|
+
# Format output
|
|
1717
|
+
formatted = format_comparison(
|
|
1718
|
+
comparison,
|
|
1719
|
+
format_name=output_format,
|
|
1720
|
+
graph=new_graph,
|
|
1721
|
+
)
|
|
1722
|
+
else:
|
|
1723
|
+
# Single file or multiple files (show stats for first)
|
|
1724
|
+
graph, filepath = graphs[0]
|
|
1725
|
+
|
|
1726
|
+
click.echo("Collecting statistics...", err=True)
|
|
1727
|
+
ontology_stats = collect_stats(
|
|
1728
|
+
graph,
|
|
1729
|
+
source=str(filepath),
|
|
1730
|
+
include=include_set,
|
|
1731
|
+
exclude=exclude_set,
|
|
1732
|
+
)
|
|
1733
|
+
|
|
1734
|
+
# Format output
|
|
1735
|
+
formatted = format_stats(
|
|
1736
|
+
ontology_stats,
|
|
1737
|
+
format_name=output_format,
|
|
1738
|
+
graph=graph,
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
# Write output
|
|
1742
|
+
if output:
|
|
1743
|
+
output.write_text(formatted)
|
|
1744
|
+
click.secho(f"✓ Wrote stats to {output}", fg="green", err=True)
|
|
1745
|
+
else:
|
|
1746
|
+
click.echo(formatted)
|
|
1747
|
+
|
|
1748
|
+
sys.exit(0)
|
|
1749
|
+
|
|
1750
|
+
except ValueError as e:
|
|
1751
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1752
|
+
sys.exit(1)
|
|
1753
|
+
except FileNotFoundError as e:
|
|
1754
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1755
|
+
sys.exit(1)
|
|
1756
|
+
except Exception as e:
|
|
1757
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1758
|
+
sys.exit(1)
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
if __name__ == "__main__":
|
|
1762
|
+
cli()
|