perf: import heavy optional dependencies on first use (#1377)

A GENETIC server with external (import) providers and EOSdash disabled
loads several large packages at start that it never uses, which costs
a noticeable part of the memory on 512 MB boards:

- matplotlib via the GENETIC/GENETIC0 PDF report modules (server/eos.py)
- statsmodels via the Holt-Winters fallback in prediction/priceabc.py
- pvlib via the weather base class and the pvlib-based providers
- bs4/lxml via the ClearOutside provider
- scipy via the self-consumption interpolator singletons, which were
  loaded at import time
- the GENETIC0 optimizer, imported by core/ems.py
- tzfpy, whose polygon data the computed general.timezone loads

Import these where they are used. The interpolator tables are loaded
on first use (thread-safe). Nothing changes in what is computed.

general.timezone is computed from latitude/longitude and cannot be set.
Add general.timezone_override (IANA name, validated with pendulum):
when set, general.timezone returns it and the lookup is skipped. When
unset (default) the lookup is unchanged.

Tests that patched the moved names now patch them at their source.
A new test starts the server module in a subprocess and checks that
none of these modules is loaded.

Co-authored-by: Christin <christin@BigBrother.bikini.bottom.zone>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christin
2026-10-11 08:40:43 +02:00
committed by GitHub
co-authored by Christin Claude Opus 5.5
parent 6d26252c6a
commit 3651d33915
21 changed files with 295 additions and 47 deletions
+6 -6
View File
@@ -1308,8 +1308,8 @@ class TestCalculation:
assert isinstance(result, pd.DataFrame)
assert result.empty
@patch("akkudoktoreos.prediction.pvforecastpvlib.ModelChain")
@patch("akkudoktoreos.prediction.pvforecastpvlib.Location")
@patch("pvlib.modelchain.ModelChain")
@patch("pvlib.location.Location")
def test_location_created(
self,
mock_location,
@@ -1352,7 +1352,7 @@ class TestCalculation:
mock_location.assert_called_once()
@patch("akkudoktoreos.prediction.pvforecastpvlib.ModelChain")
@patch("pvlib.modelchain.ModelChain")
def test_modelchain_run_called(
self,
mock_modelchain,
@@ -1395,7 +1395,7 @@ class TestCalculation:
mc.run_model.assert_called_once_with(weather)
@patch("akkudoktoreos.prediction.pvforecastpvlib.ModelChain")
@patch("pvlib.modelchain.ModelChain")
def test_negative_power_clipped(
self,
mock_modelchain,
@@ -1444,7 +1444,7 @@ class TestCalculation:
assert (result["pv_dc_power"] >= 0).all()
assert (result["ac_power"] >= 0).all()
@patch("akkudoktoreos.prediction.pvforecastpvlib.ModelChain")
@patch("pvlib.modelchain.ModelChain")
def test_dataframe_columns(
self,
mock_modelchain,
@@ -1489,7 +1489,7 @@ class TestCalculation:
assert "pv_dc_power" in result.columns
assert "ac_power" in result.columns
@patch("akkudoktoreos.prediction.pvforecastpvlib.ModelChain")
@patch("pvlib.modelchain.ModelChain")
def test_result_index_preserved(
self,
mock_modelchain,