-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathenforce_schema.py
More file actions
193 lines (158 loc) Β· 7.73 KB
/
Copy pathenforce_schema.py
File metadata and controls
193 lines (158 loc) Β· 7.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""
=======================================================================
SEMANTIC VERIFICATION ENGINE (Ref implementation: Harry Potter Trivia)
=======================================================================
-----------------------------------------------------------------------
CLI MVP (Tracer Build) -> Dataset Validation with Pydantic models
-----------------------------------------------------------------------
Shared validation module used across offline pipelines and
runtime evaluation workflows.
This module provides a unified interface for enforcing Pydantic-based
schema validation over question datasets stored in parquet or loaded
into runtime memory.
Core responsibilities:
---------------------
- Resolve schema mappings via runtime or validation registries
- Enforce Pydantic validation on raw or semi-processed DataFrames
- Partition data into validated and flagged records
- Provide deterministic validation behavior across environments
Pipeline usage:
--------------
Offline (generation / validation pipeline): enforce_schema before data-tier
DataFrame
β enforce_schema (structural validation gate)
β validated DataFrame
β branching:
Path A β Promotion / downstream processing
β enrichment / feature engineering / further pipeline logic
Path B β Storage (canonical dataset)
β normalization + serialization
β pyarrow Table
β parquet write
Runtime (execution):
parquet β pyarrow Table β DataFrame β enforce_schema β validated dataset
Design principles:
------------------
- Registry-driven schema resolution (no hardcoded models in logic)
- Pydantic-first validation (strict contract enforcement)
- Environment-agnostic (offline + runtime parity)
- Fail-safe: invalid records are isolated, not silently dropped
"""
from typing import Tuple, Dict, Optional, Type, Literal
import logging
import pandas as pd
import numpy as np
from pydantic import BaseModel, ValidationError
from core.constants import QuestionSource, QuestionType
from core.models import VALIDATION_REGISTRY, RUNTIME_REGISTRY, DataTier
logger = logging.getLogger(__name__)
## Router helpers
def get_validation_scheme(question_source: QuestionSource, data_tier: DataTier) -> dict:
"""Routes to the correct schema map from VALIDATION_REGISTRY."""
return VALIDATION_REGISTRY.get(question_source, {}).get(data_tier, {})
def get_runtime_scheme(mode: str) -> dict:
"""Routes to the correct schema map from RUNTIME_REGISTRY."""
scheme = RUNTIME_REGISTRY.get(mode)
if scheme is None:
raise ValueError(f"Unknown runtime mode '{mode}'. Expected one of: {list(RUNTIME_REGISTRY.keys())}")
return scheme
def get_scheme(mode: Optional[str] = None,
source: Optional[QuestionSource] = None,
tier: Optional[DataTier] = None) -> dict:
"""
Return mappings of QuestionType β Pydantic model classes
from either runtimve or validation registry
"""
if mode is not None:
return get_runtime_scheme(mode)
if source is not None and tier is not None:
return get_validation_scheme(source, tier)
raise ValueError("Invalid scheme request")
## Pydantic validators
def validate_record(record: dict,scheme: Dict[QuestionType, Type[BaseModel]], question_type: QuestionType
) -> tuple[BaseModel, str]:
"""Validates a single record against a pre-resolved Pydantic schema map."""
pydantic_scheme = scheme.get(question_type)
if pydantic_scheme is None:
raise ValueError(f"No schema found for {question_type}")
model_name = pydantic_scheme.__name__
valid_record = pydantic_scheme(**record)
return (valid_record, model_name)
def enforce_schema(scheme: Dict[QuestionType, Type[BaseModel]],
df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""
Enforces Pydantic schema validation over a dataset using a pre-resolved
QuestionType β Pydantic model mapping.
This function acts as a lightweight validation gate between raw (or
semi-processed) dataset inputs and downstream pipeline components.
It assumes that:
- The input DataFrame originates from a trusted ingestion layer (e.g. parquet load)
- The schema routing has already been resolved externally via registry helpers
- Each record contains a valid `question_type` field compatible with `QuestionType`
Processing Flow:
----------------
1. Normalize input DataFrame (NaN β None conversion)
2. Iterate over each record
3. Resolve QuestionType from record metadata
4. Validate record against corresponding Pydantic model
5. Partition results into:
- validated records (schema-compliant)
- flagged records (validation failures or routing errors)
Failure Modes Handled:
----------------------
- Pydantic ValidationError β record is captured in flagged output
- Missing schema mapping (ValueError) β record is flagged with routing error
- Missing or invalid question_type β record is flagged or skipped safely
Args:
scheme (dict):
Pre-resolved mapping of QuestionType β Pydantic model classes.
Typically sourced from validation or runtime registry.
df (pd.DataFrame):
Input dataset containing raw or semi-structured question records.
Returns:
Tuple[pd.DataFrame, pd.DataFrame]:
validated_df:
All records that successfully passed Pydantic validation.
flagged_df:
Records that failed validation or schema resolution,
enriched with error metadata for downstream inspection.
Notes:
------
- This function is registry-agnostic by design (no direct dependency on
VALIDATION_REGISTRY or RUNTIME_REGISTRY).
- Intended for reuse across both offline (batch) and runtime pipelines.
- Keeps validation deterministic and side-effect free.
"""
df_clean = df.copy().replace({np.nan: None})
validated_records, flagged_records = [], []
for index, record in enumerate(df_clean.to_dict("records")):
q_id = record.get("temp_qid") or record.get("original_question_id") or f"Row_{index}"
try:
q_type = QuestionType(record["question_type"])
valid_record, model_name = validate_record(record, scheme, q_type)
validated_records.append(valid_record.model_dump())
except (ValidationError, ValueError) as e:
logger.warning("Schema validation failed",
extra={"question_id": q_id,"error_type": type(e).__name__,"error_msg": str(e)})
flagged_records.append({**record,
"error_type": type(e).__name__,
"error_msg": str(e),
"question_id": q_id
})
logger.info("Schema enforcement completed",
extra={"validated": len(validated_records),"flagged": len(flagged_records)})
return (pd.DataFrame(validated_records), pd.DataFrame(flagged_records))
# convenience wrapper
def enforce_schema_pipeline(df: pd.DataFrame,
mode: Optional[str] = None,
source: Optional[QuestionSource] = None,
tier: Optional[DataTier] = None
)->Tuple[pd.DataFrame, pd.DataFrame]:
"""
- convenience wrapper to select get scheme and enforce it in one step.
- confirm records are unique in the dataset.
"""
if df["master_id"].duplicated().any():
raise ValueError("Duplicate master_id detected in dataset")
scheme = get_scheme(mode=mode, source=source, tier=tier)
return enforce_schema(scheme, df)