"""Канонические поля zootech-показателей компонента (parity tab INGREDIENT_NUTRIENT_EDIT_ROWS).""" from __future__ import annotations from typing import Any # column_name -> ключи в API/calc (первый — канон для UI) NUTRIENT_FIELD_SPECS: tuple[tuple[str, tuple[str, ...]], ...] = ( ("crude_protein", ("Сыр. Протеин",)), ("usp", ("уСП",)), ("rnb", ("RNB", "БРА", " БРА ")), ("nel_cattle", ("ЧЭЛ- КРС", " ЧЭЛ- КРС", "ЧЭЛ - КРС Форм")), ("oe_cattle", ("ОЭ-КРС", " ОЭ-КРС", "OЭ КРС форм")), ("ndf", ("Сырая клетчатка", "Сырая клетч")), ("structural_fiber", ("Структур. клетчатка", "Структур клетч")), ("crude_fat", ("Сырой жир",)), ) def _normalize_key(value: str) -> str: return " ".join((value or "").split()).strip().lower() def _parse_num(value: Any) -> float | None: if value is None or value == "": return None try: n = float(value) except (TypeError, ValueError): return None return None if n != n else n def dry_matter_g_per_kg(dry_matter_pct: float | None) -> float | None: """СВ г/кг из канонического component.dry_matter (%): 88.7% → 887 г/кг.""" dm = _parse_num(dry_matter_pct) if dm is None: return None if 0 < dm <= 100: return dm * 10.0 if dm > 100: return dm return None def resolve_sv_g_per_kg( nutrients: dict[str, Any] | None, dry_matter_pct: float | None, ) -> float | None: """Deprecated alias: СВ только из component.dry_matter (%). nutrients игнорируется.""" return dry_matter_g_per_kg(dry_matter_pct) def read_from_mapping(data: dict[str, Any] | None, keys: tuple[str, ...]) -> float | None: if not data: return None for search in keys: target = _normalize_key(search) for k, v in data.items(): if _normalize_key(str(k)) != target: continue n = _parse_num(v) if n is not None: return n return None def api_dict_to_column_values(data: dict[str, Any] | None) -> dict[str, float | None]: payload = data or {} out: dict[str, float | None] = {} for col, keys in NUTRIENT_FIELD_SPECS: out[col] = read_from_mapping(payload, keys) return out def derived_to_column_values(data: dict[str, Any] | None) -> dict[str, float | None]: return api_dict_to_column_values(data) def column_values_to_mapping(values: dict[str, float | None]) -> dict[str, float]: out: dict[str, float] = {} for col, keys in NUTRIENT_FIELD_SPECS: val = values.get(col) n = _parse_num(val) if n is not None: out[keys[0]] = n return out def mapping_to_calc_dict(data: dict[str, Any] | None) -> dict[str, float]: if not data: return {} from app.modules.zootech.lab.indicators import RATION_ALL_INDICATORS result: dict[str, float] = {} for _col, keys in NUTRIENT_FIELD_SPECS: val = read_from_mapping(data, keys) if val is None: continue for key in keys: result[key] = val seen: set[str] = set() for defn in RATION_ALL_INDICATORS: for key in defn.get("nutrient_keys") or []: if key in seen: continue seen.add(key) val = read_from_mapping(data, (key,)) if val is not None: result[key] = val return result def row_to_calc_dict(row: Any | None) -> dict[str, float]: if row is None: return {} result: dict[str, float] = {} for col, keys in NUTRIENT_FIELD_SPECS: val = getattr(row, col, None) if val is None: continue n = _parse_num(val) if n is None: continue for key in keys: result[key] = n return result def row_to_api_dict(row: Any | None) -> dict[str, float]: """API nutrients — канонические ключи tab.""" if row is None: return {} out: dict[str, float] = {} for col, keys in NUTRIENT_FIELD_SPECS: val = getattr(row, col, None) n = _parse_num(val) if n is not None: out[keys[0]] = n return out def row_is_empty(row: Any | None) -> bool: if row is None: return True for col, _keys in NUTRIENT_FIELD_SPECS: if _parse_num(getattr(row, col, None)) is not None: return False return True def legacy_json_to_column_values(raw: str | None) -> dict[str, float | None]: if not raw or not str(raw).strip() or str(raw).strip() == "{}": return {col: None for col, _ in NUTRIENT_FIELD_SPECS} import json try: data = json.loads(raw) except (json.JSONDecodeError, TypeError): data = {} if not isinstance(data, dict): data = {} return api_dict_to_column_values(data)