Migrating to 2.x¶
The 2.x line is the result of a multi-release specification-unification and
classless-factory rewrite that ran from 1.8.0 through the current 2.x line
(the folder-deletion tranche landed in 2.5.0; the head of the line is 2.8.x). It
removed a large amount of hand-maintained per-step code and replaced it with a
single declarative artifact per step (<step>.step.yaml) plus runtime synthesis.
The single most important fact for most readers:
The public pipeline-authoring API is unchanged.
PipelineDAGCompiler(and its.compile()/.compile_with_report()methods) andcompile_dag_to_pipeline(aliasedcompile_dag) behave identically to 1.x. The same DAG + the same config compiles to the same SageMaker pipeline. If your code only imports those, you have nothing to change.
Everything else below matters only if you imported per-step internals
(builder / spec / contract modules), used the old class-based pipeline catalog,
or imported the removed cursus.workspace module.
See also: Compilation, Step interfaces, Pipeline catalog, Registry & discovery.
The invariance guarantee¶
The rewrite was explicitly designed to be invisible at the public API. The
DAG compiler, the assembler, the dependency resolver, and pipeline.definition()
produce the same result for any given DAG + config, because:
The assembler instantiates a builder by calling
builder_cls(**kwargs)with noisinstancegate. A runtime-synthesized builder class is indistinguishable from a hand-written one at that call site.The step-to-step wiring graph keys entirely on
.step.yamlspec data (step_type,compatible_sources,property_path,logical_name) carried onbuilder.spec, plus the DAG node name — never on a Python class. So collapsing the 45 per-step builder classes into one shared facade leaves every edge intact.
A full compile of a real 11-node pipeline was confirmed to produce the same pipeline definition with only the per-step builder source gone.
What did NOT change¶
Surface |
Status |
|---|---|
|
Unchanged |
|
Unchanged |
|
Unchanged |
|
Unchanged |
Your |
Unchanged |
The generated SageMaker pipeline definition for a given DAG + config |
Unchanged |
# This is the supported authoring API. It works identically in 1.x and 2.x.
from cursus import PipelineDAGCompiler
from cursus.api.dag import PipelineDAG
dag = PipelineDAG()
# ... add nodes / edges ...
compiler = PipelineDAGCompiler(config_path="config.json")
pipeline = compiler.compile(dag, pipeline_name="fraud-detection")
What changed across the 1.x -> 2.x arc¶
The rewrite landed in four themes. The table maps each to the release that introduced it and the sections below.
Theme |
First landed |
Breaking? |
|---|---|---|
Spec/contract merged into one |
1.8.0 |
Only for internal spec/contract imports |
Class-based pipeline catalog replaced by data-driven catalog |
1.8.0 |
Yes, if you used the old catalog classes |
|
1.8.0 |
Only for external |
Per-step |
2.0.0 |
Only for per-step builder imports |
|
2.0.0 |
Only if you referenced that member |
|
2.5.0 |
Yes — the lazy import shim is gone |
1. Spec-unification: one .step.yaml per step (1.8.0)¶
The legacy per-step Python pairs — a *_contract.py (ScriptContract, the I/O)
and a *_spec.py (StepSpecification, the demand/supply) — were merged into a
single declarative <step>.step.yaml file under steps/interfaces/, loaded
into one Pydantic StepInterface. Contract-to-spec alignment became a
construction-time invariant (a @model_validator raises if the contract’s
paths are not a subset of the spec’s dependencies/outputs) instead of a separate
runtime validation tier.
The unified .step.yaml superseded every per-step *_spec.py + *_contract.py
pair (90+ specs and 40+ contracts under steps/specs/ and steps/contracts/);
core/base/specification_base.py was also removed (replaced by
core/base/step_interface.py and core/base/step_contract.py). The physical
*_spec.py / *_contract.py files lingered as import shims until 2.5.0, when
their folders were deleted outright (see §3 below).
If you loaded a step’s contract or spec, use the catalog instead:
from cursus.step_catalog import StepCatalog
catalog = StepCatalog() # package-only discovery
iface = catalog.get_step_interface("TabularPreprocessing")
# iface carries the unified contract + spec (StepInterface)
2. Classless-factory: builders are synthesized, not written (2.0.0)¶
All 45 hand-written builder_*.py step-builder classes were deleted. A step
builder is now a single shared facade class, TemplateStepBuilder
(core/base/builder_templates.py), whose per-step behavior is routed by five
construction-strategy handlers — ProcessingHandler, TrainingHandler,
ModelCreationHandler, TransformHandler, and SDKDelegationHandler — that all
subclass a common PatternHandler ABC. The facade picks a handler at build time
via resolve_handler(sagemaker_step_type, step_assembly): routing is keyed on the
interface’s registry.sagemaker_step_type and, for Processing, the
patterns.step_assembly (code / step_args / delegation). Per-step
differences that used to live in Python are now declarative knobs read from
the .step.yaml patterns: section.
At runtime, for any registry step with a .step.yaml interface and no physical
builder file, the catalog fabricates the class on demand
(step_catalog/builder_discovery.py::_synthesize_builder), roughly:
type(f"{Name}StepBuilder", (TemplateStepBuilder,), {"STEP_NAME": Name})
and caches it per process. The registry (STEP_NAMES and its derived maps) is no
longer a maintained table — it is derived by construction from the .step.yaml
registry: blocks (registry/interface_registry_loader.py); the standalone
registry/step_names.yaml table is gone.
Authoring a new step is now: one .step.yaml (with a registry: block) + one
config class — no builder file. The difference between, say, a Processing step
and a Training step is one string (sagemaker_step_type) in the interface.
3. cursus.steps.builders — lazy in 2.0.0, removed in 2.5.0¶
This is the sharpest edge to be aware of, because it changed twice:
2.0.0 turned
steps/builders/__init__.pyinto a PEP-562 lazy import surface.from cursus.steps.builders import XGBoostTrainingStepBuilderstill worked — the name resolved lazily to a synthesizedTemplateStepBuildersubclass.2.5.0 then physically deleted the
steps/{builders,contracts,specs}/folders, and with them that lazy shim. In the current 2.x head,import cursus.steps.buildersraisesModuleNotFoundError.
Current correct way to obtain a builder class (rarely needed for authoring):
from cursus.step_catalog import StepCatalog
catalog = StepCatalog()
BuilderCls = catalog.load_builder_class("XGBoostTraining") # synthesized on demand
Two related homes moved out of the deleted packages and are worth noting:
S3PathHandlernow lives incursus.steps.utils.StepBuilderBaselives incursus.core.base.builder_base(it gained a first-classSTEP_NAMEattribute, since a shared shell’s class name no longer carries the canonical step name).
Note also (2.5.0): StepCatalog.get_contract_entry_points() keys changed from the
old file-stem (tabular_preprocessing_contract) to the PascalCase canonical step
name (TabularPreprocessing).
4. Data-driven pipeline catalog (1.8.0)¶
The previous class-based catalog was removed and replaced with data: 44
shared DAGs shipped as *.dag.json under pipeline_catalog/shared_dags/, indexed
by catalog_index.json, with a deterministic scoring router. Load and build a
catalogued DAG through the functional API:
from cursus.pipeline_catalog.shared_dags import load_shared_dag
from cursus.pipeline_catalog.core.router import recommend_dag, auto_select_dag
from cursus.pipeline_catalog.core.builders import build_and_compile, build_mods_pipeline
dag = load_shared_dag("xgboost_mt_temporal_split_e2e") # -> PipelineDAG
# recommend_dag(...) / auto_select_dag(...) score the index
# build_and_compile(...) -> compiled SageMaker Pipeline
# build_mods_pipeline(...) -> @MODSTemplate-decorated pipeline class (MODS)
See Pipeline catalog and the generated Pipeline catalog reference.
5. cursus.workspace module removed (1.8.0)¶
The entire cursus.workspace package (api, integrator, manager,
validator) was removed — it was a dead island with no in-tree caller. This is
potentially breaking for any external code that imported cursus.workspace.
Its one goal-relevant use — enumerating pipeline projects — is replaced by
core/utils/project_discovery.py:
from cursus.core.utils import discover_pipeline_projects
from cursus.core.utils.project_discovery import summarize_project
projects = discover_pipeline_projects(root="/path/to/pipelines") # -> List[ProjectInfo]
info = summarize_project("/path/to/pipelines/my_project") # -> ProjectInfo | None
The load-bearing workspace_dirs parameter (on StepCatalog /
PipelineDAGCompiler) and the step_catalog/adapters were explicitly
preserved — only the dead cursus.workspace module was removed. For defining
your own step types outside the installed package, see the step packs
mechanism (Step packs), which lets a consumer add
native steps (interfaces/*.step.yaml + configs/ + scripts/) with no fork.
6. ValidationLevel enum change (2.0.0)¶
Because contract-to-spec alignment became a construction-time Pydantic invariant,
the old Level-2 ValidationLevel.CONTRACT_SPEC (value 2) member was removed
from the enum (validation/alignment/config/validation_ruleset.py). Validation is
now re-grounded on the three boundaries construction cannot self-check:
Member |
Value |
Boundary |
|---|---|---|
|
1 |
B1 — script ↔ interface (contract) fidelity |
|
3 |
B2 — cross-step DAG-resolvability (+ SageMaker property-path) |
|
4 |
B3 — registry ↔ handler ↔ config binding |
The surviving members keep their names and their non-contiguous integer values
(1, 3, 4), so ValidationLevel(1), ValidationLevel(3), and ValidationLevel(4)
coercion still work. Only the value-2 member is gone — remove any reference to
ValidationLevel.CONTRACT_SPEC or ValidationLevel(2).
The five per-step-type validators plus validator_factory.py were replaced by a
single RegistryBindingValidator
(validation/alignment/validators/registry_binding_validator.py) that proves a
step is constructible (the B3 boundary).
New introspection surfaces¶
Because there is no longer a per-step builder class to open and read, two new CLI/MCP surfaces make the now data-driven build self-describing:
cursus steps io <Step>/cursus steps patterns <Step>— a step’s resolved inputs/outputs/dependencies and which construction pattern + knobs it binds, read from the sameio_viewthe build uses.cursus strategies axes | list | show | for | knobs— the strategy library itself: the handler verbs, whichsagemaker_step_typemaps to which handler, and the available knobs.
The MCP tool surface gained matching steps.* and strategies.* namespaces —
see MCP tools and the
Step catalog reference. Full CLI
reference: CLI.
Before / after checklist¶
Work top to bottom. Most codebases only touch the first two rows.
If your code did this (1.x)… |
Do this instead (2.x) |
Required? |
|---|---|---|
|
Nothing — unchanged |
— |
Kept your |
Nothing — unchanged |
— |
|
|
Yes |
|
Same as above — the shim was removed in 2.5.0 |
Yes |
Subclassed or monkeypatched a per-step builder in place |
Move the behavior into the step’s |
Yes |
Imported a per-step |
|
Yes |
|
Unchanged — |
— |
Imported |
|
If moved |
Used the old class-based pipeline catalog |
|
Yes |
|
|
Yes |
Referenced |
Remove it — use B1/B2/B3 ( |
Yes |
Called |
Key by PascalCase canonical step name ( |
Yes |
Opened a builder class to understand how a step builds |
|
Optional |
Quick verification¶
After upgrading, confirm the invariant that matters most — that your DAG still compiles to the same pipeline:
# Compile from the CLI and inspect the result
cursus compile --help
# Or in Python
python -c "
from cursus import PipelineDAGCompiler
# build your dag + config, then:
# pipeline = PipelineDAGCompiler(config_path='config.json').compile(dag)
print('compiler import OK')
"
If you author steps, the interface validators are the fastest signal:
cursus validate step-interface --help
cursus steps io <StepName> # resolved I/O for a step
cursus strategies list # the strategy library the facade binds
cursus strategies knobs --axis sagemaker_step_type --name Training # knobs for one strategy
Summary¶
Do nothing if you only use the public authoring API — it is byte-for-byte compatible.
Repoint imports if you reached into per-step builder / spec / contract modules: those files, and (as of 2.5.0) the
steps/{builders,contracts,specs}/folders themselves, no longer exist. UseStepCatalog.load_builder_class(...)/.get_step_interface(...).Switch to the functional pipeline catalog (
load_shared_dag,recommend_dag,build_and_compile) if you used the removed class-based catalog.Replace
cursus.workspacewithcore.utils.project_discoveryor step packs.Drop
ValidationLevel.CONTRACT_SPEC— it is gone; the other three members keep their names and values.