durable-workflow 0.4.95__tar.gz → 0.4.97__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 (70) hide show
  1. {durable_workflow-0.4.95/src/durable_workflow.egg-info → durable_workflow-0.4.97}/PKG-INFO +1 -1
  2. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/pyproject.toml +1 -1
  3. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/client.py +13 -0
  4. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/workflow.py +9 -2
  5. {durable_workflow-0.4.95 → durable_workflow-0.4.97/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  6. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_client.py +30 -1
  7. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_replay.py +53 -0
  8. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_sync.py +22 -1
  9. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/LICENSE +0 -0
  10. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/README.md +0 -0
  11. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/setup.cfg +0 -0
  12. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/__init__.py +0 -0
  13. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/_avro.py +0 -0
  14. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/activity.py +0 -0
  15. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/auth_composition.py +0 -0
  16. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/errors.py +0 -0
  17. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/external_storage.py +0 -0
  18. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/external_task_input.py +0 -0
  19. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/external_task_result.py +0 -0
  20. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/history_bundle_verify.py +0 -0
  21. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/interceptors.py +0 -0
  22. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/invocable.py +0 -0
  23. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/metrics.py +0 -0
  24. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/nexus.py +0 -0
  25. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/py.typed +0 -0
  26. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/python_conformance.py +0 -0
  27. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/replay_conformance.py +0 -0
  28. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/replay_verify.py +0 -0
  29. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/retry_policy.py +0 -0
  30. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/serializer.py +0 -0
  31. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/sync.py +0 -0
  32. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/testing.py +0 -0
  33. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/worker.py +0 -0
  34. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  35. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  36. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  37. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  38. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow.egg-info/requires.txt +0 -0
  39. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/src/durable_workflow.egg-info/top_level.txt +0 -0
  40. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_activity_context.py +0 -0
  41. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_auth_composition.py +0 -0
  42. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_control_plane_parity_fixtures.py +0 -0
  43. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_errors.py +0 -0
  44. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_external_storage.py +0 -0
  45. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_external_task_input.py +0 -0
  46. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_external_task_result.py +0 -0
  47. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_golden_history_replay.py +0 -0
  48. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_history_bundle_verify.py +0 -0
  49. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_history_event_contract.py +0 -0
  50. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_invocable.py +0 -0
  51. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_metrics.py +0 -0
  52. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_order_processing_example.py +0 -0
  53. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_public_boundary_scanner.py +0 -0
  54. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_python_conformance.py +0 -0
  55. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_queries.py +0 -0
  56. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_readme_quickstart.py +0 -0
  57. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_release_docs_audit_workflow.py +0 -0
  58. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_replay_conformance.py +0 -0
  59. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_replay_verify.py +0 -0
  60. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_retry_policy.py +0 -0
  61. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_schedules.py +0 -0
  62. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_serializer.py +0 -0
  63. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_signals.py +0 -0
  64. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_sleep.py +0 -0
  65. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_standalone_activity_client.py +0 -0
  66. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_testing_harness.py +0 -0
  67. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_updates.py +0 -0
  68. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_wait_condition.py +0 -0
  69. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_worker.py +0 -0
  70. {durable_workflow-0.4.95 → durable_workflow-0.4.97}/tests/test_workflow_updates_conformance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.95
3
+ Version: 0.4.97
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.95"
7
+ version = "0.4.97"
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"
@@ -164,6 +164,8 @@ class WorkflowExecution:
164
164
  input: Any = None
165
165
  output: Any = None
166
166
  payload_codec: str | None = None
167
+ memo: dict[str, Any] | None = None
168
+ search_attributes: dict[str, Any] | None = None
167
169
 
168
170
 
169
171
  @dataclass
@@ -2386,6 +2388,10 @@ class Client:
2386
2388
  input=input_val,
2387
2389
  output=output_val,
2388
2390
  payload_codec=data.get("payload_codec"),
2391
+ memo=data.get("memo") if isinstance(data.get("memo"), dict) else None,
2392
+ search_attributes=(
2393
+ data.get("search_attributes") if isinstance(data.get("search_attributes"), dict) else None
2394
+ ),
2389
2395
  )
2390
2396
 
2391
2397
  async def list_workflows(
@@ -2425,6 +2431,13 @@ class Client:
2425
2431
  run_id=item.get("run_id"),
2426
2432
  workflow_type=item.get("workflow_type", ""),
2427
2433
  status=item.get("status"),
2434
+ namespace=item.get("namespace"),
2435
+ task_queue=item.get("task_queue"),
2436
+ payload_codec=item.get("payload_codec"),
2437
+ memo=item.get("memo") if isinstance(item.get("memo"), dict) else None,
2438
+ search_attributes=(
2439
+ item.get("search_attributes") if isinstance(item.get("search_attributes"), dict) else None
2440
+ ),
2428
2441
  )
2429
2442
  for item in items
