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,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)