durable-workflow 2.5.13__tar.gz → 2.5.15__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 (133) hide show
  1. {durable_workflow-2.5.13/src/durable_workflow.egg-info → durable_workflow-2.5.15}/PKG-INFO +1 -1
  2. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/pyproject.toml +4 -4
  3. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/__init__.py +2 -0
  4. durable_workflow-2.5.15/src/durable_workflow/_query_task_authority.py +153 -0
  5. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/client.py +77 -9
  6. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/errors.py +9 -0
  7. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/worker.py +65 -7
  8. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/workflow.py +43 -18
  9. {durable_workflow-2.5.13 → durable_workflow-2.5.15/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  10. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
  11. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_client.py +17 -8
  12. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_condition_replay_drift.py +4 -2
  13. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_external_storage.py +8 -4
  14. durable_workflow-2.5.15/tests/test_query_task_authority.py +305 -0
  15. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_replay_regression_corpus.py +36 -1
  16. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_storage_admission.py +16 -6
  17. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_worker.py +26 -25
  18. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/LICENSE +0 -0
  19. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/README.md +0 -0
  20. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/setup.cfg +0 -0
  21. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/_activity_process.py +0 -0
  22. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/_avro.py +0 -0
  23. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/_cancellation_scope.py +0 -0
  24. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  25. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  26. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/_prepared_local_activity.py +0 -0
  27. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  28. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/activity.py +0 -0
  29. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/auth_composition.py +0 -0
  30. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/cancellation.py +0 -0
  31. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/external_storage.py +0 -0
  32. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/external_task_input.py +0 -0
  33. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/external_task_result.py +0 -0
  34. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/history_bundle_verify.py +0 -0
  35. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/interceptors.py +0 -0
  36. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/invocable.py +0 -0
  37. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/metrics.py +0 -0
  38. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/nexus.py +0 -0
  39. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/py.typed +0 -0
  40. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/python_conformance.py +0 -0
  41. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/replay_conformance.py +0 -0
  42. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/replay_verify.py +0 -0
  43. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/retry_policy.py +0 -0
  44. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/serializer.py +0 -0
  45. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/sync.py +0 -0
  46. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/testing.py +0 -0
  47. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/worker_session.py +0 -0
  48. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  49. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  50. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  51. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow.egg-info/requires.txt +0 -0
  52. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/src/durable_workflow.egg-info/top_level.txt +0 -0
  53. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_activity_cancellation_policies.py +0 -0
  54. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_activity_context.py +0 -0
  55. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_activity_process.py +0 -0
  56. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_auth_composition.py +0 -0
  57. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_avro_value_benchmark.py +0 -0
  58. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_avro_value_protocol.py +0 -0
  59. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cancellation_context.py +0 -0
  60. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cancellation_remaining_time.py +0 -0
  61. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cancellation_scope_admission.py +0 -0
  62. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cancellation_scope_authoring.py +0 -0
  63. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cancellation_scope_boundary.py +0 -0
  64. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cancellation_scope_opening.py +0 -0
  65. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_child_workflow_policies.py +0 -0
  66. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_ci_checkout.py +0 -0
  67. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_ci_integration_endpoint.py +0 -0
  68. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_client_timeouts.py +0 -0
  69. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_codec_regression_corpus.py +0 -0
  70. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  71. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_committed_cancellation_scope_groups.py +0 -0
  72. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_committed_cancellation_scope_history.py +0 -0
  73. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_committed_cancellation_scope_replay.py +0 -0
  74. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_control_plane_parity_fixtures.py +0 -0
  75. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cooperative_cancellation.py +0 -0
  76. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cooperative_cancellation_client.py +0 -0
  77. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cooperative_cancellation_worker.py +0 -0
  78. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_cooperative_remote_worker.py +0 -0
  79. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_docstring_cross_references.py +0 -0
  80. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_durable_selection.py +0 -0
  81. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_errors.py +0 -0
  82. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_external_task_input.py +0 -0
  83. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_external_task_result.py +0 -0
  84. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_golden_history_replay.py +0 -0
  85. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_history_bundle_verify.py +0 -0
  86. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_history_event_contract.py +0 -0
  87. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_invocable.py +0 -0
  88. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_message_streams.py +0 -0
  89. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_metrics.py +0 -0
  90. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_nexus_namespace.py +0 -0
  91. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_nexus_wire.py +0 -0
  92. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_order_processing_example.py +0 -0
  93. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_parallel_completion_order_replay.py +0 -0
  94. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_parallel_saga_authoring.py +0 -0
  95. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_patch_history.py +0 -0
  96. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_pending_operation_replay.py +0 -0
  97. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_prepared_local_activity_client.py +0 -0
  98. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_prepared_local_activity_groups.py +0 -0
  99. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_prepared_local_activity_policies.py +0 -0
  100. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_prepared_local_activity_worker.py +0 -0
  101. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_public_boundary_scanner.py +0 -0
  102. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_pypi_project_surface.py +0 -0
  103. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_python_conformance.py +0 -0
  104. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_queries.py +0 -0
  105. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_query_timeout_retry.py +0 -0
  106. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_readme_quickstart.py +0 -0
  107. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_redrive_replay.py +0 -0
  108. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_registration_ambiguity.py +0 -0
  109. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_release_metadata.py +0 -0
  110. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_replay.py +0 -0
  111. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_replay_clock.py +0 -0
  112. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_replay_conformance.py +0 -0
  113. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_replay_verify.py +0 -0
  114. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_retry_policy.py +0 -0
  115. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_run_cancellation_scope_replay.py +0 -0
  116. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_runtime_external_payload_transport.py +0 -0
  117. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_schedules.py +0 -0
  118. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_scoped_run_cancellation_context.py +0 -0
  119. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_serializer.py +0 -0
  120. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_signals.py +0 -0
  121. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_sleep.py +0 -0
  122. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_standalone_activity_client.py +0 -0
  123. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_sticky_execution.py +0 -0
  124. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_sync.py +0 -0
  125. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_testing_harness.py +0 -0
  126. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_update_signal_condition_replay.py +0 -0
  127. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_updates.py +0 -0
  128. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_wait_condition.py +0 -0
  129. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_worker_session.py +0 -0
  130. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_workflow_entrypoint_validation.py +0 -0
  131. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_workflow_result_timeout.py +0 -0
  132. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/tests/test_workflow_streams.py +0 -0
  133. {durable_workflow-2.5.13 → durable_workflow-2.5.15}/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.13
3
+ Version: 2.5.15
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.13"
7
+ version = "2.5.15"
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,9 +71,9 @@ 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.13"
75
- registry-version = "2.5.13"
76
- supported-server-versions = "2.5.16"
74
+ product-train = "2.5.15"
75
+ registry-version = "2.5.15"
76
+ supported-server-versions = "2.8.0"
77
77
  worker-protocol-version = "1.20"
78
78
  control-plane-version = "2"
79
79
  durable-selection = true
@@ -88,6 +88,7 @@ from .errors import (
88
88
  NonDeterministicReplayError,
89
89
  NonRetryableError,
90
90
  QueryFailed,
91
+ QueryTaskAuthorityLost,
91
92
  RuntimeCapabilityUnsupported,
92
93
  RuntimeDiscoveryUnavailable,
93
94
  SagaCompensationFailed,
@@ -375,6 +376,7 @@ __all__ = [
375
376
  "InvalidWorkflowDefinition",
376
377
  "NoopMetrics",
377
378
  "QueryFailed",
379
+ "QueryTaskAuthorityLost",
378
380
  "RuntimeCapabilityUnsupported",
379
381
  "RuntimeDiscoveryUnavailable",
380
382
  "RuntimeExternalPayloadReference",
@@ -0,0 +1,153 @@
1
+ """A query delivery's original, non-renewing execution budget."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import re
7
+ import time
8
+ from collections.abc import Awaitable, Callable, Mapping
9
+ from contextvars import ContextVar
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from typing import Any, TypeVar
13
+
14
+ from .errors import QueryTaskAuthorityLost
15
+
16
+ _T = TypeVar("_T")
17
+ _UTC6 = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z\Z")
18
+ _OBSERVATION_INTERVAL_SECONDS = 5.0
19
+
20
+
21
+ def _timestamp(value: Any, *, canonical: bool) -> datetime:
22
+ if not isinstance(value, str) or (canonical and _UTC6.fullmatch(value) is None):
23
+ raise ValueError("missing or malformed query timing")
24
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
25
+ if parsed.tzinfo is None:
26
+ raise ValueError("query timing must include a timezone")
27
+ return parsed
28
+
29
+
30
+ @dataclass
31
+ class QueryTaskAuthority:
32
+ client: object
33
+ namespace: str
34
+ task_id: str
35
+ attempt: int
36
+ owner: str
37
+ observed: bool
38
+ timing: dict[str, str]
39
+ until: float
40
+ identity: dict[str, Any]
41
+ observer: Callable[[], Awaitable[Mapping[str, Any]]] | None = field(default=None, repr=False)
42
+
43
+ @classmethod
44
+ def delivery(cls, client: object, namespace: str, task: Mapping[str, Any], *,
45
+ owner: str, task_queue: str, observed: bool) -> QueryTaskAuthority:
46
+ task_id, attempt = task.get("query_task_id"), task.get("query_task_attempt")
47
+ if (not isinstance(owner, str) or not owner or not isinstance(task_queue, str) or not task_queue
48
+ or not isinstance(task_id, str) or not task_id or type(attempt) is not int or attempt < 1
49
+ or task.get("lease_owner") != owner or task.get("task_queue") != task_queue
50
+ or task.get("namespace", namespace) != namespace):
51
+ raise QueryTaskAuthorityLost(str(task_id or ""), "invalid_query_claim")
52
+ try:
53
+ lease = _timestamp(task.get("lease_expires_at"), canonical=observed)
54
+ timing = {"lease_expires_at": task["lease_expires_at"]}
55
+ bounds = [lease]
56
+ if observed or "query_admitted_at" in task or "query_deadline_at" in task:
57
+ admitted = _timestamp(task.get("query_admitted_at"), canonical=True)
58
+ deadline = _timestamp(task.get("query_deadline_at"), canonical=True)
59
+ if deadline < admitted or (observed and lease > deadline):
60
+ raise ValueError("inconsistent original query timing")
61
+ timing.update({"query_admitted_at": task["query_admitted_at"],
62
+ "query_deadline_at": task["query_deadline_at"]})
63
+ bounds.append(deadline)
64
+ # Legacy compatibility has no non-renewing observation. Its local
65
+ # clock check cannot establish an undisclosed caller deadline.
66
+ reference = (_timestamp(task.get("server_time"), canonical=True) if observed
67
+ else datetime.now(timezone.utc))
68
+ until = time.monotonic() + (min(bounds) - reference).total_seconds()
69
+ except (ValueError, TypeError, OverflowError) as error:
70
+ raise QueryTaskAuthorityLost(task_id, "invalid_query_timing") from error
71
+ result = cls(client, namespace, task_id, attempt, owner, observed, timing, until,
72
+ {key: task.get(key) for key in ("workflow_id", "run_id", "task_queue")})
73
+ result.remaining()
74
+ return result
75
+
76
+ def matches(self, client: object, namespace: str, task: Mapping[str, Any], owner: str) -> bool:
77
+ return (self.client is client and self.namespace == namespace and self.owner == owner
78
+ and task.get("query_task_id") == self.task_id
79
+ and type(task.get("query_task_attempt")) is int and task.get("query_task_attempt") == self.attempt
80
+ and task.get("lease_owner") == owner
81
+ and all(task.get(key) == value for key, value in self.identity.items())
82
+ and all(task.get(key) == value for key, value in self.timing.items()))
83
+
84
+ def remaining(self) -> float:
85
+ remaining = self.until - time.monotonic()
86
+ if remaining <= 0:
87
+ raise QueryTaskAuthorityLost(self.task_id, "query_execution_budget_expired")
88
+ return remaining
89
+
90
+ async def confirm(self) -> None:
91
+ remaining = self.remaining()
92
+ if not self.observed:
93
+ return
94
+ started = time.monotonic()
95
+ try:
96
+ if self.observer is None:
97
+ raise ValueError("query authority observer is unavailable")
98
+ reply = await asyncio.wait_for(self.observer(), timeout=min(5.0, remaining))
99
+ if (not isinstance(reply, Mapping) or reply.get("query_task_id") != self.task_id
100
+ or type(reply.get("query_task_attempt")) is not int
101
+ or reply.get("query_task_attempt") != self.attempt or reply.get("lease_owner") != self.owner
102
+ or any(reply.get(key) != value for key, value in self.timing.items())):
103
+ raise ValueError("query authority observation changed its original claim")
104
+ if (reply.get("can_continue") is not True or reply.get("query_task_status") != "leased"
105
+ or reply.get("reason") is not None):
106
+ raise QueryTaskAuthorityLost(self.task_id, str(reply.get("reason") or "query_claim_unavailable"))
107
+ server_time = _timestamp(reply.get("server_time"), canonical=True)
108
+ bound = min(_timestamp(value, canonical=True) for key, value in self.timing.items()
109
+ if key != "query_admitted_at")
110
+ # Charging the entire observation round trip is conservative. A
111
+ # later observation may shorten this bound but can never reset it.
112
+ self.until = min(self.until, started + (bound - server_time).total_seconds())
113
+ except QueryTaskAuthorityLost:
114
+ raise
115
+ except Exception as error:
116
+ raise QueryTaskAuthorityLost(self.task_id, "query_authority_unavailable") from error
117
+ self.remaining()
118
+
119
+ async def run(self, operation: Callable[[], Awaitable[_T]], *, observe: bool = True,
120
+ confirm_after: bool = True) -> _T:
121
+ await self.confirm()
122
+ execution = asyncio.ensure_future(operation())
123
+
124
+ async def watch() -> None:
125
+ while True:
126
+ await asyncio.sleep(min(_OBSERVATION_INTERVAL_SECONDS, self.remaining()))
127
+ await self.confirm()
128
+
129
+ observation = asyncio.create_task(watch()) if observe and self.observed else None
130
+ pending: set[asyncio.Future[Any]] = {execution}
131
+ if observation is not None:
132
+ pending.add(observation)
133
+ try:
134
+ done, _ = await asyncio.wait(pending, timeout=self.remaining(), return_when=asyncio.FIRST_COMPLETED)
135
+ if not done:
136
+ raise QueryTaskAuthorityLost(self.task_id, "query_execution_budget_expired")
137
+ if observation is not None and observation in done:
138
+ await observation
139
+ raise QueryTaskAuthorityLost(self.task_id, "query_authority_unavailable")
140
+ result = await execution
141
+ if confirm_after:
142
+ await self.confirm()
143
+ return result
144
+ finally:
145
+ for future in pending:
146
+ future.cancel()
147
+ await asyncio.gather(*pending, return_exceptions=True)
148
+
149
+
150
+ # Async task creation copies this context. Each dispatched query keeps the
151
+ # loading budget without adding private fields to the public wire dictionary
152
+ # or retaining an unbounded task-ID registry on Client/Worker.
153
+ current_query_authority: ContextVar[QueryTaskAuthority | None] = ContextVar("query_task_authority", default=None)
@@ -45,6 +45,7 @@ from ._cancellation_scope_history import (
45
45
  scope_timestamp,
46
46
  )
47
47
  from ._cooperative_cancellation import CancellationDelivery, CancellationRequest
48
+ from ._query_task_authority import QueryTaskAuthority, current_query_authority
48
49
  from .errors import (
49
50
  ExternalPayloadIntegrityMismatch,
50
51
  ExternalPayloadOversized,
@@ -1809,6 +1810,9 @@ class Client:
1809
1810
  if discovery and path != "/cluster/info":
1810
1811
  raise ValueError("cluster discovery credentials can only authorize /cluster/info")
1811
1812
 
1813
+ query_poll = worker and path == "/worker/query-tasks/poll"
1814
+ if query_poll:
1815
+ current_query_authority.set(None)
1812
1816
  request_json = json
1813
1817
  if (
1814
1818
  not discovery
@@ -1872,21 +1876,29 @@ class Client:
1872
1876
  outcome = "ok"
1873
1877
  return None
1874
1878
  result = resp.json()
1879
+ query_authority = None
1880
+ if query_poll and isinstance(result, dict) and isinstance(result.get("task"), dict):
1881
+ query_authority = await self._query_delivery_authority(result["task"], request_json)
1882
+ current_query_authority.set(query_authority)
1875
1883
  if not discovery and _contains_runtime_payload_envelope(result):
1876
- transport = await self._runtime_external_payload_transport()
1884
+ transport = (await query_authority.run(self._runtime_external_payload_transport)
1885
+ if query_authority is not None else await self._runtime_external_payload_transport())
1877
1886
  if transport is None:
1878
1887
  raise ExternalPayloadUnsupported(
1879
1888
  "runtime returned an external payload reference without advertising "
1880
1889
  "the authenticated namespace transport"
1881
1890
  )
1882
- result = await self._resolve_workflow_claim_payloads(
1883
- result,
1884
- worker=worker,
1885
- transport=transport,
1886
- path=path,
1887
- request_json=request_json,
1888
- )
1891
+ if query_authority is not None:
1892
+ result = await query_authority.run(lambda: self._resolve_runtime_payloads(
1893
+ result, worker=worker, transport=transport, resolved={},
1894
+ ))
1895
+ else:
1896
+ result = await self._resolve_workflow_claim_payloads(
1897
+ result, worker=worker, transport=transport, path=path, request_json=request_json,
1898
+ )
1889
1899
  result = self._hydrate_result_envelopes(result)
1900
+ if query_authority is not None:
1901
+ await query_authority.confirm()
1890
1902
  outcome = "ok"
1891
1903
  return result
1892
1904
  except Exception as exc:
@@ -2332,6 +2344,7 @@ class Client:
2332
2344
  *,
2333
2345
  worker: bool,
2334
2346
  transport: _RuntimeExternalPayloadTransport,
2347
+ resolved: dict[tuple[str, str, int, str], str] | None = None,
2335
2348
  ) -> Any:
2336
2349
  if isinstance(value, dict):
2337
2350
  if "external_payload" in value:
@@ -2347,6 +2360,9 @@ class Client:
2347
2360
  "runtime external payload envelope codec does not match its reference",
2348
2361
  reference_id=reference.reference_id,
2349
2362
  )
2363
+ identity = (reference.reference_id, reference.codec, reference.size_bytes, reference.sha256)
2364
+ if resolved is not None and identity in resolved:
2365
+ return {"codec": reference.codec, "blob": resolved[identity]}
2350
2366
  data = await self._fetch_runtime_payload(
2351
2367
  reference,
2352
2368
  worker=worker,
@@ -2359,12 +2375,15 @@ class Client:
2359
2375
  "runtime external payload bytes are not a UTF-8 payload blob",
2360
2376
  reference_id=reference.reference_id,
2361
2377
  ) from exc
2378
+ if resolved is not None:
2379
+ resolved[identity] = blob
2362
2380
  return {"codec": reference.codec, "blob": blob}
2363
2381
  return {
2364
2382
  key: await self._resolve_runtime_payloads(
2365
2383
  item,
2366
2384
  worker=worker,
2367
2385
  transport=transport,
2386
+ resolved=resolved,
2368
2387
  )
2369
2388
  for key, item in value.items()
2370
2389
  }
@@ -2374,6 +2393,7 @@ class Client:
2374
2393
  item,
2375
2394
  worker=worker,
2376
2395
  transport=transport,
2396
+ resolved=resolved,
2377
2397
  )
2378
2398
  for item in value
2379
2399
  ]
@@ -4825,8 +4845,10 @@ class Client:
4825
4845
  end_time: str,
4826
4846
  overlap_policy: str | None = None,
4827
4847
  ) -> ScheduleBackfillResult:
4828
- """Fire a schedule for every would-have-been moment in ``[start_time, end_time]``.
4848
+ """Fire a schedule for every would-have-been moment in ``[start_time, end_time)``.
4829
4849
 
4850
+ The start is inclusive and the end is exclusive. An occurrence equal
4851
+ to ``end_time`` is excluded and belongs to the next adjacent window.
4830
4852
  Times are ISO-8601 strings. Useful to replay a period the schedule
4831
4853
  was paused or to seed historical runs. The returned
4832
4854
  :class:`ScheduleBackfillResult` reports how many fires were attempted
@@ -5625,6 +5647,48 @@ class Client:
5625
5647
  "POST", f"/worker/workflow-tasks/{task_id}/history", worker=True, json=body
5626
5648
  )
5627
5649
 
5650
+ async def _query_delivery_authority(self, task: dict[str, Any], request: Any) -> QueryTaskAuthority:
5651
+ # Worker registration establishes discovery before it polls. Advanced
5652
+ # callers without discovery retain only the exposed local lease checks.
5653
+ info = self._cluster_info or {}
5654
+ protocol = info.get("worker_protocol")
5655
+ capabilities = protocol.get("server_capabilities") if isinstance(protocol, dict) else None
5656
+ observed = (isinstance(capabilities, dict) and capabilities.get("query_task_authority") is True
5657
+ and _supports_cooperative_cancellation_protocol(_protocol_version_from_env(
5658
+ "DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", PROTOCOL_VERSION)))
5659
+ request = request if isinstance(request, dict) else {}
5660
+ authority = QueryTaskAuthority.delivery(self, self.namespace, task, owner=request.get("worker_id", ""),
5661
+ task_queue=request.get("task_queue", ""), observed=observed)
5662
+ if observed:
5663
+ authority.observer = lambda: self.query_task_status(query_task_id=authority.task_id,
5664
+ lease_owner=authority.owner, query_task_attempt=authority.attempt)
5665
+ await authority.confirm()
5666
+ return authority
5667
+
5668
+ async def query_task_status(self, *, query_task_id: str, lease_owner: str,
5669
+ query_task_attempt: int) -> Any:
5670
+ """Observe a protocol 1.20 query claim without renewing it.
5671
+
5672
+ Requires ``query_task_authority=true`` discovery. A positive observation
5673
+ does not reserve authority for later execution or completion.
5674
+ """
5675
+ if not _supports_cooperative_cancellation_protocol(_protocol_version_from_env(
5676
+ "DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", PROTOCOL_VERSION,
5677
+ )):
5678
+ raise ValueError("query task observation requires worker protocol 1.20")
5679
+ capability_path = "worker_protocol.server_capabilities.query_task_authority"
5680
+ info = await self._runtime_discovery(operation="Client.query_task_status", required_path=capability_path)
5681
+ protocol = info.get("worker_protocol")
5682
+ capabilities = protocol.get("server_capabilities") if isinstance(protocol, dict) else None
5683
+ supported = capabilities.get("query_task_authority") if isinstance(capabilities, dict) else None
5684
+ if supported is not True:
5685
+ error_type = RuntimeCapabilityUnsupported if supported is False else RuntimeDiscoveryUnavailable
5686
+ raise error_type("Client.query_task_status", capability_path,
5687
+ "Query task observation requires Server query_task_authority=true discovery.")
5688
+ query = urlencode({"lease_owner": lease_owner, "query_task_attempt": query_task_attempt})
5689
+ return await self._request("GET", f"/worker/query-tasks/{quote(query_task_id, safe='')}/status?{query}",
5690
+ worker=True, timeout=5.0)
5691
+
5628
5692
  async def poll_query_task(
5629
5693
  self,
5630
5694
  *,
@@ -5640,6 +5704,10 @@ class Client:
5640
5704
  must route a control-plane query to a non-PHP workflow runtime.
5641
5705
  ``timeout`` controls the server long-poll window; the HTTP request
5642
5706
  uses a small grace margin.
5707
+
5708
+ Worker registration establishes capability discovery. Custom workers
5709
+ should call ``get_cluster_info()`` first to enable advertised query
5710
+ status checks; without discovery only supplied local lease bounds apply.
5643
5711
  """
5644
5712
  body: dict[str, Any] = {
5645
5713
  "worker_id": worker_id,
@@ -27,6 +27,15 @@ class DurableWorkflowError(Exception):
27
27
  """Base class for every exception raised by the SDK."""
28
28
 
29
29
 
30
+ class QueryTaskAuthorityLost(DurableWorkflowError):
31
+ """A query delivery cannot safely load or invoke its handler."""
32
+
33
+ def __init__(self, query_task_id: str, reason: str) -> None:
34
+ super().__init__(f"query task {query_task_id!r} cannot continue: {reason}")
35
+ self.query_task_id = query_task_id
36
+ self.reason = reason
37
+
38
+
30
39
  class InvalidWorkflowDefinition(DurableWorkflowError, ValueError):
31
40
  """Workflow authoring uses an unsupported entry point or result."""
32
41
 
@@ -41,6 +41,7 @@ from ._activity_process import CallbackFailure, CallbackInvocation, CallbackProc
41
41
  from ._cancellation_scope_history import CancellationScopeBudget, CancellationScopeDeliveryReceipt
42
42
  from ._cooperative_cancellation import CancellationRequest, read_cancellation_history
43
43
  from ._prepared_local_activity import PreparedAttempt, PreparedCancellationObserved, PreparedLocalRunner
44
+ from ._query_task_authority import QueryTaskAuthority, current_query_authority
44
45
  from ._sticky_workflow_cache import CacheKey, StickyWorkflowCache, complete_history
45
46
  from .activity import ActivityContext, ActivityInfo, _set_context
46
47
  from .auth_composition import (
@@ -74,6 +75,7 @@ from .errors import (
74
75
  NonDeterministicReplayError,
75
76
  NonRetryableError,
76
77
  QueryFailed,
78
+ QueryTaskAuthorityLost,
77
79
  ServerError,
78
80
  UpdateRejected,
79
81
  UpdateValidationFailed,
@@ -107,11 +109,11 @@ from .workflow import (
107
109
  ScheduleActivity,
108
110
  StartChildWorkflow,
109
111
  UpsertMemo,
112
+ _prepare_query_state,
110
113
  _validate_workflow_entrypoint,
111
114
  _workflow_handler_registries,
112
115
  apply_update,
113
116
  commands_to_server_commands,
114
- query_state,
115
117
  replay,
116
118
  validate_update,
117
119
  )
@@ -1035,8 +1037,11 @@ class Worker:
1035
1037
  external_storage: ExternalStorageDriver | None = None,
1036
1038
  external_storage_threshold_bytes: int | None = None,
1037
1039
  external_storage_cache: ExternalPayloadCache | None = None,
1040
+ require_query_task_authority: bool = False,
1038
1041
  ) -> None:
1039
1042
  self.client = client
1043
+ self.require_query_task_authority = require_query_task_authority
1044
+ self._query_authority_supported = False
1040
1045
  self.task_queue = task_queue
1041
1046
  self.workflows: dict[str, type] = {}
1042
1047
  for definition in workflows:
@@ -1300,6 +1305,14 @@ class Worker:
1300
1305
  )
1301
1306
  self._validate_cooperative_activity_handlers()
1302
1307
  self._query_tasks_supported = _server_supports_query_tasks(info)
1308
+ self._query_authority_supported = (
1309
+ isinstance(server_capabilities, Mapping) and server_capabilities.get("query_task_authority") is True
1310
+ and _supports_cooperative_cancellation_protocol(_protocol_version_from_env(
1311
+ "DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", PROTOCOL_VERSION))
1312
+ )
1313
+ if self.require_query_task_authority and not self._query_authority_supported:
1314
+ raise RuntimeError("query_task_authority_unavailable: require worker protocol 1.20 and "
1315
+ "Server query_task_authority=true before registration")
1303
1316
  self._workflow_memo_updates_supported = _server_supports_workflow_memo_updates(info)
1304
1317
  has_update_validators = any(
1305
1318
  contract["update_validators"] for contract in self.workflow_command_contracts.values()
@@ -3298,6 +3311,30 @@ class Worker:
3298
3311
  return await handler(context)
3299
3312
 
3300
3313
  async def _run_query_task(self, task: dict[str, Any], *, client: Client | None = None) -> str:
3314
+ client = client or self.client
3315
+ token = None
3316
+ try:
3317
+ namespace = getattr(client, "namespace", "default")
3318
+ authority = current_query_authority.get()
3319
+ if authority is None:
3320
+ authority = QueryTaskAuthority.delivery(client, namespace, task, owner=self.worker_id,
3321
+ task_queue=self.task_queue, observed=self._query_authority_supported)
3322
+ if authority.observed:
3323
+ authority.observer = lambda: client.query_task_status(query_task_id=authority.task_id,
3324
+ lease_owner=authority.owner, query_task_attempt=authority.attempt)
3325
+ token = current_query_authority.set(authority)
3326
+ if not authority.matches(client, namespace, task, self.worker_id):
3327
+ raise QueryTaskAuthorityLost(str(task.get("query_task_id", "")), "invalid_query_claim")
3328
+ await authority.confirm()
3329
+ return await self._run_authorized_query_task(task, client=client)
3330
+ except QueryTaskAuthorityLost as error:
3331
+ log.info("query delivery suppressed: %s", error)
3332
+ return "expired"
3333
+ finally:
3334
+ if token is not None:
3335
+ current_query_authority.reset(token)
3336
+
3337
+ async def _run_authorized_query_task(self, task: dict[str, Any], *, client: Client) -> str:
3301
3338
  client = client or self.client
3302
3339
  codec = task.get("payload_codec")
3303
3340
  try:
@@ -3341,6 +3378,11 @@ class Worker:
3341
3378
 
3342
3379
  async def _run_query_task_core(self, task: dict[str, Any], *, client: Client | None = None) -> str:
3343
3380
  client = client or self.client
3381
+ authority = current_query_authority.get()
3382
+ if authority is None or not authority.matches(client, getattr(client, "namespace", "default"),
3383
+ task, self.worker_id):
3384
+ raise QueryTaskAuthorityLost(str(task.get("query_task_id", "")), "invalid_query_claim")
3385
+ await authority.confirm()
3344
3386
  query_task_id: str = task["query_task_id"]
3345
3387
  attempt: int = task.get("query_task_attempt", 1)
3346
3388
  wf_type: str = task.get("workflow_type", "")
@@ -3390,7 +3432,7 @@ class Worker:
3390
3432
  codec=codec,
3391
3433
  context="workflow query arguments",
3392
3434
  )
3393
- result = query_state(
3435
+ invoke = _prepare_query_state(
3394
3436
  cls,
3395
3437
  history,
3396
3438
  start_input,
@@ -3405,8 +3447,14 @@ class Worker:
3405
3447
  prepare_local_activity_groups=self._prepared_local_activity_groups_supported,
3406
3448
  local_activity_cancellation_policies=self._local_activity_cancellation_policies,
3407
3449
  )
3408
- if inspect.isawaitable(result):
3409
- result = await result
3450
+ await authority.confirm()
3451
+ async def execute() -> Any:
3452
+ value = invoke()
3453
+ return await value if inspect.isawaitable(value) else value
3454
+
3455
+ result = await authority.run(execute)
3456
+ except QueryTaskAuthorityLost:
3457
+ raise
3410
3458
  except AvroNotInstalledError as e:
3411
3459
  await self._fail_query_task(
3412
3460
  query_task_id,
@@ -3452,7 +3500,7 @@ class Worker:
3452
3500
  return "failed"
3453
3501
 
3454
3502
  try:
3455
- await client.complete_query_task(
3503
+ await authority.run(lambda: client.complete_query_task(
3456
3504
  query_task_id=query_task_id,
3457
3505
  lease_owner=self.worker_id,
3458
3506
  query_task_attempt=attempt,
@@ -3462,7 +3510,9 @@ class Worker:
3462
3510
  run_id=task.get("run_id"),
3463
3511
  query_name=query_name,
3464
3512
  **self._external_storage_completion_kwargs(),
3465
- )
3513
+ ), observe=False, confirm_after=False)
3514
+ except QueryTaskAuthorityLost:
3515
+ raise
3466
3516
  except ServerError as e:
3467
3517
  if _is_storage_admission_error(e):
3468
3518
  log.warning("query task %s acknowledgement paused: %s", query_task_id, e)
@@ -3552,6 +3602,12 @@ class Worker:
3552
3602
  client: Client | None = None,
3553
3603
  ) -> None:
3554
3604
  client = client or self.client
3605
+ authority = current_query_authority.get()
3606
+ if authority is not None:
3607
+ if (authority.client is not client or authority.task_id != query_task_id or authority.attempt != attempt
3608
+ or authority.owner != self.worker_id):
3609
+ raise QueryTaskAuthorityLost(query_task_id, "invalid_query_claim")
3610
+ await authority.confirm()
3555
3611
  try:
3556
3612
  await client.fail_query_task(
3557
3613
  query_task_id=query_task_id,
@@ -4013,7 +4069,7 @@ class Worker:
4013
4069
  def _clone_client_for_query_tasks(self) -> Client:
4014
4070
  warning_config = self._payload_size_warning_config()
4015
4071
 
4016
- return Client(
4072
+ client = Client(
4017
4073
  self.client.base_url,
4018
4074
  token=self.client.token,
4019
4075
  control_token=self.client.control_token,
@@ -4035,6 +4091,8 @@ class Worker:
4035
4091
  external_storage_threshold_bytes=self.external_storage_threshold_bytes,
4036
4092
  external_storage_cache=self.external_storage_cache,
4037
4093
  )
4094
+ client._cluster_info = self.client._cluster_info
4095
+ return client
4038
4096
 
4039
4097
  def _can_clone_client_for_query_tasks(self) -> bool:
4040
4098
  return type(self.client) is Client
@@ -2991,7 +2991,7 @@ def replay(
2991
2991
  ).outcome
2992
2992
 
2993
2993
 
2994
- def query_state(
2994
+ def _prepare_query_state(
2995
2995
  workflow_cls: type,
2996
2996
  history_events: Iterable[dict[str, Any]],
2997
2997
  start_input: list[Any],
@@ -3006,17 +3006,8 @@ def query_state(
3006
3006
  prepare_local_activities: bool = False,
3007
3007
  prepare_local_activity_groups: bool = False,
3008
3008
  local_activity_cancellation_policies: tuple[str, ...] = (),
3009
- ) -> Any:
3010
- """Replay a workflow to current state and invoke a registered query.
3011
-
3012
- This is the replay and invocation core used by the current server-routed
3013
- worker query path after it fetches durable history. That path is negotiated
3014
- through ``worker_protocol.server_capabilities.query_tasks``: workers
3015
- advertise ``query_tasks`` only after discovery reports it as available,
3016
- and clients fail with typed capability or discovery errors when it is not.
3017
- Unknown query names and handler exceptions are normalized to
3018
- :class:`~durable_workflow.errors.QueryFailed`.
3019
- """
3009
+ ) -> Callable[[], Any]:
3010
+ """Prepare the query so a worker can recheck authority after replay."""
3020
3011
  try:
3021
3012
  state = _replay_state(
3022
3013
  workflow_cls,
@@ -3047,12 +3038,46 @@ def query_state(
3047
3038
  if handler is None:
3048
3039
  raise QueryFailed(f"query handler {query_name!r} is not available")
3049
3040
 
3050
- try:
3051
- return handler(*(list(args) if args is not None else []))
3052
- except QueryFailed:
3053
- raise
3054
- except Exception as exc:
3055
- raise QueryFailed(str(exc) or f"query {query_name!r} failed") from exc
3041
+ def invoke() -> Any:
3042
+ try:
3043
+ return handler(*(list(args) if args is not None else []))
3044
+ except QueryFailed:
3045
+ raise
3046
+ except Exception as exc:
3047
+ raise QueryFailed(str(exc) or f"query {query_name!r} failed") from exc
3048
+
3049
+ return invoke
3050
+
3051
+
3052
+ def query_state(
3053
+ workflow_cls: type,
3054
+ history_events: Iterable[dict[str, Any]],
3055
+ start_input: list[Any],
3056
+ query_name: str,
3057
+ args: list[Any] | None = None,
3058
+ *,
3059
+ workflow_id: str | None = None,
3060
+ run_id: str = "",
3061
+ payload_codec: str | None = None,
3062
+ external_storage: ExternalStorageDriver | None = None,
3063
+ external_storage_cache: ExternalPayloadCache | None = None,
3064
+ prepare_local_activities: bool = False,
3065
+ prepare_local_activity_groups: bool = False,
3066
+ local_activity_cancellation_policies: tuple[str, ...] = (),
3067
+ ) -> Any:
3068
+ """Replay workflow state and invoke a query, normalizing handler failures.
3069
+
3070
+ Server-routed Worker execution separately checks its original query claim
3071
+ between replay and handler invocation. Direct local use has no Server lease.
3072
+ """
3073
+ return _prepare_query_state(
3074
+ workflow_cls, history_events, start_input, query_name, args,
3075
+ workflow_id=workflow_id, run_id=run_id, payload_codec=payload_codec,
3076
+ external_storage=external_storage, external_storage_cache=external_storage_cache,
3077
+ prepare_local_activities=prepare_local_activities,
3078
+ prepare_local_activity_groups=prepare_local_activity_groups,
3079
+ local_activity_cancellation_policies=local_activity_cancellation_policies,
3080
+ )()
3056
3081
 
3057
3082
 
3058
3083
  def apply_update(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.5.13
3
+ Version: 2.5.15
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
@@ -8,6 +8,7 @@ src/durable_workflow/_cancellation_scope.py
8
8
  src/durable_workflow/_cancellation_scope_history.py
9
9
  src/durable_workflow/_cooperative_cancellation.py
10
10
  src/durable_workflow/_prepared_local_activity.py
11
+ src/durable_workflow/_query_task_authority.py
11
12
  src/durable_workflow/_sticky_workflow_cache.py
12
13
  src/durable_workflow/activity.py
13
14
  src/durable_workflow/auth_composition.py
@@ -95,6 +96,7 @@ tests/test_public_boundary_scanner.py
95
96
  tests/test_pypi_project_surface.py
96
97
  tests/test_python_conformance.py
97
98
  tests/test_queries.py
99
+ tests/test_query_task_authority.py
98
100
  tests/test_query_timeout_retry.py
99
101
  tests/test_readme_quickstart.py
100
102
  tests/test_redrive_replay.py