Skip to content

Commit 4ba8533

Browse files
author
Sergio García Prado
committed
ISSUE #198
* Add tests for `ConditionalSagaStep`. * Minor improvements.
1 parent 7f67728 commit 4ba8533

File tree

12 files changed

+260
-90
lines changed

12 files changed

+260
-90
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
ConditionalSagaStepDecoratorMeta,
1313
ConditionalSagaStepDecoratorWrapper,
1414
ElseThenAlternative,
15-
ElseThenAlternativeMeta,
16-
ElseThenAlternativeWrapper,
15+
ElseThenAlternativeDecoratorMeta,
16+
ElseThenAlternativeDecoratorWrapper,
1717
IfThenAlternative,
18-
IfThenAlternativeMeta,
19-
IfThenAlternativeWrapper,
18+
IfThenAlternativeDecoratorMeta,
19+
IfThenAlternativeDecoratorWrapper,
2020
LocalSagaStep,
2121
LocalSagaStepDecoratorMeta,
2222
LocalSagaStepDecoratorWrapper,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
ConditionalSagaStepDecoratorMeta,
1313
ConditionalSagaStepDecoratorWrapper,
1414
ElseThenAlternative,
15-
ElseThenAlternativeMeta,
16-
ElseThenAlternativeWrapper,
15+
ElseThenAlternativeDecoratorMeta,
16+
ElseThenAlternativeDecoratorWrapper,
1717
IfThenAlternative,
18-
IfThenAlternativeMeta,
19-
IfThenAlternativeWrapper,
18+
IfThenAlternativeDecoratorMeta,
19+
IfThenAlternativeDecoratorWrapper,
2020
LocalSagaStep,
2121
LocalSagaStepDecoratorMeta,
2222
LocalSagaStepDecoratorWrapper,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from inspect import (
1010
getmembers,
1111
)
12+
from operator import (
13+
attrgetter,
14+
)
1215
from typing import (
1316
Any,
1417
Optional,
@@ -70,7 +73,7 @@ def definition(self) -> Saga:
7073
for step in steps:
7174
if step.order is None:
7275
raise OrderPrecedenceException(f"The {step!r} step does not have 'order' value.")
73-
steps.sort(key=lambda s: s.order)
76+
steps.sort(key=attrgetter("order"))
7477

7578
for step in steps:
7679
self._definition.add_step(step)

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
ConditionalSagaStepDecoratorMeta,
99
ConditionalSagaStepDecoratorWrapper,
1010
ElseThenAlternative,
11-
ElseThenAlternativeMeta,
12-
ElseThenAlternativeWrapper,
11+
ElseThenAlternativeDecoratorMeta,
12+
ElseThenAlternativeDecoratorWrapper,
1313
IfThenAlternative,
14-
IfThenAlternativeMeta,
15-
IfThenAlternativeWrapper,
14+
IfThenAlternativeDecoratorMeta,
15+
IfThenAlternativeDecoratorWrapper,
1616
)
1717
from .local import (
1818
LocalSagaStep,

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

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from ...exceptions import (
3434
EmptySagaStepException,
3535
MultipleElseThenException,
36+
OrderPrecedenceException,
3637
)
3738
from ..operations import (
3839
SagaOperation,
@@ -51,10 +52,9 @@
5152
Saga,
5253
)
5354

54-
ConditionalSagaStepClass = TypeVar("ConditionalSagaStepClass", bound=type)
5555

56-
57-
class ConditionalSagaStepDecoratorWrapper(SagaStepDecoratorWrapper):
56+
@runtime_checkable
57+
class ConditionalSagaStepDecoratorWrapper(SagaStepDecoratorWrapper, Protocol):
5858
"""TODO"""
5959

6060
meta: ConditionalSagaStepDecoratorMeta
@@ -70,29 +70,37 @@ def definition(self):
7070
"""TODO"""
7171
if_then_alternatives = getmembers(
7272
self._inner,
73-
predicate=lambda x: isinstance(x, IfThenAlternativeWrapper) and isinstance(x.meta, IfThenAlternativeMeta),
73+
predicate=(
74+
lambda x: isinstance(x, IfThenAlternativeDecoratorWrapper)
75+
and isinstance(x.meta, IfThenAlternativeDecoratorMeta)
76+
),
7477
)
75-
if_then_alternatives = map(lambda member: member[1].meta.alternative, if_then_alternatives)
76-
if_then_alternatives = sorted(if_then_alternatives, key=attrgetter("order"))
78+
if_then_alternatives = list(map(lambda member: member[1].meta.alternative, if_then_alternatives))
79+
for alternative in if_then_alternatives:
80+
if alternative.order is None:
81+
raise OrderPrecedenceException(f"The {alternative!r} alternative does not have 'order' value.")
82+
if_then_alternatives.sort(key=attrgetter("order"))
7783

7884
for alternative in if_then_alternatives:
79-
self._definition.if_then_alternatives.append(alternative)
85+
self._definition.if_then(alternative)
8086

8187
else_then_alternatives = getmembers(
8288
self._inner,
8389
predicate=(
84-
lambda x: isinstance(x, ElseThenAlternativeWrapper) and isinstance(x.meta, ElseThenAlternativeMeta)
90+
lambda x: isinstance(x, ElseThenAlternativeDecoratorWrapper)
91+
and isinstance(x.meta, ElseThenAlternativeDecoratorMeta)
8592
),
8693
)
8794
else_then_alternatives = list(map(lambda member: member[1].meta.alternative, else_then_alternatives))
88-
if len(else_then_alternatives) > 1:
89-
raise MultipleElseThenException()
90-
elif len(else_then_alternatives) == 1:
91-
self._definition.else_then_alternative = else_then_alternatives[0]
95+
for alternative in else_then_alternatives:
96+
self._definition.else_then(alternative)
9297

9398
return self._definition
9499

95100

101+
TP = TypeVar("TP", bound=type)
102+
103+
96104
class ConditionalSagaStep(SagaStep):
97105
"""Conditional Saga Step class."""
98106

@@ -131,34 +139,51 @@ def _from_raw(cls, raw: dict[str, Any], **kwargs) -> ConditionalSagaStep:
131139

132140
return cls(**current)
133141

134-
def __call__(self, type_: ConditionalSagaStepClass) -> ConditionalSagaStepClass:
142+
def __call__(self, type_: TP) -> Union[TP, ConditionalSagaStepDecoratorWrapper]:
135143
meta = ConditionalSagaStepDecoratorMeta(type_, self)
136144
type_.meta = meta
137145
return type_
138146

139-
def if_then(self, condition: ConditionCallback, saga: Saga) -> ConditionalSagaStep:
147+
def if_then(
148+
self, alternative: Union[IfThenAlternative, ConditionCallback], saga: Optional[Saga] = None
149+
) -> ConditionalSagaStep:
140150
"""Add a new ``IfThenAlternative`` based on a condition and a saga.
141151
142-
:param condition: The condition that must be satisfied to execute the alternative.
152+
:param alternative: The condition that must be satisfied to execute the alternative.
143153
:param saga: The saga to be executed if the condition is satisfied.
144154
:return: This method returns the same instance that is called.
145155
"""
156+
if not isinstance(alternative, IfThenAlternative):
157+
alternative = IfThenAlternative(saga, alternative)
158+
159+
if alternative.order is None:
160+
if self.if_then_alternatives:
161+
alternative.order = self.if_then_alternatives[-1].order + 1
162+
else:
163+
alternative.order = 1
164+
165+
if self.if_then_alternatives and alternative.order <= self.if_then_alternatives[-1].order:
166+
raise OrderPrecedenceException(
167+
f"Unsatisfied precedence constraints. Previous: {self.if_then_alternatives[-1].order} "
168+
f"Current: {alternative.order} "
169+
)
146170

147-
alternative = IfThenAlternative(condition, saga)
148171
self.if_then_alternatives.append(alternative)
149172
return self
150173

151-
def else_then(self, saga: Saga) -> ConditionalSagaStep:
174+
def else_then(self, alternative: Union[ElseThenAlternative, Saga]) -> ConditionalSagaStep:
152175
"""Set the ``ElseThenAlternative`` with the given saga.
153176
154-
:param saga: The saga to be executed if not any condition is met.
177+
:param alternative: The saga to be executed if not any condition is met.
155178
:return: This method returns the same instance that is called.
156179
"""
157180

158181
if self.else_then_alternative is not None:
159182
raise MultipleElseThenException()
160183

161-
alternative = ElseThenAlternative(saga)
184+
if not isinstance(alternative, ElseThenAlternative):
185+
alternative = ElseThenAlternative(alternative)
186+
162187
self.else_then_alternative = alternative
163188
return self
164189

@@ -200,35 +225,36 @@ def __iter__(self) -> Iterable:
200225

201226

202227
@runtime_checkable
203-
class IfThenAlternativeWrapper(Protocol):
228+
class IfThenAlternativeDecoratorWrapper(Protocol):
204229
"""TODO"""
205230

206-
meta: IfThenAlternativeMeta
231+
meta: IfThenAlternativeDecoratorMeta
232+
__call__: ConditionCallback
207233

208234

209-
class IfThenAlternativeMeta:
235+
class IfThenAlternativeDecoratorMeta:
210236
"""TODO"""
211237

212-
def __init__(self, func: ConditionCallback, alternative: IfThenAlternative):
213-
self._func = func
238+
def __init__(self, inner: ConditionCallback, alternative: IfThenAlternative):
239+
self._inner = inner
214240
self._alternative = alternative
215241

216242
@cached_property
217243
def alternative(self) -> IfThenAlternative:
218244
"""TODO"""
219-
self._alternative.condition = SagaOperation(self._func)
245+
self._alternative.condition = SagaOperation(self._inner)
220246
return self._alternative
221247

222248

223249
class IfThenAlternative:
224250
"""If Then Alternative class."""
225251

226252
def __init__(
227-
self, condition: Union[SagaOperation, ConditionCallback] = None, saga: Saga = None, order: Optional[int] = None
253+
self,
254+
saga: Saga,
255+
condition: Optional[Union[ConditionCallback, SagaOperation[ConditionCallback]]] = None,
256+
order: Optional[int] = None,
228257
):
229-
if saga is None:
230-
raise ValueError("TODO")
231-
232258
if not isinstance(condition, SagaOperation):
233259
condition = SagaOperation(condition)
234260

@@ -258,10 +284,10 @@ def from_raw(cls, raw: Union[dict[str, Any], IfThenAlternative], **kwargs) -> If
258284

259285
return cls(**current)
260286

261-
def __call__(self, type_: ConditionCallback) -> Union[ConditionCallback, IfThenAlternativeMeta]:
262-
meta = IfThenAlternativeMeta(type_, self)
263-
type_.meta = meta
264-
return type_
287+
def __call__(self, func: ConditionCallback) -> IfThenAlternativeDecoratorWrapper:
288+
meta = IfThenAlternativeDecoratorMeta(func, self)
289+
func.meta = meta
290+
return func
265291

266292
def validate(self) -> None:
267293
"""Check if the alternative is valid.
@@ -283,6 +309,9 @@ def raw(self) -> dict[str, Any]:
283309
"saga": self.saga.raw,
284310
}
285311

312+
def __repr__(self) -> str:
313+
return f"{type(self).__name__}{tuple(self)}"
314+
286315
def __eq__(self, other: Any) -> bool:
287316
return isinstance(other, type(self)) and tuple(self) == tuple(other)
288317

@@ -294,17 +323,18 @@ def __iter__(self):
294323

295324

296325
@runtime_checkable
297-
class ElseThenAlternativeWrapper(Protocol):
326+
class ElseThenAlternativeDecoratorWrapper(Protocol):
298327
"""TODO"""
299328

300-
meta: ElseThenAlternativeMeta
329+
meta: ElseThenAlternativeDecoratorMeta
330+
__call__: Callable
301331

302332

303-
class ElseThenAlternativeMeta:
333+
class ElseThenAlternativeDecoratorMeta:
304334
"""TODO"""
305335

306-
def __init__(self, func: Callable, alternative: ElseThenAlternative):
307-
self._func = func
336+
def __init__(self, inner: Callable, alternative: ElseThenAlternative):
337+
self._inner = inner
308338
self._alternative = alternative
309339

310340
@cached_property
@@ -316,10 +346,7 @@ def alternative(self) -> ElseThenAlternative:
316346
class ElseThenAlternative:
317347
"""Else Then Alternative class."""
318348

319-
def __init__(self, saga: Saga = None):
320-
if saga is None:
321-
raise ValueError("TODO")
322-
349+
def __init__(self, saga: Saga):
323350
self.saga = saga
324351

325352
@classmethod
@@ -343,10 +370,10 @@ def from_raw(cls, raw: Union[dict[str, Any], ElseThenAlternative], **kwargs) ->
343370

344371
return cls(**current)
345372

346-
def __call__(self, type_: Callable) -> Union[Callable, ElseThenAlternativeMeta]:
347-
meta = ElseThenAlternativeMeta(type_, self)
348-
type_.meta = meta
349-
return type_
373+
def __call__(self, func: Callable) -> ElseThenAlternativeDecoratorWrapper:
374+
meta = ElseThenAlternativeDecoratorMeta(func, self)
375+
func.meta = meta
376+
return func
350377

351378
def validate(self) -> None:
352379
"""Check if the alternative is valid.
@@ -367,6 +394,9 @@ def raw(self) -> dict[str, Any]:
367394
"saga": self.saga.raw,
368395
}
369396

