Source code for loom.etl.declarative.source._from_config
"""Config value declaration for ETL steps.
:class:`FromConfig` names a key of the config the runner was built from. The
executor resolves it when the step runs and passes the value to ``execute()``
as a keyword argument named like the class attribute. It is not a frame
source: no reader is involved and it never becomes a
:class:`~loom.etl.declarative.source.SourceSpec`.
"""
from __future__ import annotations
[docs]
class FromConfig:
"""Declare a config value injected into ``execute()``.
The key is a dot-separated path into the runner's config (the YAML given
to :meth:`~loom.etl.ETLRunner.from_yaml`), so ``${oc.env:...}``
interpolations and resolvers such as ``${secrets:...}`` apply. The value
is resolved when the step runs, never at import or declaration:
``${oc.env:...}`` is read on every resolution, while a resolver's result
is cached by the config for the lifetime of the runner. Loom stores the
value on no plan and writes it to no log or event; a failure names the
key and the expected type only.
Args:
key: Dot-separated config path, e.g. ``"respondio.api_token"``.
value_type: Type the value is converted to with ``msgspec``: a scalar
such as ``str`` or ``int``, a ``Literal["a", "b"]``, or a
``msgspec.Struct`` for a whole section. Defaults to ``str``.
A null or absent key is always reported as not set.
Raises:
ValueError: When *key* is empty or has an empty segment.
Example::
class FetchMessages(ETLStep[DailyParams]):
api_token = FromConfig("respondio.api_token")
respondio = FromConfig("respondio", RespondioSettings)
target = IntoTemp("messages")
def execute(
self,
params: DailyParams,
*,
api_token: str,
respondio: RespondioSettings,
) -> pl.LazyFrame: ...
"""
__slots__ = ("_key", "_value_type")
def __init__(self, key: str, value_type: object = str) -> None:
if not key or any(not part for part in key.split(".")):
raise ValueError(f"FromConfig key must be a dot-separated path, got {key!r}")
self._key = key
self._value_type = value_type
@property
def key(self) -> str:
"""Dot-separated config path."""
return self._key
@property
def value_type(self) -> object:
"""Type the resolved value is converted to."""
return self._value_type
def __repr__(self) -> str:
return f"FromConfig({self._key!r}, {type_label(self._value_type)})"
def type_label(value_type: object) -> str:
"""Return a readable name for *value_type*, without any value.
Args:
value_type: A class or a typing construct such as ``Literal[...]``.
Returns:
The class name, or the typing construct's ``repr``.
"""
return value_type.__name__ if isinstance(value_type, type) else repr(value_type)