Skip to content

Commit e886b5a

Browse files
GefMarGefMars-aleshinSergei Aleshin
authored
Process as subprocess conditions (#26)
* update: pre-commit hooks * update: flake8 selected * update: versioning format * add: conditions support * 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> --------- Co-authored-by: GefMar <sergei.romanchuk@alludo.com> Co-authored-by: Sergei Aleshin <66841202+Nishela@users.noreply.github.com> Co-authored-by: Sergei Aleshin <sergei.aleshin@alludo.com>
1 parent 856ac75 commit e886b5a

File tree

15 files changed

+338
-12
lines changed

15 files changed

+338
-12
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ repos:
1818
- id: requirements-txt-fixer
1919

2020
- repo: https://github.com/astral-sh/ruff-pre-commit
21-
rev: v0.8.0
21+
rev: v0.8.6
2222
hooks:
2323
- id: ruff-format
2424
- id: ruff
@@ -32,7 +32,7 @@ repos:
3232
additional_dependencies: ["flake8-pyproject", "wemake-python-styleguide"]
3333

3434
- repo: https://github.com/pre-commit/mirrors-mypy
35-
rev: 'v1.13.0'
35+
rev: 'v1.14.1'
3636
hooks:
3737
- id: mypy
3838
additional_dependencies: [ "no_implicit_optional" ]

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from __future__ import annotations
2+
3+
from .condition import *
4+
from .operator_enums import *
5+
from .skiped import *
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
4+
__all__ = ("AttrCondition", "FunctionCondition", "OperatorCondition")
5+
6+
from functools import partial, reduce
7+
import typing
8+
9+
from ...context import BaseProcessorContext
10+
from .operator_enums import OperatorEnum
11+
12+
13+
if typing.TYPE_CHECKING:
14+
from ...protocols import CallableConditionProtocol
15+
from ..mappers import ProcessAttr
16+
17+
ContextT = typing.TypeVar("ContextT", bound=BaseProcessorContext)
18+
OperatorCallablesT = typing.Callable[[typing.Iterable[typing.Any]], bool]
19+
OperatorMapT = typing.Dict[OperatorEnum, OperatorCallablesT]
20+
21+
22+
class OperatorCondition(typing.Generic[ContextT]):
23+
operator_map: typing.ClassVar[OperatorMapT] = {
24+
OperatorEnum.AND: all,
25+
OperatorEnum.OR: any,
26+
OperatorEnum.XOR: partial(reduce, lambda itm, other: itm ^ other),
27+
}
28+
29+
def __init__(
30+
self,
31+
conditions: typing.Iterable[CallableConditionProtocol],
32+
*,
33+
operator: OperatorEnum = OperatorEnum.AND,
34+
negated: bool = False,
35+
):
36+
self.conditions = conditions
37+
self.negated = negated
38+
self.operator = operator
39+
40+
def __call__(self, context: ContextT) -> bool:
41+
operator_f = self.operator_map[self.operator]
42+
result = operator_f(bool(condition(context)) for condition in self.conditions)
43+
return not result if self.negated else result
44+
45+
def __invert__(self) -> OperatorCondition:
46+
return OperatorCondition([self], operator=self.operator, negated=not self.negated)
47+
48+
def __and__(self, other: CallableConditionProtocol) -> OperatorCondition:
49+
return OperatorCondition([self, other], operator=OperatorEnum.AND)
50+
51+
def __or__(self, other: CallableConditionProtocol) -> OperatorCondition:
52+
return OperatorCondition([self, other], operator=OperatorEnum.OR)
53+
54+
def __xor__(self, other: CallableConditionProtocol) -> OperatorCondition:
55+
return OperatorCondition([self, other], operator=OperatorEnum.XOR)
56+
57+
58+
class AttrCondition(OperatorCondition[ContextT]):
59+
def __init__(self, process_attr: ProcessAttr, *, negated: bool = False):
60+
self.process_attr = process_attr
61+
super().__init__(operator=OperatorEnum.AND, conditions=[self], negated=negated)
62+
63+
def __call__(self, context: ContextT) -> bool:
64+
value = self.process_attr.get_value(context)
65+
result = bool(value)
66+
return not result if self.negated else result
67+
68+
69+
class FunctionCondition(OperatorCondition[ContextT]):
70+
def __init__(self, func: CallableConditionProtocol, *, negated: bool = False):
71+
super().__init__(operator=OperatorEnum.AND, conditions=[func], negated=negated)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
import enum
4+
5+
6+
class OperatorEnum(enum.Enum):
7+
AND = "AND"
8+
OR = "OR"
9+
XOR = "XOR"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from __future__ import annotations
2+
3+
4+
__all__ = ("ConditionSkipped",)
5+
6+
import dataclasses
7+
import typing
8+
9+
from ...processors import BaseProcessor
10+
11+
12+
ProcessT = typing.TypeVar("ProcessT", bound=BaseProcessor)
13+
14+
15+
@dataclasses.dataclass(unsafe_hash=True)
16+
class ConditionSkipped(typing.Generic[ProcessT]):
17+
process: ProcessT
18+
19+
def __str__(self):
20+
return f"Conditions skipped for {self.process}"

logic_processes_layer/extensions/process_as_subprocess.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import typing
88

99
from ..processors import BaseProcessor
10-
from ..sub_processors import BaseSubprocessor
10+
from ..sub_processors import BaseSubprocessor, ContextT
11+
from .conditions import ConditionSkipped
1112

1213

1314
if typing.TYPE_CHECKING:
@@ -19,11 +20,21 @@
1920
@dataclasses.dataclass(unsafe_hash=True)
2021
class ProcessAsSubprocess(BaseSubprocessor, typing.Generic[ProcessT]):
2122
process_cls: type[ProcessT]
22-
init_mapper: None | InitMapper = dataclasses.field(hash=False, default=None)
23+
init_mapper: InitMapper | None = dataclasses.field(hash=False, default=None)
24+
conditions: typing.Iterable[typing.Callable[[ContextT], bool]] = dataclasses.field(
25+
hash=False, default_factory=tuple
26+
)
2327

2428
def __call__(self):
2529
args = ()
2630
kwargs: dict[str, typing.Any] = {}
2731
if self.init_mapper is not None:
2832
args, kwargs = self.init_mapper(self.context)
29-
return self.process_cls(*args, **kwargs)()
33+
34+
subprocessor = self.process_cls(*args, **kwargs)
35+
if not self.check_conditions():
36+
return ConditionSkipped(subprocessor)
37+
return subprocessor()
38+
39+
def check_conditions(self) -> bool:
40+
return all(condition(self.context) for condition in self.conditions)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from __future__ import annotations
2+
3+
from .condition import *
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import annotations
2+
3+
4+
__all__ = ("CallableConditionProtocol",)
5+
6+
import typing
7+
8+
from ..context import BaseProcessorContext
9+
10+
11+
ContextT_contra = typing.TypeVar("ContextT_contra", bound=BaseProcessorContext, contravariant=True)
12+
13+
14+
@typing.runtime_checkable
15+
class CallableConditionProtocol(typing.Protocol[ContextT_contra]):
16+
def __call__(self, context: ContextT_contra) -> bool: ... # noqa: WPS220, WPS428

logic_processes_layer/sub_processors/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33

4-
__all__ = ("BaseSubprocessor",)
4+
__all__ = ("BaseSubprocessor", "CallResultT", "ContextT")
55

66
import dataclasses
77
import typing

0 commit comments

Comments
 (0)