durable-workflow 0.4.89__tar.gz → 0.4.90__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {durable_workflow-0.4.89/src/durable_workflow.egg-info → durable_workflow-0.4.90}/PKG-INFO +1 -1
  2. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/pyproject.toml +1 -1
  3. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/workflow.py +11 -1
  4. {durable_workflow-0.4.89 → durable_workflow-0.4.90/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  5. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_wait_condition.py +67 -0
  6. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_worker.py +112 -0
  7. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/LICENSE +0 -0
  8. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/README.md +0 -0
  9. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/setup.cfg +0 -0
  10. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/__init__.py +0 -0
  11. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/_avro.py +0 -0
  12. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/activity.py +0 -0
  13. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/auth_composition.py +0 -0
  14. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/client.py +0 -0
  15. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/errors.py +0 -0
  16. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/external_storage.py +0 -0
  17. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/external_task_input.py +0 -0
  18. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/external_task_result.py +0 -0
  19. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/history_bundle_verify.py +0 -0
  20. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/interceptors.py +0 -0
  21. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/invocable.py +0 -0
  22. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/metrics.py +0 -0
  23. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/py.typed +0 -0
  24. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/python_conformance.py +0 -0
  25. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/replay_conformance.py +0 -0
  26. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/replay_verify.py +0 -0
  27. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/retry_policy.py +0 -0
  28. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/serializer.py +0 -0
  29. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/sync.py +0 -0
  30. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/testing.py +0 -0
  31. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow/worker.py +0 -0
  32. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  33. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  34. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  35. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow.egg-info/requires.txt +0 -0
  36. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/src/durable_workflow.egg-info/top_level.txt +0 -0
  37. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_activity_context.py +0 -0
  38. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_auth_composition.py +0 -0
  39. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_client.py +0 -0
  40. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_control_plane_parity_fixtures.py +0 -0
  41. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_errors.py +0 -0
  42. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_external_storage.py +0 -0
  43. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_external_task_input.py +0 -0
  44. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_external_task_result.py +0 -0
  45. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_golden_history_replay.py +0 -0
  46. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_history_bundle_verify.py +0 -0
  47. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_history_event_contract.py +0 -0
  48. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_invocable.py +0 -0
  49. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_metrics.py +0 -0
  50. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_order_processing_example.py +0 -0
  51. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_public_boundary_scanner.py +0 -0
  52. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_python_conformance.py +0 -0
  53. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_queries.py +0 -0
  54. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_readme_quickstart.py +0 -0
  55. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_release_docs_audit_workflow.py +0 -0
  56. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_replay.py +0 -0
  57. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_replay_conformance.py +0 -0
  58. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_replay_verify.py +0 -0
  59. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_retry_policy.py +0 -0
  60. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_schedules.py +0 -0
  61. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_serializer.py +0 -0
  62. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_signals.py +0 -0
  63. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_sleep.py +0 -0
  64. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_standalone_activity_client.py +0 -0
  65. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_sync.py +0 -0
  66. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_testing_harness.py +0 -0
  67. {durable_workflow-0.4.89 → durable_workflow-0.4.90}/tests/test_updates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.89
3
+ Version: 0.4.90
4
4
  Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "0.4.89"
7
+ version = "0.4.90"
8
8
  description = "Python SDK for the Durable Workflow server (language-neutral HTTP protocol)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2762,7 +2762,17 @@ def _replay_state(
2762
2762
  exception_type=type(exc).__name__,
2763
2763
  )])
2764
2764
  if resolution == "satisfied":
2765
- terminal_condition_reopen_cmd = None
2765
+ if has_reopened_same_wait:
2766
+ if not satisfied:
2767
+ consumed_reopen_for_current_wait = True
2768
+ wait_yield_count = next_wait_index
2769
+ continue
2770
+
2771
+ terminal_condition_reopen_cmd = (
2772
+ cmd
2773
+ if has_reopened_same_wait and consumed_reopen_for_current_wait
2774
+ else None
2775
+ )
2766
2776
  next_value = True
2767
2777
  wait_yield_count += 1
