Skip to content

Commit 9ec3615

Browse files
author
Sergio García Prado
authored
Merge pull request #467 from minos-framework/issue-198-saga-defined-as-class
#198 - Define `Saga` as a class
2 parents b9c5378 + 2e64837 commit 9ec3615

File tree

28 files changed

+1252
-188
lines changed

28 files changed

+1252
-188
lines changed

packages/core/minos-microservice-saga/minos/saga/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,28 @@
99
)
1010
from .definitions import (
1111
ConditionalSagaStep,
12+
ConditionalSagaStepDecoratorMeta,
13+
ConditionalSagaStepDecoratorWrapper,
1214
ElseThenAlternative,
15+
ElseThenAlternativeDecoratorMeta,
16+
ElseThenAlternativeDecoratorWrapper,
1317
IfThenAlternative,
18+
IfThenAlternativeDecoratorMeta,
19+
IfThenAlternativeDecoratorWrapper,
1420
LocalSagaStep,
21+
LocalSagaStepDecoratorMeta,
22+
LocalSagaStepDecoratorWrapper,
1523
RemoteSagaStep,
24+
RemoteSagaStepDecoratorMeta,
25+
RemoteSagaStepDecoratorWrapper,
1626
Saga,
27+
SagaDecoratorMeta,
28+
SagaDecoratorWrapper,
1729
SagaOperation,
30+
SagaOperationDecorator,
1831
SagaStep,
32+
SagaStepDecoratorMeta,
33+
SagaStepDecoratorWrapper,
1934
)
2035
from .exceptions import (
2136
AlreadyCommittedException,
@@ -27,6 +42,7 @@
2742
MultipleOnExecuteException,
2843
MultipleOnFailureException,
2944
MultipleOnSuccessException,
45+
OrderPrecedenceException,
3046
SagaException,
3147
SagaExecutionAlreadyExecutedException,
3248
SagaExecutionException,

packages/core/minos-microservice-saga/minos/saga/definitions/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
11
from .operations import (
22
SagaOperation,
3+
SagaOperationDecorator,
34
)
45
from .saga import (
56
Saga,
7+
SagaDecoratorMeta,
8+
SagaDecoratorWrapper,
69
)
710
from .steps import (
811
ConditionalSagaStep,
12+
ConditionalSagaStepDecoratorMeta,
13+
ConditionalSagaStepDecoratorWrapper,
914
ElseThenAlternative,
15+
ElseThenAlternativeDecoratorMeta,
16+
ElseThenAlternativeDecoratorWrapper,
1017
IfThenAlternative,
18+
IfThenAlternativeDecoratorMeta,
19+
IfThenAlternativeDecoratorWrapper,
1120
LocalSagaStep,
21+
LocalSagaStepDecoratorMeta,
22+
LocalSagaStepDecoratorWrapper,
1223
RemoteSagaStep,
24+
RemoteSagaStepDecoratorMeta,
25+
RemoteSagaStepDecoratorWrapper,
1326
SagaStep,
27+
SagaStepDecoratorMeta,
28+
SagaStepDecoratorWrapper,
1429
)
1530
from .types import (
31+
ConditionCallback,
1632
LocalCallback,
1733
RequestCallBack,
1834
ResponseCallBack,

packages/core/minos-microservice-saga/minos/saga/definitions/operations.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
"""Operation module."""
2+
13
from __future__ import (
24
annotations,
35
)
46

7+
from collections.abc import (
8+
Callable,
9+
Iterable,
10+
)
511
from typing import (
12+
TYPE_CHECKING,
613
Any,
7-
Callable,
814
Generic,
9-
Iterable,
1015
Optional,
1116
TypeVar,
1217
Union,
1318
)
1419

20+
if TYPE_CHECKING:
21+
from .steps import SagaStepDecoratorMeta
22+
1523
from minos.common import (
1624
classname,
1725
import_module,
@@ -24,6 +32,27 @@
2432
T = TypeVar("T", bound=Callable)
2533

2634

35+
class SagaOperationDecorator(Generic[T]):
36+
"""Saga Operation Decorator class."""
37+
38+
def __init__(self, attr_name: str = None, step_meta: SagaStepDecoratorMeta = None, *args, **kwargs):
39+
if attr_name is None:
40+
raise ValueError(f"The 'attr_name' must not be {None!r}.")
41+
if step_meta is None:
42+
raise ValueError(f"The 'step_meta' must not be {None!r}.")
43+
44+
self._step_meta = step_meta
45+
self._attr_name = attr_name
46+
47+
self._args = args
48+
self._kwargs = kwargs
49+
50+
def __call__(self, func: T) -> T:
51+
operation = SagaOperation(func, *self._args, **self._kwargs)
52+
setattr(self._step_meta, self._attr_name, operation)
53+
return func
54+
55+
2756
class SagaOperation(Generic[T]):
2857
"""Saga Step Operation class."""
2958

@@ -80,9 +109,15 @@ def from_raw(cls, raw: Optional[Union[dict[str, Any], SagaOperation[T]]], **kwar
80109
current["parameters"] = SagaContext.from_avro_str(current["parameters"])
81110
return cls(**current)
82111

112+
def __hash__(self):
113+
return hash(tuple(self))
114+
83115
def __eq__(self, other: SagaOperation) -> bool:
84116
return type(self) == type(other) and tuple(self) == tuple(other)
85117

118+
def __repr__(self) -> str:
119+
return f"{type(self).__name__}{tuple(self)}"
120+
86121
def __iter__(self) -> Iterable:
87122
yield from (
88123
self.callback,

packages/core/minos-microservice-saga/minos/saga/definitions/saga.py

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
1+
"""Saga definitions module."""
2+
13
from __future__ import (
24
annotations,
35
)
46

57
import warnings
8+
from collections.abc import (
9+
Iterable,
10+
)
11+
from inspect import (
12+
getmembers,
13+
)
14+
from operator import (
15+
attrgetter,
16+
)
617
from typing import (
718
Any,
8-
Iterable,
919
Optional,
10-
Type,
20+
Protocol,
1121
TypeVar,
1222
Union,
23+
runtime_checkable,
24+
)
25+
26+
from cached_property import (
27+
cached_property,
1328
)
1429

1530
from ..exceptions import (
1631
AlreadyCommittedException,
1732
AlreadyOnSagaException,
1833
EmptySagaException,
34+
OrderPrecedenceException,
1935
SagaNotCommittedException,
2036
)
2137
from .operations import (
@@ -26,32 +42,90 @@
2642
LocalSagaStep,
2743
RemoteSagaStep,
2844
SagaStep,
45+
SagaStepDecoratorWrapper,
2946
)
3047
from .types import (
3148
LocalCallback,
3249
RequestCallBack,
3350
)
3451

3552

53+
@runtime_checkable
54+
class SagaDecoratorWrapper(Protocol):
55+
"""Saga Decorator Wrapper class."""
56+
57+
meta: SagaDecoratorMeta
58+
59+
60+
class SagaDecoratorMeta:
61+
"""Saga Decorator Meta class."""
62+
63+
_inner: type
64+
_definition: Saga
65+
66+
def __init__(self, func: type, saga: Saga):
67+
self._inner = func
68+
self._definition = saga
69+
70+
@cached_property
71+
def definition(self) -> Saga:
72+
"""Get the saga definition.
73+
74+
:return: A ``Saga`` instance.
75+
"""
76+
steps = getmembers(self._inner, predicate=lambda x: isinstance(x, SagaStepDecoratorWrapper))
77+
steps = list(map(lambda member: member[1].meta.definition, steps))
78+
for step in steps:
79+
if step.order is None:
80+
raise OrderPrecedenceException(f"The {step!r} step does not have 'order' value.")
81+
steps.sort(key=attrgetter("order"))
82+
83+
for step in steps:
84+
self._definition.add_step(step)
85+
self._definition.commit()
86+
87+
return self._definition
88+
89+
90+
TP = TypeVar("TP", bound=type)
91+
92+
3693
class Saga:
3794
"""Saga class.
3895
3996
The purpose of this class is to define a sequence of operations among microservices.
4097
"""
4198

4299
# noinspection PyUnusedLocal
43-
def __init__(self, *args, steps: list[SagaStep] = None, committed: bool = False, commit: None = None, **kwargs):
100+
def __init__(
101+
self, *args, steps: list[SagaStep] = None, committed: Optional[bool] = None, commit: None = None, **kwargs
102+
):
103+
self.steps = list()
104+
self.committed = False
105+
44106
if steps is None:
45107
steps = list()
108+
for step in steps:
109+
self.add_step(step)
46110

47-
self.steps = steps
111+
if committed is None:
112+
committed = len(steps)
48113
self.committed = committed
49114

50115
if commit is not None:
51116
warnings.warn(f"Commit callback is being deprecated. Use {self.local_step!r} instead", DeprecationWarning)
52117
self.local_step(commit)
53118
self.committed = True
54119

120+
def __call__(self, type_: TP) -> Union[TP, SagaDecoratorWrapper]:
121+
"""Decorate the given type.
122+
123+
:param type_: The type to be decorated.
124+
:return: The decorated type.
125+
"""
126+
type_.meta = SagaDecoratorMeta(type_, self)
127+
return type_
128+
55129
@classmethod
56130
def from_raw(cls, raw: Union[dict[str, Any], Saga], **kwargs) -> Saga:
57131
"""Build a new ``Saga`` instance from raw.
@@ -77,7 +151,7 @@ def conditional_step(self, step: Optional[ConditionalSagaStep] = None) -> Condit
77151
:param step: The step to be added. If `None` is provided then a new one will be created.
78152
:return: A ``SagaStep`` instance.
79153
"""
80-
return self._add_step(ConditionalSagaStep, step)
154+
return self.add_step(step, ConditionalSagaStep)
81155

82156
def local_step(
83157
self, step: Optional[Union[LocalCallback, SagaOperation[LocalCallback], LocalSagaStep]] = None, **kwargs
@@ -90,12 +164,12 @@ def local_step(
90164
"""
91165
if step is not None and not isinstance(step, SagaStep) and not isinstance(step, SagaOperation):
92166
step = SagaOperation(step, **kwargs)
93-
return self._add_step(LocalSagaStep, step)
167+
return self.add_step(step, LocalSagaStep)
94168

95169
def step(
96170
self, step: Optional[Union[RequestCallBack, SagaOperation[RequestCallBack], RemoteSagaStep]] = None, **kwargs
97171
) -> RemoteSagaStep:
98-
"""Add a new remote step step.
172+
"""Add a new remote step.
99173
100174
:param step: The step to be added. If `None` is provided then a new one will be created.
101175
:param kwargs: Additional named parameters.
@@ -107,17 +181,23 @@ def step(
107181
def remote_step(
108182
self, step: Optional[Union[RequestCallBack, SagaOperation[RequestCallBack], RemoteSagaStep]] = None, **kwargs
109183
) -> RemoteSagaStep:
110-
"""Add a new remote step step.
184+
"""Add a new remote step.
111185
112186
:param step: The step to be added. If `None` is provided then a new one will be created.
113187
:param kwargs: Additional named parameters.
114188
:return: A ``SagaStep`` instance.
115189
"""
116190
if step is not None and not isinstance(step, SagaStep) and not isinstance(step, SagaOperation):
117191
step = SagaOperation(step, **kwargs)
118-
return self._add_step(RemoteSagaStep, step)
192+
return self.add_step(step, RemoteSagaStep)
193+
194+
def add_step(self, step: Optional[Union[SagaOperation, T]], step_cls: type[T] = SagaStep) -> T:
195+
"""Add a new step.
119196
120-
def _add_step(self, step_cls: Type[T], step: Optional[Union[SagaOperation, T]]) -> T:
197+
:param step: The step to be added.
198+
:param step_cls: The step class (for validation purposes).
199+
:return: The added step.
200+
"""
121201
if self.committed:
122202
raise AlreadyCommittedException("It is not possible to add more steps to an already committed saga.")
123203

@@ -129,9 +209,21 @@ def _add_step(self, step_cls: Type[T], step: Optional[Union[SagaOperation, T]])
129209
if step.saga is not None:
130210
raise AlreadyOnSagaException()
131211
step.saga = self
212+
132213
else:
133214
step = step_cls(step, saga=self)
134215

216+
if step.order is None:
217+
if self.steps:
218+
step.order = self.steps[-1].order + 1
219+
else:
220+
step.order = 1
221+
222+
if self.steps and step.order <= self.steps[-1].order:
223+
raise OrderPrecedenceException(
224+
f"Unsatisfied precedence constraints. Previous: {self.steps[-1].order} Current: {step.order} "
225+
)
226+
135227
self.steps.append(step)
136228
return step
137229

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
from .abc import (
22
SagaStep,
3+
SagaStepDecoratorMeta,
4+
SagaStepDecoratorWrapper,
35
)
46
from .conditional import (
57
ConditionalSagaStep,
8+
ConditionalSagaStepDecoratorMeta,
9+
ConditionalSagaStepDecoratorWrapper,
610
ElseThenAlternative,
11+
ElseThenAlternativeDecoratorMeta,
12+
ElseThenAlternativeDecoratorWrapper,
713
IfThenAlternative,
14+
IfThenAlternativeDecoratorMeta,
15+
IfThenAlternativeDecoratorWrapper,
816
)
917
from .local import (
1018
LocalSagaStep,
19+
LocalSagaStepDecoratorMeta,
20+
LocalSagaStepDecoratorWrapper,
1121
)
1222
from .remote import (
1323
RemoteSagaStep,
24+
RemoteSagaStepDecoratorMeta,
25+
RemoteSagaStepDecoratorWrapper,
1426
)

0 commit comments

Comments
 (0)