python-saga-orchestrator 0.1.3__py3-none-any.whl → 0.2.3.dev0__py3-none-any.whl
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.
- {python_saga_orchestrator-0.1.3.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/METADATA +48 -6
- python_saga_orchestrator-0.2.3.dev0.dist-info/RECORD +47 -0
- saga_orchestrator/__init__.py +33 -2
- saga_orchestrator/_version.py +24 -0
- saga_orchestrator/admin/api.py +4 -2
- saga_orchestrator/core/builder.py +40 -0
- saga_orchestrator/core/engine.py +575 -145
- saga_orchestrator/core/orchestrator.py +40 -1
- saga_orchestrator/core/repository.py +24 -27
- saga_orchestrator/domain/mixins/__init__.py +2 -0
- saga_orchestrator/domain/mixins/saga_state.py +26 -21
- saga_orchestrator/domain/mixins/saga_step_histrory.py +45 -0
- saga_orchestrator/domain/mixins/types.py +78 -0
- saga_orchestrator/domain/models/__init__.py +6 -1
- saga_orchestrator/domain/models/builder.py +13 -2
- saga_orchestrator/domain/models/context.py +126 -0
- saga_orchestrator/domain/models/enums/__init__.py +4 -0
- saga_orchestrator/domain/models/enums/saga_status.py +5 -3
- saga_orchestrator/domain/models/enums/saga_step_phase.py +7 -0
- saga_orchestrator/domain/models/enums/saga_step_status.py +7 -0
- saga_orchestrator/domain/models/notify.py +1 -0
- saga_orchestrator/domain/models/saga_snapshot.py +5 -3
- saga_orchestrator/domain/models/step.py +66 -9
- saga_orchestrator/inbox/__init__.py +27 -0
- saga_orchestrator/inbox/contracts.py +81 -0
- saga_orchestrator/inbox/dispatcher.py +120 -0
- saga_orchestrator/inbox/models.py +84 -0
- saga_orchestrator/inbox/repository.py +165 -0
- saga_orchestrator/inbox/retry.py +20 -0
- saga_orchestrator/outbox/factory.py +4 -4
- python_saga_orchestrator-0.1.3.dist-info/RECORD +0 -35
- {python_saga_orchestrator-0.1.3.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/WHEEL +0 -0
- {python_saga_orchestrator-0.1.3.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/licenses/LICENSE +0 -0
- {python_saga_orchestrator-0.1.3.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/top_level.txt +0 -0
{python_saga_orchestrator-0.1.3.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-saga-orchestrator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.3.dev0
|
|
4
4
|
Summary: Lightweight embedded saga orchestrator for asyncio Python services
|
|
5
5
|
Author-email: Maxim Vasilyev <mayxis@inbox.ru>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -51,6 +51,7 @@ Unlike external workflow platforms, this library runs inside your service and st
|
|
|
51
51
|
- persisted saga state through `SagaStateMixin`
|
|
52
52
|
- runtime execution through `SagaOrchestrator` and `SagaEngine`
|
|
53
53
|
- retry, timeout, recovery, and compensation
|
|
54
|
+
- async queue-style steps through `StepAwaitEvent` and `notify(...)`
|
|
54
55
|
- administrative operations through `SagaAdmin`
|
|
55
56
|
- PostgreSQL-first reliability using `SELECT ... FOR UPDATE`
|
|
56
57
|
|
|
@@ -136,11 +137,13 @@ Your SQLAlchemy model inherits `SagaStateMixin` to store:
|
|
|
136
137
|
## Quick start
|
|
137
138
|
|
|
138
139
|
```python
|
|
140
|
+
import uuid
|
|
139
141
|
from datetime import timedelta
|
|
140
142
|
|
|
141
143
|
from pydantic import BaseModel
|
|
144
|
+
from sqlalchemy import ForeignKey
|
|
142
145
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
143
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
146
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
144
147
|
|
|
145
148
|
from saga_orchestrator import (
|
|
146
149
|
BaseStep,
|
|
@@ -149,6 +152,7 @@ from saga_orchestrator import (
|
|
|
149
152
|
SagaBuilder,
|
|
150
153
|
SagaOrchestrator,
|
|
151
154
|
SagaStateMixin,
|
|
155
|
+
SagaStepHistoryMixin,
|
|
152
156
|
)
|
|
153
157
|
|
|
154
158
|
|
|
@@ -156,9 +160,24 @@ class Base(DeclarativeBase):
|
|
|
156
160
|
pass
|
|
157
161
|
|
|
158
162
|
|
|
163
|
+
class OrderSagaHistory(Base, SagaStepHistoryMixin):
|
|
164
|
+
__tablename__ = "order_saga_history"
|
|
165
|
+
|
|
166
|
+
saga_id: Mapped[uuid.UUID] = mapped_column(
|
|
167
|
+
ForeignKey("order_saga_state.id", ondelete="CASCADE"),
|
|
168
|
+
index=True,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
159
172
|
class OrderSagaState(Base, SagaStateMixin):
|
|
160
173
|
__tablename__ = "order_saga_state"
|
|
161
174
|
|
|
175
|
+
step_history: Mapped[list[OrderSagaHistory]] = relationship(
|
|
176
|
+
"OrderSagaHistory",
|
|
177
|
+
cascade="all, delete-orphan",
|
|
178
|
+
order_by="OrderSagaHistory.id",
|
|
179
|
+
)
|
|
180
|
+
|
|
162
181
|
|
|
163
182
|
class ReserveInput(BaseModel):
|
|
164
183
|
order_id: str
|
|
@@ -211,15 +230,20 @@ def build_order_saga():
|
|
|
211
230
|
|
|
212
231
|
|
|
213
232
|
def setup_saga(
|
|
214
|
-
|
|
215
|
-
) -> tuple[
|
|
216
|
-
|
|
233
|
+
session_maker: async_sessionmaker,
|
|
234
|
+
) -> tuple[
|
|
235
|
+
SagaOrchestrator[OrderSagaState, OrderSagaHistory],
|
|
236
|
+
SagaAdmin[OrderSagaState, OrderSagaHistory]
|
|
237
|
+
]:
|
|
238
|
+
orchestrator = SagaOrchestrator[OrderSagaState, OrderSagaHistory](
|
|
217
239
|
model_class=OrderSagaState,
|
|
240
|
+
history_model_class=OrderSagaHistory,
|
|
218
241
|
session_maker=session_maker,
|
|
219
242
|
)
|
|
220
243
|
orchestrator.register("create_order_v1", build_order_saga())
|
|
221
244
|
|
|
222
|
-
admin = SagaAdmin[OrderSagaState](engine=orchestrator.engine)
|
|
245
|
+
admin = SagaAdmin[OrderSagaState, OrderSagaHistory](engine=orchestrator.engine)
|
|
246
|
+
|
|
223
247
|
return orchestrator, admin
|
|
224
248
|
```
|
|
225
249
|
|
|
@@ -277,6 +301,23 @@ token = await orchestrator.await_event(
|
|
|
277
301
|
|
|
278
302
|
The event payload is stored in saga context and can be used by root-step `input_map` functions through `InputContext`.
|
|
279
303
|
|
|
304
|
+
For distributed consumers, use transactional inbox ingestion first, then process inbox rows:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
stored = await orchestrator.ingest_event(
|
|
308
|
+
aggregation_id="order-123",
|
|
309
|
+
event={
|
|
310
|
+
"event_id": "evt-123",
|
|
311
|
+
"event_type": "payment.completed",
|
|
312
|
+
"correlation_id": "corr-123",
|
|
313
|
+
"payload": {"payment_id": "pay-1"},
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if stored:
|
|
318
|
+
await orchestrator.run_inbox_due(limit=100)
|
|
319
|
+
```
|
|
320
|
+
|
|
280
321
|
## Administrative operations
|
|
281
322
|
|
|
282
323
|
Get the full persisted state:
|
|
@@ -334,6 +375,7 @@ A runnable end-to-end example is available in:
|
|
|
334
375
|
- [`examples/retry_recovery.py`](./examples/retry_recovery.py)
|
|
335
376
|
- [`examples/compensation_flow.py`](./examples/compensation_flow.py)
|
|
336
377
|
- [`examples/admin_skip.py`](./examples/admin_skip.py)
|
|
378
|
+
- [`examples/http_and_queue.py`](./examples/http_and_queue.py)
|
|
337
379
|
|
|
338
380
|
These examples demonstrate:
|
|
339
381
|
- basic model deployment
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
python_saga_orchestrator-0.2.3.dev0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
|
|
2
|
+
saga_orchestrator/__init__.py,sha256=FG7zoYhCzpZC_JEsy3ebDraH2ZZ9q3IGIjTGKqBzcF4,2836
|
|
3
|
+
saga_orchestrator/_version.py,sha256=b8lE-OZpo51B0ODVlQA6zcNoIpJ5YIH4JrzxWeFFTE0,533
|
|
4
|
+
saga_orchestrator/admin/__init__.py,sha256=TKwKTM7IieI4nlMAbJ0O0OI0KPKfwbclVffNjRtIyAg,80
|
|
5
|
+
saga_orchestrator/admin/api.py,sha256=u_eLELUlaHpEiuHqweNHzBwSYCtotERAiOxyVtUMe2I,1715
|
|
6
|
+
saga_orchestrator/core/__init__.py,sha256=EsUqhbO_CgCYZz0yBnf6OUXH3N-_uxMod4A7yGzvbMY,264
|
|
7
|
+
saga_orchestrator/core/builder.py,sha256=5G1kdA9UoMqZy7IT81YGz2FX1xkK9JPBhF9OMGqvrQs,5931
|
|
8
|
+
saga_orchestrator/core/engine.py,sha256=ntloRjKod4zl9iQmt1zEOYBcm8KJPqP1FgDcPN33XPU,59879
|
|
9
|
+
saga_orchestrator/core/orchestrator.py,sha256=5HvesuKh1STx4H2aSH8cQSlaFTmdQ_Y0aFBzuGN-6vw,6197
|
|
10
|
+
saga_orchestrator/core/repository.py,sha256=TWBuhc0lLRpR6O2NvdyqhuD2RxgnLbG_JcKFxfc4yVA,5698
|
|
11
|
+
saga_orchestrator/domain/__init__.py,sha256=ECVisQXiPSwx974Dbei_Ze_6SWqPVaQrTZPj_qESpYA,21
|
|
12
|
+
saga_orchestrator/domain/exceptions/__init__.py,sha256=smKhoR_G-PLtqvDkxgzCzDE7zfoztpUoFc0x4prdX4U,334
|
|
13
|
+
saga_orchestrator/domain/exceptions/saga.py,sha256=GeqxHJkHxobM8e8sVq69skP75_Sklo6mqFS9gFvTx4Y,567
|
|
14
|
+
saga_orchestrator/domain/mixins/__init__.py,sha256=ibsuLmZf0g8zAnQWs2MpDOPA0b0poptpEhMDnmbAXsI,186
|
|
15
|
+
saga_orchestrator/domain/mixins/saga_state.py,sha256=Gc7cozW0dHzKSJoP9-yTusof0H__INQ3BUxBO6Z72l0,2334
|
|
16
|
+
saga_orchestrator/domain/mixins/saga_step_histrory.py,sha256=phB_oue8ksMMtDiNSERP3r_KGIQYMwH7Ahob0NbGD4A,1975
|
|
17
|
+
saga_orchestrator/domain/mixins/types.py,sha256=FlKee4yNq6y1lXX3W_SeE385udNNg-oZqMxqVXZoEpo,2526
|
|
18
|
+
saga_orchestrator/domain/models/__init__.py,sha256=vgUSmwVdPhKNijFZsfmZ7mQZD8DJgCV4s4hMVQZNlwE,853
|
|
19
|
+
saga_orchestrator/domain/models/builder.py,sha256=D3mVYEJT7QJ0e2dNrR2UNHT5xhRK4te6s5WchU57EIA,816
|
|
20
|
+
saga_orchestrator/domain/models/context.py,sha256=a0nNYTW0S73Qk5ln85Ji9_siEe38qlb9GPH8XpL0Gi8,3886
|
|
21
|
+
saga_orchestrator/domain/models/notify.py,sha256=SuZILSFNAWcUQtjL-I_c4K6VFJXXhD5LLLC2KoMq8dw,837
|
|
22
|
+
saga_orchestrator/domain/models/retry.py,sha256=UM2ZrSGKDZRiPMj0qOGp36xiTr78CVKsceXFFxtiOug,1362
|
|
23
|
+
saga_orchestrator/domain/models/saga_snapshot.py,sha256=ysnBVB2rw059pC07Py_xzAA-Bv7d1h6ZZuubM_Fv0_4,867
|
|
24
|
+
saga_orchestrator/domain/models/step.py,sha256=-4hWbH2xZpGSI-xFQYHI1SoucXe4Xyq5vQ3t0DH0rQ4,4854
|
|
25
|
+
saga_orchestrator/domain/models/enums/__init__.py,sha256=IFgCFvhHiQl1MSCN_I_w3O8yR94tZvN5Az8M3V3bXEQ,234
|
|
26
|
+
saga_orchestrator/domain/models/enums/saga_status.py,sha256=Evz7Uy4-KrqGWFkb6RHhONCWLePd5Gjezw5u-FLmxw0,397
|
|
27
|
+
saga_orchestrator/domain/models/enums/saga_step_phase.py,sha256=L73jg8q-IdDXPpMjuMRwhfVZbAMYW_42FkdXNKqwiqI,129
|
|
28
|
+
saga_orchestrator/domain/models/enums/saga_step_status.py,sha256=DZN5qgDorhdFbhI6t6WXAu4xMpj7DE53OJ707wqj4RM,125
|
|
29
|
+
saga_orchestrator/inbox/__init__.py,sha256=uoLytCCT-2-zr369ZpGmQOB_D63fmwjV9T6sCHYGk64,656
|
|
30
|
+
saga_orchestrator/inbox/contracts.py,sha256=unV7h1Eqil6sZhmpGVE_-9c_voRQhYA3KdE63T79s4U,1768
|
|
31
|
+
saga_orchestrator/inbox/dispatcher.py,sha256=O4Zzi5fRAZIUQqlk3oJof6rl7_CPzvqSVQ_Un-1WkOY,4323
|
|
32
|
+
saga_orchestrator/inbox/models.py,sha256=i6ANKTqDTanmpA2hp5V5pky06u5DoHfo2oCWCsTVQUI,2654
|
|
33
|
+
saga_orchestrator/inbox/repository.py,sha256=BdcVlBW9zvM7WrsL2S3Rmihmn-Ri6gr69L8ji7qqip8,5286
|
|
34
|
+
saga_orchestrator/inbox/retry.py,sha256=jTxg6tZBl7Vllt89Yw-QdOSObYWXrZWjAyW36mCJHdg,590
|
|
35
|
+
saga_orchestrator/outbox/__init__.py,sha256=g-isr2D737wMV1Mf_vEPFoS34YE6n6l09iKLLAL86uU,917
|
|
36
|
+
saga_orchestrator/outbox/contracts.py,sha256=KhqIDIDvwA26xu4VQJpU6qg2ySmsRAORestIk1JqpiY,2028
|
|
37
|
+
saga_orchestrator/outbox/dispatcher.py,sha256=BX9WTuTLx1gfu-5jV8_ejpi9nnEilnKLYY_BljJkxC8,3196
|
|
38
|
+
saga_orchestrator/outbox/event.py,sha256=Kj-u_JO55jx3YMTpA9arIvmTgKEAxhH2-WQE1eTi4KU,273
|
|
39
|
+
saga_orchestrator/outbox/factory.py,sha256=_p7XT_ulouBhl9J9fcgWsZkhkjmzkpfIs9m6E55UPdA,1743
|
|
40
|
+
saga_orchestrator/outbox/models.py,sha256=ei8Q1b9NnjAWq5qm0vAIeuj22yWVOFx0UsqGszZA0Q0,2355
|
|
41
|
+
saga_orchestrator/outbox/repository.py,sha256=A3nCi5UOwQT9mlEY03BrLidyO2sFOQGgYcE4pFeY-pA,4786
|
|
42
|
+
saga_orchestrator/outbox/retry.py,sha256=9Ygz3I0HK0r9imTYevBgz14TkAZphKiOSf0grJpH99c,599
|
|
43
|
+
saga_orchestrator/outbox/serialization.py,sha256=CDicJS95CHhLP47XukJAgfm0baZ83daWXQQF3MyczDo,1687
|
|
44
|
+
python_saga_orchestrator-0.2.3.dev0.dist-info/METADATA,sha256=TKDQ42mOx7sTrACtJtxs60bcDE0AhWQNH8JZz-6c7Lo,10293
|
|
45
|
+
python_saga_orchestrator-0.2.3.dev0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
46
|
+
python_saga_orchestrator-0.2.3.dev0.dist-info/top_level.txt,sha256=XBp_2J8dacJGCoVxIDaUYhSEuOusCN3BD_uhEjBEEBA,18
|
|
47
|
+
python_saga_orchestrator-0.2.3.dev0.dist-info/RECORD,,
|
saga_orchestrator/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from .domain.exceptions import (
|
|
|
9
9
|
SagaStateError,
|
|
10
10
|
TypeValidationError,
|
|
11
11
|
)
|
|
12
|
-
from .domain.mixins import SagaStateMixin
|
|
12
|
+
from .domain.mixins import SagaStateMixin, SagaStepHistoryMixin
|
|
13
13
|
from .domain.models import (
|
|
14
14
|
AwaitingEvent,
|
|
15
15
|
BaseStep,
|
|
@@ -24,11 +24,26 @@ from .domain.models import (
|
|
|
24
24
|
SagaAdminSnapshot,
|
|
25
25
|
SagaDefinition,
|
|
26
26
|
SagaSnapshot,
|
|
27
|
+
StepAwaitEvent,
|
|
27
28
|
StepDefinition,
|
|
28
29
|
StepInputMap,
|
|
29
30
|
StepRef,
|
|
30
31
|
)
|
|
31
|
-
from .domain.models.enums import SagaStatus
|
|
32
|
+
from .domain.models.enums import SagaStatus, SagaStepPhase, SagaStepStatus
|
|
33
|
+
from .inbox import (
|
|
34
|
+
ClaimedInboxMessage,
|
|
35
|
+
FixedInboxRetry,
|
|
36
|
+
InboxDispatcher,
|
|
37
|
+
InboxMessageMixin,
|
|
38
|
+
InboxProcessor,
|
|
39
|
+
InboxProcessOutcome,
|
|
40
|
+
InboxProcessStatus,
|
|
41
|
+
InboxRepository,
|
|
42
|
+
InboxRetryPolicy,
|
|
43
|
+
InboxStatus,
|
|
44
|
+
InboxWriteMessage,
|
|
45
|
+
InboxWriter,
|
|
46
|
+
)
|
|
32
47
|
from .outbox import (
|
|
33
48
|
ClaimedOutboxMessage,
|
|
34
49
|
DefaultOutboxMessageFactory,
|
|
@@ -51,11 +66,23 @@ __all__ = [
|
|
|
51
66
|
"ActiveSagaAlreadyExistsError",
|
|
52
67
|
"AwaitingEvent",
|
|
53
68
|
"BaseStep",
|
|
69
|
+
"ClaimedInboxMessage",
|
|
54
70
|
"ClaimedOutboxMessage",
|
|
55
71
|
"DefaultOutboxMessageFactory",
|
|
56
72
|
"ExponentialRetry",
|
|
73
|
+
"FixedInboxRetry",
|
|
57
74
|
"FixedOutboxDispatchRetry",
|
|
58
75
|
"FixedRetry",
|
|
76
|
+
"InboxDispatcher",
|
|
77
|
+
"InboxMessageMixin",
|
|
78
|
+
"InboxProcessOutcome",
|
|
79
|
+
"InboxProcessStatus",
|
|
80
|
+
"InboxProcessor",
|
|
81
|
+
"InboxRepository",
|
|
82
|
+
"InboxRetryPolicy",
|
|
83
|
+
"InboxStatus",
|
|
84
|
+
"InboxWriteMessage",
|
|
85
|
+
"InboxWriter",
|
|
59
86
|
"InputContext",
|
|
60
87
|
"JsonOutboxSerializer",
|
|
61
88
|
"NotifyEvent",
|
|
@@ -85,9 +112,13 @@ __all__ = [
|
|
|
85
112
|
"SagaRepository",
|
|
86
113
|
"SagaSnapshot",
|
|
87
114
|
"SagaStateError",
|
|
115
|
+
"SagaStepHistoryMixin",
|
|
88
116
|
"SagaStateMixin",
|
|
89
117
|
"SagaStatus",
|
|
118
|
+
"SagaStepStatus",
|
|
119
|
+
"SagaStepPhase",
|
|
90
120
|
"StepDefinition",
|
|
121
|
+
"StepAwaitEvent",
|
|
91
122
|
"StepInputMap",
|
|
92
123
|
"StepRef",
|
|
93
124
|
"TypeValidationError",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.2.3.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 3, 'dev0')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
saga_orchestrator/admin/api.py
CHANGED
|
@@ -7,17 +7,19 @@ from pydantic import BaseModel
|
|
|
7
7
|
|
|
8
8
|
from ..core.engine import SagaEngine
|
|
9
9
|
from ..domain.mixins import SagaStateMixin
|
|
10
|
+
from ..domain.mixins.saga_step_histrory import SagaStepHistoryMixin
|
|
10
11
|
from ..domain.models import SagaAdminSnapshot
|
|
11
12
|
|
|
12
13
|
ModelT = TypeVar("ModelT", bound=SagaStateMixin)
|
|
14
|
+
HistoryModelT = TypeVar("HistoryModelT", bound=SagaStepHistoryMixin)
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
class SagaAdmin(Generic[ModelT]):
|
|
17
|
+
class SagaAdmin(Generic[ModelT, HistoryModelT]):
|
|
16
18
|
"""Provide administrative operations for persisted saga instances."""
|
|
17
19
|
|
|
18
20
|
def __init__(
|
|
19
21
|
self,
|
|
20
|
-
engine: SagaEngine[ModelT],
|
|
22
|
+
engine: SagaEngine[ModelT, HistoryModelT],
|
|
21
23
|
) -> None:
|
|
22
24
|
"""Initialize the admin API facade."""
|
|
23
25
|
self._engine = engine
|
|
@@ -11,6 +11,9 @@ from ..domain.models import (
|
|
|
11
11
|
BaseStep,
|
|
12
12
|
InputContext,
|
|
13
13
|
NoRetry,
|
|
14
|
+
OnFailedMap,
|
|
15
|
+
OnStartMap,
|
|
16
|
+
OnTerminalStateMap,
|
|
14
17
|
OutboxMap,
|
|
15
18
|
RetryPolicy,
|
|
16
19
|
SagaDefinition,
|
|
@@ -28,6 +31,11 @@ class SagaBuilder:
|
|
|
28
31
|
self._steps: list[StepDefinition[Any, Any]] = []
|
|
29
32
|
self._compensate_on_failure = compensate_on_failure
|
|
30
33
|
|
|
34
|
+
self._on_start_map: OnStartMap | None = None
|
|
35
|
+
self._on_completed_map: OnTerminalStateMap | None = None
|
|
36
|
+
self._on_failed_map: OnFailedMap | None = None
|
|
37
|
+
self._on_compensated_map: OnTerminalStateMap | None = None
|
|
38
|
+
|
|
31
39
|
def add_step(
|
|
32
40
|
self,
|
|
33
41
|
*,
|
|
@@ -62,6 +70,34 @@ class SagaBuilder:
|
|
|
62
70
|
self._steps.append(definition)
|
|
63
71
|
return StepRef(step_id=normalized_step_id, output_model=step.output_model)
|
|
64
72
|
|
|
73
|
+
def on_start(self, on_start_map: OnStartMap) -> SagaBuilder:
|
|
74
|
+
"""Define actions to be taken when the saga starts."""
|
|
75
|
+
if not callable(on_start_map):
|
|
76
|
+
raise SagaDefinitionError("on_start_map must be callable")
|
|
77
|
+
self._on_start_map = on_start_map
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def on_completed(self, on_completed_map: OnTerminalStateMap) -> SagaBuilder:
|
|
81
|
+
"""Define actions to be taken when the saga completes successfully."""
|
|
82
|
+
if not callable(on_completed_map):
|
|
83
|
+
raise SagaDefinitionError("on_completed_map must be callable")
|
|
84
|
+
self._on_completed_map = on_completed_map
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def on_failed(self, on_failed_map: OnFailedMap) -> SagaBuilder:
|
|
88
|
+
"""Define actions to be taken when the saga fails."""
|
|
89
|
+
if not callable(on_failed_map):
|
|
90
|
+
raise SagaDefinitionError("on_failed_map must be callable")
|
|
91
|
+
self._on_failed_map = on_failed_map
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def on_compensated(self, on_compensated_map: OnTerminalStateMap) -> SagaBuilder:
|
|
95
|
+
"""Define actions to be taken when the saga is fully compensated."""
|
|
96
|
+
if not callable(on_compensated_map):
|
|
97
|
+
raise SagaDefinitionError("on_compensated_map must be callable")
|
|
98
|
+
self._on_compensated_map = on_compensated_map
|
|
99
|
+
return self
|
|
100
|
+
|
|
65
101
|
def build(self) -> SagaDefinition:
|
|
66
102
|
"""Return the final saga definition."""
|
|
67
103
|
if not self._steps:
|
|
@@ -69,6 +105,10 @@ class SagaBuilder:
|
|
|
69
105
|
return SagaDefinition(
|
|
70
106
|
steps=tuple(self._steps),
|
|
71
107
|
compensate_on_failure=self._compensate_on_failure,
|
|
108
|
+
on_start_map=self._on_start_map,
|
|
109
|
+
on_completed_map=self._on_completed_map,
|
|
110
|
+
on_failed_map=self._on_failed_map,
|
|
111
|
+
on_compensated_map=self._on_compensated_map,
|
|
72
112
|
)
|
|
73
113
|
|
|
74
114
|
@staticmethod
|