Skip to content

xarray_annotated.temporal

xarray_annotated.temporal

Temporal-frequency domain for xarray-annotated.

Declares and validates the frequency of a DataArray’s time axis — a property derived from the values of its datetime coordinate rather than carried in its metadata, which is why it is a domain of its own rather than a fourth structural marker. Like xarray_annotated.schema (and unlike units) it only asserts: it never converts, resamples, or stamps anything.

A declaration compares on two things: spacing (always) and phase (the End/Begin convention, and the anchor where both sides spell one). So Freq("7D") accepts a weekly axis on any weekday, while Freq("W-SUN") catches the classic resample footgun of a series landing on Wednesdays::

from typing import Annotated
import xarray as xr
from xarray_annotated.temporal import declare_freq, Freq

@declare_freq
def weekly_mean(
    x: Annotated[xr.DataArray, Freq("D")],
) -> Annotated[xr.DataArray, Freq("W-SUN")]:
    return x.resample(time="W-SUN").mean()

freq_compatible is the same comparison with no array in hand, for a build-time check of a producer/consumer edge.

Freq

Freq(
    freq: str,
    *,
    dim: str | None = None,
    anchored: bool | None = None,
    on_mismatch: OnMismatch | None = None,
)

Declare the expected temporal frequency of a DataArray’s time axis.

Freq("7D") declares an array whose datetime coordinate advances in seven-day steps; Freq("W-SUN") declares the same spacing and pins the phase to Sundays. Two independent comparisons follow from the declaration:

  • spacing — always compared ("7D" and "W-WED" have the same spacing);
  • phase — the End/Begin convention ("ME" vs "MS"), always compared; and the anchor ("-WED", "-MAR"), compared only where the declaration spells it. Freq("W") therefore means “weekly, any weekday”, a deliberate divergence from pandas (which would default it to W-SUN).

Pass anchored=True to opt in to pandas’ default anchor anyway (Freq("W", anchored=True) means W-SUN and means it), or anchored=False to suppress the anchor comparison of a spelled-out anchor (Freq("W-SUN", anchored=False) = “weekly, any weekday”).

Pass dim to name the time coordinate explicitly; by default the array’s sole datetime-like coordinate is used, and an array carrying two is ambiguous.

anchored property

anchored: bool | None

Whether the anchor is binding, or None to infer it from the spelling.

dim property

dim: str | None

The declared time dimension, or None to auto-detect it.

freq property

freq: str

The declared frequency string, exactly as spelled.

on_mismatch property

on_mismatch: OnMismatch | None

Per-marker severity override, or None to use the policy default.

FreqError

Bases: Exception

Raised for a frequency mismatch under on_mismatch="error".

Deliberately not a ValueError: a mismatch (the data fails the declaration) is a distinct event from a malformed declaration (an unparseable offset string), which raises ValueError from assert_valid_freq. Keeping them on separate hierarchies means catching one never silently swallows the other.

FreqWarning

Bases: UserWarning

Warning issued for a frequency mismatch or an uninferable time axis.

Policy dataclass

Policy(
    enabled: bool = DEFAULT_ENABLED,
    on_mismatch: OnMismatch = DEFAULT_ON_MISMATCH,
    on_uninferable: OnUninferable = DEFAULT_ON_UNINFERABLE,
)

The resolved temporal-validation policy — the three axes as a single value.

Returned by get_policy. Build overrides via set_policy or the policy context manager rather than constructing this directly.

Attributes:

  • enabled (bool) –

    Master switch; False makes all validation a no-op.

  • on_mismatch (OnMismatch) –

    Default behaviour when the inferred frequency contradicts the declaration ("error", "warn", or "ignore"); a marker may override it.

  • on_uninferable (OnUninferable) –

    Behaviour when no frequency can be inferred from the time axis — too few points, or irregular spacing.

annotated_freq

annotated_freq(hint: Any) -> Freq | None

Return the Freq marker carried by an Annotated hint, or None.

The first Freq marker in the metadata, and only when the annotated base type is a DataArray (incl. DataArray | None). Non-Annotated hints, hints on non-DataArray types, and hints with no Freq marker return None (so walk_signature’s “no declaration” filter drops them).

