Skip to content

Commit b4800e7

Browse files
s-aleshinSergei Aleshin
andauthored
Process as subprocess conditions (tests + doc) (#27)
* add: condition tests update: pyproject.toml del: attrs from requirements_dev.txt * add: example with conditions add: test for the process with conditions * update: doc --------- Co-authored-by: Sergei Aleshin <sergei.aleshin@alludo.com>
1 parent 71977b4 commit b4800e7

5 files changed

Lines changed: 193 additions & 5 deletions

File tree

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ pip install logic-processes-layer
1818

1919
## New Features
2020

21-
- ProcessAsSubprocess: Use any process as a subprocess.
22-
- InitMapper: Simplifies process initialization with attribute mapping from the context.
23-
- ProcessAttr: Retrieve attributes from the process context or directly from the process.
21+
- **ProcessAsSubprocess**: Use any process as a subprocess.
22+
- **InitMapper**: Simplifies process initialization with attribute mapping from the context.
23+
- **ProcessAttr**: Retrieve attributes from the process context or directly from the process.
24+
- **Conditions Support**: Add logical conditions to control the execution of processes.
25+
- **AttrCondition**: Define conditions based on attributes of the process or context.
26+
- **FunctionCondition**: Wrap custom functions as conditions.
27+
- **Logical Operators**: Combine conditions with `&` (AND), `|` (OR), `~` (NOT), and `^` (XOR) for advanced logic.
2428
- [Examples](tests/examples) of how to use the logic_processes_layer package.
2529

2630

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ version = "1.2025.01.06"
88
requires-python = ">=3.8"
99
description = "Abstractions for create business logic"
1010
readme = "README.md"
11-
dependencies = ["attrs~=24.3"]
1211
authors = [{ name = "Sergei (Gefest) Romanchuk", email = "pod.cargoes.0u@icloud.com" }]
1312

1413
license = { file = "LICENSE" }
@@ -31,4 +30,5 @@ ignore = [
3130

3231
per-file-ignores = [
3332
"__init__.py:WPS347, WPS440",
34-
"*_enums.py:WPS115",]
33+
"*_enums.py:WPS115",
34+
"tests/test_*.py:WPS202, WPS442",]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
5+
from logic_processes_layer import BaseProcessor
6+
from logic_processes_layer.extensions import InitMapper, ProcessAsSubprocess, ProcessAttr
7+
from logic_processes_layer.extensions.conditions import AttrCondition, FunctionCondition
8+
9+
10+
_ONE_H = 100
11+
12+
13+
@dataclasses.dataclass
14+
class NotifyClientProcess(BaseProcessor):
15+
notification_message: str
16+
17+
def run(self): ... # noqa: WPS428
18+
19+
20+
_order_is_completed_condition: AttrCondition = AttrCondition(ProcessAttr("is_completed"))
21+
_order_amount_condition: FunctionCondition = FunctionCondition(lambda context: context.process.order_amount > _ONE_H)
22+
_client_agreed_condition: AttrCondition = AttrCondition(ProcessAttr("client_agreed_to_notifications"))
23+
_combined_condition = _order_is_completed_condition & (_order_amount_condition | _client_agreed_condition)
24+
25+
_notify_client_process = ProcessAsSubprocess(
26+
process_cls=NotifyClientProcess,
27+
init_mapper=InitMapper(notification_message=ProcessAttr("notification_message")),
28+
conditions=(_combined_condition,),
29+
)
30+
31+
32+
@dataclasses.dataclass
33+
class OrderStatusProcess(BaseProcessor):
34+
is_completed: bool
35+
client_agreed_to_notifications: bool
36+
order_amount: float
37+
notification_message: str = dataclasses.field(init=False, default="Order completed successfully!")
38+
39+
post_run = (_notify_client_process,)
40+
41+
def run(self): ... # noqa: WPS428

tests/test_conditions.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from __future__ import annotations
2+
3+
from unittest.mock import Mock
4+
5+
import pytest
6+
7+
from logic_processes_layer.extensions import ProcessAttr
8+
from logic_processes_layer.extensions.conditions import AttrCondition, FunctionCondition
9+
10+
11+
_TWO = 2
12+
13+
14+
@pytest.fixture
15+
def mock_process_attr():
16+
return Mock(spec=ProcessAttr)
17+
18+
19+
@pytest.fixture
20+
def mock_context():
21+
return Mock()
22+
23+
24+
@pytest.fixture
25+
def attr_condition(mock_process_attr):
26+
return AttrCondition(mock_process_attr)
27+
28+
29+
def test_attr_condition_positive(attr_condition, mock_process_attr, mock_context):
30+
mock_process_attr.get_value.return_value = True
31+
assert attr_condition(mock_context) is True
32+
mock_process_attr.get_value.assert_called_once_with(mock_context)
33+
34+
35+
def test_attr_condition_negative(attr_condition, mock_process_attr, mock_context):
36+
mock_process_attr.get_value.return_value = False
37+
assert attr_condition(mock_context) is False
38+
mock_process_attr.get_value.assert_called_once_with(mock_context)
39+
40+
41+
def test_attr_condition_negated(attr_condition, mock_process_attr, mock_context):
42+
mock_process_attr.get_value.return_value = True
43+
condition = ~attr_condition
44+
45+
assert condition(mock_context) is False
46+
mock_process_attr.get_value.assert_called_once_with(mock_context)
47+
48+
49+
def test_function_condition_positive(mock_context):
50+
mock_func = Mock(return_value=True)
51+
condition: FunctionCondition = FunctionCondition(mock_func)
52+
53+
assert condition(mock_context) is True
54+
mock_func.assert_called_once_with(mock_context)
55+
56+
57+
def test_function_condition_negative(mock_context):
58+
mock_func = Mock(return_value=False)
59+
condition: FunctionCondition = FunctionCondition(mock_func)
60+
61+
assert condition(mock_context) is False
62+
mock_func.assert_called_once_with(mock_context)
63+
64+
65+
def test_function_condition_negated(mock_context):
66+
mock_func = Mock(return_value=True)
67+
condition = ~FunctionCondition(mock_func)
68+
69+
assert condition(mock_context) is False
70+
mock_func.assert_called_once_with(mock_context)
71+
72+
73+
def test_operator_condition_and(attr_condition, mock_process_attr, mock_context):
74+
mock_process_attr.get_value.side_effect = (True, True)
75+
condition1 = attr_condition
76+
condition2 = attr_condition
77+
combined_condition = condition1 & condition2
78+
79+
assert combined_condition(mock_context) is True
80+
assert mock_process_attr.get_value.call_count == _TWO
81+
82+
83+
def test_operator_condition_or(attr_condition, mock_process_attr, mock_context):
84+
mock_process_attr.get_value.side_effect = (False, True)
85+
condition1 = attr_condition
86+
condition2 = attr_condition
87+
combined_condition = condition1 | condition2
88+
89+
assert combined_condition(mock_context) is True
90+
assert mock_process_attr.get_value.call_count == _TWO
91+
92+
93+
def test_operator_condition_complex(attr_condition, mock_process_attr, mock_context):
94+
mock_process_attr.get_value.side_effect = (True, False, True)
95+
condition1 = attr_condition
96+
condition2 = ~attr_condition
97+
condition3 = attr_condition
98+
condition4: FunctionCondition = FunctionCondition(lambda _: True)
99+
combined_condition = condition1 & (condition2 | condition3) & condition4
100+
101+
assert combined_condition(mock_context) is True
102+
assert mock_process_attr.get_value.call_count == _TWO
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
from unittest.mock import patch
4+
5+
import pytest
6+
7+
from .examples.processors.process_with_conditions import NotifyClientProcess, OrderStatusProcess
8+
9+
10+
@pytest.mark.parametrize(
11+
argnames="is_completed, client_agreed, order_amount, should_notify",
12+
argvalues=(
13+
(True, True, 150, True),
14+
(True, False, 150, True),
15+
(True, True, 50, True),
16+
(True, False, 50, False),
17+
(False, True, 150, False),
18+
(False, False, 150, False),
19+
),
20+
ids=(
21+
"Both completed and delayed, amount > 100 -> Notify",
22+
"Completed but not delayed, amount > 100 -> Notify",
23+
"Both completed and delayed, amount <= 100 -> No Notify",
24+
"Completed but not delayed, amount <= 100 -> No Notify",
25+
"Not completed, delayed, amount > 100 -> No Notify",
26+
"Not completed, not delayed, amount > 100 -> No Notify",
27+
),
28+
)
29+
def test_order_status_process(is_completed, client_agreed, order_amount, should_notify):
30+
with patch.object(NotifyClientProcess, "run", return_value=None) as mock_notify_run:
31+
process = OrderStatusProcess(
32+
is_completed=is_completed,
33+
client_agreed_to_notifications=client_agreed,
34+
order_amount=order_amount,
35+
)
36+
process()
37+
38+
if should_notify:
39+
mock_notify_run.assert_called_once()
40+
else:
41+
mock_notify_run.assert_not_called()

0 commit comments

Comments
 (0)