@reducer is here

This commit is contained in:
lda
2026-05-17 18:33:03 +07:00 Verified
parent 171fe9087d
commit 4339b8aa74
7 changed files with 287 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
from .catalog import ReducerCatalog
from .decorator import AuthoredReducer, reducer
__all__ = [
"AuthoredReducer",
"ReducerCatalog",
"reducer",
]
+15
View File
@@ -0,0 +1,15 @@
from __future__ import annotations
from typing import Any, Protocol, TypeVar
from pydantic import BaseModel
ConfigT = TypeVar("ConfigT", bound=BaseModel)
class PlainReducerCallable(Protocol):
def __call__(self, current: Any, incoming: Any, /) -> Any: ...
class ConfigReducerCallable(Protocol[ConfigT]):
def __call__(self, current: Any, incoming: Any, config: ConfigT, /) -> Any: ...
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from dataclasses import dataclass
from wf_core import ReducerSpec
from wf_core.runtime.ops.merges import ReducerDefinition
from .decorator import AuthoredReducer
@dataclass(frozen=True, slots=True)
class ReducerCatalog:
"""Collection of authored reducers ready for runtime and inventory use."""
definitions: dict[str, ReducerDefinition]
@classmethod
def from_reducers(cls, *reducers: AuthoredReducer) -> "ReducerCatalog":
return cls(
definitions={
reducer.definition.spec.name: reducer.definition
for reducer in reducers
}
)
@property
def specs(self) -> dict[str, ReducerSpec]:
return {
name: definition.spec for name, definition in self.definitions.items()
}
+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any, TypeVar, cast, overload
from pydantic import BaseModel
from wf_core import ReducerSpec
from wf_core.runtime.ops.merges import ReducerDefinition
from .callables import ConfigReducerCallable, ConfigT, PlainReducerCallable
PlainFnT = TypeVar("PlainFnT", bound=PlainReducerCallable)
@dataclass(frozen=True, slots=True)
class AuthoredReducer:
"""Authoring wrapper for one reducer implementation."""
definition: ReducerDefinition
@overload
def reducer(
fn: PlainFnT,
/,
*,
name: str | None = None,
description: str | None = None,
) -> AuthoredReducer: ...
@overload
def reducer(
*,
name: str,
description: str | None = None,
) -> Callable[[Callable[..., Any]], AuthoredReducer]: ...
@overload
def reducer(
*,
name: str,
config_model: type[ConfigT],
description: str | None = None,
) -> Callable[[Callable[..., Any]], AuthoredReducer]: ...
def reducer(
fn: Callable[..., Any] | None = None,
/,
*,
name: str | None = None,
config_model: type[BaseModel] | None = None,
description: str | None = None,
) -> AuthoredReducer | Callable[[Callable[..., Any]], AuthoredReducer]:
"""Wrap a Python reducer function as a runtime reducer definition."""
def decorate(raw: Callable[..., Any]) -> AuthoredReducer:
reducer_name = name or raw.__name__
reducer_description = description or raw.__doc__
if config_model is None:
return AuthoredReducer(
ReducerDefinition(
spec=ReducerSpec(
name=reducer_name,
description=_clean_doc(reducer_description),
),
fn=raw,
)
)
model_type = config_model
def runtime_fn(
current: Any,
incoming: Any,
config: Mapping[str, Any],
) -> Any:
parsed = model_type.model_validate(config)
return cast(ConfigReducerCallable[BaseModel], cast(object, raw))(
current,
incoming,
parsed,
)
return AuthoredReducer(
ReducerDefinition(
spec=ReducerSpec(
name=reducer_name,
description=_clean_doc(reducer_description),
config_schema=config_model.model_json_schema(),
),
fn=runtime_fn,
accepts_config=True,
)
)
if fn is not None:
return decorate(fn)
return decorate
def _clean_doc(doc: str | None) -> str | None:
if doc is None:
return None
cleaned = doc.strip()
return cleaned or None