"""Server-side range validation for the Framingham tabular fields. Blank / missing fields are skipped here (they get KNN-imputed downstream). Only *provided* values are checked: they must parse as numbers and fall inside clinically plausible bounds. Returns a list of human-readable error strings (empty == valid). """ from __future__ import annotations from typing import Any # Binary / categorical fields use "choices"; continuous fields use min/max. FIELD_SPECS: dict[str, dict[str, Any]] = { "male": {"choices": {0, 1}}, "age": {"min": 1, "max": 120}, "education": {"choices": {1, 2, 3, 4}}, "currentSmoker": {"choices": {0, 1}}, "cigsPerDay": {"min": 0, "max": 100}, "BPMeds": {"choices": {0, 1}}, "prevalentStroke": {"choices": {0, 1}}, "prevalentHyp": {"choices": {0, 1}}, "diabetes": {"choices": {0, 1}}, "totChol": {"min": 50, "max": 700}, "sysBP": {"min": 50, "max": 300}, "diaBP": {"min": 30, "max": 200}, "BMI": {"min": 10, "max": 70}, "heartRate": {"min": 20, "max": 300}, "glucose": {"min": 20, "max": 700}, } def validate_tabular(fields: dict[str, Any]) -> list[str]: errors: list[str] = [] for name, spec in FIELD_SPECS.items(): raw = fields.get(name) if raw is None or str(raw).strip() == "": continue # blank -> imputed later, nothing to validate try: v = float(raw) except (TypeError, ValueError): errors.append(f"{name}: '{raw}' is not a number") continue if "choices" in spec and v not in spec["choices"]: errors.append(f"{name}: must be one of {sorted(spec['choices'])} (got {raw})") if "min" in spec and v < spec["min"]: errors.append(f"{name}: must be >= {spec['min']} (got {raw})") if "max" in spec and v > spec["max"]: errors.append(f"{name}: must be <= {spec['max']} (got {raw})") return errors