durable-workflow 2.5.4__tar.gz → 2.6.0rc2__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 (130) hide show
  1. {durable_workflow-2.5.4/src/durable_workflow.egg-info → durable_workflow-2.6.0rc2}/PKG-INFO +1 -1
  2. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/pyproject.toml +3 -3
  3. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/client.py +53 -10
  4. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/nexus.py +15 -2
  5. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/worker.py +1 -121
  6. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/workflow.py +161 -11
  7. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  8. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/SOURCES.txt +3 -0
  9. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_client.py +3 -2
  10. durable_workflow-2.6.0rc2/tests/test_nexus_durable_wait.py +156 -0
  11. durable_workflow-2.6.0rc2/tests/test_nexus_namespace.py +160 -0
  12. durable_workflow-2.6.0rc2/tests/test_nexus_wire.py +38 -0
  13. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay_regression_corpus.py +22 -0
  14. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_runtime_external_payload_transport.py +49 -0
  15. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_worker.py +21 -58
  16. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/LICENSE +0 -0
  17. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/README.md +0 -0
  18. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/setup.cfg +0 -0
  19. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/__init__.py +0 -0
  20. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_activity_process.py +0 -0
  21. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_avro.py +0 -0
  22. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_cancellation_scope.py +0 -0
  23. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  24. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  25. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_prepared_local_activity.py +0 -0
  26. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  27. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/activity.py +0 -0
  28. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/auth_composition.py +0 -0
  29. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/cancellation.py +0 -0
  30. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/errors.py +0 -0
  31. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/external_storage.py +0 -0
  32. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/external_task_input.py +0 -0
  33. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/external_task_result.py +0 -0
  34. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/history_bundle_verify.py +0 -0
  35. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/interceptors.py +0 -0
  36. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/invocable.py +0 -0
  37. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/metrics.py +0 -0
  38. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/py.typed +0 -0
  39. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/python_conformance.py +0 -0
  40. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/replay_conformance.py +0 -0
  41. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/replay_verify.py +0 -0
  42. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/retry_policy.py +0 -0
  43. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/serializer.py +0 -0
  44. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/sync.py +0 -0
  45. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/testing.py +0 -0
  46. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/worker_session.py +0 -0
  47. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  48. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  49. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  50. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/requires.txt +0 -0
  51. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/top_level.txt +0 -0
  52. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_activity_cancellation_policies.py +0 -0
  53. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_activity_context.py +0 -0
  54. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_activity_process.py +0 -0
  55. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_auth_composition.py +0 -0
  56. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_avro_value_benchmark.py +0 -0
  57. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_avro_value_protocol.py +0 -0
  58. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_context.py +0 -0
  59. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_remaining_time.py +0 -0
  60. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_admission.py +0 -0
  61. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_authoring.py +0 -0
  62. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_boundary.py +0 -0
  63. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_opening.py +0 -0
  64. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_child_workflow_policies.py +0 -0
  65. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_ci_checkout.py +0 -0
  66. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_ci_integration_endpoint.py +0 -0
  67. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_client_timeouts.py +0 -0
  68. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_codec_regression_corpus.py +0 -0
  69. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  70. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_groups.py +0 -0
  71. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_history.py +0 -0
  72. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_replay.py +0 -0
  73. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_condition_replay_drift.py +0 -0
  74. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_control_plane_parity_fixtures.py +0 -0
  75. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_cancellation.py +0 -0
  76. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_cancellation_client.py +0 -0
  77. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_cancellation_worker.py +0 -0
  78. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_remote_worker.py +0 -0
  79. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_docstring_cross_references.py +0 -0
  80. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_durable_selection.py +0 -0
  81. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_errors.py +0 -0
  82. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_external_storage.py +0 -0
  83. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_external_task_input.py +0 -0
  84. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_external_task_result.py +0 -0
  85. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_golden_history_replay.py +0 -0
  86. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_history_bundle_verify.py +0 -0
  87. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_history_event_contract.py +0 -0
  88. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_invocable.py +0 -0
  89. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_message_streams.py +0 -0
  90. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_metrics.py +0 -0
  91. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_order_processing_example.py +0 -0
  92. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_parallel_completion_order_replay.py +0 -0
  93. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_parallel_saga_authoring.py +0 -0
  94. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_patch_history.py +0 -0
  95. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_pending_operation_replay.py +0 -0
  96. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_client.py +0 -0
  97. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_groups.py +0 -0
  98. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_policies.py +0 -0
  99. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_worker.py +0 -0
  100. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_public_boundary_scanner.py +0 -0
  101. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_pypi_project_surface.py +0 -0
  102. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_python_conformance.py +0 -0
  103. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_queries.py +0 -0
  104. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_readme_quickstart.py +0 -0
  105. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_redrive_replay.py +0 -0
  106. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_registration_ambiguity.py +0 -0
  107. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_release_metadata.py +0 -0
  108. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay.py +0 -0
  109. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay_conformance.py +0 -0
  110. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay_verify.py +0 -0
  111. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_retry_policy.py +0 -0
  112. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_run_cancellation_scope_replay.py +0 -0
  113. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_schedules.py +0 -0
  114. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_scoped_run_cancellation_context.py +0 -0
  115. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_serializer.py +0 -0
  116. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_signals.py +0 -0
  117. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_sleep.py +0 -0
  118. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_standalone_activity_client.py +0 -0
  119. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_sticky_execution.py +0 -0
  120. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_storage_admission.py +0 -0
  121. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_sync.py +0 -0
  122. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_testing_harness.py +0 -0
  123. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_update_signal_condition_replay.py +0 -0
  124. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_updates.py +0 -0
  125. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_wait_condition.py +0 -0
  126. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_worker_session.py +0 -0
  127. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_workflow_entrypoint_validation.py +0 -0
  128. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_workflow_result_timeout.py +0 -0
  129. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_workflow_streams.py +0 -0
  130. {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/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: 2.5.4
3
+ Version: 2.6.0rc2
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
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 = "2.5.4"
7
+ version = "2.6.0-rc.2"
8
8
  description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -71,8 +71,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
71
71
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
72
72
 
73
73
  [tool.durable-workflow]
74
- product-train = "2.5.4"
75
- registry-version = "2.5.4"
74
+ product-train = "2.6.0-rc.2"
75
+ registry-version = "2.6.0rc2"
76
76
  supported-server-versions = "2.5.16"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -19,6 +19,7 @@ methods without repeating the id on every call.
19
19
  from __future__ import annotations
20
20
 
21
21
  import asyncio
22
+ import copy
22
23
  import hashlib
23
24
  import json as json_module
24
25
  import math
@@ -299,8 +300,8 @@ def _contains_inline_payload_envelope(value: object) -> bool:
299
300
  if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
300
301
  return True
301
302
  if (
302
- value.get("type") == "record_side_effect"
303
- and isinstance(value.get("result"), str)
303
+ (value.get("type") == "record_side_effect" and isinstance(value.get("result"), str))
304
+ or (value.get("type") == "start_service_operation" and isinstance(value.get("request_payload"), str))
304
305
  ):
305
306
  return True
306
307
  return any(_contains_inline_payload_envelope(item) for item in value.values())
@@ -2079,21 +2080,23 @@ class Client:
2079
2080
  slot: tuple[str | int, ...] = (),
2080
2081
  ) -> Any:
