from pydantic import BaseModel, Field, BeforeValidator from typing import Annotated, List, Optional # Example constants for validation TYPICAL_MEASURE_TYPES = [ "wall_insulation", "roof_insulation", "ventilation", "floor_insulation", "windows", "fireplace", "heating", "hot_water", "low_energy_lighting", "secondary_heating", "solar_pv" ] SPECIFIC_MEASURES = [ "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", "loft_insulation", "flat_roof_insulation", "room_roof_insulation", "suspended_floor_insulation", "solid_floor_insulation", "boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump", "secondary_heating", "solar_pv", "double_glazing", "secondary_glazing", "ventilation", "low_energy_lighting", "fireplace", "hot_water" ] NON_INVASIVE_SPECIFIC_MEASURES = [ "trickle_vents", "draught_proofing", "mixed_glazing", "cavity_extract_and_refill", "extension_cavity_wall_insulation" ] # This allows us to extend high level categories for measures such as "wall_insulation" to the specific measures # such as "external_wall_insulation", "internal_wall_insulation", "cavity_wall_insulation" MEASURE_MAP = { "wall_insulation": [ "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", ], "roof_insulation": ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"], "floor_insulation": ["suspended_floor_insulation", "solid_floor_insulation"], "heating": ["boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump"], "windows": ["double_glazing", "secondary_glazing"], "heating_controls": ["roomstat_programmer_trvs", "time_temperature_zone_control"] } VALID_GOALS = ["Increasing EPC"] VALID_HOUSING_TYPES = ["Social", "Private"] # Define the validation function for inclusions/exclusions def check_inclusion_or_exclusion(value: str) -> str: if value not in TYPICAL_MEASURE_TYPES + SPECIFIC_MEASURES + NON_INVASIVE_SPECIFIC_MEASURES: raise ValueError(f"{value} is not an allowed inclusion") return value def check_goals(value: str) -> str: assert value in VALID_GOALS, f"{value} is not a valid goal" return value def check_housing_type(value: str) -> str: assert value in VALID_HOUSING_TYPES, f"{value} is not a valid housing type" return value # Use Annotated with BeforeValidator for each list item validation InclusionOrExclusionItem = Annotated[str, BeforeValidator(check_inclusion_or_exclusion)] Goal = Annotated[str, BeforeValidator(check_goals)] HousingType = Annotated[str, BeforeValidator(check_housing_type)] class PlanTriggerRequest(BaseModel): budget: Optional[float] = None goal: Goal housing_type: HousingType goal_value: str portfolio_id: int trigger_file_path: str already_installed_file_path: Optional[str] = None patches_file_path: Optional[str] = None non_invasive_recommendations_file_path: Optional[str] = None valuation_file_path: Optional[str] = None exclusions: Optional[List[InclusionOrExclusionItem]] = Field(default=None, min_length=1) inclusions: Optional[List[InclusionOrExclusionItem]] = Field(default=None, min_length=1) scenario_name: Optional[str] = "" multi_plan: Optional[bool] = False optimise: Optional[bool] = True default_u_values: Optional[bool] = True ashp_cop: Optional[float] = 2.8