python-saga-orchestrator 0.1.4__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.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/METADATA +29 -6
- python_saga_orchestrator-0.2.3.dev0.dist-info/RECORD +47 -0
- saga_orchestrator/__init__.py +5 -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 +347 -176
- saga_orchestrator/core/orchestrator.py +5 -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 +4 -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/saga_snapshot.py +5 -3
- saga_orchestrator/domain/models/step.py +24 -2
- saga_orchestrator/outbox/factory.py +4 -4
- python_saga_orchestrator-0.1.4.dist-info/RECORD +0 -41
- {python_saga_orchestrator-0.1.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/WHEEL +0 -0
- {python_saga_orchestrator-0.1.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/licenses/LICENSE +0 -0
- {python_saga_orchestrator-0.1.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/top_level.txt +0 -0
{python_saga_orchestrator-0.1.4.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
|
|
@@ -137,11 +137,13 @@ Your SQLAlchemy model inherits `SagaStateMixin` to store:
|
|
|
137
137
|
## Quick start
|
|
138
138
|
|
|
139
139
|
```python
|
|
140
|
+
import uuid
|
|
140
141
|
from datetime import timedelta
|
|
141
142
|
|
|
142
143
|
from pydantic import BaseModel
|
|
144
|
+
from sqlalchemy import ForeignKey
|
|
143
145
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
144
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
146
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
145
147
|
|
|
146
148
|
from saga_orchestrator import (
|
|
147
149
|
BaseStep,
|
|
@@ -150,6 +152,7 @@ from saga_orchestrator import (
|
|
|
150
152
|
SagaBuilder,
|
|
151
153
|
SagaOrchestrator,
|
|
152
154
|
SagaStateMixin,
|
|
155
|
+
SagaStepHistoryMixin,
|
|
153
156
|
)
|
|
154
157
|
|
|
155
158
|
|
|
@@ -157,9 +160,24 @@ class Base(DeclarativeBase):
|
|
|
157
160
|
pass
|
|
158
161
|
|
|
159
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
|
+
|
|
160
172
|
class OrderSagaState(Base, SagaStateMixin):
|
|
161
173
|
__tablename__ = "order_saga_state"
|
|
162
174
|
|
|
175
|
+
step_history: Mapped[list[OrderSagaHistory]] = relationship(
|
|
176
|
+
"OrderSagaHistory",
|
|
177
|
+
cascade="all, delete-orphan",
|
|
178
|
+
order_by="OrderSagaHistory.id",
|
|
179
|
+
)
|
|
180
|
+
|
|
163
181
|
|
|
164
182
|
class ReserveInput(BaseModel):
|
|
165
183
|
order_id: str
|
|
@@ -212,15 +230,20 @@ def build_order_saga():
|
|
|
212
230
|
|
|
213
231
|
|
|
214
232
|
def setup_saga(
|
|
215
|
-
|
|
216
|
-
) -> tuple[
|
|
217
|
-
|
|
233
|
+
session_maker: async_sessionmaker,
|
|
234
|
+
) -> tuple[
|
|
235
|
+
SagaOrchestrator[OrderSagaState, OrderSagaHistory],
|
|
236
|
+
SagaAdmin[OrderSagaState, OrderSagaHistory]
|
|
237
|
+
]:
|
|
238
|
+
orchestrator = SagaOrchestrator[OrderSagaState, OrderSagaHistory](
|
|
218
239
|
model_class=OrderSagaState,
|
|
240
|
+
history_model_class=OrderSagaHistory,
|
|
219
241
|
session_maker=session_maker,
|
|
220
242
|
)
|
|
221
243
|
orchestrator.register("create_order_v1", build_order_saga())
|
|
222
244
|
|
|
223
|
-
admin = SagaAdmin[OrderSagaState](engine=orchestrator.engine)
|
|
245
|
+
admin = SagaAdmin[OrderSagaState, OrderSagaHistory](engine=orchestrator.engine)
|
|
246
|
+
|
|
224
247
|
return orchestrator, admin
|
|
225
248
|
```
|
|
226
249
|
|
|
@@ -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,
|
|
@@ -29,7 +29,7 @@ from .domain.models import (
|
|
|
29
29
|
StepInputMap,
|
|
30
30
|
StepRef,
|
|
31
31
|
)
|
|
32
|
-
from .domain.models.enums import SagaStatus
|
|
32
|
+
from .domain.models.enums import SagaStatus, SagaStepPhase, SagaStepStatus
|
|
33
33
|
from .inbox import (
|
|
34
34
|
ClaimedInboxMessage,
|
|
35
35
|
FixedInboxRetry,
|
|
@@ -112,8 +112,11 @@ __all__ = [
|
|
|
112
112
|
"SagaRepository",
|
|
113
113
|
"SagaSnapshot",
|
|
114
114
|
"SagaStateError",
|
|
115
|
+
"SagaStepHistoryMixin",
|
|
115
116
|
"SagaStateMixin",
|
|
116
117
|
"SagaStatus",
|
|
118
|
+
"SagaStepStatus",
|
|
119
|
+
"SagaStepPhase",
|
|
117
120
|
"StepDefinition",
|
|
118
121
|
"StepAwaitEvent",
|
|
119
122
|
"StepInputMap",
|
|
@@ -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
|