2081
2082
  if isinstance(value, dict):
2082
- if (
2083
- value.get("type") == "record_side_effect"
2084
- and isinstance(value.get("result"), str)
2085
- ):
2083
+ command_type = value.get("type")
2084
+ blob_key = (
2085
+ {"record_side_effect": "result", "start_service_operation": "request_payload"}.get(command_type)
2086
+ if isinstance(command_type, str) else None
2087
+ )
2088
+ if blob_key is not None and isinstance(value.get(blob_key), str):
2086
2089
  normalized_command = dict(value)
2087
2090
  externalized_result = await self._externalize_runtime_payloads(
2088
- {"codec": serializer.AVRO_CODEC, "blob": value["result"]},
2091
+ {"codec": value.get("payload_codec", serializer.AVRO_CODEC), "blob": value[blob_key]},
2089
2092
  worker=worker,
2090
2093
  transport=transport,
2091
2094
  uploaded=uploaded,
2092
2095
  completion=completion,
2093
- slot=(*slot, "result"),
2096
+ slot=(*slot, blob_key),
2094
2097
  )
2095
2098
  if "external_payload" in externalized_result:
2096
- normalized_command["result"] = externalized_result
2099
+ normalized_command[blob_key] = externalized_result
2097
2100
  value = normalized_command
2098
2101
 
2099
2102
  if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
@@ -2672,6 +2675,26 @@ class Client:
2672
2675
  """
2673
2676
  return WorkflowHandle(self, workflow_id=workflow_id, run_id=run_id, workflow_type=workflow_type)
2674
2677
 
2678
+ def _nexus_target_client(self, target_namespace: str | None) -> Client:
2679
+ if target_namespace is not None and not target_namespace.strip():
2680
+ raise ValueError("target_namespace must be nonempty")
2681
+ if target_namespace is None or target_namespace == self.namespace:
2682
+ return self
2683
+ self._auth_token(worker=False)
2684
+ scoped = copy.copy(self)
2685
+ scoped.namespace = target_namespace
2686
+ # Share the HTTP pool, never the caller's namespace discovery or
2687
+ # provider credentials. The worker keeps using the original client.
2688
+ scoped.worker_token = None
2689
+ scoped._cluster_info = None
2690
+ scoped._cluster_info_lock = asyncio.Lock()
2691
+ scoped._runtime_external_payload_transport_cache = None
2692
+ scoped._runtime_external_payload_transport_resolved = False
2693
+ scoped.external_storage = None
2694
+ scoped.external_storage_threshold_bytes = None
2695
+ scoped.external_storage_cache = ExternalPayloadCache()
2696
+ return scoped
2697
+
2675
2698
  async def execute_nexus_operation(
2676
2699
  self,
2677
2700
  endpoint_name: str,
@@ -2685,6 +2708,7 @@ class Client:
2685
2708
  idempotency_key: str | None = None,
2686
2709
  payload_codec: str | None = None,
2687
2710
  caller_namespace: str | None = None,
2711
+ target_namespace: str | None = None,
2688
2712
  caller_workflow_instance_id: str | None = None,
2689
2713
  caller_workflow_run_id: str | None = None,
2690
2714
  service_sdk_language: str | None = None,
@@ -2708,7 +2732,11 @@ class Client:
2708
2732
  the worker uses this client method to make the single durable
2709
2733
  execute request and records the returned service-call surface into
2710
2734
  workflow history.
2735
+
2736
+ ``target_namespace`` selects the service catalog and its payload
2737
+ storage. The caller identity and worker namespace are preserved.
2711
2738
  """
2739
+ execution_client = self._nexus_target_client(target_namespace)
2712
2740
  request_body = nexus_request_payload(
2713
2741
  arguments=list(arguments) if arguments is not None else [],
2714
2742
  payload_codec=payload_codec,
@@ -2736,8 +2764,21 @@ class Client:
2736
2764
  )
2737
2765
  context = f"{endpoint_name}/{service_name}/{operation_name}"
2738
2766
 
2767
+ wire_body = request_body
2768
+ transport = await execution_client._runtime_external_payload_transport()
2769
+ if transport is not None:
2770
+ arguments_envelope = await execution_client._externalize_runtime_payloads(
2771
+ {"codec": request_body["payload_codec"], "blob": request_body["arguments"]},
2772
+ worker=False,
2773
+ transport=transport,
2774
+ uploaded={},
2775
+ slot=("arguments",),
2776
+ )
2777
+ if "external_payload" in arguments_envelope:
2778
+ wire_body = {**request_body, "arguments": arguments_envelope}
2779
+
2739
2780
  try:
2740
- data = await self._request("POST", path, json=request_body, context=context)
2781
+ data = await execution_client._request("POST", path, json=wire_body, context=context)
2741
2782
  except ServerError as exc:
2742
2783
  if exc.status != 409 or not isinstance(exc.body, dict):
