durable-workflow 0.4.98__tar.gz → 0.4.99__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-0.4.98/src/durable_workflow.egg-info → durable_workflow-0.4.99}/PKG-INFO +31 -1
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/README.md +30 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/pyproject.toml +1 -1
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/client.py +46 -3
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/errors.py +23 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/replay_verify.py +6 -1
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/sync.py +18 -2
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/worker.py +4 -1
- {durable_workflow-0.4.98 → durable_workflow-0.4.99/src/durable_workflow.egg-info}/PKG-INFO +31 -1
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_replay_verify.py +1 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_schedules.py +88 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_sync.py +22 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/LICENSE +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/setup.cfg +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/workflow.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_activity_context.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_auth_composition.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_client.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_errors.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_external_storage.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_external_task_input.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_external_task_result.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_invocable.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_metrics.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_python_conformance.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_queries.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_release_docs_audit_workflow.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_replay.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_retry_policy.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_serializer.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_signals.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_sleep.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_testing_harness.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_updates.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_wait_condition.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_worker.py +0 -0
- {durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durable-workflow
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.99
|
|
4
4
|
Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
|
|
5
5
|
Author: Durable Workflow Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -95,6 +95,36 @@ For a fuller deployable example, see
|
|
|
95
95
|
[`examples/order_processing`](examples/order_processing), which runs a
|
|
96
96
|
multi-activity order workflow against a local server with Docker Compose.
|
|
97
97
|
|
|
98
|
+
## Schedule visibility and paging
|
|
99
|
+
|
|
100
|
+
`list_schedules()` returns one typed `ScheduleList` page. Status and workflow
|
|
101
|
+
type are exact server-side filters; the visibility query uses the server's
|
|
102
|
+
documented equality-predicate grammar. All filters combine with AND semantics.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
page = await client.list_schedules(
|
|
106
|
+
status="active",
|
|
107
|
+
workflow_type="orders.rollup",
|
|
108
|
+
query='Region = "eu" AND Priority = 2',
|
|
109
|
+
page_size=25,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
while page.next_page_token is not None:
|
|
113
|
+
page = await client.list_schedules(
|
|
114
|
+
status="active",
|
|
115
|
+
workflow_type="orders.rollup",
|
|
116
|
+
query='Region = "eu" AND Priority = 2',
|
|
117
|
+
page_size=25,
|
|
118
|
+
next_page_token=page.next_page_token,
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Continuation tokens are opaque. Reuse them unchanged with the same namespace,
|
|
123
|
+
status, workflow type, and query; `None` terminates traversal. Invalid filters
|
|
124
|
+
and malformed, mismatched, cross-namespace, or stale tokens raise
|
|
125
|
+
`ScheduleListError`, which retains `status`, `reason()`, `field`, `errors`,
|
|
126
|
+
`last_safe_cursor`, and the complete server response in `body`.
|
|
127
|
+
|
|
98
128
|
## Retry policy scopes
|
|
99
129
|
|
|
100
130
|
Retry and timeout settings are scoped to the layer where you configure them:
|
|
@@ -59,6 +59,36 @@ For a fuller deployable example, see
|
|
|
59
59
|
[`examples/order_processing`](examples/order_processing), which runs a
|
|
60
60
|
multi-activity order workflow against a local server with Docker Compose.
|
|
61
61
|
|
|
62
|
+
## Schedule visibility and paging
|
|
63
|
+
|
|
64
|
+
`list_schedules()` returns one typed `ScheduleList` page. Status and workflow
|
|
65
|
+
type are exact server-side filters; the visibility query uses the server's
|
|
66
|
+
documented equality-predicate grammar. All filters combine with AND semantics.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
page = await client.list_schedules(
|
|
70
|
+
status="active",
|
|
71
|
+
workflow_type="orders.rollup",
|
|
72
|
+
query='Region = "eu" AND Priority = 2',
|
|
73
|
+
page_size=25,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
while page.next_page_token is not None:
|
|
77
|
+
page = await client.list_schedules(
|
|
78
|
+
status="active",
|
|
79
|
+
workflow_type="orders.rollup",
|
|
80
|
+
query='Region = "eu" AND Priority = 2',
|
|
81
|
+
page_size=25,
|
|
82
|
+
next_page_token=page.next_page_token,
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Continuation tokens are opaque. Reuse them unchanged with the same namespace,
|
|
87
|
+
status, workflow type, and query; `None` terminates traversal. Invalid filters
|
|
88
|
+
and malformed, mismatched, cross-namespace, or stale tokens raise
|
|
89
|
+
`ScheduleListError`, which retains `status`, `reason()`, `field`, `errors`,
|
|
90
|
+
`last_safe_cursor`, and the complete server response in `body`.
|
|
91
|
+
|
|
62
92
|
## Retry policy scopes
|
|
63
93
|
|
|
64
94
|
Retry and timeout settings are scoped to the layer where you configure them:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.99"
|
|
8
8
|
description = "Python SDK for the Durable Workflow server (language-neutral HTTP protocol)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -65,6 +65,7 @@ from .errors import (
|
|
|
65
65
|
NonRetryableError,
|
|
66
66
|
QueryFailed,
|
|
67
67
|
ScheduleAlreadyExists,
|
|
68
|
+
ScheduleListError,
|
|
68
69
|
ScheduleNotFound,
|
|
69
70
|
ServerError,
|
|
70
71
|
SignalFailed,
|
|
@@ -224,6 +225,7 @@ __all__ = [
|
|
|
224
225
|
"ScheduleHistoryEvent",
|
|
225
226
|
"ScheduleHistoryPage",
|
|
226
227
|
"ScheduleList",
|
|
228
|
+
"ScheduleListError",
|
|
227
229
|
"ScheduleNotFound",
|
|
228
230
|
"ScheduleSpec",
|
|
229
231
|
"ScheduleTriggerResult",
|
|
@@ -2973,9 +2973,37 @@ class Client:
|
|
|
2973
2973
|
sid = data.get("schedule_id", schedule_id or "")
|
|
2974
2974
|
return ScheduleHandle(self, schedule_id=sid)
|
|
2975
2975
|
|
|
2976
|
-
async def list_schedules(
|
|
2977
|
-
|
|
2978
|
-
|
|
2976
|
+
async def list_schedules(
|
|
2977
|
+
self,
|
|
2978
|
+
*,
|
|
2979
|
+
status: str | None = None,
|
|
2980
|
+
workflow_type: str | None = None,
|
|
2981
|
+
query: str | None = None,
|
|
2982
|
+
page_size: int | None = None,
|
|
2983
|
+
next_page_token: str | None = None,
|
|
2984
|
+
) -> ScheduleList:
|
|
2985
|
+
"""Return one server-filtered schedule visibility page.
|
|
2986
|
+
|
|
2987
|
+
Status, workflow type, and visibility-query filters combine with AND
|
|
2988
|
+
semantics. Continuation tokens are opaque: pass a non-null token back
|
|
2989
|
+
unchanged with the same namespace and filters. ``None`` terminates the
|
|
2990
|
+
traversal. The server validates page sizes, predicates, and cursors.
|
|
2991
|
+
"""
|
|
2992
|
+
params: dict[str, str] = {}
|
|
2993
|
+
if status is not None:
|
|
2994
|
+
params["status"] = status
|
|
2995
|
+
if workflow_type is not None:
|
|
2996
|
+
params["workflow_type"] = workflow_type
|
|
2997
|
+
if query is not None:
|
|
2998
|
+
params["query"] = query
|
|
2999
|
+
if page_size is not None:
|
|
3000
|
+
params["page_size"] = str(page_size)
|
|
3001
|
+
if next_page_token is not None:
|
|
3002
|
+
params["next_page_token"] = next_page_token
|
|
3003
|
+
|
|
3004
|
+
qs = urlencode(params)
|
|
3005
|
+
path = f"/schedules?{qs}" if qs else "/schedules"
|
|
3006
|
+
data = await self._request("GET", path, context="schedule.list")
|
|
2979
3007
|
items = data.get("schedules", [])
|
|
2980
3008
|
schedules = [
|
|
2981
3009
|
ScheduleDescription(
|
|
@@ -2985,9 +3013,24 @@ class Client:
|
|
|
2985
3013
|
action=item.get("action"),
|
|
2986
3014
|
overlap_policy=item.get("overlap_policy"),
|
|
2987
3015
|
note=item.get("note"),
|
|
3016
|
+
memo=item.get("memo") if isinstance(item.get("memo"), dict) else None,
|
|
3017
|
+
search_attributes=(
|
|
3018
|
+
item.get("search_attributes")
|
|
3019
|
+
if isinstance(item.get("search_attributes"), dict)
|
|
3020
|
+
else None
|
|
3021
|
+
),
|
|
3022
|
+
jitter_seconds=item.get("jitter_seconds"),
|
|
3023
|
+
max_runs=item.get("max_runs"),
|
|
3024
|
+
remaining_actions=item.get("remaining_actions"),
|
|
2988
3025
|
fires_count=item.get("fires_count", 0),
|
|
3026
|
+
failures_count=item.get("failures_count", 0),
|
|
2989
3027
|
next_fire_at=item.get("next_fire_at"),
|
|
2990
3028
|
last_fired_at=item.get("last_fired_at"),
|
|
3029
|
+
latest_workflow_instance_id=item.get("latest_workflow_instance_id"),
|
|
3030
|
+
paused_at=item.get("paused_at"),
|
|
3031
|
+
created_at=item.get("created_at"),
|
|
3032
|
+
updated_at=item.get("updated_at"),
|
|
3033
|
+
info=item.get("info") if isinstance(item.get("info"), dict) else None,
|
|
2991
3034
|
)
|
|
2992
3035
|
for item in items
|
|
2993
3036
|
]
|
|
@@ -152,6 +152,26 @@ class ScheduleAlreadyExists(DurableWorkflowError):
|
|
|
152
152
|
self.schedule_id = schedule_id
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
class ScheduleListError(ServerError):
|
|
156
|
+
"""A schedule visibility filter or continuation cursor was refused.
|
|
157
|
+
|
|
158
|
+
The complete parsed response remains available on :attr:`body`. Convenience
|
|
159
|
+
attributes expose the rejected field, structured field errors, and the
|
|
160
|
+
server's last safe keyset cursor without discarding status or reason.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(self, status: int, body: object) -> None:
|
|
164
|
+
super().__init__(status, body)
|
|
165
|
+
self.field = body.get("field") if isinstance(body, dict) else None
|
|
166
|
+
errors = None
|
|
167
|
+
last_safe_cursor = None
|
|
168
|
+
if isinstance(body, dict):
|
|
169
|
+
errors = body.get("validation_errors") or body.get("errors")
|
|
170
|
+
last_safe_cursor = body.get("last_safe_cursor")
|
|
171
|
+
self.errors = errors if isinstance(errors, dict) else None
|
|
172
|
+
self.last_safe_cursor = last_safe_cursor if isinstance(last_safe_cursor, dict) else None
|
|
173
|
+
|
|
174
|
+
|
|
155
175
|
class QueryFailed(DurableWorkflowError):
|
|
156
176
|
"""A workflow query was rejected or the workflow raised while handling it."""
|
|
157
177
|
|
|
@@ -447,6 +467,9 @@ def _raise_for_status(status: int, body: object, *, context: str = "") -> None:
|
|
|
447
467
|
if status == 401:
|
|
448
468
|
raise Unauthorized(message or "unauthorized")
|
|
449
469
|
|
|
470
|
+
if context == "schedule.list" and status in (400, 403, 409, 422):
|
|
471
|
+
raise ScheduleListError(status, body)
|
|
472
|
+
|
|
450
473
|
def query_failed(default: str) -> QueryFailed:
|
|
451
474
|
return QueryFailed(
|
|
452
475
|
message or default,
|
|
@@ -954,7 +954,12 @@ def _cli(argv: Sequence[str] | None = None) -> int:
|
|
|
954
954
|
parser.error("--workflows is required unless --simulate-bundles is set")
|
|
955
955
|
|
|
956
956
|
workflows = _resolve_workflow_loader(args.workflows)
|
|
957
|
-
|
|
957
|
+
required_families = REQUIRED_FAMILIES if args.strict_missing_families else ()
|
|
958
|
+
report = verify_golden_history(
|
|
959
|
+
args.fixture_dir,
|
|
960
|
+
workflows,
|
|
961
|
+
required_families=required_families,
|
|
962
|
+
)
|
|
958
963
|
|
|
959
964
|
payload = report.to_dict()
|
|
960
965
|
text = json.dumps(payload, indent=2, sort_keys=True)
|
|
@@ -629,8 +629,24 @@ class Client:
|
|
|
629
629
|
)
|
|
630
630
|
return SyncScheduleHandle(handle)
|
|
631
631
|
|
|
632
|
-
def list_schedules(
|
|
633
|
-
|
|
632
|
+
def list_schedules(
|
|
633
|
+
self,
|
|
634
|
+
*,
|
|
635
|
+
status: str | None = None,
|
|
636
|
+
workflow_type: str | None = None,
|
|
637
|
+
query: str | None = None,
|
|
638
|
+
page_size: int | None = None,
|
|
639
|
+
next_page_token: str | None = None,
|
|
640
|
+
) -> ScheduleList:
|
|
641
|
+
result: ScheduleList = _run(
|
|
642
|
+
self._async.list_schedules(
|
|
643
|
+
status=status,
|
|
644
|
+
workflow_type=workflow_type,
|
|
645
|
+
query=query,
|
|
646
|
+
page_size=page_size,
|
|
647
|
+
next_page_token=next_page_token,
|
|
648
|
+
)
|
|
649
|
+
)
|
|
634
650
|
return result
|
|
635
651
|
|
|
636
652
|
def describe_schedule(self, schedule_id: str) -> ScheduleDescription:
|
|
@@ -1823,8 +1823,11 @@ class Worker:
|
|
|
1823
1823
|
self._act_semaphore.release()
|
|
1824
1824
|
|
|
1825
1825
|
async def _poll_query_tasks(self, *, client: Client | None = None, track_tasks: bool = True) -> None:
|
|
1826
|
+
query_thread_stop = self._query_thread_stop if client is not None else None
|
|
1826
1827
|
client = client or self.client
|
|
1827
|
-
while not self._stop.is_set() and
|
|
1828
|
+
while not self._stop.is_set() and (
|
|
1829
|
+
query_thread_stop is None or not query_thread_stop.is_set()
|
|
1830
|
+
):
|
|
1828
1831
|
try:
|
|
1829
1832
|
poll_start = time.perf_counter()
|
|
1830
1833
|
task = await client.poll_query_task(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durable-workflow
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.99
|
|
4
4
|
Summary: Python SDK for the Durable Workflow server (language-neutral HTTP protocol)
|
|
5
5
|
Author: Durable Workflow Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -95,6 +95,36 @@ For a fuller deployable example, see
|
|
|
95
95
|
[`examples/order_processing`](examples/order_processing), which runs a
|
|
96
96
|
multi-activity order workflow against a local server with Docker Compose.
|
|
97
97
|
|
|
98
|
+
## Schedule visibility and paging
|
|
99
|
+
|
|
100
|
+
`list_schedules()` returns one typed `ScheduleList` page. Status and workflow
|
|
101
|
+
type are exact server-side filters; the visibility query uses the server's
|
|
102
|
+
documented equality-predicate grammar. All filters combine with AND semantics.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
page = await client.list_schedules(
|
|
106
|
+
status="active",
|
|
107
|
+
workflow_type="orders.rollup",
|
|
108
|
+
query='Region = "eu" AND Priority = 2',
|
|
109
|
+
page_size=25,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
while page.next_page_token is not None:
|
|
113
|
+
page = await client.list_schedules(
|
|
114
|
+
status="active",
|
|
115
|
+
workflow_type="orders.rollup",
|
|
116
|
+
query='Region = "eu" AND Priority = 2',
|
|
117
|
+
page_size=25,
|
|
118
|
+
next_page_token=page.next_page_token,
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Continuation tokens are opaque. Reuse them unchanged with the same namespace,
|
|
123
|
+
status, workflow type, and query; `None` terminates traversal. Invalid filters
|
|
124
|
+
and malformed, mismatched, cross-namespace, or stale tokens raise
|
|
125
|
+
`ScheduleListError`, which retains `status`, `reason()`, `field`, `errors`,
|
|
126
|
+
`last_safe_cursor`, and the complete server response in `body`.
|
|
127
|
+
|
|
98
128
|
## Retry policy scopes
|
|
99
129
|
|
|
100
130
|
Retry and timeout settings are scoped to the layer where you configure them:
|
|
@@ -23,6 +23,7 @@ from durable_workflow.client import (
|
|
|
23
23
|
from durable_workflow.errors import (
|
|
24
24
|
InvalidArgument,
|
|
25
25
|
ScheduleAlreadyExists,
|
|
26
|
+
ScheduleListError,
|
|
26
27
|
ScheduleNotFound,
|
|
27
28
|
)
|
|
28
29
|
|
|
@@ -240,6 +241,93 @@ class TestListSchedules:
|
|
|
240
241
|
result = await client.list_schedules()
|
|
241
242
|
assert result.schedules == []
|
|
242
243
|
|
|
244
|
+
@pytest.mark.asyncio
|
|
245
|
+
async def test_filters_and_continuation_token_are_forwarded_and_mapped(self, client: Client) -> None:
|
|
246
|
+
resp = _mock_response(200, {
|
|
247
|
+
"schedules": [{
|
|
248
|
+
"schedule_id": "reports-eu",
|
|
249
|
+
"status": "paused",
|
|
250
|
+
"search_attributes": {"Region": "eu west"},
|
|
251
|
+
"created_at": "2026-07-14T12:00:00Z",
|
|
252
|
+
"updated_at": "2026-07-14T12:01:00Z",
|
|
253
|
+
}],
|
|
254
|
+
"next_page_token": "opaque+/= token",
|
|
255
|
+
})
|
|
256
|
+
with patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp) as mock:
|
|
257
|
+
result = await client.list_schedules(
|
|
258
|
+
status="paused",
|
|
259
|
+
workflow_type="reports.rollup",
|
|
260
|
+
query='Region = "eu west"',
|
|
261
|
+
page_size=1,
|
|
262
|
+
next_page_token="page+/= one",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
assert mock.call_args.args[:2] == (
|
|
266
|
+
"GET",
|
|
267
|
+
"/api/schedules?status=paused&workflow_type=reports.rollup"
|
|
268
|
+
"&query=Region+%3D+%22eu+west%22&page_size=1"
|
|
269
|
+
"&next_page_token=page%2B%2F%3D+one",
|
|
270
|
+
)
|
|
271
|
+
assert result.next_page_token == "opaque+/= token"
|
|
272
|
+
assert result.schedules[0].search_attributes == {"Region": "eu west"}
|
|
273
|
+
assert result.schedules[0].created_at == "2026-07-14T12:00:00Z"
|
|
274
|
+
|
|
275
|
+
@pytest.mark.asyncio
|
|
276
|
+
@pytest.mark.parametrize(
|
|
277
|
+
("status", "body", "expected_cursor"),
|
|
278
|
+
[
|
|
279
|
+
(422, {
|
|
280
|
+
"message": "Unsupported predicate.",
|
|
281
|
+
"reason": "unsupported_schedule_visibility_predicate",
|
|
282
|
+
"field": "query",
|
|
283
|
+
"errors": {"query": ["Only equality predicates are supported."]},
|
|
284
|
+
"last_safe_cursor": None,
|
|
285
|
+
}, None),
|
|
286
|
+
(400, {
|
|
287
|
+
"message": "Malformed token.",
|
|
288
|
+
"reason": "malformed_schedule_page_token",
|
|
289
|
+
"field": "next_page_token",
|
|
290
|
+
"errors": {"next_page_token": ["Malformed token."]},
|
|
291
|
+
"last_safe_cursor": None,
|
|
292
|
+
}, None),
|
|
293
|
+
(403, {
|
|
294
|
+
"message": "Wrong namespace.",
|
|
295
|
+
"reason": "schedule_page_token_namespace_mismatch",
|
|
296
|
+
"field": "next_page_token",
|
|
297
|
+
"errors": {"next_page_token": ["Wrong namespace."]},
|
|
298
|
+
"last_safe_cursor": {"created_at": "2026-07-14T12:00:00Z", "schedule_id": "s1"},
|
|
299
|
+
}, {"created_at": "2026-07-14T12:00:00Z", "schedule_id": "s1"}),
|
|
300
|
+
(409, {
|
|
301
|
+
"message": "Stale token.",
|
|
302
|
+
"reason": "stale_schedule_page_token",
|
|
303
|
+
"field": "next_page_token",
|
|
304
|
+
"errors": {"next_page_token": ["Stale token."]},
|
|
305
|
+
"last_safe_cursor": {"created_at": "2026-07-14T12:00:00Z", "schedule_id": "s1"},
|
|
306
|
+
}, {"created_at": "2026-07-14T12:00:00Z", "schedule_id": "s1"}),
|
|
307
|
+
],
|
|
308
|
+
)
|
|
309
|
+
async def test_typed_filter_and_cursor_errors_preserve_server_evidence(
|
|
310
|
+
self,
|
|
311
|
+
client: Client,
|
|
312
|
+
status: int,
|
|
313
|
+
body: dict,
|
|
314
|
+
expected_cursor: dict | None,
|
|
315
|
+
) -> None:
|
|
316
|
+
resp = _mock_response(status, body)
|
|
317
|
+
with (
|
|
318
|
+
patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp),
|
|
319
|
+
pytest.raises(ScheduleListError) as exc_info,
|
|
320
|
+
):
|
|
321
|
+
await client.list_schedules(query='Region STARTS_WITH "e"')
|
|
322
|
+
|
|
323
|
+
error = exc_info.value
|
|
324
|
+
assert error.status == status
|
|
325
|
+
assert error.reason() == body["reason"]
|
|
326
|
+
assert error.field == body["field"]
|
|
327
|
+
assert error.errors == body["errors"]
|
|
328
|
+
assert error.last_safe_cursor == expected_cursor
|
|
329
|
+
assert error.body == body
|
|
330
|
+
|
|
243
331
|
|
|
244
332
|
class TestDescribeSchedule:
|
|
245
333
|
@pytest.mark.asyncio
|
|
@@ -727,6 +727,28 @@ class TestSyncClientContextManager:
|
|
|
727
727
|
assert client is not None
|
|
728
728
|
|
|
729
729
|
|
|
730
|
+
class TestSyncScheduleList:
|
|
731
|
+
def test_schedule_filters_and_cursor_are_forwarded(self) -> None:
|
|
732
|
+
client = Client("http://localhost:8080")
|
|
733
|
+
resp = _mock_response(200, {"schedules": [], "next_page_token": "next"})
|
|
734
|
+
|
|
735
|
+
with patch.object(client._async._http, "request", new_callable=AsyncMock, return_value=resp) as mock:
|
|
736
|
+
page = client.list_schedules(
|
|
737
|
+
status="active",
|
|
738
|
+
workflow_type="orders.process",
|
|
739
|
+
query='Region = "eu"',
|
|
740
|
+
page_size=25,
|
|
741
|
+
next_page_token="cursor",
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
assert page.next_page_token == "next"
|
|
745
|
+
assert mock.call_args.args[:2] == (
|
|
746
|
+
"GET",
|
|
747
|
+
"/api/schedules?status=active&workflow_type=orders.process"
|
|
748
|
+
"&query=Region+%3D+%22eu%22&page_size=25&next_page_token=cursor",
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
|
|
730
752
|
class TestSyncRunInsideLoop:
|
|
731
753
|
@pytest.mark.asyncio
|
|
732
754
|
async def test_raises_inside_loop(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/auth_composition.py
RENAMED
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/external_storage.py
RENAMED
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/history_bundle_verify.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow/replay_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_release_docs_audit_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_standalone_activity_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-0.4.98 → durable_workflow-0.4.99}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|