cursus.core.base.builder_templates

Template-based step builder + construction-verb handlers (Phase 0c of the simplification).

This module introduces a single TemplateStepBuilder facade and a set of construction-verb PatternHandler strategies, selected at build time by resolve_handler(sagemaker_step_type, step_assembly). The goal is to replace the 44 hand-written <Name>StepBuilder classes with thin shells that declare only STEP_NAME and inherit everything else.

Status: Phase S3 in progress — wired, 2/45 builders are live shells. This facade IS now routed: TabularPreprocessingStepBuilder and BatchTransformStepBuilder are live shells that construct through it; the remaining 43 stay hand-written until their byte-diff-gated batch migrates (see the plan’s Phase S3). All 5 construction-verb handlers (ProcessingHandler, TrainingHandler, ModelCreationHandler, TransformHandler, SDKDelegationHandler) are fully implemented + covered by session-independent parity suites in tests/core/base/.

Design notes (verified against source):
  • 6 construction verbs, but Processing-2A and Processing-2B collapse to ONE ProcessingHandler with use_step_args / split_source_dir knobs (NI-1) — they share get_inputs/get_outputs verbatim and differ only in build_step.

  • The facade does NOT impose a fixed make_compute→inputs→outputs→build order — it hands the merged inputs/outputs to handler.build_step and lets the handler orchestrate, because Transform runs make_compute last (NI-3) and ModelCreation drops caching (NI-4).

  • sagemaker_step_type alone selects the handler for 5 verbs; only Processing needs the step_assembly sub-discriminator (code | step_args | delegation).

class TemplateStepBuilder(config, sagemaker_session=None, role=None, registry_manager=None, dependency_resolver=None, spec=None)[source]

Bases: StepBuilderBase

Routed builder facade — the single concrete parent of the (future) 2-line shells.

A shell declares only its registry key:

class TabularPreprocessingStepBuilder(TemplateStepBuilder):
    STEP_NAME = "TabularPreprocessing"

The facade keeps the exact 5-kwarg __init__ contract the PipelineAssembler calls (builder_base.__init__), binds a handler via resolve_handler from the step’s sagemaker_step_type (+ step_assembly), and implements the abstract methods (_get_inputs/_get_outputs/create_step) by delegating to that handler. It is a StepBuilderBase subclass, so the assembler/catalog contract and the discovery hierarchy hold.

Wired and live (Phase S3): TabularPreprocessingStepBuilder and BatchTransformStepBuilder are real shells routing through this facade; the rest migrate per Phase S3 (byte-diff-gated).

STEP_NAME: str | None = None

Subclasses set this to their canonical registry step name (drives spec load + routing). The slot itself is declared on StepBuilderBase (the root that reads it in _get_step_name, FZ 31e1d3g3 Phase C1); re-stated here for local readability of the shell contract.

STEP_ASSEMBLY: str | None = None

Optional explicit step_assembly for Processing steps (code | step_args | delegation). If None, defaults to “code” for Processing (see resolve_handler).

HANDLER_KNOBS: Dict[str, Any] = {}

Per-step handler knobs (direct_input_keys, split_source_dir, include_job_type_in_path, …).

validate_configuration()[source]

Validate builder-context configuration requirements (optional hook).

No-op by default. The Pydantic config class is the authority for config validation — required fields, @field_validator / @model_validator constraints, and defaults are all enforced at config construction, BEFORE the builder runs. A config that constructs is valid by definition, so most builders need no override here (FZ 31e1d3e). Override ONLY to assert an invariant the config genuinely cannot express — one involving builder context (self.role / self.session / self.spec / resolved dependencies) or a cross-field rule not yet on the config model.

create_step(**kwargs)[source]

Create pipeline step.

This method should be implemented by all step builders to create a SageMaker pipeline step. It accepts a dictionary of keyword arguments that can be used to configure the step.

Common parameters that all step builders should handle: - dependencies: Optional list of steps that this step depends on - enable_caching: Whether to enable caching for this step (default: True)

