Source code for loom.etl.pipeline._step_client
"""Client-mode ETL step base type.
:class:`ClientStep` is a specialisation of :class:`~loom.etl.ETLStep` for steps
that execute engine commands directly via the native client — DDL statements,
maintenance operations, partition management, and similar side-effect-only work.
Unlike a regular step, a ``ClientStep``:
* declares **no sources** — there are no DataFrame frames to read.
* declares **no frame output** — its target is always :class:`~loom.etl.IntoClient`,
which signals the executor to skip the normal read-execute-write path.
* receives the engine client as a ``client`` keyword argument in ``execute()``.
The concrete client type depends on the backend configured in the runner:
* ClickHouse runner → ``clickhouse_connect.driver.Client`` (native HTTP client)
To use ``ClientStep``, wire the runner with a
:class:`~loom.etl.runtime.contracts.ClientCommandExecutor` implementation
(e.g. :class:`~loom.etl.io.ClickHouseClientExecutor`)::
from loom.etl import ETLRunner
from loom.etl.io import ClickHouseClientExecutor
runner = ETLRunner(
reader=...,
writer=...,
client_executor=ClickHouseClientExecutor(url="clickhouse://user:pass@host:8123/db"),
)
Example step::
from loom.etl import ClientStep, ETLParams
class DailyParams(ETLParams):
run_date: date
class OptimizeOrders(ClientStep[DailyParams]):
def execute(self, params: DailyParams, *, client: Any) -> None:
client.command("OPTIMIZE TABLE orders FINAL")
"""
from __future__ import annotations
import typing
from typing import Any, ClassVar, Generic, TypeVar, cast
from loom.etl.declarative.target import IntoFile, IntoHistory, IntoTable, IntoTemp
from loom.etl.declarative.target._client import IntoClient
from loom.etl.pipeline._step import ETLStep
ParamsT = TypeVar("ParamsT")
[docs]
class ClientStep(ETLStep[ParamsT], Generic[ParamsT]):
"""Base class for engine-client steps with no DataFrame input or output.
Subclass and implement :meth:`execute`. The executor injects the engine
client via the ``client`` keyword argument — declare it explicitly in your
overriding signature.
The ``target`` class variable is pre-set to :class:`~loom.etl.IntoClient`
and must not be overridden in subclasses.
Args:
ParamsT: Typed params struct for this step, matching the enclosing
:class:`~loom.etl.ETLProcess` / :class:`~loom.etl.ETLPipeline`.
Raises:
TypeError: At class-definition time if a subclass overrides ``target``
with anything other than an :class:`~loom.etl.IntoClient` instance.
Example::
class RebuildAggregation(ClientStep[DailyParams]):
def execute(self, params: DailyParams, *, client: Any) -> None:
client.command(
f"INSERT INTO aggregation_daily "
f"SELECT * FROM staging_daily WHERE dt = '{params.run_date}'"
)
"""
target: ClassVar[IntoTable | IntoHistory | IntoFile | IntoTemp | IntoClient | None] = (
IntoClient()
)
def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
# ETLStep.__init_subclass__ extracts _params_type from ETLStep[T], but
# ClientStep subclasses declare ClientStep[T] — not ETLStep[T] directly.
# Override _params_type here, mirroring the StepSQL pattern.
params_type = _extract_client_step_params(cls)
if params_type is not None:
cls._params_type = params_type
if "target" in cls.__dict__ and not isinstance(cls.__dict__["target"], IntoClient):
raise TypeError(
f"{cls.__qualname__}: 'target' must remain IntoClient() on ClientStep "
"subclasses — ClientStep produces no DataFrame output. "
"Remove the 'target' override or use ETLStep instead."
)
[docs]
def execute(self, params: Any, **kwargs: Any) -> None:
"""Execute engine commands via the injected client.
Must be overridden. Declare ``*, client: <ClientType>`` explicitly:
Args:
params: Typed params instance for this run.
**kwargs: The executor injects ``client=<engine_client>`` here at
runtime. Declare it as ``*, client: <ClientType>`` in your
implementation signature.
Returns:
None — client steps produce no DataFrame output.
Raises:
NotImplementedError: When the subclass has not implemented this method.
"""
raise NotImplementedError(f"{type(self).__qualname__} must implement execute()")
def _extract_client_step_params(cls: type) -> type | None:
"""Extract the ParamsT type argument from ClientStep[ParamsT] in orig_bases."""
for base in getattr(cls, "__orig_bases__", ()):
origin = getattr(base, "__origin__", None)
if origin is ClientStep:
args = typing.get_args(base)
if args:
return cast(type, args[0])
return None