rdf-construct 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdf_construct/__init__.py +12 -0
- rdf_construct/__main__.py +0 -0
- rdf_construct/cli.py +1762 -0
- rdf_construct/core/__init__.py +33 -0
- rdf_construct/core/config.py +116 -0
- rdf_construct/core/ordering.py +219 -0
- rdf_construct/core/predicate_order.py +212 -0
- rdf_construct/core/profile.py +157 -0
- rdf_construct/core/selector.py +64 -0
- rdf_construct/core/serialiser.py +232 -0
- rdf_construct/core/utils.py +89 -0
- rdf_construct/cq/__init__.py +77 -0
- rdf_construct/cq/expectations.py +365 -0
- rdf_construct/cq/formatters/__init__.py +45 -0
- rdf_construct/cq/formatters/json.py +104 -0
- rdf_construct/cq/formatters/junit.py +104 -0
- rdf_construct/cq/formatters/text.py +146 -0
- rdf_construct/cq/loader.py +300 -0
- rdf_construct/cq/runner.py +321 -0
- rdf_construct/diff/__init__.py +59 -0
- rdf_construct/diff/change_types.py +214 -0
- rdf_construct/diff/comparator.py +338 -0
- rdf_construct/diff/filters.py +133 -0
- rdf_construct/diff/formatters/__init__.py +71 -0
- rdf_construct/diff/formatters/json.py +192 -0
- rdf_construct/diff/formatters/markdown.py +210 -0
- rdf_construct/diff/formatters/text.py +195 -0
- rdf_construct/docs/__init__.py +60 -0
- rdf_construct/docs/config.py +238 -0
- rdf_construct/docs/extractors.py +603 -0
- rdf_construct/docs/generator.py +360 -0
- rdf_construct/docs/renderers/__init__.py +7 -0
- rdf_construct/docs/renderers/html.py +803 -0
- rdf_construct/docs/renderers/json.py +390 -0
- rdf_construct/docs/renderers/markdown.py +628 -0
- rdf_construct/docs/search.py +278 -0
- rdf_construct/docs/templates/html/base.html.jinja +44 -0
- rdf_construct/docs/templates/html/class.html.jinja +152 -0
- rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
- rdf_construct/docs/templates/html/index.html.jinja +110 -0
- rdf_construct/docs/templates/html/instance.html.jinja +90 -0
- rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
- rdf_construct/docs/templates/html/property.html.jinja +124 -0
- rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
- rdf_construct/lint/__init__.py +75 -0
- rdf_construct/lint/config.py +214 -0
- rdf_construct/lint/engine.py +396 -0
- rdf_construct/lint/formatters.py +327 -0
- rdf_construct/lint/rules.py +692 -0
- rdf_construct/main.py +6 -0
- rdf_construct/puml2rdf/__init__.py +103 -0
- rdf_construct/puml2rdf/config.py +230 -0
- rdf_construct/puml2rdf/converter.py +420 -0
- rdf_construct/puml2rdf/merger.py +200 -0
- rdf_construct/puml2rdf/model.py +202 -0
- rdf_construct/puml2rdf/parser.py +565 -0
- rdf_construct/puml2rdf/validators.py +451 -0
- rdf_construct/shacl/__init__.py +56 -0
- rdf_construct/shacl/config.py +166 -0
- rdf_construct/shacl/converters.py +520 -0
- rdf_construct/shacl/generator.py +364 -0
- rdf_construct/shacl/namespaces.py +93 -0
- rdf_construct/stats/__init__.py +29 -0
- rdf_construct/stats/collector.py +178 -0
- rdf_construct/stats/comparator.py +298 -0
- rdf_construct/stats/formatters/__init__.py +83 -0
- rdf_construct/stats/formatters/json.py +38 -0
- rdf_construct/stats/formatters/markdown.py +153 -0
- rdf_construct/stats/formatters/text.py +186 -0
- rdf_construct/stats/metrics/__init__.py +26 -0
- rdf_construct/stats/metrics/basic.py +147 -0
- rdf_construct/stats/metrics/complexity.py +137 -0
- rdf_construct/stats/metrics/connectivity.py +130 -0
- rdf_construct/stats/metrics/documentation.py +128 -0
- rdf_construct/stats/metrics/hierarchy.py +207 -0
- rdf_construct/stats/metrics/properties.py +88 -0
- rdf_construct/uml/__init__.py +22 -0
- rdf_construct/uml/context.py +194 -0
- rdf_construct/uml/mapper.py +371 -0
- rdf_construct/uml/odm_renderer.py +789 -0
- rdf_construct/uml/renderer.py +684 -0
- rdf_construct/uml/uml_layout.py +393 -0
- rdf_construct/uml/uml_style.py +613 -0
- rdf_construct-0.2.0.dist-info/METADATA +431 -0
- rdf_construct-0.2.0.dist-info/RECORD +88 -0
- rdf_construct-0.2.0.dist-info/WHEEL +4 -0
- rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
- rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Output formatters for lint results.
|
|
2
|
+
|
|
3
|
+
Provides text and JSON formatting for lint results, suitable for
|
|
4
|
+
terminal output and CI integration respectively.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from rdflib import Graph, URIRef
|
|
14
|
+
|
|
15
|
+
from rdf_construct.lint.engine import LintResult, LintSummary
|
|
16
|
+
from rdf_construct.lint.rules import LintIssue, Severity
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_entity_name(entity: URIRef, graph: Graph | None = None) -> str:
|
|
20
|
+
"""Format an entity URI for display.
|
|
21
|
+
|
|
22
|
+
Tries to use a prefixed form (e.g., ies:Building) if possible,
|
|
23
|
+
otherwise falls back to the local name.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
entity: The entity URI.
|
|
27
|
+
graph: Optional graph to get namespace bindings from.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Formatted entity name string.
|
|
31
|
+
"""
|
|
32
|
+
uri_str = str(entity)
|
|
33
|
+
|
|
34
|
+
# Try to get QName from graph's namespace bindings
|
|
35
|
+
if graph is not None:
|
|
36
|
+
try:
|
|
37
|
+
qname = graph.qname(entity)
|
|
38
|
+
if qname and ":" in qname:
|
|
39
|
+
return qname
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# Fall back to extracting local name
|
|
44
|
+
if "#" in uri_str:
|
|
45
|
+
return uri_str.split("#")[-1]
|
|
46
|
+
elif "/" in uri_str:
|
|
47
|
+
return uri_str.rsplit("/", 1)[-1]
|
|
48
|
+
|
|
49
|
+
return uri_str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Formatter(ABC):
|
|
53
|
+
"""Base class for output formatters."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def format_result(self, result: LintResult) -> str:
|
|
57
|
+
"""Format a single file's lint result.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
result: The lint result to format.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Formatted string output.
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def format_summary(self, summary: LintSummary) -> str:
|
|
69
|
+
"""Format a summary of multiple file results.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
summary: The lint summary to format.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Formatted string output.
|
|
76
|
+
"""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TextFormatter(Formatter):
|
|
81
|
+
"""Plain text formatter for terminal output.
|
|
82
|
+
|
|
83
|
+
Produces output like:
|
|
84
|
+
file.ttl:12: error[orphan-class]: ies:Building - Class has no superclass
|
|
85
|
+
file.ttl:18: warning[missing-label]: ies:hasFloor - Property lacks rdfs:label
|
|
86
|
+
|
|
87
|
+
Found 1 error, 1 warning, 0 info messages
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
# ANSI colour codes
|
|
91
|
+
COLOURS = {
|
|
92
|
+
Severity.ERROR: "\033[91m", # Red
|
|
93
|
+
Severity.WARNING: "\033[93m", # Yellow
|
|
94
|
+
Severity.INFO: "\033[94m", # Blue
|
|
95
|
+
"reset": "\033[0m",
|
|
96
|
+
"bold": "\033[1m",
|
|
97
|
+
"dim": "\033[2m",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def __init__(self, use_colour: bool = True, verbose: bool = False):
|
|
101
|
+
"""Initialise the text formatter.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
use_colour: Whether to use ANSI colour codes.
|
|
105
|
+
verbose: Whether to include additional details.
|
|
106
|
+
"""
|
|
107
|
+
self.use_colour = use_colour
|
|
108
|
+
self.verbose = verbose
|
|
109
|
+
|
|
110
|
+
def _colour(self, text: str, *codes: str) -> str:
|
|
111
|
+
"""Apply colour codes to text if enabled."""
|
|
112
|
+
if not self.use_colour:
|
|
113
|
+
return text
|
|
114
|
+
prefix = "".join(codes)
|
|
115
|
+
return f"{prefix}{text}{self.COLOURS['reset']}"
|
|
116
|
+
|
|
117
|
+
def _severity_colour(self, severity: Severity) -> str:
|
|
118
|
+
"""Get colour code for a severity level."""
|
|
119
|
+
return self.COLOURS.get(severity, "")
|
|
120
|
+
|
|
121
|
+
def format_issue(self, issue: LintIssue, file_path: Path, graph: Graph | None = None) -> str:
|
|
122
|
+
"""Format a single issue.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
issue: The issue to format.
|
|
126
|
+
file_path: Path to the file containing the issue.
|
|
127
|
+
graph: Optional graph for namespace resolution.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Formatted issue line.
|
|
131
|
+
"""
|
|
132
|
+
parts = []
|
|
133
|
+
|
|
134
|
+
# File location with line number
|
|
135
|
+
location = str(file_path)
|
|
136
|
+
if issue.line:
|
|
137
|
+
location = f"{location}:{issue.line}"
|
|
138
|
+
parts.append(self._colour(location, self.COLOURS["dim"]))
|
|
139
|
+
|
|
140
|
+
# Severity and rule
|
|
141
|
+
severity_str = issue.severity.value
|
|
142
|
+
rule_part = f"{severity_str}[{issue.rule_id}]"
|
|
143
|
+
parts.append(
|
|
144
|
+
self._colour(rule_part, self.COLOURS["bold"], self._severity_colour(issue.severity))
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Entity (if present) - use formatted name with namespace
|
|
148
|
+
if issue.entity:
|
|
149
|
+
entity_name = format_entity_name(issue.entity, graph)
|
|
150
|
+
parts.append(f"{entity_name}:")
|
|
151
|
+
|
|
152
|
+
# Message
|
|
153
|
+
parts.append(issue.message)
|
|
154
|
+
|
|
155
|
+
return " ".join(parts)
|
|
156
|
+
|
|
157
|
+
def format_result(self, result: LintResult) -> str:
|
|
158
|
+
"""Format a single file's lint result."""
|
|
159
|
+
if not result.issues:
|
|
160
|
+
return ""
|
|
161
|
+
|
|
162
|
+
lines = []
|
|
163
|
+
# Sort by line number (issues without lines go last)
|
|
164
|
+
sorted_issues = sorted(
|
|
165
|
+
result.issues,
|
|
166
|
+
key=lambda i: (i.line or float('inf'), i.severity)
|
|
167
|
+
)
|
|
168
|
+
for issue in sorted_issues:
|
|
169
|
+
lines.append(self.format_issue(issue, result.file_path, result.graph))
|
|
170
|
+
|
|
171
|
+
return "\n".join(lines)
|
|
172
|
+
|
|
173
|
+
def format_summary(self, summary: LintSummary) -> str:
|
|
174
|
+
"""Format a summary of multiple file results."""
|
|
175
|
+
lines = []
|
|
176
|
+
|
|
177
|
+
# Individual file results
|
|
178
|
+
for result in summary.results:
|
|
179
|
+
result_text = self.format_result(result)
|
|
180
|
+
if result_text:
|
|
181
|
+
lines.append(result_text)
|
|
182
|
+
lines.append("") # Blank line between files
|
|
183
|
+
|
|
184
|
+
# Summary line
|
|
185
|
+
summary_parts = []
|
|
186
|
+
|
|
187
|
+
if summary.total_errors > 0:
|
|
188
|
+
err_text = f"{summary.total_errors} error{'s' if summary.total_errors != 1 else ''}"
|
|
189
|
+
summary_parts.append(self._colour(err_text, self._severity_colour(Severity.ERROR)))
|
|
190
|
+
else:
|
|
191
|
+
summary_parts.append("0 errors")
|
|
192
|
+
|
|
193
|
+
if summary.total_warnings > 0:
|
|
194
|
+
warn_text = f"{summary.total_warnings} warning{'s' if summary.total_warnings != 1 else ''}"
|
|
195
|
+
summary_parts.append(self._colour(warn_text, self._severity_colour(Severity.WARNING)))
|
|
196
|
+
else:
|
|
197
|
+
summary_parts.append("0 warnings")
|
|
198
|
+
|
|
199
|
+
if summary.total_info > 0:
|
|
200
|
+
info_text = f"{summary.total_info} info"
|
|
201
|
+
summary_parts.append(self._colour(info_text, self._severity_colour(Severity.INFO)))
|
|
202
|
+
else:
|
|
203
|
+
summary_parts.append("0 info")
|
|
204
|
+
|
|
205
|
+
files_str = f"{len(summary.results)} file{'s' if len(summary.results) != 1 else ''}"
|
|
206
|
+
|
|
207
|
+
summary_line = f"Found {', '.join(summary_parts)} in {files_str}"
|
|
208
|
+
lines.append(summary_line)
|
|
209
|
+
|
|
210
|
+
return "\n".join(lines)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class JsonFormatter(Formatter):
|
|
214
|
+
"""JSON formatter for machine-readable output.
|
|
215
|
+
|
|
216
|
+
Produces output like:
|
|
217
|
+
{
|
|
218
|
+
"files": [
|
|
219
|
+
{
|
|
220
|
+
"path": "file.ttl",
|
|
221
|
+
"issues": [
|
|
222
|
+
{
|
|
223
|
+
"rule": "orphan-class",
|
|
224
|
+
"severity": "error",
|
|
225
|
+
"entity": "http://example.org/Building",
|
|
226
|
+
"entity_name": "ies:Building",
|
|
227
|
+
"message": "Class has no superclass",
|
|
228
|
+
"line": 12
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
"summary": {"errors": 1, "warnings": 0, "info": 0}
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"summary": {"errors": 1, "warnings": 0, "info": 0, "files": 1}
|
|
235
|
+
}
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def __init__(self, pretty: bool = True):
|
|
239
|
+
"""Initialise the JSON formatter.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
pretty: Whether to pretty-print the JSON.
|
|
243
|
+
"""
|
|
244
|
+
self.pretty = pretty
|
|
245
|
+
|
|
246
|
+
def _issue_to_dict(self, issue: LintIssue, graph: Graph | None = None) -> dict:
|
|
247
|
+
"""Convert an issue to a dictionary."""
|
|
248
|
+
result = {
|
|
249
|
+
"rule": issue.rule_id,
|
|
250
|
+
"severity": issue.severity.value,
|
|
251
|
+
"entity": str(issue.entity) if issue.entity else None,
|
|
252
|
+
"message": issue.message,
|
|
253
|
+
"line": issue.line,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Add formatted entity name if available
|
|
257
|
+
if issue.entity:
|
|
258
|
+
result["entity_name"] = format_entity_name(issue.entity, graph)
|
|
259
|
+
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
def format_result(self, result: LintResult) -> str:
|
|
263
|
+
"""Format a single file's lint result as JSON."""
|
|
264
|
+
data = {
|
|
265
|
+
"path": str(result.file_path),
|
|
266
|
+
"issues": [self._issue_to_dict(i, result.graph) for i in result.issues],
|
|
267
|
+
"summary": {
|
|
268
|
+
"errors": result.error_count,
|
|
269
|
+
"warnings": result.warning_count,
|
|
270
|
+
"info": result.info_count,
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if self.pretty:
|
|
275
|
+
return json.dumps(data, indent=2)
|
|
276
|
+
return json.dumps(data)
|
|
277
|
+
|
|
278
|
+
def format_summary(self, summary: LintSummary) -> str:
|
|
279
|
+
"""Format a summary of multiple file results as JSON."""
|
|
280
|
+
data = {
|
|
281
|
+
"files": [
|
|
282
|
+
{
|
|
283
|
+
"path": str(r.file_path),
|
|
284
|
+
"issues": [self._issue_to_dict(i, r.graph) for i in r.issues],
|
|
285
|
+
"summary": {
|
|
286
|
+
"errors": r.error_count,
|
|
287
|
+
"warnings": r.warning_count,
|
|
288
|
+
"info": r.info_count,
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
for r in summary.results
|
|
292
|
+
],
|
|
293
|
+
"summary": {
|
|
294
|
+
"errors": summary.total_errors,
|
|
295
|
+
"warnings": summary.total_warnings,
|
|
296
|
+
"info": summary.total_info,
|
|
297
|
+
"files": len(summary.results),
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if self.pretty:
|
|
302
|
+
return json.dumps(data, indent=2)
|
|
303
|
+
return json.dumps(data)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_formatter(format_name: str, **kwargs) -> Formatter:
|
|
307
|
+
"""Get a formatter by name.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
format_name: 'text' or 'json'.
|
|
311
|
+
**kwargs: Additional arguments passed to the formatter.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Formatter instance.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ValueError: If format name is unknown.
|
|
318
|
+
"""
|
|
319
|
+
formatters = {
|
|
320
|
+
"text": TextFormatter,
|
|
321
|
+
"json": JsonFormatter,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if format_name not in formatters:
|
|
325
|
+
raise ValueError(f"Unknown format '{format_name}'. Available: {', '.join(formatters.keys())}")
|
|
326
|
+
|
|
327
|
+
return formatters[format_name](**kwargs)
|