Source code for cursus.registry.step_names_base
"""
Base step-name registry — the single source of truth for step naming across config,
builders, and specifications.
This is a deliberately **dependency-free leaf module**: it builds the flat step table
``STEP_NAMES`` from the per-step ``.step.yaml`` ``registry:`` blocks (via
``build_registry_from_interfaces``) and derives the ``CONFIG_STEP_REGISTRY`` /
``BUILDER_STEP_NAMES`` / ``SPEC_STEP_TYPES`` mappings. The workspace-aware access layer
(``step_names.py``) and the hybrid ``UnifiedRegistryManager`` (``hybrid/manager.py``) both
read the raw data from here — keeping the data in this leaf (rather than in ``step_names.py``)
is what breaks the otherwise-circular import between the access layer and the manager.
**Source of truth (FZ 31e1/31e1f Final Phase, 2026-06-28):** the standalone
``registry/step_names.yaml`` table was DELETED. The registry is now derived SOLELY from the
``.step.yaml`` ``registry:`` blocks + a 3-row ``_EXTRAS`` map (in ``interface_registry_loader``)
for the interface-less abstract steps (``Base`` / ``Processing`` / ``HyperparameterPrep``).
To add or edit a step, edit its ``.step.yaml`` ``registry:`` block — there is no separate table.
A golden snapshot (``tests/registry/step_names_registry_snapshot.json``) gates drift.
(Formerly ``step_names_original.py``; renamed because it is the live source, not a backup.)
"""
from typing import Dict
# --- Build the step registry from the per-step .step.yaml registry: blocks ---
#
# The lazy import keeps this module a dependency-free leaf — ``interface_registry_loader``
# imports only pathlib/typing/yaml, so no circular import is introduced.
def _build_step_names() -> Dict[str, Dict[str, str]]:
from .interface_registry_loader import build_registry_from_interfaces
return build_registry_from_interfaces()
STEP_NAMES: Dict[str, Dict[str, str]] = _build_step_names()
def _rebuild_derived() -> None:
"""Rebuild the derived mappings from the current ``STEP_NAMES``, in place.
``STEP_NAMES`` is mutated in place (never reassigned) so that modules which did
``from ...step_names_base import STEP_NAMES`` keep pointing at the live dict; the three
derived globals ARE reassigned here, but callers that need workspace-aware values read them
through ``step_names.get_*`` accessors, not these snapshots.
"""
global CONFIG_STEP_REGISTRY, BUILDER_STEP_NAMES, SPEC_STEP_TYPES
CONFIG_STEP_REGISTRY = {
info["config_class"]: step_name for step_name, info in STEP_NAMES.items()
}
BUILDER_STEP_NAMES = {
step_name: info["builder_step_name"] for step_name, info in STEP_NAMES.items()
}
SPEC_STEP_TYPES = {
step_name: info["spec_type"] for step_name, info in STEP_NAMES.items()
}
# Generate the mappings that existing code expects.
CONFIG_STEP_REGISTRY: Dict[str, str] = {}
BUILDER_STEP_NAMES: Dict[str, str] = {}
SPEC_STEP_TYPES: Dict[str, str] = {}
_rebuild_derived()
[docs]
def merge_pack_registry(pack_rows: Dict[str, Dict[str, str]]) -> Dict[str, str]:
"""Merge an external step-pack's registry rows ON TOP of the package registry, in place.
ADD-ONLY overlay: package steps are the floor and are never removed. ``STEP_NAMES`` is
mutated in place (so import-time references stay live) with ``pack_rows`` layered on top,
then the derived globals are rebuilt. This is the LOW-LEVEL merge — the access layer
(:func:`cursus.registry.step_names.refresh_registry`) calls this AND then re-syncs the
hybrid manager so the catalog sees the plugin steps.
Args:
pack_rows: ``{canonical_name: {config_class, builder_step_name, spec_type,
sagemaker_step_type, description}}`` derived from the pack's ``.step.yaml`` files.
Returns:
``{name: "collision"}`` for any pack name that shadowed an EXISTING package step —
the caller decides whether to warn. Empty when every pack row is genuinely new.
"""
collisions = {name: "collision" for name in pack_rows if name in STEP_NAMES}
STEP_NAMES.update(
pack_rows
) # in place — package rows preserved, pack rows added on top
_rebuild_derived()
return collisions