Source code for loom.etl.runtime._config_values
"""Resolution of :class:`~loom.etl.FromConfig` values against a config context.
Shared by the compiler, which checks every declared key before a run, and the
executor, which resolves the values a step receives. Every failure reports
the key and the expected type, never the value: a ``msgspec`` message may
quote the rejected value, so the error is raised outside the handler that
caught it and carries neither a cause nor a context.
"""
from __future__ import annotations
from enum import StrEnum
from typing import Any
import msgspec
from loom.core.config import ConfigContext, ConfigError
from loom.etl.declarative.source._from_config import type_label
[docs]
class ConfigValueFailure(StrEnum):
"""Why a config value could not be supplied."""
MISSING = "is not set"
UNRESOLVED = "cannot be resolved (check its interpolations and resolvers)"
INVALID = "does not validate as"
[docs]
class ConfigValueError(ConfigError):
"""A declared config value is missing, unresolvable or of the wrong type.
The message names the key and the expected type only; neither the value
nor the underlying exception is attached (``__cause__`` and
``__context__`` are ``None``).
Attributes:
key: Dot-separated config path.
failure: Why the value could not be supplied.
"""
def __init__(self, key: str, value_type: object, failure: ConfigValueFailure) -> None:
reason = failure.value
if failure is ConfigValueFailure.INVALID:
reason = f"{reason} {type_label(value_type)!r}"
super().__init__(f"Config key {key!r} {reason}")
self.key = key
self.failure = failure
def resolve_config_value(context: ConfigContext, key: str, value_type: object) -> Any:
"""Resolve *key* in *context* and convert it to *value_type*.
Args:
context: Config the runner was built from.
key: Dot-separated config path.
value_type: Target type for ``msgspec.convert``.
Returns:
The converted value.
Raises:
ConfigValueError: When the key is absent or null, its interpolation
cannot be resolved, or the value does not validate.
"""
value: Any = None
failure = _presence_failure(context, key)
if failure is None:
value, failure = _read_and_convert(context, key, value_type)
if failure is not None:
raise ConfigValueError(key, value_type, failure)
return value
def _read_and_convert(
context: ConfigContext, key: str, value_type: object
) -> tuple[Any, ConfigValueFailure | None]:
"""Return ``(value, None)``, or ``(None, failure)`` without the exception.
The caller raises outside any ``except`` block, so the exception that
may quote the value is released here instead of becoming ``__context__``.
"""
try:
raw = context.section(key, object)
except ConfigError:
return None, ConfigValueFailure.UNRESOLVED
try:
return msgspec.convert(raw, value_type, strict=False), None
except msgspec.ValidationError:
return None, ConfigValueFailure.INVALID
def _presence_failure(context: ConfigContext, key: str) -> ConfigValueFailure | None:
try:
present = context.has(key)
except ValueError:
# OmegaConf reports an interpolation it cannot resolve as a ValueError.
return ConfigValueFailure.UNRESOLVED
return None if present else ConfigValueFailure.MISSING