Skip to content

Commit 2665140

Browse files
author
Rishabh Devnani
committed
refactor(pipeline): Take lineage entities per kind on LineageStep
Replaces LineageStep's single step_args argument with actions, artifacts and contexts, as requested in review. The caller states which kind each captured argument is instead of the step inferring it from the producer. This makes a misplaced entity a construction error that names the right producer -- passing an Artifact.create() result as actions now fails with "the actions of LineageStep must be obtained from Action.create()" -- rather than something the step silently files under the wrong key. The emitted Arguments are unchanged: Actions, Artifacts, Contexts and Associations, with empty kinds omitted. Each kind accepts a single captured value or a list. Testing: 39 unit tests, and the lineage integration test run end to end against the service, which creates an Action, an Artifact and a Context in one step with two associations referencing them by name and type. --- X-AI-Prompt: more comments X-AI-Tool: kiro-cli
1 parent 71972f0 commit 2665140

3 files changed

Lines changed: 118 additions & 56 deletions

File tree

‎sagemaker-mlops/src/sagemaker/mlops/workflow/lineage_step.py‎

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
create each one with the corresponding class from
1818
:mod:`sagemaker.core.lineage` under a
1919
:class:`~sagemaker.core.workflow.pipeline_context.PipelineSession` and pass
20-
the captured arguments to the step.
20+
the captured arguments to the matching argument of the step -- ``actions``,
21+
``artifacts`` or ``contexts``.
2122
2223
Associations are declared rather than captured. ``Association.create()``
2324
takes source and destination ARNs, but an entity created by the same step
@@ -31,14 +32,16 @@
3132
3233
step = LineageStep(
3334
name="RecordLineage",
34-
step_args=[
35+
actions=[
3536
Action.create(
3637
action_name="training-run",
3738
source_uri="s3://bucket/run",
3839
source_type="S3ETag",
3940
action_type="ModelTraining",
4041
sagemaker_session=pipeline_session,
4142
),
43+
],
44+
artifacts=[
4245
Artifact.create(
4346
artifact_name="trained-model",
4447
source_uri="s3://bucket/model.tar.gz",
@@ -72,16 +75,34 @@
7275
ENTITY_TYPE_ARTIFACT = "Artifact"
7376
ENTITY_TYPE_CONTEXT = "Context"
7477

75-
# Maps the captured lineage create call to the ``Arguments`` key the pipeline
78+
# One entry per kind of entity a LineageStep can create, in the order the
79+
# entities appear in the request. Each maps the step argument to the lineage
80+
# create call that must have produced it, the ``Arguments`` key the pipeline
7681
# service expects, the entity type used when referencing the entity from an
7782
# association, and the request field holding the entity name.
78-
_ENTITY_SPECS = {
79-
"create_action": ("Actions", ENTITY_TYPE_ACTION, "ActionName"),
80-
"create_artifact": ("Artifacts", ENTITY_TYPE_ARTIFACT, "ArtifactName"),
81-
"create_context": ("Contexts", ENTITY_TYPE_CONTEXT, "ContextName"),
83+
_ENTITY_KINDS = (
84+
("actions", "create_action", "Actions", ENTITY_TYPE_ACTION, "ActionName"),
85+
("artifacts", "create_artifact", "Artifacts", ENTITY_TYPE_ARTIFACT, "ArtifactName"),
86+
("contexts", "create_context", "Contexts", ENTITY_TYPE_CONTEXT, "ContextName"),
87+
)
88+
89+
# The producer each kind must come from, for the "wrong argument" error message.
90+
_EXPECTED_PRODUCER = {
91+
"actions": "Action.create()",
92+
"artifacts": "Artifact.create()",
93+
"contexts": "Context.create()",
8294
}
8395

8496

97+
def _as_list(value) -> list:
98+
"""Normalise a single captured argument or a sequence of them to a list."""
99+
if value is None:
100+
return []
101+
if isinstance(value, (list, tuple)):
102+
return list(value)
103+
return [value]
104+
105+
85106
class LineageEntityReference:
86107
"""Reference to a lineage entity, used as an association endpoint.
87108
@@ -207,7 +228,9 @@ class LineageStep(Step):
207228
def __init__(
208229
self,
209230
name: str,
210-
step_args: Optional[Union[_JobStepArguments, List[_JobStepArguments]]] = None,
231+
actions: Optional[Union[_JobStepArguments, List[_JobStepArguments]]] = None,
232+
artifacts: Optional[Union[_JobStepArguments, List[_JobStepArguments]]] = None,
233+
contexts: Optional[Union[_JobStepArguments, List[_JobStepArguments]]] = None,
211234
associations: Optional[List[LineageAssociation]] = None,
212235
display_name: Optional[str] = None,
213236
description: Optional[str] = None,
@@ -217,11 +240,13 @@ def __init__(
217240
218241
Args:
219242
name (str): The name of the step.
220-
step_args (_JobStepArguments or list): The captured arguments for
221-
each entity this step creates, obtained from
222-
``Action.create()``, ``Artifact.create()`` or
223-
``Context.create()`` called with a ``PipelineSession``. A
224-
single value is accepted for a step that creates one entity.
243+
actions (_JobStepArguments or list): Captured arguments from
244+
``Action.create()`` called with a ``PipelineSession``, for each
245+
action this step creates.
246+
artifacts (_JobStepArguments or list): Captured arguments from
247+
``Artifact.create()``, for each artifact this step creates.
248+
contexts (_JobStepArguments or list): Captured arguments from
249+
``Context.create()``, for each context this step creates.
225250
associations (List[LineageAssociation]): Associations to add after
226251
the entities are created.
227252
display_name (str): Optional display name.
@@ -237,30 +262,30 @@ def __init__(
237262
depends_on=depends_on,
238263
)
239264

240-
if step_args is None:
241-
entities = []
242-
elif isinstance(step_args, (list, tuple)):
243-
entities = list(step_args)
244-
else:
245-
entities = [step_args]
246-
247265
self.associations = list(associations) if associations else []
248266

249-
if not entities and not self.associations:
250-
raise ValueError(
251-
"A LineageStep requires at least one entity in step_args, or one association."
252-
)
267+
supplied = {"actions": actions, "artifacts": artifacts, "contexts": contexts}
268+
self._entities: Dict[str, List[_JobStepArguments]] = {}
269+
for param, caller, _, _, _ in _ENTITY_KINDS:
270+
entities = _as_list(supplied[param])
271+
for entity in entities:
272+
validate_step_args_input(
273+
step_args=entity,
274+
expected_caller={caller},
275+
error_message=(
276+
f"The {param} of LineageStep must be obtained from "
277+
f"{_EXPECTED_PRODUCER[param]} called with a PipelineSession. "
278+
"Associations are passed to the associations argument instead, "
279+
"because Association.create() cannot reference an entity created "
280+
"by the same step."
281+
),
282+
)
283+
self._entities[param] = entities
253284

254-
for entity in entities:
255-
validate_step_args_input(
256-
step_args=entity,
257-
expected_caller=set(_ENTITY_SPECS),
258-
error_message=(
259-
"The step_args of LineageStep must be obtained from Action.create(), "
260-
"Artifact.create() or Context.create() called with a PipelineSession. "
261-
"Associations are passed to the associations argument instead, because "
262-
"Association.create() cannot reference an entity created by the same step."
263-
),
285+
if not any(self._entities.values()) and not self.associations:
286+
raise ValueError(
287+
"A LineageStep requires at least one entity in actions, artifacts or "
288+
"contexts, or one association."
264289
)
265290

266291
for association in self.associations:
@@ -270,7 +295,6 @@ def __init__(
270295
f"{type(association).__name__}."
271296
)
272297

273-
self.step_args = entities
274298
self._validate_sibling_references()
275299

276300
root = Properties(step_name=name, step=self)
@@ -279,14 +303,29 @@ def __init__(
279303
root.__dict__["Associations"] = Properties(step_name=name, path="Associations")
280304
self._properties = root
281305

306+
@property
307+
def actions(self) -> List[_JobStepArguments]:
308+
"""The captured arguments for the actions this step creates."""
309+
return self._entities["actions"]
310+
311+
@property
312+
def artifacts(self) -> List[_JobStepArguments]:
313+
"""The captured arguments for the artifacts this step creates."""
314+
return self._entities["artifacts"]
315+
316+
@property
317+
def contexts(self) -> List[_JobStepArguments]:
318+
"""The captured arguments for the contexts this step creates."""
319+
return self._entities["contexts"]
320+
282321
def _created_entities(self) -> Dict[str, str]:
283322
"""Map the name of each entity created by this step to its type."""
284323
created = {}
285-
for entity in self.step_args:
286-
_, entity_type, name_field = _ENTITY_SPECS[entity.caller_name]
287-
name = entity.args.get(name_field)
288-
if isinstance(name, str):
289-
created[name] = entity_type
324+
for param, _, _, entity_type, name_field in _ENTITY_KINDS:
325+
for entity in self._entities[param]:
326+
name = entity.args.get(name_field)
327+
if isinstance(name, str):
328+
created[name] = entity_type
290329
return created
291330

292331
def _validate_sibling_references(self) -> None:
@@ -319,9 +358,10 @@ def _validate_sibling_references(self) -> None:
319358
def arguments(self) -> RequestType:
320359
"""The ``Arguments`` block: the entities and associations for this step."""
321360
request: RequestType = {}
322-
for entity in self.step_args:
323-
key, _, _ = _ENTITY_SPECS[entity.caller_name]
324-
request.setdefault(key, []).append(entity.args)
361+
for param, _, key, _, _ in _ENTITY_KINDS:
362+
entities = self._entities[param]
363+
if entities:
364+
request[key] = [entity.args for entity in entities]
325365
if self.associations:
326366
request["Associations"] = [a.to_request() for a in self.associations]
327367
return request

‎sagemaker-mlops/tests/integ/workflow/test_lineage_step.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ def test_lineage_step_execute_end_to_end(sagemaker_session, pipeline_session, ro
106106
# so a single step covers create-then-associate end to end.
107107
step = LineageStep(
108108
name="RecordLineage",
109-
step_args=[action_args, artifact_args, context_args],
109+
actions=[action_args],
110+
artifacts=[artifact_args],
111+
contexts=[context_args],
110112
associations=[
111113
LineageAssociation(
112114
source=LineageEntityReference(name=action_name, type="Action"),

‎sagemaker-mlops/tests/unit/workflow/test_inference_lineage_steps.py‎

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def action_step_args(pipeline_session):
255255

256256

257257
def test_lineage_step_action(pipeline_session, action_step_args):
258-
step = LineageStep(name="RecA", step_args=action_step_args)
258+
step = LineageStep(name="RecA", actions=action_step_args)
259259
assert step.step_type == StepTypeEnum.LINEAGE
260260
args = step.arguments
261261
assert list(args.keys()) == ["Actions"]
@@ -271,7 +271,7 @@ def test_lineage_step_artifact(pipeline_session):
271271
artifact_type="Model",
272272
sagemaker_session=pipeline_session,
273273
)
274-
step = LineageStep(name="RecB", step_args=step_args)
274+
step = LineageStep(name="RecB", artifacts=step_args)
275275
assert list(step.arguments.keys()) == ["Artifacts"]
276276
assert step.arguments["Artifacts"][0]["ArtifactName"] == "art1"
277277

@@ -283,7 +283,7 @@ def test_lineage_step_context(pipeline_session):
283283
context_type="Endpoint",
284284
sagemaker_session=pipeline_session,
285285
)
286-
step = LineageStep(name="RecC", step_args=step_args)
286+
step = LineageStep(name="RecC", contexts=step_args)
287287
assert list(step.arguments.keys()) == ["Contexts"]
288288
assert step.arguments["Contexts"][0]["ContextName"] == "ctx1"
289289

@@ -313,7 +313,7 @@ def test_lineage_step_batches_multiple_entities(pipeline_session):
313313
sagemaker_session=pipeline_session,
314314
)
315315

316-
step = LineageStep(name="Rec", step_args=actions + [artifact, context])
316+
step = LineageStep(name="Rec", actions=actions, artifacts=artifact, contexts=context)
317317
args = step.arguments
318318

319319
assert [a["ActionName"] for a in args["Actions"]] == ["act0", "act1"]
@@ -342,7 +342,8 @@ def test_lineage_step_association_references_sibling_by_name_and_type(pipeline_s
342342
)
343343
step = LineageStep(
344344
name="Rec",
345-
step_args=[action, artifact],
345+
actions=action,
346+
artifacts=artifact,
346347
associations=[
347348
LineageAssociation(
348349
source=LineageEntityReference(name="act1", type="Action"),
@@ -364,7 +365,7 @@ def test_lineage_step_association_accepts_literal_arn(pipeline_session, action_s
364365
existing = "arn:aws:sagemaker:us-west-2:123456789012:artifact/abc"
365366
step = LineageStep(
366367
name="Rec",
367-
step_args=action_step_args,
368+
actions=action_step_args,
368369
associations=[
369370
LineageAssociation(
370371
source=LineageEntityReference(name="act1", type="Action"),
@@ -385,7 +386,7 @@ def test_lineage_step_rejects_unresolvable_sibling_reference(action_step_args):
385386
with pytest.raises(ValueError, match="does not create"):
386387
LineageStep(
387388
name="Rec",
388-
step_args=action_step_args,
389+
actions=action_step_args,
389390
associations=[
390391
LineageAssociation(
391392
source=LineageEntityReference(name="act1", type="Action"),
@@ -396,15 +397,15 @@ def test_lineage_step_rejects_unresolvable_sibling_reference(action_step_args):
396397

397398

398399
def test_lineage_step_rejects_captured_association(pipeline_session, action_step_args):
399-
"""Association.create() cannot express a sibling, so it is not valid step_args."""
400+
"""Association.create() cannot express a sibling, so it is not a valid entity."""
400401
captured = Association.create(
401402
source_arn="arn:aws:sagemaker:us-west-2:123456789012:action/a",
402403
destination_arn="arn:aws:sagemaker:us-west-2:123456789012:artifact/b",
403404
association_type="Produced",
404405
sagemaker_session=pipeline_session,
405406
)
406407
with pytest.raises(ValueError, match="associations argument"):
407-
LineageStep(name="Rec", step_args=[action_step_args, captured])
408+
LineageStep(name="Rec", actions=[action_step_args, captured])
408409

409410

410411
def test_lineage_step_requires_an_entity_or_association():
@@ -431,16 +432,35 @@ def test_lineage_association_rejects_non_reference_endpoint():
431432

432433
def test_lineage_step_rejects_wrong_producer(endpoint_step_args):
433434
with pytest.raises(ValueError, match="Action.create"):
434-
LineageStep(name="Rec", step_args=endpoint_step_args)
435+
LineageStep(name="Rec", actions=endpoint_step_args)
436+
437+
438+
def test_lineage_step_rejects_entity_in_the_wrong_argument(pipeline_session):
439+
"""An artifact passed as an action is rejected, naming the right producer.
440+
441+
The per-kind arguments make the expected producer unambiguous, so a
442+
misplaced entity fails at construction instead of building a request the
443+
service would reject.
444+
"""
445+
artifact = Artifact.create(
446+
artifact_name="art1",
447+
source_uri="s3://bucket/model.tar.gz",
448+
artifact_type="Model",
449+
sagemaker_session=pipeline_session,
450+
)
451+
with pytest.raises(ValueError, match="actions of LineageStep must be obtained from"):
452+
LineageStep(name="Rec", actions=artifact)
453+
with pytest.raises(ValueError, match="contexts of LineageStep must be obtained from"):
454+
LineageStep(name="Rec", contexts=artifact)
435455

436456

437457
def test_lineage_step_rejects_raw_dict():
438458
with pytest.raises(TypeError):
439-
LineageStep(name="Rec", step_args={"Actions": []})
459+
LineageStep(name="Rec", actions={"ActionName": "a"})
440460

441461

442462
def test_lineage_step_properties(action_step_args):
443-
step = LineageStep(name="Rec", step_args=action_step_args)
463+
step = LineageStep(name="Rec", actions=action_step_args)
444464
for field in ("ActionArns", "ArtifactArns", "ContextArns", "Associations"):
445465
assert hasattr(step.properties, field)
446466
assert step.properties.ArtifactArns["x"].expr == {"Get": "Steps.Rec.ArtifactArns['x']"}

0 commit comments

Comments
 (0)