Files
site/apps/api/app/modules/zootech/lab/nutrient_schema.py
T
2026-07-17 12:57:18 +03:00

164 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Канонические поля 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)