62 lines
3.1 KiB
Python
62 lines
3.1 KiB
Python
|
|
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")
|