haoline 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.
- haoline/.streamlit/config.toml +10 -0
- haoline/__init__.py +248 -0
- haoline/analyzer.py +935 -0
- haoline/cli.py +2712 -0
- haoline/compare.py +811 -0
- haoline/compare_visualizations.py +1564 -0
- haoline/edge_analysis.py +525 -0
- haoline/eval/__init__.py +131 -0
- haoline/eval/adapters.py +844 -0
- haoline/eval/cli.py +390 -0
- haoline/eval/comparison.py +542 -0
- haoline/eval/deployment.py +633 -0
- haoline/eval/schemas.py +833 -0
- haoline/examples/__init__.py +15 -0
- haoline/examples/basic_inspection.py +74 -0
- haoline/examples/compare_models.py +117 -0
- haoline/examples/hardware_estimation.py +78 -0
- haoline/format_adapters.py +1001 -0
- haoline/formats/__init__.py +123 -0
- haoline/formats/coreml.py +250 -0
- haoline/formats/gguf.py +483 -0
- haoline/formats/openvino.py +255 -0
- haoline/formats/safetensors.py +273 -0
- haoline/formats/tflite.py +369 -0
- haoline/hardware.py +2307 -0
- haoline/hierarchical_graph.py +462 -0
- haoline/html_export.py +1573 -0
- haoline/layer_summary.py +769 -0
- haoline/llm_summarizer.py +465 -0
- haoline/op_icons.py +618 -0
- haoline/operational_profiling.py +1492 -0
- haoline/patterns.py +1116 -0
- haoline/pdf_generator.py +265 -0
- haoline/privacy.py +250 -0
- haoline/pydantic_models.py +241 -0
- haoline/report.py +1923 -0
- haoline/report_sections.py +539 -0
- haoline/risks.py +521 -0
- haoline/schema.py +523 -0
- haoline/streamlit_app.py +2024 -0
- haoline/tests/__init__.py +4 -0
- haoline/tests/conftest.py +123 -0
- haoline/tests/test_analyzer.py +868 -0
- haoline/tests/test_compare_visualizations.py +293 -0
- haoline/tests/test_edge_analysis.py +243 -0
- haoline/tests/test_eval.py +604 -0
- haoline/tests/test_format_adapters.py +460 -0
- haoline/tests/test_hardware.py +237 -0
- haoline/tests/test_hardware_recommender.py +90 -0
- haoline/tests/test_hierarchical_graph.py +326 -0
- haoline/tests/test_html_export.py +180 -0
- haoline/tests/test_layer_summary.py +428 -0
- haoline/tests/test_llm_patterns.py +540 -0
- haoline/tests/test_llm_summarizer.py +339 -0
- haoline/tests/test_patterns.py +774 -0
- haoline/tests/test_pytorch.py +327 -0
- haoline/tests/test_report.py +383 -0
- haoline/tests/test_risks.py +398 -0
- haoline/tests/test_schema.py +417 -0
- haoline/tests/test_tensorflow.py +380 -0
- haoline/tests/test_visualizations.py +316 -0
- haoline/universal_ir.py +856 -0
- haoline/visualizations.py +1086 -0
- haoline/visualize_yolo.py +44 -0
- haoline/web.py +110 -0
- haoline-0.3.0.dist-info/METADATA +471 -0
- haoline-0.3.0.dist-info/RECORD +70 -0
- haoline-0.3.0.dist-info/WHEEL +4 -0
- haoline-0.3.0.dist-info/entry_points.txt +5 -0
- haoline-0.3.0.dist-info/licenses/LICENSE +22 -0
haoline/schema.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# Copyright (c) 2025 HaoLine Contributors
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
JSON Schema definition and validation for HaoLine reports.
|
|
6
|
+
|
|
7
|
+
Provides:
|
|
8
|
+
- INSPECTION_REPORT_SCHEMA: JSON Schema for InspectionReport (auto-generated from Pydantic)
|
|
9
|
+
- validate_report(): Validate a report dict against the schema using Pydantic
|
|
10
|
+
- ValidationError: Exception raised on validation failure
|
|
11
|
+
|
|
12
|
+
The Pydantic models in pydantic_models.py were auto-generated from the original
|
|
13
|
+
JSON Schema using datamodel-code-generator. Validation is now done via Pydantic
|
|
14
|
+
for better error messages and type safety.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import warnings
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# Try to import Pydantic models (preferred)
|
|
23
|
+
try:
|
|
24
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
25
|
+
|
|
26
|
+
from .pydantic_models import HaolineInspectionReport
|
|
27
|
+
|
|
28
|
+
PYDANTIC_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
PYDANTIC_AVAILABLE = False
|
|
31
|
+
PydanticValidationError = None # type: ignore
|
|
32
|
+
HaolineInspectionReport = None # type: ignore
|
|
33
|
+
|
|
34
|
+
# Fallback to jsonschema if Pydantic not available
|
|
35
|
+
try:
|
|
36
|
+
from jsonschema import Draft7Validator
|
|
37
|
+
from jsonschema import ValidationError as JsonSchemaError
|
|
38
|
+
|
|
39
|
+
JSONSCHEMA_AVAILABLE = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
JSONSCHEMA_AVAILABLE = False
|
|
42
|
+
JsonSchemaError = None # type: ignore
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ValidationError(Exception):
|
|
46
|
+
"""Raised when JSON schema validation fails."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str, errors: list[str] | None = None):
|
|
49
|
+
super().__init__(message)
|
|
50
|
+
self.errors = errors or []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# JSON Schema for InspectionReport (Draft 7)
|
|
54
|
+
INSPECTION_REPORT_SCHEMA: dict[str, Any] = {
|
|
55
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
56
|
+
"$id": "https://github.com/mdayku/HaoLine/schema/inspection-report.schema.json",
|
|
57
|
+
"title": "HaoLine Inspection Report",
|
|
58
|
+
"description": "Schema for ONNX model inspection reports generated by HaoLine",
|
|
59
|
+
"type": "object",
|
|
60
|
+
"required": ["metadata", "generated_at", "autodoc_version"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"metadata": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"description": "Model metadata extracted from ONNX proto",
|
|
65
|
+
"required": ["path", "ir_version", "producer_name", "opsets"],
|
|
66
|
+
"properties": {
|
|
67
|
+
"path": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "Path to the ONNX model file",
|
|
70
|
+
},
|
|
71
|
+
"ir_version": {
|
|
72
|
+
"type": "integer",
|
|
73
|
+
"minimum": 1,
|
|
74
|
+
"description": "ONNX IR version",
|
|
75
|
+
},
|
|
76
|
+
"producer_name": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "Name of the tool that produced the model",
|
|
79
|
+
},
|
|
80
|
+
"producer_version": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "Version of the producer tool",
|
|
83
|
+
},
|
|
84
|
+
"domain": {"type": "string", "description": "Model domain"},
|
|
85
|
+
"model_version": {
|
|
86
|
+
"type": "integer",
|
|
87
|
+
"description": "Model version number",
|
|
88
|
+
},
|
|
89
|
+
"doc_string": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Model documentation string",
|
|
92
|
+
},
|
|
93
|
+
"opsets": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"description": "Opset versions by domain",
|
|
96
|
+
"additionalProperties": {"type": "integer", "minimum": 1},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
"generated_at": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"format": "date-time",
|
|
103
|
+
"description": "ISO 8601 timestamp when report was generated",
|
|
104
|
+
},
|
|
105
|
+
"autodoc_version": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+",
|
|
108
|
+
"description": "Version of HaoLine that generated the report",
|
|
109
|
+
},
|
|
110
|
+
"graph_summary": {
|
|
111
|
+
"type": ["object", "null"],
|
|
112
|
+
"description": "Summary statistics about the ONNX graph",
|
|
113
|
+
"properties": {
|
|
114
|
+
"num_nodes": {
|
|
115
|
+
"type": "integer",
|
|
116
|
+
"minimum": 0,
|
|
117
|
+
"description": "Total number of nodes in graph",
|
|
118
|
+
},
|
|
119
|
+
"num_inputs": {
|
|
120
|
+
"type": "integer",
|
|
121
|
+
"minimum": 0,
|
|
122
|
+
"description": "Number of graph inputs",
|
|
123
|
+
},
|
|
124
|
+
"num_outputs": {
|
|
125
|
+
"type": "integer",
|
|
126
|
+
"minimum": 0,
|
|
127
|
+
"description": "Number of graph outputs",
|
|
128
|
+
},
|
|
129
|
+
"num_initializers": {
|
|
130
|
+
"type": "integer",
|
|
131
|
+
"minimum": 0,
|
|
132
|
+
"description": "Number of initializers (weights)",
|
|
133
|
+
},
|
|
134
|
+
"input_shapes": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"description": "Input tensor shapes by name",
|
|
137
|
+
"additionalProperties": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"items": {"type": ["integer", "string"]},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
"output_shapes": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"description": "Output tensor shapes by name",
|
|
145
|
+
"additionalProperties": {
|
|
146
|
+
"type": "array",
|
|
147
|
+
"items": {"type": ["integer", "string"]},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
"op_type_counts": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"description": "Count of each operator type",
|
|
153
|
+
"additionalProperties": {"type": "integer", "minimum": 0},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
"param_counts": {
|
|
158
|
+
"type": ["object", "null"],
|
|
159
|
+
"description": "Parameter count statistics",
|
|
160
|
+
"properties": {
|
|
161
|
+
"total": {
|
|
162
|
+
"type": "integer",
|
|
163
|
+
"minimum": 0,
|
|
164
|
+
"description": "Total parameter count",
|
|
165
|
+
},
|
|
166
|
+
"trainable": {
|
|
167
|
+
"type": "integer",
|
|
168
|
+
"minimum": 0,
|
|
169
|
+
"description": "Trainable parameter count",
|
|
170
|
+
},
|
|
171
|
+
"non_trainable": {
|
|
172
|
+
"type": "integer",
|
|
173
|
+
"minimum": 0,
|
|
174
|
+
"description": "Non-trainable parameter count",
|
|
175
|
+
},
|
|
176
|
+
"by_op_type": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"description": "Parameters by operator type (fractional for shared weights)",
|
|
179
|
+
"additionalProperties": {"type": "number", "minimum": 0},
|
|
180
|
+
},
|
|
181
|
+
"shared_weights": {
|
|
182
|
+
"type": "object",
|
|
183
|
+
"description": "Information about shared weights",
|
|
184
|
+
"properties": {
|
|
185
|
+
"count": {
|
|
186
|
+
"type": "integer",
|
|
187
|
+
"minimum": 0,
|
|
188
|
+
"description": "Number of weights shared across 2+ nodes",
|
|
189
|
+
},
|
|
190
|
+
"details": {
|
|
191
|
+
"type": "object",
|
|
192
|
+
"description": "Shared weight name to list of node names using it",
|
|
193
|
+
"additionalProperties": {
|
|
194
|
+
"type": "array",
|
|
195
|
+
"items": {"type": "string"},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
"precision_breakdown": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"description": "Parameter count by data type (fp32, fp16, int8, etc.)",
|
|
203
|
+
"additionalProperties": {"type": "integer", "minimum": 0},
|
|
204
|
+
},
|
|
205
|
+
"is_quantized": {
|
|
206
|
+
"type": "boolean",
|
|
207
|
+
"description": "Whether model uses quantized weights or ops",
|
|
208
|
+
},
|
|
209
|
+
"quantized_ops": {
|
|
210
|
+
"type": "array",
|
|
211
|
+
"description": "List of quantized operation types detected",
|
|
212
|
+
"items": {"type": "string"},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
"flop_counts": {
|
|
217
|
+
"type": ["object", "null"],
|
|
218
|
+
"description": "FLOP estimation statistics",
|
|
219
|
+
"properties": {
|
|
220
|
+
"total": {
|
|
221
|
+
"type": "integer",
|
|
222
|
+
"minimum": 0,
|
|
223
|
+
"description": "Total estimated FLOPs",
|
|
224
|
+
},
|
|
225
|
+
"by_node_type": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"description": "FLOPs by operator type",
|
|
228
|
+
"additionalProperties": {"type": "integer", "minimum": 0},
|
|
229
|
+
},
|
|
230
|
+
"hotspots": {
|
|
231
|
+
"type": "array",
|
|
232
|
+
"description": "Top compute-intensive nodes",
|
|
233
|
+
"items": {
|
|
234
|
+
"type": "object",
|
|
235
|
+
"properties": {
|
|
236
|
+
"name": {"type": "string"},
|
|
237
|
+
"op_type": {"type": "string"},
|
|
238
|
+
"flops": {"type": "integer", "minimum": 0},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
"memory_estimates": {
|
|
245
|
+
"type": ["object", "null"],
|
|
246
|
+
"description": "Memory usage estimates",
|
|
247
|
+
"properties": {
|
|
248
|
+
"model_size_bytes": {
|
|
249
|
+
"type": "integer",
|
|
250
|
+
"minimum": 0,
|
|
251
|
+
"description": "Model size in bytes",
|
|
252
|
+
},
|
|
253
|
+
"peak_activation_bytes": {
|
|
254
|
+
"type": "integer",
|
|
255
|
+
"minimum": 0,
|
|
256
|
+
"description": "Peak activation memory in bytes",
|
|
257
|
+
},
|
|
258
|
+
"kv_cache_bytes_per_token": {
|
|
259
|
+
"type": "integer",
|
|
260
|
+
"minimum": 0,
|
|
261
|
+
"description": "KV cache memory per token (transformers)",
|
|
262
|
+
},
|
|
263
|
+
"kv_cache_bytes_full_context": {
|
|
264
|
+
"type": "integer",
|
|
265
|
+
"minimum": 0,
|
|
266
|
+
"description": "KV cache for full context length",
|
|
267
|
+
},
|
|
268
|
+
"kv_cache_config": {
|
|
269
|
+
"type": "object",
|
|
270
|
+
"description": "KV cache configuration",
|
|
271
|
+
"properties": {
|
|
272
|
+
"num_layers": {"type": "integer", "minimum": 0},
|
|
273
|
+
"hidden_dim": {"type": "integer", "minimum": 0},
|
|
274
|
+
"seq_len": {"type": "integer", "minimum": 0},
|
|
275
|
+
"bytes_per_element": {"type": "integer", "minimum": 1},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
"breakdown": {
|
|
279
|
+
"type": ["object", "null"],
|
|
280
|
+
"description": "Memory breakdown by component",
|
|
281
|
+
"properties": {
|
|
282
|
+
"weights_by_op_type": {
|
|
283
|
+
"type": "object",
|
|
284
|
+
"additionalProperties": {"type": "integer", "minimum": 0},
|
|
285
|
+
},
|
|
286
|
+
"activations_by_op_type": {
|
|
287
|
+
"type": "object",
|
|
288
|
+
"additionalProperties": {"type": "integer", "minimum": 0},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
"detected_blocks": {
|
|
295
|
+
"type": "array",
|
|
296
|
+
"description": "Detected architectural blocks",
|
|
297
|
+
"items": {
|
|
298
|
+
"type": "object",
|
|
299
|
+
"required": ["block_type", "name"],
|
|
300
|
+
"properties": {
|
|
301
|
+
"block_type": {
|
|
302
|
+
"type": "string",
|
|
303
|
+
"description": "Type of block (e.g., ResidualAdd, Attention)",
|
|
304
|
+
},
|
|
305
|
+
"name": {"type": "string", "description": "Block identifier"},
|
|
306
|
+
"nodes": {
|
|
307
|
+
"type": "array",
|
|
308
|
+
"items": {"type": "string"},
|
|
309
|
+
"description": "Node names in this block",
|
|
310
|
+
},
|
|
311
|
+
"start_node": {"type": "string"},
|
|
312
|
+
"end_node": {"type": "string"},
|
|
313
|
+
"attributes": {
|
|
314
|
+
"type": "object",
|
|
315
|
+
"description": "Block-specific attributes",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
"architecture_type": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"enum": ["transformer", "cnn", "mlp", "hybrid", "unknown"],
|
|
323
|
+
"description": "Detected architecture type",
|
|
324
|
+
},
|
|
325
|
+
"risk_signals": {
|
|
326
|
+
"type": "array",
|
|
327
|
+
"description": "Detected risk signals",
|
|
328
|
+
"items": {
|
|
329
|
+
"type": "object",
|
|
330
|
+
"required": ["id", "severity", "description"],
|
|
331
|
+
"properties": {
|
|
332
|
+
"id": {
|
|
333
|
+
"type": "string",
|
|
334
|
+
"description": "Risk signal identifier",
|
|
335
|
+
},
|
|
336
|
+
"severity": {
|
|
337
|
+
"type": "string",
|
|
338
|
+
"enum": ["info", "warning", "high"],
|
|
339
|
+
"description": "Severity level",
|
|
340
|
+
},
|
|
341
|
+
"description": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"description": "Human-readable description",
|
|
344
|
+
},
|
|
345
|
+
"nodes": {
|
|
346
|
+
"type": "array",
|
|
347
|
+
"items": {"type": "string"},
|
|
348
|
+
"description": "Affected node names",
|
|
349
|
+
},
|
|
350
|
+
"recommendation": {
|
|
351
|
+
"type": "string",
|
|
352
|
+
"description": "Recommended action",
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
"hardware_profile": {
|
|
358
|
+
"type": ["object", "null"],
|
|
359
|
+
"description": "Target hardware profile",
|
|
360
|
+
"properties": {
|
|
361
|
+
"name": {"type": "string"},
|
|
362
|
+
"vram_bytes": {"type": "integer", "minimum": 0},
|
|
363
|
+
"peak_fp32_tflops": {"type": "number", "minimum": 0},
|
|
364
|
+
"peak_fp16_tflops": {"type": "number", "minimum": 0},
|
|
365
|
+
"memory_bandwidth_gbps": {"type": "number", "minimum": 0},
|
|
366
|
+
"tdp_watts": {"type": ["integer", "null"]},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
"hardware_estimates": {
|
|
370
|
+
"type": ["object", "null"],
|
|
371
|
+
"description": "Hardware-specific estimates",
|
|
372
|
+
"properties": {
|
|
373
|
+
"device": {"type": "string"},
|
|
374
|
+
"precision": {"type": "string"},
|
|
375
|
+
"batch_size": {"type": "integer", "minimum": 1},
|
|
376
|
+
"vram_required_bytes": {"type": "integer", "minimum": 0},
|
|
377
|
+
"fits_in_vram": {"type": "boolean"},
|
|
378
|
+
"theoretical_latency_ms": {"type": "number", "minimum": 0},
|
|
379
|
+
"bottleneck": {"type": "string"},
|
|
380
|
+
"compute_utilization_estimate": {"type": "number", "minimum": 0},
|
|
381
|
+
"gpu_saturation": {"type": "number", "minimum": 0},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
"llm_summary": {
|
|
385
|
+
"type": ["object", "null"],
|
|
386
|
+
"description": "LLM-generated summary",
|
|
387
|
+
"properties": {
|
|
388
|
+
"success": {"type": "boolean"},
|
|
389
|
+
"short_summary": {"type": "string"},
|
|
390
|
+
"detailed_summary": {"type": "string"},
|
|
391
|
+
"model": {"type": "string"},
|
|
392
|
+
"error": {"type": "string"},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
"dataset_info": {
|
|
396
|
+
"type": ["object", "null"],
|
|
397
|
+
"description": "Dataset and class information",
|
|
398
|
+
"properties": {
|
|
399
|
+
"task": {"type": ["string", "null"]},
|
|
400
|
+
"num_classes": {"type": ["integer", "null"], "minimum": 0},
|
|
401
|
+
"class_names": {
|
|
402
|
+
"type": "array",
|
|
403
|
+
"items": {"type": "string"},
|
|
404
|
+
},
|
|
405
|
+
"source": {"type": ["string", "null"]},
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def validate_report(report_dict: dict[str, Any]) -> tuple[bool, list[str]]:
|
|
413
|
+
"""
|
|
414
|
+
Validate a report dictionary against the schema.
|
|
415
|
+
|
|
416
|
+
Uses Pydantic for validation (preferred) or falls back to jsonschema.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
report_dict: The report as a dictionary (from to_dict()).
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Tuple of (is_valid, list of error messages).
|
|
423
|
+
If neither pydantic nor jsonschema is installed, returns (True, []) with a warning.
|
|
424
|
+
"""
|
|
425
|
+
# Prefer Pydantic validation
|
|
426
|
+
if PYDANTIC_AVAILABLE:
|
|
427
|
+
try:
|
|
428
|
+
HaolineInspectionReport.model_validate(report_dict)
|
|
429
|
+
return True, []
|
|
430
|
+
except PydanticValidationError as e:
|
|
431
|
+
error_messages = []
|
|
432
|
+
for error in e.errors():
|
|
433
|
+
loc = " -> ".join(str(x) for x in error["loc"])
|
|
434
|
+
error_messages.append(f"{loc}: {error['msg']}")
|
|
435
|
+
return False, error_messages
|
|
436
|
+
|
|
437
|
+
# Fallback to jsonschema
|
|
438
|
+
if JSONSCHEMA_AVAILABLE:
|
|
439
|
+
validator = Draft7Validator(INSPECTION_REPORT_SCHEMA)
|
|
440
|
+
errors = list(validator.iter_errors(report_dict))
|
|
441
|
+
|
|
442
|
+
if not errors:
|
|
443
|
+
return True, []
|
|
444
|
+
|
|
445
|
+
error_messages = []
|
|
446
|
+
for error in errors:
|
|
447
|
+
path = " -> ".join(str(p) for p in error.absolute_path) or "root"
|
|
448
|
+
error_messages.append(f"{path}: {error.message}")
|
|
449
|
+
|
|
450
|
+
return False, error_messages
|
|
451
|
+
|
|
452
|
+
# No validation library available
|
|
453
|
+
warnings.warn(
|
|
454
|
+
"Neither pydantic nor jsonschema installed. "
|
|
455
|
+
"Install with 'pip install pydantic' for validation.",
|
|
456
|
+
UserWarning,
|
|
457
|
+
stacklevel=2,
|
|
458
|
+
)
|
|
459
|
+
return True, []
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def validate_report_strict(report_dict: dict[str, Any]) -> None:
|
|
463
|
+
"""
|
|
464
|
+
Validate a report dictionary, raising ValidationError on failure.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
report_dict: The report as a dictionary (from to_dict()).
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
ValidationError: If validation fails.
|
|
471
|
+
"""
|
|
472
|
+
is_valid, errors = validate_report(report_dict)
|
|
473
|
+
if not is_valid:
|
|
474
|
+
raise ValidationError(
|
|
475
|
+
f"Report validation failed with {len(errors)} error(s)", errors=errors
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_schema() -> dict[str, Any]:
|
|
480
|
+
"""
|
|
481
|
+
Return the JSON schema for InspectionReport.
|
|
482
|
+
|
|
483
|
+
If Pydantic is available, returns the auto-generated schema from the model.
|
|
484
|
+
Otherwise, returns the manually-defined schema.
|
|
485
|
+
"""
|
|
486
|
+
if PYDANTIC_AVAILABLE:
|
|
487
|
+
schema: dict[str, Any] = HaolineInspectionReport.model_json_schema()
|
|
488
|
+
return schema
|
|
489
|
+
return INSPECTION_REPORT_SCHEMA.copy()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def validate_with_pydantic(report_dict: dict[str, Any]) -> HaolineInspectionReport | None:
|
|
493
|
+
"""
|
|
494
|
+
Validate and parse a report dict into a Pydantic model.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
report_dict: The report as a dictionary.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
HaolineInspectionReport instance if valid, None if Pydantic not available.
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
ValidationError: If validation fails.
|
|
504
|
+
"""
|
|
505
|
+
if not PYDANTIC_AVAILABLE:
|
|
506
|
+
warnings.warn(
|
|
507
|
+
"Pydantic not installed. Install with 'pip install pydantic'.",
|
|
508
|
+
UserWarning,
|
|
509
|
+
stacklevel=2,
|
|
510
|
+
)
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
result: HaolineInspectionReport = HaolineInspectionReport.model_validate(report_dict)
|
|
515
|
+
return result
|
|
516
|
+
except PydanticValidationError as e:
|
|
517
|
+
error_messages = [
|
|
518
|
+
f"{' -> '.join(str(x) for x in err['loc'])}: {err['msg']}" for err in e.errors()
|
|
519
|
+
]
|
|
520
|
+
raise ValidationError(
|
|
521
|
+
f"Report validation failed with {len(error_messages)} error(s)",
|
|
522
|
+
errors=error_messages,
|
|
523
|
+
) from e
|