Parameters:

  • hint (Any) –

    A type hint, typically from get_type_hints(..., include_extras=True).

Returns:

  • Freq | None

    The declared Freq, or None.

assert_valid_freq

assert_valid_freq(marker: Freq, context: str) -> None

Validate a Freq declaration itself, independent of any DataArray.

Used at decoration time to fail fast on a malformed declaration (an unparseable offset string, an empty dim name) rather than only when the decorated function first runs. pandas’ own message is kept — it is the one that explains that the legacy aliases "M" and "H" are now spelled "ME" and "h".

Parameters:

  • marker (Freq) –

    The declared Freq.

  • context (str) –

    A label for the error message (e.g. "f input 'x'").

Raises:

check_freq

check_freq(
    da: DataArray,
    declared: Freq | list[Freq],
    name: str,
    on_mismatch: OnMismatch | None = None,
    on_uninferable: OnUninferable | None = None,
    qualname: str | None = None,
) -> DataArray

Validate da’s time axis against its declared frequency; return it unchanged.

Locates the declared time coordinate (the array’s sole datetime-like coordinate, unless the marker names one), infers its frequency with xarray.infer_freq, and compares that with the declaration via freq_compatible. Because pandas always hands back a fully-anchored string ("W-WED", never "7D"), the declaration alone decides how strict the comparison is — Freq("7D") accepts any weekday, Freq("W-SUN") accepts only Sundays.

Two events, two axes. A frequency that contradicts the declaration is a mismatch, as is an array with no (or an ambiguous) time axis. An axis whose frequency cannot be inferred at all — fewer than three points, or irregular spacing — is uninferable: the declaration was not violated, it was never tested, and by default that warns rather than raises.

Severity resolves per marker as: the marker’s own on_mismatch override, else the on_mismatch argument, else the policy default. When the master switch is off (enabled=False) this is a total no-op.

Parameters:

  • da (DataArray) –

    The DataArray to validate.

  • declared (Freq | list[Freq]) –

    A single Freq or a list of them declared for da.

  • name (str) –

    The parameter/field name, for error messages.

  • on_mismatch (OnMismatch | None, default: None ) –

    Per-call default severity for a mismatch, overriding the policy default but overridden by a marker’s own on_mismatch.

  • on_uninferable (OnUninferable | None, default: None ) –

    Per-call severity for an uninferable axis, overriding the policy default.

  • qualname (str | None, default: None ) –

    Optional qualified function name, prefixed to messages.

Returns:

  • DataArray

    da, unchanged (validation never mutates).

Raises:

  • FreqError

    On a mismatch when the effective severity is "error".

declare_freq

declare_freq(
    func: Callable[..., Any] | None = None,
    *,
    on_mismatch: OnMismatch | None = None,
    on_uninferable: OnUninferable | None = None,
) -> Callable[..., Any]

Apply a function’s signature-declared frequencies at runtime.

Reads the decorated function’s own type annotations once, via freq_from_signature: parameters annotated Annotated[DataArray, Freq(...)] declare an input’s temporal frequency, and a TypedDict/dataclass return (or a bare Annotated[DataArray, Freq(...)] return) declares an output’s.

On each call, under the active Policy (get_policy), the wrapper validates every declared DataArray input via check_freq, runs the wrapped function, then validates every declared output the same way. Nothing is mutated; a mismatch raises/warns/ignores per the effective on_mismatch (a marker’s own override wins over this decorator’s, which wins over the policy default), and an axis whose frequency cannot be inferred at all is reported under on_uninferable. When the policy is disabled the wrapper is a total no-op.

Usable bare (@declare_freq) or called (@declare_freq(on_mismatch=...)). Every declaration is validated at decoration time (assert_valid_freq), so a malformed offset string fails fast at import.