2768
2778
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.89
3
+ Version: 0.4.90
4
4
  Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -172,6 +172,41 @@ class SignalCounterUntilFinished:
172
172
  return self.current()
173
173
 
174
174
 
175
+ @workflow.defn(name="signal-counter-single-predicate-per-reopen")
176
+ class SignalCounterSinglePredicatePerReopen:
177
+ def __init__(self) -> None:
178
+ self.count = 0
179
+ self.done = False
180
+ self.receiver_generation = 0
181
+ self.last_predicate_generation: int | None = None
182
+ self.predicate_calls = 0
183
+
184
+ @workflow.signal("increment")
185
+ def increment(self, amount: int) -> None:
186
+ self.count += amount
187
+ self.receiver_generation += 1
188
+
189
+ @workflow.signal("finish")
190
+ def finish(self) -> None:
191
+ self.done = True
192
+ self.receiver_generation += 1
193
+
194
+ def done_once_per_receiver(self) -> bool:
195
+ self.predicate_calls += 1
196
+ if self.last_predicate_generation == self.receiver_generation:
197
+ raise RuntimeError("predicate re-evaluated before replay advanced")
198
+ self.last_predicate_generation = self.receiver_generation
199
+ return self.done
200
+
201
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
202
+ satisfied = yield ctx.wait_condition(self.done_once_per_receiver, key="done")
203
+ return {
204
+ "count": self.count,
205
+ "satisfied": satisfied,
206
+ "predicate_calls": self.predicate_calls,
207
+ }
208
+
209
+
175
210
  @workflow.defn(name="two-same-key-signal-waits")
176
211
  class TwoSameKeySignalWaits:
177
212
  def __init__(self) -> None:
@@ -455,6 +490,38 @@ class TestReplayWaitCondition:
455
490
  ],
456
491
  }
457
492
 
493
+ def test_satisfied_reopened_wait_reuses_current_predicate_result(self) -> None:
494
+ history = [
495
+ {
496
+ "event_type": "ConditionWaitOpened",
497
+ "payload": {"condition_wait_id": "wait-count", "condition_key": "done"},
498
+ },
499
+ _signal_received_event("increment", [3]),
500
+ {
501
+ "event_type": "ConditionWaitSatisfied",
502
+ "payload": {"condition_wait_id": "wait-count", "condition_key": "done"},
503
+ },
504
+ {
505
+ "event_type": "ConditionWaitOpened",
506
+ "payload": {"condition_wait_id": "wait-finish", "condition_key": "done"},
507
+ },
508
+ _signal_received_event("finish", []),
509
+ {
510
+ "event_type": "ConditionWaitSatisfied",
511
+ "payload": {"condition_wait_id": "wait-finish", "condition_key": "done"},
512
+ },
513
+ ]
514
+
515
+ outcome = replay(SignalCounterSinglePredicatePerReopen, history, [])
516
+
517
+ assert len(outcome.commands) == 1
518
+ assert isinstance(outcome.commands[0], CompleteWorkflow)
519
+ assert outcome.commands[0].result == {
520
+ "count": 3,
521
+ "satisfied": True,
522
+ "predicate_calls": 2,
523
+ }
524
+
458
525
  def test_signal_received_before_next_wait_uses_declared_workflow_sequence(self) -> None:
