from __future__ import annotations import logging logger = logging.getLogger(__name__) # metadata type -> coercion function _COERCE_MAP = { 'integer': lambda v: int(v), 'number': lambda v: float(v), 'float': lambda v: float(v), } def _coerce_bool(v): if isinstance(v, bool): return v if isinstance(v, str): if v.lower() == 'true': return True if v.lower() == 'false': return False raise ValueError(f'Cannot convert string {v!r} to bool') return bool(v) def _coerce_value(value, expected_type: str): """Convert a single value to the expected type. Returns the converted value, or the original value if no conversion needed. """ if value is None: return value if expected_type == 'boolean': if isinstance(value, bool): return value return _coerce_bool(value) coerce_fn = _COERCE_MAP.get(expected_type) if coerce_fn is None: return value # Already the correct type if expected_type == 'integer' and isinstance(value, int) and not isinstance(value, bool): return value if expected_type in ('number', 'float') and isinstance(value, (int, float)) and not isinstance(value, bool): return float(value) return coerce_fn(value) def coerce_pipeline_config( config: dict, *metadata_list: dict, ) -> None: """Coerce pipeline config values according to metadata type definitions. Walks each metadata dict (trigger, safety, ai, output) and converts config values in-place so that strings coming from the JSON column are cast to their declared types (integer, number/float, boolean). Args: config: The pipeline config dict to modify in-place. *metadata_list: Metadata dicts loaded from the YAML templates. """ for meta in metadata_list: section_name = meta.get('name') if not section_name or section_name not in config: continue section = config[section_name] if not isinstance(section, dict): continue for stage_def in meta.get('stages', []): stage_name = stage_def.get('name') if not stage_name or stage_name not in section: continue stage_config = section[stage_name] if not isinstance(stage_config, dict): continue for field_def in stage_def.get('config', []): field_name = field_def.get('name') field_type = field_def.get('type') if not field_name or not field_type or field_name not in stage_config: continue old_value = stage_config[field_name] try: new_value = _coerce_value(old_value, field_type) if new_value is not old_value: stage_config[field_name] = new_value except (ValueError, TypeError) as e: logger.warning( 'Failed to coerce config %s.%s.%s (%r) to %s: %s', section_name, stage_name, field_name, old_value, field_type, e, )