durable-workflow 2.9.1__tar.gz → 2.10.0__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.
- {durable_workflow-2.9.1/src/durable_workflow.egg-info → durable_workflow-2.10.0}/PKG-INFO +1 -1
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/pyproject.toml +3 -3
- durable_workflow-2.10.0/src/durable_workflow/_metric_routes.py +57 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/client.py +28 -34
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/history_bundle_verify.py +143 -3
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/sync.py +6 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/testing.py +283 -25
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/worker.py +48 -10
- {durable_workflow-2.9.1 → durable_workflow-2.10.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow.egg-info/SOURCES.txt +9 -0
- durable_workflow-2.10.0/tests/test_history_bundle_schema2.py +214 -0
- durable_workflow-2.10.0/tests/test_metric_routes.py +137 -0
- durable_workflow-2.10.0/tests/test_poll_failure_backoff.py +315 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_replay_regression_corpus.py +2 -0
- durable_workflow-2.10.0/tests/test_sdk_metrics_contract.py +246 -0
- durable_workflow-2.10.0/tests/test_testing_failures.py +303 -0
- durable_workflow-2.10.0/tests/test_testing_nexus.py +149 -0
- durable_workflow-2.10.0/tests/test_tls_trust.py +200 -0
- durable_workflow-2.10.0/tests/test_transport_diagnostics.py +136 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_worker.py +9 -8
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/LICENSE +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/README.md +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/setup.cfg +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/workflow.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_client.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_sync.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.9.1 → durable_workflow-2.10.0}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.10.0"
|
|
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.
|
|
80
|
-
registry-version = "2.
|
|
79
|
+
product-train = "2.10.0"
|
|
80
|
+
registry-version = "2.10.0"
|
|
81
81
|
supported-server-versions = "2.8.0"
|
|
82
82
|
worker-protocol-version = "1.20"
|
|
83
83
|
control-plane-version = "2"
|
|
@@ -0,0 +1,57 @@
|
|
|
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/sessions", "/worker/sessions/{session_id}", "/worker/sessions/{session_id}/heartbeat",
|
|
16
|
+
*(f"/worker/{kind}-tasks/poll" for kind in ("workflow", "activity", "query", "update-validation")),
|
|
17
|
+
*(f"/worker/{kind}-tasks/{{task_id}}/{action}" for kind, actions in (
|
|
18
|
+
("workflow", ("complete", "fail", "heartbeat", "history", "deliver-cancellation")),
|
|
19
|
+
("activity", ("complete", "fail", "heartbeat", "status", "renew-lease", "acknowledge-cancellation")),
|
|
20
|
+
("query", ("complete", "fail", "status")),
|
|
21
|
+
("update-validation", ("approve", "reject")),
|
|
22
|
+
) for action in actions),
|
|
23
|
+
*(f"/worker/workflow-tasks/{{task_id}}/cancellation-scopes/{action}"
|
|
24
|
+
for action in ("open", "prepare", "deliver")),
|
|
25
|
+
*(f"/worker/workflow-tasks/{{task_id}}/local-activities/{action}"
|
|
26
|
+
for action in ("checkpoint", "checkpoint-group", "prepare", "recover")),
|
|
27
|
+
*(f"/worker/workflow-tasks/{{task_id}}/local-activities/{{activity_attempt_id}}/{action}"
|
|
28
|
+
for action in ("control", "heartbeat", "outcome", "acknowledge-cancellation")),
|
|
29
|
+
"/workflows", "/workflows/{workflow_id}",
|
|
30
|
+
*(f"/workflows/{{workflow_id}}/{action}"
|
|
31
|
+
for action in ("archive", "cancel", "repair", "terminate", "request-cancellation", "runs")),
|
|
32
|
+
*(f"/workflows/{{workflow_id}}/{action}/{{name}}" for action in ("signal", "query", "update")),
|
|
33
|
+
"/workflows/{workflow_id}/message-streams/{name}/messages",
|
|
34
|
+
"/workflows/{workflow_id}/runs/{run_id}",
|
|
35
|
+
*(f"/workflows/{{workflow_id}}/runs/{{run_id}}/{action}"
|
|
36
|
+
for action in ("history", "history/export", "redrive", "request-cancellation", "streams")),
|
|
37
|
+
"/workflows/{workflow_id}/runs/{run_id}/streams/{name}",
|
|
38
|
+
*(f"/workflows/{{workflow_id}}/runs/{{run_id}}/streams/{{name}}/{action}" for action in ("items", "close")),
|
|
39
|
+
"/schedules", "/schedules/{schedule_id}",
|
|
40
|
+
*(f"/schedules/{{schedule_id}}/{action}" for action in ("backfill", "pause", "resume", "trigger", "history")),
|
|
41
|
+
"/bridge-adapters/webhook/{adapter}",
|
|
42
|
+
"/service-endpoints/{endpoint_name}/services/{service_name}/operations/{operation_name}/execute",
|
|
43
|
+
)
|
|
44
|
+
_TEMPLATES = tuple((route, tuple(route.strip("/").split("/"))) for route in _ROUTES)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def route_for_metrics(path: str) -> str:
|
|
48
|
+
"""Return a supported operation template or the single bounded fallback."""
|
|
49
|
+
clean_path = path.split("?", 1)[0].split("#", 1)[0]
|
|
50
|
+
parts = tuple(clean_path.strip("/").split("/"))
|
|
51
|
+
for route, template in _TEMPLATES:
|
|
52
|
+
if len(parts) == len(template) and all(
|
|
53
|
+
expected == actual or expected.startswith("{") and bool(actual)
|
|
54
|
+
for actual, expected in zip(parts, template, strict=True)
|
|
55
|
+
):
|
|
56
|
+
return route
|
|
57
|
+
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
|
|
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(
|
|
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
|
|
|
@@ -1993,6 +1981,9 @@ class Client:
|
|
|
1993
1981
|
await query_authority.confirm()
|
|
1994
1982
|
outcome = "ok"
|
|
1995
1983
|
return result
|
|
1984
|
+
except asyncio.CancelledError:
|
|
1985
|
+
outcome = "CancelledError"
|
|
1986
|
+
raise
|
|
1996
1987
|
except Exception as exc:
|
|
1997
1988
|
if outcome == "pending":
|
|
1998
1989
|
outcome = type(exc).__name__
|
|
@@ -2679,6 +2670,9 @@ class Client:
|
|
|
2679
2670
|
)
|
|
2680
2671
|
outcome = "bridge_rejected" if resp.status_code == 422 else "ok"
|
|
2681
2672
|
return data
|
|
2673
|
+
except asyncio.CancelledError:
|
|
2674
|
+
outcome = "CancelledError"
|
|
2675
|
+
raise
|
|
2682
2676
|
except Exception as exc:
|
|
2683
2677
|
if outcome == "pending":
|
|
2684
2678
|
outcome = type(exc).__name__
|
{durable_workflow-2.9.1 → durable_workflow-2.10.0}/src/durable_workflow/history_bundle_verify.py
RENAMED
|
@@ -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"]
|
|
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
|
-
|
|
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
|
|
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,
|