Skip to content

Commit ff247a2

Browse files
dapr-botnelson-parentesicoyle
authored
feat(workflow): align history propagation API with go-sdk (#1047) (#1055)
* feat(workflow): align history propagation API with go-sdk Cassie (durabletask-go author) flagged divergence between python-sdk #1025 and the freshly-renamed go-sdk helpers in durabletask-go #105 (merged 2026-05-19, after #1025 landed). This brings python-sdk's surface back in line for cross-SDK parity before 1.18 ships. Read API renames (mirror durabletask-go GetLast*ByName): - PropagatedHistory.get_workflow_by_name -> get_last_workflow_by_name - WorkflowResult.get_activity_by_name -> get_last_activity_by_name - WorkflowResult.get_child_workflow_by_name -> get_last_child_workflow_by_name Plural variants (get_workflows_by_name, get_activities_by_name, get_child_workflows_by_name) and the chain-level helpers are unchanged. Scheduling helpers (mirror go-sdk workflow.PropagateLineage / workflow.PropagateOwnHistory): - propagate_lineage() -> PropagationScope.LINEAGE - propagate_own_history() -> PropagationScope.OWN_HISTORY PropagationScope enum is kept as the underlying value, so both `propagation=propagate_lineage()` and `propagation=PropagationScope.LINEAGE` work. Example, README snippet, and tests updated to use the renamed/new surface. No runtime/proto changes. Refs: #1001, dapr/durabletask-go#105, dapr/go-sdk#823 * refactor(workflow): drop propagate_lineage/propagate_own_history factories Per review feedback, Go-style factory helpers are not idiomatic Python: they obscure the actual enum value at the call site and confuse static type checkers (return annotation only shows PropagationScope, not the specific member). Use PropagationScope.LINEAGE / PropagationScope.OWN_HISTORY directly instead. --------- (cherry picked from commit 2fd3237) Signed-off-by: Nelson Parente <nelson_parente@live.com.pt> Signed-off-by: dapr-bot <dapr-bot@users.noreply.github.com> Co-authored-by: Nelson Parente <nelson_parente@live.com.pt> Co-authored-by: Sam <sam@diagrid.io>
1 parent 9672733 commit ff247a2

5 files changed

Lines changed: 44 additions & 37 deletions

File tree

examples/workflow/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,8 +548,8 @@ It shows:
548548
forwards the caller's events only.
549549
- `propagation=PropagationScope.LINEAGE` on an activity call — forwards the
550550
caller's events *plus* anything the caller itself received from its parent.
551-
- `PropagatedHistory.get_workflow_by_name(...)` and `WorkflowResult.get_activity_by_name(...)`
552-
on the receiving side.
551+
- `PropagatedHistory.get_last_workflow_by_name(...)` and
552+
`WorkflowResult.get_last_activity_by_name(...)` on the receiving side.
553553

554554
> **Requires** a Dapr sidecar with workflow history propagation support
555555
> (durabletask-go PR #85 / runtime 1.18+ ). With an older sidecar the

examples/workflow/history_propagation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def log_summary(ctx: wf.WorkflowActivityContext, _: None) -> str:
5353

5454
parent = workflows[-1]
5555
try:
56-
validate = parent.get_activity_by_name('validate_merchant')
56+
validate = parent.get_last_activity_by_name('validate_merchant')
5757
except wf.PropagationNotFoundError:
5858
print('*** log_summary: parent did not run validate_merchant', flush=True)
5959
return 'parent-missing-validate'
@@ -81,7 +81,7 @@ def process_payment(ctx: wf.DaprWorkflowContext, _: None):
8181

8282
parent = workflows[-1]
8383
try:
84-
validate = parent.get_activity_by_name('validate_merchant')
84+
validate = parent.get_last_activity_by_name('validate_merchant')
8585
except wf.PropagationNotFoundError:
8686
print('*** process_payment: parent did not run validate_merchant', flush=True)
8787
return 'parent-missing-validate'

ext/dapr-ext-workflow/dapr/ext/workflow/propagation.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ class PropagationScope(Enum):
3434
3535
Values map 1:1 to the protobuf ``HistoryPropagationScope`` enum; the
3636
plumbing layer reads ``.value`` when writing to proto fields.
37+
38+
* ``OWN_HISTORY`` — propagate the caller's events only; drop any ancestor
39+
chain. Use as a trust boundary, where downstream code should only see
40+
the immediate caller.
41+
* ``LINEAGE`` — propagate the caller's events plus any ancestor events it
42+
received. Use for chain-of-custody verification, where downstream code
43+
needs visibility into the full lineage of upstream workflows.
3744
"""
3845

3946
NONE = int(pb.HISTORY_PROPAGATION_SCOPE_NONE)
@@ -144,7 +151,7 @@ def _resolve_child_workflow(
144151
class WorkflowResult:
145152
"""A scoped view of a single workflow's chunk in propagated history.
146153
147-
Use :meth:`get_activity_by_name` / :meth:`get_child_workflow_by_name`
154+
Use :meth:`get_last_activity_by_name` / :meth:`get_last_child_workflow_by_name`
148155
to query specific items inside this chunk. Methods return the most-recent
149156
occurrence by execution order.
150157
"""
@@ -158,15 +165,15 @@ def get_activities_by_name(self, name: str) -> list[ActivityResult]:
158165
"""Return every activity in this chunk whose scheduled name matches, in
159166
execution order. Empty list if none.
160167
161-
See also: :meth:`get_activity_by_name` for the most recent match only.
168+
See also: :meth:`get_last_activity_by_name` for the most recent match only.
162169
"""
163170
return [
164171
_resolve_activity(self._events, e)
165172
for e in self._events
166173
if e.HasField('taskScheduled') and e.taskScheduled.name == name
167174
]
168175

169-
def get_activity_by_name(self, name: str) -> ActivityResult:
176+
def get_last_activity_by_name(self, name: str) -> ActivityResult:
170177
"""Return the most recent activity in this chunk whose name matches.
171178
172179
Raises :class:`PropagationNotFoundError` if no activity scheduled with
@@ -186,7 +193,7 @@ def get_child_workflows_by_name(self, name: str) -> list[ChildWorkflowResult]:
186193
"""Return every child workflow in this chunk whose name matches, in
187194
execution order.
188195
189-
See also: :meth:`get_child_workflow_by_name` for the most recent match.
196+
See also: :meth:`get_last_child_workflow_by_name` for the most recent match.
190197
"""
191198
return [
192199
_resolve_child_workflow(self._events, e.eventId, name)
@@ -195,7 +202,7 @@ def get_child_workflows_by_name(self, name: str) -> list[ChildWorkflowResult]:
195202
and e.childWorkflowInstanceCreated.name == name
196203
]
197204

198-
def get_child_workflow_by_name(self, name: str) -> ChildWorkflowResult:
205+
def get_last_child_workflow_by_name(self, name: str) -> ChildWorkflowResult:
199206
"""Return the most recent child workflow in this chunk whose name matches.
200207
201208
Raises :class:`PropagationNotFoundError` if no match is found.
@@ -301,12 +308,12 @@ def get_workflows_by_name(self, name: str) -> list[WorkflowResult]:
301308
"""All workflows whose name matches, in execution order. Useful when
302309
the chain contains the same name more than once (recursion / ContinueAsNew).
303310
304-
See also: :meth:`get_workflow_by_name` for a single-result helper that
311+
See also: :meth:`get_last_workflow_by_name` for a single-result helper that
305312
returns only the most recent match.
306313
"""
307314
return [self._make_workflow_result(c) for c in self._chunks if c.workflow_name == name]
308315

309-
def get_workflow_by_name(self, name: str) -> WorkflowResult:
316+
def get_last_workflow_by_name(self, name: str) -> WorkflowResult:
310317
"""Most recent workflow in the chain whose name matches.
311318
312319
Raises :class:`PropagationNotFoundError` if no match is found.

ext/dapr-ext-workflow/tests/durabletask/test_propagation.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -196,21 +196,21 @@ def test_get_workflows_returns_chunks_in_order(history: PropagatedHistory):
196196
assert workflows[1].instance_id == 'wf-002'
197197

198198

199-
def test_get_workflow_by_name_returns_match(history: PropagatedHistory):
200-
wf = history.get_workflow_by_name('ProcessPayment')
199+
def test_get_last_workflow_by_name_returns_match(history: PropagatedHistory):
200+
wf = history.get_last_workflow_by_name('ProcessPayment')
201201
assert wf.name == 'ProcessPayment'
202202
assert wf.instance_id == 'wf-002'
203203

204204

205-
def test_get_workflow_by_name_raises_when_missing(history: PropagatedHistory):
205+
def test_get_last_workflow_by_name_raises_when_missing(history: PropagatedHistory):
206206
with pytest.raises(PropagationNotFoundError):
207-
history.get_workflow_by_name('NotARealWorkflow')
207+
history.get_last_workflow_by_name('NotARealWorkflow')
208208

209209

210210
def test_get_workflows_by_name_returns_all_matches():
211211
"""If the same workflow name appears in multiple chunks (e.g. ContinueAsNew
212212
or recursion), get_workflows_by_name returns every occurrence and
213-
get_workflow_by_name returns the last."""
213+
get_last_workflow_by_name returns the last."""
214214

215215
chunk_events = [_execution_started('Loop')]
216216
proto = pb.PropagatedHistory(
@@ -225,15 +225,15 @@ def test_get_workflows_by_name_returns_all_matches():
225225

226226
all_loops = ph.get_workflows_by_name('Loop')
227227
assert len(all_loops) == 2
228-
assert ph.get_workflow_by_name('Loop').instance_id == 'wf-2'
228+
assert ph.get_last_workflow_by_name('Loop').instance_id == 'wf-2'
229229

230230

231231
# --- Activity resolution ----------------------------------------------------
232232

233233

234-
def test_get_activity_by_name_returns_completed_result(history: PropagatedHistory):
235-
merchant = history.get_workflow_by_name('MerchantCheckout')
236-
activity = merchant.get_activity_by_name('ValidateMerchant')
234+
def test_get_last_activity_by_name_returns_completed_result(history: PropagatedHistory):
235+
merchant = history.get_last_workflow_by_name('MerchantCheckout')
236+
activity = merchant.get_last_activity_by_name('ValidateMerchant')
237237

238238
assert activity.name == 'ValidateMerchant'
239239
assert activity.started
@@ -245,7 +245,7 @@ def test_get_activity_by_name_returns_completed_result(history: PropagatedHistor
245245

246246

247247
def test_get_activities_by_name_returns_all_invocations(history: PropagatedHistory):
248-
payment = history.get_workflow_by_name('ProcessPayment')
248+
payment = history.get_last_workflow_by_name('ProcessPayment')
249249
cards = payment.get_activities_by_name('ValidateCard')
250250

251251
assert len(cards) == 2
@@ -257,20 +257,20 @@ def test_get_activities_by_name_returns_all_invocations(history: PropagatedHisto
257257
assert cards[1].error.errorMessage == 'card declined'
258258

259259

260-
def test_get_activity_by_name_returns_last_invocation(history: PropagatedHistory):
261-
"""get_activity_by_name returns the most recent invocation in execution
260+
def test_get_last_activity_by_name_returns_last_invocation(history: PropagatedHistory):
261+
"""get_last_activity_by_name returns the most recent invocation in execution
262262
order, matching Go semantics."""
263-
payment = history.get_workflow_by_name('ProcessPayment')
264-
last = payment.get_activity_by_name('ValidateCard')
263+
payment = history.get_last_workflow_by_name('ProcessPayment')
264+
last = payment.get_last_activity_by_name('ValidateCard')
265265
assert last.failed
266266
assert last.error is not None
267267
assert last.error.errorMessage == 'card declined'
268268

269269

270-
def test_get_activity_by_name_raises_when_missing(history: PropagatedHistory):
271-
payment = history.get_workflow_by_name('ProcessPayment')
270+
def test_get_last_activity_by_name_raises_when_missing(history: PropagatedHistory):
271+
payment = history.get_last_workflow_by_name('ProcessPayment')
272272
with pytest.raises(PropagationNotFoundError):
273-
payment.get_activity_by_name('NotAnActivity')
273+
payment.get_last_activity_by_name('NotAnActivity')
274274

275275

276276
def test_activity_not_yet_completed_reports_started_only():
@@ -286,7 +286,7 @@ def test_activity_not_yet_completed_reports_started_only():
286286
)
287287
ph = PropagatedHistory.from_proto(proto)
288288
assert ph is not None
289-
pending = ph.get_workflow_by_name('StillRunning').get_activity_by_name('Pending')
289+
pending = ph.get_last_workflow_by_name('StillRunning').get_last_activity_by_name('Pending')
290290

291291
assert pending.started
292292
assert not pending.completed
@@ -298,18 +298,18 @@ def test_activity_not_yet_completed_reports_started_only():
298298
# --- Child workflow resolution ----------------------------------------------
299299

300300

301-
def test_get_child_workflow_by_name(history: PropagatedHistory):
302-
merchant = history.get_workflow_by_name('MerchantCheckout')
303-
child = merchant.get_child_workflow_by_name('ProcessPayment')
301+
def test_get_last_child_workflow_by_name(history: PropagatedHistory):
302+
merchant = history.get_last_workflow_by_name('MerchantCheckout')
303+
child = merchant.get_last_child_workflow_by_name('ProcessPayment')
304304

305305
assert child.name == 'ProcessPayment'
306306
assert child.started
307307

308308

309-
def test_get_child_workflow_by_name_raises_when_missing(history: PropagatedHistory):
310-
merchant = history.get_workflow_by_name('MerchantCheckout')
309+
def test_get_last_child_workflow_by_name_raises_when_missing(history: PropagatedHistory):
310+
merchant = history.get_last_workflow_by_name('MerchantCheckout')
311311
with pytest.raises(PropagationNotFoundError):
312-
merchant.get_child_workflow_by_name('NotAChild')
312+
merchant.get_last_child_workflow_by_name('NotAChild')
313313

314314

315315
# --- from_proto / structural validation -------------------------------------

ext/dapr-ext-workflow/tests/durabletask/test_propagation_wiring.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def orchestrator(ctx: task.OrchestrationContext, _):
158158
history = captured['history']
159159
assert history is not None
160160
assert history.get_app_ids() == ['parent-app']
161-
assert history.get_workflow_by_name('Parent').instance_id == 'parent-instance'
161+
assert history.get_last_workflow_by_name('Parent').instance_id == 'parent-instance'
162162

163163

164164
def test_orchestration_executor_propagated_history_is_none_by_default():
@@ -209,7 +209,7 @@ def reading_activity(ctx: task.ActivityContext, _):
209209
history = captured['history']
210210
assert history is not None
211211
assert history.get_app_ids() == ['parent-app']
212-
assert history.get_workflow_by_name('Caller').instance_id == 'parent-instance'
212+
assert history.get_last_workflow_by_name('Caller').instance_id == 'parent-instance'
213213

214214

215215
def test_activity_executor_propagated_history_is_none_by_default():

0 commit comments

Comments
 (0)