rdf-construct 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdf_construct/__init__.py +12 -0
- rdf_construct/__main__.py +0 -0
- rdf_construct/cli.py +3429 -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/localise/__init__.py +114 -0
- rdf_construct/localise/config.py +508 -0
- rdf_construct/localise/extractor.py +427 -0
- rdf_construct/localise/formatters/__init__.py +36 -0
- rdf_construct/localise/formatters/markdown.py +229 -0
- rdf_construct/localise/formatters/text.py +224 -0
- rdf_construct/localise/merger.py +346 -0
- rdf_construct/localise/reporter.py +356 -0
- rdf_construct/main.py +6 -0
- rdf_construct/merge/__init__.py +165 -0
- rdf_construct/merge/config.py +354 -0
- rdf_construct/merge/conflicts.py +281 -0
- rdf_construct/merge/formatters.py +426 -0
- rdf_construct/merge/merger.py +425 -0
- rdf_construct/merge/migrator.py +339 -0
- rdf_construct/merge/rules.py +377 -0
- rdf_construct/merge/splitter.py +1102 -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/refactor/__init__.py +72 -0
- rdf_construct/refactor/config.py +362 -0
- rdf_construct/refactor/deprecator.py +328 -0
- rdf_construct/refactor/formatters/__init__.py +8 -0
- rdf_construct/refactor/formatters/text.py +311 -0
- rdf_construct/refactor/renamer.py +294 -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.3.0.dist-info/METADATA +496 -0
- rdf_construct-0.3.0.dist-info/RECORD +110 -0
- rdf_construct-0.3.0.dist-info/WHEEL +4 -0
- rdf_construct-0.3.0.dist-info/entry_points.txt +3 -0
- rdf_construct-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Profile and configuration management for RDF ordering."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .predicate_order import PredicateOrderConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OrderingProfile:
|
|
12
|
+
"""Represents an ordering profile from a YAML configuration.
|
|
13
|
+
|
|
14
|
+
A profile defines how to organize and order RDF subjects, typically
|
|
15
|
+
with multiple sections (classes, properties, individuals) each having
|
|
16
|
+
their own selection and sorting rules.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
name: Profile identifier
|
|
20
|
+
description: Human-readable description
|
|
21
|
+
sections: List of section configurations
|
|
22
|
+
predicate_order: Optional predicate ordering configuration
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, name: str, config: dict[str, Any]):
|
|
26
|
+
"""Initialize a profile from configuration.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
name: Profile identifier
|
|
30
|
+
config: Profile configuration dictionary from YAML
|
|
31
|
+
"""
|
|
32
|
+
self.name = name
|
|
33
|
+
self.description = config.get("description", "")
|
|
34
|
+
self.sections = config.get("sections", [])
|
|
35
|
+
self.predicate_order = PredicateOrderConfig.from_dict(
|
|
36
|
+
config.get("predicate_order")
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __repr__(self) -> str:
|
|
40
|
+
return f"OrderingProfile(name={self.name!r}, sections={len(self.sections)})"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OrderingConfig:
|
|
44
|
+
"""Configuration for RDF ordering operations.
|
|
45
|
+
|
|
46
|
+
Loads and manages YAML-based ordering specifications with support
|
|
47
|
+
for multiple profiles, default settings, selectors, and shared
|
|
48
|
+
configuration via YAML anchors.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
defaults: Default settings applied across profiles
|
|
52
|
+
selectors: Named selector definitions
|
|
53
|
+
prefix_order: Preferred order for namespace prefixes
|
|
54
|
+
profiles: Dictionary of available ordering profiles
|
|
55
|
+
predicate_order: Default predicate ordering (can be overridden per profile)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, yaml_path: Path | str):
|
|
59
|
+
"""Load ordering configuration from a YAML file.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
yaml_path: Path to YAML configuration file
|
|
63
|
+
"""
|
|
64
|
+
yaml_path = Path(yaml_path)
|
|
65
|
+
self.config = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
66
|
+
|
|
67
|
+
self.defaults = self.config.get("defaults", {}) or {}
|
|
68
|
+
self.selectors = self.config.get("selectors", {}) or {}
|
|
69
|
+
self.prefix_order = self.config.get("prefix_order", []) or []
|
|
70
|
+
|
|
71
|
+
# Load default predicate ordering (can be overridden per profile)
|
|
72
|
+
self.predicate_order = PredicateOrderConfig.from_dict(
|
|
73
|
+
self.config.get("predicate_order")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Load profiles
|
|
77
|
+
self.profiles = {}
|
|
78
|
+
for prof_name, prof_config in (self.config.get("profiles", {}) or {}).items():
|
|
79
|
+
self.profiles[prof_name] = OrderingProfile(prof_name, prof_config)
|
|
80
|
+
|
|
81
|
+
def get_profile(self, name: str) -> OrderingProfile:
|
|
82
|
+
"""Get a profile by name.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
name: Profile identifier
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
OrderingProfile instance
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
KeyError: If profile name not found
|
|
92
|
+
"""
|
|
93
|
+
if name not in self.profiles:
|
|
94
|
+
raise KeyError(
|
|
95
|
+
f"Profile '{name}' not found. Available profiles: "
|
|
96
|
+
f"{', '.join(self.profiles.keys())}"
|
|
97
|
+
)
|
|
98
|
+
return self.profiles[name]
|
|
99
|
+
|
|
100
|
+
def get_predicate_order(self, profile_name: str) -> PredicateOrderConfig | None:
|
|
101
|
+
"""Get the effective predicate ordering for a profile.
|
|
102
|
+
|
|
103
|
+
Profile-level predicate_order takes precedence over config-level.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
profile_name: Profile identifier
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
PredicateOrderConfig or None if no ordering configured
|
|
110
|
+
"""
|
|
111
|
+
profile = self.get_profile(profile_name)
|
|
112
|
+
|
|
113
|
+
# Profile-level overrides config-level
|
|
114
|
+
if profile.predicate_order.classes.first or profile.predicate_order.classes.last:
|
|
115
|
+
return profile.predicate_order
|
|
116
|
+
if profile.predicate_order.properties.first or profile.predicate_order.properties.last:
|
|
117
|
+
return profile.predicate_order
|
|
118
|
+
if profile.predicate_order.individuals.first or profile.predicate_order.individuals.last:
|
|
119
|
+
return profile.predicate_order
|
|
120
|
+
if profile.predicate_order.default.first or profile.predicate_order.default.last:
|
|
121
|
+
return profile.predicate_order
|
|
122
|
+
|
|
123
|
+
# Fall back to config-level
|
|
124
|
+
if self.predicate_order.classes.first or self.predicate_order.classes.last:
|
|
125
|
+
return self.predicate_order
|
|
126
|
+
if self.predicate_order.properties.first or self.predicate_order.properties.last:
|
|
127
|
+
return self.predicate_order
|
|
128
|
+
if self.predicate_order.individuals.first or self.predicate_order.individuals.last:
|
|
129
|
+
return self.predicate_order
|
|
130
|
+
if self.predicate_order.default.first or self.predicate_order.default.last:
|
|
131
|
+
return self.predicate_order
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def list_profiles(self) -> list[str]:
|
|
136
|
+
"""Get list of available profile names.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of profile identifier strings
|
|
140
|
+
"""
|
|
141
|
+
return list(self.profiles.keys())
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
return f"OrderingConfig(profiles={list(self.profiles.keys())})"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_yaml(path: Path | str) -> dict[str, Any]:
|
|
148
|
+
"""Load YAML file with UTF-8 encoding.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
path: Path to YAML file
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Parsed YAML content as dictionary
|
|
155
|
+
"""
|
|
156
|
+
path = Path(path)
|
|
157
|
+
return yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Subject selection logic for RDF graphs."""
|
|
2
|
+
|
|
3
|
+
from rdflib import Graph, RDF, RDFS
|
|
4
|
+
from rdflib.namespace import OWL
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def select_subjects(
|
|
8
|
+
graph: Graph, selector_key: str, selectors: dict[str, str]
|
|
9
|
+
) -> set:
|
|
10
|
+
"""Select subjects from a graph based on selector criteria.
|
|
11
|
+
|
|
12
|
+
Supports several selector shorthands:
|
|
13
|
+
- classes: owl:Class and rdfs:Class entities
|
|
14
|
+
- obj_props: owl:ObjectProperty entities
|
|
15
|
+
- data_props: owl:DatatypeProperty entities
|
|
16
|
+
- ann_props: owl:AnnotationProperty entities
|
|
17
|
+
- individuals: All subjects that aren't classes or properties
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
graph: RDF graph to select from
|
|
21
|
+
selector_key: Key identifying the selection type
|
|
22
|
+
selectors: Dictionary of selector definitions
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Set of URIRefs matching the selection criteria
|
|
26
|
+
"""
|
|
27
|
+
sel = selectors.get(selector_key, "").strip()
|
|
28
|
+
subjects: set = set()
|
|
29
|
+
|
|
30
|
+
# Classes - check both owl:Class and rdfs:Class
|
|
31
|
+
if sel in ("owl:Class", "rdf:type owl:Class") or selector_key == "classes":
|
|
32
|
+
subjects = {s for s in graph.subjects(RDF.type, OWL.Class)}
|
|
33
|
+
subjects |= {s for s in graph.subjects(RDF.type, RDFS.Class)}
|
|
34
|
+
|
|
35
|
+
# Object properties
|
|
36
|
+
elif sel in ("owl:ObjectProperty",) or selector_key == "obj_props":
|
|
37
|
+
subjects = {s for s in graph.subjects(RDF.type, OWL.ObjectProperty)}
|
|
38
|
+
|
|
39
|
+
# Datatype properties
|
|
40
|
+
elif sel in ("owl:DatatypeProperty",) or selector_key == "data_props":
|
|
41
|
+
subjects = {s for s in graph.subjects(RDF.type, OWL.DatatypeProperty)}
|
|
42
|
+
|
|
43
|
+
# Annotation properties
|
|
44
|
+
elif sel in ("owl:AnnotationProperty",) or selector_key == "ann_props":
|
|
45
|
+
subjects = {s for s in graph.subjects(RDF.type, OWL.AnnotationProperty)}
|
|
46
|
+
|
|
47
|
+
# Individuals - everything that's not a class or property
|
|
48
|
+
elif selector_key == "individuals" or sel.startswith("FILTER"):
|
|
49
|
+
classes = {s for s in graph.subjects(RDF.type, OWL.Class)}
|
|
50
|
+
classes |= {s for s in graph.subjects(RDF.type, RDFS.Class)}
|
|
51
|
+
|
|
52
|
+
properties = set()
|
|
53
|
+
for prop_type in (
|
|
54
|
+
RDF.Property,
|
|
55
|
+
OWL.ObjectProperty,
|
|
56
|
+
OWL.DatatypeProperty,
|
|
57
|
+
OWL.AnnotationProperty,
|
|
58
|
+
):
|
|
59
|
+
properties |= {s for s in graph.subjects(RDF.type, prop_type)}
|
|
60
|
+
|
|
61
|
+
all_subjects = {s for (s, _, _) in graph}
|
|
62
|
+
subjects = all_subjects - classes - properties
|
|
63
|
+
|
|
64
|
+
return subjects
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Custom RDF serialisers that preserve semantic ordering.
|
|
2
|
+
|
|
3
|
+
RDFlib's built-in serialisers always sort subjects alphabetically,
|
|
4
|
+
which defeats semantic ordering. These custom serialisers respect
|
|
5
|
+
the exact subject order provided.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rdflib import Graph, URIRef, Literal, RDF
|
|
11
|
+
from rdflib.namespace import RDFS, OWL, XSD
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from .predicate_order import (
|
|
15
|
+
PredicateOrderConfig,
|
|
16
|
+
PredicateOrderSpec,
|
|
17
|
+
classify_subject,
|
|
18
|
+
order_predicates,
|
|
19
|
+
)
|
|
20
|
+
except ImportError:
|
|
21
|
+
from predicate_order import (
|
|
22
|
+
PredicateOrderConfig,
|
|
23
|
+
PredicateOrderSpec,
|
|
24
|
+
classify_subject,
|
|
25
|
+
order_predicates,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_term(graph: Graph, term, use_prefixes: bool = True) -> str:
|
|
30
|
+
"""Format an RDF term as a Turtle string.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
graph: RDF graph containing namespace bindings
|
|
34
|
+
term: RDF term to format (URIRef, Literal, or other)
|
|
35
|
+
use_prefixes: Whether to use prefix notation for URIs
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Turtle-formatted string representation of the term
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(term, URIRef):
|
|
41
|
+
if use_prefixes:
|
|
42
|
+
try:
|
|
43
|
+
qname = graph.namespace_manager.normalizeUri(term)
|
|
44
|
+
return qname
|
|
45
|
+
except Exception:
|
|
46
|
+
return f"<{term}>"
|
|
47
|
+
return f"<{term}>"
|
|
48
|
+
|
|
49
|
+
elif isinstance(term, Literal):
|
|
50
|
+
value = str(term)
|
|
51
|
+
# Escape special characters
|
|
52
|
+
value = (
|
|
53
|
+
value.replace("\\", "\\\\")
|
|
54
|
+
.replace('"', '\\"')
|
|
55
|
+
.replace("\n", "\\n")
|
|
56
|
+
.replace("\r", "\\r")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if term.datatype:
|
|
60
|
+
dt = format_term(graph, term.datatype, use_prefixes=True)
|
|
61
|
+
return f'"{value}"^^{dt}'
|
|
62
|
+
elif term.language:
|
|
63
|
+
return f'"{value}"@{term.language}'
|
|
64
|
+
else:
|
|
65
|
+
return f'"{value}"'
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
return str(term)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def serialise_turtle(
|
|
72
|
+
graph: Graph,
|
|
73
|
+
subjects_ordered: list,
|
|
74
|
+
output_path: Path | str,
|
|
75
|
+
predicate_order: PredicateOrderConfig | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Serialise RDF graph to Turtle format with preserved subject ordering.
|
|
78
|
+
|
|
79
|
+
This custom serialiser respects the exact order of subjects provided,
|
|
80
|
+
unlike rdflib's built-in serialisers which always sort alphabetically.
|
|
81
|
+
|
|
82
|
+
Formatting features:
|
|
83
|
+
- Prefixes sorted alphabetically at top
|
|
84
|
+
- Subjects in specified order
|
|
85
|
+
- rdf:type predicate listed first for each subject
|
|
86
|
+
- Predicates ordered according to predicate_order config (or alphabetically)
|
|
87
|
+
- Objects sorted alphabetically within each predicate
|
|
88
|
+
- Proper indentation and punctuation
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
graph: RDF graph to serialise
|
|
92
|
+
subjects_ordered: List of subjects in desired output order
|
|
93
|
+
output_path: Path to write Turtle file to
|
|
94
|
+
predicate_order: Optional predicate ordering configuration
|
|
95
|
+
"""
|
|
96
|
+
lines = []
|
|
97
|
+
|
|
98
|
+
# Write prefixes
|
|
99
|
+
prefixes = sorted(graph.namespace_manager.namespaces(), key=lambda x: x[0])
|
|
100
|
+
for prefix, namespace in prefixes:
|
|
101
|
+
if prefix: # Skip the default namespace
|
|
102
|
+
lines.append(f"PREFIX {prefix}: <{namespace}>")
|
|
103
|
+
lines.append("") # Blank line after prefixes
|
|
104
|
+
|
|
105
|
+
# Write subjects in order
|
|
106
|
+
for subject in subjects_ordered:
|
|
107
|
+
preds = list(graph.predicate_objects(subject))
|
|
108
|
+
if not preds:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Write subject
|
|
112
|
+
subj_str = format_term(graph, subject)
|
|
113
|
+
lines.append(f"{subj_str}")
|
|
114
|
+
|
|
115
|
+
# Group predicates
|
|
116
|
+
pred_dict: dict[URIRef, list] = {}
|
|
117
|
+
for p, o in preds:
|
|
118
|
+
if p not in pred_dict:
|
|
119
|
+
pred_dict[p] = []
|
|
120
|
+
pred_dict[p].append(o)
|
|
121
|
+
|
|
122
|
+
# Order predicates
|
|
123
|
+
sorted_preds = _order_subject_predicates(
|
|
124
|
+
graph, subject, pred_dict, predicate_order
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Write predicate-object pairs
|
|
128
|
+
for i, (pred, objects) in enumerate(sorted_preds):
|
|
129
|
+
# Use 'a' shorthand for rdf:type
|
|
130
|
+
pred_str = "a" if pred == RDF.type else format_term(graph, pred)
|
|
131
|
+
objects_sorted = sorted(objects, key=lambda x: format_term(graph, x))
|
|
132
|
+
|
|
133
|
+
if len(objects_sorted) == 1:
|
|
134
|
+
obj_str = format_term(graph, objects_sorted[0])
|
|
135
|
+
if i == len(sorted_preds) - 1:
|
|
136
|
+
lines.append(f" {pred_str} {obj_str} .")
|
|
137
|
+
else:
|
|
138
|
+
lines.append(f" {pred_str} {obj_str} ;")
|
|
139
|
+
else:
|
|
140
|
+
# Multiple objects for same predicate
|
|
141
|
+
lines.append(f" {pred_str}")
|
|
142
|
+
for j, obj in enumerate(objects_sorted):
|
|
143
|
+
obj_str = format_term(graph, obj)
|
|
144
|
+
if j == len(objects_sorted) - 1:
|
|
145
|
+
if i == len(sorted_preds) - 1:
|
|
146
|
+
lines.append(f" {obj_str} .")
|
|
147
|
+
else:
|
|
148
|
+
lines.append(f" {obj_str} ;")
|
|
149
|
+
else:
|
|
150
|
+
lines.append(f" {obj_str} ,")
|
|
151
|
+
|
|
152
|
+
lines.append("") # Blank line after each subject
|
|
153
|
+
|
|
154
|
+
# Write to file
|
|
155
|
+
output_path = Path(output_path)
|
|
156
|
+
output_path.write_text("\n".join(lines), encoding="utf-8")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _order_subject_predicates(
|
|
160
|
+
graph: Graph,
|
|
161
|
+
subject: URIRef,
|
|
162
|
+
pred_dict: dict[URIRef, list],
|
|
163
|
+
predicate_order: PredicateOrderConfig | None,
|
|
164
|
+
) -> list[tuple[URIRef, list]]:
|
|
165
|
+
"""Order predicates for a subject according to configuration.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
graph: RDF graph
|
|
169
|
+
subject: The subject being serialised
|
|
170
|
+
pred_dict: Dictionary of predicate -> objects
|
|
171
|
+
predicate_order: Predicate ordering configuration
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of (predicate, objects) tuples in desired order
|
|
175
|
+
"""
|
|
176
|
+
sorted_preds = []
|
|
177
|
+
|
|
178
|
+
# rdf:type always first
|
|
179
|
+
if RDF.type in pred_dict:
|
|
180
|
+
sorted_preds.append((RDF.type, pred_dict[RDF.type]))
|
|
181
|
+
|
|
182
|
+
# Get remaining predicates (excluding rdf:type)
|
|
183
|
+
remaining = [p for p in pred_dict.keys() if p != RDF.type]
|
|
184
|
+
|
|
185
|
+
if predicate_order:
|
|
186
|
+
# Use configured ordering
|
|
187
|
+
subject_type = classify_subject(graph, subject)
|
|
188
|
+
spec = predicate_order.get_spec_for_type(subject_type)
|
|
189
|
+
|
|
190
|
+
ordered = order_predicates(
|
|
191
|
+
graph,
|
|
192
|
+
remaining,
|
|
193
|
+
spec,
|
|
194
|
+
lambda x: format_term(graph, x),
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
# Default: alphabetical ordering
|
|
198
|
+
ordered = sorted(remaining, key=lambda x: format_term(graph, x))
|
|
199
|
+
|
|
200
|
+
# Add ordered predicates
|
|
201
|
+
for pred in ordered:
|
|
202
|
+
sorted_preds.append((pred, pred_dict[pred]))
|
|
203
|
+
|
|
204
|
+
return sorted_preds
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def build_section_graph(base: Graph, subjects_ordered: list) -> Graph:
|
|
208
|
+
"""Build a new graph containing only the specified subjects and their triples.
|
|
209
|
+
|
|
210
|
+
Creates a filtered view of the base graph that includes all triples
|
|
211
|
+
where the subject is in the provided list. Preserves all namespace
|
|
212
|
+
bindings from the base graph.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
base: Source RDF graph to filter
|
|
216
|
+
subjects_ordered: List of subjects to include
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
New graph containing only triples for the specified subjects
|
|
220
|
+
"""
|
|
221
|
+
sg = Graph()
|
|
222
|
+
|
|
223
|
+
# Copy namespace bindings
|
|
224
|
+
for pfx, uri in base.namespace_manager.namespaces():
|
|
225
|
+
sg.namespace_manager.bind(pfx, uri, override=True, replace=True)
|
|
226
|
+
|
|
227
|
+
# Copy triples for each subject
|
|
228
|
+
for s in subjects_ordered:
|
|
229
|
+
for p, o in base.predicate_objects(s):
|
|
230
|
+
sg.add((s, p, o))
|
|
231
|
+
|
|
232
|
+
return sg
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Utilities for handling prefixes, CURIEs, and namespace operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rdflib import Graph, Namespace, URIRef
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def extract_prefix_map(graph: Graph) -> dict[str, str]:
|
|
9
|
+
"""Extract all namespace prefixes from a graph.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
graph: RDF graph to extract prefixes from
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Dictionary mapping prefix strings to namespace URIs
|
|
16
|
+
"""
|
|
17
|
+
return {pfx: str(uri) for pfx, uri in graph.namespace_manager.namespaces()}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def expand_curie(graph: Graph, curie_or_iri: str) -> Optional[URIRef]:
|
|
21
|
+
"""Expand a CURIE to a full URIRef using the graph's namespace bindings.
|
|
22
|
+
|
|
23
|
+
Handles multiple input formats:
|
|
24
|
+
- CURIE format: 'ies:Element' -> http://example.org/ies#Element
|
|
25
|
+
- Angle brackets: '<http://...>' -> http://...
|
|
26
|
+
- Full IRI: 'http://...' -> http://...
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
graph: RDF graph containing namespace bindings
|
|
30
|
+
curie_or_iri: CURIE, bracketed IRI, or full IRI string
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Expanded URIRef or None if expansion fails
|
|
34
|
+
"""
|
|
35
|
+
s = curie_or_iri.strip()
|
|
36
|
+
if not s:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
# Handle angle-bracketed IRIs
|
|
40
|
+
if s.startswith("<") and s.endswith(">"):
|
|
41
|
+
return URIRef(s[1:-1])
|
|
42
|
+
|
|
43
|
+
# Handle full IRIs
|
|
44
|
+
if "://" in s:
|
|
45
|
+
return URIRef(s)
|
|
46
|
+
|
|
47
|
+
# Handle CURIEs
|
|
48
|
+
if ":" in s:
|
|
49
|
+
pfx, local = s.split(":", 1)
|
|
50
|
+
for p, uri in graph.namespace_manager.namespaces():
|
|
51
|
+
if p == pfx:
|
|
52
|
+
return URIRef(str(uri) + local)
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def rebind_prefixes(
|
|
58
|
+
graph: Graph, ordered_prefixes: list[str], prefix_map: dict[str, str]
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Rebind prefixes in a graph according to a specified order.
|
|
61
|
+
|
|
62
|
+
This ensures deterministic prefix ordering in serialized output.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
graph: RDF graph to rebind prefixes in
|
|
66
|
+
ordered_prefixes: List of prefix strings in desired order
|
|
67
|
+
prefix_map: Dictionary mapping prefixes to namespace URIs
|
|
68
|
+
"""
|
|
69
|
+
nm = graph.namespace_manager
|
|
70
|
+
for pfx in ordered_prefixes:
|
|
71
|
+
uri = prefix_map.get(pfx)
|
|
72
|
+
if uri:
|
|
73
|
+
nm.bind(pfx, URIRef(uri), override=True, replace=True)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def qname_sort_key(graph: Graph, term) -> str:
|
|
77
|
+
"""Generate a sortable string key for an RDF term using its QName.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
graph: RDF graph containing namespace bindings
|
|
81
|
+
term: RDF term to generate key for
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Normalized URI string suitable for alphabetical sorting
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
return graph.namespace_manager.normalizeUri(term)
|
|
88
|
+
except Exception:
|
|
89
|
+
return str(term)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Competency Question (CQ) testing module.
|
|
2
|
+
|
|
3
|
+
Validates whether an ontology can answer competency questions expressed
|
|
4
|
+
as SPARQL queries with expected results.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
|
|
8
|
+
from rdf_construct.cq import run_tests
|
|
9
|
+
|
|
10
|
+
results = run_tests(
|
|
11
|
+
ontology_path=Path("ontology.ttl"),
|
|
12
|
+
test_suite_path=Path("cq-tests.yml"),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
print(f"Passed: {results.passed_count}/{results.total_count}")
|
|
16
|
+
|
|
17
|
+
Or with more control:
|
|
18
|
+
|
|
19
|
+
from rdf_construct.cq import load_test_suite, CQTestRunner
|
|
20
|
+
from rdflib import Graph
|
|
21
|
+
|
|
22
|
+
ontology = Graph()
|
|
23
|
+
ontology.parse("ontology.ttl", format="turtle")
|
|
24
|
+
|
|
25
|
+
suite = load_test_suite(Path("cq-tests.yml"))
|
|
26
|
+
suite = suite.filter_by_tags(include_tags={"core"})
|
|
27
|
+
|
|
28
|
+
runner = CQTestRunner(fail_fast=True)
|
|
29
|
+
results = runner.run(ontology, suite)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from rdf_construct.cq.loader import CQTest, CQTestSuite, load_test_suite
|
|
33
|
+
from rdf_construct.cq.runner import (
|
|
34
|
+
CQTestRunner,
|
|
35
|
+
CQTestResult,
|
|
36
|
+
CQTestResults,
|
|
37
|
+
CQStatus,
|
|
38
|
+
run_tests,
|
|
39
|
+
)
|
|
40
|
+
from rdf_construct.cq.expectations import (
|
|
41
|
+
Expectation,
|
|
42
|
+
BooleanExpectation,
|
|
43
|
+
HasResultsExpectation,
|
|
44
|
+
NoResultsExpectation,
|
|
45
|
+
CountExpectation,
|
|
46
|
+
ValuesExpectation,
|
|
47
|
+
ContainsExpectation,
|
|
48
|
+
parse_expectation,
|
|
49
|
+
)
|
|
50
|
+
from rdf_construct.cq.formatters import format_results, format_text, format_json, format_junit
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
# Loader
|
|
54
|
+
"CQTest",
|
|
55
|
+
"CQTestSuite",
|
|
56
|
+
"load_test_suite",
|
|
57
|
+
# Runner
|
|
58
|
+
"CQTestRunner",
|
|
59
|
+
"CQTestResult",
|
|
60
|
+
"CQTestResults",
|
|
61
|
+
"CQStatus",
|
|
62
|
+
"run_tests",
|
|
63
|
+
# Expectations
|
|
64
|
+
"Expectation",
|
|
65
|
+
"BooleanExpectation",
|
|
66
|
+
"HasResultsExpectation",
|
|
67
|
+
"NoResultsExpectation",
|
|
68
|
+
"CountExpectation",
|
|
69
|
+
"ValuesExpectation",
|
|
70
|
+
"ContainsExpectation",
|
|
71
|
+
"parse_expectation",
|
|
72
|
+
# Formatters
|
|
73
|
+
"format_results",
|
|
74
|
+
"format_text",
|
|
75
|
+
"format_json",
|
|
76
|
+
"format_junit",
|
|
77
|
+
]
|