durable-workflow 0.4.2__tar.gz → 0.4.4__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.2/src/durable_workflow.egg-info → durable_workflow-0.4.4}/PKG-INFO +1 -1
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/pyproject.toml +1 -1
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/__init__.py +4 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/client.py +159 -1
- {durable_workflow-0.4.2 → durable_workflow-0.4.4/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_schedules.py +186 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/LICENSE +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/README.md +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/setup.cfg +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/workflow.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_activity_context.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_auth_composition.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_client.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_errors.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_external_storage.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_external_task_input.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_external_task_result.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_invocable.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_metrics.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_queries.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_replay.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_retry_policy.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_serializer.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_signals.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_sleep.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_sync.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_testing_harness.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_updates.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_wait_condition.py +0 -0
- {durable_workflow-0.4.2 → durable_workflow-0.4.4}/tests/test_worker.py +0 -0
|
@@ -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.4"
|
|
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"
|
|
@@ -25,6 +25,8 @@ from .client import (
|
|
|
25
25
|
ScheduleBackfillResult,
|
|
26
26
|
ScheduleDescription,
|
|
27
27
|
ScheduleHandle,
|
|
28
|
+
ScheduleHistoryEvent,
|
|
29
|
+
ScheduleHistoryPage,
|
|
28
30
|
ScheduleList,
|
|
29
31
|
ScheduleSpec,
|
|
30
32
|
ScheduleTriggerResult,
|
|
@@ -171,6 +173,8 @@ __all__ = [
|
|
|
171
173
|
"ScheduleBackfillResult",
|
|
172
174
|
"ScheduleDescription",
|
|
173
175
|
"ScheduleHandle",
|
|
176
|
+
"ScheduleHistoryEvent",
|
|
177
|
+
"ScheduleHistoryPage",
|
|
174
178
|
"ScheduleList",
|
|
175
179
|
"ScheduleNotFound",
|
|
176
180
|
"ScheduleSpec",
|
|
@@ -21,7 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
import asyncio
|
|
22
22
|
import time
|
|
23
23
|
import warnings
|
|
24
|
-
from collections.abc import Callable
|
|
24
|
+
from collections.abc import AsyncIterator, Callable
|
|
25
25
|
from dataclasses import dataclass
|
|
26
26
|
from importlib.metadata import PackageNotFoundError
|
|
27
27
|
from importlib.metadata import version as _pkg_version
|
|
@@ -745,6 +745,42 @@ class ScheduleBackfillResult:
|
|
|
745
745
|
results: list[dict[str, Any]] | None = None
|
|
746
746
|
|
|
747
747
|
|
|
748
|
+
@dataclass
|
|
749
|
+
class ScheduleHistoryEvent:
|
|
750
|
+
"""One entry in a schedule's audit history stream.
|
|
751
|
+
|
|
752
|
+
Each event corresponds to a lifecycle transition recorded by the
|
|
753
|
+
server (ScheduleCreated, SchedulePaused, ScheduleResumed,
|
|
754
|
+
ScheduleUpdated, ScheduleTriggered, ScheduleTriggerSkipped, or
|
|
755
|
+
ScheduleDeleted). The ``payload`` mirrors what the workflow engine
|
|
756
|
+
recorded, including command-context attribution when the transition
|
|
757
|
+
came from a mutating API call.
|
|
758
|
+
"""
|
|
759
|
+
|
|
760
|
+
sequence: int
|
|
761
|
+
event_type: str | None = None
|
|
762
|
+
recorded_at: str | None = None
|
|
763
|
+
workflow_instance_id: str | None = None
|
|
764
|
+
workflow_run_id: str | None = None
|
|
765
|
+
payload: dict[str, Any] | None = None
|
|
766
|
+
id: str | None = None
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
@dataclass
|
|
770
|
+
class ScheduleHistoryPage:
|
|
771
|
+
"""One page of a schedule's audit history stream.
|
|
772
|
+
|
|
773
|
+
``next_cursor`` is the ``after_sequence`` value to request the next
|
|
774
|
+
page when ``has_more`` is ``True``; it is ``None`` on the final page.
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
schedule_id: str
|
|
778
|
+
events: list[ScheduleHistoryEvent]
|
|
779
|
+
has_more: bool = False
|
|
780
|
+
next_cursor: int | None = None
|
|
781
|
+
namespace: str | None = None
|
|
782
|
+
|
|
783
|
+
|
|
748
784
|
@dataclass
|
|
749
785
|
class BridgeAdapterOutcome:
|
|
750
786
|
"""Machine-readable result returned by a bridge adapter event."""
|
|
@@ -934,6 +970,32 @@ class ScheduleHandle:
|
|
|
934
970
|
self.schedule_id, start_time=start_time, end_time=end_time, overlap_policy=overlap_policy,
|
|
935
971
|
)
|
|
936
972
|
|
|
973
|
+
async def history(
|
|
974
|
+
self,
|
|
975
|
+
*,
|
|
976
|
+
limit: int | None = None,
|
|
977
|
+
after_sequence: int | None = None,
|
|
978
|
+
) -> ScheduleHistoryPage:
|
|
979
|
+
"""Return one page of this schedule's audit history. See :meth:`Client.get_schedule_history`."""
|
|
980
|
+
return await self._client.get_schedule_history(
|
|
981
|
+
self.schedule_id,
|
|
982
|
+
limit=limit,
|
|
983
|
+
after_sequence=after_sequence,
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
def iter_history(
|
|
987
|
+
self,
|
|
988
|
+
*,
|
|
989
|
+
limit: int | None = None,
|
|
990
|
+
after_sequence: int | None = None,
|
|
991
|
+
) -> AsyncIterator[ScheduleHistoryEvent]:
|
|
992
|
+
"""Iterate every audit event for this schedule. See :meth:`Client.iter_schedule_history`."""
|
|
993
|
+
return self._client.iter_schedule_history(
|
|
994
|
+
self.schedule_id,
|
|
995
|
+
limit=limit,
|
|
996
|
+
after_sequence=after_sequence,
|
|
997
|
+
)
|
|
998
|
+
|
|
937
999
|
|
|
938
1000
|
class Client:
|
|
939
1001
|
"""Async HTTP client for Durable Workflow control-plane and worker APIs.
|
|
@@ -2409,6 +2471,102 @@ class Client:
|
|
|
2409
2471
|
results=data.get("results"),
|
|
2410
2472
|
)
|
|
2411
2473
|
|
|
2474
|
+
async def get_schedule_history(
|
|
2475
|
+
self,
|
|
2476
|
+
schedule_id: str,
|
|
2477
|
+
*,
|
|
2478
|
+
limit: int | None = None,
|
|
2479
|
+
after_sequence: int | None = None,
|
|
2480
|
+
) -> ScheduleHistoryPage:
|
|
2481
|
+
"""Return one page of the audit history stream for a schedule.
|
|
2482
|
+
|
|
2483
|
+
The page is ordered by ``sequence`` ascending. Use
|
|
2484
|
+
``after_sequence=page.next_cursor`` to request the next page while
|
|
2485
|
+
``page.has_more`` is ``True``, or call :meth:`iter_schedule_history`
|
|
2486
|
+
to walk every remaining event with paging hidden.
|
|
2487
|
+
|
|
2488
|
+
History is available for deleted schedules: the audit stream
|
|
2489
|
+
records ``ScheduleDeleted`` and survives the schedule's removal
|
|
2490
|
+
exactly so operators can review what happened.
|
|
2491
|
+
|
|
2492
|
+
``limit`` is clamped by the server between 1 and 500 (default
|
|
2493
|
+
100). ``after_sequence`` must be a non-negative integer; invalid
|
|
2494
|
+
values raise :class:`~durable_workflow.errors.InvalidArgument`
|
|
2495
|
+
through the shared 4xx mapping.
|
|
2496
|
+
"""
|
|
2497
|
+
if limit is not None and limit < 1:
|
|
2498
|
+
raise ValueError("limit must be >= 1")
|
|
2499
|
+
if after_sequence is not None and after_sequence < 0:
|
|
2500
|
+
raise ValueError("after_sequence must be >= 0")
|
|
2501
|
+
|
|
2502
|
+
params: dict[str, str] = {}
|
|
2503
|
+
if limit is not None:
|
|
2504
|
+
params["limit"] = str(limit)
|
|
2505
|
+
if after_sequence is not None:
|
|
2506
|
+
params["after_sequence"] = str(after_sequence)
|
|
2507
|
+
|
|
2508
|
+
path = f"/schedules/{schedule_id}/history"
|
|
2509
|
+
if params:
|
|
2510
|
+
path = f"{path}?{urlencode(params)}"
|
|
2511
|
+
|
|
2512
|
+
data = await self._request("GET", path, context=schedule_id)
|
|
2513
|
+
raw_events = data.get("events") or []
|
|
2514
|
+
events = [
|
|
2515
|
+
ScheduleHistoryEvent(
|
|
2516
|
+
sequence=int(item.get("sequence", 0)),
|
|
2517
|
+
event_type=item.get("event_type"),
|
|
2518
|
+
recorded_at=item.get("recorded_at"),
|
|
2519
|
+
workflow_instance_id=item.get("workflow_instance_id"),
|
|
2520
|
+
workflow_run_id=item.get("workflow_run_id"),
|
|
2521
|
+
payload=item.get("payload") if isinstance(item.get("payload"), dict) else None,
|
|
2522
|
+
id=item.get("id"),
|
|
2523
|
+
)
|
|
2524
|
+
for item in raw_events
|
|
2525
|
+
]
|
|
2526
|
+
|
|
2527
|
+
raw_cursor = data.get("next_cursor")
|
|
2528
|
+
next_cursor: int | None
|
|
2529
|
+
if raw_cursor is None:
|
|
2530
|
+
next_cursor = None
|
|
2531
|
+
else:
|
|
2532
|
+
try:
|
|
2533
|
+
next_cursor = int(raw_cursor)
|
|
2534
|
+
except (TypeError, ValueError):
|
|
2535
|
+
next_cursor = None
|
|
2536
|
+
|
|
2537
|
+
return ScheduleHistoryPage(
|
|
2538
|
+
schedule_id=data.get("schedule_id", schedule_id),
|
|
2539
|
+
events=events,
|
|
2540
|
+
has_more=bool(data.get("has_more", False)),
|
|
2541
|
+
next_cursor=next_cursor,
|
|
2542
|
+
namespace=data.get("namespace"),
|
|
2543
|
+
)
|
|
2544
|
+
|
|
2545
|
+
async def iter_schedule_history(
|
|
2546
|
+
self,
|
|
2547
|
+
schedule_id: str,
|
|
2548
|
+
*,
|
|
2549
|
+
limit: int | None = None,
|
|
2550
|
+
after_sequence: int | None = None,
|
|
2551
|
+
) -> AsyncIterator[ScheduleHistoryEvent]:
|
|
2552
|
+
"""Yield every audit event for a schedule, paging under the hood.
|
|
2553
|
+
|
|
2554
|
+
Each element is a :class:`ScheduleHistoryEvent`. Paging stops once
|
|
2555
|
+
the server reports ``has_more=False``.
|
|
2556
|
+
"""
|
|
2557
|
+
cursor = after_sequence
|
|
2558
|
+
while True:
|
|
2559
|
+
page = await self.get_schedule_history(
|
|
2560
|
+
schedule_id,
|
|
2561
|
+
limit=limit,
|
|
2562
|
+
after_sequence=cursor,
|
|
2563
|
+
)
|
|
2564
|
+
for event in page.events:
|
|
2565
|
+
yield event
|
|
2566
|
+
if not page.has_more or page.next_cursor is None:
|
|
2567
|
+
return
|
|
2568
|
+
cursor = page.next_cursor
|
|
2569
|
+
|
|
2412
2570
|
# ── Worker protocol ────────────────────────────────────────────────
|
|
2413
2571
|
async def register_worker(
|
|
2414
2572
|
self,
|
|
@@ -14,6 +14,8 @@ from durable_workflow.client import (
|
|
|
14
14
|
ScheduleBackfillResult,
|
|
15
15
|
ScheduleDescription,
|
|
16
16
|
ScheduleHandle,
|
|
17
|
+
ScheduleHistoryEvent,
|
|
18
|
+
ScheduleHistoryPage,
|
|
17
19
|
ScheduleList,
|
|
18
20
|
ScheduleSpec,
|
|
19
21
|
ScheduleTriggerResult,
|
|
@@ -538,3 +540,187 @@ class TestScheduleHandle:
|
|
|
538
540
|
end_time="2026-04-14T01:00:00Z",
|
|
539
541
|
)
|
|
540
542
|
assert result.fires_attempted == 1
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class TestScheduleHistory:
|
|
546
|
+
@pytest.mark.asyncio
|
|
547
|
+
async def test_parses_events_and_pagination_cursor(self, client: Client) -> None:
|
|
548
|
+
resp = _mock_response(200, {
|
|
549
|
+
"schedule_id": "sched-1",
|
|
550
|
+
"namespace": "ns1",
|
|
551
|
+
"events": [
|
|
552
|
+
{
|
|
553
|
+
"id": "evt-1",
|
|
554
|
+
"sequence": 1,
|
|
555
|
+
"event_type": "ScheduleCreated",
|
|
556
|
+
"recorded_at": "2026-04-01T00:00:00+00:00",
|
|
557
|
+
"workflow_instance_id": None,
|
|
558
|
+
"workflow_run_id": None,
|
|
559
|
+
"payload": {},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"id": "evt-2",
|
|
563
|
+
"sequence": 2,
|
|
564
|
+
"event_type": "ScheduleTriggered",
|
|
565
|
+
"recorded_at": "2026-04-02T00:00:00+00:00",
|
|
566
|
+
"workflow_instance_id": "wf-abc",
|
|
567
|
+
"workflow_run_id": "run-abc",
|
|
568
|
+
"payload": {"outcome": "triggered", "trigger_number": 1},
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
"has_more": True,
|
|
572
|
+
"next_cursor": 2,
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
with patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp) as mock_req:
|
|
576
|
+
page = await client.get_schedule_history("sched-1")
|
|
577
|
+
|
|
578
|
+
assert isinstance(page, ScheduleHistoryPage)
|
|
579
|
+
assert page.schedule_id == "sched-1"
|
|
580
|
+
assert page.namespace == "ns1"
|
|
581
|
+
assert page.has_more is True
|
|
582
|
+
assert page.next_cursor == 2
|
|
583
|
+
assert len(page.events) == 2
|
|
584
|
+
assert isinstance(page.events[0], ScheduleHistoryEvent)
|
|
585
|
+
assert page.events[0].sequence == 1
|
|
586
|
+
assert page.events[0].event_type == "ScheduleCreated"
|
|
587
|
+
assert page.events[1].workflow_instance_id == "wf-abc"
|
|
588
|
+
assert page.events[1].payload == {"outcome": "triggered", "trigger_number": 1}
|
|
589
|
+
|
|
590
|
+
call = mock_req.call_args
|
|
591
|
+
assert call.args[0] == "GET"
|
|
592
|
+
assert call.args[1] == "/api/schedules/sched-1/history"
|
|
593
|
+
|
|
594
|
+
@pytest.mark.asyncio
|
|
595
|
+
async def test_forwards_limit_and_after_sequence_in_query(self, client: Client) -> None:
|
|
596
|
+
resp = _mock_response(200, {
|
|
597
|
+
"schedule_id": "sched-1",
|
|
598
|
+
"namespace": "ns1",
|
|
599
|
+
"events": [],
|
|
600
|
+
"has_more": False,
|
|
601
|
+
"next_cursor": None,
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
with patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp) as mock_req:
|
|
605
|
+
await client.get_schedule_history("sched-1", limit=50, after_sequence=7)
|
|
606
|
+
|
|
607
|
+
call = mock_req.call_args
|
|
608
|
+
path = call.args[1]
|
|
609
|
+
assert path.startswith("/api/schedules/sched-1/history?")
|
|
610
|
+
assert "limit=50" in path
|
|
611
|
+
assert "after_sequence=7" in path
|
|
612
|
+
|
|
613
|
+
@pytest.mark.asyncio
|
|
614
|
+
async def test_rejects_invalid_limit(self, client: Client) -> None:
|
|
615
|
+
with pytest.raises(ValueError):
|
|
616
|
+
await client.get_schedule_history("sched-1", limit=0)
|
|
617
|
+
|
|
618
|
+
@pytest.mark.asyncio
|
|
619
|
+
async def test_rejects_negative_after_sequence(self, client: Client) -> None:
|
|
620
|
+
with pytest.raises(ValueError):
|
|
621
|
+
await client.get_schedule_history("sched-1", after_sequence=-1)
|
|
622
|
+
|
|
623
|
+
@pytest.mark.asyncio
|
|
624
|
+
async def test_not_found_maps_to_schedule_not_found(self, client: Client) -> None:
|
|
625
|
+
resp = _mock_response(404, {"reason": "schedule_not_found", "message": "not found"})
|
|
626
|
+
with (
|
|
627
|
+
patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp),
|
|
628
|
+
pytest.raises(ScheduleNotFound),
|
|
629
|
+
):
|
|
630
|
+
await client.get_schedule_history("missing")
|
|
631
|
+
|
|
632
|
+
@pytest.mark.asyncio
|
|
633
|
+
async def test_iter_pages_across_multiple_server_responses(self, client: Client) -> None:
|
|
634
|
+
responses = [
|
|
635
|
+
_mock_response(200, {
|
|
636
|
+
"schedule_id": "sched-1",
|
|
637
|
+
"namespace": "ns1",
|
|
638
|
+
"events": [
|
|
639
|
+
{
|
|
640
|
+
"sequence": 1,
|
|
641
|
+
"event_type": "ScheduleCreated",
|
|
642
|
+
"recorded_at": "2026-04-01T00:00:00+00:00",
|
|
643
|
+
"payload": {},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"sequence": 2,
|
|
647
|
+
"event_type": "SchedulePaused",
|
|
648
|
+
"recorded_at": "2026-04-02T00:00:00+00:00",
|
|
649
|
+
"payload": {},
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
"has_more": True,
|
|
653
|
+
"next_cursor": 2,
|
|
654
|
+
}),
|
|
655
|
+
_mock_response(200, {
|
|
656
|
+
"schedule_id": "sched-1",
|
|
657
|
+
"namespace": "ns1",
|
|
658
|
+
"events": [
|
|
659
|
+
{
|
|
660
|
+
"sequence": 3,
|
|
661
|
+
"event_type": "ScheduleResumed",
|
|
662
|
+
"recorded_at": "2026-04-03T00:00:00+00:00",
|
|
663
|
+
"payload": {},
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
"has_more": False,
|
|
667
|
+
"next_cursor": None,
|
|
668
|
+
}),
|
|
669
|
+
]
|
|
670
|
+
|
|
671
|
+
mock = AsyncMock(side_effect=responses)
|
|
672
|
+
with patch.object(client._http, "request", new=mock):
|
|
673
|
+
sequences = [event.sequence async for event in client.iter_schedule_history("sched-1")]
|
|
674
|
+
|
|
675
|
+
assert sequences == [1, 2, 3]
|
|
676
|
+
assert mock.call_count == 2
|
|
677
|
+
second_call_path = mock.call_args_list[1].args[1]
|
|
678
|
+
assert "after_sequence=2" in second_call_path
|
|
679
|
+
|
|
680
|
+
@pytest.mark.asyncio
|
|
681
|
+
async def test_handle_history_delegates_to_client(self, client: Client) -> None:
|
|
682
|
+
resp = _mock_response(200, {
|
|
683
|
+
"schedule_id": "sched-1",
|
|
684
|
+
"namespace": "ns1",
|
|
685
|
+
"events": [
|
|
686
|
+
{
|
|
687
|
+
"sequence": 1,
|
|
688
|
+
"event_type": "ScheduleCreated",
|
|
689
|
+
"recorded_at": "2026-04-01T00:00:00+00:00",
|
|
690
|
+
"payload": {},
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
"has_more": False,
|
|
694
|
+
"next_cursor": None,
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
handle = client.get_schedule_handle("sched-1")
|
|
698
|
+
with patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp):
|
|
699
|
+
page = await handle.history(limit=10)
|
|
700
|
+
|
|
701
|
+
assert isinstance(page, ScheduleHistoryPage)
|
|
702
|
+
assert page.events[0].event_type == "ScheduleCreated"
|
|
703
|
+
|
|
704
|
+
@pytest.mark.asyncio
|
|
705
|
+
async def test_handle_iter_history_delegates_to_client(self, client: Client) -> None:
|
|
706
|
+
resp = _mock_response(200, {
|
|
707
|
+
"schedule_id": "sched-1",
|
|
708
|
+
"namespace": "ns1",
|
|
709
|
+
"events": [
|
|
710
|
+
{
|
|
711
|
+
"sequence": 1,
|
|
712
|
+
"event_type": "ScheduleCreated",
|
|
713
|
+
"recorded_at": "2026-04-01T00:00:00+00:00",
|
|
714
|
+
"payload": {},
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
"has_more": False,
|
|
718
|
+
"next_cursor": None,
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
handle = client.get_schedule_handle("sched-1")
|
|
722
|
+
with patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp):
|
|
723
|
+
events = [event async for event in handle.iter_history()]
|
|
724
|
+
|
|
725
|
+
assert len(events) == 1
|
|
726
|
+
assert events[0].event_type == "ScheduleCreated"
|
|
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.2 → durable_workflow-0.4.4}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow/external_task_result.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
|
{durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-0.4.2 → durable_workflow-0.4.4}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-0.4.2 → durable_workflow-0.4.4}/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.2 → durable_workflow-0.4.4}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|