Refactor assemble.py and disease.py for clarity and functionality
- Moved the assignment of `phenology_phase` in assemble.py to ensure it is included only for valid days. - Updated the docstring in disease.py to clarify the rounding behavior of decimal values, enhancing the understanding of the formatting logic. - Removed unnecessary checks in the decimal formatting function to streamline the code.
This commit is contained in:
parent
86f660cb81
commit
d45e4de40b
@ -48,11 +48,11 @@ def _day_entry(
|
|||||||
entry: dict[str, Any] = {"date": format_date(day)}
|
entry: dict[str, Any] = {"date": format_date(day)}
|
||||||
for key in METRIC_KEYS:
|
for key in METRIC_KEYS:
|
||||||
entry[key] = _metric_value(key, metrics.get(key))
|
entry[key] = _metric_value(key, metrics.get(key))
|
||||||
entry["phenology_phase"] = phenology.get(day)
|
|
||||||
entry["disease_forecast"] = forecasts.get(day)
|
entry["disease_forecast"] = forecasts.get(day)
|
||||||
# Observation and applied_treatment are only ever known for as_of and
|
# Phenology, observation, and applied_treatment are only ever known for
|
||||||
# earlier; future days have neither, so both fields are omitted for them.
|
# as_of and earlier; future days have none, so those fields are omitted.
|
||||||
if day <= as_of:
|
if day <= as_of:
|
||||||
|
entry["phenology_phase"] = phenology.get(day)
|
||||||
entry["observation"] = bool((observation or {}).get(day, False))
|
entry["observation"] = bool((observation or {}).get(day, False))
|
||||||
entry["applied_treatment"] = list(applied_treatments.get(day, []))
|
entry["applied_treatment"] = list(applied_treatments.get(day, []))
|
||||||
return entry
|
return entry
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pyodbc
|
import pyodbc
|
||||||
@ -55,21 +55,15 @@ def _is_incubazprimaria(label: str | None) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _format_model_value(value: Any) -> str | None:
|
def _format_model_value(value: Any) -> str | None:
|
||||||
"""Compact decimal string: 40.3, not 40.3000000001 or 4.03e+01."""
|
"""Round to 2 decimals, then compact: 40.12, 40.3, 40 — not 40.123456789."""
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(value, Decimal):
|
|
||||||
text = format(value, "f")
|
|
||||||
elif isinstance(value, float):
|
|
||||||
text = format(Decimal(str(value)), "f")
|
|
||||||
else:
|
|
||||||
text = str(value).strip()
|
|
||||||
if not text:
|
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
text = format(Decimal(text), "f")
|
rounded = round(float(value), 2)
|
||||||
except (InvalidOperation, ValueError):
|
except (TypeError, ValueError):
|
||||||
return text
|
text = str(value).strip()
|
||||||
|
return text or None
|
||||||
|
text = format(Decimal(str(rounded)), "f")
|
||||||
if "." in text:
|
if "." in text:
|
||||||
text = text.rstrip("0").rstrip(".")
|
text = text.rstrip("0").rstrip(".")
|
||||||
return text or None
|
return text or None
|
||||||
|
|||||||
44
tests/test_assemble.py
Normal file
44
tests/test_assemble.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""Unit tests for advice-generation JSON assembly."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from pipeline.stages.assemble import assemble_json_for_advice_generation
|
||||||
|
from pipeline.window import build_window, format_date
|
||||||
|
|
||||||
|
_AS_OF = date(2026, 4, 16)
|
||||||
|
_PHASE = "BBCH 15"
|
||||||
|
|
||||||
|
|
||||||
|
class AdviceGenerationPhenologyTests(unittest.TestCase):
|
||||||
|
def test_phenology_phase_only_on_past_and_today(self) -> None:
|
||||||
|
window = build_window(_AS_OF)
|
||||||
|
phenology = {day: _PHASE if day <= _AS_OF else None for day in window}
|
||||||
|
payload = assemble_json_for_advice_generation(
|
||||||
|
_AS_OF,
|
||||||
|
weather={},
|
||||||
|
phenology=phenology,
|
||||||
|
forecasts={},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(payload["date_of_today"], format_date(_AS_OF))
|
||||||
|
self.assertEqual(len(payload["meteorological_data"]), 6)
|
||||||
|
self.assertEqual(len(payload["weather_forecasts"]), 5)
|
||||||
|
|
||||||
|
for entry in payload["meteorological_data"]:
|
||||||
|
self.assertIn("phenology_phase", entry)
|
||||||
|
self.assertEqual(entry["phenology_phase"], _PHASE)
|
||||||
|
|
||||||
|
for entry in payload["weather_forecasts"]:
|
||||||
|
self.assertNotIn("phenology_phase", entry)
|
||||||
|
|
||||||
|
last_meteo = payload["meteorological_data"][-1]
|
||||||
|
self.assertEqual(last_meteo["date"], format_date(_AS_OF))
|
||||||
|
first_forecast = payload["weather_forecasts"][0]
|
||||||
|
self.assertEqual(first_forecast["date"], format_date(_AS_OF + timedelta(days=1)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
44
tests/test_disease.py
Normal file
44
tests/test_disease.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""Unit tests for INCUBAZPRIMARIA model_value rounding and labels."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from pipeline.stages.disease import _format_model_value, _incub_label
|
||||||
|
|
||||||
|
|
||||||
|
class FormatModelValueTests(unittest.TestCase):
|
||||||
|
def test_long_float_rounds_to_two_decimals(self) -> None:
|
||||||
|
self.assertEqual(_format_model_value(40.123456789), "40.12")
|
||||||
|
|
||||||
|
def test_long_decimal_rounds_to_two_decimals(self) -> None:
|
||||||
|
self.assertEqual(_format_model_value(Decimal("40.123456789")), "40.12")
|
||||||
|
|
||||||
|
def test_trailing_zeros_stripped(self) -> None:
|
||||||
|
self.assertEqual(_format_model_value(40.30), "40.3")
|
||||||
|
self.assertEqual(_format_model_value(40.0), "40")
|
||||||
|
self.assertEqual(_format_model_value(Decimal("40.10")), "40.1")
|
||||||
|
|
||||||
|
def test_none_and_empty_return_none(self) -> None:
|
||||||
|
self.assertIsNone(_format_model_value(None))
|
||||||
|
self.assertIsNone(_format_model_value(" "))
|
||||||
|
|
||||||
|
def test_non_numeric_passthrough(self) -> None:
|
||||||
|
self.assertEqual(_format_model_value("n/a"), "n/a")
|
||||||
|
|
||||||
|
|
||||||
|
class IncubLabelTests(unittest.TestCase):
|
||||||
|
def test_picks_max_raw_value_then_rounds_suffix(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
_incub_label([40.12, 40.129, 39.99]),
|
||||||
|
"INCUBAZPRIMARIA_40.13",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_empty_or_non_numeric_falls_back(self) -> None:
|
||||||
|
self.assertEqual(_incub_label([]), "INCUBAZPRIMARIA")
|
||||||
|
self.assertEqual(_incub_label([None, "n/a"]), "INCUBAZPRIMARIA")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue
Block a user