Skip to content

Commit 88ebd80

Browse files
authored
feat: rule activation mode (#40)
* feat: rule activation mode Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> * fix: add python 3.13 compatibility Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> --------- Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com>
1 parent 81e2498 commit 88ebd80

File tree

23 files changed

+394
-52
lines changed

23 files changed

+394
-52
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ repos:
2626
- id: check-added-large-files
2727
args: [--maxkb=500]
2828
- repo: https://github.com/astral-sh/ruff-pre-commit
29-
rev: v0.8.0
29+
rev: v0.8.2
3030
hooks:
3131
- id: ruff
3232
args: [--fix]

CHANGELOG.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [0.8.2] - November, 2024
7+
## [0.9.0] - December, 2024
8+
9+
### Features
10+
11+
* Add a new configuration setting for rule execution: `rule_activation_mode` (#38).
812

913
### Maintenance
1014

15+
* Compatibility with Python 3.13.
1116
* Use true Pydantic V2 (or Pydantic V1) models (`DeprecationWarning` added about Pydantic V1).
1217

18+
> [!IMPORTANT]
19+
> **Arta** + **Pydantic V1** + **Python 3.13** is not supported because Pydantic V1 is not supported for Python > 3.12 ([issue 9663](https://github.com/pydantic/pydantic/issues/9663)).
20+
1321
### Documentation
1422

15-
* New page: *"Use your business objects".*
23+
* New pages:
24+
* *Use your business objects*
25+
* *Rule activation mode*
1626

1727
### Breaking changes
1828

docs/mkdocs.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,12 @@ nav:
8383
- How to: how_to.md
8484
- Glossary: glossary.md
8585
- Advanced User Guide:
86+
- API Reference: api_reference.md
87+
- Custom conditions: custom_conditions.md
8688
- Parameters: parameters.md
89+
- Rule activation mode: rule_activation_mode.md
8790
- Rule sets: rule_sets.md
8891
- Use your business objects: business_objects.md
89-
- Custom conditions: custom_conditions.md
90-
- API Reference: api_reference.md
92+
9193
extra_css:
9294
- assets/css/mkdocs_extra.css

docs/pages/home.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,35 @@
55
<em>An Open Source Rules Engine - Make rule handling simple</em>
66
</p>
77
<p align="center">
8-
<img src="https://img.shields.io/pypi/v/arta" alt="Versions">
8+
<a href="https://pypi.org/project/arta/"><img src="https://img.shields.io/pypi/v/arta" alt="Versions"></a>
99
</p>
1010

1111
# Welcome to the documentation
1212

13-
* Want to discover what is **Arta**? :arrow_right: [Get Started](a_simple_example.md)
14-
* Want to know how to use it? :arrow_right: [User Guide](how_to.md)
13+
Want to discover what is **Arta**? :arrow_right: [Get Started](a_simple_example.md)
1514

16-
!!! info "New feature"
1715

18-
Check out the new and very convenient feature called the [simple condition](how_to.md#simple-condition). A new and lightweight way of configuring your rules' conditions.
16+
Want to know how to use it? :arrow_right: [User Guide](how_to.md)
1917

20-
**Arta** is automatically tested with:
2118

22-
![Alt Python](https://img.shields.io/pypi/pyversions/arta)
19+
!!! info inline "New feature"
20+
21+
Use **Arta** as a *process execution engine* :zap:
22+
23+
Read [this page](rule_activation_mode.md) for more details.
2324

2425
!!! tip "Releases"
2526

26-
Want to see last updates, check the [Release notes](https://github.com/MAIF/arta/releases) :rocket:
27+
Check the [Release notes](https://github.com/MAIF/arta/releases) :rocket:
28+
29+
!!! warning "Pydantic 1 compatibility is deprecated"
30+
31+
**Arta** is working with [Pydantic 2](https://docs.pydantic.dev/latest/) and Pydantic 1 but compatibility with V1 will be removed in the next **major** release.
32+
33+
**Arta** is working and automatically tested with:
34+
35+
![Alt Python](https://img.shields.io/pypi/pyversions/arta)
2736

28-
!!! success "Pydantic 2"
37+
You like **Arta**? Add a :star:
2938

30-
**Arta** is now working with [Pydantic 2](https://docs.pydantic.dev/latest/)! And of course, Pydantic 1 as well.
39+
[![GitHub Repo stars](https://img.shields.io/github/stars/maif/arta)](https://github.com/MAIF/arta)

docs/pages/how_to.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ Ensure that you have correctly installed **Arta** before, check the [Installatio
22

33
## Simple condition
44

5-
!!! beta "Beta feature"
6-
7-
**Simple condition** is still a *beta feature*, some cases could not work as designed.
8-
95
**Simple conditions** are a new and straightforward way of configuring your *conditions*.
106

117
It simplifies your rules a lot by:

docs/pages/rule_activation_mode.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
!!! example "Beta feature"
2+
3+
This new feature (i.e., `rule_activation_mode: many_by_group`) is a **beta feature**, some cases could not work as designed. Please report them using [issues](https://github.com/MAIF/arta/issues).
4+
5+
This feature was designed at MAIF when the idea to use **Arta** as a simple *process execution engine* came about.
6+
7+
Our goal was to handle different *rules* of data processing inside an ETL pipeline.
8+
9+
It's actually quite simple :zap:
10+
11+
## Illustration
12+
13+
Traditionaly, **Arta** is evaluating rules (like most rules engines) like this:
14+
15+
```mermaid
16+
---
17+
title: one_by_group
18+
---
19+
flowchart
20+
s((Start))
21+
e((End))
22+
subgraph Group_1
23+
r1(Rule 1, conditions False)-->r2(Rule 2, conditions True)-.not evaluated.->r3(Rule 3, conditions True)
24+
end
25+
r2-.execute.->a2(Action A)
26+
subgraph Group_2
27+
r1b(Rule 1, conditions True)-.not evaluated.->r2b(Rule 2, conditions False)-.not evaluated.->r3b(Rule 3, conditions True)
28+
end
29+
s-->r1
30+
r1b-.execute.->a1b(Action B)
31+
r2-->r1b
32+
r3-.->r1b
33+
r1b-->e
34+
r3b-.->e
35+
```
36+
37+
> **Only one rule is activated (i.e., meaning one action is triggered) by rule group.**
38+
39+
---
40+
41+
But if we need to use **Arta** to execute *simple workflows*, we need a *control flow* like this one:
42+
43+
```mermaid
44+
---
45+
title: many_by_group
46+
---
47+
flowchart
48+
s((Start))
49+
e((End))
50+
subgraph Group_1
51+
r1(Rule 1, conditions False)-->r2(Rule 2, conditions True)-->r3(Rule 3, conditions True)
52+
end
53+
r2-.execute.->a2(Action A)
54+
r3-.execute.->a3(Action B)
55+
subgraph Group_2
56+
r1b(Rule 1, conditions True)-->r2b(Rule 2, conditions False)-->r3b(Rule 3, conditions True)
57+
end
58+
s-->r1
59+
r1b-.execute.->a1b(Action C)
60+
r3b-.execute.->a3b(Action D)
61+
r3-->r1b
62+
r3b-->e
63+
```
64+
65+
> **All rules are evaluated.**
66+
67+
> **Therefore, many rules can be activated (i.e., meaning many actions can be triggered) by rule group.**
68+
69+
---
70+
71+
## Setting
72+
73+
You just need to add somewhere in the YAML configuration file of **Arta** the following setting:
74+
75+
### One by group
76+
77+
This *traditional* flow of control is the **default one**:
78+
79+
```yaml
80+
rule_activation_mode: one_by_group
81+
```
82+
83+
!!! note "Default value"
84+
85+
Because it is the **default value**, it is *useless* to add this line in the configuration.
86+
87+
### Many by group
88+
89+
This is the *flow of control* of a **process execution engine**:
90+
91+
```yaml
92+
rule_activation_mode: many_by_group
93+
```
94+
95+
That's all! You are all set :+1:

pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "arta"
7-
version = "0.8.2"
7+
version = "0.9.0"
88
requires-python = ">3.8.0"
99
description = "A Python Rules Engine - Make rule handling simple"
1010
readme = "README.md"
@@ -28,6 +28,7 @@ classifiers = [
2828
"Programming Language :: Python :: 3.10",
2929
"Programming Language :: Python :: 3.11",
3030
"Programming Language :: Python :: 3.12",
31+
"Programming Language :: Python :: 3.13",
3132
"License :: OSI Approved :: Apache Software License",
3233
]
3334

@@ -42,12 +43,10 @@ Documentation = "https://maif.github.io/arta/home/"
4243
Repository = "https://github.com/MAIF/arta"
4344

4445
[project.optional-dependencies]
45-
all = ["arta[test,dev,doc,mypy,ruff]"]
46+
all = ["arta[test,dev,doc]"]
4647
test = ["pytest", "tox", "pytest-cov"]
4748
dev = ["mypy", "pre-commit", "ruff"]
4849
doc = ["mkdocs-material", "mkdocstrings[python]"]
49-
mypy = ["mypy"]
50-
ruff = ["ruff"]
5150

5251
[tool.setuptools]
5352
package-dir = {"" = "src"}

src/arta/_engine.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from arta.config import load_config
1717
from arta.models import Configuration, RulesDict
1818
from arta.rule import Rule
19-
from arta.utils import ParsingErrorStrategy
19+
from arta.utils import ParsingErrorStrategy, RuleActivationMode
2020

2121

2222
class RulesEngine:
@@ -81,8 +81,10 @@ def __init__(
8181
"RulesEngine takes one (and only one) parameter: 'rules_dict' or 'config_path' or 'config_dict'."
8282
)
8383

84-
# Init. default parsing_error_strategy (probably not needed because already defined elsewhere)
84+
# Init. default global settings (useful if not set, can't be set in the Pydantic model
85+
# because of the rules dict mode)
8586
self._parsing_error_strategy: ParsingErrorStrategy = ParsingErrorStrategy.RAISE
87+
self._rule_activation_mode: RuleActivationMode = RuleActivationMode.ONE_BY_GROUP
8688

8789
# Initialize directly with a rules dict
8890
if rules_dict is not None:
@@ -112,6 +114,10 @@ def __init__(
112114
# Set parsing error handling strategy from config
113115
self._parsing_error_strategy = ParsingErrorStrategy(config.parsing_error_strategy)
114116

117+
if config.rule_activation_mode is not None:
118+
# Set rule activation mode from config
119+
self._rule_activation_mode = RuleActivationMode(config.rule_activation_mode)
120+
115121
# dict of available action functions (k: function name, v: function object)
116122
action_modules: list[str] = config.actions_source_modules
117123
action_functions: dict[str, Callable] = self._get_object_from_source_modules(action_modules)
@@ -166,7 +172,8 @@ def apply_rules(
166172
"""Apply the rules and return results.
167173
168174
For each rule group of a given rule set, rules are applied sequentially,
169-
The loop is broken when a rule is applied (an action is triggered).
175+
The loop is broken when a rule is applied (an action is triggered)
176+
or not (depends on the rule activation mode).
170177
Then, the next rule group is evaluated.
171178
And so on...
172179
@@ -241,8 +248,9 @@ def apply_rules(
241248
# Update input data with current result with key 'output' (can be used in next rules)
242249
input_data_copy["output"][group_id] = copy.deepcopy(results_dict[group_id])
243250

244-
# We can only have one result per group => break when "action_result" in rule_details
245-
break
251+
if self._rule_activation_mode is RuleActivationMode.ONE_BY_GROUP:
252+
# We can only have one result per group => break when "action_result" in rule_details
253+
break
246254

247255
# Handling non-verbose mode
248256
if not verbose:

src/arta/condition.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,12 @@ def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErro
183183
path_matches: list[str] = re.findall(data_path_patt, unitary_expr)
184184

185185
if len(path_matches) > 0:
186+
locals_ns: dict[str, Any] = {}
187+
186188
# Regular case: we have a data paths
187189
for idx, path in enumerate(path_matches):
188190
# Read data from the path
189-
locals()[f"data_{idx}"] = parse_dynamic_parameter( # noqa
191+
locals_ns[f"data_{idx}"] = parse_dynamic_parameter(
190192
parameter=path, input_data=input_data, parsing_error_strategy=parsing_error_strategy
191193
)
192194

@@ -195,7 +197,7 @@ def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErro
195197

196198
# Evaluate the expression
197199
try:
198-
bool_var = eval(unitary_expr) # noqa
200+
bool_var = eval(unitary_expr, None, locals_ns) # noqa
199201
except TypeError:
200202
# Ignore evaluation --> False
201203
pass

src/arta/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pydantic
99
from pydantic.version import VERSION
1010

11-
from arta.utils import ParsingErrorStrategy
11+
from arta.utils import ParsingErrorStrategy, RuleActivationMode
1212

1313
PYDANTIC_V1: bool = VERSION.startswith("1.")
1414

@@ -66,6 +66,7 @@ class Configuration(pydantic.BaseModel):
6666
condition_factory_mapping: Optional[dict[str, str]] = None
6767
rules: dict[str, dict[str, dict[Annotated[str, pydantic.StringConstraints(to_upper=True)], RulesConfig]]]
6868
parsing_error_strategy: Optional[ParsingErrorStrategy] = None
69+
rule_activation_mode: Optional[RuleActivationMode] = None
6970

7071
else:
7172
# Pydantic V1
@@ -141,4 +142,5 @@ class Configuration(BaseModelV2): # type: ignore[no-redef]
141142
custom_classes_source_modules: Optional[list[str]]
142143
condition_factory_mapping: Optional[dict[str, str]]
143144
rules: dict[str, dict[str, dict[pydantic.constr(to_upper=True), RulesConfig]]] # type: ignore
144-
parsing_error_strategy: Optional[ParsingErrorStrategy]
145+
parsing_error_strategy: Optional[ParsingErrorStrategy] = None
146+
rule_activation_mode: Optional[RuleActivationMode] = None

0 commit comments

Comments
 (0)