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.
Files changed (27) hide show
  1. {python_saga_orchestrator-0.1.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/METADATA +29 -6
  2. python_saga_orchestrator-0.2.3.dev0.dist-info/RECORD +47 -0
  3. saga_orchestrator/__init__.py +5 -2
  4. saga_orchestrator/_version.py +24 -0
  5. saga_orchestrator/admin/api.py +4 -2
  6. saga_orchestrator/core/builder.py +40 -0
  7. saga_orchestrator/core/engine.py +347 -176
  8. saga_orchestrator/core/orchestrator.py +5 -1
  9. saga_orchestrator/core/repository.py +24 -27
  10. saga_orchestrator/domain/mixins/__init__.py +2 -0
  11. saga_orchestrator/domain/mixins/saga_state.py +26 -21
  12. saga_orchestrator/domain/mixins/saga_step_histrory.py +45 -0
  13. saga_orchestrator/domain/mixins/types.py +78 -0
  14. saga_orchestrator/domain/models/__init__.py +4 -1
  15. saga_orchestrator/domain/models/builder.py +13 -2
  16. saga_orchestrator/domain/models/context.py +126 -0
  17. saga_orchestrator/domain/models/enums/__init__.py +4 -0
  18. saga_orchestrator/domain/models/enums/saga_status.py +5 -3
  19. saga_orchestrator/domain/models/enums/saga_step_phase.py +7 -0
  20. saga_orchestrator/domain/models/enums/saga_step_status.py +7 -0
  21. saga_orchestrator/domain/models/saga_snapshot.py +5 -3
  22. saga_orchestrator/domain/models/step.py +24 -2
  23. saga_orchestrator/outbox/factory.py +4 -4
  24. python_saga_orchestrator-0.1.4.dist-info/RECORD +0 -41
  25. {python_saga_orchestrator-0.1.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/WHEEL +0 -0
  26. {python_saga_orchestrator-0.1.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/licenses/LICENSE +0 -0
  27. {python_saga_orchestrator-0.1.4.dist-info → python_saga_orchestrator-0.2.3.dev0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-saga-orchestrator
3
- Version: 0.1.4
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
- session_maker: async_sessionmaker,
216
- ) -> tuple[SagaOrchestrator[OrderSagaState], SagaAdmin[OrderSagaState]]:
217
- orchestrator = SagaOrchestrator[OrderSagaState](
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,,
@@ -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
@@ -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