397+
def __repr__(self) -> str:
398+
return f"{type(self).__name__}{tuple(self)}"
399+
370400
def __eq__(self, other: Any) -> bool:
371401
return isinstance(other, type(self)) and tuple(self) == tuple(other)
372402

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ def __call__(self, func: LocalCallback) -> LocalSagaStepDecoratorWrapper:
112112
meta = LocalSagaStepDecoratorMeta(func, self)
113113
func.meta = meta
114114
func.on_failure = meta.on_failure
115-
# noinspection PyTypeChecker
116115
return func
117116

118117
def on_execute(

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ def __call__(self, func: RequestCallBack) -> RemoteSagaStepDecoratorWrapper:
149149
func.on_success = meta.on_success
150150
func.on_error = meta.on_error
151151
func.on_failure = meta.on_failure
152-
# noinspection PyTypeChecker
153152
return func
154153

155154
def on_execute(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@
2020
[SagaContext, SagaResponse, ...], Union[Union[Exception, SagaContext], Awaitable[Union[Exception, SagaContext]]]
2121
]
2222
LocalCallback = Callable[[SagaContext, ...], Union[Optional[SagaContext], Awaitable[Optional[SagaContext]]]]
23-
ConditionCallback = Callable[[SagaContext], Union[bool, Awaitable[bool]]]
23+
ConditionCallback = Callable[[SagaContext, ...], Union[bool, Awaitable[bool]]]

packages/core/minos-microservice-saga/tests/test_saga/test_definitions/test_saga.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
)
2626
from tests.utils import (
2727
ADD_ORDER,
28+
CONDITIONAL_ORDER_SAGA,
29+
DELETE_ORDER,
30+
ConditionalOrderSaga,
2831
DeleteOrderSaga,
2932
create_payment,
3033
send_create_order,
@@ -59,6 +62,12 @@ def step(self, context: SagaContext) -> SagaContext:
5962
with self.assertRaises(OrderPrecedenceException):
6063
_Foo.meta.definition
6164

65+
def test_equal(self):
66+
self.assertEqual(DeleteOrderSaga.meta.definition, DELETE_ORDER)
67+
68+
def test_equal_conditional(self):
69+
self.assertEqual(ConditionalOrderSaga.meta.definition, CONDITIONAL_ORDER_SAGA)
70+
6271

6372
class TestSaga(unittest.TestCase):
6473
def test_commit_constructor(self):

0 commit comments

Comments
 (0)