rdf-construct 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +1762 -0
  4. rdf_construct/core/__init__.py +33 -0
  5. rdf_construct/core/config.py +116 -0
  6. rdf_construct/core/ordering.py +219 -0
  7. rdf_construct/core/predicate_order.py +212 -0
  8. rdf_construct/core/profile.py +157 -0
  9. rdf_construct/core/selector.py +64 -0
  10. rdf_construct/core/serialiser.py +232 -0
  11. rdf_construct/core/utils.py +89 -0
  12. rdf_construct/cq/__init__.py +77 -0
  13. rdf_construct/cq/expectations.py +365 -0
  14. rdf_construct/cq/formatters/__init__.py +45 -0
  15. rdf_construct/cq/formatters/json.py +104 -0
  16. rdf_construct/cq/formatters/junit.py +104 -0
  17. rdf_construct/cq/formatters/text.py +146 -0
  18. rdf_construct/cq/loader.py +300 -0
  19. rdf_construct/cq/runner.py +321 -0
  20. rdf_construct/diff/__init__.py +59 -0
  21. rdf_construct/diff/change_types.py +214 -0
  22. rdf_construct/diff/comparator.py +338 -0
  23. rdf_construct/diff/filters.py +133 -0
  24. rdf_construct/diff/formatters/__init__.py +71 -0
  25. rdf_construct/diff/formatters/json.py +192 -0
  26. rdf_construct/diff/formatters/markdown.py +210 -0
  27. rdf_construct/diff/formatters/text.py +195 -0
  28. rdf_construct/docs/__init__.py +60 -0
  29. rdf_construct/docs/config.py +238 -0
  30. rdf_construct/docs/extractors.py +603 -0
  31. rdf_construct/docs/generator.py +360 -0
  32. rdf_construct/docs/renderers/__init__.py +7 -0
  33. rdf_construct/docs/renderers/html.py +803 -0
  34. rdf_construct/docs/renderers/json.py +390 -0
  35. rdf_construct/docs/renderers/markdown.py +628 -0
  36. rdf_construct/docs/search.py +278 -0
  37. rdf_construct/docs/templates/html/base.html.jinja +44 -0
  38. rdf_construct/docs/templates/html/class.html.jinja +152 -0
  39. rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
  40. rdf_construct/docs/templates/html/index.html.jinja +110 -0
  41. rdf_construct/docs/templates/html/instance.html.jinja +90 -0
  42. rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
  43. rdf_construct/docs/templates/html/property.html.jinja +124 -0
  44. rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
  45. rdf_construct/lint/__init__.py +75 -0
  46. rdf_construct/lint/config.py +214 -0
  47. rdf_construct/lint/engine.py +396 -0
  48. rdf_construct/lint/formatters.py +327 -0
  49. rdf_construct/lint/rules.py +692 -0
  50. rdf_construct/main.py +6 -0
  51. rdf_construct/puml2rdf/__init__.py +103 -0
  52. rdf_construct/puml2rdf/config.py +230 -0
  53. rdf_construct/puml2rdf/converter.py +420 -0
  54. rdf_construct/puml2rdf/merger.py +200 -0
  55. rdf_construct/puml2rdf/model.py +202 -0
  56. rdf_construct/puml2rdf/parser.py +565 -0
  57. rdf_construct/puml2rdf/validators.py +451 -0
  58. rdf_construct/shacl/__init__.py +56 -0
  59. rdf_construct/shacl/config.py +166 -0
  60. rdf_construct/shacl/converters.py +520 -0
  61. rdf_construct/shacl/generator.py +364 -0
  62. rdf_construct/shacl/namespaces.py +93 -0
  63. rdf_construct/stats/__init__.py +29 -0
  64. rdf_construct/stats/collector.py +178 -0
  65. rdf_construct/stats/comparator.py +298 -0
  66. rdf_construct/stats/formatters/__init__.py +83 -0
  67. rdf_construct/stats/formatters/json.py +38 -0
  68. rdf_construct/stats/formatters/markdown.py +153 -0
  69. rdf_construct/stats/formatters/text.py +186 -0
  70. rdf_construct/stats/metrics/__init__.py +26 -0
  71. rdf_construct/stats/metrics/basic.py +147 -0
  72. rdf_construct/stats/metrics/complexity.py +137 -0
  73. rdf_construct/stats/metrics/connectivity.py +130 -0
  74. rdf_construct/stats/metrics/documentation.py +128 -0
  75. rdf_construct/stats/metrics/hierarchy.py +207 -0
  76. rdf_construct/stats/metrics/properties.py +88 -0
  77. rdf_construct/uml/__init__.py +22 -0
  78. rdf_construct/uml/context.py +194 -0
  79. rdf_construct/uml/mapper.py +371 -0
  80. rdf_construct/uml/odm_renderer.py +789 -0
  81. rdf_construct/uml/renderer.py +684 -0
  82. rdf_construct/uml/uml_layout.py +393 -0
  83. rdf_construct/uml/uml_style.py +613 -0
  84. rdf_construct-0.2.0.dist-info/METADATA +431 -0
  85. rdf_construct-0.2.0.dist-info/RECORD +88 -0
  86. rdf_construct-0.2.0.dist-info/WHEEL +4 -0
  87. rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
  88. rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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
+ ]