2430
2443
  ]
@@ -2270,10 +2270,12 @@ def _child_workflow_failed_from_payload(
2270
2270
  message = _optional_str(exception.get("message"))
2271
2271
 
2272
2272
  exception_class = _optional_str(payload.get("exception_class"))
2273
+ if exception_class is None and exception is not None:
2274
+ exception_class = _optional_str(exception.get("class"))
2273
2275
  if exception_class is None:
2274
2276
  exception_class = _optional_str(payload.get("exception_type"))
2275
2277
  if exception_class is None and exception is not None:
2276
- exception_class = _optional_str(exception.get("class")) or _optional_str(exception.get("type"))
2278
+ exception_class = _optional_str(exception.get("type"))
2277
2279
 
2278
2280
  child_workflow_run_id = _optional_str(payload.get("child_workflow_run_id"))
2279
2281
  child_workflow_type = _optional_str(payload.get("child_workflow_type"))
@@ -2823,7 +2825,12 @@ def _replay_state(
2823
2825
  result_cursor = 0
2824
2826
  pending_step_cursor = 0
2825
2827
  wait_yield_count = 0
2826
- gen = instance.run(ctx, *start_input)
2828
+ try:
2829
+ gen = instance.run(ctx, *start_input)
2830
+ except NonDeterministicReplayError:
2831
+ raise
2832
+ except Exception as exc:
2833
+ return _state([_fail_workflow_from_exception(exc)])
2827
2834
  if not hasattr(gen, "__next__"):
2828
2835
  if isinstance(gen, ContinueAsNew):
2829
2836
  _assert_no_unconsumed_history("continue as new")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 0.4.95
3
+ Version: 0.4.97
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
@@ -464,6 +464,8 @@ class TestDescribeWorkflow:
464
464
  assert desc.status == response["status"]
465
465
  assert desc.payload_codec == response["payload_codec"]
466
466
  assert desc.input == response["input"]
467
+ assert desc.memo == response["memo"]
468
+ assert desc.search_attributes == response["search_attributes"]
467
469
 
468
470
  @pytest.mark.asyncio
469
471
  async def test_envelope_fields(self, client: Client) -> None:
@@ -1131,7 +1133,23 @@ class TestListWorkflows:
1131
1133
  async def test_list(self, client: Client) -> None:
1132
1134
  resp = _mock_response(200, {
1133
1135
  "workflows": [
1134
- {"workflow_id": "wf-1", "run_id": "r1", "workflow_type": "greeter", "status": "running"},
1136
+ {
1137
+ "workflow_id": "wf-1",
1138
+ "run_id": "r1",
1139
+ "workflow_type": "greeter",
1140
+ "status": "running",
1141
+ "namespace": "ns1",
1142
+ "task_queue": "q1",
1143
+ "payload_codec": "avro",
1144
+ "memo": {"source": "php-writer"},
1145
+ "search_attributes": {
1146
+ "customer_id": "cust-php-1",
1147
+ "order_total_cents": 9200,
1148
+ "discount_ratio": 0.125,
1149
+ "is_vip": True,
1150
+ "tags": ["urgent", "renewal"],
1151
+ },
1152
+ },
1135
1153
  {"workflow_id": "wf-2", "run_id": "r2", "workflow_type": "greeter", "status": "completed"},
1136
1154
  ],
1137
1155
  "next_page_token": "abc",
@@ -1141,6 +1159,17 @@ class TestListWorkflows:
1141
1159
  assert len(result.executions) == 2
1142
1160
  assert result.next_page_token == "abc"
1143
1161
  assert result.executions[0].workflow_id == "wf-1"
1162
+ assert result.executions[0].namespace == "ns1"
1163
+ assert result.executions[0].task_queue == "q1"
1164
+ assert result.executions[0].payload_codec == "avro"
1165
+ assert result.executions[0].memo == {"source": "php-writer"}
1166
+ assert result.executions[0].search_attributes == {
1167
+ "customer_id": "cust-php-1",
1168
+ "order_total_cents": 9200,
1169
+ "discount_ratio": 0.125,
1170
+ "is_vip": True,
1171
+ "tags": ["urgent", "renewal"],
1172
+ }
1144
1173
 
1145
1174
  @pytest.mark.asyncio
1146
1175
  async def test_list_request_matches_polyglot_fixture(self, client: Client) -> None:
@@ -155,6 +155,18 @@ class FailingWorkflow:
155
155
  raise ValueError("something went wrong")
156
156
 
157
157
 
158
+ @workflow.defn(name="construction-failing-workflow")
159
+ class ConstructionFailingWorkflow:
160
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
161
+ raise RuntimeError("failed before first yield")
162
+
163
+
164
+ @workflow.defn(name="construction-stop-iteration-workflow")
165
+ class ConstructionStopIterationWorkflow:
166
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
167
+ raise StopIteration("stopped before first yield")
168
+
169
+
158
170
  @workflow.defn(name="typed-compensation-failure-workflow")
159
171
  class TypedCompensationFailureWorkflow:
160
172
  def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
@@ -848,6 +860,24 @@ class TestFailingWorkflow:
848
860
  assert cmd.exception_type == "ValueError"
849
861
  assert cmd.exception_class == "builtins.ValueError"
850
862
 
863
+ def test_non_generator_exception_produces_fail_command(self) -> None:
864
+ outcome = replay(ConstructionFailingWorkflow, [], [])
865
+ assert len(outcome.commands) == 1
866
+ cmd = outcome.commands[0]
867
+ assert isinstance(cmd, FailWorkflow)
868
+ assert "failed before first yield" in cmd.message
869
+ assert cmd.exception_type == "RuntimeError"
870
+ assert cmd.exception_class == "builtins.RuntimeError"
871
+
872
+ def test_non_generator_stop_iteration_is_not_completion(self) -> None:
873
+ outcome = replay(ConstructionStopIterationWorkflow, [], [])
874
+ assert len(outcome.commands) == 1
875
+ cmd = outcome.commands[0]
876
+ assert isinstance(cmd, FailWorkflow)
877
+ assert "stopped before first yield" in cmd.message
878
+ assert cmd.exception_type == "StopIteration"
879
+ assert cmd.exception_class == "builtins.StopIteration"
880
+
851
881
  def test_fail_server_command_shape(self) -> None:
852
882
  history = [{"event_type": "ActivityCompleted", "payload": {"result": '"ok"'}}]
853
883
  outcome = replay(FailingWorkflow, history, [])
@@ -1363,6 +1393,29 @@ class TestChildWorkflow:
1363
1393
  "child_workflow_type": "sub-workflow",
1364
1394
  }
1365
1395
 
1396
+ def test_child_failed_prefers_nested_exception_class_over_type(self) -> None:
1397
+ history = [
1398
+ {
1399
+ "event_type": "ChildRunFailed",
1400
+ "payload": {
1401
+ "message": "child failed",
1402
+ "exception_type": "NestedType",
1403
+ "exception": {
1404
+ "type": "NestedType",
1405
+ "class": "pkg.NestedClass",
1406
+ "message": "child failed",
1407
+ },
1408
+ },
1409
+ },
1410
+ ]
1411
+
1412
+ outcome = replay(ChildWorkflowFailureDetailsWf, history, [])
1413
+
1414
+ assert len(outcome.commands) == 1
1415
+ cmd = outcome.commands[0]
1416
+ assert isinstance(cmd, CompleteWorkflow)
1417
+ assert cmd.result["exception_class"] == "pkg.NestedClass"
1418
+
1366
1419
  def test_child_cancelled_is_observed_by_parent_as_typed_cancellation(self) -> None:
1367
1420
  history = [
1368
1421
  {
@@ -156,11 +156,23 @@ class TestSyncClientDescribe:
156
156
  "run_id": "run-1",
157
157
  "workflow_type": "greeter",
158
158
  "status": "running",
159
+ "search_attributes": {
160
+ "customer_id": "cust-php-sync",
161
+ "order_total_cents": 6100,
162
+ "is_vip": False,
163
+ "tags": ["standard", "renewal"],
164
+ },
159
165
  },
160
166
  )
161
167
  with patch.object(client._async._http, "request", new_callable=AsyncMock, return_value=resp):
162
168
  desc = client.describe_workflow("wf-1")
163
169
  assert desc.status == "running"
170
+ assert desc.search_attributes == {
171
+ "customer_id": "cust-php-sync",
172
+ "order_total_cents": 6100,
173
+ "is_vip": False,
174
+ "tags": ["standard", "renewal"],
175
+ }
164
176
 
165
177
 
166
178
  class TestSyncClientSignal:
@@ -202,12 +214,21 @@ class TestSyncClientList:
202
214
  resp = _mock_response(
203
215
  200,
204
216
  {
205
- "workflows": [{"workflow_id": "wf-1", "run_id": "r1", "workflow_type": "g", "status": "running"}],
217
+ "workflows": [
218
+ {
219
+ "workflow_id": "wf-1",
220
+ "run_id": "r1",
221
+ "workflow_type": "g",
222
+ "status": "running",
223
+ "search_attributes": {"customer_id": "cust-php-sync"},
224
+ }
225
+ ],
206
226
  },
207
227
  )
208
228
  with patch.object(client._async._http, "request", new_callable=AsyncMock, return_value=resp):
209
229
  result = client.list_workflows(workflow_type="g")
210
230
  assert len(result.executions) == 1
231
+ assert result.executions[0].search_attributes == {"customer_id": "cust-php-sync"}
211
232
 
212
233
  def test_list_task_queues(self) -> None:
213
234
  client = Client("http://localhost:8080")