Parameters:

  • func (Callable[..., Any] | None, default: None ) –

    The function to decorate (when used bare); None when parametrised.

  • on_mismatch (OnMismatch | None, default: None ) –

    Default mismatch severity for this function, overriding the policy default. None (default) resolves per call from get_policy.

  • on_uninferable (OnUninferable | None, default: None ) –

    Severity for an uninferable time axis, overriding the policy default. None (default) resolves per call from get_policy.

Returns:

  • Callable[..., Any]

    A wrapped function that validates declared inputs and outputs.

freq_compatible

freq_compatible(a: Freq, b: Freq) -> bool

Return whether two Freq declarations can describe the same time axis.

The marker-vs-marker counterpart to check_freq (no array in hand), for a static / build-time check of a producer/consumer edge. Two declarations are compatible when their spacing is the same and their phase does not conflict — where “conflict” needs both sides to determine the thing being compared, so a declaration that does not spell an anchor never conflicts on one.

Parameters:

  • a (Freq) –

    A Freq marker.

  • b (Freq) –

    Another Freq marker.

Returns:

  • bool

    True unless the two are provably inconsistent.

Raises:

  • ValueError

    If either frequency string is unparseable.

Examples:

>>> from xarray_annotated.temporal import Freq, freq_compatible
>>> freq_compatible(Freq("7D"), Freq("W-WED"))  # same spacing, no anchor on 7D
True
>>> freq_compatible(Freq("W-SUN"), Freq("W-WED"))  # the resample-phase footgun
False
>>> freq_compatible(Freq("W"), Freq("W-WED"))  # "W" spells no anchor
True
>>> freq_compatible(Freq("QE"), Freq("3ME"))  # same spacing and convention
True
>>> freq_compatible(Freq("ME"), Freq("MS"))  # month-end is not month-start
False

freq_from_signature

freq_from_signature(
    func: object,
) -> tuple[dict[str, Freq], dict[str, Freq] | Freq | None]

Extract declared frequencies from a function’s type annotations.

Parameters:

  • func (object) –

    A callable whose hints carry Annotated[DataArray, Freq(...)].

Returns:

  • dict[str, Freq]

    An (inputs, output) pair, where each declaration is that parameter’s

  • dict[str, Freq] | Freq | None

    Freq; output is a per-field dict for a TypedDict or dataclass return,

  • tuple[dict[str, Freq], dict[str, Freq] | Freq | None]

    a single Freq for one declared DataArray return, or None.

get_policy

get_policy() -> Policy

Resolve the active temporal policy (env → process → default, per axis).

Returns:

  • Policy

    The resolved Policy.

policy

policy(
    *,
    enabled: bool | _Unset | None = _UNSET,
    on_mismatch: OnMismatch | _Unset | None = _UNSET,
    on_uninferable: OnUninferable | _Unset | None = _UNSET,
)

Temporarily override temporal-policy axes, restoring them on exit.

Sets every axis you pass in one go, and restores the previous process overrides afterwards even if an exception is raised.

Parameters:

  • enabled (bool | _Unset | None, default: _UNSET ) –

    Override the (package-wide) master switch, or None to clear.

  • on_mismatch (OnMismatch | _Unset | None, default: _UNSET ) –

    Override the on-mismatch axis, or None to clear.

  • on_uninferable (OnUninferable | _Unset | None, default: _UNSET ) –

    Override the on-uninferable axis, or None to clear.

set_policy

set_policy(
    *,
    enabled: bool | _Unset | None = _UNSET,
    on_mismatch: OnMismatch | _Unset | None = _UNSET,
    on_uninferable: OnUninferable | _Unset | None = _UNSET,
) -> None

Set process-wide temporal-policy overrides for one or more axes.

Only the axes you pass are changed. Pass a value to set it, None to clear that axis’s override (so its env var / default applies again), or omit it to leave it untouched.

Parameters:

  • enabled (bool | _Unset | None, default: _UNSET ) –

    Override the (package-wide) master switch, or None to clear.

  • on_mismatch (OnMismatch | _Unset | None, default: _UNSET ) –

    Override the on-mismatch axis, or None to clear.

  • on_uninferable (OnUninferable | _Unset | None, default: _UNSET ) –

    Override the on-uninferable axis, or None to clear.