mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-09-11 10:26:38 +00:00
fix: unify mypy environments for local checks and CI (#1291)
The isolated pre-commit mypy hook previously omitted runtime type information that make mypy used, hiding errors involving dependencies such as Pydantic and Pendulum. Makefile, pre-commit and CI now run the same full-project typing policy in the development environment defined by uv.lock. - Use uv run --locked --exact --extra dev and the same mypy arguments for Makefile and the local hook. Check all of src and tests, including on configuration-only changes. - Pin Python 3.13 for local development and the pre-commit CI job, and install the locked pre-commit version in CI. - Disable incremental analysis because existing Pendulum cache state changes mypy 2.3.1 diagnostics. Document the policy, the performance tradeoff and the existing typing debt. - Add a regression test that exercises Makefile, the hook and the CI command in a temporary project, accepting valid dependency types and detecting deliberate Pydantic/Pendulum assignment errors. Resolve the newly detected mypy diagnostics. - Enable the numpydantic and Pydantic mypy plugins, retaining strict Pydantic constructor typing with init_typed = true. Validate raw/coercible payloads through model_validate. - Propagate concrete record, provider and time-window types through generic collections, factories and lookup methods. Preserve runtime field inspection and generated time-window documentation. - Align Pendulum annotations with actual factory/arithmetic results while retaining Pydantic validation adapters at runtime. Correct optional values, array boundaries, REST handlers and plotting interfaces. - Add pinned scipy-stubs and types-psutil, update uv.lock, and supply the plugins' dependencies. - Add runtime regression coverage for validated path defaults, normalized time-series metadata, generic field inspection, invalid timestamps and unsupported provider imports. Runtime and compatibility details: - Validate path defaults as Path objects while retaining raw string defaults needed by migration serialization with exclude_defaults. - Normalize feed-in tariff lists and default charge rates to NumPy arrays; reject missing timestamps/uninitialized values explicitly. Importing into a provider without import support returns HTTP 400. - Public JSON schemas and OpenAPI structure match main (excluding the generated version). Signed-off-by: dr-dimitry Signed-off-by: dr-dimitry Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com> Co-authored-by: dr-dimitri <87113560+dr-dimitri@users.noreply.github.com> Co-authored-by: Normann <github@koldrack.com>
This commit is contained in:
co-authored by
dr-dimitri
Normann
parent
5b584cbb57
commit
1abdd345c4
+51
-51
@@ -55,11 +55,11 @@ def aware_dt(year, month, day, hour=0, minute=0, second=0, tz="Europe/Berlin"):
|
||||
|
||||
def make_window(start_h, duration_h, **kwargs):
|
||||
"""Build a TimeWindow with a naive start_time at ``start_h:00``."""
|
||||
return TimeWindow(
|
||||
return TimeWindow.model_validate(dict(
|
||||
start_time=f"{start_h:02d}:00:00",
|
||||
duration=f"{duration_h} hours",
|
||||
**kwargs,
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
@@ -73,10 +73,10 @@ class TestTimeWindowConstruction:
|
||||
|
||||
def test_aware_start_time_stripped_to_naive(self):
|
||||
"""An aware start_time is silently stripped to naive (to_time may add a tz)."""
|
||||
w = TimeWindow(
|
||||
w = TimeWindow.model_validate(dict(
|
||||
start_time=Time(8, 0, 0, tzinfo=pendulum.timezone("Europe/Berlin")),
|
||||
duration="2 hours",
|
||||
)
|
||||
))
|
||||
assert w.start_time.tzinfo is None
|
||||
assert w.start_time.hour == 8
|
||||
|
||||
@@ -375,7 +375,7 @@ class TestFitAndAvailable:
|
||||
|
||||
class TestTimeWindowSequence:
|
||||
def setup_method(self, method):
|
||||
self.seq = TimeWindowSequence(
|
||||
self.seq = TimeWindowSequence[TimeWindow](
|
||||
windows=[
|
||||
make_window(8, 2), # 08:00–10:00
|
||||
make_window(14, 3), # 14:00–17:00
|
||||
@@ -417,15 +417,15 @@ class TestTimeWindowSequence:
|
||||
assert result == pendulum.duration(hours=5)
|
||||
|
||||
def test_empty_sequence_contains_false(self):
|
||||
seq = TimeWindowSequence()
|
||||
seq = TimeWindowSequence[TimeWindow]()
|
||||
assert not seq.contains(naive_dt(2024, 6, 15, 9, 0, 0))
|
||||
|
||||
def test_empty_sequence_earliest_none(self):
|
||||
seq = TimeWindowSequence()
|
||||
seq = TimeWindowSequence[TimeWindow]()
|
||||
assert seq.earliest_start_time(pendulum.duration(hours=1), naive_dt(2024, 6, 15)) is None
|
||||
|
||||
def test_empty_sequence_available_none(self):
|
||||
seq = TimeWindowSequence()
|
||||
seq = TimeWindowSequence[TimeWindow]()
|
||||
assert seq.available_duration(naive_dt(2024, 6, 15)) is None
|
||||
|
||||
def test_get_applicable_windows(self):
|
||||
@@ -445,7 +445,7 @@ class TestTimeWindowSequence:
|
||||
assert fits[0].start_time.hour == 14
|
||||
|
||||
def test_sort_windows_by_start_time(self):
|
||||
seq = TimeWindowSequence(
|
||||
seq = TimeWindowSequence[TimeWindow](
|
||||
windows=[make_window(14, 1), make_window(8, 1)]
|
||||
)
|
||||
ref = naive_dt(2024, 6, 15)
|
||||
@@ -454,7 +454,7 @@ class TestTimeWindowSequence:
|
||||
assert seq.windows[1].start_time.hour == 14
|
||||
|
||||
def test_add_and_remove_window(self):
|
||||
seq = TimeWindowSequence()
|
||||
seq = TimeWindowSequence[TimeWindow]()
|
||||
w = make_window(10, 1)
|
||||
seq.add_window(w)
|
||||
assert len(seq) == 1
|
||||
@@ -463,7 +463,7 @@ class TestTimeWindowSequence:
|
||||
assert len(seq) == 0
|
||||
|
||||
def test_remove_from_empty_raises(self):
|
||||
seq = TimeWindowSequence()
|
||||
seq = TimeWindowSequence[TimeWindow]()
|
||||
with pytest.raises(IndexError):
|
||||
seq.remove_window(0)
|
||||
|
||||
@@ -487,20 +487,20 @@ class TestTimeWindowSequence:
|
||||
|
||||
class TestValueTimeWindow:
|
||||
def test_value_stored(self):
|
||||
w = ValueTimeWindow(start_time="08:00:00", duration="2 hours", value=0.288)
|
||||
w = ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="2 hours", value=0.288))
|
||||
assert w.value == pytest.approx(0.288)
|
||||
|
||||
def test_value_default_none(self):
|
||||
w = ValueTimeWindow(start_time="08:00:00", duration="2 hours")
|
||||
w = ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="2 hours"))
|
||||
assert w.value is None
|
||||
|
||||
def test_inherits_aware_start_time_stripped(self):
|
||||
"""ValueTimeWindow inherits the strip-to-naive behaviour from TimeWindow."""
|
||||
w = ValueTimeWindow(
|
||||
w = ValueTimeWindow.model_validate(dict(
|
||||
start_time=Time(8, 0, 0, tzinfo=pendulum.timezone("UTC")),
|
||||
duration="2 hours",
|
||||
value=0.1,
|
||||
)
|
||||
))
|
||||
assert w.start_time.tzinfo is None
|
||||
assert w.start_time.hour == 8
|
||||
|
||||
@@ -509,8 +509,8 @@ class TestValueTimeWindowSequence:
|
||||
def setup_method(self, method):
|
||||
self.seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(start_time="08:00:00", duration="4 hours", value=0.25),
|
||||
ValueTimeWindow(start_time="18:00:00", duration="4 hours", value=0.35),
|
||||
ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="4 hours", value=0.25)),
|
||||
ValueTimeWindow.model_validate(dict(start_time="18:00:00", duration="4 hours", value=0.35)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -528,7 +528,7 @@ class TestValueTimeWindowSequence:
|
||||
|
||||
def test_get_value_none_value_returns_zero(self):
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[ValueTimeWindow(start_time="08:00:00", duration="4 hours", value=None)]
|
||||
windows=[ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="4 hours", value=None))]
|
||||
)
|
||||
assert seq.get_value_for_datetime(naive_dt(2024, 6, 15, 9, 0, 0)) == pytest.approx(0.0)
|
||||
|
||||
@@ -552,7 +552,7 @@ class TestTimeWindowSequenceToArray:
|
||||
"""
|
||||
|
||||
def setup_method(self, method):
|
||||
self.seq = TimeWindowSequence(
|
||||
self.seq = TimeWindowSequence[TimeWindow](
|
||||
windows=[
|
||||
make_window(8, 2), # 08:00–10:00
|
||||
make_window(14, 3), # 14:00–17:00
|
||||
@@ -697,7 +697,7 @@ class TestTimeWindowSequenceToArray:
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_empty_sequence_all_zeros(self):
|
||||
seq = TimeWindowSequence()
|
||||
seq = TimeWindowSequence[TimeWindow]()
|
||||
start = naive_dt(2024, 6, 15, 0)
|
||||
end = naive_dt(2024, 6, 15, 4)
|
||||
arr = seq.to_array(start, end, pendulum.duration(hours=1))
|
||||
@@ -710,7 +710,7 @@ class TestTimeWindowSequenceToArray:
|
||||
|
||||
def test_day_of_week_constraint_respected(self):
|
||||
# Monday-only window; 2024-06-17 is Monday, 2024-06-18 is Tuesday
|
||||
seq = TimeWindowSequence(windows=[make_window(8, 2, day_of_week=0)])
|
||||
seq = TimeWindowSequence[TimeWindow](windows=[make_window(8, 2, day_of_week=0)])
|
||||
monday_start = naive_dt(2024, 6, 17, 7)
|
||||
tuesday_start = naive_dt(2024, 6, 18, 7)
|
||||
end_offset = pendulum.duration(hours=4)
|
||||
@@ -737,7 +737,7 @@ class TestTimeWindowSequenceToSeries:
|
||||
"""
|
||||
|
||||
def setup_method(self, method):
|
||||
self.seq = TimeWindowSequence(
|
||||
self.seq = TimeWindowSequence[TimeWindow](
|
||||
windows=[
|
||||
make_window(8, 2),
|
||||
make_window(14, 3),
|
||||
@@ -858,7 +858,7 @@ class TestTimeWindowSequenceToSeries:
|
||||
)
|
||||
|
||||
def test_empty_sequence_all_zeros(self):
|
||||
seq = TimeWindowSequence()
|
||||
seq = TimeWindowSequence[TimeWindow]()
|
||||
start = naive_dt(2024, 6, 15, 0)
|
||||
end = naive_dt(2024, 6, 15, 4)
|
||||
|
||||
@@ -885,8 +885,8 @@ class TestValueTimeWindowSequenceToArray:
|
||||
def setup_method(self, method):
|
||||
self.seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(start_time="08:00:00", duration="4 hours", value=0.25),
|
||||
ValueTimeWindow(start_time="18:00:00", duration="4 hours", value=0.35),
|
||||
ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="4 hours", value=0.25)),
|
||||
ValueTimeWindow.model_validate(dict(start_time="18:00:00", duration="4 hours", value=0.35)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -939,8 +939,8 @@ class TestValueTimeWindowSequenceToArray:
|
||||
def test_dropna_false_none_value_emits_nan(self):
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(start_time="08:00:00", duration="2 hours", value=None),
|
||||
ValueTimeWindow(start_time="12:00:00", duration="2 hours", value=0.5),
|
||||
ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="2 hours", value=None)),
|
||||
ValueTimeWindow.model_validate(dict(start_time="12:00:00", duration="2 hours", value=0.5)),
|
||||
]
|
||||
)
|
||||
start = naive_dt(2024, 6, 15, 8)
|
||||
@@ -956,8 +956,8 @@ class TestValueTimeWindowSequenceToArray:
|
||||
def test_dropna_true_none_value_step_omitted(self):
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(start_time="08:00:00", duration="2 hours", value=None),
|
||||
ValueTimeWindow(start_time="12:00:00", duration="2 hours", value=0.5),
|
||||
ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="2 hours", value=None)),
|
||||
ValueTimeWindow.model_validate(dict(start_time="12:00:00", duration="2 hours", value=0.5)),
|
||||
]
|
||||
)
|
||||
start = naive_dt(2024, 6, 15, 8)
|
||||
@@ -1018,8 +1018,8 @@ class TestValueTimeWindowSequenceToArray:
|
||||
def test_overlapping_windows_first_wins(self):
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(start_time="08:00:00", duration="4 hours", value=0.10),
|
||||
ValueTimeWindow(start_time="09:00:00", duration="4 hours", value=0.99),
|
||||
ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="4 hours", value=0.10)),
|
||||
ValueTimeWindow.model_validate(dict(start_time="09:00:00", duration="4 hours", value=0.99)),
|
||||
]
|
||||
)
|
||||
start = naive_dt(2024, 6, 15, 9)
|
||||
@@ -1040,16 +1040,16 @@ class TestValueTimeWindowSequenceToSeries:
|
||||
def setup_method(self, method):
|
||||
self.seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="08:00:00",
|
||||
duration="4 hours",
|
||||
value=0.25,
|
||||
),
|
||||
ValueTimeWindow(
|
||||
)),
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="18:00:00",
|
||||
duration="4 hours",
|
||||
value=0.35,
|
||||
),
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1096,16 +1096,16 @@ class TestValueTimeWindowSequenceToSeries:
|
||||
def test_dropna_false_none_value_emits_nan(self):
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="08:00:00",
|
||||
duration="2 hours",
|
||||
value=None,
|
||||
),
|
||||
ValueTimeWindow(
|
||||
)),
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="12:00:00",
|
||||
duration="2 hours",
|
||||
value=0.5,
|
||||
),
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1134,16 +1134,16 @@ class TestValueTimeWindowSequenceToSeries:
|
||||
def test_dropna_true_none_value_omits_timestamp(self):
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="08:00:00",
|
||||
duration="2 hours",
|
||||
value=None,
|
||||
),
|
||||
ValueTimeWindow(
|
||||
)),
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="12:00:00",
|
||||
duration="2 hours",
|
||||
value=0.5,
|
||||
),
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1254,16 +1254,16 @@ class TestValueTimeWindowSequenceToSeries:
|
||||
def test_overlapping_windows_first_wins(self):
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="08:00:00",
|
||||
duration="4 hours",
|
||||
value=0.10,
|
||||
),
|
||||
ValueTimeWindow(
|
||||
)),
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="09:00:00",
|
||||
duration="4 hours",
|
||||
value=0.99,
|
||||
),
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1452,7 +1452,7 @@ class TestAlignToIntervalTimezoneInvariance:
|
||||
def test_vtws_naive_floor_utc(self, set_other_timezone):
|
||||
set_other_timezone("UTC")
|
||||
seq = ValueTimeWindowSequence(windows=[
|
||||
ValueTimeWindow(start_time="08:00:00", duration="2 hours", value=0.25)
|
||||
ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="2 hours", value=0.25))
|
||||
])
|
||||
start = naive_dt(2024, 6, 15, 8, 10)
|
||||
end = naive_dt(2024, 6, 15, 10, 10)
|
||||
@@ -1465,7 +1465,7 @@ class TestAlignToIntervalTimezoneInvariance:
|
||||
def test_vtws_naive_floor_non_utc(self, set_other_timezone):
|
||||
set_other_timezone()
|
||||
seq = ValueTimeWindowSequence(windows=[
|
||||
ValueTimeWindow(start_time="08:00:00", duration="2 hours", value=0.25)
|
||||
ValueTimeWindow.model_validate(dict(start_time="08:00:00", duration="2 hours", value=0.25))
|
||||
])
|
||||
start = naive_dt(2024, 6, 15, 8, 10)
|
||||
end = naive_dt(2024, 6, 15, 10, 10)
|
||||
@@ -1480,11 +1480,11 @@ class TestAlignToIntervalTimezoneInvariance:
|
||||
|
||||
seq = ValueTimeWindowSequence(
|
||||
windows=[
|
||||
ValueTimeWindow(
|
||||
ValueTimeWindow.model_validate(dict(
|
||||
start_time="08:00:00",
|
||||
duration="2 hours",
|
||||
value=0.25,
|
||||
)
|
||||
))
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user