UX-08 2c: short refusals, error texts and option texts
checks / unit (pull_request) Successful in 8m50s
infra-pin / fresh (pull_request) Successful in 2s

Validator refusals name the setting by its label and key, say what is
wrong and what to change, and keep the rejected value and limit. Option
names and descriptions are short in en and nl; citations moved to DOCS
"For installers" and the test_translations citation check now pins the
instruction on the tab and the record in DOCS. No rule, key, schema,
default or exit code changed. Allowlist 167 -> 43 (log lines only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
glenn schrooyen
2026-09-27 05:55:45 +02:00
co-authored by Claude Opus 5.5
parent e1d0970b23
commit 2c516a032c
26 changed files with 1026 additions and 1236 deletions
+19 -36
View File
@@ -262,9 +262,8 @@ def _minutes(value, field: str) -> int:
parts = text.split(":")
if len(parts) != 2:
raise ProfileInvalid(
f"{field} is {value!r}; it must be \"HH:MM\" local Belgian time, "
f"e.g. \"22:00\". A bare hour or a decimal is refused rather than "
f"guessed at - 7.5 could be 07:30 or 07:50.")
f"{field} is {value!r}. Write it as \"HH:MM\" in Belgian time, "
f"for example \"22:00\".")
try:
h, m = int(parts[0]), int(parts[1])
except ValueError:
@@ -272,9 +271,8 @@ def _minutes(value, field: str) -> int:
f"integers, e.g. \"07:00\"")
if not (0 <= m < 60) or not (0 <= h <= 24) or (h == 24 and m != 0):
raise ProfileInvalid(
f"{field} is {value!r}, which is not a time of day. Hours run 0-23, "
f"minutes 0-59; \"24:00\" is accepted only as an END meaning "
f"midnight at the end of the day.")
f"{field} is {value!r}, which is not a time of day. Use 00:00 to "
f"23:59, or \"24:00\" as an end time.")
return h * 60 + m
@@ -286,11 +284,8 @@ def _days(value, field: str) -> frozenset:
"""
if value is None:
raise ProfileInvalid(
f"{field} is missing. Name the days this period applies to - "
f"\"mon-fri\", \"sat,sun\" or \"all\". It is required rather than "
f"defaulted to every day, because a Belgian night tariff that "
f"silently covered the weekend too would misprice 48 h a week with "
f"nothing reporting it.")
f"{field} is missing. Name the days, for example \"mon-fri\", "
f"\"sat,sun\" or \"all\".")
tokens = []
if isinstance(value, (list, tuple)):
for item in value:
@@ -367,10 +362,8 @@ def _price(value, field: str, means: str = None) -> float:
raise ProfileInvalid(f"{field} is {value!r}, which is not finite")
if abs(out) > MAX_PLAUSIBLE_EUR_KWH:
raise ProfileInvalid(
f"{field} is {out} EUR/kWh, beyond +-{MAX_PLAUSIBLE_EUR_KWH}. The "
f"usual cause is a figure in cents or in EUR/MWh - 34 instead of "
f"0.34 - which would make every hour look like an emergency. Same "
f"bound the fetched feeds are held to (price.py `_validate_raw`).")
f"{field} is {out} EUR/kWh, beyond +-{MAX_PLAUSIBLE_EUR_KWH}. "
f"Write the price in EUR/kWh, for example 0.34 and not 34.")
return out
@@ -422,9 +415,8 @@ class StaticProfile:
self.option = option
if not self.periods:
raise ProfileInvalid(
f"{option} has no periods. A profile with no periods is "
f"not a fixed tariff, it is an absent one - leave the option "
f"empty to say that on purpose (§11.4 then applies).")
f"{option} has no periods. Add at least one, or leave the "
f"setting empty.")
_check_tiling(self.periods, option=option)
# -- what kind of tariff this is, DERIVED --------------------------------
@@ -558,10 +550,7 @@ def _raise_gap(weekday, start, end, option="static_profile"):
raise ProfileInvalid(
f"{option} leaves {DAY_NAMES[weekday]} "
f"{_hhmm(start)}-{_hhmm(end)} with no period, so those hours have no "
f"price. Refused rather than defaulted: an invented price is one an "
f"optimiser would trade on, and §11.4 already says what a missing price "
f"means. Add a period covering it - remember a window written "
f"\"22:00-07:00 mon-fri\" does NOT cover Saturday morning.")
f"price. Add a period that covers them.")
def _raise_overlap(weekday, over, end, periods, option="static_profile"):
@@ -569,9 +558,8 @@ def _raise_overlap(weekday, over, end, periods, option="static_profile"):
names = [p.name for p in periods if p.covers(weekday, start)]
raise ProfileInvalid(
f"{option} gives {DAY_NAMES[weekday]} {_hhmm(start)}-"
f"{_hhmm(end)} {n} prices at once ({', '.join(names)}). Two prices for "
f"one hour means one of them silently wins on list order. Narrow one of "
f"the windows or split the day selector.")
f"{_hhmm(end)} {n} prices at once ({', '.join(names)}). Make the "
f"periods stop overlapping.")
def parse_static_profile(value, *, option: str = "static_profile",
@@ -609,28 +597,23 @@ def parse_static_profile(value, *, option: str = "static_profile",
value = json.loads(text)
except ValueError as e:
raise ProfileInvalid(
f"{option} is not valid JSON ({e}). It is a JSON object "
f"with a \"periods\" list, on one line, e.g.\n"
f"{option} is not valid JSON ({e}). Write it on one line with "
f"a \"periods\" list, for example\n"
f' {{"periods": [{{"name":"day","from":"07:00","to":"22:00",'
f'"days":"mon-fri","eur_per_kwh":0.34}}, ...]}}\n'
f"Leave it empty for a dynamic-only or price-free site.")
f"Leave it empty if you have no fixed tariff.")
if isinstance(value, dict):
tz = str(value.get("timezone", "") or "").strip().lower()
if tz not in SUPPORTED_TZ:
raise ProfileInvalid(
f"{option}.timezone is {value.get('timezone')!r}. Only "
f"Europe/Brussels is supported: the shipped image carries no tz "
f"database (see app/price.py), so any other zone could not be "
f"honoured and would be silently treated as Brussels - which is "
f"wrong by an hour for part of the year.")
f"Europe/Brussels is supported.")
rows = value.get("periods")
if rows is None:
raise ProfileInvalid(
f"{option} has no \"periods\" list. Even a fully-fixed "
f"tariff is written as one period covering 00:00-24:00 all days "
f"- one shape for both products, so nothing downstream branches "
f"on which kind of site this is.")
f"{option} has no \"periods\" list. A fixed price is one "
f"period from 00:00 to 24:00 on all days.")
else:
rows = value