Skip to content

Align horizon_type Literals with server enum (add quarter)#6

Merged
mabesa merged 1 commit into
mainfrom
feature/horizon-type-literal-align
Jun 12, 2026
Merged

Align horizon_type Literals with server enum (add quarter)#6
mabesa merged 1 commit into
mainfrom
feature/horizon-type-literal-align

Conversation

@mabesa

@mabesa mabesa commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Several client methods annotated horizon_type as an inline Literal[...] that had drifted out of sync with each other and with the server. Most omitted "quarter" even though the server's HorizonType enum accepts exactly day, pentad, decade, month, quarter, season, year. A caller passing horizon_type="quarter" through one of those methods got a confusing client-side type/validation error for a value the server accepts (SAPPHIRE INFRA-019).

This PR makes every horizon_type Literal equal to the full server value set and refactors so they cannot drift again.

Single source of truth

validators.py now declares the value set once:

HorizonTypeLiteral = Literal["day", "pentad", "decade", "month", "quarter", "season", "year"]
VALID_HORIZONS: Set[str] = set(get_args(HorizonTypeLiteral))   # derived, cannot drift

The Literal alias drives the static type hints; the runtime validation set is derived from it. HorizonTypeLiteral is exported from the package.

Changed horizon_type declarations

File Method Before After
validators.py:14,19 (new) HorizonTypeLiteral + derived VALID_HORIZONS VALID_HORIZONS was a hand-written set Literal is the source; set derived via get_args
preprocessing.py:109 prepare_runoff_records Literal["day","pentad","decade","month","season","year"] (no quarter) HorizonTypeLiteral
preprocessing.py:214 prepare_hydrograph_records Literal[...] (no quarter) HorizonTypeLiteral
short_term.py:114 prepare_short_term_forecast_records Literal[...] (no quarter) HorizonTypeLiteral
postprocessing.py:95 prepare_short_term_forecast_records (facade) Literal[...] (no quarter) HorizonTypeLiteral
postprocessing_base.py:100 prepare_skill_metric_records Literal[...] (already had quarter) HorizonTypeLiteral (now shared)

Also: removed the now-unused Literal import from short_term.py, postprocessing.py, postprocessing_base.py; updated two preprocessing.py docstrings to list quarter; exported HorizonTypeLiteral in __init__.py.

The shared HorizonTypeLiteral alias in validators.py is the only place the 7-value set is declared.

Separate horizon set — found and intentionally left alone

VALID_LONG_FORECAST_HORIZONS = {"month", "quarter", "season"} is a deliberately narrower set for long-term forecasts. It is not a horizon_type Literal annotation (the long_term.py horizon_type params are plain str, validated at runtime against this narrower set), so it is out of scope and was not widened.

Backward compatibility

This change only widens what the type hints accept. Runtime behavior for existing values, call sites, and signatures is unchanged.

Tests

  • test_validators.py: assert HorizonTypeLiteral equals the full server enum and that VALID_HORIZONS == set(get_args(HorizonTypeLiteral)) (single-source-of-truth check).
  • test_preprocessing.py / test_short_term.py: parametrized tests asserting prepare_runoff_records, prepare_hydrograph_records, and prepare_short_term_forecast_records accept all seven horizons (incl. quarter).
  • Existing runtime-validation tests still reject invalid horizons (e.g. "fortnight") on the read_* methods.

uv run pytest216 passed. uv run mypyclean.

Version

Bumped 0.4.00.5.0 (pyproject + __init__), CHANGELOG updated. Downstream pins by git rev/version, so this is a clean new rev for the INFRA-019 re-pin.

🤖 Generated with Claude Code

Several client methods annotated horizon_type as an inline
Literal[...] that had drifted out of sync: most omitted "quarter"
even though the server's HorizonType enum accepts it (day, pentad,
decade, month, quarter, season, year). A caller passing
horizon_type="quarter" through those methods got a confusing
client-side type/validation error for a value the server accepts
(SAPPHIRE INFRA-019).

Introduce a single shared HorizonTypeLiteral alias in validators.py
as the one place the value set is declared, and derive the runtime
VALID_HORIZONS set from it via typing.get_args so the type hint and
the validation allowlist cannot drift again. Replace every inline
horizon_type Literal with the shared alias and export it from the
package.

The narrower VALID_LONG_FORECAST_HORIZONS set ({month, quarter,
season}) is a deliberately separate concept and is left untouched;
long_term horizon_type params are str (not Literal) and out of scope.

This only widens what the type hints accept; runtime behavior for
existing values is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mabesa mabesa merged commit 4fd543e into main Jun 12, 2026
3 checks passed
@mabesa mabesa deleted the feature/horizon-type-literal-align branch June 12, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant