durable-workflow 2.9.1__tar.gz → 2.10.1__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 (161) hide show
  1. {durable_workflow-2.9.1/src/durable_workflow.egg-info → durable_workflow-2.10.1}/PKG-INFO +1 -1
  2. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/pyproject.toml +3 -3
  3. durable_workflow-2.10.1/src/durable_workflow/_metric_routes.py +58 -0
  4. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/client.py +68 -43
  5. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/errors.py +15 -0
  6. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/history_bundle_verify.py +143 -3
  7. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/sync.py +6 -0
  8. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/testing.py +283 -25
  9. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/worker.py +144 -13
  10. {durable_workflow-2.9.1 → durable_workflow-2.10.1/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  11. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow.egg-info/SOURCES.txt +10 -0
  12. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cooperative_cancellation_worker.py +8 -3
  13. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cooperative_remote_worker.py +19 -5
  14. durable_workflow-2.10.1/tests/test_history_bundle_schema2.py +214 -0
  15. durable_workflow-2.10.1/tests/test_metric_routes.py +137 -0
  16. durable_workflow-2.10.1/tests/test_poll_failure_backoff.py +315 -0
  17. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_replay_regression_corpus.py +4 -0
  18. durable_workflow-2.10.1/tests/test_sdk_metrics_contract.py +246 -0
  19. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_storage_admission.py +17 -3
  20. durable_workflow-2.10.1/tests/test_testing_failures.py +303 -0
  21. durable_workflow-2.10.1/tests/test_testing_nexus.py +149 -0
  22. durable_workflow-2.10.1/tests/test_tls_trust.py +200 -0
  23. durable_workflow-2.10.1/tests/test_transport_diagnostics.py +136 -0
  24. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_worker.py +20 -16
  25. durable_workflow-2.10.1/tests/test_worker_deregistration.py +361 -0
  26. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_worker_observation.py +12 -0
  27. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/LICENSE +0 -0
  28. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/README.md +0 -0
  29. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/setup.cfg +0 -0
  30. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/__init__.py +0 -0
  31. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_activity_lease.py +0 -0
  32. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_activity_process.py +0 -0
  33. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_avro.py +0 -0
  34. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_cancellation_scope.py +0 -0
  35. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  36. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  37. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_prepared_local_activity.py +0 -0
  38. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_query_task_authority.py +0 -0
  39. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  40. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/activity.py +0 -0
  41. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/auth_composition.py +0 -0
  42. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/cancellation.py +0 -0
  43. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/external_storage.py +0 -0
  44. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/external_task_input.py +0 -0
  45. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/external_task_result.py +0 -0
  46. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/interceptors.py +0 -0
  47. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/invocable.py +0 -0
  48. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/metrics.py +0 -0
  49. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/nexus.py +0 -0
  50. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/py.typed +0 -0
  51. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/python_conformance.py +0 -0
  52. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/replay_conformance.py +0 -0
  53. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/replay_verify.py +0 -0
  54. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/retry_policy.py +0 -0
  55. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/serializer.py +0 -0
  56. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/telemetry.py +0 -0
  57. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/telemetry_carrier.py +0 -0
  58. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/worker_session.py +0 -0
  59. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/workflow.py +0 -0
  60. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  61. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  62. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  63. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow.egg-info/requires.txt +0 -0
  64. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/src/durable_workflow.egg-info/top_level.txt +0 -0
  65. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_activity_cancellation_policies.py +0 -0
  66. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_activity_context.py +0 -0
  67. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_activity_heartbeat_authority.py +0 -0
  68. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_activity_lease_authority.py +0 -0
  69. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_activity_lease_renewal.py +0 -0
  70. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_activity_lease_worker.py +0 -0
  71. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_activity_process.py +0 -0
  72. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_auth_composition.py +0 -0
  73. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_avro_value_benchmark.py +0 -0
  74. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_avro_value_protocol.py +0 -0
  75. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cancellation_context.py +0 -0
  76. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cancellation_remaining_time.py +0 -0
  77. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cancellation_scope_admission.py +0 -0
  78. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cancellation_scope_authoring.py +0 -0
  79. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cancellation_scope_boundary.py +0 -0
  80. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cancellation_scope_opening.py +0 -0
  81. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_child_workflow_policies.py +0 -0
  82. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_ci_checkout.py +0 -0
  83. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_ci_integration_endpoint.py +0 -0
  84. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_client.py +0 -0
  85. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_client_telemetry.py +0 -0
  86. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_client_timeouts.py +0 -0
  87. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_codec_regression_corpus.py +0 -0
  88. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  89. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_committed_cancellation_scope_groups.py +0 -0
  90. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_committed_cancellation_scope_history.py +0 -0
  91. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_committed_cancellation_scope_replay.py +0 -0
  92. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_condition_replay_drift.py +0 -0
  93. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_control_plane_parity_fixtures.py +0 -0
  94. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cooperative_cancellation.py +0 -0
  95. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_cooperative_cancellation_client.py +0 -0
  96. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_docstring_cross_references.py +0 -0
  97. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_durable_selection.py +0 -0
  98. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_errors.py +0 -0
  99. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_external_storage.py +0 -0
  100. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_external_task_input.py +0 -0
  101. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_external_task_result.py +0 -0
  102. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_golden_history_replay.py +0 -0
  103. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_history_bundle_verify.py +0 -0
  104. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_history_event_contract.py +0 -0
  105. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_invocable.py +0 -0
  106. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_message_streams.py +0 -0
  107. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_metrics.py +0 -0
  108. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_nexus_durable_wait.py +0 -0
  109. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_nexus_namespace.py +0 -0
  110. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_nexus_wire.py +0 -0
  111. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_order_processing_example.py +0 -0
  112. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_parallel_completion_order_replay.py +0 -0
  113. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_parallel_saga_authoring.py +0 -0
  114. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_patch_history.py +0 -0
  115. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_pending_operation_replay.py +0 -0
  116. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_prepared_local_activity_client.py +0 -0
  117. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_prepared_local_activity_groups.py +0 -0
  118. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_prepared_local_activity_policies.py +0 -0
  119. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_prepared_local_activity_worker.py +0 -0
  120. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_public_boundary_scanner.py +0 -0
  121. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_pypi_project_surface.py +0 -0
  122. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_python_conformance.py +0 -0
  123. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_queries.py +0 -0
  124. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_query_task_authority.py +0 -0
  125. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_query_timeout_retry.py +0 -0
  126. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_readme_quickstart.py +0 -0
  127. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_redrive_replay.py +0 -0
  128. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_registration_ambiguity.py +0 -0
  129. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_release_metadata.py +0 -0
  130. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_replay.py +0 -0
  131. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_replay_clock.py +0 -0
  132. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_replay_conformance.py +0 -0
  133. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_replay_verify.py +0 -0
  134. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_retry_policy.py +0 -0
  135. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_run_cancellation_scope_replay.py +0 -0
  136. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_runtime_external_payload_transport.py +0 -0
  137. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_schedules.py +0 -0
  138. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_scoped_run_cancellation_context.py +0 -0
  139. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_serializer.py +0 -0
  140. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_signals.py +0 -0
  141. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_sleep.py +0 -0
  142. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_standalone_activity_client.py +0 -0
  143. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_sticky_execution.py +0 -0
  144. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_sync.py +0 -0
  145. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_sync_activities.py +0 -0
  146. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_telemetry.py +0 -0
  147. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_telemetry_carrier.py +0 -0
  148. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_testing_harness.py +0 -0
  149. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_threaded_heartbeat_example.py +0 -0
  150. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_update_signal_condition_replay.py +0 -0
  151. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_updates.py +0 -0
  152. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_wait_condition.py +0 -0
  153. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_worker_session.py +0 -0
  154. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_worker_telemetry_wire.py +0 -0
  155. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_workflow_entry_arguments.py +0 -0
  156. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_workflow_entrypoint_validation.py +0 -0
  157. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_workflow_handler_validation.py +0 -0
  158. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_workflow_identity.py +0 -0
  159. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_workflow_result_timeout.py +0 -0
  160. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/tests/test_workflow_streams.py +0 -0
  161. {durable_workflow-2.9.1 → durable_workflow-2.10.1}/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.9.1
3
+ Version: 2.10.1
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.9.1"
7
+ version = "2.10.1"
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"
@@ -76,8 +76,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
76
76
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
77
77
 
78
78
  [tool.durable-workflow]
79
- product-train = "2.9.1"
80
- registry-version = "2.9.1"
79
+ product-train = "2.10.1"
80
+ registry-version = "2.10.1"
81
81
  supported-server-versions = "2.8.0"
82
82
  worker-protocol-version = "1.20"
83
83
  control-plane-version = "2"
@@ -0,0 +1,58 @@
1
+ """Bound request labels to reviewed SDK operations, never arbitrary URL values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _ROUTES = (
6
+ "/", "/health", "/cluster/info", "/activities", "/activities/{activity_id}",
7
+ "/namespaces", "/namespaces/{name}", "/namespaces/{name}/external-storage",
8
+ "/search-attributes", "/search-attributes/{name}", "/storage/test",
9
+ "/task-queues", "/task-queues/{task_queue}", "/task-queues/{task_queue}/build-ids",
10
+ *(f"/task-queues/{{task_queue}}/build-ids/{action}" for action in ("drain", "promote", "resume")),
11
+ *(f"/system/{operation}{suffix}" for operation in ("activity-timeouts", "repair", "retention")
12
+ for suffix in ("", "/pass")),
13
+ "/workers", "/workers/{worker_id}",
14
+ "/worker/register", "/worker/heartbeat", "/worker/registrations/{worker_id}",
15
+ "/worker/registrations/{worker_id}/deregister",
16
+ "/worker/sessions", "/worker/sessions/{session_id}", "/worker/sessions/{session_id}/heartbeat",
17
+ *(f"/worker/{kind}-tasks/poll" for kind in ("workflow", "activity", "query", "update-validation")),
18
+ *(f"/worker/{kind}-tasks/{{task_id}}/{action}" for kind, actions in (
19
+ ("workflow", ("complete", "fail", "heartbeat", "history", "deliver-cancellation")),
20
+ ("activity", ("complete", "fail", "heartbeat", "status", "renew-lease", "acknowledge-cancellation")),
21
+ ("query", ("complete", "fail", "status")),
22
+ ("update-validation", ("approve", "reject")),
23
+ ) for action in actions),
24
+ *(f"/worker/workflow-tasks/{{task_id}}/cancellation-scopes/{action}"
25
+ for action in ("open", "prepare", "deliver")),
26
+ *(f"/worker/workflow-tasks/{{task_id}}/local-activities/{action}"
27
+ for action in ("checkpoint", "checkpoint-group", "prepare", "recover")),
28
+ *(f"/worker/workflow-tasks/{{task_id}}/local-activities/{{activity_attempt_id}}/{action}"
29
+ for action in ("control", "heartbeat", "outcome", "acknowledge-cancellation")),
30
+ "/workflows", "/workflows/{workflow_id}",
31
+ *(f"/workflows/{{workflow_id}}/{action}"
32
+ for action in ("archive", "cancel", "repair", "terminate", "request-cancellation", "runs")),
33
+ *(f"/workflows/{{workflow_id}}/{action}/{{name}}" for action in ("signal", "query", "update")),
34
+ "/workflows/{workflow_id}/message-streams/{name}/messages",
35
+ "/workflows/{workflow_id}/runs/{run_id}",
36
+ *(f"/workflows/{{workflow_id}}/runs/{{run_id}}/{action}"
37
+ for action in ("history", "history/export", "redrive", "request-cancellation", "streams")),
38
+ "/workflows/{workflow_id}/runs/{run_id}/streams/{name}",
39
+ *(f"/workflows/{{workflow_id}}/runs/{{run_id}}/streams/{{name}}/{action}" for action in ("items", "close")),
40
+ "/schedules", "/schedules/{schedule_id}",
41
+ *(f"/schedules/{{schedule_id}}/{action}" for action in ("backfill", "pause", "resume", "trigger", "history")),
42
+ "/bridge-adapters/webhook/{adapter}",
43
+ "/service-endpoints/{endpoint_name}/services/{service_name}/operations/{operation_name}/execute",
44
+ )
45
+ _TEMPLATES = tuple((route, tuple(route.strip("/").split("/"))) for route in _ROUTES)
46
+
47
+
48
+ def route_for_metrics(path: str) -> str:
49
+ """Return a supported operation template or the single bounded fallback."""
50
+ clean_path = path.split("?", 1)[0].split("#", 1)[0]
51
+ parts = tuple(clean_path.strip("/").split("/"))
52
+ for route, template in _TEMPLATES:
53
+ if len(parts) == len(template) and all(
54
+ expected == actual or expected.startswith("{") and bool(actual)
55
+ for actual, expected in zip(parts, template, strict=True)
56
+ ):
57
+ return route
58
+ return "unknown"
@@ -26,6 +26,7 @@ import json as json_module
26
26
  import math
27
27
  import os
28
28
  import re
29
+ import ssl
29
30
  import time
30
31
  import uuid
31
32
  import warnings
@@ -47,6 +48,7 @@ from ._cancellation_scope_history import (
47
48
  scope_timestamp,
48
49
  )
49
50
  from ._cooperative_cancellation import CancellationDelivery, CancellationRequest
51
+ from ._metric_routes import route_for_metrics as _route_for_metrics
50
52
  from ._query_task_authority import QueryTaskAuthority, current_query_authority
51
53
  from .errors import (
52
54
  REQUEST_ID_CONFLICT,
@@ -289,38 +291,6 @@ def _command_request_failure(exc: Exception, *, command: str, request_id: str) -
289
291
  return exc
290
292
 
291
293
 
292
- def _route_for_metrics(path: str) -> str:
293
- clean_path = path.split("?", 1)[0]
294
- parts = [part for part in clean_path.strip("/").split("/") if part]
295
- if not parts:
296
- return "/"
297
-
298
- if parts[0] == "workflows" and len(parts) >= 2:
299
- parts[1] = "{workflow_id}"
300
- if len(parts) >= 4 and parts[2] in {"signal", "query", "update"}:
301
- parts[3] = "{name}"
302
- if len(parts) >= 4 and parts[2] == "runs":
303
- parts[3] = "{run_id}"
304
- elif parts[0] == "schedules" and len(parts) >= 2:
305
- parts[1] = "{schedule_id}"
306
- elif parts[0] in {"namespaces", "search-attributes"} and len(parts) >= 2:
307
- parts[1] = "{name}"
308
- elif parts[0] == "workers" and len(parts) >= 2:
309
- parts[1] = "{worker_id}"
310
- elif parts[:2] == ["worker", "registrations"] and len(parts) >= 3:
311
- parts[2] = "{worker_id}"
312
- elif parts[:2] == ["bridge-adapters", "webhook"] and len(parts) >= 3:
313
- parts[2] = "{adapter}"
314
- elif (
315
- parts[:2] == ["worker", "workflow-tasks"]
316
- or parts[:2] == ["worker", "activity-tasks"]
317
- or parts[:2] == ["worker", "query-tasks"]
318
- ) and len(parts) >= 3:
319
- parts[2] = "{task_id}"
320
-
321
- return "/" + "/".join(parts)
322
-
323
-
324
294
  def _resolve_namespace_name(
325
295
  name: str | None,
326
296
  namespace_alias: str | None,
@@ -1631,6 +1601,10 @@ class Client:
1631
1601
 
1632
1602
  The client owns one `httpx.AsyncClient` connection pool. Use it as an async
1633
1603
  context manager or call `aclose()` when finished.
1604
+
1605
+ ``tls_context`` selects an explicit trust configuration. It must require
1606
+ certificate and hostname verification. Treat the context as read-only after
1607
+ construction; worker query clones and Nexus namespace views reuse it.
1634
1608
  """
1635
1609
 
1636
1610
  def __init__(
@@ -1642,6 +1616,7 @@ class Client:
1642
1616
  worker_token: str | None = None,
1643
1617
  namespace: str = "default",
1644
1618
  timeout: float = 60.0,
1619
+ tls_context: ssl.SSLContext | None = None,
1645
1620
  retry_policy: TransportRetryPolicy | None = None,
1646
1621
  metrics: MetricsRecorder | None = None,
1647
1622
  payload_size_limit_bytes: int = serializer.DEFAULT_PAYLOAD_SIZE_BYTES,
@@ -1652,6 +1627,11 @@ class Client:
1652
1627
  external_storage_cache: ExternalPayloadCache | None = None,
1653
1628
  telemetry: ExecutionTelemetry | None = None,
1654
1629
  ) -> None:
1630
+ if tls_context is not None:
1631
+ if not isinstance(tls_context, ssl.SSLContext):
1632
+ raise TypeError("tls_context must be an ssl.SSLContext")
1633
+ if tls_context.verify_mode != ssl.CERT_REQUIRED or not tls_context.check_hostname:
1634
+ raise ValueError("tls_context must enable certificate and hostname verification")
1655
1635
  self.base_url = _normalize_base_url(base_url)
1656
1636
  self.token = token
1657
1637
  self.control_token = control_token
@@ -1660,8 +1640,9 @@ class Client:
1660
1640
  self.telemetry = telemetry
1661
1641
  self._observability_capability = False
1662
1642
  self.timeout = timeout
1643
+ self._tls_context = tls_context
1663
1644
  self.retry_policy = retry_policy or TransportRetryPolicy()
1664
- self.metrics = metrics or NOOP_METRICS
1645
+ self.metrics = metrics if metrics is not None else NOOP_METRICS
1665
1646
  self.payload_size_warning_config = (
1666
1647
  serializer.PayloadSizeWarningConfig(
1667
1648
  limit_bytes=payload_size_limit_bytes,
@@ -1679,7 +1660,9 @@ class Client:
1679
1660
  if external_storage_cache is not None
1680
1661
  else ExternalPayloadCache()
1681
1662
  )
1682
- self._http = httpx.AsyncClient(base_url=self.base_url, timeout=timeout)
1663
+ self._http = httpx.AsyncClient(
1664
+ base_url=self.base_url, timeout=timeout, verify=tls_context if tls_context is not None else True,
1665
+ )
1683
1666
  self._cluster_info: dict[str, Any] | None = None
1684
1667
  self._cluster_info_lock = asyncio.Lock()
1685
1668
  self._runtime_external_payload_transport_cache: (
@@ -1688,6 +1671,11 @@ class Client:
1688
1671
  self._runtime_external_payload_transport_resolved = False
1689
1672
  self._workflow_payload_observers: dict[str, Callable[[dict[str, Any]], Awaitable[None]]] = {}
1690
1673
 
1674
+ @property
1675
+ def tls_context(self) -> ssl.SSLContext | None:
1676
+ """The explicit TLS context, or ``None`` for httpx's default trust selection."""
1677
+ return self._tls_context
1678
+
1691
1679
  async def aclose(self) -> None:
1692
1680
  """Close the underlying ``httpx`` connection pool.
1693
1681
 
@@ -1865,6 +1853,7 @@ class Client:
1865
1853
  json: Any = None,
1866
1854
  timeout: float | None = None,
1867
1855
  context: str = "",
1856
+ retry: bool = True,
1868
1857
  ) -> Any:
1869
1858
  if worker and discovery:
1870
1859
  raise ValueError("a request cannot be both worker-plane and cluster discovery")
@@ -1932,7 +1921,7 @@ class Client:
1932
1921
 
1933
1922
  try:
1934
1923
  try:
1935
- resp = await self.retry_policy.execute(_do_request)
1924
+ resp = await self.retry_policy.execute(_do_request) if retry else await _do_request()
1936
1925
  except httpx.HTTPStatusError as exc:
1937
1926
  status_code = str(exc.response.status_code)
1938
1927
  outcome = "http_error"
@@ -1993,6 +1982,9 @@ class Client:
1993
1982
  await query_authority.confirm()
1994
1983
  outcome = "ok"
1995
1984
  return result
1985
+ except asyncio.CancelledError:
1986
+ outcome = "CancelledError"
1987
+ raise
1996
1988
  except Exception as exc:
1997
1989
  if outcome == "pending":
1998
1990
  outcome = type(exc).__name__
@@ -2679,6 +2671,9 @@ class Client:
2679
2671
  )
2680
2672
  outcome = "bridge_rejected" if resp.status_code == 422 else "ok"
2681
2673
  return data
2674
+ except asyncio.CancelledError:
2675
+ outcome = "CancelledError"
2676
+ raise
2682
2677
  except Exception as exc:
2683
2678
  if outcome == "pending":
2684
2679
  outcome = type(exc).__name__
@@ -5251,39 +5246,69 @@ class Client:
5251
5246
  json={"worker_id": worker_id, "reason": reason},
5252
5247
  ))
5253
5248
 
5254
- async def deregister_worker_registration(self, worker_id: str) -> dict[str, Any]:
5249
+ async def deregister_worker_registration(
5250
+ self, worker_id: str, *, registration_token: str | None = None, timeout: float | None = None,
5251
+ expected_namespace: str | None = None,
5252
+ ) -> dict[str, Any]:
5255
5253
  """Remove this runtime's successful worker-plane registration.
5256
5254
 
5257
5255
  Called by :class:`~durable_workflow.Worker` after its pollers and
5258
5256
  in-flight tasks have drained. This is distinct from the operator-only
5259
5257
  :meth:`deregister_worker` control-plane management operation. Returns
5260
5258
  the worker deregistration envelope, including recovered workflow-task
5261
- count.
5259
+ count. With a registration token, use the runtime's generation-fenced
5260
+ receipt endpoint. Each call makes one attempt: Worker owns safe retries
5261
+ and the remaining shutdown budget. An unfenced DELETE cannot safely be
5262
+ repeated after an uncertain response because the worker ID may be reused.
5263
+ ``expected_namespace`` is a local registration-scope precondition, not
5264
+ a namespace override.
5262
5265
  """
5266
+ if expected_namespace is not None and self.namespace != expected_namespace:
5267
+ raise RuntimeError("worker registration namespace changed; registration removal is unconfirmed")
5268
+ if registration_token is not None and (
5269
+ not isinstance(registration_token, str) or re.fullmatch(r"[a-f0-9]{32}", registration_token) is None
5270
+ ):
5271
+ raise ValueError("registration_token must contain exactly 32 lowercase hexadecimal characters")
5272
+ fenced = registration_token is not None
5263
5273
  try:
5264
5274
  data = await self._request(
5265
- "DELETE",
5266
- f"/worker/registrations/{quote(worker_id, safe='')}",
5275
+ "POST" if fenced else "DELETE",
5276
+ f"/worker/registrations/{quote(worker_id, safe='')}" + ("/deregister" if fenced else ""),
5267
5277
  worker=True,
5268
5278
  context=worker_id,
5279
+ json={"registration_token": registration_token} if fenced else None,
5280
+ timeout=timeout,
5281
+ retry=False,
5269
5282
  )
5270
5283
  except ServerError as error:
5271
- if error.status == 404 and error.reason() == "worker_not_found":
5284
+ if not fenced and error.status == 404 and error.reason() == "worker_not_found":
5272
5285
  return {
5273
5286
  "worker_id": worker_id,
5274
5287
  "outcome": "already_deregistered",
5275
5288
  "recovered_workflow_task_count": 0,
5276
5289
  }
5277
5290
  raise
5278
- if not isinstance(data, dict):
5291
+ if not self._valid_worker_deregistration_response(data, worker_id, registration_token):
5279
5292
  raise ServerError(
5280
5293
  200,
5281
5294
  {
5282
5295
  "reason": "invalid_worker_deregistration_response",
5283
- "message": f"expected JSON object, got {type(data).__name__}",
5296
+ "message": "expected a deregistration receipt for this worker registration",
5284
5297
  },
5285
5298
  )
5286
- return data
5299
+ return cast(dict[str, Any], data)
5300
+
5301
+ @staticmethod
5302
+ def _valid_worker_deregistration_response(data: Any, worker_id: str, token: str | None) -> bool:
5303
+ if not isinstance(data, dict):
5304
+ return False
5305
+ count = data.get("recovered_workflow_task_count")
5306
+ return (
5307
+ data.get("worker_id") == worker_id
5308
+ and data.get("outcome") == "deregistered"
5309
+ and type(count) is int and count >= 0
5310
+ and (token is None or data.get("registration_token") == token)
5311
+ )
5287
5312
 
5288
5313
  async def heartbeat_worker(
5289
5314
  self,
@@ -219,6 +219,21 @@ class ServerError(DurableWorkflowError):
219
219
  return self.body.get("reason")
220
220
  return None
221
221
 
222
+ def worker_deregistration_retry_delay(self, worker_id: str, registration_token: str) -> int | None:
223
+ """Return a delay only for an identity-preserving fenced transient."""
224
+ body = self.body
225
+ if (
226
+ self.status != 503 or not isinstance(body, dict)
227
+ or body.get("reason") not in ("backend_lock_pressure", "backend_unavailable")
228
+ or body.get("operation") != "deregister_worker" or body.get("outcome") != "unknown"
229
+ or body.get("worker_id") != worker_id or body.get("registration_token") != registration_token
230
+ or body.get("retryable") is not True
231
+ or type(body.get("retry_after_seconds")) is not int
232
+ or not 0 < body["retry_after_seconds"] <= 60
233
+ ):
234
+ return None
235
+ return int(body["retry_after_seconds"])
236
+
222
237
  def poll_capacity_backpressure_delay(self, task_kind: str, task_queue: str) -> int | None:
223
238
  """Return the advertised wait only for a complete poll-capacity refusal."""
224
239
  body = self.body
@@ -23,11 +23,15 @@ a workflow-php verdict, so promotion gates can compare them directly.
23
23
  from __future__ import annotations
24
24
 
25
25
  import argparse
26
+ import base64
26
27
  import hashlib
27
28
  import hmac
28
29
  import json
30
+ import math
31
+ import re
29
32
  import sys
30
33
  from collections.abc import Mapping, Sequence
34
+ from decimal import Decimal
31
35
  from pathlib import Path
32
36
  from typing import Any
33
37
 
@@ -37,7 +41,9 @@ REPORT_SCHEMA = "durable-workflow.v2.history-bundle-verification"
37
41
  REPORT_SCHEMA_VERSION = 1
38
42
 
39
43
  BUNDLE_SCHEMA = "durable-workflow.v2.history-export"
44
+ # Retained for callers that construct the supported legacy format.
40
45
  BUNDLE_SCHEMA_VERSION = 1
46
+ SUPPORTED_BUNDLE_SCHEMA_VERSIONS = frozenset({1, 2})
41
47
 
42
48
  STATUS_OK = "ok"
43
49
  STATUS_WARNING = "warning"
@@ -138,13 +144,14 @@ def _check_envelope(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -
138
144
  "Bundle is missing the [schema_version] field.",
139
145
  )
140
146
  )
141
- elif bundle["schema_version"] != BUNDLE_SCHEMA_VERSION:
147
+ elif (type(bundle["schema_version"]) is not int
148
+ or bundle["schema_version"] not in SUPPORTED_BUNDLE_SCHEMA_VERSIONS):
142
149
  findings.append(
143
150
  _finding(
144
151
  "bundle.schema_version_unsupported",
145
152
  SEVERITY_ERROR,
146
153
  f"Bundle schema_version [{bundle['schema_version']!s}] is unsupported; "
147
- f"expected {BUNDLE_SCHEMA_VERSION}.",
154
+ "expected integer 1 or 2.",
148
155
  {"observed": bundle["schema_version"]},
149
156
  )
150
157
  )
@@ -230,6 +237,9 @@ def _check_workflow(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -
230
237
  )
231
238
  )
232
239
 
240
+ if type(bundle.get("schema_version")) is int and bundle["schema_version"] == 2:
241
+ _check_v2_memo(bundle, workflow, findings)
242
+
233
243
  declared = _int_or_none(workflow.get("last_history_sequence"))
234
244
  events = bundle.get("history_events")
235
245
  if declared is not None and isinstance(events, list) and events:
@@ -253,6 +263,85 @@ def _check_workflow(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -
253
263
  )
254
264
 
255
265
 
266
+ def _check_v2_memo(
267
+ bundle: Mapping[str, Any], workflow: Mapping[str, Any], findings: list[dict[str, Any]],
268
+ ) -> None:
269
+ redaction = bundle.get("redaction")
270
+ paths = redaction.get("paths") if isinstance(redaction, Mapping) else None
271
+ paths = paths if isinstance(paths, list) else []
272
+ if "workflow.memo_payload" in paths:
273
+ return
274
+
275
+ payload = workflow.get("memo_payload")
276
+ if not isinstance(payload, dict):
277
+ findings.append(_finding(
278
+ "workflow.memo_payload_missing", SEVERITY_ERROR,
279
+ "Schema-2 workflow.memo_payload must contain the lossless portable Avro memo authority.",
280
+ ))
281
+ return
282
+ try:
283
+ entries = _decode_memo_entries(payload)
284
+ except (TypeError, ValueError) as exc:
285
+ findings.append(_finding(
286
+ "workflow.memo_payload_invalid", SEVERITY_ERROR,
287
+ f"workflow.memo_payload is not a valid portable Avro memo map: {exc}",
288
+ ))
289
+ return
290
+ if "workflow.memo" in paths:
291
+ return
292
+
293
+ # MemoPayload::decodeEntries normalizes the top-level map to a PHP array;
294
+ # its empty display projection is [], while nested empty maps retain an
295
+ # explicit map adapter. Compare JSON kinds as well as values (True != 1).
296
+ projection = {key: _memo_value_projection(value) for key, value in entries.items()} if entries else []
297
+ try:
298
+ projection_matches = json.dumps(workflow.get("memo"), sort_keys=True) == json.dumps(projection, sort_keys=True)
299
+ except (TypeError, ValueError):
300
+ projection_matches = False
301
+ if not projection_matches:
302
+ findings.append(_finding(
303
+ "workflow.memo_projection_mismatch", SEVERITY_ERROR,
304
+ "workflow.memo does not match the JSON-safe projection of workflow.memo_payload.",
305
+ ))
306
+
307
+
308
+ def _decode_memo_entries(payload: dict[str, Any]) -> dict[str, Any]:
309
+ if set(payload) != {"codec", "blob"} or payload["codec"] != "avro":
310
+ raise ValueError("expected exactly the codec=avro and blob fields")
311
+ if not isinstance(payload["blob"], str) or not payload["blob"]:
312
+ raise ValueError("blob must be a non-empty base64 string")
313
+ entries = _avro.decode(payload["blob"])
314
+ if not isinstance(entries, dict):
315
+ raise ValueError("memo authority must decode to a string-keyed map")
316
+ for key in entries:
317
+ if not re.fullmatch(r"[A-Za-z0-9_.:-]{1,64}", key) or re.fullmatch(r"-?\d+", key, flags=re.ASCII):
318
+ raise ValueError(f"invalid portable memo key: {key!r}")
319
+ return entries
320
+
321
+
322
+ def _memo_value_projection(value: Any) -> Any:
323
+ if isinstance(value, bytes):
324
+ return {"$type": "bytes", "base64": base64.b64encode(value).decode("ascii")}
325
+ if isinstance(value, list):
326
+ return [_memo_value_projection(item) for item in value]
327
+ if isinstance(value, dict):
328
+ # PHP uses AvroMapValue for empty maps and maps containing keys that
329
+ # its native arrays would coerce to signed 64-bit integers.
330
+ if not value or any(_php_integer_key(key) for key in value):
331
+ return {"$type": "map", "entries": [
332
+ {"key": key, "value": _memo_value_projection(item)} for key, item in value.items()
333
+ ]}
334
+ return {key: _memo_value_projection(item) for key, item in value.items()}
335
+ return value
336
+
337
+
338
+ def _php_integer_key(key: str) -> bool:
339
+ # PHP numeric array keys use ASCII digits; Unicode decimal digits remain strings.
340
+ if len(key) > 20 or not re.fullmatch(r"0|-?[1-9]\d*", key, flags=re.ASCII):
341
+ return False
342
+ return -(2**63) <= int(key) <= 2**63 - 1
343
+
344
+
256
345
  def _check_history_events(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -> None:
257
346
  events = bundle.get("history_events")
258
347
  if not isinstance(events, list):
@@ -664,7 +753,57 @@ def _check_integrity(
664
753
  def _canonicalize(bundle: Mapping[str, Any]) -> str:
665
754
  cloned = {k: v for k, v in bundle.items() if k != "integrity"}
666
755
  canonical = _canonicalize_value(cloned)
667
- return json.dumps(canonical, separators=(",", ":"), ensure_ascii=False, sort_keys=False)
756
+ return _canonical_json_value(canonical)
757
+
758
+
759
+ def _canonical_json_value(value: Any) -> str:
760
+ """Encode the existing PHP json-recursive-ksort-v1 byte representation."""
761
+ if isinstance(value, dict):
762
+ if not all(isinstance(key, str) for key in value):
763
+ raise TypeError("History bundle JSON object keys must be strings")
764
+ return "{" + ",".join(_canonical_json_string(key) + ":" + _canonical_json_value(item)
765
+ for key, item in value.items()) + "}"
766
+ if isinstance(value, list):
767
+ return "[" + ",".join(_canonical_json_value(item) for item in value) + "]"
768
+ if isinstance(value, str):
769
+ return _canonical_json_string(value)
770
+ if isinstance(value, float):
771
+ return _canonical_json_float(value)
772
+ return json.dumps(value, separators=(",", ":"), allow_nan=False)
773
+
774
+
775
+ def _canonical_json_string(value: str) -> str:
776
+ # PHP escapes non-ASCII characters as UTF-16 code units while leaving
777
+ # ASCII DEL unescaped. Start with standard JSON quoting, then escape only
778
+ # its non-ASCII characters. Invalid surrogate strings fail encoding.
779
+ encoded = json.dumps(value, ensure_ascii=False)
780
+ if encoded.isascii():
781
+ return encoded
782
+ return "".join(character if ord(character) < 128 else _json_utf16_escape(character) for character in encoded)
783
+
784
+
785
+ def _json_utf16_escape(character: str) -> str:
786
+ units = character.encode("utf-16-be").hex()
787
+ return "".join("\\u" + units[offset:offset + 4] for offset in range(0, len(units), 4))
788
+
789
+
790
+ def _canonical_json_float(value: float) -> str:
791
+ if not math.isfinite(value):
792
+ raise ValueError("History bundle JSON numbers must be finite")
793
+ text = float.__repr__(value)
794
+ mantissa, separator, exponent_text = text.partition("e")
795
+ if not separator:
796
+ return text
797
+ exponent = int(exponent_text)
798
+ # PHP uses fixed notation through exponent 16 and preserves a decimal
799
+ # fraction for floats. Both runtimes otherwise use shortest-roundtrip
800
+ # digits, but PHP omits exponent leading zeros and writes 1.0e rather than 1e.
801
+ if -4 <= exponent < 17:
802
+ fixed = format(Decimal(text), "f")
803
+ return fixed if "." in fixed else fixed + ".0"
804
+ if "." not in mantissa:
805
+ mantissa += ".0"
806
+ return f"{mantissa}e{exponent:+d}"
668
807
 
669
808
 
670
809
  def _canonicalize_value(value: Any) -> Any:
@@ -846,6 +985,7 @@ __all__ = [
846
985
  "REPORT_SCHEMA_VERSION",
847
986
  "BUNDLE_SCHEMA",
848
987
  "BUNDLE_SCHEMA_VERSION",
988
+ "SUPPORTED_BUNDLE_SCHEMA_VERSIONS",
849
989
  "STATUS_OK",
850
990
  "STATUS_WARNING",
851
991
  "STATUS_FAILED",
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import ssl
6
7
  from collections.abc import Coroutine
7
8
  from typing import Any, TypeVar
8
9
 
@@ -266,6 +267,9 @@ class Client:
266
267
 
267
268
  The client keeps one event loop for its lifetime so its async HTTP connection
268
269
  pool and returned handles always run on the loop that owns them.
270
+
271
+ ``tls_context`` forwards the same verified trust configuration accepted by
272
+ the async client. Treat the supplied context as read-only during use.
269
273
  """
270
274
 
271
275
  def __init__(
@@ -277,6 +281,7 @@ class Client:
277
281
  worker_token: str | None = None,
278
282
  namespace: str = "default",
279
283
  timeout: float = 60.0,
284
+ tls_context: ssl.SSLContext | None = None,
280
285
  retry_policy: TransportRetryPolicy | None = None,
281
286
  metrics: MetricsRecorder | None = None,
282
287
  external_storage: ExternalStorageDriver | None = None,
@@ -290,6 +295,7 @@ class Client:
290
295
  worker_token=worker_token,
291
296
  namespace=namespace,
292
297
  timeout=timeout,
298
+ tls_context=tls_context,
293
299
  retry_policy=retry_policy,
294
300
  metrics=metrics,
295
301
  external_storage=external_storage,