from decimal import Decimal from typing import Any from app.domain.enums import NormalizationStatus, ValueType from app.domain.models import AttributeDefinition, AttributeStatement from app.errors import AppError class ManualNormalizer: version = "manual-v1" def validate(self, definition: AttributeDefinition, statement: AttributeStatement) -> AttributeStatement: if statement.normalization_status == NormalizationStatus.UNKNOWN: if statement.normalized_value is not None: raise AppError( 422, "NORMALIZATION_INCONSISTENT", "unknown statements must not have a normalized_value", ) return statement if statement.normalization_status != NormalizationStatus.NORMALIZED: raise AppError(422, "NORMALIZATION_STATUS", "unsupported normalization_status") if statement.normalized_value is None: raise AppError(422, "NORMALIZATION_MISSING", "normalized_value is required") _assert_type(definition, statement.normalized_value) if definition.value_type == ValueType.NUMBER: number = Decimal(str(statement.normalized_value)) if definition.key == "weight_kg" and number <= 0: raise AppError(422, "VALUE_RANGE", "weight_kg must be > 0") if definition.minimum is not None and number < definition.minimum: raise AppError(422, "VALUE_RANGE", f"{definition.key} is below the minimum") if definition.maximum is not None and number > definition.maximum: raise AppError(422, "VALUE_RANGE", f"{definition.key} is above the maximum") if definition.canonical_unit and statement.unit != definition.canonical_unit: raise AppError( 422, "UNIT_MISMATCH", "unit must match the canonical unit", {"expected": definition.canonical_unit, "actual": statement.unit}, ) if definition.canonical_unit is None and statement.unit not in {None, ""}: raise AppError(422, "UNIT_MISMATCH", "this attribute has no canonical unit") return statement def _assert_type(definition: AttributeDefinition, value: Any) -> None: if definition.value_type == ValueType.NUMBER: if isinstance(value, bool) or not isinstance(value, (int, float, Decimal)): raise AppError(422, "TYPE_MISMATCH", "normalized_value must be a number") return if definition.value_type == ValueType.STRING: if not isinstance(value, str): raise AppError(422, "TYPE_MISMATCH", "normalized_value must be a string") return if definition.value_type == ValueType.BOOLEAN: if not isinstance(value, bool): raise AppError(422, "TYPE_MISMATCH", "normalized_value must be a boolean") return if definition.value_type == ValueType.STRING_LIST: if not isinstance(value, list) or any(not isinstance(item, str) for item in value): raise AppError(422, "TYPE_MISMATCH", "normalized_value must be a list of strings")