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)}
|
||||
for key in METRIC_KEYS:
|
||||
entry[key] = _metric_value(key, metrics.get(key))
|
||||
entry["phenology_phase"] = phenology.get(day)
|
||||
entry["disease_forecast"] = forecasts.get(day)
|
||||
# Observation and applied_treatment are only ever known for as_of and
|
||||
# earlier; future days have neither, so both fields are omitted for them.
|
||||
# Phenology, observation, and applied_treatment are only ever known for
|
||||
# as_of and earlier; future days have none, so those fields are omitted.
|
||||
if day <= as_of:
|
||||
entry["phenology_phase"] = phenology.get(day)
|
||||
entry["observation"] = bool((observation or {}).get(day, False))
|
||||
entry["applied_treatment"] = list(applied_treatments.get(day, []))
|
||||
return entry
|
||||
|
||||
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
import pyodbc
|
||||
@ -55,21 +55,15 @@ def _is_incubazprimaria(label: str | None) -> bool:
|
||||
|
||||
|
||||
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:
|
||||
return None
|
||||
if isinstance(value, Decimal):
|
||||
text = format(value, "f")
|
||||
elif isinstance(value, float):
|
||||
text = format(Decimal(str(value)), "f")
|
||||
else:
|
||||
try:
|
||||
rounded = round(float(value), 2)
|
||||
except (TypeError, ValueError):
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
text = format(Decimal(text), "f")
|
||||
except (InvalidOperation, ValueError):
|
||||
return text
|
||||
return text or None
|
||||
text = format(Decimal(str(rounded)), "f")
|
||||
if "." in text:
|
||||
text = text.rstrip("0").rstrip(".")
|
||||
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