2743
2784
  raise
@@ -2747,6 +2788,7 @@ class Client:
2747
2788
  service_name=service_name,
2748
2789
  operation_name=operation_name,
2749
2790
  request_payload=request_body,
2791
+ target_namespace=target_namespace,
2750
2792
  service_sdk_language=service_sdk_language,
2751
2793
  artifact_tuple=artifact_tuple,
2752
2794
  published_artifact_worker_execution=published_artifact_worker_execution,
@@ -2770,6 +2812,7 @@ class Client:
2770
2812
  service_name=service_name,
2771
2813
  operation_name=operation_name,
2772
2814
  request_payload=request_body,
2815
+ target_namespace=target_namespace,
2773
2816
  service_sdk_language=service_sdk_language,
2774
2817
  artifact_tuple=artifact_tuple,
2775
2818
  published_artifact_worker_execution=published_artifact_worker_execution,
@@ -8,6 +8,7 @@ from collections.abc import Mapping, Sequence
8
8
  from dataclasses import dataclass, field
9
9
  from typing import Any
10
10
 
11
+ from . import serializer
11
12
  from .errors import NexusOperationFailed
12
13
 
13
14
  NEXUS_OPERATION_RESULT_SCHEMA = "durable-workflow.v2.sdk-python.nexus-operation-result"
@@ -49,6 +50,7 @@ def stable_nexus_idempotency_key(
49
50
  service_name: str,
50
51
  operation_name: str,
51
52
  arguments: Sequence[Any],
53
+ target_namespace: str | None = None,
52
54
  ) -> str:
53
55
  """Return a deterministic idempotency key for one workflow-side Nexus call."""
