Source code for ClearMap.config.config_adjusters.adjusters_api

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Optional, Tuple, Iterable, List, Callable

from ClearMap.config.config_adjusters.type_hints import (AdjusterFn, ConfigKeys, ConfigKeysLike,
                                                         AdjusterScope)


[docs] class Phase(str, Enum): PRE_VALIDATE = 'pre-validate' POST_VALIDATE = 'post-validate' PRE_COMMIT = 'pre-commit' POST_COMMIT = 'post-commit'
[docs] class Step(str, Enum): # Creation/materialization steps APPLY_RENAMES = 'apply-renames' # sample/channel_x → renamed channels CREATE_PIPELINE_SECTIONS = 'create-pipeline-sections' # pipeline-level blocks # Instance owners CREATE_CHANNELS_RECONCILE = 'create-channels-reconcile' # add missing channels # Population steps ENSURE_STATIC_BLOCKS = 'ensure-static-blocks' # fill missing keys with defaults POPULATE_DEFAULTS_MATERIALIZE = 'populate-defaults-materialize' # FIXME: rename to more explicit step name or use an existing one ADJUST = 'adjust' # custom adjustments
# Normalization steps (currently unused) # NORMALIZE_SHAPES = 'normalize-shapes' # scalars↔vectors, sentinels, etc.
[docs] class AdjusterKind(str, Enum): STATIC_FILLER = "static_filler" INSTANCE_OWNER = "instance_owner" OTHER = "other"
[docs] @dataclass(frozen=True) class AdjusterSpec: name: str fn: AdjusterFn step: Step phase: Phase watched_keys: Optional[Tuple[ConfigKeys, ...]] # run only if these prefixes match changed keys owned_keys: Optional[Tuple[ConfigKeys, ...]] # which keys this adjuster may modify kind: AdjusterKind = AdjusterKind.OTHER scope: AdjusterScope = AdjusterScope.EXPERIMENT requires_sample_manager: bool = True order: int = 100 # lower runs first. Alphabetically if identical.
[docs] def to_config_keys(x: ConfigKeysLike) -> ConfigKeys: return x if isinstance(x, tuple) else tuple(x.split('.'))
[docs] def to_config_keys_tuples(xs: Optional[Iterable[ConfigKeysLike]]) -> Optional[Tuple[ConfigKeys]]: if xs is None: return None out = tuple(to_config_keys(x) for x in xs) return out or None # empty -> None
[docs] def patch_adjuster(*, step: Step | str, phase: Phase | str = Phase.PRE_VALIDATE, scope: AdjusterScope | str = AdjusterScope.EXPERIMENT, requires_sample_manager: bool = True, watched_keys: Optional[Iterable[ConfigKeysLike]] = None, owned_keys: Optional[Iterable[ConfigKeysLike]] = None, kind: AdjusterKind | str = AdjusterKind.OTHER, order: int = 100) -> Callable[[AdjusterFn], AdjusterFn]: """ Decorator for adjusters. Decorate a pure, idempotent (view, sm) -> patch function. Parameters ---------- step: Step | str The step (one of APPLY_RENAMES, CREATE_PIPELINE_SECTIONS, CREATE_CHANNELS_RECONCILE, ENSURE_STATIC_BLOCKS, POPULATE_DEFAULTS_MATERIALIZE, ADJUST) at which to run this adjuster. phase: Phase | str The lifecycle phase (one of PRE_VALIDATE, POST_VALIDATE, PRE_COMMIT, POST_COMMIT) at which to run this adjuster. watched_keys: Optional[Iterable[ConfigKeysLike]] If provided, only run this adjuster if any of these config key prefixes overlap with the changed keys in the current adjustment context. owned_keys: Optional[Iterable[ConfigKeysLike]] The config key prefixes that this adjuster may modify. Attempting to modify keys outside this set is an error. kind: AdjusterKind | str The kind of adjuster (STATIC_FILLER, INSTANCE_OWNER, OTHER). order: int The order in which to run this adjuster relative to others. This is to break ties when multiple adjusters are registered for the same step/phase. Lower order runs first. """ # FIXME: document args step = Step(step) if isinstance(step, str) else step phase = Phase(phase) if isinstance(phase, str) else phase kind = AdjusterKind(kind) if isinstance(kind, str) else kind def _wrap(fn: AdjusterFn) -> AdjusterFn: _REGISTRY.append( AdjusterSpec( name=fn.__name__, fn=fn, step=step, phase=phase, watched_keys=to_config_keys_tuples(watched_keys), owned_keys=to_config_keys_tuples(owned_keys), kind=kind, scope=scope, requires_sample_manager=requires_sample_manager, order=order)) return fn return _wrap
_REGISTRY: List[AdjusterSpec] = [] def _config_keys_overlap(changed_keys: Optional[Iterable[ConfigKeysLike]], watched_prefixes: Optional[Tuple[ConfigKeysLike, ...]]) -> bool: """ True if `changed` touches any `keys` prefix. Example: changed=["registration.channels.Ch488.align_with"] keys=("registration.channels",) -> True keys=("stitching.channels",) -> False If `keys` is None or `changed` is None/empty, return True (do not filter). """ changed_t = to_config_keys_tuples(changed_keys) watched_prefixes_t = to_config_keys_tuples(watched_prefixes) if not watched_prefixes_t or not changed_t: return True for changed_k in changed_t: for watched_k in watched_prefixes_t: if len(changed_k) >= len(watched_k) and changed_k[:len(watched_k)] == watched_k: return True return False