Step-specific parameters should be extracted from kwargs as needed.

Parameters:

**kwargs (Any) – Keyword arguments for configuring the step

Returns:

SageMaker pipeline step

Return type:

Step

class PatternHandler(knobs=None)[source]

Bases: ABC

Base strategy for one construction verb.

A handler is stateless config: it holds per-step declarative knobs and receives the owning TemplateStepBuilder (b) on each call, reading b.config / b.spec / b.contract / b.role / b.session and calling base helpers (b._get_step_name(), b._get_base_output_path(), b._get_cache_config(), b.extract_inputs_from_dependencies(), b.log_info …).

requires_config_fields: tuple = ()

Config attributes this handler reads off b.config at BUILD time that are NOT expressible as a .step.yaml descriptor (FZ 31e1d3g3 Phase D1 / OQ 31e1d3h1). The B3 RegistryBinding validator unions this with the descriptor-derived attrs (compute *_field names, contract.input_source_overrides values) to check config-field COVERAGE — i.e. that the resolved config class actually supplies every field the bound handler will getattr at build time. Declared as DATA (not scraped from source) because some reads use a runtime attr name (getattr(b.config, attr) with attr from the contract) that is statically undecidable. Empty for handlers that read only spec/contract, not config.

abstractmethod build_step(b, **kwargs)[source]
get_inputs(b, inputs)[source]
get_outputs(b, outputs)[source]
class ProcessingHandler(knobs=None)[source]

Bases: PatternHandler

Processing verb — covers both 2A (code=) and 2B (processor.run()→step_args).

Knobs:
  • use_step_args (bool): 2B if True (processor.run() then ProcessingStep(step_args=...)), else 2A (ProcessingStep(code=...)).

  • split_source_dir (bool): for 2B, split get_script_path() into source_dir + code=entry_point.

  • include_job_type_in_path (bool): whether config.job_type is a path segment. (The output-prefix segment itself is DERIVED from the step name — canonical_to_snake — not a knob; FZ 31e1d3f1b.)

NOTE: make_compute (the processor factory) and get_environment_variables are per-step and currently still live on each builder; the migration re-homes them via a make_compute knob/hook. Until then a routed Processing step must supply its processor factory. This handler implements the shared get_inputs/get_outputs join and the build_step shape; the processor-factory wiring is completed per-step in the Phase-2 Batch-A/C work.

get_inputs(b, inputs)[source]
get_outputs(b, outputs)[source]
build_step(b, **kwargs)[source]
class TrainingHandler(knobs=None)[source]

Bases: PatternHandler

Training verb — builds a TrainingStep(estimator, inputs=channels).

Re-homed from builder_xgboost_training_step.py (3 of 4 builders use the path-parts channel parser; PyTorch keeps its own _get_inputs override). Distinctive:

  • get_inputs returns Dict[str, TrainingInput] keyed by channel name; input_path fans out to train/val/test; hyperparameters_s3_uri skipped when config.skip_hyperparameters_s3_uri.

  • get_outputs returns a single str/Join output_path (not a list).

  • build_step ORDER: get_inputs → empty-guard → get_outputs → make_compute (the estimator is created WITH output_path threaded in, so outputs run BEFORE compute).

  • caching is STANDARD (cache_config=_get_cache_config(enable_caching)).

KNOBS = (KnobSpec(name='make_compute', type='callable', default=None, required=False, doc='estimator factory; defaults to builder._create_estimator'), KnobSpec(name='direct_input_keys', type='list', default=['input_path'], required=False, doc='direct input keys (input_path threads in)'))
DEFAULT_FANOUT_CHANNELS = ('train', 'val', 'test')

Fallback sub-channels for a fan-out input when the .step.yaml does not declare contract.inputs.<name>.channels (back-compat for not-yet-annotated interfaces). The source of truth is the YAML channels list — see channels_for.

classmethod channels_for(logical_name, container_path, declared_channels=None)[source]