54
56
  source = {
@@ -61,6 +63,8 @@ def stable_nexus_idempotency_key(
61
63
  "operation_name": operation_name,
62
64
  "arguments": list(arguments),
63
65
  }
66
+ if target_namespace is not None:
67
+ source["target_namespace"] = target_namespace
64
68
  encoded = json.dumps(source, sort_keys=True, separators=(",", ":"), default=_json_default)
65
69
  return f"dw-py-nexus-{hashlib.sha256(encoded.encode()).hexdigest()}"
66
70
 
@@ -87,9 +91,10 @@ def nexus_request_payload(
87
91
  duplicate_start_policy: str | None = None,
88
92
  ) -> dict[str, Any]:
89
93
  """Build the server execute-operation request body."""
94
+ codec = payload_codec or serializer.AVRO_CODEC
90
95
  return _clean_dict({
91
- "arguments": list(arguments),
92
- "payload_codec": payload_codec,
96
+ "arguments": serializer.encode(list(arguments), codec=codec),
97
+ "payload_codec": codec,
93
98
  "mode_override": mode,
94
99
  "wait_for": wait_for,
95
100
  "wait_timeout_seconds": wait_timeout_seconds,
@@ -134,6 +139,7 @@ class NexusOperationResult:
134
139
  caller_observed_error_type: str | None = None
135
140
  typed_error_message: str | None = None
136
141
  result: Any = None
142
+ target_namespace: str | None = None
137
143
 
138
144
  @classmethod
139
145
  def from_response(
@@ -147,6 +153,7 @@ class NexusOperationResult:
147
153
  service_sdk_language: str | None = None,
148
154
  artifact_tuple: Mapping[str, Any] | None = None,
149
155
  published_artifact_worker_execution: bool | None = None,
156
+ target_namespace: str | None = None,
150
157
  ) -> NexusOperationResult:
151
158
  call = data.get("service_call")
152
159
  source = call if isinstance(call, Mapping) else data
@@ -188,6 +195,7 @@ class NexusOperationResult:
188
195
  typed_error_message=_optional_str(source.get("typed_error_message"))
189
196
  or _optional_str(data.get("typed_error_message")),
190
197
  result=response_value,
198
+ target_namespace=target_namespace,
191
199
  )
192
200
 
193
201
  @classmethod
@@ -199,6 +207,9 @@ class NexusOperationResult:
199
207
  if value.get("version") != NEXUS_OPERATION_RESULT_VERSION:
200
208
  raise ValueError("recorded Nexus operation result version is not supported")
201
209
 
210
+ if value.get("target_namespace") != expected.get("target_namespace"):
211
+ raise ValueError("recorded Nexus target namespace does not match the current workflow command")
212
+
202
213
  for key in ("endpoint_name", "service_name", "operation_name", "idempotency_key"):
203
214
  expected_value = expected.get(key)
204
215
  recorded_value = value.get(key)
@@ -235,6 +246,7 @@ class NexusOperationResult:
235
246
  caller_observed_error_type=_optional_str(value.get("caller_observed_error_type")),
236
247
  typed_error_message=_optional_str(value.get("typed_error_message")),
237
248
  result=value.get("result"),
249
+ target_namespace=_optional_str(value.get("target_namespace")),
238
250
  )
239
251
  if result.is_failure:
240
252
  raise result.to_failure()
@@ -287,6 +299,7 @@ class NexusOperationResult:
287
299
  "endpoint_name": self.endpoint_name,
288
300
  "service_name": self.service_name,
289
301
  "operation_name": self.operation_name,
302
+ "target_namespace": self.target_namespace,
290
303
  "idempotency_key": self.request_payload.get("idempotency_key"),
291
304
  "caller_workflow_instance_id": self.caller_workflow_instance_id,
292
305
  "caller_workflow_run_id": self.caller_workflow_run_id,
@@ -98,10 +98,8 @@ from .workflow import (
98
98
  Command,
99
99
  FailWorkflow,
100
100
  LocalActivityExecutionAborted,
101
- NexusServiceCall,
102
101
  PreparedLocalActivityCall,
103
102
  RecordLocalActivity,
104
- RecordSideEffect,
105
103
  ReplayOutcome,
106
104
  ScheduleActivity,
107
105
  StartChildWorkflow,
@@ -166,7 +164,6 @@ _WORKFLOW_TASK_COMPLETION_AMBIGUOUS_REJECTION_REASONS = {
166
164
  }
167
165
  _WORKFLOW_TASK_COMPLETION_MAX_ATTEMPTS = 3
168
166
  _WORKFLOW_TASK_COMPLETION_RETRY_DELAYS = (0.05, 0.2)
169
- _WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT = 100
170
167
  _LOCAL_ACTIVITY_REPORT_LIMIT = 1000
171
168
  _WORKER_WORKFLOW_FINGERPRINTS: dict[tuple[str, str], str] = {}
172
169
  _P = ParamSpec("_P")
@@ -1428,85 +1425,6 @@ class Worker:
1428
1425
 
1429
1426
  return await handler(context)
1430
1427
 
1431
- async def _resolve_workflow_nexus_commands(
1432
- self,
1433
- workflow_cls: type,
1434
- history: list[dict[str, Any]],
1435
- start_input: list[Any],
1436
- task: dict[str, Any],
1437
- *,
1438
- payload_codec: str | None,
1439
- command_codec: str,
1440
- initial_commands: list[Command],
1441
- ) -> list[Command]:
1442
- events = list(history)
1443
- commands: list[Command] = list(initial_commands)
1444
- recorded_commands: list[RecordSideEffect] = []
1445
-
1446
- for _ in range(_WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT):
1447
- nexus_commands = [command for command in commands if isinstance(command, NexusServiceCall)]
1448
- if not nexus_commands:
1449
- return recorded_commands + commands
1450
- if len(nexus_commands) != len(commands):
1451
- raise RuntimeError("Nexus service calls must be yielded one at a time")
1452
-
1453
- for command in nexus_commands:
1454
- result = await self.client.execute_nexus_operation(
1455
- command.endpoint_name,
1456
- command.service_name,
1457
- command.operation_name,
1458
- command.arguments,
1459
- mode=command.mode,
1460
- wait_for=command.wait_for,
1461
- wait_timeout_seconds=command.wait_timeout_seconds,
1462
- idempotency_key=command.idempotency_key,
1463
- payload_codec=command.payload_codec,
1464
- caller_namespace=command.caller_namespace,
1465
- caller_workflow_instance_id=_string_or_none(task.get("workflow_id")),
1466
- caller_workflow_run_id=_string_or_none(task.get("run_id")),
1467
- service_sdk_language=command.service_sdk_language,
1468
- artifact_tuple=command.artifact_tuple,
1469
- published_artifact_worker_execution=command.published_artifact_worker_execution,
1470
- target_workflow_instance_id=command.target_workflow_instance_id,
1471
- target_workflow_run_id=command.target_workflow_run_id,
1472
- connection=command.connection,
1473
- queue=command.queue,
1474
- business_key=command.business_key,
1475
- labels=command.labels,
1476
- memo=command.memo,
1477
- search_attributes=command.search_attributes,
1478
- duplicate_start_policy=command.duplicate_start_policy,
1479
- raise_on_failure=False,
1480
- )
1481
- recorded_payload = result.to_recorded_payload()
1482
- recorded_commands.append(RecordSideEffect(recorded_payload))
1483
- events.append({
1484
- "event_type": "SideEffectRecorded",
1485
- "payload": {
1486
- "payload_codec": command_codec,
1487
- "result": serializer.encode(recorded_payload, codec=command_codec),
1488
- },
1489
- })
1490
-
1491
- outcome = replay(
1492
- workflow_cls,
1493
- events,
1494
- start_input,
1495
- workflow_id=task.get("workflow_id"),
1496
- run_id=_string_or_none(task.get("run_id")) or "",
1497
- workflow_command_id=(
1498
- _string_or_none(task.get("workflow_command_id"))
1499
- or _string_or_none(task.get("task_id"))
1500
- ),
1501
- payload_codec=payload_codec,
1502
- external_storage=self.external_storage,
1503
- external_storage_cache=self.external_storage_cache,
1504
- cancel_requested=bool(task.get("cancel_requested", False)),
1505
- )
1506
- commands = list(outcome.commands)
1507
-
1508
- raise RuntimeError("workflow yielded too many consecutive Nexus service calls")
1509
-
1510
1428
  async def _renew_local_workflow_lease(self, task: dict[str, Any]) -> None:
1511
1429
  if self._cooperative_cancellation_supported and self._local_activity_shutdown.is_set():
1512
1430
  raise LocalActivityExecutionAborted("worker shutdown abandoned its local workflow claim")
@@ -2628,45 +2546,7 @@ class Worker:
2628
2546
  log.warning("failed to report replay failure: %s", fe)
2629
2547
  return None
2630
2548
 
2631
- try:
2632
- workflow_commands = await self._resolve_workflow_nexus_commands(
2633
- cls,
2634
- list(history),
2635
- start_input,
2636
- task,
2637
- payload_codec=codec,
2638
- command_codec=command_codec,
2639
- initial_commands=list(outcome.commands),
2640
- )
2641
- except Exception as e:
2642
- log.exception("workflow Nexus service call resolution failed")
2643
- last_local = max(
2644
- (index for index, command in enumerate(outcome.commands)
2645
- if isinstance(command, RecordLocalActivity) and command.outcome is not None),
2646
- default=-1,
2647
- )
2648
- if last_local >= 0:
2649
- workflow_commands = [
2650
- *outcome.commands[:last_local + 1],
2651
- FailWorkflow(
2652
- f"Nexus resolution after local activity failed: {e}",
2653
- exception_type=type(e).__name__,
2654
- non_retryable=True,
2655
- ),
2656
- ]
2657
- else:
2658
- try:
2659
- await self.client.fail_workflow_task(
2660
- task_id=task_id,
2661
- lease_owner=self.worker_id,
2662
- workflow_task_attempt=attempt,
2663
- message=f"workflow Nexus service call resolution failed: {e}",
2664
- failure_type=type(e).__name__,
2665
- stack_trace=traceback.format_exc(),
2666
- )
2667
- except Exception as fe:
2668
- log.warning("failed to report workflow Nexus resolution failure: %s", fe)
2669
- return None
2549
+ workflow_commands = list(outcome.commands)
2670
2550
 
2671
2551
  if (
2672
2552
  any(isinstance(command, UpsertMemo) for command in workflow_commands)