fix(elecprice): survive a day-ahead source that has not published yet

Every morning before the day-ahead auction is published, /v1/prediction/update
answered 400 and no prediction was produced at all.

The provider asks for prices starting at the run day, because an existing
history sets past_days to 0. The optimization horizon always reaches past the
last published price, so an update is always considered necessary - and SMARD
publishes the next day around midday. Between midnight and publication the
requested window therefore contains nothing, and ElecPriceSMARD raised
"SMARD response contains no usable day-ahead prices", which failed the whole
prediction update rather than only that provider.

ElecPriceEnergyCharts and its SMARD subclass now keep their existing history and
let the ETS/median branch extrapolate the remaining slots, the same fallback
FeedInTariffEnergyCharts already had. A cold start without any history stays
fatal. ElecPriceSMARD also separates the two cases it used to conflate: a period
the source has not published yet now reports the latest value it does have, and
only a response without a single price still reads as unusable.

Fixes the cache noise this produced as well. cache_in_file claimed its cache
entry before calling the wrapped function, so a raising function left an empty
file behind and every later call within the TTL logged "Read failed: Ran out of
input" before refetching. The entry is now created only after the call returns.
This commit is contained in:
Andreas
2026-09-09 12:57:39 +02:00
parent 280ee761e6
commit 983a23f0af
7 changed files with 183 additions and 20 deletions
+31
View File
@@ -560,6 +560,37 @@ class TestCacheFileDecorators:
cache_file.seek(0) # Move to the start of the file
assert cache_file.read() == "Some expensive computation result"
def test_cache_in_file_decorator_discards_the_entry_when_the_call_raises(
self, cache_file_store
):
"""A failing call must not leave an empty cache file behind.
The entry is created before the wrapped function runs, so a raising
function used to leave an empty file that every later call within the TTL
failed to unpickle ("Ran out of input") before falling back to a refetch.
"""
cache_file_store.clear(clear_all=True)
assert len(cache_file_store._store) == 0
calls = []
@cache_in_file(mode="w+")
def failing_function(until_date=None):
calls.append(1)
raise ValueError("upstream has nothing to offer yet")
until = datetime.now() + timedelta(days=1)
with pytest.raises(ValueError, match="upstream has nothing"):
failing_function(until_date=until)
assert len(cache_file_store._store) == 0
# The next call runs the function again and reports the same failure
# rather than a confusing unpickling error from an empty file.
with pytest.raises(ValueError, match="upstream has nothing"):
failing_function(until_date=until)
assert len(calls) == 2
def test_cache_in_file_decorator_uses_cache(self, cache_file_store):
"""Test that the cache_in_file decorator reuses cached file on subsequent calls."""
# Clear store to assure it is empty