The SageMaker training channel name(s) a dependency maps to — the SINGLE SOURCE of the channel rule, shared by get_inputs (runtime) and the steps io tool (static).

Priority: (1) the channels declared on the input in the .step.yaml (per-step DATA); (2) for the conventional input_path with no declaration, the back-compat DEFAULT_FANOUT_CHANNELS; (3) the parts[5] of an /opt/ml/input/data/<channel> container path; (4) the logical name itself. No resolved input value is needed.

get_inputs(b, inputs)[source]
get_outputs(b, outputs)[source]
build_step(b, **kwargs)[source]
class ModelCreationHandler(knobs=None)[source]

Bases: PatternHandler

ModelCreation verb — builds a CreateModelStep(model=...).

Distinctive (re-homed from builder_xgboost_model_step.py / builder_pytorch_model_step.py):
  • get_inputs is a single-key {"model_data": ...} passthrough (NOT a spec×contract join, NOT the DummyTraining 3-tier resolution); raises if model_data absent.

  • get_outputs returns None (CreateModelStep auto-exposes properties.ModelName).

  • make_compute (the model factory) runs LAST, consuming model_data.

  • caching is DROPPED — CreateModelStep takes no cache_config; warn on enable_caching=True and pass no cache config (the inverse of every other handler).

get_inputs(b, inputs)[source]
get_outputs(b, outputs)[source]
build_step(b, **kwargs)[source]
class TransformHandler(knobs=None)[source]

Bases: PatternHandler

Transform verb — builds a TransformStep(transformer, inputs=TransformInput).

Distinctive (re-homed from builder_batch_transform_step.py):
  • get_inputs returns a 2-tuple (TransformInput, model_name) — spec-only (no contract), with hard-coded model_name/processed_data logical-name dispatch.

  • get_outputs returns a single str/Join (not a list).

  • make_compute (the Transformer factory) runs LAST — it consumes both model_name (from inputs) and output_path (from outputs).

  • caching is guarded (cache_config=... if enable_caching else None).

requires_config_fields: tuple = ('job_type',)

Config fields the Transform build genuinely DEPENDS on. The Transform read-sites (builder_templates.py:691-695) also touch content_type/split_type/join_source/input_filter/ output_filter, but those are all OPTIONAL on BatchTransformStepConfig (they carry defaults, so a missing value can’t break the build), so they are NOT coverage requirements. Only job_type is required (no default) and is read at :718 — its absence WOULD break the build.

get_inputs(b, inputs)[source]
get_outputs(b, outputs)[source]
build_step(b, **kwargs)[source]
class SDKDelegationHandler(knobs=None)[source]

Bases: PatternHandler

SDKDelegation verb — instantiate a SAIS SDK MODSPredefinedProcessingStep subclass directly.

Covers Cradle / Redshift / DataUploading / Registration (re-homed from their builders). The SDK step builds its own processor/inputs internally, so there is no make_compute. The SDK step class is injected via the sdk_step_class knob (the SAIS SDK can’t be imported at registration time). Three input_mode``s: ``none ([] — Cradle/Redshift), resolve_s3 (DataUploading — resolve the input S3, pass as input_s3_location=), mims_ordered (Registration — an ordered ProcessingInput list, PackagedModel first). get_inputs returns (processing_inputs, resolved_s3).

get_inputs(b, inputs)[source]
get_outputs(b, outputs)[source]
build_step(b, **kwargs)[source]
resolve_handler(sagemaker_step_type, step_assembly=None, knobs=None)[source]

Select and instantiate the construction-verb handler for a step.

Routing is by sagemaker_step_type ONLY (never by step name — DummyTraining is Processing and must route as Processing). Processing is sub-discriminated by step_assembly (code | step_args | delegation, default code). All routing data lives in the strategy_registry; this function only maps the runtime args onto an (axis, name) lookup and merges the registry’s preset knobs under the caller’s knobs.

exception NoBuilderError[source]

Bases: ValueError

Raised when an (axis, name) has no routable strategy (abstract / builder-less type).