"""Pydantic model for a parsed CSV row. Handles string-to-type coercion via before-mode validators. All fields are nullable — None means the value was absent or empty in the CSV. Business defaults (e.g., 30-day termination) are applied by the processor, not the model. """ from pydantic import BaseModel, ConfigDict, Field, field_validator from schemas.columns import CsvColumn from schemas.enums import ( CONTRACT_TYPE_ALIASES, PERIOD_TYPE_ALIASES, RENEWAL_TYPE_ALIASES, BooleanValue, ContractType, PeriodType, RenewalType, resolve_enum, ) def _to_int(v: object) -> int | None: if not isinstance(v, str) or not v.strip() or v.strip().upper() in ('N/A', 'NA'): return None s = v.strip() try: return int(s) except ValueError: raise ValueError( f'Invalid integer: {v!r}. ' f'Expected a whole number, empty, or N/A' ) def _to_date(v: object) -> str | None: if not isinstance(v, str) or not v.strip() or v.strip().upper() in ('N/A', 'NA'): return None from datetime import datetime s = v.strip() for fmt in ('%m/%d/%Y', '%Y-%m-%d', '%d-%b-%Y'): try: return datetime.strptime(s, fmt).strftime('%Y-%m-%d') except ValueError: continue raise ValueError( f'Unrecognized date format: {v!r}. ' f'Supported formats: MM/DD/YYYY, YYYY-MM-DD, DD-Mon-YYYY' ) def _to_str(v: object) -> str | None: if not isinstance(v, str) or not v.strip(): return None return v.strip() def _to_bool(v: object) -> bool | None: if not isinstance(v, str) or not v.strip(): return None try: return BooleanValue.parse(v, default=None).to_bool() except (ValueError, AttributeError): return None class ContractRow(BaseModel): """A single parsed CSV row with typed fields. Constructed from a csv.DictReader row dict — field aliases match the CSV column headers defined in CsvColumn. """ model_config = ConfigDict(frozen=True, populate_by_name=True, extra='ignore') account_id: int | None = Field(default=None, alias=CsvColumn.ACCOUNT_ID) contract_name: str | None = Field(default=None, alias=CsvColumn.CONTRACT_NAME) contract_type: ContractType | None = Field( default=None, alias=CsvColumn.CONTRACT_TYPE ) signing_entity: str | None = Field(default=None, alias=CsvColumn.SIGNING_ENTITY) run_controller: str | None = Field(default=None, alias=CsvColumn.RUN_CONTROLLER) execution_date: str | None = Field(default=None, alias=CsvColumn.EXECUTION_DATE) is_excluded: bool | None = Field(default=None, alias=CsvColumn.IS_EXCLUDED) is_primary: bool | None = Field(default=None, alias=CsvColumn.IS_PRIMARY) current_period_start: str | None = Field( default=None, alias=CsvColumn.CURRENT_PERIOD_START ) renewal_rules: RenewalType | None = Field( default=None, alias=CsvColumn.RENEWAL_RULES ) termination_interval: int | None = Field( default=None, alias=CsvColumn.TERMINATION_INTERVAL ) termination_type: PeriodType | None = Field( default=None, alias=CsvColumn.TERMINATION_TYPE ) renew_after_interval: int | None = Field( default=None, alias=CsvColumn.RENEW_AFTER_INTERVAL ) renew_after_type: PeriodType | None = Field( default=None, alias=CsvColumn.RENEW_AFTER_TYPE ) current_period_end: str | None = Field( default=None, alias=CsvColumn.CURRENT_PERIOD_END ) collection_interval: int | None = Field( default=None, alias=CsvColumn.COLLECTION_INTERVAL ) collection_type: PeriodType | None = Field( default=None, alias=CsvColumn.COLLECTION_TYPE ) # --- Validators (string → typed value) --- @field_validator( 'account_id', 'termination_interval', 'renew_after_interval', 'collection_interval', mode='before', ) @classmethod def coerce_int(cls, v): return _to_int(v) @field_validator('contract_name', 'signing_entity', 'run_controller', mode='before') @classmethod def coerce_str(cls, v): return _to_str(v) @field_validator( 'execution_date', 'current_period_start', 'current_period_end', mode='before' ) @classmethod def coerce_date(cls, v): return _to_date(v) @field_validator('is_excluded', 'is_primary', mode='before') @classmethod def coerce_bool(cls, v): return _to_bool(v) @field_validator('contract_type', mode='before') @classmethod def coerce_contract_type(cls, v): if not isinstance(v, str) or not v.strip(): return None return resolve_enum(v, ContractType, CONTRACT_TYPE_ALIASES) @field_validator('renewal_rules', mode='before') @classmethod def coerce_renewal_type(cls, v): if not isinstance(v, str) or not v.strip(): return None return resolve_enum(v, RenewalType, RENEWAL_TYPE_ALIASES) @field_validator( 'termination_type', 'renew_after_type', 'collection_type', mode='before' ) @classmethod def coerce_period_type(cls, v): if not isinstance(v, str) or not v.strip(): return None return resolve_enum(v, PeriodType, PERIOD_TYPE_ALIASES)