netra-validator 0.1.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.
@@ -0,0 +1,13 @@
1
+ from importlib import metadata
2
+
3
+ # Single Source of Truth for Versioning
4
+ try:
5
+ __version__ = metadata.version("netra-validator")
6
+ except metadata.PackageNotFoundError:
7
+ __version__ = "0.0.0-dev"
8
+
9
+ # This allows users to do: "from netra_profiler import Profiler"
10
+ # instead of "from netra_profiler.core import Profiler"
11
+ from .engine.validator import Validator
12
+
13
+ __all__ = ["Validator", "__version__"]
File without changes
@@ -0,0 +1,201 @@
1
+ """
2
+ Business logic for pre-flight environment auditing and contract validation.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from pathlib import Path
10
+
11
+ from netra_validator.cli.models import ExecutionManifest
12
+ from netra_validator.compiler.duckdb_builder import build_global_sql_rules
13
+ from netra_validator.config import ValidatorConfig
14
+ from netra_validator.engine.duckdb_runner import validate_sql_rules_preflight
15
+ from netra_validator.exceptions import (
16
+ ConfigNotFoundError,
17
+ ContractNotFoundError,
18
+ ContractSyntaxError,
19
+ ContractValidationError,
20
+ DatasetNotFoundError,
21
+ )
22
+ from netra_validator.parser.models import DataContract
23
+ from netra_validator.parser.reader import read_contract
24
+
25
+ ONE_GB = 1024
26
+ SAFE_RAM_UTILIZATION_RATIO = 0.80
27
+
28
+ # Estimated in-memory & hash-state expansion factor per on-disk file format
29
+ FORMAT_EXPANSION_FACTORS: dict[str, float] = {
30
+ ".parquet": 5.2,
31
+ ".json": 2.5,
32
+ ".csv": 2.0,
33
+ ".ipc": 1.5,
34
+ ".arrow": 1.5,
35
+ }
36
+ DEFAULT_EXPANSION_FACTOR = 5.2
37
+
38
+
39
+ def _get_system_ram_mb() -> float:
40
+ """Extracts total physical memory available on the host OS in MB."""
41
+ if sys.platform == "win32":
42
+ import psutil # type: ignore # noqa: PLC0415
43
+
44
+ return float(psutil.virtual_memory().total) / (1024 * 1024)
45
+
46
+ return float(os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")) / (1024 * 1024)
47
+
48
+
49
+ class GlobalTargetChoice(str, Enum):
50
+ RAW = "raw"
51
+ VALIDATED = "validated"
52
+
53
+
54
+ @dataclass
55
+ class RunContext:
56
+ """Encapsulates the resolved CLI arguments and paths for the execution session."""
57
+
58
+ dataset_path: Path
59
+ contract_path: Path
60
+ valid_path: Path
61
+ quarantine_path: Path
62
+ config_path: Path | None
63
+ global_target: GlobalTargetChoice | None
64
+ force: bool
65
+
66
+
67
+ class PreFlightAuditor:
68
+ """
69
+ Evaluates system I/O, parses the data contract, and calculates hardware heuristics
70
+ prior to running the validation engine.
71
+ """
72
+
73
+ def __init__(self, context: RunContext) -> None:
74
+ self.context = context
75
+ self.errors: list[tuple[str, str]] = []
76
+ self.warnings: list[str] = []
77
+ self.loaded_config = ValidatorConfig()
78
+ self._contract_payload: DataContract | None = None
79
+
80
+ def _check_dataset(self) -> tuple[str, bool, float]:
81
+ try:
82
+ if not self.context.dataset_path.exists():
83
+ raise DatasetNotFoundError(self.context.dataset_path.name)
84
+
85
+ size_mb = os.path.getsize(self.context.dataset_path) / (1024 * 1024)
86
+ dataset_meta = f"{size_mb / 1024:.2f} GB" if size_mb > ONE_GB else f"{size_mb:.2f} MB"
87
+ return dataset_meta, False, size_mb
88
+
89
+ except DatasetNotFoundError as e:
90
+ self.errors.append((e.__class__.__name__, str(e)))
91
+ return "NOT FOUND", True, 0.0
92
+
93
+ def _check_contract(self) -> tuple[str, bool]:
94
+ try:
95
+ self._contract_payload = read_contract(self.context.contract_path)
96
+ return f"v{self._contract_payload.version}", False
97
+
98
+ except ContractNotFoundError as e:
99
+ self.errors.append((e.__class__.__name__, str(e)))
100
+ return "NOT FOUND", True
101
+
102
+ except ContractSyntaxError as e:
103
+ self.errors.append((e.__class__.__name__, str(e)))
104
+ return "SYNTAX ERR", True
105
+
106
+ except ContractValidationError as e:
107
+ for schema_error in e.errors:
108
+ clean_error = schema_error.removeprefix("- ").replace(": Value error, ", ": ")
109
+ self.errors.append(("ContractValidationError", clean_error))
110
+ return "INVALID", True
111
+
112
+ def _check_config(self) -> tuple[str, str, bool]:
113
+ if not self.context.config_path:
114
+ return "Default", "FACTORY", False
115
+
116
+ config_source = self.context.config_path.name
117
+ try:
118
+ if not self.context.config_path.exists():
119
+ raise ConfigNotFoundError(config_source)
120
+ self.loaded_config = ValidatorConfig.from_yaml(self.context.config_path)
121
+ return config_source, "CUSTOM", False
122
+
123
+ except ConfigNotFoundError as e:
124
+ self.errors.append((e.__class__.__name__, str(e)))
125
+ return config_source, "NOT FOUND", True
126
+ except Exception as e:
127
+ self.errors.append(("ConfigParseError", str(e)))
128
+ return config_source, "INVALID", True
129
+
130
+ def _evaluate_heuristics(self, size_mb: float) -> tuple[str, str, bool]:
131
+ if self.loaded_config.spill.enabled:
132
+ limit_gb = self.loaded_config.spill.memory_limit_mb / ONE_GB
133
+ return "SPILL TO DISK", f"{limit_gb:.1f} GB LIMIT", False
134
+
135
+ ext = self.context.dataset_path.suffix.lower()
136
+ expansion_factor = FORMAT_EXPANSION_FACTORS.get(ext, DEFAULT_EXPANSION_FACTOR)
137
+ estimated_footprint_mb = size_mb * expansion_factor
138
+ safe_ram_threshold_mb = _get_system_ram_mb() * SAFE_RAM_UTILIZATION_RATIO
139
+
140
+ if estimated_footprint_mb >= safe_ram_threshold_mb:
141
+ est_gb = estimated_footprint_mb / ONE_GB
142
+ total_gb = _get_system_ram_mb() / ONE_GB
143
+ self.warnings.append(
144
+ f"Estimated in-memory footprint (~{est_gb:.1f} GB) exceeds 80% of system RAM "
145
+ f"({total_gb:.1f} GB) for IN-MEMORY STREAMING. Risk of severe latency or OOM crash."
146
+ )
147
+ return "IN-MEMORY STREAMING", "OOM RISK", True
148
+
149
+ return "IN-MEMORY STREAMING", "DEFAULT", False
150
+
151
+ def execute(self) -> ExecutionManifest:
152
+ dataset_meta, dataset_critical, size_mb = self._check_dataset()
153
+ contract_meta, contract_critical = self._check_contract()
154
+ config_source, config_meta, config_critical = self._check_config()
155
+
156
+ if not dataset_critical and not contract_critical and self._contract_payload is not None:
157
+ sql_rules = build_global_sql_rules(self._contract_payload)
158
+ sql_errors = validate_sql_rules_preflight(self.context.dataset_path, sql_rules)
159
+ if sql_errors:
160
+ contract_meta = "SQL_ERR"
161
+ contract_critical = True
162
+ for rule_name, err_msg in sql_errors:
163
+ self.errors.append((f"SQL Error ({rule_name})", err_msg))
164
+
165
+ if self.context.global_target is not None:
166
+ self.loaded_config.global_target = (
167
+ "validated" if self.context.global_target == GlobalTargetChoice.VALIDATED else "raw"
168
+ )
169
+
170
+ active_target = self.loaded_config.global_target
171
+ global_target_meta = "DEFAULT" if active_target == "raw" else "POST-FILTER"
172
+
173
+ validated_protocol, validated_write_mode = "LOCAL", "APPEND"
174
+ quarantine_protocol, quarantine_write_mode = "LOCAL", "OVERWRITE"
175
+
176
+ engine_mode, engine_meta, engine_warning = self._evaluate_heuristics(size_mb)
177
+
178
+ return ExecutionManifest(
179
+ dataset_path=os.path.relpath(self.context.dataset_path),
180
+ dataset_meta=dataset_meta,
181
+ dataset_critical_error=dataset_critical,
182
+ contract_path=os.path.relpath(self.context.contract_path),
183
+ contract_meta=contract_meta,
184
+ contract_critical=contract_critical,
185
+ config_source=config_source,
186
+ config_meta=config_meta,
187
+ config_critical=config_critical,
188
+ global_target=active_target.upper(),
189
+ global_target_meta=global_target_meta,
190
+ validated_path=str(self.context.valid_path),
191
+ validated_protocol=validated_protocol,
192
+ validated_write_mode=validated_write_mode,
193
+ quarantine_path=str(self.context.quarantine_path),
194
+ quarantine_protocol=quarantine_protocol,
195
+ quarantine_write_mode=quarantine_write_mode,
196
+ engine_mode=engine_mode,
197
+ engine_meta=engine_meta,
198
+ engine_warning=engine_warning,
199
+ errors=self.errors,
200
+ warnings=self.warnings,
201
+ )