BuyingAgent/backend/app/normalization/manual.py

62 lines
3.1 KiB
Python
Raw Permalink Normal View History

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")