459
526
  history = [
460
527
  {
@@ -1443,6 +1443,118 @@ class TestWorkflowTaskExecution:
1443
1443
  )
1444
1444
  mock_client.fail_query_task.assert_not_called()
1445
1445
 
1446
+ @pytest.mark.asyncio
1447
+ async def test_query_task_replays_signal_woken_false_wait_reopens(
1448
+ self, mock_client: AsyncMock
1449
+ ) -> None:
1450
+ worker = Worker(mock_client, task_queue="q1", workflows=[CounterQueryWorkflow], activities=[])
1451
+ first_signal_arguments = serializer.encode([3], codec="json")
1452
+ second_signal_arguments = serializer.encode([5], codec="json")
1453
+ task = {
1454
+ "query_task_id": "qt-false-wait-reopens",
1455
+ "query_task_attempt": 1,
1456
+ "workflow_type": "counter-query-wf",
1457
+ "workflow_id": "wf-counter",
1458
+ "run_id": "run-counter",
1459
+ "query_name": "current",
1460
+ "history_events": [
1461
+ {
1462
+ "event_type": "ConditionWaitOpened",
1463
+ "payload": {
1464
+ "condition_wait_id": "wait-count-0",
1465
+ "condition_key": "done",
1466
+ },
1467
+ },
1468
+ {
1469
+ "event_type": "SignalReceived",
1470
+ "workflow_command_id": "cmd-increment-3",
1471
+ "payload": {
1472
+ "signal_id": "sig-increment-3",
1473
+ "workflow_command_id": "cmd-increment-3",
1474
+ "signal_name": "increment",
1475
+ },
1476
+ },
1477
+ {
1478
+ "event_type": "ConditionWaitSatisfied",
1479
+ "payload": {
1480
+ "condition_wait_id": "wait-count-0",
1481
+ "condition_key": "done",
1482
+ "workflow_signal_id": "sig-increment-3",
1483
+ "signal_name": "increment",
1484
+ },
1485
+ },
1486
+ {
1487
+ "event_type": "ConditionWaitOpened",
1488
+ "payload": {
1489
+ "condition_wait_id": "wait-count-3",
1490
+ "condition_key": "done",
1491
+ },
1492
+ },
1493
+ {
1494
+ "event_type": "SignalReceived",
1495
+ "workflow_command_id": "cmd-increment-5",
1496
+ "payload": {
1497
+ "signal_id": "sig-increment-5",
1498
+ "workflow_command_id": "cmd-increment-5",
1499
+ "signal_name": "increment",
1500
+ },
1501
+ },
1502
+ {
1503
+ "event_type": "ConditionWaitSatisfied",
1504
+ "payload": {
1505
+ "condition_wait_id": "wait-count-3",
1506
+ "condition_key": "done",
1507
+ "workflow_signal_id": "sig-increment-5",
1508
+ "signal_name": "increment",
1509
+ },
1510
+ },
1511
+ {
1512
+ "event_type": "ConditionWaitOpened",
1513
+ "payload": {
1514
+ "condition_wait_id": "wait-count-8",
1515
+ "condition_key": "done",
1516
+ },
1517
+ },
1518
+ ],
1519
+ "history_export": {
1520
+ "payloads": {"codec": "json"},
1521
+ "signals": [
1522
+ {
1523
+ "id": "sig-increment-3",
1524
+ "command_id": "cmd-increment-3",
1525
+ "name": "increment",
1526
+ "payload_codec": "json",
1527
+ "arguments": first_signal_arguments,
1528
+ },
1529
+ {
1530
+ "id": "sig-increment-5",
1531
+ "command_id": "cmd-increment-5",
1532
+ "name": "increment",
1533
+ "payload_codec": "json",
1534
+ "arguments": second_signal_arguments,
1535
+ },
1536
+ ],
1537
+ },
1538
+ "workflow_arguments": serializer.envelope([], codec="json"),
1539
+ "query_arguments": serializer.envelope([], codec="json"),
1540
+ "payload_codec": "json",
1541
+ }
1542
+
1543
+ outcome = await worker._run_query_task(task)
1544
+
1545
+ assert outcome == "completed"
1546
+ mock_client.complete_query_task.assert_awaited_once_with(
1547
+ query_task_id="qt-false-wait-reopens",
1548
+ lease_owner=worker.worker_id,
1549
+ query_task_attempt=1,
1550
+ result=8,
1551
+ codec="json",
1552
+ workflow_id="wf-counter",
1553
+ run_id="run-counter",
1554
+ query_name="current",
1555
+ )
1556
+ mock_client.fail_query_task.assert_not_called()
1557
+
1446
1558
  @pytest.mark.asyncio
1447
1559
  async def test_query_task_replays_history_from_export_after_worker_restart(
1448
1560
  self, mock